b7b93a7ac3
The drain refuses a tick inside a foreign block; applyMode wakes the reclaimer before it opens the span.
321 lines
16 KiB
C++
321 lines
16 KiB
C++
// 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/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<double, std::milli>(
|
|
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<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;
|
|
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<std::string>& 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<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;
|
|
}
|
|
|
|
// ...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<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());
|
|
}
|
|
|
|
} // 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 <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
|