// bake_land.cpp — see bake_land.h. main.cpp owns the API pointers; this TU gets them // extern. REAPER symbols used here (EnumProjects, EnumProjExtState, GetProjExtState, // SetProjExtState, ShowConsoleMsg) are verified against // vendor/reaper-sdk/sdk/reaper_plugin_functions.h. Landing one request is // bake_landing's; the undo block is shell/undo's. #include "shell/capture/bake_land.h" #include #include #include #include #include #include #include "core/capture/capture_paths.h" // projectDirOfRpp #include "core/wire/bake_wire.h" #include "core/wire/ext_state_read.h" // readProjExtStateGrowing (the shared grow loop) #include "ext_keys.h" #include "shell/capture/bake_landing.h" #include "shell/panel/panel_input.h" // bankPanelRefresh #include "shell/persist/session.h" #include "shell/undo/undo_block.h" // the ONE undo-block opener #include "reaper_plugin.h" // UNDO_STATE_MISCCFG #define REAPERAPI_MINIMAL #define REAPERAPI_WANT_EnumProjects #define REAPERAPI_WANT_EnumProjExtState #define REAPERAPI_WANT_GetProjExtState #define REAPERAPI_WANT_SetProjExtState #define REAPERAPI_WANT_ShowConsoleMsg #include "reaper_plugin_functions.h" namespace reasampler::capture { namespace { using wire::BakeOutcome; using wire::BakeRequest; using wire::BakeStatus; // Every "rsbake_*" key in one project, keys only — the value can outgrow // EnumProjExtState's fixed buffer, so it is read separately by the growing reader. std::vector pendingBakeKeys(ReaProject* proj) { std::vector keys; const std::string prefix = kProjExtBakeKeyPrefix; char keyBuf[256]; for (int idx = 0;; ++idx) { keyBuf[0] = '\0'; if (!EnumProjExtState(proj, kProjExtNamespace(), idx, keyBuf, static_cast(sizeof(keyBuf)), nullptr, 0)) break; const std::string key(keyBuf); if (key.compare(0, prefix.size(), prefix) == 0) keys.push_back(key); } return keys; } // 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 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); }); 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 { ReaProject* proj = nullptr; std::string dir; // the project's own directory; empty for a never-saved project }; // Every open project tab. The request key lives in the INSTANCE's project, which is not // necessarily the focused one, so the scan cannot assume the current tab holds it. std::vector openProjects() { std::vector out; std::vector buf(4096, '\0'); for (int idx = 0;; ++idx) { buf[0] = '\0'; ReaProject* proj = EnumProjects(idx, buf.data(), static_cast(buf.size())); if (!proj) break; out.push_back(OpenProject{proj, projectDirOfRpp(std::string(buf.data()))}); } return out; } // One scanned key: what the pass decided about it, and what to write back after the undo // block closes. The outcome is kept UNENCODED until then, because the pass's persist runs // after the whole scan and an Ok the project never took must not be sent as one. struct ScannedKey { enum class Write { None, Clear, Answer }; ReaProject* proj = nullptr; std::string key; Write write = Write::None; // None = an Ignore verdict, nothing to write BakeOutcome outcome; // Write::Answer only wire::BakeKeyOutcome report; }; // One entry's write-back, and the proof of whether it took. Guarded whole: it encodes into // a fresh string and readKey grows a buffer toward 16 MB, and a throw escaping here would // skip the ShowConsoleMsg for EVERY key — the silent console this action exists to make // impossible. Which proof a throw yields turns on whether SetProjExtState had run: before // it, the write is KNOWN not to have been made (`Rejected`); after it, the write may well // have landed and only the check failed (`Unknown`). wire::BakeWriteProof writeBackOne(ScannedKey& entry, bool persisted) { bool issued = false; try { std::string value; // empty = clear the key if (entry.write == ScannedKey::Write::Answer) { // Where a landing earns the word: the pass's persist runs after the whole scan, // so until here no bank write had been issued for it at all. Without one the // entry lives in memory alone, which is a failed bake to anyone who reloads — // so it is answered as one rather than as an Ok. const wire::BakeLanding after = wire::bakeLandingAfterPersist(entry.report.landing, persisted); if (after == wire::BakeLanding::Unpersisted) { entry.outcome = refuseBake( BakeStatus::Failed, "the bake reached the bank in memory, but this pass's persist did not " "report success, so this answer cannot promise a reload will find it", entry.outcome.generation); // `detail` left empty on purpose: describeLanding's Unpersisted clause is // that sentence's one home on the console side, and the message above is // the instrument's own copy on the wire. } else { entry.report.detail = entry.outcome.message; } entry.report.landing = after; value = wire::encodeBakeOutcome(entry.outcome); } SetProjExtState(entry.proj, kProjExtNamespace(), entry.key.c_str(), value.c_str()); issued = true; // Only re-reading THIS key can speak for it (wire::extStateWriteLanded owns why). // An oversized read proves nothing either way — folding it into that predicate's // absent case would report a clear off unread bytes. const KeyRead back = readKey(entry.proj, entry.key); if (back.oversized) return wire::BakeWriteProof::Unknown; return wire::extStateWriteLanded(value, back.value) ? wire::BakeWriteProof::Confirmed : wire::BakeWriteProof::Rejected; } catch (...) { return issued ? wire::BakeWriteProof::Unknown : wire::BakeWriteProof::Rejected; } } } // namespace void RunResampleBake(ReaSamplerSession& session) { 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 // matchesLoadedProject (below) falls out false regardless of this flag's value. const wire::BakeScanContext scanContext{loaded == active}; const std::int64_t nowSec = static_cast(std::time(nullptr)); std::vector scanned; wire::BakeScanTally tally; // every verdict below is counted, skips included std::string aborted; // set only when the scan itself threw bool persisted = false; // the pass's ONE persist ran and reported success bool bookChanged = false; // some landing added or refreshed an entry // A local, not a tally field: an entry counted here can still be re-answered as a // failure by the write-back loop below, so this is a persist GATE and never a count // anything reports. bool anyLanded = false; // The scan allocates outside attemptLanding's own guards too (readKey grows a buffer // toward 16 MB, every container here allocates), and a throw that escaped would discard // the answers already buffered — the exact no-answer-with-a-silent-console this action // exists to make impossible. UndoBlock is inside the try, so unwinding still closes it. try { UndoBlock undo(nullptr); for (const OpenProject& open : openProjects()) { ++tally.tabsScanned; const bool activeTab = static_cast(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 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(open.proj) == loaded}; const wire::BakeScanVerdict verdict = wire::classifyBakeScan(scanContext, scanKey, nowSec); ScannedKey entry; entry.proj = open.proj; entry.key = key; entry.report.verdict = verdict; entry.report.oversized = read.oversized; // 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.write = ScannedKey::Write::Clear; scanned.push_back(std::move(entry)); continue; } if (verdict == wire::BakeScanVerdict::RefuseWrongProject) { entry.outcome = refuseBake(BakeStatus::WrongProject, "this bake's project tab is not the one the extension " "has loaded -- focus that tab and try again", request->generation); } else { LandingAttempt attempt = attemptLanding(session, open.dir, *request); if (attempt.outcome.status == BakeStatus::Ok) anyLanded = true; if (attempt.changedBook) bookChanged = true; // Never Banked here, on ANY path: the pass's persist has not run yet, // and bakeLandingAfterPersist is the one place that word is earned. A // dedup hit is no exception — the entry it points at may be one this // same pass added and is about to fail to persist. if (attempt.partial) entry.report.landing = wire::BakeLanding::Partial; else if (attempt.outcome.status != BakeStatus::Ok) entry.report.landing = wire::BakeLanding::Refused; else entry.report.landing = wire::BakeLanding::Unpersisted; entry.outcome = std::move(attempt.outcome); } ++tally.answered; // QUEUED, not written — the write loop below judges that entry.write = ScannedKey::Write::Answer; scanned.push_back(std::move(entry)); } } if (anyLanded) { // A dedup hit rides the persist too, because that is the only observation that // can promote its landing — but it added nothing, so it earns neither the // refresh every open instance pays for nor an undo point named after a bake // that banked nothing. if (bookChanged) session.bumpBankGeneration(); persisted = session.saveToActiveProject(); if (persisted && bookChanged) 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 reported below; any key whose answer " "it had not prepared 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. std::string console = aborted; try { for (ScannedKey& entry : scanned) { if (entry.write != ScannedKey::Write::None) { entry.report.proof = writeBackOne(entry, persisted); if (entry.write == ScannedKey::Write::Answer) { if (entry.report.proof == wire::BakeWriteProof::Rejected) ++tally.writeFailed; else if (entry.report.proof == wire::BakeWriteProof::Unknown) ++tally.writeUnproven; } } // 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 += wire::describeBakeScan(tally); } catch (...) { // Whatever lines already accumulated must still reach the console — that guarantee // is this action's whole reason for existing. A second throw while appending the // note below is swallowed for exactly the same reason: the print is what matters, // and append gives the strong guarantee, so `console` survives either way. try { console += "ReaSampler resample: the write-back pass stopped early on an internal " "failure. The lines above are everything it managed to report; any key not " "named above may or may not have been answered.\n"; } catch (...) { } } if (!console.empty()) ShowConsoleMsg(console.c_str()); if (bookChanged) bankPanelRefresh(); } } // namespace reasampler::capture