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.
46 KiB
Λ-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
#includeinsrc/core/**is either a"core/…"sibling, one of 26 standard-library headers, or the CMake-generatedversion_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), twoNDEBUGassert 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 innormalizeSlashes. 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_CreateDialogbehind theCreateDialogParammacro (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*, everyVK_*used. WM_MOUSEWHEELreally does carry screen coords inlParamon the GDK backend — the comment atpanel_window.cpp:100–102is correct (swell-generic-gdk.cpp:1442).WM_CAPTURECHANGEDreally 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–3748vspanel_drag.cpp:141–147,159–161). GetAsyncKeyStatereturns the0x8000high bit forVK_CONTROL/VK_SHIFT/VK_MENU(swell-generic-gdk.cpp:2439–2441), sopanel_state.h:409–411works verbatim.GetTickCountis SWELL-provided (swell-functions.h:81), sopanel_drag.cpp:308,324— the only unguarded Win32-looking calls in the panel — are fine.SWELL_InitiateDragDropOfFileListdoes exist on Linux and is a real implementation, not a stub:swell-generic-gdk.cpp:3563–3592(gdk_drag_begin+ a nestedSWELL_RunMessageLoopuntil 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 atswell-functions.h:1011. The doc's claim holds.- LICE itself is portable:
lice.h:31includesswell-types.hoff Windows,lice.cpp:22includesswell.h,LICE_SysBitmap::__resizehas a SWELL framebuffer path (lice.cpp:179–182), andLICE_CachedFont::DrawTextImplcarries 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
<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 #includes are <atomic> <cstdint> <filesystem> <fstream> <string> <system_error> <vector> 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
GlobalFrees 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_asserts (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
<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), andprocessor_reload.cpp'sstd::unique_ptrcomes fromreasampler_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 needstypename/templatedisambiguation. GCC/Clang-safe as written. - Compiler extensions. Zero
#pragmaother than#pragma once; zero__declspec,__attribute__,__forceinline, anonymous struct/union, or MSVC-permissive construct insrc/**. HWND__forward declaration.drag_out_win.h:19declaresstruct HWND__;; SWELL declarestypedef struct HWND__ *HWND;(swell-types.h:211) — same tag, no-Wmismatched-tags.preview_register_tplatform fork.panel_audition.cpp:36–41,58–63andpanel_state.h:20–24match the SDK's own fork exactly (CRITICAL_SECTION cson_WIN32,pthread_mutex_t mutexotherwise —vendor/reaper-sdk/sdk/reaper_plugin.h:1308–1312).<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:24–25,81,100,126,381), never a Win32MessageBox. Cross-platform by construction. - Menus.
CreatePopupMenu/InsertMenu/TrackPopupMenu(TPM_RETURNCMD)/DestroyMenuare all SWELL-provided (swell-functions.h:528–532, andTrackPopupMenu/DestroyMenu/CreatePopupMenuentries); the "pos < 0 appends" assumption atpanel_bank_ops.cpp:226matches SWELL'sSWELL_InsertMenu. The comment at:220–222is accurate. - Keyboard/accelerator path.
accelerator_register_t+MSG/WM_KEYDOWN+GetFocus+GetParent(panel_input.cpp:503–521) are all SWELL-provided; everyVK_*used is inswell-types.h. - Double-buffered paint.
LICE_SysBitmap+getDC()+BitBlt(SRCCOPY)(panel_render.cpp:474,549) —LICE_SysBitmaphas real non-Windows paths (lice.cpp:179,lice.h:362),BitBlt/SRCCOPYare SWELL-provided. - Byte order. Handled explicitly everywhere it matters (see §1.1). No native-integer
memcpyonto a wire buffer anywhere incore/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 rootCLAUDE.mdexplicitly 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
- Process locale. Read
setlocale(LC_NUMERIC, nullptr)inside a running REAPER-Linux process (print viaShowConsoleMsg) on a machine whose user locale uses a decimal comma. Decides whether L2-04 is urgent or latent. - REAPER API completeness. Instrument
main.cpp:292to printREAPERAPI_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. - Panel file-drop routing. With the panel docked, drag a WAV from the file manager onto it
and observe whether
dlgProcseesWM_DROPFILES(L2-07). If not, addWS_EX_ACCEPTFILES. - Which SWELL GDI/locale build REAPER ships. Whether REAPER's
libSwell.sois built withSWELL_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). - 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.
SWELL_InitiateDragDropOfFileListacceptance 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.- 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
- 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.
- Is a hard
unlinkan acceptable prune on Linux, with a platform-aware confirmation string — or must XDG trash be implemented before Linux ships? (L2-05.) - Is silent font substitution acceptable, or does the kit get an explicit Linux face list? (L2-09.)
- 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.
- 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. - Make it load, visibly. L2-03 (
REAPERAPI_LoadAPIdiagnostic orREAPERAPI_MINIMAL) — must precede any runtime verification, because it is the failure mode that produces no evidence. Pairs naturally with[verify — Linux](2). - 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.
- 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. - 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. - Deletion safety. L2-05 (XDG trash or a platform-aware confirm) — gated on
[Daniel](2). - Ingest opt-in, if needed. L2-07 — strictly gated on
[verify — Linux](3). - Cosmetics. L2-09 (fonts), L2-10 (drag-out doc). Independent of everything above.
- 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_kitneeds regardless.
7. Referred to T1 (build-system; spotted, not analyzed)
- SWELL dialog resgen is not wired for Linux.
src/app/CMakeLists.txt:97— thetarget_sources(... resource.rc_mac_dlg.h)line is commented out in the Linuxelse()branch (and in theAPPLEbranch,: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.cppis hard-coded at:78where Linux needslinuxmain.cpp(vendor/vst3sdk/public.sdk/source/main/linuxmain.cppexists). SHCreateStdEnumFmtEtc/SHFileOperationW/OleInitializeneed 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:49does#include "../WDL/swell/swell.h", which does not exist undervendor/reaper-sdk/. It resolves only because${WDL_INC}isvendor/WDL/WDLandvendor/WDL/WDL/../WDL/swell/swell.his 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/andshell/both usestd::filesystemheavily.- LICE TU set.
CMakeLists.txt:79–82addslice.cpp,lice_line.cpp,lice_arc.cpp,lice_textnew.cppunconditionally. All four have non-Windows paths, but the Linux link needs them compiled againstSWELL_PROVIDED_BY_APPconsistently 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 |