Merge pull request #21784 from hrydgard/smarter-2d-filter

Smart 2D texture filtering: Use the new vertex path to check for pixel mapping in transformed geometry
This commit is contained in:
Henrik Rydgård
2026-06-04 17:24:40 +02:00
committed by GitHub
10 changed files with 109 additions and 91 deletions
+5
View File
@@ -461,6 +461,11 @@ static bool TestBoundingBoxFast(const float *cullMatrix, const void *vdata, cons
flags |= ClipInfoFlags::SoftClipCull;
}
if (minProjZ == maxProjZ) {
// Probably a 2D draw. Send it through software transform!
flags |= ClipInfoFlags::FlatZ | ClipInfoFlags::SoftClipCull;
}
if (needFragmentDepthClamp() && (minProjZ < 0 || maxProjZ > 65535)) {
if (gstate_c.Use(GPU_USE_DEPTH_CLAMP)) {
flags |= ClipInfoFlags::DepthClamp;
-10
View File
@@ -73,16 +73,6 @@ struct alignas(16) Plane8 {
float Test(int i, const float f[3]) const { return x[i] * f[0] + y[i] * f[1] + z[i] * f[2] + w[i]; }
};
enum class ClipInfoFlags {
Valid = 1,
SoftClipCull = 2,
DepthClamp = 8,
DepthClampFragment = 16,
MinMaxZClip = 32,
MinMaxZDiscard = 64,
};
ENUM_CLASS_BITOPS(ClipInfoFlags);
class DrawEngineCommon {
public:
DrawEngineCommon();
+38 -33
View File
@@ -26,6 +26,7 @@
#include "Core/System.h"
#include "GPU/GPUState.h"
#include "GPU/Math3D.h"
#include "GPU/GPUDefinitions.h"
#include "GPU/GPUStateSIMDUtil.h"
#include "GPU/Common/FramebufferManagerCommon.h"
#include "GPU/Common/GPUStateUtils.h"
@@ -667,44 +668,48 @@ static SoftwareTransformAction ProjectClipAndExpand(SoftwareTransformParams &par
// We might actually write more vertics at the end of transformed.
drawIndexCount = vertexCount;
result->pixelMapped = false;
if (throughmode) {
// Nothing to do, pass the vertices right through as-is. Well, we can go look for pixel mapping,
// but we don't do any projection, culling or clipping.
if (g_Config.bSmart2DTexFiltering && !gstate_c.textureIsVideo) {
// We check some common cases for pixel mapping.
// TODO: It's not really optimal that some previous step has removed the triangle strip.
if (vertexCount <= 6 && prim == GE_PRIM_TRIANGLES) {
// It's enough to check UV deltas vs pos deltas between vertex pairs:
// 0-1 1-3 3-2 2-0. Maybe can even skip the last one. Probably some simple math can get us that sequence.
// Unfortunately we need to reverse the previous UV scaling operation. Fortunately these are powers of two
// so the operations are exact.
bool pixelMapped = true;
const u16 *indsIn = (const u16 *)inds;
const float uscale = gstate_c.curTextureWidth;
const float vscale = gstate_c.curTextureHeight;
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);
if (du != dx || dv != dy) {
pixelMapped = false;
}
}
if (!pixelMapped) {
break;
}
// Let's go look for pixel mapping.
bool lookForPixelMapping = throughmode;
if (!lookForPixelMapping) {
// If not throughmode, we can still have pixel mapping if the clip info is valid and flat Z, since that means no clipping or perspective correction will be applied.
if (((u32)params.clipInfoFlags & ((u32)(ClipInfoFlags::Valid | ClipInfoFlags::FlatZ))) == (u32)(ClipInfoFlags::Valid | ClipInfoFlags::FlatZ)) {
lookForPixelMapping = true;
}
}
if (lookForPixelMapping && g_Config.bSmart2DTexFiltering && !gstate_c.textureIsVideo) {
// We check some common cases for pixel mapping.
// It's enough to check UV deltas vs pos deltas between vertex pairs:
// 0-1 1-3 3-2 2-0. Maybe can even skip the last one. Probably some simple math can get us that sequence.
// Unfortunately we need to reverse the previous UV scaling operation. Fortunately these are powers of two
// so the operations are exact.
bool pixelMapped = true;
const u16 *indsIn = (const u16 *)inds;
const float uscale = gstate_c.curTextureWidth;
const float vscale = gstate_c.curTextureHeight;
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);
if (du != dx || dv != dy) {
pixelMapped = false;
}
result->pixelMapped = pixelMapped;
}
if (!pixelMapped) {
break;
}
}
} else {
result->pixelMapped = pixelMapped;
}
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
// basis here anyway.
+1
View File
@@ -62,6 +62,7 @@ struct SoftwareTransformParams {
u8 *decoded;
TransformedVertex *transformed;
TransformedVertex *transformedExpanded;
ClipInfoFlags clipInfoFlags;
bool allowClear;
bool allowSeparateAlphaClear;
bool everUsedEqualDepth;
+1
View File
@@ -436,6 +436,7 @@ void DrawEngineD3D11::Flush() {
params.transformedExpanded = transformedExpanded_;
params.allowClear = true;
params.allowSeparateAlphaClear = false; // D3D11 doesn't support separate alpha clears
params.clipInfoFlags = clipInfoFlags_;
const SoftwareTransformAction action = RunSoftwareTransform(params, prim, swDec->VertexType(), swDec->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
+1
View File
@@ -388,6 +388,7 @@ void DrawEngineGLES::Flush() {
params.transformedExpanded = transformedExpanded_;
params.allowClear = true; // Clear in OpenGL respects scissor rects, so we'll use it.
params.allowSeparateAlphaClear = true;
params.clipInfoFlags = clipInfoFlags_;
const SoftwareTransformAction action = RunSoftwareTransform(params, prim, swDec->VertexType(), swDec->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
+4 -4
View File
@@ -965,7 +965,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
const bool isTriangle = IsTrianglePrim(prim);
bool canExtend = isTriangle;
u32 vertexType = gstate.vertType;
GEVertexType vertexType = (GEVertexType)gstate.vertType;
if ((vertexType & GE_VTYPE_IDX_MASK) != GE_VTYPE_IDX_NONE) {
u32 indexAddr = gstate_c.indexAddr;
const int indexShift = ((vertexType & GE_VTYPE_IDX_MASK) >> GE_VTYPE_IDX_SHIFT) - 1;
@@ -1046,7 +1046,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
if (!drawEngineCommon_->SubmitPrim(verts, inds, prim, count, decoder, vertTypeID, true, &bytesRead, flags)) {
canExtend = false;
}
onePassed = true;
// onePassed = true;
} else {
// Still need to advance bytesRead.
drawEngineCommon_->SkipPrim(prim, count, decoder, &bytesRead);
@@ -1131,7 +1131,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
canExtend = false;
}
// As soon as one passes, assume we don't need to check the rest of this batch.
onePassed = true;
// onePassed = true;
} else {
// Still need to advance bytesRead.
drawEngineCommon_->SkipPrim(newPrim, count, decoder, &bytesRead);
@@ -1150,7 +1150,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) {
goto bail;
drawEngineCommon_->FlushSkin();
canExtend = false; // TODO: Might support extending between some vertex types in the future.
vertexType = data;
vertexType = (GEVertexType)(data);
vertTypeID = GetVertTypeID(vertexType, gstate.getUVGenMode(), g_Config.bSoftwareSkinning);
decoder = drawEngineCommon_->GetVertexDecoder(vertTypeID);
}
+11
View File
@@ -195,6 +195,17 @@ enum {
FLAG_DIRTYONCHANGE = 64, // NOTE: Either this or FLAG_EXECUTE*, not both!
};
enum class ClipInfoFlags {
Valid = 1,
SoftClipCull = 2,
FlatZ = 4,
DepthClamp = 8,
DepthClampFragment = 16,
MinMaxZClip = 32,
MinMaxZDiscard = 64,
};
ENUM_CLASS_BITOPS(ClipInfoFlags);
struct TransformedVertex {
union {
struct {
+2 -1
View File
@@ -297,7 +297,7 @@ void DrawEngineVulkan::Flush() {
gstate_c.vertexFullAlpha = gstate_c.vertexFullAlpha && ((hasColor && (gstate.materialupdate & 1)) || gstate.getMaterialAmbientA() == 255) && (!gstate.isLightingEnabled() || gstate.getAmbientA() == 255);
}
if (textureNeedsApply) {
if (textureNeedsApply || true) {
textureCache_->ApplyTexture();
textureCache_->GetVulkanHandles(imageView, sampler);
if (imageView == VK_NULL_HANDLE)
@@ -449,6 +449,7 @@ void DrawEngineVulkan::Flush() {
params.allowClear = framebufferManager_->UseBufferedRendering();
params.allowSeparateAlphaClear = false;
params.everUsedEqualDepth = everUsedEqualDepth_;
params.clipInfoFlags = clipInfoFlags_;
const SoftwareTransformAction action = RunSoftwareTransform(params, prim, swDec->VertexType(), swDec->GetDecVtxFmt(), numDecodedVerts_, VERTEX_BUFFER_MAX, vertexCount, inds, RemainingIndices(inds), &result);
+46 -43
View File
@@ -280,58 +280,60 @@ enum GECommand : uint8_t {
const char *GeCmdToString(GECommand cmd);
#define GE_VTYPE_TRANSFORM (0<<23)
#define GE_VTYPE_THROUGH (1<<23)
#define GE_VTYPE_THROUGH_MASK (1<<23)
enum GEVertexType : uint32_t {
GE_VTYPE_TRANSFORM = (0<<23),
GE_VTYPE_THROUGH = (1<<23),
GE_VTYPE_THROUGH_MASK = (1<<23),
#define GE_VTYPE_TC_NONE (0<<0)
#define GE_VTYPE_TC_8BIT (1<<0)
#define GE_VTYPE_TC_16BIT (2<<0)
#define GE_VTYPE_TC_FLOAT (3<<0)
#define GE_VTYPE_TC_MASK (3<<0)
#define GE_VTYPE_TC_SHIFT 0
GE_VTYPE_TC_NONE = (0<<0),
GE_VTYPE_TC_8BIT = (1<<0),
GE_VTYPE_TC_16BIT = (2<<0),
GE_VTYPE_TC_FLOAT = (3<<0),
GE_VTYPE_TC_MASK = (3<<0),
GE_VTYPE_TC_SHIFT = 0,
#define GE_VTYPE_COL_NONE (0<<2)
#define GE_VTYPE_COL_565 (4<<2)
#define GE_VTYPE_COL_5551 (5<<2)
#define GE_VTYPE_COL_4444 (6<<2)
#define GE_VTYPE_COL_8888 (7<<2)
#define GE_VTYPE_COL_MASK (7<<2)
#define GE_VTYPE_COL_SHIFT 2
GE_VTYPE_COL_NONE = (0<<2),
GE_VTYPE_COL_565 = (4<<2),
GE_VTYPE_COL_5551 = (5<<2),
GE_VTYPE_COL_4444 = (6<<2),
GE_VTYPE_COL_8888 = (7<<2),
GE_VTYPE_COL_MASK = (7<<2),
GE_VTYPE_COL_SHIFT = 2,
#define GE_VTYPE_NRM_NONE (0<<5)
#define GE_VTYPE_NRM_8BIT (1<<5)
#define GE_VTYPE_NRM_16BIT (2<<5)
#define GE_VTYPE_NRM_FLOAT (3<<5)
#define GE_VTYPE_NRM_MASK (3<<5)
#define GE_VTYPE_NRM_SHIFT 5
GE_VTYPE_NRM_NONE = (0<<5),
GE_VTYPE_NRM_8BIT = (1<<5),
GE_VTYPE_NRM_16BIT = (2<<5),
GE_VTYPE_NRM_FLOAT = (3<<5),
GE_VTYPE_NRM_MASK = (3<<5),
GE_VTYPE_NRM_SHIFT = 5,
// No NONE, there is always a position.
#define GE_VTYPE_POS_8BIT (1<<7)
#define GE_VTYPE_POS_16BIT (2<<7)
#define GE_VTYPE_POS_FLOAT (3<<7)
#define GE_VTYPE_POS_MASK (3<<7)
#define GE_VTYPE_POS_SHIFT 7
// No NONE, there is always a position.
GE_VTYPE_POS_8BIT = (1<<7),
GE_VTYPE_POS_16BIT = (2<<7),
GE_VTYPE_POS_FLOAT = (3<<7),
GE_VTYPE_POS_MASK = (3<<7),
GE_VTYPE_POS_SHIFT = 7,
#define GE_VTYPE_WEIGHT_NONE (0<<9)
#define GE_VTYPE_WEIGHT_8BIT (1<<9)
#define GE_VTYPE_WEIGHT_16BIT (2<<9)
#define GE_VTYPE_WEIGHT_FLOAT (3<<9)
#define GE_VTYPE_WEIGHT_MASK (3<<9)
#define GE_VTYPE_WEIGHT_SHIFT 9
GE_VTYPE_WEIGHT_NONE = (0<<9),
GE_VTYPE_WEIGHT_8BIT = (1<<9),
GE_VTYPE_WEIGHT_16BIT = (2<<9),
GE_VTYPE_WEIGHT_FLOAT = (3<<9),
GE_VTYPE_WEIGHT_MASK = (3<<9),
GE_VTYPE_WEIGHT_SHIFT = 9,
#define GE_VTYPE_WEIGHTCOUNT_MASK (7<<14)
#define GE_VTYPE_WEIGHTCOUNT_SHIFT 14
GE_VTYPE_WEIGHTCOUNT_MASK = (7<<14),
GE_VTYPE_WEIGHTCOUNT_SHIFT = 14,
#define GE_VTYPE_MORPHCOUNT_MASK (7<<18)
#define GE_VTYPE_MORPHCOUNT_SHIFT 18
GE_VTYPE_MORPHCOUNT_MASK = (7<<18),
GE_VTYPE_MORPHCOUNT_SHIFT = 18,
#define GE_VTYPE_IDX_NONE (0<<11)
#define GE_VTYPE_IDX_8BIT (1<<11)
#define GE_VTYPE_IDX_16BIT (2<<11)
#define GE_VTYPE_IDX_32BIT (3<<11)
#define GE_VTYPE_IDX_MASK (3<<11)
GE_VTYPE_IDX_NONE = (0<<11),
GE_VTYPE_IDX_8BIT = (1<<11),
GE_VTYPE_IDX_16BIT = (2<<11),
GE_VTYPE_IDX_32BIT = (3<<11),
GE_VTYPE_IDX_MASK = (3<<11),
#define GE_VTYPE_IDX_SHIFT 11
};
#define GE_CLEARMODE_COLOR (1<<8)
#define GE_CLEARMODE_ALPHA (1<<9) //or stencil?
@@ -347,6 +349,7 @@ const char *GeCmdToString(GECommand cmd);
#define GE_IMM_FOG 0x00400000
#define GE_IMM_DITHER 0x00800000
enum GEMatrixType {
GE_MTX_BONE0 = 0,
GE_MTX_BONE1,