Revert Ω-W2-T4: FX-park state stranded across save/reopen

Backs out the deferred park drain split. Forced synchronous FX-park
drains return, and with them the mode-switch hitch, until the
re-snapshot-after-discard fix lands.
This commit is contained in:
2026-08-03 18:05:04 -04:00
7 changed files with 104 additions and 401 deletions
+10 -144
View File
@@ -4,11 +4,7 @@
// The properties under test: a mode switch leaves its per-FX offline work here,
// so a second switch arriving before the first drained must leave every track in
// the state the SECOND switch specifies — never the first's, never both replayed;
// a cancel must not strand the pre-park FX state it was the last record of; the
// restore/park kind split must hold, restores detaching whole while parks stay
// queued across the ticks that apply them one FX at a time; and the coalescing
// delay debounces from each enqueue's own time rather than firing once per the
// first.
// and a cancel must not strand the pre-park FX state it was the last record of.
#include "../src/shell/view/view_fx_park.h"
@@ -37,32 +33,6 @@ static const FxParkIntent* intentFor(const FxParkQueue& q, const std::string& gu
return nullptr;
}
// -- the coalescing gate ------------------------------------------------------
static void testCoalesceDebouncesFromEachEnqueuesOwnTimeNotTheFirst() {
double readyAt = parkReadyAt(0.0, 1.0);
CHECK(!parkIsReady(0.9, readyAt));
// A second enqueue arrives before the first's delay elapsed. A true debounce
// re-arms from THIS call's time; a one-shot would leave readyAt at 1.0 and
// this enqueue would have no effect.
readyAt = parkReadyAt(0.5, 1.0);
CHECK(!parkIsReady(1.0, readyAt)); // a one-shot would already be ready here
CHECK(parkIsReady(1.5, readyAt));
}
static void testRapidAToBToAStillCostsOneWaitFromTheLastFlip() {
// Three enqueues inside one delay window (A→B→A) still produce exactly one
// wait, measured from the LAST enqueue — not three separate timers and not
// one anchored to the first.
double readyAt = parkReadyAt(0.0, 1.0);
readyAt = parkReadyAt(0.3, 1.0);
readyAt = parkReadyAt(0.6, 1.0);
CHECK(!parkIsReady(1.5, readyAt));
CHECK(parkIsReady(1.6, readyAt));
}
// -- tests -------------------------------------------------------------------
static void testParkEnqueuesOneIntentCarryingNoOps() {
@@ -263,126 +233,30 @@ static void testRestorePlanOpsRebuildTheSlotKeyedSnapshotVerbatim() {
CHECK(rebuilt.states == snap.fxOffline);
}
// -- the kind split ----------------------------------------------------------
// -- re-entrancy -------------------------------------------------------------
static void testTakeRestoresDetachesRestoresAndLeavesParksQueued() {
// The forced drain's slice: a restore is unsafe to persist over and goes now,
// a park is safe and stays for the idle tick to work one FX at a time.
static void testTakeDetachesEverythingAndLeavesTheQueueEmpty() {
FxParkQueue q;
q.park("{A}");
q.restore("{B}", ops("{FX}", true));
q.park("{C}");
q.restore("{D}", ops("{FX}", false));
const std::vector<FxParkIntent> taken = q.takeRestores();
const std::vector<FxParkIntent> taken = q.take();
CHECK(taken.size() == 2);
CHECK(taken.size() == 2 && taken[0].guid == "{B}" && !taken[0].park);
CHECK(taken.size() == 2 && taken[1].guid == "{D}" && !taken[1].park);
// Enqueue order is apply order on BOTH sides of the slice.
CHECK(q.pending().size() == 2);
CHECK(q.pending().size() == 2 && q.pending()[0].guid == "{A}" && q.pending()[0].park);
CHECK(q.pending().size() == 2 && q.pending()[1].guid == "{C}" && q.pending()[1].park);
}
static void testTakeRestoresWithOnlyParksQueuedTakesNothing() {
FxParkQueue q;
q.park("{A}");
CHECK(q.takeRestores().empty());
CHECK(q.pending().size() == 1); // the park is not this drain's to consume
}
static void testNextParkGuidWalksParksInEnqueueOrderAndSkipsRestores() {
FxParkQueue q;
q.restore("{R}", ops("{FX}", false));
q.park("{A}");
q.park("{B}");
// A PEEK: repeated reads answer the same until the park is retired, and a
// restore is never handed to the park tick.
CHECK(q.nextParkGuid() == "{A}");
CHECK(q.nextParkGuid() == "{A}");
q.finishPark("{A}");
CHECK(q.nextParkGuid() == "{B}");
q.finishPark("{B}");
CHECK(q.nextParkGuid().empty());
CHECK(q.pending().size() == 1); // park progress never consumed the restore
CHECK(intentFor(q, "{R}") != nullptr);
}
static void testFinishParkNeverRetiresARestoreStandingAtThatGuid() {
FxParkQueue q;
q.park("{A}");
q.markPartial("{A}");
q.restore("{A}", ops("{FX}", false));
q.finishPark("{A}");
CHECK(q.pending().size() == 1);
CHECK(intentFor(q, "{A}") && !intentFor(q, "{A}")->park);
}
// -- the lazy park's cancel semantics ----------------------------------------
static void testAnUnstartedParkCancelledByItsRestoreCostsZeroWork() {
// The headline property of deferring the park: A→B parks {A}, B→A restores it
// before the coalescing delay let one FX move, and the flip costs no plugin
// load or unload at all — neither half of the drain has anything left to do.
FxParkQueue q;
q.park("{A}");
q.restore("{A}", ops("{FX}", false));
CHECK(taken.size() == 2 && taken[0].guid == "{A}" && taken[0].park);
CHECK(taken.size() == 2 && taken[1].guid == "{B}" && !taken[1].park);
CHECK(q.empty());
CHECK(q.nextParkGuid().empty());
CHECK(q.takeRestores().empty());
}
static void testAParkThatAlreadyWroteIsSupersededByItsRestoreNotCancelled() {
// One FX is offline, so the chain matches NEITHER endpoint. Annihilating here
// would leave it offline with the ops that describe its prior state dropped.
FxParkQueue q;
q.park("{A}");
CHECK(q.nextParkGuid() == "{A}");
q.markPartial("{A}"); // the tick is about to write this track's first FX
q.restore("{A}", ops("{FX}", false));
CHECK(q.pending().size() == 1);
const FxParkIntent* held = intentFor(q, "{A}");
CHECK(held && !held->park);
CHECK(held && held->restoreOps.size() == 1 && held->restoreOps.front().fxGuid == "{FX}");
CHECK(q.nextParkGuid().empty()); // no park work left — the restore owns the chain
}
static void testAParkOnASupersededRestoreResumesRatherThanAnnihilates() {
// The mirror hazard: the restore also never ran, so the chain is still half
// parked. Cancelling outright would strand every FX the first pass had not
// reached yet — the park must resume, and still hand back the pre-park ops.
FxParkQueue q;
q.park("{A}");
q.markPartial("{A}");
q.restore("{A}", ops("{FX}", false));
const std::vector<FxOfflineOp> cancelled = q.park("{A}");
CHECK(cancelled.size() == 1);
CHECK(cancelled.size() == 1 && cancelled.front().fxGuid == "{FX}");
CHECK(q.nextParkGuid() == "{A}");
CHECK(intentFor(q, "{A}") && intentFor(q, "{A}")->park);
}
// -- re-entrancy -------------------------------------------------------------
static void testIntentsArrivingDuringADrainSurviveIt() {
// [verify — DAW] applying an intent loads/unloads plugins, which is ASSUMED to
// pump the message loop, so a switch can re-enter and enqueue mid-drain. Those
// intents belong to the NEXT drain — the one in progress must neither see them
// nor discard them.
FxParkQueue q;
q.restore("{A}", ops("{FX}", true));
q.park("{A}");
const std::vector<FxParkIntent> draining = q.takeRestores();
const std::vector<FxParkIntent> draining = q.take();
q.restore("{B}", ops("{FX}", false)); // arrives while {A} is being applied
CHECK(draining.size() == 1);
@@ -397,7 +271,7 @@ static void testAReEntrantParkCancelsOnlyWhatIsStillPending() {
// silently annihilate against an intent that has already been applied.
FxParkQueue q;
q.restore("{A}", ops("{FX}", false));
q.takeRestores();
q.take();
const std::vector<FxOfflineOp> cancelled = q.park("{A}");
@@ -407,8 +281,6 @@ static void testAReEntrantParkCancelsOnlyWhatIsStillPending() {
}
int main() {
testCoalesceDebouncesFromEachEnqueuesOwnTimeNotTheFirst();
testRapidAToBToAStillCostsOneWaitFromTheLastFlip();
testParkEnqueuesOneIntentCarryingNoOps();
testParkReportsNothingCancelledWhenNoIntentWasPending();
testParkOnItsOwnPendingParkReportsNothingCancelled();
@@ -425,13 +297,7 @@ int main() {
testSlotKeyedRestoreDoesNotBecomeIdentityKeyedWithNoIdentities();
testRestorePlanOpsRebuildTheIdentityKeyedSnapshotVerbatim();
testRestorePlanOpsRebuildTheSlotKeyedSnapshotVerbatim();
testTakeRestoresDetachesRestoresAndLeavesParksQueued();
testTakeRestoresWithOnlyParksQueuedTakesNothing();
testNextParkGuidWalksParksInEnqueueOrderAndSkipsRestores();
testFinishParkNeverRetiresARestoreStandingAtThatGuid();
testAnUnstartedParkCancelledByItsRestoreCostsZeroWork();
testAParkThatAlreadyWroteIsSupersededByItsRestoreNotCancelled();
testAParkOnASupersededRestoreResumesRatherThanAnnihilates();
testTakeDetachesEverythingAndLeavesTheQueueEmpty();
testIntentsArrivingDuringADrainSurviveIt();
testAReEntrantParkCancelsOnlyWhatIsStillPending();