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:
2026-08-06 04:32:26 -04:00
parent 8a7056d19d
commit 8c06383fe7
11 changed files with 392 additions and 925 deletions
+53 -85
View File
@@ -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;
}