From f0aa0f1949800d80250642b8b3eaaf01da65c9ae Mon Sep 17 00:00:00 2001 From: Brian Degenhardt Date: Thu, 30 Jul 2026 10:34:21 -0700 Subject: [PATCH] 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. --- pcsx2/GS/GSState.cpp | 43 ++++++++++++++++++++++++++++++++++--------- pcsx2/GS/GSState.h | 2 +- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/pcsx2/GS/GSState.cpp b/pcsx2/GS/GSState.cpp index 46da8d631c..d037e0ef01 100644 --- a/pcsx2/GS/GSState.cpp +++ b/pcsx2/GS/GSState.cpp @@ -349,9 +349,27 @@ void GSState::Reset(bool hardware_reset) m_perfmon_frame.Reset(); } -template +template 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; \ m_fpGIFPackedRegHandlerXYZ[P][1] = &GSState::GIFPackedRegHandlerXYZF2; \ @@ -365,13 +383,13 @@ void GSState::SetPrimHandlers() m_fpGIFPackedRegHandlerSTQRGBAXYZ2[P] = &GSState::GIFPackedRegHandlerSTQRGBAXYZ2; 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(); + { + if (GSConfig.UserHacks_AutoFlush == GSHWAutoFlushLevel::SpritesOnly) + SetPrimHandlers(); + else + SetPrimHandlers(); + } else - SetPrimHandlers(); + { + SetPrimHandlers(); + } std::fill(std::begin(m_fpGIFRegHandlers), std::end(m_fpGIFRegHandlers), &GSState::GIFRegHandlerNull); diff --git a/pcsx2/GS/GSState.h b/pcsx2/GS/GSState.h index 57a0da2741..6d7b8ec839 100644 --- a/pcsx2/GS/GSState.h +++ b/pcsx2/GS/GSState.h @@ -136,7 +136,7 @@ private: void GIFRegHandlerTRXDIR(const GIFReg* RESTRICT r); void GIFRegHandlerHWREG(const GIFReg* RESTRICT r); - template void SetPrimHandlers(); + template void SetPrimHandlers(); struct GSTransferBuffer {