Files
psxrecomp/runtime/tests/test_frame_interpolation_flip_source.py
Shokunin fb832a9563 FLIP source: test the new-frame decision; guard its wiring
interp_capture's FLIP decision moves into frame_flip_is_new_frame()
(frame_interpolation.c), unit-tested in frame_interpolation_schedule_test.
A new source guard, frame_interpolation_flip_source (recompiler ctest),
pins what no unit test reaches: main.cpp hands the session's source to the
renderer at every session start, right after its interpolation settings;
interp_capture feeds the origin and the redraw test to the decision and the
tracker and returns before the copy on a duplicate; both present paths pass
the display origin and present_dirty_test; a source change restarts the
history. Deleting the hand-off, or never returning on a duplicate, now fails
the guard (both passed every test before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 21:02:43 -07:00

88 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Guard the FLIP frame-interpolation source's wiring (docs/FRAME_RATE.md).
The pure pieces (frame_flip_is_new_frame, frame_flip_tracker_vblank,
begin_phase) are exercised by frame_interpolation_schedule_test. This guard
pins where they are wired in, which no unit test reaches:
- main.cpp hands the session's source to the GL renderer at every session
start, right after the renderer's interpolation settings;
- interp_capture's FLIP branch asks frame_flip_is_new_frame with the display
origin and the redraw test, feeds the answer to the tracker, and returns
before the copy on a duplicate, so the history is not rotated;
- both present paths pass the display origin and the redraw test.
Without the hand-off FLIP never reaches the renderer: frame blend stays per
VBlank, and render passes (which require the FLIP source) are always refused.
"""
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
MAIN = (ROOT / "runtime" / "src" / "main.cpp").read_text(encoding="utf-8")
GL = (ROOT / "runtime" / "src" / "gpu_gl_renderer.c").read_text(encoding="utf-8")
def squash(text):
return re.sub(r"\s+", " ", text).strip()
def function_body(source, signature):
start = source.index(signature)
return source[start:source.index("\n}\n", start)]
# ---- session-start hand-off (main.cpp) --------------------------------------
HANDOFF = "gl_renderer_set_interpolation_source(g_frame_interpolation_source);"
assert MAIN.count(HANDOFF) == 1, "the source hand-off must exist exactly once"
handoff_at = MAIN.index(HANDOFF)
reboot_at = MAIN.index("session_reboot:")
assert reboot_at < handoff_at, \
"the hand-off must run at every session start (after session_reboot:)"
settings_at = MAIN.rindex("gl_renderer_set_interpolation(", 0, handoff_at)
assert reboot_at < settings_at, "hand-off must sit beside the session's GL setup"
between = MAIN[settings_at:handoff_at]
assert between.count(";") == 1 and between.rstrip().endswith(
"g_frame_interpolation_blend);"), \
"the hand-off must directly follow the session's interpolation settings"
setter = function_body(GL, "void gl_renderer_set_interpolation_source(int source) {")
assert "if (source != s_interp_source) interp_reset_history_unlocked();" in setter, \
"a source change must restart the blend history"
assert "s_interp_source = source;" in setter
# ---- interp_capture's FLIP branch (gpu_gl_renderer.c) -----------------------
capture = function_body(GL, "static int interp_capture(GLuint fbo,")
flip_at = capture.index("if (s_interp_source == 1) {")
flip = capture[flip_at:capture.index("} else {", flip_at)]
flat = squash(flip)
assert squash("""int new_frame = frame_flip_is_new_frame(
geometry_changed, s_interp_valid == 0, redrawn, origin_x, origin_y,
s_interp_origin_x, s_interp_origin_y);""") in flat, \
"FLIP must decide new frames with the tested helper, from origin and redraw"
assert "s_interp_origin_x = origin_x;" in flip and \
"s_interp_origin_y = origin_y;" in flip, "the last origin must be kept"
assert squash("""frame_flip_tracker_vblank(&s_interp_flip, new_frame,
&s_interp_phase_lo, &s_interp_phase_hi);""") in flat, \
"the tracker must see every presented VBlank with the decision"
assert squash("""if (!new_frame) { s_interp_duplicates++; return 1; }""") in flat, \
"a duplicate must return before the history is rotated"
assert flip.index("frame_flip_is_new_frame(") < \
flip.index("frame_flip_tracker_vblank(") < flip.index("if (!new_frame) {")
assert flip_at < capture.index("glCopyTexSubImage2D("), \
"the duplicate return must precede the capture copy"
# ---- present paths ----------------------------------------------------------
calls = [squash(m.group(0)) for m in
re.finditer(r"interp_capture\(\s*[^;]*?\);", GL[GL.index("\n}\n",
GL.index("static int interp_capture(GLuint fbo,")):])]
assert len(calls) == 2, f"expected the VRAM and wide present calls, got {len(calls)}"
assert any("GL_PRES_VRAM, disp_x, disp_y, present_dirty_test(disp_x, disp_y,"
in c for c in calls), "VRAM present must pass origin and redraw"
assert any("GL_PRES_WIDE, disp_x, disp_y, present_dirty_test(0, disp_y,"
in c for c in calls), "wide present must pass origin and redraw"
print("frame interpolation FLIP source wiring guard passed")