Fix the bake scan report's unsound absence-as-signal claim
Gate the summary on any unanswered key and on SetProjExtState's return, print one line per scanned key, and make the undo block stack RAII.
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, is what separates an extension that never ran the landing from one that refused) 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` are that same reading counted and spoken — the rationale lives at the type. The report is empty whenever a pass answered anybody, which makes its ABSENCE from the console evidence too: no line means the landing action never ran.
|
||||
- `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.
|
||||
- `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.
|
||||
|
||||
|
||||
+67
-19
@@ -187,30 +187,78 @@ BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanK
|
||||
}
|
||||
|
||||
std::string describeBakeScan(const BakeScanTally& t) {
|
||||
if (t.answered > 0) return {};
|
||||
// `answered` is pass-wide and counts a QUEUED write, so it cannot stand alone: a pass
|
||||
// that answered somebody else's key while skipping ours, or that queued our answer and
|
||||
// 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 {};
|
||||
|
||||
std::string s = "ReaSampler resample: the landing action ran and scanned " +
|
||||
countOf(t.tabsScanned, "project tab") + ", and answered nothing. ";
|
||||
if (t.keysFound == 0) {
|
||||
s += "No pending bake request was visible to it at all -- if ReaSampler 9000 "
|
||||
"reported publishing one, the plugin and the extension are not reading the "
|
||||
"same project's ext state.";
|
||||
return s + "\n";
|
||||
}
|
||||
countOf(t.tabsScanned, "project tab") + ". ";
|
||||
if (t.keysFound == 0)
|
||||
return s +
|
||||
"No rsbake_ key was visible to it in any of them. That fits a request "
|
||||
"published into a project this scan did not enumerate, a key cleared "
|
||||
"before the scan reached it, or a key set this session that the "
|
||||
"enumeration cannot yet see -- this pass cannot tell which apart.\n";
|
||||
|
||||
s += "It found " + countOf(t.keysFound, "pending request key") + ", " +
|
||||
std::to_string(t.activeTabKeys) + " of them in the active tab";
|
||||
if (t.unreadable > 0)
|
||||
s += "; " + std::to_string(t.unreadable) + " could not be read back";
|
||||
if (t.notARequest > 0)
|
||||
s += "; " + std::to_string(t.notARequest) + " held something other than a request";
|
||||
if (t.staleCleared > 0)
|
||||
s += "; " + std::to_string(t.staleCleared) +
|
||||
" were past the age bound and were cleared unanswered";
|
||||
s += "It found " + countOf(t.keysFound, "rsbake_ key") +
|
||||
" (every such key, not pending requests only), " +
|
||||
std::to_string(t.activeTabKeys) + " of them in REAPER's active tab. It answered " +
|
||||
std::to_string(t.answered);
|
||||
if (unanswered > 0) {
|
||||
s += " and left " + std::to_string(unanswered) + " unanswered";
|
||||
if (t.unreadable > 0)
|
||||
s += "; " + std::to_string(t.unreadable) + " could not be read back";
|
||||
if (t.notARequest > 0)
|
||||
s += "; " + std::to_string(t.notARequest) +
|
||||
" held something other than a request";
|
||||
if (t.staleCleared > 0)
|
||||
s += "; " + std::to_string(t.staleCleared) +
|
||||
" were past the age bound and were cleared unanswered";
|
||||
}
|
||||
s += ".";
|
||||
if (t.activeTabKeys == 0)
|
||||
s += " The tab this bake was fired against held none of them.";
|
||||
if (t.writeFailed > 0)
|
||||
s += " " + countOf(t.writeFailed, "answer") +
|
||||
" could not be written back into the project, so whoever asked for them sees "
|
||||
"no answer at all.";
|
||||
return s + "\n";
|
||||
}
|
||||
|
||||
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
|
||||
switch (outcome.verdict) {
|
||||
case BakeScanVerdict::IgnoreUnreadable:
|
||||
return head + (outcome.oversized
|
||||
? "skipped: its value was too large to read back whole.\n"
|
||||
: "skipped: the enumerator listed it but it read back "
|
||||
"empty.\n");
|
||||
case BakeScanVerdict::IgnoreNotARequest:
|
||||
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:
|
||||
return head +
|
||||
"cleared unanswered: it was past the age bound, so no reader was left "
|
||||
"for it.\n";
|
||||
case BakeScanVerdict::RefuseWrongProject:
|
||||
answered = "refused: its project tab is not the one this extension has loaded";
|
||||
break;
|
||||
case BakeScanVerdict::Land:
|
||||
answered = outcome.landed ? "landed into the bank" : "the landing was refused";
|
||||
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");
|
||||
}
|
||||
|
||||
} // namespace reasampler::wire
|
||||
|
||||
+38
-11
@@ -85,12 +85,14 @@ std::optional<BakeOutcome> decodeBakeOutcome(const std::string& wire);
|
||||
// evidence either side gets. The two classifiers below are that reading, stated once.
|
||||
|
||||
// What the instrument found under its own key once invokeExtensionAction returned. The
|
||||
// distinction that matters: `Unanswered` is the extension never having read the key at
|
||||
// all, which is a DIFFERENT fault from every refusal — a refusal is an outcome.
|
||||
// distinction that matters: `Unanswered` is nothing having WRITTEN over the key, which is
|
||||
// a DIFFERENT fault from every refusal — a refusal is an outcome. It does NOT mean the
|
||||
// landing never read the key: a scan that skipped this key, and a scan whose answer write
|
||||
// was rejected, both leave the request sitting here identically.
|
||||
enum class BakeAnswerKind {
|
||||
Answered, // a decodable outcome echoing this request's generation
|
||||
ForeignOutcome, // a decodable outcome, but for another generation
|
||||
Unanswered, // this request, unchanged: nothing on the extension side read it
|
||||
Unanswered, // this request, unchanged: nothing wrote an outcome over it
|
||||
ForeignRequest, // a request that is not ours — another instance shares this key
|
||||
Cleared, // the key holds nothing — absent, explicitly cleared, or a read
|
||||
// failure at the bridge; these are not distinguishable from here
|
||||
@@ -161,20 +163,45 @@ BakeScanVerdict classifyBakeScan(const BakeScanContext& session, const BakeScanK
|
||||
// request, untouched — and only these counts say which of them happened.
|
||||
struct BakeScanTally {
|
||||
int tabsScanned = 0;
|
||||
int keysFound = 0; // `rsbake_*` keys enumerated across every open tab
|
||||
int activeTabKeys = 0; // of those, in the tab the action was fired against
|
||||
int keysFound = 0; // EVERY `rsbake_*` key enumerated across every open tab —
|
||||
// uncollected outcomes and unreadable values included, not
|
||||
// pending requests only
|
||||
int activeTabKeys = 0; // of those, in REAPER's active tab. NOT "the tab the bake was
|
||||
// fired against": the action is invoked with the instance's
|
||||
// own project as `proj`, and whether REAPER makes that project
|
||||
// current for the action's duration is `[verify — DAW]`
|
||||
int unreadable = 0; // IgnoreUnreadable
|
||||
int notARequest = 0; // IgnoreNotARequest
|
||||
int staleCleared = 0; // ClearStale
|
||||
int answered = 0; // an outcome was written back (a landing OR a refusal)
|
||||
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 landed = 0; // of `answered`, the ones that reached the bank
|
||||
};
|
||||
|
||||
// The console sentence for a pass that answered NOTHING — the one state in which the
|
||||
// asking instance reports a no-answer and has nothing to go on. Empty string when
|
||||
// `answered > 0`, so the caller prints unconditionally and stays quiet on a pass that
|
||||
// spoke for itself. The line's absence is itself evidence: no line means the landing
|
||||
// action never ran.
|
||||
// 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.
|
||||
std::string describeBakeScan(const BakeScanTally& tally);
|
||||
|
||||
// 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
|
||||
// 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
|
||||
};
|
||||
|
||||
// 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.
|
||||
std::string describeBakeKey(const std::string& key, const BakeKeyOutcome& outcome);
|
||||
|
||||
} // namespace reasampler::wire
|
||||
|
||||
@@ -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: scans every open project tab for pending `rsbake_*` requests, lands the ones belonging to the project this session has loaded, 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. 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, and printing `wire::describeBakeScan` when the pass answered nobody. Each key is materialized before any answer is written, so no `SetProjExtState` in this action mutates a set the enumerator is still walking. 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. 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". 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.
|
||||
- `bake_land` (`shell/capture`) — the EXTENSION's half of the resample chain: scans every open project tab for pending `rsbake_*` requests, lands the ones belonging to the project this session has loaded, 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. 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 rejected, in one `ShowConsoleMsg`. It checks `SetProjExtState`'s return on every non-empty answer — an answer that did not land is the one no-answer the tally alone cannot show. Each key is materialized before any answer is written, so no `SetProjExtState` in this action mutates a set the enumerator is still walking. The undo block is stack RAII (`UndoBlock`), and `landOne`'s whole-WAV read/hash runs under a catch that converts a throw into a refusal — neither an exception nor an early return can leave an undo block open or discard the buffered answers. 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. 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". 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.
|
||||
- `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.
|
||||
- `capture_realtime_shell` (`shell/capture`) — the async realtime-record backend surface (Q-W6 split of the former fat `capture.h`): `RealtimeRecordBackend::begin`/`tick`/`abort`, transport-driven across timer ticks (a realtime record cannot block REAPER's UI for its own duration). Deliberately shares NO interface with the offline backend — the lifecycles genuinely differ (the former `ICaptureBackend` interface was deleted in Q-W3, T4-26).
|
||||
|
||||
+168
-80
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <optional>
|
||||
@@ -222,12 +223,22 @@ std::vector<std::string> pendingBakeKeys(ReaProject* proj) {
|
||||
return keys;
|
||||
}
|
||||
|
||||
std::optional<std::string> readKey(ReaProject* proj, const std::string& key) {
|
||||
// Whole value, or the reason there isn't one. Absent and Overflow both leave the scan with
|
||||
// nothing to decode — one verdict — but they are different faults to go fix, so the reason
|
||||
// survives to the per-key console line rather than collapsing here.
|
||||
struct KeyRead {
|
||||
std::optional<std::string> value;
|
||||
bool oversized = false; // the value never fit under the grow loop's ceiling
|
||||
};
|
||||
|
||||
KeyRead readKey(ReaProject* proj, const std::string& key) {
|
||||
const auto read = wire::readProjExtStateGrowing([&](char* buf, int cap) {
|
||||
return GetProjExtState(proj, kProjExtNamespace(), key.c_str(), buf, cap);
|
||||
});
|
||||
if (read.status != wire::GrowingExtStateRead::Status::Complete) return std::nullopt;
|
||||
return read.value;
|
||||
KeyRead out;
|
||||
out.oversized = read.status == wire::GrowingExtStateRead::Status::Overflow;
|
||||
if (read.status == wire::GrowingExtStateRead::Status::Complete) out.value = read.value;
|
||||
return out;
|
||||
}
|
||||
|
||||
struct OpenProject {
|
||||
@@ -249,21 +260,49 @@ std::vector<OpenProject> openProjects() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// One answer to write back after the undo block closes.
|
||||
struct Answer {
|
||||
// One scanned key: what the pass decided about it, and the answer to write back after the
|
||||
// undo block closes.
|
||||
struct ScannedKey {
|
||||
ReaProject* proj = nullptr;
|
||||
std::string key;
|
||||
std::string wire; // empty = clear the key instead of answering it
|
||||
std::string console; // empty = nothing to print
|
||||
bool writesAnswer = false; // false = an Ignore verdict, nothing to write
|
||||
std::string answerWire; // with writesAnswer: empty = clear the key instead
|
||||
std::string console; // the outcome's own sentence; empty = nothing to print
|
||||
wire::BakeKeyOutcome report;
|
||||
};
|
||||
|
||||
// Undo_BeginBlock2/EndBlock2 as stack RAII, the FxBypassGuard discipline: landOne reads a
|
||||
// whole WAV and hashes it, so a bad_alloc between the two calls would otherwise leave an
|
||||
// open undo block in the user's project. Closing with an empty description records no undo
|
||||
// point, which is what every path that landed nothing wants anyway.
|
||||
class UndoBlock {
|
||||
public:
|
||||
UndoBlock() { Undo_BeginBlock2(nullptr); }
|
||||
~UndoBlock() { Undo_EndBlock2(nullptr, description_.c_str(), flags_); }
|
||||
void recordPoint(std::string description, int flags) {
|
||||
description_ = std::move(description);
|
||||
flags_ = flags;
|
||||
}
|
||||
UndoBlock(const UndoBlock&) = delete;
|
||||
UndoBlock& operator=(const UndoBlock&) = delete;
|
||||
|
||||
private:
|
||||
std::string description_;
|
||||
int flags_ = 0;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void RunResampleBake(ReaSamplerSession& session) {
|
||||
// Whether REAPER makes Main_OnCommandEx's `proj` current for the action's duration is
|
||||
// unverified in the DAW; the per-key verdict holds either way, and a request it cannot
|
||||
// land is told why rather than silently ignored.
|
||||
const void* loaded = session.loadedProject();
|
||||
// REAPER's CURRENT tab, which is all this is. The action was invoked with the asking
|
||||
// instance's own project as `proj` — getReaperParent(3), documented as the parent
|
||||
// project in reaper_vst3_interfaces.h, and already paired with an EnumProjects-derived
|
||||
// enumeration by the shipped rsusage_* path prune's protection depends on. What is
|
||||
// `[verify — DAW]` is only whether REAPER makes that `proj` CURRENT for the action's
|
||||
// duration, so nothing below may call this "the tab the bake was fired against". The
|
||||
// per-key verdict holds either way, and a request this pass cannot land is told why
|
||||
// rather than silently ignored.
|
||||
const void* active = EnumProjects(-1, nullptr, 0);
|
||||
// `loaded == active` alone is enough: every OpenProject::proj enumerated below is a
|
||||
// real, non-null tab, so an unloaded `loaded` (nullptr) can never equal one and
|
||||
@@ -271,88 +310,137 @@ void RunResampleBake(ReaSamplerSession& session) {
|
||||
const wire::BakeScanContext scanContext{loaded == active};
|
||||
const std::int64_t nowSec = static_cast<std::int64_t>(std::time(nullptr));
|
||||
|
||||
std::vector<Answer> answers;
|
||||
std::vector<ScannedKey> scanned;
|
||||
wire::BakeScanTally tally; // every verdict below is counted, skips included
|
||||
Undo_BeginBlock2(nullptr);
|
||||
for (const OpenProject& open : openProjects()) {
|
||||
++tally.tabsScanned;
|
||||
const bool activeTab = static_cast<const void*>(open.proj) == active;
|
||||
for (const std::string& key : pendingBakeKeys(open.proj)) {
|
||||
++tally.keysFound;
|
||||
if (activeTab) ++tally.activeTabKeys;
|
||||
const std::optional<std::string> raw = readKey(open.proj, key);
|
||||
const std::optional<BakeRequest> request =
|
||||
raw ? wire::decodeBakeRequest(*raw) : std::nullopt;
|
||||
const wire::BakeScanKey scanKey{
|
||||
raw.has_value(), request.has_value(), request ? request->generation : 0,
|
||||
static_cast<const void*>(open.proj) == loaded};
|
||||
const wire::BakeScanVerdict verdict =
|
||||
wire::classifyBakeScan(scanContext, scanKey, nowSec);
|
||||
std::string aborted; // set only when the scan itself threw
|
||||
// The scan allocates outside landOne's own guard 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
|
||||
// exists to make impossible. UndoBlock is inside the try, so unwinding still closes it.
|
||||
try {
|
||||
UndoBlock undo;
|
||||
for (const OpenProject& open : openProjects()) {
|
||||
++tally.tabsScanned;
|
||||
const bool activeTab = static_cast<const void*>(open.proj) == active;
|
||||
for (const std::string& key : pendingBakeKeys(open.proj)) {
|
||||
++tally.keysFound;
|
||||
if (activeTab) ++tally.activeTabKeys;
|
||||
const KeyRead read = readKey(open.proj, key);
|
||||
const std::optional<BakeRequest> request =
|
||||
read.value ? wire::decodeBakeRequest(*read.value) : std::nullopt;
|
||||
const wire::BakeScanKey scanKey{
|
||||
read.value.has_value(), request.has_value(),
|
||||
request ? request->generation : 0,
|
||||
static_cast<const void*>(open.proj) == loaded};
|
||||
const wire::BakeScanVerdict verdict =
|
||||
wire::classifyBakeScan(scanContext, scanKey, nowSec);
|
||||
|
||||
// Leave it — the writing instance owns clearing its own key.
|
||||
if (verdict == wire::BakeScanVerdict::IgnoreUnreadable) {
|
||||
++tally.unreadable;
|
||||
continue;
|
||||
}
|
||||
if (verdict == wire::BakeScanVerdict::IgnoreNotARequest) {
|
||||
++tally.notARequest;
|
||||
continue;
|
||||
}
|
||||
if (verdict == wire::BakeScanVerdict::ClearStale) {
|
||||
++tally.staleCleared;
|
||||
answers.push_back(Answer{open.proj, key, std::string{}, std::string{}});
|
||||
continue;
|
||||
}
|
||||
ScannedKey entry;
|
||||
entry.proj = open.proj;
|
||||
entry.key = key;
|
||||
entry.report.verdict = verdict;
|
||||
entry.report.oversized = read.oversized;
|
||||
|
||||
BakeOutcome outcome;
|
||||
if (verdict == wire::BakeScanVerdict::RefuseWrongProject) {
|
||||
outcome = refuse(BakeStatus::WrongProject,
|
||||
"this bake's project tab is not the one the extension has "
|
||||
"loaded -- focus that tab and try again",
|
||||
request->generation);
|
||||
} else {
|
||||
outcome = landOne(session, open.dir, *request);
|
||||
if (outcome.status == BakeStatus::Ok) ++tally.landed;
|
||||
// Leave it — the writing instance owns clearing its own key.
|
||||
if (verdict == wire::BakeScanVerdict::IgnoreUnreadable) {
|
||||
++tally.unreadable;
|
||||
scanned.push_back(std::move(entry));
|
||||
continue;
|
||||
}
|
||||
// `!request` is folded in here rather than dereferenced below: today
|
||||
// classifyBakeScan filters every undecoded key into this verdict, but a
|
||||
// future verdict that did not would otherwise reach *request unguarded.
|
||||
if (verdict == wire::BakeScanVerdict::IgnoreNotARequest || !request) {
|
||||
++tally.notARequest;
|
||||
entry.report.verdict = wire::BakeScanVerdict::IgnoreNotARequest;
|
||||
scanned.push_back(std::move(entry));
|
||||
continue;
|
||||
}
|
||||
if (verdict == wire::BakeScanVerdict::ClearStale) {
|
||||
++tally.staleCleared;
|
||||
entry.writesAnswer = true; // empty answerWire = clear the key
|
||||
scanned.push_back(std::move(entry));
|
||||
continue;
|
||||
}
|
||||
|
||||
BakeOutcome outcome;
|
||||
if (verdict == wire::BakeScanVerdict::RefuseWrongProject) {
|
||||
outcome =
|
||||
refuse(BakeStatus::WrongProject,
|
||||
"this bake's project tab is not the one the extension has "
|
||||
"loaded -- focus that tab and try again",
|
||||
request->generation);
|
||||
} else {
|
||||
// landOne reads and hashes a whole WAV. A throw here would otherwise
|
||||
// discard every answer buffered so far and leave each asking instance
|
||||
// with a no-answer it cannot explain; converting it to a refusal keeps
|
||||
// the pass and its report intact.
|
||||
try {
|
||||
outcome = landOne(session, open.dir, *request);
|
||||
} catch (const std::exception& e) {
|
||||
outcome = refuse(BakeStatus::Failed,
|
||||
std::string("the landing failed: ") + e.what(),
|
||||
request->generation);
|
||||
} catch (...) {
|
||||
outcome = refuse(BakeStatus::Failed,
|
||||
"the landing failed for an unknown reason",
|
||||
request->generation);
|
||||
}
|
||||
if (outcome.status == BakeStatus::Ok) ++tally.landed;
|
||||
}
|
||||
++tally.answered; // QUEUED, not written — the write loop below judges that
|
||||
entry.report.landed = outcome.status == BakeStatus::Ok;
|
||||
// A WrongProject request left sitting in a tab this call did not come from
|
||||
// prints again on every OTHER tab's bake, since the scan revisits every
|
||||
// open project each time. Only a refusal in REAPER's active tab is likely
|
||||
// to be fresh feedback to a user who just clicked bake; every other one is
|
||||
// a rescan repeat. The per-key line below is printed either way.
|
||||
if (outcome.status != BakeStatus::Ok &&
|
||||
(outcome.status != BakeStatus::WrongProject || activeTab)) {
|
||||
entry.console = "ReaSampler resample: " + outcome.message + ".\n";
|
||||
}
|
||||
entry.writesAnswer = true;
|
||||
entry.answerWire = wire::encodeBakeOutcome(outcome);
|
||||
scanned.push_back(std::move(entry));
|
||||
}
|
||||
++tally.answered;
|
||||
// A WrongProject request left sitting in a tab this call did not come from
|
||||
// prints again on every OTHER tab's bake, since the scan revisits every open
|
||||
// project each time. `active` is this call's own proxy for "the invoking tab" —
|
||||
// only that tab's own refusal is fresh feedback to a user who just clicked
|
||||
// bake; every other one is a rescan repeat.
|
||||
std::string console;
|
||||
if (outcome.status != BakeStatus::Ok &&
|
||||
(outcome.status != BakeStatus::WrongProject || activeTab)) {
|
||||
console = "ReaSampler resample: " + outcome.message + ".\n";
|
||||
}
|
||||
answers.push_back(
|
||||
Answer{open.proj, key, wire::encodeBakeOutcome(outcome), console});
|
||||
}
|
||||
}
|
||||
|
||||
if (tally.landed > 0) {
|
||||
// A bake changes what a live instance would play, so the generation bump rides the
|
||||
// persist — every other open instance refreshes hands-free.
|
||||
session.bumpBankGeneration();
|
||||
const bool persisted = session.saveToActiveProject();
|
||||
Undo_EndBlock2(nullptr,
|
||||
persisted ? "ReaSampler: resample bake into bank" : "",
|
||||
persisted ? UNDO_STATE_MISCCFG : 0);
|
||||
} else {
|
||||
Undo_EndBlock2(nullptr, "", 0); // nothing landed — record no empty undo point
|
||||
if (tally.landed > 0) {
|
||||
// A bake changes what a live instance would play, so the generation bump rides
|
||||
// the persist — every other open instance refreshes hands-free.
|
||||
session.bumpBankGeneration();
|
||||
if (session.saveToActiveProject())
|
||||
undo.recordPoint("ReaSampler: resample bake into bank", UNDO_STATE_MISCCFG);
|
||||
}
|
||||
} catch (...) {
|
||||
aborted =
|
||||
"ReaSampler resample: the landing pass stopped early on an internal failure. "
|
||||
"The answers it had already prepared are written below; any key it had not "
|
||||
"reached yet went unanswered and is not named at all.\n";
|
||||
}
|
||||
|
||||
// OUTSIDE the undo block on purpose: the answer is transient handshake state, and an
|
||||
// undo point that captured it could restore the consumed request on the next Ctrl-Z —
|
||||
// which the following bake would then re-land against a temp file that is long gone.
|
||||
for (const Answer& answer : answers) {
|
||||
SetProjExtState(answer.proj, kProjExtNamespace(), answer.key.c_str(),
|
||||
answer.wire.c_str());
|
||||
if (!answer.console.empty()) ShowConsoleMsg(answer.console.c_str());
|
||||
std::string console = aborted;
|
||||
for (ScannedKey& entry : scanned) {
|
||||
if (entry.writesAnswer) {
|
||||
// SetProjExtState returns the size of the extname's state, so storing a
|
||||
// non-empty value necessarily returns > 0 and <= 0 means the write did not
|
||||
// land. A deliberate clear shrinks the state and can legitimately return 0 —
|
||||
// the same reading reaper_bridge's writeGuarded applies to the other end.
|
||||
const int rv = SetProjExtState(entry.proj, kProjExtNamespace(),
|
||||
entry.key.c_str(), entry.answerWire.c_str());
|
||||
entry.report.answerWritten = entry.answerWire.empty() || rv > 0;
|
||||
if (!entry.report.answerWritten) ++tally.writeFailed;
|
||||
}
|
||||
// Unconditional, including on success: the tally counts keys but cannot name them,
|
||||
// and the key name is the only thing that tells an instance which line is its own.
|
||||
console += wire::describeBakeKey(entry.key, entry.report);
|
||||
console += entry.console;
|
||||
}
|
||||
// Empty unless the pass answered nobody, so a bake that spoke for itself stays quiet.
|
||||
const std::string report = wire::describeBakeScan(tally);
|
||||
if (!report.empty()) ShowConsoleMsg(report.c_str());
|
||||
console += wire::describeBakeScan(tally);
|
||||
if (!console.empty()) ShowConsoleMsg(console.c_str());
|
||||
if (tally.landed > 0) bankPanelRefresh();
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ declared ahead of the instrument slots at that member in `reasampler_processor.h
|
||||
- `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).
|
||||
- `editor_stroke` — the editor's LICE side of the analytic stroker: builds a coverage mask with the pure `core/ui/stroke_aa` and blends it into the bitmap ONCE, writing straight to the bitmap's bits (the arithmetic matches LICE's own mode-0 combine, so a stroke composites identically to every other kit draw). Every radial and spline stroke on the editor routes through `strokeArcAA` / `strokePolylineAA` / `strokeLineAA`. Holds the draw-thread-only scratch mask and arc point list — reuse, not a hidden dependency: threading a canvas through the eight paint sites would grow those signatures to carry an allocation detail. Deliberately does NOT touch `shell/panel/draw_kit`: the waveform stroke, the docked bank panel and the browse cards are out of this seam's blast radius.
|
||||
- `instrument_bake` — the instrument's half of the resample chain, on the UI thread: render the dialed sound through the pure `core/instrument/bake` modules at the instance's PERSISTED PREVIEW VELOCITY (the velocity the user has been auditioning at — three velocity curves are live, so it is a property of the sound and not a render detail), stage the WAV OUTSIDE the bank folder, publish one `rsbake_<guid>` request, invoke the extension's landing action SYNCHRONOUSLY, read the outcome back over the same key, then adopt + reset in one act. What that key holds afterwards is classified by `core/wire`'s pure `classifyBakeAnswer`, and each of its five non-answers gets its OWN sentence — a silent no-answer stays a failure, but the user is told whether the extension never ran the landing, answered a stale generation, answered in a wire this build cannot read, cleared the request, or refused it. Two stack-RAII guards mirror `FxBypassGuard`'s discipline: the staged file and the request key are both cleared on every exit path, so a failed bake leaves no temp, no bank entry and no parameter reset. `bakeAvailable` is the affordance's paint gate. A cloned `instanceGuid` (two instances sharing one `rsbake_` key) is NOT handled here — the residual is contained by pre-existing tracking machinery instead: `planUsagePublish`'s sticky `unioned` poison plus `tiedUsageExists` (`core/tracking/tracking_authority.cpp`) force a clone's bake to `AddDistinct` rather than silently replacing a sibling's entry.
|
||||
- `instrument_bake` — the instrument's half of the resample chain, on the UI thread: render the dialed sound through the pure `core/instrument/bake` modules at the instance's PERSISTED PREVIEW VELOCITY (the velocity the user has been auditioning at — three velocity curves are live, so it is a property of the sound and not a render detail), stage the WAV OUTSIDE the bank folder, publish one `rsbake_<guid>` request, invoke the extension's landing action SYNCHRONOUSLY, read the outcome back over the same key, then adopt + reset in one act. What that key holds afterwards is classified by `core/wire`'s pure `classifyBakeAnswer`, and each of its five non-answers gets its OWN sentence — a silent no-answer stays a failure, but the user is told whether nothing wrote over the key, a stale generation was answered, the answer came in a wire this build cannot read, the request was cleared, or it was refused. The three sentences that are ABOUT the key name it, because the extension prints one console line per key it scanned and the key is what correlates the two in a multi-instance session. None of them claims the landing never ran — nothing on this side can observe that. Two stack-RAII guards mirror `FxBypassGuard`'s discipline: the staged file and the request key are both cleared on every exit path, so a failed bake leaves no temp, no bank entry and no parameter reset. `bakeAvailable` is the affordance's paint gate. A cloned `instanceGuid` (two instances sharing one `rsbake_` key) is NOT handled here — the residual is contained by pre-existing tracking machinery instead: `planUsagePublish`'s sticky `unioned` poison plus `tiedUsageExists` (`core/tracking/tracking_authority.cpp`) force a clone's bake to `AddDistinct` rather than silently replacing a sibling's entry.
|
||||
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
|
||||
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` TU family, included only by the editor's own shell TUs (`editor_session` / `editor_controls` / `editor_paint_*` / `editor_input_*` / `editor_platform`), never a public seam: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, and the velocity-curve box derivation — the helpers more than one band TU needs. The deck's control ids, group ids and group composition are the pure `deck_groups` module's, not this file's. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
|
||||
- `reasampler_vst.h` — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected `Steinberg::FUID`, built from the FOREVER-FROZEN macros in `core/wire/reasampler_uid.h`), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/reasampler_vst.h` directly.)*
|
||||
|
||||
@@ -196,32 +196,34 @@ BakeChainResult runBake(ReaSamplerProcessor& processor) {
|
||||
|
||||
// What the key holds now is the only evidence this side gets, and each of the five
|
||||
// non-answers is a different thing to go fix — collapsing them into one sentence is
|
||||
// what made a stale install indistinguishable from a refusal.
|
||||
// what made a stale install indistinguishable from a refusal. The three that are about
|
||||
// the KEY name it, because the landing prints one console line per key it scanned and
|
||||
// the key is what correlates the two in a multi-instance session.
|
||||
const wire::BakeAnswer answer =
|
||||
wire::classifyBakeAnswer(bridge.readReasamplerExtState(key), request);
|
||||
switch (answer.kind) {
|
||||
case wire::BakeAnswerKind::Answered:
|
||||
break;
|
||||
case wire::BakeAnswerKind::Unanswered:
|
||||
return fail(
|
||||
"the ReaSampler extension did not answer -- the request key still holds "
|
||||
"this exact request, untouched. Check the REAPER console: if the landing "
|
||||
"action printed a scan report just now, it DID run and that line says what "
|
||||
"it saw; if the console is silent, the action never ran at all");
|
||||
return fail("the ReaSampler extension did not answer " + key +
|
||||
" -- that key still holds this exact request, untouched. The "
|
||||
"landing action prints one REAPER console line per key it "
|
||||
"scanned; look for that key name there");
|
||||
case wire::BakeAnswerKind::Undecodable:
|
||||
return fail(
|
||||
"the extension answered in a format this plugin does not read -- the "
|
||||
"extension and ReaSampler 9000 are from different builds");
|
||||
case wire::BakeAnswerKind::Cleared:
|
||||
return fail(
|
||||
"the bake key came back empty -- either the request was cleared before "
|
||||
"an answer was written, or this build could not read whatever was there. "
|
||||
"The landing action's console scan report, if one appeared, names which");
|
||||
return fail("the bake key " + key +
|
||||
" came back empty -- either the request was cleared before an "
|
||||
"answer was written, or this build could not read whatever was "
|
||||
"there. Look for that key name in the landing action's console "
|
||||
"lines");
|
||||
case wire::BakeAnswerKind::ForeignRequest:
|
||||
return fail(
|
||||
"the bake key held a different pending request instead of an answer -- "
|
||||
"unexpected given the bake's single-threaded call flow; if this recurs, "
|
||||
"note the exact steps and file it");
|
||||
return fail("the bake key " + key +
|
||||
" held a different pending request instead of an answer -- "
|
||||
"unexpected given the bake's single-threaded call flow; if this "
|
||||
"recurs, note the exact steps and file it");
|
||||
case wire::BakeAnswerKind::ForeignOutcome:
|
||||
return fail("the extension answered a different bake request");
|
||||
}
|
||||
|
||||
+128
-23
@@ -392,39 +392,71 @@ int main() {
|
||||
BakeScanVerdict::IgnoreUnreadable);
|
||||
}
|
||||
|
||||
// --- The scan report: what the action tells a user it actually saw ------------------
|
||||
// The report exists because every non-answering verdict leaves the asking instance
|
||||
// with the identical evidence (its own untouched request), so only these counts
|
||||
// discriminate them. It must therefore SAY the count that fired, and must stay silent
|
||||
// whenever an answer was written.
|
||||
// --- The scan summary: silent ONLY when nothing went unanswered ---------------------
|
||||
// `answered` is pass-wide and counts a QUEUED write, so gating on it alone let two real
|
||||
// faults print nothing: a pass that answered another key while skipping ours, and a
|
||||
// pass whose own answer write was rejected. Those two are what the gate below pins.
|
||||
{
|
||||
BakeScanTally answeredOne;
|
||||
answeredOne.tabsScanned = 1;
|
||||
answeredOne.keysFound = 1;
|
||||
answeredOne.activeTabKeys = 1;
|
||||
answeredOne.answered = 1;
|
||||
answeredOne.landed = 1;
|
||||
CHECK(describeBakeScan(answeredOne).empty());
|
||||
// A refusal is still an answer, so it silences the report the same way a landing
|
||||
BakeScanTally clean;
|
||||
clean.tabsScanned = 1;
|
||||
clean.keysFound = 1;
|
||||
clean.activeTabKeys = 1;
|
||||
clean.answered = 1;
|
||||
clean.landed = 1;
|
||||
CHECK(describeBakeScan(clean).empty());
|
||||
// A refusal is still an answer, so it silences the summary the same way a landing
|
||||
// does — the refusal's own sentence has already been printed.
|
||||
BakeScanTally refusedOne = answeredOne;
|
||||
BakeScanTally refusedOne = clean;
|
||||
refusedOne.landed = 0;
|
||||
CHECK(describeBakeScan(refusedOne).empty());
|
||||
|
||||
// Nothing found at all: the state Daniel's repro produces if the request key never
|
||||
// becomes visible to the extension.
|
||||
// THE regression: some OTHER key was answered while ours was skipped. Under the old
|
||||
// `answered > 0` gate this printed nothing at all, and the instrument then told the
|
||||
// user a silent console proved the action never ran.
|
||||
for (int BakeScanTally::*skip :
|
||||
{&BakeScanTally::unreadable, &BakeScanTally::notARequest,
|
||||
&BakeScanTally::staleCleared}) {
|
||||
BakeScanTally mixed = clean;
|
||||
mixed.keysFound = 2;
|
||||
mixed.*skip = 1;
|
||||
const std::string said = describeBakeScan(mixed);
|
||||
CHECK(!said.empty());
|
||||
CHECK(said.find("left 1 unanswered") != std::string::npos);
|
||||
}
|
||||
|
||||
// THE other regression: our answer was queued (so `answered` counted it) and the
|
||||
// SetProjExtState write was rejected. Nothing else went wrong, and it must still
|
||||
// speak.
|
||||
BakeScanTally rejected = clean;
|
||||
rejected.writeFailed = 1;
|
||||
const std::string wrote = describeBakeScan(rejected);
|
||||
CHECK(!wrote.empty());
|
||||
CHECK(wrote.find("1 answer could not be written back") != std::string::npos);
|
||||
CHECK(wrote.find("sees no answer at all") != std::string::npos);
|
||||
// Plural agrees, and the count is the tally's own, not a hardcoded 1.
|
||||
BakeScanTally rejectedTwo = clean;
|
||||
rejectedTwo.answered = 2;
|
||||
rejectedTwo.writeFailed = 2;
|
||||
CHECK(describeBakeScan(rejectedTwo).find("2 answers could not be written back") !=
|
||||
std::string::npos);
|
||||
|
||||
// Nothing found at all: the observation is stated and the candidates listed, with
|
||||
// no winner picked — the enumeration's visibility of a key set but not yet saved is
|
||||
// undocumented, so "not the same project's ext state" may not name it.
|
||||
BakeScanTally nothing;
|
||||
nothing.tabsScanned = 2;
|
||||
const std::string none = describeBakeScan(nothing);
|
||||
CHECK(!none.empty());
|
||||
CHECK(none.find("2 project tabs") != std::string::npos);
|
||||
CHECK(none.find("No pending bake request was visible") != std::string::npos);
|
||||
CHECK(none.find("No rsbake_ key was visible") != std::string::npos);
|
||||
CHECK(none.find("this pass cannot tell which apart") != std::string::npos);
|
||||
CHECK(none.find("are not reading the same project") == std::string::npos);
|
||||
CHECK(none.back() == '\n');
|
||||
|
||||
// One tab is singular, not "1 project tabs".
|
||||
BakeScanTally oneTab;
|
||||
oneTab.tabsScanned = 1;
|
||||
CHECK(describeBakeScan(oneTab).find("1 project tab,") != std::string::npos);
|
||||
CHECK(describeBakeScan(oneTab).find("1 project tab.") != std::string::npos);
|
||||
|
||||
// Found but unreadable — the fact the old scan discarded.
|
||||
BakeScanTally unreadable;
|
||||
@@ -433,9 +465,12 @@ int main() {
|
||||
unreadable.activeTabKeys = 1;
|
||||
unreadable.unreadable = 1;
|
||||
const std::string unread = describeBakeScan(unreadable);
|
||||
CHECK(unread.find("1 pending request key") != std::string::npos);
|
||||
CHECK(unread.find("1 could not be read back") != std::string::npos);
|
||||
CHECK(unread.find("held something other than a request") == std::string::npos);
|
||||
// keysFound counts EVERY rsbake_ key, not pending requests only — a label that
|
||||
// said otherwise produced "3 pending request keys ... 3 held something else".
|
||||
CHECK(unread.find("1 rsbake_ key") != std::string::npos);
|
||||
CHECK(unread.find("pending request key") == std::string::npos);
|
||||
|
||||
// Found, readable, but not a request.
|
||||
BakeScanTally foreign;
|
||||
@@ -456,16 +491,86 @@ int main() {
|
||||
const std::string aged = describeBakeScan(stale);
|
||||
CHECK(aged.find("past the age bound") != std::string::npos);
|
||||
|
||||
// Keys exist, but none in the tab the bake was fired against — the multi-tab
|
||||
// mis-target, called out explicitly rather than left to be inferred from "0 of them".
|
||||
// activeTabKeys is REAPER's ACTIVE tab and nothing more. It cannot discriminate the
|
||||
// multi-tab mis-target — a genuine background-tab request classifies as
|
||||
// RefuseWrongProject, which is an ANSWER — so no sentence may claim it does, and
|
||||
// none may call it "the tab this bake was fired against" (whether REAPER makes the
|
||||
// invoking instance's project current is DAW-unverified).
|
||||
BakeScanTally elsewhere;
|
||||
elsewhere.tabsScanned = 2;
|
||||
elsewhere.keysFound = 1;
|
||||
elsewhere.activeTabKeys = 0;
|
||||
elsewhere.notARequest = 1;
|
||||
const std::string away = describeBakeScan(elsewhere);
|
||||
CHECK(away.find("0 of them in the active tab") != std::string::npos);
|
||||
CHECK(away.find("fired against held none of them") != std::string::npos);
|
||||
CHECK(away.find("0 of them in REAPER's active tab") != std::string::npos);
|
||||
CHECK(away.find("fired against") == std::string::npos);
|
||||
}
|
||||
|
||||
// --- The per-key line: the only thing that names WHICH key ---------------------------
|
||||
// The tally counts; it cannot say whose key was skipped. This line is printed for every
|
||||
// enumerated key, answered or not, and the key carries the asking instance's guid — so
|
||||
// it is what lets one instance find its own verdict in a multi-instance session.
|
||||
{
|
||||
const std::string key = "rsbake_0123abcd";
|
||||
|
||||
BakeKeyOutcome landed;
|
||||
landed.verdict = BakeScanVerdict::Land;
|
||||
landed.landed = true;
|
||||
landed.answerWritten = true;
|
||||
const std::string ok = describeBakeKey(key, landed);
|
||||
CHECK(ok.find(key) != std::string::npos);
|
||||
CHECK(ok.find("landed into the bank") != std::string::npos);
|
||||
CHECK(ok.find("the answer was written back") != std::string::npos);
|
||||
CHECK(ok.back() == '\n');
|
||||
|
||||
// A landing whose answer write was REJECTED — the state the instrument reads as
|
||||
// Unanswered. This line is the only place it is ever named.
|
||||
BakeKeyOutcome lost = landed;
|
||||
lost.answerWritten = false;
|
||||
const std::string dropped = describeBakeKey(key, lost);
|
||||
CHECK(dropped.find("landed into the bank") != std::string::npos);
|
||||
CHECK(dropped.find("could NOT be written back") != std::string::npos);
|
||||
CHECK(dropped.find("will report no answer") != std::string::npos);
|
||||
|
||||
BakeKeyOutcome bankRefused;
|
||||
bankRefused.verdict = BakeScanVerdict::Land;
|
||||
bankRefused.landed = false;
|
||||
bankRefused.answerWritten = true;
|
||||
CHECK(describeBakeKey(key, bankRefused).find("the landing was refused") !=
|
||||
std::string::npos);
|
||||
|
||||
BakeKeyOutcome wrongTab;
|
||||
wrongTab.verdict = BakeScanVerdict::RefuseWrongProject;
|
||||
wrongTab.answerWritten = true;
|
||||
const std::string refused = describeBakeKey(key, wrongTab);
|
||||
CHECK(refused.find("not the one this extension has loaded") != std::string::npos);
|
||||
CHECK(refused.find("the answer was written back") != std::string::npos);
|
||||
|
||||
BakeKeyOutcome cleared;
|
||||
cleared.verdict = BakeScanVerdict::ClearStale;
|
||||
CHECK(describeBakeKey(key, cleared).find("cleared unanswered") != std::string::npos);
|
||||
|
||||
BakeKeyOutcome notRequest;
|
||||
notRequest.verdict = BakeScanVerdict::IgnoreNotARequest;
|
||||
CHECK(describeBakeKey(key, notRequest).find("other than a pending request") !=
|
||||
std::string::npos);
|
||||
|
||||
// Absent and Overflow both yield IgnoreUnreadable, but they are different faults to
|
||||
// go fix, so the line keeps them apart where the verdict cannot.
|
||||
BakeKeyOutcome empty;
|
||||
empty.verdict = BakeScanVerdict::IgnoreUnreadable;
|
||||
BakeKeyOutcome huge = empty;
|
||||
huge.oversized = true;
|
||||
const std::string emptyLine = describeBakeKey(key, empty);
|
||||
const std::string hugeLine = describeBakeKey(key, huge);
|
||||
CHECK(emptyLine.find("read back empty") != std::string::npos);
|
||||
CHECK(hugeLine.find("too large to read back whole") != std::string::npos);
|
||||
CHECK(emptyLine != hugeLine);
|
||||
|
||||
// Every line names its own key, so two instances' lines are never confusable.
|
||||
CHECK(describeBakeKey("rsbake_ffff0000", landed).find("rsbake_ffff0000") !=
|
||||
std::string::npos);
|
||||
CHECK(describeBakeKey("rsbake_ffff0000", landed) != ok);
|
||||
}
|
||||
|
||||
// --- Every outcome bake_land actually emits survives the key round trip -------------
|
||||
|
||||
Reference in New Issue
Block a user