diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 7d04c5e7..16acd253 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -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& 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 diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp index 24fb005a..940bb189 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_Program.cpp @@ -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("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 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)) { diff --git a/MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp b/MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp index 3a462127..f9c461e4 100644 --- a/MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp +++ b/MobileGL/MG_Impl/GLImpl/Program/GL_ProgramPipeline.cpp @@ -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; diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index 36134081..9cb021e5 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -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("MG_Impl/GLImpl", caller, + std::format("target 0x{:X} is not GL_TEXTURE_BUFFER.", target))); + return false; + } + static void AttachBufferToTexture(const SharedPtr& 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( + "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("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(size < 0 ? 0 : size), __func__); } diff --git a/MobileGL/MG_State/GLState/Core.cpp b/MobileGL/MG_State/GLState/Core.cpp index 6f8a8257..75076420 100644 --- a/MobileGL/MG_State/GLState/Core.cpp +++ b/MobileGL/MG_State/GLState/Core.cpp @@ -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(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); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index acad322c..c05aa9b0 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -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); diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp index 85e27db1..90c0a9b3 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.cpp @@ -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. diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h index 08d81e0c..bad574f1 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramObject.h @@ -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& 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. diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp index bc34f77d..3d0b99ab 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.cpp @@ -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::MG_State::GLState::BufferBindingPointCount); - if (!env.HasBackend()) return frontendPoints; - return std::min(frontendPoints, std::max(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 diff --git a/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.h b/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.h index a95bd11c..161452e0 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.h +++ b/MobileGL/MG_State/GLState/ProgramState/ShaderCompileTask.h @@ -10,9 +10,48 @@ #include #include #include +#include +#include #include 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(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(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(env.params.MaxCombinedTextureImageUnits, 0); + limits.MaxImageBindings = std::max(env.params.MaxImageUnits, 0); + limits.MaxShaderStorageBufferBindings = + std::min(bindingPoints, std::max(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 diff --git a/MobileGL/MG_Test/Program/ProgramTest.cpp b/MobileGL/MG_Test/Program/ProgramTest.cpp index b42c40cd..67d4e9b8 100644 --- a/MobileGL/MG_Test/Program/ProgramTest.cpp +++ b/MobileGL/MG_Test/Program/ProgramTest.cpp @@ -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(); +} diff --git a/MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp b/MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp index 6459a0e7..efc59894 100644 --- a/MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp +++ b/MobileGL/MG_Test/State/NegativeApiErrorsTest.cpp @@ -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}, }); } diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index e9942617..0b7ba2de 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -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::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; diff --git a/MobileGL/MG_Util/ShaderTranspiler/Types.h b/MobileGL/MG_Util/ShaderTranspiler/Types.h index bb146b3a..a8ef9a91 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/Types.h +++ b/MobileGL/MG_Util/ShaderTranspiler/Types.h @@ -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> shaders; UnorderedMap explicitVertexInLocations; @@ -160,6 +187,11 @@ namespace MobileGL { UnorderedMap* explicitOpaqueUniformBindings = nullptr; std::set* storageBlocksWithoutBinding = nullptr; std::set* 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 { diff --git a/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp b/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp index 0bfec581..70596c5f 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.cpp @@ -13,6 +13,8 @@ #include "TMglGlslIoResolver.h" #include +#include +#include #include @@ -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_" 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(type.getCumulativeArraySize()); + if (cumulative > 1) elementCount = cumulative; + } + const long long lastBinding = binding + elementCount - 1; + if (lastBinding < static_cast(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); } diff --git a/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h b/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h index d8427327..f2fd0e35 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h +++ b/MobileGL/MG_Util/ShaderTranspiler/glslang/TMglGlslIoResolver.h @@ -21,27 +21,35 @@ #include #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; + using ResourceBindingLimits = MG_Util::ShaderTranspiler::ResourceBindingLimits; TMglGlslIoResolver(const glslang::TIntermediate& intermediate, const ExplicitVarSlotMap& vertexIns, const ExplicitVarSlotMap& fragOuts, const ExplicitVarSlotMap& fragOutIndices, ExplicitVarSlotMap* opaqueUniformBindings, std::set* storageBlocksWithoutBinding = nullptr, - std::set* uniformBlocksWithoutBinding = nullptr) + std::set* 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* storageBlocksWithoutBinding = nullptr, - std::set* uniformBlocksWithoutBinding = nullptr) + std::set* 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* 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 m_plainUniformLocationSizeByName; std::map m_plainUniformLocationByName; bool m_plainUniformLocationsAssigned = false;