mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
[Feat, Test] (MG_Impl, MG_State, MG_Util): opt-in MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS - shader compile status/log answer without joining, latched per compile
This commit is contained in:
@@ -136,6 +136,19 @@ namespace MobileGL::MG_Config {
|
||||
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
|
||||
// auto, which is min(4, big cores); an explicit value is honoured as given.
|
||||
Uint32 AsyncShaderCompileThreads = 0;
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is still in flight,
|
||||
// glGetShaderiv(GL_COMPILE_STATUS) answers GL_TRUE and the shader info log reads
|
||||
// empty, WITHOUT joining the job (latched per compile - see
|
||||
// ShaderObject::TakeOptimisticCompileAnswer). A deliberate, bounded spec violation:
|
||||
// a real failure still fails the program link with the compile log quoted. It
|
||||
// exists for applications that compile hundreds of shaders serially and read the
|
||||
// status right after each glCompileShader - Iris's shader-pack load - where those
|
||||
// per-shader joins are what serializes the batch on its main path (Iris's gbuffer
|
||||
// phase issues no program-level query between programs; program-level LINK_STATUS
|
||||
// and the program info log still join truthfully, so paths that check each link
|
||||
// immediately stay serial by their own construction). Off by default; never
|
||||
// advertise it.
|
||||
QuirkOverride AsyncOptimisticShaderStatus = QuirkOverride::Auto;
|
||||
};
|
||||
extern FeaturesTable Features;
|
||||
} // namespace MobileGL::MG_Config
|
||||
|
||||
@@ -183,6 +183,8 @@ namespace MobileGL::MG_ConfigLoader {
|
||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||
features.AsyncOptimisticShaderStatus =
|
||||
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||
}
|
||||
|
||||
inline void InitBackendType() {
|
||||
|
||||
@@ -744,6 +744,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
||||
}
|
||||
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while the compile job is still in flight -
|
||||
// and, via the latch below, for the rest of that node's life once any query was
|
||||
// answered this way - GL_COMPILE_STATUS reads GL_TRUE and the info log reads empty,
|
||||
// WITHOUT joining. The latch (TakeOptimisticCompileAnswer) is what makes the three
|
||||
// sites tell ONE story: without it, a job settling between an application's info-log
|
||||
// read and its status read would produce the torn pair "GL_FALSE with an empty log",
|
||||
// and an application that aborts on that never reaches the link join that carries the
|
||||
// real diagnostic. A failure hidden here still fails the program link, with the
|
||||
// compile log quoted in the program info log (ProgramLinkTask::ConsumeShaders), which
|
||||
// is where the serial compile-then-check applications this exists for do their error
|
||||
// handling.
|
||||
static Bool AnswerCompileOptimistically(const SharedPtr<MG_State::GLState::ShaderObject>& shaderObject) {
|
||||
return MG_Util::Async::OptimisticShaderStatusActive() && shaderObject->TakeOptimisticCompileAnswer();
|
||||
}
|
||||
|
||||
void GetShaderiv_State(GLuint shader, GLenum pname, GLint* params) {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
@@ -756,9 +771,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
*params = shaderObject->GetDeleteStatus();
|
||||
break;
|
||||
case GL_COMPILE_STATUS:
|
||||
if (AnswerCompileOptimistically(shaderObject)) {
|
||||
*params = GL_TRUE;
|
||||
break;
|
||||
}
|
||||
*params = shaderObject->GetCompileStatus();
|
||||
break;
|
||||
case GL_INFO_LOG_LENGTH:
|
||||
// Not cosmetic: LWJGL's one-argument glGetShaderInfoLog convenience overload
|
||||
// sizes its buffer from this query, so a joining answer here would defeat the
|
||||
// non-joining GetShaderInfoLog below.
|
||||
if (AnswerCompileOptimistically(shaderObject)) {
|
||||
*params = 0;
|
||||
break;
|
||||
}
|
||||
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
|
||||
break;
|
||||
case GL_SHADER_SOURCE_LENGTH:
|
||||
@@ -784,6 +810,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
||||
auto& shaderObject = TryToGetShaderObject(shader);
|
||||
if (!shaderObject) return;
|
||||
|
||||
// See AnswerCompileOptimistically: an in-flight compile reads as an empty log. The
|
||||
// cost is a lost compile WARNING (a successful compile whose log the application
|
||||
// reads exactly once, now, and never after the join) - accepted as part of the
|
||||
// opt-in.
|
||||
if (AnswerCompileOptimistically(shaderObject)) {
|
||||
CopyStr(bufSize, length, infoLog, "", 0);
|
||||
return;
|
||||
}
|
||||
|
||||
const auto& log = shaderObject->GetInfoLog();
|
||||
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
||||
}
|
||||
|
||||
@@ -157,6 +157,22 @@ void main() {
|
||||
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.
|
||||
class CompilerThreadScope {
|
||||
@@ -463,5 +479,81 @@ void main() {
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
// The Iris two-phase shape end to end on a real driver, with the optimistic-status
|
||||
// quirk on: phase 1 compiles each stage and reads its log then its status (both
|
||||
// answered optimistically), links, detaches and deletes the shaders for every
|
||||
// program with no program-level read anywhere; phase 2 then checks every link and
|
||||
// draws every program. Deliberately NOT built on the harness CompileProgram(),
|
||||
// whose status read would join and collapse the phase-1 overlap this exists to
|
||||
// exercise. What the unit suite cannot see - worker-produced artifacts the backend
|
||||
// then mis-renders - shows up here as a wrong quadrant signature.
|
||||
TEST_F(AsyncCompileScenario, IrisShapedTwoPhaseBatchRendersCorrectly) {
|
||||
if (!Ready()) return;
|
||||
constexpr int kPrograms = 12;
|
||||
|
||||
// Distinct per program (so neither the source memo nor the adoption map turns
|
||||
// a compile into a no-op) but a pure pass-through at runtime: the bulk sits in
|
||||
// a branch a zero-initialised uniform never takes.
|
||||
const auto fragmentSource = [](const int index) {
|
||||
std::string source = "#version 330 core\nin vec3 vColor;\nout vec4 oColor;\n";
|
||||
source += "uniform float uGate" + std::to_string(index) + ";\n";
|
||||
source += "void main() {\n oColor = vec4(vColor, 1.0);\n";
|
||||
source += " if (uGate" + std::to_string(index) + " > 1e30) {\n float acc = 1.0;\n";
|
||||
for (int i = 0; i < 60; ++i) {
|
||||
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0);\n";
|
||||
}
|
||||
source += " oColor = vec4(acc);\n }\n}\n";
|
||||
return source;
|
||||
};
|
||||
|
||||
std::vector<GLuint> programs;
|
||||
{
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(QuirkOverride::ForceOn);
|
||||
const CompilerThreadScope threads;
|
||||
glMaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
m_sources.push_back(fragmentSource(i));
|
||||
const char* fsText = m_sources.back().c_str();
|
||||
|
||||
const GLuint vs = glCreateShader(GL_VERTEX_SHADER);
|
||||
glShaderSource(vs, 1, &kVertexSource, nullptr);
|
||||
glCompileShader(vs);
|
||||
(void)ShaderInfoLog(vs); // Iris's exact order: the log first...
|
||||
(void)ShaderCompileStatus(vs); // ...then the status; both optimistic.
|
||||
|
||||
const GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
|
||||
glShaderSource(fs, 1, &fsText, nullptr);
|
||||
glCompileShader(fs);
|
||||
(void)ShaderInfoLog(fs);
|
||||
(void)ShaderCompileStatus(fs);
|
||||
|
||||
const GLuint program = glCreateProgram();
|
||||
glAttachShader(program, vs);
|
||||
glAttachShader(program, fs);
|
||||
glBindAttribLocation(program, 0, "aPos");
|
||||
glBindAttribLocation(program, 1, "aColor");
|
||||
glLinkProgram(program);
|
||||
glDetachShader(program, vs);
|
||||
glDetachShader(program, fs);
|
||||
glDeleteShader(vs);
|
||||
glDeleteShader(fs);
|
||||
programs.push_back(program);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
const GLuint program = programs[static_cast<std::size_t>(i)];
|
||||
GLint linked = GL_FALSE;
|
||||
glGetProgramiv(program, GL_LINK_STATUS, &linked);
|
||||
ASSERT_EQ(linked, GL_TRUE) << "program " << i;
|
||||
const Image image = DrawFrameWith(program);
|
||||
EXPECT_EQ(image.QuadrantSignature(), "blue,green,red,white") << "program " << i;
|
||||
}
|
||||
for (const GLuint program : programs) glDeleteProgram(program);
|
||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace MGITest
|
||||
|
||||
@@ -408,6 +408,13 @@ namespace MobileGL::MG_State::GLState {
|
||||
MG_Util::ConvertGLEnumToString(shaderType).c_str());
|
||||
|
||||
if (!compiled.compileStatus) {
|
||||
// The compile log LEADS the quoted source, and that order is load-bearing:
|
||||
// under MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS this string is the
|
||||
// application's ONLY compile diagnostic (the per-shader queries answered
|
||||
// optimistically), and applications read it through a bounded buffer -
|
||||
// Iris uses 32768 bytes - so the actionable text must come before the
|
||||
// potentially-100KB source dump. The full source stays: the device log is
|
||||
// where a failing pack gets debugged from.
|
||||
artifacts.infoLog =
|
||||
std::format("Linking a {} with compilation error, linking will now terminate. Shader error "
|
||||
"log:\n{}\nShader src:\n{}",
|
||||
|
||||
@@ -88,12 +88,19 @@ namespace MobileGL::MG_State::GLState {
|
||||
// another object, THIS object has not pulled its result yet. (An adopted node may
|
||||
// already be terminal - the join then only replays what is left of its diagnostics.)
|
||||
m_compileJoined = false;
|
||||
// A new compile is a new story: whatever the optimistic getters promised about the
|
||||
// previous node does not carry over.
|
||||
m_optimisticAnswerLatched = false;
|
||||
}
|
||||
|
||||
void ShaderObject::DropCompileNode() const {
|
||||
if (!m_compiled) return;
|
||||
m_compiled->ReleaseAdopter();
|
||||
m_compiled.reset();
|
||||
// No node means IsCompileComplete() is trivially true and the truthful answers are
|
||||
// "not compiled"; a stale latch would keep reporting a compile that no longer
|
||||
// exists as GL_TRUE.
|
||||
m_optimisticAnswerLatched = false;
|
||||
}
|
||||
|
||||
void ShaderObject::InvalidateCompiledState() {
|
||||
|
||||
@@ -116,8 +116,10 @@ namespace MobileGL {
|
||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
||||
|
||||
// Blocks until a pending compile has published its artifacts. Public for the
|
||||
// sites that must join without reading anything - ProgramObject::Link's
|
||||
// prologue, which needs every attached shader settled before it runs.
|
||||
// sites that must join without reading anything - ProgramState::
|
||||
// JoinAllPendingWork, the glMaxShaderCompilerThreadsKHR(0) path that settles
|
||||
// every outstanding job. glLinkProgram deliberately does NOT come through
|
||||
// here: its prologue takes the nodes unjoined via CompiledNodeForLink().
|
||||
void JoinCompile() const { EnsureCompileJoined(); }
|
||||
|
||||
// True while this object holds the outcome (success OR failure) of a Compile()
|
||||
@@ -141,6 +143,23 @@ namespace MobileGL {
|
||||
// outstanding to wait for.
|
||||
Bool IsCompileComplete() const { return m_compiled == nullptr || m_compiled->IsTerminal(); }
|
||||
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS's one-story-per-compile memory. The
|
||||
// three optimistic getter sites in GL_Program ask THIS instead of a raw
|
||||
// IsCompileComplete() peek, and the difference is the latch: without it, a job
|
||||
// that settles between two adjacent queries hands the application a torn pair -
|
||||
// an empty info log from the optimistic read, then the real GL_FALSE from the
|
||||
// truthful one - and an application that aborts on that status never reaches
|
||||
// the link join that quotes the real log. So the first optimistic answer
|
||||
// latches: until the next AdoptCompileNode/DropCompileNode this object keeps
|
||||
// answering optimistically even after the job settles, and a real failure
|
||||
// surfaces exactly once, at the link. Returns whether the caller should answer
|
||||
// optimistically; the caller has already checked the quirk is active.
|
||||
Bool TakeOptimisticCompileAnswer() const {
|
||||
if (!m_optimisticAnswerLatched && IsCompileComplete()) return false;
|
||||
m_optimisticAnswerLatched = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
// ---- The one and only join gate for compile output (P1 invariant I5) ----
|
||||
// The fast path - no job, or a job whose result this object has already pulled -
|
||||
@@ -231,6 +250,10 @@ namespace MobileGL {
|
||||
// Exactly-once latch for the pull above. Armed with every new job node, set by
|
||||
// the one join that consumes it.
|
||||
mutable Bool m_compileJoined = false;
|
||||
// TakeOptimisticCompileAnswer's memory: this object has answered a compile
|
||||
// query optimistically for the current node. Cleared wherever the node
|
||||
// changes hands (AdoptCompileNode) or goes away (DropCompileNode).
|
||||
mutable Bool m_optimisticAnswerLatched = false;
|
||||
};
|
||||
} // namespace MG_State::GLState
|
||||
} // namespace MobileGL
|
||||
|
||||
@@ -49,6 +49,22 @@ add_executable(
|
||||
AsyncLinkTest.cpp
|
||||
)
|
||||
|
||||
add_executable(
|
||||
OptimisticStatusTest
|
||||
OptimisticStatusTest.cpp
|
||||
)
|
||||
|
||||
target_include_directories(OptimisticStatusTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
)
|
||||
|
||||
target_link_libraries(
|
||||
OptimisticStatusTest PRIVATE
|
||||
GTest::gtest_main
|
||||
${LINK_LIBRARIES}
|
||||
)
|
||||
|
||||
target_include_directories(AsyncLinkTest PRIVATE
|
||||
${MGL_ROOT}/include
|
||||
${MGL_ROOT}/MobileGL
|
||||
@@ -170,6 +186,9 @@ gtest_discover_tests(AsyncLinkTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit T
|
||||
gtest_discover_tests(ShaderCompileAdoptionTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
# Same reason: the GL_COMPLETION_STATUS_KHR cases saturate a one-worker pool on purpose.
|
||||
gtest_discover_tests(ParallelShaderCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
# Same reason: the optimistic-window cases need a saturated one-worker pool to observe an
|
||||
# in-flight compile, and the two-phase replay links 48 programs across both flag states.
|
||||
gtest_discover_tests(OptimisticStatusTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
gtest_discover_tests(AsyncTeardownTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
# Same reason again: several cases leave A links outstanding while B compiles and links.
|
||||
gtest_discover_tests(XfbFrontendOrderInvarianceTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||
|
||||
@@ -0,0 +1,674 @@
|
||||
// MobileGL - MobileGL/MG_Test/Program/OptimisticStatusTest.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
|
||||
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS: while a compile job is in flight, the two
|
||||
// per-shader queries that would join it - GL_COMPILE_STATUS and the info log - answer
|
||||
// optimistically instead, and the first such answer latches for that compile's lifetime
|
||||
// (ShaderObject::TakeOptimisticCompileAnswer). These cases pin the corners of that
|
||||
// contract: the default still joins, the optimistic window really answers without
|
||||
// joining, the latch keeps the three queries telling one story even after the job
|
||||
// settles, a real failure still fails the program link with the compile log quoted, and
|
||||
// the Iris-shaped two-phase batch produces reflection identical to the joining path.
|
||||
//
|
||||
// Determinism note: the cases that need "a compile that cannot have settled yet" do not
|
||||
// race the pool - they occupy its single concurrency slot with a gate-blocked job
|
||||
// (PoolBlocker), so the assertions are hard EXPECTs rather than skip-if-drained guesses.
|
||||
// A quirk that silently reverts to joining DEADLOCKS such a case into its 300s ctest
|
||||
// timeout instead of passing - ugly, but a failure, which is the point.
|
||||
//
|
||||
// Like AsyncCompileTest, every case drives the real GL entry points and flips the
|
||||
// MG_Config::Features fields itself rather than reading the environment, so one binary
|
||||
// asserts both flag states regardless of how the suite was launched.
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "Config.h"
|
||||
#include "Includes.h"
|
||||
#include "Init.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/JobNode.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;
|
||||
};
|
||||
|
||||
class OptimisticStatusScope {
|
||||
public:
|
||||
explicit OptimisticStatusScope(const MG_Config::QuirkOverride mode)
|
||||
: m_saved(MG_Config::Features.AsyncOptimisticShaderStatus) {
|
||||
MG_Config::Features.AsyncOptimisticShaderStatus = mode;
|
||||
}
|
||||
~OptimisticStatusScope() { MG_Config::Features.AsyncOptimisticShaderStatus = m_saved; }
|
||||
OptimisticStatusScope(const OptimisticStatusScope&) = delete;
|
||||
OptimisticStatusScope& operator=(const OptimisticStatusScope&) = 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;
|
||||
};
|
||||
|
||||
// A job that occupies a pool slot until released, holding everything queued behind it
|
||||
// in a provably-unsettled state. Same gate idea as JobNodeTest's TestJob+Gate; waiting
|
||||
// on a test-owned gate inside a body does not violate the pool's no-job-waits-on-job
|
||||
// rule - there is no other JOB involved.
|
||||
class PoolBlocker final : public MG_Util::Async::JobNode {
|
||||
public:
|
||||
void Release() {
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(m_mutex);
|
||||
m_open = true;
|
||||
}
|
||||
m_cv.notify_all();
|
||||
}
|
||||
|
||||
protected:
|
||||
void RunBody() override {
|
||||
std::unique_lock<std::mutex> lock(m_mutex);
|
||||
m_cv.wait(lock, [this] { return m_open; });
|
||||
}
|
||||
|
||||
private:
|
||||
std::mutex m_mutex;
|
||||
std::condition_variable m_cv;
|
||||
Bool m_open = false;
|
||||
};
|
||||
|
||||
// Budget 1 + a blocked job in the only slot: from construction until Release(), no
|
||||
// shader compile posted afterwards can run, let alone settle. The destructor releases
|
||||
// and joins so no case can leak a wedged pool into the next one.
|
||||
class BlockedPoolScope {
|
||||
public:
|
||||
BlockedPoolScope() : m_blocker(MakeShared<PoolBlocker>()) {
|
||||
MaxShaderCompilerThreadsKHR(1);
|
||||
MG_Util::Async::ShaderCompilePool::Get().Post(m_blocker);
|
||||
}
|
||||
~BlockedPoolScope() { Release(); }
|
||||
|
||||
void Release() {
|
||||
m_blocker->Release();
|
||||
m_blocker->Wait();
|
||||
}
|
||||
|
||||
BlockedPoolScope(const BlockedPoolScope&) = delete;
|
||||
BlockedPoolScope& operator=(const BlockedPoolScope&) = delete;
|
||||
|
||||
private:
|
||||
SharedPtr<PoolBlocker> m_blocker;
|
||||
};
|
||||
|
||||
const char* kBrokenFs = R"(#version 460
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
void main() { fragColor = thisIdentifierWasNeverDeclared; }
|
||||
)";
|
||||
|
||||
// Expensive enough that a compile is not instantaneous, and distinct per index so the
|
||||
// source-hash memo and the stage-6 adoption map never turn a second instance into a
|
||||
// no-op. Callers pass disjoint seed ranges for the same reason - two calls in one case
|
||||
// must never regenerate the same text.
|
||||
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;
|
||||
}
|
||||
|
||||
// The two stages of one Iris-shaped program. Distinct per index (so nothing is memoized
|
||||
// across programs) but IDENTICAL between the quirk-off and quirk-on replays of the same
|
||||
// index, which is what makes the reflection comparison meaningful.
|
||||
String MakeIrisVs(const int index) {
|
||||
String source = "#version 460\nlayout(location = 0) in vec3 aPos;\n";
|
||||
source += "uniform mat4 uModel" + std::to_string(index) + ";\n";
|
||||
source += "uniform vec4 uTint;\nout vec4 vColor;\n";
|
||||
source += "void main() {\n vColor = uTint;\n gl_Position = uModel" + std::to_string(index) +
|
||||
" * vec4(aPos, 1.0);\n}\n";
|
||||
return source;
|
||||
}
|
||||
String MakeIrisFs(const int index) {
|
||||
String source = "#version 460\nlayout(location = 0) out vec4 fragColor;\nin vec4 vColor;\n";
|
||||
source += "uniform float uSeed" + std::to_string(index) + ";\nuniform vec2 uOffset;\n";
|
||||
source += "void main() {\n float acc = uSeed" + std::to_string(index) + " + uOffset.x;\n";
|
||||
for (int i = 0; i < 40; ++i) {
|
||||
source += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0);\n";
|
||||
}
|
||||
source += " fragColor = vColor + vec4(acc, uOffset.y, 0.0, 1.0);\n}\n";
|
||||
return source;
|
||||
}
|
||||
|
||||
GLuint MakeShader(const GLenum type, const char* source) {
|
||||
const GLuint shader = CreateShader(type);
|
||||
ShaderSource(shader, 1, &source, nullptr);
|
||||
CompileShader(shader);
|
||||
return shader;
|
||||
}
|
||||
|
||||
GLint QueryShaderCompletion(const GLuint shader) {
|
||||
GLint status = -1;
|
||||
GetShaderiv(shader, 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 QueryInfoLogLength(const GLuint shader) {
|
||||
GLint length = -1;
|
||||
GetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
|
||||
return length;
|
||||
}
|
||||
|
||||
String QueryShaderInfoLog(const GLuint shader) {
|
||||
std::vector<GLchar> buffer(65536);
|
||||
GLsizei written = 0;
|
||||
GetShaderInfoLog(shader, (GLsizei)buffer.size(), &written, buffer.data());
|
||||
return String(buffer.data(), static_cast<size_t>(written));
|
||||
}
|
||||
|
||||
GLint QueryLinkStatus(const GLuint program) {
|
||||
GLint status = GL_FALSE;
|
||||
GetProgramiv(program, GL_LINK_STATUS, &status);
|
||||
return status;
|
||||
}
|
||||
|
||||
GLint QueryProgramCompletion(const GLuint program) {
|
||||
GLint status = -1;
|
||||
GetProgramiv(program, GL_COMPLETION_STATUS_KHR, &status);
|
||||
return status;
|
||||
}
|
||||
|
||||
String QueryProgramInfoLog(const GLuint program) {
|
||||
// Iris reads through an explicit 32768-byte buffer; mirror that cap so the
|
||||
// log-ordering contract is asserted through the same window the application has.
|
||||
std::vector<GLchar> buffer(32768);
|
||||
GLsizei written = 0;
|
||||
GetProgramInfoLog(program, (GLsizei)buffer.size(), &written, buffer.data());
|
||||
return String(buffer.data(), static_cast<size_t>(written));
|
||||
}
|
||||
|
||||
// Enqueues `count` distinct heavy compiles without reading anything back. Seed bases
|
||||
// must be disjoint across calls within one case (see MakeBulkySource).
|
||||
Vector<GLuint> SaturatePool(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 shader = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(shader, 1, &text, nullptr);
|
||||
CompileShader(shader);
|
||||
shaders.push_back(shader);
|
||||
}
|
||||
return shaders;
|
||||
}
|
||||
|
||||
// One program driven through Iris's exact phase-1 shape: create, source, compile, read
|
||||
// the info log then the compile status (GlShader.createShader's order), attach, bind an
|
||||
// attrib, link, detach, delete. NO program-level query of any kind.
|
||||
GLuint RunIrisPhaseOne(const String& vsSource, const String& fsSource) {
|
||||
const char* vsText = vsSource.c_str();
|
||||
const char* fsText = fsSource.c_str();
|
||||
|
||||
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||
ShaderSource(vs, 1, &vsText, nullptr);
|
||||
CompileShader(vs);
|
||||
(void)QueryShaderInfoLog(vs);
|
||||
(void)QueryCompileStatus(vs);
|
||||
|
||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &fsText, nullptr);
|
||||
CompileShader(fs);
|
||||
(void)QueryShaderInfoLog(fs);
|
||||
(void)QueryCompileStatus(fs);
|
||||
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, vs);
|
||||
AttachShader(program, fs);
|
||||
BindAttribLocation(program, 0, "aPos");
|
||||
LinkProgram(program);
|
||||
DetachShader(program, vs);
|
||||
DetachShader(program, fs);
|
||||
DeleteShader(vs);
|
||||
DeleteShader(fs);
|
||||
return program;
|
||||
}
|
||||
|
||||
// Phase 2, also in Iris's order: LINK_STATUS first, then the by-name location lookups,
|
||||
// then the GL_ACTIVE_UNIFORMS enumeration ProgramUniforms$Builder.buildUniforms does.
|
||||
struct ProgramReflection {
|
||||
GLint linkStatus = GL_FALSE;
|
||||
Vector<std::pair<String, GLint>> locations; // queried name -> location
|
||||
Vector<std::tuple<String, GLenum, GLint, GLint>> activeUniforms; // name, type, size, location
|
||||
};
|
||||
|
||||
ProgramReflection RunIrisPhaseTwo(const GLuint program, const Vector<String>& names) {
|
||||
ProgramReflection out;
|
||||
out.linkStatus = QueryLinkStatus(program);
|
||||
|
||||
for (const String& name : names) {
|
||||
out.locations.emplace_back(name, GetUniformLocation(program, name.c_str()));
|
||||
}
|
||||
|
||||
GLint activeCount = 0;
|
||||
GetProgramiv(program, GL_ACTIVE_UNIFORMS, &activeCount);
|
||||
for (GLint i = 0; i < activeCount; ++i) {
|
||||
GLchar name[128] = {};
|
||||
GLsizei written = 0;
|
||||
GLint size = 0;
|
||||
GLenum type = 0;
|
||||
GetActiveUniform(program, (GLuint)i, (GLsizei)sizeof(name), &written, &size, &type, name);
|
||||
const String nameStr(name, static_cast<size_t>(written));
|
||||
out.activeUniforms.emplace_back(nameStr, type, size, GetUniformLocation(program, name));
|
||||
}
|
||||
// The enumeration order is an implementation detail; the SET is the contract.
|
||||
std::sort(out.activeUniforms.begin(), out.activeUniforms.end());
|
||||
return out;
|
||||
}
|
||||
|
||||
class OptimisticStatusTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override { MobileGL::Initialize(); }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// The default still joins
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
// With the quirk unset (Auto = the shipped default), GL_COMPILE_STATUS on a pending compile
|
||||
// must join it: after the query, the node is terminal. This is the case that guards the
|
||||
// default against ever silently flipping. No blocker here - a blocked pool would turn the
|
||||
// (correct) joining behaviour into a deadlock; a plain backlog only makes the pre-join
|
||||
// state likely, and the assertion is valid either way.
|
||||
TEST_F(OptimisticStatusTest, OffByDefaultTheStatusStillJoins) {
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::Auto);
|
||||
const CompilerThreadScope threads;
|
||||
MaxShaderCompilerThreadsKHR(1);
|
||||
|
||||
Vector<String> backlog;
|
||||
const Vector<GLuint> saturation = SaturatePool(8, 70000, backlog);
|
||||
const Vector<GLuint> probes = SaturatePool(1, 71000, backlog);
|
||||
const GLuint probe = probes[0];
|
||||
|
||||
EXPECT_EQ(QueryCompileStatus(probe), GL_TRUE);
|
||||
EXPECT_EQ(QueryShaderCompletion(probe), GL_TRUE)
|
||||
<< "GL_COMPILE_STATUS with the quirk off must have joined the job";
|
||||
|
||||
for (const GLuint shader : saturation) DeleteShader(shader);
|
||||
DeleteShader(probe);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// The optimistic window, deterministically
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
// A compile that provably cannot have settled (the pool's only slot is gate-blocked)
|
||||
// answers GL_TRUE / length 0 / empty log, and GL_COMPLETION_STATUS_KHR still reads
|
||||
// GL_FALSE after all three - i.e. none of them joined. Hard EXPECTs, no skip: if the
|
||||
// quirk silently reverts to joining, the status read deadlocks against the blocked pool
|
||||
// and the case fails by timeout.
|
||||
TEST_F(OptimisticStatusTest, PendingCompileReportsTrueAndEmptyLogWithoutJoining) {
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
||||
const CompilerThreadScope threads;
|
||||
const BlockedPoolScope blocked;
|
||||
|
||||
Vector<String> storage;
|
||||
const Vector<GLuint> probes = SaturatePool(1, 72000, storage);
|
||||
const GLuint probe = probes[0];
|
||||
|
||||
EXPECT_EQ(QueryCompileStatus(probe), GL_TRUE) << "an in-flight compile must answer GL_TRUE";
|
||||
EXPECT_EQ(QueryInfoLogLength(probe), 0) << "an in-flight compile must answer an empty log length";
|
||||
EXPECT_TRUE(QueryShaderInfoLog(probe).empty()) << "an in-flight compile must answer an empty log";
|
||||
EXPECT_EQ(QueryShaderCompletion(probe), GL_FALSE)
|
||||
<< "the three reads above must not have joined the blocked job";
|
||||
|
||||
DeleteShader(probe);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// The latch: one story per compile
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
// The torn-pair regression case. A broken shader's log and status are read while the job
|
||||
// is provably in flight (optimistic empty/GL_TRUE), the job then settles, and the app
|
||||
// re-reads: the latch must keep the answers optimistic - GL_TRUE, empty log - rather than
|
||||
// flip to the real GL_FALSE next to the already-consumed empty log. The real failure then
|
||||
// surfaces at the link, with the compile error inside the application's 32768-byte read
|
||||
// window (the compile log leads the quoted source in ConsumeShaders' format).
|
||||
TEST_F(OptimisticStatusTest, LatchKeepsOneStoryPerCompileAndTheLinkCarriesTheDiagnostic) {
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
||||
const CompilerThreadScope threads;
|
||||
|
||||
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
|
||||
const char* vsText =
|
||||
"#version 460\nlayout(location = 0) in vec3 aPos;\nvoid main() { gl_Position = vec4(aPos, 1.0); }\n";
|
||||
ShaderSource(vs, 1, &vsText, nullptr);
|
||||
|
||||
GLuint fs = 0;
|
||||
{
|
||||
const BlockedPoolScope blocked;
|
||||
CompileShader(vs);
|
||||
fs = MakeShader(GL_FRAGMENT_SHADER, kBrokenFs);
|
||||
|
||||
// Iris's order, while nothing can settle: log (empty), then status (GL_TRUE).
|
||||
EXPECT_TRUE(QueryShaderInfoLog(fs).empty());
|
||||
EXPECT_EQ(QueryCompileStatus(fs), GL_TRUE);
|
||||
EXPECT_EQ(QueryShaderCompletion(fs), GL_FALSE);
|
||||
} // blocker released and joined; the broken compile can now settle
|
||||
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
while (QueryShaderCompletion(fs) == GL_FALSE) {
|
||||
ASSERT_LT(std::chrono::steady_clock::now(), deadline) << "compile job never settled";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
|
||||
// Settled - but this shader already told the optimistic story, so it keeps telling it.
|
||||
EXPECT_EQ(QueryCompileStatus(fs), GL_TRUE)
|
||||
<< "the latch must keep a queried-while-pending compile optimistic after it settles";
|
||||
EXPECT_EQ(QueryInfoLogLength(fs), 0);
|
||||
EXPECT_TRUE(QueryShaderInfoLog(fs).empty());
|
||||
|
||||
// The truth arrives where the design routes it: at the link.
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, vs);
|
||||
AttachShader(program, fs);
|
||||
LinkProgram(program);
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_FALSE) << "a latched-over failure must still fail the link";
|
||||
EXPECT_NE(QueryProgramInfoLog(program).find("thisIdentifierWasNeverDeclared"), String::npos)
|
||||
<< "the compile error must lead the program info log, inside a 32768-byte window";
|
||||
|
||||
DeleteProgram(program);
|
||||
DeleteShader(vs);
|
||||
DeleteShader(fs);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// A shader whose FIRST query arrives after the job settled was never answered
|
||||
// optimistically, so it owes no continuity: the truth comes straight back. (The
|
||||
// completion poll does not engage the latch - it is the extension's own non-joining
|
||||
// query and always tells the truth.)
|
||||
TEST_F(OptimisticStatusTest, OnceTerminalAnUnqueriedShaderTellsTheTruth) {
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
||||
|
||||
const GLuint fs = MakeShader(GL_FRAGMENT_SHADER, kBrokenFs);
|
||||
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
while (QueryShaderCompletion(fs) == GL_FALSE) {
|
||||
ASSERT_LT(std::chrono::steady_clock::now(), deadline) << "compile job never settled";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
|
||||
EXPECT_EQ(QueryCompileStatus(fs), GL_FALSE) << "no optimistic answer was given, so no latch holds";
|
||||
EXPECT_GT(QueryInfoLogLength(fs), 0);
|
||||
EXPECT_NE(QueryShaderInfoLog(fs).find("thisIdentifierWasNeverDeclared"), String::npos);
|
||||
DeleteShader(fs);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// Recompiling resets the story: a latched optimistic answer must not survive a source
|
||||
// change (the latch clears when the node changes hands or goes away).
|
||||
TEST_F(OptimisticStatusTest, ANewCompileResetsTheLatch) {
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
||||
const CompilerThreadScope threads;
|
||||
|
||||
GLuint fs = 0;
|
||||
{
|
||||
const BlockedPoolScope blocked;
|
||||
fs = MakeShader(GL_FRAGMENT_SHADER, kBrokenFs);
|
||||
EXPECT_EQ(QueryCompileStatus(fs), GL_TRUE); // latches
|
||||
}
|
||||
|
||||
// New source, new compile, no query before it settles.
|
||||
const char* goodFs = "#version 460\nlayout(location = 0) out vec4 fragColor;\n"
|
||||
"void main() { fragColor = vec4(1.0); }\n";
|
||||
ShaderSource(fs, 1, &goodFs, nullptr);
|
||||
CompileShader(fs);
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
while (QueryShaderCompletion(fs) == GL_FALSE) {
|
||||
ASSERT_LT(std::chrono::steady_clock::now(), deadline) << "recompile never settled";
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
EXPECT_EQ(QueryCompileStatus(fs), GL_TRUE);
|
||||
EXPECT_TRUE(QueryShaderInfoLog(fs).empty());
|
||||
DeleteShader(fs);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// Failure still fails, at the link, inside the application's read window
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
// A broken fragment shader whose compile status was answered optimistically still fails
|
||||
// its program link, and the compile error is readable through a 32768-byte
|
||||
// glGetProgramInfoLog - the compile log LEADS the quoted source in ConsumeShaders'
|
||||
// format, so even this >32KB shader source cannot push it out of the window.
|
||||
TEST_F(OptimisticStatusTest, AFailingCompileStillFailsItsLink) {
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
||||
|
||||
// A >32KB broken fragment shader: the undeclared identifier sits at the top, then bulk.
|
||||
String brokenSource = "#version 460\nlayout(location = 0) out vec4 fragColor;\n";
|
||||
brokenSource += "void main() {\n float acc = thisIdentifierWasNeverDeclared;\n";
|
||||
for (int i = 0; i < 900; ++i) {
|
||||
brokenSource += " acc = acc * 1.0001 + sin(acc + " + std::to_string(i) + ".0) * cos(acc);\n";
|
||||
}
|
||||
brokenSource += " fragColor = vec4(acc);\n}\n";
|
||||
ASSERT_GT(brokenSource.size(), 32768u);
|
||||
|
||||
const GLuint vs = MakeShader(GL_VERTEX_SHADER,
|
||||
"#version 460\nlayout(location = 0) in vec3 aPos;\n"
|
||||
"void main() { gl_Position = vec4(aPos, 1.0); }\n");
|
||||
const char* brokenText = brokenSource.c_str();
|
||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &brokenText, nullptr);
|
||||
CompileShader(fs);
|
||||
(void)QueryShaderInfoLog(fs);
|
||||
(void)QueryCompileStatus(fs); // may latch optimistic GL_TRUE; must not matter
|
||||
|
||||
const GLuint program = CreateProgram();
|
||||
AttachShader(program, vs);
|
||||
AttachShader(program, fs);
|
||||
LinkProgram(program);
|
||||
|
||||
EXPECT_EQ(QueryLinkStatus(program), GL_FALSE) << "a hidden compile failure must still fail the link";
|
||||
const String log = QueryProgramInfoLog(program);
|
||||
EXPECT_NE(log.find("thisIdentifierWasNeverDeclared"), String::npos)
|
||||
<< "the compile error must be readable through a 32768-byte program info log window";
|
||||
|
||||
DeleteProgram(program);
|
||||
DeleteShader(vs);
|
||||
DeleteShader(fs);
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// The Iris two-phase replay
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
// THE LOAD-BEARING CASE. 24 programs through Iris's exact phase-1 shape (compile, read log
|
||||
// then status per shader, link, detach, delete - no program query), then phase 2 (link
|
||||
// status, by-name locations including an absent name, the active-uniform enumeration).
|
||||
// Every location and every active-uniform record must equal what the identical sequence
|
||||
// produces with the quirk off.
|
||||
//
|
||||
// Two determinism guards make this a real A/B rather than a tautology:
|
||||
// * The quirk-on arm runs FIRST, against a cold preprocess cache, and the reference arm
|
||||
// second - so it is the path under test that pays the full pipeline, not the control.
|
||||
// * The quirk-on arm's phase 1 runs over a BLOCKED pool, and every program is then
|
||||
// WITNESSED still-incomplete (GL_COMPLETION_STATUS_KHR == GL_FALSE) before the pool
|
||||
// is released: proof that no phase-1 call joined, i.e. the quirk was really engaged.
|
||||
// A quirk that silently reverts to joining deadlocks here and fails by timeout.
|
||||
TEST_F(OptimisticStatusTest, IrisTwoPhaseReplayProducesIdenticalReflection) {
|
||||
constexpr int kPrograms = 24;
|
||||
|
||||
Vector<ProgramReflection> reference;
|
||||
Vector<ProgramReflection> optimistic;
|
||||
|
||||
for (const Bool quirkOn : {true, false}) {
|
||||
const AsyncModeScope async(true);
|
||||
const OptimisticStatusScope quirk(quirkOn ? MG_Config::QuirkOverride::ForceOn
|
||||
: MG_Config::QuirkOverride::ForceOff);
|
||||
const CompilerThreadScope threads;
|
||||
|
||||
Vector<String> vsSources, fsSources;
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
vsSources.push_back(MakeIrisVs(i));
|
||||
fsSources.push_back(MakeIrisFs(i));
|
||||
}
|
||||
|
||||
Vector<GLuint> programs;
|
||||
if (quirkOn) {
|
||||
const BlockedPoolScope blocked;
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
programs.push_back(RunIrisPhaseOne(vsSources[(SizeT)i], fsSources[(SizeT)i]));
|
||||
}
|
||||
// The witness: phase 1 finished with the pool blocked, so nothing can have
|
||||
// settled and nothing can have been joined - every link must still be pending.
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
ASSERT_EQ(QueryProgramCompletion(programs[(SizeT)i]), GL_FALSE)
|
||||
<< "program " << i << " settled under a blocked pool - a phase-1 call must have joined";
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
programs.push_back(RunIrisPhaseOne(vsSources[(SizeT)i], fsSources[(SizeT)i]));
|
||||
}
|
||||
}
|
||||
|
||||
Vector<ProgramReflection>& out = quirkOn ? optimistic : reference;
|
||||
for (int i = 0; i < kPrograms; ++i) {
|
||||
const Vector<String> names = {"uModel" + std::to_string(i), "uTint",
|
||||
"uSeed" + std::to_string(i), "uOffset", "uDoesNotExist"};
|
||||
out.push_back(RunIrisPhaseTwo(programs[(SizeT)i], names));
|
||||
}
|
||||
for (const GLuint program : programs) DeleteProgram(program);
|
||||
ASSERT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
|
||||
ASSERT_EQ(reference.size(), optimistic.size());
|
||||
for (SizeT i = 0; i < reference.size(); ++i) {
|
||||
EXPECT_EQ(reference[i].linkStatus, GL_TRUE) << "program " << i;
|
||||
EXPECT_EQ(optimistic[i].linkStatus, GL_TRUE) << "program " << i;
|
||||
EXPECT_EQ(reference[i].locations, optimistic[i].locations)
|
||||
<< "program " << i << ": by-name locations diverged under the quirk";
|
||||
EXPECT_EQ(reference[i].activeUniforms, optimistic[i].activeUniforms)
|
||||
<< "program " << i << ": active-uniform enumeration diverged under the quirk";
|
||||
// The absent name answers -1 in both worlds.
|
||||
EXPECT_EQ(reference[i].locations.back().second, -1) << "program " << i;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------
|
||||
// The concurrency observable
|
||||
// ---------------------------------------------------------------------------------------
|
||||
|
||||
// The crisp A/B that phase 1 stopped joining. Quirk-on arm: the phase-1 shape over a
|
||||
// blocked pool completes without joining anything - every shader is then provably still
|
||||
// in flight (hard EXPECT; an inert quirk deadlocks and fails by timeout). Quirk-off arm:
|
||||
// the same shape joins at every status read, so nothing is left in flight afterwards.
|
||||
TEST_F(OptimisticStatusTest, PhaseOneIssuesNoCompileJoin) {
|
||||
const AsyncModeScope async(true);
|
||||
const CompilerThreadScope threads;
|
||||
|
||||
// Quirk on: nothing settles, nothing joins.
|
||||
{
|
||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOn);
|
||||
const BlockedPoolScope blocked;
|
||||
Vector<String> storage;
|
||||
Vector<GLuint> shaders;
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
storage.push_back(MakeBulkySource(90000 + i));
|
||||
const char* text = storage.back().c_str();
|
||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &text, nullptr);
|
||||
CompileShader(fs);
|
||||
(void)QueryShaderInfoLog(fs);
|
||||
(void)QueryCompileStatus(fs);
|
||||
shaders.push_back(fs);
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
EXPECT_EQ(QueryShaderCompletion(shader), GL_FALSE)
|
||||
<< "a phase-1 read joined a compile the blocked pool could not have run";
|
||||
}
|
||||
for (const GLuint shader : shaders) DeleteShader(shader);
|
||||
}
|
||||
|
||||
// Quirk off: every status read joins its shader.
|
||||
{
|
||||
const OptimisticStatusScope quirk(MG_Config::QuirkOverride::ForceOff);
|
||||
MaxShaderCompilerThreadsKHR(1);
|
||||
Vector<String> storage;
|
||||
Vector<GLuint> shaders;
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
storage.push_back(MakeBulkySource(80000 + i));
|
||||
const char* text = storage.back().c_str();
|
||||
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||
ShaderSource(fs, 1, &text, nullptr);
|
||||
CompileShader(fs);
|
||||
(void)QueryShaderInfoLog(fs);
|
||||
(void)QueryCompileStatus(fs);
|
||||
shaders.push_back(fs);
|
||||
}
|
||||
for (const GLuint shader : shaders) {
|
||||
EXPECT_EQ(QueryShaderCompletion(shader), GL_TRUE)
|
||||
<< "with the quirk off every per-shader status read must have joined";
|
||||
}
|
||||
for (const GLuint shader : shaders) DeleteShader(shader);
|
||||
}
|
||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||
}
|
||||
@@ -126,6 +126,15 @@ namespace MobileGL::MG_Util::Async {
|
||||
return AsyncShaderCompileEnabled() && !IsAsyncShaderCompileSuspended();
|
||||
}
|
||||
|
||||
Bool OptimisticShaderStatusActive() {
|
||||
switch (MG_Config::Features.AsyncOptimisticShaderStatus) {
|
||||
case MG_Config::QuirkOverride::ForceOn: return AsyncShaderCompileActive();
|
||||
case MG_Config::QuirkOverride::ForceOff: return false;
|
||||
case MG_Config::QuirkOverride::Auto: break;
|
||||
}
|
||||
return kOptimisticShaderStatusDefault && AsyncShaderCompileActive();
|
||||
}
|
||||
|
||||
Uint DetectShaderCompileThreadCount() {
|
||||
if (const Uint32 configured = MG_Config::Features.AsyncShaderCompileThreads; configured > 0) {
|
||||
// An explicit request is honoured as given - it is the escape hatch for measuring
|
||||
|
||||
@@ -59,6 +59,21 @@ namespace MobileGL::MG_Util::Async {
|
||||
// GL_COMPLETION_STATUS_KHR read immediately GL_TRUE.
|
||||
Bool AsyncShaderCompileActive();
|
||||
|
||||
// MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS (see Config.h): opt-in, off by default, and a
|
||||
// spec violation by design - GL_COMPILE_STATUS and the shader info log answer
|
||||
// optimistically while the compile job is in flight instead of joining it. Do not flip
|
||||
// this default without an enumerated CTS delta: the compile-error-reporting cases WILL
|
||||
// regress under it, deliberately.
|
||||
inline constexpr Bool kOptimisticShaderStatusDefault = false;
|
||||
|
||||
// The one question the three optimistic getter sites ask. ANDed with
|
||||
// AsyncShaderCompileActive() so that async-off (env kill switch) and
|
||||
// glMaxShaderCompilerThreadsKHR(0) both switch the quirk off structurally: in those
|
||||
// modes every compile settles before its enqueue returns, so a non-terminal node - the
|
||||
// only state the quirk changes - cannot exist, and keeping the AND means there is no
|
||||
// new mode interaction to reason about.
|
||||
Bool OptimisticShaderStatusActive();
|
||||
|
||||
// min(4, big cores), where a big core is one whose cpufreq ceiling is within 15% of the
|
||||
// machine maximum; the whole CPU count where that sysfs tree is absent. Clamped to [1, 4]
|
||||
// because peak RSS scales as workers x largest glslang arena, and four
|
||||
|
||||
Reference in New Issue
Block a user