mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Round the physics solver's step count up above 60 fps and give Ocarina a steady step.
Above 60 fps players fell off moving platforms. The physics system takes its solver step count for a frame as trunc(frames per update * 4 + 0.001): 4 at 60 fps and 8 at 30, the rates the game ships with, but 3 from 61 to 80 fps, 2 to 120, 1 to 240 (144 solver steps a second at 144 fps against 240 at 60), and 0 in a frame shorter than a quarter of a 60 Hz frame. The solver skips its whole solve and sub-integration loop at 0, so such a frame resolves no contact or friction and moves no rigid body. At a 240 fps limit, which "unlimited" paces at too, that was the frame after every late one: the pacer keeps its phase, so the next display interval is shorter than the period. The new patch set osolver rounds the count up, ceil(frames per update * 4 - 0.25): 4 and 8 as before, never fewer steps a second than at 60 fps, at least 1. physics_steps = 0 gives the game's own count. The step itself was the mean of the last five display intervals and changed with every frame: 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. The game never saw a changing step on the console. The step is now the mean of the last 12 intervals without the longest and the shortest, is the pacer's period when within 3% of it (at a 60 fps limit exactly one frame, every frame), and repays the time the game was not given at up to a tenth of the step, so game time still follows the clock. It is at least 1/480 s. step_steady = 0 gives the mean of five again. With trace = perf the fps lines show the shortest and longest step and the time owed. tests/frame_step_check.py drives frame_rate.c with made-up display times and the rest of the runtime stubbed. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -244,6 +244,11 @@ launcher imports a zip it wrote. The second builds `fs_host.c` and `save_share.c
|
||||
and runs each step as a start of the game on a profile it makes: the export, an import from a zip the launcher
|
||||
exported, its backup and swap at the next start, the zips it refuses and Cancel. No game files.
|
||||
|
||||
A change to the step Ocarina is given each frame (`runtime/frame_rate.c` `ocarina_step`) passes
|
||||
`python tests\frame_step_check.py`, which builds `frame_rate.c` with the rest of the runtime stubbed and feeds it
|
||||
display times: a held 60 fps limit gives exactly one frame every frame, a 240 fps limit never less than a quarter
|
||||
frame, uneven frame times one steady step, and game time follows the clock. No game files.
|
||||
|
||||
A change to the button prompts (`tools/make_romfs_patches.py --prompts`, `tools/button_icons.py`) passes
|
||||
`.venv-py314\Scripts\python tests\romfs_prompts_test.py`, which runs the tool on a RomFS it makes itself (no game
|
||||
files); a change to the settings passes `nx2navi.exe --probe-cfg`.
|
||||
|
||||
@@ -36,6 +36,14 @@
|
||||
# cloth_realtime = 1
|
||||
# Fix for physics that bounce at high frame rates.
|
||||
# highfps_fix = 1
|
||||
# Above 60 fps the game's physics solved contacts and friction in fewer steps a second than at 60, and in none at all
|
||||
# in a frame shorter than 1/240 s, so things standing on moving platforms were left behind. 1 rounds the step count
|
||||
# up instead; 0 is the game's own count.
|
||||
# physics_steps = 1
|
||||
# The step the game is given each frame: 1 keeps it steady (the same value from frame to frame while the frame rate
|
||||
# holds, exactly one 60 Hz frame at a 60 fps limit) and makes up for a late frame over the next second; 0 is the mean
|
||||
# of the last five frame times, which changes with every frame.
|
||||
# step_steady = 1
|
||||
|
||||
[performance]
|
||||
# The keys of this section and of [diagnostics] are for testing and troubleshooting, and take effect at the next start.
|
||||
|
||||
@@ -18,6 +18,7 @@ set wide_movie NX2NAVI_PATCH_WIDE_MOVIE 0
|
||||
set res_fixed NX2NAVI_PATCH_RES_FIXED 0
|
||||
set ostep NX2NAVI_OCARINA_REALTIME 1
|
||||
set zstd NX2NAVI_ZSTD_NATIVE 1
|
||||
set osolver NX2NAVI_PHYSICS_STEPS 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),
|
||||
@@ -28,6 +29,25 @@ set zstd NX2NAVI_ZSTD_NATIVE 1
|
||||
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]
|
||||
|
||||
# Physics solver steps: the physics system's step setup (main+0x38aeb80, and the same code in main+0x38afc10) gives the
|
||||
# rigid body world its solver step count for the frame, trunc(frames per update * count + 0.001) with the count at
|
||||
# system +0x244 (4, set by the constructor main+0x38ae800), and the world keeps the result's inverse beside it (world
|
||||
# vtable +0x88, main+0x3a24c20: world +0xc48 and +0xcb8). That is 4 at 60 fps and 8 at 30, the two rates the game ships
|
||||
# with: 240 solver steps a second. Below one frame per update it truncates to fewer a second (3 a frame from 61 to 80
|
||||
# fps, 2 to 120, 1 to 240: 144 a second at 144 fps), and to 0 under a quarter frame, with an infinite inverse. The
|
||||
# solver (main+0x180b0a0) runs its solve and sub-integration loop that many times and skips it at 0 (main+0x180b380):
|
||||
# such a frame solves no contact or friction and moves no rigid body, so whatever stands on a moving body is left
|
||||
# behind in it. At a 240 fps limit that was the frame after a late one, whose display interval is shorter than the
|
||||
# period (runtime/frame_rate.c ocarina_step). The count rounds up instead, ceil(frames per update * count - 0.25): 4 and 8 as before at 60 and 30
|
||||
# fps, also with a step a few percent off them, never fewer steps a second than at 60 fps, and at least 1 down to a
|
||||
# 16th of a frame. s2 is loaded only for this sum, and none of the words reads or writes the flags.
|
||||
site osolver main 38aebe4 bd440d02 1e2a1002 # ldr s2, [x8, #0x40c] (0.001) -> fmov s2, #0.25
|
||||
site osolver main 38aebf0 1f010800 1f218800 # fmadd s0, s0, s1, s2 -> fnmsub s0, s0, s1, s2 (s0 * s1 - s2)
|
||||
site osolver main 38aebf4 1e380001 1e280001 # fcvtzs w1, s0 -> fcvtps w1, s0 (round up)
|
||||
site osolver main 38afd10 bd440d02 1e2a1002 # the same three words in the second copy
|
||||
site osolver main 38afd1c 1f010800 1f218800
|
||||
site osolver main 38afd20 1e380001 1e280001
|
||||
|
||||
# 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
|
||||
# for the main render target set, main+0xf49040), and the display settings object (main+0x31f3890) stores the same size
|
||||
|
||||
@@ -31,7 +31,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", "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", "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)
|
||||
"AB", "AMIIBO_DIR", "ANISO", "APP_POOL", "ASPECT", "ASPECT_PATCH", "ASSUME_FOCUS", "ASTC", "ASTC_BC", "ASTC_TIME", "ASYNC_PREEMPT", "AUDIO", "AUDIO_CHANNELS", "AUDIO_DEVICE", "AUDIO_GAME_PRIO", "AUDIO_OFF", "AUDIO_PREFILL_MS", "AUDIO_STATS",
|
||||
"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",
|
||||
@@ -74,6 +74,7 @@ static const char *const known[] = {
|
||||
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"}, {"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"},
|
||||
{"SHARPNESS", "display"}, {"ASPECT", "display"}, {"FOV", "display"}, {"ANISO", "display"},
|
||||
|
||||
@@ -233,6 +233,7 @@ double frame_step_game_hz(void) {
|
||||
|
||||
// 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.
|
||||
static double paced_hz; // the game frames a second the newest frame_step_cap_ns paces at (ocarina_step); 0: none
|
||||
uint64_t frame_step_cap_ns(void) {
|
||||
frame_limit_live();
|
||||
static int rtss_allowed = -1;
|
||||
@@ -253,6 +254,7 @@ uint64_t frame_step_cap_ns(void) {
|
||||
}
|
||||
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.
|
||||
@@ -276,6 +278,7 @@ uint64_t frame_step_cap_ns(void) {
|
||||
// display's rate, the generated frames then fill the display's refreshes and none is dropped.
|
||||
if (gen > 1) 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);
|
||||
@@ -343,13 +346,87 @@ static double ocarina_step_s(void) {
|
||||
return base * speed / 60.0;
|
||||
}
|
||||
|
||||
// Free-running builds (Ocarina): the time manager's base step follows the display interval (5-frame average, 1/1000 to
|
||||
// 1/15 s, in 60 Hz frames), written at each present. 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).
|
||||
// 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;
|
||||
}
|
||||
|
||||
static double ocarina_step(double dt) {
|
||||
static int steady = -1;
|
||||
if (steady < 0) {
|
||||
const char *e = cfg_get("NX2NAVI_STEP_STEADY");
|
||||
steady = !(e && !strcmp(e, "0"));
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
static void ocarina_step_write(double dt) {
|
||||
static int on = -1;
|
||||
@@ -423,7 +500,7 @@ static void speed_report(uint64_t now, double step, int interval) {
|
||||
probe = e && !strcmp(e, "1");
|
||||
}
|
||||
double wall = (double)(now - win_t0) * 1e-9;
|
||||
char pos[160] = "";
|
||||
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,
|
||||
@@ -433,6 +510,12 @@ static void speed_report(uint64_t now, double step, int interval) {
|
||||
memcpy(last_pos, p, sizeof p);
|
||||
last_pos_ns = now;
|
||||
}
|
||||
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,
|
||||
@@ -488,14 +571,9 @@ void frame_step_present(uint64_t display_ns, int interval) {
|
||||
step_store((float)step);
|
||||
}
|
||||
}
|
||||
if (frame_free_running() && last_ns && dt > 0 && dt < STEP_RESET_S) {
|
||||
static double os[STEP_SAMPLES];
|
||||
static unsigned on_, ox;
|
||||
os[ox++ % STEP_SAMPLES] = dt;
|
||||
if (on_ < STEP_SAMPLES) on_++;
|
||||
double sum = 0;
|
||||
for (unsigned i = 0; i < on_; i++) sum += os[i];
|
||||
ocarina_step_write(sum / on_);
|
||||
if (frame_free_running() && last_ns) {
|
||||
if (dt > 0 && dt < STEP_RESET_S) ocarina_step_write(ocarina_step(dt));
|
||||
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
|
||||
|
||||
140
tests/frame_step_check.c
Normal file
140
tests/frame_step_check.c
Normal file
@@ -0,0 +1,140 @@
|
||||
// runtime/frame_rate.c: the step Ocarina is given (ocarina_step), driven through frame_step_cap_ns and
|
||||
// frame_step_present with made-up display times and the rest of the runtime stubbed. Built and run by
|
||||
// tests/frame_step_check.py; prints each failed check and exits 1 on any. With the argument "mean" it runs with
|
||||
// NX2NAVI_STEP_STEADY=0 and checks the mean of the last five intervals.
|
||||
#include "frame_rate.c"
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, ...) do { if (!(c)) { fails++; printf("FAIL line %d: ", __LINE__); printf(__VA_ARGS__); printf("\n"); } } while (0)
|
||||
|
||||
// ---- the runtime frame_rate.c reaches
|
||||
static uint8_t page[4096];
|
||||
static const char *limit = "60", *steady = "1";
|
||||
static unsigned version = 1;
|
||||
const int rt_game_version = 111;
|
||||
unsigned rt_trace;
|
||||
uint64_t rt_guest_start_ns;
|
||||
const char *cfg_get(const char *key) {
|
||||
if (!strcmp(key, "NX2NAVI_FPS_LIMIT")) return limit;
|
||||
if (!strcmp(key, "NX2NAVI_STEP_STEADY")) return steady;
|
||||
if (!strcmp(key, "NX2NAVI_RTSS")) return "0";
|
||||
return NULL;
|
||||
}
|
||||
unsigned cfg_version(void) { return version; }
|
||||
int rt_patch_set(const char *name) { return !strcmp(name, "ostep") ? 0 : -1; }
|
||||
int rt_patch_enabled(int set) { return set == 0; }
|
||||
void rt_patch_disable(int set) { (void)set; }
|
||||
void rt_patch_switch(int set, int on) { (void)set, (void)on; }
|
||||
void *rt_patch_page(void) { return page; }
|
||||
void rt_log(const char *fmt, ...) { (void)fmt; }
|
||||
double gfx_window_refresh_hz(void) { return 144; }
|
||||
int ov_movie_live(void) { return 0; }
|
||||
int vk_fg_generating(void) { return 0; }
|
||||
int vk_fg_active(void) { return 0; }
|
||||
int vk_fg_wanted(void) { return 0; }
|
||||
int vk_dlssg_multiplier(void) { return 1; }
|
||||
int vk_dlssg_active(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; }
|
||||
int plat_mem_readable(uint64_t a, size_t n) { (void)a, (void)n; return 0; }
|
||||
uint64_t kern_now_ns(void) { return 0; }
|
||||
|
||||
// ---- one present: the pacer asks for the period, then reports the display time. Returns the step in 60 Hz frames.
|
||||
static uint64_t now_ns = 1000000000ull;
|
||||
static double wall_s, game_s;
|
||||
static float present(double interval_s) {
|
||||
frame_step_cap_ns();
|
||||
now_ns += (uint64_t)(interval_s * 1e9 + 0.5);
|
||||
frame_step_present(now_ns, 1);
|
||||
float f;
|
||||
memcpy(&f, page + 256, 4);
|
||||
wall_s += interval_s;
|
||||
game_s += f / 60.0;
|
||||
return f;
|
||||
}
|
||||
static void set_limit(const char *l) {
|
||||
limit = l;
|
||||
version++;
|
||||
present(1.0); // a gap: the filter starts again
|
||||
wall_s = game_s = 0;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int mean = argc > 1 && !strcmp(argv[1], "mean");
|
||||
if (mean) steady = "0";
|
||||
frame_step_init();
|
||||
float one;
|
||||
memcpy(&one, page + 256, 4);
|
||||
CHECK(one == 1.0f, "the page holds one frame before the first present, not %g", one);
|
||||
const double p60 = 16666667e-9, p240 = 4166667e-9;
|
||||
|
||||
if (mean) { // the comparison setting: 5 and 30 ms in turn give 15 and 20 ms in turn
|
||||
set_limit("unlimited");
|
||||
float lo = 9, hi = 0;
|
||||
for (int i = 0; i < 200; i++) {
|
||||
float f = present(i & 1 ? 0.030 : 0.005);
|
||||
if (i >= 20) lo = f < lo ? f : lo, hi = f > hi ? f : hi;
|
||||
}
|
||||
CHECK(fabsf(lo - 0.9f) < 1e-3f && fabsf(hi - 1.2f) < 1e-3f, "mean of five: %g..%g frames, not 0.9..1.2", lo, hi);
|
||||
printf("frame_step_check (mean): %s\n", fails ? "FAILED" : "ok");
|
||||
return fails != 0;
|
||||
}
|
||||
|
||||
// A 60 fps limit with a late frame and the short one after it every 37 frames: one frame exactly, every frame.
|
||||
set_limit("60");
|
||||
for (int i = 0; i < 600; i++) {
|
||||
float f = present(i % 37 == 5 ? p60 + 0.004 : i % 37 == 6 ? p60 - 0.004 : p60);
|
||||
CHECK(f == 1.0f, "60 fps limit, frame %d: %.9g frames", i, f);
|
||||
}
|
||||
CHECK(fabs(game_s - wall_s) < 0.001, "60 fps limit: %.4f s stepped in %.4f s", game_s, wall_s);
|
||||
|
||||
// The 240 cap with late frames: never under a quarter frame (the game's own solver step count is 0 there).
|
||||
set_limit("240");
|
||||
for (int i = 0; i < 1200; i++) {
|
||||
double late = 0.0005 + 0.0025 * ((i * 7919) % 100) / 100.0;
|
||||
float f = present(i % 29 == 3 ? p240 + late : i % 29 == 4 ? p240 - 0.0005 - 0.0025 * (((i - 1) * 7919) % 100) / 100.0 : p240);
|
||||
CHECK(f >= 0.25f && f < 0.2525f, "240 cap, frame %d: %.6f frames", i, f);
|
||||
}
|
||||
CHECK(fabs(game_s - wall_s) < 0.003, "240 cap: %.4f s stepped in %.4f s", game_s, wall_s);
|
||||
|
||||
// Unlimited, 5 and 30 ms in turn: one step, and the clock kept.
|
||||
set_limit("unlimited");
|
||||
float prev = 0;
|
||||
for (int i = 0; i < 600; i++) {
|
||||
float f = present(i & 1 ? 0.030 : 0.005);
|
||||
if (i >= 24) CHECK(fabsf(f / prev - 1) < 0.02f, "5/30 ms, frame %d: %.4f after %.4f frames", i, f, prev);
|
||||
prev = f;
|
||||
}
|
||||
CHECK(fabs(game_s / wall_s - 1) < 0.005, "5/30 ms: %.4f s stepped in %.4f s", game_s, wall_s);
|
||||
|
||||
// 200 fps and one 60 ms frame: the step rises by a tenth at most and the 55 ms are paid back.
|
||||
set_limit("unlimited");
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
float f = present(i == 300 ? 0.060 : 0.005);
|
||||
if (i >= 24) CHECK(f >= 0.2999f && f <= 0.3301f, "hitch, frame %d: %.4f frames", i, f);
|
||||
if (i == 302) CHECK(f > 0.32f, "hitch: the step does not repay (%.4f frames two frames later)", f);
|
||||
}
|
||||
CHECK(fabs(game_s - wall_s) < 0.004, "hitch: %.4f s stepped in %.4f s", game_s, wall_s);
|
||||
|
||||
// A real change of rate, 240 to 120 fps, arrives within 12 frames.
|
||||
set_limit("unlimited");
|
||||
for (int i = 0; i < 300; i++) present(p240);
|
||||
float f = 0;
|
||||
for (int i = 0; i < 12; i++) f = present(2 * p240);
|
||||
CHECK(f > 0.49f, "240 -> 120 fps: %.4f frames after 12", f);
|
||||
|
||||
// A frame longer than 1/15 s slows the game down; the step never leaves 1/480..1/15 s.
|
||||
set_limit("unlimited");
|
||||
for (int i = 0; i < 100; i++) {
|
||||
float g = present(0.2);
|
||||
CHECK(g <= 4.0001f, "long frames: %.4f frames", g);
|
||||
}
|
||||
set_limit("unlimited");
|
||||
for (int i = 0; i < 100; i++) {
|
||||
float g = present(0.0011); // 900 fps, as a quick menu without the pacer would run
|
||||
CHECK(g >= 0.125f, "short frames: %.4f frames", g);
|
||||
}
|
||||
printf("frame_step_check: %s\n", fails ? "FAILED" : "ok");
|
||||
return fails != 0;
|
||||
}
|
||||
46
tests/frame_step_check.py
Normal file
46
tests/frame_step_check.py
Normal file
@@ -0,0 +1,46 @@
|
||||
"""Build and run tests/frame_step_check.c: the step runtime/frame_rate.c gives Ocarina, with the rest of the runtime
|
||||
stubbed. No game files: the module layout the runtime includes is a stand-in written to the test's build folder.
|
||||
|
||||
Usage: frame_step_check.py
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
||||
|
||||
|
||||
def compiler():
|
||||
if os.environ.get('CC'):
|
||||
return os.environ['CC']
|
||||
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
|
||||
return str(MSYS_BIN / 'clang.exe')
|
||||
return shutil.which('clang') or 'clang'
|
||||
|
||||
LAYOUT = '''// A stand-in for the generated layout.h (tests/frame_step_check.py).
|
||||
#pragma once
|
||||
#define MOD_MAIN_BASE 0x7000000000ull
|
||||
#define MOD_MAIN_SIZE 0x4ea0000ull
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
work = ROOT / 'build' / 'test' / 'frame_step'
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
(work / 'layout.h').write_text(LAYOUT)
|
||||
exe = work / ('frame_step_check.exe' if os.name == 'nt' else 'frame_step_check')
|
||||
env = dict(os.environ)
|
||||
if os.name == 'nt' and MSYS_BIN.exists():
|
||||
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
||||
subprocess.check_call([compiler(), '-O1', '-Wall', '-Wno-unused-function', '-fno-strict-aliasing',
|
||||
'-I', str(ROOT / 'runtime'), '-I', str(work),
|
||||
str(ROOT / 'tests' / 'frame_step_check.c'), '-o', str(exe), '-lm'], env=env)
|
||||
rc = subprocess.run([str(exe)], env=env).returncode
|
||||
return subprocess.run([str(exe), 'mean'], env=env).returncode or rc
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user