GS: take the direct vertex kick for non-sprite prims at autoflush SpritesOnly

The auto_flush instantiations of the vertex handlers exist to feed
HandleAutoFlush, which reads the incoming vertex out of m_v. To do that they
stage every vertex through m_v instead of keeping it in registers, which is why
SetPrimHandlers hands the same auto_flush argument to every primitive type.

At SpritesOnly that is wasted on everything that is not a sprite. IsAutoFlushDraw
early-outs on the prim before it looks at anything else, so those prims write a
staged vertex, read it once, and discard it. Narrow the template argument per
prim so they take the fused direct kick instead, mirroring IsAutoFlushDraw's
early-out exactly -- it keys on the level alone and not on the renderer, so the
software path narrows in step.

Dragon Quest VIII renders identically at levels 1 and 2 (same draws, passes and
copies), so level 2 is an exact staged control for level 1's direct path with no
rendering difference to confound it. GS-thread cycles over 3 runs each, 240
frames: 2043.2M staged against 1947.4M direct, ranges disjoint, -4.7%. The parse
handler itself goes 272.5M -> 156.2M, so it accounts for essentially the whole
delta. Rebuilding the old handler table and diffing against it agrees: -5.1%.

Output is unchanged, as it must be: prims, draws, render passes and copies are
identical on Dragon Quest VIII and Rogue Galaxy, and all four dumped frames are
pixel-identical under both the hardware and the software renderer.

581 GameDB entries ship autoFlush: 1.
This commit is contained in:
Brian Degenhardt
2026-07-30 21:55:58 -07:00
parent 80e4d09b99
commit f0aa0f1949
2 changed files with 35 additions and 10 deletions
+34 -9
View File
@@ -349,9 +349,27 @@ void GSState::Reset(bool hardware_reset)
m_perfmon_frame.Reset();
}
template<bool auto_flush>
template<bool auto_flush, bool sprites_only>
void GSState::SetPrimHandlers()
{
// The auto_flush instantiations exist to feed HandleAutoFlush, which reads the
// incoming vertex out of m_v -- so they stage every vertex through m_v instead of
// keeping it in registers. At SpritesOnly that is wasted on everything that is not
// a sprite: IsAutoFlushDraw early-outs on the prim before it looks at anything, so
// the staged vertex is written, read once, and discarded. Give those prims the
// fused direct kick.
//
// Measured on Dragon Quest VIII (SLUS-21207, 240 frames). It renders identically at
// levels 1 and 2 -- same draws, passes and copies -- so level 2 is an exact staged
// control for level 1's direct path, with no rendering difference to confound it:
// GS-thread cycles 2043.2M staged against 1947.4M direct over 3 runs each, ranges
// disjoint. That is -4.7% of GS-thread time in a title spending 8% of it in this one
// handler, and 581 GameDB entries ship autoFlush: 1.
//
// Mirrors IsAutoFlushDraw's early-out exactly, which keys on the level alone and
// not on the renderer, so the software path narrows in step with it.
constexpr bool non_sprite_af = auto_flush && !sprites_only;
#define SetHandlerXYZ(P, auto_flush) \
m_fpGIFPackedRegHandlerXYZ[P][0] = &GSState::GIFPackedRegHandlerXYZF2<P, 0, auto_flush>; \
m_fpGIFPackedRegHandlerXYZ[P][1] = &GSState::GIFPackedRegHandlerXYZF2<P, 1, auto_flush>; \
@@ -365,13 +383,13 @@ void GSState::SetPrimHandlers()
m_fpGIFPackedRegHandlerSTQRGBAXYZ2[P] = &GSState::GIFPackedRegHandlerSTQRGBAXYZ2<P, auto_flush>;
SetHandlerXYZ(GS_POINTLIST, true);
SetHandlerXYZ(GS_LINELIST, auto_flush);
SetHandlerXYZ(GS_LINESTRIP, auto_flush);
SetHandlerXYZ(GS_TRIANGLELIST, auto_flush);
SetHandlerXYZ(GS_TRIANGLESTRIP, auto_flush);
SetHandlerXYZ(GS_TRIANGLEFAN, auto_flush);
SetHandlerXYZ(GS_LINELIST, non_sprite_af);
SetHandlerXYZ(GS_LINESTRIP, non_sprite_af);
SetHandlerXYZ(GS_TRIANGLELIST, non_sprite_af);
SetHandlerXYZ(GS_TRIANGLESTRIP, non_sprite_af);
SetHandlerXYZ(GS_TRIANGLEFAN, non_sprite_af);
SetHandlerXYZ(GS_SPRITE, auto_flush);
SetHandlerXYZ(GS_INVALID, auto_flush);
SetHandlerXYZ(GS_INVALID, non_sprite_af);
#undef SetHandlerXYZ
}
@@ -1088,9 +1106,16 @@ void GSState::ResetHandlers()
m_fpGIFPackedRegHandlers[GIF_REG_NOP] = &GSState::GIFPackedRegHandlerNOP;
if (IsAutoFlushEnabled())
SetPrimHandlers<true>();
{
if (GSConfig.UserHacks_AutoFlush == GSHWAutoFlushLevel::SpritesOnly)
SetPrimHandlers<true, true>();
else
SetPrimHandlers<true, false>();
}
else
SetPrimHandlers<false>();
{
SetPrimHandlers<false, false>();
}
std::fill(std::begin(m_fpGIFRegHandlers), std::end(m_fpGIFRegHandlers), &GSState::GIFRegHandlerNull);
+1 -1
View File
@@ -136,7 +136,7 @@ private:
void GIFRegHandlerTRXDIR(const GIFReg* RESTRICT r);
void GIFRegHandlerHWREG(const GIFReg* RESTRICT r);
template<bool auto_flush> void SetPrimHandlers();
template<bool auto_flush, bool sprites_only> void SetPrimHandlers();
struct GSTransferBuffer
{