Reclaim drain owns its work: invalidation only bumps a generation, and a generation that moved aborts the tick's commit and its release.

This commit is contained in:
2026-08-06 21:08:34 -04:00
parent f04c704353
commit 5dcfd3a53f
7 changed files with 321 additions and 68 deletions
+111 -13
View File
@@ -92,6 +92,9 @@ public:
void forget() { since_.clear(); }
// The owner swaps the clock out for the duration of a drain — see FxReclaimQueue.
void swap(ParkClock& other) { since_.swap(other.since_); }
private:
std::map<std::string, double> since_;
};
@@ -119,6 +122,12 @@ inline int nextEligible(const std::vector<ReclaimItem>& items, const ParkClock&
struct ReclaimTickResult {
int writes = 0;
bool settled = false;
// Tracks whose park record the tick drove fully online, so the record's FX half is
// spent. REPORTED, not applied: releasing is a model write and the model is exactly
// what a re-entered load can replace mid-drain, so only the owner may commit it —
// see FxReclaimQueue::run.
std::vector<std::string> released;
};
// One tick of the convergence loop. Templated on `Host` so the ordering, budget and
@@ -131,7 +140,14 @@ struct ReclaimTickResult {
// bool writeOffline(guid, fxIndex, offline) compare-before-write inside its
// own discarded undo block;
// true iff it actually wrote
// void releaseFx(guid) release the record's FX half
// void releaseFx(guid) release the record's FX half;
// called by FxReclaimQueue::run
// after the drain has unwound,
// never from inside the drain
//
// `items` and `clock` must be the caller's OWN storage, not shared state a re-entered
// call could reach: a write pumps the message loop. FxReclaimQueue::run is what makes
// that true for the live loop.
template <class Host>
ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, Host& host) {
ReclaimTickResult result;
@@ -166,11 +182,12 @@ ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, H
for (const FxOfflineWrite& w : resolved.writes)
if (host.writeOffline(item.work.trackGuid, w.fxIndex, w.offline)) ++result.writes;
// The record's FX half is released HERE and only here: the chain has now
// been driven to every state the record captured, so the record is spent.
// Releasing it any earlier destroys the only account of which plugins the
// USER had offline before the park, and the next online blanket-enables them.
if (item.work.kind == view::FxWorkKind::Online) host.releaseFx(item.work.trackGuid);
// The record is spent only once the chain has been driven to every state it
// captured — releasing any earlier destroys the only account of which plugins
// the USER had offline before the park, and the next online blanket-enables
// them. Reported rather than applied; see ReclaimTickResult::released.
if (item.work.kind == view::FxWorkKind::Online)
result.released.push_back(item.work.trackGuid);
}
item.done = true; // both arms: a step is atomic, so no item is half-moved
@@ -181,23 +198,104 @@ ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, H
return result;
}
// --- the queue's owner ----------------------------------------------------------
struct ReclaimRunResult {
int writes = 0;
bool settled = false;
// Something invalidated the queue while the drain was on the stack. The work and
// the releases are DISCARDED, `settled` is never true, and the caller must not go
// idle — the next tick re-derives against whatever changed.
bool aborted = false;
};
// Owns the derived queue and the debounce clock across a tick that can be re-entered:
// TrackFX_SetOffline is ASSUMED to pump the message loop, so an invalidation can arrive
// with a drain half-way through a track.
//
// The two halves of that safety are structural, not guarded. `invalidate()` bumps a
// counter and touches NOTHING else, so there is nothing for it to clear under a drain.
// `run()` moves the queue and the clock into locals for the drain's whole duration, so
// the object holds nothing the drain is reading — and re-checks the counter before it
// commits either the surviving queue or a single release.
class FxReclaimQueue {
public:
void invalidate() { ++generation_; }
// No queue at all, or one derived before the last invalidation.
bool stale() const { return !derived_ || derivedAt_ != generation_; }
void adopt(std::vector<ReclaimItem> items) {
items_ = std::move(items);
derived_ = true;
derivedAt_ = generation_;
}
void discard() {
items_.clear();
derived_ = false;
}
void forgetHolds() { clock_.forget(); }
const std::vector<ReclaimItem>& items() const { return items_; }
template <class Host>
ReclaimRunResult run(Host& host) {
const unsigned long long entered = generation_;
std::vector<ReclaimItem> work;
ParkClock clock;
work.swap(items_);
clock.swap(clock_);
const ReclaimTickResult tick = drainTick(work, clock, host);
// The hold is keyed on the track and outlives any re-derivation, so it comes
// back whether or not the drain's own work survives.
clock_.swap(clock);
if (generation_ != entered) return ReclaimRunResult{tick.writes, false, true};
items_.swap(work);
// A release dropped by an abort is re-owed, not lost: the next derivation plans
// the same Online item, its writes all find the chain already there, and the
// release fires then.
for (const std::string& guid : tick.released) host.releaseFx(guid);
return ReclaimRunResult{tick.writes, tick.settled, false};
}
private:
std::vector<ReclaimItem> items_;
ParkClock clock_;
unsigned long long generation_ = 0;
unsigned long long derivedAt_ = 0;
bool derived_ = false;
};
// --- shell entry points --------------------------------------------------------
// The ONE test the timer makes per tick, mirroring the realtime capture's single
// pointer test. False means the queue is settled and there is nothing to converge.
extern bool g_fxReclaimPending;
// Discards the derived queue. Called from every path that can change what the FX chains
// SHOULD be — which is applyMode's success path and nothing else, because every
// membership, snapshot and active-mode mutation reapplies through it. The debounce clock
// deliberately survives: it is keyed on the track, not on the queue.
// Marks the derived queue for re-derivation by the NEXT tick; discards nothing here,
// because this is reachable from inside a drain. Called from applyMode and nothing else
// — every membership, snapshot and active-mode mutation reapplies through it — on the
// refusal path as well as the success one (see view.cpp for why the refusal is the
// load-bearing half). The debounce clock deliberately survives: it is keyed on the
// track, not on the queue.
void invalidateFxReclaim();
// True while a drain is on the stack. Exposed so the timer's other work can stand off
// the model the drain is holding, the same way it stands off an applyMode.
bool fxReclaimDrainInProgress();
// Drains one tick against `proj` (nullptr = current project), re-deriving the queue from
// `model` first if it was invalidated. Returns without touching the project while an
// applyMode is on the stack, while already draining (TrackFX_SetOffline is assumed to
// pump the message loop), or when the active project is not the one the queue was
// derived against.
// applyMode is on the stack, while already draining, or when the active project is not
// the one the queue was derived against.
void fxReclaimTick(ViewModeModel& model, ReaProject* proj);
} // namespace reasampler