From 28d0af6f04f690c3f754e7b1f3fb20769095be18 Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Tue, 4 Aug 2026 13:25:40 -0400 Subject: [PATCH] [Feat] (MG_Util, DirectGLES, DirectVulkan): normalize rectangle coordinates in the module Neither target API has GL_TEXTURE_RECTANGLE: ESSL has no rectangle sampler, and Vulkan's SPIR-V environment does not allow Dim::Rect. Both emulate it on a plain 2D texture, and the two differ in exactly one way - a rectangle lookup addresses texels where a 2D one addresses [0,1]. That one difference now lives in one SPIR-V pass, so neither backend has to know about it: every lookup taking normalized coordinates gets its coordinate divided by the size the texture reports, and the image type is then rewritten to 2D. Magma had no rectangle handling at all - it fed Dim::Rect straight to Vulkan, which read the texel coordinates as normalized and sampled the edge, so all fifteen KHR-GL40.texture_gather.*-2drect cases came back holding the clear colour. This replaces the ESSL text rewrite that did the same divide for DirectGLES only. Doing it in the module instead is both shorter and stricter: the pass resolves an operation's image type through the sampled-image and pointer wrappers rather than matching a sampler name in generated source, so it cannot be fooled by an expression where it expected an identifier, and it needs no help from the frontend reflection to know which samplers were rectangles. Still declined, as before: the Dref *sample* forms, whose coordinate carries the compare value in its last component, and the projective ones, where the divide would have to happen after the perspective divide. texelFetch is deliberately untouched - integer texel coordinates mean the same thing on both targets. KHR-GL40.texture_gather: Magma 66 failures -> 2, Espryt stays at 75/75. --- CMakeLists.txt | 1 + MobileGL/MG_Backend/DirectGLES/Managers.cpp | 33 +-- MobileGL/MG_Backend/DirectGLES/Managers.h | 2 +- MobileGL/MG_Backend/DirectGLES/Utils.cpp | 60 ------ MobileGL/MG_Backend/DirectGLES/Utils.h | 9 - .../DirectVulkan/Renderer/ProgramFactory.cpp | 11 + MobileGL/MG_Test/Texture/TextureTest.cpp | 2 +- .../ShaderTranspiler/ShaderCompiler.cpp | 91 +-------- .../MG_Util/ShaderTranspiler/ShaderCompiler.h | 15 +- .../NormalizeRectCoordinatesPass.cpp | 193 ++++++++++++++++++ .../NormalizeRectCoordinatesPass.h | 38 ++++ 11 files changed, 263 insertions(+), 192 deletions(-) create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp create mode 100644 MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 01457a4a..c74117b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -191,6 +191,7 @@ set(SOURCE_FILES MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/DecoratePositionInvariantPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/LowerDrawParametersPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/RebaseInstanceIndexPass.cpp + MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripUboMemberRelaxedPrecisionPass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/StripNoPerspectivePass.cpp MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/EmulateNoPerspectivePass.cpp diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index 89d4f86c..1dd07706 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -3384,18 +3384,13 @@ namespace MobileGL::MG_Backend::DirectGLES { } // ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather - // than approximating one. Rewriting the type to 2D is exact for a lookup that - // takes integer texel coordinates and needs the coordinate divided by the - // texture size for one that does not - see NormalizeRectSamplerCoordinates - // below, which the ESSL the transpiler produces goes through. The pass declines - // anything neither step can convert. + // than approximating one. The shared pass turns the type into the 2D one and + // divides the coordinate of every normalized-coordinate lookup by the texture + // size, which is the whole of the difference between the two. Vector rectLoweredSpirv; - Bool loweredRectImages = false; - if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImagesForEssl(*effectiveSpirv, - rectLoweredSpirv) && + if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(*effectiveSpirv, rectLoweredSpirv) && !rectLoweredSpirv.empty()) { effectiveSpirv = &rectLoweredSpirv; - loweredRectImages = true; } MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv, @@ -3432,26 +3427,6 @@ namespace MobileGL::MG_Backend::DirectGLES { source = ForceFlatIntegerVaryings(source, glShaderType); source = BroadcastLegacyFragColor(std::move(source), glShaderType, m_fragColorBroadcastCount); source = EmulateTextureLodBias(source); - if (loweredRectImages) { - // The image type is 2D now, so the transpiled lookups address [0,1]; the - // application wrote them in texels. Only the frontend still knows which - // samplers were declared rectangle. - Vector rectSamplerNames; - const Uint uniformCount = stateProgramObject->GetUniformCount(); - for (Uint i = 0; i < uniformCount; ++i) { - switch (stateProgramObject->GetActiveUniformType(i)) { - case GL_SAMPLER_2D_RECT: - case GL_SAMPLER_2D_RECT_SHADOW: - case GL_INT_SAMPLER_2D_RECT: - case GL_UNSIGNED_INT_SAMPLER_2D_RECT: - rectSamplerNames.push_back(stateProgramObject->GetActiveUniformName(i)); - break; - default: - break; - } - } - source = NormalizeRectSamplerCoordinates(source, rectSamplerNames); - } source = EmulateBaseInstanceInVertexShader(std::move(source), glShaderType); source = PromoteDrawParameterGlobalsToUniforms(std::move(source), glShaderType); source = ForceSupporterOutput(source); diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.h b/MobileGL/MG_Backend/DirectGLES/Managers.h index a3278219..9070bd0d 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.h +++ b/MobileGL/MG_Backend/DirectGLES/Managers.h @@ -279,7 +279,7 @@ namespace MobileGL::MG_Backend::DirectGLES { // arrays as 2D arrays (height 1, layers in depth), and rectangle textures as plain 2D - // they are single-level and already clamp, so only the non-normalized coordinates differ. // Must match the shader-side emulation: SPIRV-Cross handles 1D/1D-array itself, and - // ShaderCompiler::LowerRectImagesForEssl rewrites rectangle images (declining any module + // ShaderCompiler::LowerRectImages rewrites rectangle images (declining any module // whose lookups are not integer-coordinate, which SPIRV-Cross then still rejects). inline TextureTarget MapToBackendTextureTarget(TextureTarget target) { switch (target) { diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 05ff69fd..96735d5e 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -597,66 +597,6 @@ namespace MobileGL::MG_Backend::DirectGLES { return result; } - String NormalizeRectSamplerCoordinates(const String& glslCode, - const Vector& rectSamplerNames) { -#ifdef TRACY_ENABLE - ZoneScopedC(TRACY_ZONECOLOR_BACKEND); -#endif - if (rectSamplerNames.empty() || glslCode.find("texture") == String::npos) { - return glslCode; - } - - // Lookups whose argument 1 is a plain (non-projective) texel-space coordinate on a - // rectangle sampler. texelFetch* is absent on purpose: its coordinates are integer - // texels on the 2D target too, so it already lands in the right place. - static const char* const kRectCoordinateLookups[] = { - "textureGatherOffsets", "textureGatherOffset", "textureGather", - "textureOffset", "texture", - }; - - String result = glslCode; - // Right to left, so the offsets of the not-yet-rewritten calls stay valid. - for (SizeT scan = result.size(); scan-- > 0;) { - if (result[scan] != 't') continue; - if (scan > 0 && IsIdentifierChar(result[scan - 1])) continue; - - SizeT openParen = 0; - Bool matched = false; - for (const char* name : kRectCoordinateLookups) { - const SizeT nameLength = std::strlen(name); - if (result.compare(scan, nameLength, name) != 0) continue; - const SizeT after = result.find_first_not_of(" \t", scan + nameLength); - if (after == String::npos || result[after] != '(') continue; - openParen = after; - matched = true; - break; - } - if (!matched) continue; - - const Vector marks = SplitCallArguments(result, openParen); - if (marks.size() < 2) continue; // needs a sampler and a coordinate - - const SizeT firstArgStart = result.find_first_not_of(" \t", openParen + 1); - SizeT firstArgEnd = marks.front(); - while (firstArgEnd > firstArgStart && - (result[firstArgEnd - 1] == ' ' || result[firstArgEnd - 1] == '\t')) { - --firstArgEnd; - } - if (firstArgStart == String::npos || firstArgEnd <= firstArgStart) continue; - const String samplerName = result.substr(firstArgStart, firstArgEnd - firstArgStart); - if (std::find(rectSamplerNames.begin(), rectSamplerNames.end(), samplerName) == - rectSamplerNames.end()) { - continue; - } - - // Wrap argument 1: (coord) / vec2(textureSize(sampler, 0)). - const SizeT coordStart = marks[0] + 1; - const SizeT coordEnd = marks[1]; - result.insert(coordEnd, String(") / vec2(textureSize(") + samplerName + ", 0)))"); - result.insert(coordStart, "(("); - } - return result; - } } // namespace PrgramImpl namespace Utils { diff --git a/MobileGL/MG_Backend/DirectGLES/Utils.h b/MobileGL/MG_Backend/DirectGLES/Utils.h index 1012b5df..e433df2a 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -129,15 +129,6 @@ namespace MobileGL::MG_Backend::DirectGLES { // all have a zero bias is therefore unaffected. Returns the source unchanged when // there is nothing to rewrite. String EmulateTextureLodBias(const String& glslCode); - // GL_TEXTURE_RECTANGLE is emulated on an ES 2D texture and LowerRectImagesForEssl - // rewrites the image type to match, but a rectangle lookup addresses texels - // directly while a 2D one addresses [0,1] - so every lookup that takes normalized - // coordinates has to divide by the texture's size. `rectSamplerNames` is the set of - // samplers the program declared as rectangle; texelFetch is left alone (its - // coordinates are unnormalized on both targets) and so is anything projective, - // which LowerRectImagesForEssl still declines outright. - String NormalizeRectSamplerCoordinates(const String& glslCode, - const Vector& rectSamplerNames); } // namespace PrgramImpl namespace Utils { diff --git a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp index bd4197e2..69e733e9 100644 --- a/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp +++ b/MobileGL/MG_Backend/DirectVulkan/Renderer/ProgramFactory.cpp @@ -2388,6 +2388,17 @@ namespace MobileGL::MG_Backend::DirectVulkan { } } + // Vulkan's SPIR-V environment has no rectangle image dimension, so a + // GL_TEXTURE_RECTANGLE lookup has to become the 2D one the texture is really + // stored as - which addresses [0,1] where the application addressed texels. + { + Vector rectLoweredSpirv; + if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImages(moduleSpirvs[i], rectLoweredSpirv) && + !rectLoweredSpirv.empty()) { + moduleSpirvs[i] = Move(rectLoweredSpirv); + } + } + // GL apps depend on cross-program position invariance for multi-pass equality // depth tests (MC 26.3's OIT re-draws the cloud geometry with GEQUAL against the // depth its own first pass wrote); decorate Position outputs Invariant so diff --git a/MobileGL/MG_Test/Texture/TextureTest.cpp b/MobileGL/MG_Test/Texture/TextureTest.cpp index ac060b5b..6b338502 100644 --- a/MobileGL/MG_Test/Texture/TextureTest.cpp +++ b/MobileGL/MG_Test/Texture/TextureTest.cpp @@ -1870,7 +1870,7 @@ TEST_F(TextureTest, DirectGLESTreats2DArrayAsSupportedTextureTarget) { // Every desktop-only target is stored on an ES one (MapToBackendTextureTarget): 1D and // 1D-array as 2D / 2D-array, matching SPIRV-Cross's ES 1D-as-2D shader emission, and // rectangle as a plain 2D - it is single-level and already clamps, so only the - // non-normalized coordinates differ and LowerRectImagesForEssl handles those. + // non-normalized coordinates differ and LowerRectImages handles those. EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1D)); EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::Texture1DArray)); EXPECT_TRUE(IsSupportedTextureTarget(TextureTarget::TextureRectangle)); diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index 7aeb4f2b..169f1e77 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -19,6 +19,7 @@ #include "SpirvPasses/DecoratePositionInvariantPass.h" #include "SpirvPasses/LowerDrawParametersPass.h" #include "SpirvPasses/RebaseInstanceIndexPass.h" +#include "SpirvPasses/NormalizeRectCoordinatesPass.h" #include "SpirvPasses/StripUboMemberRelaxedPrecisionPass.h" #include "SpirvPasses/StripNoPerspectivePass.h" #include "SpirvPasses/EmulateNoPerspectivePass.h" @@ -363,90 +364,16 @@ namespace MobileGL { return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } - bool ShaderCompiler::LowerRectImagesForEssl(const Vector& inputBinary, - Vector& outputBinary) { - constexpr SizeT kSpirvHeaderWordCount = 5; - // OpTypeImage: [0] opcode/wordcount, [1] result id, [2] sampled type, [3] Dim, ... - constexpr SizeT kTypeImageDimWordIndex = 3; - constexpr SizeT kTypeImageMinWordCount = 9; - outputBinary.clear(); - if (inputBinary.size() < kSpirvHeaderWordCount || inputBinary[0] != spv::MagicNumber) { - return false; - } + bool ShaderCompiler::LowerRectImages(const Vector& inputBinary, + Vector& outputBinary) { + using namespace spvtools; + OptimizerOptions options; + options.set_run_validator(false); - Vector rectDimWordOffsets; - Vector rectCapabilityWordOffsets; - Bool hasNormalizedCoordinateLookup = false; - for (SizeT offset = kSpirvHeaderWordCount; offset < inputBinary.size();) { - const Uint32 instructionWord = inputBinary[offset]; - const SizeT wordCount = instructionWord >> 16u; - const auto opcode = static_cast(instructionWord & 0xffffu); - if (wordCount == 0 || offset + wordCount > inputBinary.size()) { - return false; - } + Optimizer optimizer(SPV_ENV_VULKAN_1_1); + optimizer.RegisterPass(NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass()); - if (opcode == spv::Op::OpTypeImage && wordCount >= kTypeImageMinWordCount) { - if (static_cast(inputBinary[offset + kTypeImageDimWordIndex]) == spv::Dim::Rect) { - rectDimWordOffsets.push_back(offset + kTypeImageDimWordIndex); - } - } else if (opcode == spv::Op::OpCapability && wordCount >= 2) { - const auto capability = static_cast(inputBinary[offset + 1]); - if (capability == spv::Capability::SampledRect || - capability == spv::Capability::ImageRect) { - rectCapabilityWordOffsets.push_back(offset + 1); - } - } else { - switch (opcode) { - // Normalized-coordinate lookups whose ESSL form the backend's - // NormalizeRectSamplerCoordinates post-pass cannot repair: the - // coordinate is either fused with something else in a single argument - // (the Dref sample forms carry the compare value in coord.z) or the - // divide would have to happen after a projective divide. Tracing each - // one back to its image type would let a module mix a normalized 2D - // lookup with a rectangle fetch, but the extra reach is not worth the - // risk of getting the trace wrong: decline the whole module instead. - // - // OpImageSampleImplicitLod, OpImageGather and OpImageDrefGather are - // absent because all three become an ESSL call whose argument 1 is the - // bare texel-space coordinate, which the post-pass divides by the - // texture size. - case spv::Op::OpImageSampleExplicitLod: - case spv::Op::OpImageSampleDrefImplicitLod: - case spv::Op::OpImageSampleDrefExplicitLod: - case spv::Op::OpImageSampleProjImplicitLod: - case spv::Op::OpImageSampleProjExplicitLod: - case spv::Op::OpImageSampleProjDrefImplicitLod: - case spv::Op::OpImageSampleProjDrefExplicitLod: - case spv::Op::OpImageSparseSampleImplicitLod: - case spv::Op::OpImageSparseSampleExplicitLod: - case spv::Op::OpImageSparseSampleDrefImplicitLod: - case spv::Op::OpImageSparseSampleDrefExplicitLod: - case spv::Op::OpImageSparseGather: - case spv::Op::OpImageSparseDrefGather: - hasNormalizedCoordinateLookup = true; - break; - default: - break; - } - } - offset += wordCount; - } - - if (rectDimWordOffsets.empty() || hasNormalizedCoordinateLookup) { - return false; - } - - outputBinary.assign(inputBinary.begin(), inputBinary.end()); - for (const SizeT dimWordOffset : rectDimWordOffsets) { - outputBinary[dimWordOffset] = static_cast(spv::Dim::Dim2D); - } - // The rectangle capabilities describe types that no longer exist. Shader is always - // declared by a graphics module, so restating it keeps the word count intact - // without leaving a capability SPIRV-Cross would key off. - for (const SizeT capabilityWordOffset : rectCapabilityWordOffsets) { - outputBinary[capabilityWordOffset] = static_cast(spv::Capability::Shader); - } - return true; + return optimizer.Run(inputBinary.data(), inputBinary.size(), &outputBinary, options); } bool ShaderCompiler::RebaseInstanceIndexForVulkan(const Vector& inputBinary, diff --git a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h index db1fbcb2..2cdb9c01 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.h @@ -43,20 +43,15 @@ namespace MobileGL { // devices lacking GL_NV_shader_noperspective_interpolation. See EmulateNoPerspectivePass. static bool EmulateNoPerspectiveForEssl(const Vector& inputBinary, Vector& outputBinary); - // Rewrites rectangle images (Dim::Rect) to plain 2D so SPIRV-Cross can emit ESSL - // for them at all - it refuses outright ("Rectangle textures are not supported on - // OpenGL ES"), which left the whole program unlinkable. Only valid while every use - // of the image takes integer texel coordinates (texelFetch / textureSize), where a - // rectangle target and a 2D target are indistinguishable; a normalized-coordinate - // lookup would also need its coordinates divided by the texture size, so the pass - // declines those modules instead of emitting something subtly wrong. Returns false - // when it changed nothing or cannot safely convert. DirectGLES only. - static bool LowerRectImagesForEssl(const Vector& inputBinary, - Vector& outputBinary); // Rebases loads of the InstanceIndex builtin to (InstanceIndex - BaseInstance) so // shaders see GL's zero-based gl_InstanceID. Vertex shaders only; DirectVulkan // backend only (glslang's relaxed mode aliases gl_InstanceID to gl_InstanceIndex, // which wrongly includes baseInstance). + // GL_TEXTURE_RECTANGLE emulated on a plain 2D texture, for every backend: + // divides the coordinate of each normalized-coordinate lookup by the texture + // size and rewrites the image type to 2D. See NormalizeRectCoordinatesPass for + // what it declines and why. + static bool LowerRectImages(const Vector& inputBinary, Vector& outputBinary); static bool RebaseInstanceIndexForVulkan(const Vector& inputBinary, Vector& outputBinary); // Adds the Invariant decoration to every Position builtin output. GL apps diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp new file mode 100644 index 00000000..28471ded --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.cpp @@ -0,0 +1,193 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.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 "NormalizeRectCoordinatesPass.h" + +#include "spirv.hpp" +#include "source/opt/constants.h" +#include "source/opt/def_use_manager.h" +#include "source/opt/instruction.h" +#include "source/opt/ir_builder.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 + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + namespace { + using spvtools::opt::Instruction; + using spvtools::opt::IRContext; + using spvtools::opt::InstructionBuilder; + + // Image operations whose coordinate operand (in-operand 1) is a plain + // normalized coordinate and nothing else. The Dref *sample* forms are absent: + // they pack the compare value into the coordinate's last component, so the + // divide cannot be applied componentwise. OpImageDrefGather is here because it + // carries the compare value in a separate operand. + bool TakesPlainNormalizedCoordinate(spv::Op opcode) { + switch (opcode) { + case spv::Op::OpImageSampleImplicitLod: + case spv::Op::OpImageSampleExplicitLod: + case spv::Op::OpImageGather: + case spv::Op::OpImageDrefGather: + return true; + default: + return false; + } + } + + // The OpTypeImage behind whatever an image operation was handed - a sampled + // image, a bare image, or a pointer to either. Returns nullptr when the operand + // is not an image at all. + Instruction* ResolveImageType(IRContext* context, uint32_t objectId) { + auto* defUseMgr = context->get_def_use_mgr(); + Instruction* object = defUseMgr->GetDef(objectId); + if (object == nullptr) return nullptr; + Instruction* type = defUseMgr->GetDef(object->type_id()); + while (type != nullptr) { + switch (type->opcode()) { + case spv::Op::OpTypeImage: + return type; + case spv::Op::OpTypeSampledImage: + case spv::Op::OpTypePointer: + // Both name their element type in their last in-operand. + type = defUseMgr->GetDef(type->GetSingleWordInOperand(type->NumInOperands() - 1)); + continue; + default: + return nullptr; + } + } + return nullptr; + } + + bool IsRectImageType(const Instruction* imageType) { + // OpTypeImage in-operands: sampled type, Dim, Depth, Arrayed, MS, Sampled, Format. + return imageType != nullptr && imageType->NumInOperands() >= 2 && + static_cast(imageType->GetSingleWordInOperand(1)) == spv::Dim::Rect; + } + + // The bare image an OpImageQuerySizeLod needs. An operation on a sampled image + // has to unwrap it first; one already holding a bare image is used as is. + uint32_t GetQueryableImage(IRContext* context, InstructionBuilder& builder, uint32_t imageOperandId, + uint32_t imageTypeId) { + auto* defUseMgr = context->get_def_use_mgr(); + Instruction* object = defUseMgr->GetDef(imageOperandId); + if (object == nullptr) return 0; + Instruction* type = defUseMgr->GetDef(object->type_id()); + if (type != nullptr && type->opcode() == spv::Op::OpTypeImage) { + return imageOperandId; + } + Instruction* unwrapped = builder.AddUnaryOp(imageTypeId, spv::Op::OpImage, imageOperandId); + return unwrapped != nullptr ? unwrapped->result_id() : 0; + } + } // namespace + + spvtools::opt::Pass::Status NormalizeRectCoordinatesPass::Process() { + auto* irContext = context(); + auto* typeMgr = irContext->get_type_mgr(); + auto* constantMgr = irContext->get_constant_mgr(); + + // Nothing to do unless the module actually declares a rectangle image. + bool hasRectImageType = false; + for (const Instruction& type : irContext->types_values()) { + if (type.opcode() == spv::Op::OpTypeImage && IsRectImageType(&type)) { + hasRectImageType = true; + break; + } + } + if (!hasRectImageType) { + return Status::SuccessWithoutChange; + } + + spvtools::opt::analysis::Integer signedInt(32, true); + spvtools::opt::analysis::Float float32(32); + spvtools::opt::analysis::Vector int2(&signedInt, 2); + spvtools::opt::analysis::Vector float2(&float32, 2); + const uint32_t int2TypeId = typeMgr->GetTypeInstruction(&int2); + const uint32_t float2TypeId = typeMgr->GetTypeInstruction(&float2); + const uint32_t lodZeroId = constantMgr->GetSIntConstId(0); + if (int2TypeId == 0 || float2TypeId == 0 || lodZeroId == 0) { + return Status::Failure; + } + + bool rewroteCoordinate = false; + for (auto& function : *irContext->module()) { + for (auto& block : function) { + for (auto& instruction : block) { + if (!TakesPlainNormalizedCoordinate(instruction.opcode()) || + instruction.NumInOperands() < 2) { + continue; + } + const uint32_t imageOperandId = instruction.GetSingleWordInOperand(0); + Instruction* imageType = ResolveImageType(irContext, imageOperandId); + if (!IsRectImageType(imageType)) { + continue; + } + + const uint32_t coordinateId = instruction.GetSingleWordInOperand(1); + InstructionBuilder builder( + irContext, &instruction, + IRContext::kAnalysisDefUse | IRContext::kAnalysisInstrToBlockMapping); + const uint32_t queryableImageId = + GetQueryableImage(irContext, builder, imageOperandId, imageType->result_id()); + if (queryableImageId == 0) { + return Status::Failure; + } + + // A rectangle image has exactly one level, so the query's level is 0. + // The lod form is what the 2D type this becomes accepts. + Instruction* size = builder.AddBinaryOp(int2TypeId, spv::Op::OpImageQuerySizeLod, + queryableImageId, lodZeroId); + Instruction* sizeFloat = + builder.AddUnaryOp(float2TypeId, spv::Op::OpConvertSToF, size->result_id()); + Instruction* normalized = builder.AddBinaryOp( + float2TypeId, spv::Op::OpFDiv, coordinateId, sizeFloat->result_id()); + instruction.SetInOperand(1, {normalized->result_id()}); + irContext->UpdateDefUse(&instruction); + rewroteCoordinate = true; + } + } + } + + // Now that no lookup depends on the rectangle semantics any more, the type can + // become the 2D one both targets accept. Done unconditionally, because a module + // that only ever fetched texels still has to lose the type. + for (Instruction& type : irContext->types_values()) { + if (type.opcode() == spv::Op::OpTypeImage && IsRectImageType(&type)) { + type.SetInOperand(1, {static_cast(spv::Dim::Dim2D)}); + } + } + // The rectangle capabilities describe types that no longer exist. Shader is + // always declared by a graphics module, so restating it keeps the instruction + // valid without leaving a capability a consumer would key off. + for (Instruction& capability : irContext->capabilities()) { + const auto value = static_cast(capability.GetSingleWordInOperand(0)); + if (value == spv::Capability::SampledRect || value == spv::Capability::ImageRect) { + capability.SetInOperand(0, {static_cast(spv::Capability::Shader)}); + } + } + if (rewroteCoordinate) { + irContext->AddCapability(spv::Capability::ImageQuery); + } + + irContext->InvalidateAnalysesExceptFor(IRContext::kAnalysisNone); + return Status::SuccessWithChange; + } + + spvtools::Optimizer::PassToken NormalizeRectCoordinatesPass::CreateNormalizeRectCoordinatesPass() { + return spvtools::Optimizer::PassToken(spvtools::MakeUnique()); + } + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL diff --git a/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.h b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.h new file mode 100644 index 00000000..a04c5bd8 --- /dev/null +++ b/MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.h @@ -0,0 +1,38 @@ +// MobileGL - MobileGL/MG_Util/ShaderTranspiler/SpirvPasses/NormalizeRectCoordinatesPass.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" + +namespace MobileGL { + namespace MG_Util { + namespace ShaderTranspiler { + // GL_TEXTURE_RECTANGLE has no counterpart in either target API: ESSL has no + // rectangle sampler at all, and Vulkan's SPIR-V environment does not allow + // Dim::Rect. Both emulate it on a plain 2D texture, which differs in exactly one + // way - a rectangle lookup addresses texels while a 2D one addresses [0,1]. + // + // This pass closes that difference in the module itself, so neither backend has + // to reason about it: every lookup that takes normalized coordinates gets its + // coordinate divided by the texture's size, and the image type is then rewritten + // to 2D. texelFetch is untouched (integer texel coordinates mean the same thing + // on both), and so is the Dref *sample* form, whose coordinate carries the + // compare value in the last component - the module keeps its rectangle type and + // the caller declines it. + class NormalizeRectCoordinatesPass final : public spvtools::opt::Pass { + public: + const char* name() const override { return "mobilegl-normalize-rect-coordinates"; } + Status Process() override; + + static spvtools::Optimizer::PassToken CreateNormalizeRectCoordinatesPass(); + }; + } // namespace ShaderTranspiler + } // namespace MG_Util +} // namespace MobileGL