// 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 #include #include #include #include "shell/persist/session.h" // flushViewState — the release's durable half #include "shell/undo/undo_block.h" // undoBlockOpen — the reverse of the nesting rule #include "shell/view/view.h" // modeApplyInProgress, transportBlocksModeSwitch #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_GetCurrentProjectInLoadSave #define REAPERAPI_WANT_ShowConsoleMsg #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, the project it was derived against, and the timer's wakeup — see // FxReclaimQueue, which owns all three. FxReclaimQueue g_queue; // A release applied to the in-memory model and not yet written — see ReleaseFlush. ReleaseFlush g_releaseFlush; // The one open undo block a mode switch and its parking share — see UndoSpan. UndoSpan g_span; // The ONE open and the ONE close. Both idempotent, so every caller may say either // unconditionally; the close reads the span's values before clearing it, because the // clear has to precede the SDK call — nothing re-entered through Undo_EndBlock2 may // find a span it could close a second time. void openSpan(ReaProject* proj, const char* label, int mask) { if (g_span.begin(proj, label, mask)) Undo_BeginBlock2(proj); } void closeSpan() { if (!g_span.open()) return; ReaProject* const proj = g_span.project(); const std::string label = g_span.label(); const int mask = g_span.mask(); g_span.end(); // The span outlives its opener, so by here the project may be the one that just // closed — its undo history went with it and there is no block left to end. if (ValidatePtr2(nullptr, proj, "ReaProject*")) Undo_EndBlock2(proj, label.c_str(), mask); } // Monotonic and sub-millisecond: the drain's budget 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 monotonicMs() { return std::chrono::duration( std::chrono::steady_clock::now().time_since_epoch()) .count(); } // 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 derive a // second queue for the same project and race the outer one's writes track for track. 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 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 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) { 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 liveTrackGuids(ReaProject* proj) { TrackHandles handles; readFolderEntries(proj, handles); std::set 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; ReaSamplerSession* session = nullptr; // One item's liveChain call plus every writeOffline call it drives all name the // SAME track, back to back, before drainTick can move to the next item — so the // full O(T) resolveFresh scan is owed once per item, not once per call. Every // call after the first re-validates the SAME pointer instead: cheap, and still // catches a track (or its whole project) that a prior write's pump took down, // which the scan-per-call version caught too, just at O(T) instead of O(1). std::string cachedGuid_; MediaTrack* cachedTrack_ = nullptr; // 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_; cachedTrack_ = resolveFresh(project, guid); cachedGuid_ = guid; return cachedTrack_; } double nowMs() const { return monotonicMs(); } // The SAME predicate applyMode refuses a real mode switch on, deliberately shared // rather than re-read here: "the transport is rolling" is one fact with one home // (view.h). drainTick narrows what it gates with this answer to offline work only. bool transportRolling() const { return transportBlocksModeSwitch(project); } // Refusing here ends the drain's REMAINING tracks after an abandon: drainTick treats a // false as "this track is gone", so the rest of the queue costs one bool apiece and // reaches no REAPER call at all. bool liveChain(const std::string& guid, std::vector& out) { if (g_queue.abandoned()) return false; MediaTrack* tr = resolveForStep(guid); if (!tr) return false; const int count = TrackFX_GetCount(tr); out.clear(); out.reserve(static_cast(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; } // ...and here for the track being written RIGHT NOW, whose own pump is how the state // load arrived. FIRST, before the openSpan below: a write past the terminator would // both land in the project the load is replacing and reopen the span it just closed. bool writeOffline(const std::string& guid, int fxIndex, bool offline) { if (g_queue.abandoned()) return false; MediaTrack* tr = resolveForStep(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 // Every write in the drain rides ONE span. Do not reintroduce a block per write: // it minted one undo point per FX toggle, because the ("", 0) discard form // coalesces rather than discards outside an action (see UndoSpan). The open is a // no-op under a switch's span, which is why the FX bit is added separately — // here, at the first real write, so a switch whose parking never matured closes // on the flags-and-items mask it opened with and serializes no FX state. openSpan(project, kReclaimUndoLabel, UNDO_STATE_FX); g_span.widen(UNDO_STATE_FX); TrackFX_SetOffline(tr, fxIndex, offline); return true; } // Reached only after the drain has unwound and the queue has proved its generation // did not move, so the session still holds the model the queue was derived against. // The spend lands in memory here and reaches disk on the project's next save. void releaseFx(const std::string& guid) { session->view().releaseSnapshotFx(guid); g_releaseFlush.arm(project); } }; // 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 g_reportedDrops; void reportDrops(ReaProject* proj, const std::vector& 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(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()); } } // namespace bool fxReclaimPending() { return g_queue.pending(); } FxReclaimActivity fxReclaimActivity() { // The QUEUE's own pin, never the active tab: the direction is a fact about the project // the drain is converging, and reading another tab's transport into it would gate the // answer on a playhead that has nothing to do with this work. No pin means idle — and // costs the caller no REAPER call at all, which is what makes this safe to poll per // frame. ReaProject* const proj = g_queue.project(); if (!proj) return FxReclaimActivity::Idle; // The pin is only re-tested at beginTick; a poll landing between two ticks can find the // project already gone, and GetPlayStateEx is about to be handed this pointer. if (!ValidatePtr2(nullptr, proj, "ReaProject*")) return FxReclaimActivity::Idle; return g_queue.activity(monotonicMs(), transportBlocksModeSwitch(proj)); } void invalidateFxReclaim() { // A generation bump and a flag, and deliberately nothing more. Discarding the queue // here is what let a re-entered apply free the element the outer drain was writing // through, and clear the very wakeup this call had just raised. g_queue.invalidate(); } bool fxReclaimDrainInProgress() { return g_draining; } void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) { // NO guard here may close the span: each names a caller that is still on the stack and // still owns it — the outer drain, or the applyMode that opened it. All re-tick. 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; // The MIRROR of the rule that no block may open over the span: an extension undo // block is open — a capture, a bank verb, an ingest — and its own message pump is how // this tick arrived. openSpan from in there would begin at depth 2, and that block's // close would then mint under the SPAN's label and destroy the user's own point. // Refused whole rather than written unspanned, which would instead absorb the FX // toggles into the user's act. Starves nothing: the wakeup, the derived queue and the // project pin are all untouched (this returns before beginTick), so the first tick // after the block closes drains exactly as if this one had never been due. if (undoBlockOpen()) return; DrainLatch latch; // beginTick owns what a pin/project mismatch means. What is local to here: the model // the re-derivation below reads is the ACTIVE project's, because reaching this line // means OnTimer ran session.poll() this tick — the two guards above are the same pair // that gate that poll. ReaProject* project = proj ? proj : EnumProjects(-1, nullptr, 0); if (!ValidatePtr2(nullptr, project, "ReaProject*")) project = nullptr; // A span belongs to the project it was opened on. Keyed on the SPAN's own project // rather than the queue's pin, which moves independently: a tab switch, a closed // project, or no active project at all ends it where it stands rather than carrying // it into another project's history — and the no-project arm below returns. if (g_span.open() && g_span.project() != project) closeSpan(); if (!g_queue.beginTick(project)) return; // DERIVED, never persisted: recomputed from view state every time the queue was // invalidated, so a REAPER quit mid-drain simply re-derives and finishes on reopen. if (g_queue.stale()) { const ViewModeModel& model = session.view(); g_queue.adopt(makeQueue( view::desiredFxState(model, model.activeModeId(), liveTrackGuids(project)))); } ReclaimHost host{project, &session}; const ReclaimRunResult run = g_queue.run(host); reportDrops(project, run.drops); if (spanEndsAfter(run)) closeSpan(); if (!run.aborted && run.settled) g_queue.goIdle(); } void fxReclaimBeginUndoSpan(ReaProject* proj) { openSpan(proj, "", 0); } void fxReclaimLabelUndoSpan(const char* label, int mask) { g_span.relabel(label ? label : "", mask); } void fxReclaimEndUndoSpan() { closeSpan(); } void fxReclaimAbandonForStateLoad() { // Close FIRST and unconditionally. The abandon below stops the drain and puts the loop // to sleep; the block is what a Ctrl-Z would otherwise close over the wrong state, and // it is open whether or not a drain is on the stack at all. closeSpan(); g_queue.abandon(); } void fxReclaimFlushRelease(ReaSamplerSession& session) { // 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(); // BEFORE the arm test, and unconditionally: a save is a user act the span must not // absorb, and the arm is not what decides whether one is open. closeSpan(); if (!g_releaseFlush.pendingFor(saving)) return; // Promptless, unlike design_view_actions' persistViewState, which can open a modal // Save-As. [verify — DAW] whether this write reaches the 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