mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-12 22:28:32 +09:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5248b8b746 | ||
|
|
d868e1c476 | ||
|
|
c3412ca394 | ||
|
|
5ccaff37af | ||
|
|
71e29f9d58 | ||
|
|
8ad07c222c | ||
|
|
5722094d6f | ||
|
|
4831387cf0 | ||
|
|
d03b72267a | ||
|
|
847ec74f48 | ||
|
|
e02e5caa17 | ||
|
|
1958934594 | ||
|
|
dec0c5eaff | ||
|
|
b6a44cd1e2 | ||
|
|
85f45d0e44 | ||
|
|
404236d337 | ||
|
|
6ea948779e |
@@ -296,6 +296,7 @@ set(SOURCE_FILES
|
|||||||
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
MobileGL/MG_State/GLState/TextureState/TextureState.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp
|
||||||
|
MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp
|
||||||
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp
|
||||||
|
|||||||
@@ -136,6 +136,19 @@ namespace MobileGL::MG_Config {
|
|||||||
// MOBILEGL_ASYNC_SHADER_COMPILE_THREADS: shader-compile worker count. 0 (unset) means
|
// 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.
|
// auto, which is min(4, big cores); an explicit value is honoured as given.
|
||||||
Uint32 AsyncShaderCompileThreads = 0;
|
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;
|
extern FeaturesTable Features;
|
||||||
} // namespace MobileGL::MG_Config
|
} // namespace MobileGL::MG_Config
|
||||||
|
|||||||
@@ -183,6 +183,8 @@ namespace MobileGL::MG_ConfigLoader {
|
|||||||
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
features.EsprytMultiDrawMode = QueryEnvGLESMultiDrawMode("MOBILEGL_ESPRYT_MULTIDRAW_MODE");
|
||||||
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
features.AsyncShaderCompile = QueryEnvQuirkOverride("MOBILEGL_ASYNC_SHADER_COMPILE");
|
||||||
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
features.AsyncShaderCompileThreads = QueryEnvUint32("MOBILEGL_ASYNC_SHADER_COMPILE_THREADS", 0, 0, 64);
|
||||||
|
features.AsyncOptimisticShaderStatus =
|
||||||
|
QueryEnvQuirkOverride("MOBILEGL_ASYNC_OPTIMISTIC_SHADER_STATUS");
|
||||||
}
|
}
|
||||||
|
|
||||||
inline void InitBackendType() {
|
inline void InitBackendType() {
|
||||||
|
|||||||
@@ -2028,7 +2028,9 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
g_currentDrawFrontendProgram = nullptr;
|
g_currentDrawFrontendProgram = nullptr;
|
||||||
g_currentDrawBackendProgram = nullptr;
|
g_currentDrawBackendProgram = nullptr;
|
||||||
|
|
||||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
// ... || !GetSpirvStatus(): see BackendProgramObjectImpl::SyncToBackend - a
|
||||||
|
// program whose SPIR-V never arrived is linked but not drawable.
|
||||||
|
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
||||||
g_GLESFuncs.glUseProgram(0);
|
g_GLESFuncs.glUseProgram(0);
|
||||||
g_lastUsedBackendProgramId = 0;
|
g_lastUsedBackendProgramId = 0;
|
||||||
return;
|
return;
|
||||||
@@ -2589,7 +2591,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
static void BindCurrentProgramWithResources(
|
static void BindCurrentProgramWithResources(
|
||||||
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
|
||||||
const TextureImpl::DrawTextureSyncKeys& keys) {
|
const TextureImpl::DrawTextureSyncKeys& keys) {
|
||||||
if (currentProgram && currentProgram->GetLinkStatus()) {
|
if (currentProgram && currentProgram->GetLinkStatus() && currentProgram->GetSpirvStatus()) {
|
||||||
#ifdef TRACY_ENABLE
|
#ifdef TRACY_ENABLE
|
||||||
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
ZoneScopedNC("BindCurrentProgram", TRACY_ZONECOLOR_BACKEND);
|
||||||
#endif
|
#endif
|
||||||
@@ -2859,7 +2861,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
|
// is pinned for the duration. Prefers the per-draw stash those preparations wrote.
|
||||||
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
|
static PrgramImpl::BackendProgramObjectImpl* GetCurrentBackendProgram() {
|
||||||
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw();
|
||||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) {
|
if (PrgramImpl::g_currentDrawFrontendProgram == currentProgram.get()) {
|
||||||
@@ -3017,7 +3019,7 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
TextureImpl::SyncImageTextureBindings();
|
TextureImpl::SyncImageTextureBindings();
|
||||||
PrgramImpl::SyncCurrentProgram(currentProgram);
|
PrgramImpl::SyncCurrentProgram(currentProgram);
|
||||||
|
|
||||||
if (!currentProgram || !currentProgram->GetLinkStatus()) {
|
if (!currentProgram || !currentProgram->GetLinkStatus() || !currentProgram->GetSpirvStatus()) {
|
||||||
g_GLESFuncs.glUseProgram(0);
|
g_GLESFuncs.glUseProgram(0);
|
||||||
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
PrgramImpl::g_lastUsedBackendProgramId = 0;
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4156,8 +4156,14 @@ namespace MobileGL::MG_Backend::DirectGLES {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stateProgramObject->GetLinkStatus()) {
|
// GetSpirvStatus() as well as GetLinkStatus(): a program whose phase-B job was
|
||||||
MGLOG_E("Program object is not linked, skipping backend sync. State program ID: %u",
|
// cancelled (teardown) or whose optimizer run failed is fully linked and fully
|
||||||
|
// queryable, but has no SPIR-V to build a driver program out of. GL cannot retract
|
||||||
|
// a LINK_STATUS it already reported true, so "linked but not drawable" is the
|
||||||
|
// answer, and this is where the ES backend expresses it.
|
||||||
|
if (!stateProgramObject->GetLinkStatus() || !stateProgramObject->GetSpirvStatus()) {
|
||||||
|
MGLOG_E("Program object is not linked or has no generated SPIR-V, skipping backend sync. State "
|
||||||
|
"program ID: %u",
|
||||||
stateProgramObject->GetExternalIndex());
|
stateProgramObject->GetExternalIndex());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4267,8 +4267,15 @@ void main() {
|
|||||||
auto writeUniform = [&](Int location, const void* data, SizeT size) {
|
auto writeUniform = [&](Int location, const void* data, SizeT size) {
|
||||||
MOBILEGL_ASSERT(location >= 0, "GenerateDepthMipmapWithShader: invalid uniform location");
|
MOBILEGL_ASSERT(location >= 0, "GenerateDepthMipmapWithShader: invalid uniform location");
|
||||||
const Uint offset = m_depthMipmapResources.program->GetUniformOffset(static_cast<Uint>(location));
|
const Uint offset = m_depthMipmapResources.program->GetUniformOffset(static_cast<Uint>(location));
|
||||||
MOBILEGL_ASSERT(offset + size <= m_depthMipmapResources.program->GetUBOSize(),
|
// A RETURN, not only an assert: the assert compiles out in release, and a program
|
||||||
"GenerateDepthMipmapWithShader: uniform write out of bounds");
|
// whose SPIR-V job settled cancelled reports kInvalidUniformOffset (~0u) with a
|
||||||
|
// zero-sized shadow - which would make the memcpy below a wild write at
|
||||||
|
// depthProgramData + 4 GiB rather than a dropped uniform.
|
||||||
|
if (offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
|
offset + size > m_depthMipmapResources.program->GetUBOSize()) {
|
||||||
|
MOBILEGL_ASSERT(false, "GenerateDepthMipmapWithShader: uniform write out of bounds");
|
||||||
|
return;
|
||||||
|
}
|
||||||
memcpy(depthProgramData + offset, data, size);
|
memcpy(depthProgramData + offset, data, size);
|
||||||
m_depthMipmapResources.program->MarkUBOContentDirty();
|
m_depthMipmapResources.program->MarkUBOContentDirty();
|
||||||
};
|
};
|
||||||
@@ -7161,8 +7168,13 @@ void main() {
|
|||||||
auto writeUniform = [&](Int location, const void* data, SizeT size) {
|
auto writeUniform = [&](Int location, const void* data, SizeT size) {
|
||||||
MOBILEGL_ASSERT(location >= 0, "TryBlitToDefaultFramebufferWithShader: invalid uniform location");
|
MOBILEGL_ASSERT(location >= 0, "TryBlitToDefaultFramebufferWithShader: invalid uniform location");
|
||||||
const Uint offset = m_blitResources.program->GetUniformOffset(static_cast<Uint>(location));
|
const Uint offset = m_blitResources.program->GetUniformOffset(static_cast<Uint>(location));
|
||||||
MOBILEGL_ASSERT(offset + size <= m_blitResources.program->GetUBOSize(),
|
// A RETURN, not only an assert - see GenerateDepthMipmapWithShader's copy of this
|
||||||
"TryBlitToDefaultFramebufferWithShader: uniform write out of bounds");
|
// guard: kInvalidUniformOffset must not reach the memcpy in a release build.
|
||||||
|
if (offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
|
offset + size > m_blitResources.program->GetUBOSize()) {
|
||||||
|
MOBILEGL_ASSERT(false, "TryBlitToDefaultFramebufferWithShader: uniform write out of bounds");
|
||||||
|
return;
|
||||||
|
}
|
||||||
memcpy(blitProgramData + offset, data, size);
|
memcpy(blitProgramData + offset, data, size);
|
||||||
};
|
};
|
||||||
writeUniform(m_blitResources.srcRectLocation, blitUniformData.srcRect, sizeof(blitUniformData.srcRect));
|
writeUniform(m_blitResources.srcRectLocation, blitUniformData.srcRect, sizeof(blitUniformData.srcRect));
|
||||||
|
|||||||
@@ -744,6 +744,21 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
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) {
|
void GetShaderiv_State(GLuint shader, GLenum pname, GLint* params) {
|
||||||
auto& shaderObject = TryToGetShaderObject(shader);
|
auto& shaderObject = TryToGetShaderObject(shader);
|
||||||
if (!shaderObject) return;
|
if (!shaderObject) return;
|
||||||
@@ -756,9 +771,20 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
*params = shaderObject->GetDeleteStatus();
|
*params = shaderObject->GetDeleteStatus();
|
||||||
break;
|
break;
|
||||||
case GL_COMPILE_STATUS:
|
case GL_COMPILE_STATUS:
|
||||||
|
if (AnswerCompileOptimistically(shaderObject)) {
|
||||||
|
*params = GL_TRUE;
|
||||||
|
break;
|
||||||
|
}
|
||||||
*params = shaderObject->GetCompileStatus();
|
*params = shaderObject->GetCompileStatus();
|
||||||
break;
|
break;
|
||||||
case GL_INFO_LOG_LENGTH:
|
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;
|
*params = shaderObject->GetInfoLog().empty() ? 0 : (GLint)shaderObject->GetInfoLog().length() + 1;
|
||||||
break;
|
break;
|
||||||
case GL_SHADER_SOURCE_LENGTH:
|
case GL_SHADER_SOURCE_LENGTH:
|
||||||
@@ -784,6 +810,15 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
auto& shaderObject = TryToGetShaderObject(shader);
|
auto& shaderObject = TryToGetShaderObject(shader);
|
||||||
if (!shaderObject) return;
|
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();
|
const auto& log = shaderObject->GetInfoLog();
|
||||||
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
CopyStr(bufSize, length, infoLog, log.c_str(), (GLsizei)log.length());
|
||||||
}
|
}
|
||||||
@@ -1085,10 +1120,9 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
if (!programObject.IsUniformOpaqueAtLocation(location)) {
|
||||||
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
MGLOG_D("%s: program = %d, location = %d, maxLocation = %d", __func__, programObject.GetExternalIndex(),
|
||||||
location, programObject.GetMaxUniformLocation());
|
location, programObject.GetMaxUniformLocation());
|
||||||
|
// Everything up to and including the clamp is phase-A data (the uniform's GL type
|
||||||
|
// decides its size), so it is answered without joining anything.
|
||||||
const SizeT size = programObject.GetUniformSizesInBytes(location);
|
const SizeT size = programObject.GetUniformSizesInBytes(location);
|
||||||
const Uint offset = programObject.GetUniformOffset(location);
|
|
||||||
char* pUBO = static_cast<char*>(programObject.MapUBO());
|
|
||||||
const SizeT uboSize = programObject.GetUBOSize();
|
|
||||||
SizeT writeSize = ItemCount * sizeof(T);
|
SizeT writeSize = ItemCount * sizeof(T);
|
||||||
if (size < writeSize) {
|
if (size < writeSize) {
|
||||||
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
// Metadata bug: degrade to a clamped copy instead of killing the process.
|
||||||
@@ -1097,6 +1131,18 @@ namespace MobileGL::MG_Impl::GLImpl {
|
|||||||
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
__func__, programObject.GetExternalIndex(), location, ItemCount * sizeof(T), size);
|
||||||
writeSize = size;
|
writeSize = size;
|
||||||
}
|
}
|
||||||
|
// The uniform shadow's LAYOUT is phase-B data, so a write that lands while the
|
||||||
|
// SPIR-V job is still running is recorded and replayed at its publish instead of
|
||||||
|
// joining it. This is the hot path for a shaderpack that sets its uniforms
|
||||||
|
// immediately after glLinkProgram. BufferUniformWrite declines (and we fall
|
||||||
|
// through, joining) only past its size budget.
|
||||||
|
if (programObject.IsSpirvPending() &&
|
||||||
|
programObject.BufferUniformWrite(location, byteOffsetInsideUniform, value, writeSize)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const Uint offset = programObject.GetUniformOffset(location);
|
||||||
|
char* pUBO = static_cast<char*>(programObject.MapUBO());
|
||||||
|
const SizeT uboSize = programObject.GetUBOSize();
|
||||||
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset ||
|
||||||
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
offset + byteOffsetInsideUniform + writeSize > uboSize) {
|
||||||
// Should not happen: linking gives every settable uniform backing
|
// Should not happen: linking gives every settable uniform backing
|
||||||
|
|||||||
@@ -157,6 +157,22 @@ void main() {
|
|||||||
const QuirkOverride m_saved;
|
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
|
// 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.
|
// it has to put the pool back or it changes how every scenario after it compiles.
|
||||||
class CompilerThreadScope {
|
class CompilerThreadScope {
|
||||||
@@ -463,5 +479,81 @@ void main() {
|
|||||||
EXPECT_EQ(FirstGLError(), GLenum(GL_NO_ERROR));
|
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
|
||||||
} // namespace MGITest
|
} // namespace MGITest
|
||||||
|
|||||||
@@ -380,8 +380,14 @@ namespace MobileGL::MG_State {
|
|||||||
// inside the same draw when it finally touched an artifact, and cache under a
|
// inside the same draw when it finally touched an artifact, and cache under a
|
||||||
// version the publish had already superseded. Settling here means every
|
// version the publish had already superseded. Settling here means every
|
||||||
// version a backend reads during a draw describes the program it is drawing.
|
// version a backend reads during a draw describes the program it is drawing.
|
||||||
// One null check in steady state.
|
// Two null checks in steady state.
|
||||||
currentProgram->JoinLink();
|
//
|
||||||
|
// BOTH phases, and that is not optional: the phase-B publish bumps those same
|
||||||
|
// versions, so joining only phase A here would leave exactly the hazard this
|
||||||
|
// site exists to close - a backend samples a version, then trips the phase-B
|
||||||
|
// gate through GetGeneratedSpirv() deeper inside the same draw, and memoizes
|
||||||
|
// under a version the publish has already superseded.
|
||||||
|
currentProgram->JoinLinkAndSpirv();
|
||||||
return currentProgram;
|
return currentProgram;
|
||||||
}
|
}
|
||||||
if (m_boundProgramPipeline == 0) return nullProgram;
|
if (m_boundProgramPipeline == 0) return nullProgram;
|
||||||
@@ -398,7 +404,7 @@ namespace MobileGL::MG_State {
|
|||||||
// programs. In steady state this is a null check per stage.
|
// programs. In steady state this is a null check per stage.
|
||||||
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
for (SizeT stage = 0; stage < static_cast<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
|
||||||
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
|
||||||
if (stageProgram) stageProgram->JoinLink();
|
if (stageProgram) stageProgram->JoinLinkAndSpirv();
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto signature = pipeline->ComputeDrawProgramSignature();
|
const auto signature = pipeline->ComputeDrawProgramSignature();
|
||||||
@@ -430,8 +436,9 @@ namespace MobileGL::MG_State {
|
|||||||
composite->Link(true);
|
composite->Link(true);
|
||||||
// P1 join site J2. The draw that asked for this program is the very next thing to
|
// P1 join site J2. The draw that asked for this program is the very next thing to
|
||||||
// happen, so enqueueing the composite's link buys nothing and only moves the wait
|
// happen, so enqueueing the composite's link buys nothing and only moves the wait
|
||||||
// to whichever backend accessor happens to touch its artifacts first.
|
// to whichever backend accessor happens to touch its artifacts first. Both phases,
|
||||||
composite->JoinLink();
|
// for the same reason: the backend is about to read its SPIR-V.
|
||||||
|
composite->JoinLinkAndSpirv();
|
||||||
pipeline->SetCachedDrawProgram(signature, Move(composite));
|
pipeline->SetCachedDrawProgram(signature, Move(composite));
|
||||||
return pipeline->GetCachedDrawProgram(signature);
|
return pipeline->GetCachedDrawProgram(signature);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -361,29 +361,53 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SPIR-V must be generated BEFORE buildReflection touches artifacts.program:
|
// ---- everything below this line up to GenerateSpirv() is the GL query surface ----
|
||||||
// reflection's live-variable analysis mutates the intermediates in ways that
|
//
|
||||||
// change subsequent GlslangToSpv output (observed: catastrophic uniform
|
// ORDERING NOTE (rewritten 2026-08-10; the constraint it records was RETESTED, not
|
||||||
// misbinding on DirectVulkan for UBO-heavy content). The old two-link pipeline
|
// dropped on a hunch). This block used to insist that SPIR-V be generated BEFORE
|
||||||
// never ran buildReflection on the SPIR-V-producing program; this order keeps
|
// buildReflection touches artifacts.program, on the grounds that reflection's
|
||||||
// that property with the single link. The glUniform*-to-scratch routing
|
// live-variable analysis mutates the shared intermediates in ways that change
|
||||||
// tables, in contrast, are sized and keyed by reflection results, so they are
|
// subsequent GlslangToSpv output - "observed: catastrophic uniform misbinding on
|
||||||
// built strictly AFTER DoReflection. (Everything else on the reflection
|
// DirectVulkan for UBO-heavy content", recorded with commit 0d052719.
|
||||||
// surface - locations, sampler units, block bindings/sizes - was measured
|
//
|
||||||
// identical in either order.)
|
// Re-measured on the glslang pin this tree vendors, with the same method 0d052719
|
||||||
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", in.externalIndex);
|
// used (per-module SPIR-V hashes, both orders, byte-compared): 636 modules across
|
||||||
GenerateSpirv();
|
// 320 programs - the whole extracted trace corpus (BSL, Complementary Reimagined,
|
||||||
|
// IterationRP, Create/Flywheel) plus adversarial synthetics - came out BYTE-IDENTICAL
|
||||||
|
// in both orders, pre-optimize and post-optimize alike. glslang's code structure
|
||||||
|
// agrees: reflection.cpp performs no AST write (no getWritableType, no const_cast, no
|
||||||
|
// qualifier assignment) and GlslangToSpv takes a const TIntermediate&.
|
||||||
|
//
|
||||||
|
// Confirmed a third time ON DEVICE, 2026-08-11, and this one closes the gap the
|
||||||
|
// desktop A/B could not: the corpus replays captured SOURCES, so it never reproduced
|
||||||
|
// Iris's glBindAttribLocation-before-link flow, which is what drives the io-resolver
|
||||||
|
// that assigns vertex-input Locations. A Complementary Reimagined pack load on an
|
||||||
|
// Adreno 830 was dumped at the pipeline the driver rejects (programHash
|
||||||
|
// 0x4a7e9a37fb49caa1) under BOTH orders and under the pre-split build 6ea94877: all
|
||||||
|
// three dumps are the same bytes (md5 39ffa10d5186a4d37be82d0b42297a8d). The order
|
||||||
|
// does not perturb SPIR-V on this pin, including on the exact flow 0d052719 feared.
|
||||||
|
//
|
||||||
|
// Not a licence to stop measuring: 0d052719's observation was real once, and the
|
||||||
|
// method (per-module hashes, both orders) is cheap. Re-run it on any glslang bump.
|
||||||
|
//
|
||||||
|
// So the order is now the other way round, and deliberately: reflection, fragment
|
||||||
|
// output validation and transform-feedback resolution are what the GL query surface
|
||||||
|
// is made of, and they are also the only remaining ways a link can FAIL, so running
|
||||||
|
// them first is what lets LINK_STATUS and every query behind it become final without
|
||||||
|
// waiting for SPIR-V (and stops a program that fails validation from paying for
|
||||||
|
// ~68 s/pack-load of SPIR-V generation it is about to throw away).
|
||||||
|
//
|
||||||
|
// What has NOT changed: the routing tables are sized and keyed by reflection results
|
||||||
|
// AND read the OPTIMIZED SPIR-V, so BuildGlobalUboRouting still runs strictly after
|
||||||
|
// both DoReflection and GenerateSpirv.
|
||||||
MGLOG_D("ProgramObject %u: Starting reflection", in.externalIndex);
|
MGLOG_D("ProgramObject %u: Starting reflection", in.externalIndex);
|
||||||
if (!DoReflection(env)) {
|
if (!DoReflection(env)) {
|
||||||
DeferLog(std::format("ProgramObject {}: Link failed during reflection: {}", in.externalIndex,
|
DeferLog(std::format("ProgramObject {}: Link failed during reflection: {}", in.externalIndex,
|
||||||
artifacts.infoLog));
|
artifacts.infoLog));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", in.externalIndex);
|
|
||||||
BuildGlobalUboRouting();
|
|
||||||
MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", in.externalIndex, (int)artifacts.linkStatus);
|
MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", in.externalIndex, (int)artifacts.linkStatus);
|
||||||
|
|
||||||
if (!ValidateFragmentOutputLocations()) {
|
if (!ValidateFragmentOutputLocations()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -393,8 +417,30 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
in.externalIndex, artifacts.infoLog));
|
in.externalIndex, artifacts.infoLog));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", in.externalIndex,
|
|
||||||
artifacts.generatedSpirv.size());
|
// ---- past this point the link cannot fail any more ----
|
||||||
|
// Everything left is SPIR-V work, and it belongs to phase B. Hand it what it needs
|
||||||
|
// and stop: from the join's point of view this program is now fully linked.
|
||||||
|
//
|
||||||
|
// The TShaders move rather than copy - `attrib` borrowed them into the TProgram as
|
||||||
|
// raw pointers and this node is now their owner of record, for as long as phase B
|
||||||
|
// (which holds this node) needs the intermediates hanging off them.
|
||||||
|
spirvHandoff.shaders = Move(attrib.shaders);
|
||||||
|
spirvHandoff.shaderTypes.resize(in.shaders.size());
|
||||||
|
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
||||||
|
spirvHandoff.shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
||||||
|
}
|
||||||
|
// Copied, not referenced: `artifacts` is MOVED out of this node by the join, and
|
||||||
|
// phase B runs after that. Measured at ~20 us per program, which is noise against the
|
||||||
|
// ~450 ms phase B spends on the same program.
|
||||||
|
spirvHandoff.reflection.program = artifacts.program;
|
||||||
|
spirvHandoff.reflection.uniformLocations = artifacts.uniformLocations;
|
||||||
|
spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram;
|
||||||
|
spirvHandoff.reflection.tProgramUniformIndexToGl = artifacts.tProgramUniformIndexToGl;
|
||||||
|
spirvHandoff.reflection.maxUniformLocation = artifacts.maxUniformLocation;
|
||||||
|
spirvHandoff.ready = true;
|
||||||
|
MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex,
|
||||||
|
spirvHandoff.shaderTypes.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
Bool ProgramLinkTask::ConsumeShaders(Vector<SharedPtr<glslang::TShader>>& outShaders) {
|
||||||
@@ -408,6 +454,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
MG_Util::ConvertGLEnumToString(shaderType).c_str());
|
MG_Util::ConvertGLEnumToString(shaderType).c_str());
|
||||||
|
|
||||||
if (!compiled.compileStatus) {
|
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 =
|
artifacts.infoLog =
|
||||||
std::format("Linking a {} with compilation error, linking will now terminate. Shader error "
|
std::format("Linking a {} with compilation error, linking will now terminate. Shader error "
|
||||||
"log:\n{}\nShader src:\n{}",
|
"log:\n{}\nShader src:\n{}",
|
||||||
@@ -836,183 +889,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramLinkTask::GenerateSpirv() {
|
|
||||||
/* As we passed first stage compilation/linking,
|
|
||||||
* we'll assume all the operations here should
|
|
||||||
* pass. We may be able to employ some optimizations
|
|
||||||
* here without the burden of error reporting.
|
|
||||||
*/
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
MGLOG_D("ProgramObject %u: GenerateSpirv - start", in.externalIndex);
|
|
||||||
|
|
||||||
// The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules)
|
|
||||||
// configuration, and artifacts.program linked those parses - so artifacts.program IS
|
|
||||||
// the program the backends consume. Generate SPIR-V straight from its
|
|
||||||
// intermediates; the full re-parse + re-link that used to live here (one
|
|
||||||
// glslang pass per shader per link) is gone.
|
|
||||||
Vector<GLenum> shaderTypes(in.shaders.size());
|
|
||||||
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
|
||||||
shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgramBinaryAttrib binaryAttrib{
|
|
||||||
.shaderTypes = shaderTypes,
|
|
||||||
.program = *artifacts.program,
|
|
||||||
};
|
|
||||||
MGLOG_D("ProgramObject %u: GenerateSpirv - requesting SPIR-V binary from program", in.externalIndex);
|
|
||||||
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
|
||||||
if (!binaryResult) {
|
|
||||||
DeferLog(std::format("ProgramObject {}: GenerateSpirv - GetSpirvBinaryFromProgram failed",
|
|
||||||
in.externalIndex));
|
|
||||||
}
|
|
||||||
MOBILEGL_ASSERT(binaryResult, "GetSpirvBinaryFromProgram failed");
|
|
||||||
artifacts.generatedSpirv = Move(binaryResult.value());
|
|
||||||
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", in.externalIndex,
|
|
||||||
artifacts.generatedSpirv.size());
|
|
||||||
|
|
||||||
// Linked SPIR-V generated, sanitize and optimize it
|
|
||||||
for (auto& spv : artifacts.generatedSpirv) {
|
|
||||||
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
|
|
||||||
MOBILEGL_ASSERT(success, "SanitizeBinary failed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ProgramLinkTask::BuildGlobalUboRouting() {
|
|
||||||
using namespace MG_Util::ShaderTranspiler;
|
|
||||||
Vector<GLenum> shaderTypes(in.shaders.size());
|
|
||||||
for (SizeT i = 0; i < in.shaders.size(); i++) {
|
|
||||||
shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(in.shaders[i].stage);
|
|
||||||
}
|
|
||||||
|
|
||||||
artifacts.uniformSizesInBytes.clear();
|
|
||||||
artifacts.uniformOffsets.clear();
|
|
||||||
artifacts.globalUboScratch.clear();
|
|
||||||
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
|
||||||
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
|
||||||
// below gives those locations tail storage so glUniform* always has a target.
|
|
||||||
artifacts.uniformOffsets.resize(artifacts.maxUniformLocation + 1, ProgramObject::kInvalidUniformOffset);
|
|
||||||
artifacts.uniformSizesInBytes.resize(artifacts.maxUniformLocation + 1, 0);
|
|
||||||
for (SizeT i = 0; i < artifacts.generatedSpirv.size(); i++) {
|
|
||||||
auto& spv = artifacts.generatedSpirv[i];
|
|
||||||
|
|
||||||
auto shaderType = shaderTypes[i];
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - parsing SPIR-V meta data for module %zu "
|
|
||||||
"(shaderType=%u, wordCount=%zu)",
|
|
||||||
in.externalIndex, i, shaderType, spv.size());
|
|
||||||
SpvcSession session(spv, SessionUsageBit::Reflection);
|
|
||||||
auto result = session.ParseMetaData();
|
|
||||||
if (result < 0) {
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SpvcSession::ParseMetaData failed for module %zu, "
|
|
||||||
"err = %d%s",
|
|
||||||
in.externalIndex, i, result,
|
|
||||||
(result == SPVC_ERROR_INVALID_SPIRV ? ". Probably no global UBO?" : ""));
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
auto& meta = session.GetMetadata();
|
|
||||||
auto size = meta.globalUboSize;
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SPIR-V meta: uboSize=%zu plainUniformCount=%zu "
|
|
||||||
"plainUniformOffsets=%zu",
|
|
||||||
in.externalIndex, meta.globalUboSize, meta.plainUniformMemberSizesInBytes.size(),
|
|
||||||
meta.plainUniformOffsetsInUBO.size());
|
|
||||||
if (size == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (artifacts.globalUboScratch.size() < size) {
|
|
||||||
artifacts.globalUboScratch.resize(size);
|
|
||||||
}
|
|
||||||
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
|
||||||
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
|
||||||
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
|
||||||
// suffix before declaring the uniform unbacked.
|
|
||||||
auto locationIt = artifacts.uniformLocations.find(name);
|
|
||||||
if (locationIt == artifacts.uniformLocations.end()) {
|
|
||||||
locationIt = artifacts.uniformLocations.find(name + "[0]");
|
|
||||||
}
|
|
||||||
if (locationIt == artifacts.uniformLocations.end()) {
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u but not found in "
|
|
||||||
"uniformLocations",
|
|
||||||
in.externalIndex, name.c_str(), offset);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const Uint baseLocation = locationIt->second;
|
|
||||||
if (!ProgramObject::IsValidUniformLocation(artifacts, static_cast<Int>(baseLocation))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Int uniformIndex = artifacts.uniformIndexInTProgram[baseLocation];
|
|
||||||
const GLint arraySize = ProgramObject::GetUniformArraySizeByTIndex(artifacts, uniformIndex);
|
|
||||||
SizeT memberSize = 0;
|
|
||||||
const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name);
|
|
||||||
if (sizeIt != meta.plainUniformMemberSizesInBytes.end()) {
|
|
||||||
memberSize = sizeIt->second;
|
|
||||||
}
|
|
||||||
Uint arrayStride = 0;
|
|
||||||
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
|
|
||||||
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
|
|
||||||
arrayStride = strideIt->second;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Array uniforms span one location per element (see DoReflection);
|
|
||||||
// give each element its real byte offset inside the UBO.
|
|
||||||
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
|
|
||||||
for (GLint element = 0; element < elementCount; ++element) {
|
|
||||||
const Uint location = baseLocation + static_cast<Uint>(element);
|
|
||||||
if (location > artifacts.maxUniformLocation ||
|
|
||||||
artifacts.uniformIndexInTProgram[location] != uniformIndex) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
artifacts.uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
|
|
||||||
const SizeT consumed = static_cast<SizeT>(element) * arrayStride;
|
|
||||||
artifacts.uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0;
|
|
||||||
}
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u stride=%u size=%zu assigned "
|
|
||||||
"to locations %u..%u",
|
|
||||||
in.externalIndex, name.c_str(), offset, arrayStride, memberSize, baseLocation,
|
|
||||||
baseLocation + static_cast<Uint>(elementCount) - 1);
|
|
||||||
}
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - finished parsing module %zu metadata",
|
|
||||||
in.externalIndex, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback pass: a linked program's active non-opaque uniforms must accept
|
|
||||||
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
|
|
||||||
// them (AggressiveDCE can remove a dead loop together with the only loads of a
|
|
||||||
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
|
|
||||||
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
|
|
||||||
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
|
|
||||||
// never reads these bytes, so this only keeps the GL-visible state coherent.
|
|
||||||
for (Uint location = 0; location <= artifacts.maxUniformLocation; ++location) {
|
|
||||||
if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue;
|
|
||||||
if (!ProgramObject::IsValidUniformLocation(artifacts, static_cast<Int>(location))) continue;
|
|
||||||
const auto& uniform = artifacts.program->getUniform(artifacts.uniformIndexInTProgram[location]);
|
|
||||||
const glslang::TType* type = uniform.getType();
|
|
||||||
if (type != nullptr && type->isOpaque()) continue;
|
|
||||||
if (uniform.index >= 0 && uniform.index < artifacts.program->getNumUniformBlocks() &&
|
|
||||||
std::strstr(artifacts.program->getUniformBlock(uniform.index).name.c_str(),
|
|
||||||
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
|
|
||||||
// Member of a named uniform block: not settable through glUniform*, so it
|
|
||||||
// needs no global-UBO shadow storage.
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// std140-style slot: the matrix upload paths write column vectors at
|
|
||||||
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
|
||||||
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
|
||||||
if (type != nullptr && type->isMatrix()) {
|
|
||||||
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
|
|
||||||
}
|
|
||||||
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
|
||||||
const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
|
||||||
artifacts.globalUboScratch.resize(slotOffset + slotSize, 0);
|
|
||||||
artifacts.uniformOffsets[location] = static_cast<Uint>(slotOffset);
|
|
||||||
artifacts.uniformSizesInBytes[location] = slotSize;
|
|
||||||
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' location %u has no UBO backing in the "
|
|
||||||
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
|
|
||||||
in.externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
Bool ProgramLinkTask::ValidateFragmentOutputLocations() {
|
||||||
if (!artifacts.program) return false;
|
if (!artifacts.program) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,16 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
SharedPtr<const ShaderCompileTask> compiled;
|
SharedPtr<const ShaderCompileTask> compiled;
|
||||||
};
|
};
|
||||||
|
|
||||||
// The unit of asynchronous linking: one glLinkProgram's worth of pure CPU work - glslang
|
// PHASE A of one glLinkProgram: the half that decides what GL can be asked about the
|
||||||
// link + mapIO, SPIR-V generation and optimization, the GL-facing reflection surface, the
|
// program - glslang link + mapIO, the GL-facing reflection surface, fragment-output
|
||||||
// global-UBO routing tables, fragment-output validation and transform-feedback
|
// validation and transform-feedback resolution - with every input it needs snapshotted at
|
||||||
// resolution - with every input it needs snapshotted at enqueue.
|
// enqueue.
|
||||||
|
//
|
||||||
|
// Every one of the eight ways a link can fail lives here, so once this node has published
|
||||||
|
// through EnsureLinkJoined() the program's LINK_STATUS, info log and entire query surface
|
||||||
|
// are FINAL and truthful. SPIR-V generation, spirv-opt and the global-UBO routing tables
|
||||||
|
// moved to ProgramSpirvTask, which chains behind this node and is joined by only five
|
||||||
|
// getters (see ProgramObject::EnsureSpirvJoined).
|
||||||
//
|
//
|
||||||
// Same ownership rule as ShaderCompileTask: the body reads nothing but `in` (all of it
|
// Same ownership rule as ShaderCompileTask: the body reads nothing but `in` (all of it
|
||||||
// owned or immutable) and writes nothing but `artifacts`. No GL call, no
|
// owned or immutable) and writes nothing but `artifacts`. No GL call, no
|
||||||
@@ -40,11 +46,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// through the CompileEnv snapshot and diagnostics are deferred to the join.
|
// through the CompileEnv snapshot and diagnostics are deferred to the join.
|
||||||
//
|
//
|
||||||
// ONE LINK IS ONE HANDLER. RunBody() runs start to finish inside a single pool handler
|
// ONE LINK IS ONE HANDLER. RunBody() runs start to finish inside a single pool handler
|
||||||
// and is the only place `artifacts` is written. Do not split it across handlers to
|
// and is the only place `artifacts` is written. Splitting it across handlers to
|
||||||
// "pipeline" the reflection half: the intermediates that GlslangToSpv and buildReflection
|
// "pipeline" the reflection half would let a cancel land between the halves and publish a
|
||||||
// share are mutated in a strict order (see the GenerateSpirv-before-DoReflection comment
|
// program whose SPIR-V and reflection describe different things - so any such split has
|
||||||
// in Run()), and a second handler would let a cancel land between them and publish a
|
// to be structural: the first half must publish a LINK_STATUS and a query surface that
|
||||||
// program whose SPIR-V and reflection describe different things.
|
// are already final, and a lost second half must degrade to "linked but not drawable",
|
||||||
|
// never to a half-published program. (The intermediates' ordering constraint that used to
|
||||||
|
// be quoted here is retested and no longer binding; see the ordering note in RunBody.)
|
||||||
class ProgramLinkTask final : public MG_Util::Async::JobNode {
|
class ProgramLinkTask final : public MG_Util::Async::JobNode {
|
||||||
public:
|
public:
|
||||||
// ---- inputs, snapshotted on the GL thread in ProgramObject::Link()'s prologue ----
|
// ---- inputs, snapshotted on the GL thread in ProgramObject::Link()'s prologue ----
|
||||||
@@ -68,6 +76,49 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Moved (never copied) into the ProgramObject by EnsureLinkJoined().
|
// Moved (never copied) into the ProgramObject by EnsureLinkJoined().
|
||||||
ProgramObject::LinkArtifacts artifacts;
|
ProgramObject::LinkArtifacts artifacts;
|
||||||
|
|
||||||
|
// ---- output: everything ProgramSpirvTask needs to run without this node's
|
||||||
|
// artifacts, filled at the tail of a successful RunBody() ----
|
||||||
|
//
|
||||||
|
// THIS IS NOT `artifacts` AND MUST NOT BE MERGED INTO IT. The GL thread MOVES
|
||||||
|
// `artifacts` out of this node at the join, and phase B runs on a worker afterwards -
|
||||||
|
// so phase B may read `spirvHandoff` and `in` (neither is ever touched by the join)
|
||||||
|
// and this node's JobState, and nothing else on it. Reading `artifacts` or
|
||||||
|
// `diagnostics` from phase B would race the publish.
|
||||||
|
struct SpirvHandoff {
|
||||||
|
// MANDATORY, and the reason this struct exists at all: TProgram::addShader stores
|
||||||
|
// a RAW TShader*, and for the one-shader-per-stage case getIntermediate() returns
|
||||||
|
// the TShader's own intermediate rather than a copy. These used to die when
|
||||||
|
// RunBody() returned, which was safe only because nothing called getIntermediate()
|
||||||
|
// afterwards. GlslangToSpv does exactly that, so phase B has to own them.
|
||||||
|
//
|
||||||
|
// MEMORY NOTE: this is the one thing the split makes live LONGER than it used to -
|
||||||
|
// a glslang arena per stage, megabytes for a shaderpack, now alive from the end of
|
||||||
|
// phase A until phase B runs instead of dying with the link body, so a deep
|
||||||
|
// phase-B backlog holds one arena per queued program. Phase B clears this vector
|
||||||
|
// as soon as GlslangToSpv returns, but read that call site's comment before
|
||||||
|
// relying on it: for the COMMON case (a shader linked into exactly one program)
|
||||||
|
// the compile node co-owns the same TShader and phase A pins that node, so the
|
||||||
|
// clear frees nothing and only the re-parsed CAS-loser shaders are actually
|
||||||
|
// released. If peak RSS ever becomes the binding constraint on a pack load, THIS
|
||||||
|
// is the field to attack - by bounding the backlog, by releasing the compile
|
||||||
|
// node's own reference at claim time, or by moving GlslangToSpv back into phase A.
|
||||||
|
Vector<SharedPtr<glslang::TShader>> shaders;
|
||||||
|
// GL enum per entry of `in.shaders`, in the same order (GetSpirvBinaryFromProgram
|
||||||
|
// walks it to pick the intermediates).
|
||||||
|
Vector<GLenum> shaderTypes;
|
||||||
|
// The reflection slice BuildGlobalUboRouting consumes: {program, uniformLocations,
|
||||||
|
// uniformIndexInTProgram, tProgramUniformIndexToGl, maxUniformLocation}. Carried
|
||||||
|
// as a LinkArtifacts with only those five fields set, so the routing pass can keep
|
||||||
|
// calling ProgramObject::IsValidUniformLocation / GetUniformArraySizeByTIndex
|
||||||
|
// unchanged. The SharedPtr copy of `program` is also what keeps the TProgram alive
|
||||||
|
// for phase B after the join has moved `artifacts` away.
|
||||||
|
ProgramObject::LinkArtifacts reflection;
|
||||||
|
|
||||||
|
// The one flag phase B tests before doing anything: false means this link never
|
||||||
|
// reached the tail of RunBody (it failed, or was cancelled mid-body).
|
||||||
|
Bool ready = false;
|
||||||
|
} spirvHandoff;
|
||||||
|
|
||||||
// Posts this job once every compile in `deps` is terminal - and not one moment
|
// Posts this job once every compile in `deps` is terminal - and not one moment
|
||||||
// earlier, so the body never waits on anything (invariant I4: no job body may block
|
// earlier, so the body never waits on anything (invariant I4: no job body may block
|
||||||
// on another job, or the pool could deadlock with all its workers waiting on each
|
// on another job, or the pool could deadlock with all its workers waiting on each
|
||||||
@@ -94,8 +145,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Bool ValidateFragmentOutputLocations();
|
Bool ValidateFragmentOutputLocations();
|
||||||
Bool ResolveTransformFeedbackVaryings();
|
Bool ResolveTransformFeedbackVaryings();
|
||||||
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate);
|
||||||
void GenerateSpirv();
|
|
||||||
void BuildGlobalUboRouting();
|
|
||||||
|
|
||||||
// Worker-side MGLOG replacement: appended to diagnostics.logLines and replayed by the
|
// Worker-side MGLOG replacement: appended to diagnostics.logLines and replayed by the
|
||||||
// join, on the GL thread, where a serial implementation would have printed it.
|
// join, on the GL thread, where a serial implementation would have printed it.
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
#include "ProgramObject.h"
|
#include "ProgramObject.h"
|
||||||
#include "ProgramLinkTask.h"
|
#include "ProgramLinkTask.h"
|
||||||
|
#include "ProgramSpirvTask.h"
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <cstring>
|
||||||
#include <MG_Util/Async/ShaderCompilePool.h>
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
|
||||||
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
|
||||||
@@ -68,12 +70,129 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
|
|
||||||
Bool ProgramObject::IsPendingLinkTerminal() const { return m_pendingLink->IsTerminal(); }
|
Bool ProgramObject::IsPendingLinkTerminal() const { return m_pendingLink->IsTerminal(); }
|
||||||
|
|
||||||
|
Bool ProgramObject::IsPendingSpirvTerminal() const { return m_pendingSpirv->IsTerminal(); }
|
||||||
|
|
||||||
|
void ProgramObject::JoinPendingSpirv() const {
|
||||||
|
MOBILEGL_ASSERT(!MG_Util::Async::ShaderCompilePool::IsPoolThread(),
|
||||||
|
"ProgramObject::EnsureSpirvJoined() reached from a pool thread; a job body must never read "
|
||||||
|
"GL-thread-owned objects");
|
||||||
|
|
||||||
|
// Move the node out FIRST, for the same reason JoinPendingLink does: everything below
|
||||||
|
// runs GL-thread-only code that reads program state, and with m_pendingSpirv still set
|
||||||
|
// that would re-enter this function.
|
||||||
|
const SharedPtr<ProgramSpirvTask> pending = Move(m_pendingSpirv);
|
||||||
|
m_pendingSpirv.reset();
|
||||||
|
|
||||||
|
pending->Wait();
|
||||||
|
if (pending->IsComplete()) {
|
||||||
|
m_spirv = Move(pending->artifacts);
|
||||||
|
}
|
||||||
|
// A node that settled as Cancelled published nothing, so m_spirv stays empty with
|
||||||
|
// spirvStatus false: linked, queryable, not drawable. Nothing to repair.
|
||||||
|
|
||||||
|
// Before the version bump, and before any caller can read the shadow: the writes the
|
||||||
|
// application made while the layout did not exist yet.
|
||||||
|
ReplayBufferedUniformWrites();
|
||||||
|
|
||||||
|
// The THIRD version bump of this link (enqueue, phase-A publish, phase-B publish), and
|
||||||
|
// it is mandatory for exactly the reason the phase-A one is (see JoinPendingLink): a
|
||||||
|
// backend memo taken during the A->B window - when the program was already answering
|
||||||
|
// as linked but had no SPIR-V and no uniform shadow - must not survive the arrival of
|
||||||
|
// either. The memos at risk are keyed on (lifetimeId, backendStateVersion).
|
||||||
|
BumpLinkObservableVersions();
|
||||||
|
|
||||||
|
MG_Util::Async::ApplyDeferredDiagnostics(*pending);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bool ProgramObject::BufferUniformWrite(const Uint location, const SizeT byteOffsetInUniform, const void* source,
|
||||||
|
const SizeT byteSize) {
|
||||||
|
if (source == nullptr || byteSize == 0) return true; // nothing to record, nothing to join for
|
||||||
|
if (m_pendingUniformBytes.size() + byteSize > kMaxBufferedUniformBytes) {
|
||||||
|
// Pressure valve: stop growing and let the caller take the join. Say so once per
|
||||||
|
// program, because the interesting fact is WHICH program did it.
|
||||||
|
MGLOG_D("ProgramObject %u: buffered uniform writes exceeded %zu bytes during the SPIR-V window; the "
|
||||||
|
"write joins instead",
|
||||||
|
m_externalIndex, kMaxBufferedUniformBytes);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const SizeT dataOffset = m_pendingUniformBytes.size();
|
||||||
|
m_pendingUniformBytes.resize(dataOffset + byteSize);
|
||||||
|
std::memcpy(m_pendingUniformBytes.data() + dataOffset, source, byteSize);
|
||||||
|
m_pendingUniformWrites.push_back(PendingUniformWrite{.location = location,
|
||||||
|
.byteOffsetInUniform =
|
||||||
|
static_cast<Uint>(byteOffsetInUniform),
|
||||||
|
.byteSize = static_cast<Uint>(byteSize),
|
||||||
|
.dataOffset = static_cast<Uint>(dataOffset)});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramObject::ReplayBufferedUniformWrites() const {
|
||||||
|
if (m_pendingUniformWrites.empty()) {
|
||||||
|
m_pendingUniformBytes.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drain into locals first: MarkUBOContentDirty below is a plain counter bump, but a
|
||||||
|
// future reader of this function should not be able to observe a half-drained buffer.
|
||||||
|
Vector<PendingUniformWrite> writes;
|
||||||
|
Vector<Uint8> bytes;
|
||||||
|
writes.swap(m_pendingUniformWrites);
|
||||||
|
bytes.swap(m_pendingUniformBytes);
|
||||||
|
|
||||||
|
if (m_spirv.globalUboScratch.empty() || m_spirv.uniformOffsets.empty()) {
|
||||||
|
// Phase B produced nothing (cancelled at teardown, or a relink superseded it).
|
||||||
|
// The program is not drawable, so there is nowhere for these to land and nothing
|
||||||
|
// that could observe them.
|
||||||
|
MGLOG_D("ProgramObject %u: dropping %zu buffered uniform write(s); the SPIR-V job published no shadow",
|
||||||
|
m_externalIndex, writes.size());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8* const scratch = m_spirv.globalUboScratch.data();
|
||||||
|
const SizeT uboSize = m_spirv.globalUboScratch.size();
|
||||||
|
for (const PendingUniformWrite& write : writes) {
|
||||||
|
if (write.location >= m_spirv.uniformOffsets.size()) continue;
|
||||||
|
const Uint offset = m_spirv.uniformOffsets[write.location];
|
||||||
|
if (offset == kInvalidUniformOffset ||
|
||||||
|
static_cast<SizeT>(offset) + write.byteOffsetInUniform + write.byteSize > uboSize) {
|
||||||
|
// Same verdict the live write path reaches for a uniform without backing
|
||||||
|
// storage: log and drop, rather than fault.
|
||||||
|
MGLOG_E("ProgramObject %u: buffered uniform write at location %u has no backing storage "
|
||||||
|
"(offset=%u size=%u uboSize=%zu); dropping write",
|
||||||
|
m_externalIndex, write.location, offset, write.byteSize, uboSize);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Uint8* const destination = scratch + offset + write.byteOffsetInUniform;
|
||||||
|
const Uint8* const sourceBytes = bytes.data() + write.dataOffset;
|
||||||
|
// The same bytes-equal dedupe the live path applies, per record and in order, so
|
||||||
|
// the "an identical write does not move the content version" property survives
|
||||||
|
// the detour byte for byte.
|
||||||
|
if (std::memcmp(destination, sourceBytes, write.byteSize) == 0) continue;
|
||||||
|
std::memcpy(destination, sourceBytes, write.byteSize);
|
||||||
|
MarkUBOContentDirty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ProgramObject::CancelLink() {
|
void ProgramObject::CancelLink() {
|
||||||
|
// Phase B first: it is chained behind phase A, so cancelling A would otherwise run A's
|
||||||
|
// continuation and post a node this call is about to abandon anyway. Cancelling it up
|
||||||
|
// front makes that continuation a no-op.
|
||||||
|
//
|
||||||
|
// Cooperative and non-blocking, both of them. A node that no worker has picked up
|
||||||
|
// settles immediately; one that is running is flagged and settles when its body
|
||||||
|
// returns, writing only into itself the whole time. Either way nothing waits, and each
|
||||||
|
// node keeps its own inputs alive for as long as it needs them.
|
||||||
|
if (m_pendingSpirv) {
|
||||||
|
m_pendingSpirv->Cancel();
|
||||||
|
m_pendingSpirv.reset();
|
||||||
|
// Buffered writes belong to the link that is being abandoned. A relink resets
|
||||||
|
// every uniform to its initial value anyway (GL 4.6 core 7.6), and the other two
|
||||||
|
// callers are destruction and glProgramBinary's mandated failure, so there is
|
||||||
|
// nothing left that could want them.
|
||||||
|
m_pendingUniformWrites.clear();
|
||||||
|
m_pendingUniformBytes.clear();
|
||||||
|
}
|
||||||
if (!m_pendingLink) return;
|
if (!m_pendingLink) return;
|
||||||
// Cooperative and non-blocking. A node that no worker has picked up settles
|
|
||||||
// immediately; one that is running is flagged and settles when its body returns,
|
|
||||||
// writing only into itself the whole time. Either way nothing waits, and the node
|
|
||||||
// keeps its own inputs alive for as long as it needs them.
|
|
||||||
m_pendingLink->Cancel();
|
m_pendingLink->Cancel();
|
||||||
m_pendingLink.reset();
|
m_pendingLink.reset();
|
||||||
}
|
}
|
||||||
@@ -103,8 +222,12 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// function has ever cleared, and its callers depend on that (they write infoLog
|
// function has ever cleared, and its callers depend on that (they write infoLog
|
||||||
// immediately AFTER calling here). Link()'s prologue does not use this - it assigns a
|
// immediately AFTER calling here). Link()'s prologue does not use this - it assigns a
|
||||||
// whole default-constructed block, where the ordering is explicit.
|
// whole default-constructed block, where the ordering is explicit.
|
||||||
|
// Phase-B output (generatedSpirv / uniformOffsets / globalUboScratch) is NOT cleared
|
||||||
|
// here and is not in LinkArtifacts at all: the link body calls this on its own block,
|
||||||
|
// where no phase-B output exists yet. The two GL-thread callers that also have to
|
||||||
|
// discard phase-B output say so themselves (MarkLinkFailedByProgramBinary clears
|
||||||
|
// m_spirv; Link()'s prologue assigns a fresh one).
|
||||||
artifacts.program.reset();
|
artifacts.program.reset();
|
||||||
artifacts.generatedSpirv.clear();
|
|
||||||
artifacts.uniformLocations.clear();
|
artifacts.uniformLocations.clear();
|
||||||
artifacts.glUniformIndexToTProgram.clear();
|
artifacts.glUniformIndexToTProgram.clear();
|
||||||
artifacts.tProgramUniformIndexToGl.clear();
|
artifacts.tProgramUniformIndexToGl.clear();
|
||||||
@@ -117,9 +240,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
artifacts.uniformBlockIndexByName.clear();
|
artifacts.uniformBlockIndexByName.clear();
|
||||||
artifacts.uniformBlockBinding.clear();
|
artifacts.uniformBlockBinding.clear();
|
||||||
artifacts.shaderStorageBlockBinding.clear();
|
artifacts.shaderStorageBlockBinding.clear();
|
||||||
artifacts.uniformOffsets.clear();
|
|
||||||
artifacts.uniformSizesInBytes.clear();
|
|
||||||
artifacts.globalUboScratch.clear();
|
|
||||||
artifacts.attribs.clear();
|
artifacts.attribs.clear();
|
||||||
artifacts.attribTypes.clear();
|
artifacts.attribTypes.clear();
|
||||||
artifacts.activeUniformCount = 0;
|
artifacts.activeUniformCount = 0;
|
||||||
@@ -238,6 +358,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// is what every gated reader sees, so it has to be the complete "not linked" state -
|
// is what every gated reader sees, so it has to be the complete "not linked" state -
|
||||||
// including the fields ResetLinkArtifacts deliberately preserves for its own callers.
|
// including the fields ResetLinkArtifacts deliberately preserves for its own callers.
|
||||||
m_artifacts = {};
|
m_artifacts = {};
|
||||||
|
m_spirv = {};
|
||||||
|
|
||||||
// ---- GL-thread-owned mutations ----
|
// ---- GL-thread-owned mutations ----
|
||||||
// Remove detached shaders first
|
// Remove detached shaders first
|
||||||
@@ -292,17 +413,33 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
task->in.shaders.push_back({shader->GetShaderStage(), shader->GetShaderSourcePtr(), node});
|
task->in.shaders.push_back({shader->GetShaderStage(), shader->GetShaderSourcePtr(), node});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase B of the same link: SPIR-V generation, spirv-opt and the global-UBO routing
|
||||||
|
// tables. Created here, alongside phase A, so that from this instant the program has
|
||||||
|
// BOTH pending nodes and every cancel site (this prologue, ~ProgramObject,
|
||||||
|
// glProgramBinary's failure) drops both through the one CancelLink().
|
||||||
|
auto spirvTask = MakeShared<ProgramSpirvTask>();
|
||||||
m_pendingLink = task;
|
m_pendingLink = task;
|
||||||
|
m_pendingSpirv = spirvTask;
|
||||||
|
|
||||||
// Flag off - or glMaxShaderCompilerThreadsKHR(0), see AsyncShaderCompileActive():
|
// Flag off - or glMaxShaderCompilerThreadsKHR(0), see AsyncShaderCompileActive():
|
||||||
// byte-identical to the synchronous implementation. RunInline() executes the same
|
// byte-identical to the synchronous implementation. RunInline() executes the same
|
||||||
// body on this thread and the join below publishes through the same code, so the two
|
// bodies on this thread, in the same order, and the join below publishes through the
|
||||||
// modes differ only in WHICH thread ran RunBody().
|
// same code, so the two modes differ only in WHICH thread ran them.
|
||||||
|
//
|
||||||
|
// Deliberately NOT expressed as SubmitAfter here: its continuation posts to the pool,
|
||||||
|
// and in this mode the pool is merely unused rather than stopped - the work would
|
||||||
|
// silently move off-thread in the one mode whose whole contract is that it does not.
|
||||||
if (!MG_Util::Async::AsyncShaderCompileActive()) {
|
if (!MG_Util::Async::AsyncShaderCompileActive()) {
|
||||||
task->RunInline();
|
task->RunInline();
|
||||||
EnsureLinkJoined();
|
spirvTask->RunInlineAfter(task);
|
||||||
|
EnsureSpirvJoined();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// The chain edge FIRST, while phase A is still Pending, so registering it is a plain
|
||||||
|
// list append rather than an inline continuation on this thread. If SubmitAfter below
|
||||||
|
// then fails to post phase A it cancels it, and that cancel fires this edge, which
|
||||||
|
// cancels phase B - nothing is left stranded either way.
|
||||||
|
spirvTask->SubmitAfter(task);
|
||||||
task->SubmitAfter(deps);
|
task->SubmitAfter(deps);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// ProgramLinkTask.h includes THIS header (it outputs a LinkArtifacts), so including it
|
// ProgramLinkTask.h includes THIS header (it outputs a LinkArtifacts), so including it
|
||||||
// back would be circular. The destructor is therefore out of line.
|
// back would be circular. The destructor is therefore out of line.
|
||||||
class ProgramLinkTask;
|
class ProgramLinkTask;
|
||||||
|
// Phase B of the same link: SPIR-V generation, spirv-opt and the global-UBO routing
|
||||||
|
// tables. Chained behind the ProgramLinkTask, forward-declared for the same reason.
|
||||||
|
class ProgramSpirvTask;
|
||||||
|
|
||||||
class ProgramObject {
|
class ProgramObject {
|
||||||
public:
|
public:
|
||||||
@@ -303,7 +306,25 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Sentinel for a uniform location without global-UBO backing storage (should not
|
// Sentinel for a uniform location without global-UBO backing storage (should not
|
||||||
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
// survive linking: GenerateBinary falls back to tail-allocated scratch storage).
|
||||||
static constexpr Uint kInvalidUniformOffset = ~0u;
|
static constexpr Uint kInvalidUniformOffset = ~0u;
|
||||||
Uint GetUniformOffset(Uint location) const { return Artifacts().uniformOffsets[location]; }
|
// PHASE B (joins the SPIR-V job; see EnsureSpirvJoined).
|
||||||
|
//
|
||||||
|
// BOUNDS-CHECKED, and that is not defensive padding - it is the load-bearing half of
|
||||||
|
// the "linked but not drawable" contract. A phase B that settles CANCELLED rather than
|
||||||
|
// Complete (its body threw, the pool failed to enqueue it, or teardown cancelled it
|
||||||
|
// while phase A had already published) publishes nothing, so the shadow is a
|
||||||
|
// default-constructed SpirvArtifacts with an EMPTY uniformOffsets - while LINK_STATUS
|
||||||
|
// stays GL_TRUE, because GL gives no way to retract one, and IsValidUniformLocation()
|
||||||
|
// keeps answering true out of phase-A reflection. Every glUniform*/glGetUniform* call
|
||||||
|
// site reaches this getter BEFORE its own kInvalidUniformOffset / null-scratch guard,
|
||||||
|
// so an unchecked operator[] here would be a null dereference on the query surface
|
||||||
|
// this design promises stays answerable. Reporting kInvalidUniformOffset instead hands
|
||||||
|
// each of those sites exactly the value their existing guard already handles - the
|
||||||
|
// same value the routing pass itself uses for a uniform the optimizer deleted.
|
||||||
|
Uint GetUniformOffset(Uint location) const {
|
||||||
|
const SpirvArtifacts& spirv = Spirv();
|
||||||
|
return location < spirv.uniformOffsets.size() ? spirv.uniformOffsets[location]
|
||||||
|
: kInvalidUniformOffset;
|
||||||
|
}
|
||||||
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); }
|
||||||
|
|
||||||
Int GetAttributeLocation(const String& name) {
|
Int GetAttributeLocation(const String& name) {
|
||||||
@@ -381,9 +402,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
const String& GetActiveAttribName(Uint index) const {
|
const String& GetActiveAttribName(Uint index) const {
|
||||||
return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast<Int>(index)).name);
|
return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast<Int>(index)).name);
|
||||||
}
|
}
|
||||||
void* MapUBO() { return Artifacts().globalUboScratch.data(); }
|
// PHASE B, all three (see EnsureSpirvJoined): the shadow buffer's layout is decided
|
||||||
const void* GetUBOData() const { return Artifacts().globalUboScratch.data(); }
|
// by the OPTIMIZED SPIR-V, so it does not exist until the SPIR-V job has settled - and
|
||||||
Uint GetUBOSize() const { return static_cast<Uint>(Artifacts().globalUboScratch.size()); }
|
// never exists at all for a program whose SPIR-V job settled cancelled. These three
|
||||||
|
// degrade to nullptr/nullptr/0 in that case, which is exactly the "no backing storage"
|
||||||
|
// shape every caller already tests for (see GetUniformOffset's note).
|
||||||
|
void* MapUBO() { return Spirv().globalUboScratch.data(); }
|
||||||
|
const void* GetUBOData() const { return Spirv().globalUboScratch.data(); }
|
||||||
|
Uint GetUBOSize() const { return static_cast<Uint>(Spirv().globalUboScratch.size()); }
|
||||||
// Content version of the CPU-side global-UBO shadow: writers bump it so backends
|
// Content version of the CPU-side global-UBO shadow: writers bump it so backends
|
||||||
// can skip re-uploading an unchanged UBO on every draw. ~0u is reserved as the
|
// can skip re-uploading an unchanged UBO on every draw. ~0u is reserved as the
|
||||||
// backends' "never uploaded" sentinel, so skip over it on wrap.
|
// backends' "never uploaded" sentinel, so skip over it on wrap.
|
||||||
@@ -391,6 +417,25 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
void MarkUBOContentDirty() const {
|
void MarkUBOContentDirty() const {
|
||||||
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
if (++m_uboContentVersion == ~0u) m_uboContentVersion = 0;
|
||||||
}
|
}
|
||||||
|
// ---- glUniform* inside the phase-A -> phase-B window ----
|
||||||
|
//
|
||||||
|
// True while the program is fully linked and fully queryable but its uniform shadow's
|
||||||
|
// LAYOUT (which the optimized SPIR-V decides) does not exist yet. A non-opaque
|
||||||
|
// glUniform* write in that window is RECORDED rather than joined, and replayed into
|
||||||
|
// the shadow at the phase-B publish - so a pack that sets its uniforms immediately
|
||||||
|
// after glLinkProgram never waits for SPIR-V.
|
||||||
|
//
|
||||||
|
// Nothing can observe the difference: the only route to those bytes is glGetUniform*
|
||||||
|
// (and a draw), and both of those go through the phase-B gate, which replays first.
|
||||||
|
// The OPAQUE branch of glUniform* is deliberately not buffered - a sampler unit is
|
||||||
|
// phase-A state (uniformSamplerOrImageUnitIndex), so glUniform1i(samplerLoc, unit)
|
||||||
|
// right after a link stays a zero-join operation, which is exactly what Iris does.
|
||||||
|
Bool IsSpirvPending() const { return m_pendingSpirv != nullptr; }
|
||||||
|
// Records one write. Returns false if it declined to buffer - the caller must then
|
||||||
|
// perform the write directly (which joins). Declining is the pressure valve for an
|
||||||
|
// application that writes megabytes of uniforms into a single pending window.
|
||||||
|
Bool BufferUniformWrite(Uint location, SizeT byteOffsetInUniform, const void* source, SizeT byteSize);
|
||||||
|
|
||||||
Uint32 GetBackendStateVersion() const { return m_backendStateVersion; }
|
Uint32 GetBackendStateVersion() const { return m_backendStateVersion; }
|
||||||
// Bumped only by (re)linking — lets backends detect that every piece of
|
// Bumped only by (re)linking — lets backends detect that every piece of
|
||||||
// link-derived reflection (locations, block order, UBO layout) is stale.
|
// link-derived reflection (locations, block order, UBO layout) is stale.
|
||||||
@@ -463,6 +508,11 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
CancelLink();
|
CancelLink();
|
||||||
BumpLinkObservableVersions();
|
BumpLinkObservableVersions();
|
||||||
ResetLinkArtifacts(Artifacts());
|
ResetLinkArtifacts(Artifacts());
|
||||||
|
// ResetLinkArtifacts is a LinkArtifacts-only operation (the link body calls it on
|
||||||
|
// its own block, where no phase-B output exists yet), so the phase-B half is
|
||||||
|
// cleared here. CancelLink() above already dropped the pending SPIR-V job, so
|
||||||
|
// this cannot be racing a publish.
|
||||||
|
m_spirv = {};
|
||||||
Artifacts().infoLog = "No program binary format is supported.";
|
Artifacts().infoLog = "No program binary format is supported.";
|
||||||
}
|
}
|
||||||
Bool GetValidateStatus() const { return m_validateStatus; }
|
Bool GetValidateStatus() const { return m_validateStatus; }
|
||||||
@@ -571,8 +621,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
return Artifacts().shaderStorageBlockBinding;
|
return Artifacts().shaderStorageBlockBinding;
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; }
|
// PHASE B (see EnsureSpirvJoined). Empty for a program whose SPIR-V job was
|
||||||
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Artifacts().generatedSpirv; }
|
// cancelled; GetSpirvStatus() below is how a backend tells that apart from a program
|
||||||
|
// that never linked.
|
||||||
|
Vector<Vector<unsigned>>& GetGeneratedSpirv() { return Spirv().generatedSpirv; }
|
||||||
|
const Vector<Vector<unsigned>>& GetGeneratedSpirv() const { return Spirv().generatedSpirv; }
|
||||||
|
// Whether phase B produced usable SPIR-V. Joins, like the four getters above: a
|
||||||
|
// backend asks this exactly where it used to ask GetLinkStatus(), i.e. right before
|
||||||
|
// it builds or draws with the program.
|
||||||
|
Bool GetSpirvStatus() const { return Spirv().spirvStatus; }
|
||||||
|
|
||||||
// The linked glslang reflection itself, for the ONE consumer that needs resource
|
// The linked glslang reflection itself, for the ONE consumer that needs resource
|
||||||
// lists no typed getter above exposes: the GL program-interface query layer
|
// lists no typed getter above exposes: the GL program-interface query layer
|
||||||
@@ -620,7 +677,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// without going through the gate.
|
// without going through the gate.
|
||||||
struct LinkArtifacts {
|
struct LinkArtifacts {
|
||||||
SharedPtr<glslang::TProgram> program;
|
SharedPtr<glslang::TProgram> program;
|
||||||
Vector<Vector<unsigned>> generatedSpirv;
|
|
||||||
|
|
||||||
// Attributes (Vertex in)
|
// Attributes (Vertex in)
|
||||||
Vector<String> attribs;
|
Vector<String> attribs;
|
||||||
@@ -664,11 +720,6 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
// SetShaderStorageBlockBinding for why this one is by name and not by index.
|
||||||
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
UnorderedMap<String, Int> shaderStorageBlockBinding;
|
||||||
|
|
||||||
// Need to be reflected after linking of SPIR-V binary
|
|
||||||
Vector<Uint> uniformOffsets;
|
|
||||||
Vector<Uint> uniformSizesInBytes;
|
|
||||||
Vector<Uint8> globalUboScratch;
|
|
||||||
|
|
||||||
Uint activeUniformCount = 0;
|
Uint activeUniformCount = 0;
|
||||||
Uint maxUniformLocation = 0;
|
Uint maxUniformLocation = 0;
|
||||||
Int uniformNameMaxLength = 0;
|
Int uniformNameMaxLength = 0;
|
||||||
@@ -697,6 +748,35 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
Uint32 xfbPackedStride = 0;
|
Uint32 xfbPackedStride = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ---- everything phase B of a link produces, in one movable block ----
|
||||||
|
//
|
||||||
|
// The membership rule is the same mechanical one LinkArtifacts uses: this is exactly
|
||||||
|
// what ProgramSpirvTask writes, which is what makes moving it THE publish. It is
|
||||||
|
// deliberately NOT part of LinkArtifacts, and that separation is what routes the five
|
||||||
|
// readers of SPIR-V-derived data through their own join gate by compiler rather than
|
||||||
|
// by review - m_spirv is private and Spirv() is the only spelling that reaches it.
|
||||||
|
//
|
||||||
|
// Why these three and nothing else: `generatedSpirv` has no GL-thread reader at all
|
||||||
|
// (every consumer is a backend draw/prepare path), and `uniformOffsets` +
|
||||||
|
// `globalUboScratch` are the ONLY things glUniform*/glGetUniform* need that are
|
||||||
|
// derived from the OPTIMIZED SPIR-V rather than from glslang reflection - spirv-opt
|
||||||
|
// runs in place and can delete a uniform, or the whole global UBO, so the offsets
|
||||||
|
// cannot be lifted out of glslang's reflection instead.
|
||||||
|
struct SpirvArtifacts {
|
||||||
|
Vector<Vector<unsigned>> generatedSpirv;
|
||||||
|
// Byte offset of each uniform location inside globalUboScratch, or
|
||||||
|
// kInvalidUniformOffset. Sized maxUniformLocation + 1 by the routing pass.
|
||||||
|
Vector<Uint> uniformOffsets;
|
||||||
|
Vector<Uint8> globalUboScratch;
|
||||||
|
// False for a program whose SPIR-V was never produced (phase B cancelled at
|
||||||
|
// teardown or by a relink) or whose optimizer run failed. GL has no way to
|
||||||
|
// retract a LINK_STATUS it already reported true, so such a program stays
|
||||||
|
// "linked" and every reflection answer it has given stays correct - it is simply
|
||||||
|
// not drawable, which the backends already express through their link-status
|
||||||
|
// gates.
|
||||||
|
Bool spirvStatus = false;
|
||||||
|
};
|
||||||
|
|
||||||
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
// ---- artifacts-only helpers, shared with ProgramLinkTask ----
|
||||||
// Static and taking the block explicitly, because from stage 4 the link BODY needs
|
// Static and taking the block explicitly, because from stage 4 the link BODY needs
|
||||||
// them while its artifacts still live on the job node, not on any ProgramObject. The
|
// them while its artifacts still live on the job node, not on any ProgramObject. The
|
||||||
@@ -736,9 +816,20 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Blocks until a pending link has published its artifacts. Public because a few call
|
// Blocks until a pending link has published its artifacts. Public because a few call
|
||||||
// sites have to join without reading anything - see the explicit-join list (J1-J8) in
|
// sites have to join without reading anything - see the explicit-join list (J1-J8) in
|
||||||
// the P1 design. GL thread only.
|
// the P1 design. GL thread only.
|
||||||
|
//
|
||||||
|
// PHASE A ONLY. After this returns, LINK_STATUS and the whole GL query surface are
|
||||||
|
// final and truthful, but the SPIR-V and the uniform shadow may still be in flight.
|
||||||
void JoinLink() const { EnsureLinkJoined(); }
|
void JoinLink() const { EnsureLinkJoined(); }
|
||||||
|
|
||||||
// Drops a link that is still in flight, without waiting for it. Called at the points
|
// Both phases. The draw path uses this, and must: the backends sample lifetimeId /
|
||||||
|
// backendStateVersion / the UBO content version OUTSIDE the gate, so a draw that
|
||||||
|
// joined only phase A would sample a version, join phase B later inside the same draw
|
||||||
|
// (through GetGeneratedSpirv), and memoize under a version the phase-B publish had
|
||||||
|
// already superseded - the exact lost-invalidation hazard J1 exists to prevent.
|
||||||
|
void JoinLinkAndSpirv() const { EnsureSpirvJoined(); }
|
||||||
|
|
||||||
|
// Drops BOTH phases of a link that is still in flight, without waiting for either.
|
||||||
|
// Called at the points
|
||||||
// where the pending link's result stops being the answer to "what did this program
|
// where the pending link's result stops being the answer to "what did this program
|
||||||
// link to": a re-link supersedes it, glProgramBinary must force LINK_STATUS false,
|
// link to": a re-link supersedes it, glProgramBinary must force LINK_STATUS false,
|
||||||
// and a destroyed program has no observers left.
|
// and a destroyed program has no observers left.
|
||||||
@@ -757,7 +848,15 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// MUST NOT JOIN - this is what GL_COMPLETION_STATUS_KHR reads when the extension
|
// MUST NOT JOIN - this is what GL_COMPLETION_STATUS_KHR reads when the extension
|
||||||
// surface lands. "No job at all" counts as complete: there is nothing outstanding to
|
// surface lands. "No job at all" counts as complete: there is nothing outstanding to
|
||||||
// wait for.
|
// wait for.
|
||||||
Bool IsLinkComplete() const { return m_pendingLink == nullptr || IsPendingLinkTerminal(); }
|
//
|
||||||
|
// BOTH phases, deliberately: an application that polls GL_COMPLETION_STATUS_KHR 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.
|
||||||
|
Bool IsLinkComplete() const { return IsPhaseALinkComplete() && IsSpirvComplete(); }
|
||||||
|
// Phase A alone, for the callers that only care about the query surface (and for the
|
||||||
|
// tests that pin the two phases apart).
|
||||||
|
Bool IsPhaseALinkComplete() const { return m_pendingLink == nullptr || IsPendingLinkTerminal(); }
|
||||||
|
Bool IsSpirvComplete() const { return m_pendingSpirv == nullptr || IsPendingSpirvTerminal(); }
|
||||||
|
|
||||||
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
|
void SetTransformFeedbackVaryings(Vector<String>&& names, GLenum bufferMode) {
|
||||||
m_requestedXfbVaryings = Move(names);
|
m_requestedXfbVaryings = Move(names);
|
||||||
@@ -824,6 +923,40 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// node's state goes through this out-of-line helper.
|
// node's state goes through this out-of-line helper.
|
||||||
Bool IsPendingLinkTerminal() const;
|
Bool IsPendingLinkTerminal() const;
|
||||||
|
|
||||||
|
// ---- the second join gate: phase-B (SPIR-V) output only ----
|
||||||
|
// Phase A FIRST, always. Two reasons: the phase-B publish replays the uniform writes
|
||||||
|
// that were buffered during its window, and those need the phase-A reflection to
|
||||||
|
// validate against; and a caller that reaches a phase-B getter without having settled
|
||||||
|
// phase A would otherwise leave the link half-published.
|
||||||
|
//
|
||||||
|
// Same inline/out-of-line split as the phase-A gate, for the same reason: the five
|
||||||
|
// getters behind this one include the per-draw uniform upload path.
|
||||||
|
void EnsureSpirvJoined() const {
|
||||||
|
if (m_pendingLink) JoinPendingLink();
|
||||||
|
if (m_pendingSpirv) JoinPendingSpirv();
|
||||||
|
}
|
||||||
|
void JoinPendingSpirv() const;
|
||||||
|
Bool IsPendingSpirvTerminal() const;
|
||||||
|
|
||||||
|
// One buffered non-opaque glUniform* write. `dataOffset` indexes m_pendingUniformBytes,
|
||||||
|
// which is one append-only blob rather than a per-record allocation.
|
||||||
|
struct PendingUniformWrite {
|
||||||
|
Uint location = 0;
|
||||||
|
Uint byteOffsetInUniform = 0;
|
||||||
|
Uint byteSize = 0;
|
||||||
|
Uint dataOffset = 0;
|
||||||
|
};
|
||||||
|
// Replays the buffer into the freshly published shadow, in write order, and drains it.
|
||||||
|
// Each record re-does the bounds check and the bytes-equal dedupe the live write path
|
||||||
|
// performs, so "an identical write does not move the content version" survives the
|
||||||
|
// detour exactly - and a record that really does change bytes moves the version, which
|
||||||
|
// is what makes a backend re-upload the UBO it cached during the window.
|
||||||
|
void ReplayBufferedUniformWrites() const;
|
||||||
|
// Past this, BufferUniformWrite declines and the write joins instead. Sized so an
|
||||||
|
// ordinary pack load never reaches it (a pending window is one program's worth of
|
||||||
|
// uniforms) while a pathological writer cannot grow the heap without bound.
|
||||||
|
static constexpr SizeT kMaxBufferedUniformBytes = 4u << 20;
|
||||||
|
|
||||||
LinkArtifacts& Artifacts() {
|
LinkArtifacts& Artifacts() {
|
||||||
EnsureLinkJoined();
|
EnsureLinkJoined();
|
||||||
return m_artifacts;
|
return m_artifacts;
|
||||||
@@ -832,6 +965,14 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
EnsureLinkJoined();
|
EnsureLinkJoined();
|
||||||
return m_artifacts;
|
return m_artifacts;
|
||||||
}
|
}
|
||||||
|
SpirvArtifacts& Spirv() {
|
||||||
|
EnsureSpirvJoined();
|
||||||
|
return m_spirv;
|
||||||
|
}
|
||||||
|
const SpirvArtifacts& Spirv() const {
|
||||||
|
EnsureSpirvJoined();
|
||||||
|
return m_spirv;
|
||||||
|
}
|
||||||
|
|
||||||
// GL-thread-only companion to ResetLinkArtifacts (see its definition). Const because
|
// GL-thread-only companion to ResetLinkArtifacts (see its definition). Const because
|
||||||
// the publish half of the join calls it; see the mutable counters below.
|
// the publish half of the join calls it; see the mutable counters below.
|
||||||
@@ -899,10 +1040,22 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// Mutable because publishing is a READ-side operation: a const getter has to be able
|
// Mutable because publishing is a READ-side operation: a const getter has to be able
|
||||||
// to settle an outstanding link before answering it.
|
// to settle an outstanding link before answering it.
|
||||||
mutable LinkArtifacts m_artifacts;
|
mutable LinkArtifacts m_artifacts;
|
||||||
|
// Phase-B output. Same mutability argument as m_artifacts, reached only through
|
||||||
|
// Spirv().
|
||||||
|
mutable SpirvArtifacts m_spirv;
|
||||||
|
|
||||||
// The link job, from enqueue until the first observable read pulls its result. Null
|
// The link job, from enqueue until the first observable read pulls its result. Null
|
||||||
// means m_artifacts is already the answer - which is the state every reader outside
|
// means m_artifacts is already the answer - which is the state every reader outside
|
||||||
// the pending window sees, and the whole reason the gate above is one branch.
|
// the pending window sees, and the whole reason the gate above is one branch.
|
||||||
mutable SharedPtr<ProgramLinkTask> m_pendingLink;
|
mutable SharedPtr<ProgramLinkTask> m_pendingLink;
|
||||||
|
// The SPIR-V job, chained behind m_pendingLink. Null means m_spirv is already the
|
||||||
|
// answer. A program can be in the window where m_pendingLink is already null (phase A
|
||||||
|
// published, the query surface is live) while this is still set.
|
||||||
|
mutable SharedPtr<ProgramSpirvTask> m_pendingSpirv;
|
||||||
|
// glUniform* writes taken while m_pendingSpirv was set, in call order, plus their
|
||||||
|
// bytes. Drained by the phase-B publish and cleared by every cancel site (a relink's
|
||||||
|
// uniforms are not the previous link's uniforms).
|
||||||
|
mutable Vector<PendingUniformWrite> m_pendingUniformWrites;
|
||||||
|
mutable Vector<Uint8> m_pendingUniformBytes;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL::MG_State::GLState
|
} // namespace MobileGL::MG_State::GLState
|
||||||
|
|||||||
@@ -0,0 +1,318 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.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
|
||||||
|
|
||||||
|
#include "ProgramSpirvTask.h"
|
||||||
|
|
||||||
|
#include <MG_State/GLState/ProgramState/ShaderCompileTask.h> // GlslangThreadAllocatorGuard
|
||||||
|
#include <MG_Util/Async/ShaderCompilePool.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/SpvcSession.h>
|
||||||
|
#include <MG_Util/ShaderTranspiler/Types.h>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
void ProgramSpirvTask::DeferLog(String line) { diagnostics.logLines.push_back(Move(line)); }
|
||||||
|
|
||||||
|
void ProgramSpirvTask::SubmitAfter(const SharedPtr<ProgramLinkTask>& phaseA) {
|
||||||
|
MOBILEGL_ASSERT(phaseA != nullptr, "ProgramSpirvTask::SubmitAfter: the phase-A node is missing");
|
||||||
|
m_phaseA = phaseA;
|
||||||
|
|
||||||
|
auto self = std::static_pointer_cast<ProgramSpirvTask>(shared_from_this());
|
||||||
|
// ONE dependency, so no counter and no guard slot: the whole race
|
||||||
|
// ProgramLinkTask::SubmitAfter's +1 exists to close (a dependency settling while the
|
||||||
|
// remaining edges are still being registered) cannot arise with a single edge.
|
||||||
|
//
|
||||||
|
// Runs inline, right here, if phase A is already terminal.
|
||||||
|
phaseA->OnTerminal([self, phaseA] {
|
||||||
|
// "Dependency did not complete, publish nothing" - the same collapse
|
||||||
|
// ProgramLinkTask::CompiledArtifacts() performs for an abandoned compile. Note
|
||||||
|
// this reads the HANDOFF, never phaseA->artifacts: the GL thread may already be
|
||||||
|
// moving those out (see the class comment).
|
||||||
|
if (!phaseA->IsComplete() || !phaseA->spirvHandoff.ready) {
|
||||||
|
self->Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// A cancel that landed before phase A settled (relink, glDeleteProgram, teardown).
|
||||||
|
// Posting would only make a worker pick up a node that immediately falls out of
|
||||||
|
// Run() again.
|
||||||
|
if (self->IsCancellationRequested()) {
|
||||||
|
self->Cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Non-throwing by construction, and it has to be: this is a JobNode continuation,
|
||||||
|
// so on the pool side it runs inside an Asio handler. Post() contains its own
|
||||||
|
// allocation failures, and the catch below CANCELS rather than swallowing - a
|
||||||
|
// phase B that is never posted is a GL thread blocked forever in
|
||||||
|
// EnsureSpirvJoined(), which is far worse than a program reported as not drawable.
|
||||||
|
try {
|
||||||
|
MG_Util::Async::ShaderCompilePool::Get().Post(self);
|
||||||
|
} catch (...) {
|
||||||
|
self->Cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramSpirvTask::RunInlineAfter(const SharedPtr<ProgramLinkTask>& phaseA) {
|
||||||
|
MOBILEGL_ASSERT(phaseA != nullptr, "ProgramSpirvTask::RunInlineAfter: the phase-A node is missing");
|
||||||
|
MOBILEGL_ASSERT(phaseA->IsTerminal(),
|
||||||
|
"ProgramSpirvTask::RunInlineAfter: phase A has not settled; the inline path must run the "
|
||||||
|
"two bodies in order on the same thread");
|
||||||
|
m_phaseA = phaseA;
|
||||||
|
RunInline();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pure CPU work only, on a pool worker (or on the GL thread in the inline mode).
|
||||||
|
// Everything this reads is either owned by this node or published by a terminal phase A;
|
||||||
|
// everything it writes is `artifacts` (and diagnostics). Same prohibitions as
|
||||||
|
// ProgramLinkTask::RunBody - no GL/EGL call, no pActiveBackendObject read, no
|
||||||
|
// pGLContext->RecordError().
|
||||||
|
void ProgramSpirvTask::RunBody() {
|
||||||
|
// glslang leaves this worker's TLS pool allocator pointing at the last arena it
|
||||||
|
// touched; reset it on the way out so an unrelated later job cannot allocate out of a
|
||||||
|
// pool that has since been freed. Declared FIRST so it is destroyed LAST - the phase-A
|
||||||
|
// release below drops the TShaders (and their pools) and must happen inside it.
|
||||||
|
const GlslangThreadAllocatorGuard glslangGuard;
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
// Drop phase A - and with it the TShaders, the TProgram reference and phase A's whole
|
||||||
|
// input snapshot - the moment this body is done, rather than at some later join. For a
|
||||||
|
// pack load that is the difference between W glslang arenas alive and all of them.
|
||||||
|
struct PhaseAReleaser {
|
||||||
|
SharedPtr<ProgramLinkTask>& node;
|
||||||
|
~PhaseAReleaser() { node.reset(); }
|
||||||
|
} const phaseAReleaser{m_phaseA};
|
||||||
|
|
||||||
|
if (!m_phaseA) return;
|
||||||
|
// Non-const: the TShaders are dropped below, the moment GlslangToSpv is finished with
|
||||||
|
// them. This is safe by ownership rather than by locking - phase A is terminal and
|
||||||
|
// therefore immutable to everyone else, the GL-thread join touches only `artifacts`
|
||||||
|
// and `diagnostics`, and this node is the sole reader of the handoff.
|
||||||
|
ProgramLinkTask::SpirvHandoff& handoff = m_phaseA->spirvHandoff;
|
||||||
|
const Uint externalIndex = m_phaseA->in.externalIndex;
|
||||||
|
if (!handoff.ready || !handoff.reflection.program) {
|
||||||
|
// Phase A did not reach its tail (it failed the link, or was cancelled mid-body).
|
||||||
|
// Publish nothing; spirvStatus stays false.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MGLOG_D("ProgramObject %u: Starting SPIR-V generation", externalIndex);
|
||||||
|
GenerateSpirv(handoff, externalIndex);
|
||||||
|
// GlslangToSpv was the only consumer of the parsed ASTs; everything after this point
|
||||||
|
// works on the SPIR-V and on the TProgram's own self-contained reflection pool. Drop
|
||||||
|
// them here rather than at the end of the body, which is ~87% of this node's runtime
|
||||||
|
// earlier (spirv-opt plus routing).
|
||||||
|
//
|
||||||
|
// WHAT THIS ACTUALLY FREES, precisely - it is LESS than "the glslang arenas", and the
|
||||||
|
// difference matters for the peak-RSS story:
|
||||||
|
// * CAS-LOSER shaders (the re-parse in ShaderCompileTask::ClaimParsedShader, i.e.
|
||||||
|
// the 2nd..Nth link of a shared shader): freed here in full. The handoff is their
|
||||||
|
// ONLY owner.
|
||||||
|
// * CAS-WINNER shaders (the common case - one shader object linked into one
|
||||||
|
// program, which is every program of an Iris pack load): NOT freed here. The
|
||||||
|
// winner branch returns a COPY of ShaderCompileTask::artifacts.shader
|
||||||
|
// (ShaderCompileTask.cpp:320) and the node never releases its own reference, while
|
||||||
|
// phase A holds that node through in.shaders[i].compiled for its whole life - and
|
||||||
|
// phase A lives until PhaseAReleaser fires at the end of this body. So the
|
||||||
|
// refcount goes 2 -> 1 here and the arena dies where it would have died anyway.
|
||||||
|
//
|
||||||
|
// Making it free the winner's arena too means releasing whatever pins the TShader
|
||||||
|
// inside the compile node, and neither obvious route is safe as a drive-by: moving out
|
||||||
|
// of artifacts.shader at claim time races ShaderObject::GetCompiledShader() on the GL
|
||||||
|
// thread and breaks JobNode's "a terminal node is immutable" invariant, and dropping
|
||||||
|
// phase A's in.shaders[i].compiled reference only helps when nothing else holds the
|
||||||
|
// node (the adoption map is a WeakPtr index, so it would also change which nodes stay
|
||||||
|
// adoptable). Both belong in a change that can be reviewed against the consume-once
|
||||||
|
// and adoption semantics on their own terms.
|
||||||
|
handoff.shaders.clear();
|
||||||
|
|
||||||
|
MGLOG_D("ProgramObject %u: Building global-UBO routing tables", externalIndex);
|
||||||
|
BuildGlobalUboRouting(handoff, externalIndex);
|
||||||
|
MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", externalIndex,
|
||||||
|
artifacts.generatedSpirv.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramSpirvTask::GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, const Uint externalIndex) {
|
||||||
|
/* As we passed first stage compilation/linking,
|
||||||
|
* we'll assume all the operations here should
|
||||||
|
* pass. We may be able to employ some optimizations
|
||||||
|
* here without the burden of error reporting.
|
||||||
|
*/
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
MGLOG_D("ProgramObject %u: GenerateSpirv - start", externalIndex);
|
||||||
|
|
||||||
|
// The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules)
|
||||||
|
// configuration, and the handoff's program linked those parses - so it IS the program
|
||||||
|
// the backends consume. Generate SPIR-V straight from its intermediates, which the
|
||||||
|
// handoff's TShaders keep alive.
|
||||||
|
ProgramBinaryAttrib binaryAttrib{
|
||||||
|
.shaderTypes = handoff.shaderTypes,
|
||||||
|
.program = *handoff.reflection.program,
|
||||||
|
};
|
||||||
|
MGLOG_D("ProgramObject %u: GenerateSpirv - requesting SPIR-V binary from program", externalIndex);
|
||||||
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
if (!binaryResult) {
|
||||||
|
DeferLog(std::format("ProgramObject {}: GenerateSpirv - GetSpirvBinaryFromProgram failed", externalIndex));
|
||||||
|
MOBILEGL_ASSERT(binaryResult, "GetSpirvBinaryFromProgram failed");
|
||||||
|
return; // spirvStatus stays false: linked, but not drawable.
|
||||||
|
}
|
||||||
|
artifacts.generatedSpirv = Move(binaryResult.value());
|
||||||
|
MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", externalIndex,
|
||||||
|
artifacts.generatedSpirv.size());
|
||||||
|
|
||||||
|
// Linked SPIR-V generated, sanitize and optimize it
|
||||||
|
Bool allOptimized = true;
|
||||||
|
{
|
||||||
|
for (auto& spv : artifacts.generatedSpirv) {
|
||||||
|
auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv);
|
||||||
|
if (!success) {
|
||||||
|
// The one genuine phase-B failure mode: one of the seven optimizer passes
|
||||||
|
// reported failure, so `spv` is whatever the run left behind. A fordebug
|
||||||
|
// build trips the assert below; a release build used to hand that binary
|
||||||
|
// to the backend regardless. It no longer does - the program keeps its
|
||||||
|
// (truthful) LINK_STATUS and its whole query surface, and the routing
|
||||||
|
// tables below still give every settable uniform storage so glUniform*
|
||||||
|
// and glGetUniform* keep working, but spirvStatus stays false and the
|
||||||
|
// backends refuse to build or draw with it.
|
||||||
|
allOptimized = false;
|
||||||
|
DeferLog(std::format("ProgramObject {}: SanitizeAndOptimizeBinary failed; the program is linked "
|
||||||
|
"and queryable but not drawable",
|
||||||
|
externalIndex));
|
||||||
|
}
|
||||||
|
MOBILEGL_ASSERT(success, "SanitizeBinary failed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
artifacts.spirvStatus = allOptimized;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgramSpirvTask::BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff,
|
||||||
|
const Uint externalIndex) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
// The phase-A reflection slice this pass keys off. Carried in the handoff rather than
|
||||||
|
// read off the phase-A node's artifacts, which the join has very likely already moved.
|
||||||
|
const ProgramObject::LinkArtifacts& reflection = handoff.reflection;
|
||||||
|
|
||||||
|
artifacts.uniformOffsets.clear();
|
||||||
|
artifacts.globalUboScratch.clear();
|
||||||
|
// kInvalidUniformOffset marks locations that end up without global-UBO backing
|
||||||
|
// (e.g. the optimizer eliminated every use of the uniform); the fallback pass
|
||||||
|
// below gives those locations tail storage so glUniform* always has a target.
|
||||||
|
artifacts.uniformOffsets.resize(reflection.maxUniformLocation + 1, ProgramObject::kInvalidUniformOffset);
|
||||||
|
for (SizeT i = 0; i < artifacts.generatedSpirv.size(); i++) {
|
||||||
|
auto& spv = artifacts.generatedSpirv[i];
|
||||||
|
|
||||||
|
auto shaderType = i < handoff.shaderTypes.size() ? handoff.shaderTypes[i] : GLenum{0};
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - parsing SPIR-V meta data for module %zu "
|
||||||
|
"(shaderType=%u, wordCount=%zu)",
|
||||||
|
externalIndex, i, shaderType, spv.size());
|
||||||
|
SpvcSession session(spv, SessionUsageBit::Reflection);
|
||||||
|
auto result = session.ParseMetaData();
|
||||||
|
if (result < 0) {
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SpvcSession::ParseMetaData failed for module %zu, "
|
||||||
|
"err = %d%s",
|
||||||
|
externalIndex, i, result,
|
||||||
|
(result == SPVC_ERROR_INVALID_SPIRV ? ". Probably no global UBO?" : ""));
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
auto& meta = session.GetMetadata();
|
||||||
|
auto size = meta.globalUboSize;
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - SPIR-V meta: uboSize=%zu plainUniformCount=%zu "
|
||||||
|
"plainUniformOffsets=%zu",
|
||||||
|
externalIndex, meta.globalUboSize, meta.plainUniformMemberSizesInBytes.size(),
|
||||||
|
meta.plainUniformOffsetsInUBO.size());
|
||||||
|
if (size == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (artifacts.globalUboScratch.size() < size) {
|
||||||
|
artifacts.globalUboScratch.resize(size);
|
||||||
|
}
|
||||||
|
for (const auto& [name, offset] : meta.plainUniformOffsetsInUBO) {
|
||||||
|
// SPIRV-Reflect leaf names never carry a "[0]" suffix; frontend
|
||||||
|
// reflection keys arrays as "arr[0]" (GL naming), so retry with the
|
||||||
|
// suffix before declaring the uniform unbacked.
|
||||||
|
auto locationIt = reflection.uniformLocations.find(name);
|
||||||
|
if (locationIt == reflection.uniformLocations.end()) {
|
||||||
|
locationIt = reflection.uniformLocations.find(name + "[0]");
|
||||||
|
}
|
||||||
|
if (locationIt == reflection.uniformLocations.end()) {
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u but not found in "
|
||||||
|
"uniformLocations",
|
||||||
|
externalIndex, name.c_str(), offset);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Uint baseLocation = locationIt->second;
|
||||||
|
if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(baseLocation))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Int uniformIndex = reflection.uniformIndexInTProgram[baseLocation];
|
||||||
|
const GLint arraySize = ProgramObject::GetUniformArraySizeByTIndex(reflection, uniformIndex);
|
||||||
|
Uint arrayStride = 0;
|
||||||
|
const auto strideIt = meta.plainUniformArrayStridesInUBO.find(name);
|
||||||
|
if (strideIt != meta.plainUniformArrayStridesInUBO.end()) {
|
||||||
|
arrayStride = strideIt->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Array uniforms span one location per element (see DoReflection);
|
||||||
|
// give each element its real byte offset inside the UBO.
|
||||||
|
const GLint elementCount = (arraySize > 1 && arrayStride == 0) ? 1 : std::max(arraySize, 1);
|
||||||
|
for (GLint element = 0; element < elementCount; ++element) {
|
||||||
|
const Uint location = baseLocation + static_cast<Uint>(element);
|
||||||
|
if (location > reflection.maxUniformLocation ||
|
||||||
|
reflection.uniformIndexInTProgram[location] != uniformIndex) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
artifacts.uniformOffsets[location] = offset + static_cast<Uint>(element) * arrayStride;
|
||||||
|
}
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u stride=%u assigned "
|
||||||
|
"to locations %u..%u",
|
||||||
|
externalIndex, name.c_str(), offset, arrayStride, baseLocation,
|
||||||
|
baseLocation + static_cast<Uint>(elementCount) - 1);
|
||||||
|
}
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - finished parsing module %zu metadata",
|
||||||
|
externalIndex, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback pass: a linked program's active non-opaque uniforms must accept
|
||||||
|
// glUniform*/glGetUniform* even when the optimized SPIR-V no longer contains
|
||||||
|
// them (AggressiveDCE can remove a dead loop together with the only loads of a
|
||||||
|
// uniform -- or the entire global UBO, leaving the scratch unallocated). Hand
|
||||||
|
// such locations CPU-side storage at the (16-byte aligned) tail of the shadow
|
||||||
|
// buffer; backends bind at least the SPIR-V-declared UBO range, and the GPU
|
||||||
|
// never reads these bytes, so this only keeps the GL-visible state coherent.
|
||||||
|
for (Uint location = 0; location <= reflection.maxUniformLocation; ++location) {
|
||||||
|
if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue;
|
||||||
|
if (!ProgramObject::IsValidUniformLocation(reflection, static_cast<Int>(location))) continue;
|
||||||
|
const auto& uniform = reflection.program->getUniform(reflection.uniformIndexInTProgram[location]);
|
||||||
|
const glslang::TType* type = uniform.getType();
|
||||||
|
if (type != nullptr && type->isOpaque()) continue;
|
||||||
|
if (uniform.index >= 0 && uniform.index < reflection.program->getNumUniformBlocks() &&
|
||||||
|
std::strstr(reflection.program->getUniformBlock(uniform.index).name.c_str(),
|
||||||
|
MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) {
|
||||||
|
// Member of a named uniform block: not settable through glUniform*, so it
|
||||||
|
// needs no global-UBO shadow storage.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// std140-style slot: the matrix upload paths write column vectors at
|
||||||
|
// 16-byte strides, so a matrix slot must cover cols * 16 bytes.
|
||||||
|
SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType);
|
||||||
|
if (type != nullptr && type->isMatrix()) {
|
||||||
|
slotSize = static_cast<SizeT>(type->getMatrixCols()) * 16u;
|
||||||
|
}
|
||||||
|
slotSize = (slotSize + 15u) & ~static_cast<SizeT>(15u);
|
||||||
|
const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast<SizeT>(15u);
|
||||||
|
artifacts.globalUboScratch.resize(slotOffset + slotSize, 0);
|
||||||
|
artifacts.uniformOffsets[location] = static_cast<Uint>(slotOffset);
|
||||||
|
MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' location %u has no UBO backing in the "
|
||||||
|
"generated SPIR-V (optimized out?); allocated %zu fallback bytes at scratch offset %zu",
|
||||||
|
externalIndex, uniform.name.c_str(), location, slotSize, slotOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
// MobileGL - MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.h
|
||||||
|
// 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
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <Includes.h>
|
||||||
|
#include <MG_State/GLState/ProgramState/ProgramLinkTask.h>
|
||||||
|
#include <MG_Util/Async/JobNode.h>
|
||||||
|
|
||||||
|
namespace MobileGL::MG_State::GLState {
|
||||||
|
// PHASE B of one glLinkProgram: GlslangToSpv, spirv-opt, and the SPIRV-Cross pass that
|
||||||
|
// builds the glUniform*-to-scratch routing tables. Chained behind exactly one
|
||||||
|
// ProgramLinkTask and joined by exactly five ProgramObject getters (GetGeneratedSpirv,
|
||||||
|
// GetUniformOffset, MapUBO, GetUBOData, GetUBOSize), so ~120 other getters and the whole
|
||||||
|
// GL query surface stay on the phase-A gate and answer without waiting for any of this.
|
||||||
|
//
|
||||||
|
// ---- what this node may read, and what it may not ----
|
||||||
|
// It holds the phase-A node by SharedPtr and reads `phaseA->spirvHandoff` plus
|
||||||
|
// `phaseA->in`. It must NEVER read `phaseA->artifacts` or `phaseA->diagnostics`: the GL
|
||||||
|
// thread MOVES the artifacts out of the node at the phase-A join and DRAINS the
|
||||||
|
// diagnostics there, and both of those can happen while this body runs. The handoff exists
|
||||||
|
// precisely so this node has a copy of everything it needs that the join does not touch.
|
||||||
|
// (The general JobNode rule - a terminal node is immutable, so its outputs need no further
|
||||||
|
// synchronization - covers everything except the two members the join consumes.)
|
||||||
|
//
|
||||||
|
// ---- lifetime ----
|
||||||
|
// The handoff owns the Vector<SharedPtr<glslang::TShader>>, and that is mandatory rather
|
||||||
|
// than tidy: glslang::TProgram stores raw TShader* and, for the one-shader-per-stage case,
|
||||||
|
// BORROWS each stage's TIntermediate from its TShader. GlslangToSpv reads exactly those
|
||||||
|
// intermediates. Before the split the shaders died when ProgramLinkTask::RunBody returned,
|
||||||
|
// which was safe only because nothing called getIntermediate() afterwards.
|
||||||
|
//
|
||||||
|
// ---- failure ----
|
||||||
|
// A cancel (relink, teardown, program destruction) or an optimizer failure publishes
|
||||||
|
// spirvStatus = false rather than a half-built program. GL cannot retract a LINK_STATUS it
|
||||||
|
// already reported true, so such a program stays linked and fully queryable; it is just
|
||||||
|
// not drawable, which the backends express through their existing link-status gates.
|
||||||
|
class ProgramSpirvTask final : public MG_Util::Async::JobNode {
|
||||||
|
public:
|
||||||
|
// ---- output: valid iff IsComplete(), immutable afterwards ----
|
||||||
|
// Moved (never copied) into the ProgramObject by EnsureSpirvJoined().
|
||||||
|
ProgramObject::SpirvArtifacts artifacts;
|
||||||
|
|
||||||
|
// Posts this job when `phaseA` goes terminal - and not one moment earlier, so the body
|
||||||
|
// never waits on anything (invariant I4: no job body may block on another job). A
|
||||||
|
// single dependency needs no counter, just the one continuation; it runs inline right
|
||||||
|
// here if `phaseA` is already terminal, which is the same case
|
||||||
|
// ProgramLinkTask::SubmitAfter already reasons about.
|
||||||
|
//
|
||||||
|
// GL thread only, and only after the caller has stored a SharedPtr to this node: the
|
||||||
|
// continuation takes shared_from_this().
|
||||||
|
void SubmitAfter(const SharedPtr<ProgramLinkTask>& phaseA);
|
||||||
|
|
||||||
|
// The async-off / glMaxShaderCompilerThreadsKHR(0) path: run the body on the calling
|
||||||
|
// thread, right now, against an ALREADY-TERMINAL phase A. Deliberately not routed
|
||||||
|
// through SubmitAfter, whose continuation would Post() to a pool that is merely
|
||||||
|
// unused rather than stopped - that would move the work off-thread in the one mode
|
||||||
|
// whose contract is "byte-identical to the synchronous implementation".
|
||||||
|
void RunInlineAfter(const SharedPtr<ProgramLinkTask>& phaseA);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RunBody() override;
|
||||||
|
|
||||||
|
void GenerateSpirv(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||||
|
void BuildGlobalUboRouting(const ProgramLinkTask::SpirvHandoff& handoff, Uint externalIndex);
|
||||||
|
|
||||||
|
// Worker-side MGLOG replacement, replayed by the join on the GL thread. Same reason as
|
||||||
|
// ProgramLinkTask::DeferLog.
|
||||||
|
void DeferLog(String line);
|
||||||
|
|
||||||
|
SharedPtr<ProgramLinkTask> m_phaseA;
|
||||||
|
};
|
||||||
|
} // namespace MobileGL::MG_State::GLState
|
||||||
@@ -111,9 +111,13 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// that can grow, and a reallocation underneath this loop would be a use-after-free
|
// that can grow, and a reallocation underneath this loop would be a use-after-free
|
||||||
// that only shows up on the one GL call that walks the whole table. The copy costs a
|
// that only shows up on the one GL call that walks the whole table. The copy costs a
|
||||||
// refcount bump on a path a mode switch takes at most once.
|
// refcount bump on a path a mode switch takes at most once.
|
||||||
|
// BOTH phases per program. This is the glMaxShaderCompilerThreadsKHR(0) path, whose
|
||||||
|
// contract is that nothing is outstanding when it returns - a program left with its
|
||||||
|
// SPIR-V job in flight would make the very next GL_COMPLETION_STATUS_KHR read GL_FALSE
|
||||||
|
// in a mode the extension says cannot have anything pending.
|
||||||
for (SizeT i = 0; i < m_programObjects.size(); ++i) {
|
for (SizeT i = 0; i < m_programObjects.size(); ++i) {
|
||||||
const SharedPtr<ProgramObject> program = m_programObjects[i];
|
const SharedPtr<ProgramObject> program = m_programObjects[i];
|
||||||
if (program) program->JoinLink();
|
if (program) program->JoinLinkAndSpirv();
|
||||||
}
|
}
|
||||||
for (SizeT i = 0; i < m_shaderObjects.size(); ++i) {
|
for (SizeT i = 0; i < m_shaderObjects.size(); ++i) {
|
||||||
const SharedPtr<ShaderObject> shader = m_shaderObjects[i];
|
const SharedPtr<ShaderObject> shader = m_shaderObjects[i];
|
||||||
@@ -122,7 +126,7 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// The currently-used program is reachable through m_programObjects unless
|
// The currently-used program is reachable through m_programObjects unless
|
||||||
// glDeleteProgram already freed its slot while it stayed current. Nothing else holds
|
// glDeleteProgram already freed its slot while it stayed current. Nothing else holds
|
||||||
// a GL-visible name for it, but a draw would still join it, so settle it here too.
|
// a GL-visible name for it, but a draw would still join it, so settle it here too.
|
||||||
if (m_currentProgram) m_currentProgram->JoinLink();
|
if (m_currentProgram) m_currentProgram->JoinLinkAndSpirv();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProgramState::MarkShaderObjectForDeletion(Uint shader) {
|
void ProgramState::MarkShaderObjectForDeletion(Uint shader) {
|
||||||
|
|||||||
@@ -88,12 +88,19 @@ namespace MobileGL::MG_State::GLState {
|
|||||||
// another object, THIS object has not pulled its result yet. (An adopted node may
|
// 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.)
|
// already be terminal - the join then only replays what is left of its diagnostics.)
|
||||||
m_compileJoined = false;
|
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 {
|
void ShaderObject::DropCompileNode() const {
|
||||||
if (!m_compiled) return;
|
if (!m_compiled) return;
|
||||||
m_compiled->ReleaseAdopter();
|
m_compiled->ReleaseAdopter();
|
||||||
m_compiled.reset();
|
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() {
|
void ShaderObject::InvalidateCompiledState() {
|
||||||
|
|||||||
@@ -116,8 +116,10 @@ namespace MobileGL {
|
|||||||
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
Bool GetDeleteStatus() const { return m_deleteStatus; }
|
||||||
|
|
||||||
// Blocks until a pending compile has published its artifacts. Public for the
|
// Blocks until a pending compile has published its artifacts. Public for the
|
||||||
// sites that must join without reading anything - ProgramObject::Link's
|
// sites that must join without reading anything - ProgramState::
|
||||||
// prologue, which needs every attached shader settled before it runs.
|
// 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(); }
|
void JoinCompile() const { EnsureCompileJoined(); }
|
||||||
|
|
||||||
// True while this object holds the outcome (success OR failure) of a Compile()
|
// True while this object holds the outcome (success OR failure) of a Compile()
|
||||||
@@ -141,6 +143,23 @@ namespace MobileGL {
|
|||||||
// outstanding to wait for.
|
// outstanding to wait for.
|
||||||
Bool IsCompileComplete() const { return m_compiled == nullptr || m_compiled->IsTerminal(); }
|
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:
|
private:
|
||||||
// ---- The one and only join gate for compile output (P1 invariant I5) ----
|
// ---- 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 -
|
// 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
|
// Exactly-once latch for the pull above. Armed with every new job node, set by
|
||||||
// the one join that consumes it.
|
// the one join that consumes it.
|
||||||
mutable Bool m_compileJoined = false;
|
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 MG_State::GLState
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
@@ -117,11 +117,26 @@ void main() { fragColor = thisIdentifierWasNeverDeclared; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The non-joining view of the program, i.e. what GL_COMPLETION_STATUS_KHR will report.
|
// The non-joining view of the program, i.e. what GL_COMPLETION_STATUS_KHR will report.
|
||||||
|
// BOTH phases: a program whose SPIR-V job is still in flight is not finished, even though
|
||||||
|
// its whole GL query surface already answers.
|
||||||
Bool LinkIsSettled(const GLuint program) {
|
Bool LinkIsSettled(const GLuint program) {
|
||||||
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||||
return object == nullptr || object->IsLinkComplete();
|
return object == nullptr || object->IsLinkComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Phase A alone: the half that decides LINK_STATUS, the info log, and every reflection
|
||||||
|
// query. This is what a read of LINK_STATUS is required to settle.
|
||||||
|
Bool PhaseALinkIsSettled(const GLuint program) {
|
||||||
|
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||||
|
return object == nullptr || object->IsPhaseALinkComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Phase B alone: SPIR-V + the uniform shadow's layout.
|
||||||
|
Bool SpirvIsSettled(const GLuint program) {
|
||||||
|
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||||
|
return object == nullptr || object->IsSpirvComplete();
|
||||||
|
}
|
||||||
|
|
||||||
// Enqueues `count` distinct heavy compiles without reading anything back, so the pool is
|
// Enqueues `count` distinct heavy compiles without reading anything back, so the pool is
|
||||||
// left with a real backlog for the caller to race against.
|
// left with a real backlog for the caller to race against.
|
||||||
Vector<GLuint> SaturatePool(const int count, Vector<String>& sourceStorage) {
|
Vector<GLuint> SaturatePool(const int count, Vector<String>& sourceStorage) {
|
||||||
@@ -516,11 +531,63 @@ TEST_F(AsyncLinkTest, LinkProgramReturnsBeforeTheWorkIsDone) {
|
|||||||
|
|
||||||
for (const GLuint program : programs) {
|
for (const GLuint program : programs) {
|
||||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||||
EXPECT_TRUE(LinkIsSettled(program)) << "reading LINK_STATUS must have joined";
|
// PHASE A only. Reading LINK_STATUS settles the half that decides it, and no more -
|
||||||
|
// the SPIR-V job may well still be running, which is the entire point of the split.
|
||||||
|
EXPECT_TRUE(PhaseALinkIsSettled(program)) << "reading LINK_STATUS must have joined phase A";
|
||||||
}
|
}
|
||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The other half of the previous case, and the property the two-phase split exists for:
|
||||||
|
// LINK_STATUS is answerable without the SPIR-V, so a run of LINK_STATUS reads over a
|
||||||
|
// backlog must leave SPIR-V jobs outstanding rather than draining them one by one.
|
||||||
|
TEST_F(AsyncLinkTest, ReadingLinkStatusDoesNotSettleTheSpirvJob) {
|
||||||
|
const AsyncModeScope async(true);
|
||||||
|
MG_Util::Async::ShaderCompilePool::Get().SetMaxConcurrency(1);
|
||||||
|
constexpr int kPrograms = 24;
|
||||||
|
|
||||||
|
const GLuint vs = MakeShader(GL_VERTEX_SHADER, kVs);
|
||||||
|
Vector<GLuint> programs;
|
||||||
|
Vector<String> sources;
|
||||||
|
for (int i = 0; i < kPrograms; ++i) {
|
||||||
|
sources.push_back(MakeBulkySource(7900 + i));
|
||||||
|
const char* text = sources.back().c_str();
|
||||||
|
const GLuint fs = CreateShader(GL_FRAGMENT_SHADER);
|
||||||
|
ShaderSource(fs, 1, &text, nullptr);
|
||||||
|
CompileShader(fs);
|
||||||
|
const GLuint program = CreateProgram();
|
||||||
|
AttachShader(program, vs);
|
||||||
|
AttachShader(program, fs);
|
||||||
|
LinkProgram(program);
|
||||||
|
programs.push_back(program);
|
||||||
|
}
|
||||||
|
|
||||||
|
int spirvOutstanding = 0;
|
||||||
|
for (int i = 0; i < kPrograms; ++i) {
|
||||||
|
const GLuint program = programs[static_cast<SizeT>(i)];
|
||||||
|
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE) << QueryProgramInfoLog(program);
|
||||||
|
EXPECT_TRUE(PhaseALinkIsSettled(program)) << "reading LINK_STATUS must have joined phase A";
|
||||||
|
// Reflection has to answer here too, out of phase A and with no further join.
|
||||||
|
const String uniformName = "uSeed" + std::to_string(7900 + i);
|
||||||
|
EXPECT_GE(GetUniformLocation(program, uniformName.c_str()), 0) << uniformName;
|
||||||
|
if (!SpirvIsSettled(program)) ++spirvOutstanding;
|
||||||
|
}
|
||||||
|
EXPECT_GT(spirvOutstanding, 0) << "the whole GL query surface was answered and yet every SPIR-V job had "
|
||||||
|
"already been drained - the reads are joining phase B";
|
||||||
|
|
||||||
|
// And the SPIR-V gate really is a gate: touching it settles the job.
|
||||||
|
for (const GLuint program : programs) {
|
||||||
|
const auto& object = MG_State::pGLContext->GetProgramObject(program);
|
||||||
|
ASSERT_NE(object, nullptr);
|
||||||
|
EXPECT_GT(object->GetGeneratedSpirv().size(), 0u);
|
||||||
|
EXPECT_TRUE(SpirvIsSettled(program));
|
||||||
|
EXPECT_TRUE(LinkIsSettled(program));
|
||||||
|
}
|
||||||
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
|
MG_Util::Async::ShaderCompilePool::Get().SetMaxConcurrency(
|
||||||
|
MG_Util::Async::ShaderCompilePool::Get().GetThreadCount());
|
||||||
|
}
|
||||||
|
|
||||||
// With the flag off, a link is finished by the time glLinkProgram returns. This is the guard
|
// With the flag off, a link is finished by the time glLinkProgram returns. This is the guard
|
||||||
// that keeps the default shippable.
|
// that keeps the default shippable.
|
||||||
TEST_F(AsyncLinkTest, LinkIsFullySynchronousWithAsyncOff) {
|
TEST_F(AsyncLinkTest, LinkIsFullySynchronousWithAsyncOff) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -49,6 +49,22 @@ add_executable(
|
|||||||
AsyncLinkTest.cpp
|
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
|
target_include_directories(AsyncLinkTest PRIVATE
|
||||||
${MGL_ROOT}/include
|
${MGL_ROOT}/include
|
||||||
${MGL_ROOT}/MobileGL
|
${MGL_ROOT}/MobileGL
|
||||||
@@ -60,6 +76,24 @@ target_link_libraries(
|
|||||||
${LINK_LIBRARIES}
|
${LINK_LIBRARIES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Its own binary, like the other async suites: its cases pin the compile pool down to one
|
||||||
|
# worker so a phase-B job really is still queued while the GL query surface is being read.
|
||||||
|
add_executable(
|
||||||
|
AsyncSpirvPhaseTest
|
||||||
|
AsyncSpirvPhaseTest.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(AsyncSpirvPhaseTest PRIVATE
|
||||||
|
${MGL_ROOT}/include
|
||||||
|
${MGL_ROOT}/MobileGL
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(
|
||||||
|
AsyncSpirvPhaseTest PRIVATE
|
||||||
|
GTest::gtest_main
|
||||||
|
${LINK_LIBRARIES}
|
||||||
|
)
|
||||||
|
|
||||||
add_executable(
|
add_executable(
|
||||||
ShaderCompileAdoptionTest
|
ShaderCompileAdoptionTest
|
||||||
ShaderCompileAdoptionTest.cpp
|
ShaderCompileAdoptionTest.cpp
|
||||||
@@ -165,11 +199,17 @@ gtest_discover_tests(ProgramInterfaceTest DISCOVERY_TIMEOUT 30 PROPERTIES LABELS
|
|||||||
# compile pool so there is something in flight to race against.
|
# compile pool so there is something in flight to race against.
|
||||||
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
gtest_discover_tests(AsyncCompileTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
gtest_discover_tests(AsyncLinkTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
gtest_discover_tests(AsyncLinkTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
|
# Same reason: every case here links a batch against a one-worker pool so that phase B is
|
||||||
|
# genuinely outstanding while phase A is being interrogated.
|
||||||
|
gtest_discover_tests(AsyncSpirvPhaseTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
||||||
# Same reason: the stage-6 cases keep a backlog in flight so a release really can race a
|
# Same reason: the stage-6 cases keep a backlog in flight so a release really can race a
|
||||||
# worker, and the 48-object stress links every one of them.
|
# worker, and the 48-object stress links every one of them.
|
||||||
gtest_discover_tests(ShaderCompileAdoptionTest DISCOVERY_TIMEOUT 60 PROPERTIES LABELS unit TIMEOUT 300)
|
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.
|
# 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)
|
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)
|
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.
|
# 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)
|
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);
|
||||||
|
}
|
||||||
@@ -239,7 +239,15 @@ TEST_F(ParallelShaderCompileTest, ProgramCompletionStatusReportsFalseWithoutJoin
|
|||||||
|
|
||||||
for (const GLuint program : programs) {
|
for (const GLuint program : programs) {
|
||||||
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
|
EXPECT_EQ(QueryLinkStatus(program), GL_TRUE);
|
||||||
EXPECT_EQ(QueryProgramCompletion(program), GL_TRUE) << "GL_LINK_STATUS must have joined";
|
// 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);
|
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
||||||
}
|
}
|
||||||
@@ -333,6 +341,54 @@ TEST_F(ParallelShaderCompileTest, ZeroCompilerThreadsJoinsEverythingAndCompilesI
|
|||||||
EXPECT_EQ(GetError(), GL_NO_ERROR);
|
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
|
// ...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.
|
// join, not eglInitialize. That is the documented contract, so it gets an assertion.
|
||||||
TEST_F(ParallelShaderCompileTest, NonzeroCompilerThreadsRestoresAsynchronousCompilation) {
|
TEST_F(ParallelShaderCompileTest, NonzeroCompilerThreadsRestoresAsynchronousCompilation) {
|
||||||
|
|||||||
@@ -9,8 +9,11 @@
|
|||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "Includes.h"
|
#include "Includes.h"
|
||||||
#include "Init.h"
|
#include "Init.h"
|
||||||
@@ -2649,3 +2652,110 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheHonorsByteBudget) {
|
|||||||
EXPECT_EQ(cache.GetEntryCount(), before);
|
EXPECT_EQ(cache.GetEntryCount(), before);
|
||||||
EXPECT_EQ(cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, kEnvA), nullptr);
|
EXPECT_EQ(cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, kEnvA), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Every vertex input that reaches SPIR-V must carry a Location decoration - including the
|
||||||
|
// declarations glslang's io-mapper considers INACTIVE.
|
||||||
|
//
|
||||||
|
// The shape is Iris's: seven attributes, only some of them bound through
|
||||||
|
// glBindAttribLocation (ProgramAttrib::explicitVertexInLocations), and at least one neither
|
||||||
|
// bound nor referenced. GL says only active inputs get generic attribute locations, so the
|
||||||
|
// resolver deliberately does not RESERVE a slot for a dead one - but it must still RESOLVE a
|
||||||
|
// location for it, because glslang emits an OpVariable for every declared global (the entry
|
||||||
|
// point's interface comes from the linker objects) and SPIR-V requires every non-built-in
|
||||||
|
// Input to be decorated (VUID-StandaloneSpirv-Location-04916).
|
||||||
|
//
|
||||||
|
// This test drives the FRONTEND rather than the GL entry points on purpose: it checks the RAW
|
||||||
|
// GlslangToSpv output, before SanitizeAndOptimizeBinary. A GL-level test cannot see the defect
|
||||||
|
// for an unreferenced attribute, because AggressiveDCE deletes the offending variable on its
|
||||||
|
// way to the backend - and yet the real victim (Iris' mc_midTexCoord, Adreno 830,
|
||||||
|
// programHash 0x4a7e9a37fb49caa1) survived DCE and killed the pipeline with VK_ERROR_UNKNOWN.
|
||||||
|
TEST_F(ProgramUtilTest, PartiallyBoundVertexInputsAllReceiveALocation) {
|
||||||
|
using namespace MG_Util::ShaderTranspiler;
|
||||||
|
|
||||||
|
const String vertexSource = R"(#version 460 core
|
||||||
|
in vec3 a_Position;
|
||||||
|
in vec4 a_Color;
|
||||||
|
in vec2 a_TexCoord;
|
||||||
|
in vec2 mc_midTexCoord;
|
||||||
|
in vec4 mc_Entity;
|
||||||
|
in vec3 iris_Normal;
|
||||||
|
in vec4 a_Unreferenced;
|
||||||
|
out vec4 v_Color;
|
||||||
|
void main() {
|
||||||
|
v_Color = a_Color + vec4(a_TexCoord, 0.0, 0.0) + vec4(mc_midTexCoord, 0.0, 0.0) + mc_Entity
|
||||||
|
+ vec4(iris_Normal, 0.0);
|
||||||
|
gl_Position = vec4(a_Position, 1.0);
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
ShaderAttrib shaderAttrib{.shaderType = GL_VERTEX_SHADER, .sourceStr = vertexSource};
|
||||||
|
auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib);
|
||||||
|
ASSERT_TRUE(shaderResult) << shaderResult.error().log;
|
||||||
|
|
||||||
|
// PARTIALLY bound, and deliberately not a dense 0..N run - exactly what Iris does.
|
||||||
|
// mc_midTexCoord and a_Unreferenced are left unbound (FastSTL's map has no
|
||||||
|
// initializer-list constructor, hence the explicit inserts).
|
||||||
|
UnorderedMap<String, Uint> explicitVertexIns;
|
||||||
|
explicitVertexIns["a_Position"] = 0;
|
||||||
|
explicitVertexIns["a_Color"] = 1;
|
||||||
|
explicitVertexIns["a_TexCoord"] = 2;
|
||||||
|
explicitVertexIns["iris_Normal"] = 10;
|
||||||
|
explicitVertexIns["mc_Entity"] = 11;
|
||||||
|
ProgramAttrib programAttrib{.shaders = {shaderResult.value()},
|
||||||
|
.explicitVertexInLocations = explicitVertexIns};
|
||||||
|
auto programResult = ShaderCompiler::LinkProgram(programAttrib);
|
||||||
|
ASSERT_TRUE(programResult) << programResult.error().log;
|
||||||
|
|
||||||
|
ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_VERTEX_SHADER}, .program = *programResult.value()};
|
||||||
|
auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib);
|
||||||
|
ASSERT_TRUE(binaryResult) << binaryResult.error().log;
|
||||||
|
ASSERT_EQ(binaryResult->size(), 1u);
|
||||||
|
const auto& vertexBinary = binaryResult->front();
|
||||||
|
|
||||||
|
// The authoritative check - this is the same validator whose VUID the driver enforces.
|
||||||
|
spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1);
|
||||||
|
String validatorMessages;
|
||||||
|
tools.SetMessageConsumer([&validatorMessages](spv_message_level_t, const char*, const spv_position_t&,
|
||||||
|
const char* message) {
|
||||||
|
if (message != nullptr) validatorMessages += String(message) + "\n";
|
||||||
|
});
|
||||||
|
EXPECT_TRUE(tools.Validate(vertexBinary))
|
||||||
|
<< "the raw vertex module is not valid SPIR-V; Adreno rejects the whole pipeline for this "
|
||||||
|
<< "while lavapipe tolerates it:\n"
|
||||||
|
<< validatorMessages;
|
||||||
|
|
||||||
|
// ...and, independently of the validator, every non-built-in Input carries a UNIQUE location.
|
||||||
|
constexpr unsigned kOpDecorate = 71, kOpVariable = 59;
|
||||||
|
constexpr unsigned kDecorationBuiltIn = 11, kDecorationLocation = 30;
|
||||||
|
constexpr unsigned kStorageClassInput = 1;
|
||||||
|
std::map<unsigned, unsigned> locationById;
|
||||||
|
std::set<unsigned> builtInIds;
|
||||||
|
std::vector<unsigned> inputIds;
|
||||||
|
for (SizeT i = 5; i < vertexBinary.size();) { // 5-word header
|
||||||
|
const unsigned wordCount = vertexBinary[i] >> 16;
|
||||||
|
const unsigned opcode = vertexBinary[i] & 0xFFFFu;
|
||||||
|
ASSERT_GT(wordCount, 0u) << "malformed SPIR-V instruction stream";
|
||||||
|
if (i + wordCount > vertexBinary.size()) break;
|
||||||
|
if (opcode == kOpDecorate && wordCount >= 4 && vertexBinary[i + 2] == kDecorationLocation) {
|
||||||
|
locationById[vertexBinary[i + 1]] = vertexBinary[i + 3];
|
||||||
|
} else if (opcode == kOpDecorate && wordCount >= 3 && vertexBinary[i + 2] == kDecorationBuiltIn) {
|
||||||
|
builtInIds.insert(vertexBinary[i + 1]);
|
||||||
|
} else if (opcode == kOpVariable && wordCount >= 4 && vertexBinary[i + 3] == kStorageClassInput) {
|
||||||
|
inputIds.push_back(vertexBinary[i + 2]);
|
||||||
|
}
|
||||||
|
i += wordCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<unsigned> usedLocations;
|
||||||
|
SizeT checked = 0;
|
||||||
|
for (const unsigned id : inputIds) {
|
||||||
|
if (builtInIds.count(id) != 0) continue;
|
||||||
|
const auto it = locationById.find(id);
|
||||||
|
ASSERT_NE(it, locationById.end())
|
||||||
|
<< "vertex input id " << id << " reached SPIR-V with no Location decoration";
|
||||||
|
EXPECT_TRUE(usedLocations.insert(it->second).second)
|
||||||
|
<< "two vertex inputs were assigned location " << it->second;
|
||||||
|
++checked;
|
||||||
|
}
|
||||||
|
EXPECT_GE(checked, 7u) << "expected all seven declared inputs to be present in the raw module";
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,6 +126,15 @@ namespace MobileGL::MG_Util::Async {
|
|||||||
return AsyncShaderCompileEnabled() && !IsAsyncShaderCompileSuspended();
|
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() {
|
Uint DetectShaderCompileThreadCount() {
|
||||||
if (const Uint32 configured = MG_Config::Features.AsyncShaderCompileThreads; configured > 0) {
|
if (const Uint32 configured = MG_Config::Features.AsyncShaderCompileThreads; configured > 0) {
|
||||||
// An explicit request is honoured as given - it is the escape hatch for measuring
|
// 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.
|
// GL_COMPLETION_STATUS_KHR read immediately GL_TRUE.
|
||||||
Bool AsyncShaderCompileActive();
|
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
|
// 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]
|
// 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
|
// because peak RSS scales as workers x largest glslang arena, and four
|
||||||
|
|||||||
@@ -105,8 +105,46 @@ namespace MobileGL {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int TMglGlslIoResolver::resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) {
|
int TMglGlslIoResolver::resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) {
|
||||||
if (!ent.live && stage == EShLangVertex && ent.symbol->getType().getQualifier().isPipeInput()) {
|
// NO dead-vertex-input early-out here, deliberately - the skip belongs in
|
||||||
return ent.newLocation = -1;
|
// reserverStorageSlot() and ONLY there.
|
||||||
|
//
|
||||||
|
// Skipping RESERVATION is the GL semantic: only active inputs get generic attribute
|
||||||
|
// locations, so a dead declaration must not consume a slot an active input should
|
||||||
|
// have. Skipping RESOLUTION as well used to look like the same statement, but it is a
|
||||||
|
// different one: it leaves the variable with no layoutLocation, and glslang still
|
||||||
|
// EMITS it - a declared input is in the shader's linker objects and therefore in the
|
||||||
|
// entry point's interface. The result is an OpVariable of storage class Input with no
|
||||||
|
// Location decoration, which SPIR-V forbids
|
||||||
|
// (VUID-StandaloneSpirv-Location-04916). lavapipe tolerates it; Adreno rejects the
|
||||||
|
// whole pipeline with VK_ERROR_UNKNOWN, which is how this shipped undetected - every
|
||||||
|
// desktop gate, retrace corpus included, is blind to it.
|
||||||
|
//
|
||||||
|
// Found 2026-08-11 on an Adreno 830: the Iris weather program (mc_midTexCoord among
|
||||||
|
// seven attributes, only some of them glBindAttribLocation-bound) died at the first
|
||||||
|
// rainy-world draw, 100% reproducible, programHash 0x4a7e9a37fb49caa1.
|
||||||
|
//
|
||||||
|
// They cannot simply be handed to the base resolver either. Auto-assignment for inputs
|
||||||
|
// WITHOUT an explicit binding happens entirely in the resolve pass, in sort order, so a
|
||||||
|
// dead declaration reaching the free-slot search first would take location 0 and push
|
||||||
|
// the active input up - which is precisely the GL violation the reservation skip
|
||||||
|
// exists to prevent (ProgramTest.InactiveExplicitVertexBindingsDoNotReserveLocations
|
||||||
|
// pins it: Iris injects Position/UV0 into packs that actually read vaPosition).
|
||||||
|
//
|
||||||
|
// So dead inputs get their locations from the TOP of the attribute range downward,
|
||||||
|
// while the base resolver hands active ones out from 0 upward. Both properties hold at
|
||||||
|
// once: every emitted input carries a Location, and no active input is displaced. The
|
||||||
|
// two allocators can only meet if live + dead exceed the attribute limit, which is an
|
||||||
|
// over-subscribed program GL would reject anyway; if that happens we leave the
|
||||||
|
// variable to the base resolver rather than hand out a colliding location.
|
||||||
|
const glslang::TType& type = ent.symbol->getType();
|
||||||
|
if (!ent.live && stage == EShLangVertex && type.getQualifier().isPipeInput() &&
|
||||||
|
!type.getQualifier().hasLocation() && !type.isBuiltIn()) {
|
||||||
|
const int size = std::max(1, glslang::TIntermediate::computeTypeLocationSize(type, stage));
|
||||||
|
if (m_nextInactiveVertexInLocation - (size - 1) >= 0) {
|
||||||
|
m_nextInactiveVertexInLocation -= (size - 1);
|
||||||
|
ent.symbol->getWritableType().getQualifier().layoutLocation = m_nextInactiveVertexInLocation;
|
||||||
|
--m_nextInactiveVertexInLocation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
|
return TDefaultGlslIoResolver::resolveInOutLocation(stage, ent);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,5 +51,13 @@ namespace MobileGL {
|
|||||||
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
|
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
|
||||||
std::map<glslang::TString, int> m_plainUniformLocationByName;
|
std::map<glslang::TString, int> m_plainUniformLocationByName;
|
||||||
bool m_plainUniformLocationsAssigned = false;
|
bool m_plainUniformLocationsAssigned = false;
|
||||||
|
// Descending allocator for INACTIVE vertex inputs (see resolveInOutLocation): they
|
||||||
|
// still have to carry a Location because glslang emits them, but they must not take a
|
||||||
|
// slot an active input would get. 15, not 31: the location survives into the ESSL
|
||||||
|
// SPIRV-Cross emits for DirectGLES, and GL/ES only guarantee GL_MAX_VERTEX_ATTRIBS
|
||||||
|
// >= 16 - a location of 31 makes the generated shader fail to compile on a real ES
|
||||||
|
// driver (caught by the super-duper-vanilla and chocapic retrace fixtures).
|
||||||
|
static constexpr int kInactiveVertexInLocationTop = 15;
|
||||||
|
int m_nextInactiveVertexInLocation = kInactiveVertexInLocationTop;
|
||||||
};
|
};
|
||||||
} // namespace MobileGL
|
} // namespace MobileGL
|
||||||
|
|||||||
Reference in New Issue
Block a user