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] 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)