From 35de8e5d978a372d0b84dc3d4db3e75a6767c3dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Henrik=20Rydg=C3=A5rd?= Date: Fri, 9 Oct 2026 03:43:17 +0200 Subject: [PATCH] Screenshot the display from wherever in a framebuffer it is The display can point inside a larger framebuffer: Silent Hill flips between two halves of a 1024-wide 565 buffer. Presenting it found the framebuffer and the offset, but screenshots looked up only an exact address match, and otherwise read RAM, which with a hardware renderer holds something stale (Silent Hill: Origins' savestate thumbnails showed the loading screen). Screenshots of the display now use the same lookup and read back the 480x272 at the offset. Headless screenshots go the same way: eight Silent Hill frame dumps (Origins and Shattered Memories) now match the PSP exactly on Vulkan and OpenGL, from 0.8k-104k differing pixels. Co-Authored-By: Claude Opus 5.5 --- GPU/Common/FramebufferManagerCommon.cpp | 90 +++++++++++++++++-------- GPU/Common/FramebufferManagerCommon.h | 4 ++ GPU/GPUCommonHW.cpp | 3 + 3 files changed, 70 insertions(+), 27 deletions(-) diff --git a/GPU/Common/FramebufferManagerCommon.cpp b/GPU/Common/FramebufferManagerCommon.cpp index de1de1fc56..9342f8f81c 100644 --- a/GPU/Common/FramebufferManagerCommon.cpp +++ b/GPU/Common/FramebufferManagerCommon.cpp @@ -1658,28 +1658,10 @@ void FramebufferManagerCommon::CopyDisplayToOutput(const DisplayLayoutConfig &co } } -void FramebufferManagerCommon::PrepareCopyDisplayToOutput(const DisplayLayoutConfig &config, bool reallyDirty) { - DownloadFramebufferOnSwitch(currentRenderVfb_); - - if (displayFramebufPtr_ == 0) { - if (GetUIState() != UISTATE_PAUSEMENU) { - if (Core_IsStepping()) - VERBOSE_LOG(Log::FrameBuf, "Display disabled, displaying only black"); - else - DEBUG_LOG(Log::FrameBuf, "Display disabled, displaying only black"); - } - // No framebuffer to display! Clear to black. - presentation_->SourceBlank(); - return; - } - - u32 offsetX = 0; - u32 offsetY = 0; - - // If it's not really dirty, we're probably frameskipping. Use the last working one. - u32 fbaddr = reallyDirty ? displayFramebufPtr_ : prevDisplayFramebufPtr_; - prevDisplayFramebufPtr_ = fbaddr; - +// The framebuffer the display shows, and where in it: at its address, or inside a larger one. +VirtualFramebuffer *FramebufferManagerCommon::FindDisplayVFB(u32 fbaddr, u32 *offsetX, u32 *offsetY) { + *offsetX = 0; + *offsetY = 0; VirtualFramebuffer *vfb = ResolveVFB(fbaddr, displayStride_, displayFormat_); if (!vfb) { // Let's search for a framebuf within this range. Note that we also look for @@ -1704,9 +1686,9 @@ void FramebufferManagerCommon::PrepareCopyDisplayToOutput(const DisplayLayoutCon continue; } // Check for the closest one. - if (offsetY == 0 || offsetY > v_offsetY) { - offsetX = v_offsetX; - offsetY = v_offsetY; + if (*offsetY == 0 || *offsetY > v_offsetY) { + *offsetX = v_offsetX; + *offsetY = v_offsetY; vfb = v; } } @@ -1715,7 +1697,7 @@ void FramebufferManagerCommon::PrepareCopyDisplayToOutput(const DisplayLayoutCon if (vfb) { // Okay, we found one above. // Log should be "Displaying from framebuf" but not worth changing the report. - DEBUG_LOG(Log::FrameBuf, "Rendering from framebuf with offset %08x -> %08x+%dx%d", addr, vfb->fb_address, offsetX, offsetY); + DEBUG_LOG(Log::FrameBuf, "Rendering from framebuf with offset %08x -> %08x+%dx%d", addr, vfb->fb_address, *offsetX, *offsetY); } } @@ -1723,6 +1705,32 @@ void FramebufferManagerCommon::PrepareCopyDisplayToOutput(const DisplayLayoutCon if (vfb && vfb->height < 64) { vfb = nullptr; } + return vfb; +} + +void FramebufferManagerCommon::PrepareCopyDisplayToOutput(const DisplayLayoutConfig &config, bool reallyDirty) { + DownloadFramebufferOnSwitch(currentRenderVfb_); + + if (displayFramebufPtr_ == 0) { + if (GetUIState() != UISTATE_PAUSEMENU) { + if (Core_IsStepping()) + VERBOSE_LOG(Log::FrameBuf, "Display disabled, displaying only black"); + else + DEBUG_LOG(Log::FrameBuf, "Display disabled, displaying only black"); + } + // No framebuffer to display! Clear to black. + presentation_->SourceBlank(); + return; + } + + u32 offsetX = 0; + u32 offsetY = 0; + + // If it's not really dirty, we're probably frameskipping. Use the last working one. + u32 fbaddr = reallyDirty ? displayFramebufPtr_ : prevDisplayFramebufPtr_; + prevDisplayFramebufPtr_ = fbaddr; + + VirtualFramebuffer *vfb = FindDisplayVFB(fbaddr, &offsetX, &offsetY); if (!vfb) { if (Memory::IsValidAddress(fbaddr)) { @@ -3141,7 +3149,22 @@ bool FramebufferManagerCommon::GetFramebuffer(u32 fb_address, int fb_stride, GEB return true; } + return ReadFramebufferForDebug(vfb, 0, 0, 0, 0, buffer, maxScaleFactor); +} + +bool FramebufferManagerCommon::GetDisplayFramebuffer(GPUDebugBuffer &buffer, int maxScaleFactor) { + u32 offsetX, offsetY; + VirtualFramebuffer *vfb = FindDisplayVFB(displayFramebufPtr_, &offsetX, &offsetY); + if (!vfb) { + return GetFramebuffer(displayFramebufPtr_, displayStride_, displayFormat_, buffer, maxScaleFactor); + } + return ReadFramebufferForDebug(vfb, offsetX, offsetY, 480, 272, buffer, maxScaleFactor); +} + +// Reads back a framebuffer, or the w x h at x, y in it (in PSP pixels) if w isn't 0. +bool FramebufferManagerCommon::ReadFramebufferForDebug(VirtualFramebuffer *vfb, int x, int y, int cropW, int cropH, GPUDebugBuffer &buffer, int maxScaleFactor) { int w = vfb->renderWidth, h = vfb->renderHeight; + int scale = vfb->renderScaleFactor; Draw::Framebuffer *bound = nullptr; @@ -3149,6 +3172,7 @@ bool FramebufferManagerCommon::GetFramebuffer(u32 fb_address, int fb_stride, GEB if (maxScaleFactor > 0 && vfb->renderWidth > vfb->width * maxScaleFactor) { w = vfb->width * maxScaleFactor; h = vfb->height * maxScaleFactor; + scale = maxScaleFactor; Draw::Framebuffer *tempFBO = GetTempFBO(TempFBO::COPY, w, h); VirtualFramebuffer tempVfb = *vfb; @@ -3173,10 +3197,22 @@ bool FramebufferManagerCommon::GetFramebuffer(u32 fb_address, int fb_stride, GEB buffer.SetIsBackbuffer(true); } + int x0 = 0; + int y0 = 0; + if (cropW > 0 && useBufferedRendering_) { + x0 = std::min(x * scale, w); + y0 = std::min(y * scale, h); + w = std::min(cropW * scale, w - x0); + h = std::min(cropH * scale, h - y0); + if (w <= 0 || h <= 0) { + return false; + } + } + // TODO: Maybe should handle flipY inside CopyFramebufferToMemorySync somehow? bool flipY = (GetGPUBackend() == GPUBackend::OPENGL && !useBufferedRendering_) ? true : false; buffer.Allocate(w, h, GE_FORMAT_8888, flipY); - bool retval = draw_->CopyFramebufferToMemory(bound, Draw::Aspect::COLOR_BIT, 0, 0, w, h, Draw::DataFormat::R8G8B8A8_UNORM, buffer.GetData(), w, Draw::ReadbackMode::BLOCK, "GetFramebuffer"); + bool retval = draw_->CopyFramebufferToMemory(bound, Draw::Aspect::COLOR_BIT, x0, y0, w, h, Draw::DataFormat::R8G8B8A8_UNORM, buffer.GetData(), w, Draw::ReadbackMode::BLOCK, "GetFramebuffer"); buffer.SetScaleFactor(vfb->renderScaleFactor); // Don't need to increment gpu stats for readback count here, this is a debugger-only function. diff --git a/GPU/Common/FramebufferManagerCommon.h b/GPU/Common/FramebufferManagerCommon.h index cb4f6c403b..596b04f26e 100644 --- a/GPU/Common/FramebufferManagerCommon.h +++ b/GPU/Common/FramebufferManagerCommon.h @@ -463,6 +463,8 @@ public: // Debug features virtual bool GetFramebuffer(u32 fb_address, int fb_stride, GEBufferFormat format, GPUDebugBuffer &buffer, int maxRes); + // The 480x272 the display shows, wherever in a framebuffer it is. + bool GetDisplayFramebuffer(GPUDebugBuffer &buffer, int maxRes); virtual bool GetDepthbuffer(u32 fb_address, int fb_stride, u32 z_address, int z_stride, GPUDebugBuffer &buffer); virtual bool GetStencilbuffer(u32 fb_address, int fb_stride, GPUDebugBuffer &buffer); virtual bool GetOutputFramebuffer(GPUDebugBuffer &buffer); @@ -556,6 +558,8 @@ protected: static bool ShouldDownloadFramebufferColor(const VirtualFramebuffer *vfb); static bool ShouldDownloadFramebufferDepth(const VirtualFramebuffer *vfb); void DownloadFramebufferOnSwitch(VirtualFramebuffer *vfb); + VirtualFramebuffer *FindDisplayVFB(u32 fbaddr, u32 *offsetX, u32 *offsetY); + bool ReadFramebufferForDebug(VirtualFramebuffer *vfb, int x, int y, int w, int h, GPUDebugBuffer &buffer, int maxScaleFactor); bool FindTransferFramebuffer(u32 basePtr, int stride, int x, int y, int w, int h, int bpp, bool destination, BlockTransferRect *rect); diff --git a/GPU/GPUCommonHW.cpp b/GPU/GPUCommonHW.cpp index cbff301403..e33e5a83f1 100644 --- a/GPU/GPUCommonHW.cpp +++ b/GPU/GPUCommonHW.cpp @@ -667,6 +667,9 @@ std::string GPUCommonHW::DebugGetShaderString(std::string id, DebugShaderType ty } bool GPUCommonHW::GetCurrentFramebuffer(GPUDebugBuffer &buffer, GPUDebugFramebufferType type, int maxRes) { + if (type == GPU_DBG_FRAMEBUF_DISPLAY) { + return framebufferManager_->GetDisplayFramebuffer(buffer, maxRes); + } u32 fb_address = type == GPU_DBG_FRAMEBUF_RENDER ? (gstate.getFrameBufRawAddress() | 0x04000000) : framebufferManager_->DisplayFramebufAddr(); int fb_stride = type == GPU_DBG_FRAMEBUF_RENDER ? gstate.FrameBufStride() : framebufferManager_->DisplayFramebufStride(); GEBufferFormat format = type == GPU_DBG_FRAMEBUF_RENDER ? gstate_c.framebufFormat : framebufferManager_->DisplayFramebufFormat();