fx_reclaim: use GetCurrentProjectInLoadSave for the save hook, stop disarming ReleaseFlush on one write, validate project before cached-track lookup

This commit is contained in:
2026-08-06 22:52:57 -04:00
parent 02790f1270
commit c95203718a
5 changed files with 91 additions and 33 deletions
+5 -5
View File
@@ -212,12 +212,12 @@ bool ReaSamplerSession::saveViewStateOnly() {
}
// Its twin bar the dirty mark — the one line that differs is the whole point of the
// split, so the two stay spelled out rather than folded behind a flag.
bool ReaSamplerSession::flushViewState() {
ReaProject* proj = activeSavedProject();
if (!proj) return false;
// split, so the two stay spelled out rather than folded behind a flag. `project` is
// trusted as given rather than resolved via activeSavedProject() — see session.h.
bool ReaSamplerSession::flushViewState(ReaProject* project) {
if (!project) return false;
writeViewState(proj, view_);
writeViewState(project, view_);
return true;
}
+9 -3
View File
@@ -31,6 +31,9 @@
#include "core/version/app_version.h"
#include "core/view/view_mode_model.h"
// Forward-declared to keep this header SDK-free; the .cpp includes the real SDK header.
class ReaProject;
namespace reasampler {
// Owns the session's BankBook (pool + named banks) and drives persistence
@@ -147,9 +150,12 @@ public:
// The same write with the dirty mark OMITTED, for a caller already inside REAPER's
// own project save: marking dirty there would leave the project modified the instant
// it was saved, which is the whole reason that caller exists (fx_reclaim's
// ReleaseFlush). Not a general-purpose sibling — a mutation site that skips the dirty
// mark silently loses its write when the user closes without saving.
bool flushViewState();
// ReleaseFlush). `project` is the SAVING project the caller resolved (e.g. via
// GetCurrentProjectInLoadSave()) — this call trusts it rather than re-deriving the
// active tab, which is exactly the mismatch fx_reclaim's caller exists to avoid. Not
// a general-purpose sibling — a mutation site that skips the dirty mark silently
// loses its write when the user closes without saving.
bool flushViewState(ReaProject* project);
// Report-only prune dry-run: feeds the pure core with (present, referenced,
// owned) — `present` from the folder enumeration, the other two from the
+23 -15
View File
@@ -17,6 +17,7 @@
#define REAPERAPI_MINIMAL
#define REAPERAPI_WANT_EnumProjects
#define REAPERAPI_WANT_GetCurrentProjectInLoadSave
#define REAPERAPI_WANT_ShowConsoleMsg
#define REAPERAPI_WANT_TrackFX_GetCount
#define REAPERAPI_WANT_TrackFX_GetOffline
@@ -50,13 +51,10 @@ struct DrainLatch {
// Resolved fresh from the GUID 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. The tick's own project pointer is
// validated once at entry, before any write has run; every call here happens AFTER
// at least one prior pumping write might have, so the project itself — not just the
// track — needs its own re-check, or CountTracks/GetTrack below run on a project that
// may have closed mid-drain.
// is not one this code may trust after it. The caller (ReclaimHost::resolveForStep)
// re-validates `proj` itself before calling this, on every arm, so this scan can
// assume a live project.
MediaTrack* resolveFresh(ReaProject* proj, const std::string& guid) {
if (!ValidatePtr2(nullptr, proj, "ReaProject*")) return nullptr;
TrackHandles handles;
readFolderEntries(proj, handles);
for (const auto& kv : handles)
@@ -86,9 +84,14 @@ struct ReclaimHost {
std::string cachedGuid_;
MediaTrack* cachedTrack_ = nullptr;
// A track is validated IN a project (reaper_plugin_functions.h:7898); the null form
// is defined only where the pointer is itself a project.
// Re-checks the project on EVERY call, both arms: the cached-track fast path used
// to hand `project` straight to ValidatePtr2 without re-validating it first, and a
// prior pumping write can have closed it since the tick's own entry check ran.
// (reaper_plugin_functions.h:7898 says `proj` is IGNORED when the pointer passed
// IS itself a project — not that passing one is only valid when it is; passing the
// project here is still right, just not for the reason once claimed.)
MediaTrack* resolveForStep(const std::string& guid) {
if (!ValidatePtr2(nullptr, project, "ReaProject*")) return nullptr;
if (guid == cachedGuid_ && cachedTrack_ &&
ValidatePtr2(project, cachedTrack_, "MediaTrack*"))
return cachedTrack_;
@@ -214,14 +217,19 @@ void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
}
void fxReclaimFlushRelease(ReaSamplerSession& session) {
// The save hook is handed no project, so the active one is the only candidate; a
// mismatch means the release belongs to a tab this save is not writing, and skipping
// leaves the arm standing for that tab's own save.
if (!g_releaseFlush.pendingFor(EnumProjects(-1, nullptr, 0))) return;
// GetCurrentProjectInLoadSave() names the project THIS hook is being asked to save
// (reaper_plugin_functions.h:1582-1587) -- not merely the active tab, which a
// Save-All or a background-project save can diverge from. EnumProjects(-1, ...) used
// to stand in for it here and was wrong on both of those: it could skip the flush for
// the project actually being written, or match the arm against the active tab while a
// different project saved and write into that wrong project's ext state.
ReaProject* saving = GetCurrentProjectInLoadSave();
if (!g_releaseFlush.pendingFor(saving)) return;
// Promptless, unlike design_view_actions' persistViewState, which can open a modal
// Save-As. Disarm only on a write that was ISSUED — a never-saved project writes
// nothing, and dropping the arm there would strand the spend for the whole session.
if (session.flushViewState()) g_releaseFlush.clear();
// Save-As. [verify — DAW] whether this write reaches the <EXTSTATE> block of the
// file being saved right now, or lands one save late (see ReleaseFlush) — either
// way the arm is deliberately left standing rather than cleared on this one issue.
session.flushViewState(saving);
}
} // namespace reasampler
+18 -6
View File
@@ -398,9 +398,20 @@ private:
// in there and the record survives on disk unspent, so the re-derived Online item drives
// the chain to it again, reverting any per-FX offline set since the reclaim finished.
//
// Keyed on the project because the save hook is handed none and the session's model is
// whichever project is active: firing for a different one would land this model in
// another project's ext state. A stale arm simply never fires — the same recovery leg.
// Keyed on the project so a release for one project's model can never land in a
// different project's ext state (checked against GetCurrentProjectInLoadSave() at the
// hook — see fxReclaimFlushRelease). A stale arm simply never fires — the same
// recovery leg.
//
// `clear()` is deliberately NOT called after a single issued write. [verify — DAW]
// whether a SetProjExtState made from inside SaveExtensionConfig reaches the
// <EXTSTATE> block of the very file being written now, or lands one save later —
// project_config_extension_t (reaper_plugin.h:1196-1208) documents no ordering, and
// SetProjExtState (reaper_plugin_functions.h:6288) promises only "restored the next
// time this project is loaded". If the unfavourable answer holds, clearing here would
// strand the spend in memory for the rest of the session with nothing to retry it —
// so the arm stays armed and a further save simply re-issues the same idempotent
// write.
class ReleaseFlush {
public:
void arm(ReaProject* project) { project_ = project; }
@@ -438,9 +449,10 @@ bool fxReclaimDrainInProgress();
// park record's FX half — see FxReclaimQueue::run.
void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj);
// Writes an in-memory release into the active project's view state. Called from the
// project-save hook and nowhere else: this is the ONE path that makes a spent record
// durable, and it deliberately does not mark the project dirty (see ReleaseFlush).
// Writes an in-memory release into the SAVING project's view state (identified via
// GetCurrentProjectInLoadSave(), not the active tab). Called from the project-save
// hook and nowhere else: this is the ONE path that makes a spent record durable, and
// it deliberately does not mark the project dirty (see ReleaseFlush).
void fxReclaimFlushRelease(ReaSamplerSession& session);
} // namespace reasampler