diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index 745adcfe..e17f8417 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -169,25 +169,24 @@ endif() option(MOBILEGL_ITEST_REQUIRE_GPU "Fail (rather than skip) the integration scenarios when the headless harness is unusable" OFF) -# DirectGLES asks the system EGL for a pbuffer config, and on Mesa the default -# platform is not X11 unless it is said out loud (run_driver_bench.sh sets the -# same variable). Wrong platform here is not a soft failure: eglCreatePbuffer -# fails and every scenario skips. -if (UNIX AND NOT APPLE AND NOT ANDROID) - set(MOBILEGL_ITEST_EGL_PLATFORM "x11" CACHE STRING - "EGL_PLATFORM for the integration tests (empty: leave the loader alone)") -else() - set(MOBILEGL_ITEST_EGL_PLATFORM "" CACHE STRING - "EGL_PLATFORM for the integration tests (empty: leave the loader alone)") -endif() +# No EGL_PLATFORM knob here on purpose. The harness pins EGL_PLATFORM=surfaceless +# itself before its first EGL call (HeadlessGL.cpp, EnsureHeadlessPlatform) so a +# developer's machine and a CI runner take the SAME path whether or not a window +# system happens to be running. This used to inject "x11", which is how the lane +# came up green on a workstation with WSLg and died on a runner with no X server. +# +# A build-system knob would not just be redundant, it would be a trap: `set(... +# CACHE ...)` does not rewrite an existing cache, so every build directory +# configured before this change would keep injecting EGL_PLATFORM=x11 and go on +# binding to a window system - silently, and only on the machines that have one. +# Someone reproducing a platform-specific bug sets EGL_PLATFORM in their own +# environment, which the harness still honours. set(MGL_ITEST_COMMON_ENV "") if (MOBILEGL_ITEST_EGL_VENDOR) list(APPEND MGL_ITEST_COMMON_ENV "__EGL_VENDOR_LIBRARY_FILENAMES=${MOBILEGL_ITEST_EGL_VENDOR}") endif() -if (MOBILEGL_ITEST_EGL_PLATFORM) - list(APPEND MGL_ITEST_COMMON_ENV "EGL_PLATFORM=${MOBILEGL_ITEST_EGL_PLATFORM}") -endif() +unset(MOBILEGL_ITEST_EGL_PLATFORM CACHE) # see above: an old cache must not resurrect x11 if (MOBILEGL_ITEST_REQUIRE_GPU) list(APPEND MGL_ITEST_COMMON_ENV "MOBILEGL_ITEST_REQUIRE_GPU=1") endif() diff --git a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp index 07af1950..3f37b9b3 100644 --- a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp +++ b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.cpp @@ -74,6 +74,40 @@ namespace MGITest { std::string renderer; }; + // The harness is headless BY CONSTRUCTION, on every machine: it must never + // reach a window system, not even where one happens to be running. This is + // not a CI accommodation - it is what keeps a developer's run and a CI run + // the same run. The lane was wired up green on a workstation and immediately + // died on the runner precisely because the workstation had a DISPLAY (WSLg) + // and took Mesa's x11 platform, while the runner has none; that divergence + // is the bug, and pinning the platform here is the fix for it. + // + // Mesa selects its EGL platform from EGL_PLATFORM at loader time, so this + // has to run before the first EGL call in the process (see EnsureHeadless + // callers). surfaceless is the platform with no window-system dependency at + // all; the surface this file then creates is still a pbuffer, which every + // platform supports and which the amendment to this rule requires as the + // fallback shape. DISPLAY/WAYLAND_DISPLAY are cleared as well so that a + // driver that consults them directly cannot reintroduce the dependency + // behind EGL's back. Desktop-only file: MG_IntegrationTest never builds + // for Android, so no device path is affected. + void EnsureHeadlessPlatform() { +#if defined(__linux__) && !defined(__ANDROID__) + static bool done = false; + if (done) { + return; + } + done = true; + // An explicit EGL_PLATFORM from the operator still wins: pinning a + // platform is exactly how someone reproduces a platform-specific bug. + if (std::getenv("EGL_PLATFORM") == nullptr) { + setenv("EGL_PLATFORM", "surfaceless", 1); + } + unsetenv("DISPLAY"); + unsetenv("WAYLAND_DISPLAY"); +#endif + } + // THE bring-up, in one function so the pre-flight child and the parent run // literally the same sequence - a pre-flight that tests something narrower // than what the parent will do is exactly the kind of "predictive" check @@ -82,6 +116,9 @@ namespace MGITest { // Returns 0 on success, or the 1-based index of the step that failed, and // fills outReason either way. int RunEglBringUp(EglBringUp& out, std::string& outReason) { + // Belt and braces: the pre-flight child and the parent both enter here, + // and neither may be the first to touch EGL without this having run. + EnsureHeadlessPlatform(); EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); if (display == EGL_NO_DISPLAY) { outReason = WithEglError("eglGetDisplay(EGL_DEFAULT_DISPLAY) returned EGL_NO_DISPLAY"); @@ -199,11 +236,10 @@ namespace MGITest { } if (child == 0) { close(channel[0]); - // The child is EXPECTED to die on a signal on an unusable - // platform; that is the measurement. Do not let each such - // measurement drop a core file next to the test binary. - const rlimit noCore{0, 0}; - setrlimit(RLIMIT_CORE, &noCore); + // No core suppression here, deliberately: when the child dies on a + // signal, the core IS the diagnosis (an rlimit that used to sit here + // made a CI-only crash undebuggable). Machines that do not want + // cores control that with the usual ulimit/core_pattern knobs. std::fprintf(stderr, "[itest] pre-flight child: attempting a full EGL bring-up\n"); EglBringUp local; std::string reason; @@ -284,9 +320,19 @@ namespace MGITest { } } // namespace + namespace { + bool EnvFlag(const char* name) { + const char* value = std::getenv(name); + return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0; + } + } // namespace + bool RequireGpu() { - const char* value = std::getenv("MOBILEGL_ITEST_REQUIRE_GPU"); - return value != nullptr && value[0] != '\0' && std::strcmp(value, "0") != 0; + return EnvFlag("MOBILEGL_ITEST_REQUIRE_GPU"); + } + + bool RequireHardwareGpu() { + return EnvFlag("MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU"); } std::ostream& operator<<(std::ostream& os, const Rgba8& c) { @@ -390,6 +436,10 @@ namespace MGITest { } HeadlessGL::HeadlessGL() { + // Before anything else in this process can reach EGL, and in particular + // before the pre-flight forks - the child must measure the same platform + // the parent will use. + EnsureHeadlessPlatform(); m_backendName = EnvOr("MOBILEGL_BACKEND_TYPE", ""); m_usable = BringUp(); } diff --git a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h index 8e1234dc..355b453b 100644 --- a/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h +++ b/MobileGL/MG_IntegrationTest/Harness/HeadlessGL.h @@ -41,6 +41,15 @@ namespace MGITest { // a job that ran everything. bool RequireGpu(); + // True when MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set: additionally asserts + // that the context did NOT land on a software rasterizer. Deliberately a + // SEPARATE switch from RequireGpu - a GPU-less CI runner is a supported and + // intended configuration for these scenarios (they pin backend draw logic, + // which llvmpipe/lavapipe execute faithfully), so CI wants the falsifiability + // of REQUIRE_GPU without the hardware demand. Use this one only where a vendor + // pin silently degrading to software would invalidate the measurement. + bool RequireHardwareGpu(); + struct Rgba8 { std::uint8_t r = 0, g = 0, b = 0, a = 0; diff --git a/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h b/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h index 21a3942c..db9b4f4b 100644 --- a/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h +++ b/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h @@ -45,12 +45,18 @@ namespace MGITest { } GTEST_SKIP() << "no usable GPU/display/ICD for backend " << gl.BackendName() << ": " << gl.SkipReason(); } - if (RequireGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) { - // "Ran on llvmpipe" must not be able to pass as "ran on the GPU": - // a misconfigured vendor pin silently lands on the software - // rasterizer, and REQUIRE_GPU exists precisely to make that loud. - FAIL() << "MOBILEGL_ITEST_REQUIRE_GPU is set but the context landed on a software rasterizer: " - << gl.RendererString(); + if (RequireHardwareGpu() && LooksLikeSoftwareRasterizer(gl.RendererString())) { + // Only when hardware was asked for BY NAME. REQUIRE_GPU means "an + // unusable harness is a failure, not a silent skip" - it is the + // falsifiability switch, and CI is exactly where it belongs. But CI + // runners have no GPU, so folding "must not be llvmpipe" into the + // same switch made the CI lane unpassable by construction: the + // scenarios pin backend draw logic, which a software rasterizer + // executes just as faithfully. Landing on llvmpipe/lavapipe there is + // the intended configuration, not a misconfiguration. A vendor pin + // that must not silently degrade sets REQUIRE_HARDWARE_GPU. + FAIL() << "MOBILEGL_ITEST_REQUIRE_HARDWARE_GPU is set but the context landed on a software " + << "rasterizer: " << gl.RendererString(); } // A scenario starts from a clean slate but shares the context (and so // the renderer's memos) with every other scenario in this process - diff --git a/MobileGL/MG_IntegrationTest/Main.cpp b/MobileGL/MG_IntegrationTest/Main.cpp index 69c5f407..e211b625 100644 --- a/MobileGL/MG_IntegrationTest/Main.cpp +++ b/MobileGL/MG_IntegrationTest/Main.cpp @@ -26,8 +26,14 @@ namespace { const MGITest::HeadlessGL& gl = MGITest::HeadlessGL::Get(); std::fprintf(stderr, "MobileGL integration scenarios: backend=%s\n", gl.BackendName().c_str()); if (gl.Usable()) { - std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless)\n", - gl.RendererString().c_str(), gl.Width(), gl.Height()); + // EGL_PLATFORM is echoed because it is the invariant this harness + // rests on: the run is headless on every machine, so a run that + // silently bound to a workstation's window system is a different + // run from CI's and must be visible as one in the log. + const char* eglPlatform = std::getenv("EGL_PLATFORM"); + std::fprintf(stderr, " renderer: %s\n surface: %dx%d pbuffer (headless, EGL_PLATFORM=%s)\n", + gl.RendererString().c_str(), gl.Width(), gl.Height(), + eglPlatform != nullptr ? eglPlatform : ""); } else if (MGITest::RequireGpu()) { std::fprintf(stderr, " FAILING every scenario (MOBILEGL_ITEST_REQUIRE_GPU is set): %s\n",