[Fix] (Review): bound copies by the requested level, reach every cube face, keep array layer counts, and give glSpecializeShader its spec error surface

This commit is contained in:
2026-08-27 05:51:58 -04:00
parent e430e1b3be
commit 9ef33f4274
16 changed files with 977 additions and 127 deletions
+44 -10
View File
@@ -650,6 +650,12 @@ namespace MobileGL::MG_State {
// a graphics program carrying a compute module, which Adreno 830 does not reject
// from vkCreateGraphicsPipelines - it SIGSEGVs inside it.
Bool anyStage = false;
// Which stages the composite ACTUALLY got a shader for. Not the same question as
// "which stages have a stage program bound": one program bound with
// GL_ALL_SHADER_BITS occupies every slot while contributing a shader to only the
// stages it was linked with. The transform-feedback capture stage is chosen off this,
// because it has to be the stage that will exist in the composite's own link.
Bool compositeHasStage[ProgramPipelineObject::kGraphicsStageCount] = {};
for (SizeT stage = 0; stage < ProgramPipelineObject::kGraphicsStageCount; ++stage) {
const auto& stageProgram = pipeline->GetStageProgram(static_cast<ShaderStage>(stage));
if (!stageProgram) continue;
@@ -665,6 +671,7 @@ namespace MobileGL::MG_State {
if (!ref.shader || static_cast<SizeT>(ref.shader->GetShaderStage()) != stage) continue;
composite->AttachShaderWithPinnedLinkInput(ref);
anyStage = true;
compositeHasStage[stage] = true;
}
}
if (!anyStage) return nullProgram;
@@ -672,20 +679,47 @@ namespace MobileGL::MG_State {
// (GL 4.6 core 11.1.2.1), and glTransformFeedbackVaryings is per-PROGRAM state that
// only the stage program carrying that stage can have been given. The composite is
// assembled out of the stage programs' shaders and inherits none of their
// GL-thread-owned request state, so without this the composite links with an empty
// capture list and glBeginTransformFeedback rejects the draw with INVALID_OPERATION
// ("the program has no transform feedback varyings") even though
// glValidateProgramPipeline had just passed. Same resolution order as
// ProgramLinkTask::ResolveTransformFeedbackVaryings: geometry, else tessellation
// evaluation, else vertex.
// GL-thread-owned state, so without this it links with an empty capture list and
// glBeginTransformFeedback rejects the draw with INVALID_OPERATION ("the program has
// no transform feedback varyings") even though glValidateProgramPipeline had passed.
//
// TWO RULES, both easy to get subtly wrong and both load-bearing:
//
// (1) THE LINKED LIST, NOT THE PENDING REQUEST. glTransformFeedbackVaryings does not
// take effect until the program's next link (GL 4.6 core 7.3/11.1.2.1), and it
// deliberately bumps no version - so a request written after the stage program's
// last link is invisible to the composite cache's signature yet would be picked up
// by the next rebuild, making the capture list depend on whether some unrelated
// event happened to invalidate the cache. Worse, a name that is not an output of
// the capture stage fails the composite's OWN link, and a failed composite makes
// every draw through the pipeline report INVALID_OPERATION. Reading the LINKED
// snapshot removes the whole class: linked state only moves at a link, and a link
// is exactly what ComputeDrawProgramSignature's per-stage link version tracks, so
// the existing cache key is sufficient by construction.
// GetTransformFeedbackInterfaceNames() is the right accessor rather than the
// resolved xfbVaryings: it is the request as that link consumed it, pseudo-varyings
// (gl_NextBuffer / gl_SkipComponentsN) included, which is what re-issuing it needs.
//
// (2) THE FIRST STAGE THAT EXISTS, not the first with something to capture. This is
// the rule ProgramLinkTask::ResolveTransformFeedbackVaryings applies (it breaks on
// getIntermediate(stage) != nullptr), and the two MUST agree: this loop picks
// WHOSE list, the link task picks WHICH stage's outputs the names resolve against.
// Skipping a geometry stage that has no capture list and installing the vertex
// stage's instead made them disagree, and the composite then resolved a vertex
// program's names against the geometry intermediate - capturing where GL says it
// must not, or failing the link and killing every draw. A capture stage with an
// empty list is not a reason to look further down: it is the answer, and
// glBeginTransformFeedback's INVALID_OPERATION is the correct consequence.
for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
if (!compositeHasStage[static_cast<SizeT>(captureStage)]) continue;
const auto& captureProgram = pipeline->GetStageProgram(captureStage);
if (!captureProgram) continue;
const auto& requested = captureProgram->GetRequestedTransformFeedbackVaryings();
if (requested.empty()) continue;
composite->SetTransformFeedbackVaryings(Vector<String>(requested),
captureProgram->GetRequestedTransformFeedbackBufferMode());
const auto& linkedNames = captureProgram->GetTransformFeedbackInterfaceNames();
if (!linkedNames.empty()) {
composite->SetTransformFeedbackVaryings(Vector<String>(linkedNames),
captureProgram->GetTransformFeedbackBufferMode());
}
break;
}
// A pipeline with no fragment stage still rasterises, so the default fragment
@@ -540,6 +540,33 @@ namespace MobileGL::MG_State::GLState {
task->in.explicitFragDataIndex = m_explicitFragDataIndex;
task->in.requestedXfbVaryings = m_requestedXfbVaryings;
task->in.requestedXfbBufferMode = m_requestedXfbBufferMode;
// ARB_gl_spirv: a program built from SPIR-V declares its transform feedback through
// XfbBuffer/XfbStride/Offset DECORATIONS, and glTransformFeedbackVaryings has no effect on
// it at all. glSpecializeShader translated those decorations into the equivalent name
// request (ShaderCompiler::SpecializeAndDecompileSpirvModule), and this is where it enters
// the link - so everything downstream, the frontend packer and both backends, sees one
// declaration form instead of two.
//
// The capture stage is the LAST vertex-processing stage the program has, which is the same
// rule ProgramLinkTask::ResolveTransformFeedbackVaryings resolves the names against. The
// application's own request wins if it made one: that can only happen on a mixed program,
// which is not a shape ARB_gl_spirv defines, and honouring what the application explicitly
// asked for is the safer of the two readings.
if (task->in.requestedXfbVaryings.empty()) {
for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
Bool stagePresent = false;
for (const auto& shader : m_shaders) {
if (!shader || shader->GetShaderStage() != captureStage) continue;
stagePresent = true;
if (shader->GetSpirvXfbVaryings().empty()) continue;
task->in.requestedXfbVaryings = shader->GetSpirvXfbVaryings();
task->in.requestedXfbBufferMode = shader->GetSpirvXfbBufferMode();
break;
}
if (stagePresent) break;
}
}
task->in.maxFragmentOutputColorNumber = m_maxFragmentOutputColorNumber;
Vector<SharedPtr<ShaderCompileTask>> deps;
@@ -1536,14 +1536,14 @@ namespace MobileGL::MG_State::GLState {
m_requestedXfbVaryings = Move(names);
m_requestedXfbBufferMode = bufferMode;
}
// The REQUEST, not the linked result: what glTransformFeedbackVaryings last recorded,
// which the next link will try to resolve. A program pipeline's draw composite reads it
// off the capturing stage program and re-issues it on itself, because the composite is
// built from the stage programs' SHADERS and would otherwise inherit no capture list at
// all - which made glBeginTransformFeedback reject every separable-program capture
// (glcSeparableProgramsTransformFeedbackTests).
const Vector<String>& GetRequestedTransformFeedbackVaryings() const { return m_requestedXfbVaryings; }
GLenum GetRequestedTransformFeedbackBufferMode() const { return m_requestedXfbBufferMode; }
// NO ACCESSOR FOR THE PENDING REQUEST, deliberately. A program pipeline's draw composite
// needs the capture list of the stage program it flattens, and the obvious source - what
// glTransformFeedbackVaryings last recorded - is the wrong one: that request does not take
// effect until the stage program's next link, and it bumps no version, so reading it makes
// the composite's capture list depend on when the composite cache happened to be
// invalidated. GetTransformFeedbackInterfaceNames() below is the source that is correct
// AND cache-safe, because linked state only moves at a link and the composite signature
// already keys on the link version. See GLContext::GetProgramForDraw.
GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
@@ -21,16 +21,31 @@ namespace MobileGL::MG_State::GLState {
ReleaseCompileNode();
m_spirvBinary = Move(binary);
m_hasSpirvBinary = true;
m_specialized = false;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_spirvXfbVaryings.clear();
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
m_source = MakeShared<const String>(String{});
InvalidateCompiledState();
}
void ShaderObject::SpecializeFromSpirv(String&& glsl) {
const String& ShaderObject::GetApplicationShaderSource() const {
static const String kNoSource;
// Both the unspecialized and the specialized windows answer empty: in the first m_source
// already is empty, in the second it holds generated GLSL that the application never wrote.
return m_hasSpirvBinary ? kNoSource : *m_source;
}
void ShaderObject::SpecializeFromSpirv(String&& glsl, Vector<String>&& xfbVaryings, GLenum xfbBufferMode) {
ReleaseCompileNode();
// The latch goes up HERE and nowhere else - this is the one path that actually specialized
// the shader.
m_specialized = true;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_spirvXfbVaryings = Move(xfbVaryings);
m_spirvXfbBufferMode = xfbBufferMode;
// The GLSL the module specializes to enters the ORDINARY pipeline from here: preprocess,
// glslang parse, reflection, transpile, both backends. Nothing downstream needs to know
// the source was not written by the application - which is the whole reason this hop
@@ -61,8 +76,11 @@ namespace MobileGL::MG_State::GLState {
m_hasSpirvBinary = false;
m_spirvBinary.clear();
m_spirvBinary.shrink_to_fit();
m_specialized = false;
m_specializationFailed = false;
m_specializationInfoLog.clear();
m_spirvXfbVaryings.clear();
m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
ReleaseCompileNode();
m_source = MakeShared<const String>(source);
InvalidateCompiledState();
@@ -78,10 +78,33 @@ namespace MobileGL {
// checks explicitly.
void SetSpirvBinary(Vector<Uint32>&& binary);
Bool HasSpirvBinary() const { return m_hasSpirvBinary; }
// ARB_gl_spirv: "Once specialized, a shader may not be re-specialized without first
// re-associating the original SPIR-V module with it, through ShaderBinary." A second
// glSpecializeShader is GL_INVALID_OPERATION, and this latch is what answers that.
//
// Set ONLY on the success path. A specialization that FAILED did not specialize the
// shader, and the conformance suite relies on that distinction: it deliberately fails
// specialization (a bad entry point, then an unknown constant id) on one shader object
// and then requires the next, well-formed call on that same object to be accepted.
Bool HasBeenSpecialized() const { return m_specialized; }
const Vector<Uint32>& GetSpirvBinary() const { return m_spirvBinary; }
// glSpecializeShader's half: hand the object the GLSL its module specializes to and
// let the ordinary pipeline compile it.
void SpecializeFromSpirv(String&& glsl);
void SpecializeFromSpirv(String&& glsl, Vector<String>&& xfbVaryings, GLenum xfbBufferMode);
// The capture the object's SPIR-V module DECLARED, as the equivalent
// glTransformFeedbackVaryings request. Empty for a GLSL shader and for a SPIR-V module
// that declares no transform feedback. ProgramObject::Link picks this up from the
// program's last vertex-processing stage, because ARB_gl_spirv makes decorations the
// only declaration form for a SPIR-V program and glTransformFeedbackVaryings has no
// effect on one.
const Vector<String>& GetSpirvXfbVaryings() const { return m_spirvXfbVaryings; }
GLenum GetSpirvXfbBufferMode() const { return m_spirvXfbBufferMode; }
// What glGetShaderSource / GL_SHADER_SOURCE_LENGTH must answer. A shader created from
// glShaderBinary never had glShaderSource called on it, so GL 4.6 core 7.1 makes its
// source the empty string - even after glSpecializeShader, when m_source holds the
// SPIRV-Cross GLSL the module was translated into. That text is MobileGL's, not the
// application's, and handing it back invites an application to cache and re-submit it.
const String& GetApplicationShaderSource() const;
// The other half: specialization itself failed (a bad entry point, a constant id the
// module does not declare, a module spirv-val rejects). There is nothing to compile,
// so the verdict is recorded directly - COMPILE_STATUS false with this log - and both
@@ -278,6 +301,13 @@ namespace MobileGL {
// the ORIGINAL words rather than the ones the first call folded.
Vector<Uint32> m_spirvBinary;
Bool m_hasSpirvBinary = false;
// "This shader has been specialized"; see HasBeenSpecialized. Cleared by anything that
// re-associates a module (SetSpirvBinary) or turns the object back into a GLSL shader
// (either SetShaderSource overload) - which is exactly the re-association ARB_gl_spirv
// names as the way to make a second specialization legal again.
Bool m_specialized = false;
Vector<String> m_spirvXfbVaryings;
GLenum m_spirvXfbBufferMode = GL_INTERLEAVED_ATTRIBS;
// A specialization that failed before any compile could start. Kept beside the
// compile artifacts rather than inside them because there is no compile job to hang
// it on - see RecordSpecializationFailure. Cleared by anything that gives the object
@@ -248,11 +248,12 @@ namespace MobileGL {
}
void RenderState::SetPolygonOffset(Float factor, Float units) {
if (m_parameters.PolygonOffsetFactor == factor && m_parameters.PolygonOffsetUnits == units) return;
m_parameters.PolygonOffsetFactor = factor;
m_parameters.PolygonOffsetUnits = units;
++m_version;
// GL 4.6 core 14.6.5 defines PolygonOffset(factor, units) as EQUIVALENT to
// PolygonOffsetClamp(factor, units, 0) - the equivalence is total, so the clamp is
// written too, not merely left alone. Leaving it meant a glPolygonOffsetClamp(1, 1,
// 0.5) followed by a plain glPolygonOffset(3, 4) still reported a clamp of 0.5, and
// the early-out below could even skip the version bump while doing it.
SetPolygonOffsetClamped(factor, units, 0.0f);
}
Float RenderState::GetPolygonOffsetFactor() const {