793 lines
57 KiB
Markdown
793 lines
57 KiB
Markdown
# Λ-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:18–20`, 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:26–31,62–74` builds and
|
||
reads little-endian by shift, not by `memcpy` of a native integer, so it is byte-order-neutral
|
||
by construction. `core/capture/wav_codec.h:52–53` states the one real assumption ("target is
|
||
x86/ARM-LE only, no big-endian byte-swap") and `wav_codec.cpp:165–168` memcpys floats under it —
|
||
correct on x86-64 and aarch64 Linux alike.
|
||
|
||
Sweeps that returned **nothing** across `src/core` **and** `src/app`: `sprintf_s`/`strcpy_s`/
|
||
`_stricmp`/`_snprintf`/`_alloca`/`__forceinline`/`__declspec`/`#pragma comment`/`#pragma warning`/
|
||
`#pragma pack`/`__int64`/`LPSTR`/`LPCSTR`/`LPWSTR`/`wchar_t`/`TCHAR`/`_T(`/`MAX_PATH`. There is
|
||
**no** `#pragma` of any kind anywhere in `src/**` other than `#pragma once`, and no
|
||
`__attribute__` / `__declspec` / anonymous-struct extension.
|
||
|
||
`long` appears in a handful of pure modules (`core/ui/card_meta.cpp:39–41,55`,
|
||
`core/instrument/ui/envelope_overlay.cpp:45`). All are local computations from clamped doubles;
|
||
LP64's wider `long` is strictly safer than LLP64's. One stale comment
|
||
(`core/instrument/ui/envelope_overlay.cpp:19`, "a huge t would overflow a 32-bit long") is
|
||
Windows-specific but the clamp it describes runs regardless. Not a defect.
|
||
|
||
**Verdict: `core/` builds and behaves identically on Linux.** The one conditional
|
||
(`capture_paths.cpp:18`) is *correct* for Linux — case-sensitive paths must not be folded — and
|
||
the pure tests already assert both branches (`tests/test_capture_paths.cpp:19–33,41–51`), which
|
||
is direct evidence the pure layer was written with a non-Windows target in mind.
|
||
|
||
### 1.2 `tests/` — portable.
|
||
|
||
91 test TUs. Only `tests/test_capture_paths.cpp` forks on `_WIN32`, and it asserts the
|
||
non-Windows behaviour explicitly (lines 28–33, 50–51). 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:186–187,243–245`, `test_bake_wire.cpp:77`, `test_drag_out.cpp:291–293,388`,
|
||
`test_origin_ledger.cpp:56`, `test_sample_usage.cpp:502`) — 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:52–53` → `__attribute__((visibility("default")))` and
|
||
`void *`), and `REAPER_PLUGIN_ENTRYPOINT` is platform-neutral (`:56`). No Win32 call, no Win32
|
||
type, no conditional. The one hazard is behavioural, not structural — see **L2-03**.
|
||
|
||
### 1.4 `shell/` — the whole cost sits here, and it is unevenly distributed.
|
||
|
||
65 platform-token lines across 30 of the 293 source files (grep:
|
||
`_WIN32|WIN32|__APPLE__|_MSC_VER|__linux__|__GNUC__|_WIN64` over `src/**/*.{cpp,h}`). Their
|
||
distribution is the story:
|
||
|
||
| Area | 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:156–269`
|
||
wraps the entire paint/input/timer/`wndProc`/`invalidate` family in `#ifdef _WIN32`, and
|
||
`editor_platform.cpp:298–306` stubs only the three IPlugView overrides that must exist. Every
|
||
`invalidate()` call site is inside a guard (verified 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:135–145`). `[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 ships** — `vendor/WDL/WDL/swell/Makefile:100–107,141–151` 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` (`:319–320`), `SetWindowPos` (`:286`).
|
||
- Messages/macros: `GET_X_LPARAM`, `GET_Y_LPARAM`, `HIWORD`, `MAKEINTRESOURCE`
|
||
(`swell-types.h:1220`), `PAINTSTRUCT`, `MSG` (`:262`), `SRCCOPY`, `WM_PAINT`, `WM_MOUSEMOVE`,
|
||
`WM_MOUSEWHEEL`, `WM_CAPTURECHANGED`, `WM_DROPFILES`, `WM_KEYDOWN`, `WM_TIMER`,
|
||
`WM_ERASEBKGND`, `TPM_RETURNCMD`, `MF_*`, `MB_*`, `ID*`, every `VK_*` used.
|
||
- `WM_MOUSEWHEEL` really does carry **screen** coords in `lParam` on the GDK backend — the
|
||
comment at `panel_window.cpp:100–102` 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:90–98`) has a live trigger.
|
||
- Cursors: `LoadCursor`→`SWELL_LoadCursor` (`swell-functions.h:728–730`), `SetCursor`
|
||
(`:740–741`), and **all seven** IDCs the panel uses map to real GDK cursors —
|
||
`IDC_ARROW`→`GDK_LEFT_PTR`, `IDC_HAND`→`GDK_HAND1`, `IDC_UPARROW`→`GDK_CENTER_PTR`,
|
||
`IDC_SIZEWE`→`GDK_RIGHT_SIDE`, `IDC_SIZEALL`→`GDK_FLEUR`, `IDC_IBEAM`→`GDK_XTERM`,
|
||
`IDC_NO`→`GDK_PIRATE` (`swell-generic-gdk.cpp:3736–3748` vs `panel_drag.cpp:141–147,159–161`).
|
||
- `GetAsyncKeyState` returns the `0x8000` high bit for `VK_CONTROL`/`VK_SHIFT`/`VK_MENU`
|
||
(`swell-generic-gdk.cpp:2439–2441`), so `panel_state.h:409–411` 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:3563–3592` 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 `3563–3592` range itself. The headless backend defines
|
||
the **same-named function as an empty no-op stub** —
|
||
`swell-generic-headless.cpp:246–248` 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:179–182`), and `LICE_CachedFont::DrawTextImpl` carries real non-Windows branches
|
||
(`lice_textnew.cpp:236–249,829,1027–1028,1040`).
|
||
|
||
Absent from SWELL, verified by grep over `vendor/WDL/WDL/swell/`: `FF_DONTCARE` (**zero hits
|
||
anywhere in `vendor/WDL/`**), `GetCurrentProcessId` (only `GetCurrentThreadId`,
|
||
`swell-functions.h:822`), `GetKeyState`, `WM_MOUSELEAVE`/`TrackMouseEvent`, `DragAcceptFiles`,
|
||
`RegisterClass*`, `CreateWindowEx*`, `DefWindowProc*`, `MoveWindow`,
|
||
`GetWindowLongPtr`/`SetWindowLongPtr`, `WHEEL_DELTA`, and any move-to-trash surface.
|
||
|
||
---
|
||
|
||
## 2. Findings
|
||
|
||
### L2-01 — `FF_DONTCARE` does not exist off Windows; `draw_kit.cpp` will not compile
|
||
**Location:** `src/shell/panel/draw_kit.cpp:73` (`DEFAULT_PITCH | FF_DONTCARE`), reached on
|
||
non-Windows through `draw_kit.cpp:12–16` (which includes `swell/swell.h`, **not** `windows.h`).
|
||
|
||
**Mechanism.** `swell-types.h` defines `DEFAULT_PITCH`, `DEFAULT_CHARSET`,
|
||
`OUT_DEFAULT_PRECIS`, `CLIP_DEFAULT_PRECIS`, `DEFAULT_QUALITY`, `FW_BOLD`, `FW_NORMAL`,
|
||
`FW_SEMIBOLD`, `TRANSPARENT` and every `DT_*` the kit uses — but **not** `FF_DONTCARE`. A grep
|
||
for `FF_DONTCARE` over the whole of `vendor/WDL/` returns nothing; on Windows it comes from
|
||
`<wingdi.h>` via `windows.h`. `draw_kit.cpp` is not platform-guarded (only its *include* is), so
|
||
the `CreateFont` call is compiled on every platform.
|
||
|
||
**Severity: Blocker** — `error: 'FF_DONTCARE' was not declared in this scope` at
|
||
`draw_kit.cpp:73`; `draw_kit` is linked into both loadable modules, so the extension does not
|
||
build at all.
|
||
**Effort: S** — the argument is `DEFAULT_PITCH | FF_DONTCARE`, and `FF_DONTCARE` is 0x00 in
|
||
wingdi.h; the family bits are advisory to Windows' font mapper and meaningless to fontconfig.
|
||
**Direction.** Drop the `| FF_DONTCARE` term, or define it locally in the non-Windows include
|
||
branch. Do not add `windows.h`.
|
||
|
||
### L2-02 — `GetCurrentProcessId()` is called with no platform branch and SWELL does not export it
|
||
**Location:** `src/shell/actions/instrument_drop_win.cpp:59` (temp `.vstpreset` filename). The TU
|
||
has **no** `_WIN32` conditional anywhere (verified: its only `#include`s are `<atomic> <cstdint>
|
||
<filesystem> <fstream> <string> <system_error> <vector>` plus project/SDK headers, lines 6–27).
|
||
|
||
**Mechanism.** On non-Windows the declaration would have to come from `reaper_plugin.h` →
|
||
`swell.h`. `swell-functions.h` declares `GetCurrentThreadId` (`:822`) and no
|
||
`GetCurrentProcessId`; the only two occurrences in the whole WDL tree are inside
|
||
`WDL/shm_msgreply.cpp:26` and `WDL/win32_utf8.c:244`, neither of which is a SWELL export.
|
||
|
||
**Severity: Blocker** — `error: 'GetCurrentProcessId' was not declared in this scope`; the
|
||
extension does not build.
|
||
**Effort: S** — the PID exists only to keep two concurrent REAPER instances from colliding in
|
||
the shared temp dir (comment at `:56–57`). `getpid()` behind a guard, or the already-imported
|
||
`GetCurrentThreadId()` plus the existing atomic counter, satisfies the same requirement.
|
||
**Direction.** Replace with a platform-neutral uniqueness source; the atomic counter at `:52`
|
||
already carries the intra-process half.
|
||
|
||
### L2-03 — `REAPERAPI_LoadAPI` is all-or-nothing over ~869 API names and fails silently
|
||
**Location:** `src/app/main.cpp:292–293` (`if (REAPERAPI_LoadAPI(rec->GetFunc) != 0) return 0;`).
|
||
`main.cpp` does **not** define `REAPERAPI_MINIMAL`, so the full table is loaded
|
||
(`vendor/reaper-sdk/sdk/reaper_plugin_functions.h:44–48`; 1738 `REAPERAPI_WANT_` guards ≈ 869
|
||
entries).
|
||
|
||
**Mechanism.** The SDK's loader accumulates one `failcnt` across the entire table
|
||
(`reaper_plugin_functions.h`, `REAPERAPI_LoadAPI` body: `failcnt += !(*table[i].dest =
|
||
getAPI(table[i].name)); return failcnt;`). If a Linux REAPER build does not export *any single
|
||
one* of those 869 names, `main.cpp` returns 0 and the extension never loads — with **no console
|
||
message, no log line, nothing**. The SDK header carries no "Windows only" annotation on any
|
||
entry (grep for `windows only|win32 only|not on mac|not on linux` returns nothing), so nothing
|
||
here says a gap exists — but nothing rules it out either, and the failure mode is maximally
|
||
opaque.
|
||
|
||
**Severity: Blocker (conditional)** `[verify — Linux]` — a silent load refusal with no
|
||
diagnostic is the worst possible first-run experience for a port. **Scope note:** the
|
||
all-or-nothing mechanism itself is identical on Windows too
|
||
(`reaper_plugin_functions.h:45–46` — "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:26–31`, `panel_audition.cpp:12–16`) 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:26–31`, `panel_audition.cpp:12–16`), or keep
|
||
the full load but print the failure count via `rec->GetFunc("ShowConsoleMsg")` before returning
|
||
0. The minimal list is also the honest inventory of what this extension actually needs.
|
||
|
||
### L2-04 — every persisted floating-point number is `LC_NUMERIC`-dependent, in both directions
|
||
**Location (writers):** `src/core/json/json.cpp:39–43` (`%.17g` — the bank index, view model and
|
||
tracking ledger all serialize through it), `src/core/model/provenance.cpp:40` (`%.17g` — the
|
||
provenance blob inside the bank JSON). 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:78–79`, which already goes through `json::numToStr` (the same
|
||
`json.cpp:39–43` codec cited above), so it is the same writer, not a third one.
|
||
**Location (readers):** `src/core/json/json.cpp:190–196` (`std::strtod`),
|
||
`src/core/wire/wire.cpp:121–130` (`Cursor::fieldDouble`, `std::strtod`),
|
||
`src/core/capture/render_settings.cpp:178–186` (`std::stod` over REAPER's own `P_RAZOREDITS`).
|
||
|
||
**Mechanism.** `snprintf("%.17g")`, `strtod` and `stod` all honour `LC_NUMERIC`. Under a
|
||
comma-decimal locale the writers emit `1,5`, which makes the bank JSON *structurally invalid*
|
||
(an extra separator inside an object) — the whole index fails to parse on the next load. The
|
||
readers are honestly fail-closed (all three require whole-token consumption:
|
||
`json.cpp:194`, `wire.cpp:127`, `render_settings.cpp: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:167–209`; the non-Windows branch is `:200–208`.
|
||
|
||
**Mechanism.** Two Windows properties are load-bearing for the deletion authority and neither
|
||
survives. (a) The Windows path routes through `SHFileOperationW` + `FOF_ALLOWUNDO` (`:186–188`)
|
||
— deletions are recoverable from the Recycle Bin; the `#else` is a hard `fs::remove` (`:203`).
|
||
The header already states this honestly (`:30–34`, `:162–166`). (b) Less obviously, the failure
|
||
taxonomy at `:205` (`if (ec) return false; // real failure (locked/permission) -> skip`) encodes
|
||
**Windows** file-sharing semantics: a bank file currently open by REAPER's audio engine (an
|
||
active `PCM_source`, an item playing from it) cannot be deleted on Windows, so it is counted as
|
||
"skipped". On Linux `unlink()` on an open file succeeds — the directory entry vanishes while the
|
||
open fd keeps playing, and when the fd closes the bytes are gone with no trash to recover from.
|
||
So the Linux prune can delete a file that is *audibly in use*, silently, and the user's only
|
||
recovery floor (the superseded-file-survives-until-prune rule, root `CLAUDE.md` §resample bake)
|
||
now has nothing under it.
|
||
|
||
**Severity: Major** — irreversible user-data loss where the Windows build is recoverable; the
|
||
"locked" skip branch is dead code on Linux.
|
||
**Effort: M** — trash is genuinely non-portable, but the XDG trash spec (`~/.local/share/Trash`
|
||
with a `.trashinfo` sidecar) is a self-contained move-plus-metadata write, and the confirm gate
|
||
already exists upstream.
|
||
**Direction.** Either implement an XDG-trash move in the `#else` (preferred — the deletion
|
||
authority is a single ~40-line function and this is exactly where the platform seam belongs), or
|
||
make the prune confirmation text platform-aware so a Linux user is told the deletion is
|
||
permanent. Do not leave the current silent asymmetry.
|
||
|
||
### L2-06 — the docked panel never opens on Linux, and fails silently when it doesn't
|
||
**Location:** `src/shell/panel/panel_window.cpp:135–137`.
|
||
|
||
**Mechanism.** `CreateDialogParam(g_hInst, MAKEINTRESOURCE(IDD_BANK_PANEL), …)` maps on SWELL to
|
||
`SWELL_CreateDialog(SWELL_curmodule_dialogresource_head, (resid), …)`
|
||
(`swell-functions.h:616,619`) — it resolves the template out of a per-module registry populated
|
||
by the **resgen-generated source**, not out of a linked `.rc`. That generated source is not
|
||
currently part of the Linux target (the `target_sources` line is commented out;
|
||
build-system detail → §7). The source-side consequence is what matters here: `SWELL_CreateDialog`
|
||
returns `nullptr`, `panel_window.cpp:137` does `if (!g_panel.hwnd) return;`, and the toggle
|
||
action is a **silent no-op** — no console line, no message box, and `bankPanelIsOpen()` keeps
|
||
reporting false so the Actions-list checkmark never lights. The user's only symptom is "the
|
||
button does nothing." Note also that `MAKEINTRESOURCE` becomes `((const char*)(UINT_PTR)(x))` on
|
||
SWELL (`swell-types.h:1220`), so the id is a pointer-shaped integer — the resgen output must
|
||
agree on `IDD_BANK_PANEL == 1000` (`src/resource.h:8`).
|
||
|
||
**Severity: Major** — the extension loads, every action works, and the primary UI surface is
|
||
absent with no diagnostic.
|
||
**Effort: S** on the source side (one failure branch), separate from T1's resgen wiring.
|
||
**Direction.** Add a one-line `ShowConsoleMsg` on the `!g_panel.hwnd` path naming the missing
|
||
dialog resource. That single line converts a mystery into a two-minute diagnosis and is worth
|
||
having on Windows too.
|
||
|
||
### L2-07 — file-drop ingest onto the panel has no opt-in on SWELL
|
||
**Location:** `src/shell/panel/panel_window.cpp:145–150` (the `DragAcceptFiles` call is
|
||
`#ifdef _WIN32`), handler at `:47–61,65–67`.
|
||
|
||
**Mechanism.** The comment at `:145–147` is accurate — SWELL exposes no `DragAcceptFiles`
|
||
(grep over `vendor/WDL/WDL/swell/` finds only `DragQueryFile` and `DragFinish`,
|
||
`swell-functions.h:1006–1007`). What the comment does not say is *how* a drop would arrive
|
||
instead. Reading the GDK backend: on a URI-list selection-notify, SWELL resolves the top-level
|
||
window, walks down with `ChildWindowFromPoint` (which **is** descending — `for(;;)` loop,
|
||
`swell-wnd-generic.cpp:6956–6981`), and `SendMessage(cw, WM_DROPFILES, (WPARAM)gobj, 0)`
|
||
(`swell-generic-gdk.cpp:1622`). The default child proc forwards 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:320–321` 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:21–22`
|
||
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", `:7725–7728`) while SWELL itself `GlobalFree`s the handle right after
|
||
`SendMessage` (`swell-generic-gdk.cpp:1623–1624`) — so `panel_window.cpp:59` calling
|
||
`DragFinish` is safe, not a double-free.
|
||
|
||
**Severity: Major `[verify — Linux]`** — if the routing does *not* reach a docked child dialog,
|
||
one of the three ingest surfaces (file drop onto the bank panel) is silently dead with no error.
|
||
The evidence above says it probably works; it is not proof, 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:37–42` (`isPlatformTypeSupported`
|
||
returns `kResultTrue` **only** for `kPlatformTypeHWND`, and only inside `#ifdef _WIN32`);
|
||
`:298–306` (the non-Windows stubs); `reasampler_editor.h:156–269` (the entire paint/input family
|
||
is `#ifdef _WIN32`); `reasampler_embed.cpp:135–141` (`REAPER_FXEMBED_WM_IS_SUPPORTED` returns 0
|
||
off Windows). `reasampler_processor.cpp:360–365` still hands the host a `ReaSamplerEditor`.
|
||
|
||
**Mechanism.** The VST3 SDK *does* support Linux — `kPlatformTypeX11EmbedWindowID`
|
||
(`vendor/vst3sdk/pluginterfaces/gui/iplugview.h:79`), `Linux::IRunLoop` with
|
||
`registerEventHandler`/`registerTimer` (`:267–279`), `Linux::IEventHandler` (`: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-set` — `docs/product/parameter-automation.md` — is landing the parameter set
|
||
concurrently, which materially improves this fallback; the *editor* gap is unaffected.)
|
||
|
||
The specific Win32 dependencies a Linux editor would have to replace, each verified absent from
|
||
SWELL: `RegisterClassW`/`CreateWindowExW`/`DefWindowProcW` (`editor_platform.cpp:79–103,294`) —
|
||
SWELL has no window-class model at all, only `SWELL_CreateDialog` and raw `HWND__` construction;
|
||
`MoveWindow` (`:133`) — SWELL has `SetWindowPos` (`swell-functions.h:286`) instead;
|
||
`GetWindowLongPtr`/`SetWindowLongPtr` (`:74,105,142`) — SWELL has the non-`Ptr` forms returning
|
||
`LONG_PTR` (`:319–320`); `TrackMouseEvent`/`WM_MOUSELEAVE` (`:176–198`) — SWELL has neither, a
|
||
gap the panel layer already documents at `panel_state.h:245–246`; `GetKeyState`
|
||
(`editor_input_waveform.cpp:38`, `editor_input_curve.cpp:50`) — SWELL has only
|
||
`GetAsyncKeyState` (`swell-functions.h:712`); `DragAcceptFiles`/`DragQueryFileW`
|
||
(`:109,283`); `GetModuleHandle` (`:75`).
|
||
|
||
**Severity: Major (instrument only)** — the plugin would load and process audio; it would be
|
||
unplayable and uneditable.
|
||
**Effort: L** — an X11-embed `IPlugView` (`attachedToParent` receiving an X11 window id rather
|
||
than an HWND), an `IRunLoop`-driven timer replacing `SetTimer`/`WM_TIMER`, an event-driven
|
||
input path replacing the `wndProc` switch, and a LICE surface bound to that window. This is a
|
||
new competence, not a port of the existing one.
|
||
**Direction.** Treat as a separate, later decision from the extension (see §3). If it is ever
|
||
taken, the natural shape is a small platform seam under `shell/instrument/` — `editor_platform`
|
||
already *is* that seam; it needs a sibling, not a rewrite.
|
||
|
||
### L2-09 — the draw kit's font faces do not exist on Linux and substitute silently
|
||
**Location:** `src/shell/panel/draw_kit.cpp:70–77` (`loadFont`), and the two literal faces it is
|
||
called with — Segoe UI and Consolas, per `draw_kit.h:63–64`.
|
||
|
||
**Mechanism.** On Linux `CreateFont` goes through fontconfig when built with
|
||
`-DSWELL_FONTCONFIG` (gated per the `[verify — Linux]` note in §1.5):
|
||
`FcPatternAddString(pat, FC_FAMILY, lfFaceName)` → `FcConfigSubstitute`/`FcDefaultSubstitute` →
|
||
`FcFontMatch` (`swell-gdi-lice.cpp:450–487`). 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:480–484` 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 `:493–539` compiles only in the `#else`
|
||
(`#ifndef SWELL_FONTCONFIG`) branch. However `swell-gdi-lice.cpp:400–401,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:236–249,829,1027–1028,1040`), not from `loadFont`'s
|
||
stated guard — `[verify — Linux]`. The two WCAG `static_assert`s (`draw_kit.cpp:53–54`) 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:154–158`,
|
||
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:154–158`). 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:505–507`), a reasonable precedent to copy.
|
||
This same change fixes macOS (San Francisco / Menlo), one code path.
|
||
|
||
### L2-10 — OS drag-out on SWELL loses the copy-only mask, the readiness probe, and the outcome
|
||
**Location:** `src/shell/actions/drag_out_win.cpp:249–282` (the `#else` branch);
|
||
`drag_out_win.h:7–11,29–33` already states the first and third honestly.
|
||
|
||
**Mechanism.** Three separate degradations, all real but none fatal. (a) `DoDragDrop`'s
|
||
`DROPEFFECT_COPY`-only mask (`:230`) is a **structural** guarantee that no target can MOVE a
|
||
bank file out of the folder; `SWELL_InitiateDragDropOfFileList` takes no effect mask
|
||
(`swell-functions.h:1011`), so the guarantee reduces to whatever the GDK drag advertises.
|
||
(b) `canInitiateDragOut` degrades to `!paths.empty()` (`:276–278`), so the caller's careful
|
||
coupling — "do not tear down the internal drag until the OS is known ready"
|
||
(`panel_drag.cpp:227–244`) — has nothing to check against. (c) `initiateDragOut` returns `true`
|
||
unconditionally (`:270`), so the advisory success return is meaningless. The caller ignores it,
|
||
so (c) is inert today. Note the call path is already Linux-correct in one important respect:
|
||
`handOffToOs` releases capture and resets drag state (`panel_drag.cpp:240–241`) *before*
|
||
invoking the drag, which matters because SWELL's implementation takes capture on its own hidden
|
||
window and spins a nested `SWELL_RunMessageLoop` until capture drops
|
||
(`swell-generic-gdk.cpp:3575–3584`, inside `SWELL_InitiateDragDropOfFileList` — the function
|
||
`drag_out_win.cpp:270` actually calls; the body is identical to the sibling
|
||
`SWELL_InitiateDragDrop` at `:3531–3560`, 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:7–11`) is the doc a Linux reviewer is pointed at, and treat "MOVE is
|
||
structurally impossible" as a Windows-only claim in any future spec text.
|
||
|
||
### L2-11 — `normalizeSlashes` case-folds on `_WIN32` only, which is right for Linux and wrong for macOS
|
||
**Location:** `src/core/capture/capture_paths.cpp:18–20`.
|
||
|
||
**Mechanism.** The one platform conditional in `core/`. Linux filesystems are case-sensitive, so
|
||
*not* folding is correct and the existing tests already assert it
|
||
(`tests/test_capture_paths.cpp:50–51`). The shared-path note: macOS's default APFS/HFS+ is
|
||
case-**insensitive**, so the same `#ifdef _WIN32` under-folds there — a pre-existing macOS
|
||
defect this audit surfaces but does not own.
|
||
|
||
**Severity: Minor** (no Linux defect). **Effort: S** — confirmed: the one platform conditional
|
||
in `core/` is a single `#ifdef _WIN32` block (`capture_paths.cpp:18–20`) 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:123–128`, using `u8string()` — is harmless on Linux (C++17, so
|
||
`u8string()` returns `std::string`; `CMakeLists.txt:28` pins `CMAKE_CXX_STANDARD 17`, and under
|
||
C++20 the `.c_str()` at `:128` would become a `const char8_t*` type error — worth knowing before
|
||
anyone bumps the standard).
|
||
|
||
**Severity: Minor / informational.** **Effort: none.** **Direction:** no action; do not "fix" it.
|
||
|
||
---
|
||
|
||
## Surfaces checked and found clean
|
||
|
||
Recorded so the sweep's negative results are as auditable as its findings.
|
||
|
||
- **Missing standard includes** (the MSVC-transitively-provides class). Ran a use-vs-include diff
|
||
across all 293 files for `<cstring> <cstdio> <memory> <atomic> <algorithm> <limits> <cmath>
|
||
<functional> <ctime> <cstdlib>`. Five raw hits; all five verified false positives on read —
|
||
four were the symbol name appearing in a *comment* (`render_settings.h:53`,
|
||
`ext_state_read.h:20`, `action_registry.h:7`) or a correct `<cmath>` `std::abs`
|
||
(`velocity_curve.cpp:229`, `<cmath>` at `:6`), 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:36–41,58–63` and
|
||
`panel_state.h:20–24` match the SDK's own fork exactly (`CRITICAL_SECTION cs` on `_WIN32`,
|
||
`pthread_mutex_t mutex` otherwise — `vendor/reaper-sdk/sdk/reaper_plugin.h:1308–1312`).
|
||
`<pthread.h>` is included on the non-Windows branch. Correct.
|
||
- **Prompts and message boxes.** Every user prompt goes through REAPER's own
|
||
`GetUserInputs`/`ShowMessageBox`/`ShowConsoleMsg` (`panel_bank_ops.cpp:24–25,81,100,126,381`),
|
||
never a Win32 `MessageBox`. Cross-platform by construction.
|
||
- **Menus.** `CreatePopupMenu`/`InsertMenu`/`TrackPopupMenu(TPM_RETURNCMD)`/`DestroyMenu` are all
|
||
SWELL-provided (`swell-functions.h:528–532`, and `TrackPopupMenu`/`DestroyMenu`/
|
||
`CreatePopupMenu` entries); the "pos < 0 appends" assumption at `panel_bank_ops.cpp:226` matches
|
||
SWELL's `SWELL_InsertMenu`. The comment at `:220–222` is accurate.
|
||
- **Keyboard/accelerator path.** `accelerator_register_t` + `MSG`/`WM_KEYDOWN` + `GetFocus` +
|
||
`GetParent` (`panel_input.cpp:503–521`) 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:139–296`), hover-leave detection without
|
||
`TrackMouseEvent`, modifier reads via `GetAsyncKeyState` instead of `GetKeyState`, and drop-accept
|
||
without `DragAcceptFiles`. The paint side is the cheapest part — every painter already draws into
|
||
a `LICE_IBitmap` through the shared kit (`editor_paint.cpp:28,51`), so the drawing survives a
|
||
window-system change intact; it is the *window and event plumbing* that is entirely absent. The
|
||
TCP/MCP embed strip (`reasampler_embed.cpp:135–141`) is a smaller, separate 0→1
|
||
(`REAPER_FXEMBED_WM_IS_SUPPORTED` currently returns 0 off Windows; the paint body at `:174+` is
|
||
LICE and would port).
|
||
|
||
**Estimate shape:** one L, and it is a genuinely new competence rather than a port.
|
||
|
||
### 3.3 The consequence for sequencing
|
||
|
||
The extension is a Linux target that is largely *already met* and needs a handful of small,
|
||
well-localized fixes. The instrument is a separate project. The two share exactly one file —
|
||
`shell/panel/draw_kit` — and that file's only Linux blocker is L2-01. Daniel rules on whether the
|
||
instrument is in scope at all; the evidence says the extension does not wait on it.
|
||
|
||
---
|
||
|
||
## 4. Feature-degradation list — what builds but behaves differently, or not at all
|
||
|
||
Blunt column: **GONE** = the capability does not exist on Linux; **DIFFERENT** = it exists with
|
||
changed behaviour; **AT RISK** = depends on an unverified assumption.
|
||
|
||
| Feature | Verdict | What actually happens | Cite |
|
||
|---|---|---|---|
|
||
| Prune → Recycle Bin | **GONE** | Hard `unlink`, unrecoverable | `prune_fs.cpp:200–208` |
|
||
| Prune's "file is locked" skip | **GONE** | `unlink` on an in-use file succeeds; the skip branch is dead code | `prune_fs.cpp:205` |
|
||
| Docked bank panel | **GONE (today)** | `SWELL_CreateDialog` returns null; toggle is a silent no-op until the resgen source is added (§7) | `panel_window.cpp:135–137` |
|
||
| VST3 editor | **GONE** | `isPlatformTypeSupported` false for every type; host shows a generic UI | `editor_platform.cpp:37–42` |
|
||
| VST3 generic-UI fallback | **GONE (today)** | Zero parameters registered as of this base; Phase Γ-W4-T1 changes this | grep `addParameter` over `shell/instrument/*.cpp` → no hits |
|
||
| TCP/MCP embed strip | **GONE** | `REAPER_FXEMBED_WM_IS_SUPPORTED` returns 0 | `reasampler_embed.cpp:135–141` |
|
||
| Kit fonts (Segoe UI / Consolas) | **DIFFERENT** | fontconfig substitutes silently; metrics and ellipsis points shift; ValueMono may lose tabular alignment | `swell-gdi-lice.cpp:450–487` |
|
||
| OS drag-out copy-only guarantee | **DIFFERENT** | No effect mask; copy-only is conventional not structural | `drag_out_win.cpp:257–270` |
|
||
| OS drag-out readiness/outcome | **DIFFERENT** | Probe degrades to "non-empty"; return is always `true`; 500 ms no-motion timeout has no Windows analog | `drag_out_win.cpp:276–278`, `swell-generic-gdk.cpp:3579–3580` |
|
||
| Panel file-drop ingest | **AT RISK** | Depends on SWELL delivering `WM_DROPFILES` without the Win32 opt-in | `panel_window.cpp:145–150` |
|
||
| Bank index float round-trip | **AT RISK** | Unparseable under a non-C `LC_NUMERIC` | `json.cpp:39–43,190–196` |
|
||
| Extension load | **AT RISK** | Any one unresolved API name in ~869 = silent refusal | `main.cpp:292–293` |
|
||
| Path case sensitivity | **DIFFERENT (correct)** | No case-fold; correct for Linux, and the tests already assert it | `capture_paths.cpp:18–20` |
|
||
| 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:113–124` |
|
||
| Audition / preview | **INTACT** | `preview_register_t` fork matches the SDK; `PlayPreview`/`StopPreview` are REAPER API | `panel_audition.cpp:36–63` |
|
||
| Docking, menus, cursors, keyboard, wheel, capture rollback, tooltips | **INTACT** | All SWELL-provided; verified call-by-call | §1.5 |
|
||
| Capture pillar (offline + realtime), persist, tracking, prune *computation* | **INTACT** | Pure `core/` + REAPER API only | §1.1 |
|
||
|
||
---
|
||
|
||
## 5. Open questions
|
||
|
||
### `[verify — Linux]` — answerable only on a Linux box
|
||
|
||
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:3579–3580`) 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 1–8 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:79–82` 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 |
|