diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index d58e2d0f..7b2cad27 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -174,6 +174,30 @@ namespace MobileGL::MG_Impl::GLImpl { return frontendCount; } + // A per-stage or combined BLOCK count is an amount of indexed binding points an + // application will occupy, and GL 4.6 table 23.64 orders the two accordingly: + // MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= every per-stage count, + // and the same for the shader-storage family. The two families are answered from + // unrelated places here - frontend constants, backend dynamic parameters, and a few + // hard-coded TODOs - so nothing kept them ordered, and a backend that reports Vulkan + // descriptor-indexing counts advertised 256 compute uniform blocks over 36 binding + // points. KHR-GL44.multi_bind.dispatch_bind_buffers_base reads the block count and binds + // that many buffers in ONE glBindBuffersBase, which is then INVALID_OPERATION before it + // binds anything. Clamping is the only direction available: the binding count is the + // capacity of the state layer's indexed-binding array, not a number we may inflate. + GLint ClampBlockCountToBindingPoints(GLint blockCount, BufferTarget bufferTarget) { + const GLint bindingPoints = static_cast(GetIndexedBufferQueryPointCount(bufferTarget)); + return std::min(std::max(blockCount, 0), bindingPoints); + } + + GLint ClampUniformBlockCount(GLint blockCount) { + return ClampBlockCountToBindingPoints(blockCount, BufferTarget::Uniform); + } + + GLint ClampStorageBlockCount(GLint blockCount) { + return ClampBlockCountToBindingPoints(blockCount, BufferTarget::ShaderStorage); + } + bool TryDecodeDrawBufferQuery(GLenum pname, SizeT& drawBufferIndex) { if (pname == GL_DRAW_BUFFER) { drawBufferIndex = 0; @@ -1397,7 +1421,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = kFrontendMaxCombinedAtomicCounters; return; case GL_MAX_COMBINED_UNIFORM_BLOCKS: - *params = kFrontendMaxCombinedUniformBlocks; + *params = ClampUniformBlockCount(kFrontendMaxCombinedUniformBlocks); return; case GL_MAX_DUAL_SOURCE_DRAW_BUFFERS: *params = 1; // TODO @@ -1412,7 +1436,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = kFrontendMaxFragmentAtomicCounters; return; case GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: - *params = 16; // TODO + *params = ClampStorageBlockCount(16); // TODO return; case GL_MAX_FRAGMENT_INPUT_COMPONENTS: *params = kFrontendMaxFragmentInputComponents; @@ -1429,13 +1453,13 @@ namespace MobileGL::MG_Impl::GLImpl { *params = kFrontendMaxFragmentUniformVectors; return; case GL_MAX_FRAGMENT_UNIFORM_BLOCKS: - *params = kFrontendMaxFragmentUniformBlocks; + *params = ClampUniformBlockCount(kFrontendMaxFragmentUniformBlocks); return; case GL_MAX_GEOMETRY_ATOMIC_COUNTERS: *params = kFrontendMaxGeometryAtomicCounters; return; case GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS: - *params = 16; // TODO + *params = ClampStorageBlockCount(16); // TODO return; case GL_MAX_GEOMETRY_INPUT_COMPONENTS: *params = kFrontendMaxGeometryInputComponents; @@ -1458,7 +1482,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = kFrontendMaxGeometryTotalOutputComponents; return; case GL_MAX_GEOMETRY_UNIFORM_BLOCKS: - *params = kFrontendMaxGeometryUniformBlocks; + *params = ClampUniformBlockCount(kFrontendMaxGeometryUniformBlocks); return; case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS: *params = kFrontendMaxGeometryUniformComponents; @@ -1500,10 +1524,10 @@ namespace MobileGL::MG_Impl::GLImpl { *params = 0; return; case GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS: - *params = 16; // TODO + *params = ClampStorageBlockCount(16); // TODO return; case GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS: - *params = 16; // TODO + *params = ClampStorageBlockCount(16); // TODO return; case GL_MAX_TEXTURE_LOD_BIAS: *params = 15; // TODO @@ -1526,7 +1550,7 @@ namespace MobileGL::MG_Impl::GLImpl { : MG_Backend::DynamicBackendParameters{}.MaxVertexImageUniforms; return; case GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: - *params = 16; // TODO + *params = ClampStorageBlockCount(16); // TODO return; case GL_MAX_VERTEX_UNIFORM_COMPONENTS: *params = kFrontendMaxVertexUniformComponents; @@ -1538,7 +1562,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = kFrontendMaxVertexOutputComponents; return; case GL_MAX_VERTEX_UNIFORM_BLOCKS: - *params = kFrontendMaxVertexUniformBlocks; + *params = ClampUniformBlockCount(kFrontendMaxVertexUniformBlocks); return; case GL_NUM_COMPRESSED_TEXTURE_FORMATS: *params = 0; // compressed texture upload entrypoints are still unimplemented @@ -1938,13 +1962,13 @@ namespace MobileGL::MG_Impl::GLImpl { *params = dynamicParameters.SubgroupQuadOperationsInAllStages ? GL_TRUE : GL_FALSE; break; case GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS: - *params = dynamicParameters.MaxComputeShaderStorageBlocks; + *params = ClampStorageBlockCount(dynamicParameters.MaxComputeShaderStorageBlocks); break; case GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS: - *params = dynamicParameters.MaxCombinedShaderStorageBlocks; + *params = ClampStorageBlockCount(dynamicParameters.MaxCombinedShaderStorageBlocks); break; case GL_MAX_COMPUTE_UNIFORM_BLOCKS: - *params = dynamicParameters.MaxComputeUniformBlocks; + *params = ClampUniformBlockCount(dynamicParameters.MaxComputeUniformBlocks); break; case GL_MAX_COMPUTE_TEXTURE_IMAGE_UNITS: *params = dynamicParameters.MaxComputeTextureImageUnits; @@ -2074,7 +2098,15 @@ namespace MobileGL::MG_Impl::GLImpl { *params = static_cast(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter)); break; case GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE: - *params = kFrontendMaxAtomicCounterBufferSize; + // The conformance suite splits this evenly across every advertised binding point and + // binds all of them in one glBindBuffersRange + // (KHR-GL44.multi_bind.functional_bind_buffers_range), so the pair has to divide: + // 32 bytes over 36 binding points is a zero-sized range, which BindBufferRange + // rejects with INVALID_VALUE before it binds anything. Floor the advertised size at + // one counter per binding point. + *params = std::max( + kFrontendMaxAtomicCounterBufferSize, + static_cast(GetIndexedBufferQueryPointCount(BufferTarget::AtomicCounter) * sizeof(GLuint))); break; case GL_MAX_TEXTURE_BUFFER_SIZE: *params = dynamicParameters.MaxTextureBufferSize; diff --git a/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp index 2e74ffbd..7cbead56 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp @@ -94,6 +94,89 @@ namespace MGITest { } } + // A per-stage block count is an amount of BINDING POINTS an application will use, so it + // can never exceed the number of binding points that exist. GL 4.6 Table 23.64 states the + // relation the other way round (MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS + // >= every per-stage count), and DirectVulkan broke it by clamping the two families + // independently: a device reporting 256 compute uniform blocks and 84 uniform binding + // points passes both ceilings and still cannot serve + // KHR-GL44.multi_bind.dispatch_bind_buffers_base, which reads the block count and binds + // that many buffers in one glBindBuffersBase - INVALID_OPERATION before a single bind. + TEST_F(AdvertisedLimitsScenario, PerStageBlockCountsFitInTheirBindingPoints) { + struct Relation { + GLenum blocks; + const char* blocksName; + GLenum bindings; + const char* bindingsName; + }; + const Relation relations[] = { + {GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS, + "GL_MAX_UNIFORM_BUFFER_BINDINGS"}, + {GL_MAX_VERTEX_UNIFORM_BLOCKS, "GL_MAX_VERTEX_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS, + "GL_MAX_UNIFORM_BUFFER_BINDINGS"}, + {GL_MAX_FRAGMENT_UNIFORM_BLOCKS, "GL_MAX_FRAGMENT_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS, + "GL_MAX_UNIFORM_BUFFER_BINDINGS"}, + {GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", GL_MAX_UNIFORM_BUFFER_BINDINGS, + "GL_MAX_UNIFORM_BUFFER_BINDINGS"}, + {GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", + GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"}, + {GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", + GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS"}, + }; + for (const Relation& relation : relations) { + GLint blocks = -1; + GLint bindings = -1; + glGetIntegerv(relation.blocks, &blocks); + glGetIntegerv(relation.bindings, &bindings); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << relation.blocksName; + EXPECT_LE(blocks, bindings) + << relation.blocksName << " = " << blocks << " exceeds " << relation.bindingsName << " = " + << bindings << "; a shader may declare more blocks than there are binding points to bind them to"; + } + } + + // KHR-GL44.multi_bind.functional_bind_buffers_range sizes each of an indexed target's + // binding points at MAX__SIZE / MAX__BINDINGS and binds all of them in + // one glBindBuffersRange. That quotient has to be a legal BindBufferRange size, which + // makes the two limits of every indexed family a PAIR: advertise a size that does not + // survive division by the binding count and the call fails with INVALID_VALUE before any + // of it binds. + TEST_F(AdvertisedLimitsScenario, IndexedTargetSizeSurvivesDivisionByItsBindingCount) { + struct IndexedFamily { + GLenum maxSize; + const char* maxSizeName; + GLenum maxBindings; + const char* maxBindingsName; + GLint sizeGranularity; // BindBufferRange's size rule for the target + }; + const IndexedFamily families[] = { + {GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, "GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE", + GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, "GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS", 1}, + {GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, "GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS", + GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, "GL_MAX_TRANSFORM_FEEDBACK_BUFFERS", 4}, + {GL_MAX_UNIFORM_BLOCK_SIZE, "GL_MAX_UNIFORM_BLOCK_SIZE", GL_MAX_UNIFORM_BUFFER_BINDINGS, + "GL_MAX_UNIFORM_BUFFER_BINDINGS", 1}, + {GL_MAX_SHADER_STORAGE_BLOCK_SIZE, "GL_MAX_SHADER_STORAGE_BLOCK_SIZE", + GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS, "GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", 1}, + }; + for (const IndexedFamily& family : families) { + GLint maxSize = -1; + GLint maxBindings = -1; + glGetIntegerv(family.maxSize, &maxSize); + glGetIntegerv(family.maxBindings, &maxBindings); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << family.maxSizeName; + ASSERT_GT(maxBindings, 0) << family.maxBindingsName; + const GLint perBinding = maxSize / maxBindings; + EXPECT_GT(perBinding, 0) + << family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " (" + << maxBindings << ") is zero, and BindBufferRange rejects a zero size"; + EXPECT_EQ(perBinding % family.sizeGranularity, 0) + << family.maxSizeName << " (" << maxSize << ") / " << family.maxBindingsName << " (" + << maxBindings << ") = " << perBinding << " is not a multiple of the " + << family.sizeGranularity << "-byte size granularity BindBufferRange requires for it"; + } + } + // The OOM case in isolation, because it is the one with a known CTS victim and the one a // future refactor is most likely to reintroduce by copying the Vulkan limit back. TEST_F(AdvertisedLimitsScenario, ComputeUniformBlocksIsAnAmountAnApplicationCouldActuallyAllocate) {