Split FX out of the park plan: flag-only plans, derived online-first FX work list, two-half record release, applied-state fingerprint
This commit is contained in:
+100
-119
@@ -10,7 +10,7 @@
|
||||
// 4. show-both leaf never appears in a park op-list and is visible in all modes.
|
||||
// 5. Unknown/stale GUID tolerated (ignore-and-prune, no crash).
|
||||
// 6. JSON round-trip lossless: modes + membership + show-both + snapshots + active.
|
||||
// 7. planToggle park path: fxOffline is empty (shell-expands-FX contract).
|
||||
// 7. planToggle plans the FLAG domain only — FX is derived (core/view/fx_state).
|
||||
// 8. Per-FX offline is keyed by FX identity, and the v1 (slot-keyed) blob lifts
|
||||
// into that keying without losing its restore.
|
||||
// 9. Nested-folder toggle: the snapshot store/clear lifecycle survives a re-park
|
||||
@@ -66,7 +66,7 @@ static int flagValue(const TrackPlan& p, Flag f) {
|
||||
static void testSerializeGoldenLiteral() {
|
||||
ViewModeModel vm;
|
||||
CHECK(vm.serialize() ==
|
||||
"{\"version\":2,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\","
|
||||
"{\"version\":3,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\","
|
||||
"\"displayName\":\"Arrange\",\"ordinal\":0},{\"id\":\"design\","
|
||||
"\"displayName\":\"Design\",\"ordinal\":1}],\"membership\":[],"
|
||||
"\"snapshots\":[],\"lanes\":[],\"soloCache\":[]}");
|
||||
@@ -287,16 +287,15 @@ static void testRestoreRoundTripSnapshotValues() {
|
||||
snap.mainSend = 0; // user had it OUT of the mix for their own reason
|
||||
snap.fxEnable = 1;
|
||||
// Slot 1's plugin was already offline before parking; each entry carries the
|
||||
// identity of the FX it came from.
|
||||
// identity of the FX it came from. The FX half's own restore is fx_state's —
|
||||
// test_fx_state covers it; here it only has to survive alongside the flags.
|
||||
snap.fxOffline = {{"{FX-A}", 0}, {"{FX-B}", 1}, {"{FX-C}", 0}};
|
||||
|
||||
TrackPlan park = makeParkPlan("{T}", /*fxCount=*/3);
|
||||
TrackPlan park = makeParkPlan("{T}");
|
||||
CHECK(flagValue(park, Flag::ShowInTcp) == 0);
|
||||
CHECK(flagValue(park, Flag::ShowInMixer) == 0);
|
||||
CHECK(flagValue(park, Flag::MainSend) == 0);
|
||||
CHECK(flagValue(park, Flag::FxEnable) == 0);
|
||||
CHECK(park.fxOffline.size() == 3);
|
||||
for (const auto& op : park.fxOffline) CHECK(op.offline == true);
|
||||
|
||||
TrackPlan restore = makeRestorePlan("{T}", snap);
|
||||
// Every flag returns to its CAPTURED value — not a hardcoded "on".
|
||||
@@ -304,14 +303,6 @@ static void testRestoreRoundTripSnapshotValues() {
|
||||
CHECK(flagValue(restore, Flag::ShowInMixer) == 1);
|
||||
CHECK(flagValue(restore, Flag::MainSend) == 0); // the "already at 0 stays 0" case
|
||||
CHECK(flagValue(restore, Flag::FxEnable) == 1);
|
||||
CHECK(restore.fxOffline.size() == 3);
|
||||
CHECK(restore.fxOffline[0].offline == false);
|
||||
CHECK(restore.fxOffline[1].offline == true); // was offline pre-park ⇒ stays offline
|
||||
CHECK(restore.fxOffline[2].offline == false);
|
||||
// Each restore op names the FX it was captured from, not just a position —
|
||||
// resolveFxRestore has something to key on even if the chain moved.
|
||||
CHECK(restore.fxOffline[1].keying == FxKeying::Identity);
|
||||
CHECK(restore.fxOffline[1].fxGuid == "{FX-B}");
|
||||
|
||||
// A snapshot entirely at 0 must restore entirely to 0 (no default leaks in).
|
||||
TrackSnapshot zero; // all zeros, empty fxOffline
|
||||
@@ -320,7 +311,6 @@ static void testRestoreRoundTripSnapshotValues() {
|
||||
CHECK(flagValue(rz, Flag::ShowInMixer) == 0);
|
||||
CHECK(flagValue(rz, Flag::MainSend) == 0);
|
||||
CHECK(flagValue(rz, Flag::FxEnable) == 0);
|
||||
CHECK(rz.fxOffline.empty());
|
||||
|
||||
// End-to-end via planToggle: a leaf tagged Design, snapshotted, parked while in
|
||||
// Arrange, then restored when we toggle back to Design.
|
||||
@@ -500,29 +490,98 @@ static void testMalformedJson() {
|
||||
}
|
||||
}
|
||||
|
||||
// -- 7. planToggle park path: fxOffline is empty (shell-expands-FX contract) --
|
||||
// -- 7. planToggle plans the FLAG domain only --------------------------------
|
||||
//
|
||||
// planToggle calls makeParkPlan(guid, /*fxCount=*/0) for each inactive leaf.
|
||||
// The D2 shell is responsible for expanding per-FX offline ops using
|
||||
// TrackFX_GetCount — the pure model has no access to REAPER FX counts at plan
|
||||
// time. This test pins that contract so a regression that passes a non-zero
|
||||
// count (and emits FX ops prematurely) is caught immediately.
|
||||
// The direct makeParkPlan(guid, 3) path (non-zero fxCount) is covered by
|
||||
// testRestoreRoundTripSnapshotValues above.
|
||||
// A TogglePlan carries the four driven scalars and nothing else; per-FX offline
|
||||
// is derived from view state (core/view/fx_state), not planned beside them.
|
||||
|
||||
static void testPlanToggleParkHasEmptyFxOffline() {
|
||||
static void testPlanTogglePlansFlagsOnly() {
|
||||
ViewModeModel vm;
|
||||
FolderTree tree;
|
||||
tree.nodes.push_back(FolderNode{"{LEAF}", "", false});
|
||||
tree.nodes.push_back(FolderNode{"{BACK}", "", false});
|
||||
vm.membership().tag("{LEAF}", kDesignModeId);
|
||||
vm.membership().tag("{BACK}", kArrangeModeId);
|
||||
TrackSnapshot snap;
|
||||
snap.showInTcp = 1;
|
||||
snap.fxOffline = {{"{FX-A}", 1}};
|
||||
vm.storeSnapshot("{BACK}", snap);
|
||||
|
||||
// Toggle to Arrange: {LEAF} is inactive ⇒ park plan emitted.
|
||||
// Toggle to Arrange: {LEAF} is inactive ⇒ parked; {BACK} returns ⇒ restored.
|
||||
auto plan = vm.planToggle(tree, kArrangeModeId);
|
||||
CHECK(plan.park.size() == 1);
|
||||
// The park plan must have an empty fxOffline — the shell expands FX ops.
|
||||
CHECK(plan.park[0].fxOffline.empty());
|
||||
// Scalar flags must still be present (the four park zeros).
|
||||
CHECK(plan.park[0].flags.size() == 4);
|
||||
CHECK(plan.park[0].flags.size() == 4); // the four park zeros, nothing else
|
||||
CHECK(plan.restore.size() == 1);
|
||||
CHECK(plan.restore[0].flags.size() == 4);
|
||||
}
|
||||
|
||||
// A record whose flag half a restore already spent must never be replanned: its
|
||||
// values are zeroed, and restoring THOSE would silently re-park the track.
|
||||
static void testReleasedFlagHalfIsNotReplanned() {
|
||||
ViewModeModel vm;
|
||||
FolderTree tree;
|
||||
tree.nodes.push_back(FolderNode{"{T}", "", false});
|
||||
vm.membership().tag("{T}", kDesignModeId);
|
||||
TrackSnapshot snap;
|
||||
snap.showInTcp = 1; snap.showInMixer = 1; snap.mainSend = 1; snap.fxEnable = 1;
|
||||
snap.fxOffline = {{"{FX-A}", 1}};
|
||||
vm.storeSnapshot("{T}", snap);
|
||||
|
||||
CHECK(vm.holdsSnapshotFlags("{T}"));
|
||||
CHECK(vm.holdsSnapshotFx("{T}"));
|
||||
CHECK(restoreFor(vm.planToggle(tree, kDesignModeId), "{T}") != nullptr);
|
||||
|
||||
vm.releaseSnapshotFlags("{T}");
|
||||
// The FX half OUTLIVES the flag half — it is the only record of which plugins
|
||||
// the user had offline before the park.
|
||||
CHECK(!vm.holdsSnapshotFlags("{T}"));
|
||||
CHECK(vm.holdsSnapshotFx("{T}"));
|
||||
CHECK(vm.snapshot("{T}") != nullptr);
|
||||
CHECK(vm.snapshot("{T}")->fxOffline.size() == 1);
|
||||
CHECK(restoreFor(vm.planToggle(tree, kDesignModeId), "{T}") == nullptr);
|
||||
|
||||
// Spending the second half is what retires the record.
|
||||
vm.releaseSnapshotFx("{T}");
|
||||
CHECK(!vm.holdsSnapshotFx("{T}"));
|
||||
CHECK(vm.snapshot("{T}") == nullptr);
|
||||
|
||||
// The other release order retires it just the same.
|
||||
vm.storeSnapshot("{T}", snap);
|
||||
vm.releaseSnapshotFx("{T}");
|
||||
CHECK(vm.holdsSnapshotFlags("{T}") && !vm.holdsSnapshotFx("{T}"));
|
||||
CHECK(vm.snapshot("{T}") && vm.snapshot("{T}")->fxOffline.empty());
|
||||
vm.releaseSnapshotFlags("{T}");
|
||||
CHECK(vm.snapshot("{T}") == nullptr);
|
||||
|
||||
// Releasing a half of a record that does not exist is a no-op, not a mint.
|
||||
vm.releaseSnapshotFlags("{GONE}");
|
||||
vm.releaseSnapshotFx("{GONE}");
|
||||
CHECK(vm.snapshot("{GONE}") == nullptr);
|
||||
}
|
||||
|
||||
// A half-released record has to survive the .rpp: the FX half outliving the flag
|
||||
// half is only useful if a save-while-draining reloads in the same state.
|
||||
static void testHalfReleasedRecordRoundTrips() {
|
||||
ViewModeModel vm;
|
||||
TrackSnapshot snap;
|
||||
snap.showInTcp = 1; snap.showInMixer = 1; snap.mainSend = 1; snap.fxEnable = 1;
|
||||
snap.fxOffline = {{"{FX-A}", 1}, {"{FX-B}", 0}};
|
||||
vm.storeSnapshot("{T}", snap);
|
||||
vm.releaseSnapshotFlags("{T}");
|
||||
|
||||
auto back = ViewModeModel::deserialize(vm.serialize());
|
||||
CHECK(back.has_value());
|
||||
CHECK(back && *back == vm);
|
||||
CHECK(back && !back->holdsSnapshotFlags("{T}"));
|
||||
CHECK(back && back->holdsSnapshotFx("{T}"));
|
||||
CHECK(back && back->snapshot("{T}") && back->snapshot("{T}")->fxOffline.size() == 2);
|
||||
|
||||
// Absent means held: a fully-held record writes neither key, so every v1/v2
|
||||
// blob still loads as one nothing has spent.
|
||||
ViewModeModel held;
|
||||
held.storeSnapshot("{T}", snap);
|
||||
CHECK(held.serialize().find("\"flagsHeld\"") == std::string::npos);
|
||||
CHECK(held.serialize().find("\"fxHeld\"") == std::string::npos);
|
||||
}
|
||||
|
||||
// -- 7b. Untagged leaves are managed by the mode system ----------------------
|
||||
@@ -583,9 +642,6 @@ static void testUntaggedLeavesManagedByModeSystem() {
|
||||
CHECK(flagValue(*r, Flag::ShowInMixer) == 1);
|
||||
CHECK(flagValue(*r, Flag::MainSend) == 0); // captured 0 comes back 0
|
||||
CHECK(flagValue(*r, Flag::FxEnable) == 1);
|
||||
CHECK(r->fxOffline.size() == 2);
|
||||
CHECK(r->fxOffline[0].offline == false);
|
||||
CHECK(r->fxOffline[1].offline == true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2022,38 +2078,14 @@ static void testReconcilePrunesTheSoloCacheAlongsideSnapshots() {
|
||||
CHECK(design && design->count("{LIVE}") == 1);
|
||||
}
|
||||
|
||||
// -- Per-FX offline: identity keying and the v1 -> v2 snapshot ladder ---------
|
||||
// -- Per-FX offline: the v1 -> v2 snapshot ladder ----------------------------
|
||||
//
|
||||
// The restore path end-to-end, at the seam the shell actually uses: a stored
|
||||
// snapshot -> planToggle -> makeRestorePlan ops -> resolveFxRestore against the
|
||||
// chain as it stands now. The chain mutations happen while the track is parked,
|
||||
// which is the whole reason a slot cannot be the key.
|
||||
// The persisted shape only. Resolving a captured FX state against the live chain
|
||||
// moved to core/view/fx_state — test_fx_state owns those cases.
|
||||
|
||||
namespace {
|
||||
|
||||
// The plan's restore ops for one parked-then-reactivated leaf.
|
||||
std::vector<FxOfflineOp> restoreOpsFor(ViewModeModel& vm, const std::string& guid) {
|
||||
FolderTree tree;
|
||||
tree.nodes.push_back(FolderNode{guid, "", false});
|
||||
const TogglePlan plan = vm.planToggle(tree, kDesignModeId);
|
||||
const TrackPlan* r = restoreFor(plan, guid);
|
||||
return r ? r->fxOffline : std::vector<FxOfflineOp>{};
|
||||
}
|
||||
|
||||
bool writeAt(const FxRestoreResolution& res, int fxIndex, bool offline) {
|
||||
for (const FxOfflineWrite& w : res.writes)
|
||||
if (w.fxIndex == fxIndex) return w.offline == offline;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool anyWriteAt(const FxRestoreResolution& res, int fxIndex) {
|
||||
for (const FxOfflineWrite& w : res.writes)
|
||||
if (w.fxIndex == fxIndex) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// A leaf tagged Design, parked with three identified FX — the state every case
|
||||
// below starts from.
|
||||
// A leaf tagged Design, parked with three identified FX.
|
||||
ViewModeModel parkedWithThreeFx() {
|
||||
ViewModeModel vm;
|
||||
vm.membership().tag("{T}", kDesignModeId);
|
||||
@@ -2066,46 +2098,6 @@ ViewModeModel parkedWithThreeFx() {
|
||||
|
||||
} // namespace
|
||||
|
||||
static void testParkReorderRestoreLandsEachPluginItsOwnState() {
|
||||
ViewModeModel vm = parkedWithThreeFx();
|
||||
// Reordered to C, A, B while parked.
|
||||
const FxRestoreResolution res =
|
||||
resolveFxRestore(restoreOpsFor(vm, "{T}"), {"{FX-C}", "{FX-A}", "{FX-B}"});
|
||||
|
||||
CHECK(res.writes.size() == 3);
|
||||
CHECK(res.drops.total() == 0);
|
||||
CHECK(writeAt(res, 1, false)); // A
|
||||
CHECK(writeAt(res, 2, true)); // B's captured offline followed B, not slot 1
|
||||
CHECK(writeAt(res, 0, false)); // C
|
||||
}
|
||||
|
||||
static void testParkDeleteFxRestoreDropsExplicitly() {
|
||||
ViewModeModel vm = parkedWithThreeFx();
|
||||
// B deleted while parked.
|
||||
const FxRestoreResolution res =
|
||||
resolveFxRestore(restoreOpsFor(vm, "{T}"), {"{FX-A}", "{FX-C}"});
|
||||
|
||||
CHECK(res.writes.size() == 2);
|
||||
CHECK(res.drops.missingIdentity == 1);
|
||||
CHECK(writeAt(res, 0, false)); // A
|
||||
CHECK(writeAt(res, 1, false)); // C — and NOT B's captured `true`
|
||||
CHECK(!describeFxRestoreDrops(res.drops, 1).empty());
|
||||
}
|
||||
|
||||
static void testParkAddFxRestoreLeavesItAlone() {
|
||||
ViewModeModel vm = parkedWithThreeFx();
|
||||
// A new plugin inserted at the head while parked.
|
||||
const FxRestoreResolution res = resolveFxRestore(
|
||||
restoreOpsFor(vm, "{T}"), {"{FX-NEW}", "{FX-A}", "{FX-B}", "{FX-C}"});
|
||||
|
||||
CHECK(res.writes.size() == 3);
|
||||
CHECK(res.drops.total() == 0);
|
||||
CHECK(!anyWriteAt(res, 0)); // the added FX is never written
|
||||
CHECK(writeAt(res, 1, false));
|
||||
CHECK(writeAt(res, 2, true));
|
||||
CHECK(writeAt(res, 3, false));
|
||||
}
|
||||
|
||||
static void testV2WritesTheLegacySlotArrayBesideIdentities() {
|
||||
// The downgrade half of the ladder: a build that predates identity keying
|
||||
// reads "fxOffline" and skips "fx", so it keeps exactly the behavior it had
|
||||
@@ -2136,7 +2128,7 @@ static void testV2WritesTheLegacySlotArrayBesideIdentities() {
|
||||
asOlder->snapshot("{T}")->fxOffline[1].offline == 1);
|
||||
}
|
||||
|
||||
static void testLegacyBlobLiftsToSlotKeyingAndStillRestores() {
|
||||
static void testLegacyBlobLiftsToSlotKeying() {
|
||||
// A view_state written before FX identity existed: no "fx" key anywhere.
|
||||
const char* v1 =
|
||||
"{\"version\":1,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\","
|
||||
@@ -2157,18 +2149,8 @@ static void testLegacyBlobLiftsToSlotKeyingAndStillRestores() {
|
||||
CHECK(snap && snap->fxOffline[1].offline == 1);
|
||||
CHECK(snap && snap->fxOffline[1].fxGuid.empty());
|
||||
CHECK(snap && snap->showInTcp == 1 && snap->mainSend == 1);
|
||||
|
||||
// It still restores — by position, which is all its bytes can support, and
|
||||
// is exactly what the pre-change build would have done with them.
|
||||
const std::vector<FxOfflineOp> ops = restoreOpsFor(*loaded, "{T}");
|
||||
CHECK(ops.size() == 3);
|
||||
CHECK(!ops.empty() && ops[0].keying == FxKeying::Slot);
|
||||
const FxRestoreResolution res = resolveFxRestore(ops, {"{FX-X}", "{FX-Y}", "{FX-Z}"});
|
||||
CHECK(res.writes.size() == 3);
|
||||
CHECK(res.drops.total() == 0);
|
||||
CHECK(writeAt(res, 0, false));
|
||||
CHECK(writeAt(res, 1, true));
|
||||
CHECK(writeAt(res, 2, false));
|
||||
// That it still RESTORES — by position, all its bytes can support — is
|
||||
// test_fx_state's case; here the lift only has to preserve the keying.
|
||||
|
||||
// Re-saving a lifted snapshot does NOT invent identities for it: the "fx"
|
||||
// key stays absent, and a second load reads the same slot-keyed shape.
|
||||
@@ -2202,7 +2184,9 @@ int main() {
|
||||
testJsonRoundTrip();
|
||||
testEmptyModelRoundTrip();
|
||||
testMalformedJson();
|
||||
testPlanToggleParkHasEmptyFxOffline();
|
||||
testPlanTogglePlansFlagsOnly();
|
||||
testReleasedFlagHalfIsNotReplanned();
|
||||
testHalfReleasedRecordRoundTrips();
|
||||
testUntaggedLeavesManagedByModeSystem();
|
||||
testTaggedLeafBehaviorUnchangedWithUntagged();
|
||||
testNestedToggleSnapshotSurvivesRepark();
|
||||
@@ -2242,12 +2226,9 @@ int main() {
|
||||
testSoloCacheMalformedJson();
|
||||
testReconcilePrunesTheSoloCacheAlongsideSnapshots();
|
||||
|
||||
// Per-FX offline identity keying + the v1 -> v2 snapshot ladder
|
||||
testParkReorderRestoreLandsEachPluginItsOwnState();
|
||||
testParkDeleteFxRestoreDropsExplicitly();
|
||||
testParkAddFxRestoreLeavesItAlone();
|
||||
// Per-FX offline: the v1 -> v2 snapshot ladder
|
||||
testV2WritesTheLegacySlotArrayBesideIdentities();
|
||||
testLegacyBlobLiftsToSlotKeyingAndStillRestores();
|
||||
testLegacyBlobLiftsToSlotKeying();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
|
||||
Reference in New Issue
Block a user