diff --git a/src/graphics/host_gpu/renderer/pipeline/blendMapping.cpp b/src/graphics/host_gpu/renderer/pipeline/blendMapping.cpp new file mode 100644 index 000000000..856aee0d0 --- /dev/null +++ b/src/graphics/host_gpu/renderer/pipeline/blendMapping.cpp @@ -0,0 +1,58 @@ +#include "graphics/host_gpu/renderer/pipeline/blendMapping.h" + +#include "graphics/guest_gpu/hardwareContext.h" + +#include + +namespace Libs::Graphics { + +bool BlendFactorIsDualSource(uint8_t factor) { + return factor >= static_cast(Prospero::BlendFactor::kSrc1Color) && + factor <= static_cast(Prospero::BlendFactor::kOneMinusSrc1Alpha); +} + +namespace { + +bool BlendFactorIsConstantColor(uint8_t factor) { + return factor == static_cast(Prospero::BlendFactor::kConstantColor) || + factor == static_cast(Prospero::BlendFactor::kOneMinusConstantColor); +} + +} // namespace + +BlendMappingSupport ClassifyBlendMapping(const HW::BlendControl& blend, + const Prospero::ColorComponentMapping& mapping) { + // Color constants are not swizzled with the exports; scalar constant alpha is unaffected. + if (!mapping.IsIdentity() && (BlendFactorIsConstantColor(blend.color_srcblend) || + BlendFactorIsConstantColor(blend.color_destblend))) { + return BlendMappingSupport::Unsupported; + } + if (mapping.Map(3) == 3u) { + return BlendMappingSupport::Direct; + } + // Moving alpha requires the same equation for all channels. + if (blend.separate_alpha_blend && (blend.alpha_srcblend != blend.color_srcblend || + blend.alpha_destblend != blend.color_destblend || + blend.alpha_comb_fcn != blend.color_comb_fcn)) { + return BlendMappingSupport::Unsupported; + } + auto support = BlendMappingSupport::Direct; + for (const auto factor: {blend.color_srcblend, blend.color_destblend}) { + if (BlendFactorIsDualSource(factor)) { + return BlendMappingSupport::Unsupported; + } + switch (static_cast(factor)) { + case Prospero::BlendFactor::kSrcAlpha: + case Prospero::BlendFactor::kOneMinusSrcAlpha: + support = BlendMappingSupport::SourceAlpha; + break; + case Prospero::BlendFactor::kDstAlpha: + case Prospero::BlendFactor::kOneMinusDstAlpha: + case Prospero::BlendFactor::kSrcAlphaSaturate: return BlendMappingSupport::Unsupported; + default: break; + } + } + return support; +} + +} // namespace Libs::Graphics diff --git a/src/graphics/host_gpu/renderer/pipeline/blendMapping.h b/src/graphics/host_gpu/renderer/pipeline/blendMapping.h new file mode 100644 index 000000000..bcc4ac5fa --- /dev/null +++ b/src/graphics/host_gpu/renderer/pipeline/blendMapping.h @@ -0,0 +1,27 @@ +#ifndef EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_PIPELINE_BLENDMAPPING_H_ +#define EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_PIPELINE_BLENDMAPPING_H_ + +#include + +namespace Libs::Graphics { + +namespace HW { +struct BlendControl; +} +namespace Prospero { +struct ColorComponentMapping; +} + +enum class BlendMappingSupport { + Direct, + SourceAlpha, // Requires logical alpha in the second blend source. + Unsupported, +}; + +bool BlendFactorIsDualSource(uint8_t factor); +BlendMappingSupport ClassifyBlendMapping(const HW::BlendControl& blend, + const Prospero::ColorComponentMapping& mapping); + +} // namespace Libs::Graphics + +#endif // EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_PIPELINE_BLENDMAPPING_H_ diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index dcd1c94a3..6be59b83f 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -11,6 +11,7 @@ #include "graphics/host_gpu/renderer/debug.h" #include "graphics/host_gpu/renderer/depthRenderTarget.h" #include "graphics/host_gpu/renderer/image/imageView.h" +#include "graphics/host_gpu/renderer/pipeline/blendMapping.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" #include "graphics/shader/recompiler/ShaderRecompiler.h" @@ -609,21 +610,30 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms( } ShaderParams pixel_params; if (pixel_active) { - pixel_params = PrepareProgram(pixel_regs, sh, target_export_mapping, pixel_info); - const auto& blend = context.GetBlendControl(0); - const auto is_dual_source = [](uint8_t factor) { - return factor >= static_cast(Prospero::BlendFactor::kSrc1Color) && - factor <= static_cast(Prospero::BlendFactor::kOneMinusSrc1Alpha); - }; + pixel_params = PrepareProgram(pixel_regs, sh, target_export_mapping, pixel_info); + const auto& blend = context.GetBlendControl(0); pixel_info.dual_source_blending = blend.enable && !context.GetRenderTarget(0).info.blend_bypass && - (is_dual_source(blend.color_srcblend) || is_dual_source(blend.color_destblend) || - (blend.separate_alpha_blend && - (is_dual_source(blend.alpha_srcblend) || is_dual_source(blend.alpha_destblend)))); + (BlendFactorIsDualSource(blend.color_srcblend) || + BlendFactorIsDualSource(blend.color_destblend) || + (blend.separate_alpha_blend && (BlendFactorIsDualSource(blend.alpha_srcblend) || + BlendFactorIsDualSource(blend.alpha_destblend)))); if (pixel_info.dual_source_blending) { - // MRT1 supplies a second blend source for the same render target as MRT0. + // MRT1 supplies the second blend source for target 0. pixel_info.target_output_mode[1] = pixel_info.target_output_mode[0]; pixel_info.target_export_mapping[1] = pixel_info.target_export_mapping[0]; + } else if (blend.enable && !context.GetRenderTarget(0).info.blend_bypass && + pixel_info.target_output_mode[0] != 0 && pixel_info.target_output_mode[0] != 7 && + std::all_of(std::begin(pixel_info.target_output_mode) + 1, + std::end(pixel_info.target_output_mode), + [](uint8_t mode) { return mode == 0; }) && + ClassifyBlendMapping(blend, pixel_info.target_export_mapping[0]) == + BlendMappingSupport::SourceAlpha) { + // Preserve logical alpha when the export mapping moves it. + pixel_info.alpha_blend_source_remap = true; + pixel_info.dual_source_blending = true; + pixel_info.target_output_mode[1] = pixel_info.target_output_mode[0]; + pixel_info.target_export_mapping[1] = {}; } } if (context.GetClipControl().clip_disable) { @@ -720,7 +730,24 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline( static_params.alpha_comb_fcn[slot] = bc.alpha_comb_fcn; static_params.alpha_destblend[slot] = bc.alpha_destblend; static_params.separate_alpha_blend[slot] = bc.separate_alpha_blend; - static_params.blend_enable[slot] = bc.enable && !rt.info.blend_bypass; + const bool alpha_remap = + slot == 0 && ps_input_info != nullptr && ps_input_info->alpha_blend_source_remap; + static_params.blend_enable[slot] = bc.enable && !rt.info.blend_bypass; + if (static_params.blend_enable[slot] && !alpha_remap && + ClassifyBlendMapping(bc, colors[i].export_mapping) != BlendMappingSupport::Direct) { + static_params.blend_enable[slot] = false; + static std::atomic_bool warned = false; + if (!warned.exchange(true, std::memory_order_relaxed)) { + Log::WriteToConsoleAndLog(fmt::format( + "Warning: blending disabled for unsupported color mapping " + "(slot={} mapping=0x{:02x} color={}/{} alpha={}/{} separate={}).\n", + slot, colors[i].export_mapping.packed, bc.color_srcblend, bc.color_destblend, + bc.alpha_srcblend, bc.alpha_destblend, bc.separate_alpha_blend ? 1 : 0)); + } + } + if (alpha_remap) { + static_params.blend_alpha_source_remap = true; + } } const bool with_depth = depth.desc.view_info.format != vk::Format::eUndefined && static_cast(depth.image_id); diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.h b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.h index 4be29854d..49dc7abfb 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.h +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.h @@ -55,6 +55,7 @@ struct PipelineStaticParameters { uint8_t alpha_destblend[RENDER_COLOR_ATTACHMENTS_MAX] = {}; bool separate_alpha_blend[RENDER_COLOR_ATTACHMENTS_MAX] = {}; bool blend_enable[RENDER_COLOR_ATTACHMENTS_MAX] = {}; + bool blend_alpha_source_remap = false; bool operator==(const PipelineStaticParameters& other) const noexcept; }; @@ -64,7 +65,7 @@ struct PipelineStaticParameters { static_assert(std::is_trivially_copyable_v); static_assert(std::is_standard_layout_v); static_assert(alignof(PipelineStaticParameters) == 1); -static_assert(sizeof(PipelineStaticParameters) == 125); +static_assert(sizeof(PipelineStaticParameters) == 126); struct PipelineRenderingState { std::array color_formats {}; diff --git a/src/graphics/host_gpu/renderer/pipeline/shaders.cpp b/src/graphics/host_gpu/renderer/pipeline/shaders.cpp index d21fc014b..df0d2644f 100644 --- a/src/graphics/host_gpu/renderer/pipeline/shaders.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/shaders.cpp @@ -137,14 +137,17 @@ static void GetInputFormat(const ShaderBufferResource& res, vk::Format& format, } } -static vk::BlendFactor GetBlendFactor(uint32_t factor) { +static vk::BlendFactor GetBlendFactor(uint32_t factor, bool remap_source_alpha) { switch (static_cast(factor)) { case Prospero::BlendFactor::kZero: return vk::BlendFactor::eZero; case Prospero::BlendFactor::kOne: return vk::BlendFactor::eOne; case Prospero::BlendFactor::kSrcColor: return vk::BlendFactor::eSrcColor; case Prospero::BlendFactor::kOneMinusSrcColor: return vk::BlendFactor::eOneMinusSrcColor; - case Prospero::BlendFactor::kSrcAlpha: return vk::BlendFactor::eSrcAlpha; - case Prospero::BlendFactor::kOneMinusSrcAlpha: return vk::BlendFactor::eOneMinusSrcAlpha; + case Prospero::BlendFactor::kSrcAlpha: + return remap_source_alpha ? vk::BlendFactor::eSrc1Color : vk::BlendFactor::eSrcAlpha; + case Prospero::BlendFactor::kOneMinusSrcAlpha: + return remap_source_alpha ? vk::BlendFactor::eOneMinusSrc1Color + : vk::BlendFactor::eOneMinusSrcAlpha; case Prospero::BlendFactor::kDstAlpha: return vk::BlendFactor::eDstAlpha; case Prospero::BlendFactor::kOneMinusDstAlpha: return vk::BlendFactor::eOneMinusDstAlpha; case Prospero::BlendFactor::kDstColor: return vk::BlendFactor::eDstColor; @@ -397,17 +400,20 @@ void CreatePipelineInternal(GraphicContext& graphics, PipelineCache::Pipeline& p color_blend_attachment[i].colorWriteMask = vk::ColorComponentFlags {static_params.color_mask[i]}; color_blend_attachment[i].blendEnable = static_params.blend_enable[i] ? VK_TRUE : VK_FALSE; + // Only target 0 can use the second blend source carrying logical alpha. + const bool remap_source_alpha = i == 0 && static_params.blend_alpha_source_remap; color_blend_attachment[i].srcColorBlendFactor = - GetBlendFactor(static_params.color_srcblend[i]); + GetBlendFactor(static_params.color_srcblend[i], remap_source_alpha); color_blend_attachment[i].dstColorBlendFactor = - GetBlendFactor(static_params.color_destblend[i]); + GetBlendFactor(static_params.color_destblend[i], remap_source_alpha); color_blend_attachment[i].colorBlendOp = GetBlendOp(static_params.color_comb_fcn[i]); color_blend_attachment[i].srcAlphaBlendFactor = - (static_params.separate_alpha_blend[i] ? GetBlendFactor(static_params.alpha_srcblend[i]) - : color_blend_attachment[i].srcColorBlendFactor); + (static_params.separate_alpha_blend[i] + ? GetBlendFactor(static_params.alpha_srcblend[i], remap_source_alpha) + : color_blend_attachment[i].srcColorBlendFactor); color_blend_attachment[i].dstAlphaBlendFactor = (static_params.separate_alpha_blend[i] - ? GetBlendFactor(static_params.alpha_destblend[i]) + ? GetBlendFactor(static_params.alpha_destblend[i], remap_source_alpha) : color_blend_attachment[i].dstColorBlendFactor); color_blend_attachment[i].alphaBlendOp = (static_params.separate_alpha_blend[i] ? GetBlendOp(static_params.alpha_comb_fcn[i]) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp index c49b1d6b1..d8bd9571d 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp @@ -452,6 +452,11 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) { if (exp.kind == IR::ExportTargetKind::Null || exp.en == 0u) { return; } + // Skip dormant color exports after their valid mask; MRT1 is reserved for logical alpha. + if (state.program.stage == ShaderType::Pixel && exp.kind == IR::ExportTargetKind::Mrt && + exp.index != 0 && state.input_info.pixel->alpha_blend_source_remap) { + return; + } EmitIfCondition(state, exec, [&]() { const auto data = ctx.Arg(inst, 0); if (exp.kind == IR::ExportTargetKind::Primitive) { @@ -492,6 +497,18 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) { const bool uint_output = MrtOutputMode(state, exp) == 7u; const auto vector_type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4); auto value = ExportVector(ctx, data, exp, uint_output); + if (state.program.stage == ShaderType::Pixel && exp.kind == IR::ExportTargetKind::Mrt && + exp.index == 0 && !uint_output && state.input_info.pixel->alpha_blend_source_remap) { + // Broadcast logical alpha before swizzling the primary output. + const auto blend_output = + OutputVariableForExport(state, {.kind = IR::ExportTargetKind::Mrt, .index = 1}); + if (blend_output != 0) { + const auto alpha = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpVectorShuffle, vector_type, alpha, value, value, + 3u, 3u, 3u, 3u); + state.builder.AddFunction(spv::OpStore, blend_output, alpha); + } + } if (state.program.stage == ShaderType::Pixel && exp.kind == IR::ExportTargetKind::Mrt && exp.index < state.input_info.pixel->target_export_mapping.size()) { const auto mapping = state.input_info.pixel->target_export_mapping[exp.index]; diff --git a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp index 8b8a4ae6a..242a0866a 100644 --- a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp @@ -326,6 +326,8 @@ void CollectBuiltinInputs(const Program& program, ShaderInfo& info) { } void CollectOutputs(const Program& program, ShaderStageInputInfo input_info, ShaderInfo& info) { + const bool alpha_remap = program.stage == ShaderType::Pixel && input_info.pixel != nullptr && + input_info.pixel->alpha_blend_source_remap; for (const auto* block: program.blocks) { for (const auto& inst: *block) { if (inst.GetOpcode() != ValueOpcode::SetAttribute) { @@ -385,8 +387,14 @@ void CollectOutputs(const Program& program, ShaderStageInputInfo input_info, Sha export_info.index, fmt::format("out_param_{}", export_info.index)); break; case ExportTargetKind::Mrt: + if (alpha_remap && export_info.index != 0) { + break; + } AddOutput(info, StageOutputKind::Mrt, export_info.index, export_info.index, fmt::format("out_mrt_{}", export_info.index)); + if (alpha_remap) { + AddOutput(info, StageOutputKind::Mrt, 1, 1, "out_mrt_1"); + } break; default: break; } diff --git a/src/graphics/shader/shader.cpp b/src/graphics/shader/shader.cpp index d4ae88166..181d2e3a7 100644 --- a/src/graphics/shader/shader.cpp +++ b/src/graphics/shader/shader.cpp @@ -693,6 +693,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector key.push_back(static_cast(info.ps_sample_mask_export_enable)); key.push_back(static_cast(info.ps_early_z)); key.push_back(static_cast(info.dual_source_blending)); + key.push_back(static_cast(info.alpha_blend_source_remap)); key.insert(key.end(), std::begin(info.target_output_mode), std::end(info.target_output_mode)); for (uint32_t base = 0; base < info.target_export_mapping.size(); base += 4u) { uint32_t packed = 0; diff --git a/src/graphics/shader/shader.h b/src/graphics/shader/shader.h index 1679ff32b..22086ca1e 100644 --- a/src/graphics/shader/shader.h +++ b/src/graphics/shader/shader.h @@ -178,6 +178,8 @@ struct ShaderPixelInputInfo { bool ps_sample_mask_export_enable = false; bool ps_sample_shading = false; bool dual_source_blending = false; + // Export logical alpha through MRT1 for blending after channel swizzling. + bool alpha_blend_source_remap = false; bool ps_early_z = false; bool ps_execute_on_noop = false; ShaderStageRuntime stage; diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index baf134088..032c70a0f 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -7,6 +7,7 @@ #include "graphics/guest_gpu/pm4.h" #include "graphics/host_gpu/renderer/image/imageView.h" #include "graphics/host_gpu/renderer/image/textureCommon.h" +#include "graphics/host_gpu/renderer/pipeline/blendMapping.h" #include "graphics/host_gpu/renderer/pipeline/shaderResourceBarrier.h" #include "graphics/shader/recompiler/ShaderRecompiler.h" #include "graphics/shader/recompiler/backend/spirv/SpirvEmitter.h" @@ -12281,6 +12282,116 @@ void TestRenderTargetReverseExportMapping() { "active reverse MRT mapping was lost before shader specialization"); } +void TestBlendMappingClassification() { + using Factor = Prospero::BlendFactor; + using Support = BlendMappingSupport; + HW::BlendControl blend{}; + blend.separate_alpha_blend = false; + const auto classify = [&](Prospero::ColorComponentMapping mapping) { + return ClassifyBlendMapping(blend, mapping); + }; + for (const auto factor : {Factor::kConstantAlpha, Factor::kOneMinusConstantAlpha}) { + blend.color_srcblend = static_cast(factor); + Check(classify(Prospero::ColorMappingAbgr) == Support::Direct, + "scalar blend constant incorrectly required physical alpha"); + } + for (const auto factor : {Factor::kConstantColor, Factor::kOneMinusConstantColor}) { + blend.color_srcblend = static_cast(factor); + Check(classify(Prospero::ColorMappingRgba) == Support::Direct && + classify(Prospero::ColorMappingBgra) == Support::Unsupported && + classify(Prospero::ColorMappingAbgr) == Support::Unsupported, + "logical blend constants were accepted with shuffled color components"); + } + blend.color_srcblend = static_cast(Factor::kOne); + blend.alpha_srcblend = static_cast(Factor::kConstantColor); + blend.separate_alpha_blend = true; + Check(classify(Prospero::ColorMappingBgra) == Support::Direct && + classify(Prospero::ColorMappingAbgr) == Support::Unsupported, + "separate constant-color alpha equation ignored the physical alpha location"); + blend.color_srcblend = static_cast(Factor::kSrcAlpha); + blend.separate_alpha_blend = false; + Check(classify(Prospero::ColorMappingAbgr) == Support::SourceAlpha, + "reversed source-alpha blending did not request logical alpha"); + blend.separate_alpha_blend = true; + Check(classify(Prospero::ColorMappingAbgr) == Support::Unsupported, + "different alpha equations were accepted on a reversed target"); + blend.separate_alpha_blend = false; + blend.color_srcblend = static_cast(Factor::kDstAlpha); + Check(classify(Prospero::ColorMappingAbgr) == Support::Unsupported, + "destination alpha incorrectly used the physical alpha channel"); +} + +void TestLogicalAlphaBlendExport() { + ShaderPixelInputInfo pixel{}; + pixel.target_output_mode[0] = 4; + pixel.target_export_mapping[0] = Prospero::ColorMappingAbgr; + const auto ordinary_key = MakeStageStaticKey(pixel); + pixel.dual_source_blending = true; + const auto guest_key = MakeStageStaticKey(pixel); + pixel.alpha_blend_source_remap = true; + const auto remapped_key = MakeStageStaticKey(pixel); + Check(ordinary_key != guest_key && guest_key != remapped_key && + ordinary_key != remapped_key, + "ordinary, guest dual-source, and logical-alpha shaders share a cache key"); + + auto options = MakeCompileOptions(ShaderType::Pixel); + options.input_info.pixel = &pixel; + for (const bool compressed : {false, true}) { + const uint32_t shader[] = { + // RGBA = (1000, 2, 3, 0.25), packed into two half pairs when compressed. + EncodeVop1(0x01, 0, 255), compressed ? 0x400063d0u : 0x447a0000u, + EncodeVop1(0x01, 1, 255), compressed ? 0x34004200u : 0x40000000u, + EncodeVop1(0x01, 2, 255), 0x40400000u, + EncodeVop1(0x01, 3, 255), 0x3e800000u, + EncodeExp0(0, 0xf, false, compressed), EncodeExp1(0, 1, 2, 3), + EncodeExp0(1, 0xf, false), EncodeExp1(0, 0, 0, 0), + EncodeExp0(2, 0xf, true, false, true), EncodeExp1(0, 0, 0, 0), + EncodeSopp(0x01), + }; + const auto result = RecompileForTest(shader, options); + CheckSpirvBinaryValidates(result.spirv); + const auto source = DisassembleSpirvBinary(result.spirv); + Check(source.find("OpDecorate %out_mrt_1 Location 0") != std::string::npos && + source.find("OpDecorate %out_mrt_1 Index 1") != std::string::npos && + CountSourceOccurrences(source, "OpStore %out_mrt_1 ") == 1 && + source.find("out_mrt_2") == std::string::npos, + "inactive MRT exports overwrote the logical-alpha output"); + Check(SpirvInstructionOpcodeCount(result.spirv, 252u) != 0, + "ignoring inactive MRT stores discarded their valid-mask export"); + uint32_t alpha_input = 0; + uint32_t color_input = 0; + for (size_t i = 5; i < result.spirv.size(); i += result.spirv[i] >> 16u) { + if ((result.spirv[i] & 0xffffu) != 79u || (result.spirv[i] >> 16u) != 9u) { + continue; + } + const auto selectors = std::span(result.spirv).subspan(i + 5, 4); + if (std::ranges::equal(selectors, std::array{3u, 3u, 3u, 3u})) { + alpha_input = result.spirv[i + 3]; + } else if (std::ranges::equal(selectors, std::array{3u, 2u, 1u, 0u})) { + color_input = result.spirv[i + 3]; + } + } + Check(alpha_input != 0 && alpha_input == color_input, + "blend source did not broadcast logical alpha before the physical export swizzle"); + } + + const uint32_t guest_shader[] = { + EncodeExp0(0, 0xf, false), EncodeExp1(0, 1, 2, 3), + EncodeExp0(1, 0xf), EncodeExp1(4, 5, 6, 7), EncodeSopp(0x01), + }; + pixel.alpha_blend_source_remap = false; + pixel.target_output_mode[1] = pixel.target_output_mode[0]; + pixel.target_export_mapping = {}; + const auto guest = RecompileForTest(guest_shader, options); + CheckSpirvBinaryValidates(guest.spirv); + const auto source = DisassembleSpirvBinary(guest.spirv); + Check(source.find("OpDecorate %out_mrt_1 Location 0") != std::string::npos && + source.find("OpDecorate %out_mrt_1 Index 1") != std::string::npos && + CountSourceOccurrences(source, "OpStore %out_mrt_1 ") == 1 && + SpirvInstructionOpcodeCount(guest.spirv, 81u) == 8u, + "guest dual-source export was replaced by synthetic alpha"); +} + void TestNewShaderRecompilerEarlyZDisabledWhenPixelKillEnabled() { constexpr uint32_t ExecutionModeEarlyFragmentTests = 9; @@ -14043,6 +14154,8 @@ int main() { TestNewShaderRecompilerPerInvocationU64Complement(); TestNewShaderRecompilerExpPixelOutputs(); TestRenderTargetReverseExportMapping(); + TestBlendMappingClassification(); + TestLogicalAlphaBlendExport(); TestNewShaderRecompilerEarlyZDisabledWhenPixelKillEnabled(); TestTypedDescriptorRealWideMoveTranslation(); TestComputeImageFill();