From 27a034e9464ce81ab25ca188ec3408e9c72df8d1 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 18:55:40 -0500 Subject: [PATCH] Keep the 240 ceiling while RivaTuner Statistics Server holds the limit, and take the limit back after 200 ms with no host present. frame_step_cap_ns returned 0 once RTSS held the limit, so pace() set each slot to now and the acquire never waited. RTSS holds frames inside the host present, and a minimised window (gfx_present.c present_image skips the present) or a frame hold gives it none to hold: the game then ran with no limiter but pace_smooth's floor, which follows the intervals down, and the quick menu is where 640-700 fps read address 0 (frame_rate.c LIMIT_FREE_MAX_HZ). gfx_present.c flip_locked now stores the time of each host present that took its picture (gfx_window_hosted_ns), and rtss_decide.h rtss_pace_period gives the pacer the limit's period before the first host present and once none went out for 200 ms (as vk_lowlat.c HOLD_IDLE_NS), and the 240 ceiling's period, never 0, while RTSS holds. The frames RTSS lets through come further apart than the ceiling, so pace() waits for none of them; no new wait site. Not reproduced before the change. frame_step_check (new cases: holding with a host present 10 and 199 ms ago, 250 ms ago, none yet, not holding, 144 minimised, unlimited) and rtss_limit_check (rtss_pace_period) pass. Co-Authored-By: Claude Opus 5.5 --- runtime/frame_rate.c | 10 ++++++---- runtime/gfx.h | 1 + runtime/gfx_present.c | 7 +++++++ runtime/rtss_decide.h | 17 +++++++++++++++++ tests/frame_step_check.c | 30 ++++++++++++++++++++++++++++-- tests/rtss_limit_check.c | 14 ++++++++++++++ 6 files changed, 73 insertions(+), 6 deletions(-) diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index 933d3105..031f782a 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -274,8 +274,10 @@ double frame_step_game_hz(void) { return !frame_free_running() && !step_word && hz > 60 ? 60 : hz > LIMIT_MAX_HZ ? LIMIT_MAX_HZ : hz; } -// RivaTuner Statistics Server (rtss_limit.c) gets the same limit; while it limits this process the pacer holds no -// frames, so the two never stack. NX2NAVI_RTSS=0 and frame generation (RTSS would count the generated frames) keep it +// RivaTuner Statistics Server (rtss_limit.c) gets the same limit; while it limits this process the pacer holds the +// frames only to LIMIT_FREE_MAX_HZ, the ceiling the game's code needs, so the two never stack, and it takes the limit +// back once no picture went out for 200 ms (rtss_decide.h rtss_pace_period): a minimised window or a frame hold leaves +// RTSS no present to hold. NX2NAVI_RTSS=0 and frame generation (RTSS would count the generated frames) keep it // out, and so does a driver's low latency while it is in effect (vk_lowlat.c): the driver holds the limit where the // frame starts, and RTSS would hold the finished picture, the wait that mode is there to remove. RTSS gets the reason // (RTSS_OUT_*), for the log and for its check of a limit set in RTSS itself. @@ -328,8 +330,8 @@ uint64_t frame_step_cap_ns(void) { if (movie_hz) hz = movie_hz; paced_hz = hz; rtss_limit_want(rtss_out ? rtss_out : (int64_t)(hz * 1000 + 0.5)); - if (rtss_limit_active()) return 0; - return (uint64_t)(1e9 / hz + 0.5); + return rtss_pace_period(rtss_limit_active(), kern_now_ns(), gfx_window_hosted_ns(), (uint64_t)(1e9 / hz + 0.5), + (uint64_t)(1e9 / LIMIT_FREE_MAX_HZ + 0.5)); } // The limit as it is set, for the performance overlay: the setting, the monitor's rate for "display", 60 at diff --git a/runtime/gfx.h b/runtime/gfx.h index 14a85f0d..f67fefd6 100644 --- a/runtime/gfx.h +++ b/runtime/gfx.h @@ -26,6 +26,7 @@ void gfx_vsync_bind(uint64_t system_event); // nn::vi vsync event uint64_t gfx_vsync_ticks(void); // ticks since start uint64_t gfx_vsync_time_ns(uint64_t tick); // kern_now_ns() time of a tick (pacing) double gfx_window_refresh_hz(void); // refresh rate of the window's monitor; 0 unknown +uint64_t gfx_window_hosted_ns(void); // kern_now_ns() of the newest host present; 0 before one int gfx_window_focused(void); // gfx_present.c: who holds the present, queue and window-start locks, read without taking them (the hang watch) typedef struct { uint32_t present, present_depth, queue, queue_depth, start; } GfxLockHolders; diff --git a/runtime/gfx_present.c b/runtime/gfx_present.c index 138c3f6f..51debbb4 100644 --- a/runtime/gfx_present.c +++ b/runtime/gfx_present.c @@ -439,6 +439,12 @@ static VkPresentModeKHR pick_present_mode(VkPhysicalDevice pd) { const char *gfx_window_present_mode(void) { return __atomic_load_n(&g_present_got, __ATOMIC_ACQUIRE); } +// kern_now_ns when the newest host present returned with its picture taken (flip_locked); 0 before the first. A +// minimised window and a frame hold present nothing, so it ages: RTSS, which holds the frames inside the host present, +// then holds none, and the game's pacer takes the limit back (rtss_decide.h rtss_pace_period). +static uint64_t g_hosted_ns; +uint64_t gfx_window_hosted_ns(void) { return __atomic_load_n(&g_hosted_ns, __ATOMIC_ACQUIRE); } + // Queues the present of a blit vk_present_blit left waiting (g_pl held). A swapchain rebuild and the next blit flush it // first; the device release drops it. static void flip_locked(void) { @@ -459,6 +465,7 @@ static void flip_locked(void) { : vk_fg_active() ? vk_fg_present(&pi) : vkQueuePresentKHR(vk_ctx()->gfx_queue, &pi); vk_queue_unlock(); vk_lowlat_present_end(&ll); + if (r == VK_SUCCESS || r == VK_SUBOPTIMAL_KHR) __atomic_store_n(&g_hosted_ns, kern_now_ns(), __ATOMIC_RELEASE); vk_device_lost_check(r, "the present"); if (r == VK_ERROR_OUT_OF_DATE_KHR || r == VK_SUBOPTIMAL_KHR) plat_xchg32(&g_dirty, 1); } diff --git a/runtime/rtss_decide.h b/runtime/rtss_decide.h index d75c971a..96124a4d 100644 --- a/runtime/rtss_decide.h +++ b/runtime/rtss_decide.h @@ -211,6 +211,23 @@ static inline void rtss_why_text(char *out, size_t n, const RtssOut *o, int fail else snprintf(out, n, "RivaTuner Statistics Server has not hooked the game yet"); } +// ---- the game's pacer beside RTSS (frame_rate.c frame_step_cap_ns) +#define RTSS_HOLD_IDLE_NS 200000000ull // no host present for this long: RTSS holds nothing (vk_lowlat.c HOLD_IDLE_NS) + +// The period the pacer keeps (now_ns and hosted_ns are kern_now_ns times; hosted_ns of the newest host present, +// gfx_present.c gfx_window_hosted_ns, 0 before the first). The limit's own period while RTSS does not hold the game, +// before the first host present, and once no host present went out for RTSS_HOLD_IDLE_NS: a minimised window or a +// frame hold (vk_backend.c frame_hold) gives RTSS nothing to hold, and a pacer that held nothing then let the quick menu +// run at the 640-700 fps that read address 0 (frame_rate.c LIMIT_FREE_MAX_HZ). While RTSS holds, the ceiling's period +// and never 0: the frames RTSS lets through come further apart than that, so pace() waits for none of them, and it +// still holds the game's frames to the ceiling the game's code needs. +static inline uint64_t rtss_pace_period(int holds, uint64_t now_ns, uint64_t hosted_ns, uint64_t period, + uint64_t ceiling) { + uint64_t idle = now_ns > hosted_ns ? now_ns - hosted_ns : 0; + if (!holds || !hosted_ns || idle >= RTSS_HOLD_IDLE_NS) return period; + return ceiling < period ? ceiling : period; +} + // ---- RTSS's own settings // RTSS's own [Framerate] settings for the game, as its profile file holds them (rtss_limit.c read_settings): a frame // rate limit (Limit over LimitDenominator), a frame time limit (LimitTime over LimitTimeDenominator) and scanline sync diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index 5b733c72..01bee9dd 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -39,7 +39,10 @@ int vk_dlssg_multiplier(void) { return fg_mult; } int vk_dlssg_active(void) { return fg_mult > 1; } int vk_lowlat_on(void) { return 0; } void rtss_limit_want(int64_t millihz) { (void)millihz; } -int rtss_limit_active(void) { return 0; } +static int rtss_holds; // RivaTuner Statistics Server holds the game to the want (rtss_limit.c) +static uint64_t kern_ns, hosted; // kern_now_ns, and its time at the newest host present (0: none yet) +int rtss_limit_active(void) { return rtss_holds; } +uint64_t gfx_window_hosted_ns(void) { return hosted; } // The time manager the sound job's gate reads its step from (frame_rate.c sound_job_step), while the sound checks run: // [main+0x4c7e890] holds the slot, the slot the manager, the manager +0x10 the step. Nothing else reads. static int fake_manager; @@ -67,7 +70,7 @@ int mem_copy_readable(uint8_t *const *dst, const uint64_t (*src)[2], int n, uint (void)dst, (void)src, (void)n, (void)ok; return 0; } -uint64_t kern_now_ns(void) { return 0; } +uint64_t kern_now_ns(void) { return kern_ns; } // the floor watch (off here) int floor_watch_wanted(void) { return 0; } int floor_watch_found(const uint64_t *addr, const float *val, const int *unit, int n, const float link[3]) { @@ -280,6 +283,29 @@ int main(int argc, char **argv) { } fg_mult = 1; + // RivaTuner Statistics Server holds the limit: the pacer keeps the 240 ceiling while host presents go out, and the + // limit's own period before the first host present and once none went out for 200 ms (a minimised window, a frame + // hold), when RTSS has nothing to hold. + { + static const struct { const char *l; int holds; uint64_t hosted_ago; double hz; const char *what; } cases[] = { + {"60", 1, 10000000, 240, "RTSS holds, a host present 10 ms ago: the ceiling alone"}, + {"60", 1, 199000000, 240, "199 ms ago: still RTSS's"}, + {"60", 1, 250000000, 60, "250 ms ago: the limit back"}, + {"60", 1, 0, 60, "no host present yet: the limit"}, + {"60", 0, 10000000, 60, "RTSS does not hold: the limit"}, + {"144", 1, 250000000, 144, "144, minimised: 144"}, + {"unlimited", 1, 10000000, 240, "unlimited, RTSS holds: the ceiling is the limit"}}; + kern_ns = 50000000000ull; + for (unsigned i = 0; i < sizeof cases / sizeof *cases; i++) { + set_limit(cases[i].l); + rtss_holds = cases[i].holds; + hosted = cases[i].hosted_ago ? kern_ns - cases[i].hosted_ago : 0; + double hz = 1e9 / (double)frame_step_cap_ns(); + CHECK(fabs(hz - cases[i].hz) < 0.01, "%s: paces at %.3f fps, not %.0f", cases[i].what, hz, cases[i].hz); + } + rtss_holds = 0, hosted = kern_ns = 0; + } + // The water's ripples at 144 fps, a step of 60/144 frame: 64 water objects asked once a frame each run 600 steps // in 1440 frames (the cut gave none), and the day's seconds asked between them keep their own fraction. { diff --git a/tests/rtss_limit_check.c b/tests/rtss_limit_check.c index 2f1c2d20..b058845c 100644 --- a/tests/rtss_limit_check.c +++ b/tests/rtss_limit_check.c @@ -380,6 +380,19 @@ static void two(void) { CHECK(!rtss_two(60000, RTSS_FAIL_NOT_LIMITING, 1, 1), "RTSS seen not to hold the game: not counted"); } +// The period the game's pacer keeps beside RTSS: the ceiling while RTSS holds and host presents go out, the limit's own +// otherwise; never 0. +static void pace_period(void) { + const uint64_t p60 = 16666667, ceiling = 4166667, now = 50000000000ull; + CHECK(rtss_pace_period(1, now, now - 10000000, p60, ceiling) == ceiling, "holds, a host present 10 ms ago"); + CHECK(rtss_pace_period(1, now, now - RTSS_HOLD_IDLE_NS + 1, p60, ceiling) == ceiling, "just under 200 ms"); + CHECK(rtss_pace_period(1, now, now - RTSS_HOLD_IDLE_NS, p60, ceiling) == p60, "200 ms with no host present"); + CHECK(rtss_pace_period(1, now, 0, p60, ceiling) == p60, "no host present yet"); + CHECK(rtss_pace_period(0, now, now - 10000000, p60, ceiling) == p60, "RTSS does not hold"); + CHECK(rtss_pace_period(1, now, now + 1000, p60, ceiling) == ceiling, "a host present stored after now was read"); + CHECK(rtss_pace_period(1, now, now - 10000000, ceiling, ceiling) == ceiling, "a limit at the ceiling"); +} + int main(void) { settings(); reasons(); @@ -394,6 +407,7 @@ int main(void) { movie(); unset_and_library(); two(); + pace_period(); printf("rtss_limit_check: %s\n", fails ? "FAIL" : "ok"); return fails != 0; }