Files
MobileGL/MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp
T
swung0x48 10ff5e2b18 [Fix, Test] (DirectGLES, DirectVulkan): advertise indirect draw capabilities accurately
- advertise GL_ARB_draw_indirect when supported
- gate GL_ARB_base_instance on complete non-zero firstInstance semantics
- synchronize Driver POST reporting
- add capability and extension-advertisement regression tests
2026-08-15 06:30:44 -04:00

565 lines
25 KiB
C++

// MobileGL - MobileGL/MG_Test/Program/ParallelShaderCompileTest.cpp
// Copyright (c) 2025-2026 MobileGL-Dev
// Licensed under the GNU Lesser General Public License v3.0:
// https://www.gnu.org/licenses/gpl-3.0.txt
// https://www.gnu.org/licenses/lgpl-3.0.txt
// SPDX-License-Identifier: LGPL-3.0-only
// End of Source File Header
// P1 stage 5: the GL_KHR_parallel_shader_compile application surface.
//
// Four things are under test, and they are the four an application actually touches:
// * GL_COMPLETION_STATUS_KHR on shaders and programs, which MUST NOT JOIN - the whole
// point of the query is to answer while the work is still outstanding;
// * glMaxShaderCompilerThreadsKHR / ...ARB, including the count == 0 mode switch the
// extension mandates and what lifts it again;
// * GL_MAX_SHADER_COMPILER_THREADS_KHR;
// * the extension string itself, which must appear if and only if asynchronous
// compilation is enabled - the kill switch has to revert the application-visible
// behaviour change, not only the threading.
//
// Like the other async suites, every case drives the real GL entry points and flips
// MG_Config::Features.AsyncShaderCompile itself, so the file behaves identically whether or
// not the suite was launched with MOBILEGL_ASYNC_SHADER_COMPILE=1.
#include <gtest/gtest.h>
#include <algorithm>
#include <string>
#include <vector>
#include "Config.h"
#include "Includes.h"
#include "Init.h"
#include "MG_Backend/BackendObjects.h"
#include "MG_Backend/DirectGLES/BackendObject_DirectGLES.h"
#include "MG_Backend/DirectVulkan/BackendObject_DirectVulkan.h"
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_State/GLState/Core.h"
#include "MG_Util/Async/ShaderCompilePool.h"
using namespace MobileGL;
using namespace MobileGL::MG_Impl::GLImpl;
namespace {
class AsyncModeScope {
public:
explicit AsyncModeScope(const Bool async) : m_saved(MG_Config::Features.AsyncShaderCompile) {
MG_Config::Features.AsyncShaderCompile =
async ? MG_Config::QuirkOverride::ForceOn : MG_Config::QuirkOverride::ForceOff;
}
~AsyncModeScope() { MG_Config::Features.AsyncShaderCompile = m_saved; }
AsyncModeScope(const AsyncModeScope&) = delete;
AsyncModeScope& operator=(const AsyncModeScope&) = delete;
private:
const MG_Config::QuirkOverride m_saved;
};
// glMaxShaderCompilerThreadsKHR writes PROCESS-wide state (the pool's concurrency budget
// and the suspension latch), so a case that touches it has to put both back or it
// poisons every case declared after it in this binary.
class CompilerThreadScope {
public:
CompilerThreadScope() = default;
~CompilerThreadScope() {
MG_Util::Async::SetAsyncShaderCompileSuspended(false);
MG_Util::Async::ShaderCompilePool::Get().SetMaxConcurrency(
MG_Util::Async::ShaderCompilePool::Get().GetThreadCount());
}
CompilerThreadScope(const CompilerThreadScope&) = delete;
CompilerThreadScope& operator=(const CompilerThreadScope&) = delete;
};
const char* kVs = R"(#version 460
layout(location = 0) in vec3 aPos;
uniform vec4 uColor;
out vec4 vColor;
void main() {
vColor = uColor;
gl_Position = vec4(aPos, 1.0);
}
)";
// Deliberately expensive, and distinct per index so the source-hash memo never turns a
// second instance into a no-op: a saturated pool is the only way to observe an
// outstanding job without asserting on timing.
String MakeBulkySource(const int index) {
String source = "#version 460\nlayout(location = 0) out vec4 fragColor;\n";
source += "uniform float uSeed" + std::to_string(index) + ";\n";
source += "void main() {\n float acc = uSeed" + std::to_string(index) + ";\n";
for (int i = 0; i < 320; ++i) {
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
}
source += " fragColor = vec4(acc, acc, acc, 1.0);\n}\n";
return source;
}
GLuint MakeShader(const GLenum type, const char* source) {
const GLuint shader = CreateShader(type);
ShaderSource(shader, 1, &source, nullptr);
return shader;
}
GLint QueryShaderCompletion(const GLuint shader) {
GLint status = -1;
GetShaderiv(shader, GL_COMPLETION_STATUS_KHR, &status);
return status;
}
GLint QueryProgramCompletion(const GLuint program) {
GLint status = -1;
GetProgramiv(program, GL_COMPLETION_STATUS_KHR, &status);
return status;
}
GLint QueryCompileStatus(const GLuint shader) {
GLint status = GL_FALSE;
GetShaderiv(shader, GL_COMPILE_STATUS, &status);
return status;
}
GLint QueryLinkStatus(const GLuint program) {
GLint status = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &status);
return status;
}
// Enqueues `count` distinct heavy compiles and returns their names without reading
// anything back, leaving the pool with a real backlog.
Vector<GLuint> EnqueueBacklog(const int count, const int seedBase, Vector<String>& sourceStorage) {
Vector<GLuint> shaders;
shaders.reserve(static_cast<SizeT>(count));
for (int i = 0; i < count; ++i) {
sourceStorage.push_back(MakeBulkySource(seedBase + i));
const char* text = sourceStorage.back().c_str();
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &text, nullptr);
CompileShader(fs);
shaders.push_back(fs);
}
return shaders;
}
Bool Advertises(const Vector<GLExtension>& extensions, const GLExtension wanted) {
return std::find(extensions.begin(), extensions.end(), wanted) != extensions.end();
}
class ParallelShaderCompileTest : public ::testing::Test {
protected:
void SetUp() override { MobileGL::Initialize(); }
};
} // namespace
// ---------------------------------------------------------------------------------------
// GL_COMPLETION_STATUS_KHR must not join
// ---------------------------------------------------------------------------------------
// The load-bearing case of the whole stage. A single-worker pool is saturated with heavy
// compiles, so jobs are demonstrably still queued; GL_COMPLETION_STATUS_KHR then has to
// report GL_FALSE for at least one of them *and leave it outstanding*. If the query joined -
// which is what happens if it is ever routed through the ordinary Compiled() gate - it could
// only ever return GL_TRUE, and the extension would be a lie that costs an application the
// exact stall it added the polling loop to avoid.
//
// Skipped rather than failed when the machine drained the backlog first, so it can never be
// a false red on a fast box.
TEST_F(ParallelShaderCompileTest, ShaderCompletionStatusReportsFalseWithoutJoining) {
const AsyncModeScope async(true);
const CompilerThreadScope threads;
// One worker: the queue behind it is the thing being observed.
MaxShaderCompilerThreadsKHR(1);
Vector<String> sources;
const Vector<GLuint> shaders = EnqueueBacklog(64, 4000, sources);
int outstanding = 0;
for (const GLuint shader : shaders) {
const GLint completion = QueryShaderCompletion(shader);
ASSERT_TRUE(completion == GL_TRUE || completion == GL_FALSE) << "completion = " << completion;
if (completion == GL_FALSE) ++outstanding;
}
if (outstanding == 0) {
GTEST_SKIP() << "the pool drained 64 heavy compiles before the first query; nothing outstanding to observe";
}
// Asking again must still not have settled anything: the query is a peek, so a second
// one cannot have made progress happen. (A joining implementation would report every
// shader complete by now.)
int stillOutstanding = 0;
for (const GLuint shader : shaders) {
if (QueryShaderCompletion(shader) == GL_FALSE) ++stillOutstanding;
}
EXPECT_GT(stillOutstanding, 0) << "GL_COMPLETION_STATUS_KHR joined - every shader settled just by being asked";
// And once the real (joining) query is used, everything is complete and correct.
for (const GLuint shader : shaders) {
EXPECT_EQ(QueryCompileStatus(shader), GL_TRUE);
EXPECT_EQ(QueryShaderCompletion(shader), GL_TRUE) << "GL_COMPILE_STATUS must have joined";
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// The program half: a link enqueued behind a saturated pool cannot be complete either, and
// asking must not drag it forward.
TEST_F(ParallelShaderCompileTest, ProgramCompletionStatusReportsFalseWithoutJoining) {
const AsyncModeScope async(true);
const CompilerThreadScope threads;
MaxShaderCompilerThreadsKHR(1);
Vector<String> sources;
EnqueueBacklog(48, 4200, sources);
Vector<GLuint> programs;
for (int i = 0; i < 8; ++i) {
sources.push_back(MakeBulkySource(4400 + i));
const char* text = sources.back().c_str();
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &text, nullptr);
CompileShader(fs);
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
CompileShader(vs);
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
programs.push_back(program);
}
int outstanding = 0;
for (const GLuint program : programs) {
const GLint completion = QueryProgramCompletion(program);
ASSERT_TRUE(completion == GL_TRUE || completion == GL_FALSE) << "completion = " << completion;
if (completion == GL_FALSE) ++outstanding;
}
if (outstanding == 0) {
GTEST_SKIP() << "the pool drained the whole backlog before the first query; nothing outstanding to observe";
}
for (const GLuint program : programs) {
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
// GL_COMPLETION_STATUS_KHR spans BOTH phases of a link, so reading GL_LINK_STATUS -
// which is answered out of phase A - is no longer enough to turn it GL_TRUE. That is
// deliberate: an application that polls completion and then draws must not be told
// "done" while the SPIR-V is still being generated, or the draw it was cleared for is
// the thing that blocks. Settling both phases is what makes the query true.
const auto& object = MG_State::pGLContext->GetProgramObject(program);
ASSERT_NE(object, nullptr);
object->JoinLinkAndSpirv();
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE) << "a full join must have settled both phases";
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// "Nothing outstanding" is the answer for an object that was never compiled or linked at
// all: the query asks whether work is pending, not whether work ever happened.
TEST_F(ParallelShaderCompileTest, CompletionStatusIsTrueForUntouchedObjects) {
const AsyncModeScope async(true);
const GLuint shader = MakeShader(GL_FRAGMENT_SHADER, "#version 460\nvoid main() {}\n");
const GLuint program = CreateProgram();
EXPECT_EQ(QueryShaderCompletion(shader), GL_TRUE);
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// With the flag off nothing is ever in flight, so the query is constant GL_TRUE - and, just
// as importantly, still a recognized pname rather than a GL_INVALID_ENUM.
TEST_F(ParallelShaderCompileTest, CompletionStatusIsAlwaysTrueWithAsyncOff) {
const AsyncModeScope async(false);
Vector<String> sources;
const Vector<GLuint> shaders = EnqueueBacklog(8, 4600, sources);
for (const GLuint shader : shaders) {
EXPECT_EQ(QueryShaderCompletion(shader), GL_TRUE);
}
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
CompileShader(vs);
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, shaders.front());
LinkProgram(program);
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// The pname is new; the rejection of everything else must be untouched.
TEST_F(ParallelShaderCompileTest, UnknownPnamesStillRaiseInvalidEnum) {
const GLuint shader = MakeShader(GL_FRAGMENT_SHADER, "#version 460\nvoid main() {}\n");
const GLuint program = CreateProgram();
GLint value = 0;
GetShaderiv(shader, GL_TEXTURE_2D, &value);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
GetProgramiv(program, GL_TEXTURE_2D, &value);
EXPECT_EQ(GetError(), GL_INVALID_ENUM);
}
// ---------------------------------------------------------------------------------------
// glMaxShaderCompilerThreadsKHR / ...ARB
// ---------------------------------------------------------------------------------------
// count == 0 is the mode switch the extension defines: no compiler threads. Two obligations
// follow, and both are asserted here - everything already in flight is settled by the time
// the call returns (so every GL_COMPLETION_STATUS_KHR reads GL_TRUE straight away), and
// compilation that happens AFTERWARDS is synchronous too.
TEST_F(ParallelShaderCompileTest, ZeroCompilerThreadsJoinsEverythingAndCompilesInline) {
const AsyncModeScope async(true);
const CompilerThreadScope threads;
MaxShaderCompilerThreadsKHR(1);
Vector<String> sources;
const Vector<GLuint> backlog = EnqueueBacklog(48, 4800, sources);
MaxShaderCompilerThreadsKHR(0);
EXPECT_TRUE(MG_Util::Async::IsAsyncShaderCompileSuspended());
EXPECT_FALSE(MG_Util::Async::AsyncShaderCompileActive());
// The configuration flag itself is untouched: the extension is still advertised, the
// application just asked for serial compilation.
EXPECT_TRUE(MG_Util::Async::AsyncShaderCompileEnabled());
for (const GLuint shader : backlog) {
EXPECT_EQ(QueryShaderCompletion(shader), GL_TRUE)
<< "glMaxShaderCompilerThreadsKHR(0) must leave nothing in flight";
EXPECT_EQ(QueryCompileStatus(shader), GL_TRUE);
}
// Anything compiled from here on is finished before its glCompileShader returns.
Vector<String> serialSources;
const Vector<GLuint> serial = EnqueueBacklog(6, 4900, serialSources);
for (const GLuint shader : serial) {
EXPECT_EQ(QueryShaderCompletion(shader), GL_TRUE) << "a compile after a zero count must be synchronous";
}
// Links too, not just compiles.
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
CompileShader(vs);
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, serial.front());
LinkProgram(program);
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE) << "a link after a zero count must be synchronous";
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// The same obligation, but for LINKS that are already in flight when the zero count arrives -
// and specifically for BOTH phases of one. A link is two chained jobs now (ProgramLinkTask,
// then ProgramSpirvTask), and GL_COMPLETION_STATUS_KHR spans both, so
// ProgramState::JoinAllPendingWork has to settle both or this query reads GL_FALSE in the one
// mode the extension says cannot have anything pending. The case above creates its program
// AFTER the zero count, so it links inline and cannot see this; here the programs are linked
// against a saturated pool BEFORE it.
TEST_F(ParallelShaderCompileTest, ZeroCompilerThreadsJoinsPendingLinksAndTheirSpirvJobs) {
const AsyncModeScope async(true);
const CompilerThreadScope threads;
MaxShaderCompilerThreadsKHR(1);
// A backlog first, so the links below cannot all drain before the zero count lands.
Vector<String> sources;
(void)EnqueueBacklog(24, 5000, sources);
Vector<GLuint> programs;
for (int i = 0; i < 8; ++i) {
sources.push_back(MakeBulkySource(5100 + i));
const char* text = sources.back().c_str();
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
ShaderSource(fs, 1, &text, nullptr);
CompileShader(fs);
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
CompileShader(vs); // this file's MakeShader only sources; it does not compile
const GLuint program = CreateProgram();
AttachShader(program, vs);
AttachShader(program, fs);
LinkProgram(program);
programs.push_back(program);
}
int outstanding = 0;
for (const GLuint program : programs) {
if (QueryProgramCompletion(program) == GL_FALSE) ++outstanding;
}
MaxShaderCompilerThreadsKHR(0);
for (const GLuint program : programs) {
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE)
<< "glMaxShaderCompilerThreadsKHR(0) must leave neither link phase in flight";
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
}
EXPECT_GT(outstanding, 0) << "every link had drained before the zero count; this case proved nothing";
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ...and a later NONZERO count is what lifts it. Nothing else does: not a new context, not a
// join, not eglInitialize. That is the documented contract, so it gets an assertion.
TEST_F(ParallelShaderCompileTest, NonzeroCompilerThreadsRestoresAsynchronousCompilation) {
const AsyncModeScope async(true);
const CompilerThreadScope threads;
MaxShaderCompilerThreadsKHR(0);
ASSERT_TRUE(MG_Util::Async::IsAsyncShaderCompileSuspended());
// Re-initializing must NOT quietly re-arm it - the application asked for serial
// compilation and has not taken that back.
MobileGL::Initialize();
EXPECT_TRUE(MG_Util::Async::IsAsyncShaderCompileSuspended());
MaxShaderCompilerThreadsKHR(4);
EXPECT_FALSE(MG_Util::Async::IsAsyncShaderCompileSuspended());
EXPECT_TRUE(MG_Util::Async::AsyncShaderCompileActive());
// And work really is being enqueued again: with the budget back at one worker a heavy
// backlog leaves something outstanding (skip-not-fail if the box drained it first).
MaxShaderCompilerThreadsKHR(1);
Vector<String> sources;
const Vector<GLuint> shaders = EnqueueBacklog(64, 5000, sources);
const Bool anyOutstanding = std::any_of(shaders.begin(), shaders.end(), [](const GLuint shader) {
return QueryShaderCompletion(shader) == GL_FALSE;
});
if (!anyOutstanding) {
GTEST_SKIP() << "the pool drained the backlog before the first query; asynchrony not observable here";
}
for (const GLuint shader : shaders) {
EXPECT_EQ(QueryCompileStatus(shader), GL_TRUE);
}
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// The three count cases map onto the pool's concurrency budget: a request above the thread
// count cannot conjure threads, 0xFFFFFFFF means "implementation maximum", and an ordinary
// value is taken as given (clamped to at least one).
TEST_F(ParallelShaderCompileTest, CompilerThreadCountIsClampedToTheThreadCount) {
const AsyncModeScope async(true);
const CompilerThreadScope threads;
auto& pool = MG_Util::Async::ShaderCompilePool::Get();
const Uint threadCount = pool.GetThreadCount();
ASSERT_GE(threadCount, 1u);
MaxShaderCompilerThreadsKHR(1);
EXPECT_EQ(pool.GetMaxConcurrency(), 1u);
MaxShaderCompilerThreadsKHR(threadCount + 1000);
EXPECT_EQ(pool.GetMaxConcurrency(), threadCount) << "asking for more threads than exist cannot create any";
MaxShaderCompilerThreadsKHR(1);
ASSERT_EQ(pool.GetMaxConcurrency(), 1u);
MaxShaderCompilerThreadsKHR(0xFFFFFFFFu);
EXPECT_EQ(pool.GetMaxConcurrency(), threadCount) << "0xFFFFFFFF is the implementation maximum";
// The ARB spelling is the same entry point, not a second piece of state.
MaxShaderCompilerThreadsARB(1);
EXPECT_EQ(pool.GetMaxConcurrency(), 1u);
MaxShaderCompilerThreadsARB(0);
EXPECT_TRUE(MG_Util::Async::IsAsyncShaderCompileSuspended());
MaxShaderCompilerThreadsKHR(threadCount);
EXPECT_FALSE(MG_Util::Async::IsAsyncShaderCompileSuspended())
<< "the KHR and ARB names must share one piece of state";
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// A zero count with the feature switched off is legal and does nothing observable: there is
// nothing to suspend, and the call must not fail just because MobileGL never had threads.
TEST_F(ParallelShaderCompileTest, CompilerThreadCallsAreHarmlessWithAsyncOff) {
const AsyncModeScope async(false);
const CompilerThreadScope threads;
MaxShaderCompilerThreadsKHR(0);
MaxShaderCompilerThreadsKHR(8);
MaxShaderCompilerThreadsARB(0xFFFFFFFFu);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------------------
// GL_MAX_SHADER_COMPILER_THREADS_KHR
// ---------------------------------------------------------------------------------------
TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsGetter) {
{
const AsyncModeScope async(true);
GLint value = -1;
GetIntegerv(GL_MAX_SHADER_COMPILER_THREADS_KHR, &value);
EXPECT_EQ(value, static_cast<GLint>(MG_Util::Async::ShaderCompilePool::Get().GetThreadCount()));
EXPECT_GE(value, 1);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
{
// No compiler threads exist in this configuration, and the extension is not
// advertised either, so zero is the honest answer.
const AsyncModeScope async(false);
GLint value = -1;
GetIntegerv(GL_MAX_SHADER_COMPILER_THREADS_KHR, &value);
EXPECT_EQ(value, 0);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
}
// The reported maximum is the pool's THREAD count, not its current concurrency budget: an
// application that lowered the budget still wants to know what the implementation can do.
TEST_F(ParallelShaderCompileTest, MaxShaderCompilerThreadsIgnoresTheCurrentBudget) {
const AsyncModeScope async(true);
const CompilerThreadScope threads;
const Uint threadCount = MG_Util::Async::ShaderCompilePool::Get().GetThreadCount();
MaxShaderCompilerThreadsKHR(1);
GLint value = -1;
GetIntegerv(GL_MAX_SHADER_COMPILER_THREADS_KHR, &value);
EXPECT_EQ(value, static_cast<GLint>(threadCount));
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------------------
// The extension string
// ---------------------------------------------------------------------------------------
// The advertisement is the riskiest half of P1 - a recorded trace cannot cover it, because
// Iris and Sodium change their submission schedule the moment they see the string - so
// MOBILEGL_ASYNC_SHADER_COMPILE=0 has to withdraw it. Asserted on both backends' own
// BuildAdvertisedExtensions, which is the single source of truth each of them (and the
// driver POST) builds the list from.
TEST_F(ParallelShaderCompileTest, BothBackendsAdvertiseTheExtensionIffAsyncIsEnabled) {
{
const AsyncModeScope async(true);
EXPECT_TRUE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
E_GL_KHR_parallel_shader_compile));
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
E_GL_KHR_parallel_shader_compile));
}
{
const AsyncModeScope async(false);
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(false, false, false, false),
E_GL_KHR_parallel_shader_compile))
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
EXPECT_FALSE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false, false),
E_GL_KHR_parallel_shader_compile))
<< "MOBILEGL_ASYNC_SHADER_COMPILE=0 must withdraw the extension, not only the threading";
}
}
// The same fact through the GL surface an application actually reads. No flag flipping here:
// a backend's advertised list is built once, at its first use, from the configuration that
// was in force then - so this case asserts against the AMBIENT configuration, which is
// exactly what makes it meaningful in both of the suite's two runs (with and without
// MOBILEGL_ASYNC_SHADER_COMPILE=1 exported).
TEST_F(ParallelShaderCompileTest, GLExtensionStringTracksTheAmbientConfiguration) {
UniquePtr<MG_Backend::BackendObject> previousBackend = Move(MG_Backend::pActiveBackendObject);
MG_Backend::pActiveBackendObject = MakeUnique<MG_Backend::DirectGLES::BackendObject_DirectGLES>();
const char* extensions = reinterpret_cast<const char*>(GetString(GL_EXTENSIONS));
ASSERT_NE(extensions, nullptr);
const String extensionString(extensions);
const Bool advertised = extensionString.find("GL_KHR_parallel_shader_compile") != String::npos;
EXPECT_EQ(advertised, MG_Util::Async::AsyncShaderCompileEnabled()) << "GL_EXTENSIONS = " << extensionString;
// glGetStringi must agree with the monolithic string - LWJGL builds GLCapabilities from
// the indexed form on a core profile.
GLint count = 0;
GetIntegerv(GL_NUM_EXTENSIONS, &count);
ASSERT_GT(count, 0);
Bool foundIndexed = false;
for (GLint i = 0; i < count; ++i) {
const char* name = reinterpret_cast<const char*>(GetStringi(GL_EXTENSIONS, static_cast<GLuint>(i)));
if (name != nullptr && std::string(name) == "GL_KHR_parallel_shader_compile") foundIndexed = true;
}
EXPECT_EQ(foundIndexed, advertised);
MG_Backend::pActiveBackendObject = Move(previousBackend);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}