Gate the FX reclaimer on the transport, both directions; move the release's persist off the drain and onto the project save
The drain no longer dirties a project on its own initiative — a spent park record rides the user's next save via OnSaveExtensionConfig.
This commit is contained in:
+11
-4
@@ -250,18 +250,25 @@ static void OnBeginLoadProjectState(bool isUndo, project_config_extension_t* /*r
|
|||||||
g_session.requestReload();
|
g_session.requestReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intentional no-ops: ReaSampler stores state via project EXT STATE, not this
|
// Intentional no-op: ReaSampler stores state via project EXT STATE, not this
|
||||||
// extension's own project lines. The struct is registered ONLY for the
|
// extension's own project lines.
|
||||||
// BeginLoadProjectState undo/redo notification.
|
|
||||||
static bool OnProcessExtensionLine(const char* /*line*/, ProjectStateContext* /*ctx*/,
|
static bool OnProcessExtensionLine(const char* /*line*/, ProjectStateContext* /*ctx*/,
|
||||||
bool /*isUndo*/, project_config_extension_t* /*reg*/)
|
bool /*isUndo*/, project_config_extension_t* /*reg*/)
|
||||||
{
|
{
|
||||||
return false; // we own no project lines — ext state carries our data
|
return false; // we own no project lines — ext state carries our data
|
||||||
}
|
}
|
||||||
|
|
||||||
static void OnSaveExtensionConfig(ProjectStateContext* /*ctx*/, bool /*isUndo*/,
|
// We write no lines into `ctx` — this is the FX reclaimer's one durability point. Its
|
||||||
|
// spent park records ride the user's save rather than dirtying the project on the
|
||||||
|
// reclaimer's own initiative (fx_reclaim.h's ReleaseFlush).
|
||||||
|
//
|
||||||
|
// A save only: an isUndo pass is REAPER capturing an undo point, not writing the file,
|
||||||
|
// and clearing the pending flush on one would leave the record unspent on disk for good.
|
||||||
|
static void OnSaveExtensionConfig(ProjectStateContext* /*ctx*/, bool isUndo,
|
||||||
project_config_extension_t* /*reg*/)
|
project_config_extension_t* /*reg*/)
|
||||||
{
|
{
|
||||||
|
if (!isUndo)
|
||||||
|
reasampler::fxReclaimFlushRelease(g_session);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Storage must outlive registration — REAPER holds this pointer until we unregister it.
|
// Storage must outlive registration — REAPER holds this pointer until we unregister it.
|
||||||
|
|||||||
@@ -211,6 +211,16 @@ bool ReaSamplerSession::saveViewStateOnly() {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
writeViewState(proj, view_);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool ReaSamplerSession::writeAssignmentRequest(const std::string& wire) {
|
bool ReaSamplerSession::writeAssignmentRequest(const std::string& wire) {
|
||||||
ReaProject* proj = activeSavedProject();
|
ReaProject* proj = activeSavedProject();
|
||||||
if (!proj) return false;
|
if (!proj) return false;
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ public:
|
|||||||
// Returns on the SAME rule as saveToActiveProject above, prohibition included.
|
// Returns on the SAME rule as saveToActiveProject above, prohibition included.
|
||||||
bool saveViewStateOnly();
|
bool saveViewStateOnly();
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
|
||||||
// Report-only prune dry-run: feeds the pure core with (present, referenced,
|
// Report-only prune dry-run: feeds the pure core with (present, referenced,
|
||||||
// owned) — `present` from the folder enumeration, the other two from the
|
// owned) — `present` from the folder enumeration, the other two from the
|
||||||
// tracking authority. FAIL-SAFE: tracking state the authority cannot read
|
// tracking authority. FAIL-SAFE: tracking state the authority cannot read
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "shell/persist/session.h" // saveViewStateOnly — the release's durable half
|
#include "shell/persist/session.h" // flushViewState — the release's durable half
|
||||||
#include "shell/view/view.h" // modeApplyInProgress
|
#include "shell/view/view.h" // modeApplyInProgress, transportBlocksModeSwitch
|
||||||
#include "shell/view/view_park.h" // fxGuidString — the one FX-identity read
|
#include "shell/view/view_park.h" // fxGuidString — the one FX-identity read
|
||||||
#include "shell/view/view_tracks.h"
|
#include "shell/view/view_tracks.h"
|
||||||
|
|
||||||
@@ -34,6 +34,9 @@ namespace {
|
|||||||
// FxReclaimQueue, which owns all three.
|
// FxReclaimQueue, which owns all three.
|
||||||
FxReclaimQueue g_queue;
|
FxReclaimQueue g_queue;
|
||||||
|
|
||||||
|
// A release applied to the in-memory model and not yet written — see ReleaseFlush.
|
||||||
|
ReleaseFlush g_releaseFlush;
|
||||||
|
|
||||||
// TrackFX_SetOffline is ASSUMED to pump the message loop, so a plugin load can re-enter
|
// 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
|
// 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.
|
// second queue for the same project and race the outer one's writes track for track.
|
||||||
@@ -103,6 +106,11 @@ struct ReclaimHost {
|
|||||||
.count();
|
.count();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The SAME predicate applyMode refuses a real mode switch on, deliberately shared
|
||||||
|
// rather than re-read here: what counts as a transport that must not have plugins
|
||||||
|
// moved under it is one fact with one home (view.h).
|
||||||
|
bool transportRolling() const { return transportBlocksModeSwitch(project); }
|
||||||
|
|
||||||
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
||||||
MediaTrack* tr = resolveForStep(guid);
|
MediaTrack* tr = resolveForStep(guid);
|
||||||
if (!tr) return false;
|
if (!tr) return false;
|
||||||
@@ -136,17 +144,10 @@ struct ReclaimHost {
|
|||||||
|
|
||||||
// Reached only after the drain has unwound and the queue has proved its generation
|
// 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.
|
// 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); }
|
// The spend lands in memory here and reaches disk on the project's next save.
|
||||||
|
void releaseFx(const std::string& guid) {
|
||||||
// saveViewStateOnly writes the ACTIVE project and takes none, so re-read that rather
|
session->view().releaseSnapshotFx(guid);
|
||||||
// than trust the project this tick pinned: a write pumps, and a tab switch inside one
|
g_releaseFlush.arm(project);
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -212,4 +213,15 @@ void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj) {
|
|||||||
if (!run.aborted && run.settled) g_queue.goIdle();
|
if (!run.aborted && run.settled) g_queue.goIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
+52
-21
@@ -173,18 +173,19 @@ struct ReclaimTickResult {
|
|||||||
// fx_reclaim.cpp holds the one live implementation. Host must provide:
|
// fx_reclaim.cpp holds the one live implementation. Host must provide:
|
||||||
//
|
//
|
||||||
// double nowMs() monotonic milliseconds
|
// double nowMs() monotonic milliseconds
|
||||||
|
// bool transportRolling() playing or recording — the tick
|
||||||
|
// may move no plugins at all
|
||||||
// bool liveChain(guid, std::vector<std::string>&) FX identity per live slot;
|
// bool liveChain(guid, std::vector<std::string>&) FX identity per live slot;
|
||||||
// false when the track is gone
|
// false when the track is gone
|
||||||
// bool writeOffline(guid, fxIndex, offline) compare-before-write inside its
|
// bool writeOffline(guid, fxIndex, offline) compare-before-write inside its
|
||||||
// own discarded undo block;
|
// own discarded undo block;
|
||||||
// true iff it actually wrote
|
// true iff it actually wrote
|
||||||
// void releaseFx(guid) release the record's FX half;
|
// void releaseFx(guid) release the record's FX half and
|
||||||
// called by FxReclaimQueue::run
|
// arm the save-time flush that
|
||||||
// after the drain has unwound,
|
// makes the spend durable; called
|
||||||
// never from inside the drain
|
// by FxReclaimQueue::run after the
|
||||||
// void persistModel() make the releases just applied
|
// drain has unwound, never from
|
||||||
// durable; run calls it ONCE per
|
// inside the drain
|
||||||
// tick and only after a release
|
|
||||||
//
|
//
|
||||||
// `items` and `clock` must be the caller's OWN storage, not shared state a re-entered
|
// `items` and `clock` must be the caller's OWN storage, not shared state a re-entered
|
||||||
// call could reach: a write pumps the message loop. FxReclaimQueue::run is what makes
|
// call could reach: a write pumps the message loop. FxReclaimQueue::run is what makes
|
||||||
@@ -192,6 +193,18 @@ struct ReclaimTickResult {
|
|||||||
template <class Host>
|
template <class Host>
|
||||||
ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, Host& host) {
|
ReclaimTickResult drainTick(std::vector<ReclaimItem>& items, ParkClock& clock, Host& host) {
|
||||||
ReclaimTickResult result;
|
ReclaimTickResult result;
|
||||||
|
|
||||||
|
// Mirrors applyMode's refusal (view.h, transportBlocksModeSwitch), and now covers
|
||||||
|
// every FX write the tool makes, since this loop owns them all. BOTH directions, not
|
||||||
|
// just the opportunistic offline one: onlining re-instantiates the very plugins the
|
||||||
|
// user is listening through — the same destruction the switch already refuses — and a
|
||||||
|
// step is a whole chain with no yield inside it. Nothing is observed on this path, so
|
||||||
|
// every debounce hold keeps its origin and the first ungated tick fires with no wait.
|
||||||
|
if (host.transportRolling()) {
|
||||||
|
result.settled = queueSettled(items);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
clock.observe(items, host.nowMs());
|
clock.observe(items, host.nowMs());
|
||||||
|
|
||||||
const int first = nextEligible(items, clock, host.nowMs());
|
const int first = nextEligible(items, clock, host.nowMs());
|
||||||
@@ -358,19 +371,9 @@ public:
|
|||||||
items_.swap(work);
|
items_.swap(work);
|
||||||
// A release dropped by an abort is re-owed, not lost: the next derivation plans
|
// A release dropped by an abort is re-owed, not lost: the next derivation plans
|
||||||
// the same Online item, its writes all find the chain already there, and the
|
// the same Online item, its writes all find the chain already there, and the
|
||||||
// release fires then.
|
// release fires then. Applied in MEMORY only — the spend reaches disk on the
|
||||||
if (!tick.released.empty()) {
|
// project's next save (ReleaseFlush), never on the drain's own initiative.
|
||||||
for (const std::string& guid : tick.released) host.releaseFx(guid);
|
for (const std::string& guid : tick.released) host.releaseFx(guid);
|
||||||
// The spend has to be as durable as the record it spends. A reload — a
|
|
||||||
// reopen, or the undo/redo loadFromProject — rebuilds the model from the
|
|
||||||
// PERSISTED view state, and a record still held there re-derives this same
|
|
||||||
// Online work and re-drives the chain, silently reverting whatever per-FX
|
|
||||||
// offline the user set once the reclaim had finished. One persist per tick,
|
|
||||||
// after every release the tick earned, and still killable in between: die
|
|
||||||
// there and the record simply survives on disk, whereupon the re-derived
|
|
||||||
// item finds the chain already online, writes nothing, and releases again.
|
|
||||||
host.persistModel();
|
|
||||||
}
|
|
||||||
return ReclaimRunResult{tick.writes, tick.settled, false, tick.drops};
|
return ReclaimRunResult{tick.writes, tick.settled, false, tick.drops};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,6 +387,30 @@ private:
|
|||||||
bool pending_ = false;
|
bool pending_ = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- the release's durable half --------------------------------------------------
|
||||||
|
|
||||||
|
// A park record spent in MEMORY only, waiting for the project's next save.
|
||||||
|
//
|
||||||
|
// The drain used to persist it itself, and that dirtied the project: open one saved
|
||||||
|
// mid-reclaim, let the drain finish, and the user is asked to save a project they only
|
||||||
|
// opened. The tool no longer marks a project dirty on its own initiative — it writes what
|
||||||
|
// it owes when the USER saves. The exposure that buys is the window between the two: die
|
||||||
|
// 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.
|
||||||
|
class ReleaseFlush {
|
||||||
|
public:
|
||||||
|
void arm(ReaProject* project) { project_ = project; }
|
||||||
|
bool pendingFor(ReaProject* active) const { return project_ && project_ == active; }
|
||||||
|
void clear() { project_ = nullptr; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
ReaProject* project_ = nullptr; // COMPARE-ONLY, never dereferenced
|
||||||
|
};
|
||||||
|
|
||||||
// --- shell entry points --------------------------------------------------------
|
// --- shell entry points --------------------------------------------------------
|
||||||
|
|
||||||
// The ONE test the timer makes per tick, mirroring the realtime capture's single
|
// The ONE test the timer makes per tick, mirroring the realtime capture's single
|
||||||
@@ -408,8 +435,12 @@ bool fxReclaimDrainInProgress();
|
|||||||
// (FxReclaimQueue::beginTick). Returns without touching the project while an applyMode is
|
// (FxReclaimQueue::beginTick). Returns without touching the project while an applyMode is
|
||||||
// on the stack, while already draining, or when there is no valid active project at all.
|
// on the stack, while already draining, or when there is no valid active project at all.
|
||||||
// Takes the SESSION rather than the model alone because completing an online spends the
|
// Takes the SESSION rather than the model alone because completing an online spends the
|
||||||
// park record's FX half, and that spend has to reach the persisted view state — see
|
// park record's FX half — see FxReclaimQueue::run.
|
||||||
// FxReclaimQueue::run.
|
|
||||||
void fxReclaimTick(ReaSamplerSession& session, ReaProject* proj);
|
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).
|
||||||
|
void fxReclaimFlushRelease(ReaSamplerSession& session);
|
||||||
|
|
||||||
} // namespace reasampler
|
} // namespace reasampler
|
||||||
|
|||||||
+179
-37
@@ -2,10 +2,11 @@
|
|||||||
// loop is templated on its host precisely so the properties that make it safe to run in
|
// loop is templated on its host precisely so the properties that make it safe to run in
|
||||||
// the background are assertable here: the debounce, the per-tick budget's between-track
|
// the background are assertable here: the debounce, the per-tick budget's between-track
|
||||||
// yield, the queue being a cache of intent that a model change discards, the user's own
|
// yield, the queue being a cache of intent that a model change discards, the user's own
|
||||||
// offline plugins surviving an online, the release reaching the persisted model, and —
|
// offline plugins surviving an online, the transport gate, the release reaching disk on
|
||||||
// via a host that invalidates from inside a write — that a re-entered invalidation
|
// the user's save rather than on the drain, and — via a host that invalidates from inside
|
||||||
// reaches nothing the drain is using. The queue owns the project pin and the timer's
|
// a write — that a re-entered invalidation reaches nothing the drain is using. The queue
|
||||||
// wakeup for the same reason: a compare-only pointer makes the tab switch assertable.
|
// owns the project pin and the timer's wakeup for the same reason: a compare-only pointer
|
||||||
|
// makes the tab switch assertable.
|
||||||
|
|
||||||
#include "../src/shell/view/fx_reclaim.h"
|
#include "../src/shell/view/fx_reclaim.h"
|
||||||
|
|
||||||
@@ -28,6 +29,11 @@ namespace {
|
|||||||
|
|
||||||
// -- a fake project ------------------------------------------------------------
|
// -- a fake project ------------------------------------------------------------
|
||||||
|
|
||||||
|
// Two project handles. The queue and the flush treat a project pointer as COMPARE-ONLY
|
||||||
|
// and never dereference it, so a fabricated address is a faithful stand-in for REAPER's.
|
||||||
|
ReaProject* const kProjA = reinterpret_cast<ReaProject*>(static_cast<std::uintptr_t>(0x1));
|
||||||
|
ReaProject* const kProjB = reinterpret_cast<ReaProject*>(static_cast<std::uintptr_t>(0x2));
|
||||||
|
|
||||||
// One live FX: its identity (empty when REAPER reports none) and whether it is offline.
|
// One live FX: its identity (empty when REAPER reports none) and whether it is offline.
|
||||||
struct FakeFx {
|
struct FakeFx {
|
||||||
std::string guid;
|
std::string guid;
|
||||||
@@ -51,7 +57,10 @@ struct FakeHost {
|
|||||||
int invalidateAfterWrite = -1;
|
int invalidateAfterWrite = -1;
|
||||||
int itemsSeenMidDrain = -1; // queue size as the re-entrant caller finds it
|
int itemsSeenMidDrain = -1; // queue size as the re-entrant caller finds it
|
||||||
|
|
||||||
|
bool rolling = false; // the transport gate's input
|
||||||
|
|
||||||
double nowMs() const { return clock; }
|
double nowMs() const { return clock; }
|
||||||
|
bool transportRolling() const { return rolling; }
|
||||||
|
|
||||||
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
bool liveChain(const std::string& guid, std::vector<std::string>& out) {
|
||||||
auto it = chains.find(guid);
|
auto it = chains.find(guid);
|
||||||
@@ -80,21 +89,18 @@ struct FakeHost {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// `model` is the live host's session model and `persisted` stands in for view_state
|
// `model` is the live host's session model and `persisted` stands in for view_state
|
||||||
// on disk — the blob a reopen or an undo/redo loadFromProject rebuilds from. Set them
|
// on disk — the blob a reopen or an undo/redo loadFromProject rebuilds from. The
|
||||||
// and the release actually lands, so a persist ordered BEFORE it would serialize a
|
// drain writes the model and NEVER `persisted`: only saveProject() below does, which
|
||||||
// record that is still held and fail the reload assertions.
|
// is what makes "the spend rides the user's save" assertable.
|
||||||
ViewModeModel* model = nullptr;
|
ViewModeModel* model = nullptr;
|
||||||
|
ReleaseFlush* flush = nullptr;
|
||||||
|
ReaProject* project = kProjA;
|
||||||
std::string persisted;
|
std::string persisted;
|
||||||
int persists = 0;
|
|
||||||
|
|
||||||
void releaseFx(const std::string& guid) {
|
void releaseFx(const std::string& guid) {
|
||||||
released.insert(guid);
|
released.insert(guid);
|
||||||
if (model) model->releaseSnapshotFx(guid);
|
if (model) model->releaseSnapshotFx(guid);
|
||||||
}
|
if (flush) flush->arm(project);
|
||||||
|
|
||||||
void persistModel() {
|
|
||||||
++persists;
|
|
||||||
if (model) persisted = model->serialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool offlineAt(const std::string& guid, std::size_t slot) const {
|
bool offlineAt(const std::string& guid, std::size_t slot) const {
|
||||||
@@ -103,6 +109,14 @@ struct FakeHost {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// What the project-save hook does (fxReclaimFlushRelease): write the in-memory model into
|
||||||
|
// this project's view state, but only when it is the project holding the release.
|
||||||
|
void saveProject(FakeHost& host, ReleaseFlush& flush, ReaProject* active) {
|
||||||
|
if (!flush.pendingFor(active)) return;
|
||||||
|
host.persisted = host.model->serialize();
|
||||||
|
flush.clear();
|
||||||
|
}
|
||||||
|
|
||||||
// -- model helpers -------------------------------------------------------------
|
// -- model helpers -------------------------------------------------------------
|
||||||
|
|
||||||
TrackSnapshot recordWithFx(std::vector<FxOfflineState> fx) {
|
TrackSnapshot recordWithFx(std::vector<FxOfflineState> fx) {
|
||||||
@@ -178,11 +192,6 @@ FxReclaimQueue queueOf(const ViewModeModel& vm, const std::set<std::string>& liv
|
|||||||
return q;
|
return q;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two project handles. The queue treats a project pointer as COMPARE-ONLY and never
|
|
||||||
// dereferences it, so a fabricated address is a faithful stand-in for REAPER's.
|
|
||||||
ReaProject* const kProjA = reinterpret_cast<ReaProject*>(static_cast<std::uintptr_t>(0x1));
|
|
||||||
ReaProject* const kProjB = reinterpret_cast<ReaProject*>(static_cast<std::uintptr_t>(0x2));
|
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
// -- 1. debounce ---------------------------------------------------------------
|
// -- 1. debounce ---------------------------------------------------------------
|
||||||
@@ -505,6 +514,8 @@ static void testAnInvalidationDuringAnOnlineDropsThatTicksRelease() {
|
|||||||
vm.releaseSnapshotFlags("{T}");
|
vm.releaseSnapshotFlags("{T}");
|
||||||
|
|
||||||
FakeHost host;
|
FakeHost host;
|
||||||
|
ReleaseFlush flush;
|
||||||
|
host.flush = &flush;
|
||||||
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
|
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
|
||||||
|
|
||||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
@@ -516,7 +527,7 @@ static void testAnInvalidationDuringAnOnlineDropsThatTicksRelease() {
|
|||||||
CHECK(r.aborted);
|
CHECK(r.aborted);
|
||||||
CHECK(r.writes == 2); // the item still completed — a step is atomic
|
CHECK(r.writes == 2); // the item still completed — a step is atomic
|
||||||
CHECK(host.released.empty()); // but its record was NOT spent
|
CHECK(host.released.empty()); // but its record was NOT spent
|
||||||
CHECK(host.persists == 0); // and nothing durable claims it was
|
CHECK(!flush.pendingFor(kProjA)); // and no save is owed one that never happened
|
||||||
|
|
||||||
// Re-owed, not lost: the re-derived item finds the chain already where the record
|
// Re-owed, not lost: the re-derived item finds the chain already where the record
|
||||||
// wants it, writes nothing more, and releases then.
|
// wants it, writes nothing more, and releases then.
|
||||||
@@ -525,45 +536,97 @@ static void testAnInvalidationDuringAnOnlineDropsThatTicksRelease() {
|
|||||||
CHECK(q.run(host).settled);
|
CHECK(q.run(host).settled);
|
||||||
CHECK(host.writeLog.size() == 2);
|
CHECK(host.writeLog.size() == 2);
|
||||||
CHECK(host.released.count("{T}") == 1);
|
CHECK(host.released.count("{T}") == 1);
|
||||||
CHECK(host.persists == 1);
|
CHECK(flush.pendingFor(kProjA));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -- 6. the release has to outlive the session ------------------------------------
|
// -- 6. the release has to outlive the session ------------------------------------
|
||||||
|
|
||||||
static void testACompletedOnlineIsPersistedSoAReloadPlansNoWork() {
|
static void testACompletedOnlineReachesDiskOnTheSaveAndNotOnTheDrain() {
|
||||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
|
||||||
vm.setActiveMode(kDesignModeId);
|
vm.setActiveMode(kDesignModeId);
|
||||||
vm.releaseSnapshotFlags("{T}");
|
vm.releaseSnapshotFlags("{T}");
|
||||||
|
|
||||||
// The bug this closes, stated as the baseline: the model as it stands BEFORE the
|
// The bug the flush closes, stated as the baseline: the model as it stands BEFORE the
|
||||||
// release re-derives the same Online work when reloaded, and re-driving it would
|
// release re-derives the same Online work when reloaded, and re-driving it would
|
||||||
// revert whatever per-FX offline the user set after the reclaim had finished.
|
// revert whatever per-FX offline the user set after the reclaim had finished.
|
||||||
const std::optional<ViewModeModel> stale = ViewModeModel::deserialize(vm.serialize());
|
|
||||||
CHECK(stale.has_value());
|
|
||||||
CHECK(derive(*stale, {"{T}"}).size() == 1);
|
|
||||||
|
|
||||||
FakeHost host;
|
FakeHost host;
|
||||||
host.model = &vm;
|
host.model = &vm;
|
||||||
|
host.persisted = vm.serialize(); // the project as it sits on disk, record unspent
|
||||||
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
|
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
|
||||||
|
ReleaseFlush flush;
|
||||||
|
host.flush = &flush;
|
||||||
|
|
||||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
drainToSettled(q, host);
|
drainToSettled(q, host);
|
||||||
CHECK(host.released.count("{T}") == 1);
|
CHECK(host.released.count("{T}") == 1);
|
||||||
CHECK(host.persists == 1); // once per tick that released, not once per release
|
|
||||||
|
|
||||||
const std::optional<ViewModeModel> reloaded = ViewModeModel::deserialize(host.persisted);
|
// The drain wrote NOTHING durable. That is the whole change: a background tick that
|
||||||
CHECK(reloaded.has_value());
|
// persisted would also have dirtied the project, asking the user to save one they
|
||||||
CHECK(reloaded->snapshot("{T}") == nullptr); // both halves spent ⇒ record erased
|
// had only opened.
|
||||||
CHECK(derive(*reloaded, {"{T}"}).empty());
|
const std::optional<ViewModeModel> beforeSave = ViewModeModel::deserialize(host.persisted);
|
||||||
|
CHECK(beforeSave.has_value());
|
||||||
|
CHECK(beforeSave->snapshot("{T}") != nullptr);
|
||||||
|
CHECK(derive(*beforeSave, {"{T}"}).size() == 1); // still stale on disk
|
||||||
|
CHECK(flush.pendingFor(kProjA)); // ...and the save knows it is owed
|
||||||
|
|
||||||
|
// The user's next save is what spends it — and afterwards a reload plans no work,
|
||||||
|
// so nothing re-drives the chain.
|
||||||
|
saveProject(host, flush, kProjA);
|
||||||
|
CHECK(!flush.pendingFor(kProjA));
|
||||||
|
const std::optional<ViewModeModel> afterSave = ViewModeModel::deserialize(host.persisted);
|
||||||
|
CHECK(afterSave.has_value());
|
||||||
|
CHECK(afterSave->snapshot("{T}") == nullptr); // both halves spent ⇒ record erased
|
||||||
|
CHECK(derive(*afterSave, {"{T}"}).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
static void testATickThatSpendsNoRecordPersistsNothing() {
|
static void testAReleaseKilledBeforeTheFlushIsSimplyReEarned() {
|
||||||
// OFFLINE work spends no record, so there is nothing to make durable — and a
|
// The exposure the save-time flush opens: between the in-memory release and the next
|
||||||
// background reclaim must not dirty the project for work it can simply redo.
|
// save, the record on disk is still unspent. Quitting there must cost a redo of the
|
||||||
|
// reclaim, never a repair.
|
||||||
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
|
||||||
|
vm.setActiveMode(kDesignModeId);
|
||||||
|
vm.releaseSnapshotFlags("{T}");
|
||||||
|
|
||||||
|
FakeHost host;
|
||||||
|
host.model = &vm;
|
||||||
|
host.persisted = vm.serialize();
|
||||||
|
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
|
||||||
|
ReleaseFlush flush;
|
||||||
|
host.flush = &flush;
|
||||||
|
|
||||||
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
|
drainToSettled(q, host);
|
||||||
|
CHECK(host.writeLog.size() == 2); // the chain really was driven online
|
||||||
|
CHECK(flush.pendingFor(kProjA));
|
||||||
|
|
||||||
|
// Killed here: no save, so the model in memory goes and the disk record survives.
|
||||||
|
const std::optional<ViewModeModel> reopened = ViewModeModel::deserialize(host.persisted);
|
||||||
|
CHECK(reopened.has_value());
|
||||||
|
ViewModeModel reloaded = *reopened;
|
||||||
|
|
||||||
|
FakeHost after;
|
||||||
|
after.chains = host.chains; // the DAW state outlives the reload: the chain is online
|
||||||
|
after.model = &reloaded;
|
||||||
|
ReleaseFlush flushAfter;
|
||||||
|
after.flush = &flushAfter;
|
||||||
|
|
||||||
|
FxReclaimQueue q2 = queueOf(reloaded, {"{T}"});
|
||||||
|
CHECK(q2.items().size() == 1); // the surviving record re-derives the same Online work
|
||||||
|
CHECK(drainToSettled(q2, after) == 1);
|
||||||
|
CHECK(after.writeLog.empty()); // compare-before-write: nothing left to move
|
||||||
|
CHECK(after.released.count("{T}") == 1);
|
||||||
|
CHECK(flushAfter.pendingFor(kProjA)); // and the spend is owed to the next save again
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testATickThatSpendsNoRecordOwesNoSaveAnything() {
|
||||||
|
// OFFLINE work spends no record, so there is nothing to make durable — and the
|
||||||
|
// reclaimer must never give the user's next save work it did not earn.
|
||||||
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||||
FakeHost host;
|
FakeHost host;
|
||||||
host.model = &vm;
|
host.model = &vm;
|
||||||
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||||
|
ReleaseFlush flush;
|
||||||
|
host.flush = &flush;
|
||||||
|
|
||||||
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
q.run(host); // observes the hold at t=0
|
q.run(host); // observes the hold at t=0
|
||||||
@@ -572,7 +635,8 @@ static void testATickThatSpendsNoRecordPersistsNothing() {
|
|||||||
|
|
||||||
CHECK(host.writeLog.size() == 1); // it did move the plugin
|
CHECK(host.writeLog.size() == 1); // it did move the plugin
|
||||||
CHECK(host.released.empty());
|
CHECK(host.released.empty());
|
||||||
CHECK(host.persists == 0);
|
CHECK(!flush.pendingFor(kProjA));
|
||||||
|
saveProject(host, flush, kProjA);
|
||||||
CHECK(host.persisted.empty());
|
CHECK(host.persisted.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,6 +836,79 @@ static void testATickThatLostNothingNeitherPrintsNorDisturbsTheDropMemo() {
|
|||||||
CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, lost, lost));
|
CHECK(!shouldReportDrops(/*sameOwnerAsLast=*/true, lost, lost));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- 9. the transport gate ------------------------------------------------------------
|
||||||
|
|
||||||
|
static void testARollingTransportMovesNoPluginsAndKeepsTheHold() {
|
||||||
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
|
||||||
|
FakeHost host;
|
||||||
|
host.chains["{T}"] = onlineChain({"{FX-A}", "{FX-B}"});
|
||||||
|
|
||||||
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
|
CHECK(!q.run(host).settled); // the hold starts at t=0
|
||||||
|
|
||||||
|
host.rolling = true;
|
||||||
|
host.clock += kParkHoldMs; // matured — the debounce is no longer what holds it back
|
||||||
|
const ReclaimRunResult gated = q.run(host);
|
||||||
|
CHECK(gated.writes == 0);
|
||||||
|
CHECK(host.writeLog.empty());
|
||||||
|
CHECK(!gated.aborted);
|
||||||
|
CHECK(!gated.settled); // the work is still owed, so the shell must not go idle
|
||||||
|
CHECK(!q.stale()); // and the queue it will resume from survives untouched
|
||||||
|
|
||||||
|
// The hold kept its origin across the gated tick: the drain fires on the FIRST tick
|
||||||
|
// after the transport stops, with no fresh 3s wait.
|
||||||
|
host.rolling = false;
|
||||||
|
CHECK(q.run(host).settled);
|
||||||
|
CHECK(host.writeLog.size() == 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testTheGateStopsTheOnlineDirectionToo() {
|
||||||
|
// The ruling is symmetric because both directions load or unload plugins: onlining
|
||||||
|
// re-instantiates the very chain the user is listening through.
|
||||||
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}", "{FX-B}"});
|
||||||
|
vm.setActiveMode(kDesignModeId);
|
||||||
|
vm.releaseSnapshotFlags("{T}");
|
||||||
|
|
||||||
|
FakeHost host;
|
||||||
|
host.model = &vm;
|
||||||
|
host.chains["{T}"] = offlineChain({"{FX-A}", "{FX-B}"});
|
||||||
|
ReleaseFlush flush;
|
||||||
|
host.flush = &flush;
|
||||||
|
host.rolling = true;
|
||||||
|
|
||||||
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
|
CHECK(q.items().size() == 1 && q.items()[0].work.kind == FxWorkKind::Online);
|
||||||
|
const ReclaimRunResult gated = q.run(host);
|
||||||
|
CHECK(gated.writes == 0);
|
||||||
|
CHECK(!gated.settled);
|
||||||
|
CHECK(host.released.empty()); // no record spent while the transport rolls
|
||||||
|
CHECK(!flush.pendingFor(kProjA));
|
||||||
|
CHECK(host.offlineAt("{T}", 0)); // the chain is untouched, still parked
|
||||||
|
|
||||||
|
// The contrast, so the assertions above are about the gate and not about an online
|
||||||
|
// queue that cannot drain: stop the transport and the same tick completes.
|
||||||
|
host.rolling = false;
|
||||||
|
CHECK(q.run(host).settled);
|
||||||
|
CHECK(host.writeLog.size() == 2);
|
||||||
|
CHECK(host.released.count("{T}") == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void testAGatedTickStillGoesIdleWithNothingLeftToConverge() {
|
||||||
|
// A settled queue must not spin the timer for the length of a playback pass: the
|
||||||
|
// gate's answer is "no writes", not "never finished".
|
||||||
|
ViewModeModel vm = parkedDesignLeaf({"{FX-A}"});
|
||||||
|
FakeHost host;
|
||||||
|
host.chains["{T}"] = onlineChain({"{FX-A}"});
|
||||||
|
|
||||||
|
FxReclaimQueue q = queueOf(vm, {"{T}"});
|
||||||
|
q.run(host);
|
||||||
|
host.clock += kParkHoldMs;
|
||||||
|
CHECK(q.run(host).settled);
|
||||||
|
|
||||||
|
host.rolling = true;
|
||||||
|
CHECK(q.run(host).settled);
|
||||||
|
}
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
testFastFlipMovesZeroPluginsButAHeldParkStillReclaims();
|
||||||
|
|
||||||
@@ -790,8 +927,9 @@ int main() {
|
|||||||
testAnInvalidationDuringTheLastItemLeavesTheTickUnsettled();
|
testAnInvalidationDuringTheLastItemLeavesTheTickUnsettled();
|
||||||
testAnInvalidationDuringAnOnlineDropsThatTicksRelease();
|
testAnInvalidationDuringAnOnlineDropsThatTicksRelease();
|
||||||
|
|
||||||
testACompletedOnlineIsPersistedSoAReloadPlansNoWork();
|
testACompletedOnlineReachesDiskOnTheSaveAndNotOnTheDrain();
|
||||||
testATickThatSpendsNoRecordPersistsNothing();
|
testAReleaseKilledBeforeTheFlushIsSimplyReEarned();
|
||||||
|
testATickThatSpendsNoRecordOwesNoSaveAnything();
|
||||||
|
|
||||||
testADropIsReportedOnceAcrossRepeatedRederivations();
|
testADropIsReportedOnceAcrossRepeatedRederivations();
|
||||||
testAnOfflineTickReportsNoDrops();
|
testAnOfflineTickReportsNoDrops();
|
||||||
@@ -801,6 +939,10 @@ int main() {
|
|||||||
testOnlyANoProjectTickClearsTheWakeup();
|
testOnlyANoProjectTickClearsTheWakeup();
|
||||||
testATickThatLostNothingNeitherPrintsNorDisturbsTheDropMemo();
|
testATickThatLostNothingNeitherPrintsNorDisturbsTheDropMemo();
|
||||||
|
|
||||||
|
testARollingTransportMovesNoPluginsAndKeepsTheHold();
|
||||||
|
testTheGateStopsTheOnlineDirectionToo();
|
||||||
|
testAGatedTickStillGoesIdleWithNothingLeftToConverge();
|
||||||
|
|
||||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||||
return g_fail ? 1 : 0;
|
return g_fail ? 1 : 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user