Validate GL fixture toolchain and exact negative result

This commit is contained in:
Alexandros Mandravillis
2026-09-06 13:36:37 +03:00
parent 54f86b4c91
commit 4c35d0515b
4 changed files with 138 additions and 29 deletions
+6
View File
@@ -42,3 +42,9 @@ DuckStation's software readback path was consulted as an alternative. This corre
authority and needs no title hook or controller policy change. Future work can
reduce row submissions or use a more precise dirty set if measured demand
justifies it.
For Clang/MinGW builds, set `PSX_GL_READBACK_COMPILER_BIN` to a GCC/MinGW
bin directory containing both gcc.exe and g++.exe. CMake checks these files
before registering the hardware test. `gl_readback_runner_test` separately
checks strict result parsing without a GPU or compiler. A negative run passes
only for exactly one failure named single-pixel bounded transfer.
+12 -4
View File
@@ -26,19 +26,27 @@ if(BUILD_TESTING)
# Optional real-hardware regression. Configure its dependencies explicitly;
# absence must remain visible, never counted as a passing GPU test.
set(PSX_GL_READBACK_SDL_ROOT "" CACHE PATH "SDL3 static build root for GL readback test")
if(MINGW AND PSX_GL_READBACK_SDL_ROOT)
get_filename_component(_gl_test_default_bin "${CMAKE_C_COMPILER}" DIRECTORY)
set(PSX_GL_READBACK_COMPILER_BIN "${_gl_test_default_bin}" CACHE PATH "GCC/MinGW bin for GL fixture")
find_package(Python3 QUIET COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(NAME gl_readback_runner_test COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_readback_runner.py")
endif()
if(MINGW AND PSX_GL_READBACK_SDL_ROOT
AND EXISTS "${PSX_GL_READBACK_COMPILER_BIN}/gcc.exe"
AND EXISTS "${PSX_GL_READBACK_COMPILER_BIN}/g++.exe")
find_package(Python3 REQUIRED COMPONENTS Interpreter)
get_filename_component(_gl_test_compiler_bin "${CMAKE_C_COMPILER}" DIRECTORY)
add_test(NAME gl_readback_region_test
COMMAND "${Python3_EXECUTABLE}"
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_readback_region.py"
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_readback_region.c"
--compiler-bin "${_gl_test_compiler_bin}"
--compiler-bin "${PSX_GL_READBACK_COMPILER_BIN}"
--sdl-root "${PSX_GL_READBACK_SDL_ROOT}"
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-readback-results")
set_tests_properties(gl_readback_region_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 180)
else()
message(STATUS "gl_readback_region_test: not registered (requires MinGW and PSX_GL_READBACK_SDL_ROOT)")
message(STATUS "gl_readback_region_test: not registered (requires MinGW, PSX_GL_READBACK_SDL_ROOT and gcc.exe/g++.exe in PSX_GL_READBACK_COMPILER_BIN)")
endif()
add_executable(launcher_device_roundtrip_test
tests/test_launcher_device_roundtrip.cpp)
+84 -25
View File
@@ -1,25 +1,84 @@
import pathlib,subprocess,json,os,sys,argparse,shutil,tempfile
parser=argparse.ArgumentParser(description="Run hidden real-GL source-owned readback tests")
parser.add_argument('--compiler-bin',required=True);parser.add_argument('--sdl-root',required=True)
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()
F=pathlib.Path(__file__).resolve().parents[2]
output=pathlib.Path(args.output).resolve();output.mkdir(parents=True,exist_ok=True)
D=pathlib.Path(tempfile.mkdtemp(prefix='readback-',dir=output))
print('Evidence directory:',D)
old=pathlib.Path(args.sdl_root).resolve();T=pathlib.Path(args.compiler_bin).resolve();env=os.environ.copy();env['PATH']=str(T)+os.pathsep+env['PATH']
if args.gl_source:shutil.copyfile(args.gl_source,D/'gpu_gl_renderer.c')
inc=['-I',str(D),'-I',str(F/'runtime/include'),'-I',str(F/'runtime/src'),'-I',str(old/'sdl3-src/include')];receipt=[]
def run(cmd):
p=subprocess.run([str(x) for x in cmd],cwd=D,capture_output=True,text=True,errors='replace',env=env);receipt.append({'cmd':[str(x) for x in cmd],'exit':p.returncode,'stdout':p.stdout,'stderr':p.stderr});(D/'receipt.json').write_text(json.dumps(receipt,indent=2));print(p.stdout[-600:],p.stderr[-2000:]);return p.returncode
for name,src in [('probe',args.fixture or F/'runtime/tests/test_gl_readback_region.c'),('sw',F/'runtime/src/gpu_sw_renderer.c')]:
if run([T/'gcc.exe','-std=c11','-O2','-flto','-DPSX_SDL3=1','-DPSX_NO_DEBUG_TOOLS=1',*inc,'-c',src,'-o',D/(name+'.o')]):sys.exit(2)
libs=['m','kernel32','user32','gdi32','winmm','imm32','ole32','oleaut32','version','uuid','advapi32','setupapi','shell32','dinput8','opengl32']
if run([T/'g++.exe','-O2','-flto',D/'probe.o',D/'sw.o',old/'sdl3-build/libSDL3.a',*['-l'+x for x in libs],'-o',D/'probe.exe']):sys.exit(2)
# Test windows are hidden. No host-specific priority or affinity policy.
for scale in [1,4]:
code=run([D/'probe.exe',str(scale)])
expected=1 if args.expect_unbounded else 0
if code!=expected:sys.exit(1)
if args.expect_unbounded and 'failures=1' not in receipt[-1]['stdout']:sys.exit(1)
"""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("--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}")
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)
sdl = pathlib.Path(args.sdl_root).resolve()
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"
for name, source in [("probe", fixture), ("sw", framework / "runtime/src/gpu_sw_renderer.c")]:
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 / "probe.o", dest / "sw.o",
sdl / "sdl3-build/libSDL3.a", *["-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())
+36
View File
@@ -0,0 +1,36 @@
import unittest
from run_gl_readback_region import probe_result_ok
class ProbeResultTest(unittest.TestCase):
def test_success(self):
self.assertTrue(probe_result_ok(0, "driver=GL\nchecks=67 failures=0\n", ""))
def test_exact_negative(self):
self.assertTrue(probe_result_ok(1, "checks=67 failures=1\n",
"FAIL single-pixel bounded transfer\n", True))
def test_negative_rejects_other_counts(self):
for count in (0, 10, 11, 100, 199):
with self.subTest(count=count):
self.assertFalse(probe_result_ok(1, f"checks=67 failures={count}\n",
"FAIL single-pixel bounded transfer\n", True))
def test_negative_requires_expected_failure(self):
for error in ("", "FAIL GL error\n", "FAIL single-pixel bounded transfer\nFAIL other\n"):
self.assertFalse(probe_result_ok(1, "checks=67 failures=1\n", error, True))
def test_rejects_missing_or_malformed_summary(self):
for text in ("", "checks=67 failures=1 extra", "checks=0 failures=0",
"checks=67 failures=0\ntrailing text", "checks=67 failures=10"):
self.assertFalse(probe_result_ok(0, text, ""))
def test_rejects_exit_and_error_mismatch(self):
self.assertFalse(probe_result_ok(1, "checks=67 failures=0", ""))
self.assertFalse(probe_result_ok(0, "checks=67 failures=0", "FAIL other"))
self.assertFalse(probe_result_ok(0, "checks=67 failures=1",
"FAIL single-pixel bounded transfer", True))
if __name__ == "__main__":
unittest.main()