[Fix] (ShaderTranspiler): enforce the layout(binding) range rule for samplers, images and uniform/atomic-counter blocks, not only for SSBOs

This commit is contained in:
2026-08-27 05:37:17 -04:00
parent 0e4302b399
commit be7bf21eb8
16 changed files with 568 additions and 23 deletions
+20
View File
@@ -668,6 +668,26 @@ namespace MobileGL::MG_State {
}
}
if (!anyStage) return nullProgram;
// Transform feedback captures the output of the LAST vertex-processing stage
// (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.
for (const ShaderStage captureStage:
{ShaderStage::Geometry, ShaderStage::TessEval, ShaderStage::Vertex}) {
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());
break;
}
// A pipeline with no fragment stage still rasterises, so the default fragment
// shader is wanted here even though the separable stage programs never get one.
composite->Link(true);
@@ -621,13 +621,21 @@ namespace MobileGL::MG_State::GLState {
// mapper's collect callback is the last point at which a resource's qualifier still
// says what the SHADER declared rather than what glslang assigned, so both captures
// have to be taken from inside the link. See TMglGlslIoResolver::reserverResourceSlot.
// The binding-range rule (GLSL 4.30 4.4.5): its ceilings in, and the first violation the
// resolver finds out. Enforced at the link because mapIO's collect callback is the last
// point at which a resource's qualifier still says what the SHADER declared - see
// TMglGlslIoResolver::CheckDeclaredBindingRange.
String resourceBindingViolation;
ProgramAttrib attrib{.shaders = Move(shaders),
.explicitVertexInLocations = in.explicitAttribLocations,
.explicitFragmentOutLocations = in.explicitFragDataLocation,
.explicitFragmentOutIndices = in.explicitFragDataIndex,
.explicitOpaqueUniformBindings = &artifacts.explicitOpaqueUniformBindings,
.storageBlocksWithoutBinding = &artifacts.storageBlocksWithoutBinding,
.uniformBlocksWithoutBinding = &artifacts.uniformBlocksWithoutBinding};
.uniformBlocksWithoutBinding = &artifacts.uniformBlocksWithoutBinding,
.resourceBindingLimits = in.env ? ResolveResourceBindingLimits(*in.env)
: MG_Util::ShaderTranspiler::ResourceBindingLimits{},
.resourceBindingViolation = &resourceBindingViolation};
MGLOG_D("ProgramObject %u: Calling ShaderCompiler::LinkProgram", in.externalIndex);
auto result = ShaderCompiler::LinkProgram(attrib);
@@ -492,6 +492,10 @@ namespace MobileGL::MG_State::GLState {
// time, for anything cached during the pending window itself.)
++m_backendStateVersion;
BumpLinkObservableVersions();
// The separable flag takes effect HERE, at the link, and nowhere else (GL 4.6 core 7.3).
// Latched before the early-outs below so a link that fails still counts as a link -
// what must not update it is a link that never happened at all.
m_linkedSeparable = m_separable;
// A whole-struct reset, unlike ResetLinkArtifacts(): during the pending window this
// is what every gated reader sees, so it has to be the complete "not linked" state -
// including the fields ResetLinkArtifacts deliberately preserves for its own callers.
@@ -915,6 +915,14 @@ namespace MobileGL::MG_State::GLState {
// subset of the stages of a program pipeline. Only takes effect on the next link,
// which is why it is plain state here rather than something Link() consults.
Bool GetSeparable() const { return m_separable; }
// What GL_PROGRAM_SEPARABLE actually reports, and what glUseProgramStages actually
// requires: the value the flag held at the program's LAST LINK, not the live flag.
// GL 4.6 core 7.3 - "the flag takes effect the next time the program is linked" - so a
// program that was told to be separable and then never linked is still NOT separable,
// which is precisely what es31cSeparateShaderObjsTests's PipelineApi and CreateShadProgApi
// assert. The live flag stays available as GetSeparable() for glGetProgramiv's sibling
// state and for the next link to latch.
Bool GetLinkedSeparable() const { return m_linkedSeparable; }
void SetSeparable(Bool separable) {
m_separable = separable;
// ---- arming the uniform-write tracking latch ----
@@ -1528,6 +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; }
GLenum GetTransformFeedbackBufferMode() const { return Artifacts().xfbBufferMode; }
SizeT GetTransformFeedbackVaryingCount() const { return Artifacts().xfbVaryings.size(); }
const XfbVarying* GetTransformFeedbackVarying(SizeT index) const {
@@ -1703,6 +1719,11 @@ namespace MobileGL::MG_State::GLState {
Bool m_deleteStatus = false;
Bool m_binaryRetrievableHint = false;
Bool m_separable = false;
// m_separable as of the last link; see GetLinkedSeparable. Latched by Link() rather than
// carried in LinkArtifacts because it is a GL-thread-owned decision made at enqueue time,
// not a result the worker computes - and because a FAILED link still latches it, exactly
// as a successful one does.
Bool m_linkedSeparable = false;
// Monotone "this program may ever be a pipeline stage" latch; see SetSeparable for why
// it is a latch and not just m_separable. Outside LinkArtifacts on purpose: a relink
// clears the write SET, but a program that was separable is still separable after it.
@@ -140,17 +140,21 @@ namespace {
return std::nullopt;
}
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers, recomputed rather than
// queried: the compile runs on a worker with no context, and the pname is not a plain backend
// parameter - the getter caps the backend's count by the state layer's fixed binding-point
// array (GL_Getter's GetIndexedBufferQueryPointCount). A shader must be judged against the
// number the application was told, not against either half of it.
// What glGetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS) answers. Derived by the shared
// ResolveResourceBindingLimits so the compile-time scan below and the link-time general check
// (TMglGlslIoResolver::CheckDeclaredBindingRange) can never disagree about the number.
//
// Why BOTH still exist. GLSL makes an over-range binding a COMPILE-time error, and this scan
// is the only place MobileGL can raise one - glslang's own ceilings are switched off by the
// relaxed Vulkan parse and cannot be turned back on without changing the parse everything
// else depends on. The link-time check covers the four kinds a lexical scan of unexpanded
// source cannot see at all (samplers, images, uniform blocks, atomic counters, whose binding
// only survives inside a synthesized block NAME) and re-covers storage blocks as a backstop.
// The conformance predicate is compile AND link, so either site satisfies it; the split is
// about WHICH error GL reports, not about whether the shader is rejected.
static MobileGL::Int MaxShaderStorageBufferBindings(
const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) {
const MobileGL::Int frontendPoints =
static_cast<MobileGL::Int>(MobileGL::MG_State::GLState::BufferBindingPointCount);
if (!env.HasBackend()) return frontendPoints;
return std::min<MobileGL::Int>(frontendPoints, std::max<MobileGL::Int>(env.params.MaxShaderStorageBufferBindings, 0));
return MobileGL::MG_State::GLState::ResolveResourceBindingLimits(env).MaxShaderStorageBufferBindings;
}
// The half of a compile that depends on nothing but the source text, the stage and the
@@ -10,9 +10,48 @@
#include <Includes.h>
#include <MG_Util/Async/JobNode.h>
#include <MG_Util/ShaderTranspiler/CompileEnv.h>
#include <MG_Util/ShaderTranspiler/Types.h>
#include <MG_State/GLState/BufferState/BufferState.h>
#include <MG_State/GLState/ProgramState/ShaderPreprocessCache.h>
namespace MobileGL::MG_State::GLState {
// THE one derivation of the binding ceilings a shader-declared layout(binding = N) is judged
// against. Two readers have to agree on them - the compile-time storage-block scan below and
// the link-time general check in TMglGlslIoResolver - and the numbers are recomputed here
// rather than queried because both readers run on a worker with no context.
//
// Each is exactly what glGetIntegerv answers for the matching pname, and none of them is a
// plain backend parameter: the buffer families are additionally capped by the state layer's
// indexed-binding array (GL_Getter's GetIndexedBufferQueryPointCount does the same), because
// a shader must be judged against the number the APPLICATION was told, not against either
// half of it. Lives in MG_State rather than in MG_Util/ShaderTranspiler/Types.h purely
// because BufferBindingPointCount is state-layer knowledge that the transpiler layer must
// not reach up for.
inline MG_Util::ShaderTranspiler::ResourceBindingLimits ResolveResourceBindingLimits(
const MG_Util::ShaderTranspiler::CompileEnv& env) {
namespace ST = MG_Util::ShaderTranspiler;
ST::ResourceBindingLimits limits;
const Int bindingPoints = static_cast<Int>(BufferBindingPointCount);
// The atomic-counter ceiling is a frontend constant, so it holds even with no backend -
// and it is the number BuildTBuiltInResource compiles a layout(binding = N) atomic_uint
// against, which is what makes it enforceable at all.
limits.MaxAtomicCounterBufferBindings = std::min<Int>(bindingPoints, ST::MAX_ATOMIC_COUNTER_BUFFER_BINDINGS);
// So is the uniform-buffer one: GL_MAX_UNIFORM_BUFFER_BINDINGS is clamped to the indexed
// binding array in the getter and its floor (the GL 4.5 core minimum of 84) is that same
// array's width, so the backend's own number never moves it.
limits.MaxUniformBufferBindings = bindingPoints;
if (!env.HasBackend()) {
// No backend: the two backend-derived ceilings have nothing to be measured against,
// and zero means "do not enforce this kind" rather than "reject everything".
return limits;
}
limits.MaxSamplerBindings = std::max<Int>(env.params.MaxCombinedTextureImageUnits, 0);
limits.MaxImageBindings = std::max<Int>(env.params.MaxImageUnits, 0);
limits.MaxShaderStorageBufferBindings =
std::min<Int>(bindingPoints, std::max<Int>(env.params.MaxShaderStorageBufferBindings, 0));
return limits;
}
// glslang has no "detach this thread" API in the vendored revision, but TShader::parse
// leaves the calling thread's TLS pool allocator pointing at the shader's own pool and
// never restores it. Left there, the next allocation this thread makes - in an unrelated