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:
+193
-1
@@ -6,7 +6,9 @@
|
||||
// the user's save rather than on the drain, and — via a host that invalidates from inside
|
||||
// a write — that a re-entered invalidation reaches nothing the drain is using. The queue
|
||||
// owns the project pin and the timer's wakeup for the same reason: a compare-only pointer
|
||||
// makes the tab switch assertable.
|
||||
// makes the tab switch assertable. The undo span is here on the same terms: its state
|
||||
// machine is pure, so open-once/close-once and the wait it may or may not outlast are
|
||||
// assertable; only the Undo_BeginBlock2/EndBlock2 calls themselves need a DAW.
|
||||
|
||||
#include "../src/shell/view/fx_reclaim.h"
|
||||
|
||||
@@ -59,6 +61,12 @@ struct FakeHost {
|
||||
|
||||
bool rolling = false; // the transport gate's input
|
||||
|
||||
// Stands in for the shell's one span instance: fx_reclaim.cpp opens it on the first
|
||||
// real write of a drain, exactly here, and widens it to carry the FX bit. Only the
|
||||
// Undo_BeginBlock2/EndBlock2 calls themselves are out of reach without a DAW.
|
||||
UndoSpan* span = nullptr;
|
||||
int spanOpens = 0;
|
||||
|
||||
double nowMs() const { return clock; }
|
||||
bool transportRolling() const { return rolling; }
|
||||
|
||||
@@ -76,6 +84,10 @@ struct FakeHost {
|
||||
if (fxIndex < 0 || fxIndex >= static_cast<int>(it->second.size())) return false;
|
||||
if (it->second[static_cast<std::size_t>(fxIndex)].offline == offline) return false;
|
||||
|
||||
if (span) {
|
||||
if (span->begin(project, kReclaimUndoLabel, 2)) ++spanOpens;
|
||||
span->widen(2);
|
||||
}
|
||||
it->second[static_cast<std::size_t>(fxIndex)].offline = offline;
|
||||
clock += stepCostMs;
|
||||
writeLog.push_back(guid + ":" + std::to_string(fxIndex) + (offline ? "=off" : "=on"));
|
||||
@@ -1011,6 +1023,180 @@ static void testAGatedTickStillGoesIdleWithNothingLeftToConverge() {
|
||||
CHECK(q.run(host).settled);
|
||||
}
|
||||
|
||||
// -- 10. the undo span ----------------------------------------------------------------
|
||||
|
||||
static void testTheSpanOpensOnceAndClosesOnceHoweverManyTimesItIsAsked() {
|
||||
// The whole safety of the design is in these two answers: a caller may say begin or
|
||||
// end unconditionally and still make no second Undo_BeginBlock2 and no unbalanced
|
||||
// Undo_EndBlock2. A leaked open block swallows everything the user does afterwards.
|
||||
UndoSpan span;
|
||||
CHECK(!span.open());
|
||||
CHECK(!span.end()); // nothing open — the caller owes REAPER nothing
|
||||
|
||||
CHECK(span.begin(kProjA, "ReaSampler: activate Design view", 9));
|
||||
CHECK(span.open());
|
||||
CHECK(span.project() == kProjA);
|
||||
CHECK(span.label() == "ReaSampler: activate Design view");
|
||||
CHECK(span.mask() == 9);
|
||||
|
||||
// The drain's own begin, arriving under a switch's span: it must adopt nothing —
|
||||
// neither the other project, nor a label that would rename the user's switch.
|
||||
CHECK(!span.begin(kProjB, kReclaimUndoLabel, 2));
|
||||
CHECK(span.project() == kProjA);
|
||||
CHECK(span.label() == "ReaSampler: activate Design view");
|
||||
CHECK(span.mask() == 9);
|
||||
|
||||
span.widen(2); // ...but the FX bit it DID write under still has to reach the point
|
||||
CHECK(span.mask() == 11);
|
||||
|
||||
span.relabel("ReaSampler: activate Arrange view", 1);
|
||||
CHECK(span.label() == "ReaSampler: activate Arrange view");
|
||||
CHECK(span.mask() == 1);
|
||||
|
||||
CHECK(span.end());
|
||||
CHECK(!span.open());
|
||||
CHECK(!span.end()); // the second close reaches nothing
|
||||
CHECK(span.project() == nullptr);
|
||||
|
||||
// And nothing a closed span carries can be revived out from under the next opener.
|
||||
span.widen(4);
|
||||
span.relabel("stale", 8);
|
||||
CHECK(span.mask() == 0);
|
||||
CHECK(span.label().empty());
|
||||
}
|
||||
|
||||
static void testTheSpanOutlastsADebounceWaitButNeverATransportDeferral() {
|
||||
// The one policy call: the span may stay open across a wait it can BOUND. The 3s
|
||||
// debounce is the dead time between the switch and its first FX write, and spanning
|
||||
// it is the whole point. A transport deferral lasts as long as the user plays.
|
||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||
FakeHost host;
|
||||
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||
const ReclaimRunResult holding = q.run(host); // the hold starts at t=0
|
||||
CHECK(!holding.settled);
|
||||
CHECK(!holding.transportDeferred);
|
||||
CHECK(!spanEndsAfter(holding)); // bounded by kParkHoldMs — the span holds
|
||||
|
||||
host.clock += kParkHoldMs;
|
||||
host.rolling = true;
|
||||
const ReclaimRunResult gated = q.run(host);
|
||||
CHECK(gated.writes == 0);
|
||||
CHECK(!gated.settled);
|
||||
CHECK(gated.transportDeferred);
|
||||
CHECK(spanEndsAfter(gated)); // unbounded — the switch's point closes without it
|
||||
|
||||
host.rolling = false;
|
||||
const ReclaimRunResult done = q.run(host);
|
||||
CHECK(done.writes == 1);
|
||||
CHECK(done.settled);
|
||||
CHECK(!done.transportDeferred);
|
||||
CHECK(spanEndsAfter(done));
|
||||
}
|
||||
|
||||
static void testAnAbortEndsTheSpanSoNoLaterSwitchNestsUnderIt() {
|
||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||
FakeHost host;
|
||||
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||
|
||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||
q.run(host);
|
||||
host.clock += kParkHoldMs;
|
||||
|
||||
host.reentrant = &q;
|
||||
host.invalidateAfterWrite = 1;
|
||||
const ReclaimRunResult aborted = q.run(host);
|
||||
CHECK(aborted.aborted);
|
||||
CHECK(!aborted.settled);
|
||||
CHECK(spanEndsAfter(aborted)); // the model moved; this span's work is superseded
|
||||
}
|
||||
|
||||
static void testOneSwitchAndItsMultiTickParkingAreOneSpan() {
|
||||
// Three tracks at a cost that admits one per tick, so the parking really does span
|
||||
// several timer callbacks — the case a per-write block turned into one undo point per
|
||||
// FX toggle. The span must open exactly once and close exactly once, on the tick that
|
||||
// settles, under the SWITCH's label rather than the reclaimer's.
|
||||
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}"}},
|
||||
{"{T2}", {"{FX-B}"}},
|
||||
{"{T3}", {"{FX-C}"}}});
|
||||
FakeHost host;
|
||||
host.chains["{T1}"] = onlineChain({"{FX-A}"});
|
||||
host.chains["{T2}"] = onlineChain({"{FX-B}"});
|
||||
host.chains["{T3}"] = onlineChain({"{FX-C}"});
|
||||
host.stepCostMs = 50.0; // dearer than kOfflineBudgetMs ⇒ one track per tick
|
||||
|
||||
// What applyMode leaves behind on a real switch: an open span, its label set, and no
|
||||
// close — the reclaimer's to finish.
|
||||
UndoSpan span;
|
||||
CHECK(span.begin(kProjA, "", 0));
|
||||
span.relabel("ReaSampler: activate Design view", 9);
|
||||
host.span = &span;
|
||||
|
||||
int closes = 0;
|
||||
FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}", "{T3}"});
|
||||
auto tick = [&]() {
|
||||
const ReclaimRunResult r = q.run(host);
|
||||
if (spanEndsAfter(r) && span.end()) ++closes;
|
||||
return r;
|
||||
};
|
||||
|
||||
CHECK(!tick().settled); // hold starts
|
||||
host.clock += kParkHoldMs;
|
||||
CHECK(!tick().settled); // {T1}
|
||||
CHECK(!tick().settled); // {T2}
|
||||
CHECK(tick().settled); // {T3}, and the queue is exhausted in the same pass
|
||||
|
||||
CHECK(host.writeLog.size() == 3);
|
||||
CHECK(host.spanOpens == 0); // the switch's span was already open — no second begin
|
||||
CHECK(closes == 1); // ...and exactly one close, on the settling tick
|
||||
CHECK(!span.open());
|
||||
|
||||
// Extra ticks after the settle must not produce a second close.
|
||||
tick();
|
||||
CHECK(closes == 1);
|
||||
}
|
||||
|
||||
static void testADrainNoSwitchOpenedMintsOnePointOfItsOwn() {
|
||||
// The non-switch origins — a membership edit's reapply, the load tick, a drain
|
||||
// resuming after the transport stopped. Nobody opened a span, so the drain opens its
|
||||
// own: one point for the whole convergence, never one per FX toggle.
|
||||
ViewModeModel vm = parkedDesignLeaves({{"{T1}", {"{FX-A}", "{FX-B}"}},
|
||||
{"{T2}", {"{FX-C}"}}});
|
||||
FakeHost host;
|
||||
host.chains["{T1}"] = onlineChain({"{FX-A}", "{FX-B}"});
|
||||
host.chains["{T2}"] = onlineChain({"{FX-C}"});
|
||||
host.stepCostMs = 50.0;
|
||||
|
||||
UndoSpan span;
|
||||
host.span = &span;
|
||||
|
||||
int closes = 0;
|
||||
FxReclaimQueue q = queueOf(vm, {"{T1}", "{T2}"});
|
||||
auto tick = [&]() {
|
||||
const ReclaimRunResult r = q.run(host);
|
||||
if (spanEndsAfter(r) && span.end()) ++closes;
|
||||
return r;
|
||||
};
|
||||
|
||||
CHECK(!tick().settled); // hold starts; nothing written, so nothing opened yet
|
||||
CHECK(!span.open());
|
||||
CHECK(host.spanOpens == 0);
|
||||
|
||||
host.clock += kParkHoldMs;
|
||||
CHECK(!tick().settled); // {T1}'s chain: the first write opens the span
|
||||
CHECK(host.spanOpens == 1);
|
||||
CHECK(span.open());
|
||||
CHECK(span.label() == kReclaimUndoLabel);
|
||||
CHECK(span.project() == kProjA);
|
||||
|
||||
CHECK(tick().settled); // {T2}, still under the SAME span
|
||||
CHECK(host.spanOpens == 1);
|
||||
CHECK(host.writeLog.size() == 3);
|
||||
CHECK(closes == 1);
|
||||
CHECK(!span.open());
|
||||
}
|
||||
|
||||
int main() {
|
||||
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
||||
|
||||
@@ -1048,6 +1234,12 @@ int main() {
|
||||
testAHoldDropsWhenItsTrackStopsOwingOfflineWorkWhileRolling();
|
||||
testAGatedTickStillGoesIdleWithNothingLeftToConverge();
|
||||
|
||||
testTheSpanOpensOnceAndClosesOnceHoweverManyTimesItIsAsked();
|
||||
testTheSpanOutlastsADebounceWaitButNeverATransportDeferral();
|
||||
testAnAbortEndsTheSpanSoNoLaterSwitchNestsUnderIt();
|
||||
testOneSwitchAndItsMultiTickParkingAreOneSpan();
|
||||
testADrainNoSwitchOpenedMintsOnePointOfItsOwn();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user