diff --git a/CMakeLists.txt b/CMakeLists.txt index 376dca6e..75c548ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -190,6 +190,7 @@ set(SOURCE_FILES MobileGL/MG_Util/Classifiers/TextureEnumClassifier.cpp + MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp MobileGL/MG_Util/ShaderTranspiler/SpvcSession.cpp MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index f2ca7f40..2b131807 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -9,6 +9,8 @@ #include "Core.h" #include "MG_State/GLState/RenderbufferState/RenderbufferObject.h" #include "MG_State/EGLState/Core.h" +#include +#include #include namespace MobileGL::MG_State { @@ -24,6 +26,18 @@ namespace MobileGL::MG_State { } namespace GLState { + const SharedPtr& GLContext::GetCompileEnv() { + const void* backend = static_cast(MG_Backend::pActiveBackendObject.get()); + if (!m_compileEnv || m_compileEnvBackend != backend) { + // First use, or the backend was swapped underneath us. Re-capturing rolls the + // fingerprint, so every P0b preprocess memo computed against the old backend's + // limits becomes structurally unreachable instead of silently reusable. + m_compileEnv = MG_Util::ShaderTranspiler::CaptureCompileEnv(); + m_compileEnvBackend = backend; + } + return m_compileEnv; + } + // Error void GLContext::RecordError(ErrorCode code, UniquePtr info) { m_errorState.RecordError(code, Move(info)); diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index ce6643c4..8e85d99a 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -21,6 +21,10 @@ #include "VertexArrayState/VertexArrayState.h" #include "RenderbufferState/RenderbufferState.h" +namespace MobileGL::MG_Util::ShaderTranspiler { + struct CompileEnv; +} + namespace MobileGL { namespace MG_State { void Init(); @@ -380,6 +384,15 @@ namespace MobileGL { Bool ValidateRenderbufferName(Uint index) const; Bool ValidateRenderbufferObject(Uint index) const; + // P1: the shader compile/link pipeline's snapshot of everything it reads from + // outside its own (stage, source) inputs. Captured lazily here because it + // cannot be captured in MG_State::Init() - that runs BEFORE MG_Backend::Init(), + // so there is no backend to query yet. Re-captured whenever the active backend + // object changes, which also rolls the fingerprint and therefore invalidates + // every P0b preprocess memo keyed against the old one. + // GL thread only. + const SharedPtr& GetCompileEnv(); + private: // State Components ErrorState m_errorState; @@ -437,6 +450,11 @@ namespace MobileGL { FramebufferState m_framebufferState; SamplerState m_samplerState; RenderbufferState m_renderbufferState; + + mutable SharedPtr m_compileEnv; + // Identity of the backend object m_compileEnv was captured against; a plain + // pointer compare, never dereferenced. + const void* m_compileEnvBackend = nullptr; }; } // namespace GLState diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 9703e53b..fb6049a0 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -9,12 +9,12 @@ #include "ProgramObject.h" #include #include -#include #include #include #include #include #include +#include #include #include @@ -29,13 +29,13 @@ namespace { // GL_MAX_VERTEX_ATTRIBS would make a legal attribute location invisible to them -- DirectGLES would // then never feed the shader that attribute's current value. Bounded by the state layer's storage // capacity, which is also the width of the Uint32 masks backends build from it. - static MobileGL::Int GetReflectionVertexAttribLimit() { + static MobileGL::Int GetReflectionVertexAttribLimit( + const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) { constexpr MobileGL::Int capacity = static_cast(MobileGL::MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); - if (!MobileGL::MG_Backend::pActiveBackendObject) return capacity; + if (!env.HasBackend()) return capacity; - const MobileGL::Int backendLimit = - MobileGL::MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs; + const MobileGL::Int backendLimit = env.params.MaxVertexAttribs; if (backendLimit <= 0) return capacity; return std::min(backendLimit, capacity); } @@ -143,46 +143,65 @@ namespace MobileGL::MG_State::GLState { return s_nextProgramLifetimeId.fetch_add(1, std::memory_order_relaxed); } - void ProgramObject::ResetLinkArtifacts() { + // EnsureLinkJoined() is defined inline in ProgramObject.h (see the comment there for + // why: ~1200 call sites, no LTO). + + void ProgramObject::BumpLinkObservableVersions() { // Relinking regenerates the SPIR-V, so any backend-cached state keyed on // m_backendStateVersion (e.g. the content-hash memo) must be invalidated, // along with every link-derived backend cache (m_linkVersion) and the // last-uploaded-UBO gate (a relink resets uniforms to their initial values, - // and that reset must reach the GPU). + // and that reset must reach the GPU). GL-THREAD ONLY: bumped once per link + // in Link()'s prologue (and by glProgramBinary's mandated failure), never + // from the link body - stage 4 moves that body onto a pool worker, and a + // non-atomic ++ there against the draw path's reads would be exactly the + // lost-invalidation memo hazard. ++m_backendStateVersion; ++m_linkVersion; MarkUBOContentDirty(); - m_program.reset(); - m_generatedSpirv.clear(); - m_uniformLocations.clear(); - m_glUniformIndexToTProgram.clear(); - m_tProgramUniformIndexToGl.clear(); - m_glBlockIndexToTProgram.clear(); - m_tProgramBlockIndexToGl.clear(); - m_linkedExplicitUniformLocations.clear(); - m_uniformIndexInTProgram.clear(); - m_uniformSamplerOrImageUnitIndex.clear(); - m_explicitOpaqueUniformBindings.clear(); - m_uniformBlockIndexByName.clear(); - m_uniformBlockBinding.clear(); - m_uniformOffsets.clear(); - m_uniformSizesInBytes.clear(); - m_globalUboScratch.clear(); - m_attribs.clear(); - m_attribTypes.clear(); - m_activeUniformCount = 0; - m_maxUniformLocation = 0; - m_uniformNameMaxLength = 0; - m_attribInNameMaxLength = 0; - m_uniformBlockNameMaxLength = 0; - m_xfbVaryings.clear(); - m_xfbStrides.clear(); - m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS; - m_xfbVaryingNameMaxLength = 0; - m_xfbNeedsScatteredCapture = false; - m_xfbPackedStride = 0; - m_gsInputPrimitive = GL_NONE; - m_linkStatus = false; + } + + void ProgramObject::ResetLinkArtifacts() { + // Worker-safe pure clear: touches LinkArtifacts only. The link-observable + // version bumps live in BumpLinkObservableVersions() on the GL thread. + + // Deliberately NOT `artifacts = {}`: infoLog, linkedFragDataLocation/Index and the + // geometry strip-capture pair live in LinkArtifacts but are not part of what this + // function has ever cleared, and Link()/MarkLinkFailedByProgramBinary() depend on + // that (both write infoLog immediately AFTER calling here). Stage 4 replaces this + // with a whole-struct reset in Link()'s prologue, where the ordering is explicit. + LinkArtifacts& artifacts = Artifacts(); + artifacts.program.reset(); + artifacts.generatedSpirv.clear(); + artifacts.uniformLocations.clear(); + artifacts.glUniformIndexToTProgram.clear(); + artifacts.tProgramUniformIndexToGl.clear(); + artifacts.glBlockIndexToTProgram.clear(); + artifacts.tProgramBlockIndexToGl.clear(); + artifacts.linkedExplicitUniformLocations.clear(); + artifacts.uniformIndexInTProgram.clear(); + artifacts.uniformSamplerOrImageUnitIndex.clear(); + artifacts.explicitOpaqueUniformBindings.clear(); + artifacts.uniformBlockIndexByName.clear(); + artifacts.uniformBlockBinding.clear(); + artifacts.uniformOffsets.clear(); + artifacts.uniformSizesInBytes.clear(); + artifacts.globalUboScratch.clear(); + artifacts.attribs.clear(); + artifacts.attribTypes.clear(); + artifacts.activeUniformCount = 0; + artifacts.maxUniformLocation = 0; + artifacts.uniformNameMaxLength = 0; + artifacts.attribInNameMaxLength = 0; + artifacts.uniformBlockNameMaxLength = 0; + artifacts.xfbVaryings.clear(); + artifacts.xfbStrides.clear(); + artifacts.xfbBufferMode = GL_INTERLEAVED_ATTRIBS; + artifacts.xfbVaryingNameMaxLength = 0; + artifacts.xfbNeedsScatteredCapture = false; + artifacts.xfbPackedStride = 0; + artifacts.gsInputPrimitive = GL_NONE; + artifacts.linkStatus = false; } namespace { @@ -241,12 +260,12 @@ namespace MobileGL::MG_State::GLState { } // namespace Bool ProgramObject::ResolveTransformFeedbackVaryings() { - m_xfbVaryings.clear(); - m_xfbStrides.clear(); - m_xfbBufferMode = m_requestedXfbBufferMode; - m_xfbVaryingNameMaxLength = 0; - m_xfbNeedsScatteredCapture = false; - m_xfbPackedStride = 0; + Artifacts().xfbVaryings.clear(); + Artifacts().xfbStrides.clear(); + Artifacts().xfbBufferMode = m_requestedXfbBufferMode; + Artifacts().xfbVaryingNameMaxLength = 0; + Artifacts().xfbNeedsScatteredCapture = false; + Artifacts().xfbPackedStride = 0; if (m_requestedXfbVaryings.empty()) { return true; } @@ -255,18 +274,18 @@ namespace MobileGL::MG_State::GLState { // tessellation evaluation, then vertex). const glslang::TIntermediate* captureIntermediate = nullptr; for (EShLanguage stage : {EShLangGeometry, EShLangTessEvaluation, EShLangVertex}) { - captureIntermediate = m_program->getIntermediate(stage); + captureIntermediate = Artifacts().program->getIntermediate(stage); if (captureIntermediate != nullptr) { break; } } if (captureIntermediate == nullptr) { - m_infoLog = "Transform feedback varyings requested but the program has no vertex-processing stage."; + Artifacts().infoLog = "Transform feedback varyings requested but the program has no vertex-processing stage."; return false; } const glslang::TIntermAggregate* linkerObjects = captureIntermediate->findLinkerObjects(); - const Bool interleaved = m_xfbBufferMode == GL_INTERLEAVED_ATTRIBS; + const Bool interleaved = Artifacts().xfbBufferMode == GL_INTERLEAVED_ATTRIBS; Uint32 interleavedOffset = 0; // ARB_transform_feedback3 lets an interleaved capture leave holes (gl_SkipComponents1..4) // and move on to the next buffer (gl_NextBuffer). Both only affect where the following @@ -280,18 +299,18 @@ namespace MobileGL::MG_State::GLState { interleavedStrides.push_back(interleavedOffset); interleavedOffset = 0; ++interleavedBufferIndex; - m_xfbNeedsScatteredCapture = true; + Artifacts().xfbNeedsScatteredCapture = true; continue; } if (interleaved && name.size() == 18 && name.compare(0, 17, "gl_SkipComponents") == 0 && name[17] >= '1' && name[17] <= '4') { interleavedOffset += static_cast(name[17] - '0') * 4; - m_xfbNeedsScatteredCapture = true; + Artifacts().xfbNeedsScatteredCapture = true; continue; } for (SizeT j = 0; j < i; ++j) { if (m_requestedXfbVaryings[j] == name) { - m_infoLog = "Transform feedback varying '" + name + "' is specified more than once."; + Artifacts().infoLog = "Transform feedback varying '" + name + "' is specified more than once."; return false; } } @@ -324,24 +343,24 @@ namespace MobileGL::MG_State::GLState { } } if (!resolved) { - m_infoLog = "Transform feedback varying '" + name + "' is not an output of the vertex stage."; + Artifacts().infoLog = "Transform feedback varying '" + name + "' is not an output of the vertex stage."; return false; } varying.byteSize = bytesPerElement * static_cast(varying.size); - varying.packedOffsetBytes = m_xfbPackedStride; - m_xfbPackedStride += varying.byteSize; + varying.packedOffsetBytes = Artifacts().xfbPackedStride; + Artifacts().xfbPackedStride += varying.byteSize; if (interleaved) { varying.bufferIndex = interleavedBufferIndex; varying.offsetBytes = interleavedOffset; interleavedOffset += varying.byteSize; } else { - varying.bufferIndex = static_cast(m_xfbVaryings.size()); + varying.bufferIndex = static_cast(Artifacts().xfbVaryings.size()); varying.offsetBytes = 0; } - m_xfbVaryingNameMaxLength = - std::max(m_xfbVaryingNameMaxLength, static_cast(name.size()) + 1); - m_xfbVaryings.push_back(Move(varying)); + Artifacts().xfbVaryingNameMaxLength = + std::max(Artifacts().xfbVaryingNameMaxLength, static_cast(name.size()) + 1); + Artifacts().xfbVaryings.push_back(Move(varying)); } constexpr Uint32 kMaxSeparateAttribs = 4; @@ -351,32 +370,32 @@ namespace MobileGL::MG_State::GLState { if (interleaved) { interleavedStrides.push_back(interleavedOffset); if (interleavedStrides.size() > kMaxTransformFeedbackBuffers) { - m_infoLog = "Transform feedback capture uses more buffers than " + Artifacts().infoLog = "Transform feedback capture uses more buffers than " "GL_MAX_TRANSFORM_FEEDBACK_BUFFERS."; return false; } for (const Uint32 stride : interleavedStrides) { if (stride > kMaxInterleavedComponents * 4) { - m_infoLog = "Transform feedback interleaved capture exceeds " + Artifacts().infoLog = "Transform feedback interleaved capture exceeds " "GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS."; return false; } } - m_xfbStrides = Move(interleavedStrides); + Artifacts().xfbStrides = Move(interleavedStrides); } else { - if (m_xfbVaryings.size() > kMaxSeparateAttribs) { - m_infoLog = "Transform feedback separate capture exceeds " + if (Artifacts().xfbVaryings.size() > kMaxSeparateAttribs) { + Artifacts().infoLog = "Transform feedback separate capture exceeds " "GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS."; return false; } - m_xfbStrides.resize(m_xfbVaryings.size()); - for (SizeT i = 0; i < m_xfbVaryings.size(); ++i) { - if (m_xfbVaryings[i].byteSize > kMaxSeparateComponents * 4) { - m_infoLog = "Transform feedback varying '" + m_xfbVaryings[i].name + + Artifacts().xfbStrides.resize(Artifacts().xfbVaryings.size()); + for (SizeT i = 0; i < Artifacts().xfbVaryings.size(); ++i) { + if (Artifacts().xfbVaryings[i].byteSize > kMaxSeparateComponents * 4) { + Artifacts().infoLog = "Transform feedback varying '" + Artifacts().xfbVaryings[i].name + "' exceeds GL_MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS."; return false; } - m_xfbStrides[i] = m_xfbVaryings[i].byteSize; + Artifacts().xfbStrides[i] = Artifacts().xfbVaryings[i].byteSize; } } @@ -428,12 +447,12 @@ namespace MobileGL::MG_State::GLState { } // namespace void ProgramObject::ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate) { - m_gsStripTriangles.clear(); - m_gsStripCaptureFixup = false; - if (captureIntermediate == nullptr || m_program == nullptr) { + Artifacts().gsStripTriangles.clear(); + Artifacts().gsStripCaptureFixup = false; + if (captureIntermediate == nullptr || Artifacts().program == nullptr) { return; } - if (m_program->getIntermediate(EShLangGeometry) != captureIntermediate) { + if (Artifacts().program->getIntermediate(EShLangGeometry) != captureIntermediate) { return; } if (captureIntermediate->getOutputPrimitive() != glslang::ElgTriangleStrip) { @@ -445,8 +464,8 @@ namespace MobileGL::MG_State::GLState { if (!traverser.hasEmit || traverser.inControlFlow || traverser.stripTriangles.empty()) { return; } - m_gsStripTriangles = Move(traverser.stripTriangles); - m_gsStripCaptureFixup = true; + Artifacts().gsStripTriangles = Move(traverser.stripTriangles); + Artifacts().gsStripCaptureFixup = true; } bool ProgramObject::ShaderIsAttached(const SharedPtr& shader) { @@ -524,8 +543,9 @@ namespace MobileGL::MG_State::GLState { void ProgramObject::Link(Bool addDefaultFSIfMissingForRenderingPipelineProgram) { MGLOG_D("ProgramObject %u: Link start, shaders to link: %zu", m_externalIndex, m_shaders.size()); ++m_backendStateVersion; + BumpLinkObservableVersions(); ResetLinkArtifacts(); - m_infoLog.clear(); + Artifacts().infoLog.clear(); // Remove detached shaders first for (const auto& detachedShader : m_detachedShaders) { RemoveShader(detachedShader); @@ -536,7 +556,7 @@ namespace MobileGL::MG_State::GLState { AddDefaultFragmentShaderIfMissing(); } if (m_shaders.empty()) { - m_infoLog = "No shader objects are attached to program."; + Artifacts().infoLog = "No shader objects are attached to program."; MGLOG_E("ProgramObject %u: Link failed - no shader objects attached.", m_externalIndex); return; } @@ -546,6 +566,16 @@ namespace MobileGL::MG_State::GLState { return a->GetShaderStage() < b->GetShaderStage(); }); + // ---- end of the GL-thread prologue ---- + // Everything above mutates GL-thread-owned state (the attach lists, the version + // counters, the default-FS fixup) and must stay on the calling thread. Everything + // below is a pure function of the snapshot taken here, which is what lets stage 4 + // lift it into a ProgramLinkTask. `env` is the first piece of that snapshot: the + // link's only window onto the backend. + const SharedPtr envPtr = + MG_Util::ShaderTranspiler::GetCurrentCompileEnv(); + const MG_Util::ShaderTranspiler::CompileEnv& env = *envPtr; + Vector shaderTypes(m_shaders.size()); Vector> shaders(m_shaders.size()); @@ -555,18 +585,18 @@ namespace MobileGL::MG_State::GLState { MG_Util::ConvertGLEnumToString(shaderTypes[i]).c_str(), m_shaders[i].get()); if (!m_shaders[i]->GetCompileStatus()) { - m_infoLog = std::format("Linking a {} with compilation error, linking will now terminate. Shader error " + Artifacts().infoLog = std::format("Linking a {} with compilation error, linking will now terminate. Shader error " "log:\n{}\nShader src:\n{}", MG_Util::ConvertGLEnumToString(shaderTypes[i]), m_shaders[i]->GetInfoLog(), m_shaders[i]->GetShaderSource()); MGLOG_E("ProgramObject %u: Link failed - shader[%zu] compile status false. InfoLog:\n%s", - m_externalIndex, i, m_infoLog.c_str()); + m_externalIndex, i, Artifacts().infoLog.c_str()); return; } if (m_shaders[i]->GetShaderStage() == ShaderStage::Compute && !ComputeShaderDeclaresLocalSize(m_shaders[i]->GetShaderSource())) { - m_infoLog = "Compute shader is missing a local_size layout declaration."; - MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, m_infoLog.c_str()); + Artifacts().infoLog = "Compute shader is missing a local_size layout declaration."; + MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, Artifacts().infoLog.c_str()); return; } String reparseLog; @@ -574,9 +604,9 @@ namespace MobileGL::MG_State::GLState { if (!shaders[i]) { // Only reachable when the consume-once re-parse of an already-compiled // source fails, which no valid state transition produces. - m_infoLog = std::format("Internal error: re-parsing an attached {} for linking failed:\n{}", + Artifacts().infoLog = std::format("Internal error: re-parsing an attached {} for linking failed:\n{}", MG_Util::ConvertGLEnumToString(shaderTypes[i]), reparseLog); - MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, m_infoLog.c_str()); + MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, Artifacts().infoLog.c_str()); return; } // Deliberately no full-source dump here: a shaderpack stage runs to ~100 KB, and @@ -591,13 +621,13 @@ namespace MobileGL::MG_State::GLState { // enforced this at mapIO; the relaxed parse no longer sees the qualifiers). for (const auto& shader : m_shaders) { for (const auto& [name, location] : shader->GetExplicitUniformLocations()) { - const auto [it, inserted] = m_linkedExplicitUniformLocations.emplace(name, location); + const auto [it, inserted] = Artifacts().linkedExplicitUniformLocations.emplace(name, location); if (!inserted && it->second != location) { - m_infoLog = std::format( + Artifacts().infoLog = std::format( "Uniform '{}' is declared with conflicting explicit locations ({} and {}) " "across stages.", name, it->second, location); - MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, m_infoLog.c_str()); + MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, Artifacts().infoLog.c_str()); return; } } @@ -605,7 +635,7 @@ namespace MobileGL::MG_State::GLState { // relaxed parse. Stage order matches the old per-stage mapIO capture, so a // name declared in several stages keeps the last stage's binding as before. for (const auto& [name, binding] : shader->GetExplicitOpaqueBindings()) { - m_explicitOpaqueUniformBindings[name] = binding; + Artifacts().explicitOpaqueUniformBindings[name] = binding; } } @@ -614,37 +644,37 @@ namespace MobileGL::MG_State::GLState { .explicitFragmentOutLocations = m_explicitFragDataLocation, .explicitFragmentOutIndices = m_explicitFragDataIndex, .explicitOpaqueUniformBindings = - &m_explicitOpaqueUniformBindings}; + &Artifacts().explicitOpaqueUniformBindings}; MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", m_externalIndex); auto result = MG_Util::ShaderTranspiler::ShaderCompiler::LinkProgram(attrib); if (result) { - m_linkStatus = true; - m_program = result.value(); - m_linkedFragDataLocation = m_explicitFragDataLocation; - m_linkedFragDataIndex = m_explicitFragDataIndex; - MGLOG_D("ProgramObject %u: LinkProgram succeeded, TProgram ptr %p", m_externalIndex, m_program.get()); + Artifacts().linkStatus = true; + Artifacts().program = result.value(); + Artifacts().linkedFragDataLocation = m_explicitFragDataLocation; + Artifacts().linkedFragDataIndex = m_explicitFragDataIndex; + MGLOG_D("ProgramObject %u: LinkProgram succeeded, TProgram ptr %p", m_externalIndex, Artifacts().program.get()); } else { - m_infoLog = result.error().log; - MGLOG_E("ProgramObject %u: LinkProgram failed. InfoLog:\n%s", m_externalIndex, m_infoLog.c_str()); + Artifacts().infoLog = result.error().log; + MGLOG_E("ProgramObject %u: LinkProgram failed. InfoLog:\n%s", m_externalIndex, Artifacts().infoLog.c_str()); return; } // GL_GEOMETRY_INPUT_TYPE. A draw's primitive type has to be compatible with it // (GL 4.6 core 11.3.1), so it is resolved for every link, not only a capturing one. - m_gsInputPrimitive = GL_NONE; - if (const glslang::TIntermediate* gs = m_program->getIntermediate(EShLangGeometry)) { + Artifacts().gsInputPrimitive = GL_NONE; + if (const glslang::TIntermediate* gs = Artifacts().program->getIntermediate(EShLangGeometry)) { switch (gs->getInputPrimitive()) { - case glslang::ElgPoints: m_gsInputPrimitive = GL_POINTS; break; - case glslang::ElgLines: m_gsInputPrimitive = GL_LINES; break; - case glslang::ElgLinesAdjacency: m_gsInputPrimitive = GL_LINES_ADJACENCY; break; - case glslang::ElgTriangles: m_gsInputPrimitive = GL_TRIANGLES; break; - case glslang::ElgTrianglesAdjacency: m_gsInputPrimitive = GL_TRIANGLES_ADJACENCY; break; + case glslang::ElgPoints: Artifacts().gsInputPrimitive = GL_POINTS; break; + case glslang::ElgLines: Artifacts().gsInputPrimitive = GL_LINES; break; + case glslang::ElgLinesAdjacency: Artifacts().gsInputPrimitive = GL_LINES_ADJACENCY; break; + case glslang::ElgTriangles: Artifacts().gsInputPrimitive = GL_TRIANGLES; break; + case glslang::ElgTrianglesAdjacency: Artifacts().gsInputPrimitive = GL_TRIANGLES_ADJACENCY; break; default: break; } } - // SPIR-V must be generated BEFORE buildReflection touches m_program: + // SPIR-V must be generated BEFORE buildReflection touches Artifacts().program: // reflection's live-variable analysis mutates the intermediates in ways that // change subsequent GlslangToSpv output (observed: catastrophic uniform // misbinding on DirectVulkan for UBO-heavy content). The old two-link pipeline @@ -658,25 +688,25 @@ namespace MobileGL::MG_State::GLState { GenerateSpirv(); MGLOG_D("ProgramObject %u: Starting reflection", m_externalIndex); - if (!DoReflection()) { - MGLOG_E("ProgramObject %u: Link failed during reflection: %s", m_externalIndex, m_infoLog.c_str()); + if (!DoReflection(env)) { + MGLOG_E("ProgramObject %u: Link failed during reflection: %s", m_externalIndex, Artifacts().infoLog.c_str()); return; } MGLOG_D("ProgramObject %u: Building global-UBO routing tables", m_externalIndex); BuildGlobalUboRouting(); - MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", m_externalIndex, (int)m_linkStatus); + MGLOG_D("ProgramObject %u: Reflection done (linkStatus=%d)", m_externalIndex, (int)Artifacts().linkStatus); if (!ValidateFragmentOutputLocations()) { return; } if (!ResolveTransformFeedbackVaryings()) { - m_linkStatus = false; + Artifacts().linkStatus = false; MGLOG_E("ProgramObject %u: transform feedback varying resolution failed: %s", m_externalIndex, - m_infoLog.c_str()); + Artifacts().infoLog.c_str()); return; } MGLOG_D("ProgramObject %u: Binary generation finished (generatedSpirv size=%zu)", m_externalIndex, - m_generatedSpirv.size()); + Artifacts().generatedSpirv.size()); } void ProgramObject::MarkAsDeleted() { @@ -696,11 +726,11 @@ namespace MobileGL::MG_State::GLState { return m_shaders; } - Bool ProgramObject::DoReflection() { - if (!m_program) { - MGLOG_E("ProgramObject %u: DoReflection called but m_program is null", m_externalIndex); - m_linkStatus = false; - m_infoLog = "DoReflection failed: no program."; + Bool ProgramObject::DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env) { + if (!Artifacts().program) { + MGLOG_E("ProgramObject %u: DoReflection called but the linked program is null", m_externalIndex); + Artifacts().linkStatus = false; + Artifacts().infoLog = "DoReflection failed: no program."; return false; } @@ -715,10 +745,10 @@ namespace MobileGL::MG_State::GLState { // - SharedStd140UBO: a DECLARED uniform block is active even when no member is // ever read (reflected from the linker objects). PreprocessShaderSource coerces // every block to std140, so this covers all of them. - if (!m_program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix | + if (!Artifacts().program->buildReflection(EShReflectionStrictArraySuffix | EShReflectionBasicArraySuffix | EShReflectionAllBlockVariables | EShReflectionSharedStd140UBO)) { - m_linkStatus = false; - m_infoLog = "Build reflection failed."; + Artifacts().linkStatus = false; + Artifacts().infoLog = "Build reflection failed."; MGLOG_E("ProgramObject %u: DoReflection - buildReflection() returned false", m_externalIndex); return false; } @@ -728,16 +758,16 @@ namespace MobileGL::MG_State::GLState { // synthesized MGL_GLOBAL_UBO is a transpiler artifact - its members are GL // default-block uniforms and the block itself must stay invisible to GL (it // did not exist in the GL-client parse this replaces). - const Int tProgramBlockCount = m_program->getNumUniformBlocks(); - m_tProgramBlockIndexToGl.assign(tProgramBlockCount, -1); - m_glBlockIndexToTProgram.clear(); + const Int tProgramBlockCount = Artifacts().program->getNumUniformBlocks(); + Artifacts().tProgramBlockIndexToGl.assign(tProgramBlockCount, -1); + Artifacts().glBlockIndexToTProgram.clear(); for (Int i = 0; i < tProgramBlockCount; i++) { - const auto& ubo = m_program->getUniformBlock(i); + const auto& ubo = Artifacts().program->getUniformBlock(i); if (std::strstr(ubo.name.c_str(), MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != nullptr) { continue; } - m_tProgramBlockIndexToGl[i] = static_cast(m_glBlockIndexToTProgram.size()); - m_glBlockIndexToTProgram.push_back(i); + Artifacts().tProgramBlockIndexToGl[i] = static_cast(Artifacts().glBlockIndexToTProgram.size()); + Artifacts().glBlockIndexToTProgram.push_back(i); } // ------------ Uniforms (GL Plain) ---------------- @@ -747,27 +777,27 @@ namespace MobileGL::MG_State::GLState { // glGetActiveUniform, glGetUniformLocation == -1): filter global-UBO members no // stage references. Named-block members keep GL's every-declared-member-is-active // semantics, exactly as before. - const Int tProgramUniformCount = m_program->getNumUniformVariables(); - m_tProgramUniformIndexToGl.assign(tProgramUniformCount, -1); - m_glUniformIndexToTProgram.clear(); + const Int tProgramUniformCount = Artifacts().program->getNumUniformVariables(); + Artifacts().tProgramUniformIndexToGl.assign(tProgramUniformCount, -1); + Artifacts().glUniformIndexToTProgram.clear(); const auto isGlobalUboMember = [this](const glslang::TObjectReflection& uniform) { - return uniform.index >= 0 && uniform.index < static_cast(m_tProgramBlockIndexToGl.size()) && - m_tProgramBlockIndexToGl[uniform.index] < 0; + return uniform.index >= 0 && uniform.index < static_cast(Artifacts().tProgramBlockIndexToGl.size()) && + Artifacts().tProgramBlockIndexToGl[uniform.index] < 0; }; for (Int i = 0; i < tProgramUniformCount; i++) { - const auto& uniform = m_program->getUniform(i); + const auto& uniform = Artifacts().program->getUniform(i); if (isGlobalUboMember(uniform) && uniform.stages == 0) { MGLOG_D("ProgramObject %u: Reflection - dead default-block uniform '%s' filtered from the GL " "surface", m_externalIndex, uniform.name.c_str()); continue; } - m_tProgramUniformIndexToGl[i] = static_cast(m_glUniformIndexToTProgram.size()); - m_glUniformIndexToTProgram.push_back(i); + Artifacts().tProgramUniformIndexToGl[i] = static_cast(Artifacts().glUniformIndexToTProgram.size()); + Artifacts().glUniformIndexToTProgram.push_back(i); } - m_activeUniformCount = static_cast(m_glUniformIndexToTProgram.size()); + Artifacts().activeUniformCount = static_cast(Artifacts().glUniformIndexToTProgram.size()); MGLOG_D("ProgramObject %u: Reflection - active uniform count = %d (of %d reflected)", m_externalIndex, - m_activeUniformCount, tProgramUniformCount); + Artifacts().activeUniformCount, tProgramUniformCount); // Effective explicit location per TProgram uniform, from two sources: // - the lexical side-channel for default-block uniforms - the relaxed parse @@ -780,15 +810,15 @@ namespace MobileGL::MG_State::GLState { Vector locationIsSourceExplicit(tProgramUniformCount, false); UnorderedMap structExplicitCursor; // declared root -> next member location const auto findExplicitLocation = [this](const String& reflectedName) -> const Int* { - auto it = m_linkedExplicitUniformLocations.find(reflectedName); - if (it == m_linkedExplicitUniformLocations.end() && reflectedName.length() > 3 && + auto it = Artifacts().linkedExplicitUniformLocations.find(reflectedName); + if (it == Artifacts().linkedExplicitUniformLocations.end() && reflectedName.length() > 3 && reflectedName.compare(reflectedName.length() - 3, 3, "[0]") == 0) { - it = m_linkedExplicitUniformLocations.find(reflectedName.substr(0, reflectedName.length() - 3)); + it = Artifacts().linkedExplicitUniformLocations.find(reflectedName.substr(0, reflectedName.length() - 3)); } - return it != m_linkedExplicitUniformLocations.end() ? &it->second : nullptr; + return it != Artifacts().linkedExplicitUniformLocations.end() ? &it->second : nullptr; }; - for (const Int i : m_glUniformIndexToTProgram) { - const auto& uniform = m_program->getUniform(i); + for (const Int i : Artifacts().glUniformIndexToTProgram) { + const auto& uniform = Artifacts().program->getUniform(i); const glslang::TType* type = uniform.getType(); const Bool inNamedBlock = uniform.index >= 0 && !isGlobalUboMember(uniform); if (inNamedBlock) continue; // block members never take glUniform locations @@ -796,13 +826,13 @@ namespace MobileGL::MG_State::GLState { if (const Int* explicitLocation = findExplicitLocation(uniform.name)) { effectiveLocation[i] = static_cast(*explicitLocation); locationIsSourceExplicit[i] = true; - } else if (!m_linkedExplicitUniformLocations.empty() && + } else if (!Artifacts().linkedExplicitUniformLocations.empty() && uniform.name.find('.') != String::npos) { // A struct uniform's explicit location spreads consecutively over its // flattened members ("s.a", "s[1].b", ...) in reflection order. const SizeT cut = uniform.name.find_first_of(".["); - const auto rootIt = m_linkedExplicitUniformLocations.find(uniform.name.substr(0, cut)); - if (rootIt != m_linkedExplicitUniformLocations.end()) { + const auto rootIt = Artifacts().linkedExplicitUniformLocations.find(uniform.name.substr(0, cut)); + if (rootIt != Artifacts().linkedExplicitUniformLocations.end()) { auto [cursor, inserted] = structExplicitCursor.emplace(rootIt->first, static_cast(rootIt->second)); (void)inserted; @@ -818,7 +848,7 @@ namespace MobileGL::MG_State::GLState { effectiveLocation[i] + static_cast(GetUniformLocationSpan(uniform)) > kNoLocation) { // Config A rejected out-of-range explicit locations at parse; keep them // from growing the location table unboundedly. - m_infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name, + Artifacts().infoLog = std::format("Uniform '{}' explicit location {} is out of range.", uniform.name, effectiveLocation[i]); ResetLinkArtifacts(); return false; @@ -826,35 +856,35 @@ namespace MobileGL::MG_State::GLState { } Int requiredUniformLocations = 0; - for (const Int i : m_glUniformIndexToTProgram) { - auto& uniform = m_program->getUniform(i); + for (const Int i : Artifacts().glUniformIndexToTProgram) { + auto& uniform = Artifacts().program->getUniform(i); const Uint location = effectiveLocation[i]; const Int locationSpan = GetUniformLocationSpan(uniform); requiredUniformLocations += locationSpan; if (location != kNoLocation) { - m_maxUniformLocation = std::max(m_maxUniformLocation, location + locationSpan - 1); + Artifacts().maxUniformLocation = std::max(Artifacts().maxUniformLocation, location + locationSpan - 1); } - m_uniformNameMaxLength = std::max(m_uniformNameMaxLength, (Int)uniform.name.length()); - m_uniformLocations[uniform.name] = location; + Artifacts().uniformNameMaxLength = std::max(Artifacts().uniformNameMaxLength, (Int)uniform.name.length()); + Artifacts().uniformLocations[uniform.name] = location; MGLOG_D("ProgramObject %u: Reflection - uniform[%d] name='%s' effectiveLocation=%d", m_externalIndex, i, uniform.name.c_str(), location); } - MGLOG_D("ProgramObject %u: Reflection - computed m_maxUniformLocation=%u m_uniformNameMaxLength=%d", - m_externalIndex, m_maxUniformLocation, m_uniformNameMaxLength); + MGLOG_D("ProgramObject %u: Reflection - computed maxUniformLocation=%u uniformNameMaxLength=%d", + m_externalIndex, Artifacts().maxUniformLocation, Artifacts().uniformNameMaxLength); - if (m_maxUniformLocation + 1 < requiredUniformLocations) { + if (Artifacts().maxUniformLocation + 1 < requiredUniformLocations) { MGLOG_D("ProgramObject %u: Reflection - maxUniformLocation+1 (%u) < requiredUniformLocations (%d), " "adjusting", - m_externalIndex, m_maxUniformLocation + 1, requiredUniformLocations); + m_externalIndex, Artifacts().maxUniformLocation + 1, requiredUniformLocations); // This means we have fewer than enough gaps to fit // unallocated uniforms - m_maxUniformLocation = requiredUniformLocations - 1; + Artifacts().maxUniformLocation = requiredUniformLocations - 1; } // i-th elements refers to uniform at layout(location = i, ...) - m_uniformIndexInTProgram.resize(m_maxUniformLocation + 1, glslang::TQualifier::layoutLocationEnd); - m_uniformSamplerOrImageUnitIndex.resize(m_maxUniformLocation + 1, -1); + Artifacts().uniformIndexInTProgram.resize(Artifacts().maxUniformLocation + 1, glslang::TQualifier::layoutLocationEnd); + Artifacts().uniformSamplerOrImageUnitIndex.resize(Artifacts().maxUniformLocation + 1, -1); Vector unallocatedUniformIndex; @@ -862,21 +892,21 @@ namespace MobileGL::MG_State::GLState { // (ARB_explicit_uniform_location), and an overlap between distinct uniforms is a // link error - config A's mapIO rejected it ("Uniform location overlaps across // stages"); the relaxed parse dropped the qualifiers, so it is enforced here. - for (const Int i : m_glUniformIndexToTProgram) { - auto& uniform = m_program->getUniform(i); + for (const Int i : Artifacts().glUniformIndexToTProgram) { + auto& uniform = Artifacts().program->getUniform(i); if (!locationIsSourceExplicit[i] || effectiveLocation[i] == kNoLocation) continue; const Uint location = effectiveLocation[i]; const Int locationSpan = GetUniformLocationSpan(uniform); for (Int element = 0; element < locationSpan; ++element) { - const Int existing = m_uniformIndexInTProgram[location + element]; + const Int existing = Artifacts().uniformIndexInTProgram[location + element]; if (existing != glslang::TQualifier::layoutLocationEnd && existing != i) { - m_infoLog = + Artifacts().infoLog = std::format("Uniform location overlap: '{}' and '{}' both occupy location {}.", - m_program->getUniform(existing).name, uniform.name, location + element); + Artifacts().program->getUniform(existing).name, uniform.name, location + element); ResetLinkArtifacts(); return false; } - m_uniformIndexInTProgram[location + element] = i; + Artifacts().uniformIndexInTProgram[location + element] = i; } MGLOG_D("ProgramObject %u: Reflection - assigned explicit-location uniform '%s' to locations " "%u..%u (indexInTProgram=%d)", @@ -886,8 +916,8 @@ namespace MobileGL::MG_State::GLState { // Pass 2: glslang-assigned locations (opaque uniforms under the relaxed parse). // Implementation-chosen, so on a collision with an explicit location the uniform // is demoted to the first-fit pass below instead of failing the link. - for (const Int i : m_glUniformIndexToTProgram) { - auto& uniform = m_program->getUniform(i); + for (const Int i : Artifacts().glUniformIndexToTProgram) { + auto& uniform = Artifacts().program->getUniform(i); if (locationIsSourceExplicit[i]) continue; const Uint location = effectiveLocation[i]; if (location == kNoLocation) { @@ -897,13 +927,13 @@ namespace MobileGL::MG_State::GLState { continue; // will allocate unallocated uniforms later } const Int locationSpan = GetUniformLocationSpan(uniform); - Bool spanIsFree = location + locationSpan - 1 <= m_maxUniformLocation; + Bool spanIsFree = location + locationSpan - 1 <= Artifacts().maxUniformLocation; for (Int element = 0; spanIsFree && element < locationSpan; ++element) { spanIsFree = - m_uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd; + Artifacts().uniformIndexInTProgram[location + element] == glslang::TQualifier::layoutLocationEnd; } if (!spanIsFree) { - m_uniformLocations[uniform.name] = kNoLocation; + Artifacts().uniformLocations[uniform.name] = kNoLocation; unallocatedUniformIndex.emplace_back(i); MGLOG_D("ProgramObject %u: Reflection - uniform '%s' auto location %u collides with an " "explicit location, demoting to first-fit", @@ -911,7 +941,7 @@ namespace MobileGL::MG_State::GLState { continue; } for (Int element = 0; element < locationSpan; ++element) { - m_uniformIndexInTProgram[location + element] = i; + Artifacts().uniformIndexInTProgram[location + element] = i; } MGLOG_D("ProgramObject %u: Reflection - assigned uniform '%s' to locations %u..%u " "(indexInTProgram=%d)", @@ -920,26 +950,26 @@ namespace MobileGL::MG_State::GLState { SizeT locNeedle = 0; std::sort(unallocatedUniformIndex.begin(), unallocatedUniformIndex.end(), [this](Int lhs, Int rhs) { - const auto& lhsUniform = m_program->getUniform(lhs); - const auto& rhsUniform = m_program->getUniform(rhs); + const auto& lhsUniform = Artifacts().program->getUniform(lhs); + const auto& rhsUniform = Artifacts().program->getUniform(rhs); return lhsUniform.name < rhsUniform.name; }); for (auto index : unallocatedUniformIndex) { - auto& uniform = m_program->getUniform(index); + auto& uniform = Artifacts().program->getUniform(index); const Int locationSpan = GetUniformLocationSpan(uniform); Bool placed = false; - for (; locNeedle <= m_maxUniformLocation; locNeedle++) { - bool hasRoom = locNeedle + locationSpan - 1 <= m_maxUniformLocation; + for (; locNeedle <= Artifacts().maxUniformLocation; locNeedle++) { + bool hasRoom = locNeedle + locationSpan - 1 <= Artifacts().maxUniformLocation; for (Int element = 0; hasRoom && element < locationSpan; ++element) { - hasRoom = m_uniformIndexInTProgram[locNeedle + element] == + hasRoom = Artifacts().uniformIndexInTProgram[locNeedle + element] == glslang::TQualifier::layoutLocationEnd; } if (!hasRoom) continue; // Found a vacant location at locNeedle for (Int element = 0; element < locationSpan; ++element) { - m_uniformIndexInTProgram[locNeedle + element] = index; + Artifacts().uniformIndexInTProgram[locNeedle + element] = index; } - m_uniformLocations[uniform.name] = locNeedle; + Artifacts().uniformLocations[uniform.name] = locNeedle; MGLOG_D("ProgramObject %u: Reflection - assigned unallocated uniform '%s' to locations %zu..%zu " "(index %d)", m_externalIndex, uniform.name.c_str(), locNeedle, locNeedle + locationSpan - 1, index); @@ -951,74 +981,74 @@ namespace MobileGL::MG_State::GLState { // Explicit-location uniforms can fragment the space so no contiguous // span is left; grow the table instead of leaving the uniform without // a location (which would make it unsettable via glUniform*). - const SizeT base = m_uniformIndexInTProgram.size(); - m_uniformIndexInTProgram.resize(base + locationSpan, glslang::TQualifier::layoutLocationEnd); - m_uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1); - m_maxUniformLocation = static_cast(base + locationSpan - 1); + const SizeT base = Artifacts().uniformIndexInTProgram.size(); + Artifacts().uniformIndexInTProgram.resize(base + locationSpan, glslang::TQualifier::layoutLocationEnd); + Artifacts().uniformSamplerOrImageUnitIndex.resize(base + locationSpan, -1); + Artifacts().maxUniformLocation = static_cast(base + locationSpan - 1); for (Int element = 0; element < locationSpan; ++element) { - m_uniformIndexInTProgram[base + element] = index; + Artifacts().uniformIndexInTProgram[base + element] = index; } - m_uniformLocations[uniform.name] = static_cast(base); + Artifacts().uniformLocations[uniform.name] = static_cast(base); MGLOG_D("ProgramObject %u: Reflection - grew location table to place uniform '%s' at %zu..%zu", m_externalIndex, uniform.name.c_str(), base, base + locationSpan - 1); locNeedle = base + locationSpan; } } - for (const Int i : m_glUniformIndexToTProgram) { - auto& uniform = m_program->getUniform(i); - const auto locationIt = m_uniformLocations.find(uniform.name); - if (locationIt == m_uniformLocations.end()) { + for (const Int i : Artifacts().glUniformIndexToTProgram) { + auto& uniform = Artifacts().program->getUniform(i); + const auto locationIt = Artifacts().uniformLocations.find(uniform.name); + if (locationIt == Artifacts().uniformLocations.end()) { continue; } const Uint location = locationIt->second; - if (location >= m_uniformSamplerOrImageUnitIndex.size() || uniform.getType() == nullptr || + if (location >= Artifacts().uniformSamplerOrImageUnitIndex.size() || uniform.getType() == nullptr || !uniform.getType()->isOpaque() || (!uniform.getType()->isTexture() && !uniform.getType()->isImage())) { continue; } // Reflection names an array "texs[0]" while the layout(binding = N) map from the IO // resolver is keyed by the declared name ("texs"); look up both spellings. - auto explicitBinding = m_explicitOpaqueUniformBindings.find(uniform.name); - if (explicitBinding == m_explicitOpaqueUniformBindings.end() && uniform.name.length() > 3 && + auto explicitBinding = Artifacts().explicitOpaqueUniformBindings.find(uniform.name); + if (explicitBinding == Artifacts().explicitOpaqueUniformBindings.end() && uniform.name.length() > 3 && uniform.name.compare(uniform.name.length() - 3, 3, "[0]") == 0) { explicitBinding = - m_explicitOpaqueUniformBindings.find(uniform.name.substr(0, uniform.name.length() - 3)); + Artifacts().explicitOpaqueUniformBindings.find(uniform.name.substr(0, uniform.name.length() - 3)); } const int initialUnit = - explicitBinding != m_explicitOpaqueUniformBindings.end() ? static_cast(explicitBinding->second) : 0; + explicitBinding != Artifacts().explicitOpaqueUniformBindings.end() ? static_cast(explicitBinding->second) : 0; const Int locationSpan = GetUniformLocationSpan(uniform); for (Int element = 0; element < locationSpan && - location + element < m_uniformSamplerOrImageUnitIndex.size(); ++element) { - m_uniformSamplerOrImageUnitIndex[location + element] = - initialUnit + (explicitBinding != m_explicitOpaqueUniformBindings.end() ? element : 0); + location + element < Artifacts().uniformSamplerOrImageUnitIndex.size(); ++element) { + Artifacts().uniformSamplerOrImageUnitIndex[location + element] = + initialUnit + (explicitBinding != Artifacts().explicitOpaqueUniformBindings.end() ? element : 0); } MGLOG_D("ProgramObject %u: Reflection - opaque uniform '%s' locations=%u..%u initialUnit=%d", m_externalIndex, uniform.name.c_str(), location, location + locationSpan - 1, initialUnit); } // ------------ attributes (vertex in) --------------- - Int inCount = m_program->getNumPipeInputs(); + Int inCount = Artifacts().program->getNumPipeInputs(); MGLOG_D("ProgramObject %u: Reflection - pipe input count (attributes) = %d", m_externalIndex, inCount); Int maxLoc = -1; for (int i = 0; i < inCount; ++i) { - Int loc = (Int)m_program->getPipeInput(i).layoutLocation(); + Int loc = (Int)Artifacts().program->getPipeInput(i).layoutLocation(); if (loc >= 0 && loc != glslang::TQualifier::layoutLocationEnd) { - const Int locationSpan = GetVertexInputLocationSpan(m_program->getPipeInput(i).glDefineType); + const Int locationSpan = GetVertexInputLocationSpan(Artifacts().program->getPipeInput(i).glDefineType); maxLoc = std::max(maxLoc, loc + locationSpan - 1); } MGLOG_D("ProgramObject %u: Reflection - pipe input[%d] name='%s' layoutLocation=%d glType=%u", - m_externalIndex, i, m_program->getPipeInput(i).name.c_str(), loc, - m_program->getPipeInput(i).glDefineType); + m_externalIndex, i, Artifacts().program->getPipeInput(i).name.c_str(), loc, + Artifacts().program->getPipeInput(i).glDefineType); } if (maxLoc < 0) { maxLoc = std::max(0, inCount - 1); } - const GLint maxAttribs = GetReflectionVertexAttribLimit(); + const GLint maxAttribs = GetReflectionVertexAttribLimit(env); MGLOG_D("ProgramObject %u: Reflection - computed maxLoc=%d, using maxAttribs=%d", m_externalIndex, maxLoc, maxAttribs); @@ -1029,28 +1059,28 @@ namespace MobileGL::MG_State::GLState { maxLoc = maxAttribs - 1; } - m_attribs.resize(maxLoc + 1); - m_attribTypes.resize(maxLoc + 1); + Artifacts().attribs.resize(maxLoc + 1); + Artifacts().attribTypes.resize(maxLoc + 1); for (int i = 0; i < inCount; ++i) { - auto& inVar = m_program->getPipeInput(i); + auto& inVar = Artifacts().program->getPipeInput(i); Int location = (Int)inVar.layoutLocation(); // Builtins reflect under their SPIR-V names here; GL_ACTIVE_ATTRIBUTE_MAX_LENGTH // must measure the GL spelling glGetActiveAttrib will report. - m_attribInNameMaxLength = - std::max(m_attribInNameMaxLength, (Int)NormalizeBuiltinPipeInputName(inVar.name).length()); + Artifacts().attribInNameMaxLength = + std::max(Artifacts().attribInNameMaxLength, (Int)NormalizeBuiltinPipeInputName(inVar.name).length()); - if (location >= 0 && location < (int)m_attribs.size()) { + if (location >= 0 && location < (int)Artifacts().attribs.size()) { const Int locationSpan = GetVertexInputLocationSpan(inVar.glDefineType); const GLenum locationType = GetVertexInputLocationType(inVar.glDefineType); for (Int locationOffset = 0; locationOffset < locationSpan; ++locationOffset) { const Int expandedLocation = location + locationOffset; - if (expandedLocation < 0 || expandedLocation >= static_cast(m_attribs.size())) { + if (expandedLocation < 0 || expandedLocation >= static_cast(Artifacts().attribs.size())) { break; } - m_attribs[expandedLocation] = inVar.name; - m_attribTypes[expandedLocation] = locationType; + Artifacts().attribs[expandedLocation] = inVar.name; + Artifacts().attribTypes[expandedLocation] = locationType; MGLOG_D( "ProgramObject %u: Reflection - got attrib '%s' at expanded location %d (baseLocation=%d glType=%u expandedType=%u)", m_externalIndex, @@ -1067,14 +1097,14 @@ namespace MobileGL::MG_State::GLState { // GL-visible blocks only (MGL_GLOBAL_UBO was filtered out above). const Int uboCount = GetActiveUniformBlocksCount(); MGLOG_D("ProgramObject %u: Reflection - uniform block count (UBO) = %d", m_externalIndex, uboCount); - m_uniformBlockBinding.resize(uboCount, -1); + Artifacts().uniformBlockBinding.resize(uboCount, -1); for (Int i = 0; i < uboCount; i++) { - auto& ubo = m_program->getUniformBlock(m_glBlockIndexToTProgram[i]); - m_uniformBlockNameMaxLength = std::max(m_uniformBlockNameMaxLength, (Int)ubo.name.length()); - m_uniformBlockIndexByName[ubo.name] = i; + auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[i]); + Artifacts().uniformBlockNameMaxLength = std::max(Artifacts().uniformBlockNameMaxLength, (Int)ubo.name.length()); + Artifacts().uniformBlockIndexByName[ubo.name] = i; // if there's binding defined in shader as layout(binding = ...), // retrieve it here - m_uniformBlockBinding[i] = ubo.getBinding(); + Artifacts().uniformBlockBinding[i] = ubo.getBinding(); MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", m_externalIndex, i, ubo.name.c_str(), ubo.size, ubo.getBinding()); } @@ -1091,7 +1121,7 @@ namespace MobileGL::MG_State::GLState { MGLOG_D("ProgramObject %u: GenerateSpirv - start", m_externalIndex); // The shaders were parsed once, in the link-compatible (relaxed Vulkan-rules) - // configuration, and m_program linked those parses - so m_program IS the + // 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. @@ -1102,7 +1132,7 @@ namespace MobileGL::MG_State::GLState { ProgramBinaryAttrib binaryAttrib{ .shaderTypes = shaderTypes, - .program = *m_program, + .program = *Artifacts().program, }; MGLOG_D("ProgramObject %u: GenerateSpirv - requesting SPIR-V binary from program", m_externalIndex); auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); @@ -1110,12 +1140,12 @@ namespace MobileGL::MG_State::GLState { MGLOG_E("ProgramObject %u: GenerateSpirv - GetSpirvBinaryFromProgram failed", m_externalIndex); } MOBILEGL_ASSERT(binaryResult, "GetSpirvBinaryFromProgram failed"); - m_generatedSpirv = Move(binaryResult.value()); + Artifacts().generatedSpirv = Move(binaryResult.value()); MGLOG_D("ProgramObject %u: GenerateSpirv - generated %zu SPIR-V modules", m_externalIndex, - m_generatedSpirv.size()); + Artifacts().generatedSpirv.size()); // Linked SPIR-V generated, sanitize and optimize it - for (auto& spv : m_generatedSpirv) { + for (auto& spv : Artifacts().generatedSpirv) { auto success = ShaderCompiler::SanitizeAndOptimizeBinary(spv, spv); MOBILEGL_ASSERT(success, "SanitizeBinary failed"); } @@ -1128,16 +1158,16 @@ namespace MobileGL::MG_State::GLState { shaderTypes[i] = MG_Util::ConvertShaderStageToGLEnum(m_shaders[i]->GetShaderStage()); } - m_uniformSizesInBytes.clear(); - m_uniformOffsets.clear(); - m_globalUboScratch.clear(); + 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. - m_uniformOffsets.resize(m_maxUniformLocation + 1, kInvalidUniformOffset); - m_uniformSizesInBytes.resize(m_maxUniformLocation + 1, 0); - for (SizeT i = 0; i < m_generatedSpirv.size(); i++) { - auto& spv = m_generatedSpirv[i]; + Artifacts().uniformOffsets.resize(Artifacts().maxUniformLocation + 1, 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 " @@ -1161,20 +1191,20 @@ namespace MobileGL::MG_State::GLState { if (size == 0) { continue; } - if (m_globalUboScratch.size() < size) { - m_globalUboScratch.resize(size); + 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 = m_uniformLocations.find(name); - if (locationIt == m_uniformLocations.end()) { - locationIt = m_uniformLocations.find(name + "[0]"); + auto locationIt = Artifacts().uniformLocations.find(name); + if (locationIt == Artifacts().uniformLocations.end()) { + locationIt = Artifacts().uniformLocations.find(name + "[0]"); } - if (locationIt == m_uniformLocations.end()) { + if (locationIt == Artifacts().uniformLocations.end()) { MGLOG_D("ProgramObject %u: BuildGlobalUboRouting - uniform '%s' offset=%u but not found in " - "m_uniformLocations", + "uniformLocations", m_externalIndex, name.c_str(), offset); continue; } @@ -1183,7 +1213,7 @@ namespace MobileGL::MG_State::GLState { continue; } - const Int uniformIndex = m_uniformIndexInTProgram[baseLocation]; + const Int uniformIndex = Artifacts().uniformIndexInTProgram[baseLocation]; const GLint arraySize = GetUniformArraySizeByTIndex(uniformIndex); SizeT memberSize = 0; const auto sizeIt = meta.plainUniformMemberSizesInBytes.find(name); @@ -1201,12 +1231,12 @@ namespace MobileGL::MG_State::GLState { 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(element); - if (location > m_maxUniformLocation || m_uniformIndexInTProgram[location] != uniformIndex) { + if (location > Artifacts().maxUniformLocation || Artifacts().uniformIndexInTProgram[location] != uniformIndex) { break; } - m_uniformOffsets[location] = offset + static_cast(element) * arrayStride; + Artifacts().uniformOffsets[location] = offset + static_cast(element) * arrayStride; const SizeT consumed = static_cast(element) * arrayStride; - m_uniformSizesInBytes[location] = memberSize > consumed ? memberSize - consumed : 0; + 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", @@ -1225,14 +1255,14 @@ namespace MobileGL::MG_State::GLState { // 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 <= m_maxUniformLocation; ++location) { - if (m_uniformOffsets[location] != kInvalidUniformOffset) continue; + for (Uint location = 0; location <= Artifacts().maxUniformLocation; ++location) { + if (Artifacts().uniformOffsets[location] != kInvalidUniformOffset) continue; if (!IsValidUniformLocation(static_cast(location))) continue; - const auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]); + 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 < m_program->getNumUniformBlocks() && - std::strstr(m_program->getUniformBlock(uniform.index).name.c_str(), + 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. @@ -1246,22 +1276,16 @@ namespace MobileGL::MG_State::GLState { slotSize = static_cast(type->getMatrixCols()) * 16u; } slotSize = (slotSize + 15u) & ~static_cast(15u); - const SizeT slotOffset = (m_globalUboScratch.size() + 15u) & ~static_cast(15u); - m_globalUboScratch.resize(slotOffset + slotSize, 0); - m_uniformOffsets[location] = static_cast(slotOffset); - m_uniformSizesInBytes[location] = slotSize; + const SizeT slotOffset = (Artifacts().globalUboScratch.size() + 15u) & ~static_cast(15u); + Artifacts().globalUboScratch.resize(slotOffset + slotSize, 0); + Artifacts().uniformOffsets[location] = static_cast(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", m_externalIndex, uniform.name.c_str(), location, slotSize, slotOffset); } } - void ProgramObject::WaitUntilGenerationCompleted() const { - MGLOG_D("ProgramObject %u: WaitUntilGenerationCompleted called (no-op)", m_externalIndex); - // currently no-op, but keep log for debugging - // will probably be useful when multi-threaded compilation - } - void ProgramObject::SetExplicitVertexInLocation(Uint index, const char* name) { MGLOG_D("ProgramObject %u: SetExplicitVertexInLocation called name='%s' index=%u", m_externalIndex, name, index); @@ -1285,12 +1309,12 @@ namespace MobileGL::MG_State::GLState { } Bool ProgramObject::ValidateFragmentOutputLocations() { - if (!m_program) return false; + if (!Artifacts().program) return false; UnorderedMap colorNumberOwners; - const Int outputCount = m_program->getNumPipeOutputs(); + const Int outputCount = Artifacts().program->getNumPipeOutputs(); for (Int index = 0; index < outputCount; ++index) { - const auto& output = m_program->getPipeOutput(index); + const auto& output = Artifacts().program->getPipeOutput(index); if (IsBuiltInPipelineOutput(output)) { continue; } @@ -1303,9 +1327,9 @@ namespace MobileGL::MG_State::GLState { const Int span = std::max(output.size, 1); if (location < 0 || location + span > m_maxFragmentOutputColorNumber) { - m_infoLog = std::format("Fragment output '{}' location range [{}, {}) exceeds GL_MAX_DRAW_BUFFERS {}.", + Artifacts().infoLog = std::format("Fragment output '{}' location range [{}, {}) exceeds GL_MAX_DRAW_BUFFERS {}.", outputName, location, location + span, m_maxFragmentOutputColorNumber); - MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, m_infoLog.c_str()); + MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, Artifacts().infoLog.c_str()); ResetLinkArtifacts(); return false; } @@ -1313,9 +1337,9 @@ namespace MobileGL::MG_State::GLState { for (Int colorNumber = location; colorNumber < location + span; ++colorNumber) { auto [owner, inserted] = colorNumberOwners.emplace(colorNumber, outputName); if (!inserted) { - m_infoLog = std::format("Fragment outputs '{}' and '{}' alias color number {}.", + Artifacts().infoLog = std::format("Fragment outputs '{}' and '{}' alias color number {}.", owner->second, outputName, colorNumber); - MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, m_infoLog.c_str()); + MGLOG_E("ProgramObject %u: Link failed - %s", m_externalIndex, Artifacts().infoLog.c_str()); ResetLinkArtifacts(); return false; } @@ -1326,14 +1350,14 @@ namespace MobileGL::MG_State::GLState { } Int ProgramObject::GetFragmentDataLocation(const char* name) { - if (!m_program || !name) return -1; + if (!Artifacts().program || !name) return -1; - const auto explicitLocation = m_linkedFragDataLocation.find(name); - const Int outputCount = m_program->getNumPipeOutputs(); + const auto explicitLocation = Artifacts().linkedFragDataLocation.find(name); + const Int outputCount = Artifacts().program->getNumPipeOutputs(); for (Int index = 0; index < outputCount; ++index) { - const auto& output = m_program->getPipeOutput(index); + const auto& output = Artifacts().program->getPipeOutput(index); if (output.name != name) continue; - if (explicitLocation != m_linkedFragDataLocation.end()) return static_cast(explicitLocation->second); + if (explicitLocation != Artifacts().linkedFragDataLocation.end()) return static_cast(explicitLocation->second); return static_cast(output.layoutLocation()); } return -1; @@ -1344,7 +1368,7 @@ namespace MobileGL::MG_State::GLState { // that. The color index defaults to 0 unless glBindFragDataLocationIndexed bound it to 1. // (Shader-side layout(index = ...) qualifiers are not reflected here, only API bindings.) if (GetFragmentDataLocation(name) < 0) return -1; - const auto it = m_linkedFragDataIndex.find(name); - return it != m_linkedFragDataIndex.end() ? static_cast(it->second) : 0; + const auto it = Artifacts().linkedFragDataIndex.find(name); + return it != Artifacts().linkedFragDataIndex.end() ? static_cast(it->second) : 0; } } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index 565fc670..37dceae6 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -44,28 +44,28 @@ namespace MobileGL::MG_State::GLState { Vector>& GetAttachedShaders(); const Vector>& GetAttachedShaders() const; - const String& GetInfoLog() const { return m_infoLog; } + const String& GetInfoLog() const { return Artifacts().infoLog; } // glCreateShaderProgramv folds the shader's compile log into the program's log, which // is the only place a caller can read it from once the shader name is gone. void AppendInfoLog(const String& text) { if (text.empty()) return; - if (!m_infoLog.empty() && m_infoLog.back() != '\n') m_infoLog += '\n'; - m_infoLog += text; + if (!Artifacts().infoLog.empty() && Artifacts().infoLog.back() != '\n') Artifacts().infoLog += '\n'; + Artifacts().infoLog += text; } - Int GetUniformMaxLength() const { return m_uniformNameMaxLength; } - Uint GetUniformCount() const { return m_activeUniformCount; } - Uint GetMaxUniformLocation() const { return m_maxUniformLocation; } + Int GetUniformMaxLength() const { return Artifacts().uniformNameMaxLength; } + Uint GetUniformCount() const { return Artifacts().activeUniformCount; } + Uint GetMaxUniformLocation() const { return Artifacts().maxUniformLocation; } Int GetUniformLocation(const String& name) const { - const auto it = m_uniformLocations.find(name); - if (it != m_uniformLocations.end()) return (Int)it->second; + const auto it = Artifacts().uniformLocations.find(name); + if (it != Artifacts().uniformLocations.end()) return (Int)it->second; // Reflection stores GL-style names: an array uniform is keyed "arr[0]" (its base // location). A bare "arr" query resolves to that entry; an "arr[k]" query resolves // to base + k because DoReflection reserves one location per array element. if (name.empty()) return -1; if (name.back() != ']') { - const auto suffixedIt = m_uniformLocations.find(name + "[0]"); - if (suffixedIt != m_uniformLocations.end()) return (Int)suffixedIt->second; + const auto suffixedIt = Artifacts().uniformLocations.find(name + "[0]"); + if (suffixedIt != Artifacts().uniformLocations.end()) return (Int)suffixedIt->second; return -1; } if (name.length() < 4) return -1; @@ -78,19 +78,19 @@ namespace MobileGL::MG_State::GLState { element = element * 10 + static_cast(name[i] - '0'); if (element > 0x0FFFFFFFu) return -1; } - auto baseIt = m_uniformLocations.find(name.substr(0, bracket) + "[0]"); - if (baseIt == m_uniformLocations.end()) { + auto baseIt = Artifacts().uniformLocations.find(name.substr(0, bracket) + "[0]"); + if (baseIt == Artifacts().uniformLocations.end()) { // Legacy key without the "[0]" suffix (defensive; reflection normally // stores the suffixed form for arrays). - baseIt = m_uniformLocations.find(name.substr(0, bracket)); - if (baseIt == m_uniformLocations.end()) return -1; + baseIt = Artifacts().uniformLocations.find(name.substr(0, bracket)); + if (baseIt == Artifacts().uniformLocations.end()) return -1; } const Int base = (Int)baseIt->second; if (!IsValidUniformLocation(base)) return -1; - const Int index = m_uniformIndexInTProgram[base]; + const Int index = Artifacts().uniformIndexInTProgram[base]; // "[k]" only addresses arrays ("scalar[0]" is not a uniform name), and only // in-range elements. - const glslang::TType* type = m_program->getUniform(index).getType(); + const glslang::TType* type = Artifacts().program->getUniform(index).getType(); if (type == nullptr || !type->isArray()) return -1; if (static_cast(element) >= GetUniformArraySizeByTIndex(index)) return -1; const Int location = base + (Int)element; @@ -101,7 +101,7 @@ namespace MobileGL::MG_State::GLState { // True when both locations are element slots of the same uniform variable. Bool UniformLocationsAliasSameUniform(Int a, Int b) const { if (!IsValidUniformLocation(a) || !IsValidUniformLocation(b)) return false; - return m_uniformIndexInTProgram[a] == m_uniformIndexInTProgram[b]; + return Artifacts().uniformIndexInTProgram[a] == Artifacts().uniformIndexInTProgram[b]; } // ---- GL index <-> glslang TProgram index translation ---- @@ -111,22 +111,22 @@ namespace MobileGL::MG_State::GLState { // index spaces; every public "index"-taking getter translates through them, so // GL and backend consumers keep seeing exactly the pre-P0a surface. Int TProgramUniformIndex(Uint glIndex) const { - return m_glUniformIndexToTProgram[glIndex]; + return Artifacts().glUniformIndexToTProgram[glIndex]; } Int GlUniformIndexFromTProgram(Int tIndex) const { - if (tIndex < 0 || tIndex >= static_cast(m_tProgramUniformIndexToGl.size())) return -1; - return m_tProgramUniformIndexToGl[tIndex]; + if (tIndex < 0 || tIndex >= static_cast(Artifacts().tProgramUniformIndexToGl.size())) return -1; + return Artifacts().tProgramUniformIndexToGl[tIndex]; } Int GlBlockIndexFromTProgram(Int tBlockIndex) const { - if (tBlockIndex < 0 || tBlockIndex >= static_cast(m_tProgramBlockIndexToGl.size())) return -1; - return m_tProgramBlockIndexToGl[tBlockIndex]; + if (tBlockIndex < 0 || tBlockIndex >= static_cast(Artifacts().tProgramBlockIndexToGl.size())) return -1; + return Artifacts().tProgramBlockIndexToGl[tBlockIndex]; } Int GetActiveUniformIndex(const String& name) const { - const Int tProgramCount = static_cast(m_tProgramUniformIndexToGl.size()); - const Int uniformIndex = m_program->getUniformIndex(name.c_str()); + const Int tProgramCount = static_cast(Artifacts().tProgramUniformIndexToGl.size()); + const Int uniformIndex = Artifacts().program->getUniformIndex(name.c_str()); if (uniformIndex >= 0 && uniformIndex < tProgramCount && - m_program->getUniform(uniformIndex).name == name) { + Artifacts().program->getUniform(uniformIndex).name == name) { return GlUniformIndexFromTProgram(uniformIndex); } @@ -135,9 +135,9 @@ namespace MobileGL::MG_State::GLState { // robustness against non-suffixed reflection entries. if (!name.empty() && name.back() != ']') { const String suffixedName = name + "[0]"; - const Int suffixedIndex = m_program->getUniformIndex(suffixedName.c_str()); + const Int suffixedIndex = Artifacts().program->getUniformIndex(suffixedName.c_str()); if (suffixedIndex >= 0 && suffixedIndex < tProgramCount && - m_program->getUniform(suffixedIndex).name == suffixedName) { + Artifacts().program->getUniform(suffixedIndex).name == suffixedName) { return GlUniformIndexFromTProgram(suffixedIndex); } return -1; @@ -145,28 +145,28 @@ namespace MobileGL::MG_State::GLState { if (name.length() <= 3 || name.compare(name.length() - 3, 3, "[0]") != 0) return -1; const String baseName = name.substr(0, name.length() - 3); - const Int baseIndex = m_program->getUniformIndex(baseName.c_str()); + const Int baseIndex = Artifacts().program->getUniformIndex(baseName.c_str()); if (baseIndex < 0 || baseIndex >= tProgramCount) return -1; - return m_program->getUniform(baseIndex).name == baseName ? GlUniformIndexFromTProgram(baseIndex) + return Artifacts().program->getUniform(baseIndex).name == baseName ? GlUniformIndexFromTProgram(baseIndex) : -1; } Bool IsValidUniformLocation(Int location) const { - if (location < 0 || location > static_cast(m_maxUniformLocation)) return false; - if (static_cast(location) >= m_uniformIndexInTProgram.size()) return false; - const Int uniformIndexInProgram = m_uniformIndexInTProgram[location]; + if (location < 0 || location > static_cast(Artifacts().maxUniformLocation)) return false; + if (static_cast(location) >= Artifacts().uniformIndexInTProgram.size()) return false; + const Int uniformIndexInProgram = Artifacts().uniformIndexInTProgram[location]; return uniformIndexInProgram != glslang::TQualifier::layoutLocationEnd && uniformIndexInProgram >= 0 && - uniformIndexInProgram < static_cast(m_tProgramUniformIndexToGl.size()); + uniformIndexInProgram < static_cast(Artifacts().tProgramUniformIndexToGl.size()); } GLenum GetUniformType(Uint location) const { - auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]); + auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]); return uniform.glDefineType; } GLenum GetActiveUniformType(Uint index) const { - auto& uniform = m_program->getUniform(TProgramUniformIndex(index)); + auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); return uniform.glDefineType; } @@ -174,9 +174,9 @@ namespace MobileGL::MG_State::GLState { // glslang's TObjectReflection.size only carries the element count for a NON-block array; for // a block array member it reports 1, so take the count from the TType, which is authoritative // for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space - // m_uniformIndexInTProgram stores). + // the artifacts' uniformIndexInTProgram stores). GLint GetUniformArraySizeByTIndex(Int tIndex) const { - const auto& uniform = m_program->getUniform(tIndex); + const auto& uniform = Artifacts().program->getUniform(tIndex); const glslang::TType* type = uniform.getType(); if (type != nullptr && type->isSizedArray()) { return type->getOuterArraySize(); @@ -189,7 +189,7 @@ namespace MobileGL::MG_State::GLState { } Int GetActiveUniformBlockIndex(Uint index) const { - auto& uniform = m_program->getUniform(TProgramUniformIndex(index)); + auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); // Members of the synthesized global UBO are default-block uniforms to GL: -1. return GlBlockIndexFromTProgram(uniform.index); } @@ -198,7 +198,7 @@ namespace MobileGL::MG_State::GLState { // uniform. The relaxed parse gives global-UBO members real byte offsets, but GL must keep // seeing them as default-block uniforms, so gate on the GL-visible block index. GLint GetActiveUniformOffset(Uint index) const { - const auto& uniform = m_program->getUniform(TProgramUniformIndex(index)); + const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1; return uniform.offset; } @@ -212,7 +212,7 @@ namespace MobileGL::MG_State::GLState { // generated SPIR-V lay the array out with std140 16-byte-rounded strides. MobileGL's UBO // layout is always std140, where every array element stride rounds up to a vec4. GLint GetActiveUniformArrayStride(Uint index) const { - const auto& uniform = m_program->getUniform(TProgramUniformIndex(index)); + const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1; const glslang::TType* type = uniform.getType(); if (type == nullptr || !type->isArray()) return 0; @@ -232,13 +232,13 @@ namespace MobileGL::MG_State::GLState { // check suffices; the getUniformBlock() fallback is defensive for a config that instead leaves // an inheriting member's layoutMatrix == ElmNone. GLint GetActiveUniformIsRowMajor(Uint index) const { - const auto& uniform = m_program->getUniform(TProgramUniformIndex(index)); + const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); if (GlBlockIndexFromTProgram(uniform.index) < 0) return 0; const glslang::TType* type = uniform.getType(); if (type == nullptr || !type->isMatrix()) return 0; glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix; if (layoutMatrix == glslang::ElmNone) { - layoutMatrix = m_program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix; + layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix; } return (layoutMatrix == glslang::ElmRowMajor) ? 1 : 0; } @@ -250,13 +250,13 @@ namespace MobileGL::MG_State::GLState { // out as std140 (packed/shared are coerced), so this matches the offsets glslang reports. For // every GL 3.3 float matrix this evaluates to 16, independent of majorness. GLint GetActiveUniformMatrixStride(Uint index) const { - const auto& uniform = m_program->getUniform(TProgramUniformIndex(index)); + const auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1; const glslang::TType* type = uniform.getType(); if (type == nullptr || !type->isMatrix()) return 0; glslang::TLayoutMatrix layoutMatrix = type->getQualifier().layoutMatrix; if (layoutMatrix == glslang::ElmNone) { - layoutMatrix = m_program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix; + layoutMatrix = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix; } const bool rowMajor = (layoutMatrix == glslang::ElmRowMajor); const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows(); @@ -268,50 +268,50 @@ namespace MobileGL::MG_State::GLState { } const glslang::TType* GetUniformTType(Uint location) const { - auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]); + auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]); return uniform.getType(); } Bool IsUniformOpaqueAtLocation(Uint location) const { return GetUniformTType(location)->isOpaque(); } const String& GetUniformName(Uint location) const { - auto& uniform = m_program->getUniform(m_uniformIndexInTProgram[location]); + auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]); return uniform.name; } const String& GetActiveUniformName(Uint index) const { - auto& uniform = m_program->getUniform(TProgramUniformIndex(index)); + auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); return uniform.name; } // Sentinel for a uniform location without global-UBO backing storage (should not // survive linking: GenerateBinary falls back to tail-allocated scratch storage). static constexpr Uint kInvalidUniformOffset = ~0u; - Uint GetUniformOffset(Uint location) const { return m_uniformOffsets[location]; } + Uint GetUniformOffset(Uint location) const { return Artifacts().uniformOffsets[location]; } Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); } Int GetAttributeLocation(const String& name) { - const auto it = std::find(m_attribs.begin(), m_attribs.end(), name); - return (it == m_attribs.end()) ? -1 : (Int)std::distance(m_attribs.begin(), it); + const auto it = std::find(Artifacts().attribs.begin(), Artifacts().attribs.end(), name); + return (it == Artifacts().attribs.end()) ? -1 : (Int)std::distance(Artifacts().attribs.begin(), it); } Uint32 GetActiveAttributeLocationMask() const { Uint32 mask = 0; - const SizeT count = std::min(m_attribs.size(), 32); + const SizeT count = std::min(Artifacts().attribs.size(), 32); for (SizeT index = 0; index < count; ++index) { - if (!m_attribs[index].empty()) { + if (!Artifacts().attribs[index].empty()) { mask |= (1u << index); } } return mask; } Uint32 GetActiveFragmentOutputLocationMask() const { - if (!m_program) { + if (!Artifacts().program) { return 0; } Uint32 mask = 0; - const Int outputCount = m_program->getNumPipeOutputs(); + const Int outputCount = Artifacts().program->getNumPipeOutputs(); for (Int index = 0; index < outputCount; ++index) { - const Int location = static_cast(m_program->getPipeOutput(index).layoutLocation()); + const Int location = static_cast(Artifacts().program->getPipeOutput(index).layoutLocation()); if (location >= 0 && location < 32) { mask |= (1u << location); } @@ -319,38 +319,38 @@ namespace MobileGL::MG_State::GLState { return mask; } Int GetActiveFragmentOutputCount() const { - return m_program ? m_program->getNumPipeOutputs() : 0; + return Artifacts().program ? Artifacts().program->getNumPipeOutputs() : 0; } const String& GetActiveFragmentOutputName(Uint index) const { - MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null"); - MOBILEGL_ASSERT(index < static_cast(m_program->getNumPipeOutputs()), + MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null"); + MOBILEGL_ASSERT(index < static_cast(Artifacts().program->getNumPipeOutputs()), "ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index); - return m_program->getPipeOutput(static_cast(index)).name; + return Artifacts().program->getPipeOutput(static_cast(index)).name; } Int GetFragmentOutputLocation(Uint index) const { - MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null"); - MOBILEGL_ASSERT(index < static_cast(m_program->getNumPipeOutputs()), + MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null"); + MOBILEGL_ASSERT(index < static_cast(Artifacts().program->getNumPipeOutputs()), "ProgramObject::GetFragmentOutputLocation: index=%u out of range", index); - return static_cast(m_program->getPipeOutput(static_cast(index)).layoutLocation()); + return static_cast(Artifacts().program->getPipeOutput(static_cast(index)).layoutLocation()); } GLint GetActiveFragmentOutputArraySize(Uint index) const { - MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null"); - MOBILEGL_ASSERT(index < static_cast(m_program->getNumPipeOutputs()), + MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null"); + MOBILEGL_ASSERT(index < static_cast(Artifacts().program->getNumPipeOutputs()), "ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index); - return m_program->getPipeOutput(static_cast(index)).size; + return Artifacts().program->getPipeOutput(static_cast(index)).size; } GLenum GetFragmentOutputType(Uint index) const { - MOBILEGL_ASSERT(m_program != nullptr, "ProgramObject::GetFragmentOutputType: program is null"); - MOBILEGL_ASSERT(index < static_cast(m_program->getNumPipeOutputs()), + MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputType: program is null"); + MOBILEGL_ASSERT(index < static_cast(Artifacts().program->getNumPipeOutputs()), "ProgramObject::GetFragmentOutputType: index=%u out of range", index); - return m_program->getPipeOutput(static_cast(index)).glDefineType; + return Artifacts().program->getPipeOutput(static_cast(index)).glDefineType; } - GLenum GetAttribType(Uint index) const { return m_attribTypes[index]; } - const String& GetAttribName(Uint index) const { return m_attribs[index]; } - GLenum GetActiveAttribType(Uint index) const { return m_program->getPipeInput(static_cast(index)).glDefineType; } - GLint GetActiveAttribArraySize(Uint index) const { return m_program->getPipeInput(static_cast(index)).size; } + GLenum GetAttribType(Uint index) const { return Artifacts().attribTypes[index]; } + const String& GetAttribName(Uint index) const { return Artifacts().attribs[index]; } + GLenum GetActiveAttribType(Uint index) const { return Artifacts().program->getPipeInput(static_cast(index)).glDefineType; } + GLint GetActiveAttribArraySize(Uint index) const { return Artifacts().program->getPipeInput(static_cast(index)).size; } // The Vulkan-semantics parse reflects the vertex builtins under their SPIR-V names; // GL must keep reporting the GL spellings (glGetActiveAttrib and the program-input // resource queries enumerate builtins). @@ -362,11 +362,11 @@ namespace MobileGL::MG_State::GLState { return name; } const String& GetActiveAttribName(Uint index) const { - return NormalizeBuiltinPipeInputName(m_program->getPipeInput(static_cast(index)).name); + return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast(index)).name); } - void* MapUBO() { return m_globalUboScratch.data(); } - const void* GetUBOData() const { return m_globalUboScratch.data(); } - Uint GetUBOSize() const { return static_cast(m_globalUboScratch.size()); } + void* MapUBO() { return Artifacts().globalUboScratch.data(); } + const void* GetUBOData() const { return Artifacts().globalUboScratch.data(); } + Uint GetUBOSize() const { return static_cast(Artifacts().globalUboScratch.size()); } // 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 // backends' "never uploaded" sentinel, so skip over it on wrap. @@ -412,20 +412,20 @@ namespace MobileGL::MG_State::GLState { } void SetUniformSamplerOrImageUnitIndex(Uint location, Int unit) { - if (location >= m_uniformSamplerOrImageUnitIndex.size() || - m_uniformSamplerOrImageUnitIndex[location] == unit) { + if (location >= Artifacts().uniformSamplerOrImageUnitIndex.size() || + Artifacts().uniformSamplerOrImageUnitIndex[location] == unit) { return; } - m_uniformSamplerOrImageUnitIndex[location] = unit; + Artifacts().uniformSamplerOrImageUnitIndex[location] = unit; ++m_backendStateVersion; } Int GetUniformSamplerOrImageUnitIndex(Uint location) const { - return m_uniformSamplerOrImageUnitIndex[location]; + return Artifacts().uniformSamplerOrImageUnitIndex[location]; } Bool GetDeleteStatus() const { return m_deleteStatus; } - Bool GetLinkStatus() const { return m_linkStatus; } + Bool GetLinkStatus() const { return Artifacts().linkStatus; } // GL_PROGRAM_BINARY_RETRIEVABLE_HINT. MobileGL exposes no program binary format // (GL_NUM_PROGRAM_BINARY_FORMATS is 0), so the hint is pure state - which is all // ARB_get_program_binary requires of it. @@ -439,26 +439,27 @@ namespace MobileGL::MG_State::GLState { // glProgramBinary always fails here (there is no format it could accept) and the // spec then requires the program's LINK_STATUS to read FALSE. void MarkLinkFailedByProgramBinary() { + BumpLinkObservableVersions(); ResetLinkArtifacts(); - m_infoLog = "No program binary format is supported."; + Artifacts().infoLog = "No program binary format is supported."; } Bool GetValidateStatus() const { return m_validateStatus; } - Int GetActiveAtomicCounterCount() const { return m_program->getNumAtomicCounters(); } - Int GetActiveAttributesCount() const { return m_program->getNumPipeInputs(); } + Int GetActiveAtomicCounterCount() const { return Artifacts().program->getNumAtomicCounters(); } + Int GetActiveAttributesCount() const { return Artifacts().program->getNumPipeInputs(); } // GL-visible uniform blocks only: the synthesized MGL_GLOBAL_UBO the relaxed parse // materializes for default-block uniforms is filtered out by DoReflection. - Int GetActiveUniformBlocksCount() const { return static_cast(m_glBlockIndexToTProgram.size()); } - GLuint GetComputeLocalSize(Uint dim) const { return m_program->getLocalSize(static_cast(dim)); } - Int GetActiveAttributesMaxLength() const { return m_attribInNameMaxLength; } - Int GetActiveUniformBlocksMaxNameLength() const { return m_uniformBlockNameMaxLength; } + Int GetActiveUniformBlocksCount() const { return static_cast(Artifacts().glBlockIndexToTProgram.size()); } + GLuint GetComputeLocalSize(Uint dim) const { return Artifacts().program->getLocalSize(static_cast(dim)); } + Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; } + Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; } Uint GetUniformBlockIndex(const char* name) const { - auto it = m_uniformBlockIndexByName.find(name); - if (it != m_uniformBlockIndexByName.end()) return it->second; + auto it = Artifacts().uniformBlockIndexByName.find(name); + if (it != Artifacts().uniformBlockIndexByName.end()) return it->second; // Instances of an arrayed block are reflected as "Block[0]".."Block[N-1]"; // a bare "Block" query resolves to the first instance per GL semantics. const String suffixedName = String(name) + "[0]"; - it = m_uniformBlockIndexByName.find(suffixedName); - if (it != m_uniformBlockIndexByName.end()) return it->second; + it = Artifacts().uniformBlockIndexByName.find(suffixedName); + if (it != Artifacts().uniformBlockIndexByName.end()) return it->second; return 0xFFFFFFFFu; // GL_INVALID_INDEX } Bool IsActiveUniformBlock(Uint index) const { @@ -471,11 +472,11 @@ namespace MobileGL::MG_State::GLState { // (like a std140 struct) occupies a vec4-rounded size, and that is what the // backend compiles: ES drivers reject draws whose bound UBO range is smaller // than the block (a block ending in ivec3 reported 12 while the driver needs 16). - return (m_program->getUniformBlock(m_glBlockIndexToTProgram[index]).size + 15u) & ~15u; + return (Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]).size + 15u) & ~15u; } const String& GetUniformBlockName(Uint index) const { - auto& ubo = m_program->getUniformBlock(m_glBlockIndexToTProgram[index]); + auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]); return ubo.name; } @@ -487,8 +488,8 @@ namespace MobileGL::MG_State::GLState { if (name.empty() || name.back() != ']') return index; const SizeT bracket = name.rfind('['); if (bracket == String::npos) return index; - const auto it = m_uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]"); - if (it != m_uniformBlockIndexByName.end()) return it->second; + const auto it = Artifacts().uniformBlockIndexByName.find(name.substr(0, bracket) + "[0]"); + if (it != Artifacts().uniformBlockIndexByName.end()) return it->second; return index; } @@ -499,31 +500,31 @@ namespace MobileGL::MG_State::GLState { Int GetUniformBlockActiveUniformCount(Uint index) const { const Int ownerIndex = static_cast(GetUniformBlockMemberOwnerIndex(index)); Int count = 0; - for (Uint uniformIndex = 0; uniformIndex < m_activeUniformCount; ++uniformIndex) { + for (Uint uniformIndex = 0; uniformIndex < Artifacts().activeUniformCount; ++uniformIndex) { if (GetActiveUniformBlockIndex(uniformIndex) == ownerIndex) ++count; } return count; } Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const { - const auto& ubo = m_program->getUniformBlock(m_glBlockIndexToTProgram[index]); + const auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]); const auto stageMask = static_cast(1 << stage); return (ubo.stages & stageMask) != 0; } // Set by glUniformBlockBinding void SetUniformBlockBinding(Uint index, Uint binding) { - if (index >= m_uniformBlockBinding.size() || m_uniformBlockBinding[index] == static_cast(binding)) { + if (index >= Artifacts().uniformBlockBinding.size() || Artifacts().uniformBlockBinding[index] == static_cast(binding)) { return; } - m_uniformBlockBinding[index] = static_cast(binding); + Artifacts().uniformBlockBinding[index] = static_cast(binding); ++m_backendStateVersion; } - Uint GetUniformBlockBinding(Uint index) const { return m_uniformBlockBinding[index]; } + Uint GetUniformBlockBinding(Uint index) const { return Artifacts().uniformBlockBinding[index]; } - Vector>& GetGeneratedSpirv() { return m_generatedSpirv; } - const Vector>& GetGeneratedSpirv() const { return m_generatedSpirv; } + Vector>& GetGeneratedSpirv() { return Artifacts().generatedSpirv; } + const Vector>& GetGeneratedSpirv() const { return Artifacts().generatedSpirv; } Int GetShaderIndexByStage(ShaderStage stage) const { auto it = std::find_if(m_shaders.begin(), m_shaders.end(), [stage](const SharedPtr& shader) { @@ -545,41 +546,134 @@ namespace MobileGL::MG_State::GLState { // layout captures into; see NeedsScatteredTransformFeedbackCapture. Uint32 packedOffsetBytes = 0; }; + + // ---- P1: everything a link PRODUCES, in one movable block ---- + // + // The membership rule is mechanical, not editorial: this is exactly the field list + // ResetLinkArtifacts() clears (plus the four it forgot to - infoLog, + // linkedFragDataLocation/Index and the geometry strip-capture pair - which are just + // as much link output). Nothing else belongs here. + // + // Why a struct: once glLinkProgram runs on a worker (P1 stage 4) the worker writes + // its OWN LinkArtifacts and the GL thread publishes it with a single move, instead + // of thirty cross-thread field assignments. Until then this is a pure refactor. + // + // Access rule (invariant I5): the member below is private and reachable ONLY + // through ProgramObject::Artifacts(), which calls EnsureLinkJoined() first. That is + // what makes "every read of link output joins the pending link" a property the + // compiler checks rather than a review item - a new reader cannot spell the field + // without going through the gate. + struct LinkArtifacts { + SharedPtr program; + Vector> generatedSpirv; + + // Attributes (Vertex in) + Vector attribs; + Vector attribTypes; + + // FragData (Frag out): the per-link snapshot of the explicit request maps. + UnorderedMap linkedFragDataLocation; + UnorderedMap linkedFragDataIndex; + + // GL-facing index spaces (see the translation helpers above): GL active-uniform + // index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram + // block index. -1 marks a TProgram entry GL does not expose (dead default-block + // uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself). + Vector glUniformIndexToTProgram; + Vector tProgramUniformIndexToGl; + Vector glBlockIndexToTProgram; + Vector tProgramBlockIndexToGl; + // Per-link merged snapshot of the attached shaders' lexically extracted + // layout(location = N) default-block uniform qualifiers (the relaxed parse drops + // them from reflection; the DoReflection assigner restores them from here). + UnorderedMap linkedExplicitUniformLocations; + UnorderedMap uniformLocations; + // Ordered by location, + // aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`" + Vector uniformIndexInTProgram; + // ditto. Will be set at glUniform1i + Vector uniformSamplerOrImageUnitIndex; + UnorderedMap explicitOpaqueUniformBindings; + + // Ordered by uniform block index + // index is DIFFERENT from binding!!! + // + // Let's define UniformBlockIndex == the order at glslang getUniformBlock() + // aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies: + // `prog->getUniformBlock(i) == "BlockName"` + // These stuff are present for GL semantics, not for backend inspection + // These may change after-link (because GL spec decided to have `glUniformBlockBinding`) + UnorderedMap uniformBlockIndexByName; + Vector uniformBlockBinding; + + // Need to be reflected after linking of SPIR-V binary + Vector uniformOffsets; + Vector uniformSizesInBytes; + Vector globalUboScratch; + + Uint activeUniformCount = 0; + Uint maxUniformLocation = 0; + Int uniformNameMaxLength = 0; + Int attribInNameMaxLength = 0; + Int uniformBlockNameMaxLength = 0; + + String infoLog; + Bool linkStatus = false; + + // Transform feedback: the linked snapshot (the request lives outside, on the + // GL-thread-owned side). + Vector xfbVaryings; + Vector xfbStrides; + Vector gsStripTriangles; + Bool gsStripCaptureFixup = false; + GLenum gsInputPrimitive = GL_NONE; + GLenum xfbBufferMode = GL_INTERLEAVED_ATTRIBS; + Int xfbVaryingNameMaxLength = 0; + Bool xfbNeedsScatteredCapture = false; + Uint32 xfbPackedStride = 0; + }; + + // Blocks until a pending link (P1 stage 4 onwards) has published its artifacts. + // Public because a few call sites have to join without reading anything - see the + // explicit-join list in the P1 design. Today there is never a pending link, so this + // is a no-op; it is wired up when glLinkProgram starts enqueueing. + void JoinLink() const { EnsureLinkJoined(); } + void SetTransformFeedbackVaryings(Vector&& names, GLenum bufferMode) { m_requestedXfbVaryings = Move(names); m_requestedXfbBufferMode = bufferMode; } - GLenum GetTransformFeedbackBufferMode() const { return m_xfbBufferMode; } - SizeT GetTransformFeedbackVaryingCount() const { return m_xfbVaryings.size(); } + GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; } + SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); } const XfbVarying* GetTransformFeedbackVarying(SizeT index) const { - return index < m_xfbVaryings.size() ? &m_xfbVaryings[index] : nullptr; + return index < Artifacts().xfbVaryings.size() ? &Artifacts().xfbVaryings[index] : nullptr; } - const Vector& GetTransformFeedbackVaryings() const { return m_xfbVaryings; } + const Vector& GetTransformFeedbackVaryings() const { return Artifacts().xfbVaryings; } // Stride of one captured vertex in the given capture buffer slot. Uint32 GetTransformFeedbackStride(Uint32 bufferIndex) const { - return bufferIndex < m_xfbStrides.size() ? m_xfbStrides[bufferIndex] : 0; + return bufferIndex < Artifacts().xfbStrides.size() ? Artifacts().xfbStrides[bufferIndex] : 0; } - SizeT GetTransformFeedbackBufferCount() const { return m_xfbStrides.size(); } - Int GetTransformFeedbackVaryingMaxLength() const { return m_xfbVaryingNameMaxLength; } + SizeT GetTransformFeedbackBufferCount() const { return Artifacts().xfbStrides.size(); } + Int GetTransformFeedbackVaryingMaxLength() const { return Artifacts().xfbVaryingNameMaxLength; } // True when the capture layout uses gl_SkipComponents / gl_NextBuffer // (ARB_transform_feedback3), which no ES driver can express: it can only pack every // captured varying into one record with no gaps. A backend that captures through // such a driver has to capture into scratch storage and scatter the records into the // application's buffers itself, using packedOffsetBytes as the source offset and // (bufferIndex, offsetBytes, stride) as the destination. - Bool NeedsScatteredTransformFeedbackCapture() const { return m_xfbNeedsScatteredCapture; } + Bool NeedsScatteredTransformFeedbackCapture() const { return Artifacts().xfbNeedsScatteredCapture; } // Bytes one gap-free captured record occupies. - Uint32 GetTransformFeedbackPackedStride() const { return m_xfbPackedStride; } + Uint32 GetTransformFeedbackPackedStride() const { return Artifacts().xfbPackedStride; } // True when the capture stage is a triangle-strip geometry shader with a // statically-known emit sequence: the Vulkan capture order then needs the GL // odd-triangle vertex swap after EndTransformFeedback. - Bool HasGsTriangleStripCaptureFixup() const { return m_gsStripCaptureFixup; } + Bool HasGsTriangleStripCaptureFixup() const { return Artifacts().gsStripCaptureFixup; } // Triangles per strip, in emission order, for ONE geometry invocation. - const Vector& GetGsStripTriangles() const { return m_gsStripTriangles; } + const Vector& GetGsStripTriangles() const { return Artifacts().gsStripTriangles; } // GL_GEOMETRY_INPUT_TYPE of the linked geometry stage (GL_POINTS, GL_LINES, // GL_LINES_ADJACENCY, GL_TRIANGLES or GL_TRIANGLES_ADJACENCY), or GL_NONE when the // program has no geometry stage. Draws must present a compatible primitive type. - GLenum GetGeometryInputType() const { return m_gsInputPrimitive; } + GLenum GetGeometryInputType() const { return Artifacts().gsInputPrimitive; } Uint GetExternalIndex() const { return m_externalIndex; } // Globally-unique, never-reused id for this program object's lifetime. Unlike the GL @@ -589,99 +683,82 @@ namespace MobileGL::MG_State::GLState { Uint64 GetLifetimeId() const { return m_lifetimeId; } private: + // ---- The one and only join gate for link output (P1 invariant I5) ---- + // Blocks until a pending link has finished and its LinkArtifacts have been + // published into m_artifacts. Today no link is ever pending - glLinkProgram still + // runs the whole body inline - so this is an unconditional no-op, and the whole + // Artifacts() indirection compiles away. It exists NOW so that the ~120 readers of + // link output are already routed through it when stage 4 makes it block: the edit + // that turns links asynchronous then touches this function and nothing else. + // + // Defined inline (not in ProgramObject.cpp) on purpose: this is called from every + // Artifacts() read - ~1200 call sites project-wide - and the project never builds + // with LTO (MOBILEGL_ENABLE_LTO=OFF), so an out-of-line empty body would leave a + // real cross-TU call at every one of them instead of folding away. Stage 4's + // version, which actually blocks, moves the wait itself out-of-line behind a + // `m_pendingLink` check that stays inline here. + void EnsureLinkJoined() const {} + LinkArtifacts& Artifacts() { + EnsureLinkJoined(); + return m_artifacts; + } + const LinkArtifacts& Artifacts() const { + EnsureLinkJoined(); + return m_artifacts; + } + void ResetLinkArtifacts(); + // GL-thread-only companion to ResetLinkArtifacts (see its definition). + void BumpLinkObservableVersions(); // Builds the GL-facing reflection surface from the linked TProgram. Returns false - // (with m_infoLog set and link artifacts reset) when reflection itself fails or an + // (with the artifacts' infoLog set and link artifacts reset) when reflection itself fails or an // explicit-uniform-location conflict makes the link invalid. - Bool DoReflection(); + Bool DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env); // Resolves the requested transform feedback varyings against the linked // vertex stage; fails the link (GL semantics) on unknown or duplicate // names or exceeded capture limits. Bool ResolveTransformFeedbackVaryings(); void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate); // The former GenerateBinary, split around DoReflection's data dependencies: - // SPIR-V must be generated BEFORE buildReflection touches m_program (its + // SPIR-V must be generated BEFORE buildReflection touches the linked TProgram (its // live-variable analysis mutates the intermediates enough to change // GlslangToSpv output), while the glUniform*-to-global-UBO routing tables are - // sized and keyed by reflection results (m_maxUniformLocation, - // m_uniformLocations) and so must run AFTER it. + // sized and keyed by reflection results (maxUniformLocation, uniformLocations) + // and so must run AFTER it. void GenerateSpirv(); void BuildGlobalUboRouting(); - void WaitUntilGenerationCompleted() const; void AddDefaultFragmentShaderIfMissing(); Bool ValidateFragmentOutputLocations(); static Uint64 AllocateLifetimeId(); + // ---- GL-thread-owned state: never joins ---- + // Most of this is never produced by a link at all. The three version counters + // (m_backendStateVersion / m_uboContentVersion / m_linkVersion) ARE + // link-observable, but they are bumped exclusively on the GL thread + // (BumpLinkObservableVersions in Link()'s prologue and glProgramBinary's + // failure path) - the link BODY, which stage 4 moves to a worker, never + // writes them. const Uint m_externalIndex = 0; const Uint64 m_lifetimeId = 0; + // The attach lists are mutated only in Link()'s GL-thread prologue, which is why + // glGetAttachedShaders / GL_ATTACHED_SHADERS / the orphan-shader sweep need no join. Vector> m_shaders; Vector> m_detachedShaders; // Store detached shaders and remove on next link - SharedPtr m_program; - - Vector> m_generatedSpirv; - - // Attributes (Vertex in) + // Link INPUTS (all "take effect at the next link" per GL): glBindAttribLocation, + // glBindFragDataLocation(Indexed), glTransformFeedbackVaryings, and the draw-buffer + // count stamped in by the entry point. A pending link snapshots these at enqueue. UnorderedMap m_explicitAttribLocations; - Vector m_attribs; - Vector m_attribTypes; - - // FragData (Frag out) UnorderedMap m_explicitFragDataLocation; - UnorderedMap m_linkedFragDataLocation; // Dual-source blend color index per output name (glBindFragDataLocationIndexed); snapshotted // into the linked map at link time, like the location maps above. UnorderedMap m_explicitFragDataIndex; - UnorderedMap m_linkedFragDataIndex; Int m_maxFragmentOutputColorNumber = 8; + Vector m_requestedXfbVaryings; + GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS; - // Uniforms - // GL-facing index spaces (see the translation helpers above): GL active-uniform - // index <-> glslang TProgram uniform index, GL uniform-block index <-> TProgram - // block index. -1 marks a TProgram entry GL does not expose (dead default-block - // uniforms swept into MGL_GLOBAL_UBO by the relaxed parse, and that block itself). - Vector m_glUniformIndexToTProgram; - Vector m_tProgramUniformIndexToGl; - Vector m_glBlockIndexToTProgram; - Vector m_tProgramBlockIndexToGl; - // Per-link merged snapshot of the attached shaders' lexically extracted - // layout(location = N) default-block uniform qualifiers (the relaxed parse drops - // them from reflection; the DoReflection assigner restores them from here). - UnorderedMap m_linkedExplicitUniformLocations; - UnorderedMap m_uniformLocations; - // Ordered by location, - // aka. m_uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`" - Vector m_uniformIndexInTProgram; - // ditto. Will be set at glUniform1i - Vector m_uniformSamplerOrImageUnitIndex; - UnorderedMap m_explicitOpaqueUniformBindings; - - // Ordered by uniform block index - // index is DIFFERENT from binding!!! - // - // Let's define UniformBlockIndex == the order at glslang getUniformBlock() - // aka `i = glGetUniformBlockIndex(prog, "BlockName")` implies: - // `prog->getUniformBlock(i) == "BlockName"` - // These stuff are present for GL semantics, not for backend inspection - // These may change after-link (because GL spec decided to have `glUniformBlockBinding`) - UnorderedMap m_uniformBlockIndexByName; - Vector m_uniformBlockBinding; - - // Need to be reflected after linking of SPIR-V binary - Vector m_uniformOffsets; - Vector m_uniformSizesInBytes; - Vector m_globalUboScratch; - - Uint m_activeUniformCount = 0; - Uint m_maxUniformLocation = 0; - Int m_uniformNameMaxLength = 0; - Int m_attribInNameMaxLength = 0; - Int m_uniformBlockNameMaxLength = 0; - - String m_infoLog; Bool m_deleteStatus = false; - Bool m_linkStatus = false; Bool m_binaryRetrievableHint = false; Bool m_separable = false; Bool m_validateStatus = true; @@ -704,17 +781,9 @@ namespace MobileGL::MG_State::GLState { Uint32 m_uboContentVersion = 0; Uint32 m_linkVersion = 0; - // Transform feedback: request (applies at next link) and linked snapshot. - Vector m_requestedXfbVaryings; - GLenum m_requestedXfbBufferMode = GL_INTERLEAVED_ATTRIBS; - Vector m_xfbVaryings; - Vector m_xfbStrides; - Vector m_gsStripTriangles; - Bool m_gsStripCaptureFixup = false; - GLenum m_gsInputPrimitive = GL_NONE; - GLenum m_xfbBufferMode = GL_INTERLEAVED_ATTRIBS; - Int m_xfbVaryingNameMaxLength = 0; - Bool m_xfbNeedsScatteredCapture = false; - Uint32 m_xfbPackedStride = 0; + // ---- Link OUTPUT ---- + // Written by the link and by the post-link setters GL allows (glUniform1i's sampler + // unit, glUniformBlockBinding). Reachable only through Artifacts(); see LinkArtifacts. + LinkArtifacts m_artifacts; }; } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp index e2d0767e..eceba376 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramState.cpp @@ -79,7 +79,7 @@ namespace MobileGL::MG_State::GLState { Uint shaderId = 0; m_programShaderNameGenerator.Generate(1, &shaderId); EnsureIndexAvail(shaderId, m_shaderObjects); - auto shaderObject = MakeShared(stage, shaderId, &m_shaderPreprocessCache); + auto shaderObject = MakeShared(stage, shaderId, m_shaderPreprocessCache); if (shaderObject == nullptr) return 0; m_shaderObjects[shaderId] = shaderObject; return shaderId; diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramState.h b/MobileGL/MG_State/GLState/ProgramState/ProgramState.h index 766a836c..e5270994 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramState.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramState.h @@ -37,7 +37,7 @@ namespace MobileGL::MG_State::GLState { // P0b layer 2. Exposed for tests and diagnostics; the GL frontend never touches it // directly - shader objects reach it through the pointer they are handed at // CreateShader(). - ShaderPreprocessCache& GetShaderPreprocessCache() { return m_shaderPreprocessCache; } + ShaderPreprocessCache& GetShaderPreprocessCache() { return *m_shaderPreprocessCache; } private: Bool ShaderHasGLVisibleAttachment(const SharedPtr& shaderObject) const; @@ -64,10 +64,11 @@ namespace MobileGL::MG_State::GLState { // object kinds keeps the names disjoint; the object tables stay separate. IndexGenerator m_programShaderNameGenerator; - // P0b layer 2: every shader object created here is handed a pointer to this cache. - // Declared FIRST on purpose - members are destroyed in reverse declaration order, - // so the cache outlives every shader object holding a pointer to it. - ShaderPreprocessCache m_shaderPreprocessCache; + // P0b layer 2: every shader object created here is handed shared ownership of this + // cache, so its lifetime no longer depends on member destruction order (P1: an + // in-flight compile job may outlive the context). The FIRST-member declaration is + // kept anyway - it costs nothing and documents the intent. + SharedPtr m_shaderPreprocessCache = MakeShared(); Vector> m_programObjects; Vector> m_shaderObjects; diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp index ecd31b32..0194cca5 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.cpp @@ -12,8 +12,8 @@ #include #include #include +#include #include -#include #include @@ -115,41 +115,22 @@ namespace { return localSize; } - static MobileGL::Uint GetComputeWorkGroupSizeLimit(MobileGL::Uint index) { - constexpr MobileGL::Uint kFrontendMinComputeWorkGroupSizes[] = {1024, 1024, 64}; - MobileGL::Int backendValue = 0; - if (MobileGL::MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) { - MobileGL::MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, - &backendValue); - } - - // TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima. - return std::max(static_cast(std::max(backendValue, 0)), - kFrontendMinComputeWorkGroupSizes[index]); - } - - static unsigned long long GetComputeWorkGroupInvocationLimit() { - constexpr unsigned long long kFrontendMaxComputeWorkGroupInvocations = 1024; - if (!MobileGL::MG_Backend::pActiveBackendObject) return kFrontendMaxComputeWorkGroupInvocations; - - return std::max(static_cast(std::max( - MobileGL::MG_Backend::pActiveBackendObject->GetDynamicParameters() - .MaxComputeWorkGroupInvocations, - 0)), - kFrontendMaxComputeWorkGroupInvocations); - } - - static std::optional ValidateComputeLocalSizeLimits(const MobileGL::String& source) { + // The device limits come from the CompileEnv snapshot, never from a live driver query. + // GL_MAX_COMPUTE_WORK_GROUP_SIZE is a real GLES call on the DirectGLES backend: issued + // off the context thread it would silently no-op and turn a legal local_size_z into + // COMPILE_STATUS=FALSE. CaptureCompileEnv() issues it once, on the GL thread. + static std::optional ValidateComputeLocalSizeLimits( + const MobileGL::String& source, const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) { const ComputeLocalSize localSize = ParseComputeLocalSize(source); if (!localSize.declared) return std::nullopt; - if (localSize.x > GetComputeWorkGroupSizeLimit(0) || localSize.y > GetComputeWorkGroupSizeLimit(1) || - localSize.z > GetComputeWorkGroupSizeLimit(2)) { + if (localSize.x > env.maxComputeWorkGroupSize[0] || localSize.y > env.maxComputeWorkGroupSize[1] || + localSize.z > env.maxComputeWorkGroupSize[2]) { return "Compute shader local_size exceeds GL_MAX_COMPUTE_WORK_GROUP_SIZE."; } const unsigned long long invocations = static_cast(localSize.x) * localSize.y * localSize.z; - if (invocations > GetComputeWorkGroupInvocationLimit()) { + if (invocations > env.maxComputeWorkGroupInvocations) { return "Compute shader local_size product exceeds GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS."; } @@ -162,23 +143,23 @@ namespace { // nothing else - the glslang parse stays per-object because its TShader is // consume-once. Deliberately free of any per-object state so the memo is sound. // - // Caveat, documented rather than defended against: the compute local-size verdict also - // reads the active backend's GL_MAX_COMPUTE_WORK_GROUP_* limits. Those are fixed for - // the lifetime of a context, and the cache is per-context, so the memo cannot outlive - // the limits it was computed against. + // The former caveat is gone: the compute local-size verdict reads `env` rather than the + // live backend, and env.fingerprint is part of the P0b cache key, so a memo can never be + // returned against limits other than the ones it was computed against. static MobileGL::MG_State::GLState::ShaderPreprocessResult RunSourceOnlyPipeline( - const MobileGL::ShaderStage stage, const MobileGL::String& source) { + const MobileGL::ShaderStage stage, const MobileGL::String& source, + const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) { using namespace MobileGL; using namespace MobileGL::MG_Util::ShaderTranspiler; using MobileGL::MG_State::GLState::ShaderPreprocessOutcome; MobileGL::MG_State::GLState::ShaderPreprocessResult result; result.preprocessedSource = source; - PreprocessShaderSource(stage, result.preprocessedSource); + PreprocessShaderSource(stage, result.preprocessedSource, env); if (stage == ShaderStage::Compute) { if (const std::optional localSizeError = - ValidateComputeLocalSizeLimits(result.preprocessedSource)) { + ValidateComputeLocalSizeLimits(result.preprocessedSource, env)) { result.outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected; result.infoLog = *localSizeError; return result; @@ -240,14 +221,14 @@ namespace MobileGL::MG_State::GLState { m_compiledSourceLength = m_source.length(); } + // EnsureCompileJoined() is defined inline in ShaderObject.h (see the comment there for + // why: no LTO, and it is called from every Compiled() read). + void ShaderObject::InvalidateCompiledState() { - m_shader.reset(); - m_preprocessedSource.clear(); - m_explicitUniformLocations.clear(); - m_explicitOpaqueBindings.clear(); - m_shaderConsumedByLink = false; - m_compileStatus = false; - m_infoLog.clear(); + // The compile artifacts are exactly what one Compile() writes, so discarding them + // wholesale IS the invalidation. (Kept as an explicit reset rather than a + // default-construct so the intent survives a future field addition.) + Compiled() = CompileArtifacts{}; m_hasCompiledState = false; m_compiledSourceHash = 0; m_compiledSourceLength = 0; @@ -260,8 +241,8 @@ namespace MobileGL::MG_State::GLState { // the exact source it still holds, so a recompile is a no-op. This covers the // failure case too - the info log stays queryable because nothing is cleared. // - // m_shaderConsumedByLink interaction: if the stored TShader already fed a link, - // the no-op leaves m_preprocessedSource and both side-channel maps intact, which + // shaderConsumedByLink interaction: if the stored TShader already fed a link, + // the no-op leaves preprocessedSource and both side-channel maps intact, which // is precisely what TakeShaderForLink's on-demand re-parse needs. A real recompile // would have handed the next link a fresh parse; the no-op hands it a fresh // re-parse of the identical source instead. Same result, one parse either way. @@ -271,59 +252,72 @@ namespace MobileGL::MG_State::GLState { const Uint64 sourceHash = ShaderPreprocessCache::HashSource(m_source); + // The compile-environment snapshot, taken here on the GL thread. Everything below + // reads the device through it and never through pActiveBackendObject, which is what + // makes the whole body movable onto a worker in stage 3. + CompileArtifacts& compiled = Compiled(); + compiled.env = MG_Util::ShaderTranspiler::GetCurrentCompileEnv(); + const MG_Util::ShaderTranspiler::CompileEnv& env = *compiled.env; + // P0b layer 2: another shader object in this context may already have run the - // source-only half over byte-identical text. - const ShaderPreprocessResult* cached = - m_preprocessCache != nullptr ? m_preprocessCache->Find(m_stage, sourceHash, m_source) : nullptr; - ShaderPreprocessResult fresh; - if (cached == nullptr) fresh = RunSourceOnlyPipeline(m_stage, m_source); - const ShaderPreprocessResult& shared = cached != nullptr ? *cached : fresh; - const Bool shouldPopulateCache = cached == nullptr && m_preprocessCache != nullptr; + // source-only half over byte-identical text under the same environment. + ShaderPreprocessResultPtr cached = + m_preprocessCache ? m_preprocessCache->Find(m_stage, sourceHash, m_source, env.fingerprint) : nullptr; + SharedPtr fresh; + if (!cached) fresh = MakeShared(RunSourceOnlyPipeline(m_stage, m_source, env)); + const ShaderPreprocessResult& shared = cached ? *cached : *fresh; + const Bool shouldPopulateCache = !cached && m_preprocessCache != nullptr; if (!shared.Preprocessed()) { // Rejected lexically, or a glslang failure this context has already seen for // this exact source (ParseFailed) - either way the parse can be skipped. - m_infoLog = shared.infoLog; - if (shouldPopulateCache) m_preprocessCache->Insert(m_stage, sourceHash, m_source, Move(fresh)); + compiled.infoLog = shared.infoLog; + if (shouldPopulateCache) { + m_preprocessCache->Insert(m_stage, sourceHash, m_source, env.fingerprint, Move(fresh)); + } RememberCompiledSource(sourceHash); return; } ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage), .sourceStr = shared.preprocessedSource, - .flags = 0}; + .flags = 0, + .env = &env}; auto result = ShaderCompiler::CompileShader(attrib); if (result) { - m_compileStatus = true; - m_shader = result.value(); + compiled.compileStatus = true; + compiled.shader = result.value(); // Copy, not move: `shared` may alias a cache entry that has to outlive us, and // `fresh` is about to be handed to the cache. - m_preprocessedSource = shared.preprocessedSource; - m_explicitUniformLocations = shared.explicitUniformLocations; - m_explicitOpaqueBindings = shared.explicitOpaqueBindings; - m_infoLog.clear(); - if (shouldPopulateCache) m_preprocessCache->Insert(m_stage, sourceHash, m_source, Move(fresh)); - } else { - m_infoLog = result.error().log; - MGLOG_D("ShaderObject::Compile: Shader %d compilation failed.\nSource:\n%s\nInfoLog:\n%s\nSetting " - "m_compileStatus = false as a result.", - m_externalIndex, shared.preprocessedSource.c_str(), m_infoLog.c_str()); + compiled.preprocessedSource = shared.preprocessedSource; + compiled.explicitUniformLocations = shared.explicitUniformLocations; + compiled.explicitOpaqueBindings = shared.explicitOpaqueBindings; + compiled.infoLog.clear(); if (shouldPopulateCache) { - fresh.outcome = ShaderPreprocessOutcome::ParseFailed; - fresh.infoLog = m_infoLog; - fresh.explicitUniformLocations.clear(); - fresh.explicitOpaqueBindings.clear(); - m_preprocessCache->Insert(m_stage, sourceHash, m_source, Move(fresh)); + m_preprocessCache->Insert(m_stage, sourceHash, m_source, env.fingerprint, Move(fresh)); + } + } else { + compiled.infoLog = result.error().log; + MGLOG_D("ShaderObject::Compile: Shader %d compilation failed.\nSource:\n%s\nInfoLog:\n%s\nSetting " + "compileStatus = false as a result.", + m_externalIndex, shared.preprocessedSource.c_str(), compiled.infoLog.c_str()); + if (shouldPopulateCache) { + fresh->outcome = ShaderPreprocessOutcome::ParseFailed; + fresh->infoLog = compiled.infoLog; + fresh->explicitUniformLocations.clear(); + fresh->explicitOpaqueBindings.clear(); + m_preprocessCache->Insert(m_stage, sourceHash, m_source, env.fingerprint, Move(fresh)); } } RememberCompiledSource(sourceHash); } SharedPtr ShaderObject::TakeShaderForLink(String& outReparseLog) { - if (m_shader && !m_shaderConsumedByLink) { - m_shaderConsumedByLink = true; - return m_shader; + CompileArtifacts& compiled = Compiled(); + if (compiled.shader && !compiled.shaderConsumedByLink) { + compiled.shaderConsumedByLink = true; + return compiled.shader; } // The stored parse already fed a link, whose mapIO mutated its intermediate. @@ -332,8 +326,11 @@ namespace MobileGL::MG_State::GLState { // here on EVERY link rather than only on reuse. using namespace MG_Util::ShaderTranspiler; ShaderAttrib attrib{.shaderType = MG_Util::ConvertShaderStageToGLEnum(m_stage), - .sourceStr = m_preprocessedSource, - .flags = 0}; + .sourceStr = compiled.preprocessedSource, + .flags = 0, + // Re-parse against the SAME environment the original parse used, + // not against whatever the backend reports now. + .env = compiled.env.get()}; auto result = ShaderCompiler::CompileShader(attrib); if (!result) { // Should be unreachable: the same source parsed successfully at Compile(). diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h index 0843ff1c..860bbf68 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderObject.h @@ -8,6 +8,7 @@ #pragma once #include +#include namespace MobileGL { enum class ShaderStage { @@ -31,9 +32,12 @@ namespace MobileGL { // `preprocessCache` is the owning context's cross-object memo (P0b layer 2); // null is fully supported and simply means "no sharing" - that is what the // context-less internal shader objects (the default FS, the blit pipeline) use. + // Shared ownership rather than a raw pointer: once compiles run on a worker the + // job outlives neither the object nor the context deterministically, and the + // cache has to stay alive for whoever is still reading it. ShaderObject(const ShaderStage stage, Uint externalIndex, - ShaderPreprocessCache* preprocessCache = nullptr) - : m_stage(stage), m_externalIndex(externalIndex), m_preprocessCache(preprocessCache) {} + SharedPtr preprocessCache = nullptr) + : m_stage(stage), m_externalIndex(externalIndex), m_preprocessCache(Move(preprocessCache)) {} void SetShaderSource(const String& source); void SetShaderSource(String&& source); void Compile(); @@ -51,29 +55,83 @@ namespace MobileGL { Uint GetExternalIndex() const { return m_externalIndex; } ShaderStage GetShaderStage() const { return m_stage; } const String& GetShaderSource() const { return m_source; } - const SharedPtr& GetCompiledShader() const { return m_shader; } - const String& GetInfoLog() const { return m_infoLog; } - const UnorderedMap& GetUniformLocations() const { return m_uniforms; } + const SharedPtr& GetCompiledShader() const { return Compiled().shader; } + const String& GetInfoLog() const { return Compiled().infoLog; } + const UnorderedMap& GetUniformLocations() const { return Compiled().uniforms; } // Explicit layout(location = N) qualifiers on this shader's default-block // uniforms, captured lexically at Compile() because the relaxed parse drops // them from reflection (see ExtractExplicitUniformLocations). const UnorderedMap& GetExplicitUniformLocations() const { - return m_explicitUniformLocations; + return Compiled().explicitUniformLocations; } // Explicit layout(binding = N) on sampler/image uniforms - their initial // texture/image units - captured lexically for the same reason (see // ExtractExplicitOpaqueBindings). - const UnorderedMap& GetExplicitOpaqueBindings() const { return m_explicitOpaqueBindings; } - Bool GetCompileStatus() const { return m_compileStatus; } + const UnorderedMap& GetExplicitOpaqueBindings() const { + return Compiled().explicitOpaqueBindings; + } + Bool GetCompileStatus() const { return Compiled().compileStatus; } Bool GetDeleteStatus() const { return m_deleteStatus; } + // Blocks until a pending compile (P1 stage 3 onwards) has published its + // artifacts. Public for the few sites that must join without reading anything. + // A no-op today - nothing is ever pending. + void JoinCompile() const { EnsureCompileJoined(); } + // True while this object holds the outcome (success OR failure) of a previous // Compile() of exactly the source it currently holds - i.e. while the P0b // layer-1 memo is armed and a glCompileShader would be a no-op. Diagnostics // and tests only; nothing in the GL frontend branches on it. + // + // Deliberately does NOT join: the memo bookkeeping below is GL-thread-owned and + // says nothing about whether a worker has finished, which is exactly the + // property GL_COMPLETION_STATUS_KHR needs when stage 3 lands. Bool HasMemoizedCompile() const { return m_hasCompiledState; } private: + // ---- P1: everything a compile PRODUCES, in one block ---- + // + // Same rule as ProgramObject::LinkArtifacts: this is exactly what + // InvalidateCompiledState() clears, i.e. exactly what one run of Compile() + // writes. Stage 3 lifts this struct wholesale into ShaderCompileTask, where a + // worker fills it in and the GL thread reads it through the same gate. + struct CompileArtifacts { + // The CompileEnv snapshot this compile ran against. Held so the + // consume-once re-parse in TakeShaderForLink() reproduces the original + // parse exactly, instead of re-reading whatever the backend says now. + SharedPtr env; + SharedPtr shader; + // The source Compile() actually parsed (after PreprocessShaderSource), kept + // for TakeShaderForLink's re-parse so a later link never depends on the + // preprocessor being deterministic across backend-state changes. + String preprocessedSource; + UnorderedMap uniforms; + UnorderedMap explicitUniformLocations; + UnorderedMap explicitOpaqueBindings; + Bool shaderConsumedByLink = false; + String infoLog; + Bool compileStatus = false; + }; + + // ---- The one and only join gate for compile output (P1 invariant I5) ---- + // Blocks until a pending compile has published into m_compiled. Today nothing + // is ever pending - glCompileShader still runs the whole body inline - so this + // is an unconditional no-op. It exists NOW so that every reader of compile + // output is already routed through it when stage 3 makes it block. + // + // Defined inline (not in ShaderObject.cpp): called from every Compiled() read, + // and the project never builds with LTO, so an out-of-line empty body would be + // a real cross-TU call at each of those call sites instead of folding away. + void EnsureCompileJoined() const {} + CompileArtifacts& Compiled() { + EnsureCompileJoined(); + return m_compiled; + } + const CompileArtifacts& Compiled() const { + EnsureCompileJoined(); + return m_compiled; + } + void InvalidateCompiledState(); // ---- P0b layer 1: per-object no-op recompile ---- // True iff `candidate` is byte-identical to the source that produced the @@ -84,31 +142,27 @@ namespace MobileGL { // Arms the layer-1 memo for the source that Compile() just processed. void RememberCompiledSource(Uint64 sourceHash); + // ---- GL-thread-owned state: never produced by a compile, so it never joins ---- const Uint m_externalIndex = 0; const ShaderStage m_stage; + // glShaderSource text. A worker only ever reads the snapshot handed to it, so + // GL_SHADER_SOURCE_LENGTH and glGetShaderSource never join. String m_source; - // The source Compile() actually parsed (after PreprocessShaderSource), kept - // for TakeShaderForLink's re-parse so a later link never depends on the - // preprocessor being deterministic across backend-state changes. - String m_preprocessedSource; - SharedPtr m_shader; - UnorderedMap m_uniforms; - UnorderedMap m_explicitUniformLocations; - UnorderedMap m_explicitOpaqueBindings; - Bool m_shaderConsumedByLink = false; - // P0b layer 2: the owning context's cross-object memo, or null. Not owned. - ShaderPreprocessCache* const m_preprocessCache = nullptr; + // P0b layer 2: the owning context's cross-object memo, or null. + const SharedPtr m_preprocessCache; // P0b layer 1. m_hasCompiledState is the invariant "m_source is byte-identical - // to the source that produced m_compileStatus/m_infoLog/m_shader"; it is armed - // at the end of every Compile() and disarmed by InvalidateCompiledState(). + // to the source that produced the compile artifacts"; it is armed at the end of + // every Compile() and disarmed by InvalidateCompiledState(). Stage 3 replaces + // all three with a pointer compare against the in-flight job's source snapshot. Bool m_hasCompiledState = false; Uint64 m_compiledSourceHash = 0; SizeT m_compiledSourceLength = 0; - String m_infoLog; Bool m_deleteStatus = false; - Bool m_compileStatus = false; + + // ---- Compile OUTPUT ---- reachable only through Compiled(). + CompileArtifacts m_compiled; }; } // namespace MG_State::GLState } // namespace MobileGL diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp b/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp index ed35c837..c84e0a4f 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.cpp @@ -9,9 +9,14 @@ #include "ShaderPreprocessCache.h" namespace MobileGL::MG_State::GLState { - const ShaderPreprocessResult* ShaderPreprocessCache::Find(const ShaderStage stage, const Uint64 sourceHash, - const String& source) const { - const Key key{.stage = stage, .sourceHash = sourceHash, .sourceLength = source.length()}; + ShaderPreprocessResultPtr ShaderPreprocessCache::Find(const ShaderStage stage, const Uint64 sourceHash, + const String& source, const Uint64 envFingerprint) const { + const Key key{.stage = stage, + .sourceHash = sourceHash, + .sourceLength = source.length(), + .envFingerprint = envFingerprint}; + + const std::lock_guard lock(m_mutex); const auto it = m_index.find(key); if (it == m_index.end()) return nullptr; @@ -20,52 +25,62 @@ namespace MobileGL::MG_State::GLState { const Entry& entry = *it->second; if (entry.originalSource != source) return nullptr; - return &entry.result; + // A copy of the SharedPtr, taken under the lock: the payload now outlives any + // eviction the caller races with. + return entry.result; } void ShaderPreprocessCache::Insert(const ShaderStage stage, const Uint64 sourceHash, const String& source, - ShaderPreprocessResult result) { - const SizeT entryBytes = EntryBytes(source, result); + const Uint64 envFingerprint, ShaderPreprocessResultPtr result) { + if (!result) return; + + const SizeT entryBytes = EntryBytes(source, *result); // A single source bigger than the whole budget would evict every other entry and // then itself; refuse it instead of thrashing the cache empty. if (entryBytes > kMaxStoredSourceBytes) return; - const Key key{.stage = stage, .sourceHash = sourceHash, .sourceLength = source.length()}; + const Key key{.stage = stage, + .sourceHash = sourceHash, + .sourceLength = source.length(), + .envFingerprint = envFingerprint}; + + const std::lock_guard lock(m_mutex); if (const auto existing = m_index.find(key); existing != m_index.end()) { // Either a re-insert of the same source (harmless) or a genuine hash collision // with a different source. Both are resolved by letting the newcomer win: one // entry per key keeps the index a plain map, and a collision is astronomically // rare enough that the loser simply misses. - EraseEntry(existing->second); + EraseEntryLocked(existing->second); } m_entries.push_back(Entry{.key = key, .originalSource = source, .result = Move(result)}); m_index[key] = std::prev(m_entries.end()); m_storedSourceBytes += entryBytes; - EvictUntilWithinBudget(); + EvictUntilWithinBudgetLocked(); } void ShaderPreprocessCache::Clear() { + const std::lock_guard lock(m_mutex); m_entries.clear(); m_index.clear(); m_storedSourceBytes = 0; } - void ShaderPreprocessCache::EraseEntry(const EntryList::iterator it) { - const SizeT bytes = EntryBytes(it->originalSource, it->result); + void ShaderPreprocessCache::EraseEntryLocked(const EntryList::iterator it) { + const SizeT bytes = EntryBytes(it->originalSource, *it->result); m_storedSourceBytes = bytes > m_storedSourceBytes ? 0 : m_storedSourceBytes - bytes; m_index.erase(it->key); m_entries.erase(it); } - void ShaderPreprocessCache::EvictUntilWithinBudget() { + void ShaderPreprocessCache::EvictUntilWithinBudgetLocked() { // FIFO: the oldest insertion goes first. Insert() already refuses entries larger // than the byte budget, so this loop always terminates with at least the entry // that was just added still resident. while (!m_entries.empty() && (m_entries.size() > kMaxEntries || m_storedSourceBytes > kMaxStoredSourceBytes)) { - EraseEntry(m_entries.begin()); + EraseEntryLocked(m_entries.begin()); } } } // namespace MobileGL::MG_State::GLState diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.h b/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.h index 9ce1bff5..0b13d1fd 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.h +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderPreprocessCache.h @@ -9,6 +9,7 @@ #pragma once #include #include +#include #include namespace MobileGL::MG_State::GLState { @@ -42,6 +43,11 @@ namespace MobileGL::MG_State::GLState { Bool Preprocessed() const { return outcome == ShaderPreprocessOutcome::Preprocessed; } }; + // Cache hits hand out shared ownership, not a raw pointer into the entry list. That is + // what makes the cache safe once compiles run concurrently: a reader keeps its payload + // alive across any eviction, and a 107 KB preprocessedSource is never copied on a hit. + using ShaderPreprocessResultPtr = SharedPtr; + // P0b layer 2: a per-context, bounded memo of the source-only half of shader // compilation, keyed by (stage, xxhash64(source), source length). // @@ -71,14 +77,22 @@ namespace MobileGL::MG_State::GLState { static constexpr SizeT kMaxEntries = 128; static constexpr SizeT kMaxStoredSourceBytes = 8u * 1024u * 1024u; - // Returns the memoized result for this exact source, or null on a miss. The - // returned pointer stays valid until the next Insert()/Clear() on this cache. - const ShaderPreprocessResult* Find(ShaderStage stage, Uint64 sourceHash, const String& source) const; + // Returns the memoized result for this exact source under this exact compile + // environment, or null on a miss. The returned SharedPtr owns its payload, so it + // stays valid for as long as the caller holds it - across Insert(), Clear(), and + // across the destruction of the cache itself. + // + // envFingerprint joins the key because the source-only pipeline's compute + // local-size verdict is computed against CompileEnv's device limits: a memo must + // never outlive the environment it was computed against (memo-hazard rule). + ShaderPreprocessResultPtr Find(ShaderStage stage, Uint64 sourceHash, const String& source, + Uint64 envFingerprint) const; // Memoizes `result` for this source. A source whose own storage cost already // exceeds the byte budget is simply not cached (caching it would evict everything // else and then itself). - void Insert(ShaderStage stage, Uint64 sourceHash, const String& source, ShaderPreprocessResult result); + void Insert(ShaderStage stage, Uint64 sourceHash, const String& source, Uint64 envFingerprint, + ShaderPreprocessResultPtr result); void Clear(); @@ -86,17 +100,25 @@ namespace MobileGL::MG_State::GLState { return static_cast(XXH64(source.data(), source.length(), 0)); } - SizeT GetEntryCount() const { return m_entries.size(); } - SizeT GetStoredSourceBytes() const { return m_storedSourceBytes; } + SizeT GetEntryCount() const { + const std::lock_guard lock(m_mutex); + return m_entries.size(); + } + SizeT GetStoredSourceBytes() const { + const std::lock_guard lock(m_mutex); + return m_storedSourceBytes; + } private: struct Key { ShaderStage stage = ShaderStage::Unknown; Uint64 sourceHash = 0; SizeT sourceLength = 0; + Uint64 envFingerprint = 0; Bool operator==(const Key& other) const { - return stage == other.stage && sourceHash == other.sourceHash && sourceLength == other.sourceLength; + return stage == other.stage && sourceHash == other.sourceHash && + sourceLength == other.sourceLength && envFingerprint == other.envFingerprint; } }; @@ -107,6 +129,7 @@ namespace MobileGL::MG_State::GLState { Uint64 mixed = key.sourceHash; mixed ^= static_cast(key.sourceLength) + 0x9e3779b97f4a7c15ull + (mixed << 6) + (mixed >> 2); mixed ^= static_cast(static_cast(key.stage)) * 0xff51afd7ed558ccdull; + mixed ^= key.envFingerprint + 0x9e3779b97f4a7c15ull + (mixed << 6) + (mixed >> 2); return static_cast(mixed); } }; @@ -116,7 +139,7 @@ namespace MobileGL::MG_State::GLState { // The full original (pre-preprocess) source, kept so a hit can be confirmed by // comparison instead of trusting the hash. String originalSource; - ShaderPreprocessResult result; + ShaderPreprocessResultPtr result; }; using EntryList = std::list; @@ -125,14 +148,16 @@ namespace MobileGL::MG_State::GLState { return source.length() + result.preprocessedSource.length(); } - void EraseEntry(EntryList::iterator it); - void EvictUntilWithinBudget(); + void EvictUntilWithinBudgetLocked(); - // P1: needs a mutex when compiles go async. Everything here is reached from - // glCompileShader on the single GL thread that owns the context, so today the - // cache is deliberately lock-free; the moment shader compilation moves onto a - // worker pool, Find/Insert/Clear all become critical sections (and Find's returned - // pointer stops being safe to hold across an Insert). + void EraseEntryLocked(EntryList::iterator it); + + // P1: every public entry point takes this. The lock alone would NOT have been + // enough - the old Find() handed back a raw pointer into an entry that a + // concurrent Insert()'s FIFO eviction could erase while the caller was still + // reading it. Shared ownership of the payload is what closes that hole; the mutex + // only protects the containers below. + mutable std::mutex m_mutex; EntryList m_entries; // front = oldest (FIFO victim) UnorderedMap m_index; SizeT m_storedSourceBytes = 0; diff --git a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp index f0dec2f9..98c09e94 100644 --- a/MobileGL/MG_Test/Program/ProgramUtilTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramUtilTest.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -2378,6 +2379,78 @@ void main() { } // --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// P1: CompileEnv - the compile pipeline's snapshot of everything outside +// (stage, source). These pin the two properties the rest of P1 rides on: the +// compute limits really are carried in the snapshot (an off-thread +// GL_MAX_COMPUTE_WORK_GROUP_SIZE query would silently return 0 and reject a +// legal local_size), and the fingerprint really does move when they do. +// --------------------------------------------------------------------------- +TEST_F(ProgramUtilTest, CompileEnvCarriesComputeLimitsAndFrontendMinima) { + using MobileGL::MG_Util::ShaderTranspiler::CaptureCompileEnv; + + const auto env = CaptureCompileEnv(); + ASSERT_NE(env, nullptr); + // With no backend the snapshot is the frontend minimum, never zero - the value an + // off-thread GetIntegeri_v would have left behind. + EXPECT_GE(env->maxComputeWorkGroupSize[0], 1024u); + EXPECT_GE(env->maxComputeWorkGroupSize[1], 1024u); + EXPECT_GE(env->maxComputeWorkGroupSize[2], 64u); + EXPECT_GE(env->maxComputeWorkGroupInvocations, 1024u); + EXPECT_NE(env->fingerprint, 0u); +} + +TEST_F(ProgramUtilTest, CompileEnvFingerprintTracksEveryInput) { + using MobileGL::MG_Util::ShaderTranspiler::CompileEnv; + using MobileGL::MG_Util::ShaderTranspiler::ComputeCompileEnvFingerprint; + + CompileEnv base; + const Uint64 baseline = ComputeCompileEnvFingerprint(base); + EXPECT_EQ(ComputeCompileEnvFingerprint(base), baseline) << "fingerprint must be deterministic"; + + // A device that allows a bigger workgroup than the frontend minimum is a DIFFERENT + // compile environment: a memo taken under the smaller limit must not be reusable. + CompileEnv biggerZ = base; + biggerZ.maxComputeWorkGroupSize[2] = 256; + EXPECT_NE(ComputeCompileEnvFingerprint(biggerZ), baseline); + + CompileEnv moreInvocations = base; + moreInvocations.maxComputeWorkGroupInvocations = 2048; + EXPECT_NE(ComputeCompileEnvFingerprint(moreInvocations), baseline); + + CompileEnv otherBackend = base; + otherBackend.backend = MobileGL::BackendType::DirectVulkan; + EXPECT_NE(ComputeCompileEnvFingerprint(otherBackend), baseline); + + CompileEnv otherLimits = base; + otherLimits.params.MaxVertexAttribs = 31; + EXPECT_NE(ComputeCompileEnvFingerprint(otherLimits), baseline); + + CompileEnv otherExtensions = base; + otherExtensions.advertisedExtensions.push_back(MobileGL::E_GL_ARB_gpu_shader_int64); + EXPECT_NE(ComputeCompileEnvFingerprint(otherExtensions), baseline); + + CompileEnv otherQuirk = base; + otherQuirk.subgroupPrefixScanQuirk = MobileGL::MG_Config::QuirkOverride::ForceOn; + EXPECT_NE(ComputeCompileEnvFingerprint(otherQuirk), baseline); +} + +// The no-backend fallback must stay exactly what the pipeline used to do inline: +// everything counts as advertised, because there is nothing to gate against. +TEST_F(ProgramUtilTest, CompileEnvWithoutBackendAdvertisesEverything) { + using MobileGL::MG_Util::ShaderTranspiler::CompileEnv; + + CompileEnv env; + EXPECT_FALSE(env.HasBackend()); + EXPECT_TRUE(env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)); + + env.backend = MobileGL::BackendType::DirectGLES; + EXPECT_TRUE(env.HasBackend()); + EXPECT_FALSE(env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)); + env.advertisedExtensions.push_back(MobileGL::E_GL_ARB_gpu_shader_int64); + EXPECT_TRUE(env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)); +} + // P0b layer 2: ShaderPreprocessCache, tested directly. The GL-level behaviour it // enables is covered end to end in ProgramTest; these pin the container itself, // where the interesting cases (hash collisions, both eviction budgets) are hard @@ -2387,13 +2460,19 @@ namespace { using MobileGL::MG_State::GLState::ShaderPreprocessCache; using MobileGL::MG_State::GLState::ShaderPreprocessOutcome; using MobileGL::MG_State::GLState::ShaderPreprocessResult; + using MobileGL::MG_State::GLState::ShaderPreprocessResultPtr; - ShaderPreprocessResult MakeResult(const String& preprocessed) { - ShaderPreprocessResult result; - result.outcome = ShaderPreprocessOutcome::Preprocessed; - result.preprocessedSource = preprocessed; - result.explicitUniformLocations["uMarker"] = 7; - result.explicitOpaqueBindings["sMarker"] = 3; + // The env fingerprint every test below keys against, unless it is specifically + // exercising the fingerprint itself. + constexpr MobileGL::Uint64 kEnvA = 0x1111'2222'3333'4444ull; + constexpr MobileGL::Uint64 kEnvB = 0x5555'6666'7777'8888ull; + + ShaderPreprocessResultPtr MakeResult(const String& preprocessed) { + auto result = MakeShared(); + result->outcome = ShaderPreprocessOutcome::Preprocessed; + result->preprocessedSource = preprocessed; + result->explicitUniformLocations["uMarker"] = 7; + result->explicitOpaqueBindings["sMarker"] = 3; return result; } } // namespace @@ -2403,10 +2482,10 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRoundTripsAndSeparatesStages) { const String source = "// a shader\nvoid main() {}\n"; const Uint64 hash = ShaderPreprocessCache::HashSource(source); - EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source), nullptr); + EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source, kEnvA), nullptr); - cache.Insert(ShaderStage::Vertex, hash, source, MakeResult("vertex-preprocessed")); - const ShaderPreprocessResult* hit = cache.Find(ShaderStage::Vertex, hash, source); + cache.Insert(ShaderStage::Vertex, hash, source, kEnvA, MakeResult("vertex-preprocessed")); + const ShaderPreprocessResultPtr hit = cache.Find(ShaderStage::Vertex, hash, source, kEnvA); ASSERT_NE(hit, nullptr); EXPECT_TRUE(hit->Preprocessed()); EXPECT_EQ(hit->preprocessedSource, "vertex-preprocessed"); @@ -2419,12 +2498,12 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRoundTripsAndSeparatesStages) { // Byte-identical source, different stage: a different key, so still a miss. Two // stages sharing one entry would hand a fragment shader a vertex preprocess. - EXPECT_EQ(cache.Find(ShaderStage::Fragment, hash, source), nullptr); - cache.Insert(ShaderStage::Fragment, hash, source, MakeResult("fragment-preprocessed")); - const ShaderPreprocessResult* fragmentHit = cache.Find(ShaderStage::Fragment, hash, source); + EXPECT_EQ(cache.Find(ShaderStage::Fragment, hash, source, kEnvA), nullptr); + cache.Insert(ShaderStage::Fragment, hash, source, kEnvA, MakeResult("fragment-preprocessed")); + const ShaderPreprocessResultPtr fragmentHit = cache.Find(ShaderStage::Fragment, hash, source, kEnvA); ASSERT_NE(fragmentHit, nullptr); EXPECT_EQ(fragmentHit->preprocessedSource, "fragment-preprocessed"); - EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source)->preprocessedSource, "vertex-preprocessed"); + EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source, kEnvA)->preprocessedSource, "vertex-preprocessed"); EXPECT_EQ(cache.GetEntryCount(), 2u); } @@ -2433,27 +2512,27 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheMemoizesRejectionVerdictsDistinctly const String reservedSource = "int packed;\n"; const String localSizeSource = "layout(local_size_x = 99999) in;\n"; - ShaderPreprocessResult reserved; - reserved.outcome = ShaderPreprocessOutcome::ReservedIdentifierRejected; - reserved.infoLog = "reserved identifier"; - ShaderPreprocessResult localSize; - localSize.outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected; - localSize.infoLog = "local_size too big"; + auto reserved = MakeShared(); + reserved->outcome = ShaderPreprocessOutcome::ReservedIdentifierRejected; + reserved->infoLog = "reserved identifier"; + auto localSize = MakeShared(); + localSize->outcome = ShaderPreprocessOutcome::ComputeLocalSizeRejected; + localSize->infoLog = "local_size too big"; - cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource, - std::move(reserved)); - cache.Insert(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource, - std::move(localSize)); + cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource, kEnvA, + Move(reserved)); + cache.Insert(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource, kEnvA, + Move(localSize)); - const ShaderPreprocessResult* reservedHit = - cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource); + const ShaderPreprocessResultPtr reservedHit = + cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(reservedSource), reservedSource, kEnvA); ASSERT_NE(reservedHit, nullptr); EXPECT_FALSE(reservedHit->Preprocessed()); EXPECT_EQ(reservedHit->outcome, ShaderPreprocessOutcome::ReservedIdentifierRejected); EXPECT_EQ(reservedHit->infoLog, "reserved identifier"); - const ShaderPreprocessResult* localSizeHit = - cache.Find(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource); + const ShaderPreprocessResultPtr localSizeHit = + cache.Find(ShaderStage::Compute, ShaderPreprocessCache::HashSource(localSizeSource), localSizeSource, kEnvA); ASSERT_NE(localSizeHit, nullptr); EXPECT_EQ(localSizeHit->outcome, ShaderPreprocessOutcome::ComputeLocalSizeRejected); EXPECT_EQ(localSizeHit->infoLog, "local_size too big"); @@ -2470,28 +2549,64 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheRejectsForgedHashCollision) { ASSERT_NE(real, impostor); const Uint64 forgedHash = 0xdeadbeefcafef00dull; - cache.Insert(ShaderStage::Vertex, forgedHash, real, MakeResult("real-preprocessed")); + cache.Insert(ShaderStage::Vertex, forgedHash, real, kEnvA, MakeResult("real-preprocessed")); - ASSERT_NE(cache.Find(ShaderStage::Vertex, forgedHash, real), nullptr); - EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, impostor), nullptr); + ASSERT_NE(cache.Find(ShaderStage::Vertex, forgedHash, real, kEnvA), nullptr); + EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, impostor, kEnvA), nullptr); // The colliding newcomer wins the slot rather than being silently dropped, so it // is the previous occupant that degrades to a miss - never a wrong hit. - cache.Insert(ShaderStage::Vertex, forgedHash, impostor, MakeResult("impostor-preprocessed")); - const ShaderPreprocessResult* impostorHit = cache.Find(ShaderStage::Vertex, forgedHash, impostor); + cache.Insert(ShaderStage::Vertex, forgedHash, impostor, kEnvA, MakeResult("impostor-preprocessed")); + const ShaderPreprocessResultPtr impostorHit = cache.Find(ShaderStage::Vertex, forgedHash, impostor, kEnvA); ASSERT_NE(impostorHit, nullptr); EXPECT_EQ(impostorHit->preprocessedSource, "impostor-preprocessed"); - EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, real), nullptr); + EXPECT_EQ(cache.Find(ShaderStage::Vertex, forgedHash, real, kEnvA), nullptr); EXPECT_EQ(cache.GetEntryCount(), 1u); } +// P1: the compile environment joins the key. A memo computed against one backend's +// GL_MAX_COMPUTE_WORK_GROUP_* limits must never be handed back after the environment +// changed (backend swap), which is exactly what CompileEnv::fingerprint keys on. +TEST_F(ProgramUtilTest, ShaderPreprocessCacheMissesOnChangedEnvFingerprint) { + ShaderPreprocessCache cache; + const String source = "layout(local_size_x = 512) in;\nvoid main() {}\n"; + const Uint64 hash = ShaderPreprocessCache::HashSource(source); + + cache.Insert(ShaderStage::Compute, hash, source, kEnvA, MakeResult("env-a-preprocessed")); + ASSERT_NE(cache.Find(ShaderStage::Compute, hash, source, kEnvA), nullptr); + EXPECT_EQ(cache.Find(ShaderStage::Compute, hash, source, kEnvB), nullptr); + + // Both environments can coexist; neither can see the other's verdict. + cache.Insert(ShaderStage::Compute, hash, source, kEnvB, MakeResult("env-b-preprocessed")); + EXPECT_EQ(cache.Find(ShaderStage::Compute, hash, source, kEnvA)->preprocessedSource, "env-a-preprocessed"); + EXPECT_EQ(cache.Find(ShaderStage::Compute, hash, source, kEnvB)->preprocessedSource, "env-b-preprocessed"); + EXPECT_EQ(cache.GetEntryCount(), 2u); +} + +// A hit hands out shared ownership, so the payload survives the eviction of its entry. +// Under the old raw-pointer API this read was a use-after-free the moment two compiles +// ran concurrently. +TEST_F(ProgramUtilTest, ShaderPreprocessCacheHitOutlivesEviction) { + ShaderPreprocessCache cache; + const String source = "void main() { int keep = 1; }\n"; + const Uint64 hash = ShaderPreprocessCache::HashSource(source); + cache.Insert(ShaderStage::Vertex, hash, source, kEnvA, MakeResult("survivor")); + + const ShaderPreprocessResultPtr held = cache.Find(ShaderStage::Vertex, hash, source, kEnvA); + ASSERT_NE(held, nullptr); + + cache.Clear(); + EXPECT_EQ(cache.Find(ShaderStage::Vertex, hash, source, kEnvA), nullptr); + EXPECT_EQ(held->preprocessedSource, "survivor"); +} + TEST_F(ProgramUtilTest, ShaderPreprocessCacheEvictsFifoOnEntryCap) { ShaderPreprocessCache cache; Vector sources; const SizeT overflow = ShaderPreprocessCache::kMaxEntries + 8; for (SizeT i = 0; i < overflow; ++i) { sources.push_back("void main() { int a = " + ToString(i) + "; }\n"); - cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources.back()), sources.back(), + cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources.back()), sources.back(), kEnvA, MakeResult("pp" + ToString(i))); EXPECT_LE(cache.GetEntryCount(), ShaderPreprocessCache::kMaxEntries); } @@ -2499,8 +2614,8 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheEvictsFifoOnEntryCap) { // FIFO: the first `overflow - kMaxEntries` insertions are gone, the rest resident. for (SizeT i = 0; i < overflow; ++i) { - const ShaderPreprocessResult* hit = - cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources[i]), sources[i]); + const ShaderPreprocessResultPtr hit = + cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(sources[i]), sources[i], kEnvA); if (i < overflow - ShaderPreprocessCache::kMaxEntries) { EXPECT_EQ(hit, nullptr) << "entry " << i << " should have been evicted"; } else { @@ -2521,7 +2636,7 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheHonorsByteBudget) { const SizeT chunk = ShaderPreprocessCache::kMaxStoredSourceBytes / 8; for (SizeT i = 0; i < 24; ++i) { String source(chunk, static_cast('a' + (i % 26))); - cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(source), source, MakeResult("")); + cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(source), source, kEnvA, MakeResult("")); EXPECT_LE(cache.GetStoredSourceBytes(), ShaderPreprocessCache::kMaxStoredSourceBytes); EXPECT_LT(cache.GetEntryCount(), ShaderPreprocessCache::kMaxEntries); } @@ -2530,7 +2645,7 @@ TEST_F(ProgramUtilTest, ShaderPreprocessCacheHonorsByteBudget) { // would evict every other entry and then immediately itself. const SizeT before = cache.GetEntryCount(); const String oversized(ShaderPreprocessCache::kMaxStoredSourceBytes + 1, 'z'); - cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, MakeResult("")); + cache.Insert(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, kEnvA, MakeResult("")); EXPECT_EQ(cache.GetEntryCount(), before); - EXPECT_EQ(cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized), nullptr); + EXPECT_EQ(cache.Find(ShaderStage::Vertex, ShaderPreprocessCache::HashSource(oversized), oversized, kEnvA), nullptr); } diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp new file mode 100644 index 00000000..52c26d9c --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp @@ -0,0 +1,98 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/CompileEnv.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 "CompileEnv.h" +#include +#include + +namespace MobileGL::MG_Util::ShaderTranspiler { + namespace { + void HashBytes(Uint64& state, const void* data, const SizeT length) { + state = static_cast(XXH64(data, length, state)); + } + + template + void HashValue(Uint64& state, const T& value) { + static_assert(std::is_trivially_copyable_v); + HashBytes(state, &value, sizeof(T)); + } + } // namespace + + Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env) { + Uint64 state = 0x9e3779b97f4a7c15ull; + HashValue(state, env.maxComputeWorkGroupSize[0]); + HashValue(state, env.maxComputeWorkGroupSize[1]); + HashValue(state, env.maxComputeWorkGroupSize[2]); + HashValue(state, env.maxComputeWorkGroupInvocations); + HashValue(state, env.backend); + // DynamicBackendParameters is a plain aggregate of scalars; hashing its object + // representation is deliberate - it means a new limit cannot be added without also + // changing the fingerprint, which is exactly the memo-hazard property wanted here. + HashBytes(state, &env.params, sizeof(env.params)); + if (!env.advertisedExtensions.empty()) { + HashBytes(state, env.advertisedExtensions.data(), + env.advertisedExtensions.size() * sizeof(GLExtension)); + } + HashValue(state, env.subgroupPrefixScanQuirk); + return state; + } + + SharedPtr CaptureCompileEnv() { + auto env = MakeShared(); + + const auto& activeBackend = MG_Backend::pActiveBackendObject; + if (activeBackend) { + env->backend = activeBackend->GetBackendType(); + env->params = activeBackend->GetDynamicParameters(); + env->advertisedExtensions = activeBackend->GetRendererInfo().RendererGLInfo.Extensions; + } + + // GL_MAX_COMPUTE_WORK_GROUP_SIZE. This is a REAL driver call on DirectGLES; it must + // happen here, on the context thread, and exactly once per context. The frontend + // minimum is the floor, matching what GL_Getter reports. + // TODO: Share these exposed compute limit helpers with GL_Getter.cpp instead of duplicating the frontend minima. + constexpr Uint kFrontendMinComputeWorkGroupSizes[3] = {1024, 1024, 64}; + for (Uint index = 0; index < 3; ++index) { + Int backendValue = 0; + if (MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v) { + MG_Backend::gBackendFunctionsTable.GL.GetIntegeri_v(GL_MAX_COMPUTE_WORK_GROUP_SIZE, index, + &backendValue); + } + env->maxComputeWorkGroupSize[index] = + std::max(static_cast(std::max(backendValue, 0)), kFrontendMinComputeWorkGroupSizes[index]); + } + + constexpr Uint64 kFrontendMaxComputeWorkGroupInvocations = 1024; + env->maxComputeWorkGroupInvocations = + activeBackend ? std::max(static_cast(std::max(env->params.MaxComputeWorkGroupInvocations, 0)), + kFrontendMaxComputeWorkGroupInvocations) + : kFrontendMaxComputeWorkGroupInvocations; + + env->subgroupPrefixScanQuirk = MG_Config::Features.SubgroupPrefixScanQuirk; + + env->fingerprint = ComputeCompileEnvFingerprint(*env); + return env; + } + + const SharedPtr& GetDefaultCompileEnv() { + // Function-local static, not a namespace-scope one: the fingerprint has to be + // computed, and this must not run before MG_Config is loaded. + static const SharedPtr kDefault = [] { + auto env = MakeShared(); + env->subgroupPrefixScanQuirk = MG_Config::Features.SubgroupPrefixScanQuirk; + env->fingerprint = ComputeCompileEnvFingerprint(*env); + return SharedPtr(Move(env)); + }(); + return kDefault; + } + + const SharedPtr& GetCurrentCompileEnv() { + if (MG_State::pGLContext) return MG_State::pGLContext->GetCompileEnv(); + return GetDefaultCompileEnv(); + } +} // namespace MobileGL::MG_Util::ShaderTranspiler diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h new file mode 100644 index 00000000..f70c3a6b --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h @@ -0,0 +1,81 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/CompileEnv.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 +#include +#include + +namespace MobileGL::MG_Util::ShaderTranspiler { + // Everything the shader compile/link pipeline reads from OUTSIDE its own (stage, source) + // inputs: backend identity, backend limits, the advertised extension list, and the one + // config quirk the source rewriter branches on. + // + // Why it exists (P1): every one of those reads is a reach-back into + // MG_Backend::pActiveBackendObject / gBackendFunctionsTable, and one of them + // (GL_MAX_COMPUTE_WORK_GROUP_SIZE) is a *real driver call* that on the DirectGLES + // backend silently no-ops off the context thread - which would turn a perfectly legal + // `local_size_z` into COMPILE_STATUS=FALSE the moment compilation moved to a worker. + // Snapshotting the whole set once per context, on the GL thread, removes every + // reach-back at once and makes the pipeline a pure function of (stage, source, env). + // + // Lifetime: captured lazily on first use by GLState::GLContext::GetCompileEnv(), and + // RE-captured if the active backend object changes. Immutable once published; held by + // value/`SharedPtr` so a worker can never observe a torn update. + // + // Memo-hazard rule: `fingerprint` hashes every member above it and is part of the P0b + // ShaderPreprocessCache key, so a memo computed against one env can never be returned + // against another. ADDING A FIELD HERE MEANS ADDING IT TO ComputeFingerprint(). + struct CompileEnv { + // --- compute limits: the ONLY former real-driver read in the pipeline --- + // GL_MAX_COMPUTE_WORK_GROUP_SIZE, already max()'d with the frontend minimum. + Uint maxComputeWorkGroupSize[3] = {1024, 1024, 64}; + // GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS, likewise. + Uint64 maxComputeWorkGroupInvocations = 1024; + + // --- backend identity + limits --- + // Unknown means "no backend was active at capture time". Every consumer keeps the + // exact no-backend fallback it had before: extensions read as advertised, limits + // read as the frontend defaults. + BackendType backend = BackendType::Unknown; + MG_Backend::DynamicBackendParameters params{}; // by value, never by reference + Vector advertisedExtensions; + + // --- config the source rewriter branches on --- + MG_Config::QuirkOverride subgroupPrefixScanQuirk = MG_Config::QuirkOverride::Auto; + + Uint64 fingerprint = 0; // set by CaptureCompileEnv() + + Bool HasBackend() const { return backend != BackendType::Unknown; } + // Matches the historical rule exactly: with no active backend every extension counts + // as advertised, because the frontend then has nothing to gate against. + Bool IsExtensionAdvertised(GLExtension extension) const { + if (!HasBackend()) return true; + return std::find(advertisedExtensions.begin(), advertisedExtensions.end(), extension) != + advertisedExtensions.end(); + } + }; + + // Hashes every semantically relevant member. Public so a test can assert that two + // different envs really do produce different P0b cache keys. + Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env); + + // GL thread only: this is where the GL_MAX_COMPUTE_WORK_GROUP_SIZE queries live now. + SharedPtr CaptureCompileEnv(); + + // The env a context-less caller gets: exactly what CaptureCompileEnv() would produce + // with no active backend. Used by the unit tests that drive the transpiler directly and + // by the internal shader objects that compile before any context exists. + const SharedPtr& GetDefaultCompileEnv(); + + // The env of the current GL context, or GetDefaultCompileEnv() when there is none. + // GL thread only (it may trigger a capture). This is the compatibility shim for the + // handful of entry points that still resolve their env implicitly; the pipeline itself + // always takes an explicit `const CompileEnv&`. + const SharedPtr& GetCurrentCompileEnv(); +} // namespace MobileGL::MG_Util::ShaderTranspiler diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 204880c4..f59e5e73 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -37,7 +37,10 @@ namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { - TBuiltInResource BuildTBuiltInResource() { + // `env` is the compile-time backend snapshot; null means "resolve from the live + // backend", which is what the standalone/test entry points do. The pipeline always + // passes one, so a worker never reaches pActiveBackendObject through here. + TBuiltInResource BuildTBuiltInResource(const CompileEnv* env) { TBuiltInResource Resources{}; Resources.maxLights = 32; Resources.maxClipPlanes = 6; @@ -139,7 +142,8 @@ namespace MobileGL { const MG_Backend::DynamicBackendParameters fallbackParameters{}; const auto& activeBackend = MG_Backend::pActiveBackendObject; const auto& dynamicParameters = - activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters; + env ? env->params + : (activeBackend ? activeBackend->GetDynamicParameters() : fallbackParameters); Resources.maxImageUnits = dynamicParameters.MaxImageUnits; Resources.maxCombinedImageUnitsAndFragmentOutputs = dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers; @@ -167,7 +171,8 @@ namespace MobileGL { // copies that could drift apart. static Result> ParseShaderSource(EShLanguage lang, GLenum shaderType, const String& source, - Flags flags) { + Flags flags, + const CompileEnv* env) { SharedPtr res; auto& tshader = res; tshader = MakeShared(lang); @@ -194,7 +199,7 @@ namespace MobileGL { tshader->setAutoMapLocations(true); tshader->setAutoMapBindings(true); tshader->setGlobalUniformBlockName(GLOBAL_UBO_NAME); - auto resources = BuildTBuiltInResource(); + auto resources = BuildTBuiltInResource(env); if (!tshader->parse(&resources, 460, ECoreProfile, /*forceDefaultVersionAndProfile: */ false, /*forwardCompatible: */ true, EShMsgDefault)) { @@ -220,7 +225,7 @@ namespace MobileGL { } const String source(attrib.sourceStr); - auto result = ParseShaderSource(lang, shaderType, source, attrib.flags); + auto result = ParseShaderSource(lang, shaderType, source, attrib.flags, attrib.env); if (result) return result; // Legacy desktop sources are normalized to "#version 330 core" (with a marker on the @@ -236,7 +241,7 @@ namespace MobileGL { return result; } - auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags); + auto retryResult = ParseShaderSource(lang, shaderType, retrySource, attrib.flags, attrib.env); if (!retryResult) return result; MGLOG_D("CompileShader: %s only parsed after retargeting its legacy #version to 460", diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp index 8d96bd98..bab0f43d 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include "EsslBuiltinFunctionNames.h" @@ -1036,15 +1037,6 @@ namespace { source = std::move(result); } - bool IsExtensionAdvertised(MobileGL::GLExtension extension) { - const auto& activeBackendObject = MobileGL::MG_Backend::pActiveBackendObject; - if (!activeBackendObject) { - return true; - } - - const auto& extensions = activeBackendObject->GetRendererInfo().RendererGLInfo.Extensions; - return std::find(extensions.begin(), extensions.end(), extension) != extensions.end(); - } MobileGL::String TrimDirectiveToken(const MobileGL::String& token) { SizeT start = 0; @@ -1059,8 +1051,9 @@ namespace { return token.substr(start, end - start); } - void FilterUnsupportedGpuShaderInt64(MobileGL::String& source) { - if (IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)) { + void FilterUnsupportedGpuShaderInt64(const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env, + MobileGL::String& source) { + if (env.IsExtensionAdvertised(MobileGL::E_GL_ARB_gpu_shader_int64)) { return; } @@ -1314,7 +1307,9 @@ namespace MobileGL { // instead of open-coding them in PreprocessShaderSource. struct ShaderSourceQuirk { const char* name; - MG_Config::QuirkOverride (*GetOverride)(); + // Reads the override out of the captured env, never out of the live + // MG_Config table: a worker must see the same config the GL thread saw. + MG_Config::QuirkOverride (*GetOverride)(const CompileEnv&); Bool (*DeviceApplies)(const ShaderSourceQuirkContext&); Bool (*Apply)(const ShaderSourceQuirkContext&, String&); }; @@ -1323,7 +1318,7 @@ namespace MobileGL { { // MOBILEGL_QUIRK_SUBGROUP_PREFIX_SCAN "subgroup-prefix-scan-rewrite", - [] { return MG_Config::Features.SubgroupPrefixScanQuirk; }, + [](const CompileEnv& env) { return env.subgroupPrefixScanQuirk; }, [](const ShaderSourceQuirkContext& ctx) { // Qualcomm's Vulkan driver miscompiles the recognized float // InclusiveScan pattern for native subgroups wider than the @@ -1339,20 +1334,21 @@ namespace MobileGL { }, }; - void ApplyShaderSourceQuirks(ShaderStage stage, String& source) { - const auto& activeBackend = MG_Backend::pActiveBackendObject; - if (!activeBackend) { + void ApplyShaderSourceQuirks(const CompileEnv& env, ShaderStage stage, String& source) { + // No backend at capture time means no device to match a quirk against, + // and (as before) no quirk can fire - not even a forced one, because + // every Apply reads device parameters that do not exist yet. + if (!env.HasBackend()) { return; } - const auto& dynamicParameters = activeBackend->GetDynamicParameters(); const ShaderSourceQuirkContext quirkContext{ stage, - activeBackend->GetBackendType(), - dynamicParameters.GpuVendor, - dynamicParameters.SubgroupSize, + env.backend, + env.params.GpuVendor, + env.params.SubgroupSize, }; for (const ShaderSourceQuirk& quirk : kShaderSourceQuirks) { - const MG_Config::QuirkOverride quirkOverride = quirk.GetOverride(); + const MG_Config::QuirkOverride quirkOverride = quirk.GetOverride(env); if (quirkOverride == MG_Config::QuirkOverride::ForceOff) { continue; } @@ -1369,6 +1365,10 @@ namespace MobileGL { } // namespace void PreprocessShaderSource(ShaderStage stage, String& source) { + PreprocessShaderSource(stage, source, *GetCurrentCompileEnv()); + } + + void PreprocessShaderSource(ShaderStage stage, String& source, const CompileEnv& env) { // Normalize while the inspector's source span still refers to the untouched input. const ShaderLanguageInfo originalLanguage = InspectShaderLanguage(source); @@ -1395,7 +1395,7 @@ namespace MobileGL { // identifier that merely contained the word. The GLES fallback for devices without // the extension lives in the backend, where device capabilities are known. - FilterUnsupportedGpuShaderInt64(source); + FilterUnsupportedGpuShaderInt64(env, source); CoerceUniformBlockPackingToStd140(source); RenameBuiltinShadowingFunctions(source); @@ -1403,7 +1403,7 @@ namespace MobileGL { ModernizeLegacyGLSL(stage, source, afterVersion); InjectDepthRangeBuiltinShim(stage, source, afterVersion); - ApplyShaderSourceQuirks(stage, source); + ApplyShaderSourceQuirks(env, stage, source); } Bool RetargetLegacyVersionDirectiveTo460(String& source) { diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h index cd340254..d2027256 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderSourceProcessor.h @@ -9,6 +9,7 @@ #pragma once #include #include +#include namespace MobileGL { enum class ShaderProfile { @@ -19,6 +20,14 @@ namespace MobileGL { namespace MG_Util { namespace ShaderTranspiler { + // The whole source-rewriting pipeline. `env` is the compile-time snapshot of + // everything outside (stage, source) this reads - advertised extensions and the + // device-quirk inputs - so the transformation is a pure function of its three + // arguments and can run on a worker thread. + void PreprocessShaderSource(ShaderStage stage, String& source, const CompileEnv& env); + // Convenience overload that resolves the current context's env itself. GL thread + // only, and deliberately not used by the compile pipeline: it exists for the unit + // tests and diagnostics that drive the preprocessor standalone. void PreprocessShaderSource(ShaderStage stage, String& source); // Some desktop-captured compute shaders build a workgroup-wide linear prefix scan diff --git a/MobileGL/MG_Util/ShaderTranspiler/Types.h b/MobileGL/MG_Util/ShaderTranspiler/Types.h index 443b2102..4f0322d9 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/Types.h +++ b/MobileGL/MG_Util/ShaderTranspiler/Types.h @@ -8,6 +8,7 @@ #pragma once #include +#include namespace MobileGL { namespace MG_Util { @@ -25,6 +26,11 @@ namespace MobileGL { GLenum shaderType; StringView sourceStr; Flags flags; + // The compile-time backend snapshot the glslang resource limits come from. + // Null means "read them off the live backend object" - only legal on the GL + // thread, and only used by the standalone/test entry points. Non-owning: the + // env outlives the attrib (it is a per-context SharedPtr). + const CompileEnv* env = nullptr; }; struct ProgramAttrib {