From a710c6193df63ea4aa5c300bb2776e50ff9b8ac4 Mon Sep 17 00:00:00 2001 From: epicmeme546 <178368684+epicmeme546@users.noreply.github.com> Date: Tue, 10 Nov 2026 06:35:28 -0500 Subject: [PATCH] Measure the overlay's frame times where the pictures go out, and keep an early picture back to seven eighths of the recent interval. The overlay's rate, lows and graph were fed from the game's present calls. With the replay on the submitting thread, work held for an event is replayed with the next frame, so the calls come in uneven steps although each frame costs about the same: 36 and 9 ms apart in turn in a village, a sawtooth in the graph and a 1% low of 14 fps, while the pictures went out 19.7 ms apart on average and changed by 3.9 ms from one to the next. The present thread now reports each picture (perfmon_shown) and the overlay takes its intervals from that; the game's calls are used only where no present thread reports. A picture that arrives early now waits until seven eighths of the recent interval have passed since the one before (vk_backend.c pace_smooth, NX2NAVI_PACE_SMOOTH, on by default; not with frame generation). In that village the change from one picture to the next went from 3.9 to 3.0 ms at 51 fps both ways, in a graveyard from 3.1 to 2.6 ms at 54. Co-Authored-By: Claude Fable 5.2 --- docs/releases/v0.6.2.md | 6 ++++++ runtime/cfg.c | 5 +++-- runtime/perfmon.c | 33 +++++++++++++++++++++++++++++++++ runtime/perfmon.h | 2 ++ runtime/vk_backend.c | 27 ++++++++++++++++++++++++++- 5 files changed, 70 insertions(+), 3 deletions(-) diff --git a/docs/releases/v0.6.2.md b/docs/releases/v0.6.2.md index f42d72a4..14543bb9 100644 --- a/docs/releases/v0.6.2.md +++ b/docs/releases/v0.6.2.md @@ -34,6 +34,12 @@ Draft notes for the release after 0.6.1. The version number is not decided yet. the same way, and this game rewrites them without waiting for the GPU. In that graveyard and a village, 3,000 frames in a row were clean with the option off, against a dark frame about every 364 with it on. The menu says so now. It gave 15 to 18 percent more frames per second in those two places. +- The performance overlay's frame rate, lows and frame-time graph are now those of the pictures as they reach the + screen. They were measured at the game's present calls, which come in uneven steps although the pictures do not: in + a village the calls came 36 and 9 ms apart in turn and the graph drew a sawtooth with a 1% low of 14 fps, while the + pictures went out about 20 ms apart and changed by 4 ms from one to the next. A picture that arrives early now also + waits until seven eighths of the recent interval have passed (`pace_smooth`, on by default), which brought that + change down to 3 ms at the same frame rate. - The performance overlay's VRAM line counted every program on the graphics card as if it were the game. It now shows the game's own share beside it (*game 6.8 GB* of 10.4 GB in use, in one measurement). - Textures out of view for 11 seconds at 165 fps (30 seconds were meant) could show a blank stand-in for a few frames diff --git a/runtime/cfg.c b/runtime/cfg.c index e82a85d2..5482fe9d 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -47,7 +47,7 @@ static const char *const known[] = { "HDR_EXPOSURE", "HDR_FLARE", "HDR_GAMMA", "HDR_GAME_NITS", "HDR_GRADE", "HDR_HIGHLIGHT_SAT", "HDR_HIGHLIGHTS", "HDR_HUE", "HDR_LIVE", "HDR_PEAK_NITS", "HDR_SATURATION", "HDR_SHADOWS", "HDR_TONEMAP", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "LANG", "LIB_PREBUILD", - "BIG_BUILD_FREE_MB", "BIG_BUILD_MAX_MB", "LIB_PREBUILD_BIG", "LIB_PREBUILD_BIG_BG", "LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LIB_WARM", "PIPE_LINK_MS", "FRAME_HOLD_MS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MEM_CENSUS", "MENU_LANG", "MENU_SOUND_VOLUME", "MENU_TEST", + "BIG_BUILD_FREE_MB", "BIG_BUILD_MAX_MB", "LIB_PREBUILD_BIG", "LIB_PREBUILD_BIG_BG", "LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LIB_WARM", "PIPE_LINK_MS", "FRAME_HOLD_MS", "PACE_SMOOTH", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MEM_CENSUS", "MENU_LANG", "MENU_SOUND_VOLUME", "MENU_TEST", "MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_CLICK", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MICROPHONE", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MONITOR", "MOTION", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_HOST", "MOVIE_PACE", "MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK", "MV_SURVEY", @@ -114,7 +114,7 @@ static const struct { const char *key, *section; } sections[] = { {"POOL_CREATE_MS", "performance"}, {"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"}, {"ONESHOT_BUDGET", "performance"}, {"CACHE_DIE", "performance"}, {"ONE_PER_CORE", "performance"}, - {"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"LIB_WARM", "performance"}, {"PIPE_LINK_MS", "performance"}, {"FRAME_HOLD_MS", "performance"}, {"PIPE_PREBUILD", "performance"}, + {"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"LIB_WARM", "performance"}, {"PIPE_LINK_MS", "performance"}, {"FRAME_HOLD_MS", "performance"}, {"PACE_SMOOTH", "performance"}, {"PIPE_PREBUILD", "performance"}, {"COMPUTE_KEEP", "performance"}, {"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"}, {"TEX_ALIAS", "performance"}, {"TEX_DISUSE", "performance"}, {"TEX_GEN_UNDO", "performance"}, {"TEX_REUSE", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"TEX_PROC_COMMIT_MB", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"}, @@ -216,6 +216,7 @@ static const CfgMeta metas[] = { {"PIPE_WAIT", "default", "default|draw", RESTART}, {"PIPE_SKIP", "any", "strict|any|fullscreen", RESTART}, {"PIPE_LINK_MS", "6", "0..100", RESTART}, // vk_exec_draw.c link_budget {"FRAME_HOLD_MS", "120", "0..500", RESTART}, // vk_backend.c frame_hold + {"PACE_SMOOTH", "1", "0|1", RESTART}, // vk_backend.c pace_smooth {"LIB_PREBUILD", "1", "0|1", RESTART}, {"LIB_PREBUILD_THREADS", "", "1..64", RESTART}, {"LIB_WARM", "1", "0|1|force", RESTART}, // vk_pipeline.c lw_start: the driver's cache of stage libraries, warmed when cold {"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "1", "0|1", RESTART}, diff --git a/runtime/perfmon.c b/runtime/perfmon.c index 0c1065ba..63d141ef 100644 --- a/runtime/perfmon.c +++ b/runtime/perfmon.c @@ -43,9 +43,42 @@ static struct { int64_t up0, up; } g_win; +// The frame times the overlay shows (the rate, the lows, the graph) are those of the pictures as they go out: the +// present thread reports each one (perfmon_shown). The game's present calls come at other times: with the replay on +// the submitting thread, work held for an event is replayed in the next frame's first submission, so the calls came +// 36 and 9 ms apart in turn in a village while the pictures went out every 17 to 27 ms, and the graph drew a sawtooth +// the screen did not show. A frame held back (vk_backend.c frame_hold) is not shown and lengthens its interval. +// Without a present thread reporting (another back end) the game's present calls are taken, as before. +static uint64_t g_shown_last; // when the last picture went out; 0: none yet +static uint64_t g_shown_report; // when perfmon_shown last ran: perfmon_frame leaves the intervals to it for a second + +static void interval_add(uint64_t now, uint64_t interval_ns) { // (lock held) + g_ring[g_ring_pos] = (float)(interval_ns / 1e6); + g_ring_at[g_ring_pos] = now; + g_ring_pos = (g_ring_pos + 1) % PERF_HISTORY; + if (g_ring_n < PERF_HISTORY) g_ring_n++; + if (now - g_low_t0 >= 1000000000ull) { // the next second: its slice starts empty + g_low_cur = (g_low_cur + 1) % LOW_SLICES; + memset(g_low_hist[g_low_cur], 0, sizeof g_low_hist[0]); + g_low_t0 = now; + } + uint64_t bin = interval_ns / 250000ull; + uint16_t *h = &g_low_hist[g_low_cur][bin < LOW_BINS ? bin : LOW_BINS - 1]; + if (*h < 65535) (*h)++; +} + +void perfmon_shown(uint64_t now) { + plat_mutex_lock(&g_lock); + // a gap of a second or more is a load or a pause, not a frame time + if (g_shown_last && now > g_shown_last && now - g_shown_last < 1000000000ull) interval_add(now, now - g_shown_last); + g_shown_last = g_shown_report = now; + plat_mutex_unlock(&g_lock); +} + void perfmon_frame(uint64_t now, uint64_t interval_ns, const uint64_t *wait_ns, uint64_t replay_ns, int64_t tex_uploads) { plat_mutex_lock(&g_lock); + if (g_shown_report && now - g_shown_report < 1000000000ull) interval_ns = 0; // perfmon_shown has them if (interval_ns) { g_ring[g_ring_pos] = (float)(interval_ns / 1e6); g_ring_at[g_ring_pos] = now; diff --git a/runtime/perfmon.h b/runtime/perfmon.h index fd0ac1bb..bb56709b 100644 --- a/runtime/perfmon.h +++ b/runtime/perfmon.h @@ -50,6 +50,8 @@ void perfmon_set_vram_game(float (*used_mb)(void)); // A game frame was presented (perf_present, the render thread, only while the overlay is on): the time since the last // one, the render thread's waits of this frame (PERF_N entries, host.h), its replay time and the running count of // texture uploads. +// A picture went out (the present thread, vk_backend.c): the overlay's frame times are the intervals between these. +void perfmon_shown(uint64_t now_ns); void perfmon_frame(uint64_t now_ns, uint64_t interval_ns, const uint64_t *wait_ns, uint64_t replay_ns, int64_t tex_uploads); // The latest numbers (the overlay, present path). diff --git a/runtime/vk_backend.c b/runtime/vk_backend.c index c33bedf8..89e19675 100644 --- a/runtime/vk_backend.c +++ b/runtime/vk_backend.c @@ -32,6 +32,7 @@ #include "platform/platform.h" #include "vk_dlaa.h" #include "vk_dlssg.h" +#include "vk_fg.h" #define WEAK __attribute__((weak)) @@ -49,6 +50,28 @@ static void wait_until(uint64_t at) { while (kern_now_ns() < at) plat_yield(); } +// NX2NAVI_PACE_SMOOTH (default 1; 0 = off): a picture goes out no sooner than seven eighths of the recent interval +// after the one before it. The game's frames reach the present thread in uneven steps although each costs about the +// same: with the replay on the submitting thread, work held for an event is replayed with the next frame, so pictures +// came 13 to 31 ms apart around a mean of 22 ms in a village (5th to 95th percentile), which shows as judder at a +// frame rate that would look even. Only the early ones wait, a few milliseconds each, so the rate stays what it was; +// the recent interval is a moving average over about four pictures of intervals under 100 ms, so the floor follows +// a scene that gets lighter within a second. Not with frame generation, which paces its own presents. +static uint64_t pace_avg_ns; // the present thread's + +static uint64_t pace_smooth(uint64_t at, uint64_t last, uint64_t now) { + static int on = -1; + if (on < 0) on = cfg_bool("NX2NAVI_PACE_SMOOTH"); + if (!on || !last || !pace_avg_ns || vk_fg_active() || vk_dlssg_active()) return at; + uint64_t floor = last + pace_avg_ns / 8 * 7; + return floor > at && floor < now + 50000000ull ? floor : at; +} + +static void pace_shown(uint64_t interval_ns) { + if (interval_ns >= 100000000ull) return; // a load or a hitch is not the rate + pace_avg_ns = pace_avg_ns ? pace_avg_ns + (uint64_t)(((int64_t)interval_ns - (int64_t)pace_avg_ns) / 4) : interval_ns; +} + static void present_main(void *arg) { (void)arg; uint64_t last = 0, last_queued = 0; @@ -72,9 +95,11 @@ static void present_main(void *arg) { plat_cond_broadcast(&pcv); plat_mutex_unlock(&pl); uint64_t t0 = kern_now_ns(); - wait_until(j.at); + wait_until(pace_smooth(j.at, last, t0)); uint64_t t1 = kern_now_ns(); + if (last) pace_shown(t1 - last); vk_present_flip(); + perfmon_shown(t1); // the overlay's frame times: the pictures as shown // NX2NAVI_TRACE=present: one line per present: the host present's interval, the interval of the game's present // calls (when the host present went out before back-edge pacing), the wait and how far past its target it went if (rt_trace & RT_TRACE_PRESENT)