GS: stop forcing an RT feedback read for Ad-masked blends on fbfetch

A destination-alpha blend with alpha writes masked (blend_c == 1, !colormask.wa)
was given require_one_barrier wherever framebuffer fetch was available, on the
reasoning that fbfetch makes the feedback read cheap. The read is cheap. The
render pass is not: binding the target as an input attachment changes the pass
configuration, and OMSetRenderTargets ends the pass every time that flag flips.

NFS Underground flips it ~697 times a frame against only 40 real target
switches, so nearly every pass boundary in the frame came from this.

Drop the framebuffer_fetch term, leaving only the no-texture-barrier case where
the fallback already copies the RT and no pass boundary is at stake. That term
was ours; upstream gated this path on texture_barrier (later texture_barrier ||
multidraw_fb_copy) until it was made unconditional, and never on framebuffer
fetch. On turnip, which exposes the EXT spelling of the rasterization-order
extension, our term switched the path on for exactly the drivers where a pass
boundary is the dominant cost.

  Adreno 650   NFSU        440 -> 64 passes/frame, 27.6 -> 7.6 ms
               FlatOut 2   954 -> 102,             37.2 -> 16.4 ms
  Mali-G52     NFSU        746 -> 69,              276.6 -> 96.8 ms
               FlatOut 2   892 -> 96,              373.6 -> 145.0 ms

Correctness is unaffected: Ad blends that genuinely need software blending are
still forced into it by blend_requires_barrier, which tests blend_ad against the
RT alpha scaling separately. Scored per-pixel against the software renderer both
GPUs came out more accurate, not less - on Adreno the worst-case error in NFSU
halves, 19 to 8, with no pixel off by more than 16. Katamari, MGS3 and Ratchet &
Clank are bit-identical on both.
This commit is contained in:
Brian Degenhardt
2026-07-25 17:10:06 -07:00
parent c4d0a8a47c
commit ec57f7f1c6
+13 -6
View File
@@ -7033,12 +7033,19 @@ void GSRendererHW::EmulateBlending(int rt_alpha_min, int rt_alpha_max, DATEOptio
// Replace Ad with As, blend flags will be used from As since we are chaging the blend_index value.
// Must be done before index calculation, after blending equation optimizations
const bool blend_ad = m_conf.ps.blend_c == 1;
// On Vulkan without framebuffer fetch (notably Mali, which reads Cd through texture-barrier), this
// path forces thousands of RT feedback reads in blend-heavy scenes — a heavy cost and a source of
// stale-tile artifacts (the G615/G57 rainbow-box blinks). Keep it only where feedback is cheap
// (fbfetch) or where the fallback already copies the RT (no texture_barrier). Ported from
// sashkinbro/EmuCoreX (Fix Vulkan Basic blending feedback cost).
const bool fast_ad_alpha_masked_feedback = features.framebuffer_fetch || !features.texture_barrier;
// This path forces an RT feedback read per Ad-masked draw. It used to be taken wherever
// feedback was believed cheap - framebuffer fetch reads the target in-tile, so the read
// itself costs almost nothing. But on a tiler the read is not what you pay for: binding
// the target as an input attachment changes the render pass configuration, and
// OMSetRenderTargets ends the pass every time that flag flips. NFS Underground toggles it
// ~697 times a frame, which is 440 render passes on Adreno and 746 on Mali. Restrict it to
// the case the fallback already copies the RT, where no pass boundary is at stake.
//
// Correctness is unaffected: Ad blends that genuinely need software blending are still
// forced into it by blend_requires_barrier below (Ad is 0.5 not 1 for 128). Measured
// against the software renderer, dropping this made both GPUs *more* accurate, not less.
// Ported originally from sashkinbro/EmuCoreX (Fix Vulkan Basic blending feedback cost).
const bool fast_ad_alpha_masked_feedback = !features.texture_barrier;
bool blend_ad_alpha_masked = blend_ad && !m_conf.colormask.wa && fast_ad_alpha_masked_feedback;
const bool is_basic_blend = GSConfig.AccurateBlendingUnit != AccBlendLevel::Minimum;
if (blend_ad_alpha_masked && ((is_basic_blend || (COLCLAMP.CLAMP == 0) || m_conf.require_one_barrier)))