From 771827a792001aba22767905d93cd1ebcc86af1c Mon Sep 17 00:00:00 2001 From: tarako Date: Tue, 24 Feb 2026 19:24:44 -1000 Subject: [PATCH] Fixed dual-source blending to correctly map shader outputs. Fixes Skyward Sword HD eye gitch and a related MoltenVK crash due to the incorrect outputs. --- .../backend/spirv/emit_spirv_special.cpp | 11 +++++++ .../backend/spirv/spirv_emit_context.cpp | 15 ++++++++-- src/shader_recompiler/runtime_info.h | 3 ++ .../renderer_vulkan/vk_pipeline_cache.cpp | 30 ++++++++++++++++++- 4 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp index fe5e70a63b..803d9066bf 100644 --- a/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp +++ b/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp @@ -91,6 +91,17 @@ void AlphaTest(EmitContext& ctx) { } // Anonymous namespace void EmitPrologue(EmitContext& ctx) { + if (ctx.stage == Stage::Fragment && ctx.runtime_info.dual_source_blend) { + // Initialize dual-source blending outputs - prevents MoltenVK crash. + const Id zero{ctx.Const(0.0f)}; + const Id one{ctx.Const(1.0f)}; + const Id default_color{ctx.ConstantComposite(ctx.F32[4], zero, zero, zero, one)}; + for (u32 i = 0; i < 2; ++i) { + if (Sirit::ValidId(ctx.frag_color[i])) { + ctx.OpStore(ctx.frag_color[i], default_color); + } + } + } if (ctx.stage == Stage::VertexB) { const Id zero{ctx.Const(0.0f)}; const Id one{ctx.Const(1.0f)}; diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp index cd3394bdf0..b9a24496c9 100644 --- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp +++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp @@ -1670,13 +1670,22 @@ void EmitContext::DefineOutputs(const IR::Program& program) { break; case Stage::Fragment: for (u32 index = 0; index < 8; ++index) { - if (!info.stores_frag_color[index] && !profile.need_declared_frag_colors) { + const bool need_dual_source = runtime_info.dual_source_blend && index <= 1; + if (!need_dual_source && !info.stores_frag_color[index] && + !profile.need_declared_frag_colors) { continue; } const Id type{GetAttributeType(*this, runtime_info.color_output_types[index])}; frag_color[index] = DefineOutput(*this, type, std::nullopt); - Decorate(frag_color[index], spv::Decoration::Location, index); - Name(frag_color[index], fmt::format("frag_color{}", index)); + // Correct mapping for dual-source blending + if (runtime_info.dual_source_blend && index <= 1) { + Decorate(frag_color[index], spv::Decoration::Location, 0u); + Decorate(frag_color[index], spv::Decoration::Index, index); + Name(frag_color[index], index == 0 ? "frag_color0" : "frag_color0_secondary"); + } else { + Decorate(frag_color[index], spv::Decoration::Location, index); + Name(frag_color[index], fmt::format("frag_color{}", index)); + } } if (info.stores_frag_depth) { frag_depth = DefineOutput(*this, F32[1], std::nullopt); diff --git a/src/shader_recompiler/runtime_info.h b/src/shader_recompiler/runtime_info.h index 613c598d0c..be10a9bb08 100644 --- a/src/shader_recompiler/runtime_info.h +++ b/src/shader_recompiler/runtime_info.h @@ -110,6 +110,9 @@ struct RuntimeInfo { /// Output types for each color attachment std::array color_output_types{}; + + /// Dual source blending + bool dual_source_blend{}; }; } // namespace Shader diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp index f3dd0f90d8..77a4e8616a 100644 --- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp +++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp @@ -237,11 +237,38 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span program } info.convert_depth_mode = gl_ndc; break; - case Shader::Stage::Fragment: + case Shader::Stage::Fragment: { info.alpha_test_func = MaxwellToCompareFunction( key.state.UnpackComparisonOp(key.state.alpha_test_func.Value())); info.alpha_test_reference = std::bit_cast(key.state.alpha_test_ref); + // Check for dual source blending + const auto& blend0 = key.state.attachments[0]; + if (blend0.enable != 0) { + using F = Maxwell::Blend::Factor; + const auto src_rgb = blend0.SourceRGBFactor(); + const auto dst_rgb = blend0.DestRGBFactor(); + const auto src_a = blend0.SourceAlphaFactor(); + const auto dst_a = blend0.DestAlphaFactor(); + info.dual_source_blend = + src_rgb == F::Source1Color_D3D || src_rgb == F::OneMinusSource1Color_D3D || + src_rgb == F::Source1Alpha_D3D || src_rgb == F::OneMinusSource1Alpha_D3D || + src_rgb == F::Source1Color_GL || src_rgb == F::OneMinusSource1Color_GL || + src_rgb == F::Source1Alpha_GL || src_rgb == F::OneMinusSource1Alpha_GL || + dst_rgb == F::Source1Color_D3D || dst_rgb == F::OneMinusSource1Color_D3D || + dst_rgb == F::Source1Alpha_D3D || dst_rgb == F::OneMinusSource1Alpha_D3D || + dst_rgb == F::Source1Color_GL || dst_rgb == F::OneMinusSource1Color_GL || + dst_rgb == F::Source1Alpha_GL || dst_rgb == F::OneMinusSource1Alpha_GL || + src_a == F::Source1Color_D3D || src_a == F::OneMinusSource1Color_D3D || + src_a == F::Source1Alpha_D3D || src_a == F::OneMinusSource1Alpha_D3D || + src_a == F::Source1Color_GL || src_a == F::OneMinusSource1Color_GL || + src_a == F::Source1Alpha_GL || src_a == F::OneMinusSource1Alpha_GL || + dst_a == F::Source1Color_D3D || dst_a == F::OneMinusSource1Color_D3D || + dst_a == F::Source1Alpha_D3D || dst_a == F::OneMinusSource1Alpha_D3D || + dst_a == F::Source1Color_GL || dst_a == F::OneMinusSource1Color_GL || + dst_a == F::Source1Alpha_GL || dst_a == F::OneMinusSource1Alpha_GL; + } + if (device.IsMoltenVK()) { for (size_t i = 0; i < 8; ++i) { const auto format = static_cast(key.state.color_formats[i]); @@ -258,6 +285,7 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span program } } break; + } default: break; }