mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
Run the sound manager's job at 60 Hz with whole steps above 60 fps.
The job (main+0x415300) hands the time manager's step to the sound system in its phase 0 and runs the sound objects' and the system's updates in phases 1 and 2. With the set ostep that step is a fraction of a frame above 60 fps, and fed fractions the game has been reported to keep sounds held after an area unloaded until no voice was free, new sounds and then the music going silent until a restart. The new set osound (sound_steps, on by default) calls the page's +0xf10 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, phases 1 and 2 follow that choice, and a job that does not run gets phase 3, which returns through its own epilogue. After phase 0's load of the step the page's +0xf00 makes a fraction one whole step. At 60 fps and below the job runs every frame with the game's step. Both sites were checked against the module; frame_step_check covers the gate at 60, 30, 144 and 240 fps. Needs the game code generated again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user