diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index d774c356..ddb042b6 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -3010,8 +3010,10 @@ namespace MobileGL::MG_Backend::DirectGLES { ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif // Single per-dispatch program resolve and texture-key capture, as in - // PrepareForDraw (nothing below can move either). - const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); + // PrepareForDraw (nothing below can move either). The DISPATCH accessor: with a + // pipeline bound this is its compute stage program, which is a whole program on its + // own - the graphics composite a draw builds carries no compute stage. + const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch(); const TextureImpl::DrawTextureSyncKeys textureKeys = TextureImpl::CaptureDrawTextureSyncKeys(); BufferImpl::SyncComputeBuffers(includeDispatchIndirectBuffer); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp index be01fb2f..e2645985 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VulkanRenderer.cpp @@ -4582,7 +4582,6 @@ void main() { MGLOG_D("GetOrCreatePipeline skipped: program has no shader stages"); return VK_NULL_HANDLE; } - // Fast path: skip the full pipeline resolution when the pipeline state is unchanged from the // previous draw (the common intra-batch case). The key provably covers every // PipelineCreatePayload field: draw mode (topology + polygon-fill depth-bias gate), program @@ -6144,7 +6143,9 @@ void main() { void VulkanRenderer::DispatchCompute(GLuint numGroupsX, GLuint numGroupsY, GLuint numGroupsZ) { m_textureManager->CollectGarbage(); auto& frame = m_frameContext.GetCurrent(); - const auto& program = *MG_State::pGLContext->GetProgramForDraw(); + // The DISPATCH accessor: with a pipeline bound this is its compute stage program + // itself, never the graphics composite (which carries no compute stage at all). + const auto& program = *MG_State::pGLContext->GetProgramForDispatch(); if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { MGLOG_E("DispatchCompute skipped: program=%u has no optimized SPIR-V", program.GetExternalIndex()); @@ -6189,7 +6190,8 @@ void main() { void VulkanRenderer::DispatchComputeIndirect(GLintptr indirect) { m_textureManager->CollectGarbage(); auto& frame = m_frameContext.GetCurrent(); - const auto& program = *MG_State::pGLContext->GetProgramForDraw(); + // See DispatchCompute: the dispatch accessor, not the draw one. + const auto& program = *MG_State::pGLContext->GetProgramForDispatch(); if (!program.GetLinkStatus() || !program.GetSpirvStatus()) { MGLOG_E("DispatchComputeIndirect skipped: program=%u has no optimized SPIR-V", program.GetExternalIndex()); diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 369339f4..eaa1c6e0 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -14,8 +14,8 @@ #include "../Getter/GL_Getter.h" namespace MobileGL::MG_Impl::GLImpl { - static Bool ValidateCurrentProgramForExecution(const char* functionName) { - const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); + static Bool ValidateProgramForExecution(const SharedPtr& currentProgram, + const char* functionName) { if (!currentProgram) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, @@ -34,10 +34,17 @@ namespace MobileGL::MG_Impl::GLImpl { return true; } + static Bool ValidateCurrentProgramForExecution(const char* functionName) { + return ValidateProgramForExecution(MG_State::pGLContext->GetProgramForDraw(), functionName); + } + + // A dispatch resolves its program through the DISPATCH accessor: with a pipeline bound + // that is the pipeline's compute stage program, not the graphics composite a draw would + // build - which no longer contains a compute stage to find at all. static Bool ValidateCurrentProgramForCompute(const char* functionName) { - if (!ValidateCurrentProgramForExecution(functionName)) return false; + const auto& currentProgram = MG_State::pGLContext->GetProgramForDispatch(); + if (!ValidateProgramForExecution(currentProgram, functionName)) return false; - const auto& currentProgram = MG_State::pGLContext->GetProgramForDraw(); if (currentProgram->GetShaderIndexByStage(ShaderStage::Compute) < 0) { MG_State::pGLContext->RecordError( ErrorCode::InvalidOperation, diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index c62b9fbf..8aa87a53 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -863,12 +863,11 @@ namespace MobileGL::MG_Impl::GLImpl { } // Bytes a uniform actually occupies in the global UBO. It is the tight GL type size for - // everything except a float matrix, whose padded columns make it wider. + // 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) { - if (ttype != nullptr && ttype->isMatrix() && ttype->getBasicType() != glslang::EbtDouble) { - return static_cast(ttype->getMatrixCols()) * 4 * sizeof(GLfloat); - } - return tightSize; + return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize); } void GetUniform_State(GLuint program, GLint location, void* params) { diff --git a/MobileGL/MG_IntegrationTest/Scenarios/ProgramPipelineScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/ProgramPipelineScenario.cpp index 5a61f5af..40d2379d 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/ProgramPipelineScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/ProgramPipelineScenario.cpp @@ -176,17 +176,15 @@ void main() { gl_Position = i_position; } gl.EndFrame(); } - // glActiveShaderProgram picks which stage program glUniform* addresses. + // glActiveShaderProgram picks which stage program glUniform* addresses - and the draw has to + // see what was written there. // - // DISABLED: a second, independent defect, left failing on purpose rather than deleted. The - // materialization fix above got the stages recorded and the pipeline drawing, but a uniform - // set through the active shader program does not reach the flattened composite: the draw - // paints u_color's default rather than the value written. GetProgramForUniform() returns the - // pipeline's active program, while GetProgramForDraw() builds a SEPARATE composite object out - // of the stage programs' shaders - so uniform values live on one object and the draw reads - // another. Enable this the moment the composite inherits (or aliases) its stage programs' - // uniform storage. - TEST_F(ProgramPipelineScenario, DISABLED_UniformsGoToTheActiveShaderProgram) { + // The second defect of the cluster, and the one the pixels expose most directly: uniform + // values live on the stage program (GetProgramForUniform returns the pipeline's active + // program) while the draw reads the composite GetProgramForDraw builds out of the stage + // programs' shaders. Two objects, two sets of uniform storage; before the composite was + // refreshed from its stage programs this painted u_color's zero default instead of green. + TEST_F(ProgramPipelineScenario, UniformsGoToTheActiveShaderProgram) { if (!Ready()) return; static const char* kUniformFS = R"(#version 430 core @@ -236,14 +234,15 @@ void main() { o_color = u_color; } // The sso-compute-pipeline shape: compute and non-compute stages on ONE pipeline object, the // compute stage writing the buffer the vertex stage then reads. // - // DISABLED: the third defect in this cluster. Attaching a compute stage alongside graphics - // stages is now accepted, but the dispatch/draw pair still paints nothing and leaves an error - // behind - GetProgramForDraw flattens EVERY stage of the pipeline into one composite, so the - // compute stage and the graphics stages end up in a single program that can serve neither - // glDispatchCompute nor glDrawArrays correctly. GL keeps them separate: a pipeline's compute - // stage is dispatched on its own and never participates in a draw. Enable this when the - // flattening splits the compute stage out from the graphics ones. - TEST_F(ProgramPipelineScenario, DISABLED_ComputeAndGraphicsStagesShareOnePipeline) { + // The third defect of the cluster: the flattening used to pull EVERY stage into one + // composite, so a single program was asked to serve both glDispatchCompute and glDrawArrays. + // GL keeps them apart - a pipeline's compute stage is a whole program dispatched on its own + // and never participates in a draw - which is why the accessors are split (GetProgramForDraw + // composites the graphics stages, GetProgramForDispatch hands back the compute stage + // program). It is also the shape that killed the process on Adreno: the composite carried a + // compute module into vkCreateGraphicsPipelines, and that driver SIGSEGVs rather than + // returning an error. + TEST_F(ProgramPipelineScenario, ComputeAndGraphicsStagesShareOnePipeline) { if (!Ready()) return; HeadlessGL& gl = Gl(); const int width = gl.Width(); diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 6e32398d..9b8503fe 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -369,6 +369,115 @@ namespace MobileGL::MG_State { return m_programState.GetCurrentProgram(); } + // Copies every default-block uniform value `source` holds into the same-named uniform of + // `destination`, by name and by location. + // + // The composite a pipeline draws through is a DIFFERENT program object from the stage + // programs the application writes uniforms to - glUniform* addresses the pipeline's + // active program and glProgramUniform* addresses a named one, neither of which is the + // composite - so without this a pipeline draw reads the composite's zero defaults and + // paints them. Values are COPIED rather than aliased: the two programs' global UBOs are + // laid out independently (the composite merges several stages' uniforms into one block, + // so the same uniform sits at a different offset in each), and a copy also means the + // composite can outlive a stage program without ever pointing into freed storage. + // + // Location-by-location so that arrays are carried across whole, and via the padded + // storage span so a mat3's std140 column padding travels with it. + static void MirrorUniformValues(ProgramObject& source, ProgramObject& destination) { + if (!source.GetLinkStatus() || !destination.GetLinkStatus()) return; + const char* sourceUbo = static_cast(source.GetUBOData()); + char* destinationUbo = static_cast(destination.MapUBO()); + const SizeT sourceUboSize = source.GetUBOSize(); + const SizeT destinationUboSize = destination.GetUBOSize(); + + const Uint uniformCount = source.GetUniformCount(); + for (Uint index = 0; index < uniformCount; ++index) { + const String& name = source.GetActiveUniformName(index); + if (name.empty()) continue; + const Int sourceBase = source.GetUniformLocation(name); + const Int destinationBase = destination.GetUniformLocation(name); + // A uniform the composite's own link dropped (or renamed) is simply not + // mirrored; the draw cannot read what does not exist. + if (sourceBase < 0 || destinationBase < 0) continue; + + const GLint arraySize = source.GetActiveUniformArraySize(index); + const Int elements = arraySize > 0 ? static_cast(arraySize) : 1; + for (Int element = 0; element < elements; ++element) { + const Int sourceLocation = sourceBase + element; + const Int destinationLocation = destinationBase + element; + if (!source.IsValidUniformLocation(sourceLocation) || + !destination.IsValidUniformLocation(destinationLocation)) { + break; + } + // Stop at the end of EITHER side's array rather than walking onto the + // neighbouring uniform of whichever program has the shorter one. + if (!source.UniformLocationsAliasSameUniform(sourceBase, sourceLocation) || + !destination.UniformLocationsAliasSameUniform(destinationBase, destinationLocation)) { + break; + } + + const Bool sourceOpaque = source.IsUniformOpaqueAtLocation(sourceLocation); + if (sourceOpaque != destination.IsUniformOpaqueAtLocation(destinationLocation)) break; + if (sourceOpaque) { + // A sampler/image unit is phase-A state, not UBO bytes. The setter + // itself is a no-op when the value already matches, so this does not + // churn the composite's backend state version. + destination.SetUniformSamplerOrImageUnitIndex( + destinationLocation, source.GetUniformSamplerOrImageUnitIndex(sourceLocation)); + continue; + } + + const SizeT span = source.GetUniformStorageSpanInBytes(sourceLocation); + if (span == 0 || span != destination.GetUniformStorageSpanInBytes(destinationLocation)) continue; + const Uint sourceOffset = source.GetUniformOffset(sourceLocation); + const Uint destinationOffset = destination.GetUniformOffset(destinationLocation); + // Either side can legitimately lack backing storage: the optimizer deletes a + // uniform nothing reads, and a program whose SPIR-V phase settled cancelled + // has no shadow at all. Both report kInvalidUniformOffset / a null shadow. + if (sourceUbo == nullptr || destinationUbo == nullptr || + sourceOffset == ProgramObject::kInvalidUniformOffset || + destinationOffset == ProgramObject::kInvalidUniformOffset || + sourceOffset + span > sourceUboSize || destinationOffset + span > destinationUboSize) { + continue; + } + if (std::memcmp(destinationUbo + destinationOffset, sourceUbo + sourceOffset, span) == 0) { + continue; + } + Memcpy(destinationUbo + destinationOffset, sourceUbo + sourceOffset, span); + destination.MarkUBOContentDirty(); + } + } + } + + // Brings the pipeline's composite up to date with the uniform values its stage programs + // now hold. Runs on every draw through a pipeline, so the common case is the version + // compare below and nothing else. + static void RefreshCompositeUniforms(ProgramPipelineObject& pipeline, const SharedPtr& composite) { + if (!composite) return; + const auto versions = pipeline.ComputeUniformMirrorVersions(); + if (versions == pipeline.GetMirroredUniformVersions()) return; + + // A program bound to two stages appears twice; mirroring it twice would be + // idempotent but is still work, and the second pass would have nothing to do. + Array mirrored{}; + SizeT mirroredCount = 0; + for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) { + const auto& stageProgram = pipeline.GetStageProgram(static_cast(stage)); + if (!stageProgram) continue; + Bool alreadyMirrored = false; + for (SizeT i = 0; i < mirroredCount; ++i) { + if (mirrored[i] == stageProgram.get()) { + alreadyMirrored = true; + break; + } + } + if (alreadyMirrored) continue; + mirrored[mirroredCount++] = stageProgram.get(); + MirrorUniformValues(*stageProgram, *composite); + } + pipeline.SetMirroredUniformVersions(versions); + } + const SharedPtr& GLContext::GetProgramForDraw() { static const SharedPtr nullProgram = nullptr; const auto& currentProgram = m_programState.GetCurrentProgram(); @@ -402,13 +511,16 @@ namespace MobileGL::MG_State { // that will never be produced again: every draw would miss the cache and rebuild // (and relink) the composite. Join first, so the signature describes settled // programs. In steady state this is a null check per stage. - for (SizeT stage = 0; stage < static_cast(ShaderStage::ShaderStageCount); ++stage) { + for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) { const auto& stageProgram = pipeline->GetStageProgram(static_cast(stage)); if (stageProgram) stageProgram->JoinLinkAndSpirv(); } const auto signature = pipeline->ComputeDrawProgramSignature(); - if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) return cached; + if (const auto& cached = pipeline->GetCachedDrawProgram(signature)) { + RefreshCompositeUniforms(*pipeline, cached); + return cached; + } // Everything downstream of here - the backends, the uniform plumbing, the draw // validation - is written against a single linked program, so the pipeline is @@ -420,8 +532,14 @@ namespace MobileGL::MG_State { // could otherwise be handed. Backend registries key on the object, not the name. auto composite = MakeShared(0u); + // GRAPHICS stages only. A pipeline may carry a compute stage alongside them (GL + // 4.6 core 7.4 forbids linking compute WITH another stage into one program, not + // attaching a compute program to a pipeline that also has graphics ones), and that + // stage belongs to glDispatchCompute, not to this draw. Compositing it in produced + // a graphics program carrying a compute module, which Adreno 830 does not reject + // from vkCreateGraphicsPipelines - it SIGSEGVs inside it. Bool anyStage = false; - for (SizeT stage = 0; stage < static_cast(ShaderStage::ShaderStageCount); ++stage) { + for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) { const auto& stageProgram = pipeline->GetStageProgram(static_cast(stage)); if (!stageProgram) continue; for (const auto& shader : stageProgram->GetAttachedShaders()) { @@ -440,7 +558,32 @@ namespace MobileGL::MG_State { // for the same reason: the backend is about to read its SPIR-V. composite->JoinLinkAndSpirv(); pipeline->SetCachedDrawProgram(signature, Move(composite)); - return pipeline->GetCachedDrawProgram(signature); + const auto& cached = pipeline->GetCachedDrawProgram(signature); + RefreshCompositeUniforms(*pipeline, cached); + return cached; + } + + const SharedPtr& GLContext::GetProgramForDispatch() { + static const SharedPtr nullProgram = nullptr; + const auto& currentProgram = m_programState.GetCurrentProgram(); + if (currentProgram) { + // Same join contract as GetProgramForDraw's glUseProgram half - see the note + // there. A dispatch reads the same non-artifact versions a draw does. + currentProgram->JoinLinkAndSpirv(); + return currentProgram; + } + if (m_boundProgramPipeline == 0) return nullProgram; + const auto& pipeline = GetBoundProgramPipeline(); + if (!pipeline) return nullProgram; + // No compositing and no cache: GL 4.6 core 7.4 makes a compute program exclusive of + // every other stage, so the pipeline's compute stage program IS the program to + // dispatch, uniforms and all. That also means glUniform* through the active program + // lands on the very object the dispatch reads - the composite's uniform refresh has + // no counterpart to do here. + const auto& computeProgram = pipeline->GetStageProgram(ShaderStage::Compute); + if (!computeProgram) return nullProgram; + computeProgram->JoinLinkAndSpirv(); + return computeProgram; } const SharedPtr& GLContext::GetProgramForUniform() { diff --git a/MobileGL/MG_State/GLState/Core.h b/MobileGL/MG_State/GLState/Core.h index a2ecd6a4..ed497efb 100644 --- a/MobileGL/MG_State/GLState/Core.h +++ b/MobileGL/MG_State/GLState/Core.h @@ -163,9 +163,15 @@ namespace MobileGL { } void UseProgram(Uint program); const SharedPtr& GetCurrentProgram(); - // What a draw or dispatch actually executes: the program in use, or - when - // there is none - the bound pipeline's stages composited into one program. + // What a DRAW executes: the program in use, or - when there is none - the bound + // pipeline's GRAPHICS stages composited into one program. A pipeline's compute + // stage is never part of that composite; ask GetProgramForDispatch for it. const SharedPtr& GetProgramForDraw(); + // What a DISPATCH executes: the program in use, or - when there is none - the + // bound pipeline's compute stage program itself. GL's compute stage is a whole + // program on its own (GL 4.6 core 7.4: it may not be linked with any other + // stage), so there is nothing to composite and no composite to cache. + const SharedPtr& GetProgramForDispatch(); // What glUniform* addresses: the program in use, or the bound pipeline's // active program (GL 4.6 core 7.6.1). const SharedPtr& GetProgramForUniform(); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index b350ed0c..4c90188d 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -326,6 +326,20 @@ namespace MobileGL::MG_State::GLState { : kInvalidUniformOffset; } Uint GetUniformSizesInBytes(Uint location) const { return MG_Util::GetGLTypeSize(GetUniformType(location)); } + // Bytes a uniform actually occupies in the global UBO, which is not its GL type size: + // std140 pads each column of a float matrix out to a vec4, so a mat3 spans 48 bytes + // even though only 36 of them carry components. 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() && type->getBasicType() != glslang::EbtDouble) { + return static_cast(type->getMatrixCols()) * 4 * sizeof(Float); + } + return tightSize; + } + SizeT GetUniformStorageSpanInBytes(Uint location) const { + return UniformStorageSpanInBytes(GetUniformTType(location), GetUniformSizesInBytes(location)); + } Int GetAttributeLocation(const String& name) { const auto it = std::find(Artifacts().attribs.begin(), Artifacts().attribs.end(), name); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h index 165de4e9..102a2a1d 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramPipelineObject.h @@ -40,17 +40,31 @@ namespace MobileGL { Uint GetExternalIndex() const { return m_externalIndex; } + // The stages a DRAW is built from: every stage but compute. GL 4.6 core 7.4 + // makes the compute stage exclusive - a program object containing a compute + // shader may contain no other stage, and a pipeline's compute stage is + // dispatched on its own and never participates in a draw. So the compute stage + // is not merely irrelevant to the composite below, it must never enter it: a + // compute module handed to vkCreateGraphicsPipelines is a driver crash rather + // than an error return (Adreno 830 SIGSEGVs inside it). + static constexpr SizeT kGraphicsStageCount = static_cast(ShaderStage::Compute); + static_assert(static_cast(ShaderStage::Compute) + 1 == + static_cast(ShaderStage::ShaderStageCount), + "ShaderStage must keep Compute last so the graphics stages are a prefix"); + // A draw sees one program, but a pipeline holds one program per stage. The - // stages are composited into a single hidden program object, rebuilt whenever - // the stage set - or any stage program's own link - changes. The signature is - // what that "changes" means: a stage program's lifetime id pins the object and - // its backend state version pins the link generation. - using DrawProgramSignature = - Array(ShaderStage::ShaderStageCount) * 2>; + // GRAPHICS stages are composited into a single hidden program object, rebuilt + // whenever the stage set - or any stage program's own link - changes. The + // signature is what that "changes" means: a stage program's lifetime id pins the + // object and its backend state version pins the link generation. It covers + // exactly the stages the composite is built from, so attaching or relinking a + // compute stage never invalidates a perfectly good graphics composite - and the + // compute stage, having no composite of its own, can never collide with it. + using DrawProgramSignature = Array; DrawProgramSignature ComputeDrawProgramSignature() const { DrawProgramSignature signature{}; - for (SizeT stage = 0; stage < static_cast(ShaderStage::ShaderStageCount); ++stage) { + for (SizeT stage = 0; stage < kGraphicsStageCount; ++stage) { const auto& program = m_stagePrograms[stage]; if (!program) continue; signature[stage * 2] = program->GetLifetimeId(); @@ -59,6 +73,32 @@ namespace MobileGL { return signature; } + // Uniform values are written to the STAGE programs - glUniform* addresses the + // pipeline's active program (GL 4.6 core 7.6.1) and glProgramUniform* addresses + // a named one - while the draw reads the composite. Two different objects' + // storage, so the composite is refreshed from its stage programs before each + // draw that needs it. These are the per-stage versions "needs it" is measured + // against: the stage program's uniform-shadow content version in the low half + // and its backend state version (which the opaque/sampler-unit writes bump) in + // the high half. All zero after a rebuild, because a fresh composite starts at + // GL's zero defaults and so needs a full refresh. + using UniformMirrorVersions = Array; + + UniformMirrorVersions ComputeUniformMirrorVersions() const { + UniformMirrorVersions versions{}; + for (SizeT stage = 0; stage < kGraphicsStageCount; ++stage) { + const auto& program = m_stagePrograms[stage]; + if (!program) continue; + versions[stage] = (static_cast(program->GetBackendStateVersion()) << 32) | + static_cast(program->GetUBOContentVersion()); + } + return versions; + } + const UniformMirrorVersions& GetMirroredUniformVersions() const { return m_mirroredUniformVersions; } + void SetMirroredUniformVersions(const UniformMirrorVersions& versions) { + m_mirroredUniformVersions = versions; + } + const SharedPtr& GetCachedDrawProgram(const DrawProgramSignature& signature) const { static const SharedPtr nullProgram = nullptr; if (!m_drawProgram || m_drawProgramSignature != signature) return nullProgram; @@ -67,6 +107,8 @@ namespace MobileGL { void SetCachedDrawProgram(const DrawProgramSignature& signature, SharedPtr program) { m_drawProgramSignature = signature; m_drawProgram = Move(program); + // A rebuilt composite holds none of its stage programs' uniform values yet. + m_mirroredUniformVersions = {}; } private: @@ -74,6 +116,7 @@ namespace MobileGL { SharedPtr m_activeProgram; SharedPtr m_drawProgram; DrawProgramSignature m_drawProgramSignature{}; + UniformMirrorVersions m_mirroredUniformVersions{}; String m_infoLog; const Uint m_externalIndex = 0; Bool m_validateStatus = false;