diff --git a/recomp/patches-v111.txt b/recomp/patches-v111.txt index 87017665..1c79d75b 100644 --- a/recomp/patches-v111.txt +++ b/recomp/patches-v111.txt @@ -15,6 +15,16 @@ set wide_scene NX2NAVI_PATCH_WIDE_SCENE 0 set wide_aspect NX2NAVI_PATCH_WIDE_ASPECT 0 set wide_hud NX2NAVI_PATCH_WIDE_HUD 0 set wide_movie NX2NAVI_PATCH_WIDE_MOVIE 0 +set ostep NX2NAVI_OCARINA_REALTIME 1 + +# Real-time step: the framework's per-frame update (main+0x6e6be0) gives the time manager **[main+0x4c7e890] its +# nominal frame through main+0x3b4e300 (+0x10 frames per update, +0x14 the inverse, +0x18 seconds, +0x1c the rate), +# as 60 (or 30) over the target refresh rate it keeps, 60. Gameplay steps base (+0x1c0, 1.0) times frames per update, +# the interface and the frame counter at update +0xd58 step frames per update, so above 60 fps all of it ran fast. +# The update takes frames per update from the patch page instead: +256, the display interval in 60 Hz frames +# (runtime/frame_rate.c). x10 is rewritten before it is read again, and the tst's flags feed only the fcsel. +site ostep main 6e6cc0 1e220c21 @page:x10 # fcsel s1, s1, s2, eq -> adrp x10, page +site ostep main 6e6cc4 1e201828 bd410148 # fdiv s8, s1, s0 -> ldr s8, [x10, #256] # Game image: the graphics setup (main+0x1202ce0) fills the renderer's configuration with the size of the picture the # game presents, 2560x1440 at every console mode (config +0x14, copied to renderer +0x9c, which main+0x346be50 reads diff --git a/runtime/cfg.c b/runtime/cfg.c index 21c123bc..09c15436 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -30,7 +30,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_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_STEP", "VI_HZ", "VI_VRR", "SHADER_DUMP", "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_SKIP", "MOVIE_OVERLAY", "MOVIES", "NPLN", "NVN2_PROGRAMS", "NVN_TRAP", "OCARINA_REALTIME", "OCARINA_STEP", "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) "AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_OFF", "AUDIO_PREFILL_MS", "BACKGROUND_WINDOW", "BIND_A", "BIND_B", "BIND_DOWN", "BIND_L", "BIND_LCLICK", "BIND_LEFT", "BIND_LSTICK_DOWN", "BIND_LSTICK_LEFT", "BIND_LSTICK_RIGHT", "BIND_LSTICK_UP", "BIND_MINUS", diff --git a/runtime/frame_rate.c b/runtime/frame_rate.c index 312d6dff..d58b87da 100644 --- a/runtime/frame_rate.c +++ b/runtime/frame_rate.c @@ -62,6 +62,7 @@ static _Atomic uint64_t *step_word; // patch page +0: ratio (low float), seconds (high float) static double step_now = STEP_FIRST_S; // the step the patch page holds: what the game steps each frame static _Atomic uint32_t *step_interval; // patch page +8: seconds * 60 (patches-v143.txt realtime) +static _Atomic uint32_t *ostep_page; // patch page +256 with the set ostep: frames per update (patches-v111.txt) static uint64_t last_ns; static double samples[STEP_SAMPLES]; static unsigned sample_count, sample_next; @@ -81,6 +82,15 @@ static void frame_limit_live(void); void frame_step_init(void) { int fps60 = rt_patch_set("fps60"), realtime = rt_patch_set("realtime"), cloth = rt_patch_set("cloth"); + int ostep = rt_patch_set("ostep"); + if (ostep >= 0 && rt_patch_enabled(ostep)) { // Ocarina: frames per update from the page, one 60 Hz frame until the first present + ostep_page = (_Atomic uint32_t *)((uint8_t *)rt_patch_page() + 256); + float one = 1.0f; + uint32_t bits; + memcpy(&bits, &one, 4); + atomic_store_explicit(ostep_page, bits, memory_order_release); + rt_log("frame rate: the game's frames per update follow the display interval (set ostep)"); + } if (realtime < 0) { rt_log("frame rate: no patch table, shipping 30 fps whole-vsync steps"); return; @@ -250,6 +260,7 @@ static double ocarina_step_s(void) { static uint64_t ostep_writes, ostep_changed; static float ostep_last = -1; + static void ocarina_step_write(double dt) { static int on = -1; if (on < 0) { @@ -257,6 +268,16 @@ static void ocarina_step_write(double dt) { on = !(e && !strcmp(e, "0")); } if (!on || dt <= 0) return; + if (ostep_page) { // the set ostep: the game makes its whole nominal frame from this, the interface's included + if (dt < 1.0 / 1000) dt = 1.0 / 1000; + if (dt > STEP_MAX_S) dt = STEP_MAX_S; + float f = (float)(dt * 60.0); + uint32_t bits; + memcpy(&bits, &f, 4); + atomic_store_explicit(ostep_page, bits, memory_order_release); + ostep_last = f; + return; + } uint64_t slot = read_ptr(MOD_MAIN_BASE + 0x4c7e890), mgr = slot ? read_ptr(slot) : 0; if (!mgr || !plat_mem_readable(mgr + 0x1c0, 4)) return; _Atomic uint32_t *word = (_Atomic uint32_t *)(uintptr_t)(mgr + 0x1c0); @@ -271,11 +292,33 @@ static void ocarina_step_write(double dt) { memcpy(&bits, &v, 4); atomic_store_explicit(word, bits, memory_order_relaxed); ostep_last = v; + if (++ostep_writes % 1200 == 0) rt_log("frame step: Ocarina base step %.3f frames (%.2f ms); the game changed it %llu times in the last 1200 presents", v, dt * 1000, (unsigned long long)ostep_changed), ostep_changed = 0; } +// NX2NAVI_TIME_DUMP=1: the time manager's first 0x280 bytes once, 30 s into the run (diagnosis) +static void time_dump(void) { + static int done; + const char *e = cfg_get("NX2NAVI_TIME_DUMP"); + if (done || !e || strcmp(e, "1") || !rt_guest_start_ns || kern_now_ns() - rt_guest_start_ns < 30000000000ull) return; + done = 1; + uint64_t slot = read_ptr(MOD_MAIN_BASE + 0x4c7e890), mgr = slot ? read_ptr(slot) : 0; + uint32_t w[160]; + if (!mgr || !read_guest(mgr, w, sizeof w)) return; + for (int i = 0; i < 160; i += 8) { + char line[512]; + int o = snprintf(line, sizeof line, "time dump +%03x:", i * 4); + for (int k = 0; k < 8; k++) { + float f; + memcpy(&f, &w[i + k], 4); + o += snprintf(line + o, sizeof line - o, " %08x(%g)", w[i + k], (double)f); + } + rt_log("%s", line); + } +} + static void speed_report(uint64_t now, double step, int interval) { if (!win_t0) { win_t0 = now; @@ -372,6 +415,7 @@ void frame_step_present(uint64_t display_ns, int interval) { memory_order_relaxed); world_clock(dt, step); if (rt_trace & RT_TRACE_PERF) { + time_dump(); if (frame_free_running()) { // the game's own step, as its time manager holds it double s = ocarina_step_s(); step = s > 0 ? s : 0; diff --git a/runtime/gfx_present.c b/runtime/gfx_present.c index a92f7e70..8d8ef55c 100644 --- a/runtime/gfx_present.c +++ b/runtime/gfx_present.c @@ -760,7 +760,8 @@ static void present_image(VkImage src, VkImageLayout layout, VkFormat fmt, int w if (every < 0) { const char *e = cfg_get("NX2NAVI_SHOT_EVERY"); every = e ? atof(e) : 0; - next_ns = plat_now_ns() + (uint64_t)(every * 1e9); + const char *after = cfg_get("NX2NAVI_SHOT_AFTER"); // seconds after the start before the first shot + next_ns = plat_now_ns() + (uint64_t)((every + (after ? atof(after) : 0)) * 1e9); } const char *dir = cfg_get("NX2NAVI_SHOT_DIR"); if (!menu_shot && every > 0 && dir && plat_now_ns() >= next_ns && n < 200) {