[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.
This commit is contained in:
2026-08-25 04:52:14 -04:00
parent b12ef4d717
commit 7f2ca68615
4 changed files with 321 additions and 79 deletions
+131
View File
@@ -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}"
)
@@ -21,12 +21,49 @@
#pragma once
#include <cctype>
#include <cstdlib>
#include <string>
#include <gtest/gtest.h>
#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<char>(std::tolower(static_cast<unsigned char>(c)));
}
if (lowered.empty() || lowered == "0" || lowered == "false") return AmbientQuirk::Off;
return AmbientQuirk::On;
}
class ScenarioTest : public ::testing::Test {
protected:
void SetUp() override {
@@ -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 <string>
#include <vector>
@@ -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<const char*>(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<const char*>(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<GLuint> 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<GLuint> programs;
{
const AsyncModeScope async(true);
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
const CompilerThreadScope threads;
glMaxShaderCompilerThreadsKHR(1);
@@ -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 <Config.h>
#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);