[Fix] (GLImpl, DirectGLES, DirectVulkan): floor every advertised sample cap and clamp the realised count in the backends

This commit is contained in:
2026-08-20 04:10:45 -04:00
parent d9abf1c2c1
commit f297af7d2b
8 changed files with 137 additions and 22 deletions
@@ -8,6 +8,7 @@
#include "BackendObject_DirectGLES.h"
#include "MG_Backend/BackendObject.h"
#include "MG_Backend/BackendObjects.h"
#include <MG_Backend/DirectGLES/DirectGLES.h>
#include <MG_Backend/DirectGLES/Managers.h>
#include <MG_Backend/DirectGLES/Utils.h>
@@ -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<SizeT>(logicalFormat);
if (pActiveBackendObject && targetIndex < kFormatCapabilityTargetCount &&
formatIndex < kFormatCapabilityFormatCount) {
// Descending, so the head is the largest count this device actually allocated.
const Vector<Int>& 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();
}
@@ -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;
+19 -5
View File
@@ -9,6 +9,7 @@
#include "Managers.h"
#include "Utils.h"
#include "DirectGLES.h"
#include "BackendObject_DirectGLES.h"
#include <Config.h>
#include <MG_Util/ShaderTranspiler/ShaderCompiler.h>
@@ -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<GLsizei>(ClampSamplesToBackendSupport(
GetFormatCapabilityTargetIndex(targetInternal), textureMipmapObject->GetFormat(),
glFormat, static_cast<Int>(stateTextureObject->GetSamples())));
switch (targetInternal) {
case TextureTarget::Texture2DMultisample:
g_GLESFuncs.glTexStorage2DMultisample(
target, static_cast<GLsizei>(stateTextureObject->GetSamples()), glInternalFormat,
target, backendSamples, glInternalFormat,
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
stateTextureObject->HasFixedSampleLocations() ? GL_TRUE : GL_FALSE);
break;
case TextureTarget::Texture2DMultisampleArray:
g_GLESFuncs.glTexStorage3DMultisample(
target, static_cast<GLsizei>(stateTextureObject->GetSamples()), glInternalFormat,
target, backendSamples, glInternalFormat,
static_cast<GLsizei>(baseSize.x()), static_cast<GLsizei>(baseSize.y()),
static_cast<GLsizei>(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<GLsizei>(samples), glInternalFormat, static_cast<GLsizei>(width),
static_cast<GLsizei>(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<GLsizei>(ClampSamplesToBackendSupport(
GetRenderbufferFormatCapabilityTargetIndex(), internalFormat, glFormat, samples));
g_GLESFuncs.glRenderbufferStorageMultisample(GL_RENDERBUFFER, backendSamples, glInternalFormat,
static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
} else {
g_GLESFuncs.glRenderbufferStorage(GL_RENDERBUFFER, glInternalFormat, static_cast<GLsizei>(width),
static_cast<GLsizei>(height));
@@ -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<Int>(format));
rounded = static_cast<Uint32>(VK_SAMPLE_COUNT_1_BIT);
}
if (rounded != 0) {
resolvedSampleCount = static_cast<VkSampleCountFlagBits>(rounded);
}
@@ -13,6 +13,7 @@
#include <MG_Backend/BackendObjects.h>
#include <MG_Util/Metrics/TextureMetrics.h>
#include <MG_Impl/GLImpl/Texture/Validators.h>
#include <MG_Impl/GLImpl/Getter/GL_Getter.h>
#include <MG_State/GLState/ErrorState/Error.h>
#include <MG_Util/Converters/GLToStr/GLEnumConverter.h>
#include <MG_Util/Converters/GLToMG/TextureEnumConverter.h>
@@ -617,16 +618,17 @@ namespace MobileGL::MG_Impl::GLImpl {
if (MG_Backend::pActiveBackendObject == nullptr) {
return std::numeric_limits<Int>::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<Int>::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) {
+16 -4
View File
@@ -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.
@@ -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
+37 -4
View File
@@ -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<SizeT>(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<Int>::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.