diff --git a/bin/resources/GameIndex.yaml b/bin/resources/GameIndex.yaml index b5fb48b087..50bc5a19cb 100644 --- a/bin/resources/GameIndex.yaml +++ b/bin/resources/GameIndex.yaml @@ -23117,7 +23117,7 @@ SLES-55215: region: "PAL-E" compat: 5 gsHWFixes: - textureInsideRT: 1 # Fixes character layering issue. + moveHandler: "MV_Growlanser" # Fixes precomputed depth buffer. SLES-55216: name: "Baroque" region: "PAL-E" @@ -26716,7 +26716,7 @@ SLPM-61148: name: "Growlanser V - Generations [Taikenban Demo]" region: "NTSC-J" gsHWFixes: - textureInsideRT: 1 # Fixes character layering issue. + moveHandler: "MV_Growlanser" # Fixes precomputed depth buffer. SLPM-61153: name: "Dengeki PS2 PlayStation D92" region: "NTSC-J" @@ -33555,7 +33555,7 @@ SLPM-66249: region: "NTSC-J" compat: 5 gsHWFixes: - textureInsideRT: 1 # Fixes character layering issue. + moveHandler: "MV_Growlanser" # Fixes precomputed depth buffer. SLPM-66250: name: "Choro Q HG 4 [Takara Best]" region: "NTSC-J" @@ -34179,7 +34179,7 @@ SLPM-66418: region: "NTSC-J" compat: 5 gsHWFixes: - textureInsideRT: 1 # Fixes character layering issue. + moveHandler: "MV_Growlanser" # Fixes precomputed depth buffer. SLPM-66419: name: "Valkyrie Profile 2 - Silmeria" region: "NTSC-J" @@ -35399,7 +35399,7 @@ SLPM-66716: region: "NTSC-J" compat: 5 gsHWFixes: - textureInsideRT: 1 # Fixes character layering issue. + moveHandler: "MV_Growlanser" # Fixes precomputed depth buffer. SLPM-66717: name: "Standard Daisenryaku - Dengekisen [Sega the Best]" region: "NTSC-J" @@ -50466,7 +50466,7 @@ SLUS-21571: region: "NTSC-U" compat: 5 gsHWFixes: - textureInsideRT: 1 # Fixes character layering issue. + moveHandler: "MV_Growlanser" # Fixes precomputed depth buffer. SLUS-21572: name: "Surf's Up" region: "NTSC-U" diff --git a/pcsx2/Config.h b/pcsx2/Config.h index 84acf87e1c..d40804eb98 100644 --- a/pcsx2/Config.h +++ b/pcsx2/Config.h @@ -748,6 +748,7 @@ struct Pcsx2Config u8 TVShader = 0; s16 GetSkipCountFunctionId = -1; s16 BeforeDrawFunctionId = -1; + s16 MoveHandlerFunctionId = -1; int SkipDrawStart = 0; int SkipDrawEnd = 0; diff --git a/pcsx2/Docs/gamedb-schema.json b/pcsx2/Docs/gamedb-schema.json index 4dfde3e93d..8859b8c062 100644 --- a/pcsx2/Docs/gamedb-schema.json +++ b/pcsx2/Docs/gamedb-schema.json @@ -287,6 +287,9 @@ }, "beforeDraw": { "type": "string" + }, + "moveHandler": { + "type": "string" } }, "additionalProperties": false diff --git a/pcsx2/GS/GS.cpp b/pcsx2/GS/GS.cpp index 8975075198..b2b087af2f 100644 --- a/pcsx2/GS/GS.cpp +++ b/pcsx2/GS/GS.cpp @@ -717,7 +717,8 @@ void GSUpdateConfig(const Pcsx2Config::GSOptions& new_config) if (GSConfig.UserHacks_DisableRenderFixes != old_config.UserHacks_DisableRenderFixes || GSConfig.UpscaleMultiplier != old_config.UpscaleMultiplier || GSConfig.GetSkipCountFunctionId != old_config.GetSkipCountFunctionId || - GSConfig.BeforeDrawFunctionId != old_config.BeforeDrawFunctionId) + GSConfig.BeforeDrawFunctionId != old_config.BeforeDrawFunctionId || + GSConfig.MoveHandlerFunctionId != old_config.MoveHandlerFunctionId) { g_gs_renderer->UpdateCRCHacks(); } diff --git a/pcsx2/GS/GS.h b/pcsx2/GS/GS.h index ece2fec6f5..2b141e63b6 100644 --- a/pcsx2/GS/GS.h +++ b/pcsx2/GS/GS.h @@ -68,6 +68,7 @@ class HostDisplay; // Returns the ID for the specified function, otherwise -1. s16 GSLookupGetSkipCountFunctionId(const std::string_view& name); s16 GSLookupBeforeDrawFunctionId(const std::string_view& name); +s16 GSLookupMoveHandlerFunctionId(const std::string_view& name); void GSinit(); void GSshutdown(); diff --git a/pcsx2/GS/GSRegs.h b/pcsx2/GS/GSRegs.h index 073e4751e1..a9bc6ad1f0 100644 --- a/pcsx2/GS/GSRegs.h +++ b/pcsx2/GS/GSRegs.h @@ -836,6 +836,15 @@ REG_END2 // The recast of TBW seems useless but it avoid tons of warning from GCC... return ((u32)TBW << 6u) < (1u << TW); } + + __forceinline static GIFRegTEX0 Create(u32 bp, u32 bw, u32 psm) + { + GIFRegTEX0 ret = {}; + ret.TBP0 = bp; + ret.TBW = bw; + ret.PSM = psm; + return ret; + } REG_END2 REG64_(GIFReg, TEX1) diff --git a/pcsx2/GS/Renderers/HW/GSHwHack.cpp b/pcsx2/GS/Renderers/HW/GSHwHack.cpp index 151c2733ff..0fdc0a47f4 100644 --- a/pcsx2/GS/Renderers/HW/GSHwHack.cpp +++ b/pcsx2/GS/Renderers/HW/GSHwHack.cpp @@ -1105,6 +1105,131 @@ bool GSHwHack::OI_HauntingGround(GSRendererHW& r, GSTexture* rt, GSTexture* ds, //////////////////////////////////////////////////////////////////////////////// +#define RBITBLTBUF r.m_env.BITBLTBUF +#define RSBP r.m_env.BITBLTBUF.SBP +#define RSBW r.m_env.BITBLTBUF.SBW +#define RSPSM r.m_env.BITBLTBUF.SPSM +#define RDBP r.m_env.BITBLTBUF.DBP +#define RDBW r.m_env.BITBLTBUF.DBW +#define RDPSM r.m_env.BITBLTBUF.DPSM +#define RWIDTH r.m_env.TRXREG.RRW +#define RHEIGHT r.m_env.TRXREG.RRH +#define RSX r.m_env.TRXPOS.SSAX +#define RSY r.m_env.TRXPOS.SSAY +#define RDX r.m_env.TRXPOS.DSAX +#define RDY r.m_env.TRXPOS.DSAY + +static bool GetMoveTargetPair(GSRendererHW& r, GSTextureCache::Target** src, GIFRegTEX0 src_desc, + GSTextureCache::Target** dst, GIFRegTEX0 dst_desc, bool req_target, bool preserve_target) +{ + // The source needs to exist. + const int src_type = + GSLocalMemory::m_psm[src_desc.PSM].depth ? GSTextureCache::DepthStencil : GSTextureCache::RenderTarget; + GSTextureCache::Target* tsrc = + g_texture_cache->LookupTarget(src_desc, GSVector2i(1, 1), r.GetTextureScaleFactor(), src_type); + if (!src) + return false; + + // The target might not. + const int dst_type = + GSLocalMemory::m_psm[dst_desc.PSM].depth ? GSTextureCache::DepthStencil : GSTextureCache::RenderTarget; + GSTextureCache::Target* tdst = g_texture_cache->LookupTarget(dst_desc, tsrc->GetUnscaledSize(), tsrc->GetScale(), + dst_type, true, 0, false, false, preserve_target, tsrc->GetUnscaledRect()); + if (!tdst) + { + if (req_target) + return false; + + tdst = g_texture_cache->CreateTarget(dst_desc, tsrc->GetUnscaledSize(), tsrc->GetScale(), dst_type, true, 0, + false, false, true, tsrc->GetUnscaledRect()); + if (!tdst) + return false; + } + + if (!preserve_target) + { + g_texture_cache->InvalidateVideoMemType( + (dst_type == GSTextureCache::RenderTarget) ? GSTextureCache::DepthStencil : GSTextureCache::RenderTarget, + dst_desc.TBP0); + + GL_INS("GetMoveTargetPair(): Clearing dirty list."); + tdst->m_dirty.clear(); + } + else + { + tdst->Update(); + } + + *src = tsrc; + *dst = tdst; + return true; +} + +#if 0 +// Disabled to avoid compiler warnings, enable when it is needed. +static bool GetMoveTargetPair(GSRendererHW& r, GSTextureCache::Target** src, GSTextureCache::Target** dst, + bool req_target = false, bool preserve_target = false) +{ + return GetMoveTargetPair(r, src, GIFRegTEX0::Create(RSBP, RSBW, RSPSM), dst, GIFRegTEX0::Create(RDBP, RDBW, RDPSM), + req_target, preserve_target); +} +#endif + +bool GSHwHack::MV_Growlanser(GSRendererHW& r) +{ + // Growlanser games have precomputed backgrounds and depth buffers, then draw the characters over the top. But + // instead of pre-swizzling it, or doing a large 512x448 move, they draw each page of depth to a temporary buffer + // (FBP 0), then move it, one quadrant (of a page) at a time to 0x1C00, in C32 format. Why they didn't just use a + // C32->Z32 move is beyond me... Anyway, since we don't swizzle targets in VRAM, the first move would need to + // readback (slow), and lose upscaling. The real issue is that because we don't preload depth targets, even with + // EE writes, the depth buffer gets cleared, and the background never occludes the foreground. So, we'll intercept + // the first move, prefill the depth buffer at 0x1C00, and skip the rest of them, so it's ready for the game. + + // Only 32x16 moves in C32. + if (RWIDTH != 32 || RHEIGHT != 16 || RSPSM != PSMCT32 || RDPSM != PSMCT32) + return false; + + // All the moves happen inbetween two draws, so we can take advantage of that to know when to stop. + static int last_hacked_move_n = 0; + if (r.s_n == last_hacked_move_n) + return true; + + GSTextureCache::Target *src, *dst; + if (!GetMoveTargetPair( + r, &src, GIFRegTEX0::Create(RSBP, RSBW, RSPSM), &dst, GIFRegTEX0::Create(RDBP, RDBW, PSMZ32), false, false)) + { + return false; + } + + const GSVector4i rc = src->GetUnscaledRect().rintersect(dst->GetUnscaledRect()); + dst->m_TEX0.TBW = src->m_TEX0.TBW; + dst->UpdateValidity(rc); + + GL_INS("MV_Growlanser: %x -> %x %dx%d", RSBP, RDBP, src->GetUnscaledWidth(), src->GetUnscaledHeight()); + + g_gs_device->StretchRect(src->GetTexture(), GSVector4(rc) / GSVector4(src->GetUnscaledSize()).xyxy(), + dst->GetTexture(), GSVector4(rc) * GSVector4(dst->GetScale()), ShaderConvert::RGBA8_TO_FLOAT32, false); + + last_hacked_move_n = r.s_n; + return true; +} + +#undef RBITBLTBUF +#undef RSBP +#undef RSBW +#undef RSPSM +#undef RDBP +#undef RDBW +#undef RDPSM +#undef RWIDTH +#undef RHEIGHT +#undef RSX +#undef RSY +#undef RDX +#undef RDY + +//////////////////////////////////////////////////////////////////////////////// + #define CRC_F(name) { #name, &GSHwHack::name } const GSHwHack::Entry GSHwHack::s_get_skip_count_functions[] = { @@ -1159,7 +1284,11 @@ const GSHwHack::Entry GSHwHack::s_before_draw_functions[] CRC_F(OI_ArTonelico2), CRC_F(OI_BurnoutGames), CRC_F(OI_Battlefield2), - CRC_F(OI_HauntingGround) + CRC_F(OI_HauntingGround), +}; + +const GSHwHack::Entry GSHwHack::s_move_handler_functions[] = { + CRC_F(MV_Growlanser), }; #undef CRC_F @@ -1186,6 +1315,17 @@ s16 GSLookupBeforeDrawFunctionId(const std::string_view& name) return -1; } +s16 GSLookupMoveHandlerFunctionId(const std::string_view& name) +{ + for (u32 i = 0; i < std::size(GSHwHack::s_move_handler_functions); i++) + { + if (name == GSHwHack::s_move_handler_functions[i].name) + return static_cast(i); + } + + return -1; +} + void GSRendererHW::UpdateCRCHacks() { GSRenderer::UpdateCRCHacks(); @@ -1195,6 +1335,7 @@ void GSRendererHW::UpdateCRCHacks() m_gsc = nullptr; m_oi = nullptr; + m_mv = nullptr; if (!GSConfig.UserHacks_DisableRenderFixes) { @@ -1209,6 +1350,12 @@ void GSRendererHW::UpdateCRCHacks() { m_oi = GSHwHack::s_before_draw_functions[GSConfig.BeforeDrawFunctionId].ptr; } + + if (GSConfig.MoveHandlerFunctionId >= 0 && + static_cast(GSConfig.MoveHandlerFunctionId) < std::size(GSHwHack::s_move_handler_functions)) + { + m_mv = GSHwHack::s_move_handler_functions[GSConfig.MoveHandlerFunctionId].ptr; + } } } diff --git a/pcsx2/GS/Renderers/HW/GSHwHack.h b/pcsx2/GS/Renderers/HW/GSHwHack.h index 0d62d9b34f..0cbba30516 100644 --- a/pcsx2/GS/Renderers/HW/GSHwHack.h +++ b/pcsx2/GS/Renderers/HW/GSHwHack.h @@ -57,6 +57,8 @@ public: static bool OI_Battlefield2(GSRendererHW& r, GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t); static bool OI_HauntingGround(GSRendererHW& r, GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t); + static bool MV_Growlanser(GSRendererHW& r); + template struct Entry { @@ -66,4 +68,5 @@ public: static const Entry s_get_skip_count_functions[]; static const Entry s_before_draw_functions[]; + static const Entry s_move_handler_functions[]; }; diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp index aaa0473321..27f21410fc 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp @@ -1219,6 +1219,12 @@ void GSRendererHW::InvalidateLocalMem(const GIFRegBITBLTBUF& BITBLTBUF, const GS void GSRendererHW::Move() { + if (m_mv && m_mv(*this)) + { + // Handled by HW hack. + return; + } + const int sx = m_env.TRXPOS.SSAX; const int sy = m_env.TRXPOS.SSAY; const int dx = m_env.TRXPOS.DSAX; diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.h b/pcsx2/GS/Renderers/HW/GSRendererHW.h index 18c1637bca..b139828820 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.h +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.h @@ -41,6 +41,7 @@ private: using GSC_Ptr = bool(*)(GSRendererHW& r, int& skip); // GSC - Get Skip Count using OI_Ptr = bool(*)(GSRendererHW& r, GSTexture* rt, GSTexture* ds, GSTextureCache::Source* t); // OI - Before draw + using MV_Ptr = bool(*)(GSRendererHW& r); // MV - Move // Require special argument bool OI_BlitFMV(GSTextureCache::Target* _rt, GSTextureCache::Source* t, const GSVector4i& r_draw); @@ -142,6 +143,7 @@ private: bool IsBadFrame(); GSC_Ptr m_gsc = nullptr; OI_Ptr m_oi = nullptr; + MV_Ptr m_mv = nullptr; int m_skip = 0; int m_skip_offset = 0; diff --git a/pcsx2/GameDatabase.cpp b/pcsx2/GameDatabase.cpp index 9e0cf3e177..6a82f9892a 100644 --- a/pcsx2/GameDatabase.cpp +++ b/pcsx2/GameDatabase.cpp @@ -230,13 +230,17 @@ void GameDatabase::parseAndInsert(const std::string_view& serial, const c4::yml: const std::string_view id_name(n.key().data(), n.key().size()); std::optional id = GameDatabaseSchema::parseHWFixName(id_name); std::optional value; - if (id.has_value() && (id.value() == GameDatabaseSchema::GSHWFixId::GetSkipCount || id.value() == GameDatabaseSchema::GSHWFixId::BeforeDraw)) + if (id.has_value() && (id.value() == GameDatabaseSchema::GSHWFixId::GetSkipCount || + id.value() == GameDatabaseSchema::GSHWFixId::BeforeDraw || + id.value() == GameDatabaseSchema::GSHWFixId::MoveHandler)) { const std::string_view str_value(n.has_val() ? std::string_view(n.val().data(), n.val().size()) : std::string_view()); if (id.value() == GameDatabaseSchema::GSHWFixId::GetSkipCount) value = GSLookupGetSkipCountFunctionId(str_value); else if (id.value() == GameDatabaseSchema::GSHWFixId::BeforeDraw) value = GSLookupBeforeDrawFunctionId(str_value); + else if (id.value() == GameDatabaseSchema::GSHWFixId::MoveHandler) + value = GSLookupMoveHandlerFunctionId(str_value); if (value.value_or(-1) < 0) { @@ -362,7 +366,8 @@ static const char* s_gs_hw_fix_names[] = { "maximumBlendingLevel", "recommendedBlendingLevel", "getSkipCount", - "beforeDraw" + "beforeDraw", + "moveHandler", }; static_assert(std::size(s_gs_hw_fix_names) == static_cast(GameDatabaseSchema::GSHWFixId::Count), "HW fix name lookup is correct size"); @@ -397,6 +402,7 @@ bool GameDatabaseSchema::isUserHackHWFix(GSHWFixId id) case GSHWFixId::PCRTCOverscan: case GSHWFixId::GetSkipCount: case GSHWFixId::BeforeDraw: + case GSHWFixId::MoveHandler: return false; default: return true; @@ -639,6 +645,9 @@ bool GameDatabaseSchema::GameEntry::configMatchesHWFix(const Pcsx2Config::GSOpti case GSHWFixId::BeforeDraw: return (static_cast(config.BeforeDrawFunctionId) == value); + case GSHWFixId::MoveHandler: + return (static_cast(config.MoveHandlerFunctionId) == value); + default: return false; } @@ -874,6 +883,10 @@ void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions& config.BeforeDrawFunctionId = static_cast(value); break; + case GSHWFixId::MoveHandler: + config.MoveHandlerFunctionId = static_cast(value); + break; + default: break; } diff --git a/pcsx2/GameDatabase.h b/pcsx2/GameDatabase.h index 8d108b89bd..ab760bcef2 100644 --- a/pcsx2/GameDatabase.h +++ b/pcsx2/GameDatabase.h @@ -96,6 +96,7 @@ namespace GameDatabaseSchema RecommendedBlendingLevel, GetSkipCount, BeforeDraw, + MoveHandler, Count }; diff --git a/pcsx2/Pcsx2Config.cpp b/pcsx2/Pcsx2Config.cpp index aaae4d6a7a..b1e0d8fd8a 100644 --- a/pcsx2/Pcsx2Config.cpp +++ b/pcsx2/Pcsx2Config.cpp @@ -609,6 +609,7 @@ bool Pcsx2Config::GSOptions::OptionsAreEqual(const GSOptions& right) const OpEqu(TVShader) && OpEqu(GetSkipCountFunctionId) && OpEqu(BeforeDrawFunctionId) && + OpEqu(MoveHandlerFunctionId) && OpEqu(SkipDrawEnd) && OpEqu(SkipDrawStart) &&