docs(product): S17 load-capture seam — settle on (B) component-state injection

Fold the decision into the existing phase-s S17 spec: (B) via
TrackFX_SetNamedConfigParm vst_chunk; (A) ext-state handshake rejected.
Flag the shared component-state blob format as an extension<->instrument
coordination dependency.
This commit is contained in:
2026-07-26 23:09:28 -04:00
parent 725f3e7d3c
commit 37cb290cbc
2 changed files with 72 additions and 47 deletions
+43 -32
View File
@@ -1913,27 +1913,40 @@ reads bank index + mapping from `"reasampler"` ext-state via the bridge — §Th
but **no entry point for an external actor to say "this fresh instance should play *this
specific* capture."** Reading the bank is not the same as being *pointed at one sample*.
This wave is the reason to add that seam, and the seam lands **inside the instrument**
(the `phase-s` artifact), not the extension. Two candidate mechanisms — **the choice is a
Phase S open question:**
- **(A) Fresh-instance ext-state handshake.** The extension writes a small "pending load"
hint into `"reasampler"` ext-state keyed to the target track/FX (e.g. the capture id +
a target GUID); a freshly-instantiated ReaSampler 9000 reads it on init via the same
bridge it already uses, claims + clears the hint, and self-selects that capture. **Pro:**
reuses the existing bridge; no new VST3 surface. **Con:** a cross-process handshake with
a claim/clear race to get right; "which instance claims which hint" needs a stable key.
- **(B) VST3 `setState` / preset injection.** The extension builds the instance's
component state (the same blob `getState`/`setState` round-trips) with the capture
pre-selected and injects it right after `TrackFX_AddByName`. **Pro:** deterministic, no
shared-state race, uses the instrument's own persistence format. **Con:** the extension
must know and construct the instrument's state blob format — a tighter cross-artifact
coupling to a format that is itself still being built in Phase S; verify whether the
extension can set an added FX's state through the REAPER API (candidate:
`TrackFX_SetNamedConfigParm` / a state-set path — **not yet confirmed against
`reaper_plugin_functions.h`**).
(the `phase-s` artifact), not the extension.
Lean: **(A)** keeps the artifacts loosely coupled through the one ext-state seam they
already share and avoids the extension hard-coding the instrument's state format — but
it inherits the claim/clear race. Daniel to decide (open question below).
**Mechanism (SETTLED — (B) VST3 component-state injection).** Right after
`TrackFX_AddByName` returns the new FX index, the extension writes the instance's component
state directly — the same blob the instrument's `getChunk`/`setChunk` round-trips — with
the target capture pre-selected. Deterministic, no shared-state race, no cross-process
handshake; it uses the instrument's own persistence format. The state-set path is
`TrackFX_SetNamedConfigParm` — **verified present** in `reaper_plugin_functions.h`: `bool
TrackFX_SetNamedConfigParm(MediaTrack* track, int fx, const char* parmname, const char*
value)`, and the header documents the write-parms `vst_chunk` / `vst_chunk_program` as the
base64-encoded VST-specific chunk. So the injection call is
`TrackFX_SetNamedConfigParm(track, fx, "vst_chunk", <base64 blob>)`.
**Load-bearing caveat — `vst_chunk` is the plugin's own serialized chunk.** `vst_chunk` is
ReaSampler 9000's **own** base64-encoded serialized state (its `getChunk`/`setChunk`
FXP/FXB-style blob), **not** a raw VST3 `IComponent::setState` stream that REAPER
re-marshals into the plugin. The extension therefore has to construct **exactly the
instrument's own state-blob bytes** with the capture pre-selected — REAPER does not
translate a neutral state representation on its behalf. This makes the **component-state
blob format a shared cross-artifact contract** — one that is **still being defined in Phase
S** — and a **coordination dependency between the extension and the instrument:** both must
agree on the exact byte layout that ReaSampler 9000's `setChunk` accepts before either half
is final. The load-capture seam and the component-state persistence work (§Where the toggle
lives / component-state version bumps) share this one blob format.
**Rejected alternative — (A) fresh-instance ext-state handshake.** The extension writes a
small "pending load" hint into `"reasampler"` ext-state keyed to the target track/FX (a
capture id + a target GUID); a freshly-instantiated ReaSampler 9000 reads it on init via
the bridge it already uses, claims + clears the hint, and self-selects that capture. It
would keep the artifacts loosely coupled through the one ext-state seam they already share
and avoid the extension hard-coding the instrument's state format — but it **loses on the
claim/clear race:** "which instance claims which hint" needs a stable key and a
cross-process handshake to get right, and (B) sidesteps that entirely by writing the state
directly and deterministically.
**Coexistence with the OS drag-out (disambiguation contract).** The two OS-vs-internal
modes are disambiguated **by pointer location, not a mode toggle** — the user never picks
@@ -1962,20 +1975,12 @@ a multi-capture drag over the FX button is either rejected or loads the first).
predicate; the M11 `drag_out` unit tests must not regress.
**Open questions (Daniel / Phase S team to decide).**
- **Load-capture seam mechanism (A vs. B above).** The single load-bearing Phase S design
choice this wave forces. Lean is (A) for loose coupling; needs Daniel's call.
- **Multi-capture drag over an FX button** — reject (only single-capture drags arm
`InstrumentDrop`), or load the first / a keymap of all? Tier-0 leans reject-or-first;
a multi-capture keymap load is a Tier-1 stretch.
- **FX-button hotspot vs. whole TCP.** Does the drop zone have to be the FX button
specifically, or is dropping anywhere on the target track's TCP enough (simpler hit
resolution, arguably clearer target)? Depends on what the SDK exposes (see must-verify).
- **Numbering vs. the `phase-s` worktree (reconciled on merge to dev).** Authored on dev
as a provisional S7; renumbered to **S17** on merge, since the worktree's authoritative
Phase S set (S7 stereo, S8 ingest, S9 change-detection, S10S16) took the lower labels.
Drop-and-load stays a distinct integration gesture (drop onto a track's FX button) —
a sibling of but not the same as S8 (ingest into the bank) and S13 (drop-to-load inside
the editor). See PLAN.md §S17.
**Must-verify before build (drop-and-load).**
- `TrackFX_AddByName` — **verified present** (`reaper_plugin_functions.h`): signature and
@@ -1985,10 +1990,16 @@ a multi-capture drag over the FX button is either rejected or loads the first).
`reaper_plugin_functions.h`); whether the FX button specifically is addressable vs. the
TCP as a whole is an open verification that also decides the "hotspot vs. whole TCP"
question.
- **Instance state injection (only if seam mechanism (B) is chosen)** — whether the
extension can set a just-added FX's state via the REAPER API
(`TrackFX_SetNamedConfigParm` or similar) — **not yet confirmed against
`reaper_plugin_functions.h`.** Moot if (A) is chosen.
- **Instance state injection (seam mechanism (B) — SETTLED, load-bearing prerequisite)** —
**verified present** in `reaper_plugin_functions.h`: `bool
TrackFX_SetNamedConfigParm(MediaTrack* track, int fx, const char* parmname, const char*
value)`, with the header documenting `vst_chunk` / `vst_chunk_program` as the
base64-encoded VST-specific chunk write-parms. The injection call is
`TrackFX_SetNamedConfigParm(track, fx, "vst_chunk", <base64 blob>)`. The remaining
prerequisite is **not** the API but the **shared component-state blob format**: `vst_chunk`
carries the instrument's *own* serialized chunk (its `setChunk` input), so the extension
must construct exactly ReaSampler 9000's state bytes — the cross-artifact contract still
being defined in Phase S. Blocks the drop half until the blob format is agreed.
## Non-goals / guardrails