From 26ca609236371174d88f4d65577869a7dfe9051e Mon Sep 17 00:00:00 2001 From: nmzik Date: Mon, 26 Oct 2026 04:06:00 +0100 Subject: [PATCH] shader: type pixel outputs of UINT and SINT render targets as integer vectors Co-authored-by: Ali Almohaya Co-authored-by: Mac Co-authored-by: Senaxx --- .../renderer/pipeline/pipelineCache.cpp | 9 ++++- .../backend/spirv/spirvEmitterAnalysis.cpp | 7 ++++ .../backend/spirv/spirvEmitterFlow.cpp | 29 ++++++++-------- .../backend/spirv/spirvEmitterInternal.h | 1 + .../backend/spirv/spirvEmitterModule.cpp | 16 +++++---- src/graphics/shader/shader.cpp | 4 ++- src/graphics/shader/shader.h | 3 ++ tests/ShaderRecompilerComputeTests.cpp | 34 +++++++++++++++++++ tests/shaderCfgTests.cpp | 23 +++++++++++++ 9 files changed, 103 insertions(+), 23 deletions(-) diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index fddee5878..e67f254c3 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -741,6 +741,11 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender const auto& rt = context.GetRenderTarget(slot); const auto target_mask = render_target_mask_slot(context.GetRenderTargetMask(), slot); if (rt.base.addr == 0 || target_mask == 0u) continue; + if (rt.info.channel_type == Prospero::ChannelType::kUInt) { + pixel_info.target_uint_mask |= 1u << export_index; + } else if (rt.info.channel_type == Prospero::ChannelType::kSInt) { + pixel_info.target_sint_mask |= 1u << export_index; + } const auto format = Prospero::ResolveRenderTargetFormat(rt.info.format, rt.info.channel_type); if (format.buffer_format != Prospero::BufferFormat::k11_11_10UNorm) continue; const auto mapping = pixel_info.target_export_mapping[export_index]; @@ -772,9 +777,11 @@ void PipelineCache::GetGraphicsPrograms(const CommandBuffer& command, DrawRender (blend.separate_alpha_blend && (BlendFactorIsDualSource(blend.alpha_srcblend) || BlendFactorIsDualSource(blend.alpha_destblend)))); if (pixel_info.dual_source_blending) { - // MRT1 supplies the second blend source for target 0. + // MRT1 supplies the second blend source for target 0; blended targets are not integer. pixel_info.target_output_mode[1] = pixel_info.target_output_mode[0]; pixel_info.target_export_mapping[1] = pixel_info.target_export_mapping[0]; + pixel_info.target_uint_mask &= ~2u; + pixel_info.target_sint_mask &= ~2u; } 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, diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp index d47e6b631..c67370790 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterAnalysis.cpp @@ -30,6 +30,13 @@ bool PixelParameterIsCustom(const EmitterState& state, uint32_t attr) { ShaderPixelParameterIsCustom(*state.input_info.pixel, attr); } +uint32_t MrtOutputType(EmitterState& state, uint32_t index) { + const auto& pixel = *state.input_info.pixel; + if ((pixel.target_sint_mask & (1u << index)) != 0u) return TypeI32Vector(state, 4); + if ((pixel.target_uint_mask & (1u << index)) != 0u) return TypeU32Vector(state, 4); + return TypeF32Vector(state, 4); +} + uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp) { if (exp.kind == IR::ExportTargetKind::Position && exp.index == 0) { return state.per_vertex_variable; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp index 8d26d7e73..2629d282c 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp @@ -291,9 +291,10 @@ uint32_t ExportRawComponent(ValueEmitContext& ctx, uint32_t vector, uint32_t com } uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo& exp, - bool uint_output) { - auto& state = ctx.state; - if (exp.compr && !uint_output) { + uint32_t vector_type) { + auto& state = ctx.state; + const bool integer_output = vector_type != TypeF32Vector(state, 4); + if (exp.compr && !integer_output) { auto unpack = GLSLstd450UnpackHalf2x16; switch (MrtOutputMode(state, exp)) { case 5: unpack = GLSLstd450UnpackUnorm2x16; break; @@ -330,9 +331,11 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo ConstantU32(state, 0), ConstantU32(state, 0), ConstantU32(state, 0), - ConstantU32(state, uint_output ? 1u : 0x3f800000u), + ConstantU32(state, integer_output ? 1u : 0x3f800000u), }; if (exp.compr) { + const auto extract = vector_type == TypeU32Vector(state, 4) ? spv::OpBitFieldUExtract + : spv::OpBitFieldSExtract; for (uint32_t pair = 0; pair < 2u; pair++) { if ((exp.en & (3u << (pair * 2u))) == 0u) { continue; @@ -344,8 +347,8 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo continue; } raw[component] = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(state), raw[component], - packed, ConstantU32(state, lane * 16u), + state.builder.AddFunction(extract, TypeU32(state), raw[component], packed, + ConstantU32(state, lane * 16u), ConstantU32(state, 16)); } } @@ -360,12 +363,10 @@ uint32_t ExportVector(ValueEmitContext& ctx, uint32_t data, const IR::ExportInfo state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), data, raw[0], raw[1], raw[2], raw[3]); } - if (uint_output) { + if (vector_type == TypeU32Vector(state, 4)) { return data; } - const auto vector = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpBitcast, TypeF32Vector(state, 4), vector, data); - return vector; + return Unary(state, spv::OpBitcast, vector_type, data); } uint32_t DistancePointer(EmitterState& state, IR::StageOutputKind kind, uint32_t index) { @@ -563,11 +564,11 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) { if (state.program.stage != ShaderType::Mesh && variable == 0) { return; } - 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); + const auto vector_type = MrtOutputMode(state, exp) != 0u ? MrtOutputType(state, exp.index) + : TypeF32Vector(state, 4); + auto value = ExportVector(ctx, data, exp, vector_type); if (state.program.stage == ShaderType::Pixel && exp.kind == IR::ExportTargetKind::Mrt && - exp.index == 0 && !uint_output && + exp.index == 0 && vector_type == TypeF32Vector(state, 4) && state.input_info.pixel->alpha_blend_source != ShaderAlphaBlendSource::None) { // Broadcast logical alpha before swizzling the primary output. const auto blend_output = diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index 99c14e255..f7a3f8e93 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -307,6 +307,7 @@ uint32_t VertexParameterScalarType(EmitterState& state, VertexInputScalarKind ki uint32_t OutputVariableForExport(const EmitterState& state, const IR::ExportInfo& exp); +uint32_t MrtOutputType(EmitterState& state, uint32_t index); uint32_t ConstantU32(EmitterState& state, uint32_t value); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp index 99128270c..cb0b14006 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp @@ -691,13 +691,15 @@ void DefineOutputs(EmitterState& state) { break; case IR::StageOutputKind::Parameter: case IR::StageOutputKind::Mrt: { - const bool uint_output = - binding.kind == IR::StageOutputKind::Mrt && - state.program.stage == ShaderType::Pixel && - binding.index < std::size(state.input_info.pixel->target_output_mode) && - (state.input_info.pixel->target_output_mode[binding.index] == 7u || - (state.input_info.pixel->target_packed_unorm_mask & (1u << binding.index)) != 0u); - const auto type = uint_output ? TypeU32Vector(state, 4) : TypeF32Vector(state, 4); + auto type = TypeF32Vector(state, 4); + if (binding.kind == IR::StageOutputKind::Mrt && + state.program.stage == ShaderType::Pixel && binding.index < 8u) { + // Packed UNorm stores its encoded bits. + const auto bit = 1u << binding.index; + type = (state.input_info.pixel->target_packed_unorm_mask & bit) != 0u + ? TypeU32Vector(state, 4) + : MrtOutputType(state, binding.index); + } binding.variable_id = DefineInterfaceVariable(state, type, spv::StorageClassOutput, binding.debug_name.c_str()); const bool dual_source = binding.kind == IR::StageOutputKind::Mrt && diff --git a/src/graphics/shader/shader.cpp b/src/graphics/shader/shader.cpp index 4752c1df1..c4bb97749 100644 --- a/src/graphics/shader/shader.cpp +++ b/src/graphics/shader/shader.cpp @@ -490,7 +490,9 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector key.push_back(static_cast(info.object_primitive_id)); 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)); - key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u)); + key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u) | + (uint32_t {info.target_uint_mask} << 16u) | + (uint32_t {info.target_sint_mask} << 24u)); 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) { diff --git a/src/graphics/shader/shader.h b/src/graphics/shader/shader.h index 8ec841a85..9db2c6299 100644 --- a/src/graphics/shader/shader.h +++ b/src/graphics/shader/shader.h @@ -181,6 +181,9 @@ struct ShaderPixelInputInfo { uint32_t target_shader_mask = UINT32_MAX; std::array target_export_mapping = {}; uint8_t target_packed_unorm_mask = 0; + // Exports to UINT and SINT targets, which take integer outputs of the same signedness. + uint8_t target_uint_mask = 0; + uint8_t target_sint_mask = 0; // CB round mode per converted export: zero adds half an LSB, one truncates. uint8_t target_round_mode = 0; uint32_t scratch_size_dwords = 0; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 4cde9df49..208e785c1 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -19808,6 +19808,40 @@ public: } Require(name, "packed rounding program distinction", half_up_program != truncate_program, "half-up and truncate exports shared one shader specialization"); + // UINT and SINT targets give their exports integer outputs. + registers.Reset(); + registers.SetViewportTransformControl(0x300); + registers.SetRenderTargetMask(0xffu); + registers.SetShaderMask(0xffu); + shaders.SetPsShaderBase(pixel_address); + for (const auto [slot, type] : {std::pair{0u, Prospero::ChannelType::kUInt}, + std::pair{1u, Prospero::ChannelType::kSInt}}) { + registers.SetTargetOutputMode(slot, 9); + registers.SetColorBase(slot, {.addr = depth_address + 0x10000 * (slot + 1)}); + registers.SetColorInfo(slot, {.format = Prospero::ChannelLayout::k32, .channel_type = type, + .channel_order = Prospero::ChannelOrder::kStandard}); + registers.SetColorAttrib2(slot, {.height = extent - 1, .width = extent - 1}); + registers.SetColorAttrib3(slot, {.tile_mode = Prospero::TileMode::kLinear, .dimension = 1}); + } + native_programs(); + Require(name, "integer target outputs", + pixel.target_uint_mask == 1u && pixel.target_sint_mask == 2u, + "UINT and SINT targets did not select integer export outputs"); + // MRT1 is the second blend source of a dual-source float target 0, not integer target 1. + HW::BlendControl dual_blend{}; + dual_blend.enable = true; + dual_blend.color_srcblend = static_cast(Prospero::BlendFactor::kSrc1Color); + registers.SetBlendControl(0, dual_blend); + for (const auto [slot, type] : {std::pair{0u, Prospero::ChannelType::kFloat}, + std::pair{1u, Prospero::ChannelType::kUInt}}) { + registers.SetColorInfo(slot, {.format = Prospero::ChannelLayout::k32, .channel_type = type, + .channel_order = Prospero::ChannelOrder::kStandard}); + native_programs(); + Require(name, "dual-source float outputs", + pixel.dual_source_blending && pixel.target_uint_mask == 0u && + pixel.target_sint_mask == 0u, + "the second blend source kept the integer type of render target 1"); + } DestroyBuffer(&packed_output); m_device.destroySampler(packed_sampler); DestroyBuffer(&stencil_readback); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 8054b7ee1..bcec112d0 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -13748,6 +13748,7 @@ void TestNewShaderRecompilerExpPixelOutputs() { ShaderPixelInputInfo uint16_info; uint16_info.target_output_mode[0] = 7; + uint16_info.target_uint_mask = 1; options.input_info.pixel = &uint16_info; auto uint16_result = RecompileForTest(shader, options); const auto uint16_source = DisassembleSpirvBinary(uint16_result.spirv); @@ -13762,6 +13763,18 @@ void TestNewShaderRecompilerExpPixelOutputs() { "compressed UINT16 MRT export was incorrectly decoded as FP16"); CheckSpirvBinaryValidates(uint16_result.spirv); + ShaderPixelInputInfo sint16_info; + sint16_info.target_output_mode[0] = 8; + sint16_info.target_sint_mask = 1; + options.input_info.pixel = &sint16_info; + auto sint16_result = RecompileForTest(shader, options); + const auto sint16_source = DisassembleSpirvBinary(sint16_result.spirv); + Check(sint16_source.find("OpVariable %_ptr_Output_v4int Output") != std::string::npos && + CountSourceOccurrences(sint16_source, "OpBitFieldSExtract") == 4u && + !SpirvContainsExtInst(sint16_result.spirv, 62), + "compressed SINT16 MRT export did not sign-extend into a signed output"); + CheckSpirvBinaryValidates(sint16_result.spirv); + ShaderPixelInputInfo unorm16_info; unorm16_info.target_output_mode[0] = 5; options.input_info.pixel = &unorm16_info; @@ -13789,6 +13802,7 @@ void TestNewShaderRecompilerExpPixelOutputs() { ShaderPixelInputInfo uint_info; uint_info.target_output_mode[0] = 7; + uint_info.target_uint_mask = 1; options.input_info.pixel = &uint_info; auto partial_uint_result = RecompileForTest(partial_shader, options); @@ -13815,6 +13829,15 @@ void TestNewShaderRecompilerExpPixelOutputs() { "compressed UINT16 BA-only export did not read and unpack VSRC1"); CheckSpirvBinaryValidates(compressed_ba_result.spirv); + // 32-bit exports keep their raw bits in a UINT target's integer output. + uint_info.target_output_mode[0] = 9; + auto raw_uint_result = RecompileForTest(partial_shader, options); + const auto raw_uint_source = DisassembleSpirvBinary(raw_uint_result.spirv); + Check(raw_uint_source.find("OpVariable %_ptr_Output_v4uint Output") != std::string::npos && + raw_uint_source.find("OpBitcast %v4float") == std::string::npos, + "32-bit MRT export to a UINT target did not use an unsigned integer output"); + CheckSpirvBinaryValidates(raw_uint_result.spirv); + unorm16_info.target_output_mode[0] = 5; options.input_info.pixel = &unorm16_info; auto unorm16_ba_result =