diff --git a/docs/building.md b/docs/building.md index 712165bf..9f9d4ebd 100644 --- a/docs/building.md +++ b/docs/building.md @@ -350,10 +350,13 @@ launcher imports a zip it wrote. The second builds `fs_host.c` and `save_share.c and runs each step as a start of the game on a profile it makes: the export, an import from a zip the launcher exported, its backup and swap at the next start, the zips it refuses and Cancel. No game files. -A change to the step Ocarina is given each frame (`runtime/frame_rate.c` `ocarina_step`) passes +A change to the step Ocarina is given each frame (`runtime/frame_rate.c` `ocarina_step`), or to the whole numbers the +game's counters get out of it (`whole_take`, `frame_rate_day_host`, `frame_rate_water_host`), passes `python tests\frame_step_check.py`, which builds `frame_rate.c` with the rest of the runtime stubbed and feeds it display times: a held 60 fps limit gives exactly one frame every frame, a 240 fps limit never less than a quarter -frame, uneven frame times one steady step, and game time follows the clock. No game files. +frame, uneven frame times one steady step, and game time follows the clock. It calls the two hooks as their patch +sites do: at 144 fps each of 64 water objects gets its ripple steps and the day its seconds, and a step of one frame +gives what the game computes. No game files. A change to the mouse camera (`runtime/mouse_direct.c`, the mouse path of `runtime/input.c`, the set `mouse_direct` of `recomp/patches-v111.txt` or `patches-v143.txt`) passes `python tests\mouse_direct_check.py --cc --deps diff --git a/recomp/patches-v111.txt b/recomp/patches-v111.txt index 9e29e2b2..1fa3800f 100644 --- a/recomp/patches-v111.txt +++ b/recomp/patches-v111.txt @@ -46,6 +46,17 @@ site ostep main 3b4eaf8 1e201820 bd410120 # fdiv s0, s1, s0 -> ldr s0, [x9, #2 # 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 +# The water's ripples: the water renderer (main+0x472e0, reached from main+0x47160 for a water object whose flags at +# +8 have bit 3, which that entry clears) runs its ripple simulation trunc(step) * substeps times, the step being the +# time manager's (its +0, or one of its group steps at +0x20 + 0x30 * n) and the substeps +0x4c of the water's +# parameters (object +0x1e8, then +0x150, or the parent those name), and skips the simulation when the product is +# under 1 (main+0x47618). With the step above under one frame that was every frame above 60 fps: the rings Link and +# everything else make in water stood still. The cut calls the page's +0xf50 instead (runtime/frame_rate.c +# frame_rate_water_host): the step goes into a remainder kept for the water object (x21, the function's x0) and w9 +# gets the whole steps in it, so the rings spread at the game's pace at any frame rate, and a step of exactly one +# frame gives what the game computes. Every path to the cut has the step in s0 (the one that asks the parameters' +# parent keeps it across its call), and x30 is the function's own saved one. +site ostep main 475f8 1e380009 95396656 # fcvtzs w9, s0 -> bl page+0xf50 # 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 diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index a4bf5e27..962e8bcc 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -482,9 +482,12 @@ static double ocarina_step(double dt) { // 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. +// The water's ripples keep their remainders per water object in a table of their own (frame_rate_water_host). #define WHOLE_SLOTS 8 -static struct { uint64_t at; float rem; uint32_t used; } whole_slot[WHOLE_SLOTS]; -static uint32_t whole_calls; +#define WATER_SLOTS 64 +typedef struct { uint64_t at; float rem; uint64_t used; } WholeSlot; // used: whole_calls at the slot's last use +static WholeSlot whole_slot[WHOLE_SLOTS], water_slot[WATER_SLOTS]; +static uint64_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 @@ -494,23 +497,26 @@ static uint32_t cut_fcvtzs(float f) { // fcvtzs w, s: toward zero, saturating, return (uint32_t)(int32_t)f; } -static uint32_t whole_frames(uint64_t at, float amount) { +// The whole part of amount plus the remainder the table slot[n] keeps for `at`; a new `at` takes the slot unused longest. +static uint32_t whole_take(WholeSlot *slot, int n, 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; + for (int i = 0; i < n; i++) { + if (slot[i].at == at) { k = i; break; } + if (slot[i].used < 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; + if (k < 0) k = oldest, slot[k].at = at, slot[k].rem = 0.0f; + slot[k].used = ++whole_calls; + float sum = slot[k].rem + amount; uint32_t whole = cut_fcvtzs(sum); - whole_slot[k].rem = sum - (float)whole; + slot[k].rem = sum - (float)whole; plat_mutex_unlock(&whole_lock); return whole; } +static uint32_t whole_frames(uint64_t at, float amount) { return whole_take(whole_slot, WHOLE_SLOTS, at, amount); } + // 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. @@ -535,6 +541,32 @@ void frame_rate_day_host(Cpu *c) { } } +// The water's ripples (main+0x472e0, patches-v111.txt): s0 holds the frame's step and w9 gets the whole steps in it, +// which the renderer multiplies by the water's substeps. The remainder is kept for the water object in x21, in a table +// of the waters' own: every water surface of a place asks once a frame, and with fewer slots than surfaces each would +// take another's slot in turn, start without a remainder every frame and stand still as it did with the cut. +// NX2NAVI_TRACE=perf: every 10 s, the steps the waters were given beside those the cut alone would have given. +void frame_rate_water_host(Cpu *c) { + uint32_t bits = (uint32_t)c->v[0]; + float step; + memcpy(&step, &bits, 4); + uint32_t whole = whole_take(water_slot, WATER_SLOTS, c->x[21], step); + c->x[9] = whole; + if (rt_trace & RT_TRACE_PERF) { + static uint64_t t0, asked, given, cut; + uint64_t now = plat_now_ns(), n = 0, g = 0, k = 0, t = 0; + plat_mutex_lock(&whole_lock); // the counts: nothing says one thread alone draws the waters + asked++, given += whole, cut += cut_fcvtzs(step); + if (!t0) t0 = now; + if (now - t0 >= 10000000000ull) n = asked, g = given, k = cut, t = now - t0, t0 = now, asked = given = cut = 0; + plat_mutex_unlock(&whole_lock); + if (n) + rt_log("frame rate: the water's ripples got %llu steps in %.1f s, asked %llu times (cut to whole steps " + "each time, as the game cuts them, %llu)", (unsigned long long)g, t / 1e9, (unsigned long long)n, + (unsigned long long)k); + } +} + // 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) { diff --git a/runtime/host.h b/runtime/host.h index 0638fdcb..51b71972 100644 --- a/runtime/host.h +++ b/runtime/host.h @@ -397,6 +397,7 @@ int frame_free_running(void); // no fps60/realtime p 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) +void frame_rate_water_host(Cpu *c); // the water's ripple steps a frame (set ostep, page +0xf50) 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 diff --git a/runtime/patches.c b/runtime/patches.c index cd40c09b..67216ae4 100644 --- a/runtime/patches.c +++ b/runtime/patches.c @@ -32,9 +32,12 @@ 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). +// The time of day's site of the set ostep (patches-v111.txt) calls the page's +0xf60 and the water renderer's site +// its +0xf50 (frame_rate.c). __attribute__((constructor)) static void day_register(void) { - if (RT_V111) rt_register_host(RT_PATCH_PAGE + 0xf60, frame_rate_day_host); + if (!RT_V111) return; + rt_register_host(RT_PATCH_PAGE + 0xf60, frame_rate_day_host); + rt_register_host(RT_PATCH_PAGE + 0xf50, frame_rate_water_host); } #endif diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index 07cd1ce0..69b32475 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -44,9 +44,12 @@ int plat_mem_readable(uint64_t a, size_t n) { (void)a, (void)n; return 0; } void mem_kernel_access(uint64_t a, uint64_t n) { (void)a, (void)n; } int mem_host_readable(uint64_t a, uint64_t n) { (void)a, (void)n; return 0; } uint64_t kern_now_ns(void) { return 0; } +void plat_mutex_lock(PlatMutex *m) { (void)m; } // one thread here +void plat_mutex_unlock(PlatMutex *m) { (void)m; } // ---- one present: the pacer asks for the period, then reports the display time. Returns the step in 60 Hz frames. static uint64_t now_ns = 1000000000ull; +uint64_t plat_now_ns(void) { return now_ns; } static double wall_s, game_s; static float present(double interval_s) { frame_step_cap_ns(); @@ -65,6 +68,25 @@ static void set_limit(const char *l) { wall_s = game_s = 0; } +// ---- the patch sites that cut the step to a whole number, called as the lifted code calls them: the value in s0, the +// object in its register, the whole number back in the register the cut wrote (all 64 bits of it). +static Cpu cpu; +static uint32_t water(uint64_t object, float step) { // frame_rate_water_host: x21, w9 + uint32_t bits; + memcpy(&bits, &step, 4); + cpu.v[0] = bits, cpu.x[21] = object, cpu.x[9] = ~0ull; + frame_rate_water_host(&cpu); + CHECK(cpu.x[9] >> 32 == 0, "the water's steps leave x9's upper half %#llx", (unsigned long long)cpu.x[9]); + return (uint32_t)cpu.x[9]; +} +static uint32_t day(uint64_t object, float secs) { // frame_rate_day_host: x19, w8 + uint32_t bits; + memcpy(&bits, &secs, 4); + cpu.v[0] = bits, cpu.x[19] = object, cpu.x[8] = ~0ull; + frame_rate_day_host(&cpu); + return (uint32_t)cpu.x[8]; +} + int main(int argc, char **argv) { int mean = argc > 1 && !strcmp(argv[1], "mean"); if (mean) steady = "0"; @@ -170,6 +192,34 @@ int main(int argc, char **argv) { caps[i].gen, game, caps[i].game_hz); } fg_mult = 1; + + // 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. + { + const float step = 60.0f / 144.0f; + const uint64_t first = 0x7100000000ull, clock = 0x7200000000ull; + uint32_t steps[64] = {0}, secs = 0; + for (int f = 0; f < 1440; f++) { + for (int k = 0; k < 64; k++) steps[k] += water(first + 0x1000 * (uint64_t)k, step); + secs += day(clock, 2 * step); + } + for (int k = 0; k < 64; k++) + CHECK(steps[k] >= 599 && steps[k] <= 600, "water object %d of 64 at 144 fps: %u steps in 1440 frames", k, steps[k]); + CHECK(secs >= 1199 && secs <= 1200, "the day beside 64 water objects: %u seconds in 1440 frames", secs); + } + // A step of one frame, and of two, is what the game computes, also after a fraction was left; a quarter frame + // gives one step every fourth frame, and what fcvtzs cuts to nothing or saturates leaves the remainder alone. + { + const uint64_t pond = 0x7300000000ull; + for (int i = 0; i < 3; i++) CHECK(water(pond, 0.25f) == 0, "a quarter frame, call %d", i); + CHECK(water(pond, 0.0f) == 0 && water(pond, -3.5f) == 0xfffffffdu && water(pond, NAN) == 0 && + water(pond, 3e9f) == 0x7fffffffu, "zero, a negative step, NaN and a step too large are cut as fcvtzs cuts"); + CHECK(water(pond, 0.25f) == 1, "the fourth quarter frame"); + CHECK(water(pond, 0.5f) == 0, "half a frame"); + for (int i = 0; i < 60; i++) CHECK(water(pond, 1.0f) == 1, "one frame, call %d", i); + for (int i = 0; i < 30; i++) CHECK(water(pond, 2.0f) == 2, "two frames, call %d", i); + CHECK(water(pond, 0.5f) == 1, "the half frame kept through the whole ones"); + } printf("frame_step_check: %s\n", fails ? "FAILED" : "ok"); return fails != 0; }