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
+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