Collapse the FX-park drain's writes into one undo point instead of one per TrackFX_SetOffline
This commit is contained in:
@@ -23,10 +23,15 @@
|
||||
#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 {
|
||||
@@ -58,30 +63,70 @@ void adoptOwner(ReaProject* proj) {
|
||||
g_owner = p;
|
||||
}
|
||||
|
||||
// Returns whether it actually wrote — the drain's undo point is discarded unless
|
||||
// something did (fxParkUndoClose).
|
||||
//
|
||||
// [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;
|
||||
bool setOfflineIfChanged(MediaTrack* tr, int fx, bool offline) {
|
||||
if (TrackFX_GetOffline(tr, fx) == offline) return false;
|
||||
TrackFX_SetOffline(tr, fx, offline);
|
||||
return true;
|
||||
}
|
||||
|
||||
void applyPark(MediaTrack* tr) {
|
||||
bool applyPark(MediaTrack* tr) {
|
||||
bool wrote = false;
|
||||
const int fxCount = TrackFX_GetCount(tr);
|
||||
for (int fx = 0; fx < fxCount; ++fx) setOfflineIfChanged(tr, fx, true);
|
||||
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.
|
||||
FxRestoreDrops applyRestore(MediaTrack* tr, const std::vector<FxOfflineOp>& ops) {
|
||||
RestoreOutcome 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;
|
||||
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: an unbalanced
|
||||
// block would swallow every later action into one point, which is far worse than
|
||||
// [verify — DAW] whatever Undo_EndBlock2 does with a ReaProject* that has closed
|
||||
// since Undo_BeginBlock2 took it.
|
||||
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
|
||||
@@ -221,27 +266,34 @@ void drainDeferredFxParks() {
|
||||
|
||||
FxRestoreDrops drops;
|
||||
int dropTracks = 0;
|
||||
for (const FxParkIntent& intent : draining) {
|
||||
auto it = byGuid.find(intent.guid);
|
||||
if (it == byGuid.end()) continue; // track deleted since the switch — prune
|
||||
{
|
||||
// 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;
|
||||
// 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) {
|
||||
applyPark(it->second);
|
||||
continue;
|
||||
}
|
||||
const FxRestoreDrops d = applyRestore(it->second, intent.restoreOps);
|
||||
if (d.total() > 0) {
|
||||
drops.add(d);
|
||||
++dropTracks;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user