mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-07 19:58:32 +09:00
[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:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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__);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user