Prove the bake's answer writes by reading the key back
SetProjExtState's return covers the whole extname, so it never saw one key. The persist verdict now reaches the report, and a throw mid-write no longer claims the landing left nothing behind.
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's write landed; `describeBakeKey` prints one line per enumerated key regardless, which is the only thing that names WHICH key — the counts cannot.
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
|
||||
+48
-13
@@ -214,9 +214,11 @@ std::string describeBakeScan(const BakeScanTally& t) {
|
||||
if (t.notARequest > 0)
|
||||
s += "; " + std::to_string(t.notARequest) +
|
||||
" held something other than a request";
|
||||
// "were cleared" would assert a write this count cannot see: whether each clear
|
||||
// actually took is a per-KEY read-back, and describeBakeKey is where it is said.
|
||||
if (t.staleCleared > 0)
|
||||
s += "; " + std::to_string(t.staleCleared) +
|
||||
" were past the age bound and were cleared unanswered";
|
||||
" were past the age bound, so no reader was left to answer";
|
||||
}
|
||||
s += ".";
|
||||
if (t.writeFailed > 0)
|
||||
@@ -226,9 +228,31 @@ std::string describeBakeScan(const BakeScanTally& t) {
|
||||
return s + "\n";
|
||||
}
|
||||
|
||||
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.
|
||||
std::string describeLanding(BakeLanding landing) {
|
||||
switch (landing) {
|
||||
case BakeLanding::Banked:
|
||||
return "landed into the bank";
|
||||
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";
|
||||
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 {};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string describeBakeKey(const std::string& key, const BakeKeyOutcome& outcome) {
|
||||
const std::string head = "ReaSampler resample: " + key + " -- ";
|
||||
std::string answered; // set only by the two verdicts that write an outcome back
|
||||
std::string clause; // set only by the verdicts that write something back
|
||||
switch (outcome.verdict) {
|
||||
case BakeScanVerdict::IgnoreUnreadable:
|
||||
return head + (outcome.oversized
|
||||
@@ -240,25 +264,36 @@ std::string describeBakeKey(const std::string& key, const BakeKeyOutcome& outcom
|
||||
"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:
|
||||
return head +
|
||||
"cleared unanswered: it was past the age bound, so no reader was left "
|
||||
"for it.\n";
|
||||
// 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");
|
||||
case BakeScanVerdict::RefuseWrongProject:
|
||||
answered = "refused: its project tab is not the one this extension has loaded";
|
||||
clause = "refused";
|
||||
break;
|
||||
case BakeScanVerdict::Land:
|
||||
answered = outcome.landed ? "landed into the bank" : "the landing was refused";
|
||||
clause = describeLanding(outcome.landing);
|
||||
break;
|
||||
}
|
||||
// Fails closed on a verdict this build has no word for, the same reason
|
||||
// answeredOutcome exists: no -Wswitch is configured, so an appended enumerator would
|
||||
// otherwise fall straight into the trailing clause and print half a sentence.
|
||||
if (answered.empty()) return head + "a verdict this build has no word for.\n";
|
||||
return head + answered +
|
||||
(outcome.answerWritten
|
||||
? ", and the answer was written back.\n"
|
||||
: ", but the answer could NOT be written back -- the instance that asked "
|
||||
"will report no answer.\n");
|
||||
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");
|
||||
}
|
||||
|
||||
bool bakeWriteLanded(const std::string& written, const std::optional<std::string>& readBack) {
|
||||
if (written.empty()) return !readBack || readBack->empty();
|
||||
return readBack && *readBack == written;
|
||||
}
|
||||
|
||||
} // namespace reasampler::wire
|
||||
|
||||
+34
-11
@@ -176,29 +176,52 @@ struct BakeScanTally {
|
||||
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 SetProjExtState reported as not
|
||||
// landed — an asking instance sees these as no answer at all
|
||||
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
|
||||
int landed = 0; // of `answered`, the ones that reached the bank
|
||||
};
|
||||
|
||||
// The console summary for one pass. Empty ONLY when the pass answered somebody AND left no
|
||||
// key unanswered AND every answer's write landed — so the summary's absence means nothing
|
||||
// went wrong, never that the action did not run. A silent console is NOT evidence the
|
||||
// landing never ran; nothing here can observe that, and no caller may say it does.
|
||||
// key unanswered AND every answer was read back from its own key — so the summary's absence
|
||||
// means nothing went wrong, never that the action did not run. A silent console is NOT
|
||||
// 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.
|
||||
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
|
||||
};
|
||||
|
||||
// What ONE scanned key ended the pass in — the per-key half of the tally above, which
|
||||
// counts but cannot name. Three of the fields are verdict-conditional; the phrasing
|
||||
// counts but cannot name. Every field but the verdict is verdict-conditional; the phrasing
|
||||
// function is the one place that pairing is spelled out.
|
||||
struct BakeKeyOutcome {
|
||||
BakeScanVerdict verdict = BakeScanVerdict::IgnoreUnreadable;
|
||||
bool oversized = false; // IgnoreUnreadable only: the value exceeded the read
|
||||
// ceiling rather than reading back empty
|
||||
bool landed = false; // Land only: the bake reached the bank
|
||||
bool answerWritten = false; // Land / RefuseWrongProject only: the outcome write
|
||||
// reported landing in the project
|
||||
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
|
||||
};
|
||||
|
||||
// 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.
|
||||
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
|
||||
// EVERY enumerated key, answered or not, because the counts above cannot tell an instance
|
||||
// which key was its own — and the key carries the asking instance's guid.
|
||||
|
||||
Reference in New Issue
Block a user