Narrow the FX reclaimer's transport gate to defer offline work only; let online drain through, and always run the park clock's observe

This commit is contained in:
2026-08-06 23:04:02 -04:00
parent c95203718a
commit 4f6cf60740
3 changed files with 120 additions and 36 deletions
+92 -19
View File
@@ -869,7 +869,7 @@ static void testATickThatLostNothingNeitherPrintsNorDisturbsTheDropMemo() {
// -- 9. the transport gate ------------------------------------------------------------
static void testARollingTransportMovesNoPluginsAndKeepsTheHold() {
static void testARollingTransportDefersOfflineWorkAndKeepsTheHold() {
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
FakeHost host;
host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}"});
@@ -883,7 +883,7 @@ static void testARollingTransportMovesNoPluginsAndKeepsTheHold() {
CHECK(gated.writes == 0);
CHECK(host.writeLog.empty());
CHECK(!gated.aborted);
CHECK(!gated.settled); // the work is still owed, so the shell must not go idle
CHECK(!gated.settled); // the work is still owed — a gated item is deferred, not dropped
CHECK(!q.stale()); // and the queue it will resume from survives untouched
// The hold kept its origin across the gated tick: the drain fires on the FIRST tick
@@ -893,9 +893,12 @@ static void testARollingTransportMovesNoPluginsAndKeepsTheHold() {
CHECK(host.writeLog.size() == 2);
}
static void testTheGateStopsTheOnlineDirectionToo() {
// The ruling is symmetric because both directions load or unload plugins: onlining
// re-instantiates the very chain the user is listening through.
static void testOnlineWorkProceedsWhileTheTransportRolls() {
// Daniel's ruling: the gate narrows to the offline direction. Onlining is the mode
// switch's own unfinished promise — restoreTrack already handed back visibility and
// B_MAINSEND/I_FXEN synchronously — so a full gate would leave the user listening to a
// dry, FX-enabled chain for the whole rest of the playback pass. Onlining still costs
// a plugin load mid-roll; that is the accepted trade, not a regression this test flags.
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
vm.setActiveMode(kDesignModeId);
vm.releaseSnapshotFlags("{T}");
@@ -909,19 +912,87 @@ static void testTheGateStopsTheOnlineDirectionToo() {
FxReclaimQueue q = queueOf(vm, {"{T}"});
CHECK(q.items().size() == 1 && q.items()[0].work.kind == FxWorkKind::Online);
const ReclaimRunResult gated = q.run(host);
CHECK(gated.writes == 0);
CHECK(!gated.settled);
CHECK(host.released.empty()); // no record spent while the transport rolls
CHECK(!flush.pendingFor(kProjA));
CHECK(host.offlineAt("{T}", 0)); // the chain is untouched, still parked
// The contrast, so the assertions above are about the gate and not about an online
// queue that cannot drain: stop the transport and the same tick completes.
host.rolling = false;
CHECK(q.run(host).settled);
const ReclaimRunResult run = q.run(host);
CHECK(run.writes == 2);
CHECK(run.settled);
CHECK(host.writeLog.size() == 2);
CHECK(host.released.count("{T}") == 1);
CHECK(host.released.count("{T}") == 1); // the record's FX half is spent
CHECK(flush.pendingFor(kProjA));
CHECK(!host.offlineAt("{T}", 0) && !host.offlineAt("{T}", 1));
}
static void testOfflineWorkStaysGatedWhileOnlineDrainsInTheSameTick() {
// A mixed queue while rolling: {Z-back} is owed an online, {A-park}'s hold is already
// matured and owed an offline. Only the online direction may move this tick.
ViewModeModel vm;
vm.membership().tag("{Z-back}", kArrangeModeId);
vm.membership().tag("{A-park}", kDesignModeId);
vm.storeSnapshot("{Z-back}", recordWithFx({{"{FX-1}", 0}}));
vm.storeSnapshot("{A-park}", recordWithFx({{"{FX-2}", 0}}));
FakeHost host;
host.chains["{Z-back}"] = offlineChain({"{FX-1}"});
host.chains["{A-park}"] = onlineChain({"{FX-2}"});
ParkClock clock;
std::vector<ReclaimItem> items = derive(vm, {"{Z-back}", "{A-park}"});
CHECK(items[0].work.kind == FxWorkKind::Online);
matureHold(items, clock, host); // {A-park}'s hold matures with time to spare below
host.rolling = true;
host.clock += kParkHoldMs;
const ReclaimTickResult tick = drainTick(items, clock, host);
CHECK(tick.writes == 1);
CHECK(host.writeLog == std::vector<std::string>{"{Z-back}:0=on"});
CHECK(!host.offlineAt("{A-park}", 0)); // matured but gated — untouched this tick
CHECK(!tick.settled); // the offline item is still owed
// The transport stops: the very next tick reclaims it, with no fresh hold to wait out.
host.rolling = false;
CHECK(drainTick(items, clock, host).settled);
CHECK(host.offlineAt("{A-park}", 0));
}
static void testAHoldDropsWhenItsTrackStopsOwingOfflineWorkWhileRolling() {
// ParkClock's own contract (fx_reclaim.h) says an entry is dropped the moment its
// track stops owing offline work — observe() has to run every tick, rolling or not,
// for that to hold once a rolling tick can itself change what a track owes (by
// draining its online item). This is the case that contract would otherwise miss.
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
FakeHost host;
// Offline in the DAW, matching a track truly parked — so bringing it online below is a
// real write, not a compare-before-write no-op.
host.chains["{T}"] = offlineChain({"{FX-A}"});
ParkClock clock;
std::vector<ReclaimItem> items = derive(vm, {"{T}"});
CHECK(drainTick(items, clock, host).writes == 0); // the hold starts at t=0
CHECK(!clock.matured("{T}", 0.0));
// {T} comes back online while the transport rolls; the online direction still drains.
host.rolling = true;
vm.setActiveMode(kDesignModeId);
vm.releaseSnapshotFlags("{T}");
items = derive(vm, {"{T}"});
CHECK(items[0].work.kind == FxWorkKind::Online);
host.clock = 100.0;
CHECK(drainTick(items, clock, host).writes == 1);
CHECK(!clock.matured("{T}", host.clock)); // dropped: {T} owes no offline work now
// {T} parks again, still rolling: the hold must start FRESH from here, not resume the
// one interrupted above — proof the drop above was real, not a no-op.
vm.setActiveMode(kArrangeModeId);
vm.storeSnapshot("{T}", recordWithFx({{"{FX-A}", 0}}));
items = derive(vm, {"{T}"});
CHECK(items[0].work.kind == FxWorkKind::Offline);
const double repark = host.clock;
CHECK(!drainTick(items, clock, host).settled); // gated, and not matured either
CHECK(!clock.matured("{T}", repark + kParkHoldMs - 1.0));
CHECK(clock.matured("{T}", repark + kParkHoldMs));
host.rolling = false;
host.clock = repark + kParkHoldMs;
CHECK(drainTick(items, clock, host).writes == 1); // matures once the gate lifts
}
static void testAGatedTickStillGoesIdleWithNothingLeftToConverge() {
@@ -971,8 +1042,10 @@ int main() {
testOnlyANoProjectTickClearsTheWakeup();
testATickThatLostNothingNeitherPrintsNorDisturbsTheDropMemo();
testARollingTransportMovesNoPluginsAndKeepsTheHold();
testTheGateStopsTheOnlineDirectionToo();
testARollingTransportDefersOfflineWorkAndKeepsTheHold();
testOnlineWorkProceedsWhileTheTransportRolls();
testOfflineWorkStaysGatedWhileOnlineDrainsInTheSameTick();
testAHoldDropsWhenItsTrackStopsOwingOfflineWorkWhileRolling();
testAGatedTickStillGoesIdleWithNothingLeftToConverge();
if (g_fail == 0) std::printf("All tests passed.\n");