mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
GS/HW: carry the blend-mix factor in the output alpha without dual-source blend
A blend mix hands the blend unit exactly one number - the alpha factor, on the PS2's 0..2 scale where 128 is opaque. A second fragment output is the usual way to carry a value on that scale, but it is not the only one: fixed-function SRC_ALPHA reads the first output's alpha, and the shader can put the factor there instead. Two cases make that free. When the target holds its alpha double-scaled, the alpha the shader would write IS the factor - tfx computes both as C.a/128 under RTA correction - so scaling the target is the whole change. Otherwise the substitution is free whenever the pass writes no alpha at all, because the output alpha is discarded on the way to the target: a draw whose alpha is masked outright, or one whose alpha write has moved into a second pass under SPLIT_RGB_ONLY. Only the plain mix1 shape qualifies. The other mix cases rewrite the second output's RGB independently of its alpha, so there the two outputs really do carry different values and no substitution exists. Without this, a GPU with no dual-source blend emulates the equation in the shader, which needs a fresh destination read per primitive. With neither a texture barrier nor a multidraw framebuffer copy available, all it gets is one snapshot taken before the draw, so every primitive after the first composites against stale pixels. That is what hollowed out God of War II's menu glyphs on Mali r44p1, where the whole text is a single draw whose drop-shadow and bright quads overlap each other 200 times. Measured against a dual-source GPU rendering the same dump: over the text the mean per-pixel error falls from 3.351 to 0.109 and the worst pixel from 163 to 25, with the lit-pixel count landing on 3896 against the reference's 3898. Frame-wide it removes 25k of the 49k differing pixels and introduces 22. It needs no barriers and no target copies at all, where matching this by refreshing the snapshot per primitive group cost ~1000 render-pass breaks a frame and two thirds of the frame rate on device. No effect where dual-source blending exists: 33 frames across 11 dumps are byte identical.
This commit is contained in:
@@ -94,6 +94,7 @@
|
||||
#define PS_COLCLIP_HW 0
|
||||
#define PS_RTA_CORRECTION 0
|
||||
#define PS_RTA_SRC_CORRECTION 0
|
||||
#define PS_BLEND_FACTOR_IN_ALPHA 0
|
||||
#define PS_COLCLIP 0
|
||||
#define PS_BLEND_A 0
|
||||
#define PS_BLEND_B 0
|
||||
@@ -1548,7 +1549,9 @@ if (bad)
|
||||
|
||||
// Output color scaling
|
||||
#if !PS_NO_COLOR
|
||||
o_col0.a = PS_RTA_CORRECTION ? C.a / 128.0f : C.a / 255.0f;
|
||||
// PS_BLEND_FACTOR_IN_ALPHA: no dual-source blend unit, so the blend factor rides in the first
|
||||
// output's alpha. Unreachable on D3D, which always has one, but the selector bit is shared.
|
||||
o_col0.a = PS_BLEND_FACTOR_IN_ALPHA ? alpha_blend.a : (PS_RTA_CORRECTION ? C.a / 128.0f : C.a / 255.0f);
|
||||
o_col0.rgb = PS_COLCLIP_HW ? float3(C.rgb / 65535.0f) : C.rgb / 255.0f;
|
||||
#if !PS_NO_COLOR1
|
||||
o_col1 = alpha_blend;
|
||||
|
||||
@@ -1412,7 +1412,11 @@ void ps_main()
|
||||
#endif
|
||||
|
||||
#if !PS_NO_COLOR
|
||||
#if PS_RTA_CORRECTION
|
||||
#if PS_BLEND_FACTOR_IN_ALPHA
|
||||
// No dual-source blend unit here. Nothing is keeping this pass's alpha, so hand the
|
||||
// blend factor to fixed-function SRC_ALPHA through it instead of a second output.
|
||||
C.a = alpha_blend.a;
|
||||
#elif PS_RTA_CORRECTION
|
||||
C.a = C.a / 128.0f;
|
||||
#else
|
||||
C.a = C.a / 255.0f;
|
||||
|
||||
@@ -1944,7 +1944,11 @@ void main()
|
||||
|
||||
// Output color scaling
|
||||
#if !PS_NO_COLOR
|
||||
#if PS_RTA_CORRECTION
|
||||
#if PS_BLEND_FACTOR_IN_ALPHA
|
||||
// No dual-source blend unit here. Nothing is keeping this pass's alpha, so hand the
|
||||
// blend factor to fixed-function SRC_ALPHA through it instead of a second output.
|
||||
o_col0.a = alpha_blend.a;
|
||||
#elif PS_RTA_CORRECTION
|
||||
o_col0.a = C.a / 128.0f;
|
||||
#else
|
||||
o_col0.a = C.a / 255.0f;
|
||||
|
||||
@@ -765,6 +765,7 @@ struct alignas(16) GSHWDrawConfig
|
||||
u32 pabe : 1;
|
||||
u32 no_color : 1; // disables color output entirely (depth only)
|
||||
u32 no_color1 : 1; // disables second color output (when unnecessary)
|
||||
u32 blend_factor_in_alpha : 1; // writes the blend factor to the first output's alpha instead of the second output (no dual-source blend)
|
||||
|
||||
// Others ways to fetch the texture
|
||||
u32 channel : 3;
|
||||
|
||||
@@ -2079,6 +2079,7 @@ void GSDevice11::SetupPS(const PSSelector& sel, const GSHWDrawConfig::PSConstant
|
||||
sm.AddMacro("PS_TEX_IS_FB", sel.tex_is_fb);
|
||||
sm.AddMacro("PS_NO_COLOR", sel.no_color);
|
||||
sm.AddMacro("PS_NO_COLOR1", sel.no_color1);
|
||||
sm.AddMacro("PS_BLEND_FACTOR_IN_ALPHA", sel.blend_factor_in_alpha);
|
||||
sm.AddMacro("PS_ZTST", sel.ztst);
|
||||
sm.AddMacro("PS_AA1", static_cast<u32>(sel.aa1));
|
||||
sm.AddMacro("PS_ABE", sel.abe);
|
||||
|
||||
@@ -3299,6 +3299,7 @@ const ID3DBlob* GSDevice12::GetTFXPixelShader(const GSHWDrawConfig::PSSelector&
|
||||
sm.AddMacro("PS_TEX_IS_FB", sel.tex_is_fb);
|
||||
sm.AddMacro("PS_NO_COLOR", sel.no_color);
|
||||
sm.AddMacro("PS_NO_COLOR1", sel.no_color1);
|
||||
sm.AddMacro("PS_BLEND_FACTOR_IN_ALPHA", sel.blend_factor_in_alpha);
|
||||
sm.AddMacro("PS_ZTST", sel.ztst);
|
||||
sm.AddMacro("PS_AA1", static_cast<u32>(sel.aa1));
|
||||
sm.AddMacro("PS_ABE", sel.abe);
|
||||
|
||||
@@ -7232,6 +7232,56 @@ void GSRendererHW::EmulateBlending(int rt_alpha_min, int rt_alpha_max, DATEOptio
|
||||
GSDevice::IsDualSourceBlendFactor(blend.src) || GSDevice::IsDualSourceBlendFactor(blend.dst) ||
|
||||
blend_mix || PABE;
|
||||
|
||||
// ...but blend mix does not always have to give up. It hands the blend unit exactly one number,
|
||||
// the alpha factor, on the PS2's 0..2 scale where 128 is opaque. A second fragment output is the
|
||||
// usual way to carry a value on that scale - it is not the only one. Fixed-function SRC_ALPHA
|
||||
// reads the primary colour output's alpha, and when the target holds its alpha double-scaled
|
||||
// (RTA correction) the shader writes that same number there as well: tfx computes both
|
||||
// alpha_blend and o_col0.a as C.a/128. So the substitution is exact, not an approximation.
|
||||
//
|
||||
// It is worth the trouble because the alternative is not merely slower, it is wrong. Full
|
||||
// software blending needs a fresh destination read per primitive, and a driver with neither a
|
||||
// texture barrier nor a multidraw framebuffer copy can only offer one pre-draw snapshot for the
|
||||
// whole draw. Every primitive after the first then composites against stale pixels - which is
|
||||
// what erodes God of War II's menu glyphs on Mali r44p1, where the text is a single draw whose
|
||||
// drop-shadow and bright quads overlap each other 200 times over.
|
||||
//
|
||||
// Only the plain mix1 shape qualifies. The other mix cases rewrite the second output's RGB
|
||||
// independently of its alpha (the blend_hw types, PABE), so there the two outputs really do
|
||||
// carry different values and no substitution exists.
|
||||
const bool blend_mix1_rewrites_src1 =
|
||||
(m_conf.ps.blend_b == m_conf.ps.blend_d && (alpha_c0_high_min_one || alpha_c1_high_min_one || alpha_c2_high_one)) ||
|
||||
(m_conf.ps.blend_a == m_conf.ps.blend_d);
|
||||
const bool blend_mix_factor_is_alpha =
|
||||
!features.dual_source_blend && blend_mix && blend_mix1 && !blend_mix2 && !blend_mix3 &&
|
||||
!blend_mix1_rewrites_src1 && !blend_ad_alpha_masked && !PABE &&
|
||||
// Only As reaches the blend unit through SRC1; Ad and Af already use DST_ALPHA/CONST_COLOR.
|
||||
m_conf.ps.blend_c == 0 && blend.dst == GSDevice::INV_SRC1_COLOR &&
|
||||
// A shuffle or an fbmask gives the primary output's alpha a meaning of its own.
|
||||
!m_conf.ps.shuffle && !m_conf.ps.fbmask &&
|
||||
// So does an alpha test that feeds the target's old alpha back into the output.
|
||||
m_conf.ps.afail != PS_AFAIL::RGB_ONLY && m_conf.ps.afail != PS_AFAIL::RGB_ONLY_SW_Z;
|
||||
|
||||
// The alpha the shader would write is already the factor when the target holds its alpha
|
||||
// double-scaled: tfx computes both alpha_blend and o_col0.a as C.a/128 under RTA correction. So
|
||||
// scale the target and the substitution costs nothing at all.
|
||||
const bool blend_mix_factor_fits_dst_alpha =
|
||||
blend_mix_factor_is_alpha &&
|
||||
// The primary alpha must still be C.a/128 when it is written, so nothing may rewrite it
|
||||
// downstream of where the factor is captured - 16-bit/FBA alpha correction does.
|
||||
m_conf.ps.dst_fmt == GSLocalMemory::PSM_FMT_32 && !m_conf.ps.fba &&
|
||||
// And the target must be able to hold its alpha double-scaled. can_scale_rt_alpha already
|
||||
// accounts for this draw's own alpha writes, so nothing above 128 is being lost here.
|
||||
(rt->m_rt_alpha_scale || can_scale_rt_alpha);
|
||||
|
||||
// Failing that, the substitution is still free whenever this pass writes no alpha: the output
|
||||
// alpha is discarded on the way to the target, so it may as well carry the factor. That covers a
|
||||
// draw whose alpha is masked outright, and - the case that matters for God of War II - one whose
|
||||
// alpha write has been moved into a second pass by SPLIT_RGB_ONLY.
|
||||
const bool blend_mix_factor_rides_masked_alpha =
|
||||
blend_mix_factor_is_alpha && !blend_mix_factor_fits_dst_alpha &&
|
||||
(m_conf.alpha_test == GSHWDrawConfig::AlphaTestMode::SPLIT_RGB_ONLY);
|
||||
|
||||
const bool force_sw_blending =
|
||||
// If we have fbfetch, use software blending when we need the fb value for anything else.
|
||||
// This saves outputting the second color when it's not needed.
|
||||
@@ -7243,7 +7293,9 @@ void GSRendererHW::EmulateBlending(int rt_alpha_min, int rt_alpha_max, DATEOptio
|
||||
|
||||
// Mobile GPUs (notably Mali) that lack a hardware dual-source blend unit: emulate the
|
||||
// SRC1 equations in-shader for exactly the draws that need it, at any accuracy level.
|
||||
(!features.dual_source_blend && blend_path_requires_dual_source) ||
|
||||
// Unless the factor can ride in the primary output's alpha instead - see above.
|
||||
(!features.dual_source_blend && blend_path_requires_dual_source &&
|
||||
!blend_mix_factor_fits_dst_alpha && !blend_mix_factor_rides_masked_alpha) ||
|
||||
|
||||
// Force SW blending with barriers.
|
||||
GSConfig.UseDebugBlend;
|
||||
@@ -7506,6 +7558,33 @@ void GSRendererHW::EmulateBlending(int rt_alpha_min, int rt_alpha_max, DATEOptio
|
||||
m_conf.ps.blend_d = 0;
|
||||
}
|
||||
|
||||
if (blend_mix_factor_fits_dst_alpha || blend_mix_factor_rides_masked_alpha)
|
||||
{
|
||||
// No second output on this GPU, so hand the blend unit the factor through the
|
||||
// primary output's alpha instead.
|
||||
pxAssert(m_conf.ps.blend_hw == 0);
|
||||
|
||||
if (blend_mix_factor_fits_dst_alpha)
|
||||
{
|
||||
// The alpha this pass writes is the factor already, once the target holds its
|
||||
// alpha double-scaled. Make sure it does; a no-op when it is already scaled.
|
||||
if (!rt->m_rt_alpha_scale)
|
||||
{
|
||||
rt->ScaleRTAlpha();
|
||||
m_conf.rt = rt->m_texture;
|
||||
}
|
||||
|
||||
new_rt_alpha_scale = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Nothing is keeping this pass's alpha, so overwrite it with the factor.
|
||||
m_conf.ps.blend_factor_in_alpha = 1;
|
||||
}
|
||||
|
||||
blend.dst = GSDevice::INV_SRC_ALPHA;
|
||||
}
|
||||
|
||||
// Elide DSB colour output if not used by dest or alpha test.
|
||||
m_conf.ps.no_color1 &= !GSDevice::IsDualSourceBlendFactor(blend.dst);
|
||||
|
||||
@@ -9207,7 +9286,8 @@ void GSRendererHW::EmulateAlphaTest(DATEOptions& date_options)
|
||||
// fragment. Under RGB_ONLY every fragment writes RGB and only the passing ones write A and Z, so
|
||||
// one pass with the test off writing RGB, then one with the test on writing A and Z, is exact.
|
||||
// It is strictly better than pass/fail, which splits RGB across two passes and so composites
|
||||
// overlapping primitives out of order.
|
||||
// overlapping primitives out of order. It also leaves the first pass with no alpha write at all,
|
||||
// which is what lets a blend mix keep its hardware blend - see EmulateBlending.
|
||||
const bool split_rgb_only =
|
||||
(afail == AFAIL_RGB_ONLY) && independent_z && independent_rgb && !features.dual_source_blend;
|
||||
const bool simple_zb_only = (afail == AFAIL_ZB_ONLY) && independent_z;
|
||||
@@ -9428,6 +9508,9 @@ void GSRendererHW::EmulateAlphaTestSecondPass()
|
||||
m_conf.alpha_second_pass.ps.atst = ps_atst;
|
||||
m_conf.alpha_second_pass.ps_aref = ps_aref;
|
||||
m_conf.alpha_second_pass.ps.afail = PS_AFAIL::KEEP;
|
||||
|
||||
// The factor substitution belongs to the blending pass only; this one writes real alpha.
|
||||
m_conf.alpha_second_pass.ps.blend_factor_in_alpha = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
|
||||
@@ -2328,6 +2328,7 @@ std::string GSDeviceOGL::GetPSSource(const PSSelector& sel)
|
||||
+ fmt::format("#define PS_SCANMSK {}\n", sel.scanmsk)
|
||||
+ fmt::format("#define PS_NO_COLOR {}\n", sel.no_color)
|
||||
+ fmt::format("#define PS_NO_COLOR1 {}\n", sel.no_color1)
|
||||
+ fmt::format("#define PS_BLEND_FACTOR_IN_ALPHA {}\n", sel.blend_factor_in_alpha)
|
||||
+ fmt::format("#define PS_ZTST {}\n", sel.ztst)
|
||||
+ fmt::format("#define PS_AA1 {}\n", static_cast<u32>(sel.aa1))
|
||||
+ fmt::format("#define PS_ABE {}\n", sel.abe)
|
||||
|
||||
@@ -6486,6 +6486,7 @@ VkShaderModule GSDeviceVK::GetTFXFragmentShader(const GSHWDrawConfig::PSSelector
|
||||
AddMacro(ss, "PS_TEX_IS_FB", sel.tex_is_fb);
|
||||
AddMacro(ss, "PS_NO_COLOR", sel.no_color);
|
||||
AddMacro(ss, "PS_NO_COLOR1", sel.no_color1);
|
||||
AddMacro(ss, "PS_BLEND_FACTOR_IN_ALPHA", sel.blend_factor_in_alpha);
|
||||
AddMacro(ss, "PS_ZTST", sel.ztst);
|
||||
AddMacro(ss, "PS_AA1", static_cast<u32>(sel.aa1));
|
||||
AddMacro(ss, "PS_ABE", sel.abe);
|
||||
|
||||
Reference in New Issue
Block a user