diff --git a/MobileGL/MG_Backend/BackendObject.h b/MobileGL/MG_Backend/BackendObject.h index 96a0ef97..223bedee 100644 --- a/MobileGL/MG_Backend/BackendObject.h +++ b/MobileGL/MG_Backend/BackendObject.h @@ -389,6 +389,19 @@ namespace MobileGL { // where there is no device to be honest about and BuildTBuiltInResource still has to // hand glslang a workable gl_MaxClipDistances. Int MaxClipDistances = 8; + // GL_MAX_CULL_DISTANCES and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, under exactly + // the contract stated for MaxClipDistances above: ZERO IS A LEGAL ANSWER and a + // backend that cannot host a cull distance MUST report it. The failure this prevents + // is worse than the clip one, because cull distance discards the whole primitive: + // glslang bounds gl_CullDistance[i] against maxCullDistances and expands + // gl_MaxCullDistances from it, SPIRV-Cross then emits + // `#extension GL_EXT_clip_cull_distance : require` into the ESSL, and a host driver + // without that extension rejects the program in an info log nobody surfaces. These + // used to be bare 8s inside BuildTBuiltInResource with no backend consulted at all. + // The DEFAULTS are the GL 4.5 core minimums for the same reason MaxClipDistances' + // is: they describe the no-backend case (standalone compiles, unit tests). + Int MaxCullDistances = 8; + Int MaxCombinedClipAndCullDistances = 8; Int MaxViewports = 16; // GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX: which vertex of a // primitive supplies gl_Layer and gl_ViewportIndex. GL 4.6 table 23.65 makes diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 91e585c1..eeb97e2f 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -1465,6 +1465,11 @@ namespace MobileGL::MG_Backend::DirectGLES { m_dynamicParameters.MaxDrawBuffers = m_GLESCapabilities.MaxDrawBuffers; m_dynamicParameters.MaxColorAttachments = m_GLESCapabilities.MaxColorAttachments; m_dynamicParameters.MaxClipDistances = m_GLESCapabilities.MaxClipDistances; + // The loader already gated both on GL_EXT_clip_cull_distance and left 0 without it, which + // is the answer that keeps glslang from accepting a gl_CullDistance the ESSL compiler + // would reject. + m_dynamicParameters.MaxCullDistances = m_GLESCapabilities.MaxCullDistances; + m_dynamicParameters.MaxCombinedClipAndCullDistances = m_GLESCapabilities.MaxCombinedClipAndCullDistances; m_dynamicParameters.MaxViewports = m_GLESCapabilities.MaxViewports; // Whatever the driver said about which vertex supplies gl_Layer, and GL_UNDEFINED_VERTEX // for gl_ViewportIndex on every driver without GL_OES_viewport_array - which is both test diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 7adec448..0a664b04 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -351,9 +351,20 @@ namespace MobileGL::MG_Backend::DirectGLES { #ifdef TRACY_ENABLE ZoneScopedC(TRACY_ZONECOLOR_BACKEND); #endif - // Only sync up to the high-water mark of app-touched points; the fixed array is 36 + // Only sync up to the high-water mark of app-touched points; the fixed array is 84 // deep but apps bind a handful, so the never-touched tail is already at GL default 0. auto bindingPointCnt = MG_State::pGLContext->GetTouchedBufferBindingPointCount(target); + // ...and never past what the ES driver itself can hold. MobileGL advertises the GL 4.5 + // minimum of 84 uniform binding points while the ES 3.2 minimum is 72, so a frontend + // index in that gap would reach glBindBufferBase as GL_INVALID_VALUE. Nothing is lost + // by stopping: this frontend-indexed pass exists for the compute path, and the + // per-program rebind in BindCurrentProgramWithResources - which is what actually feeds + // a shader - remaps every block a program declares onto a compacted ES point, so a + // block bound at GL point 83 still reaches its shader. + if (target == BufferTarget::Uniform && g_GLESCapabilities.MaxUniformBufferBindings > 0) { + bindingPointCnt = std::min(bindingPointCnt, + static_cast(g_GLESCapabilities.MaxUniformBufferBindings)); + } for (SizeT i = 0; i < bindingPointCnt; ++i) { auto& point = MG_State::pGLContext->GetBufferBindingPoint(target, i); auto& obj = point.GetBoundObject(); diff --git a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp index da91bf5f..28c714b0 100644 --- a/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/BackendObject_DirectVulkan.cpp @@ -1006,6 +1006,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { // it the limit describes a capacity no shader may use, so report none. m_dynamicParameters.MaxClipDistances = m_vulkanCaps.SupportsShaderClipDistance ? std::max(m_vulkanCaps.MaxClipDistances, 0) : 0; + // The cull pair, gated on its own feature. shaderCullDistance is separate from + // shaderClipDistance and VulkanRenderer enables it independently, so it gets its own + // gate rather than riding on the clip one. + m_dynamicParameters.MaxCullDistances = + m_vulkanCaps.SupportsShaderCullDistance ? std::max(m_vulkanCaps.MaxCullDistances, 0) : 0; + // GL 4.6 core 11.1.3.10: the combined limit is at least as large as either half. A device + // with only one of the two features must not report a combined capacity that implies the + // other, so the gate is "either feature" and the value never drops below what is enabled. + m_dynamicParameters.MaxCombinedClipAndCullDistances = + (m_vulkanCaps.SupportsShaderClipDistance || m_vulkanCaps.SupportsShaderCullDistance) + ? std::max({m_vulkanCaps.MaxCombinedClipAndCullDistances, m_dynamicParameters.MaxClipDistances, + m_dynamicParameters.MaxCullDistances}) + : 0; m_dynamicParameters.MaxViewports = m_vulkanCaps.MaxViewports; // Assigned explicitly rather than left to the struct's defaults, like every other // parameter here, so a second fill cannot inherit a stale value. GL_UNDEFINED_VERTEX is diff --git a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp index 69548d90..fa767571 100644 --- a/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp +++ b/MobileGL/MG_Impl/GLImpl/Drawing/GL_Drawing.cpp @@ -1577,8 +1577,13 @@ namespace MobileGL::MG_Impl::GLImpl { std::to_string(id) + " is not a transform feedback object name.")); return; } - // GL_MAX_VERTEX_STREAMS is 1, so stream 0 is the only one that exists. - if (stream != 0) { + // GL 4.6 core 10.3.7 bounds `stream` by GL_MAX_VERTEX_STREAMS, which is 4. Only stream 0 + // can ever have been written - nothing in the shader pipeline supports + // layout(stream = N) - so a higher stream captured zero vertices and the draw is a legal + // no-op rather than an error. The bound is read from the getter so the two cannot drift. + GLint maxVertexStreams = 1; + GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams); + if (stream >= static_cast(std::max(maxVertexStreams, 1))) { MG_State::pGLContext->RecordError( ErrorCode::InvalidValue, MakeUnique("MG_Impl/GLImpl", functionName, @@ -1596,7 +1601,9 @@ namespace MobileGL::MG_Impl::GLImpl { return; } - const Uint64 vertices = MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id); + // Only stream 0 ever records anything (see the stream bound above), so a higher stream + // replays nothing. + const Uint64 vertices = stream == 0 ? MG_State::pGLContext->GetTransformFeedbackRecordedVertices(id) : 0; if (vertices == 0) return; const auto count = static_cast(vertices); AccountTransformFeedbackPrimitives(mode, count); diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index 1623d4bc..49a10d94 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -688,16 +688,15 @@ namespace MobileGL::MG_Impl::GLImpl { // GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own // (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION. // The multisample TEXTURE path resolves the limit per format the same way - // (GL_Texture.cpp, GetMaxSupportedTextureSamples). Both are floored to the value MobileGL - // advertises: on a driver where the two differ - Adreno reports GL_MAX_SAMPLES 4 and - // GL_MAX_INTEGER_SAMPLES 1 - rejecting the advertised count here only moves the failure - // from the driver into MobileGL, so the frontend accepts it and the backend clamps the - // count it actually hands the driver. + // (GL_Texture.cpp, GetMaxSupportedTextureSamples), and both now enforce exactly what their + // pname advertises. The integer ceiling used to be floored at GL_MAX_SAMPLES so that the + // frontend would accept a count it had advertised globally - but on Adreno and Mali the + // integer path is genuinely one sample, and accepting four only moved the failure from an + // honest INVALID_OPERATION here to a silently under-allocated renderbuffer. Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) { if (MG_Backend::pActiveBackendObject == nullptr) { return std::numeric_limits::max(); } - const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(format); GLenum normalizedFormat = GL_RGBA; @@ -711,10 +710,8 @@ namespace MobileGL::MG_Impl::GLImpl { if (!isIntegerFormat) { return GetMaxRenderbufferSamples_State(); } - // Per-format still, but never below the ceiling glGetIntegerv(GL_MAX_SAMPLES) promised: - // the driver's raw GL_MAX_INTEGER_SAMPLES stays the *backend* limit and the backend - // clamps to it, while the frontend honours what it advertised. - return std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples()); + // Exactly what glGetIntegerv(GL_MAX_INTEGER_SAMPLES) reports. + return GetAdvertisedIntegerMaxSamples(); } Bool ValidateRenderbufferStorageSize_State(GLsizei width, GLsizei height, const char* caller) { diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp index fee468e0..84a9e3f4 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -93,8 +93,15 @@ namespace MobileGL::MG_Impl::GLImpl { // limits they advertise still have to be legal. constexpr GLint kFrontendMaxDebugGroupStackDepth = 64; constexpr GLint kFrontendMaxDebugLoggedMessages = 1; - constexpr GLint kFrontendMaxVertexUniformComponents = 4096; - constexpr GLint kFrontendMaxVertexUniformVectors = 128; + // The *_VECTORS answers are the *_COMPONENTS ones divided by four, never a second + // literal: they used to be independent (4096 components against 128 vectors, 64 varying + // components against 8 varying vectors) and could not both be describing the same + // capacity. Both are shared with BuildTBuiltInResource through Types.h, because + // gl_MaxVertexUniformVectors and gl_MaxVaryingVectors expand from the same numbers. + constexpr GLint kFrontendMaxVertexUniformComponents = + static_cast(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_COMPONENTS); + constexpr GLint kFrontendMaxVertexUniformVectors = + static_cast(MG_Util::ShaderTranspiler::MAX_VERTEX_UNIFORM_VECTORS); constexpr GLint kFrontendMaxVertexUniformBlocks = 14; constexpr GLint kFrontendMaxVertexOutputComponents = 64; constexpr GLint kFrontendMaxFragmentInputComponents = 128; @@ -106,21 +113,50 @@ namespace MobileGL::MG_Impl::GLImpl { constexpr GLint kFrontendMaxGeometryTextureImageUnits = 16; constexpr GLint kFrontendMaxGeometryUniformComponents = 1024; constexpr GLint kFrontendMaxGeometryUniformBlocks = 14; - constexpr GLint kFrontendMaxCombinedUniformBlocks = kFrontendMaxVertexUniformBlocks + - kFrontendMaxGeometryUniformBlocks + - kFrontendMaxFragmentUniformBlocks; - constexpr GLint kFrontendMaxVaryingComponents = 64; - constexpr GLint kFrontendMaxVaryingVectors = 8; + // ARB_geometry_shader4's per-invocation count. No TBuiltInResource field and no + // gl_MaxGeometryShaderInvocations built-in exists to keep in step, so this is a getter + // answer only; 32 is the GL 4.6 core minimum (table 23.57). + constexpr GLint kFrontendMaxGeometryShaderInvocations = 32; + constexpr GLint kFrontendMaxTessControlUniformBlocks = 14; + constexpr GLint kFrontendMaxTessEvaluationUniformBlocks = 14; + // GL 4.6's minimum is 14 uniform blocks on each of the FIVE graphics stages (70), not + // three: the two tessellation stages were simply missing from this sum, so even a + // frontend with enough binding points advertised 42. + constexpr GLint kFrontendMaxCombinedUniformBlocks = + kFrontendMaxVertexUniformBlocks + kFrontendMaxTessControlUniformBlocks + + kFrontendMaxTessEvaluationUniformBlocks + kFrontendMaxGeometryUniformBlocks + + kFrontendMaxFragmentUniformBlocks; + constexpr GLint kFrontendMaxVaryingComponents = + static_cast(MG_Util::ShaderTranspiler::MAX_VARYING_COMPONENTS); + constexpr GLint kFrontendMaxVaryingVectors = + static_cast(MG_Util::ShaderTranspiler::MAX_VARYING_VECTORS); constexpr GLint kFrontendMaxProgramTexelOffset = 7; constexpr GLint kFrontendMinProgramTexelOffset = -8; constexpr GLint kFrontendMaxTransformFeedbackInterleavedComponents = 64; constexpr GLint kFrontendMaxTransformFeedbackSeparateAttribs = 4; constexpr GLint kFrontendMaxTransformFeedbackSeparateComponents = 4; + // ARB_transform_feedback3's vertex-stream count; see the GL_MAX_VERTEX_STREAMS case for + // what streams 1..3 mean in an implementation that can only emit to stream 0. + constexpr GLint kFrontendMaxVertexStreams = 4; constexpr GLint kFrontendMaxGeometryOutputVertices = 256; constexpr GLint kFrontendMaxGeometryTotalOutputComponents = 1024; - constexpr GLint kFrontendMinUniformBufferBindings = 36; + // GL 4.5 core table 23.64 requires 84 indexed uniform binding points, and that is exactly + // how wide the state layer's array is (BufferState::BufferBindingPointCount) - see the + // GL_MAX_UNIFORM_BUFFER_BINDINGS case for why the ES driver's own, smaller count is not + // the ceiling here. + constexpr GLint kFrontendMinUniformBufferBindings = 84; constexpr GLint kFrontendSubpixelBits = 4; - constexpr GLint kFrontendMaxSamples = 4; + constexpr GLint kFrontendMaxSamples = + static_cast(MG_Util::ShaderTranspiler::MIN_ADVERTISED_MAX_SAMPLES); + // ARB_shader_subroutine's two limits. NOTHING IMPLEMENTS SUBROUTINES: there is no + // glGetSubroutineIndex / glUniformSubroutinesuiv, only the program-interface enum + // plumbing. These are answered - with the GL 4.5 core minimums - because the conformance + // suite queries them before it checks for the feature and an INVALID_ENUM both leaves the + // caller reading its own uninitialised stack slot and strands an error for the next + // unrelated call to trip over. The extension is deliberately NOT advertised, so the + // numbers are a table entry, not a capability claim. + constexpr GLint kFrontendMaxSubroutines = 256; + constexpr GLint kFrontendMaxSubroutineUniformLocations = 1024; // The floors under GL_MAX_COMPUTE_WORK_GROUP_COUNT / _SIZE. Shared with the compile // pipeline (CaptureCompileEnv floors the same driver answers at them, and @@ -455,10 +491,18 @@ namespace MobileGL::MG_Impl::GLImpl { } // namespace // GL 4.6 core table 23.53 requires GL_MAX_SAMPLES >= 4, so the driver's value is floored - // before it is advertised. Every other multisample ceiling MobileGL advertises has to be - // floored the same way: promising 4 samples globally while answering GL_MAX_INTEGER_SAMPLES - // 1 - which is exactly what Adreno reports - makes the frontend reject the very count it - // just told the application to use. The backends clamp the realised count instead. + // before it is advertised. gl_MaxSamples expands from the same floored number + // (BuildTBuiltInResource), which is also what sizes gl_SampleMask[]. + // + // THE FLOOR STOPS HERE, and that is the point. It used to be applied to + // GL_MAX_INTEGER_SAMPLES, GL_MAX_COLOR_TEXTURE_SAMPLES and GL_MAX_DEPTH_TEXTURE_SAMPLES too, + // on the reasoning that an application reads GL_MAX_SAMPLES once and hands that count to + // every glTexStorage*Multisample. Table 23.53 gives those three a minimum of ONE, and the + // reasoning had it backwards: Adreno and Mali back an integer multisample texture with a + // single sample, so flooring the query at 4 did not make four samples exist - it made the + // backend silently under-allocate (ClampSamplesToBackendSupport) while the application wrote + // per-sample data it could never read back. Reporting what was probed turns that into an + // honest "unsupported" the application can branch on. GLint GetAdvertisedMaxSamples() { if (MG_Backend::pActiveBackendObject == nullptr) { return kFrontendMaxSamples; @@ -466,6 +510,30 @@ namespace MobileGL::MG_Impl::GLImpl { return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples); } + // GL 4.6 core table 23.53 minimum for the per-category multisample ceilings. One, not four: + // see the note on GetAdvertisedMaxSamples. A zero would be a probe that never ran, so it is + // floored rather than trusted. + namespace { + GLint AdvertisedCategoryMaxSamples(Int MG_Backend::DynamicBackendParameters::*categoryLimit) { + if (MG_Backend::pActiveBackendObject == nullptr) { + return 1; + } + return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().*categoryLimit, 1); + } + } // namespace + + GLint GetAdvertisedColorTextureMaxSamples() { + return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxColorTextureSamples); + } + + GLint GetAdvertisedDepthTextureMaxSamples() { + return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxDepthTextureSamples); + } + + GLint GetAdvertisedIntegerMaxSamples() { + return AdvertisedCategoryMaxSamples(&MG_Backend::DynamicBackendParameters::MaxIntegerSamples); + } + // Declared in GL_Getter.h, so that the draw path can feed the same number to the reserved // gl_NumSamples stand-in that glGetIntegerv(GL_SAMPLES) reports. GLint ResolveDrawFramebufferSampleCount() { @@ -1227,6 +1295,13 @@ namespace MobileGL::MG_Impl::GLImpl { } switch (pname) { + case GL_MAX_ELEMENT_INDEX: + // The largest value a GL_UNSIGNED_INT index may take. It has to be answered HERE and + // not left to the 32-bit fallback below: the conformance suite reads it with + // glGetInteger64v, and widening the saturated GLint would report INT32_MAX where the + // spec requires 2^32-1. + params[0] = 0xFFFFFFFFLL; + return; case GL_MAX_SHADER_STORAGE_BLOCK_SIZE: if (MG_Backend::pActiveBackendObject) { params[0] = static_cast( @@ -1706,6 +1781,9 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_MAX_GEOMETRY_UNIFORM_COMPONENTS: *params = kFrontendMaxGeometryUniformComponents; return; + case GL_MAX_GEOMETRY_SHADER_INVOCATIONS: + *params = kFrontendMaxGeometryShaderInvocations; + return; case GL_MAX_IMAGE_SAMPLES: *params = 0; // multisampled image load/store is not exposed by the DirectGLES frontend return; @@ -1759,6 +1837,59 @@ namespace MobileGL::MG_Impl::GLImpl { *params = StageStorageBlockCount(&MG_Backend::DynamicBackendParameters::MaxTessEvaluationShaderStorageBlocks); return; + // The tessellation per-stage resource limits. Every one of these is ALSO a GLSL built-in + // constant that BuildTBuiltInResource expands, and the two must report the same number + // (KHR-GL45.limits.max_tess_* compares them directly) - which is why the values come from + // the shared block in MG_Util/ShaderTranspiler/Types.h rather than from literals here. + // They were the whole per-stage tess family: the table had been filled in only where the + // honest answer was zero (the atomic counters, the image uniforms) or where a driver + // query existed (GL_MAX_PATCH_VERTICES, GL_MAX_TESS_GEN_LEVEL), so every pname whose + // answer is a real resource count fell through to GL_INVALID_ENUM. + case GL_MAX_TESS_CONTROL_INPUT_COMPONENTS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_INPUT_COMPONENTS); + return; + case GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_OUTPUT_COMPONENTS); + return; + case GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS); + return; + case GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS); + return; + case GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS); + return; + case GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_INPUT_COMPONENTS); + return; + case GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_OUTPUT_COMPONENTS); + return; + case GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS); + return; + case GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS); + return; + case GL_MAX_TESS_PATCH_COMPONENTS: + *params = static_cast(MG_Util::ShaderTranspiler::MAX_TESS_PATCH_COMPONENTS); + return; + // Routed through the same clamp as every other per-stage block count so the + // MAX_UNIFORM_BUFFER_BINDINGS >= MAX_COMBINED_UNIFORM_BLOCKS >= per-stage ordering of + // GL 4.6 table 23.64 cannot be broken by the two families moving independently. + case GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS: + *params = ClampUniformBlockCount(kFrontendMaxTessControlUniformBlocks); + return; + case GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS: + *params = ClampUniformBlockCount(kFrontendMaxTessEvaluationUniformBlocks); + return; + case GL_MAX_SUBROUTINES: + *params = kFrontendMaxSubroutines; + return; + case GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS: + *params = kFrontendMaxSubroutineUniformLocations; + return; case GL_MAX_TEXTURE_LOD_BIAS: *params = 15; // TODO return; @@ -2174,7 +2305,12 @@ namespace MobileGL::MG_Impl::GLImpl { return; } case GL_MAX_ELEMENT_INDEX: - *params = 1024 * 1024; // TODO + // 64-bit state (see GetInteger64v); the 32-bit query saturates, per the GL + // state-query conversion rules - the same shape GL_MAX_SHADER_STORAGE_BLOCK_SIZE + // uses. The real answer is 2^32-1 because both backends draw with GL_UNSIGNED_INT + // indices and neither bounds an index value; the old `1024 * 1024` was a placeholder + // that no draw path ever consulted. + *params = INT32_MAX; return; case GL_CONTEXT_PROFILE_MASK: // Reports the requested context profile (EGL defaults 3.x contexts to core); @@ -2275,7 +2411,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = static_cast(dynamicParameters.ViewportIndexProvokingVertex); break; case GL_MAX_COLOR_TEXTURE_SAMPLES: - *params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples()); + *params = GetAdvertisedColorTextureMaxSamples(); break; case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS: *params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents, @@ -2305,7 +2441,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = dynamicParameters.MaxCubeMapTextureSize; break; case GL_MAX_DEPTH_TEXTURE_SAMPLES: - *params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples()); + *params = GetAdvertisedDepthTextureMaxSamples(); break; case GL_MAX_FRAMEBUFFER_WIDTH: *params = dynamicParameters.MaxFramebufferWidth; @@ -2332,7 +2468,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = dynamicParameters.MaxComputeImageUniforms; break; case GL_MAX_INTEGER_SAMPLES: - *params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples()); + *params = GetAdvertisedIntegerMaxSamples(); break; case GL_MAX_RENDERBUFFER_SIZE: *params = dynamicParameters.MaxRenderbufferSize; @@ -2356,12 +2492,43 @@ namespace MobileGL::MG_Impl::GLImpl { for (Uint i = 0; i < 2; ++i) params[i] = static_cast(std::lround(inner[i])); break; } + // GL 4.6 core table 23.66: whether the primitive-restart index terminates a patch. + // GL_FALSE is a legal answer and the true one - neither backend cuts a patch short, and + // the DirectVulkan draw path relies on this staying false (it resolves primitive restart + // to "never" for a PATCH_LIST topology on the strength of it). + case GL_PRIMITIVE_RESTART_FOR_PATCHES_SUPPORTED: + *params = GL_FALSE; + break; case GL_MAX_PATCH_VERTICES: *params = dynamicParameters.MaxPatchVertices; break; case GL_MAX_TESS_GEN_LEVEL: *params = dynamicParameters.MaxTessGenLevel; break; + // Same helper, and so the same arithmetic, as every other GL_MAX_COMBINED_*_UNIFORM_ + // COMPONENTS: default-block components + blocks * (block size / 4). It reproduces the + // conformance suite's own formula exactly, so the two cannot drift. + case GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS: + *params = GetMaxCombinedUniformComponents( + static_cast(MG_Util::ShaderTranspiler::MAX_TESS_CONTROL_UNIFORM_COMPONENTS), + kFrontendMaxTessControlUniformBlocks, dynamicParameters.MaxUniformBlockSize); + break; + case GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS: + *params = GetMaxCombinedUniformComponents( + static_cast(MG_Util::ShaderTranspiler::MAX_TESS_EVALUATION_UNIFORM_COMPONENTS), + kFrontendMaxTessEvaluationUniformBlocks, dynamicParameters.MaxUniformBlockSize); + break; + // ARB_cull_distance. Backend-derived exactly like GL_MAX_CLIP_DISTANCES beside it, and + // for a stronger reason: a cull distance discards the whole primitive, so advertising + // eight the rasterizer cannot serve turns every culling draw into a silent no-op. Zero is + // the honest answer on a host with no cull-distance route, and the conformance suite then + // skips the functional cases instead of failing them deep inside a pixel comparison. + case GL_MAX_CULL_DISTANCES: + *params = dynamicParameters.MaxCullDistances; + break; + case GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES: + *params = dynamicParameters.MaxCombinedClipAndCullDistances; + break; case GL_MIN_PROGRAM_TEXTURE_GATHER_OFFSET: *params = dynamicParameters.MinProgramTextureGatherOffset; break; @@ -2412,7 +2579,15 @@ namespace MobileGL::MG_Impl::GLImpl { *params = kFrontendMaxTransformFeedbackSeparateAttribs; break; case GL_MAX_VERTEX_STREAMS: - *params = 1; + // GL 4.5 core table 23.62 requires four. MobileGL can only ever EMIT to stream 0 - + // nothing in the shader pipeline supports layout(stream = N), EmitStreamVertex or a + // per-stream capture layout - but that is a statement about what a geometry shader + // may produce, not about which stream indices exist. Streams 1..3 exist and are + // permanently empty, and the two entry points that address a stream say so: an + // indexed primitive query on one answers zero (GL_Query's emptyVertexStream) and + // glDrawTransformFeedbackStream on one draws nothing. Answering 1 instead used to + // make both of them GL_INVALID_VALUE. + *params = kFrontendMaxVertexStreams; break; case GL_TRANSFORM_FEEDBACK_ACTIVE: *params = MG_State::pGLContext->IsTransformFeedbackActive() ? 1 : 0; @@ -2429,15 +2604,28 @@ namespace MobileGL::MG_Impl::GLImpl { case GL_MAX_TEXTURE_SIZE: *params = dynamicParameters.MaxTextureSize; break; - case GL_MAX_UNIFORM_BUFFER_BINDINGS: + case GL_MAX_UNIFORM_BUFFER_BINDINGS: { // Never advertise more bindings than the state layer's indexed-binding array can track // (BufferState::BufferBindingPointCount): glBindBufferBase rejects indices past that // capacity, and the GL CTS per-case state reset calls glBindBufferBase on every - // advertised index and expects no error. The floor equals the GL 3.3 core minimum - // (36), so the clamp never under-advertises. + // advertised index and expects no error. The floor is the GL 4.5 core minimum, and + // the array was widened to exactly it, so the two coincide by construction. + // + // WHY THE BACKEND'S OWN COUNT IS NOT THE CEILING HERE, unlike the shader-storage + // family. A GL uniform binding point is where an APPLICATION parks a buffer; it is + // not a driver binding point. Neither backend forwards it as one on the draw path: + // DirectGLES rebinds the blocks a program declares onto COMPACTED ES points + // (BindCurrentProgramWithResources maps block i to ES point i+1) and DirectVulkan + // resolves each block to a descriptor. So what the host driver's count bounds is how + // many blocks ONE PROGRAM may use, which is GL_MAX_COMBINED_UNIFORM_BLOCKS (70) - + // inside the ES 3.2 minimum of 72 - and not how many points an application may bind. + static_assert(static_cast(MG_State::GLState::BufferBindingPointCount) >= + kFrontendMinUniformBufferBindings, + "the indexed-binding array must be able to hold every advertised uniform binding point"); *params = std::clamp(dynamicParameters.MaxUniformBufferBindings, kFrontendMinUniformBufferBindings, static_cast(MG_State::GLState::BufferBindingPointCount)); break; + } case GL_MAX_UNIFORM_BLOCK_SIZE: *params = dynamicParameters.MaxUniformBlockSize; break; diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h index 6fa09f7f..d99e37bd 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h @@ -25,9 +25,17 @@ namespace MobileGL::MG_Impl::GLImpl { GLenum GetError(); GLenum GetGraphicsResetStatus(); // The GL_MAX_SAMPLES value MobileGL advertises, i.e. the driver's value floored to the GL - // core minimum. Frontend multisample validators have to honour this ceiling for every - // format, otherwise MobileGL rejects a sample count it advertised itself. + // core minimum of 4. This is the RENDERBUFFER ceiling; the three per-category texture + // ceilings below have a minimum of one and are reported as probed. GLint GetAdvertisedMaxSamples(); + // Exactly what GL_MAX_COLOR_TEXTURE_SAMPLES / GL_MAX_DEPTH_TEXTURE_SAMPLES / + // GL_MAX_INTEGER_SAMPLES report: the probed backend limit floored at the GL 4.6 core minimum + // of ONE (table 23.53). Exported so the frontend's storage validation enforces exactly what + // the query promised - it used to floor both at 4 and then let the backend quietly + // under-allocate whatever the driver could not actually provide. + GLint GetAdvertisedColorTextureMaxSamples(); + GLint GetAdvertisedDepthTextureMaxSamples(); + GLint GetAdvertisedIntegerMaxSamples(); // What glGetIntegerv(GL_SAMPLES) answers for the CURRENT draw framebuffer: the largest sample // count over its attachments, and 0 for a single-sample or default framebuffer (GL 4.6 core // 9.2.3 / 22.2 - GL_SAMPLE_BUFFERS is 1 exactly when this is non-zero). diff --git a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp index dcc470f5..1a79a0e2 100644 --- a/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp +++ b/MobileGL/MG_Impl/GLImpl/Query/GL_Query.cpp @@ -40,6 +40,12 @@ namespace MobileGL::MG_Impl::GLImpl { // stand in for the backend's. Uint64 accountedCaptureDrawSnapshot = 0; Uint64 geometryCaptureDrawSnapshot = 0; + // Set when glBeginQueryIndexed named a vertex stream above 0. MobileGL advertises + // GL_MAX_VERTEX_STREAMS = 4 because GL 4.5 requires it, and nothing in the shader + // pipeline can emit to a stream other than 0 - so the primitive count on any other + // stream is provably zero, and this makes the object report that instead of + // aliasing stream 0's backend counter. + Bool emptyVertexStream = false; }; // Query calls may arrive from any thread (launchers migrate the context @@ -116,6 +122,7 @@ namespace MobileGL::MG_Impl::GLImpl { queryObject->ended = false; queryObject->resultCached = false; queryObject->cachedResult = 0; + queryObject->emptyVertexStream = false; } // Callers must hold g_queryObjectsMutex. @@ -188,6 +195,23 @@ namespace MobileGL::MG_Impl::GLImpl { return false; } + // A span begun on a vertex stream this implementation can never emit to. The answer + // is zero, and it is available immediately - the backend query that ran alongside it + // counted stream 0 and must not be reported here. + if (queryObject->emptyVertexStream) { + switch (pname) { + case GL_QUERY_RESULT: + case GL_QUERY_RESULT_NO_WAIT: + outValue = 0; + return true; + case GL_QUERY_RESULT_AVAILABLE: + outValue = GL_TRUE; + return true; + default: + break; + } + } + switch (pname) { case GL_QUERY_TARGET: // The target a query was begun with (or created with, for glCreateQueries) - state @@ -741,14 +765,15 @@ namespace MobileGL::MG_Impl::GLImpl { } namespace { + Bool IsPerVertexStreamQueryTarget(GLenum target) { + return target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN; + } + // The indexed query entry points differ from the plain ones only in the vertex // stream they address (GL 4.6 core 4.2.1): index must be below GL_MAX_VERTEX_STREAMS - // for the two transform feedback targets and zero for every other target. With a - // single vertex stream both bounds are 1, so a valid call is always index 0 and - // forwards to the unindexed implementation. + // for the two transform feedback targets and zero for every other target. Bool ValidateQueryStreamIndex(const char* function, GLenum target, GLuint index) { - const Bool perStreamTarget = - target == GL_PRIMITIVES_GENERATED || target == GL_TRANSFORM_FEEDBACK_PRIMITIVES_WRITTEN; + const Bool perStreamTarget = IsPerVertexStreamQueryTarget(target); GLint maxVertexStreams = 1; if (perStreamTarget) { GetIntegerv(GL_MAX_VERTEX_STREAMS, &maxVertexStreams); @@ -761,11 +786,29 @@ namespace MobileGL::MG_Impl::GLImpl { : "index must be zero for this query target."); return false; } + + // Streams 1..GL_MAX_VERTEX_STREAMS-1 exist but nothing can emit to them, so a span begun + // on one counts zero primitives. Flagging the object is what keeps that answer honest: + // BeginQuery below still opens a real backend query (it is the only way to reuse the + // whole target/object state machine), and that query counts STREAM 0. + // + // Known simplification, spelled out rather than hidden: because the backend query is + // shared, only ONE query may be active per target here, while GL allows one per + // (target, stream) pair. A program running a stream-0 and a stream-2 + // GL_PRIMITIVES_GENERATED query at the same time gets GL_INVALID_OPERATION on the + // second. Nothing can produce a non-zero stream-2 result to be worth more than that + // until the shader pipeline grows layout(stream = N). + void MarkQueryEmptyVertexStream(GLuint id) { + const std::lock_guard lock(g_queryObjectsMutex); + auto* queryObject = FindQueryObjectLocked(id); + if (queryObject && queryObject->active) queryObject->emptyVertexStream = true; + } } // namespace void BeginQueryIndexed(GLenum target, GLuint index, GLuint id) { if (!ValidateQueryStreamIndex(__FUNCTION__, target, index)) return; BeginQuery(target, id); + if (index != 0 && IsPerVertexStreamQueryTarget(target)) MarkQueryEmptyVertexStream(id); } void EndQueryIndexed(GLenum target, GLuint index) { diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index ad440012..8ec63b27 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -522,44 +522,46 @@ namespace MobileGL::MG_Impl::GLImpl { return sampleCounts.empty() ? 0 : sampleCounts.front(); } - // The ceiling the frontend enforces, which must never be lower than the one MobileGL - // advertises: the CTS - and real applications - read GL_MAX_SAMPLES once and hand that - // exact count to glTexImage*Multisample for every format. Answering 4 there and then - // rejecting 4 here because the ES driver reports GL_MAX_INTEGER_SAMPLES 1 (Adreno) is a - // self-inconsistency, not a spec-mandated error. The backends clamp the count they hand - // the driver; the shadow state keeps reporting what the application asked for. + // The ceiling the frontend enforces, which is EXACTLY the one MobileGL advertises for + // this format's category - GL_MAX_DEPTH_TEXTURE_SAMPLES, GL_MAX_INTEGER_SAMPLES or + // GL_MAX_COLOR_TEXTURE_SAMPLES, all three of which have a GL 4.6 minimum of one and are + // reported as probed. It used to floor all three at GL_MAX_SAMPLES (4) on the reasoning + // that an application reads GL_MAX_SAMPLES once and hands that count to every + // glTexStorage*Multisample. That reasoning had it backwards: on Adreno and on Mali an + // integer multisample texture is backed by ONE sample, so accepting four here did not + // make four samples exist - ClampSamplesToBackendSupport quietly allocated one and the + // application wrote per-sample data it could never read back. Raising INVALID_OPERATION + // is what a real driver does, and it is what makes that silent squeeze unreachable for + // application-visible storage. Int GetMaxSupportedTextureSamples(TextureTarget textureTarget, TextureInternalFormat textureInternalFormat) { if (MG_Backend::pActiveBackendObject == nullptr) { return std::numeric_limits::max(); } - const Int advertisedMaxSamples = GetAdvertisedMaxSamples(); + const Bool isDepthOrStencil = MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) || + MG_Util::IsStencilFormatInternalFormat(textureInternalFormat); + Bool isIntegerFormat = false; + if (!isDepthOrStencil) { + GLenum normalizedInternalFormat = + MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat); + GLenum normalizedFormat = GL_RGBA; + GLenum normalizedType = GL_UNSIGNED_BYTE; + MG_Util::TextureFormatProcessor::NormalizePixelFormat( + normalizedInternalFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, + &normalizedFormat, &normalizedType); + isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER || + normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER; + } + const Int categoryMaxSamples = isDepthOrStencil ? GetAdvertisedDepthTextureMaxSamples() + : isIntegerFormat ? GetAdvertisedIntegerMaxSamples() + : GetAdvertisedColorTextureMaxSamples(); + // glGetInternalformativ(GL_SAMPLES) is answered from this very list (GetInternalformativ // below), and GL 4.6 core 8.8 makes that query the definition of the per-format // maximum - validating against anything else is how the two answers drifted apart. const Int probedMaxSamples = GetProbedMaxTextureSamples(textureTarget, textureInternalFormat); - if (probedMaxSamples > 0) { - return std::max(probedMaxSamples, advertisedMaxSamples); - } - - const auto& dynamicParameters = MG_Backend::pActiveBackendObject->GetDynamicParameters(); - if (MG_Util::IsDepthFormatInternalFormat(textureInternalFormat) || - MG_Util::IsStencilFormatInternalFormat(textureInternalFormat)) { - return std::max(dynamicParameters.MaxDepthTextureSamples, advertisedMaxSamples); - } - - GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat); - GLenum normalizedFormat = GL_RGBA; - GLenum normalizedType = GL_UNSIGNED_BYTE; - MG_Util::TextureFormatProcessor::NormalizePixelFormat( - normalizedInternalFormat, PixelFormatNormalizeOptionBit::None, &normalizedInternalFormat, - &normalizedFormat, &normalizedType); - const Bool isIntegerFormat = normalizedFormat == GL_RED_INTEGER || normalizedFormat == GL_RG_INTEGER || - normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER; - return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples - : dynamicParameters.MaxColorTextureSamples, - advertisedMaxSamples); + return probedMaxSamples > 0 ? std::max(probedMaxSamples, categoryMaxSamples) : categoryMaxSamples; } Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width, diff --git a/MobileGL/MG_Impl/GLImpl/VertexArray/Validators.cpp b/MobileGL/MG_Impl/GLImpl/VertexArray/Validators.cpp index 564d7b9c..adeae59f 100644 --- a/MobileGL/MG_Impl/GLImpl/VertexArray/Validators.cpp +++ b/MobileGL/MG_Impl/GLImpl/VertexArray/Validators.cpp @@ -12,15 +12,15 @@ #include #include #include +#include namespace MobileGL::MG_Impl::GLImpl::VertexArrayImpl { Uint GetMaxVertexAttribs() { - constexpr Uint capacity = static_cast(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); - if (!MG_Backend::pActiveBackendObject) return capacity; - - const Int backendLimit = MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs; - if (backendLimit <= 0) return capacity; - return std::min(static_cast(backendLimit), capacity); + // Shared with reflection's limit and with gl_MaxVertexAttribs; see ResolveMaxVertexAttribs. + const Bool hasBackend = MG_Backend::pActiveBackendObject != nullptr; + const Int backendLimit = + hasBackend ? MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxVertexAttribs : 0; + return static_cast(MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(hasBackend, backendLimit)); } Uint GetMaxVertexAttribBindings() { diff --git a/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp b/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp index c04b9d17..0316d291 100644 --- a/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp +++ b/MobileGL/MG_IntegrationTest/Scenarios/AdvertisedLimitsScenario.cpp @@ -56,7 +56,13 @@ namespace MGITest { const std::vector& BufferLimitTable() { static const std::vector table = { - {GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 36, 256}, + // 84 is the GL 4.5 core table 23.64 minimum, and also the width of the state + // layer's indexed-binding array - the two were made to coincide when the array + // was widened from 36, which had made the clamp in GL_Getter degenerate. + {GL_MAX_UNIFORM_BUFFER_BINDINGS, "GL_MAX_UNIFORM_BUFFER_BINDINGS", 84, 256}, + // 14 uniform blocks on each of the FIVE graphics stages. The sum used to count + // three, and the two tessellation stages were simply missing from it. + {GL_MAX_COMBINED_UNIFORM_BLOCKS, "GL_MAX_COMBINED_UNIFORM_BLOCKS", 70, 256}, {GL_MAX_COMPUTE_UNIFORM_BLOCKS, "GL_MAX_COMPUTE_UNIFORM_BLOCKS", 12, 256}, {GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS, "GL_MAX_COMPUTE_SHADER_STORAGE_BLOCKS", 8, 256}, {GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS, "GL_MAX_COMBINED_SHADER_STORAGE_BLOCKS", 8, 256}, @@ -199,6 +205,75 @@ namespace MGITest { "derived component limits are computed in"; } + // The GL 4.5 core minimums that had no case in the getter at all, or that were still + // carrying an ES/GL3.3-tier number. Every one of these answered GL_INVALID_ENUM or a + // too-small value against a context advertising 4.6, and each is the FIRST call its + // conformance case makes - so the case died before it could measure anything. + // + // The cull pair is deliberately absent: zero is a legal answer there (a backend with no + // cull-distance route MUST report it), so it is checked for answerability only, below. + TEST_F(AdvertisedLimitsScenario, EveryGL45CoreMinimumIsMet) { + const std::vector table = { + {GL_MAX_VARYING_VECTORS, "GL_MAX_VARYING_VECTORS", 15, 256}, + {GL_MAX_VERTEX_UNIFORM_VECTORS, "GL_MAX_VERTEX_UNIFORM_VECTORS", 256, 1 << 20}, + {GL_MAX_VARYING_COMPONENTS, "GL_MAX_VARYING_COMPONENTS", 60, 1 << 20}, + {GL_MAX_VERTEX_STREAMS, "GL_MAX_VERTEX_STREAMS", 4, 64}, + {GL_MAX_GEOMETRY_SHADER_INVOCATIONS, "GL_MAX_GEOMETRY_SHADER_INVOCATIONS", 32, 256}, + {GL_MAX_SUBROUTINES, "GL_MAX_SUBROUTINES", 256, 1 << 20}, + {GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS, "GL_MAX_SUBROUTINE_UNIFORM_LOCATIONS", 1024, 1 << 20}, + {GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_INPUT_COMPONENTS", 128, 1 << 16}, + {GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS", 128, 1 << 16}, + {GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS", 4096, + 1 << 20}, + {GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS", 16, 256}, + {GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS", 1024, 1 << 20}, + {GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS, "GL_MAX_TESS_CONTROL_UNIFORM_BLOCKS", 14, 256}, + {GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS", 128, 1 << 16}, + {GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS", 128, 1 << 16}, + {GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS", 16, 256}, + {GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS", 1024, + 1 << 20}, + {GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS, "GL_MAX_TESS_EVALUATION_UNIFORM_BLOCKS", 14, 256}, + {GL_MAX_TESS_PATCH_COMPONENTS, "GL_MAX_TESS_PATCH_COMPONENTS", 120, 1 << 16}, + {GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS, "GL_MAX_COMBINED_TESS_CONTROL_UNIFORM_COMPONENTS", + 58368, 1 << 30}, + {GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS, + "GL_MAX_COMBINED_TESS_EVALUATION_UNIFORM_COMPONENTS", 58368, 1 << 30}, + }; + for (const LimitBound& bound : table) { + GLint value = -424242; + glGetIntegerv(bound.pname, &value); + const unsigned int error = FirstGLError(); + EXPECT_EQ(error, GLenum(GL_NO_ERROR)) << bound.name << " is not answerable: " << GLErrorName(error); + if (error != GL_NO_ERROR) continue; + EXPECT_GE(value, bound.minimum) << bound.name << " = " << value << " is below the GL 4.5 minimum " + << bound.minimum; + EXPECT_LE(value, bound.ceiling) << bound.name << " = " << value << " exceeds the ceiling " + << bound.ceiling; + } + + // ARB_cull_distance's pair. Zero is honest on a backend with no cull-distance route, + // so only answerability and the combined-limit ordering are checked here. + GLint cull = -1; + GLint clip = -1; + GLint combined = -1; + glGetIntegerv(GL_MAX_CULL_DISTANCES, &cull); + glGetIntegerv(GL_MAX_CLIP_DISTANCES, &clip); + glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &combined); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)) << "the ARB_cull_distance queries must not error"; + EXPECT_GE(cull, 0); + EXPECT_GE(combined, cull) << "GL 4.6 core 11.1.3.10: the combined limit is at least the cull one"; + EXPECT_GE(combined, clip) << "GL 4.6 core 11.1.3.10: the combined limit is at least the clip one"; + + // GL_MAX_ELEMENT_INDEX is 64-bit state: the required 2^32-1 does not fit a GLint, so + // the wide query must answer it and the narrow one must saturate rather than wrap. + GLint64 elementIndex = -1; + glGetInteger64v(GL_MAX_ELEMENT_INDEX, &elementIndex); + ASSERT_EQ(FirstGLError(), GLenum(GL_NO_ERROR)); + EXPECT_GE(elementIndex, static_cast(4294967295LL)) + << "GL 4.5 core table 23.55 sets the GL_MAX_ELEMENT_INDEX minimum at 2^32-1"; + } + // ARB_viewport_array's own limits. They are advertised from three different places - // GL_MAX_VIEWPORTS from the frontend's indexed state width, the bounds range and the // subpixel bits from the backend caps table - and each backend fills that table from a diff --git a/MobileGL/MG_State/GLState/BufferState/BufferState.h b/MobileGL/MG_State/GLState/BufferState/BufferState.h index efff69da..51d3b1d6 100644 --- a/MobileGL/MG_State/GLState/BufferState/BufferState.h +++ b/MobileGL/MG_State/GLState/BufferState/BufferState.h @@ -19,7 +19,14 @@ namespace MobileGL::MG_State::GLState { BufferTarget::DrawIndirect, BufferTarget::Parameter, BufferTarget::ShaderStorage); constexpr const auto BufferBindPointTargets = ToArray(BufferTarget::Uniform, BufferTarget::TransformFeedback, BufferTarget::AtomicCounter, BufferTarget::ShaderStorage); - constexpr SizeT BufferBindingPointCount = 36; + // How many indexed binding points each of BufferBindPointTargets gets. 84 is the GL 4.5 core + // minimum for GL_MAX_UNIFORM_BUFFER_BINDINGS (table 23.64) and this array is the capacity + // that limit is clamped against - at 36 the clamp in GL_Getter was degenerate (lo == hi) and + // no application could ever be told about, or bind to, a binding point past the 36th. The + // other three targets advertise their own, smaller ceilings out of + // GetIndexedBufferQueryPointCount, so widening this does not widen what they promise; it only + // costs the unused tail of three arrays. + constexpr SizeT BufferBindingPointCount = 84; class BufferState { public: diff --git a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp index 627ada37..e4e15a7e 100644 --- a/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp +++ b/MobileGL/MG_State/GLState/ProgramState/ProgramLinkTask.cpp @@ -29,13 +29,11 @@ namespace { // capacity, which is also the width of the Uint32 masks backends build from it. static MobileGL::Int GetReflectionVertexAttribLimit( const MobileGL::MG_Util::ShaderTranspiler::CompileEnv& env) { - constexpr MobileGL::Int capacity = - static_cast(MobileGL::MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); - if (!env.HasBackend()) return capacity; - - const MobileGL::Int backendLimit = env.params.MaxVertexAttribs; - if (backendLimit <= 0) return capacity; - return std::min(backendLimit, capacity); + // One shared definition with glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) and with + // BuildTBuiltInResource's gl_MaxVertexAttribs - the three used to carry three copies of + // this formula and glslang's copy was a hardcoded 64. + return MobileGL::MG_Util::ShaderTranspiler::ResolveMaxVertexAttribs(env.HasBackend(), + env.params.MaxVertexAttribs); } // Everything the post-link query surface ever asks a glslang::TType, flattened into a diff --git a/MobileGL/MG_Test/SanityTest.cpp b/MobileGL/MG_Test/SanityTest.cpp index 184f5533..454f7296 100644 --- a/MobileGL/MG_Test/SanityTest.cpp +++ b/MobileGL/MG_Test/SanityTest.cpp @@ -41,8 +41,15 @@ namespace { class DynamicParameterBackend final : public MobileGL::MG_Backend::BackendObject { public: - explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params): - m_params(params) {} + // `type` defaults to Unknown, which is what every existing case wanted: a limits-only + // double with no backend identity. A case that captures a CompileEnv from it and then + // compares the result against glGetIntegerv has to pass a REAL type, because + // CompileEnv::HasBackend() is what BuildTBuiltInResource bounds gl_MaxVertexAttribs by + // while the getter bounds it by "a backend object exists" - two spellings of the same + // thing in production, and only in production. + explicit DynamicParameterBackend(MobileGL::MG_Backend::DynamicBackendParameters params, + MobileGL::BackendType type = MobileGL::BackendType::Unknown): + m_params(params), m_type(type) {} void Initialize() override {} MobileGL::Bool InitCapabilities() override { return true; } @@ -55,10 +62,11 @@ namespace { const MobileGL::MG_Backend::DynamicBackendParameters& GetDynamicParameters() const override { return m_params; } - MobileGL::BackendType GetBackendType() const override { return MobileGL::BackendType::Unknown; } + MobileGL::BackendType GetBackendType() const override { return m_type; } private: MobileGL::MG_Backend::DynamicBackendParameters m_params; + MobileGL::BackendType m_type = MobileGL::BackendType::Unknown; MobileGL::MG_Backend::GlobalBackendFunctionsTable m_functions{}; MobileGL::RendererInfo m_info{ .RendererName = "Test", @@ -722,6 +730,62 @@ TEST(DirectVulkanSanity, GatesClipDistancesOnTheShaderClipDistanceFeature) { EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8); } +// The cull half of the same contract. shaderCullDistance is a SEPARATE feature from +// shaderClipDistance - VulkanRenderer enables each independently - so it gets its own gate, and +// the combined limit is gated on either being present because GL 4.6 core 11.1.3.10 makes it at +// least as large as both halves. These three used to be literal 8s inside BuildTBuiltInResource +// with no device consulted at all, which let glslang accept a gl_CullDistance write that then +// discarded every primitive it touched. +TEST(DirectVulkanSanity, GatesCullDistancesOnTheShaderCullDistanceFeature) { + using namespace MobileGL; + + MG_Backend::DirectVulkan::BackendObject_DirectVulkan backend; + MG_External::VulkanCapabilities caps; + caps.MaxClipDistances = 8; + caps.MaxCullDistances = 8; + caps.MaxCombinedClipAndCullDistances = 8; + + caps.SupportsShaderClipDistance = false; + caps.SupportsShaderCullDistance = false; + backend.ApplyVulkanCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0); + + // Clip only: cull stays zero, and the combined limit still describes the clip capacity. + caps.SupportsShaderClipDistance = true; + backend.ApplyVulkanCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); + + caps.SupportsShaderCullDistance = true; + backend.ApplyVulkanCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); +} + +// DirectGLES reaches clip AND cull distances only through GL_EXT_clip_cull_distance, so the +// loader leaves all three at zero without it and the backend forwards that verbatim. Zero is the +// answer that stops a gl_CullDistance shader from reaching an ESSL compiler that would reject it. +TEST(DirectGLESSanity, ForwardsTheProbedClipAndCullDistanceLimits) { + using namespace MobileGL; + + MG_Backend::DirectGLES::BackendObject_DirectGLES backend; + MG_External::GLESCapabilities caps; + backend.ApplyGLESCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 0); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 0); + + caps.SupportsClipDistance = true; + caps.MaxClipDistances = 8; + caps.MaxCullDistances = 8; + caps.MaxCombinedClipAndCullDistances = 8; + backend.ApplyGLESCapabilitiesForTesting(caps); + EXPECT_EQ(backend.GetDynamicParameters().MaxClipDistances, 8); + EXPECT_EQ(backend.GetDynamicParameters().MaxCullDistances, 8); + EXPECT_EQ(backend.GetDynamicParameters().MaxCombinedClipAndCullDistances, 8); +} + // GL_LAYER_PROVOKING_VERTEX / GL_VIEWPORT_INDEX_PROVOKING_VERTEX were a hard-coded // GL_LAST_VERTEX_CONVENTION for both backends, derived from nothing, and wrong on both test // devices in opposite directions. DirectGLES now forwards what its loader resolved; DirectVulkan @@ -1234,6 +1298,176 @@ void main() { MG_State::pGLContext = Move(previousContext); } +// THE invariant every KHR-GL45.limits.* case checks, in one place. When the conformance table +// gives a limit both a glGetIntegerv pname and a GLSL built-in constant, it reads the query and +// then compiles a shader that writes the built-in into an SSBO and demands EXACT equality - so a +// limit answered from two unreconciled tables fails the SECOND half of the case, with a message +// about a number rather than about the two tables. Seven of them did: gl_MaxVertexAttribs said 64 +// against a query of 32, gl_MaxDrawBuffers 32 against 8, gl_MaxCombinedTextureImageUnits 80 +// against 96, gl_MaxVaryingComponents 60 against 64, gl_MaxCombinedShaderOutputResources 8 +// against 29. +// +// KEEP THIS TABLE GROWING. Every pname added to GL_Getter that also has a gl_Max* built-in +// belongs here; that is what stops the next one from drifting. +TEST(GetterSanity, EveryLimitWithABuiltinAgreesWithItsQuery) { + using namespace MobileGL; + + auto previousContext = Move(MG_State::pGLContext); + auto previousBackend = Move(MG_Backend::pActiveBackendObject); + MG_State::pGLContext = MakeUnique(); + MG_Backend::pActiveBackendObject = + MakeUnique(MG_Backend::DynamicBackendParameters{}, BackendType::DirectGLES); + + struct LimitPair { + GLenum pname; + const char* builtin; + }; + const LimitPair pairs[] = { + {GL_MAX_VERTEX_ATTRIBS, "gl_MaxVertexAttribs"}, + {GL_MAX_VERTEX_UNIFORM_COMPONENTS, "gl_MaxVertexUniformComponents"}, + {GL_MAX_VERTEX_UNIFORM_VECTORS, "gl_MaxVertexUniformVectors"}, + {GL_MAX_VERTEX_OUTPUT_COMPONENTS, "gl_MaxVertexOutputComponents"}, + {GL_MAX_VARYING_COMPONENTS, "gl_MaxVaryingComponents"}, + {GL_MAX_VARYING_VECTORS, "gl_MaxVaryingVectors"}, + {GL_MAX_VERTEX_TEXTURE_IMAGE_UNITS, "gl_MaxVertexTextureImageUnits"}, + {GL_MAX_TEXTURE_IMAGE_UNITS, "gl_MaxTextureImageUnits"}, + {GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS, "gl_MaxCombinedTextureImageUnits"}, + {GL_MAX_FRAGMENT_UNIFORM_COMPONENTS, "gl_MaxFragmentUniformComponents"}, + {GL_MAX_FRAGMENT_UNIFORM_VECTORS, "gl_MaxFragmentUniformVectors"}, + {GL_MAX_FRAGMENT_INPUT_COMPONENTS, "gl_MaxFragmentInputComponents"}, + {GL_MAX_DRAW_BUFFERS, "gl_MaxDrawBuffers"}, + {GL_MAX_IMAGE_UNITS, "gl_MaxImageUnits"}, + // The SAME token (0x8F39) under two spellings, and the two glslang fields behind them + // must therefore carry the same value. + {GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS, "gl_MaxCombinedImageUnitsAndFragmentOutputs"}, + {GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES, "gl_MaxCombinedShaderOutputResources"}, + {GL_MAX_CLIP_DISTANCES, "gl_MaxClipDistances"}, + {GL_MAX_CULL_DISTANCES, "gl_MaxCullDistances"}, + {GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, "gl_MaxCombinedClipAndCullDistances"}, + {GL_MAX_SAMPLES, "gl_MaxSamples"}, + {GL_MIN_PROGRAM_TEXEL_OFFSET, "gl_MinProgramTexelOffset"}, + {GL_MAX_PROGRAM_TEXEL_OFFSET, "gl_MaxProgramTexelOffset"}, + {GL_MAX_GEOMETRY_INPUT_COMPONENTS, "gl_MaxGeometryInputComponents"}, + {GL_MAX_GEOMETRY_OUTPUT_COMPONENTS, "gl_MaxGeometryOutputComponents"}, + {GL_MAX_GEOMETRY_TEXTURE_IMAGE_UNITS, "gl_MaxGeometryTextureImageUnits"}, + {GL_MAX_GEOMETRY_OUTPUT_VERTICES, "gl_MaxGeometryOutputVertices"}, + {GL_MAX_GEOMETRY_TOTAL_OUTPUT_COMPONENTS, "gl_MaxGeometryTotalOutputComponents"}, + {GL_MAX_GEOMETRY_UNIFORM_COMPONENTS, "gl_MaxGeometryUniformComponents"}, + {GL_MAX_PATCH_VERTICES, "gl_MaxPatchVertices"}, + {GL_MAX_TESS_GEN_LEVEL, "gl_MaxTessGenLevel"}, + {GL_MAX_TESS_CONTROL_INPUT_COMPONENTS, "gl_MaxTessControlInputComponents"}, + {GL_MAX_TESS_CONTROL_OUTPUT_COMPONENTS, "gl_MaxTessControlOutputComponents"}, + {GL_MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS, "gl_MaxTessControlTextureImageUnits"}, + {GL_MAX_TESS_CONTROL_UNIFORM_COMPONENTS, "gl_MaxTessControlUniformComponents"}, + {GL_MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS, "gl_MaxTessControlTotalOutputComponents"}, + {GL_MAX_TESS_EVALUATION_INPUT_COMPONENTS, "gl_MaxTessEvaluationInputComponents"}, + {GL_MAX_TESS_EVALUATION_OUTPUT_COMPONENTS, "gl_MaxTessEvaluationOutputComponents"}, + {GL_MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS, "gl_MaxTessEvaluationTextureImageUnits"}, + {GL_MAX_TESS_EVALUATION_UNIFORM_COMPONENTS, "gl_MaxTessEvaluationUniformComponents"}, + {GL_MAX_TESS_PATCH_COMPONENTS, "gl_MaxTessPatchComponents"}, + {GL_MAX_TRANSFORM_FEEDBACK_BUFFERS, "gl_MaxTransformFeedbackBuffers"}, + {GL_MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS, "gl_MaxTransformFeedbackInterleavedComponents"}, + // gl_MaxAtomicCounterBindings is glslang's name for the binding count; the GL spelling is + // GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS. + {GL_MAX_ATOMIC_COUNTER_BUFFER_BINDINGS, "gl_MaxAtomicCounterBindings"}, + {GL_MAX_ATOMIC_COUNTER_BUFFER_SIZE, "gl_MaxAtomicCounterBufferSize"}, + }; + + // The compile runs against a captured env, exactly as the pipeline's does - that is what + // makes "the resource table" mean the same thing here as it does in production. + const auto env = MG_Util::ShaderTranspiler::CaptureCompileEnv(); + for (const LimitPair& pair : pairs) { + GLint reported = -424242; + MG_Impl::GLImpl::GetIntegerv(pair.pname, &reported); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR) + << pair.builtin << "'s pname is not answerable at all"; + + // A negative array size is a compile error, so the stage only compiles when the built-in + // equals what the query just reported. Two-sided by construction: a resource table that + // is too permissive fails it exactly like one that is too tight. One shader per pair, so + // a failure names the limit instead of reporting "something disagreed". + const String source = String("#version 460 core\nout vec4 mgColor;\nconst int mgAgree = (") + + pair.builtin + " == " + std::to_string(reported) + + ") ? 1 : -1;\nint mgProbe[mgAgree];\nvoid main() { mgProbe[0] = 0; mgColor = " + "vec4(float(mgProbe[0])); }\n"; + auto compiled = MG_Util::ShaderTranspiler::ShaderCompiler::CompileShader({ + .shaderType = GL_FRAGMENT_SHADER, + .sourceStr = source, + .env = env.get(), + }); + EXPECT_TRUE(compiled) << pair.builtin << " does not equal glGetIntegerv's " << reported << ":\n" + << (compiled ? String() : compiled.error().log); + } + + MG_Backend::pActiveBackendObject = Move(previousBackend); + MG_State::pGLContext = Move(previousContext); +} + +// GL_MAX_ELEMENT_INDEX is 64-bit state whose required value (2^32-1) does not fit a GLint, so it +// needs its own case in BOTH widths: the 64-bit query has to answer 4294967295 and the 32-bit one +// has to saturate, per the GL state-query conversion rules. It used to be a single `1024 * 1024; +// // TODO` in the 32-bit table, and glGetInteger64v - which is how the conformance suite reads it +// - widened that. +TEST(GetterSanity, MaxElementIndexIsTheFull32BitIndexCeiling) { + using namespace MobileGL; + + auto previousContext = Move(MG_State::pGLContext); + auto previousBackend = Move(MG_Backend::pActiveBackendObject); + MG_State::pGLContext = MakeUnique(); + MG_Backend::pActiveBackendObject = MakeUnique(MG_Backend::DynamicBackendParameters{}); + + GLint64 wide = -1; + MG_Impl::GLImpl::GetInteger64v(GL_MAX_ELEMENT_INDEX, &wide); + EXPECT_EQ(wide, static_cast(0xFFFFFFFFLL)); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + GLint narrow = -1; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_ELEMENT_INDEX, &narrow); + EXPECT_EQ(narrow, INT32_MAX) << "the 32-bit query must saturate, not truncate or wrap"; + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Backend::pActiveBackendObject = Move(previousBackend); + MG_State::pGLContext = Move(previousContext); +} + +// GL 4.6 core table 23.53 gives GL_MAX_SAMPLES a minimum of four and the three per-category +// ceilings a minimum of ONE. Flooring the latter at four is the advertised-caps lie that made +// KHR-GL46.sample_variables.mask.rgba8i run at all: the frontend promised four integer samples, +// the backend clamped the realised allocation to the one the driver can back, and the application +// wrote per-sample data it could never read. +TEST(GetterSanity, PerCategoryMultisampleCeilingsAreProbedRatherThanFlooredAtFour) { + using namespace MobileGL; + + auto previousContext = Move(MG_State::pGLContext); + auto previousBackend = Move(MG_Backend::pActiveBackendObject); + MG_State::pGLContext = MakeUnique(); + + MG_Backend::DynamicBackendParameters params; + params.MaxSamples = 4; + params.MaxColorTextureSamples = 4; + params.MaxDepthTextureSamples = 2; + params.MaxIntegerSamples = 1; + MG_Backend::pActiveBackendObject = MakeUnique(params); + + GLint reported = -1; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_INTEGER_SAMPLES, &reported); + EXPECT_EQ(reported, 1) << "an integer multisample texture is backed by one sample here, and " + "saying otherwise is what the application allocates against"; + MG_Impl::GLImpl::GetIntegerv(GL_MAX_DEPTH_TEXTURE_SAMPLES, &reported); + EXPECT_EQ(reported, 2); + MG_Impl::GLImpl::GetIntegerv(GL_MAX_COLOR_TEXTURE_SAMPLES, &reported); + EXPECT_EQ(reported, 4); + // ...while GL_MAX_SAMPLES keeps its floor of four, which is the one the spec really requires. + params.MaxSamples = 1; + MG_Backend::pActiveBackendObject = MakeUnique(params); + MG_Impl::GLImpl::GetIntegerv(GL_MAX_SAMPLES, &reported); + EXPECT_EQ(reported, 4); + EXPECT_EQ(MG_Impl::GLImpl::GetError(), GL_NO_ERROR); + + MG_Backend::pActiveBackendObject = Move(previousBackend); + MG_State::pGLContext = Move(previousContext); +} + TEST(GetterSanity, ReportsKhrSubgroupDynamicParameters) { using namespace MobileGL; diff --git a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp index b3b391a6..6078e1a2 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp @@ -563,7 +563,12 @@ namespace { // BuildTBuiltInResource does not read it. a.maxComputeWorkGroupInvocations = 128; b.maxComputeWorkGroupInvocations = 2048; - // (4) a spread of DynamicBackendParameters fields the front end never reads + // (4) a spread of DynamicBackendParameters fields the front end never reads. + // MaxTextureImageUnits used to be here and is NOT any more: the GL 4.6 API-surface wave + // made BuildTBuiltInResource read it (gl_MaxTextureImageUnits expands from it), so it + // moved to TheFrontendFingerprintMovesWithEveryFrontendLimit. That migration is the + // third one this helper has survived; check BuildTBuiltInResource before adding a field + // here. a.params.MaxColorTextureSamples = 1; b.params.MaxColorTextureSamples = 8; a.params.MaxTextureSize = 4096; @@ -572,8 +577,8 @@ namespace { b.params.MaxViewports = 16; a.params.MaxUniformBufferBindings = 24; b.params.MaxUniformBufferBindings = 84; - a.params.MaxTextureImageUnits = 16; - b.params.MaxTextureImageUnits = 32; + a.params.MaxRenderbufferSize = 4096; + b.params.MaxRenderbufferSize = 16384; return {a, b}; } } // namespace @@ -677,6 +682,20 @@ TEST_F(TranslationCacheTest, TheFrontendFingerprintMovesWithEveryFrontendLimit) // wave4's 4fc3531d: glslang rejects gl_ClipDistance[i] past this at parse AND expands // gl_MaxClipDistances from it, so it is both a compile gate and a baked constant. {"params.MaxClipDistances", [](CompileEnv& e) { e.params.MaxClipDistances += 1; }}, + // The GL 4.6 API-surface wave: six more TBuiltInResource fields that used to be stock + // glslang literals. The cull pair is the MaxClipDistances story exactly (parse gate plus + // gl_MaxCullDistances / gl_MaxCombinedClipAndCullDistances); the texture-image-unit three + // and MaxSamples are baked constants (gl_MaxTextureImageUnits, + // gl_MaxVertexTextureImageUnits, gl_MaxCombinedTextureImageUnits, gl_MaxSamples - the + // last of which also sizes gl_SampleMask[]). + {"params.MaxCullDistances", [](CompileEnv& e) { e.params.MaxCullDistances += 1; }}, + {"params.MaxCombinedClipAndCullDistances", + [](CompileEnv& e) { e.params.MaxCombinedClipAndCullDistances += 1; }}, + {"params.MaxTextureImageUnits", [](CompileEnv& e) { e.params.MaxTextureImageUnits += 1; }}, + {"params.MaxVertexTextureImageUnits", [](CompileEnv& e) { e.params.MaxVertexTextureImageUnits += 1; }}, + {"params.MaxCombinedTextureImageUnits", + [](CompileEnv& e) { e.params.MaxCombinedTextureImageUnits += 1; }}, + {"params.MaxSamples", [](CompileEnv& e) { e.params.MaxSamples += 1; }}, {"maxComputeWorkGroupSize[0]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[0] += 1; }}, {"maxComputeWorkGroupSize[1]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[1] += 1; }}, {"maxComputeWorkGroupSize[2]", [](CompileEnv& e) { e.maxComputeWorkGroupSize[2] += 1; }}, diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index 1ba54549..47fb349d 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -1139,6 +1139,11 @@ namespace MobileGL::MG_Util::BackendLoader { // optimistic 8 behind, so the frontend promised eight clip planes and every draw with a // clipping program silently rendered nothing. The guarded probe below only ever widens it. GLint maxClipDistances = 0; + // The cull half of the same extension, and the same "zero is a legal answer" rule: a cull + // distance discards the whole primitive, so promising eight on a driver that has none does + // not fail loudly, it drops every draw of a culling program. + GLint maxCullDistances = 0; + GLint maxCombinedClipAndCullDistances = 0; GLint maxViewports = 16; // GL_UNDEFINED_VERTEX is what stands when the probes below cannot run, and it is a legal // answer rather than a placeholder: with neither geometry shaders nor a viewport array @@ -1333,6 +1338,23 @@ namespace MobileGL::MG_Util::BackendLoader { "rejected; reporting no clip distances"); maxClipDistances = 0; } + // GL_MAX_CULL_DISTANCES_EXT (0x82F9) and GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES_EXT + // (0x82FA) are the same tokens as their desktop spellings and arrive with the same + // extension, so they are probed under the same guard and the same drain sandwich. + drainErrors(); + glesFuncs.glGetIntegerv(GL_MAX_CULL_DISTANCES, &maxCullDistances); + if (drainErrors()) { + MGLOG_W("GL_EXT_clip_cull_distance is advertised but GL_MAX_CULL_DISTANCES was " + "rejected; reporting no cull distances"); + maxCullDistances = 0; + } + drainErrors(); + glesFuncs.glGetIntegerv(GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES, &maxCombinedClipAndCullDistances); + if (drainErrors()) { + MGLOG_W("GL_EXT_clip_cull_distance is advertised but " + "GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES was rejected; deriving it from the pair"); + maxCombinedClipAndCullDistances = 0; + } } glesFuncs.glGetIntegerv(GL_MAX_VIEWPORT_DIMS, maxViewportDims); // GL_LAYER_PROVOKING_VERTEX is ES 3.2 core (it arrives with geometry shaders, which is @@ -1562,6 +1584,14 @@ namespace MobileGL::MG_Util::BackendLoader { // A driver is free to write nonsense into an out-param it then rejects, and without the // extension the probe above never ran at all - so the flag, not the local, decides. caps.MaxClipDistances = caps.SupportsClipDistance ? std::max(maxClipDistances, 0) : 0; + caps.MaxCullDistances = caps.SupportsClipDistance ? std::max(maxCullDistances, 0) : 0; + // The combined limit can never be smaller than either half (GL 4.6 core 11.1.3.10 / the + // EXT spec say so), so a driver that rejected the combined query but answered the other + // two still gets a usable - and never over-stated - number. + caps.MaxCombinedClipAndCullDistances = + caps.SupportsClipDistance + ? std::max({maxCombinedClipAndCullDistances, caps.MaxClipDistances, caps.MaxCullDistances}) + : 0; caps.MaxViewports = maxViewports; caps.LayerProvokingVertex = layerProvokingVertex; caps.ViewportIndexProvokingVertex = viewportIndexProvokingVertex; @@ -1651,6 +1681,8 @@ namespace MobileGL::MG_Util::BackendLoader { // and "this driver has no clip distances". MGLOG_I(" GL_MAX_CLIP_DISTANCES: %d%s", caps.MaxClipDistances, caps.SupportsClipDistance ? "" : " (no GL_EXT_clip_cull_distance on this driver)"); + MGLOG_I(" GL_MAX_CULL_DISTANCES: %d", caps.MaxCullDistances); + MGLOG_I(" GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES: %d", caps.MaxCombinedClipAndCullDistances); MGLOG_I(" GL_MAX_VIEWPORTS: %d", caps.MaxViewports); MGLOG_I(" GL_MAX_VIEWPORT_DIMS: [%d, %d]", caps.MaxViewportWidth, caps.MaxViewportHeight); MGLOG_I(" GL_VIEWPORT_BOUNDS_RANGE: [%.3f, %.3f]", caps.ViewportBoundsRangeMin, diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index d5e9d9e3..03d28b0f 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1299,6 +1299,11 @@ namespace MobileGL { // GL_EXT_clip_cull_distance, so a driver without it has none. See the guarded probe // in FillInGLESCapabilities. Int MaxClipDistances = 0; + // Same contract, same reason, same extension: GL_MAX_CULL_DISTANCES_EXT and + // GL_MAX_COMBINED_CLIP_AND_CULL_DISTANCES_EXT exist in ES only under + // GL_EXT_clip_cull_distance, so zero is the honest answer without it. + Int MaxCullDistances = 0; + Int MaxCombinedClipAndCullDistances = 0; Int MaxViewports = 16; // GL_LAYER_PROVOKING_VERTEX (ES 3.2 core) and GL_VIEWPORT_INDEX_PROVOKING_VERTEX // (GL_OES_viewport_array). GL_UNDEFINED_VERTEX is a legal answer for both and is what diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp index 01b6e1e6..dfad5edf 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.cpp @@ -199,6 +199,8 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxDrawBuffers = SaturateToInt(p.limits.maxFragmentOutputAttachments); caps.MaxColorAttachments = SaturateToInt(p.limits.maxColorAttachments); caps.MaxClipDistances = SaturateToInt(p.limits.maxClipDistances); + caps.MaxCullDistances = SaturateToInt(p.limits.maxCullDistances); + caps.MaxCombinedClipAndCullDistances = SaturateToInt(p.limits.maxCombinedClipAndCullDistances); caps.MaxViewports = SaturateToInt(p.limits.maxViewports); caps.MaxViewportWidth = SaturateToInt(p.limits.maxViewportDimensions[0]); caps.MaxViewportHeight = SaturateToInt(p.limits.maxViewportDimensions[1]); @@ -239,6 +241,7 @@ namespace MobileGL::MG_Util::BackendLoader { caps.SupportsFragmentStoresAndAtomics = supportedFeatures.fragmentStoresAndAtomics == VK_TRUE; caps.SupportsGeometryShader = supportedFeatures.geometryShader == VK_TRUE; caps.SupportsShaderClipDistance = supportedFeatures.shaderClipDistance == VK_TRUE; + caps.SupportsShaderCullDistance = supportedFeatures.shaderCullDistance == VK_TRUE; caps.MaxShaderStorageBlockSize = static_cast(p.limits.maxStorageBufferRange); const Bool supportsShaderSubgroup = vk.vkGetPhysicalDeviceProperties2 && HasUsableShaderSubgroupSupport(subgroupProps); @@ -319,6 +322,8 @@ namespace MobileGL::MG_Util::BackendLoader { caps.MaxDrawBuffers = SaturateToInt(properties.limits.maxFragmentOutputAttachments); caps.MaxColorAttachments = SaturateToInt(properties.limits.maxColorAttachments); caps.MaxClipDistances = SaturateToInt(properties.limits.maxClipDistances); + caps.MaxCullDistances = SaturateToInt(properties.limits.maxCullDistances); + caps.MaxCombinedClipAndCullDistances = SaturateToInt(properties.limits.maxCombinedClipAndCullDistances); caps.MaxViewports = SaturateToInt(properties.limits.maxViewports); caps.MaxViewportWidth = SaturateToInt(properties.limits.maxViewportDimensions[0]); caps.MaxViewportHeight = SaturateToInt(properties.limits.maxViewportDimensions[1]); @@ -336,6 +341,7 @@ namespace MobileGL::MG_Util::BackendLoader { caps.SupportsFragmentStoresAndAtomics = false; caps.SupportsGeometryShader = false; caps.SupportsShaderClipDistance = false; + caps.SupportsShaderCullDistance = false; caps.MaxShaderStorageBlockSize = static_cast(properties.limits.maxStorageBufferRange); caps.SupportsShaderSubgroup = false; caps.SubgroupSize = 0; diff --git a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h index 135070ce..74364939 100644 --- a/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/Vulkan/Loader.h @@ -68,6 +68,11 @@ namespace MobileGL { Int MaxDrawBuffers = 8; Int MaxColorAttachments = 8; Int MaxClipDistances = 8; + // VkPhysicalDeviceLimits::maxCullDistances / maxCombinedClipAndCullDistances, gated + // by SupportsShaderCullDistance exactly as the clip pair is gated by + // SupportsShaderClipDistance. + Int MaxCullDistances = 8; + Int MaxCombinedClipAndCullDistances = 8; Int MaxViewports = 16; Int MaxViewportWidth = 16384; Int MaxViewportHeight = 16384; @@ -106,6 +111,11 @@ namespace MobileGL { // device has it, and without it a shader writing gl_ClipDistance is invalid. Very // widely supported, hence read from the device features and never assumed false. Bool SupportsShaderClipDistance = false; + // VkPhysicalDeviceFeatures::shaderCullDistance, the same story one field down: + // VulkanRenderer already ENABLES this feature where the device has it, but nobody + // ever read the limits it unlocks, so the frontend advertised eight cull distances + // from a literal instead of from the device. + Bool SupportsShaderCullDistance = false; SizeT MaxShaderStorageBlockSize = 128 * 1024 * 1024; Bool SupportsShaderSubgroup = false; Uint32 SubgroupSize = 0; diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp index 0d21a448..80485f01 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.cpp @@ -23,6 +23,13 @@ namespace MobileGL::MG_Util::ShaderTranspiler { } } // namespace + Int ResolveMaxVertexAttribs(const Bool hasBackend, const Int backendMaxVertexAttribs) { + constexpr Int capacity = static_cast(MG_State::GLState::VertexArrayObject::MAX_VERTEX_ATTRIBS); + if (!hasBackend) return capacity; + if (backendMaxVertexAttribs <= 0) return capacity; + return std::min(backendMaxVertexAttribs, capacity); + } + Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env) { Uint64 state = 0x9e3779b97f4a7c15ull; HashValue(state, env.maxComputeWorkGroupSize[0]); @@ -67,6 +74,24 @@ namespace MobileGL::MG_Util::ShaderTranspiler { // and expands gl_MaxClipDistances from the same number, so it decides both whether a // shader compiles at all and what a module that reads the constant generates. HashValue(state, env.params.MaxClipDistances); + // The cull-distance pair, added when the GL 4.6 API-surface wave made them env-derived: + // they were bare literals (8/8) in BuildTBuiltInResource while no backend had ever been + // asked whether it can host a cull distance. Exactly the MaxClipDistances class - glslang + // bounds gl_CullDistance[i] against maxCullDistances at parse and expands + // gl_MaxCullDistances / gl_MaxCombinedClipAndCullDistances from the same numbers. + HashValue(state, env.params.MaxCullDistances); + HashValue(state, env.params.MaxCombinedClipAndCullDistances); + // The texture-image-unit family, made env-derived in the same wave. They were stock + // glslang defaults (32/32/80) that disagreed with what glGetIntegerv answered, and + // gl_MaxTextureImageUnits / gl_MaxVertexTextureImageUnits / gl_MaxCombinedTextureImageUnits + // expand from them. + HashValue(state, env.params.MaxTextureImageUnits); + HashValue(state, env.params.MaxVertexTextureImageUnits); + HashValue(state, env.params.MaxCombinedTextureImageUnits); + // gl_MaxSamples, which also sizes gl_SampleMask[] / gl_SampleMaskIn[] and bounds a + // constant index into them, so a module that touches either generates different SPIR-V + // on two backends that report different sample counts. + HashValue(state, env.params.MaxSamples); // The compute work-group limits, likewise added by wave3 (cb155c5b). They used to be // hardcoded maxima in BuildTBuiltInResource, and the L1 key comment said in so many // words that the day they became backend-derived they would have to move in here - diff --git a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h index 769c9859..98804e7a 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h +++ b/MobileGL/MG_Util/ShaderTranspiler/CompileEnv.h @@ -84,11 +84,14 @@ namespace MobileGL::MG_Util::ShaderTranspiler { // * the DynamicBackendParameters fields BuildTBuiltInResource copies into // TBuiltInResource - MaxImageUnits, MaxDrawBuffers, MaxVertexImageUniforms, // MaxGeometryImageUniforms, MaxFragmentImageUniforms, MaxComputeImageUniforms, - // MaxCombinedImageUniforms, MaxComputeTextureImageUnits, MaxClipDistances. glslang + // MaxCombinedImageUniforms, MaxComputeTextureImageUnits, MaxClipDistances, + // MaxCullDistances, MaxCombinedClipAndCullDistances, MaxTextureImageUnits, + // MaxVertexTextureImageUnits, MaxCombinedTextureImageUnits, MaxSamples. glslang // enforces those at parse, so they decide whether a shader compiles at all and can - // change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d), - // the third time in three waves that a hardcoded TBuiltInResource field became - // env-derived - assume the next wave does it again and re-audit. + // change the link result. MaxClipDistances moved in at the wave4 merge (4fc3531d) + // and the six after it at the GL 4.6 API-surface wave - the fourth time in four + // waves that a hardcoded TBuiltInResource field became env-derived. Assume the next + // wave does it again and re-audit. // * maxComputeWorkGroupSize and maxComputeWorkGroupCount, all three components each. // These moved IN at the dev merge that brought wave3's cb155c5b, which made // BuildTBuiltInResource read them from the env instead of hardcoding a permissive @@ -160,6 +163,18 @@ namespace MobileGL::MG_Util::ShaderTranspiler { } }; + // How many vertex input locations exist, from the frontend's point of view: the backend's + // advertised count bounded by the state layer's current-value storage capacity + // (VertexArrayObject::MAX_VERTEX_ATTRIBS). ONE definition, because three places have to + // agree on it and used to carry three copies of the formula - glGetIntegerv + // (VertexArrayImpl::GetMaxVertexAttribs), the limit reflection records vertex inputs + // against (ProgramLinkTask), and gl_MaxVertexAttribs (BuildTBuiltInResource, which had a + // hardcoded 64 the other two never saw). A disagreement there is not cosmetic: glslang + // ACCEPTS a vertex input at a location the runtime cannot bind, and the draw then silently + // reads nothing. `hasBackend` false means "no backend to be bounded by" and yields the + // storage capacity, matching what all three did before. + Int ResolveMaxVertexAttribs(Bool hasBackend, Int backendMaxVertexAttribs); + // Hashes every semantically relevant member. Public so a test can assert that two // different envs really do produce different P0b cache keys. Uint64 ComputeCompileEnvFingerprint(const CompileEnv& env); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index e6a6825b..e9942617 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -101,16 +101,11 @@ namespace MobileGL { Resources.maxClipPlanes = 6; Resources.maxTextureUnits = 32; Resources.maxTextureCoords = 32; - Resources.maxVertexAttribs = 64; - Resources.maxVertexUniformComponents = 4096; - Resources.maxVaryingFloats = 64; - Resources.maxVertexTextureImageUnits = 32; - Resources.maxCombinedTextureImageUnits = 80; - Resources.maxTextureImageUnits = 32; + Resources.maxVertexUniformComponents = MAX_VERTEX_UNIFORM_COMPONENTS; + Resources.maxVaryingFloats = MAX_VARYING_COMPONENTS; Resources.maxFragmentUniformComponents = 4096; - Resources.maxDrawBuffers = 32; - Resources.maxVertexUniformVectors = 128; - Resources.maxVaryingVectors = 8; + Resources.maxVertexUniformVectors = MAX_VERTEX_UNIFORM_VECTORS; + Resources.maxVaryingVectors = MAX_VARYING_VECTORS; Resources.maxFragmentUniformVectors = 256; Resources.maxVertexOutputVectors = 16; Resources.maxFragmentInputVectors = 15; @@ -121,14 +116,12 @@ namespace MobileGL { Resources.maxComputeImageUniforms = 8; Resources.maxComputeAtomicCounters = MAX_ATOMIC_COUNTERS_PER_STAGE; Resources.maxComputeAtomicCounterBuffers = MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE; - Resources.maxVaryingComponents = 60; + Resources.maxVaryingComponents = MAX_VARYING_COMPONENTS; Resources.maxVertexOutputComponents = 64; Resources.maxGeometryInputComponents = 64; Resources.maxGeometryOutputComponents = 128; Resources.maxFragmentInputComponents = 128; Resources.maxImageUnits = 8; - Resources.maxCombinedImageUnitsAndFragmentOutputs = 8; - Resources.maxCombinedShaderOutputResources = 8; Resources.maxImageSamples = 0; Resources.maxVertexImageUniforms = 0; Resources.maxTessControlImageUniforms = 0; @@ -141,16 +134,18 @@ namespace MobileGL { Resources.maxGeometryTotalOutputComponents = 1024; Resources.maxGeometryUniformComponents = 1024; Resources.maxGeometryVaryingComponents = 64; - Resources.maxTessControlInputComponents = 128; - Resources.maxTessControlOutputComponents = 128; - Resources.maxTessControlTextureImageUnits = 16; - Resources.maxTessControlUniformComponents = 1024; - Resources.maxTessControlTotalOutputComponents = 4096; - Resources.maxTessEvaluationInputComponents = 128; - Resources.maxTessEvaluationOutputComponents = 128; - Resources.maxTessEvaluationTextureImageUnits = 16; - Resources.maxTessEvaluationUniformComponents = 1024; - Resources.maxTessPatchComponents = 120; + // The tessellation block is shared with glGetIntegerv through Types.h; see the + // "Never move one of these without the other" note there. + Resources.maxTessControlInputComponents = MAX_TESS_CONTROL_INPUT_COMPONENTS; + Resources.maxTessControlOutputComponents = MAX_TESS_CONTROL_OUTPUT_COMPONENTS; + Resources.maxTessControlTextureImageUnits = MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS; + Resources.maxTessControlUniformComponents = MAX_TESS_CONTROL_UNIFORM_COMPONENTS; + Resources.maxTessControlTotalOutputComponents = MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS; + Resources.maxTessEvaluationInputComponents = MAX_TESS_EVALUATION_INPUT_COMPONENTS; + Resources.maxTessEvaluationOutputComponents = MAX_TESS_EVALUATION_OUTPUT_COMPONENTS; + Resources.maxTessEvaluationTextureImageUnits = MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS; + Resources.maxTessEvaluationUniformComponents = MAX_TESS_EVALUATION_UNIFORM_COMPONENTS; + Resources.maxTessPatchComponents = MAX_TESS_PATCH_COMPONENTS; Resources.maxPatchVertices = 32; Resources.maxTessGenLevel = 64; Resources.maxViewports = 16; @@ -176,9 +171,6 @@ namespace MobileGL { Resources.maxAtomicCounterBufferSize = MAX_ATOMIC_COUNTER_BUFFER_SIZE; Resources.maxTransformFeedbackBuffers = 4; Resources.maxTransformFeedbackInterleavedComponents = 64; - Resources.maxCullDistances = 8; - Resources.maxCombinedClipAndCullDistances = 8; - Resources.maxSamples = 4; Resources.maxMeshOutputVerticesNV = 256; Resources.maxMeshOutputPrimitivesNV = 512; Resources.maxMeshWorkGroupSizeX_NV = 32; @@ -208,12 +200,37 @@ namespace MobileGL { Resources.maxImageUnits = dynamicParameters.MaxImageUnits; Resources.maxCombinedImageUnitsAndFragmentOutputs = dynamicParameters.MaxImageUnits + dynamicParameters.MaxDrawBuffers; + // GL_MAX_COMBINED_SHADER_OUTPUT_RESOURCES and + // GL_MAX_COMBINED_IMAGE_UNITS_AND_FRAGMENT_OUTPUTS are the SAME token (0x8F39), so + // the two glslang fields have to carry the same value: glGetIntegerv answers this + // expression while gl_MaxCombinedShaderOutputResources expanded from a stale + // literal 8, and the CTS compares the two directly. + Resources.maxCombinedShaderOutputResources = + Resources.maxCombinedImageUnitsAndFragmentOutputs; Resources.maxVertexImageUniforms = dynamicParameters.MaxVertexImageUniforms; Resources.maxGeometryImageUniforms = dynamicParameters.MaxGeometryImageUniforms; Resources.maxFragmentImageUniforms = dynamicParameters.MaxFragmentImageUniforms; Resources.maxComputeImageUniforms = dynamicParameters.MaxComputeImageUniforms; Resources.maxCombinedImageUniforms = dynamicParameters.MaxCombinedImageUniforms; Resources.maxComputeTextureImageUnits = dynamicParameters.MaxComputeTextureImageUnits; + // The texture-image-unit family and the draw-buffer count. These were stock + // glslang defaults (32 / 32 / 80 / 32) that had nothing to do with what + // glGetIntegerv answers off the same backend, and the divergence is a live + // correctness bug rather than a reporting one: gl_MaxDrawBuffers = 32 makes + // glslang ACCEPT a fragment output at location 8..31 that the runtime cannot + // bind, and gl_MaxCombinedTextureImageUnits = 80 under-reports a device that + // really has 96. + Resources.maxTextureImageUnits = dynamicParameters.MaxTextureImageUnits; + Resources.maxVertexTextureImageUnits = dynamicParameters.MaxVertexTextureImageUnits; + Resources.maxCombinedTextureImageUnits = dynamicParameters.MaxCombinedTextureImageUnits; + Resources.maxDrawBuffers = dynamicParameters.MaxDrawBuffers; + // The same number glGetIntegerv(GL_MAX_VERTEX_ATTRIBS) reports and the same one + // reflection records vertex inputs against - see ResolveMaxVertexAttribs. + Resources.maxVertexAttribs = ResolveMaxVertexAttribs( + env ? env->HasBackend() : (activeBackend != nullptr), dynamicParameters.MaxVertexAttribs); + // gl_MaxSamples, floored exactly as GL_Getter::GetAdvertisedMaxSamples floors + // GL_MAX_SAMPLES. It also sizes gl_SampleMask[] / gl_SampleMaskIn[]. + Resources.maxSamples = std::max(dynamicParameters.MaxSamples, MIN_ADVERTISED_MAX_SAMPLES); // Load-bearing, not cosmetic. glslang rejects gl_ClipDistance[i] for // i >= maxClipDistances (ParseHelper.cpp) and expands gl_MaxClipDistances from the // same number, so tracking the backend limit is what turns "the program links, @@ -222,6 +239,14 @@ namespace MobileGL { // also what makes glGetIntegerv(GL_MAX_CLIP_DISTANCES) and gl_MaxClipDistances // agree, which KHR-GLxx.clip_distance.coverage compares directly. Resources.maxClipDistances = dynamicParameters.MaxClipDistances; + // The cull pair, for the same reason and with a sharper edge: cull distance + // discards the WHOLE primitive, so a shader that gets to declare gl_CullDistance + // on a backend that cannot host one does not render subtly wrong pixels, it + // renders nothing at all. These were literal 8s that no backend was ever asked + // about; a backend without cull distances now reports 0 and glslang rejects the + // declaration with a diagnostic the application can read. + Resources.maxCullDistances = dynamicParameters.MaxCullDistances; + Resources.maxCombinedClipAndCullDistances = dynamicParameters.MaxCombinedClipAndCullDistances; // The compute work-group limits are the env's, not the backend parameters': they // are the only ones that come from a REAL indexed driver query, which diff --git a/MobileGL/MG_Util/ShaderTranspiler/Types.h b/MobileGL/MG_Util/ShaderTranspiler/Types.h index 517988a0..bb146b3a 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/Types.h +++ b/MobileGL/MG_Util/ShaderTranspiler/Types.h @@ -80,6 +80,53 @@ namespace MobileGL { inline constexpr Int MAX_ATOMIC_COUNTER_BUFFERS_PER_STAGE = 8; inline constexpr Int MAX_ATOMIC_COUNTERS_PER_STAGE = 8; + // ---- Tessellation per-stage resource limits ---- + // + // Here for exactly the reason the atomic-counter block above is here. GL 4.6 requires + // glGetIntegerv and the matching gl_MaxTess* built-in constant to report the same + // number (KHR-GL45.limits.max_tess_* reads the query and then compiles a shader that + // writes the built-in into an SSBO and demands equality), and these numbers used to + // exist ONLY inside BuildTBuiltInResource - so gl_MaxTessControlInputComponents + // compiled fine while glGetIntegerv of the same limit had no case at all and answered + // GL_INVALID_ENUM. Never move one of these without the other. + // + // The values are the GL 4.6 core minimums (table 23.55), which is what a frontend that + // synthesizes the tessellation stages onto ES/Vulkan can honestly promise. + inline constexpr Int MAX_TESS_CONTROL_INPUT_COMPONENTS = 128; + inline constexpr Int MAX_TESS_CONTROL_OUTPUT_COMPONENTS = 128; + inline constexpr Int MAX_TESS_CONTROL_TEXTURE_IMAGE_UNITS = 16; + inline constexpr Int MAX_TESS_CONTROL_UNIFORM_COMPONENTS = 1024; + inline constexpr Int MAX_TESS_CONTROL_TOTAL_OUTPUT_COMPONENTS = 4096; + inline constexpr Int MAX_TESS_EVALUATION_INPUT_COMPONENTS = 128; + inline constexpr Int MAX_TESS_EVALUATION_OUTPUT_COMPONENTS = 128; + inline constexpr Int MAX_TESS_EVALUATION_TEXTURE_IMAGE_UNITS = 16; + inline constexpr Int MAX_TESS_EVALUATION_UNIFORM_COMPONENTS = 1024; + inline constexpr Int MAX_TESS_PATCH_COMPONENTS = 120; + + // ---- Varying and default-block uniform capacities ---- + // + // The *_VECTORS limits are the *_COMPONENTS ones counted in vec4s, so they are DERIVED + // rather than typed independently: GL_MAX_VARYING_COMPONENTS said 64 while + // GL_MAX_VARYING_VECTORS said 8, and GL_MAX_VERTEX_UNIFORM_COMPONENTS said 4096 while + // GL_MAX_VERTEX_UNIFORM_VECTORS said 128 - two pairs that cannot both describe the + // same capacity, and both *_VECTORS answers were below the GL 4.5 core minimum + // (15 and 256 respectively). Shared with BuildTBuiltInResource because + // gl_MaxVaryingVectors and gl_MaxVertexUniformVectors expand from the same numbers. + inline constexpr Int MAX_VARYING_COMPONENTS = 64; + inline constexpr Int MAX_VARYING_VECTORS = MAX_VARYING_COMPONENTS / 4; + inline constexpr Int MAX_VERTEX_UNIFORM_COMPONENTS = 4096; + inline constexpr Int MAX_VERTEX_UNIFORM_VECTORS = MAX_VERTEX_UNIFORM_COMPONENTS / 4; + + // GL 4.6 core table 23.53 sets the GL_MAX_SAMPLES minimum at 4, and MobileGL floors + // the backend's answer at it (GL_Getter's GetAdvertisedMaxSamples). gl_MaxSamples has + // to expand to the SAME number - it is also what sizes gl_SampleMask[] / + // gl_SampleMaskIn[] and what bounds a constant index into them - so the floor lives + // here and both sides apply it. NOTE the deliberate asymmetry: only MAX_SAMPLES has a + // floor of 4. MAX_INTEGER_SAMPLES, MAX_COLOR_TEXTURE_SAMPLES and + // MAX_DEPTH_TEXTURE_SAMPLES have a minimum of ONE in the same table and are reported + // as the backend probed them. + inline constexpr Int MIN_ADVERTISED_MAX_SAMPLES = 4; + struct EmptyType {}; enum class ShaderCompileBits : Uint {