Defer Design View's per-FX park to an idle tick; honest undo mask, compare-before-write, PreventUIRefresh bracket, O(1) handle resolve
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
// See view_fx_park.h. Compiled into the reaper_reasampler module; includes
|
||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||
// The queue's rule is pure (header, test_view_fx_park.cpp); this file owns the
|
||||
// REAPER reads/writes and the ordering against the live enumeration.
|
||||
|
||||
#include "shell/view/view_fx_park.h"
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "core/view/view_mode_model.h"
|
||||
#include "shell/capture/track_guid.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#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
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
FxParkQueue g_queue;
|
||||
ReaProject* g_owner = nullptr; // the project the pending intents were enqueued against
|
||||
|
||||
ReaProject* currentProject() { return EnumProjects(-1, nullptr, 0); }
|
||||
|
||||
void adoptOwner(ReaProject* proj) {
|
||||
ReaProject* p = proj ? proj : currentProject();
|
||||
if (p == g_owner) return;
|
||||
g_queue.clear(); // intents planned against another project are never replayed here
|
||||
g_owner = p;
|
||||
}
|
||||
|
||||
// TrackFX_SetOffline unloads and re-instantiates the plugin, so writing a state
|
||||
// that already holds is not a no-op — it is the expensive half of a mode switch
|
||||
// spent on nothing.
|
||||
void setOfflineIfChanged(MediaTrack* tr, int fx, bool offline) {
|
||||
if (TrackFX_GetOffline(tr, fx) == offline) return;
|
||||
TrackFX_SetOffline(tr, fx, offline);
|
||||
}
|
||||
|
||||
void applyPark(MediaTrack* tr) {
|
||||
const int fxCount = TrackFX_GetCount(tr);
|
||||
for (int fx = 0; fx < fxCount; ++fx) setOfflineIfChanged(tr, fx, true);
|
||||
}
|
||||
|
||||
// Restores per-FX offline from the plan 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 applyRestore(MediaTrack* tr, const std::vector<FxOfflineOp>& ops) {
|
||||
const FxRestoreResolution res = resolveFxRestore(ops, liveFxGuids(tr));
|
||||
for (const FxOfflineWrite& w : res.writes) setOfflineIfChanged(tr, w.fxIndex, w.offline);
|
||||
return res.drops;
|
||||
}
|
||||
|
||||
// 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. Lifetime is
|
||||
// settled: the string copy is taken immediately and the GUID* is never held
|
||||
// past this call (reaper_plugin_functions.h:7348 documents no null contract for
|
||||
// TrackFX_GetFXGUID; treating null as "no identity" is the safe read).
|
||||
//
|
||||
// [verify — DAW] STABILITY across a chain mutation is not settled the same way:
|
||||
// confirm the GUID for one FX instance survives a native drag-reorder, an SWS
|
||||
// move (SNM_MoveOrRemoveTrackFX — SWS issue #802 reports the FXID lines do not
|
||||
// follow the plugin after that call, i.e. wrong-plugin restores or mass drops
|
||||
// through fx_offline on that path specifically), a save/reload round trip, and
|
||||
// two live instances of one plugin type staying distinguishable. See
|
||||
// src/shell/view/CLAUDE.md's Gotchas for the SWS-path risk this leaves open.
|
||||
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);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void deferFxPark(ReaProject* proj, const std::string& guid) {
|
||||
adoptOwner(proj);
|
||||
g_queue.park(guid);
|
||||
}
|
||||
|
||||
void deferFxRestore(ReaProject* proj, const std::string& guid, std::vector<FxOfflineOp> ops) {
|
||||
adoptOwner(proj);
|
||||
g_queue.restore(guid, std::move(ops));
|
||||
}
|
||||
|
||||
void drainDeferredFxParks(ViewModeModel& model) {
|
||||
if (g_queue.empty()) return;
|
||||
|
||||
if (g_owner != currentProject()) {
|
||||
// The project the intents were planned against was closed or switched
|
||||
// away from: its tracks are not ours to write and its handles may be
|
||||
// gone. Discard rather than apply.
|
||||
g_queue.clear();
|
||||
g_owner = nullptr;
|
||||
return;
|
||||
}
|
||||
|
||||
std::unordered_map<std::string, MediaTrack*> byGuid;
|
||||
const int count = CountTracks(g_owner);
|
||||
byGuid.reserve(static_cast<std::size_t>(count));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
MediaTrack* tr = GetTrack(g_owner, i);
|
||||
if (!tr) continue;
|
||||
std::string guid = guidString(tr);
|
||||
if (!guid.empty()) byGuid.emplace(std::move(guid), tr);
|
||||
}
|
||||
|
||||
FxRestoreDrops drops;
|
||||
int dropTracks = 0;
|
||||
for (const FxParkIntent& intent : g_queue.pending()) {
|
||||
auto it = byGuid.find(intent.guid);
|
||||
if (it == byGuid.end()) continue; // track deleted since the switch — prune
|
||||
if (intent.park) {
|
||||
applyPark(it->second);
|
||||
continue;
|
||||
}
|
||||
const FxRestoreDrops d = applyRestore(it->second, intent.restoreOps);
|
||||
if (d.total() > 0) {
|
||||
drops.add(d);
|
||||
++dropTracks;
|
||||
}
|
||||
model.clearSnapshot(intent.guid);
|
||||
}
|
||||
|
||||
g_queue.clear();
|
||||
|
||||
// Captured FX state that could not be applied is REPORTED — silence would read
|
||||
// to the user as "restore worked" while an FX sat at whatever state the park
|
||||
// left it in. The "!SHOW:" prefix (reaper_plugin_functions.h:6536) keeps it from
|
||||
// force-opening the console: this drain also runs behind an unattended
|
||||
// project-load reapply, and cannot tell that case from an interactive toggle.
|
||||
const std::string fxDropMsg = describeFxRestoreDrops(drops, dropTracks);
|
||||
if (!fxDropMsg.empty()) ShowConsoleMsg(("!SHOW:" + fxDropMsg).c_str());
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user