Add the FX reclaimer: a debounced, time-boxed convergence loop that offlines parked chains and restores returning ones off the switch path
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
// See fx_reclaim.h. Compiled into the reaper_reasampler module; includes
|
||||
// reaper_plugin_functions.h without REAPERAPI_IMPLEMENT (main.cpp owns that).
|
||||
// The drain's decisions are pure (header, test_fx_reclaim.cpp); this file owns the
|
||||
// queue's lifetime, the per-step track resolve, and the writes.
|
||||
|
||||
#include "shell/view/fx_reclaim.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "shell/view/view.h" // modeApplyInProgress
|
||||
#include "shell/view/view_park.h" // fxGuidString — the one FX-identity read
|
||||
#include "shell/view/view_tracks.h"
|
||||
|
||||
#define REAPERAPI_MINIMAL
|
||||
#define REAPERAPI_WANT_EnumProjects
|
||||
#define REAPERAPI_WANT_TrackFX_GetCount
|
||||
#define REAPERAPI_WANT_TrackFX_GetOffline
|
||||
#define REAPERAPI_WANT_TrackFX_SetOffline
|
||||
#define REAPERAPI_WANT_Undo_BeginBlock2
|
||||
#define REAPERAPI_WANT_Undo_EndBlock2
|
||||
#define REAPERAPI_WANT_ValidatePtr2
|
||||
#include "reaper_plugin_functions.h"
|
||||
|
||||
namespace reasampler {
|
||||
|
||||
namespace {
|
||||
|
||||
// The derived queue and the project it was derived against. The pointer is
|
||||
// COMPARE-ONLY and never dereferenced beyond the ValidatePtr2 the tick already makes —
|
||||
// a tab switch has to be detectable without following a pointer that may name a closed
|
||||
// project.
|
||||
std::vector<ReclaimItem> g_items;
|
||||
ReaProject* g_project = nullptr;
|
||||
ParkClock g_clock;
|
||||
|
||||
// TrackFX_SetOffline is ASSUMED to pump the message loop, so a plugin load can re-enter
|
||||
// the timer and land back here mid-drain. Fail closed: a re-entered drain would walk the
|
||||
// same items the outer one is mutating and double-write from a stale resume cursor.
|
||||
bool g_draining = false;
|
||||
struct DrainLatch {
|
||||
DrainLatch() { g_draining = true; }
|
||||
~DrainLatch() { g_draining = false; }
|
||||
DrainLatch(const DrainLatch&) = delete;
|
||||
DrainLatch& operator=(const DrainLatch&) = delete;
|
||||
};
|
||||
|
||||
// Resolved fresh from the GUID on every step rather than held across a write, for the
|
||||
// same pump premise: a plugin load can run arbitrary REAPER code, and a handle taken
|
||||
// before it is not one this code may trust after it.
|
||||
MediaTrack* resolveFresh(ReaProject* proj, const std::string& guid) {
|
||||
TrackHandles handles;
|
||||
readFolderEntries(proj, handles);
|
||||
for (const auto& kv : handles)
|
||||
if (kv.first == guid) return kv.second;
|
||||
return nullptr; // deleted, or moved to another project
|
||||
}
|
||||
|
||||
std::set<std::string> liveTrackGuids(ReaProject* proj) {
|
||||
TrackHandles handles;
|
||||
readFolderEntries(proj, handles);
|
||||
std::set<std::string> guids;
|
||||
for (const auto& kv : handles) guids.insert(kv.first);
|
||||
return guids;
|
||||
}
|
||||
|
||||
// The live half of drainTick's Host contract (see fx_reclaim.h).
|
||||
struct ReclaimHost {
|
||||
ReaProject* project = nullptr;
|
||||
ViewModeModel* model = nullptr;
|
||||
|
||||
// Monotonic and sub-millisecond: the budget below discriminates at 30ms, which
|
||||
// GetTickCount's ~16ms granularity cannot do. No wraparound to guard against, so
|
||||
// util::elapsedAtLeast (whose whole reason for existing is that wrap) does not apply.
|
||||
double nowMs() const {
|
||||
return std::chrono::duration<double, std::milli>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
}
|
||||
|
||||
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
||||
MediaTrack* tr = resolveFresh(project, guid);
|
||||
if (!tr) return false;
|
||||
const int count = TrackFX_GetCount(tr);
|
||||
out.clear();
|
||||
out.reserve(static_cast<std::size_t>(count < 0 ? 0 : count));
|
||||
// The plain 0..count-1 enumeration, matching the park snapshot's — never the
|
||||
// 0x1000000 input-FX or 0x2000000 container forms.
|
||||
for (int fx = 0; fx < count; ++fx) out.push_back(fxGuidString(tr, fx));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeOffline(const std::string& guid, int fxIndex, bool offline) {
|
||||
MediaTrack* tr = resolveFresh(project, guid);
|
||||
if (!tr) return false;
|
||||
if (fxIndex < 0 || fxIndex >= TrackFX_GetCount(tr)) return false; // chain shrank
|
||||
if (TrackFX_GetOffline(tr, fxIndex) == offline) return false; // already there
|
||||
|
||||
// DAW-VERIFIED: the same ten TrackFX_SetOffline calls mint ten undo points
|
||||
// unblocked and ZERO inside this pair. One block per write, not per tick: the
|
||||
// write itself is what pumps, so the block cannot avoid spanning a pump — it can
|
||||
// only span as little of one as possible.
|
||||
Undo_BeginBlock2(project);
|
||||
TrackFX_SetOffline(tr, fxIndex, offline);
|
||||
Undo_EndBlock2(project, "", 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void releaseFx(const std::string& guid) { model->releaseSnapshotFx(guid); }
|
||||
};
|
||||
|
||||
// Every path that stops scheduling ticks leaves the same state behind: no queue, no pin,
|
||||
// nothing pending. The debounce clock is NOT part of it — see fx_reclaim.h.
|
||||
void goIdle() {
|
||||
g_items.clear();
|
||||
g_project = nullptr;
|
||||
g_fxReclaimPending = false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool g_fxReclaimPending = false;
|
||||
|
||||
void invalidateFxReclaim() {
|
||||
goIdle();
|
||||
g_fxReclaimPending = true;
|
||||
}
|
||||
|
||||
void fxReclaimTick(ViewModeModel& model, ReaProject* proj) {
|
||||
if (g_draining) return;
|
||||
// The model is mid-rewrite and its records are being consumed out from under any
|
||||
// plan derived from it. The apply invalidates on its way out, so nothing is lost.
|
||||
if (modeApplyInProgress()) return;
|
||||
DrainLatch latch;
|
||||
|
||||
// Both arms below are "the project this queue belongs to is no longer the one in
|
||||
// front of us": no active project at all, or a tab switch / close. Drop the queue
|
||||
// rather than write another project's chains; returning to it re-derives, because
|
||||
// poll() reads that as a project load and the load glue reapplies, which invalidates.
|
||||
// The clock goes too — a hold measured in a project we stopped watching is not
|
||||
// evidence about the one we come back to.
|
||||
ReaProject* const project = proj ? proj : EnumProjects(-1, nullptr, 0);
|
||||
if (!ValidatePtr2(nullptr, project, "ReaProject*") || (g_project && g_project != project)) {
|
||||
goIdle();
|
||||
g_clock.forget();
|
||||
return;
|
||||
}
|
||||
|
||||
// DERIVED, never persisted: recomputed from view state every time the queue was
|
||||
// discarded, so a REAPER quit mid-drain simply re-derives and finishes on reopen.
|
||||
if (!g_project) {
|
||||
g_project = project;
|
||||
g_items = makeQueue(
|
||||
view::desiredFxState(model, model.activeModeId(), liveTrackGuids(project)));
|
||||
}
|
||||
|
||||
ReclaimHost host{project, &model};
|
||||
if (drainTick(g_items, g_clock, host).settled) goIdle();
|
||||
}
|
||||
|
||||
} // namespace reasampler
|
||||
Reference in New Issue
Block a user