mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
565 lines
25 KiB
C++
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),
|
|
E_GL_KHR_parallel_shader_compile));
|
|
EXPECT_TRUE(Advertises(MG_Backend::DirectVulkan::BuildAdvertisedExtensions(false, false, false),
|
|
E_GL_KHR_parallel_shader_compile));
|
|
}
|
|
{
|
|
const AsyncModeScope async(false);
|
|
EXPECT_FALSE(Advertises(MG_Backend::DirectGLES::BuildAdvertisedExtensions(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),
|
|
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);
|
|
}
|