Make a bake's Banked landing reachable only through the persist upgrade
The dedup path claimed it directly, so a key could be answered Ok pointing at an entry a failed persist never wrote. Write-back is now guarded and three-valued, and landing one request split out of the scan pass.
This commit is contained in:
@@ -80,7 +80,7 @@ This directory owns two cross-artifact contracts specifically:
|
||||
- `wire` (`core/wire`) — the ONE length-prefixed ext-state wire codec (Q-W1): `putField`/`parseUnsignedDecimal` + the bounds-checked `Cursor` (`field`/`fieldInt`/`fieldInt64`/`fieldSizeT`/`fieldDouble`), replacing four near-identical copies (`provenance` / `assignment_request` / `sample_usage` / `bank_sync`). `core/wire/bytes.h` is the sibling little-endian byte codec (`putLE`, `ByteReader`, `doubleToBits`/`bitsToDouble`) that `component_state_io` is the biggest consumer of. `core/wire/ext_state_read.h` owns the `GetProjExtState` grow-loop retry policy (Absent/Complete/Overflow) shared by `persist`, `usage_scan`, and `reaper_bridge`. `core/wire/reasampler_uid.h` (the FOREVER-FROZEN VST3 class-UID macros) also lives in this directory.
|
||||
- `reasampler_uid.h` — SDK-free header owning the FOREVER-FROZEN VST3 class-UID integer macros (stable + beta pairs, `REASAMPLER_PROC_UID_*` / `REASAMPLER_PROC_UID_BETA_*`) and the `REASAMPLER_ACTIVE_UID_*` channel-selector macros. Split out of `reasampler_vst.h` so the pure extension side (`instrument_drop`) can derive the `.vstpreset` class-ID hex string without pulling in the VST3 SDK. Both `reasampler_vst.h` (runtime `FUID`) and `instrument_drop` (preset hex string) source from this single header — the binary identity and the preset-file identity cannot diverge.
|
||||
- `assignment_request` — pure ingest-assign wire: typed request record carrying the drop payload from the `ingest` shell through to the VST3 bridge.
|
||||
- `bake_wire` — the resample bake's request/outcome pair on ONE per-instance key (`rsbake_<guid>`): the instrument writes a `BakeRequest`, invokes the extension's action synchronously, and reads the extension's `BakeOutcome` back over the same key inside that one call. Not a handshake — a call and a return, and it must not grow a claim protocol. Also the ONE home of the bake action's command-id suffix and of the leading underscore `NamedCommandLookup` needs but `rec->Register("command_id", …)` does not, so both artifacts name one action. `BakeStatus` values are WIRE INTEGERS: never renumber, only append, and an unrecognized value decodes as `Failed` rather than as the numeric default `Ok`. It owns BOTH ends' reading of that key, since the key's contents are the only evidence either side gets: `classifyBakeAnswer` (instrument side — six kinds, of which `Unanswered`, the request still sitting there untouched, separates "nothing wrote an outcome over our key" from a refusal — it does NOT identify a landing that never ran, since a skipped key and a rejected answer-write look identical from here) and `classifyBakeScan` + `kMaxRequestAgeSeconds` (extension side — the per-key Land / RefuseWrongProject / ClearStale / IgnoreUnreadable / IgnoreNotARequest verdict over every open tab, stated without a REAPER type so the multi-tab matrix is unit-provable). `BakeScanTally` + `describeBakeScan` + `describeBakeKey` are that same reading counted and spoken — the rationale lives at the types. **The report's absence is NOT evidence the landing never ran**, and no sentence either artifact prints may say it is: `answered` is pass-wide and counts a QUEUED write, so a pass can answer some other key while skipping ours, or have our own answer's `SetProjExtState` rejected. The summary is therefore silent only when the pass answered somebody, left no key unanswered, and every answer was READ BACK from its own key; `describeBakeKey` prints one line per enumerated key regardless, which is the only thing that names WHICH key — the counts cannot. `bakeWriteLanded` is that read-back's verdict and the reason it has to exist: `SetProjExtState` returns the size of the whole extname's state, which `banks` alone keeps non-zero in every case a bake can reach, so nothing but re-reading the key can say whether THAT key took the value.
|
||||
- `bake_wire` — the resample bake's request/outcome pair on ONE per-instance key (`rsbake_<guid>`): the instrument writes a `BakeRequest`, invokes the extension's action synchronously, and reads the extension's `BakeOutcome` back over the same key inside that one call. Not a handshake — a call and a return, and it must not grow a claim protocol. Also the ONE home of the bake action's command-id suffix and of the leading underscore `NamedCommandLookup` needs but `rec->Register("command_id", …)` does not, so both artifacts name one action. `BakeStatus` values are WIRE INTEGERS: never renumber, only append, and an unrecognized value decodes as `Failed` rather than as the numeric default `Ok`. It owns BOTH ends' reading of that key, since the key's contents are the only evidence either side gets: `classifyBakeAnswer` (instrument side — six kinds, of which `Unanswered`, the request still sitting there untouched, separates "nothing wrote an outcome over our key" from a refusal — it does NOT identify a landing that never ran, since a skipped key and a rejected answer-write look identical from here) and `classifyBakeScan` + `kMaxRequestAgeSeconds` (extension side — the per-key Land / RefuseWrongProject / ClearStale / IgnoreUnreadable / IgnoreNotARequest verdict over every open tab, stated without a REAPER type so the multi-tab matrix is unit-provable). `BakeScanTally` + `describeBakeScan` + `describeBakeKey` are that same reading counted and spoken — the rationale lives at the types. **The report's absence is NOT evidence the landing never ran**, and no sentence either artifact prints may say it is: `answered` is pass-wide and counts a QUEUED write, so a pass can answer some other key while skipping ours, or have our own answer's `SetProjExtState` rejected. The summary is therefore silent only when the pass answered somebody, left no key unanswered, and every answer was READ BACK from its own key; `describeBakeKey` prints one line per enumerated key regardless, which is the only thing that names WHICH key — the counts cannot. `bakeWriteLanded` is that read-back's verdict and the reason it has to exist: `SetProjExtState` returns the size of the whole extname's state, which `banks` alone keeps non-zero in every case a bake can reach, so nothing but re-reading the key can say whether THAT key took the value — and its `nullopt` means ABSENT specifically, so a caller must route a failed or overflowed read to `BakeWriteProof::Unknown` rather than folding it in as an absence. `bakeLandingAfterPersist` is the ONE route to a `Banked` landing: every `Land` verdict is assigned `Unpersisted` and passed through it, so no shell path — a dedup hit least of all, since its target may be an entry the same pass just added — can claim the word without the pass's persist having reported success.
|
||||
- `instrument_drop` — pure FX-drop payload builder: constructs a Steinberg-format `.vstpreset` image (channel-active class ID + the instrument's own component state, capture pre-selected) the shell applies via `TrackFX_SetPreset`; owns `classifyReaperSurface`, the prefix classifier mapping a `GetThingFromPoint` (info token, track-present) pair onto `core/ui/drag_out`'s `ReaperSurface`. Classifier ordering is load-bearing: the embed strip is matched before the `tcp`/`mcp` panel family, which now claims the WHOLE track panel rather than just its FX sub-elements. All-or-nothing contract — caller rolls back via `TrackFX_Delete` on any failure.
|
||||
- `sample_usage` — instance-usage wire: `UsageRecord`, `planUsagePublish` (fresh/heal/clean-replace/union/remint publish plan), `foldUsageRecords`/`usageHeldPaths` (liveness fold — protect-all when records exist but no instance is live; abort→protect-all on unreadable record; `counted` carries key-attributed live records), `identityMatches` (ReaSampler 9000 FX identity). REAPER-free, unit-tested. The mirror of `assignment_request` on the instrument→extension direction: the wire format and the two safety-critical decisions (what to write on publish, which records count at prune time) are pure so they are provable without a DAW. It lives here because it is a *wire format* with an instrument-side writer; the fold's output is consumed by `core/tracking`'s authority, which owns every consumer-facing decision built on it.
|
||||
|
||||
|
||||
+60
-19
@@ -192,7 +192,8 @@ std::string describeBakeScan(const BakeScanTally& t) {
|
||||
// had the write rejected, would otherwise print nothing at all and leave the asking
|
||||
// instance with a no-answer and a silent console.
|
||||
const int unanswered = t.unreadable + t.notARequest + t.staleCleared;
|
||||
if (t.answered > 0 && unanswered == 0 && t.writeFailed == 0) return {};
|
||||
if (t.answered > 0 && unanswered == 0 && t.writeFailed == 0 && t.writeUnproven == 0)
|
||||
return {};
|
||||
|
||||
std::string s = "ReaSampler resample: the landing action ran and scanned " +
|
||||
countOf(t.tabsScanned, "project tab") + ". ";
|
||||
@@ -225,27 +226,61 @@ std::string describeBakeScan(const BakeScanTally& t) {
|
||||
s += " " + countOf(t.writeFailed, "answer") +
|
||||
" could not be written back into the project, so whoever asked for them sees "
|
||||
"no answer at all.";
|
||||
if (t.writeUnproven > 0)
|
||||
s += " " + countOf(t.writeUnproven, "answer") +
|
||||
" could not be checked after writing, so whether they reached the project is "
|
||||
"unknown.";
|
||||
return s + "\n";
|
||||
}
|
||||
|
||||
BakeLanding bakeLandingAfterPersist(BakeLanding landing, bool persisted) {
|
||||
if (landing == BakeLanding::Unpersisted && persisted) return BakeLanding::Banked;
|
||||
return landing;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The Land verdict's own clause. Separate function so its own fail-closed default cannot
|
||||
// be swallowed by the caller's switch — an unnamed enumerator returns empty either way.
|
||||
// The Land verdict's own clause, with its own fail-closed default — an unnamed enumerator
|
||||
// must not fall out empty and get reported by the caller as an unnamed VERDICT, which is a
|
||||
// different gap in a different enum.
|
||||
std::string describeLanding(BakeLanding landing) {
|
||||
switch (landing) {
|
||||
case BakeLanding::Banked:
|
||||
return "landed into the bank";
|
||||
// The flag is ALL that was observed, so the sentence claims no more than it:
|
||||
// saveToActiveProject issues its SetProjExtState calls without reading their
|
||||
// returns. The trailing clause is SetProjExtState's own documented semantics
|
||||
// (reaper_plugin_functions.h — a value is restored "the next time this specific
|
||||
// project is loaded"), not a claim about the file's current contents.
|
||||
return "landed into the bank, and this pass's persist reported success -- the "
|
||||
".rpp itself carries it once the project is saved";
|
||||
case BakeLanding::Unpersisted:
|
||||
return "landed into the bank IN MEMORY ONLY -- this pass could not persist it, "
|
||||
"so the project's saved bank state does not carry it";
|
||||
// Says nothing about what the project holds: the persist can fail before
|
||||
// writing anything OR throw part-way through, and a dedup hit's target may
|
||||
// have been in the project since long before this pass.
|
||||
return "landed into the bank IN MEMORY ONLY -- this pass's persist did not "
|
||||
"report success, so nothing here can say the project will carry it";
|
||||
case BakeLanding::Partial:
|
||||
return "the landing failed after it had begun writing -- it may have left a "
|
||||
"file in the bank folder and an entry in memory";
|
||||
case BakeLanding::Refused:
|
||||
return "the landing was refused";
|
||||
}
|
||||
return {};
|
||||
return "a landing state this build has no word for";
|
||||
}
|
||||
|
||||
// The write-back's own clause, tail of every line that made one.
|
||||
std::string describeProof(BakeWriteProof proof) {
|
||||
switch (proof) {
|
||||
case BakeWriteProof::Confirmed:
|
||||
return ". The answer was written back.\n";
|
||||
case BakeWriteProof::Rejected:
|
||||
return ". The answer could NOT be written back -- the instance that asked will "
|
||||
"report no answer.\n";
|
||||
case BakeWriteProof::Unknown:
|
||||
break;
|
||||
}
|
||||
return ". Whether the answer reached the key is unknown -- the write-and-check itself "
|
||||
"failed.\n";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -263,15 +298,24 @@ std::string describeBakeKey(const std::string& key, const BakeKeyOutcome& outcom
|
||||
return head +
|
||||
"skipped: it holds something other than a pending request -- an answer "
|
||||
"nobody has collected, or a wire this build does not read.\n";
|
||||
case BakeScanVerdict::ClearStale:
|
||||
case BakeScanVerdict::ClearStale: {
|
||||
// The clear is a write like any other, so it is claimed only where it was read
|
||||
// back: an unconfirmed one leaves the request standing for the next pass.
|
||||
return head + (outcome.writeConfirmed
|
||||
? "past the age bound with no reader left, so it was cleared "
|
||||
"unanswered.\n"
|
||||
: "past the age bound with no reader left, but the clear "
|
||||
"could NOT be read back -- the key still holds it and the "
|
||||
"next pass will see it again.\n");
|
||||
// back: an unconfirmed one leaves a value standing for the next pass.
|
||||
const std::string aged = head + "past the age bound with no reader left";
|
||||
switch (outcome.proof) {
|
||||
case BakeWriteProof::Confirmed:
|
||||
return aged + ", so it was cleared unanswered.\n";
|
||||
case BakeWriteProof::Rejected:
|
||||
return aged +
|
||||
", but the clear could NOT be read back -- the key still holds a "
|
||||
"value and the next pass will see it again.\n";
|
||||
case BakeWriteProof::Unknown:
|
||||
break;
|
||||
}
|
||||
return aged +
|
||||
"; whether the clear took is unknown -- the write-and-check itself "
|
||||
"failed.\n";
|
||||
}
|
||||
case BakeScanVerdict::RefuseWrongProject:
|
||||
clause = "refused";
|
||||
break;
|
||||
@@ -285,10 +329,7 @@ std::string describeBakeKey(const std::string& key, const BakeKeyOutcome& outcom
|
||||
if (clause.empty()) return head + "a verdict this build has no word for.\n";
|
||||
std::string s = head + clause;
|
||||
if (!outcome.detail.empty()) s += " (" + outcome.detail + ")";
|
||||
return s + (outcome.writeConfirmed
|
||||
? ". The answer was written back.\n"
|
||||
: ". The answer could NOT be written back -- the instance that asked "
|
||||
"will report no answer.\n");
|
||||
return s + describeProof(outcome.proof);
|
||||
}
|
||||
|
||||
bool bakeWriteLanded(const std::string& written, const std::optional<std::string>& readBack) {
|
||||
|
||||
+50
-21
@@ -175,10 +175,12 @@ struct BakeScanTally {
|
||||
int staleCleared = 0; // ClearStale
|
||||
int answered = 0; // an outcome was QUEUED for write-back (a landing OR a
|
||||
// refusal) — pass-wide, not per key, and not proof the write
|
||||
// reached the project; `writeFailed` is that
|
||||
int writeFailed = 0; // of `answered`, the ones whose value could NOT be read back
|
||||
// from their key afterwards — an asking instance sees these as
|
||||
// no answer at all
|
||||
// reached the project; `writeFailed`/`writeUnproven` are that
|
||||
int writeFailed = 0; // of `answered`, the ones whose value read back as something
|
||||
// other than what was written — an asking instance sees these
|
||||
// as no answer at all
|
||||
int writeUnproven = 0; // of `answered`, the ones whose write-and-check itself failed:
|
||||
// NOT known to have landed and NOT known to have failed
|
||||
int landed = 0; // of `answered`, the ones that reached the bank
|
||||
};
|
||||
|
||||
@@ -188,14 +190,29 @@ struct BakeScanTally {
|
||||
// evidence the landing never ran; nothing here can observe that, and no caller may say it.
|
||||
std::string describeBakeScan(const BakeScanTally& tally);
|
||||
|
||||
// How far a `Land` verdict actually got. Only `Banked` is a landing the project still holds
|
||||
// after a reload: the book lives in memory and a pass that could not persist has changed
|
||||
// nothing the .rpp will carry.
|
||||
// How far a `Land` verdict actually got. `Banked` is reachable ONLY through
|
||||
// bakeLandingAfterPersist below — a landing may not claim the word on its own, because
|
||||
// nothing a landing can see says whether the pass's persist ran.
|
||||
enum class BakeLanding {
|
||||
Refused, // nothing was written and the book is untouched
|
||||
Partial, // it threw AFTER it had begun writing — a file and/or an entry may exist
|
||||
Unpersisted, // it reached the in-memory book, but the pass's persist did not happen
|
||||
Banked, // in the book AND persisted into the project
|
||||
Unpersisted, // it reached the in-memory book; the pass's persist did not report success
|
||||
Banked, // in the book, and the pass's persist reported success
|
||||
};
|
||||
|
||||
// The ONE route to `Banked`: every Land verdict's landing is assigned `Unpersisted` and
|
||||
// then passed through here. The special case this replaces — a dedup hit answered `Banked`
|
||||
// directly on the grounds that it changed nothing — was wrong precisely when the entry it
|
||||
// deduped against was one the SAME pass had just added and then failed to persist.
|
||||
BakeLanding bakeLandingAfterPersist(BakeLanding landing, bool persisted);
|
||||
|
||||
// Whether the write a verdict required actually took. Three states, not two: the read-back
|
||||
// that proves it can itself fail, and "we could not check" is a different thing to tell the
|
||||
// user from "it did not land".
|
||||
enum class BakeWriteProof {
|
||||
Confirmed, // the key read back as exactly the bytes written (absent/empty for a clear)
|
||||
Rejected, // it read back as something else — the write did not take
|
||||
Unknown, // the write-and-check itself failed; neither claim is available
|
||||
};
|
||||
|
||||
// What ONE scanned key ended the pass in — the per-key half of the tally above, which
|
||||
@@ -206,20 +223,32 @@ struct BakeKeyOutcome {
|
||||
bool oversized = false; // IgnoreUnreadable only: the value exceeded the read ceiling
|
||||
// rather than reading back empty
|
||||
BakeLanding landing = BakeLanding::Refused; // Land only
|
||||
bool writeConfirmed = false; // Land / RefuseWrongProject / ClearStale: the write this
|
||||
// verdict required — an outcome, or a clear — was READ
|
||||
// BACK from the key afterwards (see bakeWriteLanded)
|
||||
std::string detail; // the outcome's own message, so one line per key is
|
||||
// self-contained; empty for the verdicts without one
|
||||
BakeWriteProof proof = BakeWriteProof::Unknown; // Land / RefuseWrongProject /
|
||||
// ClearStale: whether the write this
|
||||
// verdict required took
|
||||
std::string detail; // the outcome's own message, so one line per key is
|
||||
// self-contained; empty for the verdicts without one, and empty
|
||||
// wherever the landing clause already carries that same sentence
|
||||
};
|
||||
|
||||
// Did the write we just made take? `readBack` is what the key holds afterwards (nullopt =
|
||||
// absent or unreadable). This exists because SetProjExtState's return is the size of the
|
||||
// WHOLE extname's state — `banks`, `project_guid` and every rsusage_ key count toward it,
|
||||
// and a bake can only reach a landing in a project whose `banks` is already populated — so
|
||||
// that return is non-zero whether or not THIS key took the value. Reading the key back is
|
||||
// the only per-key observation available under a shared extname. An empty `written` is a
|
||||
// CLEAR, which lands as an absent-or-empty key rather than as those bytes.
|
||||
// Did the write we just made take? `readBack` is what the key holds afterwards, and
|
||||
// `nullopt` means ABSENT specifically — an overflowed or failed read is NOT an absence and
|
||||
// must reach BakeWriteProof::Unknown instead of being folded in here, or a clear would be
|
||||
// claimed off a value nothing could read. This exists because SetProjExtState's return is
|
||||
// the size of the WHOLE extname's state — `banks`, `project_guid` and every rsusage_ key
|
||||
// count toward it, and a bake can only reach a landing in a project whose `banks` is
|
||||
// already populated — so that return is non-zero whether or not THIS key took the value.
|
||||
// Reading the key back is the only per-key observation available under a shared extname.
|
||||
// An empty `written` is a CLEAR, which lands as an absent-or-empty key rather than as those
|
||||
// bytes.
|
||||
//
|
||||
// `[verify — DAW]` byte equality assumes REAPER stores and returns an ext-state value
|
||||
// verbatim. Two known ways that could be false, both of which would make this report a
|
||||
// write as Rejected that REAPER in fact took as given: a value REAPER normalises on the
|
||||
// round trip, and an embedded NUL — SetProjExtState/GetProjExtState are C-string
|
||||
// transports, so a payload containing one would be truncated on write. Neither produces a
|
||||
// contradictory pair (the asking instrument reads such a key as Undecodable, itself a
|
||||
// failure), so both are diagnosis noise rather than a false success.
|
||||
bool bakeWriteLanded(const std::string& written, const std::optional<std::string>& readBack);
|
||||
|
||||
// One console line naming a key and what the pass did with it, ends in '\n'. Printed for
|
||||
|
||||
Reference in New Issue
Block a user