[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.
This commit is contained in:
BZLZHH
2026-08-04 13:25:40 -04:00
parent 44ee6b66b3
commit 28d0af6f04
11 changed files with 263 additions and 192 deletions
+4 -29
View File
@@ -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<unsigned int> 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<String> 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);
+1 -1
View File
@@ -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) {
-60
View File
@@ -597,66 +597,6 @@ namespace MobileGL::MG_Backend::DirectGLES {
return result;
}
String NormalizeRectSamplerCoordinates(const String& glslCode,
const Vector<String>& 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<SizeT> 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 {
-9
View File
@@ -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<String>& rectSamplerNames);
} // namespace PrgramImpl
namespace Utils {