Ψ-W1-T2 review remediation: solo restore drops on visible-in-target, not parked; N-mode segments read dead when unroutable

Fixes a hidden-parent solo replay that could silence the mix. Also closes the N-mode segment silent no-op, amends the invariant comment, trims view.cpp under 600 lines, hedges two SDK inferences, drops a dead null-check.
This commit is contained in:
2026-08-01 20:13:10 -04:00
parent 9c234c2e6b
commit f2cdf676f3
17 changed files with 137 additions and 73 deletions
+7 -1
View File
@@ -83,7 +83,7 @@ applies the resulting lane state to live tracks.
## Modules
- `view` — Design View shell: snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline), restores from snapshot. Owns the one discriminator (`target != active`) that separates a real switch from a reapply, and with it both the playback gate (`transportBlocksModeSwitch`) and the solo cache/clear/restore seams. **Never touches master or `B_MUTE`.**
- `view_solo` — the `I_SOLO` read/write pair behind the per-mode solo surface. Holds no policy: what to cache, clear, or replay is `core/view/solo_cache`.
- `view_solo` — the `I_SOLO` read/write pair behind the per-mode solo surface, plus `clearTrackSolos`/`restoreTrackSolos`, the outgoing-clear and incoming-replay entry points `view` drives them through. Holds no policy: what to cache, clear, or replay is `core/view/solo_cache`.
## Gotchas
@@ -94,3 +94,9 @@ applies the resulting lane state to live tracks.
(fixed-lane mechanics, mode-aware capture placement, hidden-AND-silenced) are
reflected in Invariants above; the membership/lane-ownership model concepts
it also covers live in `core/view`'s Invariants.
- REAPER's own undo restores live `I_SOLO` but not the model's solo cache or
`activeModeId` — neither rolls back with a Ctrl-Z. An undo after a mode switch
leaves the two out of step, and the next switch banks the undo-restored solos
under whatever mode id is active at that point, not the one the user undid back
to. Pre-existing: `snapshots_` already carries this same model-vs-undo split;
the solo cache inherits it rather than introducing it. Not fixed here.
+6 -18
View File
@@ -60,15 +60,6 @@ constexpr int kFreeModeFixedLanes = 2;
// deliberately excluded, nothing is moving there.
constexpr int kTransportMoving = 1 | 4;
// GUIDs the plan parks in the mode being entered — the set a solo restore skips.
std::set<std::string> parkedGuidsOf(const TogglePlan& plan) {
std::set<std::string> parked;
for (const TrackPlan& tp : plan.park) {
if (!tp.flags.empty()) parked.insert(tp.flags.front().guid);
}
return parked;
}
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
// but displays as non-fixed-lane).
@@ -394,7 +385,6 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
} // namespace
bool transportBlocksModeSwitch(ReaProject* proj) {
if (!GetPlayStateEx) return false; // fail-open: never gate on a missing API pointer
return (GetPlayStateEx(proj) & kTransportMoving) != 0;
}
@@ -439,11 +429,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
// ride the existing undo block — one mode toggle stays one Ctrl-Z.
if (realSwitch) {
model.soloCache().store(outgoingModeId, outgoingSolo);
std::vector<view::SoloOp> clearOps;
clearOps.reserve(outgoingSolo.size());
for (const auto& entry : outgoingSolo)
clearOps.push_back(view::SoloOp{entry.first, 0});
applySoloOps(handleByGuid, clearOps);
clearTrackSolos(handleByGuid, outgoingSolo);
}
// PARK: snapshot before mutating, store into the model, then apply.
@@ -502,11 +488,13 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
model.setActiveMode(targetModeId);
// Replay + consume, before the single relayout below picks the change up.
// Replay + consume, before the single relayout below picks the change up. Dropped
// against `visible` (computed above for parent visibility), not the park plan: a
// folder parent can be hidden without being parked, and a hidden track must not
// receive a replayed solo it carries no visible control to undo.
if (realSwitch) {
if (const std::map<std::string, int>* cached = model.soloCache().query(targetModeId)) {
applySoloOps(handleByGuid,
view::planSoloRestore(*cached, liveGuids, parkedGuidsOf(plan)));
restoreTrackSolos(handleByGuid, *cached, liveGuids, visible);
model.soloCache().clear(targetModeId);
}
}
+5 -2
View File
@@ -3,7 +3,8 @@
// ViewModeModel's pure planner, and applies the resulting flag / per-FX /
// lane writes. The .cpp is the sole REAPER-facing TU here (CLAUDE.md contract:
// only main.cpp defines the API pointers). See src/shell/view/CLAUDE.md for
// the enforced invariants (never touch master/mute/solo, snapshot-based restore).
// the enforced invariants (never touch master/mute, solo cached per mode and
// never lost, snapshot-based restore).
#include <string>
@@ -17,7 +18,9 @@ namespace reasampler {
// True while `proj`'s transport is playing or recording — the condition under which
// applyMode refuses a real mode switch. Exposed so the panel can paint the footer's
// [Arrange|Design] segments disabled BEFORE the click rather than only refusing on
// it. `proj` == nullptr means the current project.
// it. `proj` is passed through to GetPlayStateEx as-is; the SDK documents proj=0
// meaning "current project" for GetTrack but is silent on GetPlayStateEx, so
// `nullptr` meaning the current project here is inferred by analogy, not confirmed.
bool transportBlocksModeSwitch(ReaProject* proj);
// Snapshots each about-to-park track's flags into `model`, caches/clears the
+14
View File
@@ -44,4 +44,18 @@ void applySoloOps(const TrackHandles& handles, const std::vector<view::SoloOp>&
}
}
void clearTrackSolos(const TrackHandles& handles, const std::map<std::string, int>& soloed) {
std::vector<view::SoloOp> ops;
ops.reserve(soloed.size());
for (const auto& entry : soloed) ops.push_back(view::SoloOp{entry.first, 0});
applySoloOps(handles, ops);
}
void restoreTrackSolos(const TrackHandles& handles,
const std::map<std::string, int>& cached,
const std::set<std::string>& liveGuids,
const std::set<std::string>& visibleGuids) {
applySoloOps(handles, view::planSoloRestore(cached, liveGuids, visibleGuids));
}
} // namespace reasampler
+13
View File
@@ -3,6 +3,8 @@
// to cache, clear, or replay is core/view/solo_cache; this pair only moves I_SOLO
// between the live tracks and that pure plan.
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
@@ -25,4 +27,15 @@ std::vector<view::TrackSolo> readTrackSolos(const TrackHandles& handles);
// absent from the enumeration is skipped (stale/deleted track).
void applySoloOps(const TrackHandles& handles, const std::vector<view::SoloOp>& ops);
// The outgoing-mode clear: zeroes I_SOLO for every GUID in `soloed` (the just-banked
// values applyMode is about to cache).
void clearTrackSolos(const TrackHandles& handles, const std::map<std::string, int>& soloed);
// The incoming-mode replay: applies `cached` filtered through planSoloRestore's drop
// rules (dead GUID, not visible in the incoming mode).
void restoreTrackSolos(const TrackHandles& handles,
const std::map<std::string, int>& cached,
const std::set<std::string>& liveGuids,
const std::set<std::string>& visibleGuids);
} // namespace reasampler