From 361f473f8ccfe0ef1c7405ab572f670e5ad2b8aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Tue, 3 Nov 2026 12:37:55 +0100 Subject: [PATCH] Keep one framebuffer copy for a run of draws with the same write mask A draw with a write mask the hardware can't do reads the target from a copy, and takes a new copy for every flush. But it only reads the bits its mask keeps, and doesn't change them, so while the same mask draws on, without blending or a logic op, the first copy serves every draw. DBZ Tenkaichi Tag Team draws its shadow map one bit of red at a time, about 26 copies a frame, now about 4. The copy is dropped when the target or the mask changes, on any other draw, clear, block transfer or stencil upload, and at the start of a frame. TEXFLUSH no longer drops it, only the self-texturing copy. DBZ, 20 emulated seconds on an M1 Mac: 1.9 s to 1.1 s. On a Galaxy A21: 10.0 s to 9.7 s per 10. The 1141 dumps in ppsspp-framedumps render the same, on Vulkan and OpenGL on macOS. Co-Authored-By: Claude Opus 5.5 --- GPU/Common/DrawEngineCommon.cpp | 8 ++++++++ GPU/Common/DrawEngineCommon.h | 2 ++ GPU/Common/FramebufferManagerCommon.cpp | 22 +++++++++++++++++++++- GPU/Common/FramebufferManagerCommon.h | 22 ++++++++++++++++++++++ GPU/Common/StencilCommon.cpp | 2 ++ GPU/D3D11/StateMappingD3D11.cpp | 2 +- GPU/GLES/StateMappingGLES.cpp | 2 +- GPU/GPUCommon.cpp | 1 + GPU/GPUCommonHW.cpp | 5 ++++- GPU/Metal/DrawEngineMetal.mm | 2 +- GPU/Vulkan/StateMappingVulkan.cpp | 2 +- 11 files changed, 64 insertions(+), 6 deletions(-) diff --git a/GPU/Common/DrawEngineCommon.cpp b/GPU/Common/DrawEngineCommon.cpp index 1b9439c28b..e72bd13e8d 100644 --- a/GPU/Common/DrawEngineCommon.cpp +++ b/GPU/Common/DrawEngineCommon.cpp @@ -30,6 +30,7 @@ #include "Core/Config.h" #include "GPU/GPUCommon.h" #include "GPU/Common/DrawEngineCommon.h" +#include "GPU/Common/FramebufferManagerCommon.h" #include "GPU/GPUStateSIMDUtil.h" #include "GPU/Common/SplineCommon.h" #include "GPU/Common/DepthRaster.h" @@ -775,6 +776,13 @@ void DrawEngineCommon::ApplyFramebufferRead(FBOTexState *fboTexState) { gstate_c.Dirty(DIRTY_SHADERBLEND); } +int DrawEngineCommon::ShaderReadBindFlags() const { + // The mask forces blending and the logic op into the shader too, so check what the GE asks for. + const bool maskOnly = pipelineState_.maskState.applyFramebufferRead && !gstate.isAlphaBlendEnabled() && + !(gstate.isLogicOpEnabled() && gstate.getLogicOp() != GE_LOGIC_COPY); + return BINDFBCOLOR_MAY_COPY | (maskOnly ? BINDFBCOLOR_MASK_READ : 0) | BINDFBCOLOR_UNCACHED; +} + int DrawEngineCommon::ComputeNumVertsToDecode() const { int sum = 0; for (int i = 0; i < numDrawVerts_; i++) { diff --git a/GPU/Common/DrawEngineCommon.h b/GPU/Common/DrawEngineCommon.h index 7b614b5518..c528ba23a8 100644 --- a/GPU/Common/DrawEngineCommon.h +++ b/GPU/Common/DrawEngineCommon.h @@ -195,6 +195,8 @@ protected: int ComputeNumVertsToDecode() const; void ApplyFramebufferRead(FBOTexState *fboTexState); + // For BindFramebufferAsColorTexture, when the draw reads the target for shader blending. + int ShaderReadBindFlags() const; void InitDepthRaster(); void ShutdownDepthRaster(); diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp index 184b241fae..bd3d7fdbe1 100644 --- a/GPU/Common/FramebufferManagerCommon.cpp +++ b/GPU/Common/FramebufferManagerCommon.cpp @@ -119,6 +119,7 @@ void FramebufferManagerCommon::BeginFrame(const DisplayLayoutConfig &config) { DecimateFBOs(); presentation_->BeginFrame(config); currentRenderVfb_ = nullptr; + DiscardFramebufferCopy(); // Hack. displayLayoutConfigCopy_ = config; @@ -1420,6 +1421,11 @@ bool FramebufferManagerCommon::BindFramebufferAsColorTexture(int stage, VirtualF draw_->BindFramebufferAsTexture(currentFramebufferCopy_, stage, Draw::Aspect::COLOR_BIT, layer); return true; } + const bool maskRead = (flags & BINDFBCOLOR_MASK_READ) != 0; + if (maskRead && maskCopy_ && maskCopyVfb_ == framebuffer && maskCopyMask_ == gstate.getColorMask()) { + draw_->BindFramebufferAsTexture(maskCopy_, stage, Draw::Aspect::COLOR_BIT, layer); + return true; + } // There's a special case we can handle here, where the game is texturing from the same pixels being read, in order to // implement a DST*DST blending function, which the PSP can't do. However on the PC we can absolutely do this! @@ -1436,7 +1442,7 @@ bool FramebufferManagerCommon::BindFramebufferAsColorTexture(int stage, VirtualF return true; } - Draw::Framebuffer *renderCopy = GetTempFBO(TempFBO::COPY, framebuffer->renderWidth, framebuffer->renderHeight); + Draw::Framebuffer *renderCopy = GetTempFBO(maskRead ? TempFBO::MASK_COPY : TempFBO::COPY, framebuffer->renderWidth, framebuffer->renderHeight); if (renderCopy) { VirtualFramebuffer copyInfo = *framebuffer; copyInfo.fbo = renderCopy; @@ -1451,6 +1457,11 @@ bool FramebufferManagerCommon::BindFramebufferAsColorTexture(int stage, VirtualF if (!partial && (flags & BINDFBCOLOR_UNCACHED) == 0) { currentFramebufferCopy_ = renderCopy; } + if (!partial && maskRead) { + maskCopy_ = renderCopy; + maskCopyVfb_ = framebuffer; + maskCopyMask_ = gstate.getColorMask(); + } gpuStats.perFrame.numCopiesForSelfTex++; } else { // Failed to get temp FBO? Weird. @@ -3208,6 +3219,14 @@ void FramebufferManagerCommon::DestroyAllFBOs() { drawPixelsCache_.clear(); } +void FramebufferManagerCommon::CheckMaskCopyStillValid(const VirtualFramebuffer *vfb) { + // Anything but a draw with the same write mask, without blending or a logic op, can change the bits the copy holds. + if (vfb != maskCopyVfb_ || gstate.isModeClear() || gstate.getColorMask() != maskCopyMask_ || + gstate.isAlphaBlendEnabled() || (gstate.isLogicOpEnabled() && gstate.getLogicOp() != GE_LOGIC_COPY)) { + maskCopy_ = nullptr; + } +} + static const char *TempFBOReasonToString(TempFBO reason) { switch (reason) { case TempFBO::DEPAL: return "depal"; @@ -3216,6 +3235,7 @@ static const char *TempFBOReasonToString(TempFBO reason) { case TempFBO::STENCIL: return "stencil"; case TempFBO::DEPTH_SHIFT: return "depth_shift"; case TempFBO::SAMPLE0: return "sample0"; + case TempFBO::MASK_COPY: return "mask_copy"; default: break; } return ""; diff --git a/GPU/Common/FramebufferManagerCommon.h b/GPU/Common/FramebufferManagerCommon.h index f60b6b7f14..d4ad626feb 100644 --- a/GPU/Common/FramebufferManagerCommon.h +++ b/GPU/Common/FramebufferManagerCommon.h @@ -214,6 +214,8 @@ enum BindFramebufferColorFlags { // Used when rendering to a temporary surface (e.g. not the current render target.) BINDFBCOLOR_FORCE_SELF = 8, BINDFBCOLOR_UNCACHED = 16, + // The draw reads only the bits its write mask keeps. See maskCopy_. + BINDFBCOLOR_MASK_READ = 32, }; enum DrawTextureFlags { @@ -241,6 +243,8 @@ enum class TempFBO { DEPTH_SHIFT, // The first samples of a multisampled framebuffer. SAMPLE0, + // A copy for draws that read only the bits outside their write mask. + MASK_COPY, }; inline Draw::DataFormat GEFormatToThin3D(GEBufferFormat geFormat) { @@ -522,7 +526,19 @@ public: void DiscardFramebufferCopy() { currentFramebufferCopy_ = nullptr; + maskCopy_ = nullptr; } + // The target hasn't changed, but texturing from it should see the latest drawing. + void DiscardSelfTextureCopy() { + currentFramebufferCopy_ = nullptr; + } + // Call before a draw to the current render target. + void CheckMaskCopy(const VirtualFramebuffer *vfb) { + if (maskCopy_) { + CheckMaskCopyStillValid(vfb); + } + } + void CheckMaskCopyStillValid(const VirtualFramebuffer *vfb); bool PresentedThisFrame() const; void SetPresentationOutput(const OutputParams *output); @@ -640,6 +656,12 @@ protected: VirtualFramebuffer *currentRenderVfb_ = nullptr; Draw::Framebuffer *currentFramebufferCopy_ = nullptr; + // A draw with a write mask only reads the bits the mask keeps, and doesn't change them. So while the same + // mask draws on, without blending or a logic op, one copy serves every draw: DBZ Tenkaichi Tag Team draws + // its shadows one bit at a time, which took a copy per flush. + Draw::Framebuffer *maskCopy_ = nullptr; + const VirtualFramebuffer *maskCopyVfb_ = nullptr; + u32 maskCopyMask_ = 0; // The range of PSP memory that may contain FBOs. So we can skip iterating. u32 framebufColorRangeEnd_ = 0; diff --git a/GPU/Common/StencilCommon.cpp b/GPU/Common/StencilCommon.cpp index 5ec519b06e..1f3ea88ac0 100644 --- a/GPU/Common/StencilCommon.cpp +++ b/GPU/Common/StencilCommon.cpp @@ -348,6 +348,8 @@ bool FramebufferManagerCommon::PerformWriteStencilFromMemory(u32 addr, int size, if (!CheckStencilBits(src, dstBuffer, values, usedBits)) return false; + DiscardFramebufferCopy(); + if (usedBits == 0) { if (flags & WriteStencil::STENCIL_IS_ZERO) { // Common when creating buffers, it's already 0. diff --git a/GPU/D3D11/StateMappingD3D11.cpp b/GPU/D3D11/StateMappingD3D11.cpp index f7a4f16c01..09605ab6b7 100644 --- a/GPU/D3D11/StateMappingD3D11.cpp +++ b/GPU/D3D11/StateMappingD3D11.cpp @@ -156,7 +156,7 @@ void DrawEngineD3D11::ApplyDrawState(int prim) { ApplyStencilReplaceAndLogicOpIgnoreBlend(blendState.replaceAlphaWithStencil, blendState); if (fboTexBindState == FBO_TEX_COPY_BIND_TEX) { - framebufferManager_->BindFramebufferAsColorTexture(1, framebufferManager_->GetCurrentRenderVFB(), BINDFBCOLOR_MAY_COPY | BINDFBCOLOR_UNCACHED, 0); + framebufferManager_->BindFramebufferAsColorTexture(1, framebufferManager_->GetCurrentRenderVFB(), ShaderReadBindFlags(), 0); // No sampler required, we do a plain Load in the pixel shader. fboTexBound_ = true; fboTexBindState = FBO_TEX_NONE; diff --git a/GPU/GLES/StateMappingGLES.cpp b/GPU/GLES/StateMappingGLES.cpp index 30d1cb8c90..9dd4e9f238 100644 --- a/GPU/GLES/StateMappingGLES.cpp +++ b/GPU/GLES/StateMappingGLES.cpp @@ -148,7 +148,7 @@ void DrawEngineGLES::ApplyDrawState(int prim) { // fboTexNeedsBind_ won't be set if we can read directly from the target. if (fboTexBindState == FBO_TEX_COPY_BIND_TEX) { // Note that this is positions, not UVs, that we need the copy from. - framebufferManager_->BindFramebufferAsColorTexture(1, framebufferManager_->GetCurrentRenderVFB(), BINDFBCOLOR_MAY_COPY | BINDFBCOLOR_UNCACHED, 0); + framebufferManager_->BindFramebufferAsColorTexture(1, framebufferManager_->GetCurrentRenderVFB(), ShaderReadBindFlags(), 0); // If we are rendering at a higher resolution, linear is probably best for the dest color. renderManager->SetTextureSampler(1, GL_CLAMP_TO_EDGE, GL_CLAMP_TO_EDGE, GL_LINEAR, GL_LINEAR, 0.0f); fboTexBound_ = true; diff --git a/GPU/GPUCommon.cpp b/GPU/GPUCommon.cpp index 68a4812fbf..0fe5bc1480 100644 --- a/GPU/GPUCommon.cpp +++ b/GPU/GPUCommon.cpp @@ -1607,6 +1607,7 @@ void GPUCommon::FlushImm() { } if (vfb) { CheckDepthUsage(vfb); + framebufferManager_->CheckMaskCopy(vfb); } bool antialias = (immFlags_ & GE_IMM_ANTIALIAS) != 0; diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index 5692c45c95..8eb637b140 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -956,6 +956,7 @@ void GPUCommonHW::Execute_Prim(u32 op, u32 diff) { } CheckDepthUsage(vfb); + framebufferManager_->CheckMaskCopy(vfb); const void *verts = Memory::GetPointerUnchecked(gstate_c.vertexAddr); const void *inds = nullptr; @@ -1334,6 +1335,7 @@ void GPUCommonHW::Execute_Bezier(u32 op, u32 diff) { } CheckDepthUsage(vfb); + framebufferManager_->CheckMaskCopy(vfb); if (!Memory::IsValidAddress(gstate_c.vertexAddr)) { ERROR_LOG(Log::G3D, "Bad vertex address %08x!", gstate_c.vertexAddr); @@ -1409,6 +1411,7 @@ void GPUCommonHW::Execute_Spline(u32 op, u32 diff) { } CheckDepthUsage(vfb); + framebufferManager_->CheckMaskCopy(vfb); if (!Memory::IsValidAddress(gstate_c.vertexAddr)) { ERROR_LOG(Log::G3D, "Bad vertex address %08x!", gstate_c.vertexAddr); @@ -1522,7 +1525,7 @@ void GPUCommonHW::Execute_TexFlush(u32 op, u32 diff) { // Games call this when they need the effect of drawing to be visible to texturing. // And for a bunch of other reasons, but either way, this is what we need to do. // It's possible we could also use this as a hint for the texture cache somehow. - framebufferManager_->DiscardFramebufferCopy(); + framebufferManager_->DiscardSelfTextureCopy(); } void GPUCommonHW::FlushPendingDrawing() { diff --git a/GPU/Metal/DrawEngineMetal.mm b/GPU/Metal/DrawEngineMetal.mm index 1dbd5276d7..b93a0eb1a8 100644 --- a/GPU/Metal/DrawEngineMetal.mm +++ b/GPU/Metal/DrawEngineMetal.mm @@ -552,7 +552,7 @@ void DrawEngineMetal::ConvertStateToKeys(int prim) { void DrawEngineMetal::BindShaderBlendTex() { if (!gstate.isModeClear() && fboTexBindState_ == FBO_TEX_COPY_BIND_TEX) { VirtualFramebuffer *curRenderVfb = framebufferManager_->GetCurrentRenderVFB(); - bool bindResult = framebufferManager_->BindFramebufferAsColorTexture(1, curRenderVfb, BINDFBCOLOR_MAY_COPY | BINDFBCOLOR_UNCACHED, Draw::ALL_LAYERS); + bool bindResult = framebufferManager_->BindFramebufferAsColorTexture(1, curRenderVfb, ShaderReadBindFlags(), Draw::ALL_LAYERS); _dbg_assert_(bindResult); boundSecondary_ = (__bridge id)(void *)draw_->GetNativeObject(Draw::NativeObject::BOUND_TEXTURE1_IMAGEVIEW); fboTexBound_ = true; diff --git a/GPU/Vulkan/StateMappingVulkan.cpp b/GPU/Vulkan/StateMappingVulkan.cpp index b6833bb8dc..55398b7bee 100644 --- a/GPU/Vulkan/StateMappingVulkan.cpp +++ b/GPU/Vulkan/StateMappingVulkan.cpp @@ -352,7 +352,7 @@ void DrawEngineVulkan::BindShaderBlendTex() { if (!gstate.isModeClear()) { if (fboTexBindState_ == FBO_TEX_COPY_BIND_TEX) { VirtualFramebuffer *curRenderVfb = framebufferManager_->GetCurrentRenderVFB(); - bool bindResult = framebufferManager_->BindFramebufferAsColorTexture(1, curRenderVfb, BINDFBCOLOR_MAY_COPY | BINDFBCOLOR_UNCACHED, Draw::ALL_LAYERS); + bool bindResult = framebufferManager_->BindFramebufferAsColorTexture(1, curRenderVfb, ShaderReadBindFlags(), Draw::ALL_LAYERS); _dbg_assert_(bindResult); boundSecondary_ = (VkImageView)draw_->GetNativeObject(Draw::NativeObject::BOUND_TEXTURE1_IMAGEVIEW); fboTexBound_ = true;