From 8329ab4264d48036390a14586633645a0a5d763a Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 11:43:42 -0400 Subject: [PATCH] [Refactor] (ProgramState): answer the GL query surface from an owned reflection snapshot, not the live TProgram --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 7 +- .../MG_Impl/GLImpl/Program/GL_Program.cpp | 37 ++- .../GLState/ProgramState/ProgramLinkTask.cpp | 141 +++++++++ .../GLState/ProgramState/ProgramLinkTask.h | 3 + .../GLState/ProgramState/ProgramObject.h | 292 ++++++++++++------ .../GLState/ProgramState/ProgramSpirvTask.cpp | 23 +- 6 files changed, 377 insertions(+), 126 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index d046a0b9..191939e7 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -4670,12 +4670,11 @@ namespace MobileGL::MG_Backend::DirectGLES { const auto& name = stateProgramObject.GetUniformName(loc); if (name.empty()) continue; if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue; - const glslang::TType* type = stateProgramObject.GetUniformTType(loc); - if (type == nullptr) continue; - if (type->getQualifier().hasFormat()) { + const auto& type = stateProgramObject.GetUniformTypeFacts(loc); + if (type.hasFormat) { // Declared, and therefore left exactly as written - but a non-core spelling // still needs the extension directive to survive the ES compiler. - if (!IsCoreEsslLayoutFormat(type->getQualifier().getFormat())) { + if (!IsCoreEsslLayoutFormat(static_cast(type.layoutFormat))) { inputs.needsExtendedImageFormats = true; } continue; diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index 29090d8c..2882e7b1 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -21,6 +21,9 @@ #include namespace MobileGL::MG_Impl::GLImpl { + // The flattened uniform type these helpers used to take as a raw glslang::TType* + // pointing into the TProgram's pool allocator. See ProgramObject::TypeFacts. + using TypeFactsRef = const MG_State::GLState::ProgramObject::TypeFacts&; static GLint BoolToGLInt(bool value) { return value ? GL_TRUE : GL_FALSE; } @@ -223,14 +226,14 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } - GLint GetOpaqueUniformUnitLimit(const glslang::TType* type) { + GLint GetOpaqueUniformUnitLimit(const TypeFactsRef type) { const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); - if (type && type->isImage()) return dynamicParameters.MaxImageUnits; - if (type && type->isTexture()) return dynamicParameters.MaxCombinedTextureImageUnits; + if (type.isImage) return dynamicParameters.MaxImageUnits; + if (type.isTexture) return dynamicParameters.MaxCombinedTextureImageUnits; return 0; } - bool ValidateOpaqueUniformUnit(const char* functionName, const glslang::TType* type, GLint unit) { + bool ValidateOpaqueUniformUnit(const char* functionName, const TypeFactsRef type, GLint unit) { const GLint limit = GetOpaqueUniformUnitLimit(type); if (unit < 0 || unit >= limit) { MG_State::pGLContext->RecordError( @@ -856,10 +859,10 @@ namespace MobileGL::MG_Impl::GLImpl { // demotion makes a dmat4 a mat4 in the shader and a mat4-shaped slot here - but because it // is ROUTED differently: the caller's component-by-component EbtDouble branch has to widen // each float back to the queried type, and it undoes the same padding itself. - Bool TryGatherFloatMatrixColumns(const glslang::TType* ttype, const char* pBase, void* params) { - if (ttype == nullptr || !ttype->isMatrix() || ttype->getBasicType() == glslang::EbtDouble) return false; - const Int columns = ttype->getMatrixCols(); - const Int rows = ttype->getMatrixRows(); + Bool TryGatherFloatMatrixColumns(const TypeFactsRef ttype, const char* pBase, void* params) { + if (!ttype.isMatrix || ttype.isDouble) return false; + const Int columns = ttype.matrixCols; + const Int rows = ttype.matrixRows; for (Int column = 0; column < columns; ++column) { Memcpy(static_cast(params) + static_cast(column) * rows * sizeof(GLfloat), pBase + static_cast(column) * 4 * sizeof(GLfloat), rows * sizeof(GLfloat)); @@ -871,7 +874,7 @@ namespace MobileGL::MG_Impl::GLImpl { // everything except a float matrix, whose padded columns make it wider. The rule itself // lives on ProgramObject, because the pipeline composite's uniform refresh needs the same // one and two copies of a layout rule is one too many. - SizeT UniformStorageSpanInBytes(const glslang::TType* ttype, SizeT tightSize) { + SizeT UniformStorageSpanInBytes(const TypeFactsRef ttype, SizeT tightSize) { return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize); } @@ -904,7 +907,7 @@ namespace MobileGL::MG_Impl::GLImpl { auto offset = programObject->GetUniformOffset(location); auto size = programObject->GetUniformSizesInBytes(location); char* pUBO = (char*)programObject->MapUBO(); - auto* ttype = programObject->GetUniformTType(location); + const auto& ttype = programObject->GetUniformTypeFacts(location); const SizeT span = UniformStorageSpanInBytes(ttype, size); if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || offset + span > programObject->GetUBOSize()) { @@ -958,7 +961,7 @@ namespace MobileGL::MG_Impl::GLImpl { auto offset = programObject->GetUniformOffset(location); auto size = programObject->GetUniformSizesInBytes(location); char* pUBO = static_cast(programObject->MapUBO()); - auto* ttype = programObject->GetUniformTType(location); + const auto& ttype = programObject->GetUniformTypeFacts(location); const SizeT span = UniformStorageSpanInBytes(ttype, size); if (pUBO == nullptr || offset == MG_State::GLState::ProgramObject::kInvalidUniformOffset || offset + span > programObject->GetUBOSize()) { @@ -981,10 +984,10 @@ namespace MobileGL::MG_Impl::GLImpl { // conversion rules (7.6: round to nearest for the integer queries) apply; the value // widens back to the queried type, having lost precision at the glUniform*d that // stored it and not here. - if (ttype->getBasicType() == glslang::EbtDouble) { - const Int columns = ttype->isMatrix() ? ttype->getMatrixCols() : 1; - const Int rows = ttype->isMatrix() ? ttype->getMatrixRows() - : (ttype->isVector() ? ttype->getVectorSize() : 1); + if (ttype.isDouble) { + const Int columns = ttype.isMatrix ? ttype.matrixCols : 1; + const Int rows = ttype.isMatrix ? ttype.matrixRows + : (ttype.isVector ? ttype.vectorSize : 1); // std140 gives every matrix column its own 16-byte slot; a non-matrix is one // tightly packed run and never reaches the stride at all. const SizeT columnStride = 4 * sizeof(GLfloat); @@ -1191,8 +1194,8 @@ namespace MobileGL::MG_Impl::GLImpl { Memcpy(pUBO + offset + byteOffsetInsideUniform, value, writeSize); programObject.MarkUBOContentDirty(); } else { - auto* ttype = programObject.GetUniformTType(location); - if (!ttype->isTexture() && !ttype->isImage()) return; + const auto& ttype = programObject.GetUniformTypeFacts(location); + if (!ttype.isTexture && !ttype.isImage) return; if constexpr (!std::is_same_v, GLint> || ItemCount != 1) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index 3b871a05..bfead443 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -36,6 +36,69 @@ namespace { return std::min(backendLimit, capacity); } + // Everything the post-link query surface ever asks a glslang::TType, flattened into a + // POD. The list is closed and was audited call site by call site: nothing after the link + // walks a struct, a type name or the AST, so there is no recursion to mirror. + // + // Why it has to be flattened at all: TObjectReflection::type points into the TProgram's + // OWN TPoolAllocator (reflection.cpp clones each TType into it), so every one of these + // pointers dangles the moment the TProgram is released - and releasing it is exactly what + // lets a link be served from the L1 translation memo without a parse. + static MobileGL::MG_State::GLState::ProgramObject::TypeFacts MakeTypeFacts(const glslang::TType* type) { + MobileGL::MG_State::GLState::ProgramObject::TypeFacts facts; + if (type == nullptr) return facts; + facts.isArray = type->isArray(); + facts.isMatrix = type->isMatrix(); + facts.isVector = type->isVector(); + facts.isOpaque = type->isOpaque(); + facts.isTexture = type->isTexture(); + facts.isImage = type->isImage(); + facts.isDouble = type->getBasicType() == glslang::EbtDouble; + facts.isVoid = type->getBasicType() == glslang::EbtVoid; + facts.basicType = static_cast(type->getBasicType()); + // Stored RAW, exactly as glslang reports them (0 for a non-matrix, 1 for a scalar), + // because the callers already gate on isMatrix()/isVector() themselves. + facts.vectorSize = type->getVectorSize(); + facts.matrixCols = type->getMatrixCols(); + facts.matrixRows = type->getMatrixRows(); + const glslang::TQualifier& qualifier = type->getQualifier(); + facts.isBuffer = qualifier.storage == glslang::EvqBuffer; + facts.isPatch = qualifier.patch; + facts.hasIndex = qualifier.hasIndex(); + facts.layoutIndex = static_cast(qualifier.layoutIndex); + facts.hasFormat = qualifier.hasFormat(); + facts.layoutFormat = static_cast(qualifier.getFormat()); + facts.layoutMatrix = static_cast(qualifier.layoutMatrix); + return facts; + } + + // One glslang::TObjectReflection, flattened. Shared by uniforms, blocks, pipe inputs and + // pipe outputs, because glslang reflects all four as TObjectReflection. + static MobileGL::MG_State::GLState::ProgramObject::ResourceReflection MakeResourceReflection( + const glslang::TObjectReflection& object) { + MobileGL::MG_State::GLState::ProgramObject::ResourceReflection record; + record.name = object.name; + record.glDefineType = object.glDefineType; + record.offset = object.offset; + record.size = object.size; + record.index = object.index; + record.counterIndex = object.counterIndex; + record.arrayStride = object.arrayStride; + record.topLevelArraySize = object.topLevelArraySize; + record.topLevelArrayStride = object.topLevelArrayStride; + record.binding = object.getBinding(); + record.location = object.layoutLocation(); + record.stages = static_cast(object.stages); + record.type = MakeTypeFacts(object.getType()); + // GL_UNIFORM_SIZE / GL_ARRAY_SIZE, resolved here so no caller needs the TType: + // TObjectReflection::size carries the element count only for a NON-block array, so + // the sized-array outer count wins whenever it exists. + const glslang::TType* type = object.getType(); + record.arraySize = (type != nullptr && type->isSizedArray()) ? type->getOuterArraySize() + : (object.size < 1 ? 1 : object.size); + return record; + } + static MobileGL::String StripArrayElementSuffix(const MobileGL::String& name) { const MobileGL::SizeT bracket = name.find('['); return bracket == MobileGL::String::npos ? name : name.substr(0, bracket); @@ -497,6 +560,15 @@ namespace MobileGL::MG_State::GLState { spirvHandoff.reflection.uniformIndexInTProgram = artifacts.uniformIndexInTProgram; spirvHandoff.reflection.tProgramUniformIndexToGl = artifacts.tProgramUniformIndexToGl; spirvHandoff.reflection.maxUniformLocation = artifacts.maxUniformLocation; + // The owned reflection mirror, and the block index space its global-UBO test needs. + // BuildGlobalUboRouting reads BOTH - per-uniform array size, opaqueness, GL type and + // matrix shape, plus "is this a member of a GL-visible block". Leaving them out of the + // handoff is not a compile error, it is a SILENT one: every array collapses to a + // single element and every element past the first falls through to the fallback tail + // allocator (ProgramTest.NestedStructArrayUniformElementWrites catches exactly that). + spirvHandoff.reflection.uniformReflection = artifacts.uniformReflection; + spirvHandoff.reflection.blockReflection = artifacts.blockReflection; + spirvHandoff.reflection.tProgramBlockIndexToGl = artifacts.tProgramBlockIndexToGl; spirvHandoff.spirvCacheKey = BuildSpirvCacheKey(env); spirvHandoff.ready = true; MGLOG_D("ProgramObject %u: phase A done, %zu module(s) handed to the SPIR-V job", in.externalIndex, @@ -1032,9 +1104,78 @@ namespace MobileGL::MG_State::GLState { MGLOG_D("ProgramObject %u: Reflection - UBO[%d] name='%s' size=%u binding=%d", in.externalIndex, i, ubo.name.c_str(), ubo.size, ubo.getBinding()); } + + SnapshotGlslangReflection(); return true; } + // The last thing DoReflection does, and the thing that lets everything after it stop + // caring that a glslang::TProgram ever existed: copy every reflection record the GL query + // surface reads into LinkArtifacts' own owned tables. + // + // Indexed by TPROGRAM index throughout - the same space glUniformIndexToTProgram, + // tProgramUniformIndexToGl and uniformIndexInTProgram already speak - so the accessors + // that used to call program->getUniform(i) index uniformReflection[i] and are otherwise + // unchanged. + void ProgramLinkTask::SnapshotGlslangReflection() { + glslang::TProgram& program = *artifacts.program; + + // Blocks FIRST: a uniform's effective layoutMatrix is resolved against its owning + // block below, which needs the block records to already exist. + const Int blockCount = program.getNumUniformBlocks(); + artifacts.blockReflection.clear(); + artifacts.blockReflection.reserve(static_cast(blockCount)); + for (Int i = 0; i < blockCount; ++i) { + artifacts.blockReflection.push_back(MakeResourceReflection(program.getUniformBlock(i))); + } + + const Int uniformCount = program.getNumUniformVariables(); + artifacts.uniformReflection.clear(); + artifacts.uniformReflection.reserve(static_cast(uniformCount)); + artifacts.uniformIndexByName.clear(); + artifacts.uniformIndexByName.reserve(static_cast(uniformCount)); + for (Int i = 0; i < uniformCount; ++i) { + ProgramObject::UniformReflection record = MakeResourceReflection(program.getUniform(i)); + // A block-level layout(row_major)/(column_major) that the member did not inherit + // in its own qualifier. Resolved once HERE rather than at every GL_UNIFORM_* query, + // which is what the getUniformBlock() fallback in the old accessors was doing. + if (record.type.layoutMatrix == static_cast(glslang::ElmNone) && record.index >= 0 && + record.index < static_cast(artifacts.blockReflection.size())) { + record.type.layoutMatrix = artifacts.blockReflection[record.index].type.layoutMatrix; + } + // Keyed on the REFLECTED name and on uniforms only. That is deliberate and is the + // filtered semantics the old code hand-rolled: glslang's TReflection::nameToIndex + // also holds block and function entries, which is exactly why every + // getUniformIndex() call site re-checked getUniform(idx).name == name afterwards. + // First writer wins, so a duplicated name resolves the way a forward scan would. + artifacts.uniformIndexByName.emplace(record.name, i); + artifacts.uniformReflection.push_back(Move(record)); + } + + const Int pipeInputCount = program.getNumPipeInputs(); + artifacts.pipeInputReflection.clear(); + artifacts.pipeInputReflection.reserve(static_cast(pipeInputCount)); + for (Int i = 0; i < pipeInputCount; ++i) { + artifacts.pipeInputReflection.push_back(MakeResourceReflection(program.getPipeInput(i))); + } + + const Int pipeOutputCount = program.getNumPipeOutputs(); + artifacts.pipeOutputReflection.clear(); + artifacts.pipeOutputReflection.reserve(static_cast(pipeOutputCount)); + for (Int i = 0; i < pipeOutputCount; ++i) { + artifacts.pipeOutputReflection.push_back(MakeResourceReflection(program.getPipeOutput(i))); + } + + artifacts.atomicCounterCount = program.getNumAtomicCounters(); + for (Uint dim = 0; dim < 3u; ++dim) { + artifacts.computeLocalSize[dim] = program.getLocalSize(static_cast(dim)); + } + MGLOG_D("ProgramObject %u: Reflection - snapshot: %zu uniform(s), %zu block(s), %zu input(s), " + "%zu output(s)", + in.externalIndex, artifacts.uniformReflection.size(), artifacts.blockReflection.size(), + artifacts.pipeInputReflection.size(), artifacts.pipeOutputReflection.size()); + } + Bool ProgramLinkTask::ValidateFragmentOutputLocations() { if (!artifacts.program) return false; // The pipe-output list is the output interface of the program's LAST stage. Only a diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h index d60ae273..96f27b8f 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.h @@ -165,6 +165,9 @@ namespace MobileGL::MG_State::GLState { MG_Util::ShaderTranspiler::TranslationCacheKey BuildSpirvCacheKey( const MG_Util::ShaderTranspiler::CompileEnv& env) const; Bool DoReflection(const MG_Util::ShaderTranspiler::CompileEnv& env); + // Copies every reflection record the GL query surface reads out of the glslang + // TProgram into LinkArtifacts own owned tables. Runs at the tail of DoReflection. + void SnapshotGlslangReflection(); Bool ValidateFragmentOutputLocations(); Bool ResolveTransformFeedbackVaryings(); void ResolveGsTriangleStripCapture(const glslang::TIntermediate* captureIntermediate); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index a6cec261..aadac043 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -24,6 +24,68 @@ namespace MobileGL::MG_State::GLState { class ProgramObject { public: + // Everything the query surface ever asked a glslang::TType, flattened. Twenty + // predicates, no recursion: nothing post-link ever walks a struct, a type name or the + // AST, so a POD covers the whole surface exactly. + struct TypeFacts { + Bool isArray = false; + Bool isMatrix = false; + Bool isVector = false; + Bool isOpaque = false; + Bool isTexture = false; + Bool isImage = false; + Bool isDouble = false; // getBasicType() == EbtDouble + Bool isVoid = false; // getBasicType() == EbtVoid (hidden block members) + Bool isBuffer = false; // getQualifier().storage == EvqBuffer + Bool isPatch = false; // getQualifier().patch + Bool hasIndex = false; // getQualifier().hasIndex() + Bool hasFormat = false; // getQualifier().hasFormat() + Int vectorSize = 0; + Int matrixCols = 0; + Int matrixRows = 0; + Int layoutIndex = 0; // getQualifier().layoutIndex + Uint layoutFormat = 0; // getQualifier().getFormat() + // glslang::TLayoutMatrix, widened. For a uniform this is already RESOLVED against + // the owning block's qualifier, so the getUniformBlock() fallback the old + // accessors carried is gone. + Int layoutMatrix = 0; + // glslang::TBasicType, widened - ApplyUniformInitialValues and the typed + // glGetUniform* paths compare against a handful of enumerators. + Int basicType = 0; + }; + + // One glslang::TObjectReflection, flattened. Used for uniforms, blocks, pipe inputs + // and pipe outputs alike, because glslang reflects all four as TObjectReflection. + struct ResourceReflection { + String name; + GLenum glDefineType = 0; + Int offset = -1; + // TObjectReflection::size, RAW. For a uniform prefer `arraySize` below, which is + // the resolved GL_UNIFORM_SIZE answer. + Int size = 0; + // TObjectReflection::index - for a uniform, the TPROGRAM block index owning it + // (-1 for a default-block one; translate with GlBlockIndexFromTProgram). + Int index = -1; + Int counterIndex = -1; + Int arrayStride = 0; + Int topLevelArraySize = 0; + Int topLevelArrayStride = 0; + Int binding = -1; + Int location = -1; // layoutLocation() + // EShLanguageMask of the stages that reference it; 0 means "declared but read by + // nobody", which is what the dead-default-block-uniform filter tests. + Uint32 stages = 0; + // GL_UNIFORM_SIZE / GL_ARRAY_SIZE, already resolved through the + // isSizedArray()/getOuterArraySize()/size fallback. + GLint arraySize = 1; + TypeFacts type; + }; + + using UniformReflection = ResourceReflection; + using BlockReflection = ResourceReflection; + using PipeInputReflection = ResourceReflection; + using PipeOutputReflection = ResourceReflection; + ProgramObject(Uint externalIndex) : m_externalIndex(externalIndex), m_lifetimeId(AllocateLifetimeId()) {} // Cancel-not-join, exactly like ~ShaderObject: the link job owns its inputs, so an // in-flight link whose program just went away is safe to abandon where it stands. @@ -134,8 +196,7 @@ namespace MobileGL::MG_State::GLState { 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 = Artifacts().program->getUniform(index).getType(); - if (type == nullptr || !type->isArray()) return -1; + if (!UniformAt(index).type.isArray) return -1; if (static_cast(element) >= GetUniformArraySizeByTIndex(index)) return -1; const Int location = base + (Int)element; if (!UniformLocationsAliasSameUniform(base, location)) return -1; @@ -175,44 +236,35 @@ namespace MobileGL::MG_State::GLState { } Int GetActiveUniformIndex(const String& name) const { - const Int tProgramCount = static_cast(Artifacts().tProgramUniformIndexToGl.size()); - const Int uniformIndex = Artifacts().program->getUniformIndex(name.c_str()); - if (uniformIndex >= 0 && uniformIndex < tProgramCount && - Artifacts().program->getUniform(uniformIndex).name == name) { - return GlUniformIndexFromTProgram(uniformIndex); + // uniformIndexByName is keyed by the REFLECTED name, so a lookup that hits is + // already the exact-match the old code re-verified with a string compare after + // glslang's getUniformIndex(); a lookup that misses needs no bounds check. + const auto& byName = Artifacts().uniformIndexByName; + if (const auto direct = byName.find(name); direct != byName.end()) { + return GlUniformIndexFromTProgram(direct->second); } // Reflection stores an array uniform under "arr[0]"; accept the bare "arr" // spelling too. The reverse ("arr[0]" against a bare "arr" entry) is kept for // robustness against non-suffixed reflection entries. if (!name.empty() && name.back() != ']') { - const String suffixedName = name + "[0]"; - const Int suffixedIndex = Artifacts().program->getUniformIndex(suffixedName.c_str()); - if (suffixedIndex >= 0 && suffixedIndex < tProgramCount && - Artifacts().program->getUniform(suffixedIndex).name == suffixedName) { - return GlUniformIndexFromTProgram(suffixedIndex); - } - return -1; + const auto suffixed = byName.find(name + "[0]"); + return suffixed != byName.end() ? GlUniformIndexFromTProgram(suffixed->second) : -1; } 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 = Artifacts().program->getUniformIndex(baseName.c_str()); - if (baseIndex < 0 || baseIndex >= tProgramCount) return -1; - return Artifacts().program->getUniform(baseIndex).name == baseName ? GlUniformIndexFromTProgram(baseIndex) - : -1; + const auto base = byName.find(name.substr(0, name.length() - 3)); + return base != byName.end() ? GlUniformIndexFromTProgram(base->second) : -1; } Bool IsValidUniformLocation(Int location) const { return IsValidUniformLocation(Artifacts(), location); } GLenum GetUniformType(Uint location) const { - auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]); - return uniform.glDefineType; + return UniformAt(Artifacts().uniformIndexInTProgram[location]).glDefineType; } GLenum GetActiveUniformType(Uint index) const { - auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); - return uniform.glDefineType; + return UniformAt(TProgramUniformIndex(index)).glDefineType; } // Number of active array elements (GL_UNIFORM_SIZE / GL_ARRAY_SIZE); 1 for a non-array. @@ -229,16 +281,15 @@ namespace MobileGL::MG_State::GLState { } Int GetActiveUniformBlockIndex(Uint index) const { - auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); // Members of the synthesized global UBO are default-block uniforms to GL: -1. - return GlBlockIndexFromTProgram(uniform.index); + return GlBlockIndexFromTProgram(UniformAt(TProgramUniformIndex(index)).index); } // GL_UNIFORM_OFFSET: byte offset within the owning named block; -1 for a default-block // 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 = Artifacts().program->getUniform(TProgramUniformIndex(index)); + const auto& uniform = UniformAt(TProgramUniformIndex(index)); if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1; return uniform.offset; } @@ -252,13 +303,12 @@ 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 = Artifacts().program->getUniform(TProgramUniformIndex(index)); + const auto& uniform = UniformAt(TProgramUniformIndex(index)); if (GlBlockIndexFromTProgram(uniform.index) < 0) return -1; - const glslang::TType* type = uniform.getType(); - if (type == nullptr || !type->isArray()) return 0; - if (type->isMatrix()) { + if (!uniform.type.isArray) return 0; + if (uniform.type.isMatrix) { const bool rowMajor = GetActiveUniformIsRowMajor(index) != 0; - const int vectors = rowMajor ? type->getMatrixRows() : type->getMatrixCols(); + const int vectors = rowMajor ? uniform.type.matrixRows : uniform.type.matrixCols; return GetActiveUniformMatrixStride(index) * vectors; } return 16; // scalars and vectors: std140 rounds the element stride up to a vec4 @@ -272,15 +322,12 @@ 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 = Artifacts().program->getUniform(TProgramUniformIndex(index)); + const auto& uniform = UniformAt(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 = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix; - } - return (layoutMatrix == glslang::ElmRowMajor) ? 1 : 0; + if (!uniform.type.isMatrix) return 0; + // layoutMatrix is already resolved against the owning block's qualifier at + // snapshot time, so the getUniformBlock() fallback this used to carry is gone. + return (uniform.type.layoutMatrix == static_cast(glslang::ElmRowMajor)) ? 1 : 0; } // GL_UNIFORM_MATRIX_STRIDE: byte stride between columns (col-major) / rows (row-major) of a @@ -290,16 +337,11 @@ 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 = Artifacts().program->getUniform(TProgramUniformIndex(index)); + const auto& uniform = UniformAt(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 = Artifacts().program->getUniformBlock(uniform.index).getType()->getQualifier().layoutMatrix; - } - const bool rowMajor = (layoutMatrix == glslang::ElmRowMajor); - const int strideVectorComponents = rowMajor ? type->getMatrixCols() : type->getMatrixRows(); + if (!uniform.type.isMatrix) return 0; + const bool rowMajor = (uniform.type.layoutMatrix == static_cast(glslang::ElmRowMajor)); + const int strideVectorComponents = rowMajor ? uniform.type.matrixCols : uniform.type.matrixRows; constexpr int scalarSize = 4; // GL 3.3 core uniform matrices are float const int vectorAlignment = (strideVectorComponents <= 1) ? scalarSize : (strideVectorComponents == 2) ? 2 * scalarSize @@ -307,21 +349,39 @@ namespace MobileGL::MG_State::GLState { return (vectorAlignment + 15) & ~15; // std140 round-up to a vec4 } - const glslang::TType* GetUniformTType(Uint location) const { - auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]); - return uniform.getType(); + // The flattened type of the uniform at `location`. This is what replaced + // GetUniformTType(): the same information, owned by the program instead of by a + // glslang pool, so it stays valid for a link served from the L1 translation memo. + const TypeFacts& GetUniformTypeFacts(Uint location) const { + return UniformAt(Artifacts().uniformIndexInTProgram[location]).type; } - Bool IsUniformOpaqueAtLocation(Uint location) const { return GetUniformTType(location)->isOpaque(); } + // Replaces GetUniformTType(), which used to hand a raw glslang::TType* - into a + // pool the program no longer necessarily owns - out to the DirectGLES image-format + // bake. These are the only three things any caller ever read off it. + Bool UniformHasDeclaredImageFormat(Uint location) const { + return UniformAt(Artifacts().uniformIndexInTProgram[location]).type.hasFormat; + } + Uint GetUniformDeclaredImageFormat(Uint location) const { + return UniformAt(Artifacts().uniformIndexInTProgram[location]).type.layoutFormat; + } + // Matrix column count, 0 for a non-matrix. The global-UBO fallback allocator sizes a + // matrix slot from it. + Int GetUniformMatrixColumns(Uint location) const { + const auto& uniform = UniformAt(Artifacts().uniformIndexInTProgram[location]); + return uniform.type.isMatrix ? uniform.type.matrixCols : 0; + } + + Bool IsUniformOpaqueAtLocation(Uint location) const { + return UniformAt(Artifacts().uniformIndexInTProgram[location]).type.isOpaque; + } const String& GetUniformName(Uint location) const { - auto& uniform = Artifacts().program->getUniform(Artifacts().uniformIndexInTProgram[location]); - return uniform.name; + return UniformAt(Artifacts().uniformIndexInTProgram[location]).name; } const String& GetActiveUniformName(Uint index) const { - auto& uniform = Artifacts().program->getUniform(TProgramUniformIndex(index)); - return uniform.name; + return UniformAt(TProgramUniformIndex(index)).name; } // Sentinel for a uniform location without global-UBO backing storage (should not // survive linking: GenerateBinary falls back to tail-allocated scratch storage). @@ -355,17 +415,17 @@ namespace MobileGL::MG_State::GLState { // would, half its GL type size, and a `dmat4` is padded like any other matrix. Anything // reading or writing a whole uniform's storage - a bounds check, a copy between two // programs' shadows - wants this rather than GetUniformSizesInBytes. - static SizeT UniformStorageSpanInBytes(const glslang::TType* type, SizeT tightSize) { - if (type != nullptr && type->isMatrix()) { - return static_cast(type->getMatrixCols()) * 4 * sizeof(Float); + static SizeT UniformStorageSpanInBytes(const TypeFacts& type, SizeT tightSize) { + if (type.isMatrix) { + return static_cast(type.matrixCols) * 4 * sizeof(Float); } - if (type != nullptr && type->getBasicType() == glslang::EbtDouble) { + if (type.isDouble) { return tightSize / 2; } return tightSize; } SizeT GetUniformStorageSpanInBytes(Uint location) const { - return UniformStorageSpanInBytes(GetUniformTType(location), GetUniformSizesInBytes(location)); + return UniformStorageSpanInBytes(GetUniformTypeFacts(location), GetUniformSizesInBytes(location)); } // ---- "written since link": the per-location dirty set the pipeline composite mirrors from ---- @@ -476,14 +536,14 @@ namespace MobileGL::MG_State::GLState { return mask; } Uint32 GetActiveFragmentOutputLocationMask() const { - if (!Artifacts().program) { + if (Artifacts().pipeOutputReflection.empty()) { return 0; } Uint32 mask = 0; - const Int outputCount = Artifacts().program->getNumPipeOutputs(); + const Int outputCount = static_cast(Artifacts().pipeOutputReflection.size()); for (Int index = 0; index < outputCount; ++index) { - const Int location = static_cast(Artifacts().program->getPipeOutput(index).layoutLocation()); + const Int location = Artifacts().pipeOutputReflection[index].location; if (location >= 0 && location < 32) { mask |= (1u << location); } @@ -491,38 +551,34 @@ namespace MobileGL::MG_State::GLState { return mask; } Int GetActiveFragmentOutputCount() const { - return Artifacts().program ? Artifacts().program->getNumPipeOutputs() : 0; + return static_cast(Artifacts().pipeOutputReflection.size()); } const String& GetActiveFragmentOutputName(Uint index) const { - MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputName: program is null"); - MOBILEGL_ASSERT(index < static_cast(Artifacts().program->getNumPipeOutputs()), + MOBILEGL_ASSERT(index < static_cast(Artifacts().pipeOutputReflection.size()), "ProgramObject::GetActiveFragmentOutputName: index=%u out of range", index); - return Artifacts().program->getPipeOutput(static_cast(index)).name; + return Artifacts().pipeOutputReflection[index].name; } Int GetFragmentOutputLocation(Uint index) const { - MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputLocation: program is null"); - MOBILEGL_ASSERT(index < static_cast(Artifacts().program->getNumPipeOutputs()), + MOBILEGL_ASSERT(index < static_cast(Artifacts().pipeOutputReflection.size()), "ProgramObject::GetFragmentOutputLocation: index=%u out of range", index); - return static_cast(Artifacts().program->getPipeOutput(static_cast(index)).layoutLocation()); + return Artifacts().pipeOutputReflection[index].location; } GLint GetActiveFragmentOutputArraySize(Uint index) const { - MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetActiveFragmentOutputArraySize: program is null"); - MOBILEGL_ASSERT(index < static_cast(Artifacts().program->getNumPipeOutputs()), + MOBILEGL_ASSERT(index < static_cast(Artifacts().pipeOutputReflection.size()), "ProgramObject::GetActiveFragmentOutputArraySize: index=%u out of range", index); - return Artifacts().program->getPipeOutput(static_cast(index)).size; + return Artifacts().pipeOutputReflection[index].size; } GLenum GetFragmentOutputType(Uint index) const { - MOBILEGL_ASSERT(Artifacts().program != nullptr, "ProgramObject::GetFragmentOutputType: program is null"); - MOBILEGL_ASSERT(index < static_cast(Artifacts().program->getNumPipeOutputs()), + MOBILEGL_ASSERT(index < static_cast(Artifacts().pipeOutputReflection.size()), "ProgramObject::GetFragmentOutputType: index=%u out of range", index); - return Artifacts().program->getPipeOutput(static_cast(index)).glDefineType; + return Artifacts().pipeOutputReflection[index].glDefineType; } 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; } + GLenum GetActiveAttribType(Uint index) const { return Artifacts().pipeInputReflection[index].glDefineType; } + GLint GetActiveAttribArraySize(Uint index) const { return Artifacts().pipeInputReflection[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). @@ -534,7 +590,7 @@ namespace MobileGL::MG_State::GLState { return name; } const String& GetActiveAttribName(Uint index) const { - return NormalizeBuiltinPipeInputName(Artifacts().program->getPipeInput(static_cast(index)).name); + return NormalizeBuiltinPipeInputName(Artifacts().pipeInputReflection[index].name); } // PHASE B, all three (see EnsureSpirvJoined): the shadow buffer's layout is decided // by the OPTIMIZED SPIR-V, so it does not exist until the SPIR-V job has settled - and @@ -627,7 +683,7 @@ namespace MobileGL::MG_State::GLState { // which means the change is only honoured by regenerating the program. That // regeneration is gated on link-shaped versions, so without a counter that moves // here the new unit would never reach the driver. - if (const glslang::TType* type = GetUniformTType(location); type != nullptr && type->isImage()) { + if (GetUniformTypeFacts(location).isImage) { ++m_imageUnitVersion; } } @@ -703,19 +759,16 @@ namespace MobileGL::MG_State::GLState { // this after a failed glGetAttribLocation, and reached it as soon as the CopyTexImage2D // throw ahead of it stopped killing the run first. Int GetActiveAtomicCounterCount() const { - const auto& program = Artifacts().program; - return program ? program->getNumAtomicCounters() : 0; + return Artifacts().atomicCounterCount; } Int GetActiveAttributesCount() const { - const auto& program = Artifacts().program; - return program ? program->getNumPipeInputs() : 0; + return static_cast(Artifacts().pipeInputReflection.size()); } // 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(Artifacts().glBlockIndexToTProgram.size()); } GLuint GetComputeLocalSize(Uint dim) const { - const auto& program = Artifacts().program; - return program ? program->getLocalSize(static_cast(dim)) : 0; + return dim < 3u ? Artifacts().computeLocalSize[dim] : 0u; } Int GetActiveAttributesMaxLength() const { return Artifacts().attribInNameMaxLength; } Int GetActiveUniformBlocksMaxNameLength() const { return Artifacts().uniformBlockNameMaxLength; } @@ -739,11 +792,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 (Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]).size + 15u) & ~15u; + return (static_cast(BlockAt(Artifacts().glBlockIndexToTProgram[index]).size) + 15u) & ~15u; } const String& GetUniformBlockName(Uint index) const { - auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]); + const auto& ubo = BlockAt(Artifacts().glBlockIndexToTProgram[index]); return ubo.name; } @@ -774,7 +827,7 @@ namespace MobileGL::MG_State::GLState { } Bool IsUniformBlockReferencedByStage(Uint index, EShLanguage stage) const { - const auto& ubo = Artifacts().program->getUniformBlock(Artifacts().glBlockIndexToTProgram[index]); + const auto& ubo = BlockAt(Artifacts().glBlockIndexToTProgram[index]); const auto stageMask = static_cast(1 << stage); return (ubo.stages & stageMask) != 0; } @@ -909,9 +962,46 @@ namespace MobileGL::MG_State::GLState { // 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. + // ---- the owned mirror of glslang's reflection ---- + // + // WHY THIS EXISTS. Every GL query about a linked program used to be answered by + // asking the live glslang::TProgram - program->getUniform(i).getType()->isMatrix() + // and friends. That made the TProgram part of the program's PERMANENT state, which + // in turn made the whole front end (parse + link) unskippable: the L1 shader + // translation memo could hand back the SPIR-V but the reflection still had to be + // rebuilt from a freshly parsed AST. + // + // These three tables are a snapshot of everything the query surface ever reads off + // the TProgram, in PLAIN OWNED VALUES - no TType*, no TString, nothing pointing into + // a glslang pool. Taken once at the tail of DoReflection (SnapshotGlslangReflection), + // they are copyable, immutable after the link, and safe to memoize and share between + // ProgramObjects and threads. Once they are filled, `program` is dead weight to + // everything except DoReflection itself. + // + // INDEXED BY TPROGRAM INDEX, deliberately: that is the space uniformIndexInTProgram, + // glUniformIndexToTProgram and tProgramUniformIndexToGl already speak, so every + // accessor that used to call program->getUniform(i) indexes uniformReflection[i] + // instead, unchanged in every other respect. + struct LinkArtifacts { + // Live only between LinkProgram() and the end of DoReflection. Everything after + // that reads the owned mirror below; a link served from the L1 memo never + // constructs one at all, so this is null for such a program and MUST NOT be + // dereferenced outside DoReflection. SharedPtr program; + // The owned reflection snapshot. Indexed by TProgram index; see the structs above. + Vector uniformReflection; + Vector blockReflection; + Vector pipeInputReflection; + Vector pipeOutputReflection; + // Program-level scalars glslang answers off the linked intermediates. + Int atomicCounterCount = 0; + Array computeLocalSize{}; + // Replaces program->getUniformIndex(name). Maps the reflected name to its + // TProgram uniform index. + UnorderedMap uniformIndexByName; + // Attributes (Vertex in) Vector attribs; Vector attribTypes; @@ -1057,12 +1147,24 @@ namespace MobileGL::MG_State::GLState { // for both. GL 3.3 core uniforms are always sized. Takes a TProgram uniform index (the space // the artifacts' uniformIndexInTProgram stores). static GLint GetUniformArraySizeByTIndex(const LinkArtifacts& artifacts, Int tIndex) { - const auto& uniform = artifacts.program->getUniform(tIndex); - const glslang::TType* type = uniform.getType(); - if (type != nullptr && type->isSizedArray()) { - return type->getOuterArraySize(); + return UniformAtIn(artifacts, tIndex).arraySize; + } + + // Bounds-checked mirror lookup. Out of range yields a default-constructed entry + // rather than UB, which is the same shape the phase-B getters use: a program whose + // reflection is missing must stay answerable, not crash the query surface. + static const UniformReflection& UniformAtIn(const LinkArtifacts& artifacts, Int tIndex) { + static const UniformReflection kEmpty; + if (tIndex < 0 || static_cast(tIndex) >= artifacts.uniformReflection.size()) return kEmpty; + return artifacts.uniformReflection[tIndex]; + } + const UniformReflection& UniformAt(Int tIndex) const { return UniformAtIn(Artifacts(), tIndex); } + const BlockReflection& BlockAt(Int tBlockIndex) const { + static const BlockReflection kEmpty; + if (tBlockIndex < 0 || static_cast(tBlockIndex) >= Artifacts().blockReflection.size()) { + return kEmpty; } - return uniform.size < 1 ? 1 : uniform.size; + return Artifacts().blockReflection[tBlockIndex]; } // Blocks until a pending link has published its artifacts. Public because a few call diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp index a0ff57a2..13b00fbb 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramSpirvTask.cpp @@ -329,22 +329,25 @@ namespace MobileGL::MG_State::GLState { for (Uint location = 0; location <= reflection.maxUniformLocation; ++location) { if (artifacts.uniformOffsets[location] != ProgramObject::kInvalidUniformOffset) continue; if (!ProgramObject::IsValidUniformLocation(reflection, static_cast(location))) continue; - const auto& uniform = reflection.program->getUniform(reflection.uniformIndexInTProgram[location]); - const glslang::TType* type = uniform.getType(); - if (type != nullptr && type->isOpaque()) continue; - if (uniform.index >= 0 && uniform.index < reflection.program->getNumUniformBlocks() && - std::strstr(reflection.program->getUniformBlock(uniform.index).name.c_str(), - MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) == nullptr) { - // Member of a named uniform block: not settable through glUniform*, so it - // needs no global-UBO shadow storage. + const auto& uniform = + ProgramObject::UniformAtIn(reflection, reflection.uniformIndexInTProgram[location]); + if (uniform.type.isOpaque) continue; + // Member of a named uniform block: not settable through glUniform*, so it needs + // no global-UBO shadow storage. tProgramBlockIndexToGl[i] >= 0 means block i is + // GL-visible, i.e. NOT the synthesized MGL_GLOBAL_UBO - which is exactly what the + // strstr(GLOBAL_UBO_NAME) test this replaced was asking, without needing the + // TProgram to spell the block name. + if (uniform.index >= 0 && + uniform.index < static_cast(reflection.tProgramBlockIndexToGl.size()) && + reflection.tProgramBlockIndexToGl[uniform.index] >= 0) { continue; } // std140-style slot: the matrix upload paths write column vectors at // 16-byte strides, so a matrix slot must cover cols * 16 bytes. SizeT slotSize = MG_Util::GetGLTypeSize(uniform.glDefineType); - if (type != nullptr && type->isMatrix()) { - slotSize = static_cast(type->getMatrixCols()) * 16u; + if (uniform.type.isMatrix) { + slotSize = static_cast(uniform.type.matrixCols) * 16u; } slotSize = (slotSize + 15u) & ~static_cast(15u); const SizeT slotOffset = (artifacts.globalUboScratch.size() + 15u) & ~static_cast(15u);