Files
psxrecomp/runtime/tests/run_gl_readback_region.py
Shokunin 4a2f5a67d2 OpenGL backend: record on the emulation thread, replay on the render thread
With the render thread running, the backend's facade table records every
state change, primitive, fill, copy, CPU->VRAM upload (payload copied)
and native-wide call, and the present, into the command ring; the render
thread replays them through the unchanged glb_* functions. With it off
gl_backend_get returns the existing table and nothing new runs.

Sync points: every other GL entry point reached on the emulation thread
(readbacks, savestate restage, render passes, OpenXR, settings,
diagnostics) drains the ring and takes the context (GL_RT_SYNC); the
emulation thread then runs the rest of that frame directly, exactly as
without the thread, and hands the context back at the frame boundary when
the frame may run asynchronously again (no depth24 display, dual-raster,
frame interpolation, render pass, netplay or OpenXR).

Guest-visible VRAM stays gpu.c's array: while the render thread owns the
context, uploads stage from a private copy that receives each payload in
command order. Guest state the backend reads while drawing (per-primitive
widescreen tags, the native-wide fast-path latch) is captured at record
time; host overlays travel with the present; a present that a newer
recorded frame already supersedes is skipped. The emulation thread answers
projective/wide-present/draw-area queries from mirrors of the recorded
calls. The per-frame display-ring readback is queued (fbo_peek_deferred)
instead of being a sync point.

The GL fixtures link render_thread.c and stub the two facade hooks; the
scale fixture also gains the gpu_ws_background_requires_full_composite
stub it was already missing (it failed to link on master).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 14:25:57 -07:00

98 lines
4.5 KiB
Python

"""Run source-owned readback checks with a hidden real OpenGL context."""
import argparse
import json
import os
import pathlib
import re
import shutil
import subprocess
import sys
import tempfile
def probe_result_ok(code, stdout, stderr, expect_unbounded=False):
lines = stdout.rstrip().splitlines()
summary = re.fullmatch(r"checks=(\d+) failures=(\d+)", lines[-1]) if lines else None
if not summary or int(summary[1]) == 0:
return False
failures = [line for line in stderr.splitlines() if line.startswith("FAIL ")]
if expect_unbounded:
return (code == 1 and int(summary[2]) == 1 and
failures == ["FAIL single-pixel bounded transfer"])
return code == 0 and int(summary[2]) == 0 and not failures
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--compiler-bin", required=True)
parser.add_argument("--sdl-root", required=True)
parser.add_argument("--sdl-library", help="Explicit SDL3 library (e.g. a system DLL import library)")
parser.add_argument("--output", required=True)
parser.add_argument("--gl-source")
parser.add_argument("--fixture", type=pathlib.Path)
parser.add_argument("--expect-unbounded", action="store_true")
args = parser.parse_args()
framework = pathlib.Path(__file__).resolve().parents[2]
compiler = pathlib.Path(args.compiler_bin).resolve()
for name in ("gcc.exe", "g++.exe"):
if not (compiler / name).is_file():
parser.error(f"--compiler-bin must contain {name}: {compiler}")
sdl = pathlib.Path(args.sdl_root).resolve()
if not (sdl / "sdl3-src/include").is_dir():
parser.error(f"--sdl-root must contain the sdl3-src/include directory: {sdl}")
sdl_library = pathlib.Path(args.sdl_library).resolve() if args.sdl_library else sdl / "sdl3-build/libSDL3.a"
if not sdl_library.is_file():
parser.error(f"SDL3 library does not exist: {sdl_library}")
output = pathlib.Path(args.output).resolve()
output.mkdir(parents=True, exist_ok=True)
dest = pathlib.Path(tempfile.mkdtemp(prefix="readback-", dir=output))
print("Evidence directory:", dest)
env = os.environ.copy()
env["PATH"] = str(compiler) + os.pathsep + env.get("PATH", "")
if args.gl_source:
shutil.copyfile(args.gl_source, dest / "gpu_gl_renderer.c")
includes = ["-I", str(dest), "-I", str(framework / "runtime/include"),
"-I", str(framework / "runtime/src"), "-I", str(sdl / "sdl3-src/include")]
receipt = []
def run(command):
command = [str(value) for value in command]
result = subprocess.run(command, cwd=dest, capture_output=True,
text=True, encoding="utf-8", errors="replace", env=env)
receipt.append({"cmd": command, "exit": result.returncode,
"stdout": result.stdout, "stderr": result.stderr})
(dest / "receipt.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
print(result.stdout[-600:], result.stderr[-2000:])
return result
fixture = args.fixture or framework / "runtime/tests/test_gl_readback_region.c"
# The renderer's render-pass paths call the journal and VRAM policy in
# render_pass_plan.c (self-contained: no other runtime symbols).
sources = [("probe", fixture), ("sw", framework / "runtime/src/gpu_sw_renderer.c"),
("rp", framework / "runtime/src/render_pass_plan.c"),
("xr", framework / "runtime/src/psx_openxr.c")]
if (framework / "runtime/src/render_thread.c").exists():
sources.append(("rth", framework / "runtime/src/render_thread.c"))
for name, source in sources:
if run([compiler / "gcc.exe", "-std=c11", "-O2", "-flto", "-DPSX_SDL3=1",
"-DPSX_NO_DEBUG_TOOLS=1", *includes, "-c", source,
"-o", dest / (name + ".o")]).returncode:
return 2
libraries = ["m", "kernel32", "user32", "gdi32", "winmm", "imm32", "ole32",
"oleaut32", "version", "uuid", "advapi32", "setupapi", "shell32",
"dinput8", "opengl32"]
if run([compiler / "g++.exe", "-O2", "-flto", *[dest / (name + ".o") for name, _ in sources],
sdl_library, *["-l" + name for name in libraries],
"-o", dest / "probe.exe"]).returncode:
return 2
for scale in (1, 4):
result = run([dest / "probe.exe", scale])
if not probe_result_ok(result.returncode, result.stdout, result.stderr,
args.expect_unbounded):
return 1
return 0
if __name__ == "__main__":
sys.exit(main())