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
+20 -14
View File
@@ -32,14 +32,12 @@ namespace {
// COMPARE-ONLY and never dereferenced beyond the ValidatePtr2 the tick already makes —
// a tab switch has to be detectable without following a pointer that may name a closed
// project.
std::vector<ReclaimItem> g_items;
ReaProject* g_project = nullptr;
ParkClock g_clock;
FxReclaimQueue g_queue;
ReaProject* g_project = nullptr;
// TrackFX_SetOffline is ASSUMED to pump the message loop, so a plugin load can re-enter
// the timer and land back here mid-drain. Fail closed: a re-entered drain would walk the
// same queue the outer one holds a reference into, re-running the very item that outer
// step is still writing.
// the timer and land back here mid-drain. Fail closed: a re-entered drain would derive a
// second queue for the same project and race the outer one's writes track for track.
bool g_draining = false;
struct DrainLatch {
DrainLatch() { g_draining = true; }
@@ -112,13 +110,15 @@ struct ReclaimHost {
return true;
}
// Reached only after the drain has unwound and the queue has proved its generation
// did not move, so `model` is still the object the queue was derived against.
void releaseFx(const std::string& guid) { model->releaseSnapshotFx(guid); }
};
// Every path that stops scheduling ticks leaves the same state behind: no queue, no pin,
// nothing pending. The debounce clock is NOT part of it — see fx_reclaim.h.
void goIdle() {
g_items.clear();
g_queue.discard();
g_project = nullptr;
g_fxReclaimPending = false;
}
@@ -128,10 +128,15 @@ void goIdle() {
bool g_fxReclaimPending = false;
void invalidateFxReclaim() {
goIdle();
// A generation bump and a flag, and deliberately nothing more. Discarding the queue
// here is what let a re-entered apply free the element the outer drain was writing
// through, and clear the very wakeup this call had just raised.
g_queue.invalidate();
g_fxReclaimPending = true;
}
bool fxReclaimDrainInProgress() { return g_draining; }
void fxReclaimTick(ViewModeModel& model, ReaProject* proj) {
if (g_draining) return;
// The model is mid-rewrite and its records are being consumed out from under any
@@ -148,20 +153,21 @@ void fxReclaimTick(ViewModeModel& model, ReaProject* proj) {
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
if (!ValidatePtr2(nullptr, project, "ReaProject*") || (g_project && g_project != project)) {
goIdle();
g_clock.forget();
g_queue.forgetHolds();
return;
}
// DERIVED, never persisted: recomputed from view state every time the queue was
// discarded, so a REAPER quit mid-drain simply re-derives and finishes on reopen.
if (!g_project) {
// invalidated, so a REAPER quit mid-drain simply re-derives and finishes on reopen.
if (g_queue.stale()) {
g_project = project;
g_items = makeQueue(
view::desiredFxState(model, model.activeModeId(), liveTrackGuids(project)));
g_queue.adopt(makeQueue(
view::desiredFxState(model, model.activeModeId(), liveTrackGuids(project))));
}
ReclaimHost host{project, &model};
if (drainTick(g_items, g_clock, host).settled) goIdle();
const ReclaimRunResult run = g_queue.run(host);
if (!run.aborted && run.settled) goIdle();
}
} // namespace reasampler