From b6b3578282a997e61cc74d35c3587043b5b77596 Mon Sep 17 00:00:00 2001 From: yacineolivier-maker <234833416+yacineolivier-maker@users.noreply.github.com> Date: Tue, 20 Oct 2026 21:58:44 +0200 Subject: [PATCH 1/2] Debug: perf command, the cadence of real swaps (what the player sees) The GL swap records the interval between real swaps (ring of 4096, single writer). TCP {"cmd":"perf","window_ms":N,"reset":0|1} returns count, fps, mean / p50 / p95 / p99 / max interval and hitches (intervals over 1.5x the median) for the last N ms; independent of guest vblank counting, so frame generation, pacing and driver stalls are all visible. PSX_FPS_TELEMETRY adds the same summary to the window title. Used to measure R4 runs on a test bench (60.0 fps, p99 18 ms after the cadence fix of the video-standard switch). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Rkyma8rbyPiccs9n2bkyG6 --- runtime/src/debug_server.c | 10 ++++++ runtime/src/gpu_gl_renderer.c | 66 +++++++++++++++++++++++++++++++++++ runtime/src/main.cpp | 8 +++++ 3 files changed, 84 insertions(+) diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 553bcc1b..5385c739 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -9447,6 +9447,15 @@ static void handle_ws_aspect(int id, const char *json) } extern int psx_debug_display_aspect(int num, int den, int adaptive); +/* perf window_ms= reset=<0|1> — real swap cadence (what the player sees). */ +extern int gl_renderer_swap_stats(int window_ms, int reset, char *out, int cap); +static void handle_perf(int id, const char *json) { + char buf[512]; + gl_renderer_swap_stats(json_get_int(json, "window_ms", 0), json_get_int(json, "reset", 0), + buf, sizeof buf); + send_fmt("{\"id\":%d,\"ok\":true,%s}", id, buf); +} + static void handle_display_aspect(int id, const char *json) { int num=json_get_int(json,"num",-1), den=json_get_int(json,"den",-1); int adaptive=json_get_int(json,"adaptive",0); @@ -16315,6 +16324,7 @@ static const CmdEntry s_commands[] = { { "kernel_bless", handle_kernel_bless }, { "ws_aspect", handle_ws_aspect }, { "display_aspect", handle_display_aspect }, + { "perf", handle_perf }, { "window_size", handle_window_size }, { "ws_nw", handle_ws_nw }, { "debug_key", handle_debug_key }, diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 15a853a2..4a17576f 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -9764,6 +9764,7 @@ static int pt_cb_ctx(void *user, int current) { /* The one owned swap call: the composing thread's (direct) or the present * thread's. */ +static void gl_swap_stats_note(void); static void gl_window_swap(void) { #if defined(PSX_SDL3) psx_video_telemetry_present(SDL_GL_SwapWindow(s_win) ? 1 : 0); @@ -9771,6 +9772,71 @@ static void gl_window_swap(void) { SDL_GL_SwapWindow(s_win); psx_video_telemetry_present(-1); // SDL2 exposes no swap result. #endif + gl_swap_stats_note(); +} + +/* Swap timing (debug `perf`): intervals between real swaps, i.e. what reaches + * the screen, independent of guest vblank counting. Single writer (the swap + * thread); the reader tolerates a torn sample. */ +#define SWAP_RING 4096 +static uint32_t s_swap_us[SWAP_RING]; +static volatile uint32_t s_swap_n; +static uint64_t s_swap_last; + +static void gl_swap_stats_note(void) { + uint64_t now = SDL_GetPerformanceCounter(); + if (s_swap_last) { + uint64_t us = (now - s_swap_last) * 1000000ull / SDL_GetPerformanceFrequency(); + s_swap_us[s_swap_n % SWAP_RING] = us > 0xFFFFFFFFull ? 0xFFFFFFFFu : (uint32_t)us; + s_swap_n++; + } + s_swap_last = now; +} + +static int cmp_u32(const void *a, const void *b) { + uint32_t x = *(const uint32_t *)a, y = *(const uint32_t *)b; + return x < y ? -1 : x > y; +} + +/* Last `window_ms` of swaps (0 = whole ring): count, mean/p50/p95/p99/max + * interval (µs), and hitches = intervals > 1.5x the median. reset=1 drops the + * history. */ +typedef struct { uint32_t k, mean, p50, p95, p99, max, hitch; double span_ms, fps; } SwapStats; +static SwapStats swap_stats(int window_ms, int reset) { + static uint32_t tmp[SWAP_RING]; + SwapStats r = {0}; + uint32_t n = s_swap_n, k = 0; + uint64_t sum = 0; + if (n > SWAP_RING) n = SWAP_RING; + for (uint32_t i = 0; i < n; i++) { + uint32_t v = s_swap_us[(s_swap_n - 1 - i) % SWAP_RING]; + if (window_ms > 0 && sum + v > (uint64_t)window_ms * 1000u) break; + sum += v; + tmp[k++] = v; + } + if (reset) { s_swap_n = 0; s_swap_last = 0; } + if (!k) return r; + qsort(tmp, k, sizeof tmp[0], cmp_u32); + r.k = k; r.mean = (uint32_t)(sum / k); r.p50 = tmp[k / 2]; + r.p95 = tmp[(k - 1) * 95 / 100]; r.p99 = tmp[(k - 1) * 99 / 100]; r.max = tmp[k - 1]; + for (uint32_t i = 0; i < k; i++) if (tmp[i] * 2 > r.p50 * 3) r.hitch++; + r.span_ms = sum / 1000.0; + r.fps = sum ? k * 1e6 / (double)sum : 0.0; + return r; +} + +int gl_renderer_swap_stats(int window_ms, int reset, char *out, int cap) { + SwapStats r = swap_stats(window_ms, reset); + return snprintf(out, cap, + "\"swaps\":%u,\"span_ms\":%.1f,\"fps\":%.2f,\"mean_us\":%u,\"p50_us\":%u," + "\"p95_us\":%u,\"p99_us\":%u,\"max_us\":%u,\"hitches\":%u", + r.k, r.span_ms, r.fps, r.mean, r.p50, r.p95, r.p99, r.max, r.hitch); +} + +/* FPS telemetry title: what reached the screen over the last second. */ +void gl_renderer_swap_summary(double *fps, unsigned *hitches, unsigned *max_ms) { + SwapStats r = swap_stats(1000, 0); + *fps = r.fps; *hitches = r.hitch; *max_ms = r.max / 1000u; } static void *pt_cb_present(void *user, int slot, void *ready) { diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index fc425137..c0b8ace7 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -650,6 +650,7 @@ static void present_session_reset(void) { smooth_60_reset(); } +extern "C" void gl_renderer_swap_summary(double *fps, unsigned *hitches, unsigned *max_ms); static int fps_telemetry_enabled(void) { if (s_fps_telemetry_enabled < 0) { const char *e = std::getenv("PSX_FPS_TELEMETRY"); @@ -9262,6 +9263,13 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { snprintf(title, sizeof(title), "%s [Game %.0f fps %.2fx]", s_fps_base_title.c_str(), fps, speed); } + if (g_gl_active) { /* real swaps: what the player sees */ + double sfps = 0; unsigned hitch = 0, worst = 0; + gl_renderer_swap_summary(&sfps, &hitch, &worst); + size_t l = strlen(title); + snprintf(title + l, sizeof(title) - l, " [Screen %.1f fps, %u hitch, max %u ms]", + sfps, hitch, worst); + } SDL_SetWindowTitle(sdl_window, title); } if (!g_headless) { From 84ba1851761d571f825cc1d934cf77262a838bd4 Mon Sep 17 00:00:00 2001 From: Matthew Stanley <1379tech@gmail.com> Date: Sat, 24 Oct 2026 08:32:29 -0700 Subject: [PATCH 2/2] Publish cadence through shared video telemetry without reader races Use a bounded atomic snapshot and presenter-owned reset across all backends. Readers sort their own snapshots; report host API completion cadence. Add deterministic and concurrent reader/reset coverage. --- docs/TCP_COMMANDS.md | 5 +- runtime/include/video_telemetry.h | 12 ++++ runtime/src/debug_server.c | 6 +- runtime/src/gpu_gl_renderer.c | 66 --------------------- runtime/src/main.cpp | 11 ++-- runtime/src/video_telemetry.cpp | 79 ++++++++++++++++++++++++++ runtime/tests/test_video_telemetry.cpp | 36 ++++++++++++ 7 files changed, 139 insertions(+), 76 deletions(-) diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index c81a2eeb..ec096bd6 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -493,9 +493,9 @@ between Play and TCP availability. ## Complete command index (generated) -**386 commands registered** — 373 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). +**387 commands registered** — 374 on the native server (`runtime/src/debug_server.c`), 61 on the Beetle server (`runtime/src/beetle_debug_server.c`). -85 of 386 have prose above; **301 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. +85 of 387 have prose above; **302 are index-only**. An index-only command still works — it just has no description here yet. Send it `{"cmd":""}` and read the reply, or find its `handle_*` function in the server source. Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this block has drifted from the code. @@ -736,6 +736,7 @@ Regenerate with `python tools/gen_tcp_commands.py`; `--check` fails if this bloc | `pc_probe_arm` | ✓ | | | | `pc_probe_clear` | ✓ | | | | `pc_probe_dump` | ✓ | | | +| `perf` | ✓ | | | | `pgxp` | ✓ | | ✓ | | `pgxp_miss_ring` | ✓ | | | | `pgxp_shadow` | ✓ | | | diff --git a/runtime/include/video_telemetry.h b/runtime/include/video_telemetry.h index 55ae2753..4b9e6cc4 100644 --- a/runtime/include/video_telemetry.h +++ b/runtime/include/video_telemetry.h @@ -30,6 +30,18 @@ void psx_video_telemetry_present(int result); * Completion/error counters remain available in that case. */ void psx_video_telemetry_snapshot(PsxVideoTelemetry *out); +typedef struct PsxPresentCadence { + int valid; + uint32_t count, mean_us, p50_us, p95_us, p99_us, max_us, hitches; + double span_ms, fps; +} PsxPresentCadence; +/* Host API completion intervals, shared by all backends; no monitor-display + * guarantee. Bounded passive snapshot. reset is consumed by the presenter. */ +void psx_video_telemetry_cadence(int window_ms, int reset, PsxPresentCadence *out); +int psx_video_telemetry_cadence_json(int window_ms, int reset, char *out, int capacity); +/* Explicit monotonic host microseconds for adapters and deterministic tests. */ +void psx_video_telemetry_present_at(int result, uint64_t now_us); + /* Emulation-owner settings after runtime mod overrides, sampled every VBlank. * Actual allocated scale/dimensions come from the separate backend snapshot. */ typedef struct PsxVideoConfigTelemetry { diff --git a/runtime/src/debug_server.c b/runtime/src/debug_server.c index 5385c739..211f7b9b 100644 --- a/runtime/src/debug_server.c +++ b/runtime/src/debug_server.c @@ -9447,11 +9447,11 @@ static void handle_ws_aspect(int id, const char *json) } extern int psx_debug_display_aspect(int num, int den, int adaptive); -/* perf window_ms= reset=<0|1> — real swap cadence (what the player sees). */ -extern int gl_renderer_swap_stats(int window_ms, int reset, char *out, int cap); +/* perf: host API completion cadence across the active presentation backend. */ +extern int psx_video_telemetry_cadence_json(int window_ms, int reset, char *out, int cap); static void handle_perf(int id, const char *json) { char buf[512]; - gl_renderer_swap_stats(json_get_int(json, "window_ms", 0), json_get_int(json, "reset", 0), + psx_video_telemetry_cadence_json(json_get_int(json, "window_ms", 0), json_get_int(json, "reset", 0), buf, sizeof buf); send_fmt("{\"id\":%d,\"ok\":true,%s}", id, buf); } diff --git a/runtime/src/gpu_gl_renderer.c b/runtime/src/gpu_gl_renderer.c index 4a17576f..15a853a2 100644 --- a/runtime/src/gpu_gl_renderer.c +++ b/runtime/src/gpu_gl_renderer.c @@ -9764,7 +9764,6 @@ static int pt_cb_ctx(void *user, int current) { /* The one owned swap call: the composing thread's (direct) or the present * thread's. */ -static void gl_swap_stats_note(void); static void gl_window_swap(void) { #if defined(PSX_SDL3) psx_video_telemetry_present(SDL_GL_SwapWindow(s_win) ? 1 : 0); @@ -9772,71 +9771,6 @@ static void gl_window_swap(void) { SDL_GL_SwapWindow(s_win); psx_video_telemetry_present(-1); // SDL2 exposes no swap result. #endif - gl_swap_stats_note(); -} - -/* Swap timing (debug `perf`): intervals between real swaps, i.e. what reaches - * the screen, independent of guest vblank counting. Single writer (the swap - * thread); the reader tolerates a torn sample. */ -#define SWAP_RING 4096 -static uint32_t s_swap_us[SWAP_RING]; -static volatile uint32_t s_swap_n; -static uint64_t s_swap_last; - -static void gl_swap_stats_note(void) { - uint64_t now = SDL_GetPerformanceCounter(); - if (s_swap_last) { - uint64_t us = (now - s_swap_last) * 1000000ull / SDL_GetPerformanceFrequency(); - s_swap_us[s_swap_n % SWAP_RING] = us > 0xFFFFFFFFull ? 0xFFFFFFFFu : (uint32_t)us; - s_swap_n++; - } - s_swap_last = now; -} - -static int cmp_u32(const void *a, const void *b) { - uint32_t x = *(const uint32_t *)a, y = *(const uint32_t *)b; - return x < y ? -1 : x > y; -} - -/* Last `window_ms` of swaps (0 = whole ring): count, mean/p50/p95/p99/max - * interval (µs), and hitches = intervals > 1.5x the median. reset=1 drops the - * history. */ -typedef struct { uint32_t k, mean, p50, p95, p99, max, hitch; double span_ms, fps; } SwapStats; -static SwapStats swap_stats(int window_ms, int reset) { - static uint32_t tmp[SWAP_RING]; - SwapStats r = {0}; - uint32_t n = s_swap_n, k = 0; - uint64_t sum = 0; - if (n > SWAP_RING) n = SWAP_RING; - for (uint32_t i = 0; i < n; i++) { - uint32_t v = s_swap_us[(s_swap_n - 1 - i) % SWAP_RING]; - if (window_ms > 0 && sum + v > (uint64_t)window_ms * 1000u) break; - sum += v; - tmp[k++] = v; - } - if (reset) { s_swap_n = 0; s_swap_last = 0; } - if (!k) return r; - qsort(tmp, k, sizeof tmp[0], cmp_u32); - r.k = k; r.mean = (uint32_t)(sum / k); r.p50 = tmp[k / 2]; - r.p95 = tmp[(k - 1) * 95 / 100]; r.p99 = tmp[(k - 1) * 99 / 100]; r.max = tmp[k - 1]; - for (uint32_t i = 0; i < k; i++) if (tmp[i] * 2 > r.p50 * 3) r.hitch++; - r.span_ms = sum / 1000.0; - r.fps = sum ? k * 1e6 / (double)sum : 0.0; - return r; -} - -int gl_renderer_swap_stats(int window_ms, int reset, char *out, int cap) { - SwapStats r = swap_stats(window_ms, reset); - return snprintf(out, cap, - "\"swaps\":%u,\"span_ms\":%.1f,\"fps\":%.2f,\"mean_us\":%u,\"p50_us\":%u," - "\"p95_us\":%u,\"p99_us\":%u,\"max_us\":%u,\"hitches\":%u", - r.k, r.span_ms, r.fps, r.mean, r.p50, r.p95, r.p99, r.max, r.hitch); -} - -/* FPS telemetry title: what reached the screen over the last second. */ -void gl_renderer_swap_summary(double *fps, unsigned *hitches, unsigned *max_ms) { - SwapStats r = swap_stats(1000, 0); - *fps = r.fps; *hitches = r.hitch; *max_ms = r.max / 1000u; } static void *pt_cb_present(void *user, int slot, void *ready) { diff --git a/runtime/src/main.cpp b/runtime/src/main.cpp index c0b8ace7..84775750 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -650,7 +650,6 @@ static void present_session_reset(void) { smooth_60_reset(); } -extern "C" void gl_renderer_swap_summary(double *fps, unsigned *hitches, unsigned *max_ms); static int fps_telemetry_enabled(void) { if (s_fps_telemetry_enabled < 0) { const char *e = std::getenv("PSX_FPS_TELEMETRY"); @@ -9263,11 +9262,13 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { snprintf(title, sizeof(title), "%s [Game %.0f fps %.2fx]", s_fps_base_title.c_str(), fps, speed); } - if (g_gl_active) { /* real swaps: what the player sees */ - double sfps = 0; unsigned hitch = 0, worst = 0; - gl_renderer_swap_summary(&sfps, &hitch, &worst); + { + PsxPresentCadence cadence{}; + psx_video_telemetry_cadence(1000, 0, &cadence); + const double sfps = cadence.fps; + const unsigned hitch = cadence.hitches, worst = cadence.max_us / 1000; size_t l = strlen(title); - snprintf(title + l, sizeof(title) - l, " [Screen %.1f fps, %u hitch, max %u ms]", + snprintf(title + l, sizeof(title) - l, " [Present %.1f fps, %u hitch, max %u ms]", sfps, hitch, worst); } SDL_SetWindowTitle(sdl_window, title); diff --git a/runtime/src/video_telemetry.cpp b/runtime/src/video_telemetry.cpp index c430194d..4f97b468 100644 --- a/runtime/src/video_telemetry.cpp +++ b/runtime/src/video_telemetry.cpp @@ -1,11 +1,23 @@ #include "video_telemetry.h" #include +#include +#include +#include +#include namespace { std::atomic presents{0}, errors{0}; std::atomic status_available{0}; std::atomic generation{0}; std::atomic fields[8]{}; +constexpr unsigned cadence_capacity = 4096; +std::atomic intervals[cadence_capacity]{}; +std::atomic cadence_version{0}, cadence_count{0}; +std::atomic cadence_reset{false}; +// Only the current present owner touches these. Readers request reset rather +// than changing the producer's cursor or timestamp. +uint64_t last_present_us; +bool have_last_present; } extern "C" void psx_video_telemetry_compose(int renderer, int scale, int sw, int sh, @@ -20,8 +32,75 @@ extern "C" void psx_video_telemetry_compose(int renderer, int scale, int sw, int } extern "C" void psx_video_telemetry_present(int result) { + const auto now = std::chrono::steady_clock::now().time_since_epoch(); + psx_video_telemetry_present_at(result, + std::chrono::duration_cast(now).count()); +} + +extern "C" void psx_video_telemetry_present_at(int result, uint64_t now_us) { status_available.store(result >= 0, std::memory_order_relaxed); (result != 0 ? presents : errors).fetch_add(1, std::memory_order_relaxed); + if (result == 0) return; + cadence_version.fetch_add(1); + if (cadence_reset.exchange(false)) { + cadence_count.store(0); + have_last_present = false; + } + if (have_last_present && now_us > last_present_us) { + const uint64_t count = cadence_count.load(); + const auto delta = std::min(now_us - last_present_us, UINT32_MAX); + intervals[count % cadence_capacity].store(static_cast(delta)); + cadence_count.store(count + 1); + } + last_present_us = now_us; + have_last_present = true; + cadence_version.fetch_add(1); +} + +extern "C" void psx_video_telemetry_cadence(int window_ms, int reset, PsxPresentCadence *out) { + if (!out) return; + *out = {}; + uint32_t values[cadence_capacity]; // each reader owns its sorting storage + uint64_t sum = 0; + for (unsigned attempt = 0; attempt < 3; ++attempt) { + out->count = 0; sum = 0; + const uint64_t before = cadence_version.load(); + if (before & 1u) continue; + if (cadence_reset.load()) { out->valid = 1; break; } + const uint64_t count = cadence_count.load(); + const unsigned n = static_cast(std::min(count, cadence_capacity)); + for (unsigned i = 0; i < n; ++i) { + const uint32_t value = intervals[(count - 1 - i) % cadence_capacity].load(); + if (window_ms > 0 && sum + value > static_cast(window_ms) * 1000) break; + sum += value; values[out->count++] = value; + } + if (before == cadence_version.load()) { out->valid = 1; break; } + } + if (reset) cadence_reset.store(true); + if (!out->valid) { *out = {}; return; } + if (!out->count) return; + std::sort(values, values + out->count); + const auto k = out->count; + out->mean_us = static_cast(sum / k); + out->p50_us = values[k / 2]; + out->p95_us = values[(k - 1) * 95 / 100]; + out->p99_us = values[(k - 1) * 99 / 100]; + out->max_us = values[k - 1]; + for (unsigned i = 0; i < k; ++i) + if (static_cast(values[i]) * 2 > static_cast(out->p50_us) * 3) + ++out->hitches; + out->span_ms = sum / 1000.0; + out->fps = sum ? k * 1e6 / static_cast(sum) : 0; +} + +extern "C" int psx_video_telemetry_cadence_json(int window_ms, int reset, char *out, int capacity) { + PsxPresentCadence r{}; + psx_video_telemetry_cadence(window_ms, reset, &r); + return std::snprintf(out, capacity, + "\"valid\":%s,\"measurement\":\"host_api_completion\",\"swaps\":%u,\"span_ms\":%.1f," + "\"fps\":%.2f,\"mean_us\":%u,\"p50_us\":%u,\"p95_us\":%u,\"p99_us\":%u,\"max_us\":%u,\"hitches\":%u", + r.valid ? "true" : "false", r.count, r.span_ms, r.fps, r.mean_us, + r.p50_us, r.p95_us, r.p99_us, r.max_us, r.hitches); } extern "C" void psx_video_telemetry_snapshot(PsxVideoTelemetry *out) { diff --git a/runtime/tests/test_video_telemetry.cpp b/runtime/tests/test_video_telemetry.cpp index f89c5013..0abfb90a 100644 --- a/runtime/tests/test_video_telemetry.cpp +++ b/runtime/tests/test_video_telemetry.cpp @@ -52,6 +52,42 @@ int main() { CHECK(sample.valid && sample.internal_scale == 10000 && sample.presents == 10002); CHECK(sample.present_status_available); CHECK(sample.present_errors == 1); + PsxPresentCadence cadence{}; + psx_video_telemetry_cadence(0,1,&cadence); + psx_video_telemetry_present_at(1,1000); + psx_video_telemetry_present_at(1,11000); + psx_video_telemetry_present_at(-1,21000); + psx_video_telemetry_present_at(1,51000); + psx_video_telemetry_cadence(0,0,&cadence); + CHECK(cadence.valid && cadence.count==3 && cadence.mean_us==16666); + CHECK(cadence.p50_us==10000 && cadence.max_us==30000 && cadence.hitches==1); + psx_video_telemetry_cadence(35,0,&cadence); + CHECK(cadence.valid && cadence.count==1 && cadence.max_us==30000); + psx_video_telemetry_cadence(0,1,&cadence); + psx_video_telemetry_cadence(0,0,&cadence); + CHECK(cadence.valid && !cadence.count); + psx_video_telemetry_present_at(1,100000); + psx_video_telemetry_cadence(0,0,&cadence); + CHECK(cadence.valid && !cadence.count); // producer reset re-anchors timing + std::atomic cadence_done{false}; + std::atomic cadence_consistent{true}; + std::thread presenter([&] { + for(uint64_t i=1;i<=20000;++i) psx_video_telemetry_present_at(1,100000+i*10000); + cadence_done=true; + }); + auto reader=[&](bool resets) { + unsigned reads=0; + do { + PsxPresentCadence c{}; + psx_video_telemetry_cadence(0,resets && (++reads%17==0),&c); + if(c.valid && c.count && (c.mean_us!=10000 || c.max_us!=10000 || c.hitches!=0)) + cadence_consistent=false; + }while(!cadence_done.load()); + }; + std::thread other_reader([&]{reader(true);}); + reader(false); + presenter.join();other_reader.join(); + CHECK(cadence_consistent.load()); psx_video_telemetry_snapshot(nullptr); return 0; }