GS/HW: Add render fix for complex moves

And use it to fix Growlanser.
This commit is contained in:
Stenzek 2023-07-07 21:48:16 +10:00 committed by Connor McLaughlin
parent 819b61937f
commit dcd0a1f002
13 changed files with 198 additions and 10 deletions

View File

@ -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"

View File

@ -748,6 +748,7 @@ struct Pcsx2Config
u8 TVShader = 0;
s16 GetSkipCountFunctionId = -1;
s16 BeforeDrawFunctionId = -1;
s16 MoveHandlerFunctionId = -1;
int SkipDrawStart = 0;
int SkipDrawEnd = 0;

View File

@ -287,6 +287,9 @@
},
"beforeDraw": {
"type": "string"
},
"moveHandler": {
"type": "string"
}
},
"additionalProperties": false

View File

@ -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();
}

View File

@ -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();

View File

@ -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)

View File

@ -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<GSRendererHW::GSC_Ptr> GSHwHack::s_get_skip_count_functions[] = {
@ -1159,7 +1284,11 @@ const GSHwHack::Entry<GSRendererHW::OI_Ptr> 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<GSRendererHW::MV_Ptr> 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<s16>(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<size_t>(GSConfig.MoveHandlerFunctionId) < std::size(GSHwHack::s_move_handler_functions))
{
m_mv = GSHwHack::s_move_handler_functions[GSConfig.MoveHandlerFunctionId].ptr;
}
}
}

View File

@ -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 <typename F>
struct Entry
{
@ -66,4 +68,5 @@ public:
static const Entry<GSRendererHW::GSC_Ptr> s_get_skip_count_functions[];
static const Entry<GSRendererHW::OI_Ptr> s_before_draw_functions[];
static const Entry<GSRendererHW::MV_Ptr> s_move_handler_functions[];
};

View File

@ -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;

View File

@ -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;

View File

@ -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<GameDatabaseSchema::GSHWFixId> id = GameDatabaseSchema::parseHWFixName(id_name);
std::optional<s32> 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<u32>(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<int>(config.BeforeDrawFunctionId) == value);
case GSHWFixId::MoveHandler:
return (static_cast<int>(config.MoveHandlerFunctionId) == value);
default:
return false;
}
@ -874,6 +883,10 @@ void GameDatabaseSchema::GameEntry::applyGSHardwareFixes(Pcsx2Config::GSOptions&
config.BeforeDrawFunctionId = static_cast<s16>(value);
break;
case GSHWFixId::MoveHandler:
config.MoveHandlerFunctionId = static_cast<s16>(value);
break;
default:
break;
}

View File

@ -96,6 +96,7 @@ namespace GameDatabaseSchema
RecommendedBlendingLevel,
GetSkipCount,
BeforeDraw,
MoveHandler,
Count
};

View File

@ -609,6 +609,7 @@ bool Pcsx2Config::GSOptions::OptionsAreEqual(const GSOptions& right) const
OpEqu(TVShader) &&
OpEqu(GetSkipCountFunctionId) &&
OpEqu(BeforeDrawFunctionId) &&
OpEqu(MoveHandlerFunctionId) &&
OpEqu(SkipDrawEnd) &&
OpEqu(SkipDrawStart) &&