From 5437947240d77388bdd6f0e73fade3d49d1f633b Mon Sep 17 00:00:00 2001 From: BZLZHH Date: Tue, 4 Aug 2026 10:38:01 -0400 Subject: [PATCH] [Feat] (DirectGLES, MG_Util): normalize the coordinates of a rectangle lookup A rectangle texture is emulated on an ES 2D texture, and LowerRectImagesForEssl rewrites the image type in the SPIR-V to match. That is exact only where the lookup addresses texels directly, which is why the pass declined any module containing a lookup that takes normalized coordinates - the whole KHR-GL40.texture_gather 2drect set among them. The missing half is one divide: a rectangle lookup's coordinate is in texels and the 2D lookup it becomes wants [0,1], so the coordinate has to be divided by the texture's size. It goes in on the ESSL the transpiler produces, next to the LOD-bias emulation that already rewrites lookup arguments there, and reads the size back with textureSize() rather than plumbing a uniform down - the emulated texture is a real ES 2D texture, so the shader can ask it directly. Only the forms whose argument 1 is the bare coordinate are rewritten - texture, textureOffset and the three textureGather flavours, which covers the Dref gathers too because those carry the compare value in a separate argument. texelFetch is deliberately left alone: its coordinates are integer texels on both targets. The SPIR-V pass keeps declining everything else, so a projective lookup or a Dref sample (where the compare value rides in coord.z) still refuses the module instead of producing something subtly wrong. Which samplers were declared rectangle is no longer visible in the transpiled source - they are plain sampler2D by then - so the names come from the frontend program's reflection. --- MobileGL/MG_Backend/DirectGLES/Managers.cpp | 30 ++++++++- MobileGL/MG_Backend/DirectGLES/Utils.cpp | 61 +++++++++++++++++++ MobileGL/MG_Backend/DirectGLES/Utils.h | 9 +++ .../ShaderTranspiler/ShaderCompiler.cpp | 20 +++--- 4 files changed, 110 insertions(+), 10 deletions(-) diff --git a/MobileGL/MG_Backend/DirectGLES/Managers.cpp b/MobileGL/MG_Backend/DirectGLES/Managers.cpp index b6170424..89d4f86c 100644 --- a/MobileGL/MG_Backend/DirectGLES/Managers.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Managers.cpp @@ -3384,14 +3384,18 @@ namespace MobileGL::MG_Backend::DirectGLES { } // ES has no rectangle sampler, and SPIRV-Cross refuses the whole module rather - // than approximating one. Where every use takes integer texel coordinates a - // rectangle image is indistinguishable from a 2D one, so rewrite the type and let - // it through; the pass declines anything it cannot convert exactly. + // 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. Vector rectLoweredSpirv; + Bool loweredRectImages = false; if (MG_Util::ShaderTranspiler::ShaderCompiler::LowerRectImagesForEssl(*effectiveSpirv, rectLoweredSpirv) && !rectLoweredSpirv.empty()) { effectiveSpirv = &rectLoweredSpirv; + loweredRectImages = true; } MG_Util::ShaderTranspiler::SpvcSession spvcSession(*effectiveSpirv, @@ -3428,6 +3432,26 @@ 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/Utils.cpp b/MobileGL/MG_Backend/DirectGLES/Utils.cpp index 0693b5e7..05ff69fd 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.cpp +++ b/MobileGL/MG_Backend/DirectGLES/Utils.cpp @@ -596,6 +596,67 @@ 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 e433df2a..1012b5df 100644 --- a/MobileGL/MG_Backend/DirectGLES/Utils.h +++ b/MobileGL/MG_Backend/DirectGLES/Utils.h @@ -129,6 +129,15 @@ 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_Util/ShaderTranspiler/ShaderCompiler.cpp b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp index c2b1c510..7aeb4f2b 100644 --- a/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp +++ b/MobileGL/MG_Util/ShaderTranspiler/ShaderCompiler.cpp @@ -397,11 +397,19 @@ namespace MobileGL { } } else { switch (opcode) { - // Everything that takes normalized coordinates. 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. - case spv::Op::OpImageSampleImplicitLod: + // 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: @@ -409,8 +417,6 @@ namespace MobileGL { case spv::Op::OpImageSampleProjExplicitLod: case spv::Op::OpImageSampleProjDrefImplicitLod: case spv::Op::OpImageSampleProjDrefExplicitLod: - case spv::Op::OpImageGather: - case spv::Op::OpImageDrefGather: case spv::Op::OpImageSparseSampleImplicitLod: case spv::Op::OpImageSparseSampleExplicitLod: case spv::Op::OpImageSparseSampleDrefImplicitLod: