Split unidentified from missing FX in Design View drop report, fix message + console pop

Distinguish no-GUID-at-capture from identity-no-longer-live; rewrite the drop
message to state the real recovery step; mark FX-GUID stability [verify — DAW];
guard mismatched fx/fxOffline lengths; the report never force-opens now.
This commit is contained in:
2026-08-02 18:51:41 -04:00
parent 5f6efb7cc3
commit d5ed4f6e53
7 changed files with 109 additions and 31 deletions
+1 -1
View File
@@ -97,7 +97,7 @@ settled 2026-07-23):
## Modules
- `view_mode_model` — Design View mode system: mode registry, GUID-keyed membership, folder-tree-aware visibility derivation, snapshot-based park/restore planner, the per-mode `SoloCache` it owns, JSON round-trip.
- `fx_offline` — the per-FX offline snapshot's KEY and its restore resolution: `FxKeying` (Identity / Slot), `FxOfflineState`, the planned `FxOfflineOp`, and `resolveFxRestore`, which matches each captured state to the FX it came from against the chain as it stands at restore time. An identity that is no longer live is DROPPED and counted (`FxRestoreDrops`, reported through `describeFxRestoreDrops`), never re-pointed at a slot — the slot fallback is precisely the reorder bug identity keying exists to close. Slot keying survives only for snapshots lifted from a pre-identity `view_state` and for park plans, where every live slot is the target by construction.
- `fx_offline` — the per-FX offline snapshot's KEY and its restore resolution: `FxKeying` (Identity / Slot), `FxOfflineState`, the planned `FxOfflineOp`, and `resolveFxRestore`, which matches each captured state to the FX it came from against the chain as it stands at restore time. An identity that is no longer live is DROPPED and counted (`FxRestoreDrops`, reported through `describeFxRestoreDrops`), never re-pointed at a slot — see the `FxKeying` and `resolveFxRestore` comments in `fx_offline.h` for why. Slot keying survives only for snapshots lifted from a pre-identity `view_state` and for park plans, where every live slot is the target by construction.
- `solo_cache` — the per-mode solo surface: `SoloCache` (mode id → GUID → raw `I_SOLO`), the soloed-subset filter, and `planSoloRestore`, whose two drop rules (dead GUID, not visible in the incoming mode) and their reasoning live in its header.
- `view_tree` — pure `I_FOLDERDEPTH`→FolderTree helper for the Design View shell.
- `mode_switch` — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch.
+32 -9
View File
@@ -1,5 +1,6 @@
#include "core/view/fx_offline.h"
#include <cstddef>
#include <map>
namespace reasampler {
@@ -28,10 +29,16 @@ FxRestoreResolution resolveFxRestore(const std::vector<FxOfflineOp>& planned,
for (const FxOfflineOp& op : planned) {
if (op.keying == FxKeying::Identity) {
// An empty identity (REAPER reported none at capture) is unresolvable
// like any other miss — it does not get to use its slot instead.
if (op.fxGuid.empty()) {
// REAPER reported no GUID at capture time — distinct from a real
// captured identity going missing: the FX may still be live, we
// just never had a name for it. Counted separately so the report
// never claims it was deleted (see describeFxRestoreDrops).
++res.drops.unidentified;
continue;
}
auto it = byGuid.find(op.fxGuid);
if (op.fxGuid.empty() || it == byGuid.end()) {
if (it == byGuid.end()) {
++res.drops.missingIdentity;
continue;
}
@@ -53,16 +60,32 @@ std::string describeFxRestoreDrops(const FxRestoreDrops& drops, int trackCount)
std::string msg = "ReaSampler: Design View restore dropped " +
std::to_string(drops.total()) + " captured FX offline state(s) on " +
std::to_string(trackCount) + " track(s) -- ";
std::vector<std::string> clauses;
if (drops.missingIdentity > 0) {
msg += std::to_string(drops.missingIdentity) +
" FX no longer in the chain (deleted or replaced while parked)";
clauses.push_back(std::to_string(drops.missingIdentity) +
" FX no longer in the chain (deleted or replaced while parked)");
}
if (drops.unidentified > 0) {
clauses.push_back(std::to_string(drops.unidentified) +
" FX REAPER could not identify at capture time (no GUID reported), "
"so it could not be matched now");
}
if (drops.slotOutOfRange > 0) {
if (drops.missingIdentity > 0) msg += ", ";
msg += std::to_string(drops.slotOutOfRange) +
" from a project saved before FX identity was recorded, whose slot no longer exists";
clauses.push_back(std::to_string(drops.slotOutOfRange) +
" from a project saved before FX identity was recorded, whose slot no longer exists");
}
msg += ". Every other FX was left as it was.\n";
for (std::size_t i = 0; i < clauses.size(); ++i) {
if (i) msg += ", ";
msg += clauses[i];
}
// Every dropped entry is still sitting exactly where park left it — offline
// — because the restore that would have flipped it back never ran. Say
// that, not the reassuring-but-wrong "left as it was" (park itself was the
// change; restore is what didn't happen for these).
msg += ". Each was left offline, as park left it, with no snapshot left to "
"restore it -- switch it back on by hand.\n";
return msg;
}
+10 -7
View File
@@ -27,9 +27,9 @@ struct FxOfflineState {
}
};
// One per-FX offline write as PLANNED. The keying travels with the op so an
// entry whose identity is missing can never silently degrade into a slot write
// — that fallback is the reorder bug this keying exists to close.
// One per-FX offline write as PLANNED. The keying travels with the op; see
// FxKeying above and resolveFxRestore below for why a missing identity is
// never re-pointed at a slot.
struct FxOfflineOp {
std::string guid; // track GUID
FxKeying keying = FxKeying::Identity;
@@ -53,21 +53,24 @@ struct FxOfflineWrite {
}
};
// Captured state a restore could not apply. Both counts mean the same act: the
// entry was dropped and no FX was written in its place.
// Captured state a restore could not apply. All three counts mean the same
// act: the entry was dropped and no FX was written in its place.
struct FxRestoreDrops {
int missingIdentity = 0; // identity-keyed entry with no live FX carrying that GUID
int unidentified = 0; // identity-keyed entry whose captured fxGuid was itself empty
int slotOutOfRange = 0; // slot-keyed entry whose capture-time slot no longer exists
int total() const { return missingIdentity + slotOutOfRange; }
int total() const { return missingIdentity + unidentified + slotOutOfRange; }
void add(const FxRestoreDrops& o) {
missingIdentity += o.missingIdentity;
unidentified += o.unidentified;
slotOutOfRange += o.slotOutOfRange;
}
bool operator==(const FxRestoreDrops& o) const {
return missingIdentity == o.missingIdentity && slotOutOfRange == o.slotOutOfRange;
return missingIdentity == o.missingIdentity && unidentified == o.unidentified &&
slotOutOfRange == o.slotOutOfRange;
}
};
+13 -2
View File
@@ -197,7 +197,10 @@ TrackPlan makeParkPlan(const std::string& guid, int fxCount) {
{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.
// 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;
@@ -619,6 +622,7 @@ bool parseSnapshots(json::Reader& r, std::map<std::string, TrackSnapshot>& snaps
TrackSnapshot snap;
bool haveGuid = false;
std::vector<int> bySlot;
bool haveSlot = false;
std::vector<FxOfflineState> byIdentity;
bool haveIdentity = false;
do {
@@ -629,7 +633,7 @@ bool parseSnapshots(json::Reader& r, std::map<std::string, TrackSnapshot>& snaps
else if (k == "showInMixer") { if (!r.parseInt(snap.showInMixer)) 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 == "fxOffline") { if (!r.parseIntArray(bySlot)) return false; }
else if (k == "fxOffline") { if (!r.parseIntArray(bySlot)) return false; haveSlot = true; }
else if (k == "fx") {
if (!parseFxStates(r, byIdentity)) return false;
haveIdentity = true;
@@ -639,6 +643,13 @@ bool parseSnapshots(json::Reader& r, std::map<std::string, TrackSnapshot>& snaps
if (!r.consume('}')) return false;
if (!haveGuid || guid.empty()) return false;
// A blob carrying both arrays at different lengths is not something this
// writer (or any prior version) produces — reject rather than silently
// trusting "fx" over a slot array that disagrees with it; an unreadable
// view_state falls back to a default model per the version-ladder note
// above, which is the same leniency-direction call already made there.
if (haveIdentity && haveSlot && byIdentity.size() != bySlot.size()) return false;
// "fx" wins outright — v2 writes the slot array beside it purely so an
// older build can still read something (see the version ladder above).
if (haveIdentity) {