From 7f2ca68615aa32699d5eb411e76abfb2b388b0d7 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 25 Aug 2026 04:52:14 -0400 Subject: [PATCH] [Test] (IntegrationTest): take the shader-compiler and viewport quirks from the environment instead of internal symbols Android links this module against the SHIPPING libMobileGL.so on purpose, so the on-device run validates the real artifact - and that library is -fvisibility=hidden. Six symbols the scenarios reached for were therefore undefined and the executable could not be linked at all: MG_Config::Features, SetAsyncShaderCompileSuspended, ShaderCompilePool::Get/GetThreadCount/SetMaxConcurrency and AsyncShaderCompileEnabled. All six are gone rather than exported. CompilerThreadScope now restores the pool with glMaxShaderCompilerThreadsKHR (0xFFFFFFFF), which MaxShaderCompilerThreadsKHR_State defines as exactly the two steps it used to perform by hand, and AsyncAndSyncProgramsRenderIdenticalFrames picks its two modes with the same entry point - a zero count compiles inline, a nonzero one lifts that - so the switching is now itself under test. ExtensionStringMatchesTheConfiguration stops deriving its expectation from AsyncShaderCompileEnabled(), the very function the backends gate the extension string on: it was asserting the implementation against itself and would have passed however wrong both halves were. The expectation is now MOBILEGL_ASYNC_SHADER_COMPILE as the process inherited it, and the case skips where that is unset because the built-in default is a value only the implementation knows. Both-mode coverage in one ctest run is preserved by registration rather than by in-process forcing, and is wider than before. New entries, each APPENDING to the common/Vulkan environment so the EGL-vendor and ICD pinning is not lost - a ctest ENVIRONMENT property replaces the job environment rather than adding to it: DirectGLES./DirectVulkan.AsyncOn. run all of AsyncCompileScenario with MOBILEGL_ASYNC_SHADER_COMPILE=1; .AsyncOff. run the extension case with =0, which asserts the withdrawn side that nothing covered before; .OptimisticShaderStatus. run the Iris-shaped case with MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1 (its own entries because the quirk is not neutral for the rest of the scenario); DirectGLES.NoViewportArrayEmulation. runs the emulation control with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0. Run from a device shell with nothing set, the ambient configuration runs and the rest skip cleanly. 1836 -> 1853 ctest entries, all green. --- MobileGL/MG_IntegrationTest/CMakeLists.txt | 131 ++++++++++++ .../Harness/ScenarioFixture.h | 37 ++++ .../Scenarios/AsyncCompileScenario.cpp | 193 ++++++++++++------ .../Scenarios/ViewportArrayScenario.cpp | 39 ++-- 4 files changed, 321 insertions(+), 79 deletions(-) diff --git a/MobileGL/MG_IntegrationTest/CMakeLists.txt b/MobileGL/MG_IntegrationTest/CMakeLists.txt index d40eec92..5de3c0a9 100644 --- a/MobileGL/MG_IntegrationTest/CMakeLists.txt +++ b/MobileGL/MG_IntegrationTest/CMakeLists.txt @@ -306,6 +306,40 @@ mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ENVIRONMENT mgl_itest_join_environment(MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ESPRYT_FORCE_DS_READBACK_EMULATION=1" ${MGL_ITEST_COMMON_ENV}) +# The shader-compiler configurations AsyncCompileScenario needs, and the one +# ViewportArrayScenario's negative control needs. +# +# These used to be poked into MG_Config::Features from inside the test bodies. They +# cannot be any more - on Android this module links the SHIPPING libMobileGL.so, which +# exports nothing internal - and they should not have been anyway: half of what each of +# them decides is latched before the first GL call (the compile pool and its threads; +# the advertised extension list, which a backend builds once from the configuration in +# force at its first use), so an in-process write could only ever have moved the other +# half. Every one of them is a whole-process property, and a whole-process property is +# spelled with an environment variable and a ctest entry of its own. +# +# Note the shape of every list here: it APPENDS to MGL_ITEST_COMMON_ENV / +# MGL_ITEST_VULKAN_ENV rather than standing alone. A ctest ENVIRONMENT property REPLACES +# the job environment rather than adding to it, so an entry that lists only its mode +# variable would silently lose the EGL vendor and Vulkan ICD pinning and run against +# whatever the loader found first. +mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_COMMON_ENV}) +mgl_itest_join_environment(MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_COMMON_ENV}) +mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" ${MGL_ITEST_VULKAN_ENV}) +mgl_itest_join_environment(MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=0" ${MGL_ITEST_VULKAN_ENV}) +mgl_itest_join_environment(MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_ASYNC_SHADER_COMPILE=1" + "MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_COMMON_ENV}) +mgl_itest_join_environment(MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectVulkan" "MOBILEGL_ASYNC_SHADER_COMPILE=1" + "MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1" ${MGL_ITEST_VULKAN_ENV}) +mgl_itest_join_environment(MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT + "MOBILEGL_BACKEND_TYPE=DirectGLES" "MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0" ${MGL_ITEST_COMMON_ENV}) + # TIMEOUT on every entry: a GPU test that wedges must fail the run, not hang it. set(MGL_ITEST_TIMEOUT 120) @@ -370,3 +404,100 @@ gtest_discover_tests(MobileGLIntegrationTest TIMEOUT ${MGL_ITEST_TIMEOUT} ENVIRONMENT "${MGL_ITEST_GLES_FORCED_DS_ENVIRONMENT}" ) + +# AsyncCompileScenario, with asynchronous compilation PINNED ON per backend. +# +# Not a duplicate of what the two ambient registrations already run: they run whatever +# MobileGL's built-in default happens to be, and the day that default flips they would +# stop covering the asynchronous path without anything going red. These entries are the +# ones that keep the asynchronous half tested no matter what ships. They are also the +# only place ExtensionStringMatchesTheConfiguration can assert that the extension IS +# advertised - the case derives its expectation from this variable and nothing else, and +# skips where it is unset, precisely so that it is not asserting the implementation +# against itself. +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.AsyncOn." + TEST_FILTER "AsyncCompileScenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_ON_ENVIRONMENT}" +) +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.AsyncOn." + TEST_FILTER "AsyncCompileScenario.*" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_ON_ENVIRONMENT}" +) + +# The other side of the same switch: asynchronous compilation OFF, so +# GL_KHR_parallel_shader_compile must be WITHDRAWN from both spellings of the extension +# list and GL_MAX_SHADER_COMPILER_THREADS_KHR must read 0. Only that one case is +# registered here because it is the only one that has anything to say in this +# configuration - the other four exist to observe worker-built artifacts, and there are +# none - so registering the whole scenario would buy four guaranteed skips per backend. +# Together with the AsyncOn. entries above, one ctest run still covers both flag states, +# which is what the in-process forcing used to be for. +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.AsyncOff." + TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_ASYNC_OFF_ENVIRONMENT}" +) +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.AsyncOff." + TEST_FILTER "AsyncCompileScenario.ExtensionStringMatchesTheConfiguration" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_VULKAN_ASYNC_OFF_ENVIRONMENT}" +) + +# The optimistic-status quirk's end-to-end shape. Its own entries and not part of the +# AsyncOn. ones because the quirk is not neutral for the rest of the scenario: with it in +# force glGetShaderiv(GL_COMPILE_STATUS) deliberately answers without joining, which is +# exactly what CompletionStatusPollingThenForcedJoin asserts must NOT happen. Off by +# default and never advertised, so - unlike asynchronous compilation, which announces +# itself through the extension string - the variable is the only thing that can tell the +# case it is in force. +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.OptimisticShaderStatus." + TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_OPTIMISTIC_ENVIRONMENT}" +) +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectVulkan.OptimisticShaderStatus." + TEST_FILTER "AsyncCompileScenario.IrisShapedTwoPhaseBatchRendersCorrectly" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_VULKAN_OPTIMISTIC_ENVIRONMENT}" +) + +# The negative control for the DirectGLES gl_ViewportIndex emulation, in a process that +# has it switched off. One case, because it is the only one the switch may touch: with +# the emulation off the three positive cases in the same fixture describe behaviour the +# backend does not have, so a whole-scenario registration would be three guaranteed reds. +# DirectGLES only - the flag steers nothing on DirectVulkan, which routes natively. +gtest_discover_tests(MobileGLIntegrationTest + TEST_PREFIX "DirectGLES.NoViewportArrayEmulation." + TEST_FILTER "ViewportArrayScenario.WithoutTheEmulationEveryIndexCollapsesOntoViewportZero" + DISCOVERY_TIMEOUT 30 + PROPERTIES + LABELS integration-gpu + TIMEOUT ${MGL_ITEST_TIMEOUT} + ENVIRONMENT "${MGL_ITEST_GLES_NO_VIEWPORT_EMULATION_ENVIRONMENT}" +) diff --git a/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h b/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h index db9b4f4b..25a71d1d 100644 --- a/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h +++ b/MobileGL/MG_IntegrationTest/Harness/ScenarioFixture.h @@ -21,12 +21,49 @@ #pragma once +#include +#include +#include + #include #include "HeadlessGL.h" namespace MGITest { + // How a MOBILEGL_* quirk variable reads in THIS process's environment. + // + // A scenario that needs a non-default configuration takes it from here and skips + // when the process it was launched into is not in that configuration, rather than + // writing MG_Config::Features itself. Two reasons, and the second one decides it: + // + // - the feature table is an internal symbol. On Android this module links against + // the SHIPPING libMobileGL.so - deliberately, so the on-device run validates the + // real artifact - and that library is built -fvisibility=hidden, so nothing + // internal is reachable from here at all. + // - a quirk poked in-process is already too late for everything latched at + // initialization: the compile pool and its threads, and the backend's advertised + // extension list, which is built once from the configuration in force at first + // use. The process-wide variable is the only spelling that covers the whole + // configuration instead of the half of it that is still mutable afterwards. + // + // The reading rule is MG_ConfigLoader's, character for character (ConfigLoader.cpp, + // QueryEnvQuirkOverride / IsTruthyValue): unset is Auto - device auto-detection or a + // built-in default, i.e. a value only the implementation knows - a truthy value is + // On, and anything else that IS set ("0", "false", "") is Off. + enum class AmbientQuirk { Auto, On, Off }; + + inline AmbientQuirk AmbientQuirkFromEnvironment(const char* name) { + const char* value = std::getenv(name); + if (value == nullptr) return AmbientQuirk::Auto; + std::string lowered(value); + for (char& c : lowered) { + c = static_cast(std::tolower(static_cast(c))); + } + if (lowered.empty() || lowered == "0" || lowered == "false") return AmbientQuirk::Off; + return AmbientQuirk::On; + } + class ScenarioTest : public ::testing::Test { protected: void SetUp() override { diff --git a/MobileGL/MG_IntegrationTest/Scenarios/AsyncCompileScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/AsyncCompileScenario.cpp index 2f17555d..117b55e0 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/AsyncCompileScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/AsyncCompileScenario.cpp @@ -25,10 +25,12 @@ // be able to turn this into a red. // (b) Forcing the join afterwards produces the right answer for every one of them: // GL_COMPILE_STATUS true, an empty info log, and a program that links. -// (c) The extension string matches the configuration. This is the half a recorded -// trace can never cover - Iris and Sodium change their submission schedule the -// moment they see the string - so it is asserted against a real backend's real -// GL_EXTENSIONS, through both glGetString and glGetStringi. +// (c) The extension string matches the configuration - where "the configuration" is +// MOBILEGL_ASYNC_SHADER_COMPILE as this process inherited it, and NOT anything the +// implementation says about itself. This is the half a recorded trace can never +// cover - Iris and Sodium change their submission schedule the moment they see the +// string - so it is asserted against a real backend's real GL_EXTENSIONS, through +// both glGetString and glGetStringi. // (d) glMaxShaderCompilerThreadsKHR(0) leaves nothing in flight: every subsequent // GL_COMPLETION_STATUS_KHR reads GL_TRUE immediately, and compilation after it // is synchronous. That is what the extension requires of a zero count. @@ -40,6 +42,27 @@ // // Backend selection is the module's usual one process, one backend (MOBILEGL_BACKEND_TYPE), // so this file runs twice per ctest invocation. +// +// COMPILATION MODE IS PER PROCESS TOO. Every case here needs a particular configuration of +// MobileGL's shader compiler, and takes it from the ENVIRONMENT +// (MOBILEGL_ASYNC_SHADER_COMPILE, MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS) rather than by +// writing MG_Config::Features on the way past. Half of what those variables decide is +// latched before the first GL call - the compile pool and its threads, and the advertised +// extension list a backend builds once from the configuration in force at its first use - +// so an in-process poke could only ever have moved the other half; and on Android it could +// move nothing at all, because this module links against the shipping libMobileGL.so, which +// exports no such symbol. A case whose process is not in the configuration it needs SKIPS +// with that as its reason. CMakeLists.txt registers the extra ctest entries that put a +// process into each configuration (AsyncOn., AsyncOff., OptimisticShaderStatus.), so one +// ctest run still covers both sides of every switch. Run straight from a shell with nothing +// set - the on-device shape - the ambient configuration runs and the rest skip cleanly. +// +// WITHIN one process, "compiled on a worker" versus "compiled on this thread" is switched +// through glMaxShaderCompilerThreadsKHR, the extension's own entry point: a zero count joins +// everything outstanding and compiles inline from then on, any nonzero count lifts that +// again, and 0xFFFFFFFF asks for the implementation maximum (GL_Program.cpp, +// MaxShaderCompilerThreadsKHR_State). Doing it through the public call rather than the +// feature table means the switching is itself part of what these cases exercise. #include #include @@ -47,9 +70,6 @@ #include "../Harness/HeadlessGL.h" #include "../Harness/ScenarioFixture.h" -#include "Config.h" -#include "MG_Util/Async/ShaderCompilePool.h" - #ifdef GLAPI #undef GLAPI #endif @@ -76,8 +96,6 @@ extern "C" void glMaxShaderCompilerThreadsKHR(GLuint count); namespace MGITest { namespace { - using MobileGL::MG_Config::QuirkOverride; - // Same shape as the other scenarios: a two-attribute pass-through, so the only // thing that can differ between the two compilation modes is the compilation. constexpr const char* kVertexSource = R"(#version 330 core @@ -139,50 +157,40 @@ void main() { return source; } - // MOBILEGL_ASYNC_SHADER_COMPILE decides the ambient mode; a scenario that wants - // the other one says so here and gets the ambient one back on scope exit. Forcing - // it in-process is what lets ONE ctest run compare the two modes against each - // other - the whole point of (e). - class AsyncModeScope { - public: - explicit AsyncModeScope(bool async) : m_saved(MobileGL::MG_Config::Features.AsyncShaderCompile) { - MobileGL::MG_Config::Features.AsyncShaderCompile = - async ? QuirkOverride::ForceOn : QuirkOverride::ForceOff; + // Whether this context advertises GL_KHR_parallel_shader_compile, which is exactly + // "MobileGL is configured to compile asynchronously" as an application can see it: + // the backends gate the string on AsyncShaderCompileEnabled() and on nothing else + // (BackendObject_DirectGLES.cpp / BackendObject_DirectVulkan.cpp), and the string + // is the only way MobileGL ever tells anyone. A case that needs asynchronous + // compilation checks for it the way an application would, and skips without it. + // + // The INDEXED form, because that is the one a core-profile application reads. + bool HasParallelShaderCompile() { + GLint count = 0; + glGetIntegerv(GL_NUM_EXTENSIONS, &count); + for (GLint i = 0; i < count; ++i) { + const char* name = reinterpret_cast(glGetStringi(GL_EXTENSIONS, GLuint(i))); + if (name != nullptr && std::string(name) == "GL_KHR_parallel_shader_compile") return true; } - ~AsyncModeScope() { MobileGL::MG_Config::Features.AsyncShaderCompile = m_saved; } - AsyncModeScope(const AsyncModeScope&) = delete; - AsyncModeScope& operator=(const AsyncModeScope&) = delete; + return false; + } - private: - const QuirkOverride m_saved; - }; - - // MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS, forced in-process for the same reason - // as AsyncModeScope: one ctest run asserts the quirk against the ambient default. - class OptimisticStatusScope { - public: - explicit OptimisticStatusScope(const QuirkOverride mode) - : m_saved(MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus) { - MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = mode; - } - ~OptimisticStatusScope() { MobileGL::MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; } - OptimisticStatusScope(const OptimisticStatusScope&) = delete; - OptimisticStatusScope& operator=(const OptimisticStatusScope&) = delete; - - private: - const QuirkOverride m_saved; - }; - - // glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls - // it has to put the pool back or it changes how every scenario after it compiles. + // glMaxShaderCompilerThreadsKHR writes process-wide state; a scenario that calls it + // has to put the pool back or it changes how every scenario after it compiles. + // + // The restore is the extension's own "implementation maximum" spelling rather than a + // hand-rolled poke at the pool. glMaxShaderCompilerThreadsKHR(0xFFFFFFFF) is defined + // (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State) as precisely the two steps this + // used to perform through internal entry points - concurrency := the pool's full + // thread count, then lift any suspension a zero count had armed - in the safer order, + // since it raises the budget before re-admitting work rather than after. Going through + // the public call also puts the restore path itself under test, and it is the only + // spelling available on Android, where this module links the shipping shared library + // and can reach nothing but the GL entry points. class CompilerThreadScope { public: CompilerThreadScope() = default; - ~CompilerThreadScope() { - MobileGL::MG_Util::Async::SetAsyncShaderCompileSuspended(false); - auto& pool = MobileGL::MG_Util::Async::ShaderCompilePool::Get(); - pool.SetMaxConcurrency(pool.GetThreadCount()); - } + ~CompilerThreadScope() { glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu); } CompilerThreadScope(const CompilerThreadScope&) = delete; CompilerThreadScope& operator=(const CompilerThreadScope&) = delete; }; @@ -293,7 +301,12 @@ void main() { // interesting for shaders that (a) proved were genuinely still outstanding. TEST_F(AsyncCompileScenario, CompletionStatusPollingThenForcedJoin) { if (!Ready()) return; - const AsyncModeScope async(true); + if (!HasParallelShaderCompile()) { + GTEST_SKIP() << "this process is configured to compile inline " + "(GL_KHR_parallel_shader_compile is not advertised), so no compile can be " + "outstanding; the AsyncOn. ctest entries run this case with " + "MOBILEGL_ASYNC_SHADER_COMPILE=1"; + } const CompilerThreadScope threads; // One worker, so the queue behind it is what the poll observes. glMaxShaderCompilerThreadsKHR(1); @@ -341,14 +354,33 @@ void main() { } // ---- (c) ------------------------------------------------------------------ - // The extension string, read from a real backend that really brought a driver - // up. No mode forcing here: a backend builds its advertised list once, from the - // configuration in force at its first use, so the meaningful assertion is - // against the AMBIENT configuration - which is exactly what makes this case - // worth running in both of the suite's flag states. + // The extension string, read from a real backend that really brought a driver up. + // + // The expectation comes from the ENVIRONMENT, never from the implementation. This + // case used to derive it by calling AsyncShaderCompileEnabled() - which is the same + // function the backends gate the string on, so the two halves could only ever agree + // and the case would have passed however wrong both of them were. Asserting an + // implementation against itself pins nothing. + // + // MOBILEGL_ASYNC_SHADER_COMPILE is the whole input: the process inherited it before + // any GL call, a backend builds its advertised list once from the configuration in + // force at first use, and nothing in this process can move it afterwards. So reading + // the variable IS reading the configuration, independently. With the variable unset + // the configuration in force is MobileGL's built-in default, which only the + // implementation knows - there is nothing independent left to compare against, and + // this case says so rather than inventing an expectation. The AsyncOn. and AsyncOff. + // ctest entries pin the variable to each of its two values, so one ctest run still + // asserts both the advertised and the withdrawn side. TEST_F(AsyncCompileScenario, ExtensionStringMatchesTheConfiguration) { if (!Ready()) return; - const bool expected = MobileGL::MG_Util::Async::AsyncShaderCompileEnabled(); + const AmbientQuirk configured = AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_SHADER_COMPILE"); + if (configured == AmbientQuirk::Auto) { + GTEST_SKIP() << "MOBILEGL_ASYNC_SHADER_COMPILE is unset, so the configuration in force is " + "MobileGL's built-in default and the only way to learn it would be to ask " + "the implementation this case exists to check; the AsyncOn. and AsyncOff. " + "ctest entries run it with the variable pinned to each of its two values"; + } + const bool expected = configured == AmbientQuirk::On; const char* extensions = reinterpret_cast(glGetString(GL_EXTENSIONS)); ASSERT_NE(extensions, nullptr); @@ -385,7 +417,12 @@ void main() { // A zero count must leave nothing in flight and keep it that way. TEST_F(AsyncCompileScenario, ZeroCompilerThreadsSettlesEverythingImmediately) { if (!Ready()) return; - const AsyncModeScope async(true); + if (!HasParallelShaderCompile()) { + GTEST_SKIP() << "this process is configured to compile inline " + "(GL_KHR_parallel_shader_compile is not advertised), so a zero count has " + "nothing to settle; the AsyncOn. ctest entries run this case with " + "MOBILEGL_ASYNC_SHADER_COMPILE=1"; + } const CompilerThreadScope threads; glMaxShaderCompilerThreadsKHR(1); @@ -417,12 +454,28 @@ void main() { // Compared through the DEFAULT framebuffer deliberately: that is where the // backend's orientation and present path live, so the comparison covers the // whole pipeline rather than the reflection tables alone. + // + // The two modes are selected through glMaxShaderCompilerThreadsKHR, the extension's + // own entry point, rather than through the feature table: a zero count joins + // everything outstanding and makes every later glCompileShader/glLinkProgram run its + // body on the calling thread, and 0xFFFFFFFF lifts that again with the pool at its + // full thread count (GL_Program.cpp, MaxShaderCompilerThreadsKHR_State; the compile + // and link paths both gate on AsyncShaderCompileActive(), which is what the zero + // count switches). So this is still one process comparing worker-built artifacts + // against inline-built ones - just asked for the way an application asks. TEST_F(AsyncCompileScenario, AsyncAndSyncProgramsRenderIdenticalFrames) { if (!Ready()) return; + if (!HasParallelShaderCompile()) { + GTEST_SKIP() << "this process is configured to compile inline " + "(GL_KHR_parallel_shader_compile is not advertised), so both halves would " + "be the same inline build and the comparison would be vacuous; the " + "AsyncOn. ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1"; + } + const CompilerThreadScope threads; Image asyncImage; { - const AsyncModeScope async(true); + glMaxShaderCompilerThreadsKHR(0xFFFFFFFFu); const GLuint program = BuildProgram(); ASSERT_NE(program, 0u); asyncImage = DrawFrameWith(program); @@ -431,7 +484,7 @@ void main() { Image syncImage; { - const AsyncModeScope async(false); + glMaxShaderCompilerThreadsKHR(0); const GLuint program = BuildProgram(); ASSERT_NE(program, 0u); syncImage = DrawFrameWith(program); @@ -456,11 +509,16 @@ void main() { // candidate) shows up here and not in the single-program case above. TEST_F(AsyncCompileScenario, ABatchOfAsyncProgramsAllRenderCorrectly) { if (!Ready()) return; + if (!HasParallelShaderCompile()) { + GTEST_SKIP() << "this process is configured to compile inline " + "(GL_KHR_parallel_shader_compile is not advertised), so nothing would be " + "built on a worker and there is no per-worker state to leak; the AsyncOn. " + "ctest entries run this case with MOBILEGL_ASYNC_SHADER_COMPILE=1"; + } constexpr int kPrograms = 12; std::vector programs; { - const AsyncModeScope async(true); const CompilerThreadScope threads; glMaxShaderCompilerThreadsKHR(1); // Everything enqueued before anything is read: the only shape in which @@ -489,6 +547,21 @@ void main() { // then mis-renders - shows up here as a wrong quadrant signature. TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) { if (!Ready()) return; + // The quirk is off by default and never advertised, so unlike the cases above + // there is no GL observable that says whether it is in force - only the variable + // that put it there. It also has to be set BEFORE this process started for the + // shape to be the real one: the optimistic answer is latched per compile, and a + // quirk switched on mid-process would only cover the compiles after it. + if (AmbientQuirkFromEnvironment("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS") != AmbientQuirk::On) { + GTEST_SKIP() << "this case is the optimistic-status quirk's end-to-end shape and needs it on " + "for the whole process; the OptimisticShaderStatus. ctest entries run it with " + "MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS=1"; + } + if (!HasParallelShaderCompile()) { + GTEST_SKIP() << "the optimistic status only ever applies to a compile that is still in flight " + "(OptimisticShaderStatusActive() requires AsyncShaderCompileActive()), and " + "this process is configured to compile inline"; + } constexpr int kPrograms = 12; // Distinct per program (so neither the source memo nor the adoption map turns @@ -508,8 +581,6 @@ void main() { std::vector programs; { - const AsyncModeScope async(true); - const OptimisticStatusScope quirk(QuirkOverride::ForceOn); const CompilerThreadScope threads; glMaxShaderCompilerThreadsKHR(1); diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp index 18f0077b..6670c5d9 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/ViewportArrayScenario.cpp @@ -55,10 +55,6 @@ #include "../Harness/HeadlessGL.h" #include "../Harness/ScenarioFixture.h" -// For the emulation switch the negative-control case below flips. Nothing else in this file needs -// to know which backend it is running on. -#include - #ifdef GLAPI #undef GLAPI #endif @@ -529,7 +525,8 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); } // // Everything above is a claim about pixels, and a claim about pixels cannot tell an // emulation that works from a backend that was going to be right anyway. This case builds - // the SAME program with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION off and requires case 1's + // the SAME program in a process started with MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0 + // (the NoViewportArrayEmulation. ctest entry) and requires case 1's // result to COLLAPSE: with no routing, every geometry invocation rasterizes against // viewport 0's rectangle, so the last invocation paints the whole surface and every cell // reads 15 instead of its own index. That is the pre-emulation behaviour this backend had @@ -544,25 +541,31 @@ void main() { fragColor = vec4(float(gsIndex) * 16.0 / 255.0, 0.0, 0.0, 1.0); } "gl_ViewportIndex natively and ignores it"; } - // The feature table is a process-global and this fixture shares its context with every - // other scenario in the process, so the restore is not optional. - struct ScopedEmulationOff { - ScopedEmulationOff(): saved(MobileGL::MG_Config::Features.ViewportArrayEmulation) { - MobileGL::MG_Config::Features.ViewportArrayEmulation = - MobileGL::MG_Config::QuirkOverride::ForceOff; - } - ~ScopedEmulationOff() { MobileGL::MG_Config::Features.ViewportArrayEmulation = saved; } - MobileGL::MG_Config::QuirkOverride saved; - }; + // The switch comes from the ENVIRONMENT, and this case runs only in a process that + // was started with it off. It used to write MG_Config::Features directly, which is + // not available to it any more: on Android this module links the shipping + // libMobileGL.so - so that the on-device run validates the real artifact - and that + // library exports no such symbol. The process-wide variable is also the more honest + // spelling of the control, since it is the one a developer chasing this failure + // would actually set. CMakeLists.txt registers the NoViewportArrayEmulation. ctest + // entry for it, so the control still runs in every ctest run; anywhere else - the + // ambient ctest entries, or the binary run straight from a device shell - the + // emulation is on and this case skips. + if (AmbientQuirkFromEnvironment("MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION") != AmbientQuirk::Off) { + GTEST_SKIP() << "this is the negative control for the emulation and needs it off for the " + "whole process; the NoViewportArrayEmulation. ctest entry runs it with " + "MOBILEGL_FORCE_VIEWPORT_ARRAY_EMULATION=0"; + } IntTarget target = MakeIntTarget(kSurfaceSide, kSurfaceSide); SetupGridViewports(kCellSize, kCellSize); GLuint unroutedProgram = 0; { - const ScopedEmulationOff scopedEmulationOff; - // A FRESH program: the emitted ESSL is decided at link time and memoized on a key - // that carries this flag, so reusing m_program would just replay the routed build. + // A program of its own rather than the fixture's, even though in this process + // the fixture's was built unrouted too: the emitted ESSL is decided at link + // time and memoized on a key that carries this flag, and building it here keeps + // what this case measures independent of when SetUp happened to link. unroutedProgram = BuildProgram(kGridGeometrySource, kIntFragmentSource); ASSERT_NE(unroutedProgram, 0u) << "unrouted program failed to build: " << m_buildLog; glUseProgram(unroutedProgram);