diff --git a/nx2navi.ini.example b/nx2navi.ini.example index dbddb364..25935fe6 100644 --- a/nx2navi.ini.example +++ b/nx2navi.ini.example @@ -52,6 +52,10 @@ # frame's time holds, and the game dropped the rest: above 60 fps every frame ran none, so they stood still under Link. # 1 keeps the rest for the next frame, 60 steps a second at any frame rate; 0 is the game's own count. Read at start. # water_steps = 1 +# Above 60 fps the game handed its sound system a fraction of a frame each frame, which was reported to keep sounds +# held after an area unloaded until new sounds and then the music went silent. 1 runs the sound system 60 times a +# second with whole frames while the frame rate is above 60; 0 is the game's own step. Read at start. +# sound_steps = 1 # A movie's narration and fades follow the picture on screen: 1 counts their time from the shown picture's # timestamp, so they stay with it at any frame rate (with movie_fps = 0 too) and wait when the picture stalls; 0 counts # the game's redraws, as before, which keeps them with the music when the picture stalls. Read at start; needs game diff --git a/recomp/patches-v111.txt b/recomp/patches-v111.txt index 9514080a..6054d4c4 100644 --- a/recomp/patches-v111.txt +++ b/recomp/patches-v111.txt @@ -104,6 +104,20 @@ site owater main 3876014 1e201900 1e201900 9458abc3 # fdiv s0, s8, s0 -> fdiv site owater main 38827f8 1e201900 1e201900 945879ca # the same at ResponsiveWaterMgr's site owater main 632cfc 1e201900 1e201900 9521b889 # the same at the interactive floors' +# The sound manager's job (main+0x415300, its task's phase in w2: 0 hands the time manager's step, [[main+0x4c7e890]] +# +0x10, to the sound system; 1 runs the sound objects' updates; 2 the system's own update, main+0x3798410; any other +# returns through main+0x4155fc). Above 60 fps the set ostep makes that step a fraction of one, and fed fractions the +# game has been reported to hold sounds after an area unloads until no voice is free, then go silent +# (runtime/frame_rate.c, "the sound manager's job at whole steps"). The set +# runs the job at 60 Hz then: after the prologue (add x29, sp, #0x20) the page's +0xf10 decides from the phase and the +# step whether the job runs this frame, and puts phase 3 in w2 when it does not, so cbnz w2 and the phase tests send it +# to its own epilogue; after phase 0's load of the step (ldr s0, [x8, #0x10]) the page's +0xf00 makes a fraction one +# whole step. At a step of one or more the job runs every frame with the game's step. x30 was saved by the prologue +# (stp x29, x30, [sp, #0x20]) and is reloaded by every way out; no word reads or writes the flags. +set osound NX2NAVI_SOUND_STEPS 1 +site osound main 41531c 910083fd 910083fd 952a2efd # add x29, sp, #0x20 -> add x29, sp, #0x20; bl page+0xf10 +site osound main 415334 bd401100 bd401100 952a2ef3 # ldr s0, [x8, #0x10] -> ldr s0, [x8, #0x10]; bl page+0xf00 + # Movie-synchronised event timelines: the movie layer advances its counter at +760 on each draw of a ready # picture. main+0x3b3ec10 reads that counter for the event dispatcher, so rendering a picture twice also # advances voices and overlays twice. Both ready paths reach these sites only after main+0x3b5ce80 checks diff --git a/runtime/cfg.c b/runtime/cfg.c index 92d9d6ed..a41f1133 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -33,7 +33,7 @@ static int save_ok = 1; // the last cfg_save_to wrote (o // Every name the runtime reads (and the patch sets in generated/), without the prefix. A file key outside this list is // logged as unknown. A new variable goes here too, or its ini key logs a harmless "unknown key" line. static const char *const known[] = { - "CORES", "TM_DISPATCH", "NVN2_CS_TABLE", "SKIP_SHADERS", "MOVIE_DEBAND", "MOVIE_FPS", "MOVIE_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_REALTIME", "ONESHOT_BUDGET", "ONESHOT_WAIT_MS", "OCARINA_STEP", "PHYSICS_STEPS", "PICKUP_COUNT", "WATER_STEPS", "FLOOR_PROBE", "FLOOR_SCAN", "MOVIE_PTS", "STEP_STEADY", "VI_HZ", "VI_VRR", "TIME_DUMP", "SHADER_DUMP", "SHOT_AFTER", "SHOT_DIR", "SHOT_EVERY", // Switch 2 port: movie skip (romfs.c), NVN2 proc traps (nvn_procs.c) + "CORES", "TM_DISPATCH", "NVN2_CS_TABLE", "SKIP_SHADERS", "MOVIE_DEBAND", "MOVIE_FPS", "MOVIE_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_REALTIME", "ONESHOT_BUDGET", "ONESHOT_WAIT_MS", "OCARINA_STEP", "PHYSICS_STEPS", "PICKUP_COUNT", "WATER_STEPS", "SOUND_STEPS", "FLOOR_PROBE", "FLOOR_SCAN", "MOVIE_PTS", "STEP_STEADY", "VI_HZ", "VI_VRR", "TIME_DUMP", "SHADER_DUMP", "SHOT_AFTER", "SHOT_DIR", "SHOT_EVERY", // Switch 2 port: movie skip (romfs.c), NVN2 proc traps (nvn_procs.c) "AA", "AA_LOG", "AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CATCHUP", "AUDIO_CHANNELS", "AUDIO_DEVICE", "AUDIO_GAME_PRIO", "AUDIO_OFF", "AUDIO_PREFILL_MS", "AUDIO_RAMP", "AUDIO_STATS", "BARRIER_GAME", "MESH_AHEAD", "MESH_FALLBACK", "MESH_FALLBACK_MB", "MESH_FALLBACK_WORKERS", "PROBE_MESHFB_DIR", "BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT", @@ -103,6 +103,7 @@ static const struct { const char *key, *section; } sections[] = { {"FPS_LIMIT", "frame rate"}, {"FPS60", "frame rate"}, {"RTSS", "frame rate"}, {"REALTIME", "frame rate"}, {"CLOTH_REALTIME", "frame rate"}, {"HIGHFPS_FIX", "frame rate"}, {"PHYSICS_STEPS", "frame rate"}, {"PICKUP_COUNT", "frame rate"}, {"WATER_STEPS", "frame rate"}, + {"SOUND_STEPS", "frame rate"}, {"MOVIE_PTS", "frame rate"}, {"STEP_STEADY", "frame rate"}, {"FULLSCREEN", "display"}, {"WINDOW_SIZE", "display"}, {"PERF_OVERLAY", "display"}, {"VK_PRESENT", "display"}, {"GPU_INDEX", "display"}, {"MONITOR", "display"}, {"OUTPUT_RES", "display"}, {"GAME_ASPECT", "display"}, {"GAME_RES", "display"}, {"RENDER_SCALE", "display"}, {"UPSCALER", "display"}, @@ -243,6 +244,8 @@ static const CfgMeta metas[] = { {"HIGHFPS_FIX", "1", "0|1", RESTART}, // the set owater of patches-v111.txt (frame_rate.c frame_rate_sim_host): water and floor surfaces' fixed steps {"WATER_STEPS", "1", "0|1", RESTART}, + // the set osound of patches-v111.txt (frame_rate.c frame_rate_sound_gate_host): the sound job at 60 Hz above 60 fps + {"SOUND_STEPS", "1", "0|1", RESTART}, // frame_rate.c floor_probe: lines about Link on a moving floor (0 off, 1 = 4000 lines, or a count) {"FLOOR_PROBE", "0", NULL, RESTART}, // frame_rate.c floor_scan: finds the moving floor under Link in guest memory (0 off, 1 = 20000 lines, or a count) diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index 7db33ac4..70c2ab62 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -615,6 +615,78 @@ void frame_rate_sim_host(Cpu *c) { } } +// ---- the sound manager's job at whole steps (patches-v111.txt osound) +// The job (main+0x415300) runs in three phases a frame, the phase in w2: 0 loads the time manager's step, frames per +// update at [[main+0x4c7e890]] +0x10, and hands it to the sound system ([[main+0x4c80c38]] +0xa8, and with its inverse +// +0x18 of the object at +0x10 of [[main+0x4c806d0]]); 1 runs the sound objects' updates, 2 the system's own +// (main+0x3798410). Any other phase returns at once (main+0x4155fc); the task wrappers main+0x4151f0 and main+0x4152d0 +// skip a 3 themselves. The sound system counts its fades, delays and parameter ramps in updates of at least one step. +// With the set ostep the step is the display interval in 60 Hz frames, a fraction of one above 60 fps. Fed fractions, +// this game has been reported to keep sounds held after an area unloaded until no voice was left free, so that new +// sounds, then the music, went silent until a restart (not reproduced on this build: the fault takes a few area +// changes above 60 fps). With the set osound the job runs at 60 Hz while the step is a fraction: +// - frame_rate_sound_gate_host, after the job's prologue: phase 0 adds the step to what the job is owed and runs once +// a whole step has built up, keeping the rest; phases 1 and 2 follow that frame's choice. A job that does not run +// gets phase 3 in w2, so it returns through its own epilogue. +// - frame_rate_sound_step_host, after phase 0's load of the step: a fraction becomes one whole step. +// A step of one or more (60 fps and below) runs as the game has it and leaves what is owed for the next fractions; a +// step that is not positive (or NaN) runs as the game has it and clears it. So the sound clock keeps the game's on +// average at any frame rate, and at 60 fps the job runs every frame as before. +typedef struct { + float owed; // steps given since the job last ran, under one + int run; // phase 0's choice, which the frame's other phases follow +} SoundGate; +static SoundGate sound_gate = {0.0f, 1}; // every phase runs until a phase 0 has chosen + +static int sound_gate_take(SoundGate *g, uint32_t phase, float step) { + if (phase != 0) return g->run; + if (!(step > 0.0f)) { + g->owed = 0.0f; + return g->run = 1; + } + if (step >= 1.0f) return g->run = 1; + g->owed += step; + g->run = g->owed >= 1.0f; + if (g->run) g->owed -= 1.0f; + return g->run; +} + +// The step the job's phase 0 loads (1 when the time manager is not there: the gate then lets the phase run). +static float sound_job_step(void) { + uint64_t slot = read_ptr(MOD_MAIN_BASE + 0x4c7e890), mgr = slot ? read_ptr(slot) : 0; + float step = 1.0f; + if (mgr && !read_guest(mgr + 0x10, &step, 4)) step = 1.0f; + return step; +} + +// NX2NAVI_TRACE=perf: every 10 s, how often phase 0 ran of the times it was asked. +void frame_rate_sound_gate_host(Cpu *c) { + uint32_t phase = (uint32_t)c->x[2]; + float step = phase == 0 ? sound_job_step() : 1.0f; + uint64_t asked = 0, ran = 0, t = 0; + plat_mutex_lock(&whole_lock); // the phases may come from different threads + int run = sound_gate_take(&sound_gate, phase, step); + if (phase == 0 && (rt_trace & RT_TRACE_PERF)) { + static uint64_t t0, n, r; + uint64_t now = plat_now_ns(); + n++, r += (uint64_t)run; + if (!t0) t0 = now; + if (now - t0 >= 10000000000ull) asked = n, ran = r, t = now - t0, t0 = now, n = r = 0; + } + plat_mutex_unlock(&whole_lock); + if (!run) c->x[2] = 3; + if (asked) + rt_log("frame rate: the sound job ran %llu of %llu frames in %.1f s (step %.3f)", (unsigned long long)ran, + (unsigned long long)asked, t / 1e9, step); +} + +void frame_rate_sound_step_host(Cpu *c) { + uint32_t bits = (uint32_t)c->v[0]; + float step; + memcpy(&step, &bits, 4); + if (step > 0.0f && step < 1.0f) c->v[0] = 0x3f800000u; // 1.0f, the lanes above it zero as the load left them +} + // 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 194dcbc2..603f3250 100644 --- a/runtime/host.h +++ b/runtime/host.h @@ -410,6 +410,8 @@ double frame_step_game_hz(void); // the rate the game d 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) void frame_rate_sim_host(Cpu *c); // water and floor surfaces' fixed steps (set owater, page +0xf20) +void frame_rate_sound_gate_host(Cpu *c); // the sound job at whole steps (set osound, page +0xf10) +void frame_rate_sound_step_host(Cpu *c); // the step the sound job hands on (set osound, page +0xf00) 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 dca0d698..5c5a0d3d 100644 --- a/runtime/patches.c +++ b/runtime/patches.c @@ -33,12 +33,15 @@ uint8_t rt_patch_on[RT_PATCH_COUNT > 0 ? RT_PATCH_COUNT : 1]; // read by the g #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, the water renderer's site its -// +0xf50, and the surfaces' fixed-step sites of the set owater its +0xf20 (frame_rate.c). +// +0xf50, the surfaces' fixed-step sites of the set owater its +0xf20, and the sound job's two sites of the set osound +// its +0xf10 and +0xf00 (frame_rate.c). __attribute__((constructor)) static void day_register(void) { 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); rt_register_host(RT_PATCH_PAGE + 0xf20, frame_rate_sim_host); + rt_register_host(RT_PATCH_PAGE + 0xf10, frame_rate_sound_gate_host); + rt_register_host(RT_PATCH_PAGE + 0xf00, frame_rate_sound_step_host); } #endif diff --git a/tests/frame_step_check.c b/tests/frame_step_check.c index acc47ced..4ea47c6c 100644 --- a/tests/frame_step_check.c +++ b/tests/frame_step_check.c @@ -40,7 +40,18 @@ 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; } -int plat_mem_read(uint64_t a, void *out, size_t n) { (void)a, (void)out, (void)n; return 0; } +// 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; +static float fake_step = 1.0f; +int plat_mem_read(uint64_t a, void *out, size_t n) { + const uint64_t slot = 0x7700000000ull, manager = 0x7700001000ull; + if (!fake_manager) return 0; + if (a == MOD_MAIN_BASE + 0x4c7e890 && n == 8) return memcpy(out, &slot, 8), 1; + if (a == slot && n == 8) return memcpy(out, &manager, 8), 1; + if (a == manager + 0x10 && n == 4) return memcpy(out, &fake_step, 4), 1; + return 0; +} 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; } @@ -287,6 +298,49 @@ int main(int argc, char **argv) { for (int k = 0; k < 32; k++) CHECK(steps[k] >= 599 && steps[k] <= 600, "surface %d of 32 at 144 fps: %u steps in 10 s", k, steps[k]); } + // The sound job (set osound). At 60 and 30 fps every phase of every frame runs with the game's step; at 144 and 240 + // fps it runs 60 times a second with a step of one, phases 1 and 2 with phase 0; a skipped phase gets 3 in w2. + { + fake_manager = 1; + const float steps[] = {1.0f, 2.0f, 60.0f / 144.0f, 0.25f}; + const int frames[] = {600, 300, 1440, 2400}; // 10 s each + for (int r = 0; r < 4; r++) { + fake_step = steps[r]; + int ran[3] = {0}, odd = 0; + float fed = 0; + for (int f = 0; f < frames[r]; f++) { + for (uint32_t phase = 0; phase < 3; phase++) { + cpu.x[2] = phase; + frame_rate_sound_gate_host(&cpu); + if (cpu.x[2] == phase) ran[phase]++; + else odd += cpu.x[2] != 3; + if (phase == 0 && cpu.x[2] == 0) { // phase 0 runs on to its load of the step + uint32_t bits; + memcpy(&bits, &fake_step, 4); + cpu.v[0] = bits; + frame_rate_sound_step_host(&cpu); + float s; + memcpy(&s, &cpu.v[0], 4); + fed += s; + CHECK(cpu.v[0] >> 32 == 0, "the step's upper lanes stay zero"); + } + } + } + const int want = steps[r] >= 1.0f ? frames[r] : 600; + CHECK(ran[0] >= want - 1 && ran[0] <= want && ran[1] == ran[0] && ran[2] == ran[0] && !odd, + "step %.3f: phases ran %d, %d, %d of %d frames (want %d)", steps[r], ran[0], ran[1], ran[2], frames[r], + want); + CHECK(fed >= 599.0f && fed <= 600.5f, "step %.3f: the sound system was fed %.2f frames in 10 s", steps[r], fed); + } + // Owed fractions survive a whole step, and a step that is not positive runs and clears them. + SoundGate g = {0.0f, 1}; + CHECK(!sound_gate_take(&g, 0, 0.5f) && !sound_gate_take(&g, 1, 1.0f), "half a frame: the job and its phase 1 wait"); + CHECK(sound_gate_take(&g, 0, 1.0f) && sound_gate_take(&g, 2, 1.0f) && g.owed == 0.5f, "a whole frame runs, the half kept"); + CHECK(sound_gate_take(&g, 0, 0.5f) && g.owed == 0.0f, "the kept half and a new one run"); + CHECK(!sound_gate_take(&g, 0, 0.75f) && sound_gate_take(&g, 0, NAN) && g.owed == 0.0f && + sound_gate_take(&g, 0, 0.0f), "NaN and 0 run as the game has them and clear what is owed"); + fake_manager = 0; + } printf("frame_step_check: %s\n", fails ? "FAILED" : "ok"); return fails != 0; } diff --git a/tests/frame_step_check.py b/tests/frame_step_check.py index 5afd4c3f..7f6fa691 100644 --- a/tests/frame_step_check.py +++ b/tests/frame_step_check.py @@ -20,7 +20,8 @@ PAGE = 0x4ea0000 # the patch page of 1.1.1, main+0x4ea0000 FDIV = 0x1e201900 # fdiv s0, s8, s0 HOST_SITES = [('ostep', 0x3cdfc0, 0x1e380008, [], 0xf60), ('ostep', 0x475f8, 0x1e380009, [], 0xf50), ('owater', 0x3876014, FDIV, [FDIV], 0xf20), ('owater', 0x38827f8, FDIV, [FDIV], 0xf20), - ('owater', 0x632cfc, FDIV, [FDIV], 0xf20)] + ('owater', 0x632cfc, FDIV, [FDIV], 0xf20), + ('osound', 0x41531c, 0x910083fd, [0x910083fd], 0xf10), ('osound', 0x415334, 0xbd401100, [0xbd401100], 0xf00)] def compiler():