diff --git a/bin/resources/shaders/dx11/tfx.fx b/bin/resources/shaders/dx11/tfx.fx index 528ef12c53..bda7e9d79e 100644 --- a/bin/resources/shaders/dx11/tfx.fx +++ b/bin/resources/shaders/dx11/tfx.fx @@ -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; diff --git a/bin/resources/shaders/opengl/tfx_fs.glsl b/bin/resources/shaders/opengl/tfx_fs.glsl index 0e2ac88c97..49d8b80e58 100644 --- a/bin/resources/shaders/opengl/tfx_fs.glsl +++ b/bin/resources/shaders/opengl/tfx_fs.glsl @@ -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; diff --git a/bin/resources/shaders/vulkan/tfx.glsl b/bin/resources/shaders/vulkan/tfx.glsl index 1bb533ec41..cdb9bc44b3 100644 --- a/bin/resources/shaders/vulkan/tfx.glsl +++ b/bin/resources/shaders/vulkan/tfx.glsl @@ -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; diff --git a/pcsx2/GS/Renderers/Common/GSDevice.h b/pcsx2/GS/Renderers/Common/GSDevice.h index d1e3d1dfb5..e00840f53d 100644 --- a/pcsx2/GS/Renderers/Common/GSDevice.h +++ b/pcsx2/GS/Renderers/Common/GSDevice.h @@ -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; diff --git a/pcsx2/GS/Renderers/DX11/GSDevice11.cpp b/pcsx2/GS/Renderers/DX11/GSDevice11.cpp index 43c6724675..77ab2bbfb2 100644 --- a/pcsx2/GS/Renderers/DX11/GSDevice11.cpp +++ b/pcsx2/GS/Renderers/DX11/GSDevice11.cpp @@ -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(sel.aa1)); sm.AddMacro("PS_ABE", sel.abe); diff --git a/pcsx2/GS/Renderers/DX12/GSDevice12.cpp b/pcsx2/GS/Renderers/DX12/GSDevice12.cpp index 07f0dd9d40..731caeb6c0 100644 --- a/pcsx2/GS/Renderers/DX12/GSDevice12.cpp +++ b/pcsx2/GS/Renderers/DX12/GSDevice12.cpp @@ -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(sel.aa1)); sm.AddMacro("PS_ABE", sel.abe); diff --git a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp index d2930dd4d1..c3c5d1fe06 100644 --- a/pcsx2/GS/Renderers/HW/GSRendererHW.cpp +++ b/pcsx2/GS/Renderers/HW/GSRendererHW.cpp @@ -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 diff --git a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp index 276a144a19..de04c4181b 100644 --- a/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp +++ b/pcsx2/GS/Renderers/OpenGL/GSDeviceOGL.cpp @@ -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(sel.aa1)) + fmt::format("#define PS_ABE {}\n", sel.abe) diff --git a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp index 16371b49a4..a4de7a082b 100644 --- a/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp +++ b/pcsx2/GS/Renderers/Vulkan/GSDeviceVK.cpp @@ -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(sel.aa1)); AddMacro(ss, "PS_ABE", sel.abe);