Bound the automation hold to the window the model has not caught up on, and make that authority model stated, enforced and tested
This commit is contained in:
@@ -109,17 +109,76 @@ pure half — the frozen id table, the exposed set, the plain-value layer, the f
|
||||
the audio thread and may allocate on the way; `process()` merges that block with the host's
|
||||
automation points into `automationLive_`, which is what `SampleData::live` points at. Two
|
||||
blocks rather than one because the seqlock's single-writer contract is load-bearing and the two
|
||||
writers genuinely differ in thread. The merge republishes ONLY when either side moved, so a
|
||||
block carrying neither costs one relaxed load and the engine's read shape is unchanged.
|
||||
writers genuinely differ in thread.
|
||||
|
||||
### THE AUTHORITY MODEL — who may write a parameter's value, and until when
|
||||
|
||||
Two passes got this subtly wrong in opposite directions (the first delivered automation on the
|
||||
wrong channel; the second made a point's authority permanent), because the model was in nobody's
|
||||
head and nowhere in the tree. It is here, and the code follows it.
|
||||
|
||||
**`ReaSamplerProcessor::params_` — plus the two instance scalars beside it — is THE model, and
|
||||
the single authority.** Everything else that holds these values is a cache or a courier:
|
||||
|
||||
| Writer | Authority begins | Authority ends |
|
||||
|---|---|---|
|
||||
| Editor gesture (`commitLive` / `commitAndReload`) | mouse-down | the commit lands in the model |
|
||||
| Host controller write (`setParamNormalized`) | the call | the call returns (it writes the model) |
|
||||
| State restore (`setState`) | the call | the call returns |
|
||||
| Bake reset (`adoptBakedCapture`) | the call | the call returns |
|
||||
| Reload seed (`reloadInstrument`) | under `reloadMutex_` | the publish (it re-folds the model) |
|
||||
| **Host automation point** (`IParameterChanges`) | the block it lands in | **the UI thread has folded it into the model and republished** |
|
||||
|
||||
Every writer except the last writes the model directly, so for those "authority ends" is just
|
||||
"the write happened". The automation lane is the only one that cannot: the SDK delivers it on the
|
||||
audio thread, where the model path allocates (`resolvePlay` copies velocity curves and spline
|
||||
contours). So it patches the engine-facing block in place and is couriered to the UI thread,
|
||||
which folds it into the model on the next tick.
|
||||
|
||||
**The hold is the bridge across that gap, and nothing more.** Between the point landing and the
|
||||
fold — at most one UI tick — the model does not yet carry the value, so a model republish in that
|
||||
window (any knob move) would revert the automated parameter until the lane's next point. The hold
|
||||
re-applies the point over every merge to stop that. The instant the model carries the value, the
|
||||
hold has no job and is **released**; from then on every writer above reaches the audio normally.
|
||||
|
||||
**Contention resolves BY RULE, not by timing.** A point outranks the model while the lane is
|
||||
driving and the model has not caught up — which is VST3's own authority rule (a lane in
|
||||
read/write mode outranks a plug-in-side set). It does NOT outrank a later restore, bake reset or
|
||||
knob move, because by then the lane is no longer driving that value; the model is.
|
||||
|
||||
**Where it is enforced, and what fails if it stops holding.**
|
||||
- The decision is the pure `core/instrument/param/param_merge`'s `mergeAutomation`;
|
||||
`tests/test_param_merge.cpp`'s
|
||||
`testAHeldPointOutranksTheModelOnlyUntilTheModelCarriesIt` is the test — it asserts both halves,
|
||||
including that a writer AFTER the release reaches the audio. A latch with no release fails it.
|
||||
- The mechanism — the per-slot sequence the audio thread stamps and the UI thread answers, and
|
||||
the acquire/release ordering that makes a release imply the publish is visible — is
|
||||
`automation_channel.h`'s, at its two methods.
|
||||
- **The release is stored LAST in `drainAutomationToModel`**, after `setInstrumentParams` and
|
||||
`publishLiveParams`. Moving it earlier reintroduces a one-block revert.
|
||||
- **`setState` therefore needs no ordering guarantee against the host's first parameter block.**
|
||||
A lane that is driving re-applies over the restore; a lane that merely sent a point once, and
|
||||
had it folded, does not — which is the correct reading of the SDK rule, and the one the second
|
||||
pass got wrong.
|
||||
|
||||
**The editor's `params_` is a CACHE of the model, authoritative for one gesture only.** A commit
|
||||
writes the WHOLE set back, and `notifyParamsFromModel` diffs it — so a stale copy would
|
||||
`performEdit` superseded values the user never touched, which a lane in latch or write mode
|
||||
records. The sync tick re-seeds it (past the drag guard) whenever `paramsGeneration_` has moved
|
||||
under it: the automation fold, the host's generic panel, a state restore.
|
||||
|
||||
**Two independent gates keep a value-identical point off the per-voice fan-out**, and they cover
|
||||
different windows: `AutomationChannel::land` drops a repeat of a standing hold whole (the flat
|
||||
read-mode segment, where a host sends one point per block), and the merge publishes only when the
|
||||
merged block differs from the last (a model republish that changed nothing). Neither is measured
|
||||
against a performance budget — they are there because `VoiceEngine::refreshLive` runs
|
||||
`voice.applyLive` over every active voice, and neither case needs it.
|
||||
|
||||
- **The automation values fold back into the model on the UI thread** (`drainAutomationToModel`,
|
||||
called from `getState`, the editor's sync tick, and the bake's reload tail). The blob is
|
||||
authoritative, so a value that never came back would be lost on save. The fold is suppressed
|
||||
from notifying the host — the values came FROM it, and echoing them would let a lane in write
|
||||
mode re-record its own playback.
|
||||
- **`setState` does not need an ordering guarantee against the host's first parameter block.**
|
||||
An automation point held by the audio thread is re-applied over every merge, so a written lane
|
||||
outranks the restore whichever way round the two arrive — which is VST3's own rule, not a race
|
||||
we lost.
|
||||
- **`IMidiMapping` is deliberately NOT implemented** — no conventional CC names most of what
|
||||
is exposed, an invented map would hijack CCs the user's controller already sends, and
|
||||
`[verify — DAW]` REAPER's own per-parameter MIDI learn is expected to cover the case without
|
||||
@@ -155,7 +214,9 @@ pure half — the frozen id table, the exposed set, the plain-value layer, the f
|
||||
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
|
||||
- `editor_stroke` — the editor's LICE side of the analytic stroker: builds a coverage mask with the pure `core/ui/stroke_aa` and blends it into the bitmap ONCE, writing straight to the bitmap's bits (the arithmetic matches LICE's own mode-0 combine, so a stroke composites identically to every other kit draw). Every radial and spline stroke on the editor routes through `strokeArcAA` / `strokePolylineAA` / `strokeLineAA`. Holds the draw-thread-only scratch mask and arc point list — reuse, not a hidden dependency: threading a canvas through the eight paint sites would grow those signatures to carry an allocation detail. Deliberately does NOT touch `shell/panel/draw_kit`: the waveform stroke, the docked bank panel and the browse cards are out of this seam's blast radius.
|
||||
- `instrument_bake` — the instrument's half of the resample chain, on the UI thread: render the dialed sound through the pure `core/instrument/bake` modules at the instance's PERSISTED PREVIEW VELOCITY (the velocity the user has been auditioning at — three velocity curves are live, so it is a property of the sound and not a render detail), stage the WAV OUTSIDE the bank folder, publish one `rsbake_<guid>` request, invoke the extension's landing action SYNCHRONOUSLY, read the outcome back over the same key, then adopt + reset in one act. What that key holds afterwards is classified by `core/wire`'s pure `classifyBakeAnswer`, and each of its five non-answers gets its OWN sentence — a silent no-answer stays a failure, but the user is told whether nothing wrote over the key, a stale generation was answered, the answer came in a wire this build cannot read, the request was cleared, or it was refused. All five name the key, because the extension prints one console line per key it scanned and the key is what correlates the two in a multi-instance session. None of them claims the landing never ran — nothing on this side can observe that. Two stack-RAII guards mirror `FxBypassGuard`'s discipline: the staged file and the request key are both cleared on every exit path, so a failed bake leaves no temp, no bank entry and no parameter reset. `bakeAvailable` is the affordance's paint gate. A cloned `instanceGuid` (two instances sharing one `rsbake_` key) is NOT handled here — the residual is contained by pre-existing tracking machinery instead: `planUsagePublish`'s sticky `unioned` poison plus `tiedUsageExists` (`core/tracking/tracking_authority.cpp`) force a clone's bake to `AddDistinct` rather than silently replacing a sibling's entry.
|
||||
- `instrument_params` — the VST3 adapter over `core/instrument/param`: one `Parameter` subclass whose `toPlain`/`toNormalized` ARE the taper and whose `toString` calls the one formatter, the single construction of the unit and parameter lists (ascending id, which is also the presentation order), the `setParamNormalized` projection onto the model through each control's existing commit tier, and the `beginEdit`/`performEdit`/`endEdit` notification path every internal writer reaches through `setInstrumentParams`. Decides nothing — the pure module owns the table, the laws and the formatter.
|
||||
- `instrument_params` — the VST3 adapter over `core/instrument/param`: one `Parameter` subclass whose `toPlain`/`toNormalized` ARE the taper and whose `toString` calls the one formatter, the single construction of the unit and parameter lists (ascending id, which is also the presentation order), the `setParamNormalized` projection onto the model through each control's existing commit tier, the audio thread's queue drain and the UI thread's fold + release, and the `beginEdit`/`performEdit`/`endEdit` notification path every internal writer reaches through `setInstrumentParams`. Decides nothing — the pure module owns the table, the laws, the formatter and the merge.
|
||||
- `automation_channel.h` — the host automation lane's per-instance state and the mechanism of its authority lifetime: the audio thread's hold, the per-slot sequence it stamps, the UI thread's release answer, and the acquire/release ordering that makes a release imply the model publish is visible. The MODEL it enforces is the Authority section above; the pure decision it feeds is `core/instrument/param/param_merge`. Internal to this TU family.
|
||||
- `processor_snapshot.h` — the two namespace-scope aggregates the processor hands across its thread boundary: `LoadedInstrument` (the decoded capture plus the engine playing it, swapped through the drain slot) and `MasterBusMeter` (what the audio thread publishes per block for the editor's meter). Split out of `reasampler_processor.h` on `editor_interaction.h`'s grounds — neither is behaviour.
|
||||
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
|
||||
- `editor_interaction.h` — the editor's INTERACTION VOCABULARY: `DragKind` (what a gesture in flight is editing) and `HoverKind`/`HoverTarget` (what the pointer can be over). Split out of `reasampler_editor.h`, which had grown past the ~600-line ceiling with no seam — these two catalogues are produced by the input TUs and read by the paint TUs, and neither is behaviour, which is what makes them a responsibility rather than a bisection. Namespace-scope, so the editor's own members still spell them unqualified. Internal to this TU family, like `editor_internal.h`.
|
||||
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family, included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, the velocity-curve box derivation, and `dragModifiers()` — THE modifier read for every drag surface and gesture resolver, so the editor cannot grow a second modifier grammar — the helpers more than one band TU needs. The deck's control ids, group ids and group composition are the pure `deck_groups` module's, not this file's. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
|
||||
@@ -163,12 +224,13 @@ pure half — the frozen id table, the exposed set, the plain-value layer, the f
|
||||
|
||||
## Gotchas
|
||||
|
||||
- **`reasampler_processor.h` is a documented ~600-line-ceiling exception** (root `CLAUDE.md`,
|
||||
structural heuristic 1), on the same footing as `voice.h`'s: it is ONE class declaration, so
|
||||
the seam the heuristic asks for does not exist — a split would be an arbitrary bisection, and
|
||||
the implementation is already split across three TUs on its real seams. Its bulk is the
|
||||
drain-slot proof, the RT-discipline constraints and the two-block automation contract, all of
|
||||
which the comment conventions name as keep-worthy. Not silent overshoot.
|
||||
- **`reasampler_processor.h` no longer needs a ceiling exception, and the one it had rested on a
|
||||
false premise.** It was described as ONE class declaration; it also carried two namespace-scope
|
||||
aggregates (`MasterBusMeter`, `LoadedInstrument`) and the automation lane's own state. Both are
|
||||
now split out — `processor_snapshot.h` and `automation_channel.h`, on the same grounds
|
||||
`editor_interaction.h` was split out of `reasampler_editor.h` in this directory: neither is
|
||||
behaviour. What remains is under the ceiling. Its bulk is the drain-slot proof and the
|
||||
RT-discipline constraints, which the comment conventions name as keep-worthy.
|
||||
|
||||
- **The bake click only ARMS; the editor's sync tick runs it.** Calling
|
||||
`Main_OnCommandEx` inline from `WM_LBUTTONDOWN` would run the extension's whole landing
|
||||
|
||||
Reference in New Issue
Block a user