From 65dbfa6f268dbb04a4d4ae316bc7f2a9c32c432a Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Sun, 2 Aug 2026 04:10:01 -0400 Subject: [PATCH] [Fix] (DirectGLES, MG_Util): widen three-channel formats for multisample textures GLES has no colour-renderable three-channel format beyond RGB8, so glTexStorage2DMultisample rejects GL_RGB16 (and the SNORM variants) with GL_INVALID_ENUM and the texture is left with no storage at all - every draw into it then hit GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT and every read came back zero. The existing fallback machinery could not help: it picks one replacement format per requested format, from the driver's capabilities, and never re-checks that replacement against the target it is going to be used with. GL_RGB16's fallback is GL_RGB32F, which is a perfectly legal ES texture format and a perfectly illegal multisample storage format, and with EXT_texture_norm16 present no fallback was selected at all. Add PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget, applied only to multisample targets, mapping GL_RGB16 to GL_RGBA32F and the three-channel SNORM formats to GL_RGBA16F. Widening the channel count is safe precisely there and nowhere else: a multisample texture can never be uploaded to, only rendered into, so no transfer path has to expand three-channel client data, and the alpha a draw writes for a three-channel source is already the 1.0 the frontend format implies. The capability probe recomputes its fallback per target for the same reason, so the probed format and the format the texture is actually created with stay in agreement. --- .../DirectGLES/BackendObject_DirectGLES.cpp | 47 ++++++++++++++----- MobileGL/MG_Backend/DirectGLES/Utils.cpp | 29 +++++++++--- .../Texture/TextureFormatProcessor.cpp | 20 ++++++++ .../MG_Util/Texture/TextureFormatProcessor.h | 6 +++ 4 files changed, 84 insertions(+), 18 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 366b7a67..11344c46 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -210,6 +210,9 @@ namespace MobileGL::MG_Backend::DirectGLES { if (options & PixelFormatNormalizeOptionBit::NoDepthComponent32) { reasons.push_back("GL_DEPTH_COMPONENT32 native probe failed on OpenGL ES"); } + if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { + reasons.push_back("no three-channel multisample storage format on OpenGL ES"); + } String reason; for (SizeT i = 0; i < reasons.size(); ++i) { @@ -556,15 +559,35 @@ namespace MobileGL::MG_Backend::DirectGLES { } const GLESProbeFormatInfo nativeInfo = BuildNativeProbeFormatInfo(requestedInternalFormat); - GLESProbeFormatInfo fallbackInfo; - const Bool hasForcedFallback = - BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, fallbackInfo); - if (!hasForcedFallback) { - BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, fallbackInfo); + GLESProbeFormatInfo outerFallbackInfo; + const Bool outerHasForcedFallback = + BuildFallbackProbeFormatInfo(requestedInternalFormat, forcedOptions, true, outerFallbackInfo); + if (!outerHasForcedFallback) { + BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions, false, outerFallbackInfo); } for (SizeT targetIndex = 0; targetIndex < kFormatCapabilityTextureTargetCount; ++targetIndex) { const auto target = static_cast(targetIndex); + // A multisample texture can only ever be rendered into, so its storage format + // has to stay colour-renderable; the ordinary fallback for a three-channel + // format is a three-channel one, which ES accepts as a texture but rejects as + // multisample storage. Recompute the fallback per target so those formats get + // widened here and nowhere else. + Flags targetOptions; + if (IsGLESProbeMultisampleTarget(target)) { + targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; + } + GLESProbeFormatInfo fallbackInfo = outerFallbackInfo; + Bool hasForcedFallback = outerHasForcedFallback; + if (targetOptions) { + hasForcedFallback = BuildFallbackProbeFormatInfo( + requestedInternalFormat, forcedOptions | targetOptions, true, fallbackInfo); + if (!hasForcedFallback) { + BuildFallbackProbeFormatInfo(requestedInternalFormat, driverOptions | targetOptions, + false, fallbackInfo); + } + } + Bool shouldProbeFallback = hasForcedFallback; if (!hasForcedFallback) { Bool nativeRenderable = false; @@ -600,8 +623,8 @@ namespace MobileGL::MG_Backend::DirectGLES { } const SizeT renderbufferTargetIndex = GetRenderbufferFormatCapabilityTargetIndex(); - Bool shouldProbeFallbackRenderbuffer = hasForcedFallback; - if (!hasForcedFallback) { + Bool shouldProbeFallbackRenderbuffer = outerHasForcedFallback; + if (!outerHasForcedFallback) { const Bool nativeRenderbufferComplete = ProbeRenderbuffer(gl, nativeInfo.InternalFormat, logicalFormat, false, 1); if (nativeRenderbufferComplete) { @@ -615,16 +638,16 @@ namespace MobileGL::MG_Backend::DirectGLES { shouldProbeFallbackRenderbuffer = true; } } - if (shouldProbeFallbackRenderbuffer && fallbackInfo.InternalFormat != GL_UNKNOWN_MGL && - ProbeRenderbuffer(gl, fallbackInfo.InternalFormat, logicalFormat, false, 1)) { + if (shouldProbeFallbackRenderbuffer && outerFallbackInfo.InternalFormat != GL_UNKNOWN_MGL && + ProbeRenderbuffer(gl, outerFallbackInfo.InternalFormat, logicalFormat, false, 1)) { if (AddCaveatFormatCaps(cache, renderbufferTargetIndex, formatIndex, GetRenderbufferFeatureCaps(logicalFormat))) { - LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, fallbackInfo); + LogGLESFormatCaveat(logicalFormat, renderbufferTargetIndex, outerFallbackInfo); } const Int maxSamples = - GetGLESFormatMaxSamples(capabilities, logicalFormat, fallbackInfo.ImageFormat); + GetGLESFormatMaxSamples(capabilities, logicalFormat, outerFallbackInfo.ImageFormat); cache.SampleCounts[renderbufferTargetIndex][formatIndex] = - ProbeRenderbufferSampleCounts(gl, fallbackInfo.InternalFormat, logicalFormat, maxSamples); + ProbeRenderbufferSampleCounts(gl, outerFallbackInfo.InternalFormat, logicalFormat, maxSamples); } } } diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index bfd3d550..fdb16627 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -48,15 +48,31 @@ namespace MobileGL::MG_Backend::DirectGLES { return options; } - Flags GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat) { + Flags + GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat, Bool mustStayRenderable) { using namespace MG_Util::TextureFormatProcessor; - const Flags forcedOptions = - GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, GetForcedPixelFormatNormalizeOptions()); + Flags extraOptions; + if (mustStayRenderable) { + extraOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; + } + const Flags forcedOptions = GetApplicablePixelFormatNormalizeOptions( + requestedInternalFormat, GetForcedPixelFormatNormalizeOptions() | extraOptions); if (forcedOptions) { return forcedOptions; } - return GetApplicablePixelFormatNormalizeOptions(requestedInternalFormat, - GetDriverPixelFormatNormalizeOptions()); + return GetApplicablePixelFormatNormalizeOptions( + requestedInternalFormat, GetDriverPixelFormatNormalizeOptions() | extraOptions); + } + + // Multisample textures can only ever be rendered into, never uploaded to, so a fallback + // format for them has to stay colour-renderable - a three-channel float fallback is a legal + // ES texture format but not a legal multisample storage format. Widening to four channels + // is safe here precisely because there is no transfer path that would have to expand + // three-channel client data, and the alpha the draw writes for a three-channel source is + // already the 1.0 the frontend format implies. + Bool TargetRequiresRenderableFormat(SizeT targetIndex) { + return targetIndex == static_cast(TextureTarget::Texture2DMultisample) || + targetIndex == static_cast(TextureTarget::Texture2DMultisampleArray); } Bool HasCachedFormatCapability(TextureInternalFormat internalFormat, @@ -116,7 +132,8 @@ namespace MobileGL::MG_Backend::DirectGLES { const GLenum requestedInternalFormat = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat); Flags options; if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) { - options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat); + options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat, + TargetRequiresRenderableFormat(targetIndex)); } NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType); } diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp index f58c5a06..dbd409cc 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp @@ -29,11 +29,13 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { case GL_RGB12: // stored as RGB16 (see NormalizePixelFormat) applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16; applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRgb16; + applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; break; case GL_RGB16_SNORM: applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGB16Snorm; applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16; applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16; + applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; break; case GL_RGBA16_SNORM: case GL_RG16_SNORM: @@ -46,6 +48,9 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { applicableOptions |= options & PixelFormatNormalizeOptionBit::NoRGBA8Snorm; break; case GL_RGB8_SNORM: + applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8; + applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; + break; case GL_RG8_SNORM: case GL_R8_SNORM: applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm8; @@ -87,6 +92,13 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { *outInternalFormat = internalFormat; break; case GL_RGB16: + if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { + // GL_RGB32F is a legal ES texture format but is not colour-renderable, so + // glTexStorage2DMultisample rejects it and the attachment ends up with no + // storage at all. + *outInternalFormat = GL_RGBA32F; + break; + } if ((options & PixelFormatNormalizeOptionBit::NoNorm16) || (options & PixelFormatNormalizeOptionBit::NoRgb16)) { *outInternalFormat = GL_RGB32F; @@ -117,6 +129,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { *outInternalFormat = internalFormat; break; case GL_RGB16_SNORM: + if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { + *outInternalFormat = GL_RGBA16F; + break; + } if ((options & PixelFormatNormalizeOptionBit::NoNorm16) || (options & PixelFormatNormalizeOptionBit::NoRGB16Snorm) || (options & PixelFormatNormalizeOptionBit::NoSnorm16)) { @@ -150,6 +166,10 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { *outInternalFormat = internalFormat; break; case GL_RGB8_SNORM: + if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { + *outInternalFormat = GL_RGBA16F; + break; + } if (options & PixelFormatNormalizeOptionBit::NoSnorm8) { *outInternalFormat = GL_RGB16F; break; diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.h b/MobileGL/MG_Util/Texture/TextureFormatProcessor.h index 4efbeee5..8190c558 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.h +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.h @@ -18,6 +18,12 @@ namespace MobileGL { NoDepthComponent32 = 1 << 4, NoRGBA8Snorm = 1 << 5, NoRGB16Snorm = 1 << 6, + // The target must be colour-renderable and ES has no renderable three-channel + // form of the requested format, so it has to be widened to the four-channel one. + // Only meaningful for multisample textures: those can never be uploaded to, only + // rendered into, so the extra alpha comes from the draw (1.0 for an RGB source) + // and no transfer path has to expand three-channel client data. + NoThreeChannelRenderTarget = 1 << 7, None = 0, }; namespace MG_Util::TextureFormatProcessor {