Merge pull request #21795 from hrydgard/misc

Make the sprite detection work also if Smart 2D texture filter is off
This commit is contained in:
Henrik Rydgård
2026-06-06 13:21:08 +02:00
committed by GitHub
9 changed files with 175 additions and 94 deletions
+2 -2
View File
@@ -86,8 +86,8 @@ void Compatibility::Load(const std::string &gameID) {
}
void Compatibility::Clear() {
memset(&flags_, 0, sizeof(flags_));
memset(&vrCompat_, 0, sizeof(vrCompat_));
flags_ = {};
vrCompat_ = {};
activeList_.clear();
filesLoaded_.clear();
}
+71 -33
View File
@@ -539,16 +539,15 @@ bool DrawEngineCommon::TestBoundingBoxFast(const float *cullMatrix, const void *
// 2D bounding box test against scissor. No indexing yet.
// Only supports non-indexed draws with float positions. TODO: Add more float formats.
bool DrawEngineCommon::TestBoundingBoxThrough(const void *vdata, int vertexCount, const VertexDecoder *dec, u32 vertType, int *bytesRead) {
// Grab temp buffer space from large offsets in decoded_. Not exactly safe for large draws.
if (vertexCount > 16) {
bool DrawEngineCommon::TestBoundingBoxThrough(GEPrimitiveType prim, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, u32 vertType, int *bytesRead, ClipInfoFlags *flags) {
// Although this may lead to drawing that shouldn't happen, the viewport is more complex on VR.
// Let's always say objects are within bounds.
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY)) {
return true;
}
// Although this may lead to drawing that shouldn't happen, the viewport is more complex on VR.
// Let's always say objects are within bounds.
if (gstate_c.Use(GPU_USE_VIRTUAL_REALITY))
return true;
// For through mode, we only check FlatZ.
*flags |= ClipInfoFlags::Valid;
const int stride = dec->VertexSize();
const int posOffset = dec->posoff;
@@ -564,37 +563,76 @@ bool DrawEngineCommon::TestBoundingBoxThrough(const void *vdata, int vertexCount
const float right = gstate.getScissorX2() + 1;
const float bottom = gstate.getScissorY2() + 1;
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_FLOAT:
{
// TODO: This can be SIMD'd, with some trickery.
for (int i = 0; i < vertexCount; i++) {
const float *pos = (const float*)((const u8 *)vdata + stride * i + posOffset);
const float x = pos[0];
const float y = pos[1];
if (x >= left) {
allOutsideLeft = false;
}
if (x <= right) {
allOutsideRight = false;
}
if (y >= top) {
allOutsideTop = false;
}
if (y <= bottom) {
allOutsideBottom = false;
}
float minZ = FLT_MAX;
float maxZ = -FLT_MAX;
IndexConverter conv(vertType, idata);
// TODO: This can be SIMD'd, with some trickery.
for (int i = 0; i < vertexCount; i++) {
int index = conv(i);
float x, y, z;
switch (vertType & GE_VTYPE_POS_MASK) {
case GE_VTYPE_POS_FLOAT:
{
const float *pos = (const float*)((const u8 *)vdata + stride * index + posOffset);
x = pos[0];
y = pos[1];
z = pos[2];
}
if (allOutsideLeft || allOutsideTop || allOutsideRight || allOutsideBottom) {
break;
case GE_VTYPE_POS_8BIT:
{
// Through mode doesn't really support 8-bit though.
const u8 *pos8 = (const u8 *)vdata + stride * index + posOffset;
x = pos8[0];
y = pos8[1];
z = pos8[2];
break;
}
case GE_VTYPE_POS_16BIT:
{
const s16 *pos16 = (const s16 *)((const u8 *)vdata + stride * index + posOffset);
x = pos16[0];
y = pos16[1];
z = (u16)pos16[2];
break;
}
default:
return false;
}
return true;
if (x >= left) {
allOutsideLeft = false;
}
if (x <= right) {
allOutsideRight = false;
}
if (y >= top) {
allOutsideTop = false;
}
if (y <= bottom) {
allOutsideBottom = false;
}
// If prim is rectangles, we only update minZ and maxZ for every second vertex,
// since the Z for the whole rect is taken from the 2nd.
if (prim != GE_PRIM_RECTANGLES || (i & 1) == 1) {
if (z < minZ) {
minZ = z;
}
if (z > maxZ) {
maxZ = z;
}
}
}
default:
// Shouldn't end up here with the checks outside this function.
_dbg_assert_(false);
return true;
if (allOutsideLeft || allOutsideTop || allOutsideRight || allOutsideBottom) {
return false;
}
if (minZ == maxZ) {
*flags |= ClipInfoFlags::FlatZ;
}
return true;
}
bool DrawEngineCommon::EstimateThroughPrimSafeSize(const void *verts, const void *inds, GEPrimitiveType prim, int vertexCount, const VertexDecoder *dec, u32 vertType, int *safeWidth, int *safeHeight) {
+1 -1
View File
@@ -107,7 +107,7 @@ public:
// This is a less accurate version of TestBoundingBox, but faster. Can have more false positives.
// Doesn't support indexing.
bool TestBoundingBoxFast(const float *cullMatrix, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, u32 vertType, ClipInfoFlags *clipInfoFlags);
bool TestBoundingBoxThrough(const void *vdata, int vertexCount, const VertexDecoder *dec, u32 vertType, int *bytesRead);
bool TestBoundingBoxThrough(GEPrimitiveType prim, const void *vdata, const void *idata, int vertexCount, const VertexDecoder *dec, u32 vertType, int *bytesRead, ClipInfoFlags *flags);
bool EstimateThroughPrimSafeSize(const void *verts, const void *inds, GEPrimitiveType prim, int vertexCount, const VertexDecoder *dec, u32 vertType, int *safeWidth, int *safeHeight);
void FlushPartialDecode() {
+82 -50
View File
@@ -78,6 +78,10 @@ static void RotateUV(TransformedVertex v[4]) {
}
}
static bool ShouldApplySpriteBorderFix(const GPUgstate &gstate) {
return gstate.isAlphaBlendEnabled() && gstate.getBlendFuncA() != GE_SRCBLEND_FIXA;
}
// Clears on the PSP are best done by drawing a series of vertical strips
// in clear mode. This tries to detect that.
static bool IsReallyAClear(const TransformedVertex *transformed, int numVerts, float x2, float y2) {
@@ -594,10 +598,7 @@ static void ClipTrianglesAgainstNearPlane(
}
// Note: This modifies the U/V coordinates of transformed.
static void ApplySpriteBorderFix(TransformedVertex *transformed, const u16 *quad, float uScale, float vScale, float spriteBorderFix) {
const float invUScale = 1.0f / uScale;
const float invVScale = 1.0f / vScale;
static void ApplySpriteBorderFixTriangles(TransformedVertex *transformed, const u16 *quad, float uScale, float vScale, float spriteBorderFix) {
// We have two triangles, but the vertex order can really be anything. We just need to find the shared edge, and then check the opposite vertices.
// sharedA and sharedB are indices into transformed, picked from the quad array.
@@ -677,9 +678,14 @@ static void ApplySpriteBorderFix(TransformedVertex *transformed, const u16 *quad
}
}
const float invUScale = 1.0f / uScale;
const float invVScale = 1.0f / vScale;
if (validSprite) {
// We have a valid sprite! Apply the border fix if needed.
if (spriteBorderFix != 0.0f) {
const bool topleft = spriteBorderFix < 0.0f;
spriteBorderFix = fabsf(spriteBorderFix);
//spriteBorderFix *= 10.0f;
const float uBorderFix = spriteBorderFix * invUScale;
const float vBorderFix = spriteBorderFix * invVScale;
@@ -693,24 +699,22 @@ static void ApplySpriteBorderFix(TransformedVertex *transformed, const u16 *quad
if (du != 0.0f && fabsf(dx) != 480.0f) {
const float uSign = (du > 0.0f ? 1.0f : -1.0f);
const float uAmount = uBorderFix * uSign;
const float xAmount = spriteBorderFix * uSign;
// vSharedA.u += uAmount;
// vCornerB.u += uAmount;
if (topleft) {
vSharedA.u += uAmount;
vCornerB.u += uAmount;
}
vCornerA.u -= uAmount;
vSharedB.u -= uAmount;
vCornerA.x -= xAmount;
vSharedB.x -= xAmount;
}
if (dv != 0.0f && fabsf(dy) != 272.0f) {
const float vSign = (dv > 0.0f ? 1.0f : -1.0f);
const float vAmount = vBorderFix * vSign;
const float yAmount = spriteBorderFix * vSign;
// vSharedA.v += vAmount;
// vCornerA.v += vAmount;
if (topleft) {
vSharedA.v += vAmount;
vCornerA.v += vAmount;
}
vCornerB.v -= vAmount;
vSharedB.v -= vAmount;
vCornerB.y -= yAmount;
vSharedB.y -= yAmount;
}
}
}
@@ -797,8 +801,13 @@ static SoftwareTransformAction ProjectClipAndExpand(SoftwareTransformParams &par
// Let's go look for pixel mapping.
const bool flat = ((u32)params.clipInfoFlags & ((u32)(ClipInfoFlags::Valid | ClipInfoFlags::FlatZ))) == (u32)(ClipInfoFlags::Valid | ClipInfoFlags::FlatZ);
const bool lookForPixelMapping = throughmode || flat;
if (lookForPixelMapping && g_Config.bSmart2DTexFiltering && !gstate_c.textureIsVideo) {
const bool lookForPixelMapping = flat && gstate.isMagnifyFilteringEnabled();
// TODO: We should probably take uv scale into account?
const float uScale = gstate_c.curTextureWidth;
const float vScale = gstate_c.curTextureHeight;
bool pixelMapped = true;
if (lookForPixelMapping && !gstate_c.textureIsVideo) {
// We check some common cases for pixel mapping.
//
// It's enough to check UV deltas vs pos deltas between vertex pairs:
@@ -807,43 +816,43 @@ static SoftwareTransformAction ProjectClipAndExpand(SoftwareTransformParams &par
// so the operations are exact.
//
// Additionally, we check for sprite lists. These are used for example in GTA.
const float spriteBorderFix = PSP_CoreParameter().compat.flags().SpriteBorderFix; // if != 0.0, apply border fix.
bool pixelMapped = true;
const u16 *indsIn = (const u16 *)inds;
const float uScale = gstate_c.curTextureWidth;
const float vScale = gstate_c.curTextureHeight;
// This assumes that we have a list of two-triangle sprites. If not the position checks will fail anyway.
bool firstTriangleInQuad = true;
for (int t = 0; t < vertexCount; t += 3) {
struct { int a; int b; } pairs[] = {{0, 1}, {1, 2}, {2, 0}};
for (int i = 0; i < ARRAY_SIZE(pairs); i++) {
int a = indsIn[t + pairs[i].a];
int b = indsIn[t + pairs[i].b];
float du = fabsf((transformed[a].u - transformed[b].u) * uScale);
float dv = fabsf((transformed[a].v - transformed[b].v) * vScale);
float dx = fabsf(transformed[a].x - transformed[b].x);
float dy = fabsf(transformed[a].y - transformed[b].y);
const int a = indsIn[t + pairs[i].a];
const int b = indsIn[t + pairs[i].b];
const float du = fabsf((transformed[a].u - transformed[b].u) * uScale);
const float dv = fabsf((transformed[a].v - transformed[b].v) * vScale);
const float dx = fabsf(transformed[a].x - transformed[b].x);
const float dy = fabsf(transformed[a].y - transformed[b].y);
if (du != dx || dv != dy) {
pixelMapped = false;
}
}
if (!pixelMapped && spriteBorderFix == 0.0f) {
if (!pixelMapped) {
// Early out. Later add an early out for sprite border fix too.
break;
}
if (!firstTriangleInQuad && spriteBorderFix != 1.0f) {
// The previous triangle started three vertices ago. Now, let's do some disgustingly hacky checks,
// to identify the type of sprite (if any) and move the UV coordinates inwards a bit.
const u16 *quad = indsIn + t - 3;
ApplySpriteBorderFix(transformed, quad, uScale, vScale, spriteBorderFix);
}
firstTriangleInQuad = !firstTriangleInQuad;
}
result->pixelMapped = pixelMapped;
}
// Apply the sprite border fix, but only if pixel mapping was not detected!
if (flat) {
const float spriteBorderFix = ShouldApplySpriteBorderFix(gstate) ? PSP_CoreParameter().compat.flags().SpriteBorderFix : 0.0f; // if != 0.0, apply border fix.
if (spriteBorderFix != 0.0f) {
// This assumes that we have a list of two-triangle sprites. If not the position checks will fail anyway.
const u16 *indsIn = (const u16 *)inds;
for (int t = 0; t < vertexCount - 5; t += 6) {
// The previous triangle started three vertices ago. Now, let's do some disgustingly hacky checks,
// to identify the type of sprite (if any) and move the UV coordinates inwards a bit.
const u16 *quad = indsIn + t;
ApplySpriteBorderFixTriangles(transformed, quad, uScale, vScale, spriteBorderFix);
}
}
}
if (!throughmode) {
// Culling and clipping needs to be done here, it doesn't happen in the shader in the case of software transform.
// However, fast culling should already have taken care of the Z<-W and Z>W culling, but we check for it on a per-triangle
@@ -997,15 +1006,36 @@ static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSiz
numDecodedVerts = 4 * (vertexCount / 2);
float uscale = 1.0f;
float vscale = 1.0f;
float uScale = 1.0f;
float vScale = 1.0f;
if (throughmode) {
uscale /= gstate_c.curTextureWidth;
vscale /= gstate_c.curTextureHeight;
uScale /= gstate_c.curTextureWidth;
vScale /= gstate_c.curTextureHeight;
}
bool pixelMapped = g_Config.bSmart2DTexFiltering && !gstate_c.textureIsVideo;
float spriteBorderFixL = 0.0f;
float spriteBorderFixR = 0.0f;
float spriteBorderFixT = 0.0f;
float spriteBorderFixB = 0.0f;
float spriteBorderFix = PSP_CoreParameter().compat.flags().SpriteBorderFix;
if (spriteBorderFix && !ShouldApplySpriteBorderFix(gstate)) {
spriteBorderFix = 0.0f;
} else {
if (spriteBorderFix < 0.0f) {
spriteBorderFixL = (spriteBorderFix / uScale) / gstate_c.curTextureWidth;
spriteBorderFixT = (spriteBorderFix / vScale) / gstate_c.curTextureHeight;
spriteBorderFixR = (spriteBorderFix / uScale) / gstate_c.curTextureWidth;
spriteBorderFixB = (spriteBorderFix / vScale) / gstate_c.curTextureHeight;
} else if (spriteBorderFix > 0.0f) {
spriteBorderFixL = 0.0f;
spriteBorderFixR = (spriteBorderFix / uScale) / gstate_c.curTextureWidth;
spriteBorderFixT = 0.0f;
spriteBorderFixB = (spriteBorderFix / vScale) / gstate_c.curTextureHeight;
}
}
for (int i = 0; i < vertexCount; i += 2) {
const TransformedVertex &transVtxTL = transformed[indsIn[i + 0]];
const TransformedVertex &transVtxBR = transformed[indsIn[i + 1]];
@@ -1025,6 +1055,7 @@ static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSiz
float z = transVtxBR.z;
// Apply Z clamping. It appears clipping/culling does not affect rectangles, see #12058.
// TODO: We might want to make this 65536.999. Since those will pass, and if a game mixes through and non-through...
if (z > 65535.0f) {
z = 65535.0f;
} else if (z < 0.0f) {
@@ -1036,33 +1067,34 @@ static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSiz
// bottom right
trans[0] = transVtxBR;
trans[0].u = transVtxBR.u * uscale;
trans[0].v = transVtxBR.v * vscale;
trans[0].u = (transVtxBR.u + spriteBorderFixR) * uScale;
trans[0].v = (transVtxBR.v + spriteBorderFixB) * vScale;
trans[0].z = z;
// top right
trans[1] = transVtxBR;
trans[1].y = transVtxTL.y;
trans[1].u = transVtxBR.u * uscale;
trans[1].v = transVtxTL.v * vscale;
trans[1].u = (transVtxBR.u + spriteBorderFixR) * uScale;
trans[1].v = (transVtxTL.v - spriteBorderFixT) * vScale;
trans[1].z = z;
// top left
trans[2] = transVtxBR;
trans[2].x = transVtxTL.x;
trans[2].y = transVtxTL.y;
trans[2].u = transVtxTL.u * uscale;
trans[2].v = transVtxTL.v * vscale;
trans[2].u = (transVtxTL.u - spriteBorderFixL) * uScale;
trans[2].v = (transVtxTL.v - spriteBorderFixT) * vScale;
trans[2].z = z;
// bottom left
trans[3] = transVtxBR;
trans[3].x = transVtxTL.x;
trans[3].u = transVtxTL.u * uscale;
trans[3].v = transVtxBR.v * vscale;
trans[3].u = (transVtxTL.u - spriteBorderFixL) * uScale;
trans[3].v = (transVtxBR.v + spriteBorderFixB) * vScale;
trans[3].z = z;
// 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);
// Triangle: BR-TR-TL
+1 -1
View File
@@ -695,7 +695,7 @@ TexCacheEntry *TextureCacheCommon::SetTexture() {
gstate_c.curTextureWidth = w;
gstate_c.curTextureHeight = h;
gstate_c.SetTextureIsVideo(false);
gstate_c.SetTextureIsVideo((entry->status & TexCacheEntry::STATUS_VIDEO) != 0);
gstate_c.SetTextureIs3D((entry->status & TexCacheEntry::STATUS_3D) != 0);
gstate_c.SetTextureIsArray(false); // Ordinary 2D textures still aren't used by array view in VK. We probably might as well, though, at this point..
gstate_c.SetTextureIsFramebuffer(false);
+5 -5
View File
@@ -997,10 +997,12 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
}
}
// Through mode early-out for simple float 2D draws, like in Fate Extra CCC (very beneficial there due to avoiding texture loads)
if ((vertexType & (GE_VTYPE_THROUGH_MASK | GE_VTYPE_POS_MASK | GE_VTYPE_IDX_MASK)) == (GE_VTYPE_THROUGH_MASK | GE_VTYPE_POS_FLOAT | GE_VTYPE_IDX_NONE)) {
// Through mode early-out. Very beneficial for Fate Extra CCC (very beneficial there due to avoiding texture loads)
// Also we take the opportunity to check for flat draws, where we can detect sprites (to fix filter artifacts).
ClipInfoFlags flags{};
if (gstate.isModeThrough()) {
int bytesRead = 0;
if (!drawEngineCommon_->TestBoundingBoxThrough(verts, count, decoder, vertexType, &bytesRead)) {
if (!drawEngineCommon_->TestBoundingBoxThrough(prim, verts, inds, count, decoder, vertexType, &bytesRead, &flags)) {
gpuStats.perFrame.numCulledDraws++;
int cycles = vertexCost_ * count;
gpuStats.perFrame.vertexGPUCycles += cycles;
@@ -1024,8 +1026,6 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
#endif
ClipInfoFlags flags{};
// If certain conditions are true, do frustum culling.
bool passCulling = PASSES_CULLING;
if (!passCulling) {
+2 -2
View File
@@ -106,7 +106,7 @@ android {
}
}
compileSdk = 36
compileSdk = 37
ndkVersion = "29.0.14206865"
compileOptions {
@@ -132,7 +132,7 @@ android {
file("versioncode.txt").writeText(gitVersionCode.toString())
minSdk = 21
targetSdk = 36
targetSdk = 37
if (project.hasProperty("ANDROID_VERSION_CODE") && project.hasProperty("ANDROID_VERSION_NAME")) {
versionCode = (project.property("ANDROID_VERSION_CODE") as String).toInt()
versionName = project.property("ANDROID_VERSION_NAME") as String
+4
View File
@@ -65,6 +65,7 @@
"location": "South Africa",
"location-emoji": "🇿🇦",
"description": "For players looking to play any games",
"status_data_json": "https://relay-sa.arenaanywhere.site/data.json",
"data_mode": "AemuPostoffice"
},
{
@@ -76,6 +77,7 @@
"location": "Europe",
"location-emoji": "🇪🇺",
"description": "For players looking to play any games",
"status_data_json": "https://relay.arenaanywhere.site/data.json",
"data_mode": "AemuPostoffice"
},
{
@@ -84,6 +86,7 @@
"discord": "https://discord.gg/GdsXWmNHq5",
"web": "https://arenaanywhere.site/lobby-status.html",
"location": "US",
"location-emoji": "🇺🇸",
"description": "For players looking to play any games",
"data_mode": "AemuPostoffice"
},
@@ -96,6 +99,7 @@
"location": "Singapore",
"location-emoji": "🇸🇬",
"description": "For players looking to play any games",
"status_data_json": "https://relay-asia.arenaanywhere.site/data.json",
"data_mode": "AemuPostoffice"
},
{
+7
View File
@@ -2090,6 +2090,9 @@ ULES00969 = true
# NOTE: This is currently tuned only for GTA, but may work with some other games too with filtering
# problems. 0.5 is half a texel, which seems to be enough to smooth everything out.
# NOTE: Negative values are interpreted as applying from both top, left, bottom and right,
# while positive values only apply to bottom and right. Different games need different measures.
# Grand Theft Auto: Vice City Stories
ULUS10160 = 0.5
ULES00502 = 0.5
@@ -2114,3 +2117,7 @@ ULUX80146 = 0.5
ULUS01826 = 0.5
# Seen in Liberty City (GTA LCS romhack)
ULUS11826 = 0.5
# Tales of Destiny 2
# Unfortunately, this affects game backgrounds negatively, although fixes lines in the menus.
ULJS00097 = -0.25