From 21a1346e27db4788515cf8d0954164a27ad637eb Mon Sep 17 00:00:00 2001 From: Claxten Date: Wed, 7 Oct 2026 18:22:46 +0400 Subject: [PATCH] graphics: route compact color exports through CB_SHADER_MASK --- src/graphics/host_gpu/renderer/renderDraw.cpp | 10 ++- .../ir/passes/ShaderInfoCollection.cpp | 13 +++- src/graphics/shader/shader.cpp | 8 ++- src/graphics/shader/shader.h | 2 + src/graphics/shader/shaderPixelParameter.cpp | 13 ++++ tests/ShaderRecompilerComputeTests.cpp | 67 +++++++++++-------- tests/shaderCfgTests.cpp | 14 ++++ 7 files changed, 92 insertions(+), 35 deletions(-) diff --git a/src/graphics/host_gpu/renderer/renderDraw.cpp b/src/graphics/host_gpu/renderer/renderDraw.cpp index 826633965..17a724ace 100644 --- a/src/graphics/host_gpu/renderer/renderDraw.cpp +++ b/src/graphics/host_gpu/renderer/renderDraw.cpp @@ -589,8 +589,12 @@ static uint32_t DrawColorOutputMask(const HW::Context& ctx) { const auto& sh_regs = ctx.GetShaderRegisters(); const auto write_mask = ctx.GetRenderTargetMask() & sh_regs.m_cbShaderMask; uint32_t output_mask = 0; - for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) { - if (sh_regs.target_output_mode[slot] != 0 && + for (uint32_t index = 0; index < RENDER_COLOR_ATTACHMENTS_MAX; index++) { + const auto slot = ShaderPixelExportTarget(sh_regs.m_cbShaderMask, index); + if (slot >= RENDER_COLOR_ATTACHMENTS_MAX) { + break; + } + if (sh_regs.target_output_mode[index] != 0 && render_target_mask_slot(write_mask, slot) != 0) { output_mask |= 1u << slot; } @@ -902,7 +906,7 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal if (state.ps_active) { for (const auto& output: state.ps_input_info.stage.program->info.outputs) { if (output.kind == ShaderRecompiler::IR::StageOutputKind::Mrt) { - mrt_mask |= 1u << output.index; + mrt_mask |= 1u << output.location; } } } diff --git a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp index ead67fa76..3f7eb662c 100644 --- a/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp @@ -390,16 +390,23 @@ void CollectOutputs(const Program& program, ShaderStageInputInfo input_info, Sha AddOutput(info, StageOutputKind::Parameter, export_info.index, export_info.index, fmt::format("out_param_{}", export_info.index)); break; - case ExportTargetKind::Mrt: + case ExportTargetKind::Mrt: { if (alpha_remap && export_info.index != 0) { break; } - AddOutput(info, StageOutputKind::Mrt, export_info.index, export_info.index, + const auto slot = input_info.pixel->dual_source_blending + ? 0u : ShaderPixelExportTarget(input_info.pixel->target_shader_mask, + export_info.index); + if (slot >= 8) { + break; + } + AddOutput(info, StageOutputKind::Mrt, export_info.index, slot, fmt::format("out_mrt_{}", export_info.index)); if (alpha_remap) { - AddOutput(info, StageOutputKind::Mrt, 1, 1, "out_mrt_1"); + AddOutput(info, StageOutputKind::Mrt, 1, 0, "out_mrt_1"); } break; + } default: break; } } diff --git a/src/graphics/shader/shader.cpp b/src/graphics/shader/shader.cpp index 810c174db..58cbcb040 100644 --- a/src/graphics/shader/shader.cpp +++ b/src/graphics/shader/shader.cpp @@ -561,10 +561,12 @@ static void ShaderGetStaticInputInfoPS( ps_info.interpolator_settings[i] = sh.ps_interpolator_settings[i]; } + ps_info.target_shader_mask = sh.m_cbShaderMask; for (int i = 0; i < 8; i++) { + const auto slot = ShaderPixelExportTarget(ps_info.target_shader_mask, i); ps_info.target_output_mode[i] = sh.target_output_mode[i]; - ps_info.target_export_mapping[i] = sh.target_output_mode[i] != 0 - ? target_export_mapping[i] + ps_info.target_export_mapping[i] = sh.target_output_mode[i] != 0 && slot < 8 + ? target_export_mapping[slot] : Prospero::ColorComponentMapping {}; } } @@ -668,6 +670,8 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector key.push_back(static_cast(info.dual_source_blending)); key.push_back(static_cast(info.alpha_blend_source)); key.insert(key.end(), std::begin(info.target_output_mode), std::end(info.target_output_mode)); + const auto mask = info.target_shader_mask; + key.push_back((mask | (mask >> 1u) | (mask >> 2u) | (mask >> 3u)) & 0x11111111u); for (uint32_t base = 0; base < info.target_export_mapping.size(); base += 4u) { uint32_t packed = 0; for (uint32_t i = 0; i < 4u; i++) { diff --git a/src/graphics/shader/shader.h b/src/graphics/shader/shader.h index e666b01d2..e20eff2a0 100644 --- a/src/graphics/shader/shader.h +++ b/src/graphics/shader/shader.h @@ -170,6 +170,7 @@ struct ShaderPixelInputInfo { uint32_t ps_perspective_sample_vgpr = UINT32_MAX; uint32_t ps_perspective_centroid_vgpr = UINT32_MAX; uint8_t target_output_mode[8] = {}; + uint32_t target_shader_mask = UINT32_MAX; std::array target_export_mapping = {}; uint32_t scratch_size_dwords = 0; bool ps_pos_x = false; @@ -310,6 +311,7 @@ struct ShaderMappedData { void ShaderInit(); void ShaderMapUserData(uint64_t addr, const ShaderMappedData& data); +uint32_t ShaderPixelExportTarget(uint32_t shader_mask, uint32_t export_index); void ShaderDbgDumpInputInfo(const ShaderVertexInputInfo& info); void ShaderDbgDumpInputInfo(const ShaderPixelInputInfo& info); diff --git a/src/graphics/shader/shaderPixelParameter.cpp b/src/graphics/shader/shaderPixelParameter.cpp index 8ba5a771b..290082d91 100644 --- a/src/graphics/shader/shaderPixelParameter.cpp +++ b/src/graphics/shader/shaderPixelParameter.cpp @@ -11,6 +11,19 @@ constexpr uint32_t PsInputFlatShade = 0x00000400u; } // namespace +uint32_t ShaderPixelExportTarget(uint32_t shader_mask, uint32_t export_index) { + for (uint32_t target = 0; target < 8u; target++) { + if (((shader_mask >> (target * 4u)) & 0xfu) == 0u) { + continue; + } + if (export_index == 0u) { + return target; + } + export_index--; + } + return UINT32_MAX; +} + uint32_t ShaderPixelParameterMappedLocation(const ShaderPixelInputInfo& info, uint32_t input) { return input < info.input_num ? info.interpolator_settings[input] & PsInputOffsetMask : input; } diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index b74d6c0f8..e2e2af234 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -15610,6 +15610,7 @@ public: HW::UserConfig user_config{}; HW::Shader shaders{}; registers.SetRenderTargetMask(0xf); + registers.SetShaderMask(0xf); scheduler.Begin(registers, user_config, shaders); auto &resources = context; auto &cache = context.GetTextureCache(); @@ -16353,8 +16354,9 @@ public: registers.SetColorAttrib2(slot, {.height = side - 1, .width = side - 1}); registers.SetColorAttrib3(slot, {.tile_mode = Prospero::TileMode::kLinear, .dimension = 1}); - registers.SetTargetOutputMode(slot, 4); } + registers.SetTargetOutputMode(0, 4); + registers.SetTargetOutputMode(1, 4); stencil_target.z_info.htile_acceleration = true; stencil_target.htile_data_base_addr = depth_address + 0x30000; registers.SetDepthRenderTarget(stencil_target); @@ -16416,7 +16418,7 @@ public: registers.SetDepthShaderControl({}); registers.SetDepthControl({.z_enable = true, .z_write_enable = true, .zfunc = static_cast(vk::CompareOp::eAlways)}); - for (const auto slot : {0u, 3u}) { + for (const auto slot : {0u, 1u}) { registers.SetTargetOutputMode(slot, 0); } RenderExecutorTestAccess::DrawAuto( @@ -16427,37 +16429,48 @@ public: std::ranges::all_of(depth_only_pixels, [](u32 v) { return v == 0; }), "disabled color exports invoked the stale PS or clipped fixed-function depth"); registers.SetPsInControl(0x8000); - for (const auto slot : {0u, 3u}) { - registers.SetTargetOutputMode(slot, 4); - } + registers.SetTargetOutputMode(0, 4); - // With MRT0 still bound, an MRT3-only export must retain location3 in - // rendering attachments, pipeline formats, blend masks and dynamic write enables. - static const auto sparse_pixel = [] { - auto code = native_pixel; - *std::ranges::find(code, EncodeExp0(0x00, 0xf)) = EncodeExp0(0x03, 0xf); - return code; + // Export ordinals follow CB_SHADER_MASK. A disabled CB_TARGET_MASK slot + // still consumes an ordinal, and stale MRT0 must not clip physical MRT3. + static const auto sparse_pixels_code = [] { + std::array, 2> result{native_pixel, native_pixel}; + auto &code = result[1]; + const auto exp = std::ranges::find(code, EncodeExp0(0, 0xf)); + *exp = EncodeExp0(0, 0xf, false); + *(exp + 1) = EncodeExp1(0, 0, 0, 0); + code.insert(exp + 2, {EncodeExp0(1, 0xf), EncodeExp1(1, 1, 1, 2)}); + return result; }(); - const auto sparse_address = reinterpret_cast(sparse_pixel.data()); - ShaderMapUserData(sparse_address, - {.type = Prospero::ShaderBinaryType::kPs, - .code_size_bytes = static_cast(sparse_pixel.size() * sizeof(u32))}); - shaders.SetPsShaderBase(sparse_address); + registers.SetRenderTargetMask(0xf000); registers.SetDepthControl({}); registers.SetDepthShaderControl({}); - RenderExecutorTestAccess::DrawAuto( - executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1}); RenderColorInfo sparse_color{}; - RenderExecutorTestAccess::ResolveRenderColorTarget( - executor, scheduler.Current(), sparse_color, 3); - const auto sparse_pixels = ReadCachedTexel(name, context, sparse_color.image_id, - {}, {extent, extent, 1}); - for (size_t component = 0; component < sparse_pixels.size(); component++) { - const auto expected = component % 4 == 3 ? 0x3f800000u : 0x3e800000u; - Require(name, "sparse MRT3 output", sparse_pixels[component] == expected, - "MRT3 was compacted to a different slot or clipped by unused MRT0"); + for (uint32_t index = 0; index < sparse_pixels_code.size(); index++) { + const auto &code = sparse_pixels_code[index]; + const auto address = reinterpret_cast(code.data()); + ShaderMapUserData(address, + {.type = Prospero::ShaderBinaryType::kPs, + .code_size_bytes = static_cast(code.size() * sizeof(u32))}); + shaders.SetPsShaderBase(address); + registers.SetShaderMask(index == 0 ? 0xf000 : 0xf00f); + registers.SetTargetOutputMode(1, index == 0 ? 0 : 4); + RenderExecutorTestAccess::ResolveRenderColorTarget( + executor, scheduler.Current(), sparse_color, 3); + TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), sparse_color.image_id, + {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, {}); + RenderExecutorTestAccess::DrawAuto( + executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1}); + const auto pixels = ReadCachedTexel(name, context, sparse_color.image_id, + {}, {extent, extent, 1}); + for (size_t component = 0; component < pixels.size(); component++) { + const auto expected = component % 4 == 3 ? 0x3f800000u : 0x3e800000u; + Require(name, "compact export to sparse MRT3", pixels[component] == expected, + "shader output routing used the write mask or clipped MRT3 with stale MRT0"); + } } + // A fourth VS invocation reads beyond the descriptor instead of reconstructing the corner. const ShaderBufferResource rect_buffer{{ static_cast(rect_address), @@ -16483,7 +16496,7 @@ public: } AppendVMovU32(&code, 22, 0); AppendVMovLiteral(&code, 23, 0x3f800000u); - code.insert(code.end(), {EncodeExp0(0x03, 0xf), EncodeExp1(20, 21, 22, 23)}); + code.insert(code.end(), {EncodeExp0(0, 0xf), EncodeExp1(20, 21, 22, 23)}); AppendEnd(&code); return code; }(); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 23a0112d4..522ae810b 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -13023,9 +13023,23 @@ void TestRenderTargetReverseExportMapping() { "inactive reverse MRT mapping was not normalized out of the shader " "cache key"); sh.target_output_mode[0] = 4; + sh.m_cbShaderMask = 0xf; PrepareProgram(regs, sh, mappings, compiled_info); Check(compiled_info.target_export_mapping[0] == gr32.export_mapping, "active reverse MRT mapping was lost before shader specialization"); + const auto first_slot_key = MakeStageStaticKey(compiled_info); + sh.m_cbShaderMask = 0xf000; + mappings[3] = gr32.export_mapping; + PrepareProgram(regs, sh, mappings, compiled_info); + Check(compiled_info.target_shader_mask == sh.m_cbShaderMask && + compiled_info.target_export_mapping[0] == gr32.export_mapping && + first_slot_key != MakeStageStaticKey(compiled_info), + "compact MRT ordinal lost its physical export mapping or cache identity"); + const auto sparse_slot_key = MakeStageStaticKey(compiled_info); + sh.m_cbShaderMask = 0x1000; + PrepareProgram(regs, sh, mappings, compiled_info); + Check(sparse_slot_key == MakeStageStaticKey(compiled_info), + "component-only shader mask changes introduced a duplicate shader variant"); } void TestBlendMappingClassification() {