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