From 33c2715912fed9287d64f412aa7b63160610a176 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Tue, 11 Aug 2026 20:53:42 -0400 Subject: [PATCH] [Fix] (MG_State, MG_Util): a default-block uniform starts at its declared initializer instead of zero --- .../GLState/ProgramState/ProgramLinkTask.cpp | 23 +++++ .../GLState/ProgramState/ProgramObject.cpp | 91 ++++++++++++++++++- .../GLState/ProgramState/ProgramObject.h | 11 +++ .../glslang/MachineIndependent/ParseHelper.h | 1 + .../MachineIndependent/localintermediate.h | 27 ++++++ 5 files changed, 151 insertions(+), 2 deletions(-) diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index ebb0be57..d0cd328e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -301,6 +301,29 @@ namespace MobileGL::MG_State::GLState { Vector> shaders; if (!ConsumeShaders(shaders)) return; + // Harvest the declared default-block uniform initializers before the TShaders are + // handed to the linker. They come from the parse itself (glslang folds the constant + // and hands it over instead of dropping it), not from a lexical scan, so an + // expression like vec3(10, 20, 30) or int[](1, 2, 3) is already evaluated. + // + // Stage order decides a tie. GLSL requires a uniform declared in several stages to be + // declared identically, initializer included, so a conflict is a malformed program; + // taking the first stage's value keeps a link that other implementations accept from + // failing here, and both stages agree in every well-formed one. + for (const auto& shader : shaders) { + const glslang::TIntermediate* intermediate = shader ? shader->getIntermediate() : nullptr; + if (intermediate == nullptr) continue; + for (const auto& initializer : intermediate->getUniformInitializers()) { + const auto known = std::find_if(artifacts.uniformInitialValues.begin(), + artifacts.uniformInitialValues.end(), + [&initializer](const auto& existing) { + return existing.name == initializer.name; + }); + if (known != artifacts.uniformInitialValues.end()) continue; + artifacts.uniformInitialValues.push_back(initializer); + } + } + // Merge the shaders' lexically extracted explicit uniform locations. The same // uniform declared in several stages must agree on its location (config-A glslang // enforced this at mapIO; the relaxed parse no longer sees the qualifiers). diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 4a54eda9..d8750824 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -90,8 +90,11 @@ namespace MobileGL::MG_State::GLState { // A node that settled as Cancelled published nothing, so m_spirv stays empty with // spirvStatus false: linked, queryable, not drawable. Nothing to repair. - // Before the version bump, and before any caller can read the shadow: the writes the - // application made while the layout did not exist yet. + // Order matters, and it is the GL order. The shadow arrives zero-filled; the shaders' + // declared uniform initializers are what it should actually start from, and only then + // do the application's own writes - the ones it made while the layout did not exist + // yet - land on top. Seeding after the replay would clobber them. + ApplyUniformInitialValues(); ReplayBufferedUniformWrites(); // The THIRD version bump of this link (enqueue, phase-A publish, phase-B publish), and @@ -126,6 +129,89 @@ namespace MobileGL::MG_State::GLState { return true; } + // "uniform vec3 v = vec3(10, 20, 30);" - legal desktop GLSL since 1.20, and the value is + // what the uniform reads until glUniform* replaces it (and again after every relink). + // MobileGL parses with Vulkan-relaxed rules, which sweep default-block uniforms into + // MGL_GLOBAL_UBO; a block member cannot carry an initializer in SPIR-V, so glslang hands + // the folded constants over as a side-channel (TIntermediate::getUniformInitializers) and + // this is where they are honoured. Without it every such uniform silently read zero - + // which is what half of KHR-GL43.shader_storage_buffer_object was actually failing on. + // + // Writes go straight into the shadow rather than through glUniform*: this runs INSIDE the + // phase-B publish, so re-entering the join gate is not available, and the location space + // reflection assigns (one location per array element) is all that is needed. + void ProgramObject::ApplyUniformInitialValues() const { + const auto& initializers = m_artifacts.uniformInitialValues; + if (initializers.empty()) return; + if (m_spirv.globalUboScratch.empty() || m_spirv.uniformOffsets.empty()) { + // Phase B published no shadow (cancelled, or superseded by a relink). The program + // is not drawable; there is nowhere for these to land. + return; + } + + Uint8* const scratch = m_spirv.globalUboScratch.data(); + const SizeT uboSize = m_spirv.globalUboScratch.size(); + + for (const auto& init : initializers) { + // Scalars per array ELEMENT. A matrix element carries cols * rows of them, laid + // out column by column - which is also the order glslang folded them in. + const Int columns = init.matrixCols; + const Int rows = init.matrixRows; + const Int componentsPerElement = columns > 0 ? columns * rows : init.vectorSize; + const Int elements = init.arraySize; + if (componentsPerElement <= 0 || elements <= 0) continue; + + const Bool isFloat = init.basicType == glslang::EbtFloat || init.basicType == glslang::EbtFloat16; + const Bool isInt = init.basicType == glslang::EbtInt || init.basicType == glslang::EbtUint || + init.basicType == glslang::EbtBool; + // Anything else (fp64, 64-bit integers) has no 32-bit shadow encoding here, and a + // half-written uniform is worse than an untouched one. + if (!isFloat && !isInt) continue; + const SizeT provided = isFloat ? init.floatValues.size() : init.intValues.size(); + if (provided < static_cast(componentsPerElement) * static_cast(elements)) continue; + + const Int baseLocation = GetUniformLocation(init.name); + if (baseLocation < 0) continue; // optimized away, or not a default-block uniform + + for (Int element = 0; element < elements; ++element) { + const Int location = baseLocation + element; + if (element > 0 && !UniformLocationsAliasSameUniform(baseLocation, location)) break; + if (!IsValidUniformLocation(location)) break; + const Uint offset = GetUniformOffset(static_cast(location)); + if (offset == kInvalidUniformOffset) continue; + + // std140 pads every column of a float matrix out to a vec4, so the columns of + // a mat3 are 16 bytes apart even though each carries 12. The slot's own span + // states the stride the rest of the pipeline agreed on rather than guessing it. + const SizeT slotSpan = GetUniformStorageSpanInBytes(static_cast(location)); + const SizeT columnStride = + columns > 0 ? slotSpan / static_cast(columns) : slotSpan; + const Int componentsPerColumn = columns > 0 ? rows : componentsPerElement; + const Int columnCount = columns > 0 ? columns : 1; + + for (Int column = 0; column < columnCount; ++column) { + const SizeT byteOffset = static_cast(offset) + static_cast(column) * columnStride; + const SizeT writeSize = static_cast(componentsPerColumn) * sizeof(Uint32); + if (byteOffset + writeSize > uboSize) break; + const SizeT firstComponent = static_cast(element) * componentsPerElement + + static_cast(column) * componentsPerColumn; + for (Int component = 0; component < componentsPerColumn; ++component) { + const SizeT source = firstComponent + static_cast(component); + Uint8* const destination = scratch + byteOffset + component * sizeof(Uint32); + if (isFloat) { + const Float value = static_cast(init.floatValues[source]); + std::memcpy(destination, &value, sizeof(value)); + } else { + const Int32 value = static_cast(init.intValues[source]); + std::memcpy(destination, &value, sizeof(value)); + } + } + } + } + } + MarkUBOContentDirty(); + } + void ProgramObject::ReplayBufferedUniformWrites() const { if (m_pendingUniformWrites.empty()) { m_pendingUniformBytes.clear(); @@ -234,6 +320,7 @@ namespace MobileGL::MG_State::GLState { artifacts.glBlockIndexToTProgram.clear(); artifacts.tProgramBlockIndexToGl.clear(); artifacts.linkedExplicitUniformLocations.clear(); + artifacts.uniformInitialValues.clear(); artifacts.uniformIndexInTProgram.clear(); artifacts.uniformSamplerOrImageUnitIndex.clear(); artifacts.explicitOpaqueUniformBindings.clear(); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index 482166e3..6bc49abc 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -756,6 +756,13 @@ namespace MobileGL::MG_State::GLState { // layout(location = N) default-block uniform qualifiers (the relaxed parse drops // them from reflection; the DoReflection assigner restores them from here). UnorderedMap linkedExplicitUniformLocations; + // Per-link snapshot of the default-block uniform INITIALIZERS the attached shaders + // declared ("uniform int i = 1;"). Desktop GLSL says that value is what the uniform + // reads until the application overwrites it, and relinking restores it - but the + // relaxed parse turns those uniforms into members of MGL_GLOBAL_UBO, where SPIR-V + // cannot carry an initializer, so the value only survives as this side-channel. + // Applied into the uniform shadow at the phase-B publish (ApplyUniformInitialValues). + Vector uniformInitialValues; UnorderedMap uniformLocations; // Ordered by location, // aka. uniformIndexInTProgram[loc] == "uniform index of TProgram at location `loc`" @@ -1010,6 +1017,10 @@ namespace MobileGL::MG_State::GLState { // detour exactly - and a record that really does change bytes moves the version, which // is what makes a backend re-upload the UBO it cached during the window. void ReplayBufferedUniformWrites() const; + // Seeds the freshly published uniform shadow with the declared initializers. Runs at + // the phase-B publish, BEFORE ReplayBufferedUniformWrites, so an application write + // made during the A->B window still wins - which is the GL ordering. + void ApplyUniformInitialValues() const; // Past this, BufferUniformWrite declines and the write joins instead. Sized so an // ordinary pack load never reaches it (a pending window is one program's worth of // uniforms) while a pathological writer cannot grow the heap without bound. diff --git a/include/glslang/MachineIndependent/ParseHelper.h b/include/glslang/MachineIndependent/ParseHelper.h index a2de165e..42bc00c7 100644 --- a/include/glslang/MachineIndependent/ParseHelper.h +++ b/include/glslang/MachineIndependent/ParseHelper.h @@ -367,6 +367,7 @@ public: TIntermTyped* vkRelaxedRemapFunctionCall(const TSourceLoc&, TFunction*, TIntermNode*); // returns true if the variable was remapped to something else + void recordUniformInitializer(const TString&, const TType&, const TConstUnionArray&); bool vkRelaxedRemapUniformVariable(const TSourceLoc&, TString&, const TPublicType&, TArraySizes*, TIntermTyped*, TType&); void vkRelaxedRemapUniformMembers(const TSourceLoc&, const TPublicType&, const TType&, const TString&); void vkRelaxedRemapFunctionParameter(TFunction*, TParameter&, std::vector* newParams = nullptr); diff --git a/include/glslang/MachineIndependent/localintermediate.h b/include/glslang/MachineIndependent/localintermediate.h index 0d299bda..e19bc4d7 100644 --- a/include/glslang/MachineIndependent/localintermediate.h +++ b/include/glslang/MachineIndependent/localintermediate.h @@ -611,6 +611,32 @@ public: void setGlobalUniformBinding(unsigned int binding) { globalUniformBlockBinding = binding; } unsigned int getGlobalUniformBinding() const { return globalUniformBlockBinding; } + // A default-block uniform's initializer, folded to constants at parse time. + // + // Desktop GLSL 1.20+ lets a default-block uniform carry an initializer, and that value is + // what the uniform reads until the application overwrites it with glUniform*. Vulkan-relaxed + // parsing sweeps such uniforms into a uniform BLOCK, and a block member cannot carry an + // initializer in SPIR-V - so the value has nowhere to live in the generated module and used + // to be dropped outright, leaving the uniform silently zero. The CLIENT is the only party + // that can still honor it, by writing the value into the block's backing storage once the + // program links, so the folded constants are handed out here instead of discarded. + // + // Scalars appear in the same flattened order glslang folds them in: array element by array + // element, and within a matrix, column by column. Exactly one of the two value vectors is + // populated, chosen by basicType. + struct TUniformInitializer { + std::string name; + TBasicType basicType = EbtVoid; + int vectorSize = 1; // components per vector; 1 for a scalar + int matrixCols = 0; // 0 when the type is not a matrix + int matrixRows = 0; + int arraySize = 1; // outer array element count; 1 when not an array + std::vector intValues; + std::vector floatValues; + }; + void addUniformInitializer(TUniformInitializer&& init) { uniformInitializers.push_back(std::move(init)); } + const std::vector& getUniformInitializers() const { return uniformInitializers; } + void setAtomicCounterBlockName(const char* name) { atomicCounterBlockName = std::string(name); } const char* getAtomicCounterBlockName() const { return atomicCounterBlockName.c_str(); } void setAtomicCounterBlockSet(unsigned int set) { atomicCounterBlockSet = set; } @@ -1223,6 +1249,7 @@ protected: std::string globalUniformBlockName; std::string atomicCounterBlockName; + std::vector uniformInitializers; unsigned int globalUniformBlockSet; unsigned int globalUniformBlockBinding; unsigned int atomicCounterBlockSet;