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.
This commit is contained in:
2026-08-02 06:07:28 -04:00
parent 6e937b9c61
commit 893455594f
@@ -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
`<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). Six other files matched a
`C:\`/`C:/` grep — every hit is 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`), 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 | 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: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: 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:135145`). 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:100107,141151`) — 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` (`: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 real implementation, not a
stub: `swell-generic-gdk.cpp:35633592` (`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: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.
**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: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), `src/core/capture/tail_control.cpp:74` (`%.17g` — the
per-project tail setting).
**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: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: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 `WM_DROPFILES` up to the parent
only when the window *lacks* `WS_EX_ACCEPTFILES` (`swell-wnd-generic.cpp:76807688`) — 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", `: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.
**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:319320`, `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: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` (`: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: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: `FcPatternAddString(pat, FC_FAMILY,
lfFaceName)``FcFontMatch` (`swell-gdi-lice.cpp:450487`). `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:7074`) 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:5354`) 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:505507`), 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: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:35433552`) — 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.**
**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.**
- **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:35473548` |
| 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` |
| 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:35473548`) 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 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; 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 |