mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
09bcfdc6ae
[video] frame_generation (opt-in, with render_thread): the render thread keeps each game frame's replayed draw list, matches the triangles of the last two (frame_gen.c), and presents in-between frames at the display refresh by redrawing the newer list into separate surfaces with matched vertices interpolated. It never runs guest code. Real frames are composed at their flip and presented after the generated ones, with the same pixels and order. Generation uses only measured surplus: a plan from render-thread CPU, generated-frame cost and swap time, a breaker after backpressure or late guest frames, and a pause while dynamic resolution is over budget. The render-thread core gains a timed tick hook. Frame-cost queries pause around generation, so dynamic resolution measures real frames only. Tests: frame_gen_test, render_thread_test (tick), gl_frame_gen_test (real frames identical off/on; phase 0/1 equal the endpoints). Docs: docs/FRAME_GENERATION.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
148 lines
6.6 KiB
Python
148 lines
6.6 KiB
Python
"""Build and run test_gl_frame_gen.c on a hidden real OpenGL context.
|
|
|
|
Frame generation ([video] frame_generation, docs/FRAME_GENERATION.md): per
|
|
scale and present path (VRAM, native-wide), the fixture runs with the render
|
|
thread on and generation off, then on (forced). The real presented images, in
|
|
order, must be identical; the run with generation on must have presented
|
|
generated frames and passes its own endpoint checks (phase 1 = the newer real
|
|
image, phase 0 = the older one). Build flags and skips as
|
|
run_gl_render_thread.py.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import pathlib
|
|
import platform
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
MAC_FRAMEWORKS = ["Cocoa", "OpenGL", "IOKit", "CoreVideo", "CoreAudio", "AudioToolbox",
|
|
"Carbon", "ForceFeedback", "GameController", "Metal", "QuartzCore",
|
|
"CoreMedia", "AVFoundation", "Foundation", "CoreHaptics",
|
|
"UniformTypeIdentifiers"]
|
|
# The static SDL3's Win32 dependencies (as run_gl_texture_filter.py links them).
|
|
WIN_LIBS = ["opengl32", "kernel32", "user32", "gdi32", "winmm", "imm32", "ole32",
|
|
"oleaut32", "version", "uuid", "advapi32", "setupapi", "shell32", "dinput8"]
|
|
SKIP_EXIT = 77
|
|
WINDOWS = os.name == "nt" or platform.system().startswith(("MINGW", "MSYS", "CYGWIN"))
|
|
KEYS = ("real",)
|
|
|
|
|
|
def parse(stdout):
|
|
out = {}
|
|
for k in KEYS:
|
|
m = re.search(r"^" + k + r"=([0-9a-f]{16})$", stdout, re.M)
|
|
out[k] = m[1] if m else None
|
|
m = re.search(r"^checks=(\d+) failures=(\d+)$", stdout, re.M)
|
|
out["failures"] = int(m[2]) if m else None
|
|
return out
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description=__doc__)
|
|
ap.add_argument("--cc", default="cc")
|
|
ap.add_argument("--sdl-include", required=True)
|
|
ap.add_argument("--sdl-library", required=True)
|
|
ap.add_argument("--output", required=True)
|
|
ap.add_argument("--fixture", type=pathlib.Path)
|
|
ap.add_argument("--scales", default="1,3")
|
|
ap.add_argument("--frames", default="24")
|
|
args = ap.parse_args()
|
|
sdl_includes = [str(pathlib.Path(d).resolve()) for d in args.sdl_include.split(";") if d]
|
|
args.sdl_library = str(pathlib.Path(args.sdl_library).resolve())
|
|
framework = pathlib.Path(__file__).resolve().parents[2]
|
|
fixture = args.fixture or framework / "runtime/tests/test_gl_frame_gen.c"
|
|
out_root = pathlib.Path(args.output).resolve()
|
|
out_root.mkdir(parents=True, exist_ok=True)
|
|
dest = pathlib.Path(tempfile.mkdtemp(prefix="fg-", dir=out_root))
|
|
print("Evidence directory:", dest)
|
|
receipt = []
|
|
|
|
def run(command):
|
|
command = [str(c) for c in command]
|
|
r = subprocess.run(command, cwd=dest, capture_output=True, text=True,
|
|
encoding="utf-8", errors="replace")
|
|
receipt.append({"cmd": command, "exit": r.returncode,
|
|
"stdout": r.stdout, "stderr": r.stderr[-4000:]})
|
|
(dest / "receipt.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
|
|
return r
|
|
|
|
includes = ["-I", framework / "runtime/include", "-I", framework / "runtime/src"]
|
|
for d in sdl_includes:
|
|
includes += ["-I", d]
|
|
src = framework / "runtime/src"
|
|
# The fixture #includes gpu_gl_renderer.c and drives it through the real
|
|
# facade (gpu_render.c) and render thread (render_thread.c).
|
|
sources = [("probe", fixture), ("sw", src / "gpu_sw_renderer.c"),
|
|
("fi", src / "frame_interpolation.c"), ("rp", src / "render_pass_plan.c"),
|
|
("xr", src / "psx_openxr.c"), ("rth", src / "render_thread.c"),
|
|
("facade", src / "gpu_render.c"), ("fg", src / "frame_gen.c")]
|
|
sections = [] if platform.system() == "Darwin" else ["-ffunction-sections", "-fdata-sections"]
|
|
# MinGW's PE linker reports undefined references from sections it later
|
|
# collects, so there LTO drops them first (as run_gl_scale_invariance.py).
|
|
if WINDOWS:
|
|
sections.append("-flto")
|
|
objs = []
|
|
for name, path in sources:
|
|
o = dest / (name + ".o")
|
|
r = run([args.cc, "-std=gnu11", "-O1", "-DPSX_SDL3=1", "-DPSX_NO_DEBUG_TOOLS=1",
|
|
"-DGL_SILENCE_DEPRECATION=1", "-w", *sections, *includes, "-c", path, "-o", o])
|
|
if r.returncode:
|
|
print(r.stderr[-3000:])
|
|
return 2
|
|
objs.append(o)
|
|
probe = dest / ("probe.exe" if WINDOWS else "probe")
|
|
link = [args.cc, *objs, args.sdl_library, "-o", probe]
|
|
if platform.system() == "Darwin":
|
|
for f in MAC_FRAMEWORKS:
|
|
link += ["-framework", f]
|
|
link += ["-liconv", "-lm", "-lpthread", "-Wl,-dead_strip"]
|
|
elif WINDOWS:
|
|
# -static: no libwinpthread/libgcc DLLs needed next to probe.exe.
|
|
link += ["-flto", "-static", *["-l" + x for x in WIN_LIBS], "-lm", "-Wl,--gc-sections"]
|
|
else:
|
|
link += ["-lGL", "-lm", "-ldl", "-lpthread", "-Wl,--gc-sections"]
|
|
r = run(link)
|
|
if r.returncode:
|
|
print(r.stderr[-3000:])
|
|
return 2
|
|
|
|
ok = True
|
|
first = True
|
|
for s in [int(v) for v in args.scales.split(",") if v]:
|
|
for path, timing in (("vram", "flip"), ("wide", "flip"), ("vram", "late"), ("wide", "late")):
|
|
runs = {}
|
|
for fg in (0, 1):
|
|
r = run([probe, s, fg, path, args.frames, timing])
|
|
if first and r.returncode == SKIP_EXIT:
|
|
print("SKIP:", r.stderr.strip()[-600:])
|
|
return SKIP_EXIT
|
|
first = False
|
|
p = parse(r.stdout)
|
|
tail = r.stdout.strip().splitlines()[-6:]
|
|
print(f"scale {s} {path} {timing} frame_generation={fg}: exit={r.returncode}", tail,
|
|
r.stderr.strip()[-800:])
|
|
if r.returncode or p["failures"] != 0 or p["real"] is None:
|
|
ok = False
|
|
m = re.search(r"^presents=(\d+) generated=(\d+)$", r.stdout, re.M)
|
|
p["presents"] = int(m[1]) if m else -1
|
|
p["generated"] = int(m[2]) if m else -1
|
|
runs[fg] = p
|
|
if runs[0]["real"] != runs[1]["real"] or runs[0]["presents"] != runs[1]["presents"]:
|
|
print(f"FAIL scale {s} {path} {timing}: the real frames differ with generation on "
|
|
f"({runs[0]['real']}/{runs[0]['presents']} off, "
|
|
f"{runs[1]['real']}/{runs[1]['presents']} on)")
|
|
ok = False
|
|
if runs[0]["generated"] != 0 or runs[1]["generated"] <= 0:
|
|
print(f"FAIL scale {s} {path} {timing}: generated frames off={runs[0]['generated']} "
|
|
f"on={runs[1]['generated']}")
|
|
ok = False
|
|
print("PASS" if ok else "FAIL")
|
|
return 0 if ok else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|