From 3a12f6d4f3850e7c188714f5621b8438903f51a5 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Fri, 21 Aug 2026 03:21:09 -0400 Subject: [PATCH] [Fix, Test] (DirectGLES, ShaderTranspiler): emulate the 17 exactly-carriable non-core image formats by channel widening --- CMakeLists.txt | 1 + MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp | 29 +- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 259 ++++++++- MobileGL/MG_Backend/DirectGLES/Utils.cpp | 38 ++ MobileGL/MG_Backend/DirectGLES/Utils.h | 57 ++ .../MG_Test/ShaderTranspiler/CMakeLists.txt | 1 + .../ShaderTranspiler/TranslationCacheTest.cpp | 7 + .../WidenImageFormatsTest.cpp | 397 +++++++++++++ MobileGL/MG_Test/Texture/TextureTest.cpp | 74 +++ .../ShaderTranspiler/ShaderCompiler.cpp | 30 + .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 25 + .../SpirvPasses/BakeImageFormatsPass.cpp | 15 +- .../SpirvPasses/WidenImageFormatsPass.cpp | 540 ++++++++++++++++++ .../SpirvPasses/WidenImageFormatsPass.h | 101 ++++ .../ShaderTranspiler/TranslationCache.cpp | 1 + .../ShaderTranspiler/TranslationCache.h | 5 + 16 files changed, 1551 insertions(+), 29 deletions(-) create mode 100644 MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d64f3f5e..5bb93da1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -294,6 +294,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/Lower1DArrayImagesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/ClampMultisampleFetchPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/PrivateToEntryLocalPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUniformLocationsPass.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp index 2dbc8753..a09886f9 100644 --- a/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp +++ b/MobileGL/MG_Backend/DirectGLES/DirectGLES.cpp @@ -1463,8 +1463,35 @@ namespace MobileGL::MG_Backend::DirectGLES { const Bool layerable = SupportsLayeredImageBinding(imageBinding.Texture->GetTarget()); const GLboolean layered = layerable ? imageBinding.Layered : GL_FALSE; const GLint layer = layerable ? imageBinding.Layer : 0; + // The bind half of the image-format widening. SyncTextureObjectToBackend has just + // allocated this texture's storage in the core carrier of its format (the call above + // is the one that marks it image-bindable), and glBindImageTexture's `format` has to + // name the storage the texture really has: a GL_RG32F bind is GL_INVALID_VALUE on + // Adreno for nineteen of the twenty-six non-core formats and on both Malis for + // twenty-five, and every driver that DOES accept a narrow texture through a wide + // image accepts it silently, reading and writing out of bounds. The frontend's own + // ImageTextureBinding keeps the application's format untouched, so + // GL_IMAGE_BINDING_FORMAT still answers what was passed in. + // + // Widened from the format the APPLICATION named rather than from the texture's own, + // because GL lets the two differ inside one format class and the shader was widened + // from the class the application named too (an r32ui view of an r32f image is a legal + // reinterpretation). The two carriers always have the same texel size - every format + // in a class widens to the four-channel form of that same class - so the storage + // still describes what the bind claims. Gated on the TEXTURE having been widened, so + // a bind format that names a class the storage does not have is left alone: GL + // already calls that undefined, and inventing a carrier for it would only make the + // out-of-class read wider. + GLenum bindFormat = imageBinding.Format; + if (TextureImpl::GetImageBindableStorageWidening(imageBinding.Texture->GetFormat())) { + const auto boundFormatWidening = TextureImpl::GetImageBindableStorageWidening( + MG_Util::ConvertGLEnumToTextureInternalFormat(imageBinding.Format)); + if (boundFormatWidening) { + bindFormat = boundFormatWidening.InternalFormat; + } + } g_GLESFuncs.glBindImageTexture(unit, backendTexture->GetBackendTextureId(), imageBinding.Level, - layered, layer, imageBinding.Access, imageBinding.Format); + layered, layer, imageBinding.Access, bindFormat); } // A buffer texture bound to a WRITABLE image unit is a buffer the shader is about to diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 0da1f4f2..0d5b0c31 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -2282,6 +2282,14 @@ namespace MobileGL::MG_Backend::DirectGLES { } m_imageBindableStorageRequired = true; m_isInitialized = false; + // The storage this re-mints may also be CHANNEL WIDENED (a GL_RG32F image is not + // bindable on this driver at all, so it becomes a GL_RGBA32F carrying two channels), + // and a widened texture's sampled view has to answer the channels the logical format + // does not have with 0 and 1 - which is a swizzle. The parameter sync is gated on the + // frontend's params version, which this transition does not move, so without the + // override an application that never touched GL_TEXTURE_SWIZZLE_* would keep the + // driver at its defaults and sample the carrier's surplus channels raw. + m_forceTextureParamsResync = true; } void BackendTextureObject::RecreateBackendTexture() { @@ -2579,7 +2587,12 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector& widenedData, Bool integerData) { Uint8 oneBits[8] = {}; SizeT componentSize = 0; - if (componentCount != 3 || data == nullptr || byteSize == 0 || + // One and two source components as well as three: the image-format widening carries + // GL_R8UI in a GL_RGBA8UI and GL_RG32F in a GL_RGBA32F (see + // TextureImpl::GetImageBindableStorageWidening), and their surplus channels take the + // same values the three-channel case gives its single added one - zeroes, and the + // format's implied 1 in alpha. + if (componentCount == 0 || componentCount > 3 || data == nullptr || byteSize == 0 || !GetUploadComponentOneBits(uploadType, integerData, oneBits, &componentSize)) { return data; } @@ -2606,7 +2619,11 @@ namespace MobileGL::MG_Backend::DirectGLES { Memcpy(dst, src, srcTexelBytes); src += srcTexelBytes; } - Memcpy(dst + srcTexelBytes, oneBits, componentSize); + // ALWAYS at component 3, never at `componentCount`: GL's implied 1 is the ALPHA + // channel, and a one- or two-component source leaves the channels between it and + // alpha at the zero `assign` already wrote. For three components the two + // expressions coincide, which is what this used to be written as. + Memcpy(dst + componentSize * 3, oneBits, componentSize); } return widenedData.data(); } @@ -2689,6 +2706,42 @@ namespace MobileGL::MG_Backend::DirectGLES { widenedData, IsIntegerWidenableFormat(format)); } + // The transfer half of the image-format widening: an image-bindable texture whose ES + // storage was widened to a core carrier is described to the driver as a four-component + // transfer, so its one- or two-component client data has to be repacked the same way the + // three-channel colour-renderable widening repacks its own. + // + // Composes with PrepareFallbackUpload rather than replacing it, and the composition is a + // no-op by construction: none of the seventeen widened formats is a three-channel one + // (GetWidenableClientComponentCount reports 0 for every one of them), and the SNORM + // shadow-to-float conversion only fires for a GL_FLOAT transfer type, which the widened + // triple never picks for the two SNORM8 formats. So the shadow reaches this untouched and + // one repack is all that runs. + static const void* PrepareImageWidenedUpload(const TextureImpl::ImageBindableStorageWidening& widening, + const IntVec3& texelSize, const void* data, SizeT byteSize, + Vector& widenedData) { + if (!widening || widening.SourceChannels == 0 || widening.SourceChannels >= 4) { + return data; + } + return PrepareChannelWidenedUpload(widening.SourceChannels, texelSize, data, byteSize, widening.Type, + widenedData, widening.IntegerData); + } + + // Overwrites the (internal format, format, type) triple GenerateTextureFormatInfo chose + // with the widened carrier's. Deliberately unconditional on anything but the widening + // itself: whatever renderability fallback the triple carried, an image the driver refuses + // to bind is useless, so the image constraint wins. + static void ApplyImageBindableStorageWidening(const TextureImpl::ImageBindableStorageWidening& widening, + GLenum* inOutInternalFormat, GLenum* inOutFormat, + GLenum* inOutType) { + if (!widening) { + return; + } + if (inOutInternalFormat) *inOutInternalFormat = widening.InternalFormat; + if (inOutFormat) *inOutFormat = widening.Format; + if (inOutType) *inOutType = widening.Type; + } + // RGB565/RGB5_A1 shadow data is stored as 8-bit unorm; uploading it as GL_UNSIGNED_BYTE // leaves the 8-bit -> 5/6-bit requantization to the driver, whose rounding direction is // implementation-defined: Adreno rounds to nearest (lossless round trip) but Mali floors, @@ -2884,6 +2937,15 @@ namespace MobileGL::MG_Backend::DirectGLES { Bind(target); } + // Only a texture that is actually image-bound pays for the widening: it doubles + // or quadruples the storage, and RequireImageBindableStorage is sticky, so a + // texture that is merely sampled keeps its narrow format for life. See + // TextureImpl::GetImageBindableStorageWidening for what widens and why. + const TextureImpl::ImageBindableStorageWidening imageWidening = + m_imageBindableStorageRequired + ? TextureImpl::GetImageBindableStorageWidening(textureMipmapObject->GetFormat()) + : TextureImpl::ImageBindableStorageWidening{}; + const Bool canAppendMipmaps = m_isInitialized && !m_imageBindableStorageRequired && @@ -2989,6 +3051,7 @@ namespace MobileGL::MG_Backend::DirectGLES { GLenum glInternalFormat, glType, glFormat; TextureImpl::GenerateTextureFormatInfo(textureMipmapObject->GetFormat(), &glInternalFormat, &glFormat, &glType, targetInternal); + ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType); const auto& uploadTargets = textureMipmapObject->GetUploadTargets(); if (TextureImpl::IsMultisampleTextureTarget(targetInternal)) { @@ -3104,6 +3167,9 @@ namespace MobileGL::MG_Backend::DirectGLES { uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), levelTexelSize, uploadData, levelByteSize, &glType, packedUploadData); + Vector imageWidenedUploadData; + uploadData = PrepareImageWidenedUpload(imageWidening, levelTexelSize, uploadData, + levelByteSize, imageWidenedUploadData); DebugImpl::ErrorLopper::Clear(); BufferImpl::BindPixelUnpackBufferId(0); // no-op once the resting 0 state is pinned @@ -3243,6 +3309,11 @@ namespace MobileGL::MG_Backend::DirectGLES { GLenum glInternalFormat, glType, glFormat; TextureImpl::GenerateTextureFormatInfo(textureMipmapObject->GetFormat(), &glInternalFormat, &glFormat, &glType, targetInternal); + // The storage this level is being written into was widened when it was minted + // (see above), so the transfer pair has to describe the carrier here too - ES + // requires glTexSubImage's `format` to match the storage's base internal + // format, so a GL_RG upload into a GL_RGBA32F image is GL_INVALID_OPERATION. + ApplyImageBindableStorageWidening(imageWidening, &glInternalFormat, &glFormat, &glType); const auto& uploadTargets = textureMipmapObject->GetUploadTargets(); ScopedDefaultUnpackState unpackState; for (auto& uploadTarget : uploadTargets) { @@ -3281,6 +3352,13 @@ namespace MobileGL::MG_Backend::DirectGLES { Vector packedUploadData; uploadData = PreparePackedNormUpload(textureMipmapObject->GetFormat(), texelSize, uploadData, byteSize, &glType, packedUploadData); + // Leaves `uploadData` pointing at its own buffer when it fires, which + // is exactly what takes the sub-rect fast path below out of play: that + // path strides into the SHADOW, and the widened texels are four + // components wide where the shadow's are one or two. + Vector imageWidenedUploadData; + uploadData = PrepareImageWidenedUpload(imageWidening, texelSize, uploadData, byteSize, + imageWidenedUploadData); const IntVec3 uploadSize = GetBackendUploadSize(stateTextureObject->GetTarget(), texelSize); // Sub-rect upload: when only a region of the level changed (a @@ -3726,6 +3804,35 @@ namespace MobileGL::MG_Backend::DirectGLES { } } } + // The same composition for the image-format widening, which can add TWO or THREE + // channels rather than one (GL_R8UI carried in a GL_RGBA8UI). GL reads a channel the + // format does not have as 0, except alpha, which reads as 1 - so a sampler must see + // those constants and not whatever the widened storage holds. The upload and the + // shader's own store mask already keep them at exactly these values; this covers + // storage nothing has written yet (glTexStorage with no upload), whose surplus + // channels are undefined. Composed with the application's own swizzle for the same + // reason as the alpha case above: GL_TEXTURE_SWIZZLE names a SOURCE channel of the + // logical texel, so it is the source that is substituted, never the destination. + if (const auto imageWidening = + m_imageBindableStorageRequired + ? TextureImpl::GetImageBindableStorageWidening(stateTextureObject->GetFormat()) + : TextureImpl::ImageBindableStorageWidening{}) { + for (SizeT channel = 0; channel < 4; ++channel) { + switch (swizzleParams[channel]) { + case TextureSwizzleParam::Green: + if (imageWidening.SourceChannels < 2) swizzleParams[channel] = TextureSwizzleParam::Zero; + break; + case TextureSwizzleParam::Blue: + if (imageWidening.SourceChannels < 3) swizzleParams[channel] = TextureSwizzleParam::Zero; + break; + case TextureSwizzleParam::Alpha: + if (imageWidening.SourceChannels < 4) swizzleParams[channel] = TextureSwizzleParam::One; + break; + default: + break; + } + } + } if (swizzleParams != m_cacheSwizzleParams) { #define SYNC_TEX_SWIZZLE_PARAM_IF_CHANGED(func, glEnum) \ if (m_cacheSwizzleParams.func != swizzleParams.func) { \ @@ -5042,6 +5149,45 @@ namespace MobileGL::MG_Backend::DirectGLES { return false; } } + + // The GL internal format a glslang layout format names, for the seventeen non-core + // formats WidenImageFormatsForEssl carries exactly plus nothing else: the only + // question asked of it is "does this DECLARED format widen", and answering 0 for + // everything else is the same "no" a non-widenable format gets. Kept as its own + // switch rather than routed through the frontend's enum converters because a + // TLayoutFormat is a glslang value and the reflection snapshot stores it raw. + Uint GLInternalFormatOfLayoutFormat(glslang::TLayoutFormat format) { + switch (format) { + case glslang::ElfRg32f: return 0x8230; // GL_RG32F + case glslang::ElfRg16f: return 0x822F; // GL_RG16F + case glslang::ElfR16f: return 0x822D; // GL_R16F + case glslang::ElfRg8: return 0x822B; // GL_RG8 + case glslang::ElfR8: return 0x8229; // GL_R8 + case glslang::ElfRg8Snorm: return 0x8F95; // GL_RG8_SNORM + case glslang::ElfR8Snorm: return 0x8F94; // GL_R8_SNORM + case glslang::ElfRg32i: return 0x823B; // GL_RG32I + case glslang::ElfRg16i: return 0x8239; // GL_RG16I + case glslang::ElfR16i: return 0x8233; // GL_R16I + case glslang::ElfRg8i: return 0x8237; // GL_RG8I + case glslang::ElfR8i: return 0x8231; // GL_R8I + case glslang::ElfRg32ui: return 0x823C; // GL_RG32UI + case glslang::ElfRg16ui: return 0x823A; // GL_RG16UI + case glslang::ElfR16ui: return 0x8234; // GL_R16UI + case glslang::ElfRg8ui: return 0x8238; // GL_RG8UI + case glslang::ElfR8ui: return 0x8232; // GL_R8UI + default: + return 0; + } + } + + // Whether the ESSL chain will re-declare an image of this format in a core carrier + // and mask its accesses (WidenImageFormatsForEssl). Armed only where there is no + // GL_NV_image_formats to spell the format natively - a driver that has the extension + // keeps the declaration and gets the `#extension` directive instead. + Bool ImageFormatWillBeWidened(Uint glInternalFormat) { + return !g_GLESCapabilities.SupportsExtendedImageFormats && glInternalFormat != 0 && + MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(glInternalFormat) != 0; + } } // namespace // What the format bake needs from the frontend, collected in one walk of the uniform @@ -5074,17 +5220,24 @@ namespace MobileGL::MG_Backend::DirectGLES { if (!IsImageUniformType(stateProgramObject.GetUniformType(loc))) continue; const auto& type = stateProgramObject.GetUniformTypeFacts(loc); if (type.hasFormat) { - // Declared, and therefore left exactly as written - but a non-core spelling - // still needs the extension directive to survive the ES compiler. - if (!IsCoreEsslLayoutFormat(static_cast(type.layoutFormat))) { - inputs.needsExtendedImageFormats = true; - if (!g_GLESCapabilities.SupportsExtendedImageFormats) { - // From the OWNED TypeFacts, not from a live TType: the reflection - // snapshot already carries the declared layout format, and there is - // no glslang object to ask on a translation-cache L1 hit. - recordUnspellableFormat( - name, glslang::TQualifier::getLayoutFormatString( - static_cast(type.layoutFormat))); + // Declared, and therefore never overridden by the BAKE - but a non-core + // spelling still has to become legal ESSL somehow. + const auto declaredFormat = static_cast(type.layoutFormat); + if (!IsCoreEsslLayoutFormat(declaredFormat)) { + // Seventeen of the twenty-six non-core formats are re-declared in the core + // format that carries them exactly, with every access masked back to the + // channels GL says they have (WidenImageFormatsForEssl, and the matching + // storage/bind widening in TextureImpl). Those need neither the extension + // nor the diagnostic: there IS a legal spelling for them now. + if (!ImageFormatWillBeWidened(GLInternalFormatOfLayoutFormat(declaredFormat))) { + inputs.needsExtendedImageFormats = true; + if (!g_GLESCapabilities.SupportsExtendedImageFormats) { + // From the OWNED TypeFacts, not from a live TType: the reflection + // snapshot already carries the declared layout format, and there + // is no glslang object to ask on a translation-cache L1 hit. + recordUnspellableFormat( + name, glslang::TQualifier::getLayoutFormatString(declaredFormat)); + } } } continue; @@ -5102,20 +5255,31 @@ namespace MobileGL::MG_Backend::DirectGLES { if (boundFormat == 0) continue; if (!MG_Util::ShaderTranspiler::ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(boundFormat)) { - // Outside the GLSL ES core set, so the emitted ESSL only compiles with - // GL_NV_image_formats. Without the extension there is no legal spelling at - // all, and baking one would trade a "no format qualifier" compile error for - // an "unsupported format" one - so the image is left format-less. Its unit - // stays in the key, so a rebind to a core format still rebuilds and works. if (!g_GLESCapabilities.SupportsExtendedImageFormats) { - MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which GLSL ES " - "core cannot spell and this driver has no GL_NV_image_formats for.", - name.c_str(), unit, boundFormat); - recordUnspellableFormat( - name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat)); - continue; + // Outside the GLSL ES core set and with no GL_NV_image_formats to spell + // it. If the format widens exactly it is still baked HERE, narrow, and + // WidenImageFormatsForEssl re-declares it in its core carrier immediately + // afterwards - both routes into that pass end in the same place. + // + // If it does not widen there is no legal ESSL for this stage at all. + // Leaving the image format-LESS is NOT a softer failure: all three test + // devices reject a format-less image declaration outright ("all images + // have to define layout format"), readonly and writeonly alike, so it + // trades one hard compile error for another. The unit stays in the + // rebuild key either way, so a rebind to a spellable format still + // rebuilds and works. + if (!ImageFormatWillBeWidened(boundFormat)) { + MGLOG_D("Image uniform '%s' has no declared format and its unit %d holds 0x%x, which " + "GLSL ES core cannot spell, this driver has no GL_NV_image_formats for, and " + "no core format carries exactly.", + name.c_str(), unit, boundFormat); + recordUnspellableFormat( + name, MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(boundFormat)); + continue; + } + } else { + inputs.needsExtendedImageFormats = true; } - inputs.needsExtendedImageFormats = true; } const String baseName = ImageUniformBaseName(name); const auto existing = inputs.glFormatByUniformName.find(baseName); @@ -5147,6 +5311,13 @@ namespace MobileGL::MG_Backend::DirectGLES { if (MG_Util::ShaderTranspiler::ShaderCompiler::SpirvCrossCanPrintEsslImageFormat(entry.second)) { continue; } + // A format the widening carries stays on the module route even though SPIRV-Cross + // would not print it: by the time SPIRV-Cross sees the declaration it names the + // core carrier, which it does print. Writing the narrow spelling into the text + // instead would put back exactly the token the driver rejects. + if (ImageFormatWillBeWidened(entry.second)) { + continue; + } String spelling = MG_Util::ShaderTranspiler::ShaderCompiler::EsslImageFormatSpelling(entry.second); if (spelling.empty()) continue; // no image-format spelling at all; nothing to write inputs.esslFormatQualifierByUniformName.emplace(entry.first, Move(spelling)); @@ -5197,8 +5368,8 @@ namespace MobileGL::MG_Backend::DirectGLES { // // Reads (audited): the arguments; g_GLESCapabilities.{SupportsViewportArray, // MaxSamples, MaxColorTextureSamples, MaxIntegerSamples, MaxDepthTextureSamples, - // SupportsNoperspectiveInterpolation, GLESVersion} (the last via - // ResolveBackendEsslVersion); and m_backendProgramId, for a log line only. + // SupportsNoperspectiveInterpolation, SupportsExtendedImageFormats, GLESVersion} + // (the last via ResolveBackendEsslVersion); and m_backendProgramId, for a log line only. // // Deliberately NOT in here, and therefore NOT in the key: the text-level passes that // follow in SyncToBackend. They are cheap string work and they read a long tail of @@ -5445,6 +5616,38 @@ namespace MobileGL::MG_Backend::DirectGLES { effectiveSpirv = &imageFormatSpirv; } + // GL has forty image formats and GLSL ES core has thirteen; the other twenty-seven + // reach ES only through GL_NV_image_formats, which no tested driver advertises. A + // shader declaring one of them has NO legal ESSL spelling at all - SPIRV-Cross throws + // for some of them and the driver rejects the token for the rest ("'rg32f' : not a + // legal layout qualifier id"), and dropping the qualifier is refused too ("all images + // have to define layout format") - so the stage is lost and every draw with the + // program silently renders nothing. Seventeen of them widen EXACTLY into a core format + // of the same per-channel width, and this rewrites those declarations to the carrier + // and masks every access back to the channels GL says the format has. The other nine + // have no exact carrier and keep the honest diagnostic + // CollectImageFormatBakeInputs emits. + // + // AFTER the bake above, deliberately: a format-less image whose unit holds a non-core + // format is baked with that format and widened here, so both routes end in the same + // place and there is no second widening rule for baked declarations. + // + // KEY MATERIAL: g_GLESCapabilities.SupportsExtendedImageFormats, which ARMS this - + // see EsslTranslationKeyInputs::supportsExtendedImageFormats. The pass takes no other + // input: what it rewrites is a pure function of the module's own declared formats, + // and the module is already the largest thing in the L2 key. + // + // DirectVulkan is deliberately not given this: it takes the declared format natively + // and resolves the descriptor's view format from the same bind state. + Vector widenedImageFormatSpirv; + if (!g_GLESCapabilities.SupportsExtendedImageFormats && + MG_Util::ShaderTranspiler::ShaderCompiler::DeclaresWidenableImageFormat(*effectiveSpirv) && + MG_Util::ShaderTranspiler::ShaderCompiler::WidenImageFormatsForEssl( + *effectiveSpirv, widenedImageFormatSpirv, enableSpirvValidation) && + !widenedImageFormatSpirv.empty()) { + effectiveSpirv = &widenedImageFormatSpirv; + } + // GLSL ES demands a constant integral expression to index a fragment output // array; SPIR-V does not, so a shader that writes coeff[i] from a loop // reaches SPIRV-Cross intact and comes out as ESSL a strict driver rejects @@ -5781,6 +5984,8 @@ namespace MobileGL::MG_Backend::DirectGLES { esslKeyInputs.supportsViewportArray = g_GLESCapabilities.SupportsViewportArray; esslKeyInputs.supportsNoperspectiveInterpolation = g_GLESCapabilities.SupportsNoperspectiveInterpolation; + esslKeyInputs.supportsExtendedImageFormats = + g_GLESCapabilities.SupportsExtendedImageFormats; esslKeyInputs.maxColorTextureSamples = g_GLESCapabilities.MaxColorTextureSamples; esslKeyInputs.maxIntegerSamples = g_GLESCapabilities.MaxIntegerSamples; esslKeyInputs.maxDepthTextureSamples = g_GLESCapabilities.MaxDepthTextureSamples; diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index a6a6bd8d..1f5a53ef 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -12,6 +12,7 @@ #include "MG_Backend/BackendObjects.h" #include "MG_Util/Converters/GLToMG/FramebufferEnumConverter.h" #include "MG_Util/Texture/TextureFormatProcessor.h" +#include "MG_Util/ShaderTranspiler/ShaderCompiler.h" #include #include @@ -233,6 +234,43 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat) { return BackendFormatAddsAlpha(internalFormat, GetRenderbufferFormatCapabilityTargetIndex()); } + + ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat) { + // Same arming as WidenImageFormatsForEssl. A driver with GL_NV_image_formats spells + // the narrow format natively, and widening the storage behind a shader that still + // says `rg32f` would make the two disagree about the texel size. + if (g_GLESCapabilities.SupportsExtendedImageFormats) { + return {}; + } + const GLenum requested = MG_Util::ConvertTextureInternalFormatToGLEnum(internalFormat); + const auto carrier = static_cast( + MG_Util::ShaderTranspiler::ShaderCompiler::WidenedCoreEsslImageFormat(requested)); + if (carrier == 0) { + return {}; + } + ImageBindableStorageWidening widening; + widening.InternalFormat = carrier; + widening.SourceChannels = + MG_Util::ShaderTranspiler::ShaderCompiler::ImageFormatChannelCount(requested); + switch (carrier) { + case GL_RGBA32UI: + case GL_RGBA16UI: + case GL_RGBA8UI: + case GL_RGBA32I: + case GL_RGBA16I: + case GL_RGBA8I: + widening.IntegerData = true; + break; + default: + widening.IntegerData = false; + break; + } + // The carrier is a core ES format in every case, so it needs no fallback options of + // its own; this call is only here to spell the transfer pair that describes it. + MG_Util::TextureFormatProcessor::NormalizePixelFormat(carrier, Flags{}, + nullptr, &widening.Format, &widening.Type); + return widening; + } } // namespace TextureImpl namespace PrgramImpl { String ProcessOutColorLocations(const String& glslCode) { diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index e4712ad1..0bae6e95 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -60,6 +60,63 @@ namespace MobileGL::MG_Backend::DirectGLES { Bool BackendTextureFormatAddsAlpha(TextureInternalFormat internalFormat, TextureTarget target); Bool BackendRenderbufferFormatAddsAlpha(TextureInternalFormat internalFormat); Bool ShouldUseCaveatRenderbufferFormat(TextureInternalFormat internalFormat); + + // The CHANNEL WIDENING an image-bindable texture's ES storage takes, so that a format + // GLSL ES cannot spell as an image is carried by one it can. + // + // GL has forty image formats, GLSL ES core has thirteen, and no test device advertises + // GL_NV_image_formats - so a shader declaring one of the other twenty-six has no legal + // ESSL at all and glBindImageTexture rejects the narrow format outright for most of them + // (GL_INVALID_VALUE for nineteen of twenty-six on Adreno, twenty-five on both Malis). + // Seventeen have a core format of the SAME per-channel width and component type, + // differing only in channel count, and in one of those the emulation is EXACT: GL already + // defines an imageLoad from a narrower format as (r, 0, 0, 1) and an imageStore as + // dropping the components the format does not have, so the carrier's surplus channels + // hold values GL has already named. WidenImageFormatsPass pins them in the shader; this + // is the storage half, and DirectGLES::TextureImpl::SyncImageTextureBinding the bind + // half. All three ask WidenedCoreEsslImageFormat, so they cannot pick different carriers. + // + // Reports nothing (InternalFormat == GL_UNKNOWN_MGL) for a format that is core already, + // for the nine with no exact carrier (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16, + // r16, rgba16_snorm, rg16_snorm, r16_snorm - those keep the honest "no GLSL ES spelling" + // diagnostic rather than a silent approximation), and on a driver that HAS + // GL_NV_image_formats, where the shader keeps the declared format and no widening may + // happen behind it. + // + // The widened triple REPLACES what GenerateTextureFormatInfo chose, including any + // renderability substitution: an image that cannot be image-bound is useless whatever its + // attachment behaviour, so the image constraint wins. In practice that only bites + // RG8_SNORM/R8_SNORM on a driver without EXT_render_snorm, where the storage stays + // signed-normalized instead of becoming the half float that fallback would have picked - + // so an image-bound texture in one of those two formats is no longer attachable, and + // glGetTexImage on it falls through to the CPU shadow, which a shader-side imageStore + // does not update. Accepted deliberately: before the widening, an image binding in either + // format was refused outright by every driver tested and the stage that declared it never + // compiled at all, so nothing that works today is being given up. + // + // KNOWN GAP, for the same "all three layers move together" reason: a widened texture that + // is ALSO an FBO colour attachment gains one to three writable channels, and a draw into + // it can leave values in channels GL says are 0 and 1. Sampling and imageLoad are covered + // (the swizzle composition in SyncTextureParamsToBackend and the shader-side mask), but a + // glReadPixels/glGetTexImage that asks for more channels than the frontend format has + // would see them. Closing it needs the per-draw-buffer colour mask the three-channel + // widening already carries (FramebufferImpl::g_alphaWidenedDrawBufferMask) generalized + // from "alpha" to a channel count, which is its own change. + struct ImageBindableStorageWidening { + GLenum InternalFormat = GL_UNKNOWN_MGL; + GLenum Format = GL_UNKNOWN_MGL; + GLenum Type = GL_UNKNOWN_MGL; + // Channels the FRONTEND format has, i.e. how many of the carrier's four the client + // data fills. The rest are uploaded as 0, and the fourth as the format's implied 1. + Uint SourceChannels = 0; + // Whether that implied 1 is the integer one or a saturated normalized field - the + // transfer type cannot tell the two apart (GL_UNSIGNED_BYTE serves both RG8 and + // RG8UI), so the carrier decides. + Bool IntegerData = false; + + explicit operator Bool() const { return InternalFormat != GL_UNKNOWN_MGL; } + }; + ImageBindableStorageWidening GetImageBindableStorageWidening(TextureInternalFormat internalFormat); } // namespace TextureImpl namespace FramebufferImpl {} // namespace FramebufferImpl diff --git a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt index 3235ce07..ba0df960 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt +++ b/MobileGL/MG_Test/ShaderTranspiler/CMakeLists.txt @@ -14,6 +14,7 @@ add_executable( ClampMultisampleFetchTest.cpp LegalizeStorageBlockArrayIndexTest.cpp FlattenAtomicCounterBlockTest.cpp + WidenImageFormatsTest.cpp ) target_include_directories(SpirvPassTest PRIVATE diff --git a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp index 2ad8d82c..e68f0af1 100644 --- a/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp +++ b/MobileGL/MG_Test/ShaderTranspiler/TranslationCacheTest.cpp @@ -863,6 +863,13 @@ TEST_F(TranslationCacheTest, L2KeyMovesWithEveryGateThatSteersTheEsslChain) { v.supportsNoperspectiveInterpolation = true; variants.emplace_back("supportsNoperspectiveInterpolation", BuildEsslTranslationKey(v)); } + { // arms WidenImageFormatsForEssl - a driver WITH GL_NV_image_formats keeps the declared + // rg32f/r8ui/... image formats, one without has them re-declared in a core carrier and + // every access to them masked, so the two get materially different ESSL from one module. + EsslTranslationKeyInputs v = base; + v.supportsExtendedImageFormats = true; + variants.emplace_back("supportsExtendedImageFormats", BuildEsslTranslationKey(v)); + } { // arms AND parameterizes ClampMultisampleFetchesForEssl EsslTranslationKeyInputs v = base; v.maxColorTextureSamples = 1; diff --git a/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp b/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp new file mode 100644 index 00000000..294edd6a --- /dev/null +++ b/MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp @@ -0,0 +1,397 @@ +// MobileGL - MobileGL/MG_Test/ShaderTranspiler/WidenImageFormatsTest.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header +// +// WidenImageFormatsPass exists because GL has forty image formats and GLSL ES core has thirteen, +// and no device MobileGL runs on advertises GL_NV_image_formats - so a shader declaring one of the +// other twenty-six has no legal ESSL spelling at all. SPIRV-Cross throws for some of them and the +// driver rejects the token for the rest ("'rg32f' : not a legal layout qualifier id"), and dropping +// the qualifier is refused too ("all images have to define layout format"), so the stage is lost +// and every draw with the program silently renders nothing while GL_LINK_STATUS still says TRUE. +// +// What has to hold is the emulation's exactness, in three parts at once: the DECLARED format must +// become the core carrier of the same per-channel width, every imageStore through it must have its +// surplus components replaced by GL's own (0.., 1) so the carrier's extra channels never hold +// anything GL has not defined, and every imageLoad must come back masked the same way. A module +// that declares only core formats - or one of the nine formats with no exact carrier - must come +// out untouched, because widening those would be an approximation rather than an emulation. Real +// GLSL through the same glslang path the backends use, for the same reason +// ClampMultisampleFetchTest.cpp does it: what matters is what glslang actually emits. + +#include + +#define SPV_ENABLE_UTILITY_CODE +#include "glslang/SPIRV/spirv.hpp11" +#undef SPV_ENABLE_UTILITY_CODE + +#include "Includes.h" +#include "Init.h" +#include +#include + +#include + +#include +#include + +using namespace MobileGL; +using MobileGL::MG_Util::ShaderTranspiler::ShaderCompiler; + +namespace { + constexpr SizeT kSpirvHeaderWordCount = 5u; + + template + void ForEachInstruction(const Vector& spirv, Visitor&& visit) { + for (SizeT offset = kSpirvHeaderWordCount; offset < spirv.size();) { + const Uint32 wordCount = spirv[offset] >> 16u; + if (wordCount == 0u || offset + wordCount > spirv.size()) break; + visit(static_cast(spirv[offset] & 0xffffu), &spirv[offset], wordCount); + offset += wordCount; + } + } + + Vector CompileFragment(const String& source) { + using namespace MobileGL::MG_Util::ShaderTranspiler; + ShaderAttrib shaderAttrib{.shaderType = GL_FRAGMENT_SHADER, .sourceStr = source}; + auto shaderResult = ShaderCompiler::CompileShader(shaderAttrib); + EXPECT_TRUE(shaderResult) << (shaderResult ? String{} : shaderResult.error().log); + if (!shaderResult) return {}; + + ProgramAttrib programAttrib{.shaders = {shaderResult.value()}}; + auto programResult = ShaderCompiler::LinkProgram(programAttrib); + EXPECT_TRUE(programResult) << (programResult ? String{} : programResult.error().log); + if (!programResult) return {}; + + ProgramBinaryAttrib binaryAttrib{.shaderTypes = {GL_FRAGMENT_SHADER}, + .program = *programResult.value()}; + auto binaryResult = ShaderCompiler::GetSpirvBinaryFromProgram(binaryAttrib); + EXPECT_TRUE(binaryResult) << (binaryResult ? String{} : binaryResult.error().log); + if (!binaryResult || binaryResult->empty()) return {}; + return binaryResult->front(); + } + + bool Validates(const Vector& spirv) { + spvtools::SpirvTools tools(SPV_ENV_VULKAN_1_1); + tools.SetMessageConsumer( + [](spv_message_level_t, const char*, const spv_position_t& position, const char* message) { + ADD_FAILURE() << "spirv-val at word " << position.index << ": " << message; + }); + return tools.Validate(spirv); + } + + // OpTypeImage words: 0 opcode/count, 1 result id, 2 sampled type, 3 Dim, 4 Depth, 5 Arrayed, + // 6 MS, 7 Sampled, 8 Format. Sampled == 2 is a storage image, the only kind with a format. + struct StorageImageType { + Uint32 resultId = 0u; + Uint32 format = 0u; + }; + + Vector CollectStorageImageTypes(const Vector& spirv) { + Vector types; + ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) { + if (opcode != spv::Op::OpTypeImage || wordCount < 9u) return; + if (words[7] != 2u) return; + types.push_back(StorageImageType{words[1], words[8]}); + }); + return types; + } + + // OpVectorShuffle words: 0 opcode/count, 1 result type, 2 result id, 3 vector 1, 4 vector 2, + // 5.. the component selectors. + struct VectorShuffle { + Uint32 resultId = 0u; + Uint32 firstVectorId = 0u; + Uint32 secondVectorId = 0u; + Vector components; + }; + + Vector CollectVectorShuffles(const Vector& spirv) { + Vector shuffles; + ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) { + if (opcode != spv::Op::OpVectorShuffle || wordCount < 5u) return; + VectorShuffle shuffle{}; + shuffle.resultId = words[2]; + shuffle.firstVectorId = words[3]; + shuffle.secondVectorId = words[4]; + for (Uint32 word = 5u; word < wordCount; ++word) { + shuffle.components.push_back(words[word]); + } + shuffles.push_back(shuffle); + }); + return shuffles; + } + + // OpImageWrite words: 0 opcode/count, 1 image, 2 coordinate, 3 texel. + Vector CollectImageWriteTexelIds(const Vector& spirv) { + Vector texels; + ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) { + if (opcode != spv::Op::OpImageWrite || wordCount < 4u) return; + texels.push_back(words[3]); + }); + return texels; + } + + // OpImageRead words: 0 opcode/count, 1 result type, 2 result id, 3 image, 4 coordinate. + Vector CollectImageReadResultIds(const Vector& spirv) { + Vector results; + ForEachInstruction(spirv, [&](spv::Op opcode, const Uint32* words, Uint32 wordCount) { + if (opcode != spv::Op::OpImageRead || wordCount < 5u) return; + results.push_back(words[2]); + }); + return results; + } + + Bool HasComponents(const VectorShuffle& shuffle, const Vector& expected) { + return shuffle.components == expected; + } + + const VectorShuffle* FindShuffleWithResult(const Vector& shuffles, Uint32 resultId) { + for (const VectorShuffle& shuffle : shuffles) { + if (shuffle.resultId == resultId) return &shuffle; + } + return nullptr; + } + + const VectorShuffle* FindShuffleOver(const Vector& shuffles, Uint32 firstVectorId) { + for (const VectorShuffle& shuffle : shuffles) { + if (shuffle.firstVectorId == firstVectorId) return &shuffle; + } + return nullptr; + } + + // rg32f: two float channels, and the entry the four CTS allFormats walkers abort on. Both an + // imageLoad and an imageStore, so both masks are exercised on one image. + const char* const kRg32fLoadStore = R"(#version 430 core +layout(rg32f, binding = 0) uniform image2D img; +out vec4 fragColor; +void main() { + vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy)); + imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0)); + fragColor = texel; +} +)"; + + // r8ui: ONE unsigned-integer channel, and the only format + // KHR-GL43.shader_image_load_store.single-byte_data_alignment declares. SPIRV-Cross refuses to + // print this one for ESSL at all, so before the widening the stage produced no text whatsoever. + const char* const kR8uiLoadStore = R"(#version 430 core +layout(r8ui, binding = 0) uniform uimage2D img; +out vec4 fragColor; +void main() { + uvec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy)); + imageStore(img, ivec2(gl_FragCoord.xy), uvec4(7u, 8u, 9u, 10u)); + fragColor = vec4(texel); +} +)"; + + // rgba32f is one of the thirteen GLSL ES already has; nothing may move. + const char* const kCoreFormatLoadStore = R"(#version 430 core +layout(rgba32f, binding = 0) uniform image2D img; +out vec4 fragColor; +void main() { + vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy)); + imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0)); + fragColor = texel; +} +)"; + + // rg16 is one of the NINE with no core carrier of the same per-channel width. Widening it + // would change the quantisation an application sees, so it must be left alone and keep the + // honest "no GLSL ES spelling" diagnostic instead. + const char* const kRg16LoadStore = R"(#version 430 core +layout(rg16, binding = 0) uniform image2D img; +out vec4 fragColor; +void main() { + vec4 texel = imageLoad(img, ivec2(gl_FragCoord.xy)); + imageStore(img, ivec2(gl_FragCoord.xy), vec4(1.0, 2.0, 3.0, 4.0)); + fragColor = texel; +} +)"; +} // namespace + +// The table itself, which is the single source of truth all three layers of the emulation ask - +// the shader rewrite, the ES texture storage and the glBindImageTexture argument. If it drifts +// the three stop agreeing, and a narrow texture read through a wide image goes out of bounds +// silently on every driver tested. +TEST(WidenImageFormats, SeventeenNonCoreFormatsHaveAnExactSameWidthCarrier) { + struct Case { + Uint requested; + Uint carrier; + Uint channels; + const char* name; + }; + const Case cases[] = { + {0x8230, 0x8814, 2, "GL_RG32F -> GL_RGBA32F"}, + {0x822F, 0x881A, 2, "GL_RG16F -> GL_RGBA16F"}, + {0x822D, 0x881A, 1, "GL_R16F -> GL_RGBA16F"}, + {0x822B, 0x8058, 2, "GL_RG8 -> GL_RGBA8"}, + {0x8229, 0x8058, 1, "GL_R8 -> GL_RGBA8"}, + {0x8F95, 0x8F97, 2, "GL_RG8_SNORM -> GL_RGBA8_SNORM"}, + {0x8F94, 0x8F97, 1, "GL_R8_SNORM -> GL_RGBA8_SNORM"}, + {0x823B, 0x8D82, 2, "GL_RG32I -> GL_RGBA32I"}, + {0x8239, 0x8D88, 2, "GL_RG16I -> GL_RGBA16I"}, + {0x8233, 0x8D88, 1, "GL_R16I -> GL_RGBA16I"}, + {0x8237, 0x8D8E, 2, "GL_RG8I -> GL_RGBA8I"}, + {0x8231, 0x8D8E, 1, "GL_R8I -> GL_RGBA8I"}, + {0x823C, 0x8D70, 2, "GL_RG32UI -> GL_RGBA32UI"}, + {0x823A, 0x8D76, 2, "GL_RG16UI -> GL_RGBA16UI"}, + {0x8234, 0x8D76, 1, "GL_R16UI -> GL_RGBA16UI"}, + {0x8238, 0x8D7C, 2, "GL_RG8UI -> GL_RGBA8UI"}, + {0x8232, 0x8D7C, 1, "GL_R8UI -> GL_RGBA8UI"}, + }; + for (const Case& testCase : cases) { + EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(testCase.requested), testCase.carrier) + << testCase.name; + EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(testCase.requested), testCase.channels) + << testCase.name; + // Every carrier is one of the thirteen ES has in core, or the widening would have moved + // the problem rather than solved it - and every carrier has four channels, or the mask + // selectors would address components that are not there. + EXPECT_TRUE(ShaderCompiler::GLInternalFormatIsCoreEsslImageFormat(testCase.carrier)) + << testCase.name; + EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(testCase.carrier), 4u) << testCase.name; + } +} + +TEST(WidenImageFormats, CoreFormatsAndTheNineWithoutAnExactCarrierAreRefused) { + // The thirteen GLSL ES already has: nothing to carry. + for (const Uint coreFormat : {0x8814u /*RGBA32F*/, 0x881Au /*RGBA16F*/, 0x822Eu /*R32F*/, + 0x8058u /*RGBA8*/, 0x8F97u /*RGBA8_SNORM*/, 0x8D82u /*RGBA32I*/, + 0x8D88u /*RGBA16I*/, 0x8D8Eu /*RGBA8I*/, 0x8235u /*R32I*/, + 0x8D70u /*RGBA32UI*/, 0x8D76u /*RGBA16UI*/, 0x8D7Cu /*RGBA8UI*/, + 0x8236u /*R32UI*/}) { + EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(coreFormat), 0u) + << "core format 0x" << std::hex << coreFormat; + } + // The nine with no core format of the same per-channel width. Carrying these would be an + // approximation - a different quantisation, or a different numeric domain for anything that + // samples the same texture - so they are deliberately left to the honest diagnostic. + for (const Uint hardFormat : {0x8C3Au /*R11F_G11F_B10F*/, 0x8059u /*RGB10_A2*/, + 0x906Fu /*RGB10_A2UI*/, 0x805Bu /*RGBA16*/, 0x822Cu /*RG16*/, + 0x822Au /*R16*/, 0x8F9Bu /*RGBA16_SNORM*/, 0x8F99u /*RG16_SNORM*/, + 0x8F98u /*R16_SNORM*/}) { + EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(hardFormat), 0u) + << "format without an exact carrier 0x" << std::hex << hardFormat; + } + // Not an image format at all. + EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0x8051 /*GL_RGB8*/), 0u); + EXPECT_EQ(ShaderCompiler::ImageFormatChannelCount(0x8051 /*GL_RGB8*/), 0u); + EXPECT_EQ(ShaderCompiler::WidenedCoreEsslImageFormat(0), 0u); +} + +TEST(WidenImageFormats, TwoChannelFloatImageBecomesRgba32fWithBothAccessesMasked) { + const Vector spirv = CompileFragment(kRg32fLoadStore); + ASSERT_FALSE(spirv.empty()); + ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv)); + + Vector widened; + ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*enableSpirvValidation=*/true)); + ASSERT_FALSE(widened.empty()); + EXPECT_TRUE(Validates(widened)); + // ...and there is nothing left for a second run to do. + EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(widened)); + + const auto beforeTypes = CollectStorageImageTypes(spirv); + ASSERT_EQ(beforeTypes.size(), 1u); + EXPECT_EQ(beforeTypes.front().format, static_cast(spv::ImageFormat::Rg32f)); + + const auto afterTypes = CollectStorageImageTypes(widened); + ASSERT_EQ(afterTypes.size(), 1u); + EXPECT_EQ(afterTypes.front().format, static_cast(spv::ImageFormat::Rgba32f)); + + const auto shuffles = CollectVectorShuffles(widened); + + // The STORE. GL drops the components a two-channel format does not have, so the carrier's + // blue and alpha must be written as its own 0 and 1, never as what the shader passed. + const auto texelIds = CollectImageWriteTexelIds(widened); + ASSERT_EQ(texelIds.size(), 1u); + const VectorShuffle* storeMask = FindShuffleWithResult(shuffles, texelIds.front()); + ASSERT_NE(storeMask, nullptr) << "the imageStore texel is not a masked value"; + EXPECT_TRUE(HasComponents(*storeMask, {0u, 1u, 6u, 7u})) + << "expected (r, g, 0, 1) - components 0 and 1 of the texel, then 2 and 3 of (0,0,0,1)"; + + // The LOAD. Same mask, on the other side: GL defines an imageLoad from a two-channel format + // as (r, g, 0, 1) whatever the storage holds, which matters for storage this shader never + // wrote (glTexStorage with no upload leaves the surplus channels undefined). + const auto readIds = CollectImageReadResultIds(widened); + ASSERT_EQ(readIds.size(), 1u); + const VectorShuffle* loadMask = FindShuffleOver(shuffles, readIds.front()); + ASSERT_NE(loadMask, nullptr) << "the imageLoad result is consumed unmasked"; + EXPECT_TRUE(HasComponents(*loadMask, {0u, 1u, 6u, 7u})); + EXPECT_NE(loadMask->resultId, readIds.front()) + << "the mask must be a separate value, or it would feed itself"; +} + +TEST(WidenImageFormats, SingleChannelUnsignedImageBecomesRgba8uiWithBothAccessesMasked) { + const Vector spirv = CompileFragment(kR8uiLoadStore); + ASSERT_FALSE(spirv.empty()); + ASSERT_TRUE(ShaderCompiler::DeclaresWidenableImageFormat(spirv)); + + Vector widened; + ASSERT_TRUE(ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*enableSpirvValidation=*/true)); + ASSERT_FALSE(widened.empty()); + EXPECT_TRUE(Validates(widened)); + + const auto afterTypes = CollectStorageImageTypes(widened); + ASSERT_EQ(afterTypes.size(), 1u); + EXPECT_EQ(afterTypes.front().format, static_cast(spv::ImageFormat::Rgba8ui)); + + const auto shuffles = CollectVectorShuffles(widened); + const auto texelIds = CollectImageWriteTexelIds(widened); + ASSERT_EQ(texelIds.size(), 1u); + const VectorShuffle* storeMask = FindShuffleWithResult(shuffles, texelIds.front()); + ASSERT_NE(storeMask, nullptr); + // Only red survives; green and blue take the constant's zeroes and alpha its one - the + // INTEGER one, not a saturated field, which is what makes the uvec4 constant's fourth + // component 1 rather than 0xFF. + EXPECT_TRUE(HasComponents(*storeMask, {0u, 5u, 6u, 7u})); + + const auto readIds = CollectImageReadResultIds(widened); + ASSERT_EQ(readIds.size(), 1u); + const VectorShuffle* loadMask = FindShuffleOver(shuffles, readIds.front()); + ASSERT_NE(loadMask, nullptr); + EXPECT_TRUE(HasComponents(*loadMask, {0u, 5u, 6u, 7u})); +} + +TEST(WidenImageFormats, CoreFormatModuleIsHandedBackUntouched) { + const Vector spirv = CompileFragment(kCoreFormatLoadStore); + ASSERT_FALSE(spirv.empty()); + // The cheap probe is what keeps every ordinary shader off the optimizer entirely. + EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv)); + + Vector widened; + ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*enableSpirvValidation=*/true); + if (!widened.empty()) { + EXPECT_EQ(CollectVectorShuffles(widened).size(), CollectVectorShuffles(spirv).size()) + << "a core-format module must gain no masks"; + const auto types = CollectStorageImageTypes(widened); + ASSERT_EQ(types.size(), 1u); + EXPECT_EQ(types.front().format, static_cast(spv::ImageFormat::Rgba32f)); + } +} + +TEST(WidenImageFormats, FormatWithoutAnExactCarrierIsLeftAlone) { + const Vector spirv = CompileFragment(kRg16LoadStore); + ASSERT_FALSE(spirv.empty()); + + const auto beforeTypes = CollectStorageImageTypes(spirv); + ASSERT_EQ(beforeTypes.size(), 1u); + EXPECT_EQ(beforeTypes.front().format, static_cast(spv::ImageFormat::Rg16)); + + // rg16 has no core format with 16-bit unsigned-normalized channels behind it. Anything wider + // would requantize differently from what the application asked for, so the pass declines and + // CollectImageFormatBakeInputs reports the format as unspellable instead. + EXPECT_FALSE(ShaderCompiler::DeclaresWidenableImageFormat(spirv)); + + Vector widened; + ShaderCompiler::WidenImageFormatsForEssl(spirv, widened, /*enableSpirvValidation=*/true); + if (!widened.empty()) { + const auto afterTypes = CollectStorageImageTypes(widened); + ASSERT_EQ(afterTypes.size(), 1u); + EXPECT_EQ(afterTypes.front().format, static_cast(spv::ImageFormat::Rg16)); + } +} diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index 8bdfc511..a638602f 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -5212,3 +5212,77 @@ TEST_F(TextureTest, ImageFormatCompatibilityTypeAgreesAcrossEveryTexParameterGet MG_Impl::GLImpl::DeleteTextures(1, &texture); DrainPendingGlErrors(); } + +// The image-format widening's transfer half. GL has forty image formats and GLSL ES core has +// thirteen, so an image-bindable GL_R8UI texture is stored as a GL_RGBA8UI and an image-bindable +// GL_RG32F as a GL_RGBA32F (TextureImpl::GetImageBindableStorageWidening). The driver is then told +// the transfer is four components wide, and one- or two-component client data has to be repacked to +// match - with the SAME values GL gives the channels a narrow format does not have, so that a +// later sample, imageLoad or glGetTexImage cannot tell the carrier from the real thing. +TEST_F(TextureTest, ImageWidenedUploadExpandsOneAndTwoChannelDataWithGLsMissingChannelValues) { + using MobileGL::MG_Backend::DirectGLES::TextureImpl::PrepareChannelWidenedUpload; + + const IntVec3 texelSize(2, 1, 1); + + // GL_RG32F -> GL_RGBA32F. Blue is 0 and alpha 1.0, which is exactly what GL answers for the + // two channels an rg32f image does not have. + { + const Float source[] = {0.25f, -0.5f, 1.5f, -2.5f}; + Vector widened; + const auto* result = static_cast( + PrepareChannelWidenedUpload(2, texelSize, source, sizeof(source), GL_FLOAT, widened, false)); + ASSERT_NE(result, static_cast(source)); + ASSERT_EQ(widened.size(), 8 * sizeof(Float)); + const Float expected[] = {0.25f, -0.5f, 0.0f, 1.0f, 1.5f, -2.5f, 0.0f, 1.0f}; + for (SizeT i = 0; i < 8; ++i) { + EXPECT_FLOAT_EQ(result[i], expected[i]) << "component " << i; + } + } + + // GL_R8UI -> GL_RGBA8UI. Three added channels, and the one in alpha is the INTEGER one: an + // integer format's missing alpha reads back as 1, not as the saturated field a normalized + // format's does, and GL_UNSIGNED_BYTE serves both classes so the type alone cannot decide. + { + const Uint8 source[] = {7, 8}; + Vector widened; + const auto* result = static_cast(PrepareChannelWidenedUpload( + 1, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened, /*integerData=*/true)); + ASSERT_NE(result, static_cast(source)); + const Uint8 expected[] = {7, 0, 0, 1, 8, 0, 0, 1}; + ASSERT_EQ(widened.size(), sizeof(expected)); + EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0); + } + + // GL_R8 -> GL_RGBA8, the normalized twin of the case above: same transfer type, saturated one. + { + const Uint8 source[] = {7, 8}; + Vector widened; + const auto* result = static_cast(PrepareChannelWidenedUpload( + 1, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened, /*integerData=*/false)); + const Uint8 expected[] = {7, 0, 0, 0xFF, 8, 0, 0, 0xFF}; + ASSERT_NE(result, static_cast(source)); + EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0); + } + + // GL_RG8_SNORM -> GL_RGBA8_SNORM keeps GL_BYTE, whose 1.0 is the positive maximum. + { + const Int8 source[] = {-1, 2, 3, -4}; + Vector widened; + const auto* result = static_cast( + PrepareChannelWidenedUpload(2, texelSize, source, sizeof(source), GL_BYTE, widened, false)); + const Int8 expected[] = {-1, 2, 0, 0x7F, 3, -4, 0, 0x7F}; + ASSERT_NE(result, static_cast(source)); + EXPECT_EQ(std::memcmp(result, expected, sizeof(expected)), 0); + } + + // A four-component source is already the carrier's shape: nothing to repack, and the caller's + // sub-rect upload fast path depends on the pointer coming back unchanged when that is so. + { + const Uint8 source[] = {1, 2, 3, 4, 5, 6, 7, 8}; + Vector widened; + EXPECT_EQ(PrepareChannelWidenedUpload(4, texelSize, source, sizeof(source), GL_UNSIGNED_BYTE, widened, + false), + static_cast(source)); + EXPECT_TRUE(widened.empty()); + } +} diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 9c30ed6d..2343fcae 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -34,6 +34,7 @@ #include "SpirvPasses/NormalizeRectCoordinatesPass.h" #include "SpirvPasses/Lower1DArrayImagesPass.h" #include "SpirvPasses/BakeImageFormatsPass.h" +#include "SpirvPasses/WidenImageFormatsPass.h" #include "SpirvPasses/ClampMultisampleFetchPass.h" #include "SpirvPasses/PrivateToEntryLocalPass.h" #include "SpirvPasses/StripUniformLocationsPass.h" @@ -771,6 +772,35 @@ namespace MobileGL { BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat(glInternalFormat)); } + bool ShaderCompiler::WidenImageFormatsForEssl(const Vector& inputBinary, + Vector& outputBinary, + const bool enableSpirvValidation) { + using namespace spvtools; + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(WidenImageFormatsPass::CreateWidenImageFormatsPass()); + // Two image types that differed only in a format the widening collapses - + // `layout(rg32f)` and `layout(rgba32f)` in one module - are one type afterwards, + // and duplicate non-aggregate type declarations are invalid SPIR-V. This joins + // them, and cascades to the pointer and array types that named them; the pass + // itself deliberately does not carry a join of its own. + optimizer.RegisterPass(CreateRemoveDuplicatesPass()); + + return RunOptimizerChecked("WidenImageFormatsForEssl", optimizer, inputBinary, outputBinary, + true, enableSpirvValidation); + } + + bool ShaderCompiler::DeclaresWidenableImageFormat(const Vector& binary) { + return WidenImageFormatsPass::DeclaresWidenableImageFormat(binary); + } + + Uint ShaderCompiler::WidenedCoreEsslImageFormat(Uint glInternalFormat) { + return WidenImageFormatsPass::WidenedCoreEsslImageFormat(glInternalFormat); + } + + Uint ShaderCompiler::ImageFormatChannelCount(Uint glInternalFormat) { + return WidenImageFormatsPass::ImageFormatChannelCount(glInternalFormat); + } + bool ShaderCompiler::FlattenXfbInterfaceBlocksForEssl(const Vector& inputBinary, const std::set& blockNames, std::set& flattenedBlockNames, diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index f9d49220..3557aeff 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -231,6 +231,31 @@ namespace MobileGL { // must not ask BakeImageFormatsForEssl for those, and completes them in the // emitted text instead. static bool SpirvCrossCanPrintEsslImageFormat(Uint glInternalFormat); + // Re-declares every storage image whose DECLARED format GLSL ES cannot spell in + // the core format that carries it exactly, and masks each access back to the + // channels the original format has. The 26 formats outside the ES core set have no + // legal ESSL spelling on any tested driver (none exposes GL_NV_image_formats), and + // a format-less declaration is rejected too, so the stage is otherwise lost + // whatever this backend emits. DirectGLES transpile path only - Vulkan takes the + // declared format natively. See WidenImageFormatsPass for the table, for the nine + // formats it deliberately does NOT widen, and for why the texture storage and the + // glBindImageTexture argument have to move with it. + static bool WidenImageFormatsForEssl(const Vector& inputBinary, + Vector& outputBinary, + bool enableSpirvValidation = false); + // Whether the module declares a storage image WidenImageFormatsForEssl would + // widen. One module parse, so the ~every shader that declares none pays no + // optimizer run. + static bool DeclaresWidenableImageFormat(const Vector& binary); + // The core-ESSL GL internal format that carries `glInternalFormat` exactly, or 0 + // when it needs no widening or cannot be widened exactly. The single source of + // truth for all three layers of the emulation: this one answers the shader, and + // DirectGLES asks it again for the texture storage and the image bind, so the two + // sides cannot drift. + static Uint WidenedCoreEsslImageFormat(Uint glInternalFormat); + // Channels a GL image internal format really has (1-4), 0 when it is not one of + // the forty image formats. + static Uint ImageFormatChannelCount(Uint glInternalFormat); static bool RebaseInstanceIndexForVulkan(const Vector& inputBinary, Vector& outputBinary, bool enableSpirvValidation = false); diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp index aa5bfc51..cd04e94e 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/BakeImageFormatsPass.cpp @@ -8,6 +8,8 @@ #include "BakeImageFormatsPass.h" +#include "WidenImageFormatsPass.h" + #include "spirv.hpp" #include "source/opt/build_module.h" #include "source/opt/def_use_manager.h" @@ -418,7 +420,18 @@ namespace MobileGL { // Those formats are completed in the emitted text instead (see // PrgramImpl::BakeImageFormatQualifiers); the module is left format-less for // them, which is exactly the state that pass looks for. - if (!IsSpirvCrossEsslPrintableFormat(static_cast(format))) continue; + // + // UNLESS the format widens exactly: WidenImageFormatsPass runs immediately + // after this one on the ESSL chain and rewrites it to a core four-channel + // carrier SPIRV-Cross does print, masking the accesses back to the channels + // the baked format has. So for those the module IS the right place, and + // routing them to the text completion instead would spell the narrow format + // the driver rejects. The two lists are asked in this order because + // printability is the cheaper and more common answer. + if (!IsSpirvCrossEsslPrintableFormat(static_cast(format)) && + WidenImageFormatsPass::WidenedCoreEsslImageFormat(formatIt->second) == 0) { + continue; + } // spirv-val: "Expected Image Format to match Sampled Type". A bind format // whose class disagrees with the declaration is an application error GL // leaves undefined; baking it would turn that into an invalid module, so it diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp new file mode 100644 index 00000000..07798e8d --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp @@ -0,0 +1,540 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.cpp +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#include "WidenImageFormatsPass.h" + +#include "spirv.hpp" +#include "source/opt/build_module.h" +#include "source/opt/constants.h" +#include "source/opt/def_use_manager.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_context.h" +#include "source/opt/module.h" +#include "source/opt/type_manager.h" +#include "source/opt/types.h" +#include "source/util/make_unique.h" + +#include +#include +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + using spvtools::opt::Operand; + namespace analysis = spvtools::opt::analysis; + + // OpTypeImage in-operands: 0 sampled type, 1 Dim, 2 Depth, 3 Arrayed, 4 MS, + // 5 Sampled, 6 Format. + constexpr uint32_t kImageSampledTypeOperand = 0; + constexpr uint32_t kImageSampledOperand = 5; + constexpr uint32_t kImageFormatOperand = 6; + // A storage image, i.e. one reached through imageLoad/imageStore rather than a + // sampler. The only kind that carries a format qualifier in any GLSL dialect. + constexpr uint32_t kSampledStorageImage = 2; + + // OpImageRead in-operands: 0 image, 1 coordinate, 2.. optional image operands. + // OpImageWrite in-operands: 0 image, 1 coordinate, 2 texel, 3.. optional. + constexpr uint32_t kImageAccessImageOperand = 0; + constexpr uint32_t kImageWriteTexelOperand = 2; + + // The exact carrier of a non-core image format: the core GLSL ES format with the + // SAME component type and the SAME per-channel width, differing only in channel + // count. `channels` is what the original format really has, which is what every + // access through the carrier is masked back to. + // + // Only formats that widen EXACTLY appear here. r11f_g11f_b10f, rgb10_a2, + // rgb10_a2ui, rgba16, rg16, r16, rgba16_snorm, rg16_snorm and r16_snorm have no + // same-width core carrier - every candidate is either lossy or changes the numeric + // domain a sampler would read - and are deliberately absent, so they keep the + // honest "no GLSL ES spelling" diagnostic rather than a silent approximation. + struct ImageFormatWidening { + spv::ImageFormat Carrier = spv::ImageFormat::Unknown; + uint32_t Channels = 0; + + explicit operator bool() const { return Carrier != spv::ImageFormat::Unknown; } + }; + + ImageFormatWidening WideningOfSpirvImageFormat(spv::ImageFormat format) { + switch (format) { + // Float. + case spv::ImageFormat::Rg32f: return {spv::ImageFormat::Rgba32f, 2}; + case spv::ImageFormat::Rg16f: return {spv::ImageFormat::Rgba16f, 2}; + case spv::ImageFormat::R16f: return {spv::ImageFormat::Rgba16f, 1}; + // Unsigned normalized. + case spv::ImageFormat::Rg8: return {spv::ImageFormat::Rgba8, 2}; + case spv::ImageFormat::R8: return {spv::ImageFormat::Rgba8, 1}; + // Signed normalized. + case spv::ImageFormat::Rg8Snorm: return {spv::ImageFormat::Rgba8Snorm, 2}; + case spv::ImageFormat::R8Snorm: return {spv::ImageFormat::Rgba8Snorm, 1}; + // Signed integer. + case spv::ImageFormat::Rg32i: return {spv::ImageFormat::Rgba32i, 2}; + case spv::ImageFormat::Rg16i: return {spv::ImageFormat::Rgba16i, 2}; + case spv::ImageFormat::R16i: return {spv::ImageFormat::Rgba16i, 1}; + case spv::ImageFormat::Rg8i: return {spv::ImageFormat::Rgba8i, 2}; + case spv::ImageFormat::R8i: return {spv::ImageFormat::Rgba8i, 1}; + // Unsigned integer. + case spv::ImageFormat::Rg32ui: return {spv::ImageFormat::Rgba32ui, 2}; + case spv::ImageFormat::Rg16ui: return {spv::ImageFormat::Rgba16ui, 2}; + case spv::ImageFormat::R16ui: return {spv::ImageFormat::Rgba16ui, 1}; + case spv::ImageFormat::Rg8ui: return {spv::ImageFormat::Rgba8ui, 2}; + case spv::ImageFormat::R8ui: return {spv::ImageFormat::Rgba8ui, 1}; + default: + return {}; + } + } + + // The GL 4.2 image format table (core spec table 8.26) as SPIR-V ImageFormats. + // Written as literals rather than through the GL headers because this lives in + // MG_Util, which the GL frontend's enums do not reach; the same list, in the same + // order, as BakeImageFormatsPass::SpirvImageFormatFromGLInternalFormat. + spv::ImageFormat SpirvImageFormatOfGL(Uint glInternalFormat) { + switch (glInternalFormat) { + case 0x8814: /*GL_RGBA32F*/ return spv::ImageFormat::Rgba32f; + case 0x881A: /*GL_RGBA16F*/ return spv::ImageFormat::Rgba16f; + case 0x8230: /*GL_RG32F*/ return spv::ImageFormat::Rg32f; + case 0x822F: /*GL_RG16F*/ return spv::ImageFormat::Rg16f; + case 0x8C3A: /*GL_R11F_G11F_B10F*/ return spv::ImageFormat::R11fG11fB10f; + case 0x822E: /*GL_R32F*/ return spv::ImageFormat::R32f; + case 0x822D: /*GL_R16F*/ return spv::ImageFormat::R16f; + case 0x8D70: /*GL_RGBA32UI*/ return spv::ImageFormat::Rgba32ui; + case 0x8D76: /*GL_RGBA16UI*/ return spv::ImageFormat::Rgba16ui; + case 0x8D7C: /*GL_RGBA8UI*/ return spv::ImageFormat::Rgba8ui; + case 0x906F: /*GL_RGB10_A2UI*/ return spv::ImageFormat::Rgb10a2ui; + case 0x823C: /*GL_RG32UI*/ return spv::ImageFormat::Rg32ui; + case 0x823A: /*GL_RG16UI*/ return spv::ImageFormat::Rg16ui; + case 0x8238: /*GL_RG8UI*/ return spv::ImageFormat::Rg8ui; + case 0x8236: /*GL_R32UI*/ return spv::ImageFormat::R32ui; + case 0x8234: /*GL_R16UI*/ return spv::ImageFormat::R16ui; + case 0x8232: /*GL_R8UI*/ return spv::ImageFormat::R8ui; + case 0x8D82: /*GL_RGBA32I*/ return spv::ImageFormat::Rgba32i; + case 0x8D88: /*GL_RGBA16I*/ return spv::ImageFormat::Rgba16i; + case 0x8D8E: /*GL_RGBA8I*/ return spv::ImageFormat::Rgba8i; + case 0x823B: /*GL_RG32I*/ return spv::ImageFormat::Rg32i; + case 0x8239: /*GL_RG16I*/ return spv::ImageFormat::Rg16i; + case 0x8237: /*GL_RG8I*/ return spv::ImageFormat::Rg8i; + case 0x8235: /*GL_R32I*/ return spv::ImageFormat::R32i; + case 0x8233: /*GL_R16I*/ return spv::ImageFormat::R16i; + case 0x8231: /*GL_R8I*/ return spv::ImageFormat::R8i; + case 0x8058: /*GL_RGBA8*/ return spv::ImageFormat::Rgba8; + case 0x805B: /*GL_RGBA16*/ return spv::ImageFormat::Rgba16; + case 0x8059: /*GL_RGB10_A2*/ return spv::ImageFormat::Rgb10A2; + case 0x822B: /*GL_RG8*/ return spv::ImageFormat::Rg8; + case 0x822C: /*GL_RG16*/ return spv::ImageFormat::Rg16; + case 0x8229: /*GL_R8*/ return spv::ImageFormat::R8; + case 0x822A: /*GL_R16*/ return spv::ImageFormat::R16; + case 0x8F97: /*GL_RGBA8_SNORM*/ return spv::ImageFormat::Rgba8Snorm; + case 0x8F9B: /*GL_RGBA16_SNORM*/ return spv::ImageFormat::Rgba16Snorm; + case 0x8F95: /*GL_RG8_SNORM*/ return spv::ImageFormat::Rg8Snorm; + case 0x8F99: /*GL_RG16_SNORM*/ return spv::ImageFormat::Rg16Snorm; + case 0x8F94: /*GL_R8_SNORM*/ return spv::ImageFormat::R8Snorm; + case 0x8F98: /*GL_R16_SNORM*/ return spv::ImageFormat::R16Snorm; + default: + return spv::ImageFormat::Unknown; + } + } + + Uint GLInternalFormatOfSpirvImageFormat(spv::ImageFormat format) { + switch (format) { + case spv::ImageFormat::Rgba32f: return 0x8814; // GL_RGBA32F + case spv::ImageFormat::Rgba16f: return 0x881A; // GL_RGBA16F + case spv::ImageFormat::Rgba8: return 0x8058; // GL_RGBA8 + case spv::ImageFormat::Rgba8Snorm: return 0x8F97; // GL_RGBA8_SNORM + case spv::ImageFormat::Rgba32i: return 0x8D82; // GL_RGBA32I + case spv::ImageFormat::Rgba16i: return 0x8D88; // GL_RGBA16I + case spv::ImageFormat::Rgba8i: return 0x8D8E; // GL_RGBA8I + case spv::ImageFormat::Rgba32ui: return 0x8D70; // GL_RGBA32UI + case spv::ImageFormat::Rgba16ui: return 0x8D76; // GL_RGBA16UI + case spv::ImageFormat::Rgba8ui: return 0x8D7C; // GL_RGBA8UI + default: + // Only the carriers need the reverse direction, and every carrier is one + // of the four-channel core formats above. + return 0; + } + } + + uint32_t ChannelsOfSpirvImageFormat(spv::ImageFormat format) { + switch (format) { + case spv::ImageFormat::R32f: + case spv::ImageFormat::R16f: + case spv::ImageFormat::R16: + case spv::ImageFormat::R8: + case spv::ImageFormat::R16Snorm: + case spv::ImageFormat::R8Snorm: + case spv::ImageFormat::R32i: + case spv::ImageFormat::R16i: + case spv::ImageFormat::R8i: + case spv::ImageFormat::R32ui: + case spv::ImageFormat::R16ui: + case spv::ImageFormat::R8ui: + return 1; + case spv::ImageFormat::Rg32f: + case spv::ImageFormat::Rg16f: + case spv::ImageFormat::Rg16: + case spv::ImageFormat::Rg8: + case spv::ImageFormat::Rg16Snorm: + case spv::ImageFormat::Rg8Snorm: + case spv::ImageFormat::Rg32i: + case spv::ImageFormat::Rg16i: + case spv::ImageFormat::Rg8i: + case spv::ImageFormat::Rg32ui: + case spv::ImageFormat::Rg16ui: + case spv::ImageFormat::Rg8ui: + return 2; + case spv::ImageFormat::R11fG11fB10f: + return 3; + case spv::ImageFormat::Rgba32f: + case spv::ImageFormat::Rgba16f: + case spv::ImageFormat::Rgba16: + case spv::ImageFormat::Rgb10A2: + case spv::ImageFormat::Rgba8: + case spv::ImageFormat::Rgba16Snorm: + case spv::ImageFormat::Rgba8Snorm: + case spv::ImageFormat::Rgba32i: + case spv::ImageFormat::Rgba16i: + case spv::ImageFormat::Rgba8i: + case spv::ImageFormat::Rgba32ui: + case spv::ImageFormat::Rgba16ui: + case spv::ImageFormat::Rgba8ui: + case spv::ImageFormat::Rgb10a2ui: + return 4; + default: + return 0; + } + } + + Bool IsWidenableStorageImageType(const Instruction* type) { + if (type == nullptr || type->opcode() != spv::Op::OpTypeImage) return false; + if (type->GetSingleWordInOperand(kImageSampledOperand) != kSampledStorageImage) return false; + const auto format = + static_cast(type->GetSingleWordInOperand(kImageFormatOperand)); + return static_cast(WideningOfSpirvImageFormat(format)); + } + } // namespace + + Uint WidenImageFormatsPass::WidenedCoreEsslImageFormat(Uint glInternalFormat) { + const ImageFormatWidening widening = + WideningOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat)); + if (!widening) return 0; + return GLInternalFormatOfSpirvImageFormat(widening.Carrier); + } + + Uint WidenImageFormatsPass::ImageFormatChannelCount(Uint glInternalFormat) { + return ChannelsOfSpirvImageFormat(SpirvImageFormatOfGL(glInternalFormat)); + } + + bool WidenImageFormatsPass::DeclaresWidenableImageFormat(const Vector& binary) { + std::unique_ptr context = spvtools::BuildModule( + SPV_ENV_VULKAN_1_1, [](spv_message_level_t, const char*, const spv_position_t&, const char*) {}, + binary.data(), binary.size()); + if (!context) { + return false; + } + for (const Instruction& type : context->module()->types_values()) { + if (IsWidenableStorageImageType(&type)) { + return true; + } + } + return false; + } + + spvtools::opt::Pass::Status WidenImageFormatsPass::Process() { + auto* irContext = context(); + auto* defUseMgr = irContext->get_def_use_mgr(); + + // Cheap gate first: no widenable image type, and the module is handed back + // byte-identical - which is every shader but a handful. + std::vector imageTypes; + for (Instruction& type : irContext->types_values()) { + if (IsWidenableStorageImageType(&type)) { + imageTypes.push_back(&type); + } + } + if (imageTypes.empty()) { + return Status::SuccessWithoutChange; + } + + // What each widenable image type becomes, and the mask its accesses take. Keyed on + // the type's result id so the access walk below can ask about an image VALUE by + // its type without re-deriving anything. + struct WidenedImage { + spv::ImageFormat Carrier = spv::ImageFormat::Unknown; + uint32_t Channels = 0; + uint32_t SampledTypeId = 0; + }; + std::map widenedByTypeId; + for (Instruction* type : imageTypes) { + const auto format = + static_cast(type->GetSingleWordInOperand(kImageFormatOperand)); + const ImageFormatWidening widening = WideningOfSpirvImageFormat(format); + widenedByTypeId.emplace(type->result_id(), + WidenedImage{widening.Carrier, widening.Channels, + type->GetSingleWordInOperand(kImageSampledTypeOperand)}); + } + + // Collect the accesses BEFORE anything is mutated, and refuse the whole rewrite if + // any of them is a shape this pass cannot mask end to end. A widened declaration + // whose accesses were left unmasked is worse than the compile error it replaced: + // the shader runs and quietly reads the carrier's surplus channels, which GL says + // are 0 and 1. Refusing hands the stage back to the "no GLSL ES spelling" + // diagnostic instead, which at least names the failure. + std::vector reads; + std::vector writes; + Bool rewritable = true; + for (auto funcIt = irContext->module()->begin(); + funcIt != irContext->module()->end() && rewritable; ++funcIt) { + funcIt->ForEachInst([&](Instruction* inst) { + if (!rewritable) return; + switch (inst->opcode()) { + case spv::Op::OpImageRead: + case spv::Op::OpImageWrite: + case spv::Op::OpImageSparseRead: + case spv::Op::OpImageTexelPointer: + break; + default: + return; + } + // OpImageTexelPointer names the image VARIABLE (a pointer), the other + // three an image VALUE; both reach the OpTypeImage through the def's + // type, one hop further for the pointer. + const Instruction* imageDef = + defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand)); + if (imageDef == nullptr) return; + uint32_t imageTypeId = imageDef->type_id(); + if (const Instruction* imageType = defUseMgr->GetDef(imageTypeId); + imageType != nullptr && imageType->opcode() == spv::Op::OpTypePointer) { + imageTypeId = imageType->GetSingleWordInOperand(1); + } + const auto widenedIt = widenedByTypeId.find(imageTypeId); + if (widenedIt == widenedByTypeId.end()) return; + + if (inst->opcode() == spv::Op::OpImageRead) { + reads.push_back(inst); + return; + } + if (inst->opcode() == spv::Op::OpImageWrite) { + writes.push_back(inst); + return; + } + // OpImageSparseRead yields a struct rather than a plain texel vector, and + // OpImageTexelPointer is an image atomic - which spirv-val already + // restricts to r32i/r32ui/r32f, all three of them core formats that never + // reach this table. Neither is expressible in the ESSL this backend emits, + // so rather than mask a shape that has never been seen, decline. + rewritable = false; + }); + } + if (!rewritable) { + return Status::SuccessWithoutChange; + } + + // The four-component (0, .., 0, 1) constant each mask shuffles its surplus + // channels out of, one per component type in play. GL defines an imageLoad from a + // format with fewer than four channels as (r, 0, 0, 1) and an imageStore as + // dropping the components the format does not have, so pinning the carrier's + // surplus channels to exactly these values is the whole of the emulation. + std::map zeroOneConstantBySampledType; // sampled type id -> constant id + std::map vec4TypeBySampledType; // sampled type id -> v4 type id + auto resolveMaskMaterial = [&](uint32_t sampledTypeId, uint32_t& outConstantId, + uint32_t& outVec4TypeId) -> Bool { + if (const auto cached = zeroOneConstantBySampledType.find(sampledTypeId); + cached != zeroOneConstantBySampledType.end()) { + outConstantId = cached->second; + outVec4TypeId = vec4TypeBySampledType[sampledTypeId]; + return outConstantId != 0 && outVec4TypeId != 0; + } + const Instruction* sampledType = defUseMgr->GetDef(sampledTypeId); + if (sampledType == nullptr) return false; + + uint32_t oneWord = 0; + std::unique_ptr component; + if (sampledType->opcode() == spv::Op::OpTypeFloat && + sampledType->GetSingleWordInOperand(0) == 32) { + component = spvtools::MakeUnique(32); + oneWord = 0x3F800000u; // 1.0f + } else if (sampledType->opcode() == spv::Op::OpTypeInt && + sampledType->GetSingleWordInOperand(0) == 32) { + // OpTypeInt in-operands: 0 width, 1 signedness. + component = spvtools::MakeUnique( + 32, sampledType->GetSingleWordInOperand(1) != 0); + oneWord = 1u; + } else { + return false; + } + + auto* typeMgr = irContext->get_type_mgr(); + auto* constantMgr = irContext->get_constant_mgr(); + analysis::Type* componentReg = typeMgr->GetRegisteredType(component.get()); + if (componentReg == nullptr) return false; + const analysis::Constant* zero = constantMgr->GetConstant(componentReg, {0u}); + const analysis::Constant* one = constantMgr->GetConstant(componentReg, {oneWord}); + if (zero == nullptr || one == nullptr) return false; + const Instruction* zeroInst = constantMgr->GetDefiningInstruction(zero); + const Instruction* oneInst = constantMgr->GetDefiningInstruction(one); + if (zeroInst == nullptr || oneInst == nullptr) return false; + + analysis::Vector vector(componentReg, 4); + const uint32_t vec4TypeId = typeMgr->GetTypeInstruction(&vector); + if (vec4TypeId == 0) return false; + // Through the id rather than through GetRegisteredType(&vector): the + // instruction the line above declared (or found) is the one the constant has + // to be typed by, and asking the manager for its type is what guarantees the + // two are the same registered object. + analysis::Type* vectorReg = typeMgr->GetType(vec4TypeId); + if (vectorReg == nullptr) return false; + // A vector constant's "literal words" are the IDS of its components + // (ConstantManager::CreateConstant -> GetConstantsFromIds). + const analysis::Constant* zeroOne = constantMgr->GetConstant( + vectorReg, {zeroInst->result_id(), zeroInst->result_id(), zeroInst->result_id(), + oneInst->result_id()}); + if (zeroOne == nullptr) return false; + const Instruction* zeroOneInst = constantMgr->GetDefiningInstruction(zeroOne); + if (zeroOneInst == nullptr) return false; + + outConstantId = zeroOneInst->result_id(); + outVec4TypeId = vec4TypeId; + zeroOneConstantBySampledType.emplace(sampledTypeId, outConstantId); + vec4TypeBySampledType.emplace(sampledTypeId, outVec4TypeId); + return true; + }; + + // OpVectorShuffle selects components 0-3 from the first vector and 4-7 from the + // second, so with (0, 0, 0, 1) as the second operand the mask for a `channels`- + // channel format is [0 .. channels-1] followed by 4 + i for the rest: the surplus + // channels take the constant's 0s and, at index 3, its 1. + auto maskComponents = [](uint32_t channels) { + std::vector components; + components.reserve(4); + for (uint32_t i = 0; i < 4; ++i) { + components.push_back( + {SPV_OPERAND_TYPE_LITERAL_INTEGER, {i < channels ? i : 4u + i}}); + } + return components; + }; + + auto widenedOf = [&](const Instruction* inst) -> const WidenedImage* { + const Instruction* imageDef = + defUseMgr->GetDef(inst->GetSingleWordInOperand(kImageAccessImageOperand)); + if (imageDef == nullptr) return nullptr; + const auto it = widenedByTypeId.find(imageDef->type_id()); + return it == widenedByTypeId.end() ? nullptr : &it->second; + }; + + // Every constant and vector type the masks will need, declared BEFORE the first + // instruction is inserted. The constant and type managers append to the module's + // globals and keep their own def-use bookkeeping straight; the shuffles below do + // not (this pass invalidates every analysis at the end instead), so doing the two + // in the other order would have the managers consult a def-use map that no longer + // describes the function bodies. + for (const auto& widened : widenedByTypeId) { + uint32_t unusedConstantId = 0; + uint32_t unusedVec4TypeId = 0; + if (!resolveMaskMaterial(widened.second.SampledTypeId, unusedConstantId, unusedVec4TypeId)) { + return Status::SuccessWithoutChange; + } + } + + // Masks first, while every image type still carries its ORIGINAL format: the + // rewrite below only touches the format operand, so the accesses' types do not + // move and the order is free either way - but doing it first keeps a failed + // resolve from leaving a half-widened module behind. + for (Instruction* write : writes) { + const WidenedImage* widened = widenedOf(write); + if (widened == nullptr) continue; + uint32_t zeroOneId = 0; + uint32_t vec4TypeId = 0; + if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) { + return Status::SuccessWithoutChange; + } + const uint32_t texelId = write->GetSingleWordInOperand(kImageWriteTexelOperand); + const Instruction* texel = defUseMgr->GetDef(texelId); + // SPIR-V allows a scalar texel; GLSL's imageStore always passes a gvec4, and a + // shape this has never seen is refused rather than guessed at. + if (texel == nullptr || texel->type_id() != vec4TypeId) { + return Status::SuccessWithoutChange; + } + const uint32_t maskedId = irContext->TakeNextId(); + if (maskedId == 0) return Status::Failure; + Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {texelId}}, + {SPV_OPERAND_TYPE_ID, {zeroOneId}}}; + for (const Operand& component : maskComponents(widened->Channels)) { + shuffleOperands.push_back(component); + } + write->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpVectorShuffle, vec4TypeId, maskedId, shuffleOperands)); + write->SetInOperand(kImageWriteTexelOperand, {maskedId}); + } + + for (Instruction* read : reads) { + const WidenedImage* widened = widenedOf(read); + if (widened == nullptr) continue; + uint32_t zeroOneId = 0; + uint32_t vec4TypeId = 0; + if (!resolveMaskMaterial(widened->SampledTypeId, zeroOneId, vec4TypeId)) { + return Status::SuccessWithoutChange; + } + if (read->type_id() != vec4TypeId) { + return Status::SuccessWithoutChange; + } + // The ORIGINAL instruction keeps its result id and becomes the shuffle, and a + // copy of the read is inserted in front of it under a fresh id. That way every + // existing use of the read stays intact without a ReplaceAllUsesWith that + // would also rewrite the shuffle's own operand (the idiom + // EmulateNoPerspectivePass uses for the same reason). + const uint32_t rawReadId = irContext->TakeNextId(); + if (rawReadId == 0) return Status::Failure; + Instruction::OperandList readOperands; + for (uint32_t i = 0; i < read->NumInOperands(); ++i) { + readOperands.push_back(read->GetInOperand(i)); + } + read->InsertBefore(spvtools::MakeUnique( + irContext, spv::Op::OpImageRead, vec4TypeId, rawReadId, readOperands)); + read->SetOpcode(spv::Op::OpVectorShuffle); + Instruction::OperandList shuffleOperands{{SPV_OPERAND_TYPE_ID, {rawReadId}}, + {SPV_OPERAND_TYPE_ID, {zeroOneId}}}; + for (const Operand& component : maskComponents(widened->Channels)) { + shuffleOperands.push_back(component); + } + read->SetInOperands(Move(shuffleOperands)); + } + + // The declaration itself, last. Only the format operand moves: the carrier has the + // same component type as the original by construction, so the OpTypeImage's + // Sampled Type still agrees with it (which is what spirv-val checks) and no + // pointer, array or access-chain type has to be rebuilt. + // + // Two image types can COLLIDE here - `layout(rg32f)` and `layout(rgba32f)` in one + // module both become Rgba32f - and duplicate non-aggregate type declarations are + // invalid SPIR-V. The caller runs spirv-tools' RemoveDuplicates pass immediately + // after this one, which joins them (and cascades to the pointer and array types + // that named them) rather than this pass carrying its own join. + for (Instruction* type : imageTypes) { + const auto widenedIt = widenedByTypeId.find(type->result_id()); + if (widenedIt == widenedByTypeId.end()) continue; + type->SetInOperand(kImageFormatOperand, {static_cast(widenedIt->second.Carrier)}); + defUseMgr->AnalyzeInstUse(type); + } + + // StorageImageExtendedFormats is deliberately left declared even though every + // remaining format is now one of the thirteen that need no capability: a + // capability a module no longer exercises is valid SPIR-V, and dropping one is + // only safe after proving no extended format is left ANYWHERE, including in image + // types this pass declined. + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken WidenImageFormatsPass::CreateWidenImageFormatsPass() { + return spvtools::Optimizer::PassToken(spvtools::MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h new file mode 100644 index 00000000..aa280553 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h @@ -0,0 +1,101 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/WidenImageFormatsPass.h +// Copyright (c) 2025-2026 MobileGL-Dev +// Licensed under the GNU Lesser General Public License v3.0: +// https://www.gnu.org/licenses/gpl-3.0.txt +// https://www.gnu.org/licenses/lgpl-3.0.txt +// SPDX-License-Identifier: LGPL-3.0-only +// End of Source File Header + +#pragma once + +#include "spirv-tools/optimizer.hpp" +#include "source/opt/pass.h" + +#include + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // Emulates the desktop-GL image formats GLSL ES cannot spell by CHANNEL WIDENING: a + // storage image DECLARED `layout(rg32f)` is re-declared `layout(rgba32f)` and every + // access through it is masked back to the two channels GL says it has. + // + // WHY IT IS NEEDED AT ALL. GL 4.2 has forty image formats; GLSL ES 3.1 has thirteen, + // and GL_NV_image_formats - the only extension that adds the rest - is advertised by + // none of Adreno 830, Mali-G1-Ultra MC12 or Mali-G925-Immortalis MC12 (probed on all + // three, with `#extension ... : enable` also rejected, so "the driver implements it + // unadvertised" is refuted rather than assumed). A shader that declares one of the + // other twenty-six therefore has NO legal ESSL spelling, and it fails in one of two + // ways: SPIRV-Cross throws for its is_desktop_only_format set and no text is produced + // at all, or the token reaches the driver and is rejected ("'rg32f' : not a legal + // layout qualifier id"). Either way the stage is lost, the backend program is + // unusable, and every draw with it silently renders nothing while the frontend keeps + // reporting GL_LINK_STATUS = TRUE. Dropping the qualifier instead is not an escape: + // all three drivers reject a format-LESS image declaration outright ("all images have + // to define layout format" / "S0001: Image must specify a format layout qualifier"), + // readonly and writeonly alike, at both #version 310 es and 320 es. And unlike a + // numeric limit there is nothing honest to report either - GL has no "this image + // format is unsupported" query - so the format has to be emulated. + // + // WHAT WIDENING MEANS. Seventeen of the twenty-six have a core ESSL format of the + // SAME PER-CHANNEL WIDTH AND COMPONENT TYPE, differing only in channel count + // (rg32f -> rgba32f, r8ui -> rgba8ui, rg8_snorm -> rgba8_snorm, ...). Carried in one + // of those the emulation is EXACT, not approximate: every value is representable bit + // for bit, and GL's own image semantics do the rest - + // + // * imageLoad on a format with fewer than four channels returns (r, 0, 0, 1); + // * imageStore drops the components the format does not have. + // + // so the two surplus channels of the carrier are not free storage, they are values GL + // already defines. This pass pins them: every OpImageWrite through a widened image has + // its texel replaced by (r[, g[, b]], 0.., 1) and every OpImageRead has its result + // masked the same way. Masking BOTH is deliberate belt and braces - the write mask + // alone keeps the storage canonical for a sampler and for glGetTexImage, the read mask + // alone survives storage this shader never wrote (glTexStorage with no upload, whose + // surplus channels are undefined). + // + // The other NINE (r11f_g11f_b10f, rgb10_a2, rgb10_a2ui, rgba16, rg16, r16, + // rgba16_snorm, rg16_snorm, r16_snorm) have NO same-width core carrier and are + // deliberately NOT widened here: every carrier for them is either lossy or changes the + // numeric domain of the texture a `sampler2D` would read from it. They keep the honest + // "no GLSL ES spelling" diagnostic instead of silently changing an application's + // quantisation behaviour. + // + // MUST MOVE WITH THE OTHER TWO LAYERS. The widening is not a shader-local rewrite: the + // ES texture behind the image has to be allocated in the carrier format too, and + // glBindImageTexture has to be handed the carrier (on Adreno the bind of the narrow + // format is GL_INVALID_VALUE for nineteen of the twenty-six, and on both Malis for + // twenty-five). Both are done in DirectGLES against the same table below, so the two + // sides agree by construction rather than by convention. Binding a narrow texture + // through a wide image is NOT an option: every tested driver accepts it silently, so + // it reads and writes out of bounds undetected. + // + // ESSL ONLY. DirectVulkan takes the declared format natively and resolves the view + // format from the same bind state, so the module must reach it unchanged. + class WidenImageFormatsPass final : public spvtools::opt::Pass { + public: + const char* name() const override { return "mobilegl-widen-image-formats"; } + Status Process() override; + + // Whether the module declares a storage image whose format this pass would widen, + // i.e. whether running it could change anything. Answered from a single parse so + // the caller can skip the optimizer run entirely - which is every shader but a + // handful. + static bool DeclaresWidenableImageFormat(const Vector& binary); + + // The core-ESSL GL internal format that carries `glInternalFormat` exactly, or 0 + // when the format needs no widening (it is core already) or cannot be widened + // exactly (the nine above, and anything that is not an image format at all). + // Used by DirectGLES for the texture storage and the glBindImageTexture argument, + // so that all three layers pick the same carrier. + static Uint WidenedCoreEsslImageFormat(Uint glInternalFormat); + + // Channels the GL internal format really has (1-4), or 0 when it is not one of the + // forty image formats. The count the widened accesses are masked back to. + static Uint ImageFormatChannelCount(Uint glInternalFormat); + + static spvtools::Optimizer::PassToken CreateWidenImageFormatsPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp index 8a9f0bb6..00c58c50 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.cpp @@ -162,6 +162,7 @@ namespace MobileGL::MG_Util::ShaderTranspiler { builder.Value(static_cast(inputs.shaderType)); builder.Value(static_cast(inputs.supportsViewportArray)); builder.Value(static_cast(inputs.supportsNoperspectiveInterpolation)); + builder.Value(static_cast(inputs.supportsExtendedImageFormats)); builder.Value(inputs.maxColorTextureSamples); builder.Value(inputs.maxIntegerSamples); builder.Value(inputs.maxDepthTextureSamples); diff --git a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h index 980ddb6b..b371e8f0 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h +++ b/MobileGL/MG_Util/ShaderTranspiler/TranslationCache.h @@ -602,6 +602,11 @@ namespace MobileGL::MG_Util::ShaderTranspiler { // --- driver capability bits that arm or steer a pass --- Bool supportsViewportArray = false; Bool supportsNoperspectiveInterpolation = false; + // GL_NV_image_formats. Arms WidenImageFormatsForEssl, which re-declares every storage + // image whose format GLSL ES core cannot spell in the core format that carries it and + // masks its accesses back - so a driver that HAS the extension and one that does not get + // materially different ESSL from the same module. + Bool supportsExtendedImageFormats = false; Int32 maxColorTextureSamples = 0; Int32 maxIntegerSamples = 0; Int32 maxDepthTextureSamples = 0;