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:
@@ -211,6 +211,16 @@ bool ReaSamplerSession::saveViewStateOnly() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Its twin bar the dirty mark — the one line that differs is the whole point of the
|
||||
// split, so the two stay spelled out rather than folded behind a flag.
|
||||
bool ReaSamplerSession::flushViewState() {
|
||||
ReaProject* proj = activeSavedProject();
|
||||
if (!proj) return false;
|
||||
|
||||
writeViewState(proj, view_);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReaSamplerSession::writeAssignmentRequest(const std::string& wire) {
|
||||
ReaProject* proj = activeSavedProject();
|
||||
if (!proj) return false;
|
||||
|
||||
@@ -144,6 +144,13 @@ public:
|
||||
// Returns on the SAME rule as saveToActiveProject above, prohibition included.
|
||||
bool saveViewStateOnly();
|
||||
|
||||
// The same write with the dirty mark OMITTED, for a caller already inside REAPER's
|
||||
// own project save: marking dirty there would leave the project modified the instant
|
||||
// it was saved, which is the whole reason that caller exists (fx_reclaim's
|
||||
// ReleaseFlush). Not a general-purpose sibling — a mutation site that skips the dirty
|
||||
// mark silently loses its write when the user closes without saving.
|
||||
bool flushViewState();
|
||||
|
||||
// Report-only prune dry-run: feeds the pure core with (present, referenced,
|
||||
// owned) — `present` from the folder enumeration, the other two from the
|
||||
// tracking authority. FAIL-SAFE: tracking state the authority cannot read
|
||||
|
||||
@@ -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
|
||||
|
||||
+53
-22
@@ -173,18 +173,19 @@ struct ReclaimTickResult {
|
||||
// fx_reclaim.cpp holds the one live implementation. Host must provide:
|
||||
//
|
||||
// double nowMs() monotonic milliseconds
|
||||
// bool transportRolling() playing or recording — the tick
|
||||
// may move no plugins at all
|
||||
// bool liveChain(guid, std::vector<std::string>&) FX identity per live slot;
|
||||
// false when the track is gone
|
||||
// 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;
|
||||
// called by FxReclaimQueue::run
|
||||
// after the drain has unwound,
|
||||
// never from inside the drain
|
||||
// void persistModel() make the releases just applied
|
||||
// durable; run calls it ONCE per
|
||||
// tick and only after a release
|
||||
// void releaseFx(guid) release the record's FX half and
|
||||
// arm the save-time flush that
|
||||
// makes the spend durable; 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
|
||||
@@ -192,6 +193,18 @@ struct ReclaimTickResult {
|
||||
template <class Host>
|
||||
ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, Host& host) {
|
||||
ReclaimTickResult result;
|
||||
|
||||
// Mirrors applyMode's refusal (view.h, transportBlocksModeSwitch), and now covers
|
||||
// every FX write the tool makes, since this loop owns them all. BOTH directions, not
|
||||
// just the opportunistic offline one: onlining re-instantiates the very plugins the
|
||||
// user is listening through — the same destruction the switch already refuses — and a
|
||||
// step is a whole chain with no yield inside it. Nothing is observed on this path, so
|
||||
// every debounce hold keeps its origin and the first ungated tick fires with no wait.
|
||||
if (host.transportRolling()) {
|
||||
result.settled = queueSettled(items);
|
||||
return result;
|
||||
}
|
||||
|
||||
clock.observe(items, host.nowMs());
|
||||
|
||||
const int first = nextEligible(items, clock, host.nowMs());
|
||||
@@ -358,19 +371,9 @@ public:
|
||||
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.
|
||||
if (!tick.released.empty()) {
|
||||
for (const std::string& guid : tick.released) host.releaseFx(guid);
|
||||
// The spend has to be as durable as the record it spends. A reload — a
|
||||
// reopen, or the undo/redo loadFromProject — rebuilds the model from the
|
||||
// PERSISTED view state, and a record still held there re-derives this same
|
||||
// Online work and re-drives the chain, silently reverting whatever per-FX
|
||||
// offline the user set once the reclaim had finished. One persist per tick,
|
||||
// after every release the tick earned, and still killable in between: die
|
||||
// there and the record simply survives on disk, whereupon the re-derived
|
||||
// item finds the chain already online, writes nothing, and releases again.
|
||||
host.persistModel();
|
||||
}
|
||||
// release fires then. Applied in MEMORY only — the spend reaches disk on the
|
||||
// project's next save (ReleaseFlush), never on the drain's own initiative.
|
||||
for (const std::string& guid : tick.released) host.releaseFx(guid);
|
||||
return ReclaimRunResult{tick.writes, tick.settled, false, tick.drops};
|
||||
}
|
||||
|
||||
@@ -384,6 +387,30 @@ private:
|
||||
bool pending_ = false;
|
||||
};
|
||||
|
||||
// --- the release's durable half --------------------------------------------------
|
||||
|
||||
// A park record spent in MEMORY only, waiting for the project's next save.
|
||||
//
|
||||
// The drain used to persist it itself, and that dirtied the project: open one saved
|
||||
// mid-reclaim, let the drain finish, and the user is asked to save a project they only
|
||||
// opened. The tool no longer marks a project dirty on its own initiative — it writes what
|
||||
// it owes when the USER saves. The exposure that buys is the window between the two: die
|
||||
// in there and the record survives on disk unspent, so the re-derived Online item drives
|
||||
// the chain to it again, reverting any per-FX offline set since the reclaim finished.
|
||||
//
|
||||
// Keyed on the project because the save hook is handed none and the session's model is
|
||||
// whichever project is active: firing for a different one would land this model in
|
||||
// another project's ext state. A stale arm simply never fires — the same recovery leg.
|
||||
class ReleaseFlush {
|
||||
public:
|
||||
void arm(ReaProject* project) { project_ = project; }
|
||||
bool pendingFor(ReaProject* active) const { return project_ && project_ == active; }
|
||||
void clear() { project_ = nullptr; }
|
||||
|
||||
private:
|
||||
ReaProject* project_ = nullptr; // COMPARE-ONLY, never dereferenced
|
||||
};
|
||||
|
||||
// --- shell entry points --------------------------------------------------------
|
||||
|
||||
// The ONE test the timer makes per tick, mirroring the realtime capture's single
|
||||
@@ -408,8 +435,12 @@ bool fxReclaimDrainInProgress();
|
||||
// (FxReclaimQueue::beginTick). Returns without touching the project while an applyMode is
|
||||
// on the stack, while already draining, or when there is no valid active project at all.
|
||||
// Takes the SESSION rather than the model alone because completing an online spends the
|
||||
// park record's FX half, and that spend has to reach the persisted view state — see
|
||||
// FxReclaimQueue::run.
|
||||
// park record's FX half — see FxReclaimQueue::run.
|
||||
void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj);
|
||||
|
||||
// Writes an in-memory release into the active project's view state. Called from the
|
||||
// project-save hook and nowhere else: this is the ONE path that makes a spent record
|
||||
// durable, and it deliberately does not mark the project dirty (see ReleaseFlush).
|
||||
void fxReclaimFlushRelease(ReaSamplerSession& session);
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
Reference in New Issue
Block a user