From eabc8ed7f529ea3695f00847dc7dbb5e7b0a5e30 Mon Sep 17 00:00:00 2001 From: Shokunin <30949000+tetrisgm@users.noreply.github.com> Date: Tue, 6 Oct 2026 02:37:08 -0700 Subject: [PATCH] tests: build and run the render thread tests on Windows (MinGW) render_thread_test is registered on MinGW (winpthreads provides the fixture's pthreads and nanosleep; the core takes its Win32 path). gl_render_thread_test gains the MinGW link path run_gl_scale_invariance.py uses on test/gl-scale-invariance-windows (static SDL3, opengl32 and its Win32 libraries, LTO, probe.exe), and a host with no window or GL 3.3 core context (headless, Windows over SSH) exits 77 from a preflight run, reported by CTest as a skip. Co-Authored-By: Claude Opus 5.5 --- runtime/CMakeLists.txt | 12 ++++++---- runtime/tests/run_gl_render_thread.py | 34 +++++++++++++++++++++++---- runtime/tests/test_gl_render_thread.c | 21 +++++++++++++++-- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/runtime/CMakeLists.txt b/runtime/CMakeLists.txt index 3903652f..4bfa5ea8 100644 --- a/runtime/CMakeLists.txt +++ b/runtime/CMakeLists.txt @@ -214,7 +214,9 @@ if(BUILD_TESTING) # Render thread ([video] render_thread) on vs off: the values the # guest reads back, native VRAM, the frame at S and the native-wide # surface are identical for a scripted stream with readbacks, mask - # bits, uploads, ring wraps and per-primitive widescreen tags. + # bits, uploads, ring wraps and per-primitive widescreen tags. A host + # with no window/GL 3.3 context (headless, Windows over SSH) exits 77: + # reported as skipped. add_test(NAME gl_render_thread_test COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py" @@ -223,7 +225,8 @@ if(BUILD_TESTING) --sdl-include "${_gl_scale_sdl_inc}" --sdl-library "$" --output "${CMAKE_CURRENT_BINARY_DIR}/gl-render-thread-results") - set_tests_properties(gl_render_thread_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 300) + set_tests_properties(gl_render_thread_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 macOS, Linux or MinGW, Python 3 and SDL3)") @@ -1127,8 +1130,9 @@ if(BUILD_TESTING) # Render thread core ([video] render_thread, docs/RENDER_THREAD.md): # ordering across ring wraps, the frame bound, stale-frame detection and # the drain + context hand-off sync point, against a fake executor. - # The fixture itself uses POSIX threads and nanosleep. - if(NOT WIN32) + # The fixture itself uses POSIX threads and nanosleep (MinGW has both; + # the core takes its Win32 path there). + if(NOT MSVC) add_executable(render_thread_test tests/test_render_thread.c src/render_thread.c) target_include_directories(render_thread_test PRIVATE include) find_package(Threads REQUIRED) diff --git a/runtime/tests/run_gl_render_thread.py b/runtime/tests/run_gl_render_thread.py index c6dc7c82..e78b39fd 100644 --- a/runtime/tests/run_gl_render_thread.py +++ b/runtime/tests/run_gl_render_thread.py @@ -8,12 +8,16 @@ 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: 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. +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 @@ -25,6 +29,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 +WINDOWS = os.name == "nt" or platform.system().startswith(("MINGW", "MSYS", "CYGWIN")) KEYS = ("rb", "digest", "hires", "wide") @@ -78,6 +87,10 @@ def main(): ("xr", src / "psx_openxr.c"), ("rth", src / "render_thread.c"), ("facade", src / "gpu_render.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") @@ -87,11 +100,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", "-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) @@ -100,10 +117,17 @@ def main(): 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([dest / "probe", s, threaded, args.frames]) + 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, diff --git a/runtime/tests/test_gl_render_thread.c b/runtime/tests/test_gl_render_thread.c index e2bb7035..3b73852b 100644 --- a/runtime/tests/test_gl_render_thread.c +++ b/runtime/tests/test_gl_render_thread.c @@ -210,7 +210,12 @@ int main(int argc, char **argv) { int scale = argc > 1 ? atoi(argv[1]) : 1; int threaded = argc > 2 && argv[2][0] == '1'; int frames = argc > 3 ? atoi(argv[3]) : 80; - if (SDL_Init(SDL_INIT_VIDEO) != 0) return 2; + /* No video device, window or GL context (headless, Windows over SSH): + * exit 77, which the harness reports as a CTest skip. */ + 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); @@ -219,7 +224,19 @@ int main(int argc, char **argv) { #endif SDL_Window *win = SDL_CreateWindow("Render thread hidden test", 0, 0, 320, 240, 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: only a probe tells "no + * context on this host" from "the renderer's init broke". */ + 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); + } gr_set_backend(GR_BACKEND_OPENGL); gr_init(vram); gr_set_scale(scale);