From 73122400e331dcb944a312c6e8e2a178f562928b Mon Sep 17 00:00:00 2001 From: Connor McLaughlin Date: Mon, 23 May 2022 19:35:42 +1000 Subject: [PATCH] GSTextureCache: Expand target to fit readout height This handles a case where you have two images stacked on top of one another (usually FMVs), and the size of the top framebuffer is larger than the height of the image. Usually happens when conservative FB is on, as off it'll create a 1280 high framebuffer. The game alternates DISPFB between the top image, where the block pointer matches the target, but when it switches to the other buffer, LookupTarget() will score a partial match on the target because e.g. 448 < 512, but the target doesn't actually contain the full image. This usually leads to flickering. Test case: Neo Contra intro FMVs. So, for these cases, we simply expand the target to include both images, based on the read height. It won't affect normal rendering, since that doesn't go through this path. --- pcsx2/GS/Renderers/HW/GSRendererHW.cpp | 2 +- pcsx2/GS/Renderers/HW/GSRendererHW.h | 3 ++ pcsx2/GS/Renderers/HW/GSTextureCache.cpp | 65 ++++++++++++++++++++++++ pcsx2/GS/Renderers/HW/GSTextureCache.h | 4 ++ 4 files changed, 73 insertions(+), 1 deletion(-) diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp index 452d4501c3..f309b7af5a 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp @@ -125,7 +125,7 @@ void GSRendererHW::SetScaling() // Until performance issue is properly fixed, let's keep an option to reduce the framebuffer size. // // m_large_framebuffer has been inverted to m_conservative_framebuffer, it isn't an option that benefits being enabled all the time for everyone. - int fb_height = 1280; + int fb_height = MAX_FRAMEBUFFER_HEIGHT; if (GSConfig.ConservativeFramebuffer) { fb_height = fb_width < 1024 ? std::max(512, crtc_size.y) : 1024; diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.h b/pcsx2/GS/Renderers/HW/GSRendererHW.h index 9bff266850..25044956c4 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.h +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.h @@ -22,6 +22,9 @@ class GSRendererHW : public GSRenderer { +public: + static constexpr int MAX_FRAMEBUFFER_HEIGHT = 1280; + private: int m_width; int m_height; diff --git a/pcsx2/GS/Renderers/HW/GSTextureCache.cpp b/pcsx2/GS/Renderers/HW/GSTextureCache.cpp index f39921ded2..8b8e96ae23 100644 --- a/pcsx2/GS/Renderers/HW/GSTextureCache.cpp +++ b/pcsx2/GS/Renderers/HW/GSTextureCache.cpp @@ -465,32 +465,46 @@ GSTextureCache::Target* GSTextureCache::LookupTarget(const GIFRegTEX0& TEX0, con assert(type == RenderTarget); // Let's try to find a perfect frame that contains valid data for (auto t : list) + { if (bp == t->m_TEX0.TBP0 && t->m_end_block >= bp) { dst = t; GL_CACHE("TC: Lookup Frame %dx%d, perfect hit: %d (0x%x -> 0x%x %s)", size.x, size.y, dst->m_texture->GetID(), bp, t->m_end_block, psm_str(TEX0.PSM)); break; } + } // 2nd try ! Try to find a frame that include the bp if (!dst) + { for (auto t : list) + { if (t->m_TEX0.TBP0 < bp && bp <= t->m_end_block) { dst = t; GL_CACHE("TC: Lookup Frame %dx%d, inclusive hit: %d (0x%x, took 0x%x -> 0x%x %s)", size.x, size.y, t->m_texture->GetID(), bp, t->m_TEX0.TBP0, t->m_end_block, psm_str(TEX0.PSM)); + if (real_h > 0) + ScaleTargetForDisplay(dst, TEX0, real_h); + break; } + } + } // 3rd try ! Try to find a frame that doesn't contain valid data (honestly I'm not sure we need to do it) if (!dst) + { for (auto t : list) + { if (bp == t->m_TEX0.TBP0) { dst = t; GL_CACHE("TC: Lookup Frame %dx%d, empty hit: %d (0x%x -> 0x%x %s)", size.x, size.y, dst->m_texture->GetID(), bp, t->m_end_block, psm_str(TEX0.PSM)); break; } + } + } + if (dst) dst->m_TEX0.TBW = TEX0.TBW; // Fix Jurassic Park - Operation Genesis loading disk logo. } @@ -612,6 +626,57 @@ GSTextureCache::Target* GSTextureCache::LookupTarget(const GIFRegTEX0& TEX0, con return LookupTarget(TEX0, size, RenderTarget, true, 0, true, real_h); } +void GSTextureCache::ScaleTargetForDisplay(Target* t, const GIFRegTEX0& dispfb, int real_h) +{ + // This handles a case where you have two images stacked on top of one another (usually FMVs), and + // the size of the top framebuffer is larger than the height of the image. Usually happens when + // conservative FB is on, as off it'll create a 1280 high framebuffer. + + // The game alternates DISPFB between the top image, where the block pointer matches the target, + // but when it switches to the other buffer, LookupTarget() will score a partial match on the target + // because e.g. 448 < 512, but the target doesn't actually contain the full image. This usually leads + // to flickering. Test case: Neo Contra intro FMVs. + + // So, for these cases, we simply expand the target to include both images, based on the read height. + // It won't affect normal rendering, since that doesn't go through this path. + + // Compute offset into the target that we'll start reading from. + const int delta = dispfb.TBP0 - t->m_TEX0.TBP0; + int y_offset = 0; + if (delta > 0 && t->m_TEX0.TBW != 0) + { + const int pages = delta >> 5u; + const int y_pages = pages / t->m_TEX0.TBW; + y_offset = y_pages * GSLocalMemory::m_psm[t->m_TEX0.PSM].pgs.y; + } + + // Take that into consideration to find the extent of the target which will be sampled. + GSTexture* old_texture = t->m_texture; + const int needed_height = std::min(real_h + y_offset, GSRendererHW::MAX_FRAMEBUFFER_HEIGHT); + const int scaled_needed_height = static_cast(static_cast(needed_height) * old_texture->GetScale().y); + if (scaled_needed_height <= old_texture->GetHeight()) + return; + + // We're expanding, so create a new texture. + GSTexture* new_texture = g_gs_device->CreateRenderTarget(old_texture->GetWidth(), scaled_needed_height, GSTexture::Format::Color, false); + if (!new_texture) + { + // Memory allocation failure, do our best to hobble along. + return; + } + + GL_CACHE("Expanding target for display output, target height %d @ 0x%X, display %d @ 0x%X offset %d needed %d", + t->m_texture->GetHeight(), t->m_TEX0.TBP0, real_h, dispfb.TBP0, y_offset, needed_height); + + // Fill the new texture with the old data, and discard the old texture. + g_gs_device->StretchRect(old_texture, new_texture, GSVector4(old_texture->GetSize()).zwxy(), ShaderConvert::COPY, false); + g_gs_device->Recycle(old_texture); + t->m_texture = new_texture; + + // We unconditionally preload the frame here, because otherwise we'll end up with blackness for one frame (when the expand happens). + t->m_dirty.push_back(GSDirtyRect(GSVector4i(0, 0, t->m_TEX0.TBW * 64, needed_height), t->m_TEX0.PSM, t->m_TEX0.TBW)); +} + // Goal: Depth And Target at the same address is not possible. On GS it is // the same memory but not on the Dx/GL. Therefore a write to the Depth/Target // must invalidate the Target/Depth respectively diff --git a/pcsx2/GS/Renderers/HW/GSTextureCache.h b/pcsx2/GS/Renderers/HW/GSTextureCache.h index 5d671ed98e..7d044fdce3 100644 --- a/pcsx2/GS/Renderers/HW/GSTextureCache.h +++ b/pcsx2/GS/Renderers/HW/GSTextureCache.h @@ -334,6 +334,10 @@ public: SurfaceOffset ComputeSurfaceOffset(const uint32_t bp, const uint32_t bw, const uint32_t psm, const GSVector4i& r, const Target* t); SurfaceOffset ComputeSurfaceOffset(const SurfaceOffsetKey& sok); + /// Expands a target when the block pointer for a display framebuffer is within another target, but the read offset + /// plus the height is larger than the current size of the target. + static void ScaleTargetForDisplay(Target* t, const GIFRegTEX0& dispfb, int real_h); + /// Invalidates a temporary source, a partial copy only created from the current RT/DS for the current draw. void InvalidateTemporarySource();