From 169f473b95883f40863dc01f5b44ecfd7e953e84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Thu, 4 Jun 2026 17:01:23 +0200 Subject: [PATCH] Smart 2D texture filtering: Use the new vertex path to check for pixel mapping in transformed geometry Previously, "Smart 2D texture filtering" was limited to through-mode draws, but many games use fully transformed draws to do 2D elements. An example of this is Outrun in the menus, and it's always been plagued by horrible filtering artifacts. With the improved Smart 2D texture filtering, the artifacts are gone! Of course, instead of glitchy bilinear you instead get point sampled texturing so it looks more pixellated, but no more ugly borders between letters in the game. --- GPU/Common/DrawEngineCommon.cpp | 5 ++ GPU/Common/DrawEngineCommon.h | 10 --- GPU/Common/SoftwareTransformCommon.cpp | 71 ++++++++++---------- GPU/Common/SoftwareTransformCommon.h | 1 + GPU/D3D11/DrawEngineD3D11.cpp | 1 + GPU/GLES/DrawEngineGLES.cpp | 1 + GPU/GPUCommonHW.cpp | 8 +-- GPU/GPUDefinitions.h | 11 ++++ GPU/Vulkan/DrawEngineVulkan.cpp | 3 +- GPU/ge_constants.h | 89 +++++++++++++------------- 10 files changed, 109 insertions(+), 91 deletions(-) diff --git a/GPU/Common/DrawEngineCommon.cpp b/GPU/Common/DrawEngineCommon.cpp index 0148d95125..a6908b2553 100644 --- a/GPU/Common/DrawEngineCommon.cpp +++ b/GPU/Common/DrawEngineCommon.cpp @@ -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; diff --git a/GPU/Common/DrawEngineCommon.h b/GPU/Common/DrawEngineCommon.h index a11e8c389f..4e38c5a7b1 100644 --- a/GPU/Common/DrawEngineCommon.h +++ b/GPU/Common/DrawEngineCommon.h @@ -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(); diff --git a/GPU/Common/SoftwareTransformCommon.cpp b/GPU/Common/SoftwareTransformCommon.cpp index cd56bdfd89..ba839e13b6 100644 --- a/GPU/Common/SoftwareTransformCommon.cpp +++ b/GPU/Common/SoftwareTransformCommon.cpp @@ -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. diff --git a/GPU/Common/SoftwareTransformCommon.h b/GPU/Common/SoftwareTransformCommon.h index f1e1c8f20c..7943658fd0 100644 --- a/GPU/Common/SoftwareTransformCommon.h +++ b/GPU/Common/SoftwareTransformCommon.h @@ -62,6 +62,7 @@ struct SoftwareTransformParams { u8 *decoded; TransformedVertex *transformed; TransformedVertex *transformedExpanded; + ClipInfoFlags clipInfoFlags; bool allowClear; bool allowSeparateAlphaClear; bool everUsedEqualDepth; diff --git a/GPU/D3D11/DrawEngineD3D11.cpp b/GPU/D3D11/DrawEngineD3D11.cpp index db77aaa012..48bb621421 100644 --- a/GPU/D3D11/DrawEngineD3D11.cpp +++ b/GPU/D3D11/DrawEngineD3D11.cpp @@ -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); diff --git a/GPU/GLES/DrawEngineGLES.cpp b/GPU/GLES/DrawEngineGLES.cpp index c798fd7f19..f04d7fb88d 100644 --- a/GPU/GLES/DrawEngineGLES.cpp +++ b/GPU/GLES/DrawEngineGLES.cpp @@ -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); diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index b4c79808a9..c2ee12c663 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -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); } diff --git a/GPU/GPUDefinitions.h b/GPU/GPUDefinitions.h index ea051fdba5..e15cdae392 100644 --- a/GPU/GPUDefinitions.h +++ b/GPU/GPUDefinitions.h @@ -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 { diff --git a/GPU/Vulkan/DrawEngineVulkan.cpp b/GPU/Vulkan/DrawEngineVulkan.cpp index 77c3602b94..1567e608bf 100644 --- a/GPU/Vulkan/DrawEngineVulkan.cpp +++ b/GPU/Vulkan/DrawEngineVulkan.cpp @@ -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); diff --git a/GPU/ge_constants.h b/GPU/ge_constants.h index 15d47b0929..c065bb5a99 100644 --- a/GPU/ge_constants.h +++ b/GPU/ge_constants.h @@ -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,