diff --git a/src/core/capture/CLAUDE.md b/src/core/capture/CLAUDE.md index 104a57c..32c6ca3 100644 --- a/src/core/capture/CLAUDE.md +++ b/src/core/capture/CLAUDE.md @@ -52,7 +52,7 @@ Detail specific to these pure modules: - `capture_name` — the REAPER-free composition of one capture's label + file-stem base from its source-track name(s), a local-calendar discriminator (`MM-DD HHMM`, from the shell's clock read), and an optional batch ordinal. The label and the stem deliberately diverge: the stem still passes through `capture_paths::sanitizeStem` (so a name that sanitizes to nothing files as `capture`), while the label keeps the source name verbatim. Stem uniqueness stays entirely `makeUniqueTag`'s — this module never disambiguates. - `insert_plan` — the REAPER-free logic behind the `insert` shell (M6): computes the `InsertMedia` `mode` bitmask from an `InsertOptions` struct (placement target, tempo-conform ratio, preserve-pitch flag), guaranteeing the &4 stretch-to-time-selection bit is never set and that no tempo bits are set when `conform == None`. - `render_settings` — the REAPER-free logic behind the capture action family: `SourceMode` → `RENDER_SETTINGS` bit mapping, `P_RAZOREDITS` string parsing + range-union bounds, razor-else-time range inference, the FX-scope bypass plan (`fxBypassPlanFor`), the tail-mode → `RENDER_TAILFLAG`/`RENDER_NORMALIZE`/`RENDER_TRIMEND` mapping (`tailRenderSettingsFor`) and its realtime-window analog (`realtimeRecordWindowEnd`), the capture-action taxonomy table (`captureActionTable`) `main.cpp` iterates to register the CAPTURE_ITEM/CAPTURE_TRACK family, and `renderSourceLabel` (the source named in the offline backend's bounds refusal). -- `render_window` — the REAPER-free frame arithmetic behind exact capture bounds: `frameCountFor` (the frame count a project-time window occupies at the project rate — the number the offline backend checks the rendered file against before landing it, so a render that printed something other than the window is refused rather than banked), `renderHonoredBounds` (the gate's verdict and the sole home of its one-frame tolerance, which is empirical rather than proven — the header states which renderer models it covers and which it does not), and `itemExtentPrintsWindow`, the predicate `render_settings::sourceModeForScope` consults to decide whether REAPER's selected-items render source can express a requested window at all. +- `render_window` — the REAPER-free frame arithmetic behind exact capture bounds: `frameCountFor` (the frame count a project-time window occupies at the project rate — the number the offline backend checks the rendered file against before landing it, so a render that printed something other than the window is refused rather than banked), `renderHonoredBounds` (the gate's verdict and the sole home of its one-frame tolerance, which is empirical rather than proven — the header states which renderer models it covers and which it does not), and `itemExtentPrintsWindow`, the predicate `render_settings::sourceModeForScope` consults to decide whether REAPER's selected-items render source can express a requested window at all. It also owns the two short-render diagnostics: `msFlooredEndFrameCount` (the frames a window holds with its end floored to the millisecond — the shape two live short renders matched, quoted by the refusal as a count coincidence and nothing more) and `describeBoundsDrift` (the sentence the offline backend prints when `RENDER_STARTPOS`/`RENDER_ENDPOS` do not read back as they were written). - `track_topology` — the REAPER-free folder arithmetic over a project's flat `I_FOLDERDEPTH` delta list: `directChildIndices` names a folder parent's DIRECT children, the set `shell/capture/render_isolation` silences so a ranged item capture does not print its track's children. Grandchildren are excluded by construction — they reach the parent only through the child that owns them. - `tail_control` — the REAPER-free logic behind the docked `bank_panel`'s tail-mode toggle: the cycle order (None → Auto → Manual → None), the Manual-length clamp/scroll-wheel fine-adjust (`clampManualMs`/`adjustManualMs`, 250 ms/notch, 2000 ms default), the toggle's label text (e.g. "Tail: Manual 2.0s"), and the `TailSetting` JSON round-trip persist stores per-project. diff --git a/src/core/capture/render_window.cpp b/src/core/capture/render_window.cpp index e32e642..40e2840 100644 --- a/src/core/capture/render_window.cpp +++ b/src/core/capture/render_window.cpp @@ -3,6 +3,7 @@ #include "core/capture/render_window.h" #include +#include namespace reasampler::capture { @@ -15,6 +16,22 @@ long long frameIndexAt(double seconds, int sampleRate) { return std::llround(seconds * static_cast(sampleRate)); } +// See the header for why whole milliseconds get a tolerance and why it is this small. +double floorToMilliseconds(double seconds) { + const double ms = seconds * 1000.0; + const double nearest = std::nearbyint(ms); + if (std::fabs(ms - nearest) < 1e-6) return nearest / 1000.0; + return std::floor(ms) / 1000.0; +} + +// Full round-trip precision: a drift report whose two numbers print identically +// would be evidence of nothing. +std::string exactly(double seconds) { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%.17g", seconds); + return buf; +} + } // namespace long long frameCountFor(double startSeconds, double endSeconds, int sampleRate) { @@ -41,4 +58,33 @@ bool itemExtentPrintsWindow(double reqStart, double reqEnd, && frameIndexAt(reqEnd, sampleRate) == frameIndexAt(itemEnd, sampleRate); } +long long msFlooredEndFrameCount(double startSeconds, double endSeconds, + int sampleRate) { + return frameCountFor(startSeconds, floorToMilliseconds(endSeconds), sampleRate); +} + +std::string describeBoundsDrift(double reqStart, double reqEnd, + double storedStart, double storedEnd, + int sampleRate) { + // Bit equality, deliberately: the caller wrote these exact doubles and read them + // straight back, so anything but the same bits is a value REAPER changed. + if (storedStart == reqStart && storedEnd == reqEnd) return {}; + + // Says only that the two differ, not why -- a legitimate clamp (negative start, + // end past project end) reads back differently for the same reason a precision + // defect would, and this sentence cannot tell those apart. + std::string s = "REAPER read back different render bounds than it was handed -- " + "asked for [" + + exactly(reqStart) + "s, " + exactly(reqEnd) + "s), read back [" + + exactly(storedStart) + "s, " + exactly(storedEnd) + "s)."; + if (sampleRate > 0) { + s += " The stored window is " + + std::to_string(frameCountFor(storedStart, storedEnd, sampleRate)) + + " frames against the " + + std::to_string(frameCountFor(reqStart, reqEnd, sampleRate)) + + " the request asks for, at " + std::to_string(sampleRate) + " Hz."; + } + return s; +} + } // namespace reasampler::capture diff --git a/src/core/capture/render_window.h b/src/core/capture/render_window.h index a26bfdb..3f96e6f 100644 --- a/src/core/capture/render_window.h +++ b/src/core/capture/render_window.h @@ -1,9 +1,14 @@ #pragma once // render_window — pure frame arithmetic for a capture's requested window: the -// frame count a project-time range occupies, and whether a render whose bounds -// come from the selected items' own extent already prints that window. +// frame count a project-time range occupies, whether a render whose bounds come +// from the selected items' own extent already prints that window, and the two +// diagnostics that bound a short render without locating it: whether the stored +// RENDER_* bounds round-tripped, and whether the shortfall matches a millisecond- +// floor coincidence. // NO REAPER types; unit-tested by tests/test_render_window.cpp. +#include + namespace reasampler::capture { // Frames the [startSeconds, endSeconds) window occupies at `sampleRate`. Both @@ -42,4 +47,28 @@ bool itemExtentPrintsWindow(double reqStart, double reqEnd, double itemStart, double itemEnd, int sampleRate); +// --- Diagnostics: where a short render lost its frames ------------------------ + +// The frames this window would hold if its END were resolved on a whole-millisecond +// grid, floored, instead of exactly. Two live short renders (48 kHz, TailMode::None) +// matched this count to the frame, which is the entire reason it exists. +// +// A COINCIDENCE OF COUNTS, not a claim about how anything resolved the end: nothing +// renders from this number and no capture path asks for it. Whole-millisecond values +// are recognized within a nanosecond, because a decimal millisecond is not always one +// in binary (1.007 * 1000 lands just below 1007) and a bare floor would drop a +// millisecond from a window already on the grid. A nanosecond is far under one frame +// at any rate we render, so a real sub-millisecond remainder still floors. +long long msFlooredEndFrameCount(double startSeconds, double endSeconds, + int sampleRate); + +// The sentence a capture prints when the render bounds it handed REAPER did not read +// back unchanged — the requested window, what came back, and both frame counts at +// `sampleRate` (omitted when the rate is unknown). EMPTY when both edges read back +// bit-identical, which is the only answer proving the request crossed into REAPER +// intact; a caller prints this only when it is non-empty. +std::string describeBoundsDrift(double reqStart, double reqEnd, + double storedStart, double storedEnd, + int sampleRate); + } // namespace reasampler::capture diff --git a/src/shell/capture/capture.cpp b/src/shell/capture/capture.cpp index 6e7355a..9c71758 100644 --- a/src/shell/capture/capture.cpp +++ b/src/shell/capture/capture.cpp @@ -34,6 +34,7 @@ #include "core/capture/wav_codec.h" // hashWavContent / collapseToMono — the one WAV/RIFF owner #include "core/util/file_bytes.h" #include "core/capture/render_settings.h" +#include "core/capture/render_window.h" // describeBoundsDrift — the read-back's verdict #include "shell/capture/render_bounds_gate.h" // the exact-bounds verdict on the landed render #define REAPERAPI_MINIMAL @@ -424,6 +425,12 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) { GetSetProjectInfo(proj, "RENDER_STARTPOS", request.startSeconds, true); GetSetProjectInfo(proj, "RENDER_ENDPOS", request.endSeconds, true); + // The requested window crosses out of this process HERE and nowhere else, so the + // read-back is the only evidence available on this side of that boundary for + // whether REAPER kept it. Reported below, once the project rate is known. + const double storedStart = GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false); + const double storedEnd = GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false); + // TAILFLAG/TAILMS/NORMALIZE/TRIMEND from the pure mapping: None -> exact // bounds + disable-all normalize; Auto -> 8s tail + surgical trim-end // normalize + -72 dB TRIMEND; Manual -> clamped fixed tail + disable-all, no @@ -453,6 +460,22 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) { GetSetProjectInfo(proj, "RENDER_SRATE", static_cast(effectiveSampleRate), true); } + + // Silent unless a bound came back changed. Fires on EVERY tail mode on purpose: + // only None is judged against its window after the render, so this is the sole + // signal an Auto/Manual capture was shortened before it ever started. Named by + // checkpoint so a DAW observation is self-locating: three reads bracket the two + // places REAPER could quantize — the store, and the render itself. + auto reportDrift = [&](const char* checkpoint, double atStart, double atEnd) { + const std::string drift = + describeBoundsDrift(request.startSeconds, request.endSeconds, + atStart, atEnd, effectiveSampleRate); + if (!drift.empty()) + ShowConsoleMsg(("ReaSampler capture (" + std::string(checkpoint) + "): " + + drift + "\n").c_str()); + }; + reportDrift("at store", storedStart, storedEnd); + GetSetProjectInfo(proj, "RENDER_CHANNELS", static_cast(request.channelCount), true); @@ -485,8 +508,22 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) { } setProjString(proj, "RENDER_FORMAT", fmtBase64); + // Read again right here: a mismatch against the store-time read-back above + // means something between the two writes and this line moved the bounds, + // before the render ever ran. + reportDrift("before render", + GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false), + GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false)); + Main_OnCommand(kActionRenderUsingMostRecentSettings, 0); + // And once more here, while the guard above is still live and before it restores + // anything: only the gap between this read and the one immediately above can be + // the render itself. + reportDrift("after render", + GetSetProjectInfo(proj, "RENDER_STARTPOS", 0.0, false), + GetSetProjectInfo(proj, "RENDER_ENDPOS", 0.0, false)); + // Main_OnCommand returns void, so a failed render is silent — stat the // expected output path to detect it. const std::string expectedPath = paths.absoluteDir + "/" + paths.fileName; diff --git a/src/shell/capture/capture_realtime_shell.cpp b/src/shell/capture/capture_realtime_shell.cpp index 41a307b..e596091 100644 --- a/src/shell/capture/capture_realtime_shell.cpp +++ b/src/shell/capture/capture_realtime_shell.cpp @@ -369,6 +369,11 @@ RealtimeRecordBackend::begin(const CaptureRequest& request, // recordWindowEnd extends past the range end for a tail mode so the // transport captures the decay; cursor + time selection are restored by restore(). + // `[verify — DAW]` whether rs/re come back changed on this isSet=true call: the SDK + // header names both `double*` but documents no read-back semantics for either + // direction, and nothing here reads rs/re again after the call to notice. Lower + // stakes than the offline RENDER_* store: completion is driven by the play cursor + // reaching the range end (tick(), below), not by re-reading this pair. double rs = request.startSeconds, re = st->recordWindowEnd_; GetSet_LoopTimeRange(true, false, &rs, &re, false); SetEditCurPos(request.startSeconds, false, false); diff --git a/src/shell/capture/render_bounds_gate.cpp b/src/shell/capture/render_bounds_gate.cpp index 9fffa26..c0b7311 100644 --- a/src/shell/capture/render_bounds_gate.cpp +++ b/src/shell/capture/render_bounds_gate.cpp @@ -99,6 +99,22 @@ BoundsVerdict checkRenderedBounds(const std::string& renderedPath, const long long actualFrames = static_cast(layout.frameCount()); if (renderHonoredBounds(expectedFrames, actualFrames)) return v; + // Says whether this shortfall has the one shape two live short renders already + // matched to the frame: the END alone floored to the millisecond. Checked against + // the END only -- a refusal whose START is also off-grid and independently floored + // would not match this shape, and this note's silence on that refusal is this + // check not covering it, not the coincidence breaking. Excludes 0, which every + // sub-millisecond window (a legitimate day-one capture) also floors to, and which + // would otherwise match a render that produced nothing. A count coincidence only — + // it does not establish how the render resolved anything. + const long long msFlooredEnd = + msFlooredEndFrameCount(request.startSeconds, request.endSeconds, rate); + const std::string msNote = + (msFlooredEnd > 0 && actualFrames == msFlooredEnd) + ? " Those are exactly the frames this window holds with its end floored to" + " the millisecond -- a match on the count, not a measured cause." + : std::string(); + v.refused = true; v.message = "Render produced " + std::to_string(actualFrames) + " frames but the requested range is " + std::to_string(expectedFrames) + @@ -108,7 +124,7 @@ BoundsVerdict checkRenderedBounds(const std::string& renderedPath, std::to_string(request.endSeconds) + "s) -> frame indices [" + std::to_string(std::llround(request.startSeconds * rate)) + ", " + std::to_string(std::llround(request.endSeconds * rate)) + ")." + - retainRefusedRender(renderedPath, projectDir); + msNote + retainRefusedRender(renderedPath, projectDir); return v; } diff --git a/tests/test_render_window.cpp b/tests/test_render_window.cpp index ee7cbc1..9e0f0b6 100644 --- a/tests/test_render_window.cpp +++ b/tests/test_render_window.cpp @@ -1,13 +1,14 @@ // Standalone tests for reasampler::render_window — no REAPER, no framework. // Covers the bounds-equality number (a window's exact frame count at the project -// rate), the verdict the offline backend refuses a capture on, and the predicate +// rate), the verdict the offline backend refuses a capture on, the predicate // that decides whether REAPER's selected-items render source can express a -// requested window at all. +// requested window at all, and the two short-render diagnostics. #include "../src/core/capture/render_window.h" #include #include +#include using namespace reasampler::capture; @@ -15,6 +16,10 @@ static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) +static bool contains(const std::string& haystack, const std::string& needle) { + return haystack.find(needle) != std::string::npos; +} + // --- frameCountFor: the bounds equality, stated as a number ------------------ static void testFrameCountIsExactNotRounded() { @@ -226,6 +231,146 @@ static void testMultiItemUnionExtent() { CHECK(!itemExtentPrintsWindow(1.0, 4.0, 1.0, 9.0, 48000)); } +// --- msFlooredEndFrameCount: the shape both live short renders had ------------ + +static void testMillisecondFlooredEndReproducesBothShortRenders() { + // Both DAW observations, as arithmetic. 48 kHz, TailMode::None, start at 0: the + // requested window's count, and the count its end floored to the millisecond + // holds — which is what each render actually printed. + CHECK(frameCountFor(0.0, 4.067797, 48000) == 195254); + CHECK(msFlooredEndFrameCount(0.0, 4.067797, 48000) == 195216); + CHECK(frameCountFor(0.0, 4.067797, 48000) - + msFlooredEndFrameCount(0.0, 4.067797, 48000) == 38); + + CHECK(frameCountFor(0.0, 1.655172, 48000) == 79448); + CHECK(msFlooredEndFrameCount(0.0, 1.655172, 48000) == 79440); + CHECK(frameCountFor(0.0, 1.655172, 48000) - + msFlooredEndFrameCount(0.0, 1.655172, 48000) == 8); +} + +static void testTheSixDecimalDisplayDidNotCreateTheEffect() { + // Both reported ends were printed to six decimals by the refusal. Each is one 4/4 + // bar — at 59 BPM and at 145 BPM — so the full-precision doubles behind them are + // 240/59 and 240/145. Same counts either way: the display rounding is not what + // produces the shortfall. + CHECK(frameCountFor(0.0, 240.0 / 59.0, 48000) == 195254); + CHECK(msFlooredEndFrameCount(0.0, 240.0 / 59.0, 48000) == 195216); + CHECK(frameCountFor(0.0, 240.0 / 145.0, 48000) == 79448); + CHECK(msFlooredEndFrameCount(0.0, 240.0 / 145.0, 48000) == 79440); +} + +static void testWindowAlreadyOnTheMillisecondGridLosesNothing() { + // The "sometimes it works" case: a bar at 120 BPM is exactly 2 s. + CHECK(msFlooredEndFrameCount(0.0, 2.0, 48000) == frameCountFor(0.0, 2.0, 48000)); + + // The binary-representation trap a bare floor would fall into. The premise, not an + // outcome: 1.007 s is a whole millisecond that really does land BELOW 1007 ms in + // double, so flooring it without a tolerance drops a millisecond from a window + // already on the grid. + CHECK(1.007 * 1000.0 < 1007.0); + CHECK(frameCountFor(0.0, 1.007, 48000) == 48336); + CHECK(msFlooredEndFrameCount(0.0, 1.007, 48000) == 48336); + // Same end reached from a non-zero start, so nothing here rests on the window + // beginning at 0. + CHECK(msFlooredEndFrameCount(0.5, 1.007, 48000) == + frameCountFor(0.5, 1.007, 48000)); +} + +static void testOneFrameOfRemainderStillFloors() { + // The whole-millisecond tolerance must sit far below a frame, or it would swallow + // the very remainder this diagnostic exists to find. A remainder JUST BELOW a + // millisecond boundary is the discriminating case: one frame short of 1.0 s is + // 999.979166 ms, only ~0.0208 ms off the next whole millisecond. The shipped + // nanosecond tolerance still floors it down; a tolerance any wider than ~0.021 ms + // would snap it up to the millisecond instead and this test would then see 48000, + // not 47952 — which is what would fail if the tolerance regressed to something + // that wide. + const double oneFrame = 1.0 / 48000.0; + CHECK(frameCountFor(0.0, 1.0 - oneFrame, 48000) == 47999); + CHECK(msFlooredEndFrameCount(0.0, 1.0 - oneFrame, 48000) == 47952); +} + +static void testMillisecondFloorAt44100WhereAMillisecondIsNotWholeFrames() { + // 44.1 kHz: a millisecond is 44.1 frames, so a floored end cannot be described as + // dropping a whole number of frames — the count still resolves exactly. + CHECK(frameCountFor(0.0, 0.0105, 44100) == 463); + CHECK(msFlooredEndFrameCount(0.0, 0.0105, 44100) == 441); + // And a window that IS on the millisecond grid there is untouched, even though its + // edge is not on a frame boundary. + CHECK(frameCountFor(0.0, 0.010, 44100) == 441); + CHECK(msFlooredEndFrameCount(0.0, 0.010, 44100) == 441); +} + +static void testASubMillisecondStartWouldNotHideItself() { + // Both observations started at 0.000000s, the one value that hides a start-side + // truncation. A window whose START carries a sub-millisecond remainder counts from + // that exact start... + const double start = 1.0001724, end = 2.0001724; + CHECK(frameCountFor(start, end, 48000) == 48000); + // ...so a start floored to the millisecond would print a DIFFERENT count — 8 frames + // more, the same remainder the second observation lost off its end. A start-side + // truncation is therefore visible to the same frame-count gate, not silent. + CHECK(frameCountFor(1.000, end, 48000) == 48008); + CHECK(!renderHonoredBounds(frameCountFor(start, end, 48000), + frameCountFor(1.000, end, 48000))); +} + +// --- describeBoundsDrift: the read-back's verdict ------------------------------ + +static void testBoundsThatReadBackUnchangedDescribeNothing() { + // The answer that proves the request crossed into REAPER intact — including for a + // window whose end is nowhere near a millisecond boundary. + CHECK(describeBoundsDrift(0.0, 4.067797, 0.0, 4.067797, 48000).empty()); + CHECK(describeBoundsDrift(1.0001724, 2.0001724, 1.0001724, 2.0001724, 48000).empty()); +} + +static void testADriftedEndNamesBothWindowsAndBothCounts() { + const std::string s = + describeBoundsDrift(0.0, 4.067797, 0.0, 4.067, 48000); + CHECK(!s.empty()); + // Both counts as literals from the DAW observation, not re-derived from the same + // functions the sentence was built with. + CHECK(contains(s, "195254")); // what the request asks for + CHECK(contains(s, "195216")); // what the drifted window would hold + CHECK(contains(s, "48000 Hz")); +} + +static void testTheReportPrintsEnoughDigitsToShowTheDrift() { + // A report whose two numbers print identically is evidence of nothing. Two ends a + // single ULP apart — far under the sixth decimal a shorter rendering would stop at + // — must still read as two different numbers. Pinned as the actual %.17g literals + // (not the needle the two ends share, "s)", which occurs at every precision and so + // proves nothing): a report that regressed to a shorter format like %.6g would + // print the same six significant digits for both ends, and these two `contains` + // checks would then fail. + const double asked = 4.067797; + const double stored = std::nextafter(asked, 5.0); + char askedBuf[32], storedBuf[32]; + std::snprintf(askedBuf, sizeof(askedBuf), "%.17g", asked); + std::snprintf(storedBuf, sizeof(storedBuf), "%.17g", stored); + CHECK(std::string(askedBuf) != std::string(storedBuf)); + + const std::string s = describeBoundsDrift(0.0, asked, 0.0, stored, 48000); + CHECK(!s.empty()); + CHECK(contains(s, askedBuf)); + CHECK(contains(s, storedBuf)); +} + +static void testADriftedStartIsCaughtToo() { + // The edge both observations could not test. + const std::string s = describeBoundsDrift(1.0001724, 2.0, 1.000, 2.0, 48000); + CHECK(!s.empty()); + CHECK(contains(s, "1.0001724")); +} + +static void testAnUnknownRateStillReportsTheDriftWithoutFrames() { + // A project that never pinned a rate reads 0. The drift is still worth saying; a + // frame count over an unknown rate is not. + const std::string s = describeBoundsDrift(0.0, 4.067797, 0.0, 4.067, 0); + CHECK(!s.empty()); + CHECK(!contains(s, "frames")); +} + int main() { testFrameCountIsExactNotRounded(); testFrameCountIsADifferenceOfIndicesNotADuration(); @@ -244,6 +389,17 @@ int main() { testSubFrameDriftStillPrintsTheSameFrames(); testUnknownRateFallsBackToExactEquality(); testMultiItemUnionExtent(); + testMillisecondFlooredEndReproducesBothShortRenders(); + testTheSixDecimalDisplayDidNotCreateTheEffect(); + testWindowAlreadyOnTheMillisecondGridLosesNothing(); + testOneFrameOfRemainderStillFloors(); + testMillisecondFloorAt44100WhereAMillisecondIsNotWholeFrames(); + testASubMillisecondStartWouldNotHideItself(); + testBoundsThatReadBackUnchangedDescribeNothing(); + testADriftedEndNamesBothWindowsAndBothCounts(); + testTheReportPrintsEnoughDigitsToShowTheDrift(); + testADriftedStartIsCaughtToo(); + testAnUnknownRateStillReportsTheDriftWithoutFrames(); if (g_fail) { std::printf("%d check(s) FAILED\n", g_fail); return 1; } std::printf("render_window: all checks passed\n");