[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
@@ -40,6 +40,15 @@ namespace MobileGL::MG_Impl::GLImpl {
// below resolves it exactly once and hands it to both users.
static Bool ValidateResolvedProgramForDraw(const SharedPtr<MG_State::GLState::ProgramObject>& currentProgram,
const char* functionName) {
// "If there is no current program object or bound program pipeline object, the results of
// a draw are UNDEFINED" - and undefined is not an error (GL 4.6 core 7.3, ES 3.1 7.3).
// The draw is dropped, silently, which is one of the shapes "undefined" is allowed to
// take; recording INVALID_OPERATION here is not, and es31cSeparateShaderObjsTests'
// StateInteraction reads exactly that error back after useProgram(0) + bindProgramPipeline(0).
// A DISPATCH is the opposite rule ("INVALID_OPERATION if there is no active program for
// the compute shader stage"), which is why this lives on the draw path and not in the
// shared ValidateProgramForExecution below.
if (!currentProgram) return false;
if (!ValidateProgramForExecution(currentProgram, functionName)) return false;
// GL 4.6 core 7.4.1, the pipeline validation rule every vertex-transferring command
+22 -3
View File
@@ -768,7 +768,10 @@ namespace MobileGL::MG_Impl::GLImpl {
*params = programObject->GetBinaryRetrievableHint() ? GL_TRUE : GL_FALSE;
break;
case GL_PROGRAM_SEPARABLE:
*params = programObject->GetSeparable() ? GL_TRUE : GL_FALSE;
// The LATCHED flag, not the live one: glProgramParameteri's write takes effect at the
// next link (GL 4.6 core 7.3), so a program told to be separable and then never
// linked still reports GL_FALSE.
*params = programObject->GetLinkedSeparable() ? GL_TRUE : GL_FALSE;
break;
// The geometry and tessellation link properties (GL 4.6 core table 23.35). Same shape as
@@ -951,6 +954,16 @@ namespace MobileGL::MG_Impl::GLImpl {
GLint GetUniformLocation_State(GLuint program, const GLchar* name) {
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return -1;
// GL 4.6 core 7.6: "INVALID_OPERATION is generated if program has not been successfully
// linked". Answering -1 silently is not the same thing - the conformance suite reads the
// error, not the location.
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
"program " + std::to_string(program) + " is not linked."));
return -1;
}
auto loc = programObject->GetUniformLocation(name);
MGLOG_D("%s: loc %02d = %s", __func__, loc, name);
return loc;
@@ -1363,11 +1376,13 @@ namespace MobileGL::MG_Impl::GLImpl {
template <GLsizei ItemCount, typename T>
void ProgramUniformv_State(GLuint program, GLint location, GLsizei count, T* value) {
if (location == -1) return;
auto& programObject = TryToGetProgramObject(program);
if (!programObject) return;
// The link check comes BEFORE the location == -1 early-out, not after. GL 4.6 core 7.6
// makes an unlinked program INVALID_OPERATION regardless of the location, and -1 is
// exactly the location an application holds after glGetUniformLocation on such a program -
// so checking -1 first swallowed the very case the rule exists for.
if (!programObject->GetLinkStatus()) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidOperation,
@@ -1375,6 +1390,10 @@ namespace MobileGL::MG_Impl::GLImpl {
"program " + std::to_string(program) + " is not linked."));
return;
}
// "If location is equal to -1, the data passed in will be silently ignored and the
// specified uniform variable will not be changed" - after the program itself has been
// found acceptable.
if (location == -1) return;
for (GLint offset = 0; offset < count; offset++) {
if (offset > 0 && !programObject->UniformLocationsAliasSameUniform(location, location + offset)) {
@@ -192,6 +192,15 @@ namespace MobileGL::MG_Impl::GLImpl {
std::format("Program {} has not been linked successfully.", program));
return;
}
// GL 4.6 core 7.4: "INVALID_OPERATION is generated if program was not linked with its
// PROGRAM_SEPARABLE status set". The LATCHED flag is the one that decides - a program
// whose live flag was cleared after a separable link is still a legal stage, and a
// program whose live flag was set after a non-separable link is not.
if (!programObject->GetLinkedSeparable()) {
RecordPipelineError(ErrorCode::InvalidOperation, __func__,
std::format("Program {} was not linked as a separable program.", program));
return;
}
}
const GLbitfield selected = stages == GL_ALL_SHADER_BITS ? kAllStageBits : stages;
+35 -1
View File
@@ -2649,6 +2649,22 @@ namespace MobileGL::MG_Impl::GLImpl {
}
}
// GL 4.6 core 8.9 / GL_EXT_texture_buffer: the two TARGET-taking forms (glTexBuffer,
// glTexBufferRange) accept exactly GL_TEXTURE_BUFFER, and anything else is GL_INVALID_ENUM.
// Checked up front rather than left to fall out of "the bound object is not a buffer texture"
// deeper in, because that path's error code depends on which entry point took it - the
// name-taking DSA forms owe GL_INVALID_OPERATION for the same shape - and because for some
// targets it did not reach that check at all. esextcTextureBufferErrors walks every other
// texture target through both entry points and reads the code back each time.
static Bool ValidateBufferTextureTarget(GLenum target, const char* caller) {
if (target == GL_TEXTURE_BUFFER) return true;
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", caller,
std::format("target 0x{:X} is not GL_TEXTURE_BUFFER.", target)));
return false;
}
static void AttachBufferToTexture(const SharedPtr<MG_State::GLState::ITextureObject>& textureObject,
GLenum internalformat, GLuint buffer, GLintptr offset, SizeT size,
const char* caller) {
@@ -2731,9 +2747,21 @@ namespace MobileGL::MG_Impl::GLImpl {
TextureInternalFormat textureInternalFormat = MG_Util::ConvertGLEnumToTextureInternalFormat(internalformat);
// ===================== Error Checking ==============================
if (!ValidateBufferTextureTarget(target, __func__)) return;
if (!TextureImpl::ValidateTextureUploadTarget(textureUploadTarget)) return;
if (!TextureImpl::ValidateTextureInternalFormat(textureInternalFormat)) return;
// TODO: make sure `internalformat` is in one of supported format for TexBuffer
// The sized-format table a buffer texture accepts (GL 4.6 core table 8.15). The DSA and
// range forms have always run this through AttachBufferToTexture; this one carried a TODO
// instead, so glTexBuffer(GL_TEXTURE_BUFFER, GL_DEPTH_COMPONENT32F, ...) succeeded.
if (!IsBufferTextureInternalFormat(internalformat)) {
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>(
"MG_Impl/GLImpl", __func__,
std::format("internalformat 0x{:X} is not one of the sized formats a buffer texture accepts.",
internalformat)));
return;
}
// GL 3.3 core 3.8.5: buffer zero detaches any buffer from the buffer texture - only a
// nonzero name that is not an existing buffer object is an error. This is reachable on
// the default buffer texture (bound whenever texture 0 is bound to GL_TEXTURE_BUFFER),
@@ -2757,6 +2785,8 @@ namespace MobileGL::MG_Impl::GLImpl {
// silent no-op; the slot is never empty now that every unit/target holds its default.
if (!TextureImpl::ValidateTextureObject(textureObject)) return;
if (textureObject->GetStorageType() != TextureStorageType::Buffer) {
// Defensive: the target gate above already rejected every target but GL_TEXTURE_BUFFER,
// whose binding slot only ever holds buffer textures.
MG_State::pGLContext->RecordError(
ErrorCode::InvalidEnum,
MakeUnique<GenericErrorInfo>("MG_Impl/GLImpl", __func__,
@@ -6593,6 +6623,10 @@ namespace MobileGL::MG_Impl::GLImpl {
}
void TexBufferRange(GLenum target, GLenum internalformat, GLuint buffer, GLintptr offset, GLsizeiptr size) {
// The TARGET-taking form owes GL_INVALID_ENUM for a target that is not GL_TEXTURE_BUFFER,
// where the name-taking DSA forms below owe GL_INVALID_OPERATION for the corresponding
// "that texture is not a buffer texture". Same shared body, different gate.
if (!ValidateBufferTextureTarget(target, __func__)) return;
AttachBufferToTexture(GetBoundBufferTexture(target, __func__), internalformat, buffer, offset,
static_cast<SizeT>(size < 0 ? 0 : size), __func__);
}
+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
+227
View File
@@ -19,6 +19,7 @@
#include "MG_Backend/BackendObjects.h"
#include "MG_Impl/GLImpl/Getter/GL_Getter.h"
#include "MG_Impl/GLImpl/Program/GL_Program.h"
#include "MG_Impl/GLImpl/Program/GL_ProgramPipeline.h"
#include "MG_State/GLState/Core.h"
#include "MG_State/GLState/ProgramState/ShaderPreprocessCache.h"
#include "MG_Util/Async/ShaderCompilePool.h"
@@ -4136,3 +4137,229 @@ TEST_F(ProgramTest, ContextWideTessellationPropertiesAnswerEveryWidth) {
EXPECT_EQ(restart, GL_FALSE);
EXPECT_EQ(GetError(), GL_NO_ERROR);
}
// ---------------------------------------------------------------------------------------------
// The binding-range rule (GLSL 4.30 4.4.5 / ES 3.1 4.4.4): layout(binding = N) at or above the
// resource kind's implementation limit is an error. glslang cannot enforce it for MobileGL - it
// owns ceilings for samplers/images and for atomic counters and the relaxed Vulkan parse switches
// both OFF, and for uniform and storage BLOCKS it has no ceiling at all - so MobileGL enforces it
// itself, at the link, from TMglGlslIoResolver::CheckDeclaredBindingRange. es31cLayoutBindingTests
// accepts a link-time rejection: its predicate, compiledAndLinked(), is the AND of the two.
//
// The two ceilings asserted here are frontend constants, so they hold with no backend active,
// which is what makes them testable in this GPU-free binary. The sampler and image ceilings are
// backend-derived and read zero here, i.e. "do not enforce"; their arm is the same code path.
// ---------------------------------------------------------------------------------------------
namespace {
// Links a compute program from one source and returns its LINK_STATUS. Compute, because every
// kind this rule covers can be declared in a compute shader and nothing else has to be
// supplied alongside it.
GLint LinkComputeProgramStatus(const char* source, String* outInfoLog = nullptr) {
char infoLog[2048] = "";
const GLuint shader = CreateShader(GL_COMPUTE_SHADER);
ShaderSource(shader, 1, &source, nullptr);
CompileShader(shader);
GLint compileStatus = GL_FALSE;
GetShaderiv(shader, GL_COMPILE_STATUS, &compileStatus);
if (compileStatus != GL_TRUE) {
GetShaderInfoLog(shader, sizeof(infoLog), nullptr, infoLog);
if (outInfoLog) *outInfoLog = infoLog;
// A compile-time rejection satisfies the same rule; report it as "not linked".
return GL_FALSE;
}
const GLuint program = CreateProgram();
AttachShader(program, shader);
LinkProgram(program);
GLint linkStatus = GL_FALSE;
GetProgramiv(program, GL_LINK_STATUS, &linkStatus);
GetProgramInfoLog(program, sizeof(infoLog), nullptr, infoLog);
if (outInfoLog) *outInfoLog = infoLog;
return linkStatus;
}
} // namespace
TEST_F(ProgramTest, UniformBlockBindingAtTheLimitIsRejected) {
DrainProgramTestErrors();
GLint maxUniformBufferBindings = 0;
GetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxUniformBufferBindings);
ASSERT_GT(maxUniformBufferBindings, 0);
const String legal = "#version 430 core\nlayout(local_size_x = 1) in;\nlayout(binding = " +
std::to_string(maxUniformBufferBindings - 1) +
", std140) uniform Blk { vec4 v; } blk;\nvoid main() { }\n";
EXPECT_EQ(LinkComputeProgramStatus(legal.c_str()), GL_TRUE)
<< "the last binding in the range is legal and must still link";
String infoLog;
const String overRange = "#version 430 core\nlayout(local_size_x = 1) in;\nlayout(binding = " +
std::to_string(maxUniformBufferBindings) +
", std140) uniform Blk { vec4 v; } blk;\nvoid main() { }\n";
EXPECT_EQ(LinkComputeProgramStatus(overRange.c_str(), &infoLog), GL_FALSE)
<< "a uniform block binding at GL_MAX_UNIFORM_BUFFER_BINDINGS must be rejected";
EXPECT_NE(infoLog.find("GL_MAX_UNIFORM_BUFFER_BINDINGS"), String::npos)
<< "the info log must name the limit the declaration broke; got: " << infoLog;
DrainProgramTestErrors();
}
TEST_F(ProgramTest, AtomicCounterBindingAtTheLimitIsRejected) {
DrainProgramTestErrors();
GLint maxAtomicBindings = 0;
GetIntegerv(GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, &maxAtomicBindings);
ASSERT_GT(maxAtomicBindings, 0);
const String legal = "#version 430 core\nlayout(local_size_x = 1) in;\nlayout(binding = " +
std::to_string(maxAtomicBindings - 1) +
") uniform atomic_uint counter;\nvoid main() { atomicCounterIncrement(counter); }\n";
EXPECT_EQ(LinkComputeProgramStatus(legal.c_str()), GL_TRUE)
<< "the last counter binding in the range is legal and must still link";
String infoLog;
const String overRange = "#version 430 core\nlayout(local_size_x = 1) in;\nlayout(binding = " +
std::to_string(maxAtomicBindings) +
") uniform atomic_uint counter;\nvoid main() { atomicCounterIncrement(counter); }\n";
EXPECT_EQ(LinkComputeProgramStatus(overRange.c_str(), &infoLog), GL_FALSE)
<< "an atomic_uint binding at GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS must be rejected";
DrainProgramTestErrors();
}
// The arrayed-instance half of the rule: an array of N takes base .. base + N - 1, and every one
// of them has to fit. A base that is itself legal is therefore not enough.
TEST_F(ProgramTest, ArrayedUniformBlockInstanceMustFitEntirelyBelowTheBindingLimit) {
DrainProgramTestErrors();
GLint maxUniformBufferBindings = 0;
GetIntegerv(GL_MAX_UNIFORM_BUFFER_BINDINGS, &maxUniformBufferBindings);
ASSERT_GE(maxUniformBufferBindings, 4);
const String fits = "#version 430 core\nlayout(local_size_x = 1) in;\nlayout(binding = " +
std::to_string(maxUniformBufferBindings - 4) +
", std140) uniform Blk { vec4 v; } blk[4];\nvoid main() { }\n";
EXPECT_EQ(LinkComputeProgramStatus(fits.c_str()), GL_TRUE)
<< "base + count - 1 is the last legal binding, so this array fits exactly";
const String spills = "#version 430 core\nlayout(local_size_x = 1) in;\nlayout(binding = " +
std::to_string(maxUniformBufferBindings - 3) +
", std140) uniform Blk { vec4 v; } blk[4];\nvoid main() { }\n";
EXPECT_EQ(LinkComputeProgramStatus(spills.c_str()), GL_FALSE)
<< "the array's last element is past the limit even though its base is not";
DrainProgramTestErrors();
}
// The storage-block arm still has its own COMPILE-time enforcement (the lexical scan glslang's
// relaxed parse leaves MobileGL to do), and the link-time check is a backstop for it. Both agree
// because both read ResolveResourceBindingLimits; this pins the outcome rather than the site.
TEST_F(ProgramTest, StorageBlockBindingAtTheLimitIsStillRejected) {
DrainProgramTestErrors();
GLint maxStorageBindings = 0;
GetIntegerv(GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, &maxStorageBindings);
if (maxStorageBindings <= 0) {
GTEST_SKIP() << "no storage-buffer binding points advertised in this configuration";
}
const String overRange = "#version 430 core\nlayout(local_size_x = 1) in;\nlayout(binding = " +
std::to_string(maxStorageBindings) +
", std430) buffer Blk { vec4 v; } blk;\nvoid main() { blk.v = vec4(0.0); }\n";
EXPECT_EQ(LinkComputeProgramStatus(overRange.c_str()), GL_FALSE);
DrainProgramTestErrors();
}
// ---------------------------------------------------------------------------------------------
// GL_PROGRAM_SEPARABLE is LATCHED at link (GL 4.6 core 7.3), and glUseProgramStages tests the
// latched flag, not the live one.
// ---------------------------------------------------------------------------------------------
TEST_F(ProgramTest, ProgramSeparableIsLatchedAtLinkNotReportedLive) {
DrainProgramTestErrors();
const GLuint program = CreateProgram();
GLint separable = GL_TRUE;
GetProgramiv(program, GL_PROGRAM_SEPARABLE, &separable);
EXPECT_EQ(separable, GL_FALSE) << "a fresh program is not separable";
// Requested but never linked: the request has not taken effect yet.
ProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_TRUE);
GetProgramiv(program, GL_PROGRAM_SEPARABLE, &separable);
EXPECT_EQ(separable, GL_FALSE) << "GL_PROGRAM_SEPARABLE takes effect at the NEXT link";
EXPECT_EQ(GetError(), GL_NO_ERROR);
// Link it, and the request lands.
const char* vsSource = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }\n";
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSource, nullptr);
CompileShader(vs);
AttachShader(program, vs);
LinkProgram(program);
GetProgramiv(program, GL_PROGRAM_SEPARABLE, &separable);
EXPECT_EQ(separable, GL_TRUE);
// Clearing the live flag does not un-separate the EXECUTABLE that was already linked.
ProgramParameteri(program, GL_PROGRAM_SEPARABLE, GL_FALSE);
GetProgramiv(program, GL_PROGRAM_SEPARABLE, &separable);
EXPECT_EQ(separable, GL_TRUE) << "the latched flag only moves at a link";
DrainProgramTestErrors();
}
TEST_F(ProgramTest, UseProgramStagesRequiresAProgramLinkedAsSeparable) {
DrainProgramTestErrors();
const char* vsSource = "#version 330 core\nvoid main() { gl_Position = vec4(0.0); }\n";
const GLuint vs = CreateShader(GL_VERTEX_SHADER);
ShaderSource(vs, 1, &vsSource, nullptr);
CompileShader(vs);
// Linked, but NOT as a separable program.
const GLuint monolithic = CreateProgram();
AttachShader(monolithic, vs);
LinkProgram(monolithic);
GLint linkStatus = GL_FALSE;
GetProgramiv(monolithic, GL_LINK_STATUS, &linkStatus);
ASSERT_EQ(linkStatus, GL_TRUE);
DrainProgramTestErrors();
GLuint pipeline = 0;
GenProgramPipelines(1, &pipeline);
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, monolithic);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION)
<< "GL 4.6 core 7.4: the program must have been LINKED with PROGRAM_SEPARABLE set";
// The same program, relinked as separable, is accepted.
ProgramParameteri(monolithic, GL_PROGRAM_SEPARABLE, GL_TRUE);
LinkProgram(monolithic);
DrainProgramTestErrors();
UseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, monolithic);
EXPECT_EQ(GetError(), GL_NO_ERROR);
DeleteProgramPipelines(1, &pipeline);
DrainProgramTestErrors();
}
// GL 4.6 core 7.6: an unlinked program is GL_INVALID_OPERATION for both of these, and
// glProgramUniform*'s location == -1 early-out must not swallow it - -1 is exactly what an
// application holds after asking an unlinked program for a location.
TEST_F(ProgramTest, UniformEntryPointsRejectAnUnlinkedProgram) {
DrainProgramTestErrors();
const GLuint program = CreateProgram();
EXPECT_EQ(GetUniformLocation(program, "uAnything"), -1);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
const GLfloat value = 1.0f;
ProgramUniform1fv(program, -1, 1, &value);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION)
<< "the link check has to run BEFORE the location == -1 early-out";
ProgramUniform1f(program, 0, 1.0f);
EXPECT_EQ(GetError(), GL_INVALID_OPERATION);
DrainProgramTestErrors();
}
@@ -536,10 +536,15 @@ void main() { g_color = vec4(1); }
[] { DrawElementsIndirect(kBadMode, GL_UNSIGNED_INT, nullptr); }, GL_INVALID_ENUM},
{"glDrawArraysIndirect with an unaccepted mode", [] { DrawArraysIndirect(kBadMode, nullptr); },
GL_INVALID_ENUM},
// A mode the enum check accepts falls through to the guard, so the INVALID_OPERATION
// that used to win is still raised for the calls it is actually about.
// A mode the enum check accepts falls through to the no-program path, which is now
// a SILENT drop rather than an error: GL 4.6 core 7.3 and ES 3.1 7.3 both make a draw
// with no current program and no bound pipeline UNDEFINED, not erroneous, and
// es31cSeparateShaderObjsTests.StateInteraction reads glGetError() straight after
// useProgram(0) + bindProgramPipeline(0) + glDrawElements and requires GL_NO_ERROR.
// Dropping the draw is one of the shapes "undefined" may take; inventing an error is
// not. The enum check above still outranks it, which is what this case is really for.
{"glDrawArrays with a legal mode and no program bound", [] { DrawArrays(GL_TRIANGLES, 0, 3); },
GL_INVALID_OPERATION},
GL_NO_ERROR},
});
}
@@ -548,12 +548,28 @@ namespace MobileGL {
attrib.explicitFragmentOutIndices,
attrib.explicitOpaqueUniformBindings,
attrib.storageBlocksWithoutBinding,
attrib.uniformBlocksWithoutBinding);
attrib.uniformBlocksWithoutBinding,
&attrib.resourceBindingLimits,
attrib.resourceBindingViolation);
break;
}
auto ioMapper = UniquePtr<glslang::TIoMapper>(glslang::GetGlslIoMapper());
if (!program->mapIO(resolver.get(), ioMapper.get())) {
const bool mapped = program->mapIO(resolver.get(), ioMapper.get());
// The binding-range verdict is read BEFORE mapIO's own outcome, and unconditionally:
// the resolver fills it during the collect phase, which runs whether or not doMap()
// later succeeds, and a shader that names an out-of-range binding is rejected for
// THAT reason no matter what else the mapper made of it. Reporting the mapper's
// generic failure instead would hand the application an info log that says nothing
// about the declaration it has to fix.
if (attrib.resourceBindingViolation != nullptr && !attrib.resourceBindingViolation->empty()) {
ResultInfo r;
r.log = *attrib.resourceBindingViolation;
r.errc = -5;
return std::unexpected(r);
}
if (!mapped) {
ResultInfo r;
r.log = "Error: [glslang] Cannot mapIO:\n" + std::string(program->getInfoLog());
r.errc = -4;
+32
View File
@@ -145,6 +145,33 @@ namespace MobileGL {
const CompileEnv* env = nullptr;
};
// The per-device ceilings a shader-declared `layout(binding = N)` is measured
// against - one per resource kind, because GL gives each kind its own limit and they
// differ by an order of magnitude on real hardware (a Mali-G925 reports 96 combined
// texture image units and 21 image units).
//
// These exist because glslang cannot enforce them for MobileGL. It owns ceilings for
// samplers/images and for atomic counters, and both are switched OFF by the parse
// configuration MobileGL uses everywhere - `spvVersion.vulkan == 0` gates the first
// and `!spvVersion.vulkanRelaxed` the second (ParseHelper.cpp layoutTypeCheck), and
// MobileGL always parses with setEnvClient(EShClientVulkan) +
// setEnvInputVulkanRulesRelaxed(). For uniform and storage BLOCKS glslang quotes the
// spec sentence and then checks nothing at all. Flipping to the OpenGL client to wake
// those checks is not an option (it would change the parse the whole relaxed
// lowering pipeline is built on) and would not even be correct: glslang measures
// IMAGE bindings against the SAMPLER limit and hardcodes that limit at 80, so it
// would reject legal bindings 80..95 and keep under-rejecting images.
//
// Zero or negative means "no ceiling to enforce for this kind" - a backendless
// environment, which every unit test and the pre-init preload path run in.
struct ResourceBindingLimits {
Int MaxSamplerBindings = 0; // GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS
Int MaxImageBindings = 0; // GL_MAX_IMAGE_UNITS
Int MaxUniformBufferBindings = 0; // GL_MAX_UNIFORM_BUFFER_BINDINGS
Int MaxShaderStorageBufferBindings = 0; // GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS
Int MaxAtomicCounterBufferBindings = 0; // GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS
};
struct ProgramAttrib {
Vector<SharedPtr<glslang::TShader>> shaders;
UnorderedMap<String, Uint> explicitVertexInLocations;
@@ -160,6 +187,11 @@ namespace MobileGL {
UnorderedMap<String, Uint>* explicitOpaqueUniformBindings = nullptr;
std::set<String>* storageBlocksWithoutBinding = nullptr;
std::set<String>* uniformBlocksWithoutBinding = nullptr;
// IN: the ceilings above. OUT: the first violation the resolver found, in the
// same capture window and for the same reason - past mapIO's doMap() every
// resource carries an ASSIGNED binding and the question can no longer be asked.
ResourceBindingLimits resourceBindingLimits{};
String* resourceBindingViolation = nullptr;
};
struct ProgramBinaryAttrib {
@@ -13,6 +13,8 @@
#include "TMglGlslIoResolver.h"
#include <cstring>
#include <cstdlib>
#include <string>
#include <MG_Util/ShaderTranspiler/Types.h>
@@ -165,6 +167,88 @@ namespace MobileGL {
// before the preprocessor's macros were expanded and therefore could not read
// `binding = SOME_MACRO` - the spelling Flywheel's indirect engine uses for every one of
// its storage blocks. Asking the AST instead makes the macro case ordinary.
// GLSL 4.30 4.4.5 and ES 3.1 4.4.4: `layout(binding = N)` on any opaque uniform, uniform
// block, storage block or atomic counter is a COMPILE-TIME error when N is not less than that
// resource kind's implementation limit - and, for an ARRAY of them, when base + count - 1 is
// not. MobileGL enforces it here rather than at compile because here is the last point where
// `qualifier.hasBinding()` still means "the SHADER said so" (see the comment on the caller),
// and because the per-device ceilings are deliberately not part of the compile pipeline's
// memo keys. The conformance suite accepts a link-time rejection: its predicate is
// compiledAndLinked(), which is the AND of the two.
//
// ONE enforcement point for all five kinds, on purpose. Before this, exactly one kind -
// shader-storage blocks - was checked, by a bespoke lexical scan of the shader source, which
// is why the storage sub-family was the one that passed while sampler, image, uniform-block
// and atomic-counter bindings sailed past every ceiling. That scanner is retired; a second
// enforcement point is a second thing to drift.
void TMglGlslIoResolver::CheckDeclaredBindingRange(const glslang::TType& type, const glslang::TString& name) {
if (m_bindingLimits == nullptr || m_bindingViolation == nullptr) return;
if (!m_bindingViolation->empty()) return; // first violation wins; the link is already lost
const glslang::TQualifier& qualifier = type.getQualifier();
const char* kind = nullptr;
const char* limitName = nullptr;
Int limit = 0;
long long binding = -1;
if (type.getBasicType() == glslang::EbtSampler && qualifier.hasBinding()) {
const bool isImage = type.getSampler().isImage();
kind = isImage ? "image" : "sampler";
limitName = isImage ? "GL_MAX_IMAGE_UNITS" : "GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS";
limit = isImage ? m_bindingLimits->MaxImageBindings : m_bindingLimits->MaxSamplerBindings;
binding = qualifier.layoutBinding;
} else if (type.getBasicType() == glslang::EbtBlock) {
// An atomic counter never reaches here as a counter: the relaxed parse has already
// folded it into a synthesized "gl_AtomicCounterBlock_<binding>" storage block whose
// TRAILING NUMBER is the GL binding the shader asked for (ParseContextBase::
// growAtomicCounterBlock names it from bufferBinding). That name is the only surviving
// record of the declaration, so it is what the counter ceiling is read off.
const Int counterBinding = MG_Util::ShaderTranspiler::AtomicCounterBlockGlBinding(
StringView(name.c_str(), name.size()));
if (counterBinding >= 0) {
kind = "atomic_uint";
limitName = "GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS";
limit = m_bindingLimits->MaxAtomicCounterBufferBindings;
binding = counterBinding;
} else if (qualifier.hasBinding() && qualifier.storage == glslang::EvqUniform &&
name.compare(MG_Util::ShaderTranspiler::GLOBAL_UBO_NAME) != 0) {
kind = "uniform block";
limitName = "GL_MAX_UNIFORM_BUFFER_BINDINGS";
limit = m_bindingLimits->MaxUniformBufferBindings;
binding = qualifier.layoutBinding;
} else if (qualifier.hasBinding() && qualifier.storage == glslang::EvqBuffer) {
kind = "buffer block";
limitName = "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS";
limit = m_bindingLimits->MaxShaderStorageBufferBindings;
binding = qualifier.layoutBinding;
}
}
if (kind == nullptr || limit <= 0 || binding < 0) return;
// The ARRAYED-INSTANCE rule: an array of N takes bindings base .. base + N - 1, and every
// one of them has to fit. getCumulativeArraySize() folds a multi-dimensional array into
// the count of leaf elements, which is exactly how many consecutive bindings GL hands out.
// An unsized or implicitly-sized array reports 0; treat it as one binding rather than
// guess, since it cannot be the shape the rule is about.
long long elementCount = 1;
if (type.isArray()) {
const int cumulative = static_cast<int>(type.getCumulativeArraySize());
if (cumulative > 1) elementCount = cumulative;
}
const long long lastBinding = binding + elementCount - 1;
if (lastBinding < static_cast<long long>(limit)) return;
String message = "Error: layout(binding = " + std::to_string(binding) + ") on " + kind + " '" +
String(name.c_str()) + "'";
if (elementCount > 1) {
message += " (an array of " + std::to_string(elementCount) + ", occupying bindings " +
std::to_string(binding) + ".." + std::to_string(lastBinding) + ")";
}
message += " is not less than " + String(limitName) + " (" + std::to_string(limit) + ").";
*m_bindingViolation = Move(message);
}
void TMglGlslIoResolver::reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) {
const glslang::TType& type = ent.symbol->getType();
const glslang::TQualifier& qualifier = type.getQualifier();
@@ -207,6 +291,8 @@ namespace MobileGL {
m_uniformBlocksWithoutBinding->insert(name.c_str());
}
CheckDeclaredBindingRange(type, name);
TDefaultGlslIoResolver::reserverResourceSlot(ent, infoSink);
}
@@ -21,27 +21,35 @@
#include <glslang/MachineIndependent/iomapper.h>
#include "TVarEntryInfo.h"
#include "MG_Util/Types.h"
#include "MG_Util/ShaderTranspiler/Types.h"
namespace MobileGL {
class TMglGlslIoResolver : public glslang::TDefaultGlslIoResolver {
public:
using ExplicitVarSlotMap = UnorderedMap<String, Uint>;
using ResourceBindingLimits = MG_Util::ShaderTranspiler::ResourceBindingLimits;
TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns,
const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices,
ExplicitVarSlotMap* opaqueUniformBindings,
std::set<String>* storageBlocksWithoutBinding = nullptr,
std::set<String>* uniformBlocksWithoutBinding = nullptr)
std::set<String>* uniformBlocksWithoutBinding = nullptr,
const ResourceBindingLimits* bindingLimits = nullptr,
String* bindingViolation = nullptr)
: TDefaultGlslIoResolver(intermediate), m_explicitVertexIns(vertexIns), m_explicitFragOuts(fragOuts),
m_explicitFragOutIndices(fragOutIndices), m_explicitOpaqueUniformBindings(opaqueUniformBindings),
m_storageBlocksWithoutBinding(storageBlocksWithoutBinding),
m_uniformBlocksWithoutBinding(uniformBlocksWithoutBinding) {}
m_uniformBlocksWithoutBinding(uniformBlocksWithoutBinding), m_bindingLimits(bindingLimits),
m_bindingViolation(bindingViolation) {}
TMglGlslIoResolver(const glslang::TProgram& program, const EShLanguage stage,
const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts,
const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings,
std::set<String>* storageBlocksWithoutBinding = nullptr,
std::set<String>* uniformBlocksWithoutBinding = nullptr)
std::set<String>* uniformBlocksWithoutBinding = nullptr,
const ResourceBindingLimits* bindingLimits = nullptr,
String* bindingViolation = nullptr)
: TMglGlslIoResolver(*program.getIntermediate(stage), vertexIns, fragOuts, fragOutIndices,
opaqueUniformBindings, storageBlocksWithoutBinding, uniformBlocksWithoutBinding) {}
opaqueUniformBindings, storageBlocksWithoutBinding, uniformBlocksWithoutBinding,
bindingLimits, bindingViolation) {}
void reserverStorageSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
void reserverResourceSlot(glslang::TVarEntryInfo& ent, TInfoSink& infoSink) override;
int resolveInOutLocation(EShLanguage stage, glslang::TVarEntryInfo& ent) override;
@@ -72,6 +80,10 @@ namespace MobileGL {
// resource kind on set 0), so an unbound block declared after an unbound image lands on
// 1. See ProgramLinkTask's UBO reflection loop for what is done with them.
std::set<String>* m_uniformBlocksWithoutBinding = nullptr;
// The binding-range rule, IN and OUT. See RecordBindingRangeViolation.
const ResourceBindingLimits* m_bindingLimits = nullptr;
String* m_bindingViolation = nullptr;
void CheckDeclaredBindingRange(const glslang::TType& type, const glslang::TString& name);
std::map<glslang::TString, int> m_plainUniformLocationSizeByName;
std::map<glslang::TString, int> m_plainUniformLocationByName;
bool m_plainUniformLocationsAssigned = false;