Keep the time of day at the game's pace under the real-time step, its fraction carried from frame to frame.

The day's update (main+0x3cdf70) adds trunc(2 x frames per update) game seconds a frame. With the set ostep's step,
the display interval in 60 Hz frames, that sum lost its fraction every frame: 1 a frame from 61 to 119 fps, 0 above
120 (the clock stood still), and at 60 fps a step a little under one frame dropped a second now and then. Its convert
now calls the patch page's +0xf60 (runtime/frame_rate.c frame_rate_day_host), which keeps the fraction per counter and
hands out the whole seconds. NX2NAVI_TRACE=perf logs, every 10 s, the seconds given beside those the cut would have
given.

Measured in Kakariko, 10 s windows: at a 60 fps limit 998 game seconds against 881 from the cut alone; at 144 fps
1,201 against 219 to 972 (the steady 1,200 a 10 s window should have).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-12 03:26:09 -05:00
parent 0c343e04a4
commit 7dcfb72b70
4 changed files with 72 additions and 0 deletions

View File

@@ -38,6 +38,14 @@ site ostep main 6e6cc4 1e201828 bd410148 # fdiv s8, s1, s0 -> ldr s8, [x10, #
# never differ. x9 is not read in the function and is free across the call that follows; the store keeps x8.
site ostep main 3b4eaf4 5e21d800 @page:x9 # scvtf s0, s0 -> adrp x9, page
site ostep main 3b4eaf8 1e201820 bd410120 # fdiv s0, s1, s0 -> ldr s0, [x9, #256]
# The time of day: its update (main+0x3cdf70) adds trunc(2 * frames per update) game seconds a frame to the seconds of
# the day (+0x3c of its object, carried into the day count by the code after it). With the step above below one frame
# per update the sum was 1 from 61 to 119 fps (the day at 83% of its pace at 100 fps) and 0 above 120, where the clock
# stood still and the sun swung back and forth around a frozen hour; at 60 fps a step a little under one frame lost a
# second too. The cut calls the page's +0xf60 instead (runtime/frame_rate.c frame_rate_day_host): the frame's seconds
# with the fraction kept for the next frame, so the day keeps the game's pace at any frame rate. x30 is the function's
# own saved one, and the call at main+0x3cdfc8 sets it again.
site ostep main 3cdfc0 1e380008 952b4be8 # fcvtzs w8, s0 -> bl page+0xf60
# Physics solver steps: the physics system's step setup (main+0x38aeb80, and the same code in main+0x38afc10) gives the
# rigid body world its solver step count for the frame, trunc(frames per update * count + 0.001) with the count at

View File

@@ -476,6 +476,65 @@ static double ocarina_step(double dt) {
return step;
}
// ---- whole frames out of the real-time step (the set ostep's sites that cut it to a whole number, patches-v111.txt)
// The game adds the step, or the step times a whole number, to a few counters and cuts the sum to a whole number: at the
// console's 60 fps the step is 1 and they count frames. With the step the display interval in 60 Hz frames, under one
// above 60 fps, the cut loses the fraction every frame. whole_frames keeps it per counter (by its guest address) and
// hands out the whole part, so each counter moves at the game's pace at any frame rate; at a step of exactly one it
// gives what the game computes. A value that is not positive, or too large for a count, is cut as fcvtzs cuts it.
#define WHOLE_SLOTS 8
static struct { uint64_t at; float rem; uint32_t used; } whole_slot[WHOLE_SLOTS];
static uint32_t whole_calls;
static PlatMutex whole_lock = PLAT_MUTEX_INIT;
static uint32_t cut_fcvtzs(float f) { // fcvtzs w, s: toward zero, saturating, NaN 0
if (f != f) return 0;
if (f >= 2147483648.0f) return 0x7fffffffu;
if (f <= -2147483648.0f) return 0x80000000u;
return (uint32_t)(int32_t)f;
}
static uint32_t whole_frames(uint64_t at, float amount) {
if (!(amount > 0.0f) || amount >= 2147483648.0f) return cut_fcvtzs(amount);
plat_mutex_lock(&whole_lock);
int k = -1, oldest = 0;
for (int i = 0; i < WHOLE_SLOTS; i++) {
if (whole_slot[i].at == at) { k = i; break; }
if (whole_slot[i].used < whole_slot[oldest].used) oldest = i;
}
if (k < 0) k = oldest, whole_slot[k].at = at, whole_slot[k].rem = 0.0f;
whole_slot[k].used = ++whole_calls;
float sum = whole_slot[k].rem + amount;
uint32_t whole = cut_fcvtzs(sum);
whole_slot[k].rem = sum - (float)whole;
plat_mutex_unlock(&whole_lock);
return whole;
}
// The time of day (main+0x3cdf70, patches-v111.txt): s0 holds twice the step, the game seconds of this frame; w8 gets
// the whole ones, the counter being the seconds of the day at +0x3c of the object in x19.
// NX2NAVI_TRACE=perf: every 10 s, the game seconds the day was given beside those the cut alone would have given.
void frame_rate_day_host(Cpu *c) {
uint32_t bits = (uint32_t)c->v[0];
float secs;
memcpy(&secs, &bits, 4);
uint32_t whole = whole_frames(c->x[19] + 0x3c, secs);
c->x[8] = whole;
if (rt_trace & RT_TRACE_PERF) {
static uint64_t t0, given, cut;
static double exact;
uint64_t now = plat_now_ns();
given += whole, cut += cut_fcvtzs(secs), exact += secs;
if (!t0) t0 = now;
if (now - t0 >= 10000000000ull) {
rt_log("frame rate: the time of day got %llu game seconds in %.1f s (the frames' step makes %.1f; cut "
"to whole seconds each frame, as the game cuts them, %llu)", (unsigned long long)given,
(now - t0) / 1e9, exact, (unsigned long long)cut);
t0 = now, given = cut = 0, exact = 0;
}
}
}
// The 60 Hz frames the game steps at each update where the set ostep gives them (Ocarina); 1 elsewhere, the game's own
// nominal frame. The mouse camera divides by it (mouse_direct.c): the game multiplies camera input by this step.
float frame_step_frames(void) {

View File

@@ -393,6 +393,7 @@ int frame_limit_adjustable(void); // the frame limit set
int frame_free_running(void); // no fps60/realtime patch set: the game steps by the clock
double frame_step_limit_hz(void); // the frame rate the limit gives (overlay); 0 unknown
double frame_step_game_hz(void); // the rate the game draws at under the limit; 0 unknown
void frame_rate_day_host(Cpu *c); // the time of day's seconds a frame (set ostep, page +0xf60)
float frame_step_frames(void); // the 60 Hz frames the game steps per update (set ostep), else 1
void frame_step_flight(float *page, float *game, uint64_t *presents); // flight.c: the step at a host present
// rtss_limit.c: RivaTuner Statistics Server takes the frame limit where it runs

View File

@@ -32,6 +32,10 @@ uint8_t rt_patch_on[RT_PATCH_COUNT > 0 ? RT_PATCH_COUNT : 1]; // read by the g
#ifdef RT_PATCH_PAGE
#include "debug_tools.h"
__attribute__((constructor)) static void freecam_register(void) { rt_register_host(RT_PATCH_PAGE + 0xf70, dbg_freecam_host); }
// The time of day's site of the set ostep (patches-v111.txt) calls the page's +0xf60 (frame_rate.c).
__attribute__((constructor)) static void day_register(void) {
if (RT_V111) rt_register_host(RT_PATCH_PAGE + 0xf60, frame_rate_day_host);
}
#endif
#if RT_PATCH_COUNT