Gate the FX reclaimer on the transport, both directions; move the release's persist off the drain and onto the project save

The drain no longer dirties a project on its own initiative — a spent park record rides the user's next save via OnSaveExtensionConfig.
This commit is contained in:
2026-08-06 22:31:47 -04:00
parent 224dcf84df
commit 02790f1270
6 changed files with 288 additions and 79 deletions
+25 -13
View File
@@ -10,8 +10,8 @@
#include <string>
#include <vector>
#include "shell/persist/session.h" // saveViewStateOnly — the release's durable half
#include "shell/view/view.h" // modeApplyInProgress
#include "shell/persist/session.h" // flushViewState — the release's durable half
#include "shell/view/view.h" // modeApplyInProgress, transportBlocksModeSwitch
#include "shell/view/view_park.h" // fxGuidString — the one FX-identity read
#include "shell/view/view_tracks.h"
@@ -34,6 +34,9 @@ namespace {
// FxReclaimQueue, which owns all three.
FxReclaimQueue g_queue;
// A release applied to the in-memory model and not yet written — see ReleaseFlush.
ReleaseFlush g_releaseFlush;
// 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.
@@ -103,6 +106,11 @@ struct ReclaimHost {
.count();
}
// The SAME predicate applyMode refuses a real mode switch on, deliberately shared
// rather than re-read here: what counts as a transport that must not have plugins
// moved under it is one fact with one home (view.h).
bool transportRolling() const { return transportBlocksModeSwitch(project); }
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
MediaTrack* tr = resolveForStep(guid);
if (!tr) return false;
@@ -136,17 +144,10 @@ struct ReclaimHost {
// Reached only after the drain has unwound and the queue has proved its generation
// did not move, so the session still holds the model the queue was derived against.
void releaseFx(const std::string& guid) { session->view().releaseSnapshotFx(guid); }
// saveViewStateOnly writes the ACTIVE project and takes none, so re-read that rather
// than trust the project this tick pinned: a write pumps, and a tab switch inside one
// would otherwise land this model in the wrong project's ext state. Skipping is
// self-healing — coming back to the tab reloads the model from the record still on
// disk, and the re-derived online writes nothing and releases again. Promptless,
// unlike design_view_actions' persistViewState, which can open a modal Save-As.
void persistModel() {
if (EnumProjects(-1, nullptr, 0) != project) return;
session->saveViewStateOnly();
// The spend lands in memory here and reaches disk on the project's next save.
void releaseFx(const std::string& guid) {
session->view().releaseSnapshotFx(guid);
g_releaseFlush.arm(project);
}
};
@@ -212,4 +213,15 @@ void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
if (!run.aborted && run.settled) g_queue.goIdle();
}
void fxReclaimFlushRelease(ReaSamplerSession& session) {
// The save hook is handed no project, so the active one is the only candidate; a
// mismatch means the release belongs to a tab this save is not writing, and skipping
// leaves the arm standing for that tab's own save.
if (!g_releaseFlush.pendingFor(EnumProjects(-1, nullptr, 0))) return;
// Promptless, unlike design_view_actions' persistViewState, which can open a modal
// Save-As. Disarm only on a write that was ISSUED — a never-saved project writes
// nothing, and dropping the arm there would strand the spend for the whole session.
if (session.flushViewState()) g_releaseFlush.clear();
}
} // namespace reasampler