From 987a904cffe2a51fabc64da286faa95993bd24fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Sun, 18 Oct 2026 01:12:33 +0200 Subject: [PATCH] Metal: Fail backbuffer readbacks until something draws to it Headless renders to an offscreen backbuffer that nothing ever draws to, so the GE debugger's screenshot read uninitialized memory. Fail the readback instead, like headless Vulkan (which has no backbuffer), so the debugger falls back to the displayed PSP framebuffer. All the GE debugger buffers (screenshot, render color/depth/stencil, texture, CLUT) now read back the same as Vulkan's. Co-Authored-By: Claude Opus 5.5 (1M context) --- Common/GPU/Metal/MetalRenderManager.h | 3 +++ Common/GPU/Metal/MetalRenderManager.mm | 1 + Common/GPU/Metal/thin3d_metal.mm | 3 ++- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/Common/GPU/Metal/MetalRenderManager.h b/Common/GPU/Metal/MetalRenderManager.h index f30d2b8676..12955927ad 100644 --- a/Common/GPU/Metal/MetalRenderManager.h +++ b/Common/GPU/Metal/MetalRenderManager.h @@ -360,6 +360,8 @@ public: MTLPixelFormat BackbufferFormat() const { return MTLPixelFormatBGRA8Unorm; } // The offscreen backbuffer, or nil when rendering to the layer. id OffscreenBackbuffer() const { return offscreenBackbuffer_; } + // False until something renders to the backbuffer (headless never does). + bool BackbufferDrawn() const { return backbufferDrawn_; } // Render passes. The pass is started lazily, see above. void BindRenderPass(const MetalRenderPassDesc &desc); @@ -480,6 +482,7 @@ private: // Recording state (emu thread). MetalCommandChunk *chunk_ = nullptr; id offscreenBackbuffer_; + bool backbufferDrawn_ = false; id bbDepth_; int bbWidth_ = 0; int bbHeight_ = 0; diff --git a/Common/GPU/Metal/MetalRenderManager.mm b/Common/GPU/Metal/MetalRenderManager.mm index a606a20efa..4b322bf321 100644 --- a/Common/GPU/Metal/MetalRenderManager.mm +++ b/Common/GPU/Metal/MetalRenderManager.mm @@ -494,6 +494,7 @@ void MetalRenderManager::BindBackbufferPass(MTLLoadAction colorLoad, MTLClearCol // Already rendering to it. return; } + backbufferDrawn_ = true; EndRenderPass(); MetalRenderPassDesc desc{}; desc.colorLoad = colorLoad; diff --git a/Common/GPU/Metal/thin3d_metal.mm b/Common/GPU/Metal/thin3d_metal.mm index 0228ecb025..dcc54fc4d6 100644 --- a/Common/GPU/Metal/thin3d_metal.mm +++ b/Common/GPU/Metal/thin3d_metal.mm @@ -1039,7 +1039,8 @@ bool MetalDrawContext::CopyFramebufferToMemory(Framebuffer *srcfb, Aspect aspect texW = (int)srcTex.width; texH = (int)srcTex.height; srcFormat = srcTex.pixelFormat; - } else if (aspect != Aspect::COLOR_BIT) { + } else if (aspect != Aspect::COLOR_BIT || !rm_->BackbufferDrawn()) { + // Like headless Vulkan, which has no backbuffer. The GE debugger shows the PSP's framebuffer instead. return false; } else if (rm_->OffscreenBackbuffer()) { srcTex = rm_->OffscreenBackbuffer();