Clear the park snapshot where the restore is planned, not where it drains; make the drain re-entrant and reload-aware

This commit is contained in:
2026-08-03 13:35:30 -04:00
parent a4a1c3860f
commit 7169d7f22b
7 changed files with 298 additions and 91 deletions
+41 -16
View File
@@ -9,7 +9,6 @@
#include <unordered_map>
#include <vector>
#include "core/view/view_mode_model.h"
#include "shell/capture/track_guid.h"
#define REAPERAPI_MINIMAL
@@ -40,9 +39,11 @@ void adoptOwner(ReaProject* proj) {
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.
// [verify — DAW] TrackFX_SetOffline unloads and re-instantiates the plugin, so
// writing a state that already holds is assumed to cost the same unload/reload
// as a real change — the header documents no internal short-circuit either way.
// The compare is cheap and correct regardless; only the size of what it saves is
// unconfirmed.
void setOfflineIfChanged(MediaTrack* tr, int fx, bool offline) {
if (TrackFX_GetOffline(tr, fx) == offline) return;
TrackFX_SetOffline(tr, fx, offline);
@@ -94,9 +95,24 @@ std::vector<std::string> liveFxGuids(MediaTrack* tr) {
return guids;
}
void deferFxPark(ReaProject* proj, const std::string& guid) {
PreParkFx snapshotFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& cancelled) {
PreParkFx carried = preParkFxFromCancelledRestore(cancelled);
if (!carried.states.empty()) return carried;
// Nothing was cancelled, so the chain reads true. fxKeying stays Identity —
// a live capture always knows the chain.
PreParkFx live;
const std::vector<std::string> guids = liveFxGuids(tr);
live.states.reserve(guids.size());
for (std::size_t fx = 0; fx < guids.size(); ++fx)
live.states.push_back(
FxOfflineState{guids[fx], TrackFX_GetOffline(tr, static_cast<int>(fx)) ? 1 : 0});
return live;
}
std::vector<FxOfflineOp> deferFxPark(ReaProject* proj, const std::string& guid) {
adoptOwner(proj);
g_queue.park(guid);
return g_queue.park(guid);
}
void deferFxRestore(ReaProject* proj, const std::string& guid, std::vector<FxOfflineOp> ops) {
@@ -104,23 +120,35 @@ void deferFxRestore(ReaProject* proj, const std::string& guid, std::vector<FxOff
g_queue.restore(guid, std::move(ops));
}
void drainDeferredFxParks(ViewModeModel& model) {
void discardDeferredFxParks() {
g_queue.clear();
g_owner = nullptr;
}
void drainDeferredFxParks() {
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;
// gone. Discard rather than apply. This catches only a DIFFERENT live
// pointer; a REAPER-recycled address is caught upstream, by the caller
// discarding on the session's own load transition.
discardDeferredFxParks();
return;
}
ReaProject* const proj = g_owner;
// Detached before the first write: applying pumps the message loop (plugins
// load and unload), so a re-entrant switch can enqueue while this runs.
const std::vector<FxParkIntent> draining = g_queue.take();
std::unordered_map<std::string, MediaTrack*> byGuid;
const int count = CountTracks(g_owner);
const int count = CountTracks(proj);
byGuid.reserve(static_cast<std::size_t>(count));
for (int i = 0; i < count; ++i) {
MediaTrack* tr = GetTrack(g_owner, i);
MediaTrack* tr = GetTrack(proj, i);
if (!tr) continue;
std::string guid = guidString(tr);
if (!guid.empty()) byGuid.emplace(std::move(guid), tr);
@@ -128,7 +156,7 @@ void drainDeferredFxParks(ViewModeModel& model) {
FxRestoreDrops drops;
int dropTracks = 0;
for (const FxParkIntent& intent : g_queue.pending()) {
for (const FxParkIntent& intent : draining) {
auto it = byGuid.find(intent.guid);
if (it == byGuid.end()) continue; // track deleted since the switch — prune
if (intent.park) {
@@ -140,11 +168,8 @@ void drainDeferredFxParks(ViewModeModel& model) {
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