b7b93a7ac3
The drain refuses a tick inside a foreign block; applyMode wakes the reclaimer before it opens the span.
318 lines
16 KiB
C++
318 lines
16 KiB
C++
// 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 <cstdint>
|
|
#include <ctime>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#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<std::string> pendingBakeKeys(ReaProject* proj) {
|
|
std::vector<std::string> 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<int>(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<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);
|
|
});
|
|
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<OpenProject> openProjects() {
|
|
std::vector<OpenProject> out;
|
|
std::vector<char> buf(4096, '\0');
|
|
for (int idx = 0;; ++idx) {
|
|
buf[0] = '\0';
|
|
ReaProject* proj = EnumProjects(idx, buf.data(), static_cast<int>(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::int64_t>(std::time(nullptr));
|
|
|
|
std::vector<ScannedKey> scanned;
|
|
wire::BakeScanTally tally; // every verdict below is counted, skips included
|
|
std::string aborted; // set only when the scan itself threw
|
|
bool persisted = false; // the pass's ONE persist 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<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);
|
|
|
|
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
|