Key Design View's parked FX-offline state to the FX's own GUID, not its slot

view_state v2 writes identities beside the v1 slot array, so a downgrade keeps
what it had. An FX gone at restore time is dropped and reported, never restored
onto whatever took its place.
This commit is contained in:
2026-08-02 18:31:08 -04:00
parent 4231b2321c
commit 5f6efb7cc3
10 changed files with 780 additions and 58 deletions
+53 -20
View File
@@ -14,6 +14,7 @@
#include <vector>
#include "shell/capture/item_read.h"
#include "core/view/fx_offline.h"
#include "core/view/lane_keys.h"
#include "core/view/solo_cache.h"
#include "shell/capture/track_guid.h"
@@ -27,9 +28,12 @@
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
#define REAPERAPI_WANT_GetSetMediaTrackInfo_String
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_TrackFX_GetCount
#define REAPERAPI_WANT_TrackFX_GetFXGUID
#define REAPERAPI_WANT_TrackFX_GetOffline
#define REAPERAPI_WANT_TrackFX_SetOffline
#define REAPERAPI_WANT_guidToString
#define REAPERAPI_WANT_Undo_BeginBlock2
#define REAPERAPI_WANT_Undo_EndBlock2
#define REAPERAPI_WANT_TrackList_AdjustWindows
@@ -123,6 +127,29 @@ MediaTrack* resolve(const std::vector<std::pair<std::string, MediaTrack*>>& hand
return nullptr; // stale/deleted GUID — pruned by being skipped
}
// The FX's own durable identity, braced exactly like the track GUID keys. Empty
// when REAPER reports none — an FX we cannot name is one we cannot restore, and
// fx_offline treats it that way rather than guessing at its slot.
std::string fxGuidString(MediaTrack* tr, int fx) {
GUID* g = TrackFX_GetFXGUID(tr, fx);
if (!g) return {};
char buf[64] = {0}; // guidToString needs a >=64-char destination (SDK contract)
guidToString(g, buf);
return std::string(buf);
}
// The chain as it stands now: identity by current slot. Snapshot, park and
// restore all address FX through this one plain 0..TrackFX_GetCount-1
// enumeration — never the 0x1000000/0x2000000 input-FX or container forms — so
// whatever it covers, all three cover identically.
std::vector<std::string> liveFxGuids(MediaTrack* tr) {
const int fxCount = TrackFX_GetCount(tr);
std::vector<std::string> guids;
guids.reserve(static_cast<std::size_t>(fxCount));
for (int fx = 0; fx < fxCount; ++fx) guids.push_back(fxGuidString(tr, fx));
return guids;
}
// Captures prior driven-flag state before parking. Never reads B_MUTE/I_SOLO;
// ints preserve whatever REAPER reported (TrackSnapshot's defensive contract).
TrackSnapshot snapshotTrack(MediaTrack* tr) {
@@ -132,12 +159,13 @@ TrackSnapshot snapshotTrack(MediaTrack* tr) {
snap.mainSend = static_cast<int>(GetMediaTrackInfo_Value(tr, "B_MAINSEND"));
snap.fxEnable = static_cast<int>(GetMediaTrackInfo_Value(tr, "I_FXEN"));
int fxCount = TrackFX_GetCount(tr);
snap.fxOffline.reserve(static_cast<std::size_t>(fxCount));
for (int fx = 0; fx < fxCount; ++fx) {
snap.fxOffline.push_back(TrackFX_GetOffline(tr, fx) ? 1 : 0);
const std::vector<std::string> guids = liveFxGuids(tr);
snap.fxOffline.reserve(guids.size());
for (std::size_t fx = 0; fx < guids.size(); ++fx) {
snap.fxOffline.push_back(
FxOfflineState{guids[fx], TrackFX_GetOffline(tr, static_cast<int>(fx)) ? 1 : 0});
}
return snap;
return snap; // fxKeying stays Identity — a live capture always knows the chain
}
void applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags) {
@@ -155,20 +183,13 @@ void parkFxOffline(MediaTrack* tr) {
}
}
// Restores per-FX offline from the snapshot verbatim — each slot back to its
// captured value, never a blanket "online" — bounds-checked against the live
// FX count (prune-safe if the chain changed while parked).
//
// HAZARD (open, tracked in docs/TODO.md): this remaps by slot INDEX, not
// plugin identity. If the FX chain reshuffled while parked, snapshot slot k
// restores onto whatever plugin now occupies slot k. Accepted for now;
// identity-based reconciliation is future hardening.
void restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline) {
int fxCount = TrackFX_GetCount(tr);
for (const FxOfflineOp& op : fxOffline) {
if (op.fxIndex < 0 || op.fxIndex >= fxCount) continue;
TrackFX_SetOffline(tr, op.fxIndex, op.offline);
}
// Restores per-FX offline from the snapshot verbatim — never a blanket "online".
// Which live FX each captured state belongs to is resolveFxRestore's call, and
// what it could not place comes back for the caller to report.
FxRestoreDrops restoreFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& fxOffline) {
const FxRestoreResolution res = resolveFxRestore(fxOffline, liveFxGuids(tr));
for (const FxOfflineWrite& w : res.writes) TrackFX_SetOffline(tr, w.fxIndex, w.offline);
return res.drops;
}
// Managed-lane application: the pure planner keys LanePlayOps by the lane's
@@ -451,6 +472,8 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
}
// RESTORE: apply verbatim, then drop the consumed snapshot.
FxRestoreDrops fxDrops;
int fxDropTracks = 0;
for (const TrackPlan& tp : plan.restore) {
if (tp.flags.empty()) continue;
const std::string& guid = tp.flags.front().guid;
@@ -458,10 +481,20 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
if (!tr) continue; // stale GUID — prune
applyFlags(tr, tp.flags);
restoreFxOffline(tr, tp.fxOffline);
const FxRestoreDrops drops = restoreFxOffline(tr, tp.fxOffline);
if (drops.total() > 0) {
fxDrops.add(drops);
++fxDropTracks;
}
model.clearSnapshot(guid);
}
// Captured FX state that could not be applied is REPORTED. Silence here would
// read to the user as "restore worked" while an FX sat at whatever state the
// park left it in.
const std::string fxDropMsg = describeFxRestoreDrops(fxDrops, fxDropTracks);
if (!fxDropMsg.empty()) ShowConsoleMsg(fxDropMsg.c_str());
// MANAGED LANES: drive C_LANEPLAYS so the active mode's lane plays+shows
// and every other managed lane is silenced+hidden. Empty for a D1-only
// project, leaving that behavior byte-identical.