mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Validate GL fixture toolchain and exact negative result
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user