From 8be9cbbe5469b205214f77bd4373670d8a8a5d97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 2 Nov 2026 23:25:04 +0100 Subject: [PATCH 1/2] Depal: Skip the bilinear filter on exactly pixel-mapped draws Shader depal filters by hand, with four texel and four palette lookups per pixel. DBZ Tenkaichi Tag Team does two full-screen through-mode depal passes per frame, and on a PowerVR GE8320 (Galaxy A21) they took DBZ from 8.3 to 14.4 seconds per 10 emulated seconds once #22204 had made framebuffer depal work again. They're 1:1, where bilinear gives the nearest texel anyway, so rectangles that put every pixel center on a texel center (equal UV and position deltas, at a whole-texel offset) now count as pixel mapped for depal, which turns the filter off. Unlike the smart 2D filtering check, this changes nothing on screen: the frame dumps render identically on Vulkan and OpenGL, 69 of them through this path. DBZ is down to 9.2s. The depal uniform is also repacked when pixelMapped changes between draws, rather than only when the texture is reapplied. Co-Authored-By: Claude Opus 5.5 --- GPU/Common/ShaderUniforms.cpp | 7 +++++++ GPU/Common/ShaderUniforms.h | 2 ++ GPU/Common/SoftwareTransformCommon.cpp | 25 ++++++++++++++++++++++++- GPU/D3D11/ShaderManagerD3D11.cpp | 1 + GPU/GLES/ShaderManagerGLES.cpp | 1 + GPU/GPUState.h | 2 ++ GPU/Metal/ShaderManagerMetal.mm | 1 + GPU/Vulkan/ShaderManagerVulkan.cpp | 1 + 8 files changed, 39 insertions(+), 1 deletion(-) diff --git a/GPU/Common/ShaderUniforms.cpp b/GPU/Common/ShaderUniforms.cpp index d2525a5f5e..7bb4f6b92a 100644 --- a/GPU/Common/ShaderUniforms.cpp +++ b/GPU/Common/ShaderUniforms.cpp @@ -180,7 +180,14 @@ void BaseUpdateUniforms(UB_VS_FS_Base *ub, uint64_t dirtyUniforms, bool useBuffe } } +void DirtyDepalIfPixelMappedChanged(bool pixelMapped) { + if (gstate_c.shaderDepalMode != ShaderDepalMode::OFF && pixelMapped != gstate_c.depalPixelMapped) { + gstate_c.Dirty(DIRTY_DEPAL); + } +} + uint32_t PackDepalBits(bool pixelMapped) { + gstate_c.depalPixelMapped = pixelMapped; const int indexMask = gstate.getClutIndexMask(); const int indexShift = gstate.getClutIndexShift(); const int indexOffset = gstate.getClutIndexStartPos() >> 4; diff --git a/GPU/Common/ShaderUniforms.h b/GPU/Common/ShaderUniforms.h index f387978d0d..e85fc16b83 100644 --- a/GPU/Common/ShaderUniforms.h +++ b/GPU/Common/ShaderUniforms.h @@ -110,6 +110,8 @@ void LightUpdateUniforms(UB_VS_Lights *ub, uint64_t dirtyUniforms); uint32_t PackLightControlBits(); uint32_t PackDepalBits(bool pixelMapped); +// Call before reading the dirty uniforms: the depal uniform depends on pixelMapped, which changes per draw. +void DirtyDepalIfPixelMappedChanged(bool pixelMapped); void UpdateFogCoef(const GEState &state, float fogCoef[2]); diff --git a/GPU/Common/SoftwareTransformCommon.cpp b/GPU/Common/SoftwareTransformCommon.cpp index 745c03c250..d8c43ded07 100644 --- a/GPU/Common/SoftwareTransformCommon.cpp +++ b/GPU/Common/SoftwareTransformCommon.cpp @@ -1095,6 +1095,29 @@ static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSiz } } + // Shader depal filters by hand, with four texel and four palette lookups per pixel, which is a lot for a slow + // GPU on a full-screen pass (DBZ Tenkaichi Tag Team has two per frame). When every pixel center lands on a texel + // center (1:1 in both directions, at a whole-texel offset), that gives the nearest texel anyway, so report the + // draw as pixel mapped, which turns the filter off. Unlike the check above, this changes nothing on screen. + bool depalPixelMapped = false; + if (!pixelMapped && throughmode && gstate_c.shaderDepalMode != ShaderDepalMode::OFF && + spriteBorderFixL == 0.0f && spriteBorderFixR == 0.0f && spriteBorderFixT == 0.0f && spriteBorderFixB == 0.0f) { + depalPixelMapped = true; + for (int i = 0; i < vertexCount; i += 2) { + const TransformedVertex &transVtxTL = transformed[indsIn[i + 0]]; + const TransformedVertex &transVtxBR = transformed[indsIn[i + 1]]; + const float dx = transVtxBR.x - transVtxTL.x; + const float dy = transVtxBR.y - transVtxTL.y; + const float offsetU = transVtxTL.u - transVtxTL.x; + const float offsetV = transVtxTL.v - transVtxTL.y; + if (dx <= 0 || dy <= 0 || transVtxBR.u - transVtxTL.u != dx || transVtxBR.v - transVtxTL.v != dy || + offsetU != floorf(offsetU) || offsetV != floorf(offsetV)) { + depalPixelMapped = false; + break; + } + } + } + // Like the software renderer's ProcessRect: rectangles aren't clipped, one with a vertex behind the camera // or outside the screen range is dropped whole. With depth clamp, a vertex past the near plane is exempt @@ -1213,7 +1236,7 @@ static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSiz } numDecodedVerts = outVerts; inds = newInds; - *pixelMappedExactly = pixelMapped; + *pixelMappedExactly = pixelMapped || depalPixelMapped; *drawIndexCount = indsOut - newInds; return true; } diff --git a/GPU/D3D11/ShaderManagerD3D11.cpp b/GPU/D3D11/ShaderManagerD3D11.cpp index ef0bfc08f3..b7db0c056c 100644 --- a/GPU/D3D11/ShaderManagerD3D11.cpp +++ b/GPU/D3D11/ShaderManagerD3D11.cpp @@ -149,6 +149,7 @@ void ShaderManagerD3D11::ClearShaders() { } uint64_t ShaderManagerD3D11::UpdateUniforms(bool useBufferedRendering, bool pixelMapped) { + DirtyDepalIfPixelMappedChanged(pixelMapped); uint64_t dirty = gstate_c.GetDirtyUniforms(); if (dirty != 0) { D3D11_MAPPED_SUBRESOURCE map; diff --git a/GPU/GLES/ShaderManagerGLES.cpp b/GPU/GLES/ShaderManagerGLES.cpp index e64ab6d1dd..5d1191af4f 100644 --- a/GPU/GLES/ShaderManagerGLES.cpp +++ b/GPU/GLES/ShaderManagerGLES.cpp @@ -717,6 +717,7 @@ Shader *ShaderManagerGLES::ApplyVertexShader(bool useHWTransform, u32 vertexType } LinkedShader *ShaderManagerGLES::ApplyFragmentShader(VShaderID VSID, Shader *vs, const ComputedPipelineState &pipelineState, ClipInfoFlags clipInfoFlags, bool pixelMapped) { + DirtyDepalIfPixelMappedChanged(pixelMapped); uint64_t dirty = gstate_c.GetDirtyUniforms(); if (dirty) { if (lastShader_) diff --git a/GPU/GPUState.h b/GPU/GPUState.h index 3cdf60b567..b22c2fec11 100644 --- a/GPU/GPUState.h +++ b/GPU/GPUState.h @@ -682,6 +682,8 @@ public: ShaderDepalMode shaderDepalMode; GEBufferFormat depalTextureFormat; + // The pixelMapped that the depal uniform was last packed with (it turns bilinear depal off). + bool depalPixelMapped; void SetShaderDepal(ShaderDepalMode mode, GEBufferFormat texFormat = {}) { if (mode != shaderDepalMode || (mode != ShaderDepalMode::OFF && texFormat != depalTextureFormat)) { diff --git a/GPU/Metal/ShaderManagerMetal.mm b/GPU/Metal/ShaderManagerMetal.mm index 9cecf3bf9c..068b495d69 100644 --- a/GPU/Metal/ShaderManagerMetal.mm +++ b/GPU/Metal/ShaderManagerMetal.mm @@ -86,6 +86,7 @@ void ShaderManagerMetal::ClearShaders() { } uint64_t ShaderManagerMetal::UpdateUniforms(bool useBufferedRendering, bool pixelMapped) { + DirtyDepalIfPixelMappedChanged(pixelMapped); uint64_t dirty = gstate_c.GetDirtyUniforms(); if (dirty != 0) { if (dirty & DIRTY_BASE_UNIFORMS) diff --git a/GPU/Vulkan/ShaderManagerVulkan.cpp b/GPU/Vulkan/ShaderManagerVulkan.cpp index 6bcc8b4c00..7aec3872c7 100644 --- a/GPU/Vulkan/ShaderManagerVulkan.cpp +++ b/GPU/Vulkan/ShaderManagerVulkan.cpp @@ -218,6 +218,7 @@ void ShaderManagerVulkan::ClearShaders() { } uint64_t ShaderManagerVulkan::UpdateUniforms(bool useBufferedRendering, bool pixelMapped) { + DirtyDepalIfPixelMappedChanged(pixelMapped); uint64_t dirty = gstate_c.GetDirtyUniforms(); if (dirty != 0) { if (dirty & DIRTY_BASE_UNIFORMS) From 73450028736cd95719577ac9b7bd3ecc130b1c31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Mon, 2 Nov 2026 23:49:40 +0100 Subject: [PATCH 2/2] Software transform: Look for both kinds of pixel mapping in one loop The smart 2D filtering check and the exact one for depal walked the rectangles separately. Now one loop does both, stopping once neither can hold, and a rect of no size ends both. Co-Authored-By: Claude Opus 5.5 --- GPU/Common/SoftwareTransformCommon.cpp | 76 ++++++++++++-------------- 1 file changed, 34 insertions(+), 42 deletions(-) diff --git a/GPU/Common/SoftwareTransformCommon.cpp b/GPU/Common/SoftwareTransformCommon.cpp index d8c43ded07..792be7833d 100644 --- a/GPU/Common/SoftwareTransformCommon.cpp +++ b/GPU/Common/SoftwareTransformCommon.cpp @@ -1051,26 +1051,37 @@ static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSiz vScale /= gstate_c.curTextureHeight; } + // Look for pixel mapping, two ways at once: + // - pixelMapped (with smart 2D filtering): each rect is 1:1 in at least one direction. Turns the texture filter + // off and the sprite border fix with it. This one can change what's on screen. + // - exactlyPixelMapped: each rect is 1:1 in both directions at a whole-texel offset, so every pixel center lands + // on a texel center and bilinear gives the nearest texel anyway. Shader depal filters by hand, with four + // texel and four palette lookups per pixel, a lot for a slow GPU on a full-screen pass (DBZ Tenkaichi Tag + // Team has two per frame), so for it this turns the filter off without changing anything. Positions are only + // in pixels in through mode. bool pixelMapped = g_Config.bSmart2DTexFiltering && !gstate_c.textureIsVideo; - if (pixelMapped) { - // Check for pixel mapping. If we find pixel mapping, abandon spriteBorderFix. - for (int i = 0; i < vertexCount; i += 2) { - const TransformedVertex &transVtxTL = transformed[indsIn[i + 0]]; - const TransformedVertex &transVtxBR = transformed[indsIn[i + 1]]; - - if (pixelMapped) { - float dx = transVtxBR.x - transVtxTL.x; - float dy = transVtxBR.y - transVtxTL.y; - float du = transVtxBR.u - transVtxTL.u; - float dv = transVtxBR.v - transVtxTL.v; - - // NOTE: We will accept it as pixel mapped if only one dimension is stretched. This fixes dialog frames in FFI. - // Though, there could be false positives in other games due to this. Let's see if it is a problem... - if (dx <= 0 || dy <= 0 || (dx != du && dy != dv)) { - pixelMapped = false; - break; - } - } + bool exactlyPixelMapped = throughmode && gstate_c.shaderDepalMode != ShaderDepalMode::OFF; + for (int i = 0; i < vertexCount && (pixelMapped || exactlyPixelMapped); i += 2) { + const TransformedVertex &transVtxTL = transformed[indsIn[i + 0]]; + const TransformedVertex &transVtxBR = transformed[indsIn[i + 1]]; + const float dx = transVtxBR.x - transVtxTL.x; + const float dy = transVtxBR.y - transVtxTL.y; + const float du = transVtxBR.u - transVtxTL.u; + const float dv = transVtxBR.v - transVtxTL.v; + if (dx <= 0 || dy <= 0) { + pixelMapped = false; + exactlyPixelMapped = false; + break; + } + // NOTE: We will accept it as pixel mapped if only one dimension is stretched. This fixes dialog frames in FFI. + // Though, there could be false positives in other games due to this. Let's see if it is a problem... + if (dx != du && dy != dv) { + pixelMapped = false; + } + const float offsetU = transVtxTL.u - transVtxTL.x; + const float offsetV = transVtxTL.v - transVtxTL.y; + if (dx != du || dy != dv || offsetU != floorf(offsetU) || offsetV != floorf(offsetV)) { + exactlyPixelMapped = false; } } @@ -1095,30 +1106,11 @@ static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSiz } } - // Shader depal filters by hand, with four texel and four palette lookups per pixel, which is a lot for a slow - // GPU on a full-screen pass (DBZ Tenkaichi Tag Team has two per frame). When every pixel center lands on a texel - // center (1:1 in both directions, at a whole-texel offset), that gives the nearest texel anyway, so report the - // draw as pixel mapped, which turns the filter off. Unlike the check above, this changes nothing on screen. - bool depalPixelMapped = false; - if (!pixelMapped && throughmode && gstate_c.shaderDepalMode != ShaderDepalMode::OFF && - spriteBorderFixL == 0.0f && spriteBorderFixR == 0.0f && spriteBorderFixT == 0.0f && spriteBorderFixB == 0.0f) { - depalPixelMapped = true; - for (int i = 0; i < vertexCount; i += 2) { - const TransformedVertex &transVtxTL = transformed[indsIn[i + 0]]; - const TransformedVertex &transVtxBR = transformed[indsIn[i + 1]]; - const float dx = transVtxBR.x - transVtxTL.x; - const float dy = transVtxBR.y - transVtxTL.y; - const float offsetU = transVtxTL.u - transVtxTL.x; - const float offsetV = transVtxTL.v - transVtxTL.y; - if (dx <= 0 || dy <= 0 || transVtxBR.u - transVtxTL.u != dx || transVtxBR.v - transVtxTL.v != dy || - offsetU != floorf(offsetU) || offsetV != floorf(offsetV)) { - depalPixelMapped = false; - break; - } - } + // The sprite border fix moves the UVs off the texel centers. + if (spriteBorderFixL != 0.0f || spriteBorderFixR != 0.0f || spriteBorderFixT != 0.0f || spriteBorderFixB != 0.0f) { + exactlyPixelMapped = false; } - // Like the software renderer's ProcessRect: rectangles aren't clipped, one with a vertex behind the camera // or outside the screen range is dropped whole. With depth clamp, a vertex past the near plane is exempt // from the range check (gpu/clipping/guardband). @@ -1236,7 +1228,7 @@ static bool ExpandRectangles(int vertexCount, int &numDecodedVerts, int vertsSiz } numDecodedVerts = outVerts; inds = newInds; - *pixelMappedExactly = pixelMapped || depalPixelMapped; + *pixelMappedExactly = pixelMapped || exactlyPixelMapped; *drawIndexCount = indsOut - newInds; return true; }