Prove the render bounds at the boundary they cross, and name a short render whose count is exactly a millisecond-floored window
No truncation exists on our side of that boundary, so the read-back is the only evidence available for whether REAPER kept the window — and it fires on every tail mode, where only None was ever judged.
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "core/capture/render_window.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
@@ -15,6 +16,22 @@ long long frameIndexAt(double seconds, int sampleRate) {
|
||||
return std::llround(seconds * static_cast<double>(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,29 @@ 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 {};
|
||||
|
||||
std::string s = "REAPER did not keep the render bounds 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
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#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 say where a short render lost its frames.
|
||||
// NO REAPER types; unit-tested by tests/test_render_window.cpp.
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace reasampler::capture {
|
||||
|
||||
// Frames the [startSeconds, endSeconds) window occupies at `sampleRate`. Both
|
||||
@@ -42,4 +45,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 (0.029 * 1000 lands just below 29) 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
|
||||
|
||||
@@ -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,18 @@ CaptureResult OfflineRenderBackend::capture(const CaptureRequest& request) {
|
||||
GetSetProjectInfo(proj, "RENDER_SRATE",
|
||||
static_cast<double>(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.
|
||||
{
|
||||
const std::string drift =
|
||||
describeBoundsDrift(request.startSeconds, request.endSeconds,
|
||||
storedStart, storedEnd, effectiveSampleRate);
|
||||
if (!drift.empty())
|
||||
ShowConsoleMsg(("ReaSampler capture: " + drift + "\n").c_str());
|
||||
}
|
||||
|
||||
GetSetProjectInfo(proj, "RENDER_CHANNELS",
|
||||
static_cast<double>(request.channelCount), true);
|
||||
|
||||
|
||||
@@ -99,6 +99,17 @@ BoundsVerdict checkRenderedBounds(const std::string& renderedPath,
|
||||
const long long actualFrames = static_cast<long long>(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, so every refusal from here on adds to (or breaks) that
|
||||
// evidence instead of needing the arithmetic done by hand. A count coincidence
|
||||
// only — it does not establish how the render resolved anything.
|
||||
const std::string msNote =
|
||||
actualFrames == msFlooredEndFrameCount(request.startSeconds,
|
||||
request.endSeconds, rate)
|
||||
? " 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 +119,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
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,131 @@ 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. One frame at 48 kHz is 20.8 us
|
||||
// — four orders of magnitude above the nanosecond tolerance.
|
||||
const double oneFrame = 1.0 / 48000.0;
|
||||
CHECK(frameCountFor(0.0, 1.0 + oneFrame, 48000) == 48001);
|
||||
CHECK(msFlooredEndFrameCount(0.0, 1.0 + oneFrame, 48000) == 48000);
|
||||
}
|
||||
|
||||
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.
|
||||
const double asked = 4.067797;
|
||||
const double stored = std::nextafter(asked, 5.0);
|
||||
const std::string s = describeBoundsDrift(0.0, asked, 0.0, stored, 48000);
|
||||
CHECK(!s.empty());
|
||||
CHECK(!contains(s, "4.067797s, read back [0s, 4.067797s)"));
|
||||
}
|
||||
|
||||
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 +374,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");
|
||||
|
||||
Reference in New Issue
Block a user