Collapse a Design View mode switch and its background FX parking to ONE undo point
applyMode now returns with its undo block open; the reclaimer closes it when the drain settles. A transport deferral or a tab switch ends it early -- two points, never a leak.
This commit is contained in:
@@ -38,6 +38,28 @@ FxReclaimQueue g_queue;
|
||||
// A release applied to the in-memory model and not yet written — see ReleaseFlush.
|
||||
ReleaseFlush g_releaseFlush;
|
||||
|
||||
// The one open undo block a mode switch and its parking share — see UndoSpan.
|
||||
UndoSpan g_span;
|
||||
|
||||
// The ONE open and the ONE close. Both idempotent, so every caller may say either
|
||||
// unconditionally; the close reads the span's values before clearing it, because the
|
||||
// clear has to precede the SDK call — nothing re-entered through Undo_EndBlock2 may
|
||||
// find a span it could close a second time.
|
||||
void openSpan(ReaProject* proj, const char* label, int mask) {
|
||||
if (g_span.begin(proj, label, mask)) Undo_BeginBlock2(proj);
|
||||
}
|
||||
|
||||
void closeSpan() {
|
||||
if (!g_span.open()) return;
|
||||
ReaProject* const proj = g_span.project();
|
||||
const std::string label = g_span.label();
|
||||
const int mask = g_span.mask();
|
||||
g_span.end();
|
||||
// The span outlives its opener, so by here the project may be the one that just
|
||||
// closed — its undo history went with it and there is no block left to end.
|
||||
if (ValidatePtr2(nullptr, proj, "ReaProject*")) Undo_EndBlock2(proj, label.c_str(), mask);
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -132,16 +154,15 @@ struct ReclaimHost {
|
||||
if (fxIndex < 0 || fxIndex >= TrackFX_GetCount(tr)) return false; // chain shrank
|
||||
if (TrackFX_GetOffline(tr, fxIndex) == offline) return false; // already there
|
||||
|
||||
// DAW-VERIFIED: the same ten TrackFX_SetOffline calls mint ten undo points
|
||||
// unblocked and ZERO inside this pair. One block per WRITE even though a step is
|
||||
// now one whole track: the write itself is what pumps, so the block cannot avoid
|
||||
// spanning a pump — it can only span as little of one as possible. Widening it to
|
||||
// the track would multiply that exposure by the chain length while suppressing no
|
||||
// extra undo point (the discard form mints none either way), and a user action
|
||||
// landing inside a discarded block can lose its own Ctrl-Z.
|
||||
Undo_BeginBlock2(project);
|
||||
// Every write in the drain rides ONE span. Do not reintroduce a block per write:
|
||||
// it minted one undo point per FX toggle, because the ("", 0) discard form
|
||||
// coalesces rather than discards outside an action (see UndoSpan). The open is a
|
||||
// no-op under a switch's span, which is why the FX bit is added separately —
|
||||
// here, at the first real write, so a switch whose parking never matured closes
|
||||
// on the flags-and-items mask it opened with and serializes no FX state.
|
||||
openSpan(project, kReclaimUndoLabel, UNDO_STATE_FX);
|
||||
g_span.widen(UNDO_STATE_FX);
|
||||
TrackFX_SetOffline(tr, fxIndex, offline);
|
||||
Undo_EndBlock2(project, "", 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -188,6 +209,8 @@ void invalidateFxReclaim() {
|
||||
bool fxReclaimDrainInProgress() { return g_draining; }
|
||||
|
||||
void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
|
||||
// Neither guard may close the span: both name a caller that is still on the stack and
|
||||
// still owns it — the outer drain, or the applyMode that opened it. Both re-tick.
|
||||
if (g_draining) return;
|
||||
// The model is mid-rewrite and its records are being consumed out from under any
|
||||
// plan derived from it. The apply invalidates on its way out, so nothing is lost.
|
||||
@@ -200,6 +223,12 @@ void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
|
||||
// that gate that poll.
|
||||
ReaProject* project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||
if (!ValidatePtr2(nullptr, project, "ReaProject*")) project = nullptr;
|
||||
|
||||
// A span belongs to the project it was opened on. Keyed on the SPAN's own project
|
||||
// rather than the queue's pin, which moves independently: a tab switch, a closed
|
||||
// project, or no active project at all ends it where it stands rather than carrying
|
||||
// it into another project's history — and the no-project arm below returns.
|
||||
if (g_span.open() && g_span.project() != project) closeSpan();
|
||||
if (!g_queue.beginTick(project)) return;
|
||||
|
||||
// DERIVED, never persisted: recomputed from view state every time the queue was
|
||||
@@ -213,9 +242,20 @@ void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
|
||||
ReclaimHost host{project, &session};
|
||||
const ReclaimRunResult run = g_queue.run(host);
|
||||
reportDrops(project, run.drops);
|
||||
if (spanEndsAfter(run)) closeSpan();
|
||||
if (!run.aborted && run.settled) g_queue.goIdle();
|
||||
}
|
||||
|
||||
void fxReclaimBeginUndoSpan(ReaProject* proj) {
|
||||
openSpan(proj, "", 0);
|
||||
}
|
||||
|
||||
void fxReclaimLabelUndoSpan(const char* label, int mask) {
|
||||
g_span.relabel(label ? label : "", mask);
|
||||
}
|
||||
|
||||
void fxReclaimEndUndoSpan() { closeSpan(); }
|
||||
|
||||
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
|
||||
@@ -224,6 +264,9 @@ void fxReclaimFlushRelease(ReaSamplerSession& session) {
|
||||
// the project actually being written, or match the arm against the active tab while a
|
||||
// different project saved and write into that wrong project's ext state.
|
||||
ReaProject* saving = GetCurrentProjectInLoadSave();
|
||||
// BEFORE the arm test, and unconditionally: a save is a user act the span must not
|
||||
// absorb, and the arm is not what decides whether one is open.
|
||||
closeSpan();
|
||||
if (!g_releaseFlush.pendingFor(saving)) return;
|
||||
// Promptless, unlike design_view_actions' persistViewState, which can open a modal
|
||||
// Save-As. [verify — DAW] whether this write reaches the <EXTSTATE> block of the
|
||||
|
||||
Reference in New Issue
Block a user