GPU: Move sprites on pixel centers back half a pixel above 1x (compat SnapHalfPixelSprites)

The PS1 emulator draws sprites with their corners on pixel centers (x.5) and
their texture coordinates on texel centers. At 1x that covers whole pixels,
but above 1x it covers half a pixel more on the right and bottom and reads
the neighbouring texels, which shows as seams between tiles. With compat.ini's
SnapHalfPixelSprites, such sprites and their texture coordinates are moved back
half a pixel, which draws the same at 1x. Texture coordinates are also nudged
a 64th of a texel in, as a scaled sprite can sample right on a texel edge,
where interpolation can fall short of the texel the GE reads (the emulator's
video unpacking does).

Set for PS1 games (POPS00000).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-21 14:51:06 +02:00
1 parent cf788d72ca
commit ef7ca8b85f
4 files changed
+42

No files matched your search

+1
View File
@@ -170,6 +170,7 @@ void Compatibility::CheckSettings(IniFile &iniFile, const std::string &gameID) {
CheckSetting(iniFile, gameID, "ReadbackShadow", &flags_.ReadbackShadow);
CheckSetting(iniFile, gameID, "ColorPickingDrawSyncReadback", &flags_.ColorPickingDrawSyncReadback);
CheckSetting(iniFile, gameID, "SeparateLogicOpDraws", &flags_.SeparateLogicOpDraws);
CheckSetting(iniFile, gameID, "SnapHalfPixelSprites", &flags_.SnapHalfPixelSprites);
CheckSetting(iniFile, gameID, "FixedFramebuffer", &flags_.FixedFramebuffer);
CheckSetting(iniFile, gameID, "NativeResolutionFramebuffer", &flags_.NativeResolutionFramebuffer);
}
+1
View File
@@ -136,6 +136,7 @@ struct CompatFlags {
bool ReadbackShadow;
bool ColorPickingDrawSyncReadback;
bool SeparateLogicOpDraws;
bool SnapHalfPixelSprites;
CompatFramebuffer FixedFramebuffer;
uint32_t NativeResolutionFramebuffer;
};
+34
View File
@@ -1009,6 +1009,20 @@ static SoftwareTransformAction ProjectClipAndExpand(SoftwareTransformParams &par
return SW_DRAW_INDEXED;
}
// A sprite with its corners on pixel centers (x.5), as the PS1 emulator draws them. Above 1x it covers
// half a pixel more on the right and bottom and reads its neighbours' texels (seams between tiles).
// Moved back half a pixel, with its texture coordinates, it draws the same at 1x.
static bool IsHalfPixelSprite(const TransformedVertex &tl, const TransformedVertex &br) {
auto onCenter = [](float f) {
const float frac = f - floorf(f);
return frac > 0.49f && frac < 0.51f;
};
if (!onCenter(tl.x) || !onCenter(tl.y) || !onCenter(br.x) || !onCenter(br.y)) {
return false;
}
return br.x > tl.x && br.y > tl.y;
}
static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSize, u16 *&inds, int indsSize, const TransformedVertex *transformed, TransformedVertex *transformedExpanded, int *drawIndexCount, bool throughmode, bool *pixelMappedExactly) {
// Before we start, do a sanity check - does the output fit?
// The expanded indices are written after the input ones, at inds + vertexCount.
@@ -1160,6 +1174,26 @@ static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSiz
trans[3].v = (transVtxBR.v - spriteBorderFixB) * vScale;
trans[3].z = z;
if (PSP_CoreParameter().compat.flags().SnapHalfPixelSprites && gstate.isTextureMapEnabled()) {
// In texels for through mode (scaled below), else normalized.
const float texW = throughmode ? 1.0f : (float)gstate.getTextureWidth(0);
const float texH = throughmode ? 1.0f : (float)gstate.getTextureHeight(0);
if (IsHalfPixelSprite(transVtxTL, transVtxBR)) {
// Half a pixel in texture coordinates. Plus a nudge in: a scaled sprite can sample right on a
// texel edge, where interpolation here can fall short of the texel the GE reads.
const float du = 0.5f * (transVtxBR.u - transVtxTL.u) / (transVtxBR.x - transVtxTL.x) * (throughmode ? uScale : 1.0f);
const float dv = 0.5f * (transVtxBR.v - transVtxTL.v) / (transVtxBR.y - transVtxTL.y) * (throughmode ? vScale : 1.0f);
const float nudgeU = (1.0f / 64.0f) / texW * (throughmode ? uScale : 1.0f);
const float nudgeV = (1.0f / 64.0f) / texH * (throughmode ? vScale : 1.0f);
for (int c = 0; c < 4; c++) {
trans[c].x -= 0.5f;
trans[c].y -= 0.5f;
trans[c].u += nudgeU - du;
trans[c].v += nudgeV - dv;
}
}
}
// That's the four corners. Now process UV rotation.
// TODO: Should we apply the sprite border fix before or after rotation? Likely after, right?
RotateUV(trans);
+6
View File
@@ -2270,3 +2270,9 @@ POPS00000 = 0x04100000
# draw's rectangles one by one), so each sees the ones before in the framebuffer copy. Costs a copy per draw.
# The PS1 emulator puts each 24-bit video frame together by ORing passes into the same pixels.
POPS00000 = true
[SnapHalfPixelSprites]
# Textured sprites with their corners on pixel centers (x.5) and their texture coordinates on texel centers
# are moved back half a pixel, which draws the same at 1x. Above 1x they otherwise cover half a pixel more
# and read their neighbours' texels: seams between tiles. The PS1 emulator draws its sprites like that.
POPS00000 = true