diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 5c7aafff..d3808546 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -340,6 +340,22 @@ namespace MobileGL { Int MaxVertexAttribs = 16; Int MaxComputeShaderStorageBlocks = 8; Int MaxCombinedShaderStorageBlocks = 32; + // Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Zero is a legal answer for the four + // non-compute, non-fragment stages and these defaults are the spec minimums, not + // placeholders: GL 4.6 table 23.64 and ES 3.2 table 21.44 both set the minimum for + // vertex, tessellation control, tessellation evaluation and geometry at 0, and only + // fragment (8 in GL, 4 in ES) and compute are guaranteed to have any. Every real ARM + // GLES driver takes that allowance - a Mali-G925 reports 0 for all four - so a + // backend that cannot honour a graphics-stage storage block MUST report 0 here + // rather than a hopeful number. Advertising a non-zero count the driver will refuse + // does not make the block work; it only moves the failure from an honest + // "unsupported" at query time to a backend link error the frontend never surfaces, + // after which every draw with that program silently renders nothing. + Int MaxVertexShaderStorageBlocks = 0; + Int MaxTessControlShaderStorageBlocks = 0; + Int MaxTessEvaluationShaderStorageBlocks = 0; + Int MaxGeometryShaderStorageBlocks = 0; + Int MaxFragmentShaderStorageBlocks = 8; Int MaxComputeUniformBlocks = 12; Int MaxComputeWorkGroupInvocations = 128; Int MaxShaderStorageBufferBindings = 8; diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 2a212c4e..36f450f5 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1251,9 +1251,31 @@ namespace MobileGL::MG_Backend::DirectGLES { static_cast(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS)); m_dynamicParameters.MaxComputeShaderStorageBlocks = m_GLESCapabilities.MaxComputeShaderStorageBlocks; m_dynamicParameters.MaxCombinedShaderStorageBlocks = m_GLESCapabilities.MaxCombinedShaderStorageBlocks; + // Per-stage storage-block counts, forwarded from the host driver rather than invented. + // A stage the driver cannot serve reports 0, which is a legal answer everywhere these + // limits appear (GL 4.6 table 23.64, ES 3.2 table 21.44 - the minimum is 0 for every + // graphics stage except fragment) and is the only answer that lets an application take + // its own fallback instead of building a program the driver will refuse to link. The + // stage limit cannot exceed the combined limit or the number of binding points there + // are to bind buffers to, so clamp to both. + const auto clampStageStorageBlocks = [this](Int stageLimit) { + return std::min({std::max(stageLimit, 0), std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0), + std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0)}); + }; + m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings; + m_dynamicParameters.MaxVertexShaderStorageBlocks = + clampStageStorageBlocks(m_GLESCapabilities.MaxVertexShaderStorageBlocks); + m_dynamicParameters.MaxTessControlShaderStorageBlocks = + clampStageStorageBlocks(m_GLESCapabilities.MaxTessControlShaderStorageBlocks); + m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = + clampStageStorageBlocks(m_GLESCapabilities.MaxTessEvaluationShaderStorageBlocks); + m_dynamicParameters.MaxGeometryShaderStorageBlocks = + clampStageStorageBlocks(m_GLESCapabilities.MaxGeometryShaderStorageBlocks); + m_dynamicParameters.MaxFragmentShaderStorageBlocks = + clampStageStorageBlocks(m_GLESCapabilities.MaxFragmentShaderStorageBlocks); m_dynamicParameters.MaxComputeUniformBlocks = m_GLESCapabilities.MaxComputeUniformBlocks; m_dynamicParameters.MaxComputeWorkGroupInvocations = m_GLESCapabilities.MaxComputeWorkGroupInvocations; - m_dynamicParameters.MaxShaderStorageBufferBindings = m_GLESCapabilities.MaxShaderStorageBufferBindings; + // (MaxShaderStorageBufferBindings is assigned above, before the per-stage clamp reads it.) // This is the number glGetIntegerv(GL_MAX_TEXTURE_BUFFER_SIZE) hands the application, and // on a host without buffer textures it is knowingly a floor MobileGL cannot honour rather // than a driver answer (m_GLESCapabilities.MaxTextureBufferSizeIsDriverReported says diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index b5c062cd..05d1a543 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -847,6 +847,38 @@ namespace MobileGL::MG_Backend::DirectVulkan { m_dynamicParameters.MaxShaderStorageBufferBindings = clampLimit("GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS", m_vulkanCaps.MaxShaderStorageBufferBindings, kMaxAdvertisedBufferBlocks); + // Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS. Vulkan has one descriptor limit for every + // stage (maxPerStageDescriptorStorageBuffers, which is what MaxComputeShaderStorageBlocks + // carries), so the stage limits differ only by whether the stage can have blocks at all. + // + // Deliberately NOT gated on vertexPipelineStoresAndAtomics, unlike the per-stage image + // uniforms below. That gate reads as the obvious one and is wrong here in practice: a + // Mali-G925-Immortalis reports vertexPipelineStoresAndAtomics=false (supported AND + // enabled) and yet runs all 433 KHR-GL43.constant_expressions.*_tess_* cases correctly + // through this backend - those write their result through a storage block declared in a + // tessellation stage. Gating would report 0 and turn 433 passing cases into + // "unsupported", removing function that demonstrably works. + // + // The asymmetry with DirectGLES is real and is the point. There, 0 prevents a program + // the driver refuses outright at link time; the honest limit converts a silent + // wrong-render into a capability an application can route around. Here there is no such + // failure to prevent, so the limit stays at what the device can address. If a Vulkan + // device is ever found that genuinely rejects such a pipeline, the gate belongs at + // pipeline creation where the rejection is observable, not on a feature bit this driver + // reports inaccurately. + { + const Int maxPerStageStorageBlocks = + std::min(std::max(m_dynamicParameters.MaxComputeShaderStorageBlocks, 0), + std::min(std::max(m_dynamicParameters.MaxCombinedShaderStorageBlocks, 0), + std::max(m_dynamicParameters.MaxShaderStorageBufferBindings, 0))); + m_dynamicParameters.MaxVertexShaderStorageBlocks = maxPerStageStorageBlocks; + m_dynamicParameters.MaxTessControlShaderStorageBlocks = maxPerStageStorageBlocks; + m_dynamicParameters.MaxTessEvaluationShaderStorageBlocks = maxPerStageStorageBlocks; + // The one hard capability in the set: no geometry stage means no blocks in it. + m_dynamicParameters.MaxGeometryShaderStorageBlocks = + m_vulkanCaps.SupportsGeometryShader ? maxPerStageStorageBlocks : 0; + m_dynamicParameters.MaxFragmentShaderStorageBlocks = maxPerStageStorageBlocks; + } m_dynamicParameters.MaxTextureBufferSize = clampLimit( "GL_MAX_TEXTURE_BUFFER_SIZE", m_vulkanCaps.MaxTextureBufferSize, kMaxAdvertisedTextureBufferSize); m_dynamicParameters.TextureBufferOffsetAlignment = m_vulkanCaps.TextureBufferOffsetAlignment; diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 6e0128b0..b22d522f 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -692,8 +692,12 @@ namespace MobileGL::MG_Util::BackendLoader { !f.glUnmapBuffer || !f.glMemoryBarrier || !f.glCreateShader || !f.glCreateProgram) { return false; } - GLint maxVertexSsboBlocks = 0; - f.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexSsboBlocks); + // Read from caps, not re-queried: the per-stage limits are resolved (and their query + // errors drained) before this probe runs, so asking the driver again would be a second + // round trip that can disagree with the number MobileGL actually advertises - and, on + // the early return below, would leave its own GL_INVALID_ENUM in the queue for the + // application's first glGetError to find. + const GLint maxVertexSsboBlocks = caps.MaxVertexShaderStorageBlocks; if (maxVertexSsboBlocks < 1) { // The native indirect machinery cannot read the command buffer from the vertex // stage on this driver anyway; assume conforming zero-based gl_InstanceID. @@ -1036,6 +1040,15 @@ namespace MobileGL::MG_Util::BackendLoader { GLint maxVertexAttribs = 16; GLint maxComputeShaderStorageBlocks = 8; GLint maxCombinedShaderStorageBlocks = 32; + // ES 3.2 table 21.44 minimums. Zero for the four graphics stages below fragment is not a + // placeholder - it is what the spec permits and what ARM's GLES driver actually reports, + // so a probe that never runs (pre-ES 3.2, unsupported pname) leaves behind the truthful + // answer rather than an optimistic one. + GLint maxVertexShaderStorageBlocks = 0; + GLint maxTessControlShaderStorageBlocks = 0; + GLint maxTessEvaluationShaderStorageBlocks = 0; + GLint maxGeometryShaderStorageBlocks = 0; + GLint maxFragmentShaderStorageBlocks = 4; GLint maxComputeUniformBlocks = 12; GLint maxComputeWorkGroupInvocations = 128; GLint maxShaderStorageBufferBindings = 8; @@ -1127,6 +1140,59 @@ namespace MobileGL::MG_Util::BackendLoader { (caps.GLESVersion.Major == 3 && caps.GLESVersion.Minor >= 2)) { glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_IMAGE_UNIFORMS, &maxGeometryImageUniforms); } + // Per-stage storage-block counts. Deliberately NOT batched with the unconditional probes + // above, for the reason GL_MAX_TEXTURE_BUFFER_SIZE is not: the vertex and fragment pnames + // are ES 3.1, but the tessellation and geometry ones only exist from ES 3.2 on (or under + // EXT_tessellation_shader / EXT_geometry_shader), so on an older context they raise + // GL_INVALID_ENUM, leave the local untouched, and - with nothing draining the queue until + // some later probe - let that error be misattributed to an unrelated query in between, or + // leak into the application's first glGetError. + // + // A stage whose probe does not run keeps the spec minimum, which for all four graphics + // stages is 0. That is the honest answer: DirectGLES emits ESSL 3.10 on an ES 3.1 context, + // where those stages do not exist at all. + { + const auto drainErrors = [&glesFuncs]() { + Bool hadError = false; + if (glesFuncs.glGetError) { + while (glesFuncs.glGetError() != GL_NO_ERROR) hadError = true; + } + return hadError; + }; + + // Isolate from errors raised by the preceding probes so the drain below reports on + // these queries only. + drainErrors(); + glesFuncs.glGetIntegerv(GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS, &maxVertexShaderStorageBlocks); + glesFuncs.glGetIntegerv(GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS, &maxFragmentShaderStorageBlocks); + if (drainErrors()) { + MGLOG_W("Per-stage shader storage block query failed for the vertex/fragment " + "stages; assuming the ES minimums (vertex 0, fragment 4)"); + maxVertexShaderStorageBlocks = 0; + maxFragmentShaderStorageBlocks = 4; + } + if (esAtLeast32) { + glesFuncs.glGetIntegerv(GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS, + &maxTessControlShaderStorageBlocks); + glesFuncs.glGetIntegerv(GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS, + &maxTessEvaluationShaderStorageBlocks); + glesFuncs.glGetIntegerv(GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS, &maxGeometryShaderStorageBlocks); + if (drainErrors()) { + MGLOG_W("Per-stage shader storage block query failed for the tessellation/" + "geometry stages; assuming the ES minimum of 0"); + maxTessControlShaderStorageBlocks = 0; + maxTessEvaluationShaderStorageBlocks = 0; + maxGeometryShaderStorageBlocks = 0; + } + } + // A driver is free to report a negative or nonsensical count into an untouched + // out-param; clamp before anything downstream treats it as a capacity. + maxVertexShaderStorageBlocks = std::max(maxVertexShaderStorageBlocks, 0); + maxTessControlShaderStorageBlocks = std::max(maxTessControlShaderStorageBlocks, 0); + maxTessEvaluationShaderStorageBlocks = std::max(maxTessEvaluationShaderStorageBlocks, 0); + maxGeometryShaderStorageBlocks = std::max(maxGeometryShaderStorageBlocks, 0); + maxFragmentShaderStorageBlocks = std::max(maxFragmentShaderStorageBlocks, 0); + } glesFuncs.glGetIntegerv(GL_MAX_DRAW_BUFFERS, &maxDrawBuffers); glesFuncs.glGetIntegerv(GL_MAX_COLOR_ATTACHMENTS, &maxColorAttachments); glesFuncs.glGetIntegerv(GL_MAX_CLIP_DISTANCES, &maxClipDistances); @@ -1271,6 +1337,11 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxVertexAttribs = maxVertexAttribs; caps.MaxComputeShaderStorageBlocks = maxComputeShaderStorageBlocks; caps.MaxCombinedShaderStorageBlocks = maxCombinedShaderStorageBlocks; + caps.MaxVertexShaderStorageBlocks = maxVertexShaderStorageBlocks; + caps.MaxTessControlShaderStorageBlocks = maxTessControlShaderStorageBlocks; + caps.MaxTessEvaluationShaderStorageBlocks = maxTessEvaluationShaderStorageBlocks; + caps.MaxGeometryShaderStorageBlocks = maxGeometryShaderStorageBlocks; + caps.MaxFragmentShaderStorageBlocks = maxFragmentShaderStorageBlocks; caps.MaxComputeUniformBlocks = maxComputeUniformBlocks; caps.MaxComputeWorkGroupInvocations = maxComputeWorkGroupInvocations; caps.MaxShaderStorageBufferBindings = maxShaderStorageBufferBindings; @@ -1348,6 +1419,14 @@ namespace MobileGL::MG_Util::BackendLoader { MGLOG_I(" GL_MAX_VERTEX_ATTRIBS: %d", caps.MaxVertexAttribs); MGLOG_I(" GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS: %d", caps.MaxComputeShaderStorageBlocks); MGLOG_I(" GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS: %d", caps.MaxCombinedShaderStorageBlocks); + // Worth a line each: a zero here is what stops an application's storage block from ever + // working in that stage, and reading it back from an artifact is the difference between + // "MobileGL dropped my draw" and "this driver has no SSBOs outside compute". + MGLOG_I(" GL_MAX_VERTEX_SHADER_STORAGE_BLOCKS: %d", caps.MaxVertexShaderStorageBlocks); + MGLOG_I(" GL_MAX_TESS_CONTROL_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessControlShaderStorageBlocks); + MGLOG_I(" GL_MAX_TESS_EVALUATION_SHADER_STORAGE_BLOCKS: %d", caps.MaxTessEvaluationShaderStorageBlocks); + MGLOG_I(" GL_MAX_GEOMETRY_SHADER_STORAGE_BLOCKS: %d", caps.MaxGeometryShaderStorageBlocks); + MGLOG_I(" GL_MAX_FRAGMENT_SHADER_STORAGE_BLOCKS: %d", caps.MaxFragmentShaderStorageBlocks); MGLOG_I(" GL_MAX_COMPUTE_UNIFORM_BLOCKS: %d", caps.MaxComputeUniformBlocks); MGLOG_I(" GL_MAX_COMPUTE_WORK_GROUP_INVOCATIONS: %d", caps.MaxComputeWorkGroupInvocations); MGLOG_I(" GL_MAX_SHADER_STORAGE_BUFFER_BINDINGS: %d", caps.MaxShaderStorageBufferBindings); diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index 2d986489..41e5d63a 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1248,6 +1248,17 @@ namespace MobileGL { Int MaxVertexAttribs = 16; Int MaxComputeShaderStorageBlocks = 8; Int MaxCombinedShaderStorageBlocks = 32; + // Per-stage GL_MAX_*_SHADER_STORAGE_BLOCKS as the host GLES driver reports them. + // The defaults are the ES 3.2 minimums (table 21.44): 0 for every graphics stage + // except fragment, which is 4. ES only gained the tessellation and geometry pnames + // in 3.2 (or with EXT_tessellation_shader / EXT_geometry_shader), so those two are + // queried behind a support check and left at the default otherwise - see + // FillInGLESCapabilities. + Int MaxVertexShaderStorageBlocks = 0; + Int MaxTessControlShaderStorageBlocks = 0; + Int MaxTessEvaluationShaderStorageBlocks = 0; + Int MaxGeometryShaderStorageBlocks = 0; + Int MaxFragmentShaderStorageBlocks = 4; Int MaxComputeUniformBlocks = 12; Int MaxComputeWorkGroupInvocations = 128; Int MaxShaderStorageBufferBindings = 8;