Fix missing aircraft ground shadow on GPUs without constant-alpha blending (Blackwell and Battlemage)

The aircraft's ground shadow is drawn with the Xenos CONSTANT_ALPHA blend factors, which dfd8b92a routed to D3D12's ALPHA_FACTOR family, an OPTIONAL feature that must be checked via OPTIONS13::AlphaBlendFactorSupported first. We never checked, so on a device reporting false the shadow's pipeline is rejected and, being indistinguishable from one still compiling, its draw is dropped from every frame in silence. Now the capability is queried, and where it is absent the pixel shader carries the blend constant's alpha in its alpha output so INV_SRC_ALPHA reproduces the same blend exactly.
This commit is contained in:
Dipshet
2026-08-09 19:36:57 +02:00
parent 0164b919f8
commit 566817d628
6 changed files with 160 additions and 9 deletions
@@ -92,6 +92,12 @@ class D3D12Provider : public GraphicsProvider {
D3D12_RESOURCE_BINDING_TIER GetResourceBindingTier() const { return resource_binding_tier_; }
D3D12_TILED_RESOURCES_TIER GetTiledResourcesTier() const { return tiled_resources_tier_; }
bool AreUnalignedBlockTexturesSupported() const { return unaligned_block_textures_supported_; }
// D3D12_BLEND_ALPHA_FACTOR / INV_ALPHA_FACTOR are an OPTIONAL feature: the
// docs require D3D12_FEATURE_DATA_D3D12_OPTIONS13::AlphaBlendFactorSupported
// to be TRUE before either may be used. Using them without checking gets the
// whole pipeline rejected, and the draw disappears with it. False when the
// runtime is too old to answer, which is the safe reading.
bool IsAlphaBlendFactorSupported() const { return alpha_blend_factor_supported_; }
uint32_t GetVirtualAddressBitsPerResource() const { return virtual_address_bits_per_resource_; }
// Proxies for DirectX functions since they are loaded dynamically.
@@ -167,6 +173,7 @@ class D3D12Provider : public GraphicsProvider {
bool ps_specified_stencil_reference_supported_;
bool rasterizer_ordered_views_supported_;
bool unaligned_block_textures_supported_;
bool alpha_blend_factor_supported_;
};
} // namespace rex::ui::d3d12
@@ -111,7 +111,7 @@ class DxbcShaderTranslator : public ShaderTranslator {
// If anything in this is structure is changed in a way not compatible with
// the previous layout, invalidate the pipeline storages by increasing this
// version number (0xYYYYMMDD)!
static constexpr uint32_t kVersion = 0x20260410;
static constexpr uint32_t kVersion = 0x20260809;
enum class DepthStencilMode : uint32_t {
kNoModifiers,
@@ -176,6 +176,18 @@ class DxbcShaderTranslator : public ShaderTranslator {
uint32_t dynamic_addressable_register_count : 8;
// Non-ROV - depth / stencil output mode.
DepthStencilMode depth_stencil_mode : 2;
// Non-ROV - write the blend constant's alpha into the alpha of colour
// output 0 instead of what the guest shader produced.
//
// The colour blend equation has no way of its own to reach the blend
// constant's alpha, and D3D12's ALPHA_FACTOR family - which exists for
// exactly that - is an optional feature some devices do not have. Putting
// the value in the alpha output lets INV_SRC_ALPHA stand in for
// ONE_MINUS_CONSTANT_ALPHA exactly, using only always-available factors.
//
// Only set when the alpha equation does not read the alpha output, so
// overwriting it cannot change the alpha result.
uint32_t alpha_output_is_blend_constant : 1;
} pixel;
explicit Modification(uint64_t modification_value = 0) : value(modification_value) {
@@ -4702,6 +4702,19 @@ void D3D12CommandProcessor::UpdateSystemConstantValues(
system_constants_.edram_blend_constant[3] = regs.Get<float>(XE_GPU_REG_RB_BLEND_ALPHA);
}
// The blend constant is read by the pixel shader on the RTV path too when
// alpha_output_is_blend_constant is set, so upload it regardless of path.
// Four dirty-tracked floats; a shader reading a stale constant would cost
// far more than the upload does.
dirty |= system_constants_.edram_blend_constant[0] != regs.Get<float>(XE_GPU_REG_RB_BLEND_RED);
system_constants_.edram_blend_constant[0] = regs.Get<float>(XE_GPU_REG_RB_BLEND_RED);
dirty |= system_constants_.edram_blend_constant[1] != regs.Get<float>(XE_GPU_REG_RB_BLEND_GREEN);
system_constants_.edram_blend_constant[1] = regs.Get<float>(XE_GPU_REG_RB_BLEND_GREEN);
dirty |= system_constants_.edram_blend_constant[2] != regs.Get<float>(XE_GPU_REG_RB_BLEND_BLUE);
system_constants_.edram_blend_constant[2] = regs.Get<float>(XE_GPU_REG_RB_BLEND_BLUE);
dirty |= system_constants_.edram_blend_constant[3] != regs.Get<float>(XE_GPU_REG_RB_BLEND_ALPHA);
system_constants_.edram_blend_constant[3] = regs.Get<float>(XE_GPU_REG_RB_BLEND_ALPHA);
cbuffer_binding_system_.up_to_date &= !dirty;
}
+102 -6
View File
@@ -61,8 +61,66 @@ REXCVAR_DEFINE_INT32(d3d12_pipeline_creation_threads, -1, "GPU/D3D12",
REXCVAR_DEFINE_BOOL(d3d12_tessellation_wireframe, false, "GPU/D3D12",
"Render tessellation as wireframe");
REXCVAR_DEFINE_BOOL(d3d12_constant_alpha_blend, true, "GPU/D3D12",
"Use the D3D12 constant-alpha blend factors where the device reports "
"support for them. Turning this off forces the substitute encoding even "
"on a device that allows them, which is the only way to exercise that "
"path for testing; devices without support always use it regardless")
.lifecycle(rex::cvar::Lifecycle::kRequiresRestart);
namespace rex::graphics::d3d12 {
// Whether the D3D12 constant-alpha blend factors may be emitted: the device
// must report AlphaBlendFactorSupported - they are an optional feature, and
// using them without it makes CreateGraphicsPipelineState reject the pipeline
// outright - and the cvar must not be forcing the substitute for testing.
static bool UseConstantAlphaBlendFactors(const D3D12CommandProcessor& command_processor) {
return command_processor.GetD3D12Provider().IsAlphaBlendFactorSupported() &&
REXCVAR_GET(d3d12_constant_alpha_blend);
}
// Whether this draw may carry the blend constant's alpha in the pixel shader's
// alpha output so INV_SRC_ALPHA can stand in for ONE_MINUS_CONSTANT_ALPHA.
//
// Needed only when a COLOUR slot asks for the constant's alpha (guest 14/15) -
// the colour equation is the one with no other route to it. Permitted only when
// nothing else reads the alpha the shader writes, because the route overwrites
// it: no slot anywhere may use a source-alpha factor (guest 6, 7 and the
// saturating 16 read it; in the alpha equation guest 4 and 5 collapse onto it
// too), and neither the alpha test nor alpha-to-mask may be enabled.
//
// Scans all four blend controls rather than only the bound ones. That can only
// withhold the route, never grant it wrongly, which is the safe direction.
static bool DrawNeedsBlendConstantAlphaInOutput(const RegisterFile& regs) {
auto color_control = regs.Get<reg::RB_COLORCONTROL>();
if (color_control.alpha_test_enable || color_control.alpha_to_mask_enable) {
return false;
}
bool colour_slot_wants_constant_alpha = false;
for (uint32_t i = 0; i < xenos::kMaxColorRenderTargets; ++i) {
auto blendcontrol =
regs.Get<reg::RB_BLENDCONTROL>(reg::RB_BLENDCONTROL::rt_register_indices[i]);
uint32_t colour_factors[] = {uint32_t(blendcontrol.color_srcblend),
uint32_t(blendcontrol.color_destblend)};
uint32_t alpha_factors[] = {uint32_t(blendcontrol.alpha_srcblend),
uint32_t(blendcontrol.alpha_destblend)};
for (uint32_t factor : colour_factors) {
if (factor == 14 || factor == 15) {
colour_slot_wants_constant_alpha = true;
}
if (factor == 6 || factor == 7 || factor == 16) {
return false;
}
}
for (uint32_t factor : alpha_factors) {
if (factor == 4 || factor == 5 || factor == 6 || factor == 7 || factor == 16) {
return false;
}
}
}
return colour_slot_wants_constant_alpha;
}
// Generated with `xb buildshaders`.
namespace shaders {
#include "../shaders/bytecode/d3d12_5_1/adaptive_quad_hs.h"
@@ -872,6 +930,13 @@ DxbcShaderTranslator::Modification PipelineCache::GetCurrentPixelShaderModificat
modification.pixel.param_gen_point = 0;
}
modification.pixel.alpha_output_is_blend_constant =
(!UseConstantAlphaBlendFactors(command_processor_) &&
render_target_cache_.GetPath() == RenderTargetCache::Path::kHostRenderTargets &&
DrawNeedsBlendConstantAlphaInOutput(regs))
? 1
: 0;
if (render_target_cache_.GetPath() == RenderTargetCache::Path::kHostRenderTargets) {
using DepthStencilMode = DxbcShaderTranslator::Modification::DepthStencilMode;
if (render_target_cache_.depth_float24_convert_in_pixel_shader() &&
@@ -1534,10 +1599,13 @@ bool PipelineCache::GetCurrentStateDescription(
/* 12 */ PipelineBlendFactor::kBlendFactor,
// ONE_MINUS_CONSTANT_COLOR
/* 13 */ PipelineBlendFactor::kInvBlendFactor,
// CONSTANT_ALPHA - uses the constant's ALPHA, not RGB.
/* 14 */ PipelineBlendFactor::kBlendFactorAlpha,
// ONE_MINUS_CONSTANT_ALPHA - uses 1 - constant ALPHA, not 1 - RGB.
/* 15 */ PipelineBlendFactor::kInvBlendFactorAlpha,
// CONSTANT_ALPHA / ONE_MINUS_CONSTANT_ALPHA. This is the ALPHA blend
// equation, where D3D12's plain BLEND_FACTOR already takes the
// constant's alpha component - ALPHA_FACTOR computes the same value
// here. Use the always-available factor so the alpha slots never depend
// on an optional feature; only the colour slots genuinely need one.
/* 14 */ PipelineBlendFactor::kBlendFactor,
/* 15 */ PipelineBlendFactor::kInvBlendFactor,
/* 16 */ PipelineBlendFactor::kSrcAlphaSat,
};
// While it's okay to specify fewer render targets in the pipeline state
@@ -1552,6 +1620,32 @@ bool PipelineCache::GetCurrentStateDescription(
// multisampled render targets bound (happens in 4D5307E6 main menu).
// TODO(Triang3l): Investigate interaction of OMSetRenderTargets with
// non-null depth and DSVFormat DXGI_FORMAT_UNKNOWN in the same case.
// The colour blend equation cannot reach the blend constant's alpha on its
// own, so the Xenos CONSTANT_ALPHA / ONE_MINUS_CONSTANT_ALPHA factors
// (14/15) need D3D12's optional ALPHA_FACTOR family there. Where that is
// unavailable the pixel shader carries the constant's alpha in its alpha
// output instead, and SRC_ALPHA / INV_SRC_ALPHA reproduce them exactly. If
// even that route is closed, fall back to the constant-colour family -
// wrong, but it keeps the draw.
const bool substitute_constant_alpha = !UseConstantAlphaBlendFactors(command_processor_);
const bool alpha_via_output =
substitute_constant_alpha && DrawNeedsBlendConstantAlphaInOutput(regs);
auto map_colour_blend_factor = [substitute_constant_alpha,
alpha_via_output](PipelineBlendFactor factor) {
if (!substitute_constant_alpha) {
return factor;
}
switch (factor) {
case PipelineBlendFactor::kBlendFactorAlpha:
return alpha_via_output ? PipelineBlendFactor::kSrcAlpha
: PipelineBlendFactor::kBlendFactor;
case PipelineBlendFactor::kInvBlendFactorAlpha:
return alpha_via_output ? PipelineBlendFactor::kInvSrcAlpha
: PipelineBlendFactor::kInvBlendFactor;
default:
return factor;
}
};
for (uint32_t i = 0; i < 4; ++i) {
if (!(bound_depth_and_color_render_target_bits & (uint32_t(1) << (1 + i)))) {
continue;
@@ -1565,8 +1659,10 @@ bool PipelineCache::GetCurrentStateDescription(
if (rt.write_mask) {
auto blendcontrol =
regs.Get<reg::RB_BLENDCONTROL>(reg::RB_BLENDCONTROL::rt_register_indices[i]);
rt.src_blend = kBlendFactorMap[uint32_t(blendcontrol.color_srcblend)];
rt.dest_blend = kBlendFactorMap[uint32_t(blendcontrol.color_destblend)];
rt.src_blend =
map_colour_blend_factor(kBlendFactorMap[uint32_t(blendcontrol.color_srcblend)]);
rt.dest_blend =
map_colour_blend_factor(kBlendFactorMap[uint32_t(blendcontrol.color_destblend)]);
rt.blend_op = blendcontrol.color_comb_fcn;
rt.src_blend_alpha = kBlendFactorAlphaMap[uint32_t(blendcontrol.alpha_srcblend)];
rt.dest_blend_alpha = kBlendFactorAlphaMap[uint32_t(blendcontrol.alpha_destblend)];
@@ -1563,6 +1563,17 @@ void DxbcShaderTranslator::CompletePixelShader_WriteToRTVs() {
}
a_.OpEndIf();
}
if (i == 0 && GetDxbcShaderModification().pixel.alpha_output_is_blend_constant) {
// Hand the blend constant's alpha to the output merger through the alpha
// output, so INV_SRC_ALPHA can express ONE_MINUS_CONSTANT_ALPHA exactly
// on devices without the constant-alpha blend factors. Safe only because
// this modification is set exclusively for draws whose alpha equation
// ignores the alpha output - see PixelShaderModification.
a_.OpMov(dxbc::Dest::R(system_temp_color, 0b1000),
LoadSystemConstant(SystemConstants::Index::kEdramBlendConstant,
offsetof(SystemConstants, edram_blend_constant),
dxbc::Src::kWWWW));
}
// Copy the color from a readable temp register to an output register.
a_.OpMov(dxbc::Dest::O(i), dxbc::Src::R(system_temp_color));
}
@@ -435,6 +435,16 @@ bool D3D12Provider::Initialize() {
device->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS8, &options8, sizeof(options8)))) {
unaligned_block_textures_supported_ = bool(options8.UnalignedBlockTexturesSupported);
}
// Optional as of the Direct3D 12 Agility SDK: the constant-alpha blend
// factors may not be used at all unless this says so. Left false when the
// query is unavailable - an older runtime that cannot answer is not a
// licence to assume yes.
alpha_blend_factor_supported_ = false;
D3D12_FEATURE_DATA_D3D12_OPTIONS13 options13;
if (SUCCEEDED(device->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS13, &options13,
sizeof(options13)))) {
alpha_blend_factor_supported_ = bool(options13.AlphaBlendFactorSupported);
}
virtual_address_bits_per_resource_ = 0;
D3D12_FEATURE_DATA_GPU_VIRTUAL_ADDRESS_SUPPORT virtual_address_support;
if (SUCCEEDED(device->CheckFeatureSupport(D3D12_FEATURE_GPU_VIRTUAL_ADDRESS_SUPPORT,
@@ -452,13 +462,15 @@ bool D3D12Provider::Initialize() {
"* Rasterizer-ordered views: {}\n"
"* Resource binding: tier {}\n"
"* Tiled resources: tier {}\n"
"* Unaligned block-compressed textures: {}",
"* Unaligned block-compressed textures: {}\n"
"* Constant-alpha blend factors (ALPHA_FACTOR): {}",
virtual_address_bits_per_resource_,
(heap_flag_create_not_zeroed_ & D3D12_HEAP_FLAG_CREATE_NOT_ZEROED) ? "yes" : "no",
ps_specified_stencil_reference_supported_ ? "yes" : "no",
uint32_t(programmable_sample_positions_tier_),
rasterizer_ordered_views_supported_ ? "yes" : "no", uint32_t(resource_binding_tier_),
uint32_t(tiled_resources_tier_), unaligned_block_textures_supported_ ? "yes" : "no");
uint32_t(tiled_resources_tier_), unaligned_block_textures_supported_ ? "yes" : "no",
alpha_blend_factor_supported_ ? "yes" : "no");
// Get the graphics analysis interface, will silently fail if PIX is not
// attached.