Product doc for the extension + ReaSampler 9000 on Linux, downstream of the two Λ-W1 audits: Daniel's six rulings, seven waves, the non-REAPER-host safety contract, and four open forks.
79 KiB
Linux readiness — product notes
Framing, rationale, and design-direction calls behind Phase Λ — ReaSampler on Linux:
the reaper_reasampler extension and the ReaSampler 9000 VST3 instrument, both
shipped. The tickable spec belongs in docs/PLAN.md (§Phase Λ); the architecture
detail belongs in the affected src/**/CLAUDE.md files once tracks land. This doc holds
the why — what "Linux-ready" is defined to mean here, what the tree's actual posture
is, the six decisions Daniel settled on 2026-08-02, the sequencing argument, and the four
forks still open.
This doc is downstream of two audits and does not repeat them.
docs/product/audit-notes/lambda-w1-t1-build-toolchain.md (findings Λ-01…Λ-10, verify
items V1…V10) owns the build system, toolchain, vendored dependencies, resources, test
harness and packaging. docs/product/audit-notes/lambda-w1-t2-source-runtime.md (findings
L2-01…L2-12) owns source portability and runtime behaviour. Both were code-reviewed against
the tree and remediated in place. Everything below cites them by finding number rather
than re-deriving. Grep the audits for a cited number; do not read either whole.
Status: framed by product-designer (2026-08-02) after Daniel's rulings the same day. Four forks are open — Λ-F1 (CI), Λ-F2 (the dialog-resource route), Λ-F3 (the copy-only drag-out invariant), Λ-F4 (the declared support floor) — each stated with the evidence for both sides in §"Open forks". Λ-F2 gates one track and nothing else; the other three gate nothing. Everything not marked as a fork is a product-designer call with its reasoning stated; contradict it in review with an argument, not a preference.
Slug convention. Λ → l, so tracks dispatch into pl-w<wave>-t<track>-<slug> —
Λ-W2-T1 into pl-w2-t1-linux-compile-blockers. The two audits already ran under
pl-w1-t1-build-toolchain-audit and pl-w1-t2-source-runtime-audit. docs/PLAN.md:33–36
lists the transliterations for Θ / Ξ / Γ / Ψ / Ε / Ρ and does not yet carry Λ — adding it
is a plan-doc edit this phase's PLAN.md entry must make.
Why now, and what "Linux-ready" means here
The reason to do this now is not demand — it is that the cost is already sunk and the window for it to stay sunk is closing. Two facts from the audits:
docs/product/versioning-and-release.mdalready commits, in writing, to "three platform artifacts per channel per release" (T1 §1e). Linux is not a new promise; it is an unredeemed one.- The panel layer — the product's primary surface — was written against SWELL's
vocabulary throughout and is portable call-for-call, verified name-by-name against
the vendored headers (T2 §1.5, §"Surfaces checked"). That is an asset the tree acquired
by discipline, not by luck, and it decays every time someone writes a Win32 call without
a
#else. Phase Ψ, Phase Ε and Phase Ρ are all adding extension-side surface right now. Compiling on Linux once converts that discipline from a convention nobody can check into a build that fails when it is broken.
What "Linux-ready" is defined to mean in this phase. A Linux REAPER user installs
reaper_reasampler.so and reasampler_9000.vst3 and gets the same product a Windows user
gets, with four named exceptions:
- Prune deletes permanently. No Recycle Bin, no trash. The confirmation says so. (Λ-D5.)
- The OS drag-out's copy-only guarantee is conventional, not structural. SWELL's file-list drag takes no effect mask, so nothing at the API level forbids a target from treating the drag as a move (L2-10). Λ-F3 decides how the invariant is worded.
- Fonts are not Segoe UI and Consolas. The kit gets an explicit Linux face list (see §"Calls made here"); the faces are different, the layout is not.
- The ReaSampler 9000 editor comes up under REAPER only. In any other Linux host the plugin loads, scans, instantiates, and processes audio — and shows the host's generic parameter UI instead of its own editor. This is a contract, not a shortfall; §"The non-REAPER-host safety criterion" specifies it as an observable.
What a Linux user explicitly does not get: an XDG-trash prune, a macOS build, the TCP/MCP embed strip, or an editor in Bitwig/Ardour. §"What Phase Λ does not do" states each with its reason.
The posture as found
Compressed from the two audits. Each claim below is theirs; the citations are the ones this doc re-read against the tree while writing.
The extension is close — two one-line compile blockers stand between the tree and a GCC/Clang build.
src/shell/panel/draw_kit.cpp:73passesDEFAULT_PITCH | FF_DONTCAREtoCreateFont.FF_DONTCAREhas zero occurrences anywhere invendor/WDL/(L2-01); the file is not platform-guarded, only its include is (:70–77).draw_kitlinks into both modules, so nothing builds.src/shell/actions/instrument_drop_win.cpp:59callsGetCurrentProcessId()insidewriteTempPresetwith no platform branch anywhere in the TU. SWELL exportsGetCurrentThreadIdand not this (L2-02). The PID exists only to keep two concurrent REAPER instances from colliding in the shared temp dir; the atomic counter at:52already carries the intra-process half.
core/ is genuinely pure, and it was verified rather than assumed. Every #include
under src/core/** is a core/ sibling, one of 26 standard headers, or the generated
version_generated.h — zero REAPER, SWELL, WDL, LICE, VST3 or windows.h (T2 §1.1). The
whole directory contains nine preprocessor conditional lines, exactly one of which is a
platform fork, and that one (capture_paths.cpp:18–20, the Windows case-fold) is correct
for Linux with both branches already asserted by tests/test_capture_paths.cpp. All 91
test TUs under tests/ are platform-neutral.
The panel layer is already SWELL-portable, call for call. Dialog lifecycle, docking,
double-buffered LICE WM_PAINT, mouse/wheel/capture, WM_CAPTURECHANGED rollback, all
seven cursors, menus, the keyboard accelerator path, modifier reads, tooltips, drag-out and
DragQueryFile/DragFinish were each checked by name against swell-functions.h /
swell-types.h and are present (T2 §1.5). The Windows-only escapes are three:
DragAcceptFiles (panel_window.cpp:148–150, #ifdef _WIN32), SHFileOperationW
(prune), and OLE DoDragDrop (drag-out) — each already carrying a non-Windows branch or a
documented reason it does not. This is the audits' single most load-bearing finding.
The dock panel will not appear until the dialog-resource question is answered.
panel_window.cpp:135 is CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), …),
which SWELL resolves out of a per-module registry populated by a resgen-generated source
file that is not in the Linux target: src/app/CMakeLists.txt:97 has the target_sources
line commented out (and :86 for macOS). The registry head stays null, SWELL_CreateDialog
returns null, panel_window.cpp:137 returns, and the toggle action is a silent no-op with
no console line and no Actions-list checkmark (Λ-01, L2-06). Three defects stack in the
commented-out instructions themselves: the script named at :96 (mac_resgen.php) does
not exist, the output filename is wrong, and the output is an #include-only artifact that
cannot be a target_sources entry at all (Λ-01). Λ-F2 decides the route.
The instrument's Linux editor is a from-scratch X11 job, and today the target does not
configure at all. src/shell/instrument/CMakeLists.txt:9 is
if(WIN32 AND EXISTS "${VST3_SDK}/…/pluginfactory.cpp") — a conjunction, so a Linux
configure silently omits reasampler_vst even with the submodule slice fully initialised.
Beyond the gate: the wrong module entry point is compiled, the artifact is a file where
Linux wants a directory bundle, nothing hands a VST3 plugin the SWELL function table, and
the view reports kPlatformTypeHWND only (B1–B4, L2-08). The paint side is the cheapest
part — every painter already draws into a LICE_IBitmap through the shared kit, so the
drawing survives a window-system change intact. It is the window and event plumbing that
is entirely absent.
Nothing about the build is optimized, and the documented ship command is a no-op on
Linux. Root CMakeLists.txt:28–30 is the complete list of language settings — there is
no CMAKE_BUILD_TYPE, no CMAKE_CXX_FLAGS, no IPO/LTO, and no target_compile_options
anywhere in the tree. --config Release is accepted and ignored by Ninja and Make, so the
README's ship incantation produces a binary with no -O flag at all, on a tree whose
peaks hot path is documented as presuming an optimizing build (Λ-02). There is also no
symbol-visibility control (Λ-03), no thread library linked (Λ-04), no warning flags
(Λ-09), no install() rule anywhere, and no CI configuration in the repo on any platform
(T1 §1e).
Settled decisions (Daniel, 2026-08-02)
Six rulings. They are settled; do not re-litigate them. Each is recorded with the reasoning given, because the reasoning is what a later reader will need.
Λ-D1 — The VST3 instrument is in scope. (Audit fork D1 = Fork B.)
Phase Λ ships both artifacts on Linux, not just the extension.
Why. The instrument is not a standalone product on any platform. The capture browser
reads the extension's bank over the bridge, and the resample bake invokes an extension
action synchronously over the VST3 host bridge — root CLAUDE.md §"The resample bake"
makes the crossing structural. A non-REAPER Linux host was never getting the full
instrument regardless, because the two features that make it ReaSampler rather than a
generic sampler both tie into the extension directly. So the marginal thing Λ-D1 buys is
precisely a REAPER-hosted Linux instrument — which is the only complete one that exists on
any platform.
What it costs. T1 §4 states the asymmetry plainly: Fork A's build work is small and its unknown is source portability; Fork B's build work is genuinely large and carries an unbounded source tail and requires reversing a recorded decision. They are not "the same phase, one bigger" — they are different-shaped commitments, and this doc's wave structure keeps them separable (Λ-W6…Λ-W8 can be halted without unshipping Λ-W2…Λ-W5).
Consequence — D5 is reversed, in writing. "No cross-platform… Windows-only, VST3-only,
REAPER-only (D5)" is a recorded invariant in three files: src/core/instrument/CLAUDE.md
(twice — Invariants and Non-goals), src/shell/instrument/CLAUDE.md (Non-goals), and
src/shell/panel/CLAUDE.md ("Windows-only (D5) — font/GDI/HFONT choices assume Windows; no
cross-platform font-fallback concern"). D5's other two clauses survive untouched: still
VST3-only, still REAPER-only. Only the platform clause moves, and it moves to
"Windows and Linux; the editor is REAPER-hosted." Rewriting those three passages is
audit finding B5 and is a named deliverable of Λ-W6-T1.
Λ-D2 — The instrument reaches SWELL by dlopening REAPER's own libSwell.so. (D2 = route B3a.)
vendor/WDL/WDL/swell/swell-modstub-generic.cpp:69–131 is the vendored implementation:
resolve the host executable's directory from /proc/self/exe (falling back to dladdr),
dlopen libSwell.so beside it, dlsym SWELLAPI_GetFunc, call SWELL_set_app_main,
populate the API table.
Route B3b is rejected. Building SWELL into the module means the 14 SWELL TUs plus 8
LICE TUs from SWELL's own Makefile and a wholly new third-party dependency
surface — GDK/GTK3, FreeType, Fontconfig, OpenGL — that the extension deliberately avoids
via SWELL_PROVIDED_BY_APP. That is a scope expansion in a phase whose entire cost
advantage comes from not having one.
The consequence Daniel accepted: B3a makes the instrument's UI load-bearing on being hosted by REAPER specifically. That is consistent with D5's surviving REAPER-only clause and with Λ-D1's reasoning above. It is also why Λ-D3 exists — the coupling has to be safe, not merely intended.
Important: the vendored stub cannot be used as-is. See §"The non-REAPER-host safety
criterion" — swell-modstub-generic.cpp:102 calls exit(2) on a failed dlopen, from a
static constructor. Route B3a is the ruled route; how it is implemented is specified
there and is not a re-litigation of this ruling.
Λ-D3 — Non-REAPER hosts must be protected. First-class acceptance criterion.
Daniel, verbatim: "REAPER-only is good for now, but we need to protext other hosts."
A working editor on Linux is required under REAPER only. But the plugin must degrade
safely in Bitwig, Ardour, or any other Linux host: no crash, no host takedown, defined
behaviour when libSwell.so is absent or unusable. The audits flagged the absent-libSwell
failure mode as unverified; this phase must both determine it and hard-guard it. It is
specified as an observable contract in its own section below, and it is an acceptance
criterion of two tracks, not a footnote on one.
Λ-D4 — macOS is out. (D3 = out.)
Daniel, verbatim: "I don't have a mac to compile on."
Λ-01, Λ-03, Λ-04, Λ-07, Λ-09 and L2-09 are all marked shared macOS/Linux in the audits,
meaning the same edit covers both. Those edits still get made in their shared form and
noted as shared — a #ifdef _WIN32 / #else that is right for both costs nothing extra
and does not require a mac. What is out is: macOS as a phase deliverable, any macOS
verification, the swell-modstub.mm-under-a-CXX-only-project() question (root
CMakeLists.txt:26 is LANGUAGES CXX), signing and notarization, and the macOS-only
half of L2-11 (normalizeSlashes under-folds on case-insensitive APFS — a real pre-existing
defect this phase surfaces and does not own).
Do not "fix macOS while you're in there." An unverifiable edit to the APPLE branch is worse than no edit, because it looks tested.
Λ-D5 — The Linux artifact is shipped, not developer-only. (D6 = shipped.) Hard unlink is acceptable. (T2 [Daniel] 2 = yes.)
Two rulings that travel together, because both are about what "shipped" obliges.
Shipped means Λ-02 (an actual Release/optimized build) and Λ-08 (a documented install
path and artifact name) are must-fix, not nice-to-have, and that a Linux install()
rule is worth authoring. A developer-only artifact could have skipped all of it.
Hard unlink is acceptable, with a platform-aware confirmation string. XDG trash
is not a ship blocker. The user must be told, at the confirm, that the deletion is
permanent on this platform. Note that L2-05 has a second half the ruling does not
cover: on Windows a bank file held open by REAPER's audio engine cannot be deleted and is
counted as "skipped"; on Linux unlink on an open file succeeds, the audio keeps playing
from the open fd, and the bytes are gone when it closes. That is not a trash question — it
is the disappearance of the recovery floor root CLAUDE.md §"The resample bake" relies on
("the superseded file survives on disk until a prune reclaims it"). Λ-W4-T1 owns it.
Λ-D6 — Fork B's editor work is real work, and it is sequenced last.
Not a separate Daniel ruling but the direct consequence of Λ-D1 + Λ-D2 + Λ-D3: the X11 editor is the phase's only L-band source item (L2-08; T1 B4 costs it "build S, source L") and it is the last thing sequenced. Everything before it ships something. If Λ-W8 is halted, Linux users still have a shipped extension (Λ-W5) and a loading, processing, generic-UI instrument (Λ-W6) — which is a real product, not a stub.
The non-REAPER-host safety criterion
Λ-D3 stated as something a person can check. This section is the specification the two instrument waves are graded against.
The finding that makes this urgent
The audits flagged the absent-libSwell.so failure mode as unverified. It is verifiable
from the vendored source, and the answer is bad. Reading
vendor/WDL/WDL/swell/swell-modstub-generic.cpp:
:125declaresSwellAPPInitializer m_swell_appAPIinit;— a file-scope static object, so its constructor runs when our.soisdlopened, i.e. during the host's plugin scan, before any VST3 code of ours executes.:98–103—dlopen(fn, RTLD_LAZY); if it returns null, the stub prints to stdout and callsexit(2).:117— ifdoinitreports any unresolved SWELL name,exit(1).
So compiling the vendored SWELL_LOAD_SWELL_DYLIB path unmodified means: a Bitwig or
Ardour plugin scan on a machine with no libSwell.so beside the host binary terminates the
host process. Not a failed scan — a killed DAW, mid-scan, with the user's session. That is
precisely the host takedown Λ-D3 forbids, and it is a certainty rather than a risk.
The mitigating detail, worth knowing because it shapes the fix: doinit (:53–67) does
not leave unresolved entries null. It substitutes &dummyFunc (:63), and dummyFunc
(:51) is static int dummyFunc() { return 0; }. A partial SWELL therefore degrades to
"every missing call returns 0/NULL" rather than jumping through a null pointer. A partial
load is survivable; exit() is not.
The implementation shape this obliges
Route B3a (Λ-D2) is the ruled route. It must be reached without the stub's own loader:
- Never define
SWELL_LOAD_SWELL_DYLIB. Compileswell-modstub-generic.cppin its default branch — the same branch the extension already uses (src/app/CMakeLists.txt:91–92) — which exportsSWELL_dllMain(hInst, callMode, GetFunc)(:135) and callsdoiniton the pointer it is handed. The whole file is inside#ifdef SWELL_PROVIDED_BY_APP(:21), so the VST3 target must define that symbol too; today it does not. - Own the load. A new
shell/instrument/swell_bootstrapTU does the work the stub would have done — resolve the host executable's directory,dlopenlibSwell.so,dlsymSWELLAPI_GetFunc,dlsymand callSWELL_set_app_main, then call our own exportedSWELL_dllMain(hinst, DLL_PROCESS_ATTACH, getfunc)— using only exported surface, with every failure returning a recorded state instead of exiting. No vendored file is patched. This also dissolves theSWELLAppMain-as-a-link-requirement noted in T1 B3a: we pass an app-main because we choose to, not because the linker demands one.[verify — Linux]whether a minimal app-main is sufficient for a plugin that only ever creates child windows inside a host-supplied X11 window. - Probe the host, not just the library. A non-REAPER host has no SWELL message loop —
REAPER's own is what pumps SWELL windows. So even a successful
dlopenin Ardour would produce a window nothing drives. Gate the bootstrap on the host name viaIHostApplication::getName()and on thedlopensucceeding; either failing means no editor. Cheap, and it makes the REAPER-only coupling explicit rather than emergent. - Probe once, lazily, off the audio thread. The result is a tri-state — untried /
available / unavailable — computed on first
createViewand never recomputed. Nodlopenfromprocess(), no per-createViewretry, no static-constructor work.
The observable contract
In any Linux host, with or without libSwell.so:
- Scan. The host opens
reasampler_9000.vst3, findsModuleEntry/ModuleExit/GetPluginFactory, and enumerates the one class. No crash, no hang, no process exit, no blacklist entry. - Instantiate and run.
setActive(true)andprocess()produce audio (or silence with nothing loaded). MIDI notes play. The component state round-trips throughsetState/getStatebyte-identically with the Windows build. - Editor request. When no usable platform surface exists,
isPlatformTypeSupportedreturnskResultFalsefor every type — includingkPlatformTypeX11EmbedWindowID— andcreateView(kEditor)returnsnullptr. It never returns a view whoseattached()then fails, and never returns a view that draws nothing. The host falls back to its generic parameter UI. - Diagnosis. Exactly one line, once per process, naming why the editor is unavailable
(host not REAPER /
libSwell.sonot found at<path>/ SWELL API incomplete). Written through the SDK's own logging or stderr — never a modal, never a message box, never repeated per instantiation. - Teardown. Unloading the module does not crash. Nothing calls
exit(),abort(), or leaves a partially-initialised SWELL table reachable by a later call.
Under REAPER on Linux the same contract holds with the editor branch taken instead: the
view reports kPlatformTypeX11EmbedWindowID, attaches to the host-supplied window id, and
drives its timer through Linux::IRunLoop.
How it is verified
The VST3 SDK ships a non-REAPER host harness and it is already on disk.
vendor/vst3sdk/public.sdk/samples/vst-hosting/validator/source/validator.cpp is present
in the vendored slice (it lives under public.sdk/, which the documented narrow submodule
init already pulls). It is a scriptable, headless, REAPER-free host that instantiates a
plugin and exercises the standard suite. It is the right instrument for this criterion
and neither audit named it, because neither swept the SDK's samples/ tree.
The verification set, all [verify — Linux]:
validator reasampler_9000.vst3completes and reports no failures, on a machine with nolibSwell.soon the filesystem. This is the load-bearing check: the validator process surviving is the direct negation of theexit(2)finding.- The same, in Ardour or Bitwig, through a real plugin scan: the DAW finishes scanning, the plugin appears in the browser, an instance instantiates, and the generic parameter UI opens. A hand check, but the one that answers Daniel's actual sentence.
- The same, under REAPER on Linux: the editor opens.
- A negative-control run: rename
libSwell.sobeside a Linux REAPER, load the plugin, and confirm the generic-UI fallback and the single diagnostic line — not a crash, not a blank window.
The acceptance criteria above must pass with zero VST3 parameters registered. See §"Cross-phase dependency: Γ-W4-T1" — today the generic UI is empty, and "the host shows an empty generic UI" is a pass. It is a poor experience, and Γ fixes it; it is not Λ's to fix.
Wave and track structure
Why this sequence
Three constraints shape it, and they conflict.
One: the [verify — Linux] items are worthless as a late checklist and near-free as an
early wave. T1's own §6 Stage 0 says it outright — running V1 and V2 on a Linux box is
"the one action that reprices everything", and every effort band in both audits is
provisional until it happens. A plan that puts verification at the end has, in effect, no
estimates at all.
Two: nothing can be verified from this Windows box. Every acceptance criterion for the
first Linux edits is itself a [verify — Linux] item. This is not a fixable property of
the plan; it is the phase. The honest structure is to say so: Λ-W2's edits are authored
blind, from the audits' citations, and Λ-W3 is the session that discharges Λ-W2's
acceptance criteria and answers the sweep in the same sitting. Λ-W2 is not "done" in the
usual sense until Λ-W3 runs. Pretending otherwise would launder unverified work into
landed work, which is exactly what the audits' [verify] discipline exists to prevent.
Three: the sweep cannot answer the panel-dependent items until the panel renders. T2 §6 step 4 flags this and notes the critical path runs through the other track. The resolution is to pull the dialog-resource work forward into Λ-W2 — which is why Λ-F2 is worth ruling early, and why the plan states what happens if it is not (Λ-W2-T3 slips and Λ-W3 splits).
Two further sequencing decisions, both deliberate:
- The locale codec (L2-04) is pulled forward into Λ-W2, ahead of everything it looks
like it should follow. It is the phase's one substantial fix that needs no Linux box at
all — two writers and three readers, all in
core/, all unit-testable on Windows today — and its failure mode is that the bank index is written unparseable and the project's whole bank is lost on reload. It should land before any Linux user saves a project, and there is no reason to spend a Linux session discovering that. - V9 (does REAPER's Linux install ship
libSwell.sobeside its executable?) runs in the sweep, not in the instrument waves. It is a one-linels, and the entire Λ-D2 route rests on it. If it is false, B3a is void and the instrument's editor needs a re-ruling — a thing to learn in Λ-W3, not four waves later.
On the audits' severity disagreement. T2 grades L2-03 a Blocker on failure-mode quality with no direct evidence, and L2-04 a Major with a confirmed mechanism, and flags the inconsistency as a scope call for whoever prioritises the two. This plan dissolves it rather than resolving it: both land in Λ-W2, in different tracks, so the relative grade never has to be adjudicated.
Λ-W1 — The audits (complete)
Two tracks, both landed: T1 build-toolchain-audit (Λ-01…Λ-10, V1…V10, D1…D7) and
T2 source-runtime-audit (L2-01…L2-12). Recorded here so the wave numbering matches the
audit filenames and branch names; nothing to re-spec.
Λ-W2 — Make it buildable, and make it honest
Four tracks, disjoint at the file level. Three of them are authored blind against the audits and verified in Λ-W3; T4 is verifiable on the current box.
Λ-W2-T1 — linux-compile-blockers
Goal. The extension compiles and links under GCC/Clang, and when it refuses to load it says why instead of vanishing.
Surface boundary — owns: src/shell/panel/draw_kit.cpp (loadFont, :70–77),
src/shell/actions/instrument_drop_win.cpp (writeTempPreset, :50–61), src/app/main.cpp
(the REAPERAPI_LoadAPI failure branch only). Does not own: any CMakeLists.txt,
panel_window.cpp, or any core/ file.
Behavior.
- Drop the
| FF_DONTCAREterm or define it locally in the non-Windows include branch. Do not addwindows.h(L2-01's stated direction). The family bits are advisory to Windows' font mapper and meaningless to fontconfig. - Replace
GetCurrentProcessId()with a platform-neutral uniqueness source behind a guard; the atomic counter at:52already carries the intra-process half (L2-02). - On the load-failure branch, either switch
main.cpptoREAPERAPI_MINIMALplus an explicitWANTlist — the patternpanel_window.cppandpanel_audition.cppalready use — or keep the full load and print the failure count viarec->GetFunc("ShowConsoleMsg")before returning 0 (L2-03). The minimal list is also the honest inventory of what this extension actually needs.
Acceptance criteria.
cmake -B build -S . -G Ninja && cmake --build buildproducesbuild/reaper_reasampler.sowith no errors.[verify — Linux]= V1.ctest --test-dir build --output-on-failurepasses all 91 targets, no-Cflag needed.[verify — Linux]= V2.- A deliberately misspelled entry in the WANT list (or a forced non-zero
failcnt) produces a visible REAPER console line naming the count, not a silent refusal. - Windows build and
ctestunchanged.
Prerequisites. None. Discharges: L2-01, L2-02, L2-03; enables V1, V2, V3.
Λ-W2-T2 — toolchain-floor
Goal. The Linux build is optimized when asked, links what it uses, hides what it does not export, and reports diagnostics no one has seen yet.
Surface boundary — owns: root CMakeLists.txt, src/app/CMakeLists.txt (the target
property and platform blocks; not the source list — that is Λ-W4-T3's),
cmake/reasampler_targets.cmake, src/shell/instrument/CMakeLists.txt (thread linkage
only; the WIN32 gate is Λ-W6-T1's), README.md and root CLAUDE.md §"Build and test" /
§"Install / reload" / §"One-time submodule setup". Does not own: any .cpp or .h.
Behavior.
- Λ-02 — default
CMAKE_BUILD_TYPEwhen neither it norCMAKE_CONFIGURATION_TYPESis set, and correct the docs' ship instruction to name the single-config-generator path. The README currently sends a Linux user tobuild/Release/, which on Ninja/Make does not exist; the module lands atbuild/reaper_reasampler.so. - Λ-03 —
CXX_VISIBILITY_PRESET hidden+VISIBILITY_INLINES_HIDDENon both module targets. SWELL's own build already uses-fvisibility=hidden; the two symbols that must stay exported carry their ownvisibility("default")attributes (reaper_plugin.h'sREAPER_PLUGIN_DLL_EXPORT,fplatform.h'sSMTG_EXPORT_SYMBOL, andswell-modstub-generic.cpp:135'sSWELL_dllMain). - Λ-04 —
find_package(Threads REQUIRED)+Threads::Threads. Correct on all three platforms, costs nothing on Windows. - Λ-09 —
-Wall -WextraandCMAKE_CXX_EXTENSIONS OFF. No-Werrorin this change (the audit is explicit); the diagnostic-set size over the tree is not estimable from Windows. Separately,-Wl,--no-undefinedon the module targets, to restore the fail-at-link-time behaviour MSVC gives and GNUlddoes not — or a recorded reason whySWELL_PROVIDED_BY_APP's function-pointer design makes the gap moot. - Λ-05 — pin the
reaper_plugin.h→../WDL/swell/swell.hinclude coincidence with a comment or anINTERFACEtarget carrying both include dirs as one unit. It is invisible on Windows and load-bearing off it. - Λ-07 — record the compiler floor, or add
-lstdc++fsand document why. - Λ-10 — one sentence in the platform-support docs: on Linux
vendor/vst3sdkis optional until Λ-W6, sogit submodule update --init vendor/reaper-sdk vendor/WDLis the complete extension-only prerequisite.
Acceptance criteria.
compile_commands.json(or a verbose build log) shows an explicit-Oflag on a barecmake --build build.[verify — Linux].nm -D --defined-only reaper_reasampler.so | grep -E 'ReaperPluginEntry|SWELL_dllMain'finds both after the visibility preset.[verify — Linux]= V6.- The extension links with
Threads::Threadsremoved, proving the include-only pthread dependency needs no flag — or the symbol forcing it is named.[verify — Linux]= V5. capture_paths_testslinks without an explicit-lstdc++fson the declared floor, or the flag is added and the floor documented.[verify — Linux]= V8.- The warning count from the first
-Wall -Wextrabuild is recorded, not fixed, and handed to Λ-W4 as an input. - Windows build unchanged;
--config Releasestill behaves as documented there.
Prerequisites. None. Runs concurrently with T1, T3, T4. Discharges: Λ-02, Λ-03, Λ-04, Λ-05, Λ-07, Λ-09, Λ-10; enables V5, V6, V8.
Λ-W2-T3 — panel-dialog-resource (gated on Λ-F2)
Goal. The docked bank panel opens on Linux, and if it ever fails to, it says so.
Surface boundary — owns: src/shell/panel/panel_window.cpp (the CreateDialogParam
call at :135–137, the dialog proc's platform contract, the drop-accept opt-in at
:145–150), src/resource.rc, src/resource.h, and — under the resgen route only —
one target_sources line in src/app/CMakeLists.txt's else() branch plus a new
include-shim TU. Does not own: any other panel TU, draw_kit, or any CMake target
property.
Behavior. Whichever route Λ-F2 picks, plus, unconditionally: a one-line
ShowConsoleMsg on the !g_panel.hwnd path naming the missing dialog resource (L2-06).
That single line converts a mystery into a two-minute diagnosis and is worth having on
Windows too — land it on both platforms, not behind a guard. The fix is shared
macOS/Linux either way (Λ-D4: made in shared form, verified on Linux only).
Acceptance criteria.
- The panel toggle action docks a visible, LICE-drawn bank panel in a Linux REAPER.
[verify — Linux]= V7. - With the resource deliberately unavailable, the toggle prints one console line rather than doing nothing. Verifiable on Windows by forcing the branch.
- Dragging a WAV from the file manager onto the docked panel ingests it — or, if it does
not, the failure is understood rather than mysterious.
[verify — Linux], T2 §5 item 3. This criterion's difficulty depends on the Λ-F2 route — see the fork. - Windows panel behaviour byte-for-byte unchanged.
Prerequisites. Λ-F2 must be ruled before dispatch. If it is not, this track slips to Λ-W4 and Λ-W3's sweep splits into a panel-independent half (run early) and a panel-dependent half (run after this lands) — a second Linux session, which is the cost of leaving the fork open. Discharges: Λ-01, L2-06, L2-07; enables V7.
Λ-W2-T4 — locale-independent-numerics
Goal. A persisted float round-trips identically regardless of the process locale, so a bank index written on a comma-decimal machine is not written unparseable.
Surface boundary — owns: src/core/json/json.cpp (the %.17g writer and the strtod
reader), src/core/model/provenance.cpp (its own %.17g), src/core/wire/wire.cpp
(Cursor::fieldDouble), src/core/capture/render_settings.cpp (the std::stod over
REAPER's P_RAZOREDITS), and the corresponding tests/. Does not own: any shell TU,
any CMake file, wav_codec (its byte-order handling is already explicit and correct).
Behavior. Make the number codec locale-independent at its two writers and three
readers — std::to_chars/std::from_chars, or a std::locale::classic()-bound stream.
Do not "fix" this by calling setlocale; an extension must not mutate the host's
locale. Add pure tests that pin the emitted text for a fractional value, and that a
fractional value survives a write/read round trip.
Why this is not paranoia: on Windows the CRT starts in the "C" locale and nothing here
calls setlocale, which is why it has never fired. On Linux, SWELL's GDK backend calls
gtk_init_check/gdk_init_check and never calls gtk_disable_setlocale, and any GTK or Qt
plugin in the same process can do the same (L2-04). The readers are honestly fail-closed —
they require whole-token consumption — so the failure is "the field disappears", not "the
field is silently wrong".
Acceptance criteria.
- New pure tests pass on Windows today, under a forced comma-decimal
LC_NUMERICin the test itself. This is the one Λ-W2 track that does not wait for Λ-W3. - The bank index, view model, tracking ledger, provenance blob and tail setting all round-trip a fractional value under that forced locale.
- Byte-for-byte identical output to today's writer under the
"C"locale — this is a persisted format; a changed representation is a compatibility event. - Nothing on a hot path is touched. The JSON/persist path is declared off all hot paths in
root
CLAUDE.md, and this change stays there.
Prerequisites. None. Discharges: L2-04.
Note: T2 §5 item 1 (read LC_NUMERIC inside a running REAPER-Linux process) stays in
the Λ-W3 sweep, but only to record how urgent this was — the work is not gated on it.
Λ-W3 — First light, and the verification sweep
One track, deliberately. This is a person at a Linux box working a checklist where each answer reprices the next; splitting it across specialists buys no concurrency and loses the thread. Precedent: Ρ-W1 and Γ-W4 are both single-track waves for the same reason.
Λ-W3-T1 — linux-verification-sweep
Goal. Discharge every acceptance criterion Λ-W2 could not check from Windows, answer
every [verify — Linux] item in both audits, and reprice the remaining waves against what
is actually true.
Surface boundary — owns: no source file and no CMake file. Its deliverable is a
verification record at docs/product/audit-notes/lambda-w3-verification.md, one entry
per item: the exact check run, the observed result, and what it changes. Any fix the sweep
motivates is filed to Λ-W4, not made here. The one exception: a defect that blocks
further sweeping (the build does not link at all) is fixed in place and recorded as a
deviation, because the alternative is a wasted session.
Behavior — the sweep set.
| From | Items |
|---|---|
| T1 | V1 compile, V2 ctest, V3 does REAPER's Linux build call SWELL_dllMain and populate the API table, V4 where UserPlugins/ actually is and whether reaper_*.so is the right glob, V5 pthread link flag, V6 visibility vs. the two exported symbols, V7 the docked panel, V8 -lstdc++fs, V9 does libSwell.so sit beside REAPER's executable, V10 which uname -m values the VST3 bundle must carry |
| T2 | process LC_NUMERIC; REAPERAPI_LoadAPI's actual return value and the name of any gap; panel file-drop routing; which SWELL GDI/locale build REAPER ships; fontconfig's substitution for "Consolas"; SWELL_InitiateDragDropOfFileList acceptance by common targets and whether its 500 ms no-motion timeout cancels a slow gesture; prune against an in-use file |
| New | The -Wall -Wextra diagnostic set, counted and categorised; whether -Wl,--no-undefined links clean or names an undefined set; whether the four-TU LICE slice links without lice_colorspace.cpp (T1 §3 leaves this an unreconciled inference) |
Three items are load-bearing beyond their own answer and must be run first:
V1 (nothing else is observable until it passes), V9 (a false answer voids Λ-D2's
route and needs a re-ruling before Λ-W6 is dispatched), and the REAPERAPI_LoadAPI count
(a non-zero result promotes L2-03 from a diagnostic to a real Blocker and names the gap).
Acceptance criteria.
- Every item above has a recorded answer or an explicit "could not determine, because X". A blank is a failure of this track, not a deferral.
- Λ-W2's four tracks each have their
[verify — Linux]criteria marked discharged or failed, by name. - Λ-W4 and Λ-W5's scope is restated against the answers, with any effort band that moved called out. The audits' bands are provisional by their own statement; this is where they stop being.
- Λ-W6's prerequisites (V9, V10) are answered, or Λ-W6 is explicitly blocked pending a Daniel re-ruling on Λ-D2.
Prerequisites. Λ-W2-T1, T2, T4. T3 if Λ-F2 was ruled — otherwise the panel-dependent items (V7, file-drop routing, and any font check that needs a rendered panel) are deferred to a second session and that deferral is recorded. Discharges: V1–V10 and T2 §5's seven items, as answers rather than as fixes.
Λ-W4 — Correctness and safety, repriced
Three tracks, disjoint by directory. Everything here is known work whose size the sweep may have moved.
Λ-W4-T1 — prune-deletion-safety
Goal. Prune on Linux deletes only what it means to, tells the truth about what it reclaimed, and tells the user the deletion is permanent.
Surface boundary — owns: src/shell/persist/prune_fs.cpp (the deletion authority's
non-Windows branch and the reclaim scan), and the prune confirmation text wherever it is
composed (shell/actions/prune_action / shell/panel/panel_bank_ops). Does not own:
core/reclaim/'s orphan computation (pure, portable, unaffected), any other persist TU.
Behavior.
- The confirmation string becomes platform-aware (Λ-D5). On Linux it states the deletion is permanent and there is no Recycle Bin. On Windows it says what it says today. Same code path, one platform-dependent phrase — not a second dialog.
- The dead "locked" branch is reckoned with.
if (ec) return false; // real failure (locked/permission) -> skipencodes Windows file-sharing semantics; on Linuxunlinkon an open file succeeds and the branch never fires. Either the branch is documented as Windows-only in place, or the Linux path acquires an equivalent guard. What it must not do is stay silently asymmetric — the recovery floor rootCLAUDE.mdrelies on ("the superseded file survives on disk until a prune reclaims it") has nothing under it on Linux otherwise. - The symlink hazard is fixed.
fs::directory_iterator+is_regular_file()follows symlinks under C++17; size is read from the target viafile_size()butfs::removedeletes the link, not the target — so prune reports N bytes reclaimed and reclaims zero. Symlinked media folders are far more idiomatic on Linux than on Windows. This is a reporting lie, not a cosmetic issue, and it is not covered by Λ-D5's ruling.
Acceptance criteria.
- A Linux prune of a bank file currently playing behaves as recorded in Λ-W3's sweep, and
the behaviour matches what the confirmation promised.
[verify — Linux]. - A symlinked bank file is either skipped, or deleted with its target, and the reclaimed-byte
figure matches what actually left the disk in both cases. Unit-testable for the
computation;
[verify — Linux]for the filesystem half. - The Windows path —
SHFileOperationW+FOF_ALLOWUNDO, Recycle-Bin recoverable — is bit-for-bit unchanged. - Prune remains the only file-deletion path in the system.
Prerequisites. Λ-W3-T1 (the in-use-file check). Discharges: L2-05 (both halves) and the symlink row of T2 §4.
Λ-W4-T2 — linux-font-faces
Goal. The kit asks for faces that exist on a stock Linux distro, so type is chosen rather than substituted.
Surface boundary — owns: src/shell/panel/draw_kit.cpp's five loadFont call sites and
the face constants in draw_kit.h. Does not own: loadFont itself beyond the literals,
the palette, any geometry, or the two WCAG static_asserts (which are on pixel height and
weight, not on the face, and hold regardless).
Behavior. A platform face list at the five call sites: DejaVu Sans / DejaVu Sans Mono as the Linux defaults, following SWELL's own no-fontconfig fallback list (LiberationSans/DejaVuSans, LiberationMono/DejaVuSansMono) as precedent. One code path, shared with macOS's eventual San Francisco/Menlo (Λ-D4: made in shared form, not verified).
The subtlety worth carrying into the work: draw_kit.cpp:74's if (!hf) return guard does
not catch the failure mode here. SWELL's CreateFont always returns a non-null handle
even when the face never resolved — the failure is recorded internally as a null
typedata, not as a null return. So a wrong or missing face is not observable at the call
site, only in the rendering.
Acceptance criteria.
- Every kit string renders in the intended face on a stock distro, and the numeric readouts
are tabular.
[verify — Linux]. - Windows renders Segoe UI and Consolas exactly as today.
- Row heights, ellipsis points and label truncation are unchanged on Windows; on Linux they are measured rather than assumed correct.
Prerequisites. Λ-W3-T1's fontconfig answer (T2 §5 item 5), which decides whether this is cosmetic or a readability regression. Discharges: L2-09.
Λ-W4-T3 — source-partition-and-invariants
Goal. The build's source list stops relying on every TU's own #ifdef discipline, and
the invariants Linux weakens are stated where a reviewer will read them.
Surface boundary — owns: the target_sources list in src/app/CMakeLists.txt:8–51
(the list; the property blocks are Λ-W2-T2's), any new platform-sibling TU the sweep
showed was needed, src/shell/actions/drag_out_win.h's invariant comment, and the
corresponding src/shell/**/CLAUDE.md invariant passages. Does not own: any behaviour
change in a shipped code path.
Behavior.
- Λ-06 — partition the source list where the Λ-W3 diagnostic set says a TU needs it.
drag_out_win.cppis the model (a real#ifdef _WIN32/#elsesplit);arrange_drop_win.cppandinstrument_drop_win.cppare_win-suffixed for the surface they serve, not for a platform dependency, and the audits found them portable by inspection — confirm against the actual compile rather than re-inspecting. - L2-10 — make
drag_out_win.h:7–11the doc a Linux reviewer is pointed at, and treat "MOVE is structurally impossible" as a Windows-scoped claim. The wording is Λ-F3's ruling; the edit is this track's regardless of which way it goes. - L2-11 — no Linux action. If the predicate is touched at all it becomes "case-insensitive filesystem", not "Windows" — but Λ-D4 puts macOS out, so the right move is a comment recording the known macOS defect, not a speculative fix.
Acceptance criteria.
- No TU compiles on Linux only because of an
#ifdefthat happens to be complete; every platform-specific TU is either partitioned in CMake or carries a deliberate, commented guard. - The copy-only invariant's text says the same thing in
drag_out_win.h, the owningCLAUDE.md, and any spec text that cites it. - Zero behaviour change on Windows.
Prerequisites. Λ-W3-T1. Λ-F3 for the L2-10 wording only (non-gating: the track can land the partition and leave the sentence for a follow-up). Discharges: Λ-06, L2-10, L2-11's Linux half.
Λ-W5 — Ship the extension
Λ-W5-T1 — linux-packaging-and-install
Goal. A Linux user can install a correctly-built reaper_reasampler.so by following a
document, and a pipeline can install it by following a rule.
Surface boundary — owns: an install() rule in src/app/CMakeLists.txt, README.md's
platform-support and install sections, root CLAUDE.md §"Install / reload", and
docs/product/versioning-and-release.md's artifact and pipeline paragraphs.
Does not own: any source file, the toolchain properties (Λ-W2-T2's), or CI (Λ-F1).
Behavior.
- Document the Linux
UserPlugins/root as V4 actually found it, and the single-config-generator output path (build/reaper_reasampler.so) that no doc currently names. - Add the first
install()rule in the tree, for both channels. The channel fork is platform-independent by construction —REASAMPLER_CHANNELthreads throughconfigure_fileinto names only — soreaper_reasampler_beta.soneeds no separate mechanism, only a separate destination check. - Amend
versioning-and-release.md, which already promises three platform artifacts per channel per release and mentions no Linux install path, no signing and no CI. Λ-D4 removes macOS from the near-term promise; that document should say two platform artifacts per channel for now, with macOS named as deferred rather than silently dropped. - The beta channel on Linux is uncosted in both audits. It is almost certainly free — the fork is name-only — but "almost certainly" is not a ship criterion. Build and install both channels side by side once.
Acceptance criteria.
- A clean-machine walkthrough: clone, narrow submodule init, configure, build, install,
restart REAPER, actions present, panel docks. Following only the README.
[verify — Linux]. reaper_reasampler.soandreaper_reasampler_beta.socoexist in one REAPER with separate ext-state namespaces, command ids and dock idents.[verify — Linux].- The installed binary shows an explicit
-Oflag in its build log — the Λ-02 criterion, re-checked on the artifact that actually ships. versioning-and-release.mdno longer promises an artifact this phase does not produce.
Prerequisites. Λ-W3-T1 (V4), Λ-W4 (all three tracks — shipping means the correctness fixes are in). Discharges: Λ-08, and Λ-02's shipped half.
Λ-W6 — The instrument module, and the host-safety contract
Two tracks, disjoint: one is build + docs, the other is source. At the end of this wave the Linux instrument loads in any host, processes audio, and has no editor at all — which is a defined, verifiable, shippable state, not a half-done one.
Λ-W6-T1 — vst-linux-module
Goal. reasampler_9000.vst3 configures, builds and installs on Linux as the directory
bundle a Linux host expects, and the decision that forbade it is reversed in writing.
Surface boundary — owns: src/shell/instrument/CMakeLists.txt (the gate, the entry-point
source selection, the bundle POST_BUILD and install rule, SWELL_PROVIDED_BY_APP, the
modstub TU), and the three D5 passages in src/core/instrument/CLAUDE.md,
src/shell/instrument/CLAUDE.md and src/shell/panel/CLAUDE.md. Does not own: any
.cpp or .h under shell/instrument/.
Behavior.
- B5 first, as a documentation act. Rewrite the three D5 passages: platform clause becomes "Windows and Linux; the editor is REAPER-hosted", VST3-only and REAPER-only survive verbatim. Nothing else in this wave may land before this does — the invariant files are what a future implementer reads.
- B1 — swap
public.sdk/source/main/dllmain.cppforlinuxmain.cppon Linux.dllmainincludes<windows.h>with noSMTG_OS_*guard;linuxmain.cppexportsModuleEntryandModuleExit, both mandatory — the SDK's own loader refuses the module without either. Both files are already vendored; this is a source swap plus a platformif(). - Split the
WIN32 AND EXISTSconjunction atsrc/shell/instrument/CMakeLists.txt:9. TheEXISTShalf stays (a fresh clone with no VST3 slice must still configure); theWIN32half becomes a Windows-or-Linux predicate. - B2 — the artifact becomes a directory:
reasampler_9000.vst3/Contents/<uname -m>-linux/reasampler_9000.so, per V10's answer.Contents/Resources/moduleinfo.jsonis optional — the SDK'sgetModuleInfoPathreturns empty when absent rather than failing — so do not author one. Install roots:$HOME/.vst3/,/usr/lib/vst3/,/usr/local/lib/vst3/,$APPFOLDER/vst3/. SWELL_PROVIDED_BY_APP+ the modstub TU are added to this target, in the default (non-SWELL_LOAD_SWELL_DYLIB) branch. Thedlopenitself is Λ-W7's; what this track lands is the compiled-in, inert table plus the exportedSWELL_dllMain.- The channel fork applies:
reasampler_9000_beta.vst3with its own class UID. The UID pair is FOREVER-FROZEN and must not change — a Linux build is a new platform, not a new identity, and a saved project rebinds by UID.
Acceptance criteria.
- A Linux configure produces the
reasampler_vsttarget; a Windows configure is unchanged. - The built bundle's directory shape matches what the SDK's
module_linux.cppopens, and the SDK's ownvalidatorloads it.[verify — Linux]. - Both channels build and install side by side; the two class UIDs are byte-identical to the Windows build's.
- The three
CLAUDE.mdfiles no longer claim Windows-only, and still claim VST3-only and REAPER-only.
Prerequisites. Λ-W2-T2 (thread linkage, visibility, warnings), Λ-W3-T1 (V10, and V9 if Λ-D2 is to survive). Discharges: B1, B2, B5, and Λ-10's "vst3sdk is optional on Linux" caveat becomes conditional.
Λ-W6-T2 — vst-host-safety-contract
Goal. The plugin's editor surface refuses cleanly on every Linux host, so that adding a real editor later is a change of branch taken and not a change of contract.
Surface boundary — owns: src/shell/instrument/editor_platform.cpp (the
isPlatformTypeSupported / createView decision and the non-Windows stubs),
reasampler_processor.cpp's createView site, reasampler_embed.cpp's
REAPER_FXEMBED_WM_IS_SUPPORTED, and a new availability-probe seam under
shell/instrument/. Does not own: any CMake file, the paint or input families
(untouched — they stay whole-region-guarded), any pure core/instrument/ module.
Behavior.
- The editor's availability becomes a tri-state probe (untried / available / unavailable), computed once, lazily, off the audio thread. In this wave the Linux answer is unconditionally unavailable; Λ-W7 gives it a real computation without changing a single call site.
isPlatformTypeSupportedreturnskResultFalsefor every type when unavailable;createView(kEditor)returnsnullptr. Never a view that fails to attach.- One diagnostic line per process, naming the reason. Never a modal.
reasampler_embed'sREAPER_FXEMBED_WM_IS_SUPPORTEDcontinues to return 0 off Windows. That is correct and stays correct until someone decides otherwise; see §"What Phase Λ does not do".- The Windows path is not restructured to accommodate this. The probe is a Linux branch on an existing decision, not a new abstraction over both.
Acceptance criteria. The full observable contract in §"The non-REAPER-host safety
criterion", verified by all four of its checks: validator with no libSwell.so present,
a real Ardour or Bitwig scan, a REAPER-on-Linux load, and the renamed-libSwell.so negative
control. Plus:
- Windows editor behaviour bit-for-bit unchanged — same window class, same
wndProc, sameCS_DBLCLKSfall-through. - Nothing added to
process(); nodlopen, no probe, no branch on the per-voice-per-sample path. - The criteria pass with zero VST3 parameters registered (see the Γ dependency below).
Prerequisites. Λ-W6-T1 (there must be a module to load). Concurrent with nothing else in this wave. Discharges: Λ-D3's Λ-W6 half; L2-08's fallback half.
Λ-W7 — The SWELL bootstrap
Λ-W7-T1 — swell-dylib-bootstrap
Goal. The plugin acquires a working SWELL function table under REAPER on Linux, without
the vendored stub's exit() behaviour and without vendoring SWELL.
Surface boundary — owns: a new src/shell/instrument/swell_bootstrap.{h,cpp}, the
availability-probe computation Λ-W6-T2 left stubbed, and the SWELLAppMain-shaped callback.
Does not own: any vendored file (nothing under vendor/ is patched), the CMake source
list beyond adding one TU, or the editor.
Behavior. Exactly the four-point implementation shape in §"The non-REAPER-host safety
criterion": never define SWELL_LOAD_SWELL_DYLIB; own the load through exported surface
only (SWELLAPI_GetFunc, SWELL_set_app_main, our own SWELL_dllMain); gate on the host
name and the dlopen; probe once, lazily, off the audio thread.
The failure taxonomy the probe must distinguish, because they need different messages: host
is not REAPER / executable path unresolvable / libSwell.so not present at the resolved
path / SWELLAPI_GetFunc missing or version-mismatched (the stub checks
SWELLAPI_GetFunc(NULL)==(void*)0x100) / API table incomplete. The last one is not fatal
— doinit substitutes a zero-returning dummyFunc for each miss, so a partial table
degrades rather than crashes; whether to accept a partial table or refuse is a judgment for
implementation review, with "refuse if any name the editor actually calls is missing" as the
starting proposal.
Acceptance criteria.
- Under REAPER on Linux, the probe reports available and the SWELL table resolves with
zero misses.
[verify — Linux]. - Under
validatorwith nolibSwell.soanywhere, the probe reports unavailable, the process exits normally, and one diagnostic line is printed. This is the direct negation of theexit(2)finding and is the track's headline criterion.[verify — Linux]. - Under Ardour or Bitwig with a
libSwell.soreachable, the host-name gate still refuses — proving the gate is on the host, not only on the library.[verify — Linux]. - No vendored file is modified;
git statusundervendor/is clean. - Windows build unaffected — the whole TU is behind a platform guard.
Prerequisites. Λ-W6-T1, Λ-W6-T2, and V9 answered affirmatively. If V9 is negative, this track does not exist and Λ-D2 needs a re-ruling. Discharges: B3 (route B3a); Λ-D3's runtime half.
Λ-W8 — The X11 editor
Λ-W8-T1 — x11-embed-view
Goal. The ReaSampler 9000 editor opens, draws and responds under REAPER on Linux.
Surface boundary — owns: a Linux sibling to src/shell/instrument/editor_platform.cpp
(window creation and parenting, the run-loop timer, the event-driven input path), and the
#ifdef _WIN32 region boundaries in reasampler_editor.h and the eight
editor_input_* / editor_paint* TUs — boundaries only, not their contents.
Does not own: draw_kit, any pure core/instrument/ module, any painter's drawing
logic, instrument_bake, or the processor.
Behavior. The audits cost this at L, and call it a new competence rather than a port
(L2-08; T1 B4 = "build S, source L"). The build-side cost is nil — the Linux IIDs are
already in the vendored slice (commoniids.cpp defines Linux::IEventHandler,
Linux::ITimerHandler, Linux::IRunLoop under #if SMTG_OS_LINUX, and that file is
already in the vst3_sdk source list). What must be written:
kPlatformTypeX11EmbedWindowIDinstead ofkPlatformTypeHWND;attachedToParentreceiving an X11 window id.- A
Linux::IRunLoop-driven timer replacingSetTimer/WM_TIMER— including the editor's sync tick, which the bake's arm-then-run discipline rides on. - An event-driven input path replacing the
wndProcswitch.RegisterClassW/CreateWindowExW/DefWindowProcWhave no SWELL analogue at all — SWELL has no window-class model, onlySWELL_CreateDialogand rawHWND__construction. - Substitutions the audit already resolved by name:
MoveWindow→SetWindowPos;GetWindowLongPtr/SetWindowLongPtr→ the non-Ptrforms returningLONG_PTR;GetKeyState→GetAsyncKeyState;TrackMouseEvent/WM_MOUSELEAVE→ nothing, so hover-leave needs its own derivation (the panel layer already documents the same gap);DragAcceptFiles/DragQueryFileW→ the drop path Λ-W2-T3 settled for the panel.
A worked reference implementation is already on disk and neither audit named it.
vendor/vst3sdk/public.sdk/samples/vst-hosting/editorhost/source/platform/linux/ contains
window.cpp (returning {kPlatformTypeX11EmbedWindowID, …} and answering
Linux::IRunLoop::iid from queryInterface), runloop.cpp, and irunloopimpl.h (a
RunLoopImpl implementing registerEventHandler / registerTimer / their unregisters
against Linux::IRunLoop). It is the host side of the contract rather than the plug-in
side, which makes it a precise specification of what our plug-in side must satisfy. It sits
under public.sdk/, so the documented narrow submodule init already pulls it. This does
not shrink the L band — it is a reading input, not a library — but it removes the "we are
guessing at the contract" risk from the estimate.
Acceptance criteria.
- The editor opens, draws every face identically to the Windows build (compare screenshots
at the same window size), and every drag, click, wheel and keyboard interaction behaves
the same.
[verify — Linux]. - Hover-leave is correct on every hover surface, without
TrackMouseEvent. - The bake's arm-then-run tick fires under
IRunLoop, and a bake completes end-to-end on Linux: staged file, extension action invoked, outcome read back, adopt and reset.[verify — Linux]. - Λ-W6-T2's contract still holds — a non-REAPER host still gets
nullptrfromcreateView. Re-run all four checks; this is a regression criterion, not a new one. - Nothing added to
process(). No dispatch added to any per-sample path (the phase-wide structural heuristic in rootCLAUDE.md). - Every new file lands under the ~600-line ceiling with a responsibility seam, not a
bisection. The editor's existing band-axis split (
_chrome/_waveform/_deck/_browse/_curve) is the seam vocabulary to reuse — the Linux platform TU is a sibling toeditor_platform, per L2-08's own direction: "it needs a sibling, not a rewrite."
Prerequisites. Λ-W7-T1. Discharges: B4, L2-08.
Concurrency, and the shared files
Wave order is serial; tracks within a wave are concurrent. Two deliberate exceptions to the strict reading:
- Λ-W6 may run concurrently with Λ-W4 and Λ-W5. It touches
src/shell/instrument/and that directory's CMake andCLAUDE.mdfiles, plussrc/core/instrument/CLAUDE.md— none of which Λ-W4 or Λ-W5 opens. The only reason to serialise is attention, not contention. - Λ-W6-T2 is prerequisite-gated on Λ-W6-T1 (there must be a module before it can refuse to show a view). They are file-disjoint but not order-free — the same shape PLAN.md already records for Γ-W3 ("T1 before T2 — serial").
Shared files, named — all textual adjacency, not semantic contention, unless marked:
| File | Tracks | Nature |
|---|---|---|
src/app/CMakeLists.txt |
Λ-W2-T2 (property + platform blocks), Λ-W2-T3 (one target_sources line, resgen route only), Λ-W4-T3 (the source list), Λ-W5-T1 (the install() rule) |
Four disjoint regions of one file. Λ-W2-T2 and Λ-W2-T3 are the only pair in the same wave; one line each. |
src/shell/panel/draw_kit.cpp |
Λ-W2-T1 (loadFont's CreateFont args, :73), Λ-W4-T2 (the five call sites, :154–158) |
Different waves. |
src/shell/instrument/CMakeLists.txt |
Λ-W2-T2 (thread linkage), Λ-W6-T1 (gate, entry point, bundle, install), Λ-W7-T1 (one added TU) | Different waves. |
src/shell/panel/panel_window.cpp |
Λ-W2-T3 alone | Deliberately not split. The L2-06 diagnostic and the Λ-01 resource route are the same function; under the resource-id-0 route they are the same line. Splitting them would be semantic contention. |
src/shell/instrument/editor_platform.cpp |
Λ-W6-T2 (the refusal branch), Λ-W8-T1 (the real branch) | Different waves; the second replaces the first's computation without touching its call sites. |
Cross-phase dependency: Γ-W4-T1
Verified against docs/PLAN.md while writing this: Γ-W4-T1 vst3-parameter-set is
listed as pending (PLAN.md §Γ-W4, and in the plan's summary map). Its own spec states the
current position plainly: "Today the plugin has zero parameters" —
ReaSamplerProcessor::initialize never populates SingleComponentEffect::parameters, so
getParameterCount() returns the SDK default 0. The T2 audit confirms the same by grep.
Why it matters to Λ. The host's fallback when a plug-in reports no usable editor is its generic parameter UI. With zero parameters registered, that fallback is empty — so today, "no editor" degrades to nothing, not to controls. Once Γ-W4-T1 lands its 44 derived parameters, the identical Λ code path degrades to a usable generic UI instead.
The dependency and its direction:
- Λ does not block Γ, and Γ does not block Λ. Λ-W6-T2's acceptance criteria are written to pass with zero parameters — "the host shows its generic UI, which may be empty" is a pass. Nothing in Λ is scheduled against Γ.
- Γ landing strictly improves Λ's user-visible outcome, at no cost to Λ. If Γ-W4-T1 is
already on
devwhen Λ-W6-T2 is verified, the tester will see a populated generic UI; that is a better result, not a different criterion. - Λ must not register any VST3 parameter itself. Γ-W4-T1's
ParamIDtable is FOREVER-FROZEN from the moment it ships, on the same footing as the command-id strings and the class UIDs. A parameter minted in Λ to make a Linux fallback look better would collide with a table Λ does not own. This is a hard boundary, and it is the only Γ↔Λ interaction that could actually go wrong. - Another team is running Γ concurrently. Λ assumes no schedule for it — not "Γ will
land first", not "Γ will land later". If a Λ track's brief needs a Γ fact, it reads
devat dispatch time.
Calls made here (product-designer calls, not forks)
Two questions the audits raised as [Daniel] that this doc answers rather than escalates.
Contradict either in review with an argument.
Silent font substitution is not acceptable; the kit gets an explicit Linux face list
(T2 [Daniel] 3, landed as Λ-W4-T2). The reasoning is docs/product/visual-design-language.md:
the kit's type is part of a deliberate visual identity, and the palette-role discipline
exists precisely so a visual direction is a single-file change rather than an emergent
property of the host. Letting fontconfig pick is the opposite of that. The cost is five
string literals. The one thing that would change the call is if the sweep shows the stock
substitution is already the intended face on every target distro — unlikely, and it would
make the face list a no-op rather than wrong.
The [verify — Linux] items are not forks and do not wait for Daniel. T2's [Daniel]
list included items that a Linux box settles (which SWELL build REAPER ships, whether
fontconfig lands Consolas on a monospace face, whether drag-out is accepted as a copy).
Those are Λ-W3 sweep items. A question is only a fork here if reading code and running a
Linux box both leave it open.
What Phase Λ does not do
- macOS. Λ-D4. Shared-form edits are made and noted as shared; no macOS build, no macOS
verification, no signing or notarization, no
swell-modstub.mm-under-CXX-only investigation, no fix for L2-11's case-insensitive-filesystem defect. - XDG trash. Λ-D5. Hard
unlinkwith a platform-aware confirmation. The XDG spec (~/.local/share/Trashplus a.trashinfosidecar) is a self-contained move-plus-metadata write and remains a clean follow-up if a Linux user ever asks for it — file it todocs/TODO.md, do not smuggle it into Λ-W4-T1. - Route B3b's dependency surface. Λ-D2. No vendored SWELL build, no GDK/GTK3, no
FreeType, no Fontconfig, no OpenGL, no
pkg-configin this tree. If a track finds itself reaching for one of those, the design has drifted and the track stops. - An editor in a non-REAPER host. Λ-D3 requires safe degradation, not capability. Even
with
libSwell.soreachable, a non-REAPER host has no SWELL message loop, so the host-name gate refuses on purpose. - The TCP/MCP embed strip on Linux.
reasampler_embed'sREAPER_FXEMBED_WM_IS_SUPPORTEDkeeps returning 0 off Windows. It is a second 0→1 on a read-only secondary surface, its paint body is LICE and would port, and it becomes genuinely cheap once Λ-W8 lands. Deliberately out, and worth revisiting after. - VST2 / AU / CLAP / standalone. D5's format and host clauses survive Λ-D1 untouched.
- CI. Λ-F1 is open; until it is ruled, Λ ships nothing that presumes a runner.
- Any parameter registration. Γ-W4-T1's, exclusively. See above.
- Windows behaviour changes. Every track carries a "Windows unchanged" criterion. This is a port, and a port that improves Windows by accident has also changed Windows by accident.
Open forks — Daniel's
Λ-F1 — Does CI get built in this phase, and on what runner? (audit D4, unruled)
No recommendation. This one is genuinely a resourcing call, and both sides are defensible enough that a product-designer preference would be noise.
For building it now: versioning-and-release.md already commits to multiple platform
artifacts per channel per release and hands "the pipeline" to dev-ops — and there is no CI
configuration anywhere in the repo, on any platform (.github and .gitlab-ci.yml are both
absent). The moment Linux ships, the tree has two toolchains and one of them is not on the
developer's machine. Every Windows-only commit after that is a Linux regression waiting to
be discovered by a user. A GitHub-Actions job running cmake + ctest on Ubuntu would
catch L2-01-class breakage — an undefined Win32 symbol — at push time, which is exactly the
class of defect this whole phase exists to clear.
For deferring it: CI is infrastructure, not product, and this phase is already the largest thing in the plan. A runner also forces Λ-F4 (the support floor) to be answered immediately, because the runner image is the declared floor. And CI's value is highest after the first Linux build works, not before — a red pipeline on day one teaches nothing the Λ-W3 sweep is not already teaching.
What closing it unblocks: whether Λ-W5-T1 writes a pipeline paragraph or a "deferred to dev-ops" paragraph, and whether Λ-F4 must be closed in this phase or can be carried. Nothing is gated on it before Λ-W5.
Λ-F2 — The dialog-resource route: resource-id-0, or resgen? (audit D7, unruled)
This is the only fork that gates a track. Both audits now carry the evidence.
Route A — the resource-id-0 escape hatch. SWELL documents it in
swell-functions.h:606–608: a CreateDialog creating a child window may pass resource id
0, which creates an opaque child window, provided a WNDPROC returning LRESULT (cast to
DLGPROC) is passed instead of a real DLGPROC. The implementation confirms both halves —
swell-dlg-generic.cpp skips the resource lookup entirely when resid is 0. And
IDD_BANK_PANEL is precisely the case it was written for: src/resource.rc:18–22 is a
WS_CHILD dialog with an empty BEGIN/END body and zero controls, whose own header
comment says "the bank_panel shell owns every pixel and draws the sample grid with LICE in
WM_PAINT". Taking this route deletes the entire resgen pipeline from the non-Windows
builds — the PHP dependency, the generated-file-in-the-source-tree problem, the
per-platform regeneration story, and the three stacked defects in the current commented-out
instructions. Cost: a source change in panel_window.cpp and the DLGPROC→WNDPROC return
contract.
Route B — wire up resgen properly. find_program(PHP …), an add_custom_command
generating into ${CMAKE_CURRENT_BINARY_DIR} (never the source tree — .gitignore has no
pattern for the generated name), a one-line shim TU that includes swell-dlggen.h then the
generated file, and that shim added to target_sources. This is the M end of Λ-01's band.
The asymmetry neither audit stated, and it is the reason this is worth ruling rather than
delegating. L2-07 establishes that SWELL's only opt-in surface for WS_EX_ACCEPTFILES is
the dialog resource — swell_resgen translates a dialog's WS_EX_ACCEPTFILES style
into SWELL_DLG_WS_DROPTARGET, and swell-dlg-generic.cpp sets the ex-style from that flag
at creation time. Under Route A there is no resource, so that bit can never be set from
one. L2-07's own analysis says the bit only suppresses the unhandled-drop
forward-to-parent and that the drop reaches dlgProc anyway via the GDK backend's
ChildWindowFromPoint descent — and notes, correctly, that with zero child controls the
descent has nowhere to land but the panel HWND itself. That reasoning is good but it is
reasoning, not a run. So:
- Route A is cheaper and structurally simpler, and it stakes the panel's file-drop ingest — one of three ingest surfaces — entirely on that unverified descent.
- Route B is more machinery and one more build-time dependency, and it can set the bit explicitly if the descent turns out not to be enough.
What closing it unblocks: Λ-W2-T3's dispatch, and therefore whether Λ-W3 is one Linux session or two. Leaving it open has a real cost and it is the only fork here that does.
A third option, if Daniel wants the cheap route without the exposure: take Route A and treat the file-drop check as a hard gate in Λ-W3's sweep — if the drop does not arrive, Route B becomes a follow-up track rather than a rewrite, because the panel is already rendering by then and the resgen work is additive. That is the option I would take, and it is stated as an option rather than a recommendation because it spends a second Linux session in the bad case.
Λ-F3 — Does "copy-only is structural" survive as a shipped invariant? (T2 [Daniel] 4, unruled)
Recommendation: keep the invariant, scope the enforcement claim to Windows. The
product promise ("dragging a bank file out of ReaSampler copies it; the bank is never
raided by a drop target") stays; the mechanism sentence in drag_out_win.h becomes
"structurally enforced on Windows via DoDragDrop's DROPEFFECT_COPY-only mask;
advertised, not enforced, on SWELL, which takes no effect mask."
The counter-argument is real. An invariant that one platform cannot enforce is arguably not an invariant, and the tree's house style treats invariants as load-bearing rather than aspirational. Weakening the global wording to "copy-only, best effort" would be the honest alternative — and it would also correctly warn a Windows reader not to rely on the guarantee in shared code. What that loses is a genuinely stronger Windows property, stated as weaker than it is.
Non-gating. Λ-W4-T3 makes the edit either way; only the sentence changes. Answerable at implementation review.
Λ-F4 — What is the declared support floor? (new; the audits raised the inputs, not the decision)
The audits surface three separate manifestations of one unmade decision: Λ-07 (no stated
minimum compiler; GCC 8 needs an explicit -lstdc++fs for std::filesystem, GCC ≥ 9 does
not), V8 (the same, as a check), and V10 (which uname -m values the VST3 bundle must
carry — module_linux.cpp keys the bundle directory off exactly that string, and which
architectures REAPER itself ships for Linux is an external fact with no source in this tree).
Recommendation: declare a narrow floor — current-stable-distro glibc, GCC ≥ 9,
x86_64-linux only — and widen it on request. ReaSampler is a per-project capture tool
for people running a commercial DAW; the audience running a five-year-old toolchain on
armv7 is small, and every architecture in the bundle is a build target someone has to
maintain and verify.
The counter: REAPER's own Linux reputation is partly built on running well on modest and
non-x86 hardware, and aarch64 in particular is no longer exotic. Adding it later means a
second bundle directory and a second verification pass on every future release, which is
cheaper to establish once than to retrofit.
What closing it unblocks: Λ-W2-T2's compiler-floor line, Λ-W5-T1's install and README text, Λ-W6-T1's bundle directory set, and Λ-F1's runner image. Non-gating for Λ-W2 (a recorded floor can be widened), gating for Λ-W5 — a shipped artifact has to say what it runs on.
Where the audits disagreed, or left a hole
Recorded so a reader knows which parts of this doc are synthesis rather than citation.
-
The
WS_EX_ACCEPTFILES/ resource-id-0 interaction is a genuine gap between the two audits. T1's Λ-01 recommends the id-0 route and says it makes resgen disappear; T2's L2-07 independently establishes that the drop-accept ex-style bit can only come from the dialog resource. Neither doc composed the two. §Λ-F2 does, and it is the substantive reason that fork is worth ruling rather than delegating to review. -
T2 flags its own severity inconsistency and hands it on. L2-03 is graded Blocker on failure-mode quality with no direct evidence; L2-04 is graded Major with a confirmed mechanism and an equally severe outcome. T2 says explicitly that resolving the axis is a scope call for whoever prioritises the two. This doc dissolves rather than resolves it — both land in Λ-W2, in different tracks — so the grade never has to be adjudicated.
-
The absent-
libSwell.sofailure mode was flagged unverified and is, in fact, readable. Both audits treat it as a[verify — Linux]question. It is not: the vendoredswell-modstub-generic.cppcallsexit(2)on a faileddlopenfrom a static constructor, andexit(1)on an incomplete API table. That is a fact about a file in this tree, and it converts Daniel's Λ-D3 from a defensive precaution into a specific design constraint on the ruled route. Filled here, in §"The non-REAPER-host safety criterion". -
Neither audit swept the VST3 SDK's
samples/tree. Both enumerate the vendored slice the build compiles, which is correct for their questions, butpublic.sdk/samples/vst-hosting/is on disk under the documented narrow submodule init and contains two directly relevant things:validator/(a headless non-REAPER host harness — the right instrument for Λ-D3's acceptance criteria) andeditorhost/source/platform/linux/(a workedLinux::IRunLoopimplementation and an X11-embed window). Named in Λ-W6-T2, Λ-W7-T1 and Λ-W8-T1. This does not move the L band on L2-08 — the audits' estimate stands; it removes a "we are guessing at the contract" risk from it. -
The beta channel on Linux is uncosted in both audits. T1 confirms the channel fork is platform-independent by construction and stops there; neither audit builds, installs, or coexists two channels on Linux, and neither considers the VST3 bundle's per-channel name and UID on that platform. Almost certainly free; assigned to Λ-W5-T1 and Λ-W6-T1 as an explicit criterion rather than an assumption.
-
Two loose ends left as-is, deliberately. T1's
lice_colorspace.cppquestion (SWELL's own Makefile lists a fifth LICE TU our four-TU slice omits; the audit calls it "unreconciled but harmless") and the-Wl,--no-undefinedquestion are both answered by a single successful Linux link. Both are Λ-W3 sweep items, not findings.
Wave map
Phase Λ — ReaSampler on Linux (extension + ReaSampler 9000; macOS out)
W1 The audits [COMPLETE]
T1 build-toolchain-audit ....... Λ-01..Λ-10, V1..V10, D1..D7
T2 source-runtime-audit ........ L2-01..L2-12
W2 Make it buildable, and make it honest [4 tracks, file-disjoint]
T1 linux-compile-blockers ...... L2-01, L2-02, L2-03 -> V1, V2
T2 toolchain-floor ............. Λ-02..Λ-05, Λ-07, Λ-09, Λ-10 -> V5, V6, V8
T3 panel-dialog-resource ....... Λ-01, L2-06, L2-07 -> V7 [GATED: Λ-F2]
T4 locale-independent-numerics . L2-04 [the one track verifiable on Windows]
W3 First light, and the verification sweep [1 track, deliberately]
T1 linux-verification-sweep .... V1..V10 + T2 §5's seven; deliverable is a
record, not a fix. V1 / V9 / the API-load count
run first — each reprices what follows.
W4 Correctness and safety, repriced [3 tracks, disjoint by dir]
T1 prune-deletion-safety ....... L2-05 both halves + the symlink reclaim lie
T2 linux-font-faces ............ L2-09
T3 source-partition-and-invariants . Λ-06, L2-10, L2-11 (Linux half)
W5 Ship the extension [1 track]
T1 linux-packaging-and-install . Λ-08 + Λ-02's shipped half + both channels
W6 The instrument module, and the host-safety contract [T1 before T2 — serial]
T1 vst-linux-module ............ B5 (the D5 reversal, first), B1, B2
T2 vst-host-safety-contract .... Λ-D3's observable contract; passes with zero params
W7 The SWELL bootstrap [1 track]
T1 swell-dylib-bootstrap ....... B3 via route B3a, WITHOUT the stub's exit(2)
[prereq: V9 affirmative, else Λ-D2 re-rules]
W8 The X11 editor [1 track]
T1 x11-embed-view .............. B4, L2-08. The phase's one L-band source item.
W6 may run concurrently with W4 and W5 — file-disjoint; the only reason to serialise
is attention. W2's edits are authored BLIND and their acceptance criteria are
discharged in W3; W2 is not "done" until W3 runs, and the plan says so rather than
pretending otherwise.
Open: Λ-F1 (CI), Λ-F2 (resource route — the only one that gates a track),
Λ-F3 (copy-only invariant wording), Λ-F4 (declared support floor — gates W5).