feat(S9/S8): bank-generation change-detection + instrument-side assignment reader

Extension stamps a monotonic bank_generation counter, bumped at content
mutations; the VST3 instrument polls it off-thread on an editor timer and
consumes assignment requests, refreshing playback hands-free.
This commit is contained in:
2026-07-26 23:46:06 -04:00
parent 725f3e7d3c
commit 66d47fb410
21 changed files with 838 additions and 36 deletions
+37
View File
@@ -122,6 +122,27 @@ public:
// resolved selection id ("" if nothing was loaded) for the editor to reflect.
std::string reloadFromBank();
// The result of a bank-sync poll (S9/S8): what pollBankSync did this tick, so the editor
// can react (repaint / re-snapshot its own view) only when something actually changed.
struct BankSyncResult {
bool reloaded = false; // the bank generation changed -> reloadFromBank ran
bool applied = false; // a new assignment request was applied -> selection changed
};
// Poll the S9 bank-generation counter and the S8 assignment request over the bridge, OFF
// THE AUDIO THREAD (the editor's UI timer drives this — NEVER process()). Semantics:
// * S9: if the bank generation differs from what we last saw, call reloadFromBank() so a
// recapture/ingest refreshes playback hands-free (atomic swap, glitch-free).
// * S8: if a NEW (generation > last consumed) assignment request names a resolvable
// sample AND this instance is the target (isFocusedTarget), apply it as the selection
// and reload; an unresolvable request is DROPPED silently (marker advanced, no change);
// a non-target instance neither applies nor advances its marker.
// The consumed marker advances in component state (marked dirty via the host handler) so a
// re-open does not re-apply. `isFocusedTarget` is the shell's thundering-herd policy input
// (the editor passes true only for the instance whose editor is open — see the handoff).
// Idempotent on an idle tick (generation unchanged + no new request -> no work).
BankSyncResult pollBankSync(bool isFocusedTarget);
// The bridge, for the editor's live-state readout + sample list. Owned here; the
// editor borrows it (outlives the editor).
ReaperBridge& bridge() { return bridge_; }
@@ -210,6 +231,22 @@ private:
std::mutex channelModeMutex_;
ChannelMode channelMode_ = ChannelMode::Mono;
// The last assignment-request generation this instance CONSUMED (S8 reader). Persisted in
// component state (v5) so a re-open does not re-apply a request the user already got and
// then changed away from. Written by pollBankSync (UI/timer thread) and getState; read by
// pollBankSync + getState; seeded by setState. Guarded against a getState/poll race. NEVER
// read on the audio thread. Default 0 -> a genuinely new first assign (gen >= 1) applies.
std::mutex assignMarkerMutex_;
std::int64_t lastConsumedAssignGeneration_ = 0;
// The bank generation this instance last SAW (S9 reader). UI/timer-thread only (pollBankSync
// is the sole reader/writer) — no mutex needed, and it is NOT persisted. Initialized to a
// -1 SENTINEL (no real generation can be negative — parseBankGeneration yields >= 0) so the
// FIRST poll after an editor open BASELINES the seen value without a redundant reload (setState
// already loaded the current bank); a subsequent generation CHANGE then drives the reload.
// NOT read on the audio thread.
std::int64_t lastSeenBankGeneration_ = -1;
// Latched from setupProcessing so setActive/reload can size against it. Read
// off-thread only.
double sampleRate_ = 44100.0;