tests: run gl_scale_invariance_test on Windows (MinGW) when a GL context exists

The harness gains a MinGW link path (static SDL3 plus opengl32 and its Win32
libraries, LTO so the PE linker never sees the renderer's unused references,
probe.exe) and CMake registers the test on MinGW
as well as macOS/Linux. A host that cannot create a hidden window with a GL
3.3 core context (headless, or Windows over SSH) now exits 77 from a
preflight run, and the test carries SKIP_RETURN_CODE 77 so CTest reports a
skip rather than a pass or a failure.

Also stub gpu_ws_background_requires_full_composite in the fixture: since
7b2962ef the renderer references it and the fixture no longer linked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Shokunin
2026-10-06 00:58:51 -07:00
parent 4c3c69eaec
commit 3eb37ecb09
3 changed files with 66 additions and 22 deletions
+10 -8
View File
@@ -167,12 +167,13 @@ if(BUILD_TESTING)
else()
message(STATUS "gl_readback_region_test: not registered (requires MinGW, SDL3 headers/static library in PSX_GL_READBACK_SDL_ROOT and gcc.exe/g++.exe in PSX_GL_READBACK_COMPILER_BIN)")
endif()
# Real-GL internal-resolution invariance (macOS/Linux, hidden window): the
# guest-visible VRAM is identical at every scale, lines stay one native
# pixel thick, and an over-limit request clamps inside GL. Needs the SDL3
# this tree fetched and a GPU session; labelled hardware like the readback
# test above.
if(NOT WIN32 AND Python3_Interpreter_FOUND AND TARGET SDL3::SDL3)
# Real-GL internal-resolution invariance (macOS, Linux and Windows/MinGW,
# hidden window): the guest-visible VRAM is identical at every scale, lines
# stay one native pixel thick, and an over-limit request clamps inside GL.
# Needs the SDL3 this tree fetched and a GPU session; labelled hardware like
# the readback test above. A host with no window/GL 3.3 context (headless,
# or Windows over SSH) exits 77 and is reported as skipped.
if((NOT WIN32 OR MINGW) AND Python3_Interpreter_FOUND AND TARGET SDL3::SDL3)
if(DEFINED sdl3_SOURCE_DIR)
set(_gl_scale_sdl_inc "${sdl3_SOURCE_DIR}/include")
else()
@@ -186,10 +187,11 @@ if(BUILD_TESTING)
--sdl-include "${_gl_scale_sdl_inc}"
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-scale-results")
set_tests_properties(gl_scale_invariance_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 300)
set_tests_properties(gl_scale_invariance_test PROPERTIES
LABELS "gpu;opengl;hardware" TIMEOUT 300 SKIP_RETURN_CODE 77)
unset(_gl_scale_sdl_inc)
else()
message(STATUS "gl_scale_invariance_test: not registered (needs a non-Windows host, Python 3 and SDL3)")
message(STATUS "gl_scale_invariance_test: not registered (needs macOS, Linux or MinGW, Python 3 and SDL3)")
endif()
if(Python3_Interpreter_FOUND AND TARGET SDL3::SDL3-static)
add_test(NAME gl_texture_filter_test
+35 -12
View File
@@ -34,10 +34,13 @@ Pass runs (mode passes, where the renderer has the frame-rate stack's render
passes) check that the window mode refuses them and the full-VRAM surface
offers them.
macOS/Linux: pass the SDL3 include directory and static library (for example
from a runtime build tree's _deps/sdl3-src/include and
macOS/Linux/Windows (MinGW): pass the SDL3 include directory and static
library (for example from a runtime build tree's _deps/sdl3-src/include and
_deps/sdl3-build/libSDL3.a) and a C compiler. 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 (a
headless or non-interactive session, such as Windows over SSH) the fixture
exits SKIP_EXIT and so does this script: CTest reports a skip, not a pass.
"""
import argparse
import json
@@ -53,6 +56,11 @@ MAC_FRAMEWORKS = ["Cocoa", "OpenGL", "IOKit", "CoreVideo", "CoreAudio", "AudioTo
"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 # CTest SKIP_RETURN_CODE, as wtrace_dump_test uses it
WINDOWS = os.name == "nt" or platform.system().startswith(("MINGW", "MSYS", "CYGWIN"))
def parse_run(stdout):
@@ -120,8 +128,12 @@ def main():
sources.append(("xr", framework / "runtime/src/psx_openxr.c"))
# Unused renderer functions reference the rest of the runtime; the linker
# drops them (-dead_strip, or per-function sections with --gc-sections).
# MinGW's PE linker reports undefined references from sections it later
# collects, so there LTO drops them first (as run_gl_readback_region.py).
# Anything still unresolved is a link error, not a NULL call at run time.
sections = [] if platform.system() == "Darwin" else ["-ffunction-sections", "-fdata-sections"]
if WINDOWS:
sections.append("-flto")
# Render passes (the frame-rate stack): build the fixture's passes mode.
renderer = (framework / "runtime/src/gpu_gl_renderer.c").read_text(encoding="utf-8")
passes = "uint32_t gl_renderer_pass_unavailable(void)" in renderer
@@ -136,11 +148,15 @@ def main():
print(r.stderr[-3000:])
return 2
objs.append(o)
link = [args.cc, *objs, args.sdl_library, "-o", dest / "probe"]
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", "-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)
@@ -148,11 +164,18 @@ def main():
print(r.stderr[-3000:])
return 2
# One preflight run: no window/GL context on this host means skip, before
# any check is counted. After this, a skip exit is a failure like any other.
r = run([probe, 1])
if r.returncode == SKIP_EXIT:
print("SKIP:", r.stderr.strip()[-600:])
return SKIP_EXIT
ok = True
digests = {}
hires_full = {}
for s in [int(v) for v in args.scales.split(",") if v]:
r = run([dest / "probe", s])
r = run([probe, s])
parsed = parse_run(r.stdout)
print(f"scale {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-3:],
r.stderr.strip()[-600:])
@@ -164,7 +187,7 @@ def main():
wenv = dict(env)
wenv["PSX_GL_HIRES_WINDOW"] = "1"
for s in [int(v) for v in args.window_scales.split(",") if v]:
r = run([dest / "probe", s, "window"], env=wenv)
r = run([probe, s, "window"], env=wenv)
parsed = parse_run(r.stdout)
print(f"window {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-3:],
r.stderr.strip()[-600:])
@@ -192,7 +215,7 @@ def main():
for label, s, extra, ref, tiles in sbs_runs:
e = dict(env)
e.update(extra)
r = run([dest / "probe", s, "sbs"], env=e)
r = run([probe, s, "sbs"], env=e)
parsed = parse_run(r.stdout)
got_tiles = re.search(r"^tiles=(\d+)$", r.stdout, re.M)
print(f"sbs {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-5:],
@@ -218,7 +241,7 @@ def main():
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"})):
e = dict(env)
e.update(extra)
r = run([dest / "probe", s, "lines"], env=e)
r = run([probe, s, "lines"], env=e)
parsed = parse_run(r.stdout)
print(f"lines {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-4:],
r.stderr.strip()[-600:])
@@ -241,7 +264,7 @@ def main():
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"}, True)):
e = dict(env)
e.update(extra)
r = run([dest / "probe", s, "capture"], env=e)
r = run([probe, s, "capture"], env=e)
parsed = parse_run(r.stdout)
print(f"capture {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-2:],
r.stderr.strip()[-600:])
@@ -257,7 +280,7 @@ def main():
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"})):
e = dict(env)
e.update(extra)
r = run([dest / "probe", s, "mask"], env=e)
r = run([probe, s, "mask"], env=e)
parsed = parse_run(r.stdout)
print(f"mask {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-1:],
r.stderr.strip()[-600:])
@@ -271,7 +294,7 @@ def main():
e.update(extra)
got = {}
for on in (0, 1):
r = run([dest / "probe", s, "twin", on], env=e)
r = run([probe, s, "twin", on], env=e)
parsed = parse_run(r.stdout)
m = re.search(r"^twin_flushes=(\d+) batches=(\d+)$", r.stdout, re.M)
print(f"twin {label} {s} batching={on}: exit={r.returncode}",
@@ -293,7 +316,7 @@ def main():
("window", 18, {"PSX_GL_HIRES_WINDOW": "1"})) if passes else ()):
e = dict(env)
e.update(extra)
r = run([dest / "probe", s, "passes"], env=e)
r = run([probe, s, "passes"], env=e)
parsed = parse_run(r.stdout)
print(f"passes {label} {s}: exit={r.returncode}", r.stdout.strip().splitlines()[-2:],
r.stderr.strip()[-600:])
@@ -303,7 +326,7 @@ def main():
e = dict(env)
if budget is not None:
e["PSX_GL_VRAM_BUDGET_MB"] = budget
r = run([dest / "probe", s, "clamp"], env=e)
r = run([probe, s, "clamp"], env=e)
parsed = parse_run(r.stdout)
print(f"clamp {label}: exit={r.returncode}", r.stdout.strip().splitlines()[-2:],
r.stderr.strip()[-600:])
+21 -2
View File
@@ -76,6 +76,7 @@ int gpu_display_is_depth24(void){return 0;}
void gpu_get_display_info(GpuDisplayInfo *out){memset(out,0,sizeof(*out));out->width=320;out->height=240;}
int psx_ws_prim_in_backdrop(void){return 0;}
int gpu_ws_nw_flat_backdrop_enabled(void){return 0;}
int gpu_ws_background_requires_full_composite(void){return 0;}
int g_ws_tex_edge_pct=0;
int psx_ws_prim_is_tagged(void){return 0;}
void psx_ws_dbg_gate_frame_snapshot(void){}
@@ -782,7 +783,14 @@ int main(int argc, char **argv) {
int scale = argc > 1 ? atoi(argv[1]) : 1;
const char *mode = argc > 2 ? argv[2] : "scene";
int window = !strcmp(mode, "window");
if (SDL_Init(SDL_INIT_VIDEO) != 0) return 2;
/* No video device, window or GL context (a headless or non-interactive
* session, such as Windows over SSH) is "this host cannot run the test":
* exit 77, which the harness reports as a CTest skip. A context that
* exists but fails the renderer's init is a failure. */
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
fprintf(stderr, "SKIP no video (%s)\n", SDL_GetError());
return 77;
}
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
@@ -791,7 +799,18 @@ int main(int argc, char **argv) {
#endif
SDL_Window *win = SDL_CreateWindow("Scale invariance hidden test", 0, 0, 128, 128,
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
if (!win) return 2;
if (!win) { fprintf(stderr, "SKIP no window (%s)\n", SDL_GetError()); return 77; }
/* Probe with the renderer's attributes: the renderer drops its context on
* any init failure, so only a probe tells "no context" from "broken". */
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GLContext probe = SDL_GL_CreateContext(win);
if (!probe) {
fprintf(stderr, "SKIP no GL 3.3 core context (%s)\n", SDL_GetError());
return 77;
}
SDL_GL_MakeCurrent(win, NULL);
SDL_GL_DeleteContext(probe);
for (int i = 0; i < 1024*512; i++) vram[i] = 0;
glb_init(vram);
glb_set_scale(scale);