Files
reasampler/docs/product/audit-notes/lambda-w1-t2-source-runtime.md
T

57 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). A C:\/C:/ grep over tests/ hits six files total, one of which is test_capture_paths.cpp itself; the five OTHER files — every hit opaque string data fed to a pure function (test_bank_model.cpp:186187,243245, test_bake_wire.cpp:77, test_drag_out.cpp:291293,388, test_origin_ledger.cpp:56, test_sample_usage.cpp:502) — are not platform behaviour. They pass identically on Linux.

1.3 app/ — portable.

src/app/main.cpp is one TU of pointers + entry + dispatch. REAPER_PLUGIN_DLL_EXPORT and REAPER_PLUGIN_HINSTANCE are already GCC/Clang-correct in the SDK (vendor/reaper-sdk/sdk/reaper_plugin.h:5253__attribute__((visibility("default"))) and void *), and REAPER_PLUGIN_ENTRYPOINT is platform-neutral (:56). No Win32 call, no Win32 type, no conditional. The one hazard is behavioural, not structural — see L2-03.

1.4 shell/ — the whole cost sits here, and it is unevenly distributed.

65 platform-token lines across 30 of the 293 source files (grep: _WIN32|WIN32|__APPLE__|_MSC_VER|__linux__|__GNUC__|_WIN64 over src/**/*.{cpp,h}). Their distribution is the story:

Area Lines Shape
shell/instrument/ (VST3 editor + embed) 51 of 65 Whole-TU #ifdef _WIN32#endif — Windows-only by design
shell/panel/ 8 Include-selection + two small #else bodies
shell/persist/, shell/capture/, shell/actions/ 4 Trash fallback, localtime_r, the SWELL drag-out branch
core/ 2 The case-fold

Two shell/ directories are absent from this table because they were swept and came back completely clean, not because they went unchecked: shell/view/ and shell/bank_ops/ both return zero hits on the same grep (confirmed). Naming them here, since the table above is by hit-count and gives a reader no way to tell a zero-hit sweep from an unswept directory.

src/resource.h and src/ext_keys.h mention platform names only in comments — zero hits on this specific grep (re-run and confirmed), so they are not a row of this distribution; see resource.h/resource.rc under §7 for their actual (build-system) relevance.

The extension's guards are all complete: every #ifdef _WIN32 that gates a definition has an #else (draw_kit.cpp:12, capture.cpp:218, panel_audition.cpp:36,58, panel_state.h:20, prune_fs.cpp:169, drag_out_win.cpp:8); the ones without an #else gate only an #include or a single statement (panel_window.cpp:19,148, prune_fs.cpp:35, draw_kit.h:27, capture_paths.cpp:18). There is no undefined-symbol gap from this guard structure — every #ifdef that needs an #else has one. This is narrower than "no undefined-symbol gap in the extension" full stop: L2-02's GetCurrentProcessId (instrument_drop_win.cpp:59) is an undefined symbol on Linux precisely because it carries no #ifdef at all, so it falls outside what this guard-completeness check measures.

The instrument's guards are complete in the opposite direction: reasampler_editor.h:156269 wraps the entire paint/input/timer/wndProc/invalidate family in #ifdef _WIN32, and editor_platform.cpp:298306 stubs only the three IPlugView overrides that must exist. Every invalidate() call site is inside a guard (verified exhaustively over src/shell/instrument/: 53 call sites across eight files — editor_input.cpp (5), editor_input_browse.cpp (11), editor_input_chrome.cpp (10), editor_input_curve.cpp (4), editor_input_deck.cpp (9), editor_input_waveform.cpp (4), editor_platform.cpp (3, excluding the definition itself at :65), and editor_session.cpp (7, at 119, 126, 139, 141, 150, 156, 174, excluding a same-named mention inside a comment at :106) — each file whole-file- or whole-region-guarded by #ifdef _WIN32 (e.g. editor_input_browse.cpp:8:177); reasampler_editor.h:266 is the declaration, not a call site. editor_controls.cpp, editor_models.cpp, instrument_bake.cpp have zero call sites, confirmed). So the editor links off Windows — it just does nothing.

1.5 What the platform layer actually provides (vendor-verified)

The extension's non-Windows path is SWELL-provided-by-host (SWELL_PROVIDED_BY_APP → function pointers resolved through SWELL_dllMain, vendor/WDL/WDL/swell/swell-modstub-generic.cpp:135145). [verify — Linux] What is stated here about REAPER's own libSwell.so is only evidence about WDL's own Makefile, not about what Cockos actually shipsvendor/WDL/WDL/swell/Makefile:100107,141151 shows swell-gdi-lice.o/-DSWELL_LICE_GDI built whenever GDK is enabled (ifndef NOGDK, :111), with -DSWELL_FREETYPE gated behind ifndef NOFREETYPE (:144) and -lfontconfig gated a level deeper behind ifndef NOFONTCONFIG (:149) — i.e. the Makefile makes the LICE-backed, freetype/fontconfig SWELL the default build, not a certainty about the binary REAPER distributes. Every downstream claim built on "REAPER's Linux SWELL is the fontconfig-backed GDK build" inherits this same unstated assumption — that includes L2-09's entire mechanism, the GDK-backend evidence cited for L2-07, and the cursor (GDK_*) / modifier-key (GetAsyncKeyState) claims below, none of which are re-flagged individually; this paragraph is their one shared source of doubt. Contrast with the null stub in swell-gdi-generic.cpp (#ifndef SWELL_PROVIDED_BY_APP, line 24), which is not in play for a REAPER-hosted extension either way.

Everything the panel layer calls was checked by name against vendor/WDL/WDL/swell/swell-functions.h and swell-types.h. Present and real:

  • Window/dialog: SWELL_CreateDialog behind the CreateDialogParam macro (swell-functions.h:616,619), DestroyWindow, SetFocus, GetFocus, GetParent, GetCapture/SetCapture/ReleaseCapture, IsWindowVisible, InvalidateRect, ScreenToClient/ClientToScreen, BeginPaint/EndPaint, SetTimer/KillTimer, GetWindowLong/SetWindowLong (:319320), SetWindowPos (:286).
  • Messages/macros: GET_X_LPARAM, GET_Y_LPARAM, HIWORD, MAKEINTRESOURCE (swell-types.h:1220), PAINTSTRUCT, MSG (:262), SRCCOPY, WM_PAINT, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_CAPTURECHANGED, WM_DROPFILES, WM_KEYDOWN, WM_TIMER, WM_ERASEBKGND, TPM_RETURNCMD, MF_*, MB_*, ID*, every VK_* used.
  • WM_MOUSEWHEEL really does carry screen coords in lParam on the GDK backend — the comment at panel_window.cpp:100102 is correct (swell-generic-gdk.cpp:1442).
  • WM_CAPTURECHANGED really is delivered on capture loss (swell-generic-gdk.cpp:1795, swell-wnd-generic.cpp:7141,7150), so the panel's rollback handler (panel_window.cpp:9098) has a live trigger.
  • Cursors: 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 GDK implementation on the GDK backend: swell-generic-gdk.cpp:35633592 sets up a hidden drop-source window and spins a nested SWELL_RunMessageLoop until capture drops, with a 500 ms no-motion timeout; the actual gdk_drag_begin call is inside the shared dropSourceWndProc helper it invokes (:3446), not textually inside the 35633592 range itself. The headless backend defines the same-named function as an empty no-op stubswell-generic-headless.cpp:246248 is void SWELL_InitiateDragDropOfFileList(...) { } — so the "not a stub" framing only holds for the GDK backend specifically. The two backends are mutually exclusive (swell-generic-headless.cpp:28 is #ifndef SWELL_TARGET_GDK), so nothing breaks in a GDK build, but a reader should not take "does exist on Linux" to mean "is implemented in every Linux SWELL backend." macOS (swell-dlg.mm:3534) also defines it; it is declared at swell-functions.h:1011.
  • LICE itself is portable: lice.h:31 includes swell-types.h off Windows, lice.cpp:22 includes swell.h, LICE_SysBitmap::__resize has a SWELL framebuffer path (lice.cpp:179182), and LICE_CachedFont::DrawTextImpl carries real non-Windows branches (lice_textnew.cpp:236249,829,10271028,1040).

Absent from SWELL, verified by grep over vendor/WDL/WDL/swell/: FF_DONTCARE (zero hits anywhere in vendor/WDL/), GetCurrentProcessId (only GetCurrentThreadId, swell-functions.h:822), GetKeyState, WM_MOUSELEAVE/TrackMouseEvent, DragAcceptFiles, RegisterClass*, CreateWindowEx*, DefWindowProc*, MoveWindow, GetWindowLongPtr/SetWindowLongPtr, WHEEL_DELTA, and any move-to-trash surface.


2. Findings

L2-01 — FF_DONTCARE does not exist off Windows; draw_kit.cpp will not compile

Location: src/shell/panel/draw_kit.cpp:73 (DEFAULT_PITCH | FF_DONTCARE), reached on non-Windows through draw_kit.cpp:1216 (which includes swell/swell.h, not windows.h).

Mechanism. swell-types.h defines DEFAULT_PITCH, DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, FW_BOLD, FW_NORMAL, FW_SEMIBOLD, TRANSPARENT and every DT_* the kit uses — but not FF_DONTCARE. A grep for FF_DONTCARE over the whole of vendor/WDL/ returns nothing; on Windows it comes from <wingdi.h> via windows.h. draw_kit.cpp is not platform-guarded (only its include is), so the CreateFont call is compiled on every platform.

Severity: 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. Scope note: the all-or-nothing mechanism itself is identical on Windows too (reaper_plugin_functions.h:4546 — "an older version of REAPER may not succeed in loading", no platform fork in the loader) — nothing about the mechanism is Linux-specific, only whether this particular build happens to be missing an entry is unknown. Graded here as a Linux Blocker on failure-mode quality alone (per the Mechanism above), the same axis L2-04 is graded on below, despite L2-04's mechanism being certain rather than speculative — see L2-04's calculus note. Arguably this belongs as a cross-platform robustness note rather than a Linux-exclusive finding; left here because the fix is trivial and worth doing regardless of platform. Effort: S — confirmed against the two Direction options below: the ShowConsoleMsg diagnostic is one new line on the existing failure branch (main.cpp:293), and the REAPERAPI_MINIMAL swap follows a pattern already used elsewhere in this codebase (panel_window.cpp:2631, panel_audition.cpp:1216) rather than inventing a new one. Direction. Either switch main.cpp to REAPERAPI_MINIMAL + an explicit WANT list (the other TUs already do this — e.g. panel_window.cpp:2631, panel_audition.cpp:1216), or keep the full load but print the failure count via rec->GetFunc("ShowConsoleMsg") before returning 0. The minimal list is also the honest inventory of what this extension actually needs.

L2-04 — every persisted floating-point number is LC_NUMERIC-dependent, in both directions

Location (writers): src/core/json/json.cpp:3943 (%.17g — the bank index, view model and tracking ledger all serialize through it), src/core/model/provenance.cpp:40 (%.17g — the provenance blob inside the bank JSON). Two writers, not three: tail_control.cpp:74 is a comment describing the format ("Byte-identical to the former snprintf writer: {"mode":%d,"manualMs":%.17g}"), not a write site — the actual per-project tail-setting write is tail_control.cpp:7879, which already goes through json::numToStr (the same json.cpp:3943 codec cited above), so it is the same writer, not a third one. Location (readers): src/core/json/json.cpp:190196 (std::strtod), src/core/wire/wire.cpp:121130 (Cursor::fieldDouble, std::strtod), src/core/capture/render_settings.cpp:178186 (std::stod over REAPER's own P_RAZOREDITS).

Mechanism. snprintf("%.17g"), strtod and stod all honour LC_NUMERIC. Under a comma-decimal locale the writers emit 1,5, which makes the bank JSON structurally invalid (an extra separator inside an object) — the whole index fails to parse on the next load. The readers are honestly fail-closed (all three require whole-token consumption: json.cpp:194, wire.cpp:127, render_settings.cpp:184), so they degrade to "malformed" rather than silently truncating — but that means a razor range or a bank field simply disappears. On Windows the CRT's start-up locale is "C" and nothing in this codebase calls setlocale, which is why this has never fired. On Linux the process locale is far more likely to be set by something else in the address space: SWELL's GDK backend calls gtk_init_check when built with SWELL_SUPPORT_GTK (swell-generic-gdk.cpp:366; the #else branch uses gdk_init_check, :368) and never calls gtk_disable_setlocale (grep over vendor/WDL/WDL/swell/ returns no hit), and any GTK/Qt-based plugin loaded into the same process can do the same.

Severity: Major — builds and runs; under a non-C LC_NUMERIC the bank index is written unparseable and the project's whole bank is lost on reload. Calculus note: unlike L2-03 (graded Blocker above on zero direct evidence, purely on failure-mode quality), this finding's mechanism is confirmed by reading the actual writer/reader call sites, and its failure mode — the entire bank index becomes unparseable — is at least as severe as L2-03's. It is graded only Major here because it additionally requires a non-C LC_NUMERIC in the hosting process, a likelihood factor L2-03 does not apply to its own claim. Reading both on the same axis (either both by failure-mode quality, or both by likelihood) would put them closer together than Blocker/Major suggests; resolving that is a scope call for whoever prioritizes the two, not a fact this audit can settle statically. Effort: M — two writers and three readers, all in core/, all unit-testable; the fix is a locale-independent path (std::to_chars/std::from_chars, C++17, or an explicit std::locale::classic()-bound stream), not a setlocale call in a plugin. Direction. Make the number codec locale-independent at its two writers and three readers; add a pure test that pins the emitted text for a fractional value. Do not "fix" this by calling setlocale — an extension must not mutate the host's locale. [verify — Linux] — read LC_NUMERIC inside a running REAPER-Linux process (e.g. ShowConsoleMsg(setlocale(LC_NUMERIC, nullptr))) before sizing the work.

L2-05 — prune loses the Recycle Bin and the "file is locked" backstop at the same time

Location: src/shell/persist/prune_fs.cpp:167209; the non-Windows branch is :200208.

Mechanism. Two Windows properties are load-bearing for the deletion authority and neither survives. (a) The Windows path routes through SHFileOperationW + FOF_ALLOWUNDO (:186188) — deletions are recoverable from the Recycle Bin; the #else is a hard fs::remove (:203). The header already states this honestly (:3034, :162166). (b) Less obviously, the failure taxonomy at :205 (if (ec) return false; // real failure (locked/permission) -> skip) encodes Windows file-sharing semantics: a bank file currently open by REAPER's audio engine (an active PCM_source, an item playing from it) cannot be deleted on Windows, so it is counted as "skipped". On Linux unlink() on an open file succeeds — the directory entry vanishes while the open fd keeps playing, and when the fd closes the bytes are gone with no trash to recover from. So the Linux prune can delete a file that is audibly in use, silently, and the user's only recovery floor (the superseded-file-survives-until-prune rule, root CLAUDE.md §resample bake) now has nothing under it.

Severity: Major — irreversible user-data loss where the Windows build is recoverable; the "locked" skip branch is dead code on Linux. Effort: M — trash is genuinely non-portable, but the XDG trash spec (~/.local/share/Trash with a .trashinfo sidecar) is a self-contained move-plus-metadata write, and the confirm gate already exists upstream. Direction. Either implement an XDG-trash move in the #else (preferred — the deletion authority is a single ~40-line function and this is exactly where the platform seam belongs), or make the prune confirmation text platform-aware so a Linux user is told the deletion is permanent. Do not leave the current silent asymmetry.

L2-06 — the docked panel never opens on Linux, and fails silently when it doesn't

Location: src/shell/panel/panel_window.cpp:135137.

Mechanism. CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), …) maps on SWELL to SWELL_CreateDialog(SWELL_curmodule_dialogresource_head, (resid), …) (swell-functions.h:616,619) — it resolves the template out of a per-module registry populated by the resgen-generated source, not out of a linked .rc. That generated source is not currently part of the Linux target (the target_sources line is commented out; build-system detail → §7). The source-side consequence is what matters here: SWELL_CreateDialog returns nullptr, panel_window.cpp:137 does if (!g_panel.hwnd) return;, and the toggle action is a silent no-op — no console line, no message box, and bankPanelIsOpen() keeps reporting false so the Actions-list checkmark never lights. The user's only symptom is "the button does nothing." Note also that MAKEINTRESOURCE becomes ((const char*)(UINT_PTR)(x)) on SWELL (swell-types.h:1220), so the id is a pointer-shaped integer — the resgen output must agree on IDD_BANK_PANEL == 1000 (src/resource.h:8).

Severity: Major — the extension loads, every action works, and the primary UI surface is absent with no diagnostic. Effort: S on the source side (one failure branch), separate from T1's resgen wiring. Direction. Add a one-line ShowConsoleMsg on the !g_panel.hwnd path naming the missing dialog resource. That single line converts a mystery into a two-minute diagnosis and is worth having on Windows too.

L2-07 — file-drop ingest onto the panel has no opt-in on SWELL

Location: src/shell/panel/panel_window.cpp:145150 (the DragAcceptFiles call is #ifdef _WIN32), handler at :4761,6567.

Mechanism. The comment at :145147 is accurate — SWELL exposes no DragAcceptFiles (grep over vendor/WDL/WDL/swell/ finds only DragQueryFile and DragFinish, swell-functions.h:10061007). What the comment does not say is how a drop would arrive instead. Reading the GDK backend: on a URI-list selection-notify, SWELL resolves the top-level window, walks down with ChildWindowFromPoint (which is descending — for(;;) loop, swell-wnd-generic.cpp:69566981), and SendMessage(cw, WM_DROPFILES, (WPARAM)gobj, 0) (swell-generic-gdk.cpp:1622). The default child proc forwards an unhandled WM_DROPFILES up to the parent only when the window lacks WS_EX_ACCEPTFILES (swell-wnd-generic.cpp:7681) — but that bit's only effect is to suppress the up-forward; it does not enable acceptance, and the message already reaches the window under the pointer via the ChildWindowFromPoint descent regardless of the bit's state. So the panel's own dlgProc plausibly receives the drop without any opt-in at all, but the earlier "ex-style bit, not an API call" framing was backwards about what the bit does. The real SWELL opt-in surface for WS_EX_ACCEPTFILES is the dialog resource, not a runtime call: swell_resgen.pl:10 and swell_resgen.php:204 translate a dialog's WS_EX_ACCEPTFILES style to SWELL_DLG_WS_DROPTARGET, and swell-dlg-generic.cpp:320321 sets h->m_exstyle |= WS_EX_ACCEPTFILES from that flag at dialog-creation time. src/resource.rc:19 declares STYLE WS_CHILD only — no WS_EX_ACCEPTFILES — so the bit is off on Linux regardless of anything panel_window.cpp does at runtime. That makes L2-07's real seam src/resource.rc + resgen, the SAME work as L2-06 and §7 bullet 1, not an independent one-liner: a runtime SetWindowLong(..., GWL_EXSTYLE, ... | WS_EX_ACCEPTFILES) would only suppress the unhandled-drop forward-to-parent — it would not change whether the drop reaches dlgProc in the first place, which already happens via the ChildWindowFromPoint descent independent of the bit. Note also the doc's own conclusion is better-supported than it knew: src/resource.rc:2122 is BEGIN/END — zero child controls — so the GDK ChildWindowFromPoint descent has nowhere to land but the panel HWND itself. Two supporting details still check out: SWELL's DragQueryFile reads the same DROPFILES layout the handler assumes (swell-wnd-generic.cpp:7730+), and SWELL's DragFinish is a documented no-op ("caller will free hdrops", :77257728) while SWELL itself 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, and it cannot be exercised until the panel itself renders (T1's resgen wiring — see L2-06). Effort: no independent source-side fix exists — this is the same resgen work already costed under L2-06 / §7 bullet 1. Do not add an Effort: S line here; adding the SetWindowLong call would compile, do nothing observable, and send an implementer looking for a bug that isn't where they'd look. Direction. No separate fix. Once the resgen work lands the panel dialog with WS_EX_ACCEPTFILES set from the resource, verify: [verify — Linux] drop a WAV onto the docked panel and confirm WM_DROPFILES reaches dlgProc.

L2-08 — the VST3 instrument has no Linux editor, and no automatable parameters to fall back to

Location: src/shell/instrument/editor_platform.cpp:3742 (isPlatformTypeSupported returns kResultTrue only for kPlatformTypeHWND, and only inside #ifdef _WIN32); :298306 (the non-Windows stubs); reasampler_editor.h:156269 (the entire paint/input family is #ifdef _WIN32); reasampler_embed.cpp:135141 (REAPER_FXEMBED_WM_IS_SUPPORTED returns 0 off Windows). reasampler_processor.cpp:360365 still hands the host a ReaSamplerEditor.

Mechanism. The VST3 SDK does support Linux — kPlatformTypeX11EmbedWindowID (vendor/vst3sdk/pluginterfaces/gui/iplugview.h:79), Linux::IRunLoop with registerEventHandler/registerTimer (:267279), Linux::IEventHandler (:223), Linux::ITimerHandler (:239), and a linuxmain.cpp module entry (vendor/vst3sdk/public.sdk/source/main/linuxmain.cpp). The instrument simply does not implement any of it. The consequence compounds: with isPlatformTypeSupported false for every type, the host falls back to a generic parameter UI — and a grep for addParameter|parameters\.add|getParameterCount across src/shell/instrument/*.cpp returns nothing. Zero VST3 parameters are registered as of this branch's base. A Linux user would get an instrument with no editor and no controls whatsoever. (Phase Γ-W4-T1 vst3-parameter-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 when built with -DSWELL_FONTCONFIG (gated per the [verify — Linux] note in §1.5): FcPatternAddString(pat, FC_FAMILY, lfFaceName)FcConfigSubstitute/FcDefaultSubstituteFcFontMatch (swell-gdi-lice.cpp:450487). Whether FcFontMatch itself can return failure is a claim about fontconfig's own internals — fontconfig is not vendored here, so it is dropped rather than asserted uncited (this doc's own evidence-discipline rule). What the vendored code does show: even a successful match doesn't guarantee a loadable face — swell-gdi-lice.cpp:480484 only sets face when FcPatternGetString(hit, FC_FILE, …) resolves to a nonempty string AND the following FT_New_Face succeeds; either step can fail, leaving face NULL, and in this build path there is no further fallback — the MatchFont/LiberationSans/DejaVuSans list at :493539 compiles only in the #else (#ifndef SWELL_FONTCONFIG) branch. However swell-gdi-lice.cpp:400401,561,564 show CreateFont always allocates and returns a non-null HGDIOBJ__* regardless of whether face resolved — the failure is recorded internally (font->typedata = NULL), not as a null return — so draw_kit.cpp:74's own if (!hf) return guard does not catch this failure mode; hf comes back non-null either way. Whatever degraded outcome exists here comes from LICE_CachedFont's own handling of a null-typedata font (§1.5 already cites real non-Windows branches in lice_textnew.cpp:236249,829,10271028,1040), not from loadFont's stated guard — [verify — Linux]. The two WCAG static_asserts (draw_kit.cpp:5354) are on pixel height and weight, not on the face, so they still hold regardless.

Severity: Minor [verify — Linux] — cosmetic at best; whether it degrades further than wrong metrics (e.g. to no text at all) rests on the unconfirmed downstream null-face handling above, not on anything this audit can read statically. Effort: S — confirmed against the actual call sites: five total (draw_kit.cpp:154158, four using "Segoe UI", one "Consolas"), each a single string-literal argument. A platform fallback is a #ifdef-guarded literal swap at those five sites, not a new mechanism — draw_kit.cpp:69's comment ("the face is chosen here so a change is one line") describes one call's literal, not all five; the S estimate rests on there being only five call sites, not on that comment's wording. Direction. Add a platform fallback face list at the five loadFont call sites (draw_kit.cpp:154158). DejaVu Sans / DejaVu Sans Mono are the safe Linux defaults; SWELL's own no-fontconfig fallback list names LiberationSans/DejaVuSans and LiberationMono/DejaVuSansMono (swell-gdi-lice.cpp:505507), a reasonable precedent to copy. This same change fixes macOS (San Francisco / Menlo), one code path.

L2-10 — OS drag-out on SWELL loses the copy-only mask, the readiness probe, and the outcome

Location: src/shell/actions/drag_out_win.cpp:249282 (the #else branch); drag_out_win.h:711,2933 already states the first and third honestly.

Mechanism. Three separate degradations, all real but none fatal. (a) DoDragDrop's DROPEFFECT_COPY-only mask (:230) is a structural guarantee that no target can MOVE a bank file out of the folder; SWELL_InitiateDragDropOfFileList takes no effect mask (swell-functions.h:1011), so the guarantee reduces to whatever the GDK drag advertises. (b) canInitiateDragOut degrades to !paths.empty() (:276278), so the caller's careful coupling — "do not tear down the internal drag until the OS is known ready" (panel_drag.cpp:227244) — has nothing to check against. (c) initiateDragOut returns true unconditionally (:270), so the advisory success return is meaningless. The caller ignores it, so (c) is inert today. Note the call path is already Linux-correct in one important respect: handOffToOs releases capture and resets drag state (panel_drag.cpp:240241) before invoking the drag, which matters because SWELL's implementation takes capture on its own hidden window and spins a nested SWELL_RunMessageLoop until capture drops (swell-generic-gdk.cpp:35753584, inside SWELL_InitiateDragDropOfFileList — the function drag_out_win.cpp:270 actually calls; the body is identical to the sibling SWELL_InitiateDragDrop at :35313560, which is where an earlier draft of this citation pointed) — with a 500 ms no-motion timeout that has no Windows analog.

Severity: Minor — the feature works; one safety property becomes conventional rather than structural. Effort: S — mostly documentation; there is no SWELL surface to restore the mask with. Direction. Leave the implementation; make sure the copy-only invariant's home (drag_out_win.h:711) is the doc a Linux reviewer is pointed at, and treat "MOVE is structurally impossible" as a Windows-only claim in any future spec text.

L2-11 — normalizeSlashes case-folds on _WIN32 only, which is right for Linux and wrong for macOS

Location: src/core/capture/capture_paths.cpp:1820.

Mechanism. The one platform conditional in core/. Linux filesystems are case-sensitive, so not folding is correct and the existing tests already assert it (tests/test_capture_paths.cpp:5051). The shared-path note: macOS's default APFS/HFS+ is case-insensitive, so the same #ifdef _WIN32 under-folds there — a pre-existing macOS defect this audit surfaces but does not own.

Severity: Minor (no Linux defect). Effort: S — confirmed: the one platform conditional in core/ is a single #ifdef _WIN32 block (capture_paths.cpp:1820) with no other call site depending on the token; a predicate swap is a one-block change. Direction. No Linux action. If macOS is ever targeted, the predicate wants to be "case-insensitive filesystem", not "Windows".

L2-12 — fs::path::string() narrowing is strictly better on Linux (informational)

Location: prune_fs.cpp:118, ingest.cpp:222,254, capture_paths.cpp:95, panel_bank_ops.cpp:39, scope_resolve.cpp:207, insert.cpp:71, capture_realtime_shell.cpp:306, capture.cpp:408, instrument_bake.cpp:166.

Mechanism. On MSVC std::filesystem::path::string() narrows through the active code page; on Linux path is already char-based, so .string() is a byte passthrough and UTF-8 survives. The one site that deliberately works around the Windows behaviour — instrument_drop_win.cpp:123128, using u8string() — is harmless on Linux (C++17, so u8string() returns std::string; CMakeLists.txt:28 pins CMAKE_CXX_STANDARD 17, and under C++20 the .c_str() at :128 would become a const char8_t* type error — worth knowing before anyone bumps the standard).

Severity: Minor / informational. Effort: none. Direction: no action; do not "fix" it.


Surfaces checked and found clean

Recorded so the sweep's negative results are as auditable as its findings.

  • Missing standard includes (the MSVC-transitively-provides class). Ran a use-vs-include diff across all 293 files for <cstring> <cstdio> <memory> <atomic> <algorithm> <limits> <cmath> <functional> <ctime> <cstdlib>. Five raw hits; all five verified false positives on read — four were the symbol name appearing in a comment (render_settings.h:53, ext_state_read.h:20, action_registry.h:7) or a correct <cmath> std::abs (velocity_curve.cpp:229, <cmath> at :6), and processor_reload.cpp's std::unique_ptr comes from reasampler_processor.h:12. No missing include found — but this only covers the ten headers above. The two headers most likely to produce the classic MSVC-transitively-provides failure under libstdc++, <vector> and <string>, were excluded from that sweep; re-run including them: 45 files use std::vector without #include <vector>, and 52 use std::string without #include <string> (raw grep-diff counts, unread). <map> and <set> add 2 and 3 more respectively. These 102 raw hits were not individually verified the way the five above were (that would mean reading 102 files); the claim this sweep actually supports is "up to 102 files may rely on transitive inclusion for <vector>/<string>/<map>/<set>, unconfirmed one by one" — not "no missing include found." The two header cases that matter most for the extension's own build (shell/capture/capture_orchestrator.h, src/ext_keys.h) are confirmed satisfied transitively.
  • Templates / two-phase lookup. Exactly 7 templates in the tree (core/wire/bytes.h:25,61, core/wire/ext_state_read.h:37, core/instrument/engine/play_params.h:191,203,208,218). None derives from a dependent base, none calls an unqualified dependent name, none needs typename/template disambiguation. GCC/Clang-safe as written.
  • Compiler extensions. Zero #pragma other than #pragma once; zero __declspec, __attribute__, __forceinline, anonymous struct/union, or MSVC-permissive construct in src/**.
  • HWND__ forward declaration. drag_out_win.h:19 declares struct HWND__;; SWELL declares typedef struct HWND__ *HWND; (swell-types.h:211) — same tag, no -Wmismatched-tags.
  • preview_register_t platform fork. panel_audition.cpp:3641,5863 and panel_state.h:2024 match the SDK's own fork exactly (CRITICAL_SECTION cs on _WIN32, pthread_mutex_t mutex otherwise — vendor/reaper-sdk/sdk/reaper_plugin.h:13081312). <pthread.h> is included on the non-Windows branch. Correct.
  • Prompts and message boxes. Every user prompt goes through REAPER's own GetUserInputs/ShowMessageBox/ShowConsoleMsg (panel_bank_ops.cpp:2425,81,100,126,381), never a Win32 MessageBox. Cross-platform by construction.
  • Menus. CreatePopupMenu/InsertMenu/TrackPopupMenu(TPM_RETURNCMD)/DestroyMenu are all SWELL-provided (swell-functions.h:528532, and TrackPopupMenu/DestroyMenu/ CreatePopupMenu entries); the "pos < 0 appends" assumption at panel_bank_ops.cpp:226 matches SWELL's SWELL_InsertMenu. The comment at :220222 is accurate.
  • Keyboard/accelerator path. accelerator_register_t + MSG/WM_KEYDOWN + GetFocus + GetParent (panel_input.cpp:503521) are all SWELL-provided; every VK_* used is in swell-types.h.
  • Double-buffered paint. LICE_SysBitmap + getDC() + BitBlt(SRCCOPY) (panel_render.cpp:474,549) — LICE_SysBitmap has real non-Windows paths (lice.cpp:179, lice.h:362), BitBlt/SRCCOPY are SWELL-provided.
  • Byte order. Handled explicitly everywhere it matters (see §1.1). No native-integer memcpy onto a wire buffer anywhere in core/wire.
  • Hot-path guardrails. Nothing in this audit's remediation directions touches peaks::computeEnvelope (still a free function), the audition call-through (panel_audition.cpp — direct calls, no interface), or the realtime tick's single-pointer-test idle path (main.cpp:156). L2-04's number-codec change is on the JSON/persist path, which root CLAUDE.md explicitly declares off all hot paths. No recommendation here adds a hot-path indirection.

3. Extension vs. instrument — the cost split

The two artifacts are cleanly separable, and the evidence says the split is very lopsided.

3.1 Extension only (reaper_reasampler)

What must change in source: two compile Blockers (L2-01, L2-02 — both one-line), one load hazard (L2-03), one silent-failure diagnostic (L2-06), one correctness/safety fix (L2-05), one data-integrity fix (L2-04), and cosmetics (L2-09). Nothing here is architectural. The panel's whole SWELL/LICE surface — dialog lifecycle, docking, WM_PAINT double-buffered LICE draw, mouse/wheel/capture, cursors, menus, keyboard accelerator, modifier keys, tooltips, drag-out, drop-in — was checked call-by-call against the vendor headers and is already SWELL-portable; see §1.5 and "Surfaces checked". This is the single most load-bearing finding of the audit: the panel was written against SWELL's vocabulary throughout, and the Windows-only escapes are three small ones (DragAcceptFiles, SHFileOperationW, OLE DoDragDrop) that each already carry a non-Windows branch or a documented reason they do not.

Estimate shape: S+S+S+S+M+M+S. No L item.

3.2 Instrument additionally (reasampler_vst)

What must be written from nothing: an X11-embed IPlugView (window creation and parenting without RegisterClass/CreateWindowEx), an IRunLoop/ITimerHandler-driven replacement for the SetTimer/WM_TIMER sync poll, an event-driven input path replacing the entire wndProc switch (~150 lines of editor_platform.cpp:139296), hover-leave detection without TrackMouseEvent, modifier reads via GetAsyncKeyState instead of GetKeyState, and drop-accept without DragAcceptFiles. The paint side is the cheapest part — every painter already draws into a LICE_IBitmap through the shared kit (editor_paint.cpp:28,51), so the drawing survives a window-system change intact; it is the window and event plumbing that is entirely absent. The TCP/MCP embed strip (reasampler_embed.cpp:135141) is a smaller, separate 0→1 (REAPER_FXEMBED_WM_IS_SUPPORTED currently returns 0 off Windows; the paint body at :174+ is LICE and would port).

Estimate shape: one L, and it is a genuinely new competence rather than a port.

3.3 The consequence for sequencing

The extension is a Linux target that is largely already met and needs a handful of small, well-localized fixes. The instrument is a separate project. The two share exactly one file — shell/panel/draw_kit — and that file's only Linux blocker is L2-01. Daniel rules on whether the instrument is in scope at all; the evidence says the extension does not wait on it.


4. Feature-degradation list — what builds but behaves differently, or not at all

Blunt column: GONE = the capability does not exist on Linux; DIFFERENT = it exists with changed behaviour; AT RISK = depends on an unverified assumption.

Feature Verdict What actually happens Cite
Prune → Recycle Bin GONE Hard unlink, unrecoverable prune_fs.cpp:200208
Prune's "file is locked" skip GONE unlink on an in-use file succeeds; the skip branch is dead code prune_fs.cpp:205
Docked bank panel GONE (today) SWELL_CreateDialog returns null; toggle is a silent no-op until the resgen source is added (§7) panel_window.cpp:135137
VST3 editor GONE isPlatformTypeSupported false for every type; host shows a generic UI editor_platform.cpp:3742
VST3 generic-UI fallback GONE (today) Zero parameters registered as of this base; Phase Γ-W4-T1 changes this grep addParameter over shell/instrument/*.cpp → no hits
TCP/MCP embed strip GONE REAPER_FXEMBED_WM_IS_SUPPORTED returns 0 reasampler_embed.cpp:135141
Kit fonts (Segoe UI / Consolas) DIFFERENT fontconfig substitutes silently; metrics and ellipsis points shift; ValueMono may lose tabular alignment swell-gdi-lice.cpp:450487
OS drag-out copy-only guarantee DIFFERENT No effect mask; copy-only is conventional not structural drag_out_win.cpp:257270
OS drag-out readiness/outcome DIFFERENT Probe degrades to "non-empty"; return is always true; 500 ms no-motion timeout has no Windows analog drag_out_win.cpp:276278, swell-generic-gdk.cpp:35793580
Panel file-drop ingest AT RISK Depends on SWELL delivering WM_DROPFILES without the Win32 opt-in panel_window.cpp:145150
Bank index float round-trip AT RISK Unparseable under a non-C LC_NUMERIC json.cpp:3943,190196
Extension load AT RISK Any one unresolved API name in ~869 = silent refusal main.cpp:292293
Path case sensitivity DIFFERENT (correct) No case-fold; correct for Linux, and the tests already assert it capture_paths.cpp:1820
Prune reclaim on a symlinked bank file AT RISK fs::directory_iterator + is_regular_file() follows symlinks (C++17); size is read from the target via file_size() but fs::remove deletes the link, not the target — prune reports N bytes reclaimed and reclaims zero. Symlinked media folders are far more idiomatic on Linux than Windows. prune_fs.cpp:113124
Audition / preview INTACT preview_register_t fork matches the SDK; PlayPreview/StopPreview are REAPER API panel_audition.cpp:3663
Docking, menus, cursors, keyboard, wheel, capture rollback, tooltips INTACT All SWELL-provided; verified call-by-call §1.5
Capture pillar (offline + realtime), persist, tracking, prune computation INTACT Pure core/ + REAPER API only §1.1

5. Open questions

[verify — Linux] — answerable only on a Linux box

  1. Process locale. Read setlocale(LC_NUMERIC, nullptr) inside a running REAPER-Linux process (print via ShowConsoleMsg) on a machine whose user locale uses a decimal comma. Decides whether L2-04 is urgent or latent.
  2. REAPER API completeness. Instrument main.cpp:292 to print REAPERAPI_LoadAPI's return value instead of discarding it, load once, and record the count. Non-zero decides whether L2-03 is a real Blocker and names the gap.
  3. Panel file-drop routing. With the panel docked, drag a WAV from the file manager onto it and observe whether dlgProc sees WM_DROPFILES (L2-07). If not, add WS_EX_ACCEPTFILES.
  4. Which SWELL GDI/locale build REAPER ships. Whether REAPER's libSwell.so is built with SWELL_SUPPORT_GTK (→ gtk_init_check, swell-generic-gdk.cpp:366) or without (→ gdk_init_check, :368) changes the likelihood in (1). Observable indirectly via (1).
  5. fontconfig substitution for "Consolas". Whether it lands on a monospaced face on a stock distro decides whether L2-09 is cosmetic or a real readability regression on numeric readouts.
  6. SWELL_InitiateDragDropOfFileList acceptance semantics. Whether a GDK-initiated file drag is accepted as a copy by common targets (a file manager, another DAW), and whether the 500 ms no-motion timeout (swell-generic-gdk.cpp:35793580) cancels a slow user gesture.
  7. Prune against an in-use file. Play an item from a bank file, prune it, confirm the audio survives until the fd closes and the file is then unrecoverable — the concrete demonstration behind L2-05(b).

[Daniel] — scope decisions, not knowledge gaps

  1. Is the VST3 instrument in scope for Linux at all? §3 supplies the cost split; the extension does not depend on the answer. This is the phase's one real fork.
  2. Is a hard unlink an acceptable prune on Linux, with a platform-aware confirmation string — or must XDG trash be implemented before Linux ships? (L2-05.)
  3. Is silent font substitution acceptable, or does the kit get an explicit Linux face list? (L2-09.)
  4. Does "copy-only is structural" survive as a shipped invariant when one platform can only offer it conventionally? (L2-10.)

6. Ordering sketch

Dependency order only — this is not a plan, and it assigns no waves.

  1. Make it compile. L2-01 (FF_DONTCARE), L2-02 (GetCurrentProcessId). Nothing else can be observed until these land; both are one-line and independent of each other.
  2. Make it load, visibly. L2-03 (REAPERAPI_LoadAPI diagnostic or REAPERAPI_MINIMAL) — must precede any runtime verification, because it is the failure mode that produces no evidence. Pairs naturally with [verify — Linux] (2).
  3. Make the panel appear. L2-06 (the missing-dialog diagnostic) alongside T1's resgen wiring. The diagnostic should land first so the resgen step can be confirmed rather than assumed.
  4. Then, and only then, verify. [verify — Linux] items 1, 3, 5, 6, 7 all need a running panel. Item 2 is already answered by step 2. The critical path for this step runs through the parallel build-system track, not this one: "a running panel" means T1's resgen wiring (§7 bullet 1) has landed — this doc doesn't own that work and doesn't know its schedule, so step 3's "alongside T1's resgen wiring" is doing real scheduling work, not just sequencing flavor. Anyone driving this ordering sketch needs to coordinate with T1 before step 4 can start, not just after step 3 finishes on this track's own items.
  5. Data integrity. L2-04 (locale-independent number codec) — pure core/ work, unit-testable on Windows, and it should land before any Linux user saves a project. Sequenced after the verify only so its urgency is known; the work is not blocked.
  6. Deletion safety. L2-05 (XDG trash or a platform-aware confirm) — gated on [Daniel] (2).
  7. Ingest opt-in, if needed. L2-07 — no independent fix; it is the resgen work already sequenced in step 3, strictly gated on [verify — Linux] (3) to confirm it's needed at all.
  8. Cosmetics. L2-09 (fonts), L2-10 (drag-out doc). Independent of everything above.
  9. Separately, if ruled in. L2-08 — the instrument's X11 editor. No dependency in either direction on 18 except L2-01, which the shared draw_kit needs regardless.

7. Referred to T1 (build-system; spotted, not analyzed)

  • SWELL dialog resgen is not wired for Linux. src/app/CMakeLists.txt:97 — the target_sources(... resource.rc_mac_dlg.h) line is commented out in the Linux else() branch (and in the APPLE branch, :86). This is the mechanical cause of L2-06.
  • VST3 target is if(WIN32 …)-gated. src/shell/instrument/CMakeLists.txt:9 — the target is not configured at all off Windows; dllmain.cpp is hard-coded at :78 where Linux needs linuxmain.cpp (vendor/vst3sdk/public.sdk/source/main/linuxmain.cpp exists).
  • SHCreateStdEnumFmtEtc / SHFileOperationW / OleInitialize need shell32/ole32. Only on the Windows branch, but worth confirming the Linux branch links none of them.
  • reaper_plugin.h's relative SWELL include. vendor/reaper-sdk/sdk/reaper_plugin.h:49 does #include "../WDL/swell/swell.h", which does not exist under vendor/reaper-sdk/. It resolves only because ${WDL_INC} is vendor/WDL/WDL and vendor/WDL/WDL/../WDL/swell/swell.h is the same file. Works, but it is a coincidence worth knowing before anyone moves an include path.
  • <filesystem> link requirement. GCC < 9 needs -lstdc++fs; core/ and shell/ both use std::filesystem heavily.
  • LICE TU set. CMakeLists.txt:7982 adds lice.cpp, lice_line.cpp, lice_arc.cpp, lice_textnew.cpp unconditionally. All four have non-Windows paths, but the Linux link needs them compiled against SWELL_PROVIDED_BY_APP consistently with the module.

Summary table

ID Finding Severity Effort Artifact
L2-01 FF_DONTCARE undefined off Windows (draw_kit.cpp:73) Blocker S both
L2-02 GetCurrentProcessId() unguarded, absent from SWELL Blocker S extension
L2-03 REAPERAPI_LoadAPI all-or-nothing over ~869 names, silent Blocker [verify] S extension
L2-04 Persisted floats are LC_NUMERIC-dependent, both directions Major M both
L2-05 Prune loses trash AND the locked-file backstop Major M extension
L2-06 Panel dialog never created; silent no-op Major S extension
L2-07 No drop-accept opt-in on SWELL; real seam is resgen, same as L2-06 Major [verify] — (see L2-06) extension
L2-08 No Linux VST3 editor; no parameters to fall back to Major L instrument
L2-09 Segoe UI / Consolas substitute silently via fontconfig Minor S both
L2-10 SWELL drag-out: no copy mask, no probe, no outcome Minor S extension
L2-11 Case-fold is _WIN32-only (right for Linux, wrong for macOS) Minor S both
L2-12 fs::path::string() narrowing — Linux is strictly better Informational both