[Fix, Test] (MG_State, MG_Impl, MG_Backend): a program pipeline's compute stage is dispatched on its own, and the graphics composite draws its stage programs' uniform values

This commit is contained in:
2026-08-11 20:10:17 -04:00
parent ba9af18033
commit 2ce0595fab
9 changed files with 260 additions and 45 deletions
@@ -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);
@@ -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());
+11 -4
View File
@@ -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<MG_State::GLState::ProgramObject>& 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,
@@ -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<SizeT>(ttype->getMatrixCols()) * 4 * sizeof(GLfloat);
}
return tightSize;
return MG_State::GLState::ProgramObject::UniformStorageSpanInBytes(ttype, tightSize);
}
void GetUniform_State(GLuint program, GLint location, void* params) {
@@ -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();
+147 -4
View File
@@ -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<const char*>(source.GetUBOData());
char* destinationUbo = static_cast<char*>(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<Int>(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<ProgramObject>& 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<ProgramObject*, ProgramPipelineObject::kGraphicsStageCount> mirrored{};
SizeT mirroredCount = 0;
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
const auto& stageProgram = pipeline.GetStageProgram(static_cast<ShaderStage>(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<ProgramObject>& GLContext::GetProgramForDraw() {
static const SharedPtr<ProgramObject> 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<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(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<ProgramObject>(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<SizeT>(ShaderStage::ShaderStageCount); ++stage) {
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(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<ProgramObject>& GLContext::GetProgramForDispatch() {
static const SharedPtr<ProgramObject> 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<ProgramObject>& GLContext::GetProgramForUniform() {
+8 -2
View File
@@ -163,9 +163,15 @@ namespace MobileGL {
}
void UseProgram(Uint program);
const SharedPtr<ProgramObject>& 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<ProgramObject>& 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<ProgramObject>& GetProgramForDispatch();
// What glUniform* addresses: the program in use, or the bound pipeline's
// active program (GL 4.6 core 7.6.1).
const SharedPtr<ProgramObject>& GetProgramForUniform();
@@ -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<SizeT>(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);
@@ -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<SizeT>(ShaderStage::Compute);
static_assert(static_cast<SizeT>(ShaderStage::Compute) + 1 ==
static_cast<SizeT>(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<Uint64, static_cast<SizeT>(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<Uint64, kGraphicsStageCount * 2>;
DrawProgramSignature ComputeDrawProgramSignature() const {
DrawProgramSignature signature{};
for (SizeT stage = 0; stage < static_cast<SizeT>(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<Uint64, kGraphicsStageCount>;
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<Uint64>(program->GetBackendStateVersion()) << 32) |
static_cast<Uint64>(program->GetUBOContentVersion());
}
return versions;
}
const UniformMirrorVersions& GetMirroredUniformVersions() const { return m_mirroredUniformVersions; }
void SetMirroredUniformVersions(const UniformMirrorVersions& versions) {
m_mirroredUniformVersions = versions;
}
const SharedPtr<ProgramObject>& GetCachedDrawProgram(const DrawProgramSignature& signature) const {
static const SharedPtr<ProgramObject> nullProgram = nullptr;
if (!m_drawProgram || m_drawProgramSignature != signature) return nullProgram;
@@ -67,6 +107,8 @@ namespace MobileGL {
void SetCachedDrawProgram(const DrawProgramSignature& signature, SharedPtr<ProgramObject> 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<ProgramObject> m_activeProgram;
SharedPtr<ProgramObject> m_drawProgram;
DrawProgramSignature m_drawProgramSignature{};
UniformMirrorVersions m_mirroredUniformVersions{};
String m_infoLog;
const Uint m_externalIndex = 0;
Bool m_validateStatus = false;