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:
2026-08-06 16:31:59 -04:00
parent 3eae942625
commit 2423225f0c
14 changed files with 897 additions and 164 deletions
+147
View File
@@ -0,0 +1,147 @@
// Standalone tests for reasampler::view::appliedState — no REAPER, no test
// framework. The fingerprint's whole job is discriminating "the model differs
// from what we applied" from "something happened"; the negative cases below are
// the load-bearing half.
#include "../src/core/view/applied_state.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::view;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
namespace {
TrackSnapshot record(int tcp, int mixer, int mainSend, int fxEnable) {
TrackSnapshot snap;
snap.showInTcp = tcp;
snap.showInMixer = mixer;
snap.mainSend = mainSend;
snap.fxEnable = fxEnable;
snap.fxOffline = {{"{FX-A}", 0}};
return snap;
}
// Two leaves parked out of Design, one of which the user had OUT of the mix by
// hand (mainSend 0) — the value the sunk attempt re-enabled.
ViewModeModel parkedPair() {
ViewModeModel vm;
vm.membership().tag("{T1}", kDesignModeId);
vm.membership().tag("{T2}", kDesignModeId);
vm.storeSnapshot("{T1}", record(1, 1, 1, 1));
vm.storeSnapshot("{T2}", record(1, 1, 0, 1));
return vm;
}
} // namespace
static void testSameModelFingerprintsEqual() {
const AppliedState a = appliedState(parkedPair());
const AppliedState b = appliedState(parkedPair());
CHECK(a == b);
CHECK(!(a != b));
CHECK(a.activeModeId == kArrangeModeId);
CHECK(a.parked.size() == 2);
CHECK(a.parked.size() == 2 && a.parked[0].guid == "{T1}"); // GUID order
CHECK(a.parked.size() == 2 && a.parked[1].mainSend == 0); // captured, not driven
}
static void testACapturedValueChangingIsADifference() {
// The reconcile case: a rolled-back view_state carries a different record for
// the same track, so what a restore would drive is no longer what we applied.
ViewModeModel vm = parkedPair();
const AppliedState before = appliedState(vm);
vm.storeSnapshot("{T2}", record(1, 1, 1, 1)); // mainSend the user had at 0
CHECK(appliedState(vm) != before);
}
static void testTheParkedSetChangingIsADifference() {
ViewModeModel vm = parkedPair();
const AppliedState before = appliedState(vm);
vm.clearSnapshot("{T2}");
CHECK(appliedState(vm) != before);
CHECK(appliedState(vm).parked.size() == 1);
ViewModeModel grown = parkedPair();
grown.storeSnapshot("{T3}", record(1, 1, 1, 1));
CHECK(appliedState(grown) != before);
}
static void testActiveModeChangingIsADifference() {
ViewModeModel vm = parkedPair();
const AppliedState before = appliedState(vm);
CHECK(vm.setActiveMode(kDesignModeId));
CHECK(appliedState(vm) != before);
}
static void testASpentFlagHalfLeavesTheFingerprint() {
// A restored track is no longer one the tool holds away from the user's
// values; its zeroed record must not read as a parked track at all zeros.
ViewModeModel vm = parkedPair();
vm.releaseSnapshotFlags("{T1}");
const AppliedState after = appliedState(vm);
CHECK(after.parked.size() == 1);
CHECK(after.parked.size() == 1 && after.parked[0].guid == "{T2}");
// The FX half outliving the flag half does NOT put it back.
CHECK(vm.holdsSnapshotFx("{T1}"));
CHECK(appliedState(vm) == after);
}
static void testEditsTheApplyDidNotDriveAreNotDifferences() {
// The bug that sank the previous attempt was repairing on evidence of nothing.
// Each edit below changes the model without changing what the apply drove.
ViewModeModel vm = parkedPair();
const AppliedState before = appliedState(vm);
vm.membership().tag("{OTHER}", kDesignModeId);
CHECK(appliedState(vm) == before);
vm.membership().setShowBoth("{OTHER}", true);
CHECK(appliedState(vm) == before);
vm.lanes().setManaged("{T1}", "reasampler:design", kDesignModeId);
CHECK(appliedState(vm) == before);
vm.soloCache().store(kDesignModeId, {{"{T1}", 1}});
CHECK(appliedState(vm) == before);
// Per-FX offline is DERIVED — never persisted, never in an undo record — so a
// difference there is evidence of nothing about the model.
TrackSnapshot fxMoved = record(1, 1, 1, 1);
fxMoved.fxOffline = {{"{FX-A}", 1}, {"{FX-B}", 0}};
vm.storeSnapshot("{T1}", fxMoved);
CHECK(appliedState(vm) == before);
vm.releaseSnapshotFx("{T2}");
CHECK(appliedState(vm) == before);
}
static void testEmptyModelFingerprintsEqualAndNotParked() {
ViewModeModel a;
ViewModeModel b;
CHECK(appliedState(a) == appliedState(b));
CHECK(appliedState(a).parked.empty());
CHECK(appliedState(a) != appliedState(parkedPair()));
}
int main() {
testSameModelFingerprintsEqual();
testACapturedValueChangingIsADifference();
testTheParkedSetChangingIsADifference();
testActiveModeChangingIsADifference();
testASpentFlagHalfLeavesTheFingerprint();
testEditsTheApplyDidNotDriveAreNotDifferences();
testEmptyModelFingerprintsEqualAndNotParked();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}
+307
View File
@@ -0,0 +1,307 @@
// Standalone tests for reasampler::view's derived FX state — no REAPER, no test
// framework. Two halves: what desiredFxState decides is owed, and how one work
// item resolves against the chain as it stands (the chain mutates while a track
// is parked, which is the whole reason a slot cannot be the key).
#include "../src/core/view/fx_state.h"
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::view;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
// -- helpers -----------------------------------------------------------------
namespace {
TrackSnapshot recordWithFx(std::vector<FxOfflineState> fx) {
TrackSnapshot snap;
snap.showInTcp = 1; snap.showInMixer = 1; snap.mainSend = 1; snap.fxEnable = 1;
snap.fxOffline = std::move(fx);
return snap;
}
// A Design leaf parked with three identified FX, the middle one offline before
// the park because the USER put it there.
ViewModeModel parkedWithThreeFx() {
ViewModeModel vm;
vm.membership().tag("{T}", kDesignModeId);
vm.storeSnapshot("{T}", recordWithFx({{"{FX-A}", 0}, {"{FX-B}", 1}, {"{FX-C}", 0}}));
return vm;
}
const FxTrackWork* workFor(const std::vector<FxTrackWork>& work, const std::string& guid) {
for (const FxTrackWork& w : work)
if (w.trackGuid == guid) return &w;
return nullptr;
}
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;
}
} // namespace
// -- desiredFxState: what is owed --------------------------------------------
static void testActiveTrackIsOwedOnlineInactiveIsOwedOffline() {
ViewModeModel vm = parkedWithThreeFx();
vm.membership().tag("{P}", kArrangeModeId);
vm.storeSnapshot("{P}", recordWithFx({{"{FX-P}", 0}}));
// Active mode is Arrange: {T} (Design) stays parked, {P} is coming back.
const std::vector<FxTrackWork> work = desiredFxState(vm, {"{T}", "{P}"});
CHECK(work.size() == 2);
CHECK(workFor(work, "{P}") && workFor(work, "{P}")->kind == FxWorkKind::Online);
CHECK(workFor(work, "{T}") && workFor(work, "{T}")->kind == FxWorkKind::Offline);
// Offline work names no targets: only the shell can read the live chain, and
// that chain IS the target set.
CHECK(workFor(work, "{T}") && workFor(work, "{T}")->targets.empty());
CHECK(workFor(work, "{P}") && workFor(work, "{P}")->targets.size() == 1);
}
static void testOnlineWorkSortsBeforeOfflineWork() {
// GUIDs chosen so plain GUID order would put every Offline track FIRST — the
// ordering under test has to beat the map's own order to pass.
ViewModeModel vm;
vm.membership().tag("{A-park}", kDesignModeId);
vm.membership().tag("{B-park}", kDesignModeId);
vm.membership().tag("{Y-back}", kArrangeModeId);
vm.membership().tag("{Z-back}", kArrangeModeId);
for (const char* g : {"{A-park}", "{B-park}", "{Y-back}", "{Z-back}"})
vm.storeSnapshot(g, recordWithFx({{"{FX}", 0}}));
const std::vector<FxTrackWork> work =
desiredFxState(vm, {"{A-park}", "{B-park}", "{Y-back}", "{Z-back}"});
CHECK(work.size() == 4);
// Onlining restores state the user owns and is 2-6x the cost of offlining;
// offlining is opportunistic reclaim and may be deferred indefinitely.
CHECK(work[0].trackGuid == "{Y-back}" && work[0].kind == FxWorkKind::Online);
CHECK(work[1].trackGuid == "{Z-back}" && work[1].kind == FxWorkKind::Online);
CHECK(work[2].trackGuid == "{A-park}" && work[2].kind == FxWorkKind::Offline);
CHECK(work[3].trackGuid == "{B-park}" && work[3].kind == FxWorkKind::Offline);
// Deterministic: a re-derivation yields the identical list, so a drain's
// position in it survives one.
CHECK(desiredFxState(vm, {"{A-park}", "{B-park}", "{Y-back}", "{Z-back}"}) == work);
}
static void testUserOfflinedPluginIsNeverBlanketOnlined() {
// The property this whole split exists for. {FX-B} was offline before the
// park because the user offlined it; coming back must leave it offline.
ViewModeModel vm = parkedWithThreeFx();
CHECK(vm.setActiveMode(kDesignModeId));
const std::vector<FxTrackWork> work = desiredFxState(vm, {"{T}"});
CHECK(work.size() == 1);
CHECK(work[0].kind == FxWorkKind::Online);
const FxRestoreResolution res = resolveFxWork(work[0], {"{FX-A}", "{FX-B}", "{FX-C}"});
CHECK(res.writes.size() == 3);
CHECK(res.drops.total() == 0);
CHECK(writeAt(res, 0, false));
CHECK(writeAt(res, 1, true)); // stays offline — NOT blanket-onlined
CHECK(writeAt(res, 2, false));
}
static void testReleasedFxHalfIsOwedNothing() {
// The chain is already back online; the record survives only for its flags.
ViewModeModel vm = parkedWithThreeFx();
CHECK(vm.setActiveMode(kDesignModeId));
vm.releaseSnapshotFx("{T}");
CHECK(desiredFxState(vm, {"{T}"}).empty());
CHECK(fxRestoreTargets(vm, "{T}").empty());
// ...and the mirror: a record whose FLAG half is spent still owes its FX.
ViewModeModel vm2 = parkedWithThreeFx();
CHECK(vm2.setActiveMode(kDesignModeId));
vm2.releaseSnapshotFlags("{T}");
const std::vector<FxTrackWork> work = desiredFxState(vm2, {"{T}"});
CHECK(work.size() == 1);
CHECK(work.size() == 1 && work[0].targets.size() == 3);
}
static void testDeadAndUnparkedTracksAreOwedNothing() {
ViewModeModel vm = parkedWithThreeFx();
// The track is gone from the project: there is nothing to write to.
CHECK(desiredFxState(vm, {}).empty());
// A track with no park record is owed nothing either way — that is also how a
// REFUSED park stays refused: it wrote no record, so no reclaim targets it.
ViewModeModel bare;
bare.membership().tag("{U}", kDesignModeId);
CHECK(desiredFxState(bare, {"{U}"}).empty());
}
// -- resolveFxWork: against the chain as it stands ----------------------------
static void testOfflineWorkTargetsEveryLiveSlot() {
ViewModeModel vm = parkedWithThreeFx(); // active mode Arrange ⇒ {T} parks
const std::vector<FxTrackWork> work = desiredFxState(vm, {"{T}"});
CHECK(work.size() == 1);
if (work.empty()) return;
// Two plugins added while parked, one of which REAPER reports no GUID for.
// Reclaim has to reach those too, which is why offline work keys by slot.
const FxRestoreResolution res =
resolveFxWork(work[0], {"{FX-A}", "", "{FX-B}", "{FX-C}", "{FX-NEW}"});
CHECK(res.writes.size() == 5);
CHECK(res.drops.total() == 0);
for (int i = 0; i < 5; ++i) CHECK(writeAt(res, i, true));
}
static void testReorderRestoreLandsEachPluginItsOwnState() {
ViewModeModel vm = parkedWithThreeFx();
CHECK(vm.setActiveMode(kDesignModeId));
const std::vector<FxTrackWork> work = desiredFxState(vm, {"{T}"});
CHECK(work.size() == 1);
if (work.empty()) return;
// Reordered to C, A, B while parked.
const FxRestoreResolution res = resolveFxWork(work[0], {"{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 testDeletedFxIsDroppedExplicitly() {
ViewModeModel vm = parkedWithThreeFx();
CHECK(vm.setActiveMode(kDesignModeId));
const std::vector<FxTrackWork> work = desiredFxState(vm, {"{T}"});
CHECK(work.size() == 1);
if (work.empty()) return;
// B deleted while parked.
const FxRestoreResolution res = resolveFxWork(work[0], {"{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 testAddedFxIsLeftAlone() {
ViewModeModel vm = parkedWithThreeFx();
CHECK(vm.setActiveMode(kDesignModeId));
const std::vector<FxTrackWork> work = desiredFxState(vm, {"{T}"});
CHECK(work.size() == 1);
if (work.empty()) return;
const FxRestoreResolution res =
resolveFxWork(work[0], {"{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 testSlotKeyedRecordStillRestoresByPosition() {
// A record lifted from a project saved before FX identity existed: position
// is all its bytes can support, and the targets carry that keying through.
ViewModeModel vm;
vm.membership().tag("{T}", kDesignModeId);
CHECK(vm.setActiveMode(kDesignModeId));
TrackSnapshot snap = recordWithFx({{"", 0}, {"", 1}, {"", 0}});
snap.fxKeying = FxKeying::Slot;
vm.storeSnapshot("{T}", snap);
const std::vector<FxOfflineOp> targets = fxRestoreTargets(vm, "{T}");
CHECK(targets.size() == 3);
CHECK(!targets.empty() && targets[0].keying == FxKeying::Slot);
const std::vector<FxTrackWork> work = desiredFxState(vm, {"{T}"});
CHECK(work.size() == 1);
if (work.empty()) return;
const FxRestoreResolution res = resolveFxWork(work[0], {"{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));
}
// -- the resume point ---------------------------------------------------------
static void testResumePointSkipsWhatTheDrainAlreadyDid() {
ViewModeModel vm = parkedWithThreeFx();
CHECK(vm.setActiveMode(kDesignModeId));
std::vector<FxTrackWork> work = desiredFxState(vm, {"{T}"});
CHECK(work.size() == 1);
if (work.empty()) return;
work[0].resumeAfterFxGuid = "{FX-A}";
const FxRestoreResolution res = resolveFxWork(work[0], {"{FX-A}", "{FX-B}", "{FX-C}"});
CHECK(res.writes.size() == 2);
CHECK(!anyWriteAt(res, 0)); // A is done
CHECK(writeAt(res, 1, true));
CHECK(writeAt(res, 2, false));
// Resuming after the LAST identity leaves nothing owed — the item is finished.
work[0].resumeAfterFxGuid = "{FX-C}";
CHECK(resolveFxWork(work[0], {"{FX-A}", "{FX-B}", "{FX-C}"}).writes.empty());
}
static void testResumePointThatLeftTheChainRestartsRatherThanStrands() {
ViewModeModel vm = parkedWithThreeFx();
CHECK(vm.setActiveMode(kDesignModeId));
std::vector<FxTrackWork> work = desiredFxState(vm, {"{T}"});
CHECK(work.size() == 1);
if (work.empty()) return;
// The drain's cursor named a plugin the user has since deleted. Every write is
// compare-before-write, so re-walking the item is cheap and correct; honoring a
// cursor that matches nothing would strand the rest of the chain.
work[0].resumeAfterFxGuid = "{FX-GONE}";
const FxRestoreResolution res = resolveFxWork(work[0], {"{FX-A}", "{FX-B}", "{FX-C}"});
CHECK(res.writes.size() == 3);
CHECK(writeAt(res, 1, true));
// Same rule on the offline side, where the cursor keys into the live chain.
ViewModeModel parked = parkedWithThreeFx(); // Arrange active ⇒ {T} parks
std::vector<FxTrackWork> offlineWork = desiredFxState(parked, {"{T}"});
CHECK(offlineWork.size() == 1);
if (offlineWork.empty()) return;
offlineWork[0].resumeAfterFxGuid = "{FX-A}";
CHECK(resolveFxWork(offlineWork[0], {"{FX-A}", "{FX-B}", "{FX-C}"}).writes.size() == 2);
offlineWork[0].resumeAfterFxGuid = "{FX-GONE}";
CHECK(resolveFxWork(offlineWork[0], {"{FX-A}", "{FX-B}", "{FX-C}"}).writes.size() == 3);
}
int main() {
testActiveTrackIsOwedOnlineInactiveIsOwedOffline();
testOnlineWorkSortsBeforeOfflineWork();
testUserOfflinedPluginIsNeverBlanketOnlined();
testReleasedFxHalfIsOwedNothing();
testDeadAndUnparkedTracksAreOwedNothing();
testOfflineWorkTargetsEveryLiveSlot();
testReorderRestoreLandsEachPluginItsOwnState();
testDeletedFxIsDroppedExplicitly();
testAddedFxIsLeftAlone();
testSlotKeyedRecordStillRestoresByPosition();
testResumePointSkipsWhatTheDrainAlreadyDid();
testResumePointThatLeftTheChainRestartsRatherThanStrands();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}
+2 -2
View File
@@ -45,14 +45,14 @@ struct FlagValues {
static void testAChainSittingAtEveryValueTheParkWouldWriteReadsAsParked() {
// The four zeros are what a parked track's driven flags actually read; if
// makeParkPlan ever writes something else, this is the test that says so.
const TrackPlan park = makeParkPlan("{A}", /*fxCount=*/0);
const TrackPlan park = makeParkPlan("{A}");
CHECK(park.flags.size() == 4);
CHECK(parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 0, 0}));
}
static void testOneFlagStillAtTheUsersValueMeansNoParkReachedTheChain() {
const TrackPlan park = makeParkPlan("{A}", /*fxCount=*/0);
const TrackPlan park = makeParkPlan("{A}");
// Each case varies exactly one flag, so together they also prove all four are
// in the plan: a missing op makes its case read parked and fail here.
+100 -119
View File
@@ -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;