Persist the reclaimer's FX release so a reload cannot re-drive a spent record, and report the FX-restore drops it was swallowing

This commit is contained in:
2026-08-06 21:26:55 -04:00
parent 5dcfd3a53f
commit 7687ea76fc
4 changed files with 247 additions and 19 deletions
+41 -6
View File
@@ -10,12 +10,14 @@
#include <string>
#include <vector>
#include "shell/persist/session.h" // saveViewStateOnly — the release's durable half
#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_ShowConsoleMsg
#define REAPERAPI_WANT_TrackFX_GetCount
#define REAPERAPI_WANT_TrackFX_GetOffline
#define REAPERAPI_WANT_TrackFX_SetOffline
@@ -67,8 +69,8 @@ std::set<std::string> liveTrackGuids(ReaProject* proj) {
// The live half of drainTick's Host contract (see fx_reclaim.h).
struct ReclaimHost {
ReaProject* project = nullptr;
ViewModeModel* model = nullptr;
ReaProject* project = nullptr;
ReaSamplerSession* session = nullptr;
// Monotonic and sub-millisecond: the budget below discriminates at 30ms, which
// GetTickCount's ~16ms granularity cannot do. No wraparound to guard against, so
@@ -111,10 +113,41 @@ struct ReclaimHost {
}
// Reached only after the drain has unwound and the queue has proved its generation
// did not move, so `model` is still the object the queue was derived against.
void releaseFx(const std::string& guid) { model->releaseSnapshotFx(guid); }
// did not move, so the session still holds the model the queue was derived against.
void releaseFx(const std::string& guid) { session->view().releaseSnapshotFx(guid); }
// saveViewStateOnly writes the ACTIVE project and takes none, so re-read that rather
// than trust the project this tick pinned: a write pumps, and a tab switch inside one
// would otherwise land this model in the wrong project's ext state. Skipping is
// self-healing — coming back to the tab reloads the model from the record still on
// disk, and the re-derived online writes nothing and releases again. Promptless,
// unlike design_view_actions' persistViewState, which can open a modal Save-As.
void persistModel() {
if (EnumProjects(-1, nullptr, 0) != project) return;
session->saveViewStateOnly();
}
};
// The drop set the last console report named, and the project it named it for. Same
// one-per-changed-set memo as view_park's reportExposedTracks, which owns the reasoning;
// here the repeat it swallows is an aborted tick's re-derivation resolving identically.
// The pointer is COMPARE-ONLY and never dereferenced.
ReaProject* g_reportedDropOwner = nullptr;
std::vector<TrackFxDrops> g_reportedDrops;
void reportDrops(ReaProject* proj, const std::vector<TrackFxDrops>& drops) {
if (!shouldReportDrops(proj == g_reportedDropOwner, g_reportedDrops, drops)) return;
g_reportedDropOwner = proj;
g_reportedDrops = drops;
FxRestoreDrops total;
for (const TrackFxDrops& d : drops) total.add(d.drops);
const std::string msg = describeFxRestoreDrops(total, static_cast<int>(drops.size()));
// "!SHOW:" so a background tick cannot force the console open
// (reaper_plugin_functions.h:6536).
if (!msg.empty()) ShowConsoleMsg(("!SHOW:" + msg).c_str());
}
// 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() {
@@ -137,7 +170,7 @@ void invalidateFxReclaim() {
bool fxReclaimDrainInProgress() { return g_draining; }
void fxReclaimTick(ViewModeModel& model, ReaProject* proj) {
void fxReclaimTick(ReaSamplerSession& session, 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.
@@ -161,12 +194,14 @@ void fxReclaimTick(ViewModeModel& model, ReaProject* proj) {
// invalidated, so a REAPER quit mid-drain simply re-derives and finishes on reopen.
if (g_queue.stale()) {
g_project = project;
const ViewModeModel& model = session.view();
g_queue.adopt(makeQueue(
view::desiredFxState(model, model.activeModeId(), liveTrackGuids(project))));
}
ReclaimHost host{project, &model};
ReclaimHost host{project, &session};
const ReclaimRunResult run = g_queue.run(host);
reportDrops(project, run.drops);
if (!run.aborted && run.settled) goIdle();
}