Files
psxrecomp/runtime/tests/run_gl_render_thread.py
T
Shokunin f10db8a0f0 Present thread: swap off the render thread ([video] present_thread)
Opt-in under the render thread. Composed frames go to a ring of offscreen
slots at the window's size (binds of framebuffer 0 on the composing context
land on the current slot); a present thread with a second, shared context on
the same window waits on the slot's fence, copies it 1:1 and swaps. The
compositor's wait moves off rendering; the composing side waits only while
every slot is queued or on screen. Falls back to direct swaps when the shared
context or sync objects are unavailable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 12:56:48 -07:00

167 lines
7.5 KiB
Python

"""Build and run test_gl_render_thread.c on a hidden real OpenGL context.
Each scale runs twice, render thread off and on ([video] render_thread). The
values the scripted guest read back (rb), the native VRAM (digest), the frame
at internal resolution (hires) and the native-wide surface (wide) must be the
same both ways, and the threaded run must actually have recorded and replayed
the stream (its own checks). A third run per scale with one frame in flight
(PSX_TEST_RT_FRAMES is not needed: the fixture takes the bound as argv) is not
required; the in-flight bound itself is covered by render_thread_test.
macOS/Linux/Windows (MinGW): pass the SDL3 include directory and static
library and a C compiler, as for run_gl_scale_invariance.py. Evidence
(commands, output) is written to receipt.json under --output. When the host
cannot create a hidden window with a GL 3.3 core context (headless, or Windows
over SSH) the fixture exits SKIP_EXIT and so does this script: CTest reports a
skip, not a pass.
"""
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 = ("rb", "digest", "hires", "wide")
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,4")
ap.add_argument("--frames", default="80")
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_render_thread.c"
out_root = pathlib.Path(args.output).resolve()
out_root.mkdir(parents=True, exist_ok=True)
dest = pathlib.Path(tempfile.mkdtemp(prefix="rth-", 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"), ("pth", src / "present_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]:
runs = {}
for threaded in (0, 1):
r = run([probe, s, threaded, args.frames])
if first and r.returncode == SKIP_EXIT:
# No window/GL context on this host: skip. Only the first
# run may skip; after it, 77 is a failure like any other.
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} render_thread={threaded}: exit={r.returncode}", tail,
r.stderr.strip()[-800:])
if r.returncode or p["failures"] != 0 or any(p[k] is None for k in KEYS):
ok = False
runs[threaded] = p
for k in KEYS:
if runs[0][k] != runs[1][k]:
print(f"FAIL scale {s}: {k} differs with the render thread on "
f"({runs[0][k]} off, {runs[1][k]} on)")
ok = False
# Dynamic resolution: the same level steps between frames, recorded with
# the thread on (scales above 1 only; 1x has no levels below it).
for s in [int(v) for v in args.scales.split(",") if v and int(v) > 1]:
runs = {}
for threaded in (0, 1):
r = run([probe, s, threaded, args.frames, "dynres"])
p = parse(r.stdout)
tail = r.stdout.strip().splitlines()[-7:]
print(f"scale {s} dynres render_thread={threaded}: exit={r.returncode}", tail,
r.stderr.strip()[-800:])
if r.returncode or p["failures"] != 0 or any(p[k] is None for k in KEYS):
ok = False
runs[threaded] = p
for k in KEYS:
if runs[0][k] != runs[1][k]:
print(f"FAIL scale {s} dynres: {k} differs with the render thread on "
f"({runs[0][k]} off, {runs[1][k]} on)")
ok = False
print("PASS" if ok else "FAIL")
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())