Prove every ext-state write by reading the key back, so no guard is constant-true
SetProjExtState's return describes the whole extname. The persist and the instrument's publish now re-read their key; both refusals can finally fire.
This commit is contained in:
@@ -57,7 +57,7 @@ detail not covered there:
|
||||
- `render_selection` (`shell/capture`) — the transient track selection a selected-tracks render (`&128`) requires, as a stack RAII guard: REAPER prints whatever tracks are selected, so `renderOffline` makes the request's own tracks BE the selection for the render's duration and restores the user's set on every exit path. Engaged ONLY for that source mode, which leaves a stated residual: a `&32` selected-items render still prints whatever ITEMS the user has selected. Live captures are unaffected (that selection is the source), but a recipe replay of a `SelectedItems` capture renders against whatever happens to be selected then — the recipe stores tracks and a range, never item GUIDs, so this guard cannot close it. Filed in `docs/TODO.md`.
|
||||
- `render_isolation` (`shell/capture`) — the transient upstream silencing a ranged ITEM render needs, as a stack RAII guard alongside the two above: the selected-tracks source prints everything flowing INTO the track, so each direct folder child's `B_MAINSEND` and each of the track's receives' `B_MUTE` are cut for the render and restored on every exit path. Direct children only — a grandchild reaches the track through the child that owns it. The child-set walk is pure (`core/capture/track_topology`).
|
||||
- `capture_orchestrator` (`shell/capture`) — single-capture orchestration + the realtime/insert action bodies (Q-W3 hoist, T4-02): `renderOffline` (one offline render under the scope's FX-bypass guard), `captureAndIndexOne` (render + provenance stamp + bank add + tracking-ledger record, unpersisted), `RunCapture`/`RunCaptureItemAssign`, `RunCaptureRealtimeTrack`/`RunCancelRealtime` (the realtime action bodies — the in-flight state lives in `realtime_lifecycle`), and `RunInsertSelected` (the ONE deliberate exception to capture-never-places).
|
||||
- `bake_land` (`shell/capture`) — the EXTENSION's half of the resample chain, the SCAN PASS: scans every open project tab for pending `rsbake_*` requests, lands the ones belonging to the project this session has loaded (via `bake_landing`, below), and refuses the rest with `WrongProject` — one undo point for the batch, each answered over its own key inside the invoking instance's synchronous action call. It owns every ext-state read and write in the chain. The per-key verdict itself is NOT this TU's: it is `core/wire`'s pure `classifyBakeScan`, so this shell only enumerates, reads, and applies — counting every verdict into a `wire::BakeScanTally` as it goes, printing `wire::describeBakeKey` for EVERY enumerated key (the only thing that names which key is whose) plus `wire::describeBakeScan` whenever any key went unanswered or any answer's write was not confirmed, in one `ShowConsoleMsg`. It PROVES every write — answer or stale-clear — by reading the key back (`wire::bakeWriteLanded`), because `SetProjExtState`'s return describes the whole extname's state and cannot speak for one key; an answer that did not land is the one no-answer the tally alone cannot show. That proof is three-valued (`wire::BakeWriteProof`): a read-back that itself failed or overflowed reports Unknown, never a rejection, because "we could not check" is a different claim from "it did not land". Each key is materialized before any answer is written, so no `SetProjExtState` in this action mutates a set the enumerator is still walking. Answers are held UNENCODED until after the pass's single persist, so a landing the project would not take is answered as a failure rather than as an `Ok` no reload would honour — `wire::bakeLandingAfterPersist` is the ONE route to a `Banked` landing, and no path here (dedup included) may assign that word itself. The undo block is stack RAII (`UndoBlock`). Both loops are guarded: a throw in the scan still writes the answers already prepared, and a throw in the write-back loop still prints the lines already accumulated — no path through this action can end in a silent console. It RENDERS NOTHING — the instrument already did, through its own engine in its own process, which is what makes the baked audio the sound the user approved and what keeps the voice engine out of the extension's link graph.
|
||||
- `bake_land` (`shell/capture`) — the EXTENSION's half of the resample chain, the SCAN PASS: scans every open project tab for pending `rsbake_*` requests, lands the ones belonging to the project this session has loaded (via `bake_landing`, below), and refuses the rest with `WrongProject` — one undo point for the batch, each answered over its own key inside the invoking instance's synchronous action call. It owns every ext-state read and write in the chain. The per-key verdict itself is NOT this TU's: it is `core/wire`'s pure `classifyBakeScan`, so this shell only enumerates, reads, and applies — counting every verdict into a `wire::BakeScanTally` as it goes, printing `wire::describeBakeKey` for EVERY enumerated key (the only thing that names which key is whose) plus `wire::describeBakeScan` whenever any key went unanswered or any answer's write was not confirmed, in one `ShowConsoleMsg`. It PROVES every write — answer or stale-clear — by reading the key back (`wire::extStateWriteLanded`, whose home is `core/wire/ext_state_read.h`); an answer that did not land is the one no-answer the tally alone cannot show. That proof is three-valued (`wire::BakeWriteProof`): a read-back that overflowed, or a throw AFTER the `SetProjExtState` call, reports Unknown; a throw BEFORE it reports Rejected, because the write is then known not to have been made. Each key is materialized before any answer is written, so no `SetProjExtState` in this action mutates a set the enumerator is still walking. Answers are held UNENCODED until after the pass's single persist, so a landing the project would not take is answered as a failure rather than as an `Ok` no reload would honour — `wire::bakeLandingAfterPersist` is the ONE route to a `Banked` landing, and no path here (dedup included) may assign that word itself. The undo block is stack RAII (`UndoBlock`). Both loops are guarded: a throw in the scan still writes the answers already prepared, and a throw in the write-back loop still prints the lines already accumulated — no path through this action can end in a silent console. It RENDERS NOTHING — the instrument already did, through its own engine in its own process, which is what makes the baked audio the sound the user approved and what keeps the voice engine out of the extension's link graph.
|
||||
- `bake_landing` (`shell/capture`) — landing ONE bake request, split off `bake_land` on the one-request / whole-pass seam; touches no REAPER API at all. Non-mutating `prepareLanding` and mutating `commitLanding` sit under separate catches in `attemptLanding` — a throw before anything was written is a clean refusal, a throw after it is reported as possibly partial. Replace-vs-add comes from `tracking::resampleLanding`; a replace keeps the entry's id and slot and never deletes the superseded file. Hash-dedup applies on the add path only, before the disk write, matching `updateSampleInPlace`'s "an in-place refresh is not an insert" — and a dedup hit still rides the pass's persist, because the entry it points at may be one the same pass just added. A refused index withdraws the bytes this call had just written — the self-cleanup carve-out from prune's deletion authority, stated in `prune_fs.cpp`'s header. It never persists: the pass does that once for its whole batch, which is why no landing may report itself as banked.
|
||||
- `capture_batch` (`shell/capture`) — the batch-capture family + re-capture-from-source (Q-W3 hoist, T4-02): `RunBatchCaptureItems` (one sample per selected item), `RunBatchCaptureRazor` (one sample per razor area), `RunRecaptureFromSource` (regenerate a provenanced sample from its recorded source's current state, bank-only). Every unit routes through `capture_orchestrator` so every precision invariant holds; persist is batched to one ext-state write per action.
|
||||
- `realtime_lifecycle` (`shell/capture`) — the in-flight realtime-capture state machine + globals (Q-W3 hoist): the action starts it, `OnTimer` drives it per tick via `DriveRealtimeCapture` (a single-pointer-test idle fast path — load-bearing hot-path guardrail), `CommitRealtimeResult` lands a finished capture in the bank, `AbortRealtimeCaptureForUnload` tears down cleanly on extension unload.
|
||||
@@ -90,3 +90,11 @@ detail not covered there:
|
||||
- **Realtime is the one capture path that accepts a multi-track selection**, and it is
|
||||
correct to: its per-source-track sends sum in the one temp track, which is a real mix
|
||||
rather than a stem collapse. The offline refusal above does not apply to it.
|
||||
- **`bake_land`'s panel refresh and its generation bump can disagree after a throw.**
|
||||
The refresh rides `bookChanged` outside the guarded scan, while the bump sits inside
|
||||
it — so a pass that landed an entry and then threw before reaching the persist block
|
||||
repaints the docked panel from the in-memory book without having bumped the bank
|
||||
generation, and other open instances stay on the old generation until the next bump.
|
||||
Accepted: the panel showing what the pass actually did is the more useful of the two,
|
||||
and the next bank mutation reconciles it. Do not "fix" it by moving the refresh inside
|
||||
the try — that would trade a stale generation for a stale panel.
|
||||
|
||||
@@ -130,9 +130,11 @@ private:
|
||||
// One entry's write-back, and the proof of whether it took. Guarded whole: it encodes into
|
||||
// a fresh string and readKey grows a buffer toward 16 MB, and a throw escaping here would
|
||||
// skip the ShowConsoleMsg for EVERY key — the silent console this action exists to make
|
||||
// impossible. A throw is `Unknown` rather than a failure because the write may well have
|
||||
// landed before the check did.
|
||||
// impossible. Which proof a throw yields turns on whether SetProjExtState had run: before
|
||||
// it, the write is KNOWN not to have been made (`Rejected`); after it, the write may well
|
||||
// have landed and only the check failed (`Unknown`).
|
||||
wire::BakeWriteProof writeBackOne(ScannedKey& entry, bool persisted) {
|
||||
bool issued = false;
|
||||
try {
|
||||
std::string value; // empty = clear the key
|
||||
if (entry.write == ScannedKey::Write::Answer) {
|
||||
@@ -146,7 +148,8 @@ wire::BakeWriteProof writeBackOne(ScannedKey& entry, bool persisted) {
|
||||
entry.outcome = refuseBake(
|
||||
BakeStatus::Failed,
|
||||
"the bake reached the bank in memory, but this pass's persist did not "
|
||||
"report success, so this answer cannot promise a reload will find it",
|
||||
"confirm its bank write, so this answer cannot promise a reload will "
|
||||
"find it",
|
||||
entry.outcome.generation);
|
||||
// `detail` left empty on purpose: describeLanding's Unpersisted clause is
|
||||
// that sentence's one home on the console side, and the message above is
|
||||
@@ -158,16 +161,16 @@ wire::BakeWriteProof writeBackOne(ScannedKey& entry, bool persisted) {
|
||||
value = wire::encodeBakeOutcome(entry.outcome);
|
||||
}
|
||||
SetProjExtState(entry.proj, kProjExtNamespace(), entry.key.c_str(), value.c_str());
|
||||
// SetProjExtState's own return is the size of the whole extname's state, which
|
||||
// `banks` alone keeps non-zero in every case a bake can reach, so only re-reading
|
||||
// THIS key can speak for it. An oversized read proves nothing either way — folding
|
||||
// it into bakeWriteLanded's absent case would report a clear off unread bytes.
|
||||
issued = true;
|
||||
// Only re-reading THIS key can speak for it (wire::extStateWriteLanded owns why).
|
||||
// An oversized read proves nothing either way — folding it into that predicate's
|
||||
// absent case would report a clear off unread bytes.
|
||||
const KeyRead back = readKey(entry.proj, entry.key);
|
||||
if (back.oversized) return wire::BakeWriteProof::Unknown;
|
||||
return wire::bakeWriteLanded(value, back.value) ? wire::BakeWriteProof::Confirmed
|
||||
: wire::BakeWriteProof::Rejected;
|
||||
return wire::extStateWriteLanded(value, back.value) ? wire::BakeWriteProof::Confirmed
|
||||
: wire::BakeWriteProof::Rejected;
|
||||
} catch (...) {
|
||||
return wire::BakeWriteProof::Unknown;
|
||||
return issued ? wire::BakeWriteProof::Unknown : wire::BakeWriteProof::Rejected;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,8 +196,12 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
std::vector<ScannedKey> scanned;
|
||||
wire::BakeScanTally tally; // every verdict below is counted, skips included
|
||||
std::string aborted; // set only when the scan itself threw
|
||||
bool persisted = false; // the pass's ONE persist reported success
|
||||
bool persisted = false; // the pass's ONE persist confirmed its bank write
|
||||
bool bookChanged = false; // some landing added or refreshed an entry
|
||||
// A local, not a tally field: an entry counted here can still be re-answered as a
|
||||
// failure by the write-back loop below, so this is a persist GATE and never a count
|
||||
// anything reports.
|
||||
bool anyLanded = false;
|
||||
// The scan allocates outside attemptLanding's own guards too (readKey grows a buffer
|
||||
// toward 16 MB, every container here allocates), and a throw that escaped would discard
|
||||
// the answers already buffered — the exact no-answer-with-a-silent-console this action
|
||||
@@ -253,7 +260,7 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
request->generation);
|
||||
} else {
|
||||
LandingAttempt attempt = attemptLanding(session, open.dir, *request);
|
||||
if (attempt.outcome.status == BakeStatus::Ok) ++tally.landed;
|
||||
if (attempt.outcome.status == BakeStatus::Ok) anyLanded = true;
|
||||
if (attempt.changedBook) bookChanged = true;
|
||||
// Never Banked here, on ANY path: the pass's persist has not run yet,
|
||||
// and bakeLandingAfterPersist is the one place that word is earned. A
|
||||
@@ -273,7 +280,7 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
}
|
||||
}
|
||||
|
||||
if (tally.landed > 0) {
|
||||
if (anyLanded) {
|
||||
// A dedup hit rides the persist too, because that is the only observation that
|
||||
// can promote its landing — but it added nothing, so it earns neither the
|
||||
// refresh every open instance pays for nor an undo point named after a bake
|
||||
|
||||
@@ -103,7 +103,7 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
|
||||
## Modules
|
||||
|
||||
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` with `getReaperParent(3)`, the instance's OWN project tab, as `proj` — a request, not a DAW-verified guarantee; see the header) and `projectTempoBpm`.
|
||||
- `reaper_bridge` — READ-ONLY bank consumer: receives bank snapshots from the extension and exposes them as a read-only view. **Never writes to the extension's bank** — this is a load-bearing invariant; no mutation path exists in this module. It owns TWO prefix-guarded ext-state write entry points, `writeUsageExtState` (`rsusage_`) and `writeBakeExtState` (`rsbake_`), each refusing every other key; neither weakens the read-only-*bank* invariant, because neither payload is bank state and `banks`/`view`/`tail`/`assign` stay structurally unwritable. Both PROVE the write by reading the key back (`wire::extStateWriteLanded`) — `SetProjExtState`'s own return cannot speak for one key, so testing it was a guard that could never fire, and the bake's "could not publish" refusal was consequently unreachable. It also owns the bake crossing — `extensionActionAvailable` / `invokeExtensionAction` (`NamedCommandLookup` + `Main_OnCommandEx` with `getReaperParent(3)`, the instance's OWN project tab, as `proj` — a request, not a DAW-verified guarantee; see the header) and `projectTempoBpm`.
|
||||
- `reasampler_processor` (`shell/instrument/`: `reasampler_processor.cpp` lifecycle + `process()`, `processor_state.cpp` component-state I/O + UI-thread parameter accessors, `processor_reload.cpp` the off-audio-thread `reloadInstrument`/publish family — Q-W2v, T4-12 split; `process()` and its per-block work stay ONE TU on purpose, no cross-TU call on the per-sample path) — VST3 `SingleComponentEffect` shell: declares event-input bus + **permanently stereo** output (GA fix: dynamic mono↔stereo bus renegotiation deleted; `ChannelMode` is now decode-only), marshals MIDI note-on/off into the VoiceEngine, renders audio; owns off-audio-thread `reloadInstrument` + atomic pointer swap so `process()` does no allocation, no file I/O, no bridge calls. The instance state is `{loaded capture id, one InstrumentParams}`, and `reloadInstrument` resolves + decodes exactly that one capture into the `SampleData` the engine plays. **Self-contained playback (pS):** `ComponentState` v10 adds a `SampleRefs` table — per referenced sample, a project-relative path + decode intrinsics (root, loop, channels, displayName); `reloadInstrument` decodes directly from `SampleRefs`, bank-free (plays with the extension absent). The bank/bridge is a browser source: loading a capture copies its reference in; the reopen-heal timer + poll-to-play apparatus are removed. `retireIdleDrain()` retires fully-idle drain snapshots on the UI-timer cadence. Voice-param edits (`setVoiceCount`/`setVoiceMode`/`setMonoTrigger`) rebuild the engine from the already-decoded `SampleData` via the drain-slot swap — no bank re-read, no WAV re-decode, no audible cut to ringing tails. **FB1:** applies the post-mixer `masterGainLinear` (from `ComponentState` v8) as a per-sample ramp over the summed output — no zipper noise. **GA v9:** `channelModeExplicit_` flag persisted; `channelModeFor()` auto-defaults the mode from the loaded capture's channel count when the flag is not set. **pS:** `ComponentState` bumped v9→v10 (`SampleRefs` table); pre-v10 blobs lift to empty refs and re-save self-contained. **pS-usage:** publishes instance usage (held `SampleRefs` paths) to `rsusage_<instanceGuid>` at the tail of `reloadInstrument` (off audio thread) via `reaper_bridge::writeUsageExtState`; `ComponentState` bumped v10→**v11** (`instanceGuid` field); pre-v11 blobs mint guid on first publish.
|
||||
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (the ONE `faceLayout` band resolve every paint and hit-test path shares, the node-drag bounds, the value labels, and the per-instance controls the parameter set does not carry — the parameter-set binding itself is the pure `core/instrument/ui/deck_values` module this only adapts int ids onto), `editor_models` (the orthogonal half: which stored struct each transient editor selection names — the staged-envelope pack/unpack, the drawn contour, and the three velocity curves), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
|
||||
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
|
||||
|
||||
@@ -186,8 +186,13 @@ BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
|
||||
const std::string key = bakeKeyFor(instanceGuid);
|
||||
RequestKeyGuard keyGuard(bridge, key);
|
||||
// The bridge PROVES this by reading the key back, so a false here is real: the
|
||||
// extension would find nothing to land. It does not say which of the three ways failed,
|
||||
// so neither does this sentence.
|
||||
if (!bridge.writeBakeExtState(key, wire::encodeBakeRequest(request)))
|
||||
return fail("could not publish the bake request");
|
||||
return fail("the bake request under " + key +
|
||||
" could not be confirmed -- it was either never written or did not "
|
||||
"read back as written, so the extension has nothing to land");
|
||||
|
||||
// Synchronous: the extension's landing runs to completion inside this call and writes
|
||||
// its outcome back over the same key before returning.
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
#include "shell/instrument/reaper_bridge.h"
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/instrument/map/bridge_marshal.h"
|
||||
@@ -112,7 +114,9 @@ std::optional<std::string> ReaperBridge::readReasamplerExtState(const std::strin
|
||||
|
||||
bool ReaperBridge::writeGuarded(const std::string& key, const std::string& value,
|
||||
const char* requiredPrefix) {
|
||||
if (!setProjExtState_ || !hostApp_) return false;
|
||||
// getProjExtState_ is required too: without it the write could not be proven, and an
|
||||
// unprovable write may not be reported as a landed one.
|
||||
if (!setProjExtState_ || !getProjExtState_ || !hostApp_) return false;
|
||||
// Read-only-BANK guard: this module writes the two sanctioned per-instance prefixes
|
||||
// and nothing else. Any other key is refused rather than widening the instrument's
|
||||
// write surface (banks/view/tail/assign stay extension-owned).
|
||||
@@ -121,14 +125,20 @@ bool ReaperBridge::writeGuarded(const std::string& key, const std::string& value
|
||||
|
||||
auto* reaper = static_cast<Steinberg::IReaperHostApplication*>(hostApp_);
|
||||
void* proj = reaper->getReaperParent(3); // null = current project (same as reads)
|
||||
// SetProjExtState returns the size of the extname's state — after storing a
|
||||
// non-empty value that's necessarily > 0, so <= 0 means the write did not land (the
|
||||
// publish path retries next reload tick; a silent drop would leave holds unprotected).
|
||||
// A deliberate CLEAR (empty value) shrinks the state and can legitimately return 0,
|
||||
// so it is reported as landed.
|
||||
const int rv =
|
||||
setProjExtState_(proj, kProjExtNamespace(), key.c_str(), value.c_str());
|
||||
return value.empty() ? true : rv > 0;
|
||||
// The return is DELIBERATELY discarded: it describes the whole extname's state, not
|
||||
// this key, so testing it was a guard that could not fire for the bake publish
|
||||
// (wire::extStateWriteLanded owns the reasoning). The read-back below is the proof.
|
||||
setProjExtState_(proj, kProjExtNamespace(), key.c_str(), value.c_str());
|
||||
|
||||
const auto back = wire::readProjExtStateGrowing([&](char* buf, int cap) {
|
||||
return getProjExtState_(proj, kProjExtNamespace(), key.c_str(), buf, cap);
|
||||
});
|
||||
if (back.status == wire::GrowingExtStateRead::Status::Overflow)
|
||||
return false; // could not check -> report unconfirmed, never a claimed success
|
||||
return wire::extStateWriteLanded(
|
||||
value, back.status == wire::GrowingExtStateRead::Status::Complete
|
||||
? std::optional<std::string>(back.value)
|
||||
: std::nullopt);
|
||||
}
|
||||
|
||||
bool ReaperBridge::writeUsageExtState(const std::string& usageKey,
|
||||
|
||||
@@ -47,10 +47,16 @@ public:
|
||||
// The instrument's TWO sanctioned ext-state write surfaces, each accepting exactly one
|
||||
// key prefix and refusing every other key. That structural refusal is what keeps the
|
||||
// read-only-BANK invariant intact — banks/view/tail/assign stay unwritable from here —
|
||||
// and neither payload is bank state. Both return true iff the write landed (the
|
||||
// SetProjExtState return is checked) and neither is RT-safe: the call sites are the
|
||||
// and neither payload is bank state. Neither is RT-safe: the call sites are the
|
||||
// off-audio-thread reload path and the editor's UI tick.
|
||||
//
|
||||
// Both return true iff the key READ BACK as exactly the value written (an empty value
|
||||
// is a clear, which lands as an absent-or-empty key). SetProjExtState's own return
|
||||
// cannot answer that — wire::extStateWriteLanded owns why, and testing it here was a
|
||||
// guard that could never fire. A `false` does not distinguish a write that was never
|
||||
// issued (unconnected host, refused prefix) from one that did not take or could not be
|
||||
// checked; a caller must not name one of the three.
|
||||
//
|
||||
// Neither marks the project dirty. A usage change always rides a component-state change
|
||||
// that already does; a bake request is transient and is cleared in the same tick.
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ REAPER/filesystem-facing half only, and it gathers rather than decides.
|
||||
|
||||
## Modules
|
||||
|
||||
- `shell/persist` (`session` / `ext_state_io` / `prune_fs`) — the persist seam, split by responsibility (Q-W5; the former `persist.cpp` god-TU and its `persist.h` compatibility umbrella are both retired — callers include `shell/persist/session.h` / `ext_state_io.h` directly). `session` owns the `ReaSamplerSession` lifecycle: the poll identity-transition detection (load / Save-As / forked sibling / recycled pointer) and the `projectconfig`-driven deferred undo/redo reload. `ext_state_io` owns project ext state (`SetProjExtState`/`GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON, `ViewModeModel` JSON, `TailSetting` JSON, the tracking ledger JSON, the writing-version stamp, GUID minting, and bank-folder relocation. `session` additionally owns `recordCreated` — **the one writer of a birth record**, called at the same point the `Sample` is added, deriving lineage from that `Sample`'s own provenance. `prune_fs` hosts the prune dry-run / full-set orphan queries (gathering `referencedPaths()` plus `tracking::pruneProtection`'s two inputs for the `prune_reconcile` pure core) — and, beside them, `tiedUsageFor`, the resample's replace-vs-add input, deliberately co-located so "both answers come out of one `TrackingState`" is structural rather than a rule two files must remember. It is also **the single file-deletion authority over user files in the bank folder** (`deleteOrphanFile` via `SHFileOperationW`); nothing else in the system deletes bank-folder bytes. Dry-run / orphan-set / reclaim each independently abort (delete nothing) when the authority reports a block.
|
||||
- `shell/persist` (`session` / `ext_state_io` / `prune_fs`) — the persist seam, split by responsibility (Q-W5; the former `persist.cpp` god-TU and its `persist.h` compatibility umbrella are both retired — callers include `shell/persist/session.h` / `ext_state_io.h` directly). `session` owns the `ReaSamplerSession` lifecycle: the poll identity-transition detection (load / Save-As / forked sibling / recycled pointer) and the `projectconfig`-driven deferred undo/redo reload. `ext_state_io` owns project ext state (`SetProjExtState`/`GetProjExtState`, namespace `"reasampler"`) ↔ `BankBook` JSON, `ViewModeModel` JSON, `TailSetting` JSON, the tracking ledger JSON, the writing-version stamp, GUID minting, and bank-folder relocation. `saveToActiveProject` PROVES its `banks` write by reading that key back (`wire::extStateWriteLanded`) and returns that verdict — the sibling keys are written but not verified, so its `true` means "the bank state is in the project", never "everything persisted". `session` additionally owns `recordCreated` — **the one writer of a birth record**, called at the same point the `Sample` is added, deriving lineage from that `Sample`'s own provenance. `prune_fs` hosts the prune dry-run / full-set orphan queries (gathering `referencedPaths()` plus `tracking::pruneProtection`'s two inputs for the `prune_reconcile` pure core) — and, beside them, `tiedUsageFor`, the resample's replace-vs-add input, deliberately co-located so "both answers come out of one `TrackingState`" is structural rather than a rule two files must remember. It is also **the single file-deletion authority over user files in the bank folder** (`deleteOrphanFile` via `SHFileOperationW`); nothing else in the system deletes bank-folder bytes. Dry-run / orphan-set / reclaim each independently abort (delete nothing) when the authority reports a block.
|
||||
- `usage_scan` — extension-side prune-scan shell: enumerates every `rsusage_*` ext-state key, decodes each `sample_usage` wire record, enumerates every ReaSampler 9000 FX instance across all tracks + master / normal + record chains / containers (recursive) / take FX, and returns the pure `sample_usage::foldUsageRecords` result verbatim. One of the two inputs `tracking::pruneProtection` reads; it decides nothing itself. Read-only: writes no ext-state.
|
||||
- `persist_internal.h` — internal-only shared helpers for the persist TU family (`session` / `ext_state_io` / `prune_fs`); included only by those three TUs, never a public seam (mirror of the panel's `panel_state.h` / the editor's `editor_internal.h` precedent). Holds the former anonymous-namespace helpers more than one split TU needs (active-project + `.rpp` path lookup, project-dir derivation, growing `GetProjExtState` read, project-GUID minting, bank-folder relocation) — all definitions live in `ext_state_io.cpp`. REAPER-free header: the project handle crosses this seam as the same opaque `void*` the public `session` header already uses.
|
||||
|
||||
|
||||
@@ -182,7 +182,24 @@ bool ReaSamplerSession::saveToActiveProject() {
|
||||
instrument::map::formatBankGeneration(bankGeneration_).c_str());
|
||||
|
||||
MarkProjectDirty(static_cast<ReaProject*>(proj));
|
||||
return true;
|
||||
|
||||
// Prove the ONE key a caller's answer hinges on. Each SetProjExtState above returns the
|
||||
// size of the whole extname's state, which the six writes here keep non-zero between
|
||||
// them, so no single one of those returns can speak for `banks`
|
||||
// (wire::extStateWriteLanded owns the reasoning). The sibling keys stay unobserved and
|
||||
// no caller claims otherwise; `banks` is the one whose absence would make a landed
|
||||
// capture vanish on reload.
|
||||
const wire::GrowingExtStateRead back = wire::readProjExtStateGrowing(
|
||||
[&](char* buf, int cap) {
|
||||
return GetProjExtState(static_cast<ReaProject*>(proj), projExtNamespace(),
|
||||
kProjExtBanksKey, buf, cap);
|
||||
});
|
||||
if (back.status == wire::GrowingExtStateRead::Status::Overflow)
|
||||
return false; // could not check -> do not claim; never folded in as an absence
|
||||
return wire::extStateWriteLanded(
|
||||
banksJson, back.status == wire::GrowingExtStateRead::Status::Complete
|
||||
? std::optional<std::string>(back.value)
|
||||
: std::nullopt);
|
||||
}
|
||||
|
||||
bool ReaSamplerSession::writeAssignmentRequest(const std::string& wire) {
|
||||
|
||||
@@ -108,8 +108,16 @@ public:
|
||||
void bumpBankGeneration() { ++bankGeneration_; }
|
||||
|
||||
// Serializes book/view/tail to ext state, clears the retired legacy
|
||||
// `bank_index` key. No-ops with no active/saved project. Returns true iff
|
||||
// a persist happened, so a caller can skip an undo block when nothing was written.
|
||||
// `bank_index` key. No-ops with no active/saved project.
|
||||
//
|
||||
// Returns true iff the `banks` key READ BACK as exactly what this call wrote —
|
||||
// the only per-key observation available under a shared extname
|
||||
// (wire::extStateWriteLanded owns why SetProjExtState's own return cannot
|
||||
// answer it). A false therefore covers four things without distinguishing
|
||||
// them: no active project, an unsaved project, a rejected write, and a
|
||||
// read-back that could not complete. The sibling keys (view/tail/ledger/
|
||||
// version/generation) are written but NOT verified, so no caller may read
|
||||
// this as "everything persisted" — only as "the bank state is in the project".
|
||||
bool saveToActiveProject();
|
||||
|
||||
// Report-only prune dry-run: feeds the pure core with (present, referenced,
|
||||
|
||||
Reference in New Issue
Block a user