Merge Wave 1: Design View pure-layer split (flag-only park plans, derived FX work list, two-half record, applied-state fingerprint)
This commit is contained in:
@@ -56,7 +56,7 @@ add_library(reaper_reasampler MODULE
|
|||||||
${REASAMPLER_SRC_DIR}/shell/actions/insert_fx_action.cpp
|
${REASAMPLER_SRC_DIR}/shell/actions/insert_fx_action.cpp
|
||||||
${REASAMPLER_SRC_DIR}/shell/persist/usage_scan.cpp
|
${REASAMPLER_SRC_DIR}/shell/persist/usage_scan.cpp
|
||||||
)
|
)
|
||||||
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths capture_name peaks bank_grid mode_switch tab_strip view_mode_model view_tree guid_diff lane_keys solo_cache insert_plan render_settings render_window track_topology batch_capture tail_control capture_realtime bank_book wav_codec origin_ledger tracking_authority prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable insert_fx_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage bake_wire resample_name export_bank package_pickers)
|
target_link_libraries(reaper_reasampler PRIVATE json wire file_bytes bank_model capture_paths capture_name peaks bank_grid mode_switch tab_strip view_mode_model view_tree fx_state guid_diff lane_keys solo_cache insert_plan render_settings render_window track_topology batch_capture tail_control capture_realtime bank_book wav_codec origin_ledger tracking_authority prune_reconcile prune_button app_version provenance drag_out instrument_drop theme component_geometry action_bar footer_bar overflow_menu mode_enable insert_fx_enable tooltip card_meta card_drag assignment_request bank_sync sample_usage bake_wire resample_name export_bank package_pickers)
|
||||||
# NOT linked here, deliberately: sampler_core / pitch_shift / the filter / limiter. The
|
# NOT linked here, deliberately: sampler_core / pitch_shift / the filter / limiter. The
|
||||||
# instrument renders its own bake in its own process, which is what keeps the extension's
|
# instrument renders its own bake in its own process, which is what keeps the extension's
|
||||||
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
# link graph free of the voice engine — a link edge to it here means the design drifted.
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ reasampler_test(view_mode_model LINK view_mode_model)
|
|||||||
reasampler_pure_library(view_tree SOURCES view_tree.cpp LINK PUBLIC view_mode_model)
|
reasampler_pure_library(view_tree SOURCES view_tree.cpp LINK PUBLIC view_mode_model)
|
||||||
reasampler_test(view_tree LINK view_tree)
|
reasampler_test(view_tree LINK view_tree)
|
||||||
|
|
||||||
|
reasampler_pure_library(fx_state SOURCES fx_state.cpp LINK PUBLIC view_mode_model)
|
||||||
|
reasampler_test(fx_state LINK fx_state)
|
||||||
|
|
||||||
|
reasampler_pure_library(applied_state SOURCES applied_state.cpp LINK PUBLIC view_mode_model)
|
||||||
|
reasampler_test(applied_state LINK applied_state)
|
||||||
|
|
||||||
reasampler_pure_library(guid_diff SOURCES guid_diff.cpp)
|
reasampler_pure_library(guid_diff SOURCES guid_diff.cpp)
|
||||||
reasampler_test(guid_diff LINK guid_diff)
|
reasampler_test(guid_diff LINK guid_diff)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
#include "core/view/applied_state.h"
|
||||||
|
|
||||||
|
namespace reasampler::view {
|
||||||
|
|
||||||
|
AppliedState appliedState(const ViewModeModel& model) {
|
||||||
|
AppliedState out;
|
||||||
|
out.activeModeId = model.activeModeId();
|
||||||
|
for (const auto& [guid, snap] : model.snapshots()) { // std::map ⇒ GUID order
|
||||||
|
if (!snap.flagsHeld) continue;
|
||||||
|
out.parked.push_back(
|
||||||
|
AppliedTrack{guid, snap.showInTcp, snap.showInMixer, snap.mainSend, snap.fxEnable});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::view
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
// A comparable fingerprint of what an apply put into the project, so a reconcile
|
||||||
|
// can ask "did our applied output drift from what a restore would drive it back
|
||||||
|
// to?" rather than "did an undo happen?". The second question is not the same
|
||||||
|
// one: an undo that touched nothing we drive still answers yes, and repairing on
|
||||||
|
// it re-enables state the user disabled by hand. Narrower still than "did the
|
||||||
|
// model change?": membership, lane ownership and the solo cache are excluded by
|
||||||
|
// design (see AppliedState below), so an edit to any of those changes what a
|
||||||
|
// future apply must drive while leaving this fingerprint bit-identical. Not a
|
||||||
|
// sole reapply trigger for anything membership can affect.
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/view/view_mode_model.h"
|
||||||
|
|
||||||
|
namespace reasampler::view {
|
||||||
|
|
||||||
|
// One track the tool currently holds parked, and the flag values a restore would
|
||||||
|
// drive it back to. Per-FX offline is deliberately absent: it is derived, never
|
||||||
|
// persisted and never in an undo record, so a difference there is evidence of
|
||||||
|
// nothing about the model.
|
||||||
|
struct AppliedTrack {
|
||||||
|
std::string guid;
|
||||||
|
int showInTcp = 0;
|
||||||
|
int showInMixer = 0;
|
||||||
|
int mainSend = 0;
|
||||||
|
int fxEnable = 0;
|
||||||
|
|
||||||
|
bool operator==(const AppliedTrack& o) const {
|
||||||
|
return guid == o.guid && showInTcp == o.showInTcp &&
|
||||||
|
showInMixer == o.showInMixer && mainSend == o.mainSend &&
|
||||||
|
fxEnable == o.fxEnable;
|
||||||
|
}
|
||||||
|
bool operator!=(const AppliedTrack& o) const { return !(*this == o); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Membership edits, lane ownership and the solo cache are all deliberately out:
|
||||||
|
// they change constantly without changing what the apply drove, and folding them
|
||||||
|
// in would put a reconcile back to firing on every unrelated edit.
|
||||||
|
struct AppliedState {
|
||||||
|
std::string activeModeId;
|
||||||
|
std::vector<AppliedTrack> parked; // GUID order
|
||||||
|
|
||||||
|
bool operator==(const AppliedState& o) const {
|
||||||
|
return activeModeId == o.activeModeId && parked == o.parked;
|
||||||
|
}
|
||||||
|
bool operator!=(const AppliedState& o) const { return !(*this == o); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// A track appears iff its park record still HOLDS its flag half — that is
|
||||||
|
// precisely the set the tool is currently driving away from the user's values.
|
||||||
|
AppliedState appliedState(const ViewModeModel& model);
|
||||||
|
|
||||||
|
} // namespace reasampler::view
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#include "core/view/fx_state.h"
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace reasampler::view {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Everything up to and including the resume point is already done. An empty
|
||||||
|
// `resumeAfterFxGuid` is ambiguous by itself — "nothing done yet" and "the last
|
||||||
|
// completed op had no identity" look identical — so an empty guid falls back to
|
||||||
|
// matching by slot instead; `resumeAfterSlot < 0` is what actually means
|
||||||
|
// "nothing done yet". Not found ⇒ nothing dropped: the resume point left the
|
||||||
|
// chain between ticks, and re-walking a compare-before-write list beats
|
||||||
|
// trusting a cursor that no longer points at anything.
|
||||||
|
std::vector<FxOfflineOp> remainingAfter(const std::vector<FxOfflineOp>& ops,
|
||||||
|
const std::string& resumeAfterFxGuid,
|
||||||
|
int resumeAfterSlot) {
|
||||||
|
if (resumeAfterFxGuid.empty() && resumeAfterSlot < 0) return ops;
|
||||||
|
for (std::size_t i = 0; i < ops.size(); ++i) {
|
||||||
|
const bool isResumePoint = resumeAfterFxGuid.empty()
|
||||||
|
? ops[i].slot == resumeAfterSlot
|
||||||
|
: ops[i].fxGuid == resumeAfterFxGuid;
|
||||||
|
if (isResumePoint)
|
||||||
|
return std::vector<FxOfflineOp>(ops.begin() + static_cast<std::ptrdiff_t>(i) + 1,
|
||||||
|
ops.end());
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slot keying, deliberately: the live chain IS the target set, so its positions
|
||||||
|
// are current by construction. Identity keying would drop an FX REAPER reports no
|
||||||
|
// GUID for, and reclaim has to reach those too — the fxGuid rides along only so
|
||||||
|
// the resume point has something to match.
|
||||||
|
std::vector<FxOfflineOp> offlineTargets(const std::string& trackGuid,
|
||||||
|
const std::vector<std::string>& liveFxGuids) {
|
||||||
|
std::vector<FxOfflineOp> ops;
|
||||||
|
ops.reserve(liveFxGuids.size());
|
||||||
|
for (std::size_t i = 0; i < liveFxGuids.size(); ++i)
|
||||||
|
ops.push_back(FxOfflineOp{trackGuid, FxKeying::Slot, liveFxGuids[i],
|
||||||
|
static_cast<int>(i), true});
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::vector<FxOfflineOp> fxRestoreTargets(const ViewModeModel& model,
|
||||||
|
const std::string& trackGuid) {
|
||||||
|
std::vector<FxOfflineOp> ops;
|
||||||
|
if (!model.holdsSnapshotFx(trackGuid)) return ops;
|
||||||
|
|
||||||
|
const TrackSnapshot* snap = model.snapshot(trackGuid);
|
||||||
|
ops.reserve(snap->fxOffline.size());
|
||||||
|
for (std::size_t i = 0; i < snap->fxOffline.size(); ++i) {
|
||||||
|
const FxOfflineState& fx = snap->fxOffline[i];
|
||||||
|
ops.push_back(FxOfflineOp{trackGuid, snap->fxKeying, fx.fxGuid,
|
||||||
|
static_cast<int>(i), fx.offline != 0});
|
||||||
|
}
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<FxTrackWork> desiredFxState(const ViewModeModel& model,
|
||||||
|
const std::string& targetMode,
|
||||||
|
const std::set<std::string>& liveTrackGuids) {
|
||||||
|
std::vector<FxTrackWork> online;
|
||||||
|
std::vector<FxTrackWork> offline;
|
||||||
|
|
||||||
|
for (const auto& [guid, snap] : model.snapshots()) { // std::map ⇒ GUID order
|
||||||
|
if (liveTrackGuids.count(guid) == 0) continue; // dead track — nothing to write
|
||||||
|
|
||||||
|
if (!model.leafBelongsToMode(guid, targetMode)) {
|
||||||
|
offline.push_back(FxTrackWork{guid, FxWorkKind::Offline, {}, {}});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Active with the FX half already released: the chain is back online and
|
||||||
|
// the record is only waiting on its flags. Nothing owed either way.
|
||||||
|
if (!snap.fxHeld) continue;
|
||||||
|
online.push_back(
|
||||||
|
FxTrackWork{guid, FxWorkKind::Online, fxRestoreTargets(model, guid), {}});
|
||||||
|
}
|
||||||
|
|
||||||
|
online.reserve(online.size() + offline.size());
|
||||||
|
for (FxTrackWork& w : offline) online.push_back(std::move(w));
|
||||||
|
return online;
|
||||||
|
}
|
||||||
|
|
||||||
|
FxRestoreResolution resolveFxWork(const FxTrackWork& work,
|
||||||
|
const std::vector<std::string>& liveFxGuids) {
|
||||||
|
if (work.kind == FxWorkKind::Offline) {
|
||||||
|
return resolveFxRestore(
|
||||||
|
remainingAfter(offlineTargets(work.trackGuid, liveFxGuids), work.resumeAfterFxGuid,
|
||||||
|
work.resumeAfterSlot),
|
||||||
|
liveFxGuids);
|
||||||
|
}
|
||||||
|
return resolveFxRestore(
|
||||||
|
remainingAfter(work.targets, work.resumeAfterFxGuid, work.resumeAfterSlot), liveFxGuids);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace reasampler::view
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
#pragma once
|
||||||
|
// What the FX chains SHOULD be, derived from view state: an ordered per-track
|
||||||
|
// work list plus its resolution against a live chain. Derived, never persisted
|
||||||
|
// and never in an undo record — the park record's captured FX half is the only
|
||||||
|
// persisted half. Pure: FX identities are opaque strings the shell reads from
|
||||||
|
// REAPER (TrackFX_GetFXGUID) and hands in.
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "core/view/fx_offline.h"
|
||||||
|
#include "core/view/view_mode_model.h"
|
||||||
|
|
||||||
|
namespace reasampler::view {
|
||||||
|
|
||||||
|
enum class FxWorkKind {
|
||||||
|
Online, // back to the park record's captured per-FX state, verbatim
|
||||||
|
Offline, // reclaim: every live FX offline
|
||||||
|
};
|
||||||
|
|
||||||
|
// One track's whole FX move. Track-granular so a drain may stop between tracks,
|
||||||
|
// and resumable WITHIN a track by FX identity: a slot cursor would silently skip
|
||||||
|
// or repeat a plugin if the chain were edited between two ticks.
|
||||||
|
struct FxTrackWork {
|
||||||
|
std::string trackGuid;
|
||||||
|
FxWorkKind kind = FxWorkKind::Offline;
|
||||||
|
|
||||||
|
// Online: the captured targets, verbatim — never a blanket "online", which is
|
||||||
|
// exactly what would lose the plugins the USER had offline before the park.
|
||||||
|
// Offline: empty. The live chain is the target set, and only the shell can
|
||||||
|
// read it, so resolveFxWork synthesises the targets from what it is handed.
|
||||||
|
std::vector<FxOfflineOp> targets;
|
||||||
|
|
||||||
|
// Identity of the last FX this item's drain completed; empty means EITHER
|
||||||
|
// nothing has completed yet OR the completed FX itself carried no identity
|
||||||
|
// (REAPER reports no GUID for it, or it came from a Slot-keyed record) — see
|
||||||
|
// resumeAfterSlot below, which disambiguates the two. This is load-bearing
|
||||||
|
// for forward progress, not an optimisation: without the disambiguation, an
|
||||||
|
// unidentified FX would look like "none yet" and the drain would restart at
|
||||||
|
// the top of the list every tick, forever. Compare-before-write makes every
|
||||||
|
// write idempotent, so an identity that has since left the chain safely
|
||||||
|
// restarts the item rather than stranding it.
|
||||||
|
std::string resumeAfterFxGuid;
|
||||||
|
|
||||||
|
// Position of the last completed op, consulted only when resumeAfterFxGuid
|
||||||
|
// is empty, to tell "resumed past an unidentified FX at this slot" apart
|
||||||
|
// from "nothing done yet" (-1). Every FxOfflineOp carries its slot
|
||||||
|
// regardless of keying, so this is available for both Online and Offline
|
||||||
|
// work.
|
||||||
|
int resumeAfterSlot = -1;
|
||||||
|
|
||||||
|
bool operator==(const FxTrackWork& o) const {
|
||||||
|
return trackGuid == o.trackGuid && kind == o.kind && targets == o.targets &&
|
||||||
|
resumeAfterFxGuid == o.resumeAfterFxGuid &&
|
||||||
|
resumeAfterSlot == o.resumeAfterSlot;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// The whole project's FX work, from view state alone — no folder tree, because a
|
||||||
|
// park record is only ever written for a track the planner already decided to
|
||||||
|
// park (parents and show-both leaves never get one).
|
||||||
|
//
|
||||||
|
// ONLINE WORK SORTS FIRST: onlining restores state the user owns, offlining is
|
||||||
|
// opportunistic reclaim that may be deferred indefinitely without losing
|
||||||
|
// anything. Within a kind, GUID order — deterministic, so a drain's position in
|
||||||
|
// the list survives a re-derivation.
|
||||||
|
//
|
||||||
|
// `targetMode` is explicit, mirroring planToggle, rather than read from
|
||||||
|
// `model.activeModeId()`: applyMode sets the active mode LAST, after its park
|
||||||
|
// and restore loops, so a caller reading it from inside or before that block
|
||||||
|
// would classify every track against the OUTGOING mode instead.
|
||||||
|
std::vector<FxTrackWork> desiredFxState(const ViewModeModel& model,
|
||||||
|
const std::string& targetMode,
|
||||||
|
const std::set<std::string>& liveTrackGuids);
|
||||||
|
|
||||||
|
// One track's Online targets from its park record's FX half; empty when the
|
||||||
|
// record holds none. Each target carries the record's keying, so a snapshot
|
||||||
|
// lifted from a pre-identity project still restores by slot.
|
||||||
|
std::vector<FxOfflineOp> fxRestoreTargets(const ViewModeModel& model,
|
||||||
|
const std::string& trackGuid);
|
||||||
|
|
||||||
|
// Resolves one work item against the chain as it stands, where `liveFxGuids[i]`
|
||||||
|
// is the identity of the FX at slot `i` right now (empty when REAPER reported
|
||||||
|
// none). Honors the resume point, then matches by identity: an identity that is
|
||||||
|
// not live is DROPPED, never re-pointed at a slot.
|
||||||
|
FxRestoreResolution resolveFxWork(const FxTrackWork& work,
|
||||||
|
const std::vector<std::string>& liveFxGuids);
|
||||||
|
|
||||||
|
} // namespace reasampler::view
|
||||||
@@ -188,7 +188,7 @@ LaneMintPlan planLaneMinting(const ViewModeModel& model, const FolderTree& tree,
|
|||||||
return plan;
|
return plan;
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
|
TrackPlan makeParkPlan(const std::string& guid) {
|
||||||
TrackPlan p;
|
TrackPlan p;
|
||||||
p.flags = {
|
p.flags = {
|
||||||
{guid, Flag::ShowInTcp, 0},
|
{guid, Flag::ShowInTcp, 0},
|
||||||
@@ -196,13 +196,6 @@ TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
|
|||||||
{guid, Flag::MainSend, 0},
|
{guid, Flag::MainSend, 0},
|
||||||
{guid, Flag::FxEnable, 0},
|
{guid, Flag::FxEnable, 0},
|
||||||
};
|
};
|
||||||
// Park has no identity question to answer: it offlines every slot that is
|
|
||||||
// live right now, so slot keying IS the addressing. In production fxCount
|
|
||||||
// is always 0 here — planToggle calls this with fxCount=0 and the D2 shell
|
|
||||||
// expands the real writes itself via TrackFX_GetCount (parkFxOffline in
|
|
||||||
// shell/view/view.cpp); a nonzero fxCount only exercises this loop in tests.
|
|
||||||
for (int i = 0; i < fxCount; ++i)
|
|
||||||
p.fxOffline.push_back(FxOfflineOp{guid, FxKeying::Slot, {}, i, true});
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,11 +207,6 @@ TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap) {
|
|||||||
{guid, Flag::MainSend, snap.mainSend},
|
{guid, Flag::MainSend, snap.mainSend},
|
||||||
{guid, Flag::FxEnable, snap.fxEnable},
|
{guid, Flag::FxEnable, snap.fxEnable},
|
||||||
};
|
};
|
||||||
for (std::size_t i = 0; i < snap.fxOffline.size(); ++i) {
|
|
||||||
const FxOfflineState& fx = snap.fxOffline[i];
|
|
||||||
p.fxOffline.push_back(FxOfflineOp{guid, snap.fxKeying, fx.fxGuid,
|
|
||||||
static_cast<int>(i), fx.offline != 0});
|
|
||||||
}
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,6 +241,49 @@ const TrackSnapshot* ViewModeModel::snapshot(const std::string& guid) const {
|
|||||||
return it == snapshots_.end() ? nullptr : &it->second;
|
return it == snapshots_.end() ? nullptr : &it->second;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ViewModeModel::holdsSnapshotFlags(const std::string& guid) const {
|
||||||
|
const TrackSnapshot* s = snapshot(guid);
|
||||||
|
return s && s->flagsHeld;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ViewModeModel::holdsSnapshotFx(const std::string& guid) const {
|
||||||
|
const TrackSnapshot* s = snapshot(guid);
|
||||||
|
return s && s->fxHeld;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// A record with neither half left records nothing — keeping it would make
|
||||||
|
// snapshot() answer yes to a track the model no longer describes.
|
||||||
|
void eraseIfSpent(std::map<std::string, TrackSnapshot>& snapshots,
|
||||||
|
std::map<std::string, TrackSnapshot>::iterator it) {
|
||||||
|
if (!it->second.flagsHeld && !it->second.fxHeld) snapshots.erase(it);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void ViewModeModel::releaseSnapshotFlags(const std::string& guid) {
|
||||||
|
auto it = snapshots_.find(guid);
|
||||||
|
if (it == snapshots_.end()) return;
|
||||||
|
it->second.flagsHeld = false;
|
||||||
|
it->second.showInTcp = 0;
|
||||||
|
it->second.showInMixer = 0;
|
||||||
|
it->second.mainSend = 0;
|
||||||
|
it->second.fxEnable = 0;
|
||||||
|
eraseIfSpent(snapshots_, it);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ViewModeModel::releaseSnapshotFx(const std::string& guid) {
|
||||||
|
auto it = snapshots_.find(guid);
|
||||||
|
if (it == snapshots_.end()) return;
|
||||||
|
it->second.fxHeld = false;
|
||||||
|
it->second.fxOffline.clear();
|
||||||
|
// fxKeying is left as captured: the record's own keying, not a claim about
|
||||||
|
// one it never had. Harmless today (fxOffline is empty right above), but
|
||||||
|
// whoever reads fxKeying after this point should see the truth.
|
||||||
|
eraseIfSpent(snapshots_, it);
|
||||||
|
}
|
||||||
|
|
||||||
std::size_t ViewModeModel::reconcile(const std::set<std::string>& liveGuids) {
|
std::size_t ViewModeModel::reconcile(const std::set<std::string>& liveGuids) {
|
||||||
// See header: snapshots are pruned, membership is not (undo-delete rationale).
|
// See header: snapshots are pruned, membership is not (undo-delete rationale).
|
||||||
soloCache_.reconcile(liveGuids);
|
soloCache_.reconcile(liveGuids);
|
||||||
@@ -322,11 +353,12 @@ TogglePlan ViewModeModel::planToggle(const FolderTree& tree, const std::string&
|
|||||||
|
|
||||||
const bool active = leafBelongsToMode(guid, targetMode);
|
const bool active = leafBelongsToMode(guid, targetMode);
|
||||||
if (active) {
|
if (active) {
|
||||||
if (const TrackSnapshot* snap = snapshot(guid))
|
// holdsSnapshotFlags, not snapshot(): a record whose FX half is still
|
||||||
plan.restore.push_back(makeRestorePlan(guid, *snap));
|
// draining carries zeroed flags, and restoring THOSE would re-park it.
|
||||||
|
if (holdsSnapshotFlags(guid))
|
||||||
|
plan.restore.push_back(makeRestorePlan(guid, *snapshot(guid)));
|
||||||
} else {
|
} else {
|
||||||
// fxOffline is empty here; the D2 shell expands it via TrackFX_GetCount.
|
plan.park.push_back(makeParkPlan(guid));
|
||||||
plan.park.push_back(makeParkPlan(guid, /*fxCount=*/0));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,11 +402,15 @@ std::string intToStr(int v) { return json::numToStr(v); }
|
|||||||
using ObjWriter = json::Writer;
|
using ObjWriter = json::Writer;
|
||||||
|
|
||||||
// Version ladder for the stored blob, under the FOREVER-STABLE "view_state" key.
|
// Version ladder for the stored blob, under the FOREVER-STABLE "view_state" key.
|
||||||
// Only the per-FX snapshot shape has ever moved:
|
|
||||||
//
|
//
|
||||||
// v1 "snapshots":[{...,"fxOffline":[0,1,0]}] — offline state by SLOT
|
// v1 "snapshots":[{...,"fxOffline":[0,1,0]}] — offline state by SLOT
|
||||||
// v2 "snapshots":[{...,"fxOffline":[0,1,0], — the v1 array, still written
|
// v2 "snapshots":[{...,"fxOffline":[0,1,0], — the v1 array, still written
|
||||||
// "fx":[{"guid":"{..}","offline":0},...]}] — by FX IDENTITY
|
// "fx":[{"guid":"{..}","offline":0},...]}] — by FX IDENTITY
|
||||||
|
// v3 adds "flagsHeld"/"fxHeld", each written ONLY when false. A record's two
|
||||||
|
// halves are released independently, and absent-means-held keeps every v1/v2
|
||||||
|
// blob (and every fully-held v3 record) byte-identical to what v2 wrote. A v2
|
||||||
|
// reader sees a half-released record as fully held, which restores flags it
|
||||||
|
// should not — the same leniency direction the version field itself takes.
|
||||||
//
|
//
|
||||||
// v2 writes BOTH: "fx" is what this build reads, and the v1 array is what a build
|
// v2 writes BOTH: "fx" is what this build reads, and the v1 array is what a build
|
||||||
// that predates identity keying reads — a downgrade keeps exactly the behavior it
|
// that predates identity keying reads — a downgrade keeps exactly the behavior it
|
||||||
@@ -387,7 +423,7 @@ using ObjWriter = json::Writer;
|
|||||||
// view_state falls back to a default model, which loses every membership tag, so
|
// view_state falls back to a default model, which loses every membership tag, so
|
||||||
// leniency is the safe direction here — the opposite call from origin_ledger,
|
// leniency is the safe direction here — the opposite call from origin_ledger,
|
||||||
// where a misread blob would put prune's deletion authority on bad data.
|
// where a misread blob would put prune's deletion authority on bad data.
|
||||||
constexpr int kViewStateVersion = 2;
|
constexpr int kViewStateVersion = 3;
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
@@ -439,7 +475,7 @@ std::string ViewModeModel::serialize() const {
|
|||||||
out += ']';
|
out += ']';
|
||||||
|
|
||||||
// snapshots: array of { guid, showInTcp, showInMixer, mainSend, fxEnable,
|
// snapshots: array of { guid, showInTcp, showInMixer, mainSend, fxEnable,
|
||||||
// fxOffline[], fx[] } — see the version ladder above.
|
// fxOffline[], flagsHeld?, fxHeld?, fx[] } — see the version ladder above.
|
||||||
root.keyBegin("snapshots");
|
root.keyBegin("snapshots");
|
||||||
out += '[';
|
out += '[';
|
||||||
{
|
{
|
||||||
@@ -460,6 +496,10 @@ std::string ViewModeModel::serialize() const {
|
|||||||
e.keyBegin("fxOffline");
|
e.keyBegin("fxOffline");
|
||||||
writeIntArray(out, bySlot);
|
writeIntArray(out, bySlot);
|
||||||
|
|
||||||
|
// Absent means held — see the version ladder above.
|
||||||
|
if (!snap.flagsHeld) e.keyRaw("flagsHeld", "false");
|
||||||
|
if (!snap.fxHeld) e.keyRaw("fxHeld", "false");
|
||||||
|
|
||||||
// A Slot-keyed snapshot has no identities to write — emitting an
|
// A Slot-keyed snapshot has no identities to write — emitting an
|
||||||
// "fx" array for it would invent the very keys it lacks.
|
// "fx" array for it would invent the very keys it lacks.
|
||||||
if (snap.fxKeying == FxKeying::Identity) {
|
if (snap.fxKeying == FxKeying::Identity) {
|
||||||
@@ -634,6 +674,8 @@ bool parseSnapshots(json::Reader& r, std::map<std::string, TrackSnapshot>& snaps
|
|||||||
else if (k == "mainSend") { if (!r.parseInt(snap.mainSend)) return false; }
|
else if (k == "mainSend") { if (!r.parseInt(snap.mainSend)) return false; }
|
||||||
else if (k == "fxEnable") { if (!r.parseInt(snap.fxEnable)) return false; }
|
else if (k == "fxEnable") { if (!r.parseInt(snap.fxEnable)) return false; }
|
||||||
else if (k == "fxOffline") { if (!r.parseIntArray(bySlot)) return false; haveSlot = true; }
|
else if (k == "fxOffline") { if (!r.parseIntArray(bySlot)) return false; haveSlot = true; }
|
||||||
|
else if (k == "flagsHeld") { if (!r.parseBool(snap.flagsHeld)) return false; }
|
||||||
|
else if (k == "fxHeld") { if (!r.parseBool(snap.fxHeld)) return false; }
|
||||||
else if (k == "fx") {
|
else if (k == "fx") {
|
||||||
if (!parseFxStates(r, byIdentity)) return false;
|
if (!parseFxStates(r, byIdentity)) return false;
|
||||||
haveIdentity = true;
|
haveIdentity = true;
|
||||||
|
|||||||
@@ -196,6 +196,11 @@ struct FolderTree {
|
|||||||
// parking — restore's source of truth. Ints, not bools, so a snapshot
|
// parking — restore's source of truth. Ints, not bools, so a snapshot
|
||||||
// faithfully round-trips whatever REAPER reported (defensive against
|
// faithfully round-trips whatever REAPER reported (defensive against
|
||||||
// non-0/1 values).
|
// non-0/1 values).
|
||||||
|
//
|
||||||
|
// The two halves are released INDEPENDENTLY. The flag half is spent the moment a
|
||||||
|
// restore writes it back; the FX half has to outlive that, because it is the only
|
||||||
|
// record of which plugins the USER had offline before the park — release it with
|
||||||
|
// the flag half and a later online blanket-enables them.
|
||||||
struct TrackSnapshot {
|
struct TrackSnapshot {
|
||||||
int showInTcp = 0; // B_SHOWINTCP prior value
|
int showInTcp = 0; // B_SHOWINTCP prior value
|
||||||
int showInMixer = 0; // B_SHOWINMIXER prior value
|
int showInMixer = 0; // B_SHOWINMIXER prior value
|
||||||
@@ -208,15 +213,19 @@ struct TrackSnapshot {
|
|||||||
std::vector<FxOfflineState> fxOffline;
|
std::vector<FxOfflineState> fxOffline;
|
||||||
FxKeying fxKeying = FxKeying::Identity;
|
FxKeying fxKeying = FxKeying::Identity;
|
||||||
|
|
||||||
|
bool flagsHeld = true; // false ⇒ a restore already wrote the flags back
|
||||||
|
bool fxHeld = true; // false ⇒ the chain is back online; nothing left to restore
|
||||||
|
|
||||||
bool operator==(const TrackSnapshot& o) const {
|
bool operator==(const TrackSnapshot& o) const {
|
||||||
return showInTcp == o.showInTcp && showInMixer == o.showInMixer &&
|
return showInTcp == o.showInTcp && showInMixer == o.showInMixer &&
|
||||||
mainSend == o.mainSend && fxEnable == o.fxEnable &&
|
mainSend == o.mainSend && fxEnable == o.fxEnable &&
|
||||||
fxOffline == o.fxOffline && fxKeying == o.fxKeying;
|
fxOffline == o.fxOffline && fxKeying == o.fxKeying &&
|
||||||
|
flagsHeld == o.flagsHeld && fxHeld == o.fxHeld;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Which scalar flag a TrackFlagOp drives. FX-offline is carried separately (it
|
// Which scalar flag a TrackFlagOp drives. FX-offline is not one of these and is
|
||||||
// is per-slot, variable length) — see TrackParkPlan::fxOffline.
|
// not planned beside them — it is derived from view state (core/view/fx_state).
|
||||||
enum class Flag {
|
enum class Flag {
|
||||||
ShowInTcp, // B_SHOWINTCP
|
ShowInTcp, // B_SHOWINTCP
|
||||||
ShowInMixer, // B_SHOWINMIXER
|
ShowInMixer, // B_SHOWINMIXER
|
||||||
@@ -249,14 +258,12 @@ struct LanePlayOp {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// The complete set of operations to park one inactive leaf, or restore one
|
// One leaf's FLAG-DOMAIN plan: park writes fixed zeros, restore writes the
|
||||||
// leaf. Park uses fixed zeros; restore uses a snapshot's values. Park's
|
// snapshot's captured values. Per-FX offline is deliberately absent — it is a
|
||||||
// fxOffline ops are slot-keyed (every live slot goes offline); restore's carry
|
// derived function of view state (core/view/fx_state), not a peer of the flags,
|
||||||
// the snapshot's keying and are resolved against the live chain by
|
// and carrying it here only ever produced an always-empty park-side vector.
|
||||||
// resolveFxRestore before any write.
|
|
||||||
struct TrackPlan {
|
struct TrackPlan {
|
||||||
std::vector<TrackFlagOp> flags;
|
std::vector<TrackFlagOp> flags;
|
||||||
std::vector<FxOfflineOp> fxOffline;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The plan for a toggle to a target mode. Parents and show-both leaves never
|
// The plan for a toggle to a target mode. Parents and show-both leaves never
|
||||||
@@ -295,11 +302,27 @@ public:
|
|||||||
bool setActiveMode(const std::string& modeId);
|
bool setActiveMode(const std::string& modeId);
|
||||||
|
|
||||||
// The shell calls store before it parks a track, so restore survives a save.
|
// The shell calls store before it parks a track, so restore survives a save.
|
||||||
|
// Both halves are held from here; each is released by the act that spends it.
|
||||||
void storeSnapshot(const std::string& guid, const TrackSnapshot& snap);
|
void storeSnapshot(const std::string& guid, const TrackSnapshot& snap);
|
||||||
void clearSnapshot(const std::string& guid);
|
void clearSnapshot(const std::string& guid); // both halves at once
|
||||||
const TrackSnapshot* snapshot(const std::string& guid) const;
|
const TrackSnapshot* snapshot(const std::string& guid) const;
|
||||||
const std::map<std::string, TrackSnapshot>& snapshots() const { return snapshots_; }
|
const std::map<std::string, TrackSnapshot>& snapshots() const { return snapshots_; }
|
||||||
|
|
||||||
|
// A held half is one nothing has spent yet. `snapshot()` returning non-null is
|
||||||
|
// NOT that question: a record outlives its flag half, so a caller that reads
|
||||||
|
// the flag values off a record without asking here will drive stale ones.
|
||||||
|
bool holdsSnapshotFlags(const std::string& guid) const;
|
||||||
|
bool holdsSnapshotFx(const std::string& guid) const;
|
||||||
|
|
||||||
|
// Spent by the restore that writes the captured flags back. Zeroes them, so a
|
||||||
|
// record that no longer claims them cannot be read for them either.
|
||||||
|
void releaseSnapshotFlags(const std::string& guid);
|
||||||
|
|
||||||
|
// Spent only once the chain is ACTUALLY back online — not by the flag restore.
|
||||||
|
// Releasing both together destroys the only record of which plugins the user
|
||||||
|
// had offline before the park, and a later online then blanket-enables them.
|
||||||
|
void releaseSnapshotFx(const std::string& guid);
|
||||||
|
|
||||||
// Drops every snapshot whose GUID is NOT in `liveGuids`, and prunes the solo
|
// Drops every snapshot whose GUID is NOT in `liveGuids`, and prunes the solo
|
||||||
// cache the same way (see SoloCache::reconcile). Returns the count of
|
// cache the same way (see SoloCache::reconcile). Returns the count of
|
||||||
// SNAPSHOTS removed — the solo cache's own count is available from it directly.
|
// SNAPSHOTS removed — the solo cache's own count is available from it directly.
|
||||||
@@ -330,10 +353,7 @@ public:
|
|||||||
// restored from it. Parents and show-both leaves are never parked.
|
// restored from it. Parents and show-both leaves are never parked.
|
||||||
// Untagged leaves are Arrange members and park/restore accordingly. Tree
|
// Untagged leaves are Arrange members and park/restore accordingly. Tree
|
||||||
// membership is the enumeration source, so stale membership GUIDs absent
|
// membership is the enumeration source, so stale membership GUIDs absent
|
||||||
// from the tree are ignored.
|
// from the tree are ignored. Flag domain only; FX comes from desiredFxState.
|
||||||
//
|
|
||||||
// Park plans here carry an empty fxOffline vector — the D2 shell expands
|
|
||||||
// per-FX offline writes via TrackFX_GetCount (not available to the pure model).
|
|
||||||
TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const;
|
TogglePlan planToggle(const FolderTree& tree, const std::string& targetMode) const;
|
||||||
|
|
||||||
// Managed lanes only, from the ownership index — the set a toggle may
|
// Managed lanes only, from the ownership index — the set a toggle may
|
||||||
@@ -357,11 +377,11 @@ private:
|
|||||||
view::SoloCache soloCache_; // modeId -> guid -> raw I_SOLO
|
view::SoloCache soloCache_; // modeId -> guid -> raw I_SOLO
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fixed-zero park plan for one leaf, offlining `fxCount` slots.
|
// Fixed-zero park plan for one leaf.
|
||||||
TrackPlan makeParkPlan(const std::string& guid, int fxCount);
|
TrackPlan makeParkPlan(const std::string& guid);
|
||||||
|
|
||||||
// Restore plan for one leaf from its snapshot — every flag to its captured
|
// Restore plan for one leaf from its snapshot — every flag to its captured
|
||||||
// value, never a default.
|
// value, never a default. Callers must have checked holdsSnapshotFlags.
|
||||||
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap);
|
TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap);
|
||||||
|
|
||||||
// Auto-tag decision: new content takes the active mode at creation; the
|
// Auto-tag decision: new content takes the active mode at creation; the
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
#include "shell/capture/item_read.h"
|
#include "shell/capture/item_read.h"
|
||||||
#include "core/view/fx_offline.h"
|
#include "core/view/fx_offline.h"
|
||||||
|
#include "core/view/fx_state.h"
|
||||||
#include "core/view/lane_keys.h"
|
#include "core/view/lane_keys.h"
|
||||||
#include "core/view/solo_cache.h"
|
#include "core/view/solo_cache.h"
|
||||||
#include "shell/capture/track_guid.h"
|
#include "shell/capture/track_guid.h"
|
||||||
@@ -471,7 +472,8 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
|||||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||||
if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
if (!tr || !trackStillLive(project, tr)) continue; // stale/deleted GUID — prune
|
||||||
|
|
||||||
const TrackApplyResult r = restoreTrack(model, guid, tr, tp.fxOffline, tp.flags);
|
const std::vector<FxOfflineOp> fxOps = view::fxRestoreTargets(model, guid);
|
||||||
|
const TrackApplyResult r = restoreTrack(model, guid, tr, fxOps, tp.flags);
|
||||||
if (r.wrote) wrote = true;
|
if (r.wrote) wrote = true;
|
||||||
if (r.fxMoved) undoMask |= UNDO_STATE_FX;
|
if (r.fxMoved) undoMask |= UNDO_STATE_FX;
|
||||||
if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; }
|
if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; }
|
||||||
|
|||||||
@@ -203,15 +203,17 @@ TrackApplyResult restoreTrack(ViewModeModel& model, const std::string& guid, Med
|
|||||||
for (const FxOfflineWrite& w : res.writes)
|
for (const FxOfflineWrite& w : res.writes)
|
||||||
if (setOfflineIfChanged(tr, w.fxIndex, w.offline)) out.fxMoved = out.wrote = true;
|
if (setOfflineIfChanged(tr, w.fxIndex, w.offline)) out.fxMoved = out.wrote = true;
|
||||||
out.drops = res.drops;
|
out.drops = res.drops;
|
||||||
|
// The chain is back online HERE, which is the only thing that spends the FX half.
|
||||||
|
model.releaseSnapshotFx(guid);
|
||||||
|
|
||||||
if (applyFlags(tr, flags, out.fxMoved)) out.wrote = true;
|
if (applyFlags(tr, flags, out.fxMoved)) out.wrote = true;
|
||||||
|
model.releaseSnapshotFlags(guid);
|
||||||
|
|
||||||
// Unconditional, even when every write above was elided: the snapshot is consumed
|
// Unconditional, even when every write above was elided: the record is consumed
|
||||||
// here, so this apply changed state and is not repeatable — and `wrote`, whose one
|
// here, so this apply changed state and is not repeatable — and `wrote`, whose one
|
||||||
// reader asks exactly that, would be a lie at false. NOT a claim about what the
|
// reader asks exactly that, would be a lie at false. NOT a claim about what the
|
||||||
// undo record holds: the block never carries the snapshot either way, that being
|
// undo record holds: the block never carries the record either way, that being
|
||||||
// model state persisted after Undo_EndBlock2 (src/shell/view/CLAUDE.md).
|
// model state persisted after Undo_EndBlock2 (src/shell/view/CLAUDE.md).
|
||||||
model.clearSnapshot(guid);
|
|
||||||
out.wrote = true;
|
out.wrote = true;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// 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);
|
||||||
|
|
||||||
|
// {T1} is already parked (tagged Design, active mode Arrange); retagging it
|
||||||
|
// INTO the active mode changes what the next apply must drive (release it)
|
||||||
|
// while its park record — and so the fingerprint — is untouched. Pinning
|
||||||
|
// this on a GUID actually in the parked set is the point: an unrelated GUID
|
||||||
|
// would pass even if membership were folded into the fingerprint by mistake.
|
||||||
|
vm.membership().tag("{T1}", kArrangeModeId);
|
||||||
|
CHECK(appliedState(vm) == before);
|
||||||
|
|
||||||
|
vm.membership().setShowBoth("{T1}", 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
// 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, kArrangeModeId, {"{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, kArrangeModeId, {"{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, kArrangeModeId, {"{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, kDesignModeId, {"{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, kDesignModeId, {"{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, kDesignModeId, {"{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, kArrangeModeId, {}).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, kArrangeModeId, {"{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, kArrangeModeId, {"{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, kDesignModeId, {"{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, kDesignModeId, {"{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, kDesignModeId, {"{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, kDesignModeId, {"{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, kDesignModeId, {"{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, kDesignModeId, {"{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, kArrangeModeId, {"{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);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testResumeAfterUnidentifiedFxMakesForwardProgress() {
|
||||||
|
// The drain completed slot 1, an FX REAPER reports no GUID for. Before the
|
||||||
|
// slot-cursor fix, an empty resumeAfterFxGuid was indistinguishable from
|
||||||
|
// "nothing done yet" and this item would restart at slot 0 every tick,
|
||||||
|
// forever — the non-terminating drain this test pins against.
|
||||||
|
ViewModeModel parked = parkedWithThreeFx(); // Arrange active ⇒ {T} parks (Offline, slot-keyed)
|
||||||
|
std::vector<FxTrackWork> work = desiredFxState(parked, kArrangeModeId, {"{T}"});
|
||||||
|
CHECK(work.size() == 1);
|
||||||
|
if (work.empty()) return;
|
||||||
|
|
||||||
|
work[0].resumeAfterFxGuid = "";
|
||||||
|
work[0].resumeAfterSlot = 1;
|
||||||
|
const FxRestoreResolution res =
|
||||||
|
resolveFxWork(work[0], {"{FX-A}", "", "{FX-B}", "{FX-C}"});
|
||||||
|
CHECK(res.writes.size() == 2); // slots 2 and 3 only — 0 and 1 already done
|
||||||
|
CHECK(!anyWriteAt(res, 0));
|
||||||
|
CHECK(!anyWriteAt(res, 1));
|
||||||
|
CHECK(anyWriteAt(res, 2));
|
||||||
|
CHECK(anyWriteAt(res, 3));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
testActiveTrackIsOwedOnlineInactiveIsOwedOffline();
|
||||||
|
testOnlineWorkSortsBeforeOfflineWork();
|
||||||
|
testUserOfflinedPluginIsNeverBlanketOnlined();
|
||||||
|
testReleasedFxHalfIsOwedNothing();
|
||||||
|
testDeadAndUnparkedTracksAreOwedNothing();
|
||||||
|
|
||||||
|
testOfflineWorkTargetsEveryLiveSlot();
|
||||||
|
testReorderRestoreLandsEachPluginItsOwnState();
|
||||||
|
testDeletedFxIsDroppedExplicitly();
|
||||||
|
testAddedFxIsLeftAlone();
|
||||||
|
testSlotKeyedRecordStillRestoresByPosition();
|
||||||
|
|
||||||
|
testResumePointSkipsWhatTheDrainAlreadyDid();
|
||||||
|
testResumePointThatLeftTheChainRestartsRatherThanStrands();
|
||||||
|
testResumeAfterUnidentifiedFxMakesForwardProgress();
|
||||||
|
|
||||||
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
|
return g_fail ? 1 : 0;
|
||||||
|
}
|
||||||
@@ -45,14 +45,14 @@ struct FlagValues {
|
|||||||
static void testAChainSittingAtEveryValueTheParkWouldWriteReadsAsParked() {
|
static void testAChainSittingAtEveryValueTheParkWouldWriteReadsAsParked() {
|
||||||
// The four zeros are what a parked track's driven flags actually read; if
|
// 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.
|
// 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(park.flags.size() == 4);
|
||||||
CHECK(parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 0, 0}));
|
CHECK(parkFlagsAlreadyApplied(park.flags, FlagValues{0, 0, 0, 0}));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void testOneFlagStillAtTheUsersValueMeansNoParkReachedTheChain() {
|
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
|
// 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.
|
// in the plan: a missing op makes its case read parked and fail here.
|
||||||
|
|||||||
+100
-119
@@ -10,7 +10,7 @@
|
|||||||
// 4. show-both leaf never appears in a park op-list and is visible in all modes.
|
// 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).
|
// 5. Unknown/stale GUID tolerated (ignore-and-prune, no crash).
|
||||||
// 6. JSON round-trip lossless: modes + membership + show-both + snapshots + active.
|
// 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
|
// 8. Per-FX offline is keyed by FX identity, and the v1 (slot-keyed) blob lifts
|
||||||
// into that keying without losing its restore.
|
// into that keying without losing its restore.
|
||||||
// 9. Nested-folder toggle: the snapshot store/clear lifecycle survives a re-park
|
// 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() {
|
static void testSerializeGoldenLiteral() {
|
||||||
ViewModeModel vm;
|
ViewModeModel vm;
|
||||||
CHECK(vm.serialize() ==
|
CHECK(vm.serialize() ==
|
||||||
"{\"version\":2,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\","
|
"{\"version\":3,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\","
|
||||||
"\"displayName\":\"Arrange\",\"ordinal\":0},{\"id\":\"design\","
|
"\"displayName\":\"Arrange\",\"ordinal\":0},{\"id\":\"design\","
|
||||||
"\"displayName\":\"Design\",\"ordinal\":1}],\"membership\":[],"
|
"\"displayName\":\"Design\",\"ordinal\":1}],\"membership\":[],"
|
||||||
"\"snapshots\":[],\"lanes\":[],\"soloCache\":[]}");
|
"\"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.mainSend = 0; // user had it OUT of the mix for their own reason
|
||||||
snap.fxEnable = 1;
|
snap.fxEnable = 1;
|
||||||
// Slot 1's plugin was already offline before parking; each entry carries the
|
// 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}};
|
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::ShowInTcp) == 0);
|
||||||
CHECK(flagValue(park, Flag::ShowInMixer) == 0);
|
CHECK(flagValue(park, Flag::ShowInMixer) == 0);
|
||||||
CHECK(flagValue(park, Flag::MainSend) == 0);
|
CHECK(flagValue(park, Flag::MainSend) == 0);
|
||||||
CHECK(flagValue(park, Flag::FxEnable) == 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);
|
TrackPlan restore = makeRestorePlan("{T}", snap);
|
||||||
// Every flag returns to its CAPTURED value — not a hardcoded "on".
|
// 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::ShowInMixer) == 1);
|
||||||
CHECK(flagValue(restore, Flag::MainSend) == 0); // the "already at 0 stays 0" case
|
CHECK(flagValue(restore, Flag::MainSend) == 0); // the "already at 0 stays 0" case
|
||||||
CHECK(flagValue(restore, Flag::FxEnable) == 1);
|
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).
|
// A snapshot entirely at 0 must restore entirely to 0 (no default leaks in).
|
||||||
TrackSnapshot zero; // all zeros, empty fxOffline
|
TrackSnapshot zero; // all zeros, empty fxOffline
|
||||||
@@ -320,7 +311,6 @@ static void testRestoreRoundTripSnapshotValues() {
|
|||||||
CHECK(flagValue(rz, Flag::ShowInMixer) == 0);
|
CHECK(flagValue(rz, Flag::ShowInMixer) == 0);
|
||||||
CHECK(flagValue(rz, Flag::MainSend) == 0);
|
CHECK(flagValue(rz, Flag::MainSend) == 0);
|
||||||
CHECK(flagValue(rz, Flag::FxEnable) == 0);
|
CHECK(flagValue(rz, Flag::FxEnable) == 0);
|
||||||
CHECK(rz.fxOffline.empty());
|
|
||||||
|
|
||||||
// End-to-end via planToggle: a leaf tagged Design, snapshotted, parked while in
|
// End-to-end via planToggle: a leaf tagged Design, snapshotted, parked while in
|
||||||
// Arrange, then restored when we toggle back to Design.
|
// 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.
|
// A TogglePlan carries the four driven scalars and nothing else; per-FX offline
|
||||||
// The D2 shell is responsible for expanding per-FX offline ops using
|
// is derived from view state (core/view/fx_state), not planned beside them.
|
||||||
// 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.
|
|
||||||
|
|
||||||
static void testPlanToggleParkHasEmptyFxOffline() {
|
static void testPlanTogglePlansFlagsOnly() {
|
||||||
ViewModeModel vm;
|
ViewModeModel vm;
|
||||||
FolderTree tree;
|
FolderTree tree;
|
||||||
tree.nodes.push_back(FolderNode{"{LEAF}", "", false});
|
tree.nodes.push_back(FolderNode{"{LEAF}", "", false});
|
||||||
|
tree.nodes.push_back(FolderNode{"{BACK}", "", false});
|
||||||
vm.membership().tag("{LEAF}", kDesignModeId);
|
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);
|
auto plan = vm.planToggle(tree, kArrangeModeId);
|
||||||
CHECK(plan.park.size() == 1);
|
CHECK(plan.park.size() == 1);
|
||||||
// The park plan must have an empty fxOffline — the shell expands FX ops.
|
CHECK(plan.park[0].flags.size() == 4); // the four park zeros, nothing else
|
||||||
CHECK(plan.park[0].fxOffline.empty());
|
CHECK(plan.restore.size() == 1);
|
||||||
// Scalar flags must still be present (the four park zeros).
|
CHECK(plan.restore[0].flags.size() == 4);
|
||||||
CHECK(plan.park[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 ----------------------
|
// -- 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::ShowInMixer) == 1);
|
||||||
CHECK(flagValue(*r, Flag::MainSend) == 0); // captured 0 comes back 0
|
CHECK(flagValue(*r, Flag::MainSend) == 0); // captured 0 comes back 0
|
||||||
CHECK(flagValue(*r, Flag::FxEnable) == 1);
|
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);
|
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
|
// The persisted shape only. Resolving a captured FX state against the live chain
|
||||||
// snapshot -> planToggle -> makeRestorePlan ops -> resolveFxRestore against the
|
// moved to core/view/fx_state — test_fx_state owns those cases.
|
||||||
// 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.
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// The plan's restore ops for one parked-then-reactivated leaf.
|
// A leaf tagged Design, parked with three identified FX.
|
||||||
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.
|
|
||||||
ViewModeModel parkedWithThreeFx() {
|
ViewModeModel parkedWithThreeFx() {
|
||||||
ViewModeModel vm;
|
ViewModeModel vm;
|
||||||
vm.membership().tag("{T}", kDesignModeId);
|
vm.membership().tag("{T}", kDesignModeId);
|
||||||
@@ -2066,46 +2098,6 @@ ViewModeModel parkedWithThreeFx() {
|
|||||||
|
|
||||||
} // namespace
|
} // 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() {
|
static void testV2WritesTheLegacySlotArrayBesideIdentities() {
|
||||||
// The downgrade half of the ladder: a build that predates identity keying
|
// 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
|
// 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);
|
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.
|
// A view_state written before FX identity existed: no "fx" key anywhere.
|
||||||
const char* v1 =
|
const char* v1 =
|
||||||
"{\"version\":1,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\","
|
"{\"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].offline == 1);
|
||||||
CHECK(snap && snap->fxOffline[1].fxGuid.empty());
|
CHECK(snap && snap->fxOffline[1].fxGuid.empty());
|
||||||
CHECK(snap && snap->showInTcp == 1 && snap->mainSend == 1);
|
CHECK(snap && snap->showInTcp == 1 && snap->mainSend == 1);
|
||||||
|
// That it still RESTORES — by position, all its bytes can support — is
|
||||||
// It still restores — by position, which is all its bytes can support, and
|
// test_fx_state's case; here the lift only has to preserve the keying.
|
||||||
// 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));
|
|
||||||
|
|
||||||
// Re-saving a lifted snapshot does NOT invent identities for it: the "fx"
|
// 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.
|
// key stays absent, and a second load reads the same slot-keyed shape.
|
||||||
@@ -2202,7 +2184,9 @@ int main() {
|
|||||||
testJsonRoundTrip();
|
testJsonRoundTrip();
|
||||||
testEmptyModelRoundTrip();
|
testEmptyModelRoundTrip();
|
||||||
testMalformedJson();
|
testMalformedJson();
|
||||||
testPlanToggleParkHasEmptyFxOffline();
|
testPlanTogglePlansFlagsOnly();
|
||||||
|
testReleasedFlagHalfIsNotReplanned();
|
||||||
|
testHalfReleasedRecordRoundTrips();
|
||||||
testUntaggedLeavesManagedByModeSystem();
|
testUntaggedLeavesManagedByModeSystem();
|
||||||
testTaggedLeafBehaviorUnchangedWithUntagged();
|
testTaggedLeafBehaviorUnchangedWithUntagged();
|
||||||
testNestedToggleSnapshotSurvivesRepark();
|
testNestedToggleSnapshotSurvivesRepark();
|
||||||
@@ -2242,12 +2226,9 @@ int main() {
|
|||||||
testSoloCacheMalformedJson();
|
testSoloCacheMalformedJson();
|
||||||
testReconcilePrunesTheSoloCacheAlongsideSnapshots();
|
testReconcilePrunesTheSoloCacheAlongsideSnapshots();
|
||||||
|
|
||||||
// Per-FX offline identity keying + the v1 -> v2 snapshot ladder
|
// Per-FX offline: the v1 -> v2 snapshot ladder
|
||||||
testParkReorderRestoreLandsEachPluginItsOwnState();
|
|
||||||
testParkDeleteFxRestoreDropsExplicitly();
|
|
||||||
testParkAddFxRestoreLeavesItAlone();
|
|
||||||
testV2WritesTheLegacySlotArrayBesideIdentities();
|
testV2WritesTheLegacySlotArrayBesideIdentities();
|
||||||
testLegacyBlobLiftsToSlotKeyingAndStillRestores();
|
testLegacyBlobLiftsToSlotKeying();
|
||||||
|
|
||||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user