Collapse a Design View mode switch to ONE undo point: FX writes run inline in applyMode's block, deferred park queue deleted
This commit is contained in:
+108
-197
@@ -1,70 +1,40 @@
|
||||
// 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.
|
||||
// The decisions are pure (header, test_view_fx_park.cpp); this file owns the
|
||||
// REAPER reads/writes and their ordering against the live enumeration.
|
||||
|
||||
#include "shell/view/view_fx_park.h"
|
||||
|
||||
#include <cstring> // strnlen — bounded read of GetTrackName's buffer
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "shell/capture/track_guid.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_CountTracks
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_GetMediaTrackInfo_Value
|
||||
#define REAPERAPI_WANT_GetTrack
|
||||
#define REAPERAPI_WANT_GetTrackName
|
||||
#define REAPERAPI_WANT_SetMediaTrackInfo_Value
|
||||
#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_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#define REAPERAPI_WANT_ValidatePtr2
|
||||
#define REAPERAPI_WANT_guidToString
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
static_assert(reasampler::kFxParkUndoMask == UNDO_STATE_FX,
|
||||
"the SDK-free copy of the drain's undo mask has drifted from UNDO_STATE_FX");
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
FxParkQueue g_queue;
|
||||
ReaProject* g_owner = nullptr; // the project the pending intents were enqueued against
|
||||
bool g_draining = false;
|
||||
|
||||
// The last refusal report, so an unchanged one is not reprinted on every reapply.
|
||||
ReaProject* g_reportedOwner = nullptr;
|
||||
std::vector<std::string> g_reportedRefusals;
|
||||
|
||||
// Makes "one drain at a time" explicit rather than implied by the call sites.
|
||||
// RAII because an apply can throw and a stuck flag would silence the queue for
|
||||
// the rest of the session.
|
||||
struct DrainScope {
|
||||
DrainScope() { g_draining = true; }
|
||||
~DrainScope() { g_draining = false; }
|
||||
DrainScope(const DrainScope&) = delete;
|
||||
DrainScope& operator=(const DrainScope&) = delete;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Returns whether it actually wrote — the drain's undo point is discarded unless
|
||||
// something did (fxParkUndoClose).
|
||||
// Returns whether it actually wrote — the apply's undo point and its
|
||||
// UNDO_STATE_FX bit both hang off that verdict.
|
||||
//
|
||||
// [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
|
||||
@@ -77,62 +47,6 @@ bool setOfflineIfChanged(MediaTrack* tr, int fx, bool offline) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool applyPark(MediaTrack* tr) {
|
||||
bool wrote = false;
|
||||
const int fxCount = TrackFX_GetCount(tr);
|
||||
for (int fx = 0; fx < fxCount; ++fx)
|
||||
if (setOfflineIfChanged(tr, fx, true)) wrote = true;
|
||||
return wrote;
|
||||
}
|
||||
|
||||
struct RestoreOutcome {
|
||||
FxRestoreDrops drops;
|
||||
bool wrote = false;
|
||||
};
|
||||
|
||||
// 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.
|
||||
RestoreOutcome applyRestore(MediaTrack* tr, const std::vector<FxOfflineOp>& ops) {
|
||||
const FxRestoreResolution res = resolveFxRestore(ops, liveFxGuids(tr));
|
||||
RestoreOutcome out;
|
||||
for (const FxOfflineWrite& w : res.writes)
|
||||
if (setOfflineIfChanged(tr, w.fxIndex, w.offline)) out.wrote = true;
|
||||
out.drops = res.drops;
|
||||
return out;
|
||||
}
|
||||
|
||||
// ONE drain is ONE undo point (see src/shell/view/CLAUDE.md for the measured
|
||||
// per-FX fragmentation this closes). The block is deliberately the drain's OWN
|
||||
// and never the switch's — the FX point has to stay ABOVE the switch's.
|
||||
//
|
||||
// Closing is unconditional on every exit, the abort path included. Which of two
|
||||
// undocumented models REAPER uses decides whether that is the safe call:
|
||||
// under a GLOBAL block-nesting counter, leaving this one unbalanced would
|
||||
// swallow every later action — in ANY project — into one point, far worse than
|
||||
// calling Undo_EndBlock2 on a ReaProject* that closed since Undo_BeginBlock2
|
||||
// took it. Under a PER-PROJECT counter, an unbalanced block on a now-dead
|
||||
// project costs nothing, and the closing call against a stale pointer is the
|
||||
// riskier of the two. Closing unconditionally is the safer bet under the
|
||||
// global model and merely unnecessary under the per-project one, hence the
|
||||
// choice. [verify — DAW] which model REAPER actually implements.
|
||||
class FxParkUndoBlock {
|
||||
public:
|
||||
explicit FxParkUndoBlock(ReaProject* proj) : proj_(proj) { Undo_BeginBlock2(proj_); }
|
||||
~FxParkUndoBlock() {
|
||||
const FxParkUndoClose close = fxParkUndoClose(wrote_);
|
||||
Undo_EndBlock2(proj_, close.label, close.mask);
|
||||
}
|
||||
FxParkUndoBlock(const FxParkUndoBlock&) = delete;
|
||||
FxParkUndoBlock& operator=(const FxParkUndoBlock&) = delete;
|
||||
|
||||
void noteWrite() { wrote_ = true; }
|
||||
|
||||
private:
|
||||
ReaProject* proj_;
|
||||
bool wrote_ = false;
|
||||
};
|
||||
|
||||
// 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
|
||||
@@ -155,6 +69,18 @@ std::string fxGuidString(MediaTrack* tr, int fx) {
|
||||
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;
|
||||
}
|
||||
|
||||
// ANY offline FX, not every — see the snapshot-source invariant (view_fx_park.h,
|
||||
// src/shell/view/CLAUDE.md) for why.
|
||||
bool anyFxOffline(MediaTrack* tr) {
|
||||
@@ -164,6 +90,41 @@ bool anyFxOffline(MediaTrack* tr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Captures prior driven-flag state before parking. Never reads B_MUTE/I_SOLO;
|
||||
// ints preserve whatever REAPER reported (TrackSnapshot's defensive contract).
|
||||
// Must run before EITHER half of the park writes — both halves are read here.
|
||||
// `fxKeying` stays Identity: a live read always knows the chain.
|
||||
TrackSnapshot snapshotTrack(MediaTrack* tr) {
|
||||
TrackSnapshot snap;
|
||||
auto read = [tr](Flag f) {
|
||||
return static_cast<int>(GetMediaTrackInfo_Value(tr, trackFlagParm(f)));
|
||||
};
|
||||
snap.showInTcp = read(Flag::ShowInTcp);
|
||||
snap.showInMixer = read(Flag::ShowInMixer);
|
||||
snap.mainSend = read(Flag::MainSend);
|
||||
snap.fxEnable = read(Flag::FxEnable);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Returns whether it wrote; raises `fxMoved` when a flag in the FX domain moved.
|
||||
bool applyFlags(MediaTrack* tr, const std::vector<TrackFlagOp>& flags, bool& fxMoved) {
|
||||
bool wrote = false;
|
||||
for (const TrackFlagOp& op : flags) {
|
||||
if (!writeIfChanged(tr, trackFlagParm(op.flag), static_cast<double>(op.value))) continue;
|
||||
wrote = true;
|
||||
// I_FXEN is the only FX-domain flag driven here, so it is the only one
|
||||
// whose undo record has to carry UNDO_STATE_FX.
|
||||
if (op.flag == Flag::FxEnable) fxMoved = true;
|
||||
}
|
||||
return wrote;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// GetTrackName, not P_NAME, matching capture::trackName's reason for the same
|
||||
@@ -195,121 +156,71 @@ void reportRefusedParks(ReaProject* proj, const std::vector<std::string>& refuse
|
||||
|
||||
const std::string msg = describeRefusedParks(refusedNames);
|
||||
// "!SHOW:" so an unattended project-load reapply cannot force the console open
|
||||
// (reaper_plugin_functions.h:6536), same as the drain's drop report.
|
||||
// (reaper_plugin_functions.h:6536).
|
||||
if (!msg.empty()) ShowConsoleMsg(("!SHOW:" + msg).c_str());
|
||||
}
|
||||
|
||||
std::vector<std::string> liveFxGuids(MediaTrack* tr) {
|
||||
bool writeIfChanged(MediaTrack* tr, const char* parm, double value) {
|
||||
if (GetMediaTrackInfo_Value(tr, parm) == value) return false;
|
||||
SetMediaTrackInfo_Value(tr, parm, value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool trackStillLive(ReaProject* proj, MediaTrack* tr) {
|
||||
return ValidatePtr2(proj ? proj : currentProject(), tr, "MediaTrack*");
|
||||
}
|
||||
|
||||
TrackApplyResult parkTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<TrackFlagOp>& flags) {
|
||||
TrackApplyResult out;
|
||||
|
||||
const ParkAction action = decideParkForTrack(tr, model.snapshot(guid) != nullptr, flags);
|
||||
if (action == ParkAction::Refuse) {
|
||||
out.refused = true;
|
||||
return out;
|
||||
}
|
||||
if (action == ParkAction::SnapshotThenPark) {
|
||||
model.storeSnapshot(guid, snapshotTrack(tr));
|
||||
out.wrote = true;
|
||||
}
|
||||
|
||||
if (applyFlags(tr, flags, out.fxMoved)) out.wrote = true;
|
||||
|
||||
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;
|
||||
for (int fx = 0; fx < fxCount; ++fx)
|
||||
if (setOfflineIfChanged(tr, fx, true)) out.fxMoved = out.wrote = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
PreParkFx snapshotFxOffline(MediaTrack* tr, const std::vector<FxOfflineOp>& cancelled) {
|
||||
PreParkFx carried = preParkFxFromCancelledRestore(cancelled);
|
||||
if (!carried.states.empty()) return carried;
|
||||
TrackApplyResult restoreTrack(ViewModeModel& model, const std::string& guid, MediaTrack* tr,
|
||||
const std::vector<FxOfflineOp>& fxOps,
|
||||
const std::vector<TrackFlagOp>& flags) {
|
||||
TrackApplyResult out;
|
||||
|
||||
// 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;
|
||||
// Which live FX each captured state belongs to is resolveFxRestore's call —
|
||||
// the plan is written VERBATIM, never a blanket "online".
|
||||
const FxRestoreResolution res = resolveFxRestore(fxOps, liveFxGuids(tr));
|
||||
for (const FxOfflineWrite& w : res.writes)
|
||||
if (setOfflineIfChanged(tr, w.fxIndex, w.offline)) out.fxMoved = out.wrote = true;
|
||||
out.drops = res.drops;
|
||||
|
||||
if (applyFlags(tr, flags, out.fxMoved)) out.wrote = true;
|
||||
|
||||
// Unconditional, even when every write above was elided: the snapshot is
|
||||
// gone, so this apply is not repeatable and must not close on the discard
|
||||
// form. Model state, not project state — whether an undo actually puts the
|
||||
// snapshot back is the ext-state question in src/shell/view/CLAUDE.md.
|
||||
model.clearSnapshot(guid);
|
||||
out.wrote = true;
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<FxOfflineOp> deferFxPark(ReaProject* proj, const std::string& guid) {
|
||||
adoptOwner(proj);
|
||||
return 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 discardDeferredFxParks() {
|
||||
g_queue.clear();
|
||||
g_owner = nullptr;
|
||||
}
|
||||
|
||||
void drainDeferredFxParks() {
|
||||
if (g_queue.empty()) return;
|
||||
if (g_draining) return; // re-entered mid-apply: those intents are the outer drain's next pass
|
||||
|
||||
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. 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;
|
||||
const DrainScope scope;
|
||||
|
||||
// Detached before the first write: [verify — DAW] applying is ASSUMED to pump
|
||||
// the message loop (plugins load and unload), so a re-entrant switch can
|
||||
// enqueue while this runs. Everything defensive below rests on that one
|
||||
// assumption; each piece is correct regardless of whether it holds.
|
||||
const std::vector<FxParkIntent> draining = g_queue.take();
|
||||
|
||||
std::unordered_map<std::string, MediaTrack*> byGuid;
|
||||
const int count = CountTracks(proj);
|
||||
byGuid.reserve(static_cast<std::size_t>(count));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
MediaTrack* tr = GetTrack(proj, i);
|
||||
if (!tr) continue;
|
||||
std::string guid = guidString(tr);
|
||||
if (!guid.empty()) byGuid.emplace(std::move(guid), tr);
|
||||
}
|
||||
|
||||
FxRestoreDrops drops;
|
||||
int dropTracks = 0;
|
||||
{
|
||||
// Scoped so the block closes before the console report below: a report is
|
||||
// not a project write and must not join what a Ctrl-Z rolls back, the same
|
||||
// ordering applyMode gives reportRefusedParks.
|
||||
FxParkUndoBlock undo(proj);
|
||||
for (const FxParkIntent& intent : draining) {
|
||||
auto it = byGuid.find(intent.guid);
|
||||
if (it == byGuid.end()) continue; // track deleted since the switch — prune
|
||||
|
||||
// Re-validated PER INTENT, not once above: under the same pumping
|
||||
// assumption, a project closed or a track deleted between two applies
|
||||
// leaves the handle resolved above dangling — a use-after-free, not a
|
||||
// pruned intent. Same gate capture_realtime_shell's teardown uses; a null
|
||||
// first argument validates the ReaProject* itself (SDK: proj is ignored
|
||||
// when the pointer is a project).
|
||||
if (!ValidatePtr2(nullptr, proj, "ReaProject*")) return;
|
||||
if (!ValidatePtr2(proj, it->second, "MediaTrack*")) continue;
|
||||
|
||||
if (intent.park) {
|
||||
if (applyPark(it->second)) undo.noteWrite();
|
||||
continue;
|
||||
}
|
||||
const RestoreOutcome r = applyRestore(it->second, intent.restoreOps);
|
||||
if (r.wrote) undo.noteWrite();
|
||||
if (r.drops.total() > 0) {
|
||||
drops.add(r.drops);
|
||||
++dropTracks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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());
|
||||
void reportFxRestoreDrops(const FxRestoreDrops& drops, int trackCount) {
|
||||
const std::string msg = describeFxRestoreDrops(drops, trackCount);
|
||||
// "!SHOW:" (reaper_plugin_functions.h:6536) keeps it from force-opening the
|
||||
// console: this also runs behind an unattended project-load reapply, and
|
||||
// cannot tell that case from an interactive toggle.
|
||||
if (!msg.empty()) ShowConsoleMsg(("!SHOW:" + msg).c_str());
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
|
||||
Reference in New Issue
Block a user