Files
PS5_RetroArch/tests/test_framegen_plan.py
T
Mihawk 5c4256b202 Frame generation plans on the flips the output really shows (0131)
The 59.94 Hz mode takes 120 flips a second on a VRR display and 60 on an
output without 120 Hz. 0121 assumed 119.88 Hz for every 59.94 Hz mode, so
on a 60 Hz output each picture was presented twice and games ran at half
speed with broken audio (a tester's v2.0.0-beta-2 logs: "display 119.880
Hz ... each shown 2x", 58.8 presents/s, the core at 29.6 calls/s).

Patch 0131. khr_display_ctx.c times flips once per process, on the first
swapchain, where the mode is below 100 Hz: the median of nine acquire
intervals once every image is queued, 1 s timeouts, and on a failure a
warning and the mode is believed. vulkan_common.c keeps the rate. The plan
in retroarch.c takes 119.88 only above 100 Hz, and a Target Framerate
above the display's rate becomes the display's (120 on a 60 Hz output is
60, and a 30 fps game still gets a frame made between its frames).

Evidence: evidence/framegen-flip-rate (python3 tools/evidence.py compare
evidence: 128 replayed, 0 failed). tests/test_framegen_plan.py composes
the plan from EDITS as the script applies it and runs nine cases; with
0131's edits left out it fails on the 60 Hz case. The eboot.bin built from
this tree (identity 1f81446a..., sha256 d3dc2a31...) ran on the tester's
60 Hz output, and the owner reports it worked perfectly; no log of that run
was received. Not run yet: the owner's base PS5 with its VRR display (the
console did not answer).

format PASS; evidence PASS; unit 192 of 197 OK. The 5 errors are tests
that read .deps/{ppsspp,azahar,desmume}-src, deleted with .deps on
2026-10-15 along with every core source the build gate's catalog step
reads.
2026-10-17 17:02:49 -04:00

75 lines
3.5 KiB
Python

"""Run frame generation's plan (patches 0121 and 0131) as the port inserts it, for each kind of output."""
import importlib.util
import subprocess
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
class FramegenPlan(unittest.TestCase):
def test_the_plan_follows_the_flips_the_output_shows(self):
spec = importlib.util.spec_from_file_location('patches', ROOT / 'tools/apply-port-patches.py')
patches = importlib.util.module_from_spec(spec)
spec.loader.exec_module(patches)
# The plan as 0121 inserts it, with every later retroarch.c edit applied in order, as main() does.
edits = [e for e in patches.EDITS if e[0] == 'retroarch.c']
start = next(i for i, e in enumerate(edits) if e[3] == 'Output Rate Auto: the presents per game frame')
text = edits[start][2]
for _, anchor, replacement, marker in edits[start + 1:]:
if marker not in text and anchor in text:
text = text.replace(anchor, replacement, 1)
plan = text[:text.index(' unsigned shader_subframes')]
source = r'''
#include <assert.h>
#include <math.h>
#include <stdbool.h>
#define RARCH_LOG(...) ((void)0)
typedef struct {
struct { bool video_framegen, video_framegen_auto; } bools;
struct { unsigned video_framegen_target, video_shader_subframes; } uints;
struct { float video_refresh_rate; } floats;
} settings_t;
unsigned vulkan_ps5_display_refresh_x1000, vulkan_ps5_flip_rate_x1000, ps5_fg_dup;
float ps5_fg_core_fps, ps5_fg_target_fps;
/* The presents per game frame for a mode and the flips timed in it; refresh: what RetroArch then paces on. */
static unsigned plan(unsigned mode, unsigned flips, unsigned target, double input_fps, float *refresh)
{
settings_t s = {{true, true}, {target, 1}, {mode / 1000.0f}};
settings_t *settings = &s;
float video_refresh_rate = settings->floats.video_refresh_rate;
vulkan_ps5_display_refresh_x1000 = mode;
vulkan_ps5_flip_rate_x1000 = flips;
''' + plan + r'''
(void)video_refresh_rate;
*refresh = settings->floats.video_refresh_rate;
return settings->uints.video_shader_subframes;
}
int main(void)
{
float refresh;
/* An output without 120 Hz: no picture held for two refreshes, paced at 59.94 */
assert(plan(59940, 59940, 120, 58.944, &refresh) == 1 && refresh < 60.0f);
assert(plan(59940, 59940, 120, 60.0, &refresh) == 1 && refresh < 60.0f);
assert(plan(59940, 59940, 60, 60.0, &refresh) == 1);
/* ...and a 30 fps game still gets a frame between its frames, at the display's 60 */
assert(plan(59940, 59940, 120, 30.0, &refresh) == 2 && fabsf(ps5_fg_target_fps - 59.94f) < 0.01f);
/* VRR: 120 flips a second in the 59.94 Hz mode */
assert(plan(59940, 119880, 120, 60.0, &refresh) == 2 && refresh > 119.0f && ps5_fg_dup == 1);
assert(plan(59940, 119880, 60, 60.0, &refresh) == 2 && ps5_fg_dup == 2);
/* Flips not timed: the mode is believed */
assert(plan(59940, 0, 120, 60.0, &refresh) == 1 && refresh < 60.0f);
/* The 119.88 Hz mode itself */
assert(plan(119880, 0, 120, 60.0, &refresh) == 2 && ps5_fg_dup == 1);
assert(plan(119880, 0, 60, 60.0, &refresh) == 2 && ps5_fg_dup == 2);
return 0;
}
'''
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / 'plan.c'
path.write_text(source)
exe = Path(tmp) / 'plan'
subprocess.run(['cc', '-std=gnu99', str(path), '-o', str(exe), '-lm'], check=True)
subprocess.run([str(exe)], check=True)