mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
The physics' two water managers and the game's interactive floor simulation (the Deku Tree's ground webs) step their surfaces in fixed steps of 1/60 s, as many whole steps as the frame's time holds, and drop the fraction, so every frame shorter than 1/60 s ran none and the surfaces stood still above 60 fps. The new set owater (water_steps, on by default) calls the page between the division and the game's cut, which keeps the fraction per object; at 60 fps the count is the game's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
794 lines
44 KiB
C
794 lines
44 KiB
C
// Contains code Copyright (c) 2026 MacNativeGames contributors (MIT; see THIRD_PARTY_NOTICES.md).
|
|
// Frame rate: the frame limit and the real-time frame step, the host half of the patch sets fps60, realtime and cloth
|
|
// (build tables patches-v121.txt, patches-v143.txt).
|
|
//
|
|
// NX2navi's adaptive present-interval controller (main+0x8f26c8) turns the main thread's measured work into a number of
|
|
// 60 Hz ticks clamped to [minimum, maximum]; shipping data sets the minimum to 2 (30 fps), the set fps60 lowers it to
|
|
// 1. The controller still raises the interval when a frame does not fit. Shipping code then steps the game by whole
|
|
// vsyncs (VFR step main+0x7ede6c: ratio = interval * 0.5, seconds = interval / 60; global ratio at main+0x8f2394), so
|
|
// game speed is right only when frames take exactly their interval. With the set realtime both read the patch page
|
|
// instead: +0 ratio = seconds * 30, +4 seconds (1.4.3: +8 interval = seconds * 60, the producer's nominal interval
|
|
// before its half-step coefficient), the moving average of the last 5 display intervals, at most 1/15 s
|
|
// (slower frames slow the game down, as on the console) and at least 1/240 s, so a higher frame-rate cap only changes
|
|
// the pacer (frame_step_cap_ns, NX2NAVI_FPS_LIMIT). The set cloth gives hair and cloth the same clock and needs realtime.
|
|
//
|
|
// The floor: with a shorter step (ratio below 0.125) 1.4.3 aborts within seconds of some save loads (issue #241, a
|
|
// 280 Hz monitor): Havok's hkDefaultMemoryImpl reports "Out of memory" for the buffer the physics pre-solve (profiler
|
|
// scope "TtPreSolve") resizes in main+0x1261630. Forced steps on one save: 1/240 s plays, 1/244 s and shorter
|
|
// abort; the field, sky island and fight saves play at 1/280 s, the town save aborts after about 800 frames. The
|
|
// same short steps also crash a physics worker reading that buffer's contacts: an access violation in main+0x1292560
|
|
// or its caller main+0x1286870 (narrowphase contact setup, reached from the grid task main+0x12f1ea0), reading a
|
|
// Havok body's shape at body+0x78 as null or garbage (issues #29 and #92, 240 Hz monitors with fps_limit = display).
|
|
// Steps drawn from 1/252..1/230 s crash that save's load in seconds; 1/240..1/230 s plays. The frame limit
|
|
// stops at 240 too (LIMIT_MAX_HZ), so the floor only catches the average dipping under 1/240 s at that limit: a
|
|
// present late by less than a period keeps the pacer's phase (nvn_api_obj.c, pace), so the next display interval is
|
|
// shorter than the period. No limit makes the game run slower than the clock.
|
|
//
|
|
// The sample is the display time the pacer gives each present (nvnQueuePresentTexture), so the step follows the
|
|
// frames as shown, pacing waits included. With the step on, the pacer ignores the game's interval and holds a frame
|
|
// only to the frame limit (nvn_api_obj.c present_period): the controller's 2 or 3 vsyncs would only add latency. The
|
|
// vsync event stays at 60 Hz whatever the limit: the game does not wait for it each frame (1.4.3 shows its title at 144
|
|
// fps with the event at 60 Hz).
|
|
// One 64-bit and one 32-bit store per present; the guest reads the floats on its own threads, and each stays finite
|
|
// and in range whichever store it sees.
|
|
//
|
|
// The world clock (frame_step_world_us) sums the steps from the first present that finds the player actor away from
|
|
// the origin, where the title screen keeps it (1.1.1 has no player actor until the save has loaded): the end of the
|
|
// first save load. Recorded and scripted input (input.c) runs on it, so a replay presses at the same game time at any
|
|
// frame rate and after a first load of any length; a later load (fast travel) still shifts what follows by its length.
|
|
//
|
|
// NX2NAVI_TRACE=perf adds a line per 120 presents: fps, the game seconds stepped against wall seconds, and with
|
|
// NX2NAVI_SPEED_PROBE=1 the player's position and ground speed (an independent check: Link runs at the same speed at
|
|
// any frame rate when game time follows wall time).
|
|
#include <math.h>
|
|
#include <stdatomic.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "debug_tools.h"
|
|
#include "cfg.h"
|
|
#include "gfx.h"
|
|
#include "host.h"
|
|
#include "layout.h"
|
|
#include "platform/platform.h"
|
|
#include "vk_dlssg.h"
|
|
#include "vk_fg.h"
|
|
#include "vk_lowlat.h"
|
|
|
|
#define STEP_SAMPLES 5
|
|
#define STEP_MAX_S (1.0 / 15.0) // longest step; a longer frame slows the game down
|
|
#define STEP_RESET_S 0.25 // a gap this long (loading, a debugger) restarts the average
|
|
#define STEP_FIRST_S (1.0 / 60.0)
|
|
#define STEP_MIN_S (1.0 / 240.0) // shortest step; a shorter one makes the game's physics run out of memory
|
|
|
|
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;
|
|
|
|
static void step_store(float seconds) {
|
|
step_now = seconds;
|
|
float ratio = seconds * 30.0f, interval = seconds * 60.0f;
|
|
uint32_t lo, hi, iv;
|
|
memcpy(&lo, &ratio, 4);
|
|
memcpy(&hi, &seconds, 4);
|
|
memcpy(&iv, &interval, 4);
|
|
atomic_store_explicit(step_word, (uint64_t)lo | (uint64_t)hi << 32, memory_order_release);
|
|
atomic_store_explicit(step_interval, iv, memory_order_release);
|
|
}
|
|
|
|
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;
|
|
}
|
|
if (!rt_patch_enabled(realtime) && rt_patch_enabled(cloth)) {
|
|
rt_patch_disable(cloth);
|
|
rt_log("frame rate: cloth clock off, it needs the real-time step (NX2NAVI_REALTIME=0)");
|
|
}
|
|
if (rt_patch_enabled(realtime)) {
|
|
step_word = (_Atomic uint64_t *)rt_patch_page();
|
|
step_interval = (_Atomic uint32_t *)((uint8_t *)rt_patch_page() + 8);
|
|
step_store((float)STEP_FIRST_S);
|
|
}
|
|
frame_limit_live(); // NX2NAVI_FPS_LIMIT decides the set fps60 from the start
|
|
rt_log("frame rate: present interval %s, %s", rt_patch_enabled(fps60) ? "1..3 (above 30 fps)" : "2..3 (shipping 30 fps)",
|
|
step_word ? "real-time step (5-frame average of display intervals, at most 1/15 s)" : "whole-vsync steps");
|
|
}
|
|
|
|
int frame_step_realtime(void) { return step_word != NULL; }
|
|
|
|
// The frame limit (menu, NX2NAVI_FPS_LIMIT, live): 30, 60, 90, 120, 144, any whole rate from 30 to 240, or display (the
|
|
// refresh rate of the window's monitor, at most 240). A higher number gives 240, the rate of the shortest step the
|
|
// game survives (STEP_MIN_S). Unset, the older NX2NAVI_FPS60=0 still means 30. A limit of 30 switches the set
|
|
// fps60 off (the shipping minimum interval 2), every other limit switches it on; the controller reads its minimum
|
|
// interval from the patched sites each frame. With the real-time step the pacer holds frames to the limit; above 60
|
|
// only the pacer changes, as the step's floor is the limit's ceiling. Without it the game steps whole 60 Hz vsyncs and the pacer
|
|
// follows the game's interval, so the limit stops at 60.
|
|
#define LIMIT_MIN_HZ 30
|
|
#define LIMIT_MAX_HZ 240 // 1 / STEP_MIN_S
|
|
static int fps60_set = -2;
|
|
static int game_interval = 1; // the present interval of the newest frame (frame_step_present)
|
|
|
|
// The rate "display" paces at: 0 is a monitor rate not known yet (no window), taken as 60; a real one below the
|
|
// lowest limit (a 24 or 25 Hz TV) is held at that limit rather than read as unknown and doubled to 60.
|
|
static double shown_hz(double hz) { return hz <= 0 ? 60 : hz < LIMIT_MIN_HZ ? LIMIT_MIN_HZ : hz; }
|
|
|
|
// A build without the fps60 and realtime patch sets (Ocarina) steps its game by the clock itself: walked by the same
|
|
// frame-numbered input script, Link covered half the distance at 120 fps that he covers at 60. There the pacer holds
|
|
// frames to the limit alone (nvn_api_obj.c present_period), the game's own 60 Hz interval aside, and the limit may be
|
|
// any rate up to LIMIT_FREE_MAX_HZ, or "unlimited", which paces the game at that rate too. Ocarina's code does fail
|
|
// past some rate: on the quick menu, where a frame costs the GPU almost nothing, it ran at 640 to 700 fps and after
|
|
// about two seconds read address 0 in its own draw code (main+0x175b2a0, the same call stack twice), while 92 to 255
|
|
// fps ran on. 240 keeps every common monitor's rate; with frame generation it counts the game's own frames, so the
|
|
// screen still shows the generated ones above it.
|
|
#define LIMIT_FREE_MAX_HZ 240
|
|
int frame_free_running(void) { return rt_patch_set("fps60") < 0 && rt_patch_set("realtime") < 0; }
|
|
int frame_limit_adjustable(void) { return 1; }
|
|
static unsigned limit_seen;
|
|
static int limit_read;
|
|
static int limit_hz = -1; // 0: display; -2: unlimited (free-running builds); -1 before the first read
|
|
|
|
static int limit_parse(void) {
|
|
const char *e = cfg_get("NX2NAVI_FPS_LIMIT"), *old = cfg_get("NX2NAVI_FPS60");
|
|
if (!e || !*e) return old && !strcmp(old, "0") ? 30 : 60;
|
|
if (!strcmp(e, "display")) return 0;
|
|
int free_run = frame_free_running();
|
|
if (!strcmp(e, "unlimited")) {
|
|
if (free_run) return -2;
|
|
rt_log("frame rate: NX2NAVI_FPS_LIMIT=unlimited needs a game that steps by the clock; %d", LIMIT_MAX_HZ);
|
|
return LIMIT_MAX_HZ;
|
|
}
|
|
char *end;
|
|
long n = strtol(e, &end, 10);
|
|
if (!*end && free_run && n >= LIMIT_MIN_HZ) return n > LIMIT_FREE_MAX_HZ ? LIMIT_FREE_MAX_HZ : (int)n;
|
|
if (!*end && n > LIMIT_MAX_HZ) {
|
|
rt_log("frame rate: NX2NAVI_FPS_LIMIT=%s is above %d, where the game's physics runs out of memory; %d", e,
|
|
LIMIT_MAX_HZ, LIMIT_MAX_HZ);
|
|
return LIMIT_MAX_HZ;
|
|
}
|
|
if (!*end && n >= LIMIT_MIN_HZ) return (int)n;
|
|
rt_log("frame rate: NX2NAVI_FPS_LIMIT=%s is not %d..%d, display or unlimited; 60", e, LIMIT_MIN_HZ, LIMIT_MAX_HZ);
|
|
return 60;
|
|
}
|
|
|
|
static void frame_limit_live(void) {
|
|
unsigned v = cfg_version();
|
|
if (limit_read && v == limit_seen) return;
|
|
limit_read = 1;
|
|
limit_seen = v;
|
|
int hz = limit_parse();
|
|
if (hz == limit_hz) return;
|
|
limit_hz = hz;
|
|
if (frame_free_running()) {
|
|
if (hz == -2) rt_log("frame rate: unlimited (the game steps by the clock), at most %d game frames a second",
|
|
LIMIT_FREE_MAX_HZ);
|
|
else if (hz) rt_log("frame rate: limit %d fps (the game steps by the clock)", hz);
|
|
else rt_log("frame rate: limit at the monitor's refresh rate (the game steps by the clock)");
|
|
return;
|
|
}
|
|
if (fps60_set == -2) fps60_set = rt_patch_set("fps60");
|
|
if (fps60_set >= 0) rt_patch_switch(fps60_set, hz != 30);
|
|
if ((hz > 60 || hz == 0) && !step_word)
|
|
rt_log("frame rate: limits above 60 need the real-time step (NX2NAVI_REALTIME=1); 60 fps");
|
|
else if (hz) rt_log("frame rate: limit %d fps", hz);
|
|
else if (gfx_window_refresh_hz() > 0) rt_log("frame rate: limit at the monitor's refresh rate (%.3f Hz now)", gfx_window_refresh_hz());
|
|
else rt_log("frame rate: limit at the monitor's refresh rate (60 until the window measures it)");
|
|
}
|
|
|
|
// NX2NAVI_MOVIE_FPS (default 60; 0 = off): the game's frame rate while a movie plays (ov_movie2.c ov_movie_live). A
|
|
// movie's event counts the game's updates as 60 Hz frames whatever the step: at 144 fps the intro's narration lines
|
|
// started 2.4 times too early, each over the one before, and its closing fade to black came at 65 s of the movie's
|
|
// 154, hiding the whole dream. The movies are 60 fps, so the cap shows the same pictures. It is the rate in both
|
|
// directions: under a lower limit (40 fps, or 60 shown with frame generation, which is 30 of the game's) the movie ran
|
|
// at that fraction of its speed and ended many seconds behind its sound, so the game is paced at the movie's rate
|
|
// while one plays, above the limit too.
|
|
// With set omovie of recomp/patches-v111.txt (NX2NAVI_MOVIE_PTS, on by default) the event count follows the shown
|
|
// picture's timestamp instead of the redraws, so with the cap off the cues no longer run ahead; the cap stays for the
|
|
// pacing in both directions and for builds without those sites.
|
|
static int movie_cap_hz(void) {
|
|
static int hz = -1;
|
|
if (hz < 0) {
|
|
const char *e = cfg_get("NX2NAVI_MOVIE_FPS");
|
|
hz = e && *e ? atoi(e) : 60;
|
|
if (hz && hz < LIMIT_MIN_HZ) hz = 60;
|
|
}
|
|
return hz && ov_movie_live() ? hz : 0;
|
|
}
|
|
|
|
// Frames on screen per game frame while frame generation runs: 2 with AMD's (vk_fg.cpp, one generated frame), the
|
|
// multiplier with NVIDIA's (vk_dlssg.c, one to three); 1 while neither generates (off, or paused for the menu).
|
|
static int generated_factor(void) { return vk_fg_generating() ? 2 : vk_dlssg_multiplier(); }
|
|
|
|
// The limit counts the frames the game draws, and frame generation adds its frames on top: a limit of 60 at 4x is 60
|
|
// drawn and 240 shown. Counted on the screen instead, the game was paced at the limit over the factor, so each step up
|
|
// of the multiplier took drawn frames away (60 at 4x drew 15) and the generated ones had less to work from and more
|
|
// delay. Only "display" is a rate on the screen, the monitor's: there the game draws that rate over the factor, since a
|
|
// frame past the monitor's refresh is never seen. The result is game frames a second, before the ceiling of the step.
|
|
static double limit_game_hz(int limit, int gen) {
|
|
if (limit) return shown_hz(limit);
|
|
double hz = shown_hz(gfx_window_refresh_hz());
|
|
return gen > 1 ? hz / gen : hz;
|
|
}
|
|
|
|
// The rate the game renders at under the limit (mouse_direct.c, whose camera input follows the game's own frame time);
|
|
// 0 unknown or unlimited.
|
|
double frame_step_game_hz(void) {
|
|
if (frame_step_limit_hz() <= 0) return 0;
|
|
double hz = limit_game_hz(limit_hz, generated_factor());
|
|
return !frame_free_running() && !step_word && hz > 60 ? 60 : hz > LIMIT_MAX_HZ ? LIMIT_MAX_HZ : hz;
|
|
}
|
|
|
|
// RivaTuner Statistics Server (rtss_limit.c) gets the same limit; while it limits this process the pacer holds no
|
|
// frames, so the two never stack. NX2NAVI_RTSS=0 and frame generation (RTSS would count the generated frames) keep it
|
|
// out, and so does a driver's low latency while it is in effect (vk_lowlat.c): the driver holds the limit where the
|
|
// frame starts, and RTSS would hold the finished picture, the wait that mode is there to remove.
|
|
static double paced_hz; // the game frames a second the newest frame_step_cap_ns paces at (ocarina_step); 0: none
|
|
static int movie_paced; // the movie rate the newest frame_step_cap_ns paces at (ocarina_step, frame_step_movie_hz); 0: none
|
|
int frame_step_movie_hz(void) { return movie_paced; }
|
|
uint64_t frame_step_cap_ns(void) {
|
|
frame_limit_live();
|
|
static int rtss_allowed = -1;
|
|
if (rtss_allowed < 0) {
|
|
const char *e = cfg_get("NX2NAVI_RTSS");
|
|
rtss_allowed = !(e && !strcmp(e, "0"));
|
|
}
|
|
int rtss = rtss_allowed && !vk_fg_wanted() && !vk_dlssg_active() && !vk_lowlat_on();
|
|
int gen = generated_factor();
|
|
int movie_hz = movie_cap_hz();
|
|
static int movie_clock = -1; // NX2NAVI_MOVIE_CLOCK=0: the steady step and the usual pacing through a movie too
|
|
if (movie_clock < 0) {
|
|
const char *e = cfg_get("NX2NAVI_MOVIE_CLOCK");
|
|
movie_clock = !(e && !strcmp(e, "0"));
|
|
}
|
|
movie_paced = movie_clock ? movie_hz : 0;
|
|
static int movie_was, movie_logs;
|
|
if (!!movie_hz != movie_was) {
|
|
movie_was = !!movie_hz;
|
|
if (movie_logs++ < 40) { // the first 20 movies
|
|
if (movie_hz) rt_log("frame rate: a movie plays: %d fps until it ends (NX2NAVI_MOVIE_FPS)", movie_hz);
|
|
else rt_log("frame rate: the movie ended: the frame limit again");
|
|
}
|
|
}
|
|
if (limit_hz == -2 && !movie_hz && !frame_free_running()) { // unlimited: the pacer does not hold frames, and
|
|
rtss_limit_want(rtss ? 0 : -1); // RTSS's limit is cleared
|
|
paced_hz = 0;
|
|
return 0;
|
|
}
|
|
// unlimited in a build that steps by the clock: LIMIT_FREE_MAX_HZ game frames; during a movie, the movie's rate
|
|
double hz = limit_hz == -2 ? LIMIT_FREE_MAX_HZ : limit_game_hz(limit_hz, gen);
|
|
double max = frame_free_running() ? LIMIT_FREE_MAX_HZ : LIMIT_MAX_HZ;
|
|
if (hz > max) hz = max;
|
|
static double logged_hz;
|
|
static int logged_gen, gen_logs;
|
|
if (gen > 1 && (hz != logged_hz || gen != logged_gen) && gen_logs < 40) {
|
|
gen_logs++, logged_hz = hz, logged_gen = gen;
|
|
rt_log("frame rate: the game draws up to %.0f frames a second and frame generation shows %d for each (%.0f on "
|
|
"screen)", hz, gen, hz * gen);
|
|
}
|
|
if (movie_hz) hz = movie_hz;
|
|
paced_hz = hz;
|
|
rtss_limit_want(rtss ? (int64_t)(hz * 1000 + 0.5) : -1);
|
|
if (rtss_limit_active()) return 0;
|
|
return (uint64_t)(1e9 / hz + 0.5);
|
|
}
|
|
|
|
// The limit as it is set, for the performance overlay: the setting, the monitor's rate for "display", 60 at
|
|
// most without the real-time step; the game's own rate (60 Hz over its present interval) in a build without the patch
|
|
// sets; 0 before the first frame read the setting.
|
|
double frame_step_limit_hz(void) {
|
|
if (frame_free_running()) { // 0 for unlimited, as before the first read
|
|
frame_limit_live();
|
|
int limit = limit_hz;
|
|
if (limit == -2 || limit == -1) return 0;
|
|
return shown_hz(limit ? limit : gfx_window_refresh_hz());
|
|
}
|
|
int hz = limit_hz;
|
|
if (hz < 0) return 0;
|
|
double r = shown_hz(hz ? hz : gfx_window_refresh_hz());
|
|
return !step_word && r > 60 ? 60 : r > LIMIT_MAX_HZ ? LIMIT_MAX_HZ : r;
|
|
}
|
|
|
|
// ---- NX2NAVI_TRACE=perf speed lines
|
|
static uint64_t win_t0, win_frames, win_hist[4]; // win_hist: frames by present interval 1, 2, 3+ ([0] unused)
|
|
static double win_game;
|
|
static int probe = -1;
|
|
static float last_pos[3];
|
|
static uint64_t last_pos_ns;
|
|
|
|
// plat_mem_read copies through the kernel, which fails on a heap page NX2NAVI_HEAP_LAZY has not committed yet
|
|
static int read_guest(uint64_t a, void *out, size_t n) {
|
|
if (!a) return 0;
|
|
mem_kernel_access(a, n);
|
|
return plat_mem_read(a, out, n);
|
|
}
|
|
static uint64_t read_ptr(uint64_t a) {
|
|
uint64_t p = 0;
|
|
return read_guest(a, &p, 8) ? p : 0;
|
|
}
|
|
// 1.1.1: [[main+0x4e371d0] + 0x480] is the Player actor, its name pointer at +0x20 reads "Player" and its position, in
|
|
// metres, is the float triplet at +0x31c. The slot is empty until the first save has loaded and then holds the actor.
|
|
// The other builds: [main+0x4728538] (1.1.0: [main+0x4630a68], 1.4.3: [main+0x3add238]) +0x1e8 scene, its component 13
|
|
// lists the resident actors; the one named "Player" has its position at +0x2b4.
|
|
// The 1.1.0 slot was measured from its own dereference chain: 181 scene accesses in that build
|
|
// read it, and one of them reproduces this walk (the 14-component check, component 13, the 0x70 stride and the name
|
|
// compare) instruction for instruction.
|
|
static int player_name(uint64_t name) {
|
|
char text[7];
|
|
return name && read_guest(name, text, 7) && !memcmp(text, "Player", 7);
|
|
}
|
|
|
|
static int player_pos(float xyz[3]) {
|
|
if (RT_V111) {
|
|
uint64_t manager = read_ptr(MOD_MAIN_BASE + 0x4e371d0), actor = manager ? read_ptr(manager + 0x480) : 0;
|
|
return actor && player_name(read_ptr(actor + 0x20)) && read_guest(actor + 0x31c, xyz, 12) && isfinite(xyz[0]) &&
|
|
isfinite(xyz[2]);
|
|
}
|
|
uint64_t module = read_ptr(MOD_MAIN_BASE + (RT_V143 ? 0x3add238 : RT_V110 ? 0x4630a68 : 0x4728538)),
|
|
scene = module ? read_ptr(module + 0x1e8) : 0;
|
|
int32_t count = 0;
|
|
if (!scene || !read_guest(scene + 0x50, &count, 4) || count < 14 || count > 256) return 0;
|
|
uint64_t comps = read_ptr(scene + 0x58), resident = comps ? read_ptr(comps + 13 * 8) : 0;
|
|
if (!resident || !read_guest(resident + 0x20, &count, 4) || count < 1 || count > 64) return 0;
|
|
uint64_t links = read_ptr(resident + 0x28);
|
|
for (int i = 0; links && i < count; i++) {
|
|
uint64_t data = read_ptr(links + (uint64_t)i * 0x70 + 8), actor = data ? read_ptr(data + 0x40) : 0;
|
|
if (actor && player_name(read_ptr(actor + 0x218)))
|
|
return read_guest(actor + 0x2b4, xyz, 12) && isfinite(xyz[0]) && isfinite(xyz[2]);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// Ocarina 1.1.1's time manager, **[main+0x4c7e890]: +0x1c0 the frame's base step in 60 Hz frames, +0x10 the game
|
|
// speed (main+0x2c84c makes each time category's {frames, 1/frames, seconds, 1/seconds} from them and the category's
|
|
// factor). Read by the speed lines of a free-running build: the game seconds a frame stepped, or -1.
|
|
static double ocarina_step_s(void) {
|
|
uint64_t slot = read_ptr(MOD_MAIN_BASE + 0x4c7e890), mgr = slot ? read_ptr(slot) : 0;
|
|
float base = 0, speed = 0;
|
|
if (!mgr || !read_guest(mgr + 0x1c0, &base, 4) || !read_guest(mgr + 0x10, &speed, 4) || !isfinite(base) || !isfinite(speed))
|
|
return -1;
|
|
return base * speed / 60.0;
|
|
}
|
|
|
|
static _Atomic uint64_t guest_presents; // frame_step_present calls so far (frame_step_flight)
|
|
|
|
// The flight recorder's view of the step at a host present (flight.c, render thread): the newest step the patch page
|
|
// holds in 60 Hz frames per update (set ostep; -1 without), the game's own step from its time manager in 60 Hz frames
|
|
// (-1 unknown or not free-running), and the game's presents so far, which name the update those two belong to.
|
|
void frame_step_flight(float *page, float *game, uint64_t *presents) {
|
|
*page = ostep_page ? frame_step_frames() : -1.0f;
|
|
double s = frame_free_running() ? ocarina_step_s() : -1;
|
|
*game = s > 0 ? (float)(s * 60.0) : -1.0f;
|
|
*presents = atomic_load_explicit(&guest_presents, memory_order_relaxed);
|
|
}
|
|
|
|
// Free-running builds (Ocarina): the game's step follows the display interval (ocarina_step, in 60 Hz frames), written
|
|
// at each present: frames per update on the patch page with the set ostep, else the time manager's base step. The
|
|
// game left it at one 60 Hz frame whatever the frame rate, so at 144 fps it ran 2.4 times too fast.
|
|
// NX2NAVI_OCARINA_STEP=0 leaves it. Counts how often the game itself had changed the value since the last write
|
|
// (diagnosis).
|
|
static uint64_t ostep_writes, ostep_changed;
|
|
static float ostep_last = -1;
|
|
|
|
// The step Ocarina is given for a display interval dt, in seconds. The game ships with a step of one 60 Hz frame (two
|
|
// at 30 fps) that never changes in play, so nothing in it was written for a step that differs from frame to frame: its
|
|
// physics takes the step as the length of the next solve while the bodies' velocities come from the step before
|
|
// (the physics system keeps both, main+0x38af1f0), and it derives whole numbers from it (the solver step count,
|
|
// patches-v111.txt osolver). The mean of the last 5 intervals, which stood here, moved with every one of them: display
|
|
// intervals of 5 and 30 ms in turn gave steps of 15 and 20 ms in turn, and one 60 ms frame at 200 fps took the step
|
|
// from 5 to 16 ms for five frames and back. So the step is kept steady and the clock is kept by a slow correction:
|
|
// - the base is the mean of the last 12 intervals without their longest and shortest: one late frame and the short
|
|
// one after it (nvn_api_obj.c pace keeps the phase) leave it alone, intervals alternating in a period of 2, 3 or 4
|
|
// frames give one value, and a real change of frame rate arrives within 12 frames;
|
|
// - a base within 3% of the pacer's period is that period: at a frame limit the game holds, the step is the same
|
|
// number every frame, at 60 fps exactly the one frame the game ships with;
|
|
// - what the display showed and the game was not given is owed (the late frame cut from the base, the 3% of the
|
|
// snap). Past half a step of it the step repays a 64th a frame, 10% of the step at most, so the game still follows
|
|
// the clock and a hitch is paid back over the next second rather than in five frames. More than STEP_RESET_S owed
|
|
// is dropped: the game runs slower than the clock, as it does for a frame longer than STEP_MAX_S.
|
|
// The step is at least OSTEP_MIN_S: the solver step count of the set osolver reaches 0 under a 16th of a frame.
|
|
// NX2NAVI_STEP_STEADY=0 gives the mean of the last 5 intervals again (comparison runs).
|
|
#define OSTEP_WINDOW 12
|
|
#define OSTEP_SNAP 0.03
|
|
#define OSTEP_REPAY_MAX 0.10
|
|
#define OSTEP_MIN_S (1.0 / 480.0)
|
|
static double os_ring[OSTEP_WINDOW], os_owed;
|
|
static unsigned os_count, os_next;
|
|
static double os_step_lo, os_step_hi; // the steps given since the last speed line (speed_report)
|
|
|
|
static void ocarina_step_reset(void) {
|
|
os_count = os_next = 0;
|
|
os_owed = 0;
|
|
}
|
|
|
|
// While a movie plays (frame_step_cap_ns paces the game at the movie's rate) the step is exactly one movie frame, whatever
|
|
// the display intervals; NX2NAVI_MOVIE_CLOCK=0 leaves movies to the steady step (comparison runs). The game's movie
|
|
// player (main+0x3b5ca60) adds the time manager's seconds times 60 to an accumulator at each update and advances the
|
|
// movie by its whole pictures, at most the player's limit per update (+0x20, set at main+0x3b5d094: 1, or 2 in one of
|
|
// its modes), keeping the rest. The Triforce movie showed one picture per update (600 pictures in 600 updates at 59.6
|
|
// updates a second), so the time a late frame left owed, which the steady step pays back as steps over one frame, never
|
|
// became pictures: the picture fell behind its sound, which plays by the clock, by each late frame for good. The
|
|
// movie's event counts updates as 60 Hz frames too (frame_step_cap_ns). So the updates themselves keep the clock
|
|
// (nvn_api_obj.c pace keeps the movie's slots through a late frame) and each counts as one movie frame, for the
|
|
// picture, the event and the game's clock alike. The filter starts again at a movie's start and end, so neither rate's
|
|
// intervals reach the other.
|
|
static double ocarina_step(double dt) {
|
|
static int steady = -1, movie_was;
|
|
if (steady < 0) {
|
|
const char *e = cfg_get("NX2NAVI_STEP_STEADY");
|
|
steady = !(e && !strcmp(e, "0"));
|
|
}
|
|
if (movie_paced != movie_was) {
|
|
movie_was = movie_paced;
|
|
ocarina_step_reset();
|
|
}
|
|
if (movie_paced > 0) {
|
|
double step = 1.0 / movie_paced;
|
|
if (os_step_lo <= 0 || step < os_step_lo) os_step_lo = step;
|
|
if (step > os_step_hi) os_step_hi = step;
|
|
return step;
|
|
}
|
|
if (dt > STEP_MAX_S) dt = STEP_MAX_S;
|
|
os_ring[os_next++ % OSTEP_WINDOW] = dt;
|
|
if (os_count < OSTEP_WINDOW) os_count++;
|
|
double step;
|
|
if (!steady) {
|
|
unsigned n = os_count < STEP_SAMPLES ? os_count : STEP_SAMPLES;
|
|
double sum = 0;
|
|
for (unsigned i = 1; i <= n; i++) sum += os_ring[(os_next - i) % OSTEP_WINDOW];
|
|
step = sum / n;
|
|
if (step < 1.0 / 1000) step = 1.0 / 1000;
|
|
} else {
|
|
double sum = 0, lo = os_ring[0], hi = os_ring[0];
|
|
for (unsigned i = 0; i < os_count; i++) {
|
|
sum += os_ring[i];
|
|
if (os_ring[i] < lo) lo = os_ring[i];
|
|
if (os_ring[i] > hi) hi = os_ring[i];
|
|
}
|
|
double base = os_count >= 4 ? (sum - lo - hi) / (os_count - 2) : sum / os_count;
|
|
double period = paced_hz > 0 ? 1.0 / paced_hz : 0;
|
|
if (period > 0 && fabs(base - period) <= OSTEP_SNAP * period) base = period;
|
|
double over = fabs(os_owed) - base * 0.5, repay = 0;
|
|
if (over > 0) {
|
|
repay = over / 64 < OSTEP_REPAY_MAX * base ? over / 64 : OSTEP_REPAY_MAX * base;
|
|
if (os_owed < 0) repay = -repay;
|
|
}
|
|
step = base + repay;
|
|
if (step < OSTEP_MIN_S) step = OSTEP_MIN_S;
|
|
if (step > STEP_MAX_S) step = STEP_MAX_S;
|
|
os_owed += dt - step;
|
|
if (os_owed > STEP_RESET_S) os_owed = STEP_RESET_S;
|
|
if (os_owed < -STEP_RESET_S) os_owed = -STEP_RESET_S;
|
|
}
|
|
if (os_step_lo <= 0 || step < os_step_lo) os_step_lo = step;
|
|
if (step > os_step_hi) os_step_hi = step;
|
|
return step;
|
|
}
|
|
|
|
// ---- whole frames out of the real-time step (the set ostep's sites that cut it to a whole number, patches-v111.txt)
|
|
// The game adds the step, or the step times a whole number, to a few counters and cuts the sum to a whole number: at the
|
|
// console's 60 fps the step is 1 and they count frames. With the step the display interval in 60 Hz frames, under one
|
|
// 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), and so do
|
|
// the fixed steps of the water and floor surfaces (frame_rate_sim_host).
|
|
#define WHOLE_SLOTS 8
|
|
#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
|
|
if (f != f) return 0;
|
|
if (f >= 2147483648.0f) return 0x7fffffffu;
|
|
if (f <= -2147483648.0f) return 0x80000000u;
|
|
return (uint32_t)(int32_t)f;
|
|
}
|
|
|
|
// 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 < n; i++) {
|
|
if (slot[i].at == at) { k = i; break; }
|
|
if (slot[i].used < slot[oldest].used) oldest = i;
|
|
}
|
|
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);
|
|
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.
|
|
void frame_rate_day_host(Cpu *c) {
|
|
uint32_t bits = (uint32_t)c->v[0];
|
|
float secs;
|
|
memcpy(&secs, &bits, 4);
|
|
uint32_t whole = whole_frames(c->x[19] + 0x3c, secs);
|
|
c->x[8] = whole;
|
|
if (rt_trace & RT_TRACE_PERF) {
|
|
static uint64_t t0, given, cut;
|
|
static double exact;
|
|
uint64_t now = plat_now_ns();
|
|
given += whole, cut += cut_fcvtzs(secs), exact += secs;
|
|
if (!t0) t0 = now;
|
|
if (now - t0 >= 10000000000ull) {
|
|
rt_log("frame rate: the time of day got %llu game seconds in %.1f s (the frames' step makes %.1f; cut "
|
|
"to whole seconds each frame, as the game cuts them, %llu)", (unsigned long long)given,
|
|
(now - t0) / 1e9, exact, (unsigned long long)cut);
|
|
t0 = now, given = cut = 0, exact = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 fixed steps of the physics' height-field surfaces (patches-v111.txt owater): the water managers (main+0x3875f20,
|
|
// main+0x3882660) and the interactive floors' simulation (main+0x632cb0, the spider webs' sinking floor). Each update
|
|
// divides its seconds by 1/60 s into s0 and cuts that to the number of steps of exactly 1/60 s it runs (at most 3),
|
|
// dropping the fraction, so every frame shorter than 1/60 s ran none. The site calls this between the division and
|
|
// the game's own cut: s0 becomes the whole steps in the quotient plus the remainder kept for the object in x19, as a
|
|
// float the cut keeps exactly, and only s0 changes (its upper lanes stay zero, as the division left them). A quotient
|
|
// that is not positive, or too large for a count, stays for the cut. The remainders are in a table of their own: an
|
|
// object that shares slots with the counters above would lose its fraction to them.
|
|
// NX2NAVI_TRACE=perf: every 10 s, the steps the objects were given beside those the cut alone would have given.
|
|
#define SIM_SLOTS 8
|
|
static WholeSlot sim_slot[SIM_SLOTS];
|
|
|
|
void frame_rate_sim_host(Cpu *c) {
|
|
uint32_t bits = (uint32_t)c->v[0];
|
|
float steps;
|
|
memcpy(&steps, &bits, 4);
|
|
uint32_t whole = whole_take(sim_slot, SIM_SLOTS, c->x[19], steps);
|
|
if (steps > 0.0f && steps < 2147483648.0f) { // whole_take's own range: outside it the game's cut stands
|
|
float f = (float)whole;
|
|
memcpy(&bits, &f, 4);
|
|
c->v[0] = bits;
|
|
}
|
|
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);
|
|
asked++, given += whole, cut += cut_fcvtzs(steps);
|
|
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 and floor surfaces got %llu fixed 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) {
|
|
if (!ostep_page) return 1.0f;
|
|
uint32_t bits = atomic_load_explicit(ostep_page, memory_order_acquire);
|
|
float f;
|
|
memcpy(&f, &bits, 4);
|
|
return f > 0 ? f : 1.0f;
|
|
}
|
|
|
|
static void ocarina_step_write(double dt) {
|
|
static int on = -1;
|
|
if (on < 0) {
|
|
const char *e = cfg_get("NX2NAVI_OCARINA_STEP");
|
|
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 || !mem_host_readable(mgr + 0x1c0, 4)) return;
|
|
_Atomic uint32_t *word = (_Atomic uint32_t *)(uintptr_t)(mgr + 0x1c0);
|
|
uint32_t now_bits = atomic_load_explicit(word, memory_order_relaxed);
|
|
float now_v;
|
|
memcpy(&now_v, &now_bits, 4);
|
|
if (ostep_last >= 0 && now_v != ostep_last) ostep_changed++;
|
|
if (dt < 1.0 / 1000) dt = 1.0 / 1000;
|
|
if (dt > STEP_MAX_S) dt = STEP_MAX_S;
|
|
float v = (float)(dt * 60.0);
|
|
uint32_t bits;
|
|
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;
|
|
return;
|
|
}
|
|
win_frames++;
|
|
win_game += step;
|
|
win_hist[interval < 1 ? 1 : interval > 3 ? 3 : interval]++;
|
|
if (win_frames < 120) return;
|
|
if (probe < 0) {
|
|
const char *e = cfg_get("NX2NAVI_SPEED_PROBE");
|
|
probe = e && !strcmp(e, "1");
|
|
}
|
|
double wall = (double)(now - win_t0) * 1e-9;
|
|
char pos[240] = "";
|
|
float p[3];
|
|
if (probe && player_pos(p)) {
|
|
double d = last_pos_ns ? hypot(p[0] - last_pos[0], p[2] - last_pos[2]) : 0,
|
|
s = last_pos_ns ? (double)(now - last_pos_ns) * 1e-9 : 0;
|
|
snprintf(pos, sizeof pos, "; player %.1f %.1f %.1f, %.2f m in %.3f s = %.2f m/s", p[0], p[1], p[2], d, s,
|
|
s > 0 ? d / s : 0);
|
|
memcpy(last_pos, p, sizeof p);
|
|
last_pos_ns = now;
|
|
}
|
|
float global_step; // Ocarina 1.1.1: the engine's global step (patches-v111.txt, the set ostep's second site)
|
|
if (probe && ostep_page && read_guest(MOD_MAIN_BASE + 0x4d41fc8, &global_step, 4)) {
|
|
size_t o = strlen(pos);
|
|
snprintf(pos + o, sizeof pos - o, "; global step %.3f frames", (double)global_step);
|
|
}
|
|
if (os_step_hi > 0) { // Ocarina: the shortest and longest step given in these frames, and the time still owed
|
|
size_t o = strlen(pos);
|
|
snprintf(pos + o, sizeof pos - o, "; step %.3f..%.3f frames, %.1f ms owed", os_step_lo * 60, os_step_hi * 60,
|
|
os_owed * 1000);
|
|
os_step_lo = os_step_hi = 0;
|
|
}
|
|
if (step_word || win_game > 0)
|
|
rt_log("fps: %llu frames in %.3f s (%.1f fps), %.3f s game stepped, speed %.3f; interval 1/2/3+ %llu/%llu/%llu%s",
|
|
(unsigned long long)win_frames, wall, (double)win_frames / wall, win_game, win_game / wall,
|
|
(unsigned long long)win_hist[1], (unsigned long long)win_hist[2], (unsigned long long)win_hist[3], pos);
|
|
else
|
|
rt_log("fps: %llu frames in %.3f s (%.1f fps), whole-vsync steps; interval 1/2/3+ %llu/%llu/%llu%s",
|
|
(unsigned long long)win_frames, wall, (double)win_frames / wall, (unsigned long long)win_hist[1],
|
|
(unsigned long long)win_hist[2], (unsigned long long)win_hist[3], pos);
|
|
win_t0 = now;
|
|
win_frames = 0;
|
|
win_game = 0;
|
|
memset(win_hist, 0, sizeof win_hist);
|
|
}
|
|
|
|
static _Atomic int64_t world_us = -1;
|
|
static _Atomic uint64_t game_us; // game time stepped since the first present (frame_step_ticks)
|
|
static uint64_t presents;
|
|
|
|
int64_t frame_step_world_us(void) { return atomic_load_explicit(&world_us, memory_order_relaxed); }
|
|
uint64_t frame_step_ticks(void) { return atomic_load_explicit(&game_us, memory_order_relaxed) * 60 / 1000000; }
|
|
|
|
static void world_clock(double dt, double step) {
|
|
presents++;
|
|
int64_t w = atomic_load_explicit(&world_us, memory_order_relaxed);
|
|
if (w < 0) {
|
|
float p[3];
|
|
if (!player_pos(p) || fabsf(p[0]) + fabsf(p[2]) < 1.0f) return; // a few guest reads, until the load ends
|
|
rt_log("frame step: world clock starts at present %llu, %.2f s after the guest started (the player at %.1f %.1f %.1f)",
|
|
(unsigned long long)presents, rt_guest_start_ns ? (kern_now_ns() - rt_guest_start_ns) / 1e9 : 0.0, p[0],
|
|
p[1], p[2]);
|
|
atomic_store_explicit(&world_us, 0, memory_order_relaxed);
|
|
return;
|
|
}
|
|
if (!step_word) step = dt > STEP_MAX_S ? STEP_MAX_S : dt; // whole-vsync steps: the display time stands in
|
|
atomic_store_explicit(&world_us, w + (int64_t)(step * 1e6 + 0.5), memory_order_relaxed);
|
|
}
|
|
|
|
void frame_step_present(uint64_t display_ns, int interval) {
|
|
atomic_fetch_add_explicit(&guest_presents, 1, memory_order_relaxed);
|
|
game_interval = interval > 0 ? interval : 1;
|
|
frame_limit_live();
|
|
double step = 0, dt = last_ns ? (double)(display_ns - last_ns) * 1e-9 : 0;
|
|
if (step_word && last_ns) {
|
|
if (dt > STEP_RESET_S || dt < 0.001) {
|
|
sample_count = sample_next = 0;
|
|
} else {
|
|
if (dt > STEP_MAX_S) dt = STEP_MAX_S;
|
|
samples[sample_next++ % STEP_SAMPLES] = dt;
|
|
if (sample_count < STEP_SAMPLES) sample_count++;
|
|
double sum = 0;
|
|
for (unsigned i = 0; i < sample_count; i++) sum += samples[i];
|
|
step = sum / sample_count;
|
|
if (step < STEP_MIN_S) step = STEP_MIN_S;
|
|
step_store((float)step);
|
|
}
|
|
}
|
|
if (frame_free_running() && last_ns) {
|
|
// the debug menu's speed factor (debug_tools.c) multiplies the step the game is given
|
|
if (dt > 0 && dt < STEP_RESET_S) ocarina_step_write(ocarina_step(dt) * (double)dbg_speed_factor());
|
|
else ocarina_step_reset(); // a load or a debugger stop: the intervals before it say nothing about the next
|
|
}
|
|
last_ns = display_ns;
|
|
// The game clock of untimed script lines: what the game stepped this frame, the patch page's step with the
|
|
// real-time step (kept through a reset), whole vsyncs without it.
|
|
double game = step_word ? step_now : ostep_last > 0 ? ostep_last / 60.0 : (interval > 0 ? interval : 1) / 60.0;
|
|
atomic_store_explicit(&game_us, atomic_load_explicit(&game_us, memory_order_relaxed) + (uint64_t)(game * 1e6 + 0.5),
|
|
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;
|
|
}
|
|
speed_report(display_ns, step, interval);
|
|
}
|
|
}
|