mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Give Ocarina's own frames-per-update the display interval, so its menus and interface keep their speed above 60 fps.
The base-step write fixed gameplay, but the interface and the frame counter step by the time manager's nominal frame, which the framework update (main+0x6e6be0) sets every frame as 60 over a target refresh rate it keeps at 60 (main+0x3b4e300 writes frames per update, its inverse, seconds and the rate). The new patch set ostep (recomp/patches-v111.txt, on by default) makes that update read frames per update from patch page +256, which the runtime writes at each present as the 5-frame average of the display interval in 60 Hz frames. The game then makes the whole nominal frame itself and the base step stays 1.0. At 144 fps the Plus menu opened in 0.17 s against 0.35 s at 60; now in 0.25 s, within the screenshot capture's noise. Builds without the set keep writing the base step. NX2NAVI_SHOT_AFTER delays the first NX2NAVI_SHOT_EVERY shot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user