mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 18:25:35 -04:00
fb832a9563
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>
88 lines
4.3 KiB
Python
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")
|