FA1: unity-Preserve bypass kills preview onset latency; drain-slot reload keeps voices ringing through curve edits; velocity path proven end-to-end
This commit is contained in:
@@ -204,39 +204,42 @@ private:
|
||||
|
||||
ReaperBridge bridge_;
|
||||
|
||||
// --- The audio-thread handoff (S4 real-time discipline) -----------------
|
||||
// process() atomically loads `live_` at block start and marshals/renders against it —
|
||||
// a single atomic acquire, no lock, no free on the audio thread.
|
||||
// --- The audio-thread handoff (S4 real-time discipline, FA1 drain slot) --
|
||||
// process() atomically loads `live_` AND `draining_` at block start and marshals/renders
|
||||
// against them — two atomic acquires, no lock, no free on the audio thread.
|
||||
//
|
||||
// reloadFromBank() (off-thread, serialized by reloadMutex_) builds a new
|
||||
// LoadedInstrument and atomically swaps it into `live_`. The DISPLACED instrument is
|
||||
// NOT freed on the reload path: process() may still be mid-block reading it, and two
|
||||
// rapid reloads could otherwise free a pointer process is using. Instead it is parked
|
||||
// in `graveyard_` tagged with the reload generation at which it was displaced.
|
||||
// NOT freed and NOT silenced: it moves into `draining_`, where process() keeps
|
||||
// rendering its already-sounding voices (and routes note-offs to it) so a reload —
|
||||
// a curve/param edit, a bank-generation refresh, an applied assignment — never cuts a
|
||||
// ringing note (FA1, bug 3b). New note-ons go ONLY to the live instrument, so the next
|
||||
// trigger plays the new state. The instrument evicted FROM the drain slot (two reloads
|
||||
// old) is parked in `graveyard_` for reclaim — a rapid second reload hard-cuts only the
|
||||
// oldest edit's tails (bounded compromise, documented).
|
||||
//
|
||||
// Bounded reclaim: process() publishes inst->installedAt (the generation at which the
|
||||
// held instrument was installed) via processGeneration_ — a single atomic store, RT-
|
||||
// safe. The reload path prunes graveyard entries where displacedAt <= seen (where seen
|
||||
// is the last published processGeneration_).
|
||||
// Bounded reclaim: process() publishes the MINIMUM installedAt over the (non-null)
|
||||
// pointers it holds this block via processGeneration_ — a single atomic store, RT-safe.
|
||||
// The reload path frees graveyard entries whose installedAt < seen (the last published
|
||||
// value).
|
||||
//
|
||||
// Safety argument: an entry with displacedAt == D was displaced by reload D, which
|
||||
// simultaneously installed its successor with installedAt == D. process() publishing
|
||||
// seen == D means it holds that successor (or a later one). In either case, the
|
||||
// displaced entry is not the pointer process is using, so freeing it is safe. The
|
||||
// pruning condition is therefore <= (not strict <): an entry displaced at exactly the
|
||||
// published generation is also provably unreachable.
|
||||
// Safety argument: both slots are monotone in installedAt over time (live_ receives
|
||||
// successively newer builds; draining_ receives successively newer displaced lives), so
|
||||
// the published minimum is monotone across blocks, and any future process() load yields
|
||||
// installedAt >= seen. An entry only reaches the graveyard by leaving BOTH slots
|
||||
// (single-writer under reloadMutex_), so a graveyard entry with installedAt < seen can
|
||||
// never again be loaded and is not currently held — freeing it is safe. process()
|
||||
// publishes BEFORE rendering, so the pointers it renders with are covered by the value
|
||||
// the pruner reads (a stale lower read is merely conservative).
|
||||
//
|
||||
// The graveyard's upper bound is the number of reloads since process last ran
|
||||
// (typically 0–1 in normal use). Remaining entries drain at setActive(false) /
|
||||
// terminate(), when the host guarantees process is stopped.
|
||||
std::atomic<LoadedInstrument*> live_{nullptr};
|
||||
std::atomic<LoadedInstrument*> draining_{nullptr}; // displaced instrument still rendering its tails
|
||||
std::atomic<std::uint64_t> reloadGeneration_{0}; // incremented by each reload (off-thread, under reloadMutex_; read atomically by process)
|
||||
std::atomic<std::uint64_t> processGeneration_{0}; // generation last seen by process (written on audio thread, read off-thread)
|
||||
struct GraveyardEntry {
|
||||
std::uint64_t displacedAt = 0; // reloadGeneration_ value when this was displaced
|
||||
std::unique_ptr<LoadedInstrument> instrument;
|
||||
};
|
||||
std::vector<GraveyardEntry> graveyard_; // drained on reclaim + setActive(false) + terminate
|
||||
std::atomic<std::uint64_t> processGeneration_{0}; // min installedAt held by process (written on audio thread, read off-thread)
|
||||
std::vector<std::unique_ptr<LoadedInstrument>> graveyard_; // drained on reclaim + setActive(false) + terminate
|
||||
std::mutex reloadMutex_; // serializes off-thread reloads + graveyard access
|
||||
|
||||
// The single-capture selection id (S10: the ONE picked capture; "" = no pick -> silence).
|
||||
|
||||
Reference in New Issue
Block a user