Files
reasampler/docs/product/audit-notes/lambda-w1-t2-source-runtime.md
T
daniel 893455594f 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.
2026-08-02 06:07:28 -04:00

46 KiB
Raw Blame History

Λ-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: LoadCursorSWELL_LoadCursor (swell-functions.h:728730), SetCursor (:740741), and all seven IDCs the panel uses map to real GDK cursors — IDC_ARROWGDK_LEFT_PTR, IDC_HANDGDK_HAND1, IDC_UPARROWGDK_CENTER_PTR, IDC_SIZEWEGDK_RIGHT_SIDE, IDC_SIZEALLGDK_FLEUR, IDC_IBEAMGDK_XTERM, IDC_NOGDK_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: Blockererror: '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 #includes 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.hswell.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: Blockererror: '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 GlobalFrees 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-setdocs/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_asserts (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