From e45f7ae5d44771f6719ae21e63f4938cfcd94366 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Sun, 2 Aug 2026 04:24:23 -0400 Subject: [PATCH] [Fix] (DirectGLES, MG_Util): keep 16-bit SNORM precision through the widening GL_RGB16_SNORM widened to GL_RGBA16F to stay renderable as multisample storage, and a half float's 11-bit mantissa cannot hold a 16-bit signed-normalized channel: KHR-GL33.texture_swizzle's blue channel came back several units of 32767 away from the value the reference computes, well outside its one-unit tolerance. GL_EXT_render_snorm makes the signed-normalized formats colour-renderable on ES, so widen to GL_RGBA16_SNORM instead wherever it and EXT_texture_norm16 are both present, and only fall back to the half float otherwise. Threaded through as its own normalize option so the capability probe and the runtime pick the same format, the way every other driver-dependent substitution here is decided. --- .../DirectGLES/BackendObject_DirectGLES.cpp | 6 ++++++ MobileGL/MG_Backend/DirectGLES/Utils.cpp | 21 +++++++++++++------ .../MG_Util/BackendLoaders/OpenGL/Loader.cpp | 3 +++ .../MG_Util/BackendLoaders/OpenGL/Loader.h | 3 +++ .../Texture/TextureFormatProcessor.cpp | 9 +++++++- .../MG_Util/Texture/TextureFormatProcessor.h | 5 +++++ 6 files changed, 40 insertions(+), 7 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp index 490b5ebe..a8cea1b5 100644 --- a/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/BackendObject_DirectGLES.cpp @@ -213,6 +213,9 @@ namespace MobileGL::MG_Backend::DirectGLES { if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { reasons.push_back("no three-channel multisample storage format on OpenGL ES"); } + if (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) { + reasons.push_back("EXT_render_snorm not supported"); + } String reason; for (SizeT i = 0; i < reasons.size(); ++i) { @@ -576,6 +579,9 @@ namespace MobileGL::MG_Backend::DirectGLES { Flags targetOptions; if (IsGLESProbeMultisampleTarget(target)) { targetOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; + if (!capabilities.SupportsRenderSnorm || !capabilities.SupportsNorm16Texture) { + targetOptions |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget; + } } GLESProbeFormatInfo fallbackInfo = outerFallbackInfo; Bool hasForcedFallback = outerHasForcedFallback; diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index fdb16627..1ccb8aa7 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -49,12 +49,9 @@ namespace MobileGL::MG_Backend::DirectGLES { } Flags - GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat, Bool mustStayRenderable) { + GetRuntimeFallbackNormalizeOptions(GLenum requestedInternalFormat, + Flags extraOptions) { using namespace MG_Util::TextureFormatProcessor; - Flags extraOptions; - if (mustStayRenderable) { - extraOptions |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; - } const Flags forcedOptions = GetApplicablePixelFormatNormalizeOptions( requestedInternalFormat, GetForcedPixelFormatNormalizeOptions() | extraOptions); if (forcedOptions) { @@ -75,6 +72,18 @@ namespace MobileGL::MG_Backend::DirectGLES { targetIndex == static_cast(TextureTarget::Texture2DMultisampleArray); } + Flags GetRenderTargetNormalizeOptions(SizeT targetIndex) { + Flags options; + if (!TargetRequiresRenderableFormat(targetIndex)) { + return options; + } + options |= PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; + if (!g_GLESCapabilities.SupportsRenderSnorm || !g_GLESCapabilities.SupportsNorm16Texture) { + options |= PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget; + } + return options; + } + Bool HasCachedFormatCapability(TextureInternalFormat internalFormat, SizeT targetIndex, Bool caveat, @@ -133,7 +142,7 @@ namespace MobileGL::MG_Backend::DirectGLES { Flags options; if (!pActiveBackendObject || ShouldUseCaveatFormat(internalFormat, targetIndex)) { options = GetRuntimeFallbackNormalizeOptions(requestedInternalFormat, - TargetRequiresRenderableFormat(targetIndex)); + GetRenderTargetNormalizeOptions(targetIndex)); } NormalizePixelFormat(requestedInternalFormat, options, outInternalFormat, outFormat, outType); } diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp index ac006e8e..ff73b843 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.cpp @@ -824,6 +824,9 @@ namespace MobileGL::MG_Util::BackendLoader { if (std::strcmp(extension, "GL_EXT_texture_norm16") == 0) { caps.SupportsNorm16Texture = true; } + if (std::strcmp(extension, "GL_EXT_render_snorm") == 0) { + caps.SupportsRenderSnorm = true; + } if (std::strcmp(extension, "GL_EXT_sRGB_write_control") == 0) { caps.SupportsSrgbWriteControl = true; } diff --git a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h index f9196485..45ba3ed3 100644 --- a/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h +++ b/MobileGL/MG_Util/BackendLoaders/OpenGL/Loader.h @@ -1031,6 +1031,9 @@ namespace MobileGL { String GLESShadingLanguageVersionString; Bool SupportsPersistentMapping = false; Bool SupportsNorm16Texture = false; + // GL_EXT_render_snorm is present, so the signed-normalized formats are colour-renderable + // (and usable as multisample texture storage) rather than texture-only. + Bool SupportsRenderSnorm = false; // GL_EXT_sRGB_write_control is present, so GL_FRAMEBUFFER_SRGB can be turned off. // GLES has no such switch in core: writes into an sRGB attachment are ALWAYS encoded, // while desktop GL leaves GL_FRAMEBUFFER_SRGB disabled by default and writes raw. diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp index dbd409cc..800aaeab 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.cpp @@ -36,6 +36,9 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { applicableOptions |= options & PixelFormatNormalizeOptionBit::NoNorm16; applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16; applicableOptions |= options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget; + if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { + applicableOptions |= options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget; + } break; case GL_RGBA16_SNORM: case GL_RG16_SNORM: @@ -130,7 +133,11 @@ namespace MobileGL::MG_Util::TextureFormatProcessor { break; case GL_RGB16_SNORM: if (options & PixelFormatNormalizeOptionBit::NoThreeChannelRenderTarget) { - *outInternalFormat = GL_RGBA16F; + // A half float loses the low bits of a 16-bit SNORM channel, so keep the + // signed-normalized encoding whenever the driver can render to it. + *outInternalFormat = (options & PixelFormatNormalizeOptionBit::NoSnorm16RenderTarget) + ? GL_RGBA16F + : GL_RGBA16_SNORM; break; } if ((options & PixelFormatNormalizeOptionBit::NoNorm16) || diff --git a/MobileGL/MG_Util/Texture/TextureFormatProcessor.h b/MobileGL/MG_Util/Texture/TextureFormatProcessor.h index 8190c558..c5fcd9fa 100644 --- a/MobileGL/MG_Util/Texture/TextureFormatProcessor.h +++ b/MobileGL/MG_Util/Texture/TextureFormatProcessor.h @@ -24,6 +24,11 @@ namespace MobileGL { // 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, + // Pairs with the bit above: the widened four-channel format has to stay renderable AND + // keep 16-bit signed-normalized precision, which needs both EXT_texture_norm16 and + // EXT_render_snorm. Without them the only renderable widening left is a half float, whose + // 11-bit mantissa cannot represent a 16-bit SNORM channel exactly. + NoSnorm16RenderTarget = 1 << 8, None = 0, }; namespace MG_Util::TextureFormatProcessor {