Hold the game to 240 frames a second when the limit is unlimited or higher, which stops the quick menu's crash.

Ocarina steps its game by the clock, and the frame limiter let "unlimited" run free and took numbers up to 1000
(frame_rate.c LIMIT_FREE_MAX_HZ), on the belief that the game has no lower limit to its step. It has one: on the quick
menu, where a frame costs the GPU almost nothing, the game ran at 640 to 700 fps and after about two seconds read
address 0 in its own draw code (main+0x175b2a0), twice in a row with the same call stack, while 92 to 255 fps ran on.
The game now runs at most 240 frames a second, "unlimited", "display" and numbers included; with frame generation the
cap counts the game's own frames, so the generated ones still go above it on screen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
chrg6
2026-11-09 13:15:37 -05:00
parent 9c10827018
commit 9bba578e58
2 changed files with 18 additions and 9 deletions

View File

@@ -4,8 +4,9 @@
# '#' and ';' start a comment; [sections] only group lines.
[frame rate]
# Frame rate limit: 30, 40, 45, 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 rate gives 240: above it the game's physics runs out of memory. Above 60
# Frame rate limit: 30, 40, 45, 60, 90, 120, 144, any whole rate from 30 to 240, display (the refresh rate of the
# window's monitor, at most 240) or unlimited (as fast as the PC goes, at most 240 frames of the game a second). A
# higher rate gives 240: far above it the game's own code fails (its quick menu crashed at 700 fps). Above 60
# is experimental and needs realtime = 1. Changes while the game runs take effect at once.
# With mailbox or fifo presentation (vk_present, the default) a frame reaches the screen at a vertical blank, so a limit
# that does not divide the monitor's refresh rate keeps each frame on screen for a different number of refreshes. On a

View File

@@ -133,8 +133,12 @@ static double shown_hz(double hz) { return hz <= 0 ? 60 : hz < LIMIT_MIN_HZ ? LI
// 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 or none (LIMIT_FREE_MAX_HZ, "unlimited"); LIMIT_MAX_HZ is for builds whose physics fail past it, Ocarina has no such floor.
#define LIMIT_FREE_MAX_HZ 1000
// 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;
@@ -175,7 +179,8 @@ static void frame_limit_live(void) {
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)");
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;
@@ -243,11 +248,12 @@ uint64_t frame_step_cap_ns(void) {
else rt_log("frame rate: the movie ended: the frame limit again");
}
}
if (limit_hz == -2 && !movie_hz) { // unlimited: the pacer does not hold frames, and RTSS's limit is cleared
rtss_limit_want(rtss ? 0 : -1);
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
return 0;
}
// unlimited during a movie: the movie's rate, times frame generation's factor for the division below
// unlimited in a build that steps by the clock: LIMIT_FREE_MAX_HZ game frames; during a movie, the movie's rate.
// Both times frame generation's factor for the division below.
int follow = frame_limit_follows_display(), limit = follow ? 0 : limit_hz;
static int follow_was, follow_logs;
if (follow != follow_was) {
@@ -257,8 +263,10 @@ uint64_t frame_step_cap_ns(void) {
else rt_log("frame rate: limit %d fps again", limit_hz);
}
}
double hz = limit == -2 ? movie_hz * gen : shown_hz(limit ? limit : gfx_window_refresh_hz());
double hz = limit == -2 ? (double)(movie_hz ? movie_hz : LIMIT_FREE_MAX_HZ) * gen
: shown_hz(limit ? limit : gfx_window_refresh_hz());
double max = frame_free_running() ? LIMIT_FREE_MAX_HZ : LIMIT_MAX_HZ;
if (limit == -2) max *= gen; // the cap is on the game's frames there, not the ones shown
if (hz > max) hz = max;
// Frame generation presents one to three generated frames between each two of the game's: the game is paced at the
// limit over that factor (half, a third, a quarter), so the screen shows the limit. With VSync and the limit at the