diff --git a/docs/TCP_COMMANDS.md b/docs/TCP_COMMANDS.md index 9a686e77..e65796f4 100644 --- a/docs/TCP_COMMANDS.md +++ b/docs/TCP_COMMANDS.md @@ -737,6 +737,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 3dfc9c89..3055bb6c 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: 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]; + 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); +} + 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); @@ -16370,6 +16379,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/main.cpp b/runtime/src/main.cpp index cb5e61c7..8b00f22d 100644 --- a/runtime/src/main.cpp +++ b/runtime/src/main.cpp @@ -9263,6 +9263,15 @@ static NetplayVblankEpilogue sdl_vblank_present_body(void) { snprintf(title, sizeof(title), "%s [Game %.0f fps %.2fx]", s_fps_base_title.c_str(), fps, speed); } + { + 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, " [Present %.1f fps, %u hitch, max %u ms]", + sfps, hitch, worst); + } SDL_SetWindowTitle(sdl_window, title); } if (!g_headless) { 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; }