mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user