Put the FX reclaimer to sleep on a project-state load instead of waking it

abandon() clears the wakeup where invalidate() raises it, so File > Revert
cannot converge FX or spend park records against a model nothing re-read.
This commit is contained in:
2026-08-07 14:23:56 -04:00
parent 39b995e824
commit 67d9ec3da9
4 changed files with 106 additions and 9 deletions
+16
View File
@@ -113,6 +113,22 @@ decide membership or mode rules.
`abandon`. Daniel's ruling, and the accepted cost: undoing a switch mid-drain leaves
a PARTIAL park — some FX offlined, some not — covered by the one point the
terminator closed.
- **A state load puts the reclaimer to SLEEP; only a re-read of the model wakes it.**
`abandon` clears the wakeup where `invalidate` raises it, because the two say
different things: an invalidation says the MODEL moved and owes a re-derivation, a
load says the PROJECT moved under a model nobody has re-read yet. `applyMode` is
`invalidateFxReclaim`'s one caller and `fxReclaimPending` gates the tick that is the
one path to any FX write or park-record spend, so every load that re-reads the model
wakes the loop on the same tick, through the reapply the reload drives — and a load
that re-reads nothing wakes nothing. `File > Revert` is exactly that load: same
`ReaProject`, same GUID, same path, so the identity poll answers `NoOp` and the model
is never re-read. Waking there was the only thing that ran, and it converged the FX
chains toward the pre-revert model and spent the park records the reverted project
still needed. The accepted cost of the sleep: an outstanding reclaim in a project
whose interrupting load was some OTHER project's waits for that project's next apply
rather than resuming on the next tick. (That a revert leaves the view model stale at
all is the identity poll's defect, not the reclaimer's — this invariant only keeps the
reclaimer from acting on it.)
- **A reapply that wrote nothing leaves no undo point** — and a reapply never holds
the span open, so a project open, a membership edit and `render_in_place`'s inner
reapply all keep the block shape they had. The load-tick reapply
+3 -3
View File
@@ -281,9 +281,9 @@ void fxReclaimLabelUndoSpan(const char* label, int mask) {
void fxReclaimEndUndoSpan() { closeSpan(); }
void fxReclaimAbandonForStateLoad() {
// Close FIRST and unconditionally. The abandon below only stops future writes; the
// block is what a Ctrl-Z would otherwise close over the wrong state, and it is open
// whether or not a drain is on the stack at all.
// Close FIRST and unconditionally. The abandon below stops the drain and puts the loop
// to sleep; the block is what a Ctrl-Z would otherwise close over the wrong state, and
// it is open whether or not a drain is on the stack at all.
closeSpan();
g_queue.abandon();
}
+19 -3
View File
@@ -369,9 +369,25 @@ public:
// closed. A load that keeps the SAME ReaProject (a revert) is what makes the stop
// load-bearing rather than tidy: the drain's pin and cached track handle both still
// validate afterwards, so nothing else would keep it out of the reloaded project.
//
// The wakeup goes DOWN, where an invalidation raises it — do not reintroduce the raise.
// An invalidation says the MODEL moved and owes a re-derivation; a load says the
// PROJECT moved under a model nobody has re-read yet, which is the opposite of a reason
// to converge. Every load that re-reads the model raises the wakeup itself, on that
// same tick, through the reapply the reload drives (applyMode is invalidateFxReclaim's
// one caller); File > Revert re-reads nothing, so waking here was the only thing that
// ever ran, and it wrote FX and spent park records against the pre-revert model.
//
// Safe to clear state under a drain, unlike invalidate(): the queue's own storage is
// empty for the drain's whole duration (run() moved it out), and run()'s abort path
// returns before it would read derived_ or commit anything.
void abandon() {
invalidate();
++generation_;
abandoned_ = true;
discard();
pending_ = false;
// The pin STAYS — it is what the next tick's retarget tests the debounce clock
// against, and that clock outlives this exactly as it outlives an invalidation.
}
// Read by the shell's host on every step; false again from the next run(), because the
@@ -666,8 +682,8 @@ void fxReclaimLabelUndoSpan(const char* label, int mask);
void fxReclaimEndUndoSpan();
// EVERY project-state load, undo-driven or not — Ctrl-Z, Ctrl-Shift-Z, a project open, a
// File > Revert, a new project. Ends the span and stops any drain on the stack
// (FxReclaimQueue::abandon).
// File > Revert, a new project. Ends the span, stops any drain on the stack, and leaves the
// loop ASLEEP until something that re-read the model wakes it (FxReclaimQueue::abandon).
//
// Called from the projectconfig BeginLoadProjectState hook, which fires BEFORE REAPER
// restores anything: that is the ONLY point at which the block can still close over the