From f297af7d2b12545c03d31a4aa1d6644b0479f04b Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 20 Aug 2026 04:10:45 -0400 Subject: [PATCH] [Fix] (GLImpl, DirectGLES, DirectVulkan): floor every advertised sample cap and clamp the realised count in the backends --- .../DirectGLES/BackendObject_DirectGLES.cpp | 24 +++++++++++ .../DirectGLES/BackendObject_DirectGLES.h | 10 +++++ MobileGL/MG_Backend/DirectGLES/Managers.cpp | 24 ++++++++--- .../Renderer/VkTextureManager.cpp | 13 ++++++ .../GLImpl/Framebuffer/GL_Framebuffer.cpp | 23 +++++++---- MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp | 20 +++++++-- MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h | 4 ++ .../MG_Impl/GLImpl/Texture/GL_Texture.cpp | 41 +++++++++++++++++-- 8 files changed, 137 insertions(+), 22 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 1df1de0f..b35b82cc 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -8,6 +8,7 @@ #include "BackendObject_DirectGLES.h" #include "MG_Backend/BackendObject.h" +#include "MG_Backend/BackendObjects.h" #include #include #include @@ -782,6 +783,29 @@ namespace MobileGL::MG_Backend::DirectGLES { PopulateFormatCapabilitiesImpl(gl, capabilities, cache); } + Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat, + Int samples) { + if (samples <= 1) { + return samples; + } + + Int maxSamples = 0; + const SizeT formatIndex = static_cast(logicalFormat); + if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount && + formatIndex < kFormatCapabilityFormatCount) { + // Descending, so the head is the largest count this device actually allocated. + const Vector& probedCounts = + pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex]; + if (!probedCounts.empty()) { + maxSamples = probedCounts.front(); + } + } + if (maxSamples <= 0) { + maxSamples = GetGLESFormatMaxSamples(g_GLESCapabilities, logicalFormat, imageFormat); + } + return std::min(samples, std::max(maxSamples, 1)); + } + BackendObject_DirectGLES::~BackendObject_DirectGLES() { DestroyEGLContext(); } diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h index 8aa4fdba..bb9a48be 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.h @@ -18,6 +18,16 @@ namespace MobileGL::MG_Backend::DirectGLES { const MG_External::GLESCapabilities& capabilities, FormatCapabilityCache& cache); + // Clamps a requested sample count down to what the ES driver can really deliver for this + // format on this format-capability target: the probed per-format list when there is one, the + // driver's per-class GL_MAX_*_SAMPLES otherwise. The frontend deliberately validates against + // the count MobileGL advertises instead (GL_Getter's GetAdvertisedMaxSamples), which on a + // driver reporting GL_MAX_INTEGER_SAMPLES 1 is higher than the driver accepts, so every ES + // allocation call has to come through here. The shadow state keeps the requested count, so + // GL_TEXTURE_SAMPLES and framebuffer completeness still answer what the application asked for. + Int ClampSamplesToBackendSupport(SizeT targetIndex, TextureInternalFormat logicalFormat, GLenum imageFormat, + Int samples); + class BackendObject_DirectGLES : public BackendObject { public: ~BackendObject_DirectGLES() override; diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 45d32f75..a279a4ec 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -9,6 +9,7 @@ #include "Managers.h" #include "Utils.h" #include "DirectGLES.h" +#include "BackendObject_DirectGLES.h" #include #include @@ -2648,16 +2649,24 @@ namespace MobileGL::MG_Backend::DirectGLES { if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) { DebugImpl::ErrorLopper::Clear(); BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned + // The frontend validates against the count MobileGL advertises, which can + // exceed what the driver takes for this format (Adreno: GL_MAX_SAMPLES 4, + // GL_MAX_INTEGER_SAMPLES 1). Clamp the ES call - and only the ES call: + // stateTextureObject keeps the requested count so GL_TEXTURE_SAMPLES and + // framebuffer completeness still report what the application asked for. + const auto backendSamples = static_cast(ClampSamplesToBackendSupport( + GetFormatCapabilityTargetIndex(targetInternal), textureMipmapObject->GetFormat(), + glFormat, static_cast(stateTextureObject->GetSamples()))); switch (targetInternal) { case TextureTarget::Texture2DMultisample: g_GLESFuncs.glTexStorage2DMultisample( - target, static_cast(stateTextureObject->GetSamples()), glInternalFormat, + target, backendSamples, glInternalFormat, static_cast(baseSize.x()), static_cast(baseSize.y()), stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE); break; case TextureTarget::Texture2DMultisampleArray: g_GLESFuncs.glTexStorage3DMultisample( - target, static_cast(stateTextureObject->GetSamples()), glInternalFormat, + target, backendSamples, glInternalFormat, static_cast(baseSize.x()), static_cast(baseSize.y()), static_cast(baseSize.z()), stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE); @@ -5621,9 +5630,14 @@ namespace MobileGL::MG_Backend::DirectGLES { TextureImpl::GenerateRenderbufferFormatInfo(internalFormat, &glInternalFormat, &glFormat, &glType); if (samples > 0) { - g_GLESFuncs.glRenderbufferStorageMultisample( - GL_RENDERBUFFER, static_cast(samples), glInternalFormat, static_cast(width), - static_cast(height)); + // Same clamp as the multisample texture path: the frontend accepts the count it + // advertised, the driver only takes the count it supports for this format, and + // the state object keeps reporting the requested one. + const auto backendSamples = static_cast(ClampSamplesToBackendSupport( + GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples)); + g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat, + static_cast(width), + static_cast(height)); } else { g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast(width), static_cast(height)); diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp index 233f472d..4c5f7ac7 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/VkTextureManager.cpp @@ -1848,6 +1848,19 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } } + if (rounded == 0 && (supported & VK_SAMPLE_COUNT_1_BIT) != 0) { + // Nothing at two samples or above. Reachable because the frontend validates + // multisample allocations against the count MobileGL ADVERTISES (GL requires + // GL_MAX_SAMPLES >= 4) rather than against the device's per-format support, so + // a format this device cannot multisample at all now gets here instead of + // being refused up front. Keeping the unsupported count would hand + // vkCreateImage an invalid VkImageCreateInfo; one sample is at least a legal + // image, and the samples-08726 hazard above is the lesser of the two. + MGLOG_W_ONCE("Multisample texture format %d supports no count above one on this device; " + "backing it with a single sample", + static_cast(format)); + rounded = static_cast(VK_SAMPLE_COUNT_1_BIT); + } if (rounded != 0) { resolvedSampleCount = static_cast(rounded); } diff --git a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp index 2b2701fc..39638faa 100644 --- a/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp +++ b/MobileGL/MG_Impl/GLImpl/Framebuffer/GL_Framebuffer.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -617,16 +618,17 @@ namespace MobileGL::MG_Impl::GLImpl { if (MG_Backend::pActiveBackendObject == nullptr) { return std::numeric_limits::max(); } - return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, 1); + return GetAdvertisedMaxSamples(); } - // GL_MAX_SAMPLES is the ceiling over all formats; an integer format has its own, lower - // one (GL_MAX_INTEGER_SAMPLES) and GL 4.6 core 9.2.4 makes exceeding it INVALID_OPERATION. - // The multisample TEXTURE path already resolves the limit per format - // (GL_Texture.cpp, GetMaxTextureSamplesForFormat); renderbuffers only ever compared - // against GL_MAX_SAMPLES, so on a driver where the two differ - Adreno reports - // GL_MAX_SAMPLES 4 and GL_MAX_INTEGER_SAMPLES 1 - an integer renderbuffer accepted a - // sample count the format cannot deliver, and said GL_NO_ERROR about it. + // 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. Int GetMaxRenderbufferSamplesForFormat_State(TextureInternalFormat format) { if (MG_Backend::pActiveBackendObject == nullptr) { return std::numeric_limits::max(); @@ -645,7 +647,10 @@ namespace MobileGL::MG_Impl::GLImpl { if (!isIntegerFormat) { return GetMaxRenderbufferSamples_State(); } - return std::max(dynamicParameters.MaxIntegerSamples, 1); + // 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()); } 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 893f4909..091eb225 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.cpp @@ -422,6 +422,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. + GLint GetAdvertisedMaxSamples() { + if (MG_Backend::pActiveBackendObject == nullptr) { + return kFrontendMaxSamples; + } + return std::max(MG_Backend::pActiveBackendObject->GetDynamicParameters().MaxSamples, kFrontendMaxSamples); + } + /* @INSERTION_POINT:FUNCTION_IMPLEMENTATION@ */ const GLubyte* GetString(GLenum name) { static String vendorString; @@ -2117,7 +2129,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = dynamicParameters.MaxClipDistances; break; case GL_MAX_COLOR_TEXTURE_SAMPLES: - *params = dynamicParameters.MaxColorTextureSamples; + *params = std::max(dynamicParameters.MaxColorTextureSamples, GetAdvertisedMaxSamples()); break; case GL_MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS: *params = GetMaxCombinedUniformComponents(kFrontendMaxFragmentUniformComponents, @@ -2147,7 +2159,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = dynamicParameters.MaxCubeMapTextureSize; break; case GL_MAX_DEPTH_TEXTURE_SAMPLES: - *params = dynamicParameters.MaxDepthTextureSamples; + *params = std::max(dynamicParameters.MaxDepthTextureSamples, GetAdvertisedMaxSamples()); break; case GL_MAX_FRAMEBUFFER_WIDTH: *params = dynamicParameters.MaxFramebufferWidth; @@ -2174,7 +2186,7 @@ namespace MobileGL::MG_Impl::GLImpl { *params = dynamicParameters.MaxComputeImageUniforms; break; case GL_MAX_INTEGER_SAMPLES: - *params = dynamicParameters.MaxIntegerSamples; + *params = std::max(dynamicParameters.MaxIntegerSamples, GetAdvertisedMaxSamples()); break; case GL_MAX_RENDERBUFFER_SIZE: *params = dynamicParameters.MaxRenderbufferSize; @@ -2340,7 +2352,7 @@ namespace MobileGL::MG_Impl::GLImpl { : dynamicParameters.MaxDrawBuffers; break; case GL_MAX_SAMPLES: - *params = std::max(dynamicParameters.MaxSamples, kFrontendMaxSamples); + *params = GetAdvertisedMaxSamples(); break; case GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT: // Float state (see GetFloatv); rounded to nearest for the integer query per GL 3.3 6.1.2. diff --git a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h index 29df6e87..ee8faf65 100644 --- a/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h +++ b/MobileGL/MG_Impl/GLImpl/Getter/GL_Getter.h @@ -24,4 +24,8 @@ namespace MobileGL::MG_Impl::GLImpl { void GetInteger64i_v(GLenum target, GLuint index, GLint64* data); 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. + GLint GetAdvertisedMaxSamples(); } // namespace MobileGL::MG_Impl::GLImpl diff --git a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp index f1327ef3..5e960ad9 100644 --- a/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp +++ b/MobileGL/MG_Impl/GLImpl/Texture/GL_Texture.cpp @@ -474,15 +474,48 @@ namespace MobileGL::MG_Impl::GLImpl { target == TextureTarget::Texture2DMultisampleArray; } - Int GetMaxSupportedTextureSamples(TextureInternalFormat textureInternalFormat) { + // The largest count the backend actually probed for this format on this target, or 0 when + // it has no answer for the pair. Both backends build the list in descending order. + Int GetProbedMaxTextureSamples(TextureTarget textureTarget, TextureInternalFormat textureInternalFormat) { + if (MG_Backend::pActiveBackendObject == nullptr) { + return 0; + } + const SizeT targetIndex = MG_Backend::GetFormatCapabilityTargetIndex(textureTarget); + const SizeT formatIndex = static_cast(textureInternalFormat); + if (targetIndex >= MG_Backend::kFormatCapabilityTargetCount || + formatIndex >= MG_Backend::kFormatCapabilityFormatCount) { + return 0; + } + const auto& sampleCounts = + MG_Backend::pActiveBackendObject->GetFormatCapabilities().SampleCounts[targetIndex][formatIndex]; + 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. + Int GetMaxSupportedTextureSamples(TextureTarget textureTarget, + TextureInternalFormat textureInternalFormat) { if (MG_Backend::pActiveBackendObject == nullptr) { return std::numeric_limits::max(); } + const Int advertisedMaxSamples = GetAdvertisedMaxSamples(); + // 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, 1); + return std::max(dynamicParameters.MaxDepthTextureSamples, advertisedMaxSamples); } GLenum normalizedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(textureInternalFormat); @@ -495,7 +528,7 @@ namespace MobileGL::MG_Impl::GLImpl { normalizedFormat == GL_RGB_INTEGER || normalizedFormat == GL_RGBA_INTEGER; return std::max(isIntegerFormat ? dynamicParameters.MaxIntegerSamples : dynamicParameters.MaxColorTextureSamples, - 1); + advertisedMaxSamples); } Bool ValidateTextureMultisampleStorage(TextureTarget textureTarget, GLsizei samples, GLsizei width, @@ -532,7 +565,7 @@ namespace MobileGL::MG_Impl::GLImpl { // dimensions, and GL CTS's per-case state reset (gluStateReset) clears the default // GL_TEXTURE_2D_MULTISAMPLE_ARRAY texture with glTexImage3DMultisample(..., 0, 0, 0). - const Int maxSamples = GetMaxSupportedTextureSamples(textureInternalFormat); + const Int maxSamples = GetMaxSupportedTextureSamples(textureTarget, textureInternalFormat); if (samples > maxSamples) { // GL specifies INVALID_OPERATION - not INVALID_VALUE - when the sample count // exceeds what the format supports, and the native Adreno driver agrees.