Close pass four: undetented host curve read, LiveValues guard, ordering assert, static-lane fix, docs

Points toPlain's exponent arm at the undetented curve map so host reads
match the editor; adds a sizeof guard plus field-poison test for
LiveValues::operator==; skips the model write when an automation value
hasn't moved; corrects five stale doc citations.
This commit is contained in:
2026-08-02 18:22:21 -04:00
parent 1fd38bbd57
commit 9b5393098b
14 changed files with 163 additions and 59 deletions
+16 -11
View File
@@ -126,14 +126,18 @@ the single authority.** Everything else that holds these values is a cache or a
| 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) |
| Limiter toggle (`setLimiterEnabled`) | the call | the call returns (it writes the model too, but through neither `commitLive` nor `commitAndReload`) |
| Reload seed (`reloadInstrument`) | never — it does not write `params_` | it only republishes a live block folded from whatever the model already holds |
| **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.
Every writer above except the last two writes the model directly, so for those "authority ends"
is just "the write happened". Reload seed is not itself a model write — `reloadInstrument` never
touches `params_`; the only write in the tree is `setInstrumentParams`'s, `processor_state.cpp:192`
— which is why its row states no authority window of its own. The automation lane is the only one
that cannot write directly: 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
@@ -171,14 +175,15 @@ under it: the automation fold, the host's generic panel, a state restore.
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
against a performance budget — they are there because `VoiceEngine::applyLiveToActive` 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.
called from `getState`, the editor's sync tick, and `instrument_bake.cpp:125` — at the HEAD of
the bake chain, before the render, not its 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.
- **`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