diff --git a/games/totk/tests/fps_overlay/probe.c b/games/totk/tests/fps_overlay/probe.c index 081c28a83..ec48c59de 100644 --- a/games/totk/tests/fps_overlay/probe.c +++ b/games/totk/tests/fps_overlay/probe.c @@ -205,6 +205,12 @@ int main(void) { sysload_busy b = {0, 0}; sysload_busy_add(&b, 1.0, 1.005); sysload_busy_add(&b, 1.005, 1.010); sysload_busy_add(&b, 1.008, 1.012); sysload_busy_add(&b, 1.020, 1.020); CHECK(b.busy > 0.01199 && b.busy < 0.01201, "busy union of 0-5, 5-10, 8-12 ms (and an empty one) is 12 ms, got %.6f s", b.busy); + CHECK(sysload_busy_add(&b, 0, 2943907.0) == 0 && sysload_busy_add(&b, 1.013, 0) == 0 && + sysload_busy_add(&b, 1.013, INFINITY) == 0 && sysload_busy_add(&b, NAN, 1.014) == 0 && + b.last_end == 1.012 && b.busy > 0.01199 && b.busy < 0.01201, + "missing or nonfinite Metal timestamps must not change the measured interval"); + CHECK(sysload_busy_add(&b, 1.013, 1.014) > 0 && b.last_end == 1.014, + "valid samples must remain usable after a missing timestamp"); pthread_t spin; pthread_create(&spin, NULL, spin_main, NULL); // a thread that keeps one core busy: the busiest thread sysload_start(); usleep(1200000); sysload_info real; int got = sysload_get(&real); diff --git a/games/totk/third_party/d2r_gfx/gfx/nvn/nvn_mtl.m b/games/totk/third_party/d2r_gfx/gfx/nvn/nvn_mtl.m index 7ebd0348f..d00c8ec4e 100644 --- a/games/totk/third_party/d2r_gfx/gfx/nvn/nvn_mtl.m +++ b/games/totk/third_party/d2r_gfx/gfx/nvn/nvn_mtl.m @@ -598,7 +598,10 @@ static void keepalive_start(void) { id cb = [q commandBuffer]; id ce = [cb computeCommandEncoder]; [ce setComputePipelineState:ps]; [ce setBuffer:buf offset:0 atIndex:0]; [ce setBytes:&iters length:4 atIndex:1]; [ce dispatchThreads:MTLSizeMake(32, 1, 1) threadsPerThreadgroup:MTLSizeMake(32, 1, 1)]; [ce endEncoding]; - [cb addCompletedHandler:^(id c) { double ms = (c.GPUEndTime - c.GPUStartTime) * 1e3; static int said; if (said++ < 3) MLOG("TOTK_GPU_KEEPALIVE:自旋核一次 %.3f ms", ms); + [cb addCompletedHandler:^(id c) { + double start = c.GPUStartTime, end = c.GPUEndTime; + if (!sysload_gpu_span_valid(start, end)) { dispatch_semaphore_signal(sem); return; } + double ms = (end - start) * 1e3; static int said; if (said++ < 3) MLOG("TOTK_GPU_KEEPALIVE:自旋核一次 %.3f ms", ms); uint64_t us = (uint64_t)(ms * 1e3); atomic_fetch_add(&g_ka_n, 1); atomic_fetch_add(&g_ka_us, us); uint64_t mx = atomic_load(&g_ka_max_us); while (us > mx && !atomic_compare_exchange_weak(&g_ka_max_us, &mx, us)) {} if (ms > 0.01) { double k = (target_us * .001) / ms; if (k > 2) k = 2; if (k < 0.5) k = 0.5; double m = n * k; n = m < 2000 ? 2000 : m > 400000 ? 400000 : (uint32_t)m; } dispatch_semaphore_signal(sem); }]; @@ -2236,9 +2239,8 @@ static void gpu_tag_done(uint64_t f, double g0, double g1) { sysload_note_gpu(g0, g1); // the load line's game GPU share unsigned k = f % GRING; if (atomic_load(&g_gpu[k].frame) != f) return; - double d = g1 - g0; - if (d > 0) { - atomic_fetch_add(&g_gpu[k].ns, (uint64_t)(d * 1e9)); + if (sysload_gpu_span_valid(g0, g1)) { + atomic_fetch_add(&g_gpu[k].ns, (uint64_t)((g1 - g0) * 1e9)); uint64_t s0 = (uint64_t)(g0 * 1e9), s1 = (uint64_t)(g1 * 1e9), v; for (v = atomic_load(&g_gpu[k].t0); s0 < v && !atomic_compare_exchange_weak(&g_gpu[k].t0, &v, s0);) {} for (v = atomic_load(&g_gpu[k].t1); s1 > v && !atomic_compare_exchange_weak(&g_gpu[k].t1, &v, s1);) {} @@ -2318,7 +2320,10 @@ static void mtl_sub_split(void) { if (f.error) { atomic_store(&g_last_commit_error, 1); static int n; if (n++ < 8) MLOG("【红】分段命令缓冲错误 fence=%llu:%s", (unsigned long long)fence_next, f.error.description.UTF8String); } }]; if (totk_tl_on()) { uint64_t tlid = totk_tl_commit_id(); totk_tl(TL_COMMIT, tlid, 0); - [o addFeedbackHandler:^(id fb) { totk_tl_at(TL_GPU, (uint64_t)(fb.GPUStartTime * 1e9), tlid, (uint64_t)(fb.GPUEndTime * 1e9)); }]; } + [o addFeedbackHandler:^(id fb) { + double start = fb.GPUStartTime, end = fb.GPUEndTime; + if (sysload_gpu_span_valid(start, end)) totk_tl_at(TL_GPU, (uint64_t)(start * 1e9), tlid, (uint64_t)(end * 1e9)); + }]; } id next = [g_dev newCommandBuffer]; if (early) { w0 = clock_gettime_nsec_np(CLOCK_UPTIME_RAW); rs_sync(); fc1 = fc0 + (clock_gettime_nsec_np(CLOCK_UPTIME_RAW) - w0); } // fc1 - fc0: the wait, as before [g_cb endCommandBuffer]; @@ -2423,7 +2428,10 @@ the texture dump (with DM_NVN_TEXSNAP_DEPTH=256x144 also the occluder depth). A pt_finish(g_cb, o, gpu_f); res_commit(); if (totk_tl_on()) { uint64_t tlid = totk_tl_commit_id(); totk_tl(TL_COMMIT, tlid, 1); totk_tl(TL_FENCE, tlid, fence); - [o addFeedbackHandler:^(id fb) { totk_tl_at(TL_GPU, (uint64_t)(fb.GPUStartTime * 1e9), tlid, (uint64_t)(fb.GPUEndTime * 1e9)); }]; } + [o addFeedbackHandler:^(id fb) { + double start = fb.GPUStartTime, end = fb.GPUEndTime; + if (sysload_gpu_span_valid(start, end)) totk_tl_at(TL_GPU, (uint64_t)(start * 1e9), tlid, (uint64_t)(end * 1e9)); + }]; } if (g_b2_capture >= 0) { // stereo B2: the left pass's picture for the headset (the presented texture as drawn, no MetalFX) const int eye = g_b2_capture; g_b2_capture = -1; if (g_b2_swap) g_b2_pics++; else g_b2_nopic++; @@ -3747,8 +3755,8 @@ static void up_flush(void) { ser_bump(0); // 让各贴图号的视图缓存走一次慢路(缓存命中时不会经过 tex_mtl,看不到这份记录) | Make the view cache of every texture ID take the slow path once (on a cache hit tex_mtl is not passed through and this record is not seen) MLOG("%s上传命令缓冲被丢:这一批 %lu 张贴图记为内容未写,下次取用时重传(待重传共 %d 张)", fake_lost ? "DM_TEST_UP_LOST:" : "", (unsigned long)dsts.count, n); } - double d = f.GPUEndTime - f.GPUStartTime; - if (d > 0) atomic_fetch_add(&g_upload_gpu_ns, (uint64_t)(d * 1e9)); + double start = f.GPUStartTime, end = f.GPUEndTime; + if (sysload_gpu_span_valid(start, end)) atomic_fetch_add(&g_upload_gpu_ns, (uint64_t)((end - start) * 1e9)); for (id obj in staging) res_remove(obj); [staging removeAllObjects]; for (int i = 0; i < UP_POOL; i++) if (pool_mask >> i & 1) atomic_store(&g_up_pool_busy[i], 0); diff --git a/games/totk/third_party/d2r_gfx/gfx/nvn/totk_sysload.c b/games/totk/third_party/d2r_gfx/gfx/nvn/totk_sysload.c index 309e3be2d..0493aba91 100644 --- a/games/totk/third_party/d2r_gfx/gfx/nvn/totk_sysload.c +++ b/games/totk/third_party/d2r_gfx/gfx/nvn/totk_sysload.c @@ -37,7 +37,7 @@ int sysload_enabled(void) { } double sysload_busy_add(sysload_busy *b, double s, double e) { - if (!(e > s)) return 0; + if (!sysload_gpu_span_valid(s, e)) return 0; double from = s > b->last_end ? s : b->last_end, add = e > from ? e - from : 0; if (e > b->last_end) b->last_end = e; b->busy += add; diff --git a/games/totk/third_party/d2r_gfx/gfx/nvn/totk_sysload.h b/games/totk/third_party/d2r_gfx/gfx/nvn/totk_sysload.h index 81ec61b69..fb8cf4b53 100644 --- a/games/totk/third_party/d2r_gfx/gfx/nvn/totk_sysload.h +++ b/games/totk/third_party/d2r_gfx/gfx/nvn/totk_sysload.h @@ -1,6 +1,7 @@ // TOTK: system load for the bottom-right line (user, 2026-10-16 night: the load of every CPU core and of the GPU, on one // line like the FPS counter). See totk_sysload.c. #pragma once +#include #include #include @@ -18,6 +19,10 @@ int sysload_get(sysload_info *out); // 0 before the first sample void sysload_note_gpu(double start_s, double end_s); // one of our command buffers ran on the GPU (CACurrentMediaTime seconds) /* "P 55% 56% 55% 53% E 43% 29% 20% 12% 9% 7% T 47% GPU 99% (70%)" (T: the busiest thread) */ void sysload_text(char *out, size_t n, const sysload_info *in); +/* Missing Metal timestamps can be zero independently, even on completion. */ +static inline int sysload_gpu_span_valid(double start_s, double end_s) { + return start_s > 0 && end_s > start_s && isfinite(end_s); +} /* Busy time of command buffers that complete in order: the union of their [start, end) intervals. Returns what was added. */ typedef struct { double last_end, busy; } sysload_busy; double sysload_busy_add(sysload_busy *b, double start_s, double end_s);