Merge Λ-W1-T2: Linux source/runtime portability audit

This commit is contained in:
2026-08-02 06:53:54 -04:00
@@ -0,0 +1,792 @@
# Λ-W1 Track 2 — source/runtime Linux-portability audit
Static analysis of `src/**/*.{cpp,h}` (293 files) plus the vendored SWELL / WDL / LICE /
REAPER-SDK / VST3-SDK headers those files consume, 2026-08-02, branch
`pl-w1-t2-source-runtime-audit` (cut from `dev` at 6e937b9). Answers one question: **what in
the source blocks or degrades a Linux build/run**, for the extension (`reaper_reasampler`) and,
separately, for the ReaSampler 9000 VST3 instrument (`reasampler_vst`).
Build-system surface (CMake, `cmake/`, resgen as a *build step*, vendor configuration,
packaging, install) is a parallel track's; anything spotted here is listed under
**§7 Referred to T1** rather than analyzed. macOS is out of scope except where a Linux fix
shares its path — noted inline, one line, never a separate finding.
**Evidence discipline.** Every capability claim about SWELL / LICE / the REAPER SDK / the VST3
SDK cites the vendor file it was read in. The author is on Windows and **cannot build or run on
Linux**: nothing below was compiled or executed on Linux. Claims that need a Linux box carry
`[verify — Linux]` with the exact check. Line numbers were re-read against the working tree
immediately before writing.
---
## 1. Baseline — the portability posture today, by layer
### 1.1 `core/` — genuinely portable. Verified, not assumed.
Two independent checks, both exhaustive over the directory:
- **Include surface.** Every `#include` in `src/core/**` is either a `"core/…"` sibling, one of
26 standard-library headers, or the CMake-generated `version_generated.h`
(`src/core/version/app_version.cpp:12`, `src/core/wire/reasampler_uid.h:13`). **Zero** REAPER,
SWELL, WDL, LICE, VST3-SDK, `windows.h`, or other vendor includes. The 26 headers are
`<algorithm> <array> <atomic> <cassert> <cctype> <cerrno> <climits> <cmath> <cstddef>
<cstdint> <cstdio> <cstdlib> <cstring> <filesystem> <fstream> <limits> <map> <optional>
<set> <sstream> <string> <type_traits> <unordered_map> <unordered_set> <utility> <vector>`
all C++17, all present on libstdc++/libc++.
- **Preprocessor surface.** The whole of `core/` contains exactly **nine** preprocessor
conditional lines, in three places: the channel fork (`core/wire/reasampler_uid.h:29,34,39`),
two `NDEBUG` assert guards (`core/view/view_mode_model.cpp:326,328,331,334`), and **one**
platform conditional — `core/capture/capture_paths.cpp:1820`, the Windows-only case-fold in
`normalizeSlashes`. That is the entire platform-dependence of the pure core.
Byte-order is explicitly handled rather than assumed: `core/wire/bytes.h:2631,6274` builds and
reads little-endian by shift, not by `memcpy` of a native integer, so it is byte-order-neutral
by construction. `core/capture/wav_codec.h:5253` states the one real assumption ("target is
x86/ARM-LE only, no big-endian byte-swap") and `wav_codec.cpp:165168` memcpys floats under it —
correct on x86-64 and aarch64 Linux alike.
Sweeps that returned **nothing** across `src/core` **and** `src/app`: `sprintf_s`/`strcpy_s`/
`_stricmp`/`_snprintf`/`_alloca`/`__forceinline`/`__declspec`/`#pragma comment`/`#pragma warning`/
`#pragma pack`/`__int64`/`LPSTR`/`LPCSTR`/`LPWSTR`/`wchar_t`/`TCHAR`/`_T(`/`MAX_PATH`. There is
**no** `#pragma` of any kind anywhere in `src/**` other than `#pragma once`, and no
`__attribute__` / `__declspec` / anonymous-struct extension.
`long` appears in a handful of pure modules (`core/ui/card_meta.cpp:3941,55`,
`core/instrument/ui/envelope_overlay.cpp:45`). All are local computations from clamped doubles;
LP64's wider `long` is strictly safer than LLP64's. One stale comment
(`core/instrument/ui/envelope_overlay.cpp:19`, "a huge t would overflow a 32-bit long") is
Windows-specific but the clamp it describes runs regardless. Not a defect.
**Verdict: `core/` builds and behaves identically on Linux.** The one conditional
(`capture_paths.cpp:18`) is *correct* for Linux — case-sensitive paths must not be folded — and
the pure tests already assert both branches (`tests/test_capture_paths.cpp:1933,4151`), which
is direct evidence the pure layer was written with a non-Windows target in mind.
### 1.2 `tests/` — portable.
91 test TUs. Only `tests/test_capture_paths.cpp` forks on `_WIN32`, and it asserts the
non-Windows behaviour explicitly (lines 2833, 5051). A `C:\`/`C:/` grep over `tests/` hits
six files total, one of which is `test_capture_paths.cpp` itself; the five OTHER files — every
hit opaque *string data* fed to a pure function
(`test_bank_model.cpp:186187,243245`, `test_bake_wire.cpp:77`, `test_drag_out.cpp:291293,388`,
`test_origin_ledger.cpp:56`, `test_sample_usage.cpp:502`) — are not platform behaviour. They pass
identically on Linux.
### 1.3 `app/` — portable.
`src/app/main.cpp` is one TU of pointers + entry + dispatch. `REAPER_PLUGIN_DLL_EXPORT` and
`REAPER_PLUGIN_HINSTANCE` are already GCC/Clang-correct in the SDK
(`vendor/reaper-sdk/sdk/reaper_plugin.h:5253``__attribute__((visibility("default")))` and
`void *`), and `REAPER_PLUGIN_ENTRYPOINT` is platform-neutral (`:56`). No Win32 call, no Win32
type, no conditional. The one hazard is behavioural, not structural — see **L2-03**.
### 1.4 `shell/` — the whole cost sits here, and it is unevenly distributed.
65 platform-token lines across 30 of the 293 source files (grep:
`_WIN32|WIN32|__APPLE__|_MSC_VER|__linux__|__GNUC__|_WIN64` over `src/**/*.{cpp,h}`). Their
distribution is the story:
| Area | Lines | Shape |
|---|---|---|
| `shell/instrument/` (VST3 editor + embed) | 51 of 65 | Whole-TU `#ifdef _WIN32``#endif` — Windows-only by design |
| `shell/panel/` | 8 | Include-selection + two small `#else` bodies |
| `shell/persist/`, `shell/capture/`, `shell/actions/` | 4 | Trash fallback, `localtime_r`, the SWELL drag-out branch |
| `core/` | 2 | The case-fold |
Two `shell/` directories are absent from this table because they were swept and came back
completely clean, not because they went unchecked: `shell/view/` and `shell/bank_ops/` both
return **zero** hits on the same grep (confirmed). Naming them here, since the table above is
by hit-count and gives a reader no way to tell a zero-hit sweep from an unswept directory.
`src/resource.h` and `src/ext_keys.h` mention platform names only in comments — zero hits on
this specific grep (re-run and confirmed), so they are not a row of this distribution; see
`resource.h`/`resource.rc` under §7 for their actual (build-system) relevance.
The extension's guards are all *complete*: every `#ifdef _WIN32` that gates a **definition**
has an `#else` (`draw_kit.cpp:12`, `capture.cpp:218`, `panel_audition.cpp:36,58`,
`panel_state.h:20`, `prune_fs.cpp:169`, `drag_out_win.cpp:8`); the ones without an `#else`
gate only an `#include` or a single statement (`panel_window.cpp:19,148`, `prune_fs.cpp:35`,
`draw_kit.h:27`, `capture_paths.cpp:18`). **There is no undefined-symbol gap from this guard
structure** — every `#ifdef` that needs an `#else` has one. This is narrower than "no
undefined-symbol gap in the extension" full stop: L2-02's `GetCurrentProcessId`
(`instrument_drop_win.cpp:59`) is an undefined symbol on Linux precisely because it carries
**no** `#ifdef` at all, so it falls outside what this guard-completeness check measures.
The instrument's guards are complete in the opposite direction: `reasampler_editor.h:156269`
wraps the entire paint/input/timer/`wndProc`/`invalidate` family in `#ifdef _WIN32`, and
`editor_platform.cpp:298306` stubs only the three IPlugView overrides that must exist. Every
`invalidate()` call site is inside a guard (verified exhaustively over `src/shell/instrument/`:
53 call sites across eight files — `editor_input.cpp` (5), `editor_input_browse.cpp` (11),
`editor_input_chrome.cpp` (10), `editor_input_curve.cpp` (4), `editor_input_deck.cpp` (9),
`editor_input_waveform.cpp` (4), `editor_platform.cpp` (3, excluding the definition itself at
`:65`), and `editor_session.cpp` (7, at 119, 126, 139, 141, 150, 156, 174, excluding a
same-named mention inside a comment at `:106`) — each file whole-file- or whole-region-guarded
by `#ifdef _WIN32` (e.g. `editor_input_browse.cpp:8``:177`); `reasampler_editor.h:266` is
the declaration, not a call site. `editor_controls.cpp`, `editor_models.cpp`,
`instrument_bake.cpp` have zero call sites, confirmed). So the editor *links* off Windows — it
just does nothing.
### 1.5 What the platform layer actually provides (vendor-verified)
The extension's non-Windows path is SWELL-provided-by-host
(`SWELL_PROVIDED_BY_APP` → function pointers resolved through `SWELL_dllMain`,
`vendor/WDL/WDL/swell/swell-modstub-generic.cpp:135145`). `[verify — Linux]` **What is stated
here about REAPER's own `libSwell.so` is only evidence about WDL's own Makefile, not about what
Cockos actually ships** — `vendor/WDL/WDL/swell/Makefile:100107,141151` shows
`swell-gdi-lice.o`/`-DSWELL_LICE_GDI` built whenever GDK is enabled (`ifndef NOGDK`, `:111`),
with `-DSWELL_FREETYPE` gated behind `ifndef NOFREETYPE` (`:144`) and `-lfontconfig` gated a
level deeper behind `ifndef NOFONTCONFIG` (`:149`) — i.e. the Makefile makes the LICE-backed,
freetype/fontconfig SWELL the *default* build, not a certainty about the binary REAPER
distributes. **Every downstream claim built on "REAPER's Linux SWELL is the fontconfig-backed
GDK build" inherits this same unstated assumption** — that includes L2-09's entire mechanism,
the GDK-backend evidence cited for L2-07, and the cursor (`GDK_*`) / modifier-key
(`GetAsyncKeyState`) claims below, none of which are re-flagged individually; this paragraph is
their one shared source of doubt. Contrast with the null stub in `swell-gdi-generic.cpp`
(`#ifndef SWELL_PROVIDED_BY_APP`, line 24), which is not in play for a REAPER-hosted extension
either way.
Everything the panel layer calls was checked by name against
`vendor/WDL/WDL/swell/swell-functions.h` and `swell-types.h`. Present and real:
- Window/dialog: `SWELL_CreateDialog` behind the `CreateDialogParam` macro
(`swell-functions.h:616,619`), `DestroyWindow`, `SetFocus`, `GetFocus`, `GetParent`,
`GetCapture`/`SetCapture`/`ReleaseCapture`, `IsWindowVisible`, `InvalidateRect`,
`ScreenToClient`/`ClientToScreen`, `BeginPaint`/`EndPaint`, `SetTimer`/`KillTimer`,
`GetWindowLong`/`SetWindowLong` (`:319320`), `SetWindowPos` (`:286`).
- Messages/macros: `GET_X_LPARAM`, `GET_Y_LPARAM`, `HIWORD`, `MAKEINTRESOURCE`
(`swell-types.h:1220`), `PAINTSTRUCT`, `MSG` (`:262`), `SRCCOPY`, `WM_PAINT`, `WM_MOUSEMOVE`,
`WM_MOUSEWHEEL`, `WM_CAPTURECHANGED`, `WM_DROPFILES`, `WM_KEYDOWN`, `WM_TIMER`,
`WM_ERASEBKGND`, `TPM_RETURNCMD`, `MF_*`, `MB_*`, `ID*`, every `VK_*` used.
- `WM_MOUSEWHEEL` really does carry **screen** coords in `lParam` on the GDK backend — the
comment at `panel_window.cpp:100102` is correct (`swell-generic-gdk.cpp:1442`).
- `WM_CAPTURECHANGED` really is delivered on capture loss (`swell-generic-gdk.cpp:1795`,
`swell-wnd-generic.cpp:7141,7150`), so the panel's rollback handler
(`panel_window.cpp:9098`) has a live trigger.
- Cursors: `LoadCursor``SWELL_LoadCursor` (`swell-functions.h:728730`), `SetCursor`
(`:740741`), and **all seven** IDCs the panel uses map to real GDK cursors —
`IDC_ARROW``GDK_LEFT_PTR`, `IDC_HAND``GDK_HAND1`, `IDC_UPARROW``GDK_CENTER_PTR`,
`IDC_SIZEWE``GDK_RIGHT_SIDE`, `IDC_SIZEALL``GDK_FLEUR`, `IDC_IBEAM``GDK_XTERM`,
`IDC_NO``GDK_PIRATE` (`swell-generic-gdk.cpp:37363748` vs `panel_drag.cpp:141147,159161`).
- `GetAsyncKeyState` returns the `0x8000` high bit for `VK_CONTROL`/`VK_SHIFT`/`VK_MENU`
(`swell-generic-gdk.cpp:24392441`), so `panel_state.h:409411` works verbatim.
- `GetTickCount` is SWELL-provided (`swell-functions.h:81`), so `panel_drag.cpp:308,324` — the
only unguarded Win32-looking calls in the panel — are fine.
- `SWELL_InitiateDragDropOfFileList` **does exist on Linux**, and is a GDK implementation on
the GDK backend: `swell-generic-gdk.cpp:35633592` sets up a hidden drop-source window and
spins a nested `SWELL_RunMessageLoop` until capture drops, with a 500 ms no-motion timeout;
the actual `gdk_drag_begin` call is inside the shared `dropSourceWndProc` helper it invokes
(`:3446`), not textually inside the `35633592` range itself. The headless backend defines
the **same-named function as an empty no-op stub**
`swell-generic-headless.cpp:246248` is `void SWELL_InitiateDragDropOfFileList(...) { }` — so
the "not a stub" framing only holds for the GDK backend specifically. The two backends are
mutually exclusive (`swell-generic-headless.cpp:28` is `#ifndef SWELL_TARGET_GDK`), so nothing
breaks in a GDK build, but a reader should not take "does exist on Linux" to mean "is
implemented in every Linux SWELL backend." macOS (`swell-dlg.mm:3534`) also defines it; it is
declared at `swell-functions.h:1011`.
- LICE itself is portable: `lice.h:31` includes `swell-types.h` off Windows, `lice.cpp:22`
includes `swell.h`, `LICE_SysBitmap::__resize` has a SWELL framebuffer path
(`lice.cpp:179182`), and `LICE_CachedFont::DrawTextImpl` carries real non-Windows branches
(`lice_textnew.cpp:236249,829,10271028,1040`).
Absent from SWELL, verified by grep over `vendor/WDL/WDL/swell/`: `FF_DONTCARE` (**zero hits
anywhere in `vendor/WDL/`**), `GetCurrentProcessId` (only `GetCurrentThreadId`,
`swell-functions.h:822`), `GetKeyState`, `WM_MOUSELEAVE`/`TrackMouseEvent`, `DragAcceptFiles`,
`RegisterClass*`, `CreateWindowEx*`, `DefWindowProc*`, `MoveWindow`,
`GetWindowLongPtr`/`SetWindowLongPtr`, `WHEEL_DELTA`, and any move-to-trash surface.
---
## 2. Findings
### L2-01 — `FF_DONTCARE` does not exist off Windows; `draw_kit.cpp` will not compile
**Location:** `src/shell/panel/draw_kit.cpp:73` (`DEFAULT_PITCH | FF_DONTCARE`), reached on
non-Windows through `draw_kit.cpp:1216` (which includes `swell/swell.h`, **not** `windows.h`).
**Mechanism.** `swell-types.h` defines `DEFAULT_PITCH`, `DEFAULT_CHARSET`,
`OUT_DEFAULT_PRECIS`, `CLIP_DEFAULT_PRECIS`, `DEFAULT_QUALITY`, `FW_BOLD`, `FW_NORMAL`,
`FW_SEMIBOLD`, `TRANSPARENT` and every `DT_*` the kit uses — but **not** `FF_DONTCARE`. A grep
for `FF_DONTCARE` over the whole of `vendor/WDL/` returns nothing; on Windows it comes from
`<wingdi.h>` via `windows.h`. `draw_kit.cpp` is not platform-guarded (only its *include* is), so
the `CreateFont` call is compiled on every platform.
**Severity: Blocker**`error: 'FF_DONTCARE' was not declared in this scope` at
`draw_kit.cpp:73`; `draw_kit` is linked into both loadable modules, so the extension does not
build at all.
**Effort: S** — the argument is `DEFAULT_PITCH | FF_DONTCARE`, and `FF_DONTCARE` is 0x00 in
wingdi.h; the family bits are advisory to Windows' font mapper and meaningless to fontconfig.
**Direction.** Drop the `| FF_DONTCARE` term, or define it locally in the non-Windows include
branch. Do not add `windows.h`.
### L2-02 — `GetCurrentProcessId()` is called with no platform branch and SWELL does not export it
**Location:** `src/shell/actions/instrument_drop_win.cpp:59` (temp `.vstpreset` filename). The TU
has **no** `_WIN32` conditional anywhere (verified: its only `#include`s are `<atomic> <cstdint>
<filesystem> <fstream> <string> <system_error> <vector>` plus project/SDK headers, lines 627).
**Mechanism.** On non-Windows the declaration would have to come from `reaper_plugin.h`
`swell.h`. `swell-functions.h` declares `GetCurrentThreadId` (`:822`) and no
`GetCurrentProcessId`; the only two occurrences in the whole WDL tree are inside
`WDL/shm_msgreply.cpp:26` and `WDL/win32_utf8.c:244`, neither of which is a SWELL export.
**Severity: Blocker**`error: 'GetCurrentProcessId' was not declared in this scope`; the
extension does not build.
**Effort: S** — the PID exists only to keep two concurrent REAPER instances from colliding in
the shared temp dir (comment at `:5657`). `getpid()` behind a guard, or the already-imported
`GetCurrentThreadId()` plus the existing atomic counter, satisfies the same requirement.
**Direction.** Replace with a platform-neutral uniqueness source; the atomic counter at `:52`
already carries the intra-process half.
### L2-03 — `REAPERAPI_LoadAPI` is all-or-nothing over ~869 API names and fails silently
**Location:** `src/app/main.cpp:292293` (`if (REAPERAPI_LoadAPI(rec->GetFunc) != 0) return 0;`).
`main.cpp` does **not** define `REAPERAPI_MINIMAL`, so the full table is loaded
(`vendor/reaper-sdk/sdk/reaper_plugin_functions.h:4448`; 1738 `REAPERAPI_WANT_` guards ≈ 869
entries).
**Mechanism.** The SDK's loader accumulates one `failcnt` across the entire table
(`reaper_plugin_functions.h`, `REAPERAPI_LoadAPI` body: `failcnt += !(*table[i].dest =
getAPI(table[i].name)); return failcnt;`). If a Linux REAPER build does not export *any single
one* of those 869 names, `main.cpp` returns 0 and the extension never loads — with **no console
message, no log line, nothing**. The SDK header carries no "Windows only" annotation on any
entry (grep for `windows only|win32 only|not on mac|not on linux` returns nothing), so nothing
here says a gap exists — but nothing rules it out either, and the failure mode is maximally
opaque.
**Severity: Blocker (conditional)** `[verify — Linux]` — a silent load refusal with no
diagnostic is the worst possible first-run experience for a port. **Scope note:** the
all-or-nothing mechanism itself is identical on Windows too
(`reaper_plugin_functions.h:4546` — "an older version of REAPER may not succeed in loading",
no platform fork in the loader) — nothing about the *mechanism* is Linux-specific, only whether
*this particular* build happens to be missing an entry is unknown. Graded here as a Linux
Blocker on failure-mode quality alone (per the Mechanism above), the same axis L2-04 is
graded on below, despite L2-04's mechanism being certain rather than speculative — see L2-04's
calculus note. Arguably this belongs as a cross-platform robustness note rather than a
Linux-exclusive finding; left here because the fix is trivial and worth doing regardless of
platform.
**Effort: S** — confirmed against the two Direction options below: the `ShowConsoleMsg`
diagnostic is one new line on the existing failure branch (`main.cpp:293`), and the
`REAPERAPI_MINIMAL` swap follows a pattern already used elsewhere in this codebase
(`panel_window.cpp:2631`, `panel_audition.cpp:1216`) rather than inventing a new one.
**Direction.** Either switch `main.cpp` to `REAPERAPI_MINIMAL` + an explicit `WANT` list (the
other TUs already do this — e.g. `panel_window.cpp:2631`, `panel_audition.cpp:1216`), or keep
the full load but print the failure count via `rec->GetFunc("ShowConsoleMsg")` before returning
0. The minimal list is also the honest inventory of what this extension actually needs.
### L2-04 — every persisted floating-point number is `LC_NUMERIC`-dependent, in both directions
**Location (writers):** `src/core/json/json.cpp:3943` (`%.17g` — the bank index, view model and
tracking ledger all serialize through it), `src/core/model/provenance.cpp:40` (`%.17g` — the
provenance blob inside the bank JSON). Two writers, not three: `tail_control.cpp:74` is a
comment describing the format ("Byte-identical to the former snprintf writer:
`{"mode":%d,"manualMs":%.17g}`"), not a write site — the actual per-project tail-setting write
is `tail_control.cpp:7879`, which already goes through `json::numToStr` (the same
`json.cpp:3943` codec cited above), so it is the same writer, not a third one.
**Location (readers):** `src/core/json/json.cpp:190196` (`std::strtod`),
`src/core/wire/wire.cpp:121130` (`Cursor::fieldDouble`, `std::strtod`),
`src/core/capture/render_settings.cpp:178186` (`std::stod` over REAPER's own `P_RAZOREDITS`).
**Mechanism.** `snprintf("%.17g")`, `strtod` and `stod` all honour `LC_NUMERIC`. Under a
comma-decimal locale the writers emit `1,5`, which makes the bank JSON *structurally invalid*
(an extra separator inside an object) — the whole index fails to parse on the next load. The
readers are honestly fail-closed (all three require whole-token consumption:
`json.cpp:194`, `wire.cpp:127`, `render_settings.cpp:184`), so they degrade to "malformed"
rather than silently truncating — but that means a razor range or a bank field simply
disappears. On Windows the CRT's start-up locale is `"C"` and nothing in this codebase calls
`setlocale`, which is why this has never fired. On Linux the process locale is far more likely
to be set by something else in the address space: SWELL's GDK backend calls `gtk_init_check`
when built with `SWELL_SUPPORT_GTK` (`swell-generic-gdk.cpp:366`; the `#else` branch uses
`gdk_init_check`, `:368`) and **never** calls `gtk_disable_setlocale` (grep over
`vendor/WDL/WDL/swell/` returns no hit), and any GTK/Qt-based plugin loaded into the same
process can do the same.
**Severity: Major** — builds and runs; under a non-C `LC_NUMERIC` the bank index is written
unparseable and the project's whole bank is lost on reload. **Calculus note:** unlike L2-03
(graded Blocker above on zero direct evidence, purely on failure-mode quality), this finding's
mechanism is confirmed by reading the actual writer/reader call sites, and its failure mode —
the entire bank index becomes unparseable — is at least as severe as L2-03's. It is graded only
Major here because it additionally requires a non-C `LC_NUMERIC` in the hosting process, a
likelihood factor L2-03 does not apply to its own claim. Reading both on the same axis (either
both by failure-mode quality, or both by likelihood) would put them closer together than
Blocker/Major suggests; resolving that is a scope call for whoever prioritizes the two, not a
fact this audit can settle statically.
**Effort: M** — two writers and three readers, all in `core/`, all unit-testable; the fix is
a locale-independent path (`std::to_chars`/`std::from_chars`, C++17, or an explicit
`std::locale::classic()`-bound stream), not a `setlocale` call in a plugin.
**Direction.** Make the number codec locale-independent at its two writers and three readers;
add a pure test that pins the emitted text for a fractional value. Do **not** "fix" this by
calling `setlocale` — an extension must not mutate the host's locale.
`[verify — Linux]` — read `LC_NUMERIC` inside a running REAPER-Linux process (e.g.
`ShowConsoleMsg(setlocale(LC_NUMERIC, nullptr))`) before sizing the work.
### L2-05 — prune loses the Recycle Bin *and* the "file is locked" backstop at the same time
**Location:** `src/shell/persist/prune_fs.cpp:167209`; the non-Windows branch is `:200208`.
**Mechanism.** Two Windows properties are load-bearing for the deletion authority and neither
survives. (a) The Windows path routes through `SHFileOperationW` + `FOF_ALLOWUNDO` (`:186188`)
— deletions are recoverable from the Recycle Bin; the `#else` is a hard `fs::remove` (`:203`).
The header already states this honestly (`:3034`, `:162166`). (b) Less obviously, the failure
taxonomy at `:205` (`if (ec) return false; // real failure (locked/permission) -> skip`) encodes
**Windows** file-sharing semantics: a bank file currently open by REAPER's audio engine (an
active `PCM_source`, an item playing from it) cannot be deleted on Windows, so it is counted as
"skipped". On Linux `unlink()` on an open file succeeds — the directory entry vanishes while the
open fd keeps playing, and when the fd closes the bytes are gone with no trash to recover from.
So the Linux prune can delete a file that is *audibly in use*, silently, and the user's only
recovery floor (the superseded-file-survives-until-prune rule, root `CLAUDE.md` §resample bake)
now has nothing under it.
**Severity: Major** — irreversible user-data loss where the Windows build is recoverable; the
"locked" skip branch is dead code on Linux.
**Effort: M** — trash is genuinely non-portable, but the XDG trash spec (`~/.local/share/Trash`
with a `.trashinfo` sidecar) is a self-contained move-plus-metadata write, and the confirm gate
already exists upstream.
**Direction.** Either implement an XDG-trash move in the `#else` (preferred — the deletion
authority is a single ~40-line function and this is exactly where the platform seam belongs), or
make the prune confirmation text platform-aware so a Linux user is told the deletion is
permanent. Do not leave the current silent asymmetry.
### L2-06 — the docked panel never opens on Linux, and fails silently when it doesn't
**Location:** `src/shell/panel/panel_window.cpp:135137`.
**Mechanism.** `CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), …)` maps on SWELL to
`SWELL_CreateDialog(SWELL_curmodule_dialogresource_head, (resid), …)`
(`swell-functions.h:616,619`) — it resolves the template out of a per-module registry populated
by the **resgen-generated source**, not out of a linked `.rc`. That generated source is not
currently part of the Linux target (the `target_sources` line is commented out;
build-system detail → §7). The source-side consequence is what matters here: `SWELL_CreateDialog`
returns `nullptr`, `panel_window.cpp:137` does `if (!g_panel.hwnd) return;`, and the toggle
action is a **silent no-op** — no console line, no message box, and `bankPanelIsOpen()` keeps
reporting false so the Actions-list checkmark never lights. The user's only symptom is "the
button does nothing." Note also that `MAKEINTRESOURCE` becomes `((const char*)(UINT_PTR)(x))` on
SWELL (`swell-types.h:1220`), so the id is a pointer-shaped integer — the resgen output must
agree on `IDD_BANK_PANEL == 1000` (`src/resource.h:8`).
**Severity: Major** — the extension loads, every action works, and the primary UI surface is
absent with no diagnostic.
**Effort: S** on the source side (one failure branch), separate from T1's resgen wiring.
**Direction.** Add a one-line `ShowConsoleMsg` on the `!g_panel.hwnd` path naming the missing
dialog resource. That single line converts a mystery into a two-minute diagnosis and is worth
having on Windows too.
### L2-07 — file-drop ingest onto the panel has no opt-in on SWELL
**Location:** `src/shell/panel/panel_window.cpp:145150` (the `DragAcceptFiles` call is
`#ifdef _WIN32`), handler at `:4761,6567`.
**Mechanism.** The comment at `:145147` is accurate — SWELL exposes no `DragAcceptFiles`
(grep over `vendor/WDL/WDL/swell/` finds only `DragQueryFile` and `DragFinish`,
`swell-functions.h:10061007`). What the comment does not say is *how* a drop would arrive
instead. Reading the GDK backend: on a URI-list selection-notify, SWELL resolves the top-level
window, walks down with `ChildWindowFromPoint` (which **is** descending — `for(;;)` loop,
`swell-wnd-generic.cpp:69566981`), and `SendMessage(cw, WM_DROPFILES, (WPARAM)gobj, 0)`
(`swell-generic-gdk.cpp:1622`). The default child proc forwards an *unhandled* `WM_DROPFILES`
up to the parent only when the window *lacks* `WS_EX_ACCEPTFILES`
(`swell-wnd-generic.cpp:7681`) — but that bit's only effect is to **suppress** the up-forward;
it does not enable acceptance, and the message already reaches the window under the pointer via
the `ChildWindowFromPoint` descent regardless of the bit's state. So the panel's own `dlgProc`
plausibly receives the drop **without any opt-in at all**, but the earlier "ex-style bit, not
an API call" framing was backwards about what the bit does. The real SWELL opt-in surface for
`WS_EX_ACCEPTFILES` is the dialog *resource*, not a runtime call: `swell_resgen.pl:10` and
`swell_resgen.php:204` translate a dialog's `WS_EX_ACCEPTFILES` style to
`SWELL_DLG_WS_DROPTARGET`, and `swell-dlg-generic.cpp:320321` sets
`h->m_exstyle |= WS_EX_ACCEPTFILES` from that flag at dialog-creation time. `src/resource.rc:19`
declares `STYLE WS_CHILD` only — no `WS_EX_ACCEPTFILES` — so the bit is off on Linux regardless
of anything `panel_window.cpp` does at runtime. That makes L2-07's real seam `src/resource.rc` +
resgen, the SAME work as L2-06 and §7 bullet 1, not an independent one-liner: a runtime
`SetWindowLong(..., GWL_EXSTYLE, ... | WS_EX_ACCEPTFILES)` would only suppress the
unhandled-drop forward-to-parent — it would not change whether the drop reaches `dlgProc` in
the first place, which already happens via the `ChildWindowFromPoint` descent independent of
the bit. Note also the doc's own conclusion is better-supported than it knew: `src/resource.rc:2122`
is `BEGIN`/`END` — zero child controls — so the GDK `ChildWindowFromPoint` descent has nowhere
to land but the panel HWND itself. Two supporting details still check out: SWELL's
`DragQueryFile` reads the same `DROPFILES` layout the handler assumes
(`swell-wnd-generic.cpp:7730+`), and SWELL's `DragFinish` is a documented no-op ("caller will
free hdrops", `:77257728`) while SWELL itself `GlobalFree`s the handle right after
`SendMessage` (`swell-generic-gdk.cpp:16231624`) — so `panel_window.cpp:59` calling
`DragFinish` is safe, not a double-free.
**Severity: Major `[verify — Linux]`** — if the routing does *not* reach a docked child dialog,
one of the three ingest surfaces (file drop onto the bank panel) is silently dead with no error.
The evidence above says it probably works; it is not proof, and it cannot be exercised until
the panel itself renders (T1's resgen wiring — see L2-06).
**Effort:** no independent source-side fix exists — this is the same resgen work already
costed under L2-06 / §7 bullet 1. Do not add an `Effort: S` line here; adding the
`SetWindowLong` call would compile, do nothing observable, and send an implementer looking for
a bug that isn't where they'd look.
**Direction.** No separate fix. Once the resgen work lands the panel dialog with
`WS_EX_ACCEPTFILES` set from the resource, verify: `[verify — Linux]` drop a WAV onto the
docked panel and confirm `WM_DROPFILES` reaches `dlgProc`.
### L2-08 — the VST3 instrument has no Linux editor, and no automatable parameters to fall back to
**Location:** `src/shell/instrument/editor_platform.cpp:3742` (`isPlatformTypeSupported`
returns `kResultTrue` **only** for `kPlatformTypeHWND`, and only inside `#ifdef _WIN32`);
`:298306` (the non-Windows stubs); `reasampler_editor.h:156269` (the entire paint/input family
is `#ifdef _WIN32`); `reasampler_embed.cpp:135141` (`REAPER_FXEMBED_WM_IS_SUPPORTED` returns 0
off Windows). `reasampler_processor.cpp:360365` still hands the host a `ReaSamplerEditor`.
**Mechanism.** The VST3 SDK *does* support Linux — `kPlatformTypeX11EmbedWindowID`
(`vendor/vst3sdk/pluginterfaces/gui/iplugview.h:79`), `Linux::IRunLoop` with
`registerEventHandler`/`registerTimer` (`:267279`), `Linux::IEventHandler` (`:223`),
`Linux::ITimerHandler` (`:239`), and a `linuxmain.cpp` module entry
(`vendor/vst3sdk/public.sdk/source/main/linuxmain.cpp`). The instrument simply does not
implement any of it. The consequence compounds: with `isPlatformTypeSupported` false for every
type, the host falls back to a **generic parameter UI** — and a grep for
`addParameter|parameters\.add|getParameterCount` across `src/shell/instrument/*.cpp` returns
**nothing**. Zero VST3 parameters are registered as of this branch's base. A Linux user would
get an instrument with no editor and no controls whatsoever. (Phase Γ-W4-T1
`vst3-parameter-set``docs/product/parameter-automation.md` — is landing the parameter set
concurrently, which materially improves this fallback; the *editor* gap is unaffected.)
The specific Win32 dependencies a Linux editor would have to replace, each verified absent from
SWELL: `RegisterClassW`/`CreateWindowExW`/`DefWindowProcW` (`editor_platform.cpp:79103,294`) —
SWELL has no window-class model at all, only `SWELL_CreateDialog` and raw `HWND__` construction;
`MoveWindow` (`:133`) — SWELL has `SetWindowPos` (`swell-functions.h:286`) instead;
`GetWindowLongPtr`/`SetWindowLongPtr` (`:74,105,142`) — SWELL has the non-`Ptr` forms returning
`LONG_PTR` (`:319320`); `TrackMouseEvent`/`WM_MOUSELEAVE` (`:176198`) — SWELL has neither, a
gap the panel layer already documents at `panel_state.h:245246`; `GetKeyState`
(`editor_input_waveform.cpp:38`, `editor_input_curve.cpp:50`) — SWELL has only
`GetAsyncKeyState` (`swell-functions.h:712`); `DragAcceptFiles`/`DragQueryFileW`
(`:109,283`); `GetModuleHandle` (`:75`).
**Severity: Major (instrument only)** — the plugin would load and process audio; it would be
unplayable and uneditable.
**Effort: L** — an X11-embed `IPlugView` (`attachedToParent` receiving an X11 window id rather
than an HWND), an `IRunLoop`-driven timer replacing `SetTimer`/`WM_TIMER`, an event-driven
input path replacing the `wndProc` switch, and a LICE surface bound to that window. This is a
new competence, not a port of the existing one.
**Direction.** Treat as a separate, later decision from the extension (see §3). If it is ever
taken, the natural shape is a small platform seam under `shell/instrument/``editor_platform`
already *is* that seam; it needs a sibling, not a rewrite.
### L2-09 — the draw kit's font faces do not exist on Linux and substitute silently
**Location:** `src/shell/panel/draw_kit.cpp:7077` (`loadFont`), and the two literal faces it is
called with — Segoe UI and Consolas, per `draw_kit.h:6364`.
**Mechanism.** On Linux `CreateFont` goes through fontconfig when built with
`-DSWELL_FONTCONFIG` (gated per the `[verify — Linux]` note in §1.5):
`FcPatternAddString(pat, FC_FAMILY, lfFaceName)``FcConfigSubstitute`/`FcDefaultSubstitute`
`FcFontMatch` (`swell-gdi-lice.cpp:450487`). Whether `FcFontMatch` itself can return failure is
a claim about fontconfig's own internals — fontconfig is not vendored here, so it is dropped
rather than asserted uncited (this doc's own evidence-discipline rule). What the vendored code
does show: even a successful match doesn't guarantee a loadable face —
`swell-gdi-lice.cpp:480484` only sets `face` when `FcPatternGetString(hit, FC_FILE, …)`
resolves to a nonempty string AND the following `FT_New_Face` succeeds; either step can fail,
leaving `face` NULL, and in this build path there is no further fallback — the
`MatchFont`/LiberationSans/DejaVuSans list at `:493539` compiles only in the `#else`
(`#ifndef SWELL_FONTCONFIG`) branch. However `swell-gdi-lice.cpp:400401,561,564` show
`CreateFont` always allocates and returns a non-null `HGDIOBJ__*` regardless of whether `face`
resolved — the failure is recorded internally (`font->typedata = NULL`), not as a null return —
so `draw_kit.cpp:74`'s own `if (!hf) return` guard does **not** catch this failure mode; `hf`
comes back non-null either way. Whatever degraded outcome exists here comes from
`LICE_CachedFont`'s own handling of a null-`typedata` font (§1.5 already cites real
non-Windows branches in `lice_textnew.cpp:236249,829,10271028,1040`), not from `loadFont`'s
stated guard — `[verify — Linux]`. The two WCAG `static_assert`s (`draw_kit.cpp:5354`) are on
**pixel height and weight**, not on the face, so they still hold regardless.
**Severity: Minor** `[verify — Linux]` — cosmetic at best; whether it degrades further than
wrong metrics (e.g. to no text at all) rests on the unconfirmed downstream null-face handling
above, not on anything this audit can read statically.
**Effort: S** — confirmed against the actual call sites: five total (`draw_kit.cpp:154158`,
four using "Segoe UI", one "Consolas"), each a single string-literal argument. A platform
fallback is a `#ifdef`-guarded literal swap at those five sites, not a new mechanism —
`draw_kit.cpp:69`'s comment ("the face is chosen here so a change is one line") describes one
call's literal, not all five; the S estimate rests on there being only five call sites, not on
that comment's wording.
**Direction.** Add a platform fallback face list at the five `loadFont` call sites
(`draw_kit.cpp:154158`). DejaVu Sans / DejaVu Sans Mono are the safe Linux defaults; SWELL's
own no-fontconfig fallback list names LiberationSans/DejaVuSans and
LiberationMono/DejaVuSansMono (`swell-gdi-lice.cpp:505507`), a reasonable precedent to copy.
This same change fixes macOS (San Francisco / Menlo), one code path.
### L2-10 — OS drag-out on SWELL loses the copy-only mask, the readiness probe, and the outcome
**Location:** `src/shell/actions/drag_out_win.cpp:249282` (the `#else` branch);
`drag_out_win.h:711,2933` already states the first and third honestly.
**Mechanism.** Three separate degradations, all real but none fatal. (a) `DoDragDrop`'s
`DROPEFFECT_COPY`-only mask (`:230`) is a **structural** guarantee that no target can MOVE a
bank file out of the folder; `SWELL_InitiateDragDropOfFileList` takes no effect mask
(`swell-functions.h:1011`), so the guarantee reduces to whatever the GDK drag advertises.
(b) `canInitiateDragOut` degrades to `!paths.empty()` (`:276278`), so the caller's careful
coupling — "do not tear down the internal drag until the OS is known ready"
(`panel_drag.cpp:227244`) — has nothing to check against. (c) `initiateDragOut` returns `true`
unconditionally (`:270`), so the advisory success return is meaningless. The caller ignores it,
so (c) is inert today. Note the call path is already Linux-correct in one important respect:
`handOffToOs` releases capture and resets drag state (`panel_drag.cpp:240241`) *before*
invoking the drag, which matters because SWELL's implementation takes capture on its own hidden
window and spins a nested `SWELL_RunMessageLoop` until capture drops
(`swell-generic-gdk.cpp:35753584`, inside `SWELL_InitiateDragDropOfFileList` — the function
`drag_out_win.cpp:270` actually calls; the body is identical to the sibling
`SWELL_InitiateDragDrop` at `:35313560`, which is where an earlier draft of this citation
pointed) — with a 500 ms no-motion timeout that has no Windows analog.
**Severity: Minor** — the feature works; one safety property becomes conventional rather than
structural.
**Effort: S** — mostly documentation; there is no SWELL surface to restore the mask with.
**Direction.** Leave the implementation; make sure the copy-only invariant's home
(`drag_out_win.h:711`) is the doc a Linux reviewer is pointed at, and treat "MOVE is
structurally impossible" as a Windows-only claim in any future spec text.
### L2-11 — `normalizeSlashes` case-folds on `_WIN32` only, which is right for Linux and wrong for macOS
**Location:** `src/core/capture/capture_paths.cpp:1820`.
**Mechanism.** The one platform conditional in `core/`. Linux filesystems are case-sensitive, so
*not* folding is correct and the existing tests already assert it
(`tests/test_capture_paths.cpp:5051`). The shared-path note: macOS's default APFS/HFS+ is
case-**insensitive**, so the same `#ifdef _WIN32` under-folds there — a pre-existing macOS
defect this audit surfaces but does not own.
**Severity: Minor** (no Linux defect). **Effort: S** — confirmed: the one platform conditional
in `core/` is a single `#ifdef _WIN32` block (`capture_paths.cpp:1820`) with no other call
site depending on the token; a predicate swap is a one-block change.
**Direction.** No Linux action. If macOS is ever targeted, the predicate wants to be
"case-insensitive filesystem", not "Windows".
### L2-12 — `fs::path::string()` narrowing is strictly better on Linux (informational)
**Location:** `prune_fs.cpp:118`, `ingest.cpp:222,254`, `capture_paths.cpp:95`,
`panel_bank_ops.cpp:39`, `scope_resolve.cpp:207`, `insert.cpp:71`,
`capture_realtime_shell.cpp:306`, `capture.cpp:408`, `instrument_bake.cpp:166`.
**Mechanism.** On MSVC `std::filesystem::path::string()` narrows through the active code page;
on Linux `path` is already `char`-based, so `.string()` is a byte passthrough and UTF-8 survives.
The one site that deliberately works around the Windows behaviour —
`instrument_drop_win.cpp:123128`, using `u8string()` — is harmless on Linux (C++17, so
`u8string()` returns `std::string`; `CMakeLists.txt:28` pins `CMAKE_CXX_STANDARD 17`, and under
C++20 the `.c_str()` at `:128` would become a `const char8_t*` type error — worth knowing before
anyone bumps the standard).
**Severity: Minor / informational.** **Effort: none.** **Direction:** no action; do not "fix" it.
---
## Surfaces checked and found clean
Recorded so the sweep's negative results are as auditable as its findings.
- **Missing standard includes** (the MSVC-transitively-provides class). Ran a use-vs-include diff
across all 293 files for `<cstring> <cstdio> <memory> <atomic> <algorithm> <limits> <cmath>
<functional> <ctime> <cstdlib>`. Five raw hits; all five verified false positives on read —
four were the symbol name appearing in a *comment* (`render_settings.h:53`,
`ext_state_read.h:20`, `action_registry.h:7`) or a correct `<cmath>` `std::abs`
(`velocity_curve.cpp:229`, `<cmath>` at `:6`), and `processor_reload.cpp`'s `std::unique_ptr`
comes from `reasampler_processor.h:12`. **No missing include found — but this only covers the
ten headers above.** The two headers most likely to produce the classic
MSVC-transitively-provides failure under libstdc++, `<vector>` and `<string>`, were excluded
from that sweep; re-run including them: **45 files use `std::vector` without `#include
<vector>`, and 52 use `std::string` without `#include <string>`** (raw grep-diff counts,
unread). `<map>` and `<set>` add 2 and 3 more respectively. These 102 raw hits were **not**
individually verified the way the five above were (that would mean reading 102 files); the
claim this sweep actually supports is "up to 102 files may rely on transitive inclusion for
`<vector>`/`<string>`/`<map>`/`<set>`, unconfirmed one by one" — not "no missing include
found." The two header cases that matter most for the extension's own build
(`shell/capture/capture_orchestrator.h`, `src/ext_keys.h`) are confirmed satisfied
transitively.
- **Templates / two-phase lookup.** Exactly 7 templates in the tree
(`core/wire/bytes.h:25,61`, `core/wire/ext_state_read.h:37`,
`core/instrument/engine/play_params.h:191,203,208,218`). None derives from a dependent base,
none calls an unqualified dependent name, none needs `typename`/`template` disambiguation.
GCC/Clang-safe as written.
- **Compiler extensions.** Zero `#pragma` other than `#pragma once`; zero `__declspec`,
`__attribute__`, `__forceinline`, anonymous struct/union, or MSVC-permissive construct in
`src/**`.
- **`HWND__` forward declaration.** `drag_out_win.h:19` declares `struct HWND__;`; SWELL declares
`typedef struct HWND__ *HWND;` (`swell-types.h:211`) — same tag, no `-Wmismatched-tags`.
- **`preview_register_t` platform fork.** `panel_audition.cpp:3641,5863` and
`panel_state.h:2024` match the SDK's own fork exactly (`CRITICAL_SECTION cs` on `_WIN32`,
`pthread_mutex_t mutex` otherwise — `vendor/reaper-sdk/sdk/reaper_plugin.h:13081312`).
`<pthread.h>` is included on the non-Windows branch. Correct.
- **Prompts and message boxes.** Every user prompt goes through REAPER's own
`GetUserInputs`/`ShowMessageBox`/`ShowConsoleMsg` (`panel_bank_ops.cpp:2425,81,100,126,381`),
never a Win32 `MessageBox`. Cross-platform by construction.
- **Menus.** `CreatePopupMenu`/`InsertMenu`/`TrackPopupMenu(TPM_RETURNCMD)`/`DestroyMenu` are all
SWELL-provided (`swell-functions.h:528532`, and `TrackPopupMenu`/`DestroyMenu`/
`CreatePopupMenu` entries); the "pos < 0 appends" assumption at `panel_bank_ops.cpp:226` matches
SWELL's `SWELL_InsertMenu`. The comment at `:220222` is accurate.
- **Keyboard/accelerator path.** `accelerator_register_t` + `MSG`/`WM_KEYDOWN` + `GetFocus` +
`GetParent` (`panel_input.cpp:503521`) are all SWELL-provided; every `VK_*` used is in
`swell-types.h`.
- **Double-buffered paint.** `LICE_SysBitmap` + `getDC()` + `BitBlt(SRCCOPY)`
(`panel_render.cpp:474,549`) — `LICE_SysBitmap` has real non-Windows paths
(`lice.cpp:179`, `lice.h:362`), `BitBlt`/`SRCCOPY` are SWELL-provided.
- **Byte order.** Handled explicitly everywhere it matters (see §1.1). No native-integer
`memcpy` onto a wire buffer anywhere in `core/wire`.
- **Hot-path guardrails.** Nothing in this audit's remediation directions touches
`peaks::computeEnvelope` (still a free function), the audition call-through
(`panel_audition.cpp` — direct calls, no interface), or the realtime tick's single-pointer-test
idle path (`main.cpp:156`). L2-04's number-codec change is on the JSON/persist path, which root
`CLAUDE.md` explicitly declares off all hot paths. **No recommendation here adds a hot-path
indirection.**
---
## 3. Extension vs. instrument — the cost split
The two artifacts are cleanly separable, and the evidence says the split is very lopsided.
### 3.1 Extension only (`reaper_reasampler`)
**What must change in source:** two compile Blockers (L2-01, L2-02 — both one-line), one load
hazard (L2-03), one silent-failure diagnostic (L2-06), one correctness/safety fix (L2-05), one
data-integrity fix (L2-04), and cosmetics (L2-09). Nothing here is architectural. The panel's
whole SWELL/LICE surface — dialog lifecycle, docking, `WM_PAINT` double-buffered LICE draw,
mouse/wheel/capture, cursors, menus, keyboard accelerator, modifier keys, tooltips, drag-out,
drop-in — was checked call-by-call against the vendor headers and is **already SWELL-portable**;
see §1.5 and "Surfaces checked". This is the single most load-bearing finding of the audit: the
panel was written against SWELL's vocabulary throughout, and the Windows-only escapes are three
small ones (`DragAcceptFiles`, `SHFileOperationW`, OLE `DoDragDrop`) that each already carry a
non-Windows branch or a documented reason they do not.
**Estimate shape:** S+S+S+S+M+M+S. No L item.
### 3.2 Instrument additionally (`reasampler_vst`)
**What must be written from nothing:** an X11-embed `IPlugView` (window creation and parenting
without `RegisterClass`/`CreateWindowEx`), an `IRunLoop`/`ITimerHandler`-driven replacement for
the `SetTimer`/`WM_TIMER` sync poll, an event-driven input path replacing the entire `wndProc`
switch (~150 lines of `editor_platform.cpp:139296`), hover-leave detection without
`TrackMouseEvent`, modifier reads via `GetAsyncKeyState` instead of `GetKeyState`, and drop-accept
without `DragAcceptFiles`. The paint side is the cheapest part — every painter already draws into
a `LICE_IBitmap` through the shared kit (`editor_paint.cpp:28,51`), so the drawing survives a
window-system change intact; it is the *window and event plumbing* that is entirely absent. The
TCP/MCP embed strip (`reasampler_embed.cpp:135141`) is a smaller, separate 0→1
(`REAPER_FXEMBED_WM_IS_SUPPORTED` currently returns 0 off Windows; the paint body at `:174+` is
LICE and would port).
**Estimate shape:** one L, and it is a genuinely new competence rather than a port.
### 3.3 The consequence for sequencing
The extension is a Linux target that is largely *already met* and needs a handful of small,
well-localized fixes. The instrument is a separate project. The two share exactly one file —
`shell/panel/draw_kit` — and that file's only Linux blocker is L2-01. Daniel rules on whether the
instrument is in scope at all; the evidence says the extension does not wait on it.
---
## 4. Feature-degradation list — what builds but behaves differently, or not at all
Blunt column: **GONE** = the capability does not exist on Linux; **DIFFERENT** = it exists with
changed behaviour; **AT RISK** = depends on an unverified assumption.
| Feature | Verdict | What actually happens | Cite |
|---|---|---|---|
| Prune → Recycle Bin | **GONE** | Hard `unlink`, unrecoverable | `prune_fs.cpp:200208` |
| Prune's "file is locked" skip | **GONE** | `unlink` on an in-use file succeeds; the skip branch is dead code | `prune_fs.cpp:205` |
| Docked bank panel | **GONE (today)** | `SWELL_CreateDialog` returns null; toggle is a silent no-op until the resgen source is added (§7) | `panel_window.cpp:135137` |
| VST3 editor | **GONE** | `isPlatformTypeSupported` false for every type; host shows a generic UI | `editor_platform.cpp:3742` |
| VST3 generic-UI fallback | **GONE (today)** | Zero parameters registered as of this base; Phase Γ-W4-T1 changes this | grep `addParameter` over `shell/instrument/*.cpp` → no hits |
| TCP/MCP embed strip | **GONE** | `REAPER_FXEMBED_WM_IS_SUPPORTED` returns 0 | `reasampler_embed.cpp:135141` |
| Kit fonts (Segoe UI / Consolas) | **DIFFERENT** | fontconfig substitutes silently; metrics and ellipsis points shift; ValueMono may lose tabular alignment | `swell-gdi-lice.cpp:450487` |
| OS drag-out copy-only guarantee | **DIFFERENT** | No effect mask; copy-only is conventional not structural | `drag_out_win.cpp:257270` |
| OS drag-out readiness/outcome | **DIFFERENT** | Probe degrades to "non-empty"; return is always `true`; 500 ms no-motion timeout has no Windows analog | `drag_out_win.cpp:276278`, `swell-generic-gdk.cpp:35793580` |
| Panel file-drop ingest | **AT RISK** | Depends on SWELL delivering `WM_DROPFILES` without the Win32 opt-in | `panel_window.cpp:145150` |
| Bank index float round-trip | **AT RISK** | Unparseable under a non-C `LC_NUMERIC` | `json.cpp:3943,190196` |
| Extension load | **AT RISK** | Any one unresolved API name in ~869 = silent refusal | `main.cpp:292293` |
| Path case sensitivity | **DIFFERENT (correct)** | No case-fold; correct for Linux, and the tests already assert it | `capture_paths.cpp:1820` |
| Prune reclaim on a symlinked bank file | **AT RISK** | `fs::directory_iterator` + `is_regular_file()` follows symlinks (C++17); size is read from the target via `file_size()` but `fs::remove` deletes the link, not the target — prune reports N bytes reclaimed and reclaims zero. Symlinked media folders are far more idiomatic on Linux than Windows. | `prune_fs.cpp:113124` |
| Audition / preview | **INTACT** | `preview_register_t` fork matches the SDK; `PlayPreview`/`StopPreview` are REAPER API | `panel_audition.cpp:3663` |
| Docking, menus, cursors, keyboard, wheel, capture rollback, tooltips | **INTACT** | All SWELL-provided; verified call-by-call | §1.5 |
| Capture pillar (offline + realtime), persist, tracking, prune *computation* | **INTACT** | Pure `core/` + REAPER API only | §1.1 |
---
## 5. Open questions
### `[verify — Linux]` — answerable only on a Linux box
1. **Process locale.** Read `setlocale(LC_NUMERIC, nullptr)` inside a running REAPER-Linux
process (print via `ShowConsoleMsg`) on a machine whose user locale uses a decimal comma.
Decides whether L2-04 is urgent or latent.
2. **REAPER API completeness.** Instrument `main.cpp:292` to print
`REAPERAPI_LoadAPI`'s return value instead of discarding it, load once, and record the count.
Non-zero decides whether L2-03 is a real Blocker and names the gap.
3. **Panel file-drop routing.** With the panel docked, drag a WAV from the file manager onto it
and observe whether `dlgProc` sees `WM_DROPFILES` (L2-07). If not, add `WS_EX_ACCEPTFILES`.
4. **Which SWELL GDI/locale build REAPER ships.** Whether REAPER's `libSwell.so` is built with
`SWELL_SUPPORT_GTK` (→ `gtk_init_check`, `swell-generic-gdk.cpp:366`) or without
(→ `gdk_init_check`, `:368`) changes the likelihood in (1). Observable indirectly via (1).
5. **fontconfig substitution for "Consolas".** Whether it lands on a monospaced face on a stock
distro decides whether L2-09 is cosmetic or a real readability regression on numeric readouts.
6. **`SWELL_InitiateDragDropOfFileList` acceptance semantics.** Whether a GDK-initiated file drag
is accepted as a copy by common targets (a file manager, another DAW), and whether the 500 ms
no-motion timeout (`swell-generic-gdk.cpp:35793580`) cancels a slow user gesture.
7. **Prune against an in-use file.** Play an item from a bank file, prune it, confirm the audio
survives until the fd closes and the file is then unrecoverable — the concrete demonstration
behind L2-05(b).
### `[Daniel]` — scope decisions, not knowledge gaps
1. **Is the VST3 instrument in scope for Linux at all?** §3 supplies the cost split; the
extension does not depend on the answer. This is the phase's one real fork.
2. **Is a hard `unlink` an acceptable prune on Linux**, with a platform-aware confirmation
string — or must XDG trash be implemented before Linux ships? (L2-05.)
3. **Is silent font substitution acceptable**, or does the kit get an explicit Linux face list?
(L2-09.)
4. **Does "copy-only is structural" survive as a shipped invariant** when one platform can only
offer it conventionally? (L2-10.)
---
## 6. Ordering sketch
Dependency order only — this is not a plan, and it assigns no waves.
1. **Make it compile.** L2-01 (`FF_DONTCARE`), L2-02 (`GetCurrentProcessId`). Nothing else can
be observed until these land; both are one-line and independent of each other.
2. **Make it load, visibly.** L2-03 (`REAPERAPI_LoadAPI` diagnostic or `REAPERAPI_MINIMAL`) —
must precede any runtime verification, because it is the failure mode that produces no
evidence. Pairs naturally with `[verify — Linux]` (2).
3. **Make the panel appear.** L2-06 (the missing-dialog diagnostic) alongside T1's resgen wiring.
The diagnostic should land first so the resgen step can be confirmed rather than assumed.
4. **Then, and only then, verify.** `[verify — Linux]` items 1, 3, 5, 6, 7 all need a running
panel. Item 2 is already answered by step 2. **The critical path for this step runs through
the parallel build-system track, not this one:** "a running panel" means T1's resgen wiring
(§7 bullet 1) has landed — this doc doesn't own that work and doesn't know its schedule, so
step 3's "alongside T1's resgen wiring" is doing real scheduling work, not just sequencing
flavor. Anyone driving this ordering sketch needs to coordinate with T1 before step 4 can
start, not just after step 3 finishes on this track's own items.
5. **Data integrity.** L2-04 (locale-independent number codec) — pure `core/` work, unit-testable
on Windows, and it should land before any Linux user saves a project. Sequenced after the
verify only so its urgency is known; the *work* is not blocked.
6. **Deletion safety.** L2-05 (XDG trash or a platform-aware confirm) — gated on `[Daniel]` (2).
7. **Ingest opt-in, if needed.** L2-07 — no independent fix; it is the resgen work already
sequenced in step 3, strictly gated on `[verify — Linux]` (3) to confirm it's needed at all.
8. **Cosmetics.** L2-09 (fonts), L2-10 (drag-out doc). Independent of everything above.
9. **Separately, if ruled in.** L2-08 — the instrument's X11 editor. No dependency in either
direction on 18 except L2-01, which the shared `draw_kit` needs regardless.
---
## 7. Referred to T1 (build-system; spotted, not analyzed)
- **SWELL dialog resgen is not wired for Linux.** `src/app/CMakeLists.txt:97` — the
`target_sources(... resource.rc_mac_dlg.h)` line is commented out in the Linux `else()` branch
(and in the `APPLE` branch, `:86`). This is the mechanical cause of L2-06.
- **VST3 target is `if(WIN32 …)`-gated.** `src/shell/instrument/CMakeLists.txt:9` — the target is
not configured at all off Windows; `dllmain.cpp` is hard-coded at `:78` where Linux needs
`linuxmain.cpp` (`vendor/vst3sdk/public.sdk/source/main/linuxmain.cpp` exists).
- **`SHCreateStdEnumFmtEtc` / `SHFileOperationW` / `OleInitialize` need shell32/ole32.** Only on
the Windows branch, but worth confirming the Linux branch links none of them.
- **`reaper_plugin.h`'s relative SWELL include.** `vendor/reaper-sdk/sdk/reaper_plugin.h:49`
does `#include "../WDL/swell/swell.h"`, which does not exist under `vendor/reaper-sdk/`. It
resolves only because `${WDL_INC}` is `vendor/WDL/WDL` and `vendor/WDL/WDL/../WDL/swell/swell.h`
is the same file. Works, but it is a coincidence worth knowing before anyone moves an include
path.
- **`<filesystem>` link requirement.** GCC < 9 needs `-lstdc++fs`; `core/` and `shell/` both use
`std::filesystem` heavily.
- **LICE TU set.** `CMakeLists.txt:7982` adds `lice.cpp`, `lice_line.cpp`, `lice_arc.cpp`,
`lice_textnew.cpp` unconditionally. All four have non-Windows paths, but the Linux link needs
them compiled against `SWELL_PROVIDED_BY_APP` consistently with the module.
---
## Summary table
| ID | Finding | Severity | Effort | Artifact |
|-------|-----------------------------------------------------------------|----------|--------|--------------|
| L2-01 | `FF_DONTCARE` undefined off Windows (`draw_kit.cpp:73`) | Blocker | S | both |
| L2-02 | `GetCurrentProcessId()` unguarded, absent from SWELL | Blocker | S | extension |
| L2-03 | `REAPERAPI_LoadAPI` all-or-nothing over ~869 names, silent | Blocker `[verify]` | S | extension |
| L2-04 | Persisted floats are `LC_NUMERIC`-dependent, both directions | Major | M | both |
| L2-05 | Prune loses trash AND the locked-file backstop | Major | M | extension |
| L2-06 | Panel dialog never created; silent no-op | Major | S | extension |
| L2-07 | No drop-accept opt-in on SWELL; real seam is resgen, same as L2-06 | Major `[verify]` | — (see L2-06) | extension |
| L2-08 | No Linux VST3 editor; no parameters to fall back to | Major | L | instrument |
| L2-09 | Segoe UI / Consolas substitute silently via fontconfig | Minor | S | both |
| L2-10 | SWELL drag-out: no copy mask, no probe, no outcome | Minor | S | extension |
| L2-11 | Case-fold is `_WIN32`-only (right for Linux, wrong for macOS) | Minor | S | both |
| L2-12 | `fs::path::string()` narrowing — Linux is strictly better | Informational | — | both |