Collapse a Design View mode switch to ONE undo point: FX writes run inline in applyMode's block, deferred park queue deleted
This commit is contained in:
+53
-85
@@ -1,7 +1,8 @@
|
||||
// See view.h. Compiled into the reaper_reasampler module; includes
|
||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||
// Tree arithmetic lives in view_tree (pure); this file owns REAPER reads/writes
|
||||
// and the snapshot-before-park ordering.
|
||||
// Tree arithmetic lives in view_tree (pure) and one track's park/restore in
|
||||
// view_fx_park; this file owns the plan iteration, lanes, parents and the one
|
||||
// undo block they all ride.
|
||||
|
||||
#include "shell/view/view.h"
|
||||
|
||||
@@ -65,8 +66,8 @@ constexpr int kTransportMoving = 1 | 4;
|
||||
// (UNDO_STATE_MISCCFG covers extension state). NOT UNDO_STATE_ALL, which
|
||||
// includes UNDO_STATE_FX and so makes every undo record carry every track's FX
|
||||
// state — a cost that scales with the project's plugin count rather than with
|
||||
// what the switch changed. FX is OR'd back in per apply, only when an FX-domain
|
||||
// flag really moved.
|
||||
// what the switch changed. FX is OR'd back in per apply, only when I_FXEN or a
|
||||
// per-FX offline state really moved (TrackApplyResult::fxMoved).
|
||||
// [verify — DAW] INFERRED, not documented: UNDO_STATE_TRACKCFG reads
|
||||
// "track/master vol/pan/routing" (reaper_plugin.h:1541) and names neither
|
||||
// B_SHOWINTCP/B_SHOWINMIXER nor the fixed-lane properties (assumed to ride
|
||||
@@ -146,45 +147,6 @@ MediaTrack* resolve(const TrackByGuid& byGuid, const std::string& guid) {
|
||||
return it == byGuid.end() ? nullptr : it->second; // stale/deleted GUID — pruned by being skipped
|
||||
}
|
||||
|
||||
// Captures prior driven-flag state before parking. Never reads B_MUTE/I_SOLO;
|
||||
// ints preserve whatever REAPER reported (TrackSnapshot's defensive contract).
|
||||
// The FX half is snapshotFxOffline's — only it knows when the live chain has
|
||||
// stopped being a trustworthy source.
|
||||
TrackSnapshot snapshotTrack(MediaTrack* tr, const std::vector<FxOfflineOp>& cancelledRestore) {
|
||||
auto read = [tr](Flag f) {
|
||||
return static_cast<int>(GetMediaTrackInfo_Value(tr, trackFlagParm(f)));
|
||||
};
|
||||
TrackSnapshot snap;
|
||||
snap.showInTcp = read(Flag::ShowInTcp);
|
||||
snap.showInMixer = read(Flag::ShowInMixer);
|
||||
snap.mainSend = read(Flag::MainSend);
|
||||
snap.fxEnable = read(Flag::FxEnable);
|
||||
|
||||
PreParkFx fx = snapshotFxOffline(tr, cancelledRestore);
|
||||
snap.fxOffline = std::move(fx.states);
|
||||
snap.fxKeying = fx.keying;
|
||||
return snap;
|
||||
}
|
||||
|
||||
// A reapply (tag/untag, project load) re-plans every flag it already applied, so
|
||||
// most of what it writes is a value the track already holds. The read is ASSUMED
|
||||
// cheaper than the write it elides (unmeasured); what it certainly does is keep
|
||||
// those no-op writes out of the undo record's mask.
|
||||
bool writeIfChanged(MediaTrack* tr, const char* parm, double value) {
|
||||
if (GetMediaTrackInfo_Value(tr, parm) == value) return false;
|
||||
SetMediaTrackInfo_Value(tr, parm, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
void applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags, int& undoMask) {
|
||||
for (const TrackFlagOp& op : flags) {
|
||||
if (!writeIfChanged(tr, trackFlagParm(op.flag), static_cast<double>(op.value))) continue;
|
||||
// I_FXEN is the only FX-domain flag driven here, so it is the only one
|
||||
// whose undo record has to carry UNDO_STATE_FX.
|
||||
if (op.flag == Flag::FxEnable) undoMask |= UNDO_STATE_FX;
|
||||
}
|
||||
}
|
||||
|
||||
// Managed-lane application: LanePlayOps are keyed by the lane's DURABLE name but
|
||||
// C_LANEPLAYS:N by current ordinal, which renumbers on reorder — so every write
|
||||
// re-resolves durable key -> ordinal first, and a lane whose name lacks the
|
||||
@@ -216,25 +178,32 @@ std::map<std::string, int> managedLaneOrdinals(MediaTrack* tr) {
|
||||
// item-side C_LANEPLAYS is read-only, so no per-item write exists or is
|
||||
// needed). B_FIXEDLANE_HIDDEN is also read-only — hide/show follows from
|
||||
// C_LANEPLAYS=0/1, never written directly.
|
||||
void applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
||||
bool applyLanePlays(MediaTrack* tr, int laneIdx, int lanePlays) {
|
||||
char parm[32];
|
||||
std::snprintf(parm, sizeof(parm), "C_LANEPLAYS:%d", laneIdx);
|
||||
writeIfChanged(tr, parm, static_cast<double>(lanePlays));
|
||||
return writeIfChanged(tr, parm, static_cast<double>(lanePlays));
|
||||
}
|
||||
|
||||
// `freeMode` alone decides whether UpdateTimeline is owed; `wrote` is any project
|
||||
// write at all, which is what the apply's undo-point decision reads.
|
||||
struct LaneApplyResult {
|
||||
bool freeMode = false;
|
||||
bool wrote = false;
|
||||
};
|
||||
|
||||
// Groups ops by track, reconciles each op's durable laneKey to the track's
|
||||
// current ordinal (a stale/renamed/deleted key is pruned, never mis-driven),
|
||||
// enables fixed-lane mode on any track carrying a managed lane, and drives
|
||||
// C_LANEPLAYS. UpdateTimeline() is the caller's job when this returns true
|
||||
// (SDK: required after an I_FREEMODE change).
|
||||
bool applyLaneOps(const TrackByGuid& handleByGuid,
|
||||
const std::vector<LanePlayOp>& lanes) {
|
||||
if (lanes.empty()) return false;
|
||||
// C_LANEPLAYS. UpdateTimeline() is the caller's job when `freeMode` comes back
|
||||
// true (SDK: required after an I_FREEMODE change).
|
||||
LaneApplyResult applyLaneOps(const TrackByGuid& handleByGuid,
|
||||
const std::vector<LanePlayOp>& lanes) {
|
||||
LaneApplyResult out;
|
||||
if (lanes.empty()) return out;
|
||||
|
||||
std::map<std::string, std::vector<const LanePlayOp*>> byTrack;
|
||||
for (const LanePlayOp& op : lanes) byTrack[op.trackGuid].push_back(&op);
|
||||
|
||||
bool touchedFreeMode = false;
|
||||
for (const auto& [guid, ops] : byTrack) {
|
||||
MediaTrack* tr = resolve(handleByGuid, guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
@@ -248,17 +217,18 @@ bool applyLaneOps(const TrackByGuid& handleByGuid,
|
||||
SetMediaTrackInfo_Value(tr, "I_FREEMODE",
|
||||
static_cast<double>(kFreeModeFixedLanes));
|
||||
applyTransparentLaneDisplay(tr);
|
||||
touchedFreeMode = true;
|
||||
out.freeMode = true;
|
||||
out.wrote = true;
|
||||
}
|
||||
|
||||
const std::map<std::string, int> ordinals = managedLaneOrdinals(tr);
|
||||
for (const LanePlayOp* op : ops) {
|
||||
auto it = ordinals.find(op->laneKey);
|
||||
if (it == ordinals.end()) continue; // key not live on this track — prune
|
||||
applyLanePlays(tr, it->second, op->lanePlays);
|
||||
if (applyLanePlays(tr, it->second, op->lanePlays)) out.wrote = true;
|
||||
}
|
||||
}
|
||||
return touchedFreeMode;
|
||||
return out;
|
||||
}
|
||||
|
||||
// Managed-lane minting: the DECISION (which tracks split, which lanes, which item
|
||||
@@ -442,7 +412,10 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
|
||||
int undoMask = kApplyUndoMask;
|
||||
bool laneModeChanged = false;
|
||||
bool wrote = false; // anything at all this apply changed — see applyMintsUndoPoint
|
||||
std::vector<std::string> refusedParkNames;
|
||||
FxRestoreDrops fxDrops;
|
||||
int fxDropTracks = 0;
|
||||
|
||||
Undo_BeginBlock2(proj);
|
||||
{
|
||||
@@ -455,48 +428,41 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
clearTrackSolos(handleByGuid, outgoingSolo);
|
||||
}
|
||||
|
||||
// PARK: snapshot before mutating. The per-FX offline half is deferred
|
||||
// (view_fx_park) — the expensive half, and the new mode's appearance
|
||||
// does not wait on it.
|
||||
// PARK / RESTORE: view_fx_park owns one track's whole move, flags and
|
||||
// per-FX offline together. A refusal is named HERE, while the handle is
|
||||
// fresh — never held past Undo_EndBlock2/TrackList_AdjustWindows.
|
||||
for (const TrackPlan& tp : plan.park) {
|
||||
if (tp.flags.empty()) continue; // every op in a TrackPlan targets one track
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
// Re-validated per track, not once at resolve: offlining a plugin is
|
||||
// ASSUMED to pump the message loop, so a track deleted during an
|
||||
// earlier iteration's FX writes would leave this handle dangling.
|
||||
if (!tr || !trackStillLive(proj, tr)) continue; // stale/deleted GUID — prune
|
||||
|
||||
// decidePark's contract; view_fx_park owns the reads. A refusal is named HERE, not held past Undo_EndBlock2.
|
||||
const ParkAction action =
|
||||
decideParkForTrack(tr, model.snapshot(guid) != nullptr, tp.flags);
|
||||
if (action == ParkAction::Refuse) { refusedParkNames.push_back(trackDisplayName(tr)); continue; }
|
||||
|
||||
// Enqueued FIRST: a park landing on this track's own pending restore
|
||||
// cancels it, and those ops are then the only surviving record of the
|
||||
// pre-park FX state.
|
||||
const std::vector<FxOfflineOp> cancelled = deferFxPark(proj, guid);
|
||||
if (action == ParkAction::SnapshotThenPark)
|
||||
model.storeSnapshot(guid, snapshotTrack(tr, cancelled));
|
||||
applyFlags(tr, tp.flags, undoMask);
|
||||
const TrackApplyResult r = parkTrack(model, guid, tr, tp.flags);
|
||||
if (r.refused) { refusedParkNames.push_back(trackDisplayName(tr)); continue; }
|
||||
if (r.wrote) wrote = true;
|
||||
if (r.fxMoved) undoMask |= UNDO_STATE_FX;
|
||||
}
|
||||
|
||||
// RESTORE: flags verbatim now, per-FX offline on the drain.
|
||||
for (const TrackPlan& tp : plan.restore) {
|
||||
if (tp.flags.empty()) continue;
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
MediaTrack* tr = resolve(trackByGuid, guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
if (!tr || !trackStillLive(proj, tr)) continue; // stale/deleted GUID — prune
|
||||
|
||||
applyFlags(tr, tp.flags, undoMask);
|
||||
deferFxRestore(proj, guid, tp.fxOffline);
|
||||
// Consumed HERE, not on the drain: from this line the flags are back
|
||||
// at their captured values, and a persist or reapply landing inside
|
||||
// the drain window must not replan a restore over what the user has
|
||||
// changed since.
|
||||
model.clearSnapshot(guid);
|
||||
const TrackApplyResult r = restoreTrack(model, guid, tr, tp.fxOffline, tp.flags);
|
||||
if (r.wrote) wrote = true;
|
||||
if (r.fxMoved) undoMask |= UNDO_STATE_FX;
|
||||
if (r.drops.total() > 0) { fxDrops.add(r.drops); ++fxDropTracks; }
|
||||
}
|
||||
|
||||
// MANAGED LANES: the active mode's lane plays+shows, every other managed
|
||||
// lane is silenced+hidden. Empty on a D1-only project — byte-identical there.
|
||||
laneModeChanged = applyLaneOps(trackByGuid, plan.lanes);
|
||||
const LaneApplyResult lanes = applyLaneOps(trackByGuid, plan.lanes);
|
||||
laneModeChanged = lanes.freeMode;
|
||||
if (lanes.wrote) wrote = true;
|
||||
|
||||
// PARENT VISIBILITY (never parked): recomputed every toggle, never
|
||||
// snapshotted. Only the two visibility flags — never mainSend/FX on a parent.
|
||||
@@ -506,8 +472,8 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
MediaTrack* tr = resolve(trackByGuid, node.guid);
|
||||
if (!tr) continue; // stale GUID — prune
|
||||
const double show = visible.count(node.guid) ? 1.0 : 0.0;
|
||||
writeIfChanged(tr, "B_SHOWINTCP", show);
|
||||
writeIfChanged(tr, "B_SHOWINMIXER", show);
|
||||
if (writeIfChanged(tr, "B_SHOWINTCP", show)) wrote = true;
|
||||
if (writeIfChanged(tr, "B_SHOWINMIXER", show)) wrote = true;
|
||||
}
|
||||
|
||||
model.setActiveMode(targetModeId);
|
||||
@@ -535,11 +501,13 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
|
||||
// fixed lanes this apply (SDK requirement for I_FREEMODE changes).
|
||||
if (laneModeChanged) UpdateTimeline();
|
||||
|
||||
Undo_EndBlock2(proj, undoLabel.c_str(), undoMask);
|
||||
const bool mint = applyMintsUndoPoint(realSwitch, wrote);
|
||||
Undo_EndBlock2(proj, mint ? undoLabel.c_str() : "", mint ? undoMask : 0);
|
||||
|
||||
// After the undo block and the UI hold: a refusal is a report, not a project
|
||||
// write, and it must not join what a Ctrl-Z rolls back.
|
||||
// After the undo block and the UI hold: a report is not a project write, and
|
||||
// it must not join what a Ctrl-Z rolls back.
|
||||
reportRefusedParks(proj, refusedParkNames);
|
||||
reportFxRestoreDrops(fxDrops, fxDropTracks);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user