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 <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-10 01:04:23 +02:00
1 parent b9b1a29c42
commit 763bcde73e
5 files changed
+29 -16

No files matched your search

+11 -4
View File
@@ -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);
}
+11 -5
View File
@@ -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 <bool check>
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);
+4 -4
View File
@@ -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);
+2 -2
View File
@@ -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<false>(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);