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 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*. 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** 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 (the `phase-s` artifact), not the extension.
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`**).
Lean: **(A)** keeps the artifacts loosely coupled through the one ext-state seam they **Mechanism (SETTLED — (B) VST3 component-state injection).** Right after
already share and avoids the extension hard-coding the instrument's state format — but `TrackFX_AddByName` returns the new FX index, the extension writes the instance's component
it inherits the claim/clear race. Daniel to decide (open question below). 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 **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 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. predicate; the M11 `drag_out` unit tests must not regress.
**Open questions (Daniel / Phase S team to decide).** **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 - **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; `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. 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 - **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 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). 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).** **Must-verify before build (drop-and-load).**
- `TrackFX_AddByName` — **verified present** (`reaper_plugin_functions.h`): signature and - `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 `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" TCP as a whole is an open verification that also decides the "hotspot vs. whole TCP"
question. question.
- **Instance state injection (only if seam mechanism (B) is chosen)** — whether the - **Instance state injection (seam mechanism (B) — SETTLED, load-bearing prerequisite)** —
extension can set a just-added FX's state via the REAPER API **verified present** in `reaper_plugin_functions.h`: `bool
(`TrackFX_SetNamedConfigParm` or similar) — **not yet confirmed against TrackFX_SetNamedConfigParm(MediaTrack* track, int fx, const char* parmname, const char*
`reaper_plugin_functions.h`.** Moot if (A) is chosen. 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 ## Non-goals / guardrails
+29 -15
View File
@@ -956,9 +956,25 @@ The current Phase S spec gives the instrument a *live-state read* seam (it reads
index + mapping from `"reasampler"` ext-state via the bridge) but **no entry point for an index + mapping from `"reasampler"` ext-state via the bridge) but **no entry point for an
external actor to say "instantiate playing *this specific* capture."** This wave is the external actor to say "instantiate playing *this specific* capture."** This wave is the
reason to add that seam. It is a Phase S dependency, not extension-side, and must land in reason to add that seam. It is a Phase S dependency, not extension-side, and must land in
the instrument before drop-and-load's drop half can work end-to-end. See CONTEXT.md the instrument before drop-and-load's drop half can work end-to-end. **Mechanism (SETTLED
§Phase S for the two candidate seam mechanisms (fresh-instance ext-state handshake vs. — (B) VST3 component-state injection):** after `TrackFX_AddByName`, the extension writes the
VST3 `setState` preset injection) and the open question on which is chosen. new instance's component state directly via `TrackFX_SetNamedConfigParm(track, fx,
"vst_chunk", <blob>)` — **verified present** in `reaper_plugin_functions.h` (`bool
TrackFX_SetNamedConfigParm(MediaTrack*, int fx, const char* parmname, const char* value)`;
the header documents the `vst_chunk`/`vst_chunk_program` parms as the base64-encoded
VST-specific chunk write-path). Deterministic, no shared-state race, no cross-process
handshake. **Load-bearing caveat:** `vst_chunk` is the plugin's **own** base64-encoded
serialized chunk (its `getChunk`/`setChunk` FXP/FXB-style blob), **not** a raw VST3
`IComponent::setState` stream REAPER re-marshals — so the extension must construct exactly
ReaSampler 9000's own state-blob bytes with the target capture pre-selected. That makes the
**component-state blob format a shared cross-artifact contract** — still being defined in
Phase S — and a coordination dependency between the extension and the instrument: both must
agree on the exact byte layout the instrument's `setChunk` accepts. Rejected alternative —
**(A) fresh-instance ext-state handshake** (extension writes a "pending load" hint into
`"reasampler"` ext-state, the fresh instance claims + clears it on init): loosely coupled
through the existing bridge, but loses on the claim/clear race — "which instance claims
which hint" needs a stable key and a cross-process handshake to get right. See CONTEXT.md
§Phase S (drop-and-load) for the full seam decision.
**Coexistence with the M11 OS drag-out (disambiguation, load-bearing):** the two drag **Coexistence with the M11 OS drag-out (disambiguation, load-bearing):** the two drag
modes are disambiguated by **where the pointer goes**, not by a mode toggle. Inside the modes are disambiguated by **where the pointer goes**, not by a mode toggle. Inside the
panel client rect → `Internal` (unchanged). Left the panel but still over REAPER's own panel client rect → `Internal` (unchanged). Left the panel but still over REAPER's own
@@ -976,7 +992,9 @@ still works unchanged; no media item is ever inserted into the arrange; the inst
holds no private copy (it reads the one authoritative bank). holds no private copy (it reads the one authoritative bank).
**Depends on:** M11 (`drag_out` gesture machinery — the mode it extends); **Phase S S4** **Depends on:** M11 (`drag_out` gesture machinery — the mode it extends); **Phase S S4**
(a loadable, playing ReaSampler 9000 instance must exist) **AND the new load-capture seam (a loadable, playing ReaSampler 9000 instance must exist) **AND the new load-capture seam
added inside ReaSampler 9000**. Composes with — but is distinct from — **S8** (ingest added inside ReaSampler 9000 via (B) component-state injection — the shared component-state
blob contract must be defined so the extension can construct it and the instrument's
`setChunk` accept it**. Composes with — but is distinct from — **S8** (ingest
through the bank: capture/import/drop *into* the bank) and **S13** (drop-to-load *inside* through the bank: capture/import/drop *into* the bank) and **S13** (drop-to-load *inside*
the editor). S17 is the third integration gesture: drop *onto a track's FX button* to the editor). S17 is the third integration gesture: drop *onto a track's FX button* to
instantiate a player. Gated on the rest of Phase S; the instrument seam is a Phase S instantiate a player. Gated on the rest of Phase S; the instrument seam is a Phase S
@@ -998,21 +1016,17 @@ artifact, not extension-only.
capture. Batched into one REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`) so the capture. Batched into one REAPER undo point (`Undo_BeginBlock2`/`EndBlock2`) so the
whole gesture is one Ctrl-Z (mirrors the bank-verb undo discipline). whole gesture is one Ctrl-Z (mirrors the bank-verb undo discipline).
- [ ] **ReaSampler 9000 (Phase S artifact):** add the **load-capture seam** — the entry - [ ] **ReaSampler 9000 (Phase S artifact):** add the **load-capture seam** — the entry
point that lets the just-added instance be told which capture to play (mechanism chosen point that lets the just-added instance be told which capture to play via **(B)
per the CONTEXT.md open question). This is the cross-artifact half; it lands in the component-state injection**: the instrument's `getChunk`/`setChunk` must round-trip a
instrument, not the extension. component-state blob whose byte layout is the **shared cross-artifact contract** (defined
in Phase S) that the extension constructs with the target capture pre-selected and writes
via `TrackFX_SetNamedConfigParm(..., "vst_chunk", ...)`. This is the cross-artifact half;
it lands in the instrument, not the extension. **Coordination dependency:** the blob
format must be agreed between extension and instrument before either half is final.
- [ ] Tests: gesture disambiguation (inside-panel / over-REAPER-UI / left-REAPER) across - [ ] Tests: gesture disambiguation (inside-panel / over-REAPER-UI / left-REAPER) across
single- and multi-capture payloads; FX-button hit resolution (pure geometry where it single- and multi-capture payloads; FX-button hit resolution (pure geometry where it
can be factored out); M11 OS drag-out and internal bank-to-bank drag both unchanged. can be factored out); M11 OS drag-out and internal bank-to-bank drag both unchanged.
> **Numbering reconciliation (resolved on merge to dev).** This wave was authored on
> **dev** as a provisional **S7** while dev's Phase S ran only S1S6. On merge with the
> `phase-s` worktree, the worktree's authoritative numbering (S7 stereo, S8 ingest, S9
> change-detection, S10S16) took the lower labels, so drop-and-load was renumbered to
> **S17** — the next free label past the worktree's Phase S set. It 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).
## Phase S — held and optional-forever (noted, not specified) ## Phase S — held and optional-forever (noted, not specified)
- **Tier 2 — "expressive" (HELD).** Velocity layers, round-robin (anti-machine-gun), - **Tier 2 — "expressive" (HELD).** Velocity layers, round-robin (anti-machine-gun),
full ADSR, per-sample tuning/gain trim, sustain loops. The next depth increment once full ADSR, per-sample tuning/gain trim, sustain loops. The next depth increment once