Fix the reclaimer stranding a tab-switched-into project: lift the pin and wakeup into FxReclaimQueue, retarget instead of going idle

Also validate the cached track in its project, and stop a drop-free tick resetting the report memo.
This commit is contained in:
2026-08-06 22:16:41 -04:00
parent 35fdfca805
commit 224dcf84df
4 changed files with 234 additions and 56 deletions
+14 -34
View File
@@ -30,12 +30,9 @@ namespace reasampler {
namespace {
// The derived queue and the project it was derived against. The pointer is
// 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.
// The derived queue, the project it was derived against, and the timer's wakeup — see
// FxReclaimQueue, which owns all three.
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 derive a
@@ -86,9 +83,11 @@ struct ReclaimHost {
std::string cachedGuid_;
MediaTrack* cachedTrack_ = nullptr;
// A track is validated IN a project (reaper_plugin_functions.h:7898); the null form
// is defined only where the pointer is itself a project.
MediaTrack* resolveForStep(const std::string& guid) {
if (guid == cachedGuid_ && cachedTrack_ &&
ValidatePtr2(nullptr, cachedTrack_, "MediaTrack*"))
ValidatePtr2(project, cachedTrack_, "MediaTrack*"))
return cachedTrack_;
cachedTrack_ = resolveFresh(project, guid);
cachedGuid_ = guid;
@@ -171,24 +170,15 @@ void reportDrops(ReaProject* proj, const std::vector<TrackFxDrops>& drops) {
if (!msg.empty()) ShowConsoleMsg(("!SHOW:" + msg).c_str());
}
// 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_queue.discard();
g_project = nullptr;
g_fxReclaimPending = false;
}
} // namespace
bool g_fxReclaimPending = false;
bool fxReclaimPending() { return g_queue.pending(); }
void invalidateFxReclaim() {
// 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; }
@@ -200,27 +190,17 @@ void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
if (modeApplyInProgress()) return;
DrainLatch latch;
// Belt-and-braces, not the tab-switch mechanism: OnTimer runs session.poll() before
// this tick, and poll() already classifies a tab switch as a Load whose reapply
// invalidates the queue — so by the time this test runs, a switch has normally
// already been handled and g_project already matches. This still catches it when
// poll() was itself skipped this tick (modeApplyInProgress/drain-in-progress
// deferral) and the two go stale together. Both arms below mean the same thing —
// no active project at all, or a project this queue no longer belongs to — so drop
// the queue rather than write another project's chains; returning to it re-derives.
// The clock goes too — a hold measured in a project we stopped watching is not
// evidence about the one we come back to.
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
if (!ValidatePtr2(nullptr, project, "ReaProject*") || (g_project && g_project != project)) {
goIdle();
g_queue.forgetHolds();
return;
}
// beginTick owns what a pin/project mismatch means. What is local to here: the model
// the re-derivation below reads is the ACTIVE project's, because reaching this line
// means OnTimer ran session.poll() this tick — the two guards above are the same pair
// that gate that poll.
ReaProject* project = proj ? proj : EnumProjects(-1, nullptr, 0);
if (!ValidatePtr2(nullptr, project, "ReaProject*")) project = nullptr;
if (!g_queue.beginTick(project)) return;
// DERIVED, never persisted: recomputed from view state every time the queue was
// invalidated, so a REAPER quit mid-drain simply re-derives and finishes on reopen.
if (g_queue.stale()) {
g_project = project;
const ViewModeModel& model = session.view();
g_queue.adopt(makeQueue(
view::desiredFxState(model, model.activeModeId(), liveTrackGuids(project))));
@@ -229,7 +209,7 @@ void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
ReclaimHost host{project, &session};
const ReclaimRunResult run = g_queue.run(host);
reportDrops(project, run.drops);
if (!run.aborted && run.settled) goIdle();
if (!run.aborted && run.settled) g_queue.goIdle();
}
} // namespace reasampler