softgpu: Fix subpixel rounding of negative screen coords, and a stale +1

Two leftovers from the sample point having been a sixteenth of a pixel off
center. They're one commit because they can't be separated: each was
compensating for the other, so applying either alone makes
gpu/filtering/precisionnearest3d fail. I tried both orderings.

1. DrawRectangle advanced ST to the first sample with (minX - entireX1 + 1).
   The +1 existed to make up for centerOff being 7 instead of 8; now that minX
   already sits at the pixel center it double-counts and pushes the texture
   coordinate a sixteenth of a texel too far. Only visible when that lands
   exactly on a texel boundary, which is what precisionnearest2d's offset-7 case
   constructs: the sprite spans x from -7/16, so u at pixel 0's center is 0.9375
   and should sample texel 0, but the extra sixteenth made it exactly 1.0.

2. Removing the +1 exposed the other one. ClipToScreenInternal used a plain
   (int) cast, which truncates toward zero - so once the region offset makes the
   value negative it rounds the opposite way from the positive case, and from
   the through-mode path that computes screenpos directly. The two disagreed by
   one subpixel for negative coordinates, which is why the 2D and 3D variants of
   the same test failed at different offsets, 7 and 8. floorf is what the +0.375
   nudge was always meant to pair with, and it makes both paths agree.

Now passing: gpu/filtering/precisionnearest2d, promoted to tests_good since it
passes on Vulkan too. precisionlinear2d and precisionlinear3d also start passing
on software but still fail on the hardware backends, so they stay in tests_next
with a comment saying so, as does gpu/primitives/continue from the last commit.

317 pspautotests pass, 0 fail.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vd8ntC2brCUtCrDJMqLbs8
This commit is contained in:
Henrik RydgårdandClaude Opus 5 committed 2026-08-29 13:02:09 +02:00
1 parent c9aac4f992
commit d603396f30
3 files changed
+14 -5

No files matched your search

+5 -2
View File
@@ -1293,8 +1293,11 @@ void DrawRectangle(const VertexData &v0, const VertexData &v1, const BinCoords &
}
// Okay, now move ST to the minX, minY position.
rowST += (stx / (float)(SCREEN_SCALE_FACTOR * 2)) * (minX - entireX1 + 1);
rowST += (sty / (float)(SCREEN_SCALE_FACTOR * 2)) * (minY - entireY1 + 1);
// This is the exact distance from the primitive's edge to the sample point - minX and minY
// already sit at the pixel center. The +1 that used to be here was compensating for
// TriangleEdge::Start's centerOff being a sixteenth of a pixel short of center.
rowST += (stx / (float)(SCREEN_SCALE_FACTOR * 2)) * (minX - entireX1);
rowST += (sty / (float)(SCREEN_SCALE_FACTOR * 2)) * (minY - entireY1);
}
// And now what we add to spread out to 4 values.
+5 -2
View File
@@ -176,8 +176,11 @@ static ScreenCoords ClipToScreenInternal(Vec3f scaled, const ClipCoords &coords,
// 16 = 0xFFFF / 4095.9375
// Round up at 0.625 to the nearest subpixel.
static_assert(SCREEN_SCALE_FACTOR == 16, "Currently only supports scale 16");
int x = (int)(scaled.x * 16.0f + 0.375f - gstate.getOffsetX16());
int y = (int)(scaled.y * 16.0f + 0.375f - gstate.getOffsetY16());
// floorf, not a plain (int) cast: the cast truncates toward zero, so once the region offset
// pushes the result negative it rounds the opposite way from the positive case - and from the
// through-mode path below, which is what made the two disagree by one subpixel.
int x = (int)floorf(scaled.x * 16.0f + 0.375f - gstate.getOffsetX16());
int y = (int)floorf(scaled.y * 16.0f + 0.375f - gstate.getOffsetY16());
return ScreenCoords(x, y, scaled.z);
}
+4 -1
View File
@@ -167,6 +167,7 @@ tests_good = [
"gpu/displaylist/alignment",
"gpu/dither/dither",
"gpu/filtering/mipmaplinear",
"gpu/filtering/precisionnearest2d",
"gpu/filtering/precisionnearest3d",
"gpu/ge/break",
"gpu/ge/context",
@@ -424,12 +425,14 @@ tests_next = [
"gpu/displaylist/state",
"gpu/filtering/linear",
"gpu/filtering/nearest",
# These two pass with --graphics=software, but still fail on the hardware backends,
# so they stay here rather than in tests_good until those match too.
"gpu/filtering/precisionlinear2d",
"gpu/filtering/precisionlinear3d",
"gpu/filtering/precisionnearest2d",
"gpu/ge/edramswizzle",
"gpu/ge/get",
"gpu/primitives/bezier",
# Passes with --graphics=software, still fails on the hardware backends.
"gpu/primitives/continue",
"gpu/primitives/immediate",
"gpu/primitives/lines",