From 893455594f9db312ca04b9aaab634979bb420c19 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 2 Aug 2026 06:07:28 -0400 Subject: [PATCH 1/2] docs: audit source/runtime Linux-portability for extension and VST3 instrument Two one-line compile blockers, a locale hazard in the persisted number codec, and a cost split showing the extension is nearly there while the instrument editor is a from-scratch X11 job. --- .../lambda-w1-t2-source-runtime.md | 677 ++++++++++++++++++ 1 file changed, 677 insertions(+) create mode 100644 docs/product/audit-notes/lambda-w1-t2-source-runtime.md diff --git a/docs/product/audit-notes/lambda-w1-t2-source-runtime.md b/docs/product/audit-notes/lambda-w1-t2-source-runtime.md new file mode 100644 index 0000000..e561ade --- /dev/null +++ b/docs/product/audit-notes/lambda-w1-t2-source-runtime.md @@ -0,0 +1,677 @@ +# Λ-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 + ` + + ` — + 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:18–20`, 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:26–31,62–74` 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:52–53` states the one real assumption ("target is +x86/ARM-LE only, no big-endian byte-swap") and `wav_codec.cpp:165–168` 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:39–41,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:19–33,41–51`), 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 28–33, 50–51). Six other files matched a +`C:\`/`C:/` grep — every hit is opaque *string data* fed to a pure function +(`test_bank_model.cpp:186–187,243–245`, `test_bake_wire.cpp:77`, `test_drag_out.cpp:291–293,388`, +`test_origin_ledger.cpp:56`, `test_sample_usage.cpp:502`), 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:52–53` → `__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 | Guarded blocks | Shape | +|---|---|---| +| `shell/instrument/` (VST3 editor + embed) | 44 of 65 | Whole-TU `#ifdef _WIN32` … `#endif` — Windows-only by design | +| `shell/panel/` | 9 | Include-selection + two small `#else` bodies | +| `shell/persist/`, `shell/capture/`, `shell/actions/` | 6 | Trash fallback, `localtime_r`, the SWELL drag-out branch | +| `core/` | 1 | The case-fold | +| `src/resource.h`, `src/ext_keys.h` | comments only | — | + +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 in the extension.** + +The instrument's guards are complete in the opposite direction: `reasampler_editor.h:156–269` +wraps the entire paint/input/timer/`wndProc`/`invalidate` family in `#ifdef _WIN32`, and +`editor_platform.cpp:298–306` stubs only the three IPlugView overrides that must exist. Every +`invalidate()` call site is inside a guard (verified: 7 calls in `editor_session.cpp` at +119, 126, 139, 141, 150, 156, 174 — all within the `#ifdef _WIN32` regions opened at 105 and +173; `editor_controls.cpp`, `editor_models.cpp`, `instrument_bake.cpp` have zero). 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:135–145`). On Linux REAPER's own +`libSwell.so` is built with `swell-gdi-lice.o`, `-DSWELL_LICE_GDI`, `-DSWELL_FREETYPE` and +`-lfontconfig` (`vendor/WDL/WDL/swell/Makefile:100–107,141–151`) — i.e. the GDI SWELL +answers with is the **LICE-backed, freetype/fontconfig** one, not the null stub in +`swell-gdi-generic.cpp` (that file is `#ifndef SWELL_PROVIDED_BY_APP`, line 24). + +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` (`:319–320`), `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:100–102` 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:90–98`) has a live trigger. +- Cursors: `LoadCursor`→`SWELL_LoadCursor` (`swell-functions.h:728–730`), `SetCursor` + (`:740–741`), 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:3736–3748` vs `panel_drag.cpp:141–147,159–161`). +- `GetAsyncKeyState` returns the `0x8000` high bit for `VK_CONTROL`/`VK_SHIFT`/`VK_MENU` + (`swell-generic-gdk.cpp:2439–2441`), so `panel_state.h:409–411` 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 real implementation, not a + stub: `swell-generic-gdk.cpp:3563–3592` (`gdk_drag_begin` + a nested `SWELL_RunMessageLoop` + until capture drops, with a 500 ms no-motion timeout). The headless backend + (`swell-generic-headless.cpp:246`) and macOS (`swell-dlg.mm:3534`) also define it; it is + declared at `swell-functions.h:1011`. **The doc's claim holds.** +- 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:179–182`), and `LICE_CachedFont::DrawTextImpl` carries real non-Windows branches + (`lice_textnew.cpp:236–249,829,1027–1028,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:12–16` (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 +`` 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 ` + ` plus project/SDK headers, lines 6–27). + +**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 `:56–57`). `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:292–293` (`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:44–48`; 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. +**Effort: S.** +**Direction.** Either switch `main.cpp` to `REAPERAPI_MINIMAL` + an explicit `WANT` list (the +other TUs already do this — e.g. `panel_window.cpp:26–31`, `panel_audition.cpp:12–16`), 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:39–43` (`%.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), `src/core/capture/tail_control.cpp:74` (`%.17g` — the +per-project tail setting). +**Location (readers):** `src/core/json/json.cpp:190–196` (`std::strtod`), +`src/core/wire/wire.cpp:121–130` (`Cursor::fieldDouble`, `std::strtod`), +`src/core/capture/render_settings.cpp:178–186` (`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:183`), 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. +**Effort: M** — three 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 three 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:167–209`; the non-Windows branch is `:200–208`. + +**Mechanism.** Two Windows properties are load-bearing for the deletion authority and neither +survives. (a) The Windows path routes through `SHFileOperationW` + `FOF_ALLOWUNDO` (`:186–188`) +— deletions are recoverable from the Recycle Bin; the `#else` is a hard `fs::remove` (`:203`). +The header already states this honestly (`:30–34`, `:162–166`). (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:135–137`. + +**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:145–150` (the `DragAcceptFiles` call is +`#ifdef _WIN32`), handler at `:47–61,65–67`. + +**Mechanism.** The comment at `:145–147` is accurate — SWELL exposes no `DragAcceptFiles` +(grep over `vendor/WDL/WDL/swell/` finds only `DragQueryFile` and `DragFinish`, +`swell-functions.h:1006–1007`). 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:6956–6981`), and `SendMessage(cw, WM_DROPFILES, (WPARAM)gobj, 0)` +(`swell-generic-gdk.cpp:1622`). The default child proc forwards `WM_DROPFILES` up to the parent +only when the window *lacks* `WS_EX_ACCEPTFILES` (`swell-wnd-generic.cpp:7680–7688`) — i.e. the +opt-in on SWELL is an ex-style bit, not an API call, and the message reaches the window under +the pointer either way. So the panel's own `dlgProc` plausibly receives the drop **without any +opt-in at all**. Two supporting details 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", `:7725–7728`) while SWELL itself +`GlobalFree`s the handle right after `SendMessage` (`swell-generic-gdk.cpp:1623–1624`) — 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. +**Effort: S** — if the verify fails, the fix is +`SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLong(hwnd, GWL_EXSTYLE) | WS_EX_ACCEPTFILES)` +(both symbols exist: `swell-functions.h:319–320`, `swell-types.h`). +**Direction.** Verify first, then either do nothing or add the ex-style set in the `#else`. +`[verify — Linux]` drop a WAV onto the docked panel and check whether `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:37–42` (`isPlatformTypeSupported` +returns `kResultTrue` **only** for `kPlatformTypeHWND`, and only inside `#ifdef _WIN32`); +`:298–306` (the non-Windows stubs); `reasampler_editor.h:156–269` (the entire paint/input family +is `#ifdef _WIN32`); `reasampler_embed.cpp:135–141` (`REAPER_FXEMBED_WM_IS_SUPPORTED` returns 0 +off Windows). `reasampler_processor.cpp:360–365` 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` (`:267–279`), `Linux::IEventHandler` (`:224`), +`Linux::ITimerHandler` (`:238`), 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:79–103,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` (`:319–320`); `TrackMouseEvent`/`WM_MOUSELEAVE` (`:176–198`) — SWELL has neither, a +gap the panel layer already documents at `panel_state.h:245–246`; `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:70–77` (`loadFont`), and the two literal faces it is +called with — Segoe UI and Consolas, per `draw_kit.h:63–64`. + +**Mechanism.** On Linux `CreateFont` goes through fontconfig: `FcPatternAddString(pat, FC_FAMILY, +lfFaceName)` → `FcFontMatch` (`swell-gdi-lice.cpp:450–487`). `FcFontMatch` never fails — it +returns the best available substitute. So `loadFont`'s failure branch (`draw_kit.cpp:74`, +"if (!hf) return") will not fire, and the kit will draw in *some* font with different metrics. +Consequences: the kit's end-ellipsis clipping (`draw_kit.h:70–74`) is tuned against Segoe's +advance widths, so labels will clip at different points; `Font::ValueMono` is only tabular if +fontconfig's substitution for "Consolas" happens to be monospaced, which is a distro-config +property, not a guarantee. The two WCAG `static_assert`s (`draw_kit.cpp:53–54`) are on **pixel +height and weight**, not on the face, so they still hold — the contrast entitlement is not at +risk. + +**Severity: Minor** — cosmetic; nothing breaks, layout gets uglier and possibly clipped. +**Effort: S.** +**Direction.** Add a platform fallback face list at the one place the faces are chosen +(`draw_kit.cpp`'s `loadFont` call sites — the header already promises "the face is chosen here so +a change is one line", `:69`). DejaVu Sans / DejaVu Sans Mono are the safe Linux defaults; note +that SWELL's own no-fontconfig fallback list names LiberationSans/DejaVuSans and +LiberationMono/DejaVuSansMono (`swell-gdi-lice.cpp:505–507`), which is 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:249–282` (the `#else` branch); +`drag_out_win.h:7–11,29–33` 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()` (`:276–278`), so the caller's careful +coupling — "do not tear down the internal drag until the OS is known ready" +(`panel_drag.cpp:227–244`) — 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:240–241`) *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:3543–3552`) — 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:7–11`) 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:18–20`. + +**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:50–51`). 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.** +**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:123–128`, 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 ` + `. 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 `` `std::abs` + (`velocity_curve.cpp:229`, `` at `:6`), and `processor_reload.cpp`'s `std::unique_ptr` + comes from `reasampler_processor.h:12`. **No missing include found.** +- **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:36–41,58–63` and + `panel_state.h:20–24` match the SDK's own fork exactly (`CRITICAL_SECTION cs` on `_WIN32`, + `pthread_mutex_t mutex` otherwise — `vendor/reaper-sdk/sdk/reaper_plugin.h:1308–1312`). + `` 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:24–25,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:528–532`, and `TrackPopupMenu`/`DestroyMenu`/ + `CreatePopupMenu` entries); the "pos < 0 appends" assumption at `panel_bank_ops.cpp:226` matches + SWELL's `SWELL_InsertMenu`. The comment at `:220–222` is accurate. +- **Keyboard/accelerator path.** `accelerator_register_t` + `MSG`/`WM_KEYDOWN` + `GetFocus` + + `GetParent` (`panel_input.cpp:503–521`) 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:139–296`), 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:135–141`) 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:200–208` | +| 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:135–137` | +| VST3 editor | **GONE** | `isPlatformTypeSupported` false for every type; host shows a generic UI | `editor_platform.cpp:37–42` | +| 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:135–141` | +| Kit fonts (Segoe UI / Consolas) | **DIFFERENT** | fontconfig substitutes silently; metrics and ellipsis points shift; ValueMono may lose tabular alignment | `swell-gdi-lice.cpp:450–487` | +| OS drag-out copy-only guarantee | **DIFFERENT** | No effect mask; copy-only is conventional not structural | `drag_out_win.cpp:257–270` | +| 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:276–278`, `swell-generic-gdk.cpp:3547–3548` | +| Panel file-drop ingest | **AT RISK** | Depends on SWELL delivering `WM_DROPFILES` without the Win32 opt-in | `panel_window.cpp:145–150` | +| Bank index float round-trip | **AT RISK** | Unparseable under a non-C `LC_NUMERIC` | `json.cpp:39–43,190–196` | +| Extension load | **AT RISK** | Any one unresolved API name in ~869 = silent refusal | `main.cpp:292–293` | +| Path case sensitivity | **DIFFERENT (correct)** | No case-fold; correct for Linux, and the tests already assert it | `capture_paths.cpp:18–20` | +| Audition / preview | **INTACT** | `preview_register_t` fork matches the SDK; `PlayPreview`/`StopPreview` are REAPER API | `panel_audition.cpp:36–63` | +| 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:3547–3548`) 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. +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 — strictly gated on `[verify — Linux]` (3). +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 1–8 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. +- **`` link requirement.** GCC < 9 needs `-lstdc++fs`; `core/` and `shell/` both use + `std::filesystem` heavily. +- **LICE TU set.** `CMakeLists.txt:79–82` 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; ingest-by-drop unverified | Major `[verify]` | S | 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 | From b1726b0ad3e47a0ee3af387491ab5a3eb4aa50aa Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Sun, 2 Aug 2026 06:39:36 -0400 Subject: [PATCH 2/2] Fix audit citations, counts, and the L2-07/L2-09 mechanisms after code-review verification --- .../lambda-w1-t2-source-runtime.md | 267 +++++++++++++----- 1 file changed, 191 insertions(+), 76 deletions(-) diff --git a/docs/product/audit-notes/lambda-w1-t2-source-runtime.md b/docs/product/audit-notes/lambda-w1-t2-source-runtime.md index e561ade..e2651bb 100644 --- a/docs/product/audit-notes/lambda-w1-t2-source-runtime.md +++ b/docs/product/audit-notes/lambda-w1-t2-source-runtime.md @@ -65,10 +65,11 @@ 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 28–33, 50–51). Six other files matched a -`C:\`/`C:/` grep — every hit is opaque *string data* fed to a pure function +non-Windows behaviour explicitly (lines 28–33, 50–51). 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:186–187,243–245`, `test_bake_wire.cpp:77`, `test_drag_out.cpp:291–293,388`, -`test_origin_ledger.cpp:56`, `test_sample_usage.cpp:502`), not platform behaviour. They pass +`test_origin_ledger.cpp:56`, `test_sample_usage.cpp:502`) — are not platform behaviour. They pass identically on Linux. ### 1.3 `app/` — portable. @@ -85,37 +86,64 @@ type, no conditional. The one hazard is behavioural, not structural — see **L2 `_WIN32|WIN32|__APPLE__|_MSC_VER|__linux__|__GNUC__|_WIN64` over `src/**/*.{cpp,h}`). Their distribution is the story: -| Area | Guarded blocks | Shape | +| Area | Lines | Shape | |---|---|---| -| `shell/instrument/` (VST3 editor + embed) | 44 of 65 | Whole-TU `#ifdef _WIN32` … `#endif` — Windows-only by design | -| `shell/panel/` | 9 | Include-selection + two small `#else` bodies | -| `shell/persist/`, `shell/capture/`, `shell/actions/` | 6 | Trash fallback, `localtime_r`, the SWELL drag-out branch | -| `core/` | 1 | The case-fold | -| `src/resource.h`, `src/ext_keys.h` | comments only | — | +| `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 in the extension.** +`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:156–269` wraps the entire paint/input/timer/`wndProc`/`invalidate` family in `#ifdef _WIN32`, and `editor_platform.cpp:298–306` stubs only the three IPlugView overrides that must exist. Every -`invalidate()` call site is inside a guard (verified: 7 calls in `editor_session.cpp` at -119, 126, 139, 141, 150, 156, 174 — all within the `#ifdef _WIN32` regions opened at 105 and -173; `editor_controls.cpp`, `editor_models.cpp`, `instrument_bake.cpp` have zero). So the -editor *links* off Windows — it just does nothing. +`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:135–145`). On Linux REAPER's own -`libSwell.so` is built with `swell-gdi-lice.o`, `-DSWELL_LICE_GDI`, `-DSWELL_FREETYPE` and -`-lfontconfig` (`vendor/WDL/WDL/swell/Makefile:100–107,141–151`) — i.e. the GDI SWELL -answers with is the **LICE-backed, freetype/fontconfig** one, not the null stub in -`swell-gdi-generic.cpp` (that file is `#ifndef SWELL_PROVIDED_BY_APP`, line 24). +`vendor/WDL/WDL/swell/swell-modstub-generic.cpp:135–145`). `[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:100–107,141–151` 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: @@ -143,11 +171,18 @@ Everything the panel layer calls was checked by name against (`swell-generic-gdk.cpp:2439–2441`), so `panel_state.h:409–411` 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 real implementation, not a - stub: `swell-generic-gdk.cpp:3563–3592` (`gdk_drag_begin` + a nested `SWELL_RunMessageLoop` - until capture drops, with a 500 ms no-motion timeout). The headless backend - (`swell-generic-headless.cpp:246`) and macOS (`swell-dlg.mm:3534`) also define it; it is - declared at `swell-functions.h:1011`. **The doc's claim holds.** +- `SWELL_InitiateDragDropOfFileList` **does exist on Linux**, and is a GDK implementation on + the GDK backend: `swell-generic-gdk.cpp:3563–3592` 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 `3563–3592` range itself. The headless backend defines + the **same-named function as an empty no-op stub** — + `swell-generic-headless.cpp:246–248` 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:179–182`), and `LICE_CachedFont::DrawTextImpl` carries real non-Windows branches @@ -216,8 +251,20 @@ here says a gap exists — but nothing rules it out either, and the failure mode opaque. **Severity: Blocker (conditional)** `[verify — Linux]` — a silent load refusal with no -diagnostic is the worst possible first-run experience for a port. -**Effort: S.** +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:45–46` — "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:26–31`, `panel_audition.cpp:12–16`) 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:26–31`, `panel_audition.cpp:12–16`), or keep the full load but print the failure count via `rec->GetFunc("ShowConsoleMsg")` before returning @@ -226,8 +273,11 @@ the full load but print the failure count via `rec->GetFunc("ShowConsoleMsg")` b ### L2-04 — every persisted floating-point number is `LC_NUMERIC`-dependent, in both directions **Location (writers):** `src/core/json/json.cpp:39–43` (`%.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), `src/core/capture/tail_control.cpp:74` (`%.17g` — the -per-project tail setting). +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:78–79`, which already goes through `json::numToStr` (the same +`json.cpp:39–43` codec cited above), so it is the same writer, not a third one. **Location (readers):** `src/core/json/json.cpp:190–196` (`std::strtod`), `src/core/wire/wire.cpp:121–130` (`Cursor::fieldDouble`, `std::strtod`), `src/core/capture/render_settings.cpp:178–186` (`std::stod` over REAPER's own `P_RAZOREDITS`). @@ -236,7 +286,7 @@ per-project tail setting). 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:183`), so they degrade to "malformed" +`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 @@ -247,11 +297,19 @@ when built with `SWELL_SUPPORT_GTK` (`swell-generic-gdk.cpp:366`; the `#else` br 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. -**Effort: M** — three writers and three readers, all in `core/`, all unit-testable; the fix is +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 three writers and three readers; +**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. @@ -316,25 +374,43 @@ having on Windows too. 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:6956–6981`), and `SendMessage(cw, WM_DROPFILES, (WPARAM)gobj, 0)` -(`swell-generic-gdk.cpp:1622`). The default child proc forwards `WM_DROPFILES` up to the parent -only when the window *lacks* `WS_EX_ACCEPTFILES` (`swell-wnd-generic.cpp:7680–7688`) — i.e. the -opt-in on SWELL is an ex-style bit, not an API call, and the message reaches the window under -the pointer either way. So the panel's own `dlgProc` plausibly receives the drop **without any -opt-in at all**. Two supporting details 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", `:7725–7728`) while SWELL itself -`GlobalFree`s the handle right after `SendMessage` (`swell-generic-gdk.cpp:1623–1624`) — so -`panel_window.cpp:59` calling `DragFinish` is safe, not a double-free. +(`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:320–321` 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:21–22` +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", `:7725–7728`) while SWELL itself `GlobalFree`s the handle right after +`SendMessage` (`swell-generic-gdk.cpp:1623–1624`) — 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. -**Effort: S** — if the verify fails, the fix is -`SetWindowLong(hwnd, GWL_EXSTYLE, GetWindowLong(hwnd, GWL_EXSTYLE) | WS_EX_ACCEPTFILES)` -(both symbols exist: `swell-functions.h:319–320`, `swell-types.h`). -**Direction.** Verify first, then either do nothing or add the ex-style set in the `#else`. -`[verify — Linux]` drop a WAV onto the docked panel and check whether `WM_DROPFILES` reaches -`dlgProc`. +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:37–42` (`isPlatformTypeSupported` @@ -345,8 +421,8 @@ off Windows). `reasampler_processor.cpp:360–365` still hands the host a `ReaSa **Mechanism.** The VST3 SDK *does* support Linux — `kPlatformTypeX11EmbedWindowID` (`vendor/vst3sdk/pluginterfaces/gui/iplugview.h:79`), `Linux::IRunLoop` with -`registerEventHandler`/`registerTimer` (`:267–279`), `Linux::IEventHandler` (`:224`), -`Linux::ITimerHandler` (`:238`), and a `linuxmain.cpp` module entry +`registerEventHandler`/`registerTimer` (`:267–279`), `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 @@ -381,25 +457,41 @@ already *is* that seam; it needs a sibling, not a rewrite. **Location:** `src/shell/panel/draw_kit.cpp:70–77` (`loadFont`), and the two literal faces it is called with — Segoe UI and Consolas, per `draw_kit.h:63–64`. -**Mechanism.** On Linux `CreateFont` goes through fontconfig: `FcPatternAddString(pat, FC_FAMILY, -lfFaceName)` → `FcFontMatch` (`swell-gdi-lice.cpp:450–487`). `FcFontMatch` never fails — it -returns the best available substitute. So `loadFont`'s failure branch (`draw_kit.cpp:74`, -"if (!hf) return") will not fire, and the kit will draw in *some* font with different metrics. -Consequences: the kit's end-ellipsis clipping (`draw_kit.h:70–74`) is tuned against Segoe's -advance widths, so labels will clip at different points; `Font::ValueMono` is only tabular if -fontconfig's substitution for "Consolas" happens to be monospaced, which is a distro-config -property, not a guarantee. The two WCAG `static_assert`s (`draw_kit.cpp:53–54`) are on **pixel -height and weight**, not on the face, so they still hold — the contrast entitlement is not at -risk. +**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:450–487`). 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:480–484` 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 `:493–539` compiles only in the `#else` +(`#ifndef SWELL_FONTCONFIG`) branch. However `swell-gdi-lice.cpp:400–401,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:236–249,829,1027–1028,1040`), not from `loadFont`'s +stated guard — `[verify — Linux]`. The two WCAG `static_assert`s (`draw_kit.cpp:53–54`) are on +**pixel height and weight**, not on the face, so they still hold regardless. -**Severity: Minor** — cosmetic; nothing breaks, layout gets uglier and possibly clipped. -**Effort: S.** -**Direction.** Add a platform fallback face list at the one place the faces are chosen -(`draw_kit.cpp`'s `loadFont` call sites — the header already promises "the face is chosen here so -a change is one line", `:69`). DejaVu Sans / DejaVu Sans Mono are the safe Linux defaults; note -that SWELL's own no-fontconfig fallback list names LiberationSans/DejaVuSans and -LiberationMono/DejaVuSansMono (`swell-gdi-lice.cpp:505–507`), which is a reasonable precedent to -copy. This same change fixes macOS (San Francisco / Menlo), one code path. +**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:154–158`, +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:154–158`). 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:505–507`), 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:249–282` (the `#else` branch); @@ -417,7 +509,10 @@ so (c) is inert today. Note the call path is already Linux-correct in one import `handOffToOs` releases capture and resets drag state (`panel_drag.cpp:240–241`) *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:3543–3552`) — with a 500 ms no-motion timeout that has no Windows analog. +(`swell-generic-gdk.cpp:3575–3584`, inside `SWELL_InitiateDragDropOfFileList` — the function +`drag_out_win.cpp:270` actually calls; the body is identical to the sibling +`SWELL_InitiateDragDrop` at `:3531–3560`, 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. @@ -435,7 +530,9 @@ structurally impossible" as a Windows-only claim in any future spec text. 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.** +**Severity: Minor** (no Linux defect). **Effort: S** — confirmed: the one platform conditional +in `core/` is a single `#ifdef _WIN32` block (`capture_paths.cpp:18–20`) 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". @@ -466,7 +563,18 @@ Recorded so the sweep's negative results are as auditable as its findings. 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 `` `std::abs` (`velocity_curve.cpp:229`, `` at `:6`), and `processor_reload.cpp`'s `std::unique_ptr` - comes from `reasampler_processor.h:12`. **No missing include found.** + 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++, `` and ``, were excluded + from that sweep; re-run including them: **45 files use `std::vector` without `#include + `, and 52 use `std::string` without `#include `** (raw grep-diff counts, + unread). `` and `` 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 + ``/``/``/``, 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, @@ -564,11 +672,12 @@ changed behaviour; **AT RISK** = depends on an unverified assumption. | TCP/MCP embed strip | **GONE** | `REAPER_FXEMBED_WM_IS_SUPPORTED` returns 0 | `reasampler_embed.cpp:135–141` | | Kit fonts (Segoe UI / Consolas) | **DIFFERENT** | fontconfig substitutes silently; metrics and ellipsis points shift; ValueMono may lose tabular alignment | `swell-gdi-lice.cpp:450–487` | | OS drag-out copy-only guarantee | **DIFFERENT** | No effect mask; copy-only is conventional not structural | `drag_out_win.cpp:257–270` | -| 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:276–278`, `swell-generic-gdk.cpp:3547–3548` | +| 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:276–278`, `swell-generic-gdk.cpp:3579–3580` | | Panel file-drop ingest | **AT RISK** | Depends on SWELL delivering `WM_DROPFILES` without the Win32 opt-in | `panel_window.cpp:145–150` | | Bank index float round-trip | **AT RISK** | Unparseable under a non-C `LC_NUMERIC` | `json.cpp:39–43,190–196` | | Extension load | **AT RISK** | Any one unresolved API name in ~869 = silent refusal | `main.cpp:292–293` | | Path case sensitivity | **DIFFERENT (correct)** | No case-fold; correct for Linux, and the tests already assert it | `capture_paths.cpp:18–20` | +| 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:113–124` | | Audition / preview | **INTACT** | `preview_register_t` fork matches the SDK; `PlayPreview`/`StopPreview` are REAPER API | `panel_audition.cpp:36–63` | | 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 | @@ -594,7 +703,7 @@ changed behaviour; **AT RISK** = depends on an unverified assumption. 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:3547–3548`) cancels a slow user gesture. + no-motion timeout (`swell-generic-gdk.cpp:3579–3580`) 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). @@ -624,12 +733,18 @@ Dependency order only — this is not a plan, and it assigns no waves. 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. + 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 — strictly gated on `[verify — Linux]` (3). +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 1–8 except L2-01, which the shared `draw_kit` needs regardless. @@ -669,7 +784,7 @@ Dependency order only — this is not a plan, and it assigns no waves. | 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; ingest-by-drop unverified | Major `[verify]` | 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 |