From 763bcde73e73b3f0b2cb0ee0466ffbd2bcdbcf5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 9 Oct 2026 00:09:15 +0200 Subject: [PATCH] Sample the last texel where the projected q isn't positive The GE doesn't divide by a q <= 0: it samples the last texel in u and v, whatever s and t are, in clamp and repeat modes alike (gpu/texmtx/negq). The hardware backends divided, which mirrors a projected texture behind its projector: Fate/Extra's character shadows ran up the school's walls (#18667). The software renderer sampled (0, 0) instead. Co-Authored-By: Claude Opus 5.5 --- GPU/Common/FragmentShaderGenerator.cpp | 15 +++++++++++---- GPU/Software/GEMath.h | 16 +++++++++++----- GPU/Software/Rasterizer.cpp | 8 ++++---- pspautotests | 2 +- unittest/TestGEMath.cpp | 4 ++-- 5 files changed, 29 insertions(+), 16 deletions(-) diff --git a/GPU/Common/FragmentShaderGenerator.cpp b/GPU/Common/FragmentShaderGenerator.cpp index 022386c7c5..28331abd78 100644 --- a/GPU/Common/FragmentShaderGenerator.cpp +++ b/GPU/Common/FragmentShaderGenerator.cpp @@ -556,6 +556,13 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu if (doTexture) { char texcoord[64] = "v_texcoord"; + if (doTextureProjection) { + // Where q isn't positive, the GE samples the last texel instead of dividing (gpu/texmtx/negq, and + // GE_NONPOSITIVE_Q_UV in the software renderer). Dividing by a negative q mirrors the texture behind the + // projector: Fate/Extra's projected shadows ran up the walls (#18667). + WRITE(p, " vec3 projTexcoord = v_texcoord.z > 0.0 ? v_texcoord : vec3(0.99998474, 0.99998474, 1.0);\n"); + truncate_cpy(texcoord, "projTexcoord"); + } // TODO: Not sure the right way to do this for projection. // This path destroys resolution on older PowerVR no matter what I do if projection is needed, // so we disable it on SGX 540 and lesser, and live with the consequences. @@ -572,8 +579,8 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu std::string ucoord = "v_texcoord.x"; std::string vcoord = "v_texcoord.y"; if (doTextureProjection) { - ucoord = "(v_texcoord.x / v_texcoord.z)"; - vcoord = "(v_texcoord.y / v_texcoord.z)"; + ucoord = "(projTexcoord.x / projTexcoord.z)"; + vcoord = "(projTexcoord.y / projTexcoord.z)"; } std::string modulo = (gl_extensions.bugs & BUG_PVR_SHADER_PRECISION_BAD) ? "mymod" : "mod"; @@ -602,13 +609,13 @@ bool GenerateFragmentShader(const FShaderID &id, char *buffer, const ShaderLangu if (compat.shaderLanguage == HLSL_D3D11) { if (texture3D) { if (doTextureProjection) { - WRITE(p, " vec4 t = tex.Sample(texSamp, vec3(v_texcoord.xy / v_texcoord.z, u_mipBias));\n"); + WRITE(p, " vec4 t = tex.Sample(texSamp, vec3(%s.xy / %s.z, u_mipBias));\n", texcoord, texcoord); } else { WRITE(p, " vec4 t = tex.Sample(texSamp, vec3(%s.xy, u_mipBias));\n", texcoord); } } else { if (doTextureProjection) { - WRITE(p, " vec4 t = tex.Sample(texSamp, v_texcoord.xy / v_texcoord.z);\n"); + WRITE(p, " vec4 t = tex.Sample(texSamp, %s.xy / %s.z);\n", texcoord, texcoord); } else { WRITE(p, " vec4 t = tex.Sample(texSamp, %s.xy);\n", texcoord); } diff --git a/GPU/Software/GEMath.h b/GPU/Software/GEMath.h index 58f6fe2ec3..f1f91d7523 100644 --- a/GPU/Software/GEMath.h +++ b/GPU/Software/GEMath.h @@ -451,12 +451,18 @@ inline float GEUVProduct(double d) { return (float)d; } +// Where q isn't positive, the GE samples the last texel in u and v, whatever s and t are and in both clamp +// and wrap modes (gpu/texmtx/negq): a saturated coordinate. 1 - 2^-16 lands on texel size - 1 with the highest +// subtexel fraction, for any texture size. +constexpr float GE_NONPOSITIVE_Q_UV = 1.0f - 1.0f / 65536.0f; +constexpr int32_t GE_NONPOSITIVE_Q_UV_BITS = 0x3F7FFF00; + // The rasterizer's texture coordinates for four pixels (Rasterizer's GetTextureCoordinatesGE): from the UV // planes' values at them (each times 2^scaleExp), q as a float24, and s and t as GEUVProduct(float24 * -// GERecip(q)), or 0 where q isn't positive. False, with the outputs untouched, when a lane needs more than -// the vector path does (a value of 2^24 or more, a denormal or out of range result): then do it scalar. -// The core takes the values as lanes, each below 2^24; with check false, the caller knows they're inside -// what the vector path covers (GEUVSpanSafe). +// GERecip(q)), or GE_NONPOSITIVE_Q_UV where q isn't positive. False, with the outputs untouched, when a lane +// needs more than the vector path does (a value of 2^24 or more, a denormal or out of range result): then do +// it scalar. The core takes the values as lanes, each below 2^24; with check false, the caller knows they're +// inside what the vector path covers (GEUVSpanSafe). template inline bool GEUVSpanCore(Vec4S32 qv, Vec4S32 sv, Vec4S32 tv, int expQ, int expS, int expT, float *s, float *t, float *q) { const Vec4S32 expMask = Vec4S32::Splat(0x7F800000); @@ -517,7 +523,7 @@ inline bool GEUVSpanCore(Vec4S32 qv, Vec4S32 sv, Vec4S32 tv, int expQ, int expS, const Vec4S32 sign = xb & Vec4S32::Splat((int)0x80000000); const Vec4S32 result = (Vec4S32FromBits(Vec4F32FromS32(m)) + adjust.Shl<23>()) | sign; // Zero stays as it is, with its sign. - return ((result.AndNot(xZero) | (xb & xZero)) & valid); + return ((result.AndNot(xZero) | (xb & xZero)) & valid) | Vec4S32::Splat(GE_NONPOSITIVE_Q_UV_BITS).AndNot(valid); }; const Vec4S32 so = product(sb); const Vec4S32 to = product(tb); diff --git a/GPU/Software/Rasterizer.cpp b/GPU/Software/Rasterizer.cpp index ada338e643..048bb96044 100644 --- a/GPU/Software/Rasterizer.cpp +++ b/GPU/Software/Rasterizer.cpp @@ -1236,8 +1236,8 @@ static inline void GetTextureCoordinatesGE(const UVPlanes &planes, const int64_t const float q = TruncateToFloat24((float)((double)qs[i] * planes.scaleQ)); qOut[i] = q; if (!(q > 0.0f)) { - s[i] = 0.0f; - t[i] = 0.0f; + s[i] = GE_NONPOSITIVE_Q_UV; + t[i] = GE_NONPOSITIVE_Q_UV; continue; } const double r = GERecip(q); @@ -2616,8 +2616,8 @@ static float LineUVComponentAt(const int64_t c[2], double scale, const VertexDat static void LineTextureCoordinatesAt(const LineUV &uv, const VertexData &v0, const VertexData &v1, int px, int py, float &s, float &t, float &q) { q = LineUVComponentAt(uv.q, uv.scaleQ, v0, v1, px, py); if (!(q > 0.0f)) { - s = 0.0f; - t = 0.0f; + s = GE_NONPOSITIVE_Q_UV; + t = GE_NONPOSITIVE_Q_UV; return; } const double r = GERecip(q); diff --git a/pspautotests b/pspautotests index c662bd80cd..e1d59fe556 160000 --- a/pspautotests +++ b/pspautotests @@ -1 +1 @@ -Subproject commit c662bd80cdb312df62086ff87e52174ede5bd3f0 +Subproject commit e1d59fe5560bcc25eebb5c5bf1427bea4566d90a diff --git a/unittest/TestGEMath.cpp b/unittest/TestGEMath.cpp index 9b32879f6f..d50efdccfb 100644 --- a/unittest/TestGEMath.cpp +++ b/unittest/TestGEMath.cpp @@ -281,7 +281,7 @@ static bool TestGEUVSpan() { fast++; for (int l = 0; l < 4; ++l) { const float wq = TruncateToFloat24((float)ldexp((double)qs[l], expQ)); - float ws = 0.0f, wt = 0.0f; + float ws = GE_NONPOSITIVE_Q_UV, wt = GE_NONPOSITIVE_Q_UV; if (wq > 0.0f) { const double r = GERecip(wq); ws = GEUVProduct((double)TruncateToFloat24((float)ldexp((double)ss[l], expS)) * r); @@ -312,7 +312,7 @@ static bool TestGEUVSpan() { GEUVSpanCore(Vec4S32::Load(qv), Vec4S32::Load(sv), Vec4S32::Load(tv), expQ, expS, expT, s, t, q); for (int l = 0; l < 4; ++l) { const float wq = TruncateToFloat24((float)ldexp((double)qv[l], expQ)); - float ws = 0.0f, wt = 0.0f; + float ws = GE_NONPOSITIVE_Q_UV, wt = GE_NONPOSITIVE_Q_UV; if (wq > 0.0f) { const double r = GERecip(wq); ws = GEUVProduct((double)TruncateToFloat24((float)ldexp((double)sv[l], expS)) * r);