view: split the deferred FX-park drain by kind — restores stay forced, parks go one FX per idle tick behind a 1s coalescing delay
A rapid A→B→A flip now costs no plugin work: the restore cancels the still-pending park outright. A park that has already written one FX carries a `partial` flag and is superseded by its inverse rather than cancelled, so a half-parked chain is never stranded.
This commit is contained in:
+115
-11
@@ -4,7 +4,9 @@
|
||||
// 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;
|
||||
// and a cancel must not strand the pre-park FX state it was the last record of.
|
||||
// a cancel must not strand the pre-park FX state it was the last record of; and
|
||||
// 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.
|
||||
|
||||
#include "../src/shell/view/view_fx_park.h"
|
||||
|
||||
@@ -233,30 +235,126 @@ static void testRestorePlanOpsRebuildTheSlotKeyedSnapshotVerbatim() {
|
||||
CHECK(rebuilt.states == snap.fxOffline);
|
||||
}
|
||||
|
||||
// -- re-entrancy -------------------------------------------------------------
|
||||
// -- the kind split ----------------------------------------------------------
|
||||
|
||||
static void testTakeDetachesEverythingAndLeavesTheQueueEmpty() {
|
||||
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.
|
||||
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.take();
|
||||
const std::vector<FxParkIntent> taken = q.takeRestores();
|
||||
|
||||
CHECK(taken.size() == 2);
|
||||
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(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(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.park("{A}");
|
||||
q.restore("{A}", ops("{FX}", true));
|
||||
|
||||
const std::vector<FxParkIntent> draining = q.take();
|
||||
const std::vector<FxParkIntent> draining = q.takeRestores();
|
||||
q.restore("{B}", ops("{FX}", false)); // arrives while {A} is being applied
|
||||
|
||||
CHECK(draining.size() == 1);
|
||||
@@ -271,7 +369,7 @@ static void testAReEntrantParkCancelsOnlyWhatIsStillPending() {
|
||||
// silently annihilate against an intent that has already been applied.
|
||||
FxParkQueue q;
|
||||
q.restore("{A}", ops("{FX}", false));
|
||||
q.take();
|
||||
q.takeRestores();
|
||||
|
||||
const std::vector<FxOfflineOp> cancelled = q.park("{A}");
|
||||
|
||||
@@ -297,7 +395,13 @@ int main() {
|
||||
testSlotKeyedRestoreDoesNotBecomeIdentityKeyedWithNoIdentities();
|
||||
testRestorePlanOpsRebuildTheIdentityKeyedSnapshotVerbatim();
|
||||
testRestorePlanOpsRebuildTheSlotKeyedSnapshotVerbatim();
|
||||
testTakeDetachesEverythingAndLeavesTheQueueEmpty();
|
||||
testTakeRestoresDetachesRestoresAndLeavesParksQueued();
|
||||
testTakeRestoresWithOnlyParksQueuedTakesNothing();
|
||||
testNextParkGuidWalksParksInEnqueueOrderAndSkipsRestores();
|
||||
testFinishParkNeverRetiresARestoreStandingAtThatGuid();
|
||||
testAnUnstartedParkCancelledByItsRestoreCostsZeroWork();
|
||||
testAParkThatAlreadyWroteIsSupersededByItsRestoreNotCancelled();
|
||||
testAParkOnASupersededRestoreResumesRatherThanAnnihilates();
|
||||
testIntentsArrivingDuringADrainSurviveIt();
|
||||
testAReEntrantParkCancelsOnlyWhatIsStillPending();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user