Close the FX reclaimer's undo span at the state-load hook, and expose the drain's direction

A Ctrl-Z mid-span used to close the pre-undo block over post-undo state; it now
terminates at BeginLoadProjectState and stops the drain where it stands.
fxReclaimActivity reports Online/Offline/Idle; fxReclaimPending is unchanged.
This commit is contained in:
2026-08-07 13:45:58 -04:00
parent 86aa8dabba
commit 9b6f725747
5 changed files with 357 additions and 34 deletions
+40 -8
View File
@@ -60,6 +60,15 @@ void closeSpan() {
if (ValidatePtr2(nullptr, proj, "ReaProject*")) Undo_EndBlock2(proj, label.c_str(), mask);
}
// Monotonic and sub-millisecond: the drain's budget discriminates at 30ms, which
// GetTickCount's ~16ms granularity cannot do. No wraparound to guard against, so
// util::elapsedAtLeast (whose whole reason for existing is that wrap) does not apply.
double monotonicMs() {
return std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
// 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
// second queue for the same project and race the outer one's writes track for track.
@@ -122,21 +131,18 @@ struct ReclaimHost {
return cachedTrack_;
}
// Monotonic and sub-millisecond: the budget below discriminates at 30ms, which
// GetTickCount's ~16ms granularity cannot do. No wraparound to guard against, so
// util::elapsedAtLeast (whose whole reason for existing is that wrap) does not apply.
double nowMs() const {
return std::chrono::duration<double, std::milli>(
std::chrono::steady_clock::now().time_since_epoch())
.count();
}
double nowMs() const { return monotonicMs(); }
// The SAME predicate applyMode refuses a real mode switch on, deliberately shared
// rather than re-read here: "the transport is rolling" is one fact with one home
// (view.h). drainTick narrows what it gates with this answer to offline work only.
bool transportRolling() const { return transportBlocksModeSwitch(project); }
// Refusing here ends the drain's REMAINING tracks after an abandon: drainTick treats a
// false as "this track is gone", so the rest of the queue costs one bool apiece and
// reaches no REAPER call at all.
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
if (g_queue.abandoned()) return false;
MediaTrack* tr = resolveForStep(guid);
if (!tr) return false;
const int count = TrackFX_GetCount(tr);
@@ -148,7 +154,11 @@ struct ReclaimHost {
return true;
}
// ...and here for the track being written RIGHT NOW, whose own pump is how the state
// load arrived. FIRST, before the openSpan below: a write past the terminator would
// both land in the project the load is replacing and reopen the span it just closed.
bool writeOffline(const std::string& guid, int fxIndex, bool offline) {
if (g_queue.abandoned()) return false;
MediaTrack* tr = resolveForStep(guid);
if (!tr) return false;
if (fxIndex < 0 || fxIndex >= TrackFX_GetCount(tr)) return false; // chain shrank
@@ -199,6 +209,20 @@ void reportDrops(ReaProject* proj, const std::vector<TrackFxDrops>& drops) {
bool fxReclaimPending() { return g_queue.pending(); }
FxReclaimActivity fxReclaimActivity() {
// The QUEUE's own pin, never the active tab: the direction is a fact about the project
// the drain is converging, and reading another tab's transport into it would gate the
// answer on a playhead that has nothing to do with this work. No pin means idle — and
// costs the caller no REAPER call at all, which is what makes this safe to poll per
// frame.
ReaProject* const proj = g_queue.project();
if (!proj) return FxReclaimActivity::Idle;
// The pin is only re-tested at beginTick; a poll landing between two ticks can find the
// project already gone, and GetPlayStateEx is about to be handed this pointer.
if (!ValidatePtr2(nullptr, proj, "ReaProject*")) return FxReclaimActivity::Idle;
return g_queue.activity(monotonicMs(), transportBlocksModeSwitch(proj));
}
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
@@ -256,6 +280,14 @@ 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.
closeSpan();
g_queue.abandon();
}
void fxReclaimFlushRelease(ReaSamplerSession& session) {
// GetCurrentProjectInLoadSave() names the project THIS hook is being asked to save
// (reaper_plugin_functions.h:1582-1587) -- not merely the active tab, which a