From b4406b04670093533fc289983641ae4bdbb40166 Mon Sep 17 00:00:00 2001 From: nmzik Date: Thu, 15 Oct 2026 04:35:56 +0200 Subject: [PATCH] graphics: render packed 11-11-10 UNorm targets with exact rounding Co-authored-by: fxpw --- src/graphics/guest_gpu/gpu_format.cpp | 2 +- .../host_gpu/renderer/image/textureCommon.cpp | 5 +- .../renderer/pipeline/pipelineCache.cpp | 26 +++ .../backend/spirv/spirvEmitterFlow.cpp | 22 +++ .../backend/spirv/spirvEmitterInternal.h | 8 +- .../spirv/spirvEmitterMemoryHelpers.cpp | 17 +- .../backend/spirv/spirvEmitterModule.cpp | 3 +- src/graphics/shader/shader.cpp | 1 + src/graphics/shader/shader.h | 3 + tests/ShaderRecompilerComputeTests.cpp | 169 ++++++++++++++++++ tests/shaderCfgTests.cpp | 45 +++++ 11 files changed, 291 insertions(+), 10 deletions(-) diff --git a/src/graphics/guest_gpu/gpu_format.cpp b/src/graphics/guest_gpu/gpu_format.cpp index fe0adfb8f..6cb11db6e 100644 --- a/src/graphics/guest_gpu/gpu_format.cpp +++ b/src/graphics/guest_gpu/gpu_format.cpp @@ -42,7 +42,7 @@ constexpr FormatInfo kFormatInfo[] = { {BufferFormat::k16_16UInt, 4, 0, 4, true, true}, {BufferFormat::k16_16SInt, 4, 0, 4, true, false, true}, {BufferFormat::k16_16Float, 4, 0, 4, true, false}, - {BufferFormat::k11_11_10UNorm, 4, 0, 0, true, false}, + {BufferFormat::k11_11_10UNorm, 4, 0, 4, true, false}, {BufferFormat::k11_11_10UInt, 4, 0, 4, true, true}, {BufferFormat::k11_11_10Float, 4, 0, 4, true, false}, {BufferFormat::k10_11_11Float, 4, 0, 4, true, false}, diff --git a/src/graphics/host_gpu/renderer/image/textureCommon.cpp b/src/graphics/host_gpu/renderer/image/textureCommon.cpp index 794b62fd3..b6ac817fa 100644 --- a/src/graphics/host_gpu/renderer/image/textureCommon.cpp +++ b/src/graphics/host_gpu/renderer/image/textureCommon.cpp @@ -64,7 +64,10 @@ RenderTargetFormatInfo TextureGetRenderTargetFormat(Prospero::ChannelLayout layo Prospero::ChannelOrder order) { const auto encoding = Prospero::ResolveRenderTargetFormat(layout, type); if (encoding.IsValid() && encoding.SupportsOrder(order)) { - const auto host_format = ResolveHostFormat(encoding.buffer_format, order); + const auto backing_format = encoding.buffer_format == Prospero::BufferFormat::k11_11_10UNorm + ? Prospero::RemapTextureFormat(encoding.buffer_format) + : encoding.buffer_format; + const auto host_format = ResolveHostFormat(backing_format, order); const auto bytes = Prospero::RenderTargetBytesPerElement(encoding.buffer_format); if (host_format.format != vk::Format::eUndefined && bytes != 0) { const auto order_mapping = diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index f417138ce..0d3e43380 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -6,6 +6,7 @@ #include "common/logging/log.h" #include "common/profiler.h" #include "common/threads.h" +#include "graphics/guest_gpu/gpu_format.h" #include "graphics/guest_gpu/hardwareContext.h" #include "graphics/host_gpu/renderer/colorRenderTarget.h" #include "graphics/host_gpu/renderer/debug.h" @@ -681,6 +682,28 @@ PipelineCache::GraphicsPrograms PipelineCache::GetGraphicsPrograms( ShaderParams pixel_params; if (pixel_active) { pixel_params = PrepareProgram(pixel_regs, sh, target_export_mapping, pixel_info); + for (uint32_t export_index = 0; export_index < 8u; export_index++) { + const auto slot = ShaderPixelExportTarget(pixel_info.target_shader_mask, export_index); + if (slot >= 8u || pixel_info.target_output_mode[export_index] == 0u) continue; + 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; + 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]; + const auto shader_mask = render_target_mask_slot(pixel_info.target_shader_mask, slot); + const bool blend = context.GetBlendControl(slot).enable && !rt.info.blend_bypass; + if (blend || (mapping.ApplyMask(target_mask) & 7u) != 7u || + (mapping.ApplyMask(shader_mask) & 7u) != 7u || + pixel_info.target_output_mode[export_index] != 9u) { + EXIT("unsupported packed UNorm render-target export: slot=%u export=%u mode=%u " + "target_mask=0x%x shader_mask=0x%x mapping=0x%02x blend=%u\n", + slot, export_index, pixel_info.target_output_mode[export_index], target_mask, + shader_mask, mapping.packed, static_cast(blend)); + } + pixel_info.target_packed_unorm_mask |= 1u << export_index; + pixel_info.target_round_mode |= static_cast(rt.info.round_mode) << export_index; + } if (!mesh_active && !tess_active && Prospero::IsRectList(user_config.GetPrimType())) { pixel_info.parameter_mode = ShaderPixelParameterMode::Rectangle; } else if (context.GetModeControl().provoking_vtx_last) { @@ -803,6 +826,9 @@ PipelineCache::Pipeline& PipelineCache::GetGraphicsPipeline( EXIT_IF(!colors[i].image_id || colors[i].desc.view_info.format == vk::Format::eUndefined); static_params.color_mask[slot] = colors[i].export_mapping.ApplyMask( render_target_mask_slot(ctx.GetRenderTargetMask(), colors[i].target_slot)); + if (colors[i].desc.info.guest_format == Prospero::BufferFormat::k11_11_10UNorm) { + static_params.color_mask[slot] = 1u; + } rendering.color_formats[slot] = colors[i].desc.view_info.format; if (attachment_samples == 0) { attachment_samples = colors[i].desc.info.samples; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp index 6cb865046..09d0ac7c5 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterFlow.cpp @@ -567,6 +567,28 @@ void EmitSetAttribute(ValueEmitContext& ctx, const IR::Inst& inst) { mapping.Map(3)); value = mapped; } + if ((state.input_info.pixel->target_packed_unorm_mask & (1u << exp.index)) != 0u) { + if (exp.compr || MrtOutputMode(state, exp) != 9u || + (mapping.ApplyMask(exp.en) & 7u) != 7u) { + EXIT("unsupported packed UNorm EXP: export=%u mode=%u en=0x%x compr=%u\n", + exp.index, MrtOutputMode(state, exp), exp.en, static_cast(exp.compr)); + } + const auto info = Format::GetFormatInfo(Prospero::BufferFormat::k11_11_10UNorm); + const auto rounding = (state.input_info.pixel->target_round_mode & (1u << exp.index)) != 0 + ? UnormRounding::Truncate : UnormRounding::HalfUp; + const auto raw = Unary(state, spv::OpBitcast, TypeU32Vector(state, 4), value); + auto packed = ConstantU32(state, 0); + for (uint32_t component = 0; component < info.component_count; component++) { + const auto encoded = EncodePackedColorComponent( + state, info, component, ExportRawComponent(ctx, raw, component), 32u, rounding); + packed = component == 0u ? encoded + : Binary(state, spv::OpBitwiseOr, TypeU32(state), packed, encoded); + } + value = state.builder.AllocateId(); + state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(state, 4), value, + packed, ConstantU32(state, 0), ConstantU32(state, 0), + ConstantU32(state, 0)); + } } if (exp.kind == IR::ExportTargetKind::Position && state.input_info.vertex->clip_space.enabled) { diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index 05ebceb1a..25347d7a6 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -456,11 +456,15 @@ uint32_t EmitUFloatToF32Bits(EmitterState& state, uint32_t raw, uint32_t bits); uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferFormatInfo& info, uint32_t component, uint32_t raw); +enum class UnormRounding { RoundEven, HalfUp, Truncate }; + uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info, - uint32_t component, uint32_t data, uint32_t data_bits); + uint32_t component, uint32_t data, uint32_t data_bits, + UnormRounding rounding = UnormRounding::RoundEven); uint32_t EncodePackedColorComponent(EmitterState& state, const Format::BufferFormatInfo& info, - uint32_t component, uint32_t data, uint32_t data_bits); + uint32_t component, uint32_t data, uint32_t data_bits, + UnormRounding rounding = UnormRounding::RoundEven); spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp index 79d267c82..09f8ef3c7 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp @@ -429,7 +429,9 @@ uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferForma } uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info, - uint32_t component, uint32_t data, uint32_t data_bits) { + uint32_t component, uint32_t data, uint32_t data_bits, + UnormRounding rounding) { + EXIT_IF(rounding != UnormRounding::RoundEven && info.type != Format::ComponentType::Unorm); const auto bits = info.component_bits[component]; if (data_bits == 16u) { if (info.type == Format::ComponentType::Uint) { @@ -464,8 +466,12 @@ uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::Buffer value = EmitGlsl( state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper)); if (normalized) { - value = EmitFPRoundEven32( - state, EmitFPMul32(state, value, ConstantF32Value(state, max_value))); + value = EmitFPMul32(state, value, ConstantF32Value(state, max_value)); + if (rounding == UnormRounding::RoundEven) { + value = EmitFPRoundEven32(state, value); + } else if (rounding == UnormRounding::HalfUp) { + value = EmitFPAdd32(state, value, ConstantF32Value(state, 0.5f)); + } } // Float-to-integer conversion truncates scaled values toward zero. value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU, @@ -481,8 +487,9 @@ uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::Buffer } uint32_t EncodePackedColorComponent(EmitterState& state, const Format::BufferFormatInfo& info, - uint32_t component, uint32_t data, uint32_t data_bits) { - auto value = EncodeFormattedStoreComponent(state, info, component, data, data_bits); + uint32_t component, uint32_t data, uint32_t data_bits, + UnormRounding rounding) { + auto value = EncodeFormattedStoreComponent(state, info, component, data, data_bits, rounding); if (info.type == Format::ComponentType::Uint) { value = EmitUMin32(state, value, ConstantU32(state, (1u << info.component_bits[component]) - 1u)); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp index d1ec67f6d..0170727fb 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterModule.cpp @@ -663,7 +663,8 @@ void DefineOutputs(EmitterState& state) { 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_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); binding.variable_id = DefineInterfaceVariable(state, type, spv::StorageClassOutput, binding.debug_name.c_str()); diff --git a/src/graphics/shader/shader.cpp b/src/graphics/shader/shader.cpp index 9f2544109..6c60b2587 100644 --- a/src/graphics/shader/shader.cpp +++ b/src/graphics/shader/shader.cpp @@ -675,6 +675,7 @@ 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)); + key.push_back(info.target_packed_unorm_mask | (uint32_t {info.target_round_mode} << 8u)); 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 e5b8285bc..efd02d016 100644 --- a/src/graphics/shader/shader.h +++ b/src/graphics/shader/shader.h @@ -175,6 +175,9 @@ struct ShaderPixelInputInfo { uint8_t target_output_mode[8] = {}; uint32_t target_shader_mask = UINT32_MAX; std::array target_export_mapping = {}; + uint8_t target_packed_unorm_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; bool ps_pos_x = false; bool ps_pos_y = false; diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 1c121e836..d0987ebed 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -18093,6 +18093,175 @@ public: } } } + // Captured format30 uses uncompressed mode9 at MRT2. Keep a blended + // floating-point MRT live beside it, including sparse export routing. + constexpr uint64_t packed_address = depth_address + 0x28000; + static const auto packed_pixels = [] { + std::array, 2> result; + constexpr std::array, 2> inputs{{ + {0x3aa01403u, 0x3ae01c04u, 0x3c282a0bu}, // just above 2.5/3.5/10.5 LSB + {0xbf800000u, 0x40000000u, 0x3e800000u}}}; + for (u32 i = 0; i < result.size(); ++i) { + auto &code = result[i]; + for (u32 component = 0; component < 3; ++component) + AppendVMovLiteral(&code, component, inputs[i][component]); + AppendVMovLiteral(&code, 4, 0x38004400u); // FP16 RG: 4, 0.5 + AppendVMovLiteral(&code, 5, 0x4000bc00u); // FP16 BA: -1, 2 + for (u32 ordinal = 0; ordinal < 7; ++ordinal) { + code.push_back(EncodeExp0(ordinal, 0xf, ordinal == 6, ordinal != 2)); + code.push_back(ordinal == 2 ? EncodeExp1(0, 1, 2, 0) : EncodeExp1(4, 5, 0, 0)); + } + AppendEnd(&code); + } + return result; + }(); + TestCase packed_read; + packed_read.name = "PackedUnormRenderTargetRead"; + packed_read.has_user_data = true; + packed_read.image_descriptor_swizzle = DstSel(4, 5, 6, 1); + const ShaderTextureResource packed_descriptor{{static_cast(packed_address >> 8u), + (static_cast(Prospero::BufferFormat::k11_11_10UNorm) << 20u) | (3u << 30u), + ((extent - 1u) >> 2u) | ((extent - 1u) << 14u), + packed_read.image_descriptor_swizzle | + (static_cast(Prospero::ImageType::kColor2D) << 28u), + 0, 0x00700000u, 0, 0}}; + std::copy_n(packed_descriptor.fields, 8, packed_read.user_data.begin()); + packed_read.user_data[8] = 0x92u; + packed_read.user_data[9] = 0x00fff000u; + packed_read.user_data[10] = 0x0a500000u; // Existing packed-UNorm bilinear clamp contract. + packed_read.user_data[50] = 8 * sizeof(u32); + AppendVMovU32(&packed_read.code, 20, extent / 2); + AppendVMovU32(&packed_read.code, 21, extent / 2); + packed_read.code.insert(packed_read.code.end(), + {EncodeMimg0(0x00, 0xf), EncodeMimg1(0, 20)}); + AppendVMovLiteral(&packed_read.code, 20, 0x3f000000u); + AppendVMovLiteral(&packed_read.code, 21, 0x3f000000u); + packed_read.code.insert(packed_read.code.end(), + {EncodeMimg0(0x27, 0xf), EncodeMimg1(4, 20, 0, 2)}); + for (u32 component = 0; component < 8; ++component) + AppendStoreVgpr(&packed_read.code, component, component); + AppendEnd(&packed_read.code); + const auto packed_reader = CompileCase(packed_read, SubgroupSize()); + auto packed_output = CreateStorageBuffer(name, {}, 8); + const auto packed_sampler = CreateSampler(name); + uint64_t half_up_program = 0, truncate_program = 0; + struct PackedDraw { u32 slot, wave; bool truncate, clamp; }; + for (const auto test : {PackedDraw{2, 64, false, false}, {2, 64, true, false}, + {2, 64, false, false}, {3, 32, false, false}, + {3, 32, false, true}}) { + const u32 float_slot = test.slot == 2 ? 6 : 7; + registers.Reset(); + registers.SetViewportTransformControl(0x300); + registers.SetViewportScaleOffset(0, extent / 2, extent / 2, + extent / 2, extent / 2, 1, 0); + registers.SetViewportZMax(0, 1); + registers.SetScreenScissor(0, 0, extent, extent); + registers.SetWindowScissor(0, 0, extent, extent, false); + registers.SetGenericScissor(0, 0, extent, extent, false); + registers.SetViewportScissor(0, 0, 0, extent, extent, false); + registers.SetRenderTargetMask((7u << (4u * test.slot)) | (0xfu << (4u * float_slot))); + registers.SetShaderMask(test.slot == 2 ? 0x0fffffffu : 0xfffff0ffu); + registers.SetPsInControl(test.wave == 32 ? 0x8000 : 0); + constexpr std::array modes{4, 4, 9, 5, 4, 4, 4}; + for (u32 ordinal = 0; ordinal < modes.size(); ++ordinal) + registers.SetTargetOutputMode(ordinal, modes[ordinal]); + for (const auto slot : {test.slot, float_slot}) { + const bool packed = slot == test.slot; + registers.SetColorBase(slot, {.addr = packed ? packed_address : depth_address + 0x10000}); + registers.SetColorInfo(slot, + {.blend_clamp = packed, .round_mode = packed && test.truncate, + .format = packed ? Prospero::ChannelLayout::k11_11_10 + : Prospero::ChannelLayout::k32_32_32_32, + .channel_type = packed ? Prospero::ChannelType::kUNorm : Prospero::ChannelType::kFloat, + .channel_order = Prospero::ChannelOrder::kStandard}); + registers.SetColorAttrib2(slot, {.height = extent - 1, .width = extent - 1}); + registers.SetColorAttrib3(slot, {.tile_mode = Prospero::TileMode::kLinear, .dimension = 1}); + } + HW::BlendControl additive{}; + additive.enable = true; + additive.color_srcblend = additive.color_destblend = + additive.alpha_srcblend = additive.alpha_destblend = + static_cast(Prospero::BlendFactor::kOne); + registers.SetBlendControl(float_slot, additive); + const auto &code = packed_pixels[test.clamp]; + const auto ps_address = reinterpret_cast(code.data()); + ShaderMapUserData(ps_address, {.type = Prospero::ShaderBinaryType::kPs, + .code_size_bytes = static_cast(code.size() * sizeof(u32))}); + shaders.SetEsShaderBase(vertex_address); + shaders.SetGsShaderResource2({}); + shaders.SetPsShaderBase(ps_address); + user_config.SetPrimitiveType(Prospero::PrimitiveType::kTriList); + native_vertex_regs.es_regs.data_addr = vertex_address; + native_pixel_regs.ps_regs.data_addr = ps_address; + const auto programs = context.GetPipelineCache().GetGraphicsPrograms( + native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(), + registers, user_config, export_mapping, true, native_vertex_info, pixel); + Require(name, "packed format follows export ordinal", + pixel.target_output_mode[2] == 9 && pixel.wave_size == test.wave && + pixel.target_packed_unorm_mask == 4u && + pixel.target_round_mode == (test.truncate ? 4u : 0u), + "packed conversion or rounding contaminated another MRT or followed a physical slot"); + if (test.slot == 2) { + auto &id = test.truncate ? truncate_program : half_up_program; + Require(name, "packed rounding program reuse", id == 0 || id == programs.pixel.id, + "restoring half-up rounding failed to reuse its program"); + id = programs.pixel.id; + } + RenderColorInfo packed_target{}, float_target{}; + RenderExecutorTestAccess::ResolveRenderColorTarget( + executor, scheduler.Current(), packed_target, test.slot); + RenderExecutorTestAccess::ResolveRenderColorTarget( + executor, scheduler.Current(), float_target, float_slot); + Require(name, "packed target storage", packed_target.desc.info.bytes_per_block == 4 && + packed_target.desc.info.pixel_format == vk::Format::eR32Uint, + "packed target expanded its physical storage or used floating-point bit encoding"); + vk::ClearValue packed_clear{}, float_clear{}; + packed_clear.color.uint32[0] = 0xdeadbeefu; + float_clear.color.float32 = std::array{-2.f, 0.25f, 3.f, 0.5f}; + const vk::ImageSubresourceRange range{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; + TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), packed_target.image_id, range, packed_clear); + TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), float_target.image_id, range, float_clear); + RenderExecutorTestAccess::DrawAuto( + executor, scheduler.Current(), {.vertex_count = 3, .instance_count = 1}); + const std::array channels = test.clamp ? std::array{0, 2047, 256} + : test.truncate ? std::array{2, 3, 10} : std::array{3, 4, 11}; + const u32 word = channels[0] | (channels[1] << 11u) | (channels[2] << 22u); + Require(name, "packed MRT words", + ReadCachedTexel(name, context, packed_target.image_id, {}, {extent, extent, 1}) == + std::vector(extent * extent, word), + "mode9 export lost packed widths, half-up/truncate rounding, clamp or RGB write mask"); + const auto floats = ReadCachedTexel(name, context, float_target.image_id, {}, {extent, extent, 1}); + constexpr std::array blended{2.f, 0.75f, 2.f, 2.5f}; + for (size_t i = 0; i < floats.size(); ++i) + Require(name, "unconverted MRT blending", floats[i] == std::bit_cast(blended[i % 4]), + "packed MRT handling changed another target's floating-point export or blending"); + ShaderRecompiler::IR::DescriptorValue value{}; + value.dword_count = 8; + std::copy_n(packed_descriptor.fields, 8, value.dwords.begin()); + const auto binding = RenderExecutorTestAccess::ResolveTexture( + executor, packed_reader.program.info.images.at(0), value); + Require(name, "packed target sampled alias", binding.image_id == packed_target.image_id, + "sampling the packed target allocated a different image or lost the rendered data"); + Image sampled; + sampled.view = cache.FindTexture(binding.image_id, binding.desc); + sampled.layout = cache.GetImage(binding.image_id).backing.state.layout; + scheduler.Finish(); + Dispatch(packed_read, packed_reader, packed_output, nullptr, &sampled, + nullptr, nullptr, packed_sampler); + const auto loaded = ReadBuffer(name, packed_output, 8); + for (u32 i = 0; i < loaded.size(); ++i) { + const auto component = i % 4; + const float expected = component == 3 ? 1.f + : channels[component] / (component == 2 ? 1023.f : 2047.f); + Require(name, "rendered packed load and sample", + std::abs(std::bit_cast(loaded[i]) - expected) < 0.000001f, + "packed rendered contents lost normalization, ordering or texture visibility"); + } + } + Require(name, "packed rounding program distinction", half_up_program != truncate_program, + "half-up and truncate exports shared one shader specialization"); + DestroyBuffer(&packed_output); + m_device.destroySampler(packed_sampler); DestroyBuffer(&stencil_readback); vertex_shader = owned_vertex_shader; diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index 188f946fd..5bb86ad6f 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -13526,6 +13526,12 @@ void TestRenderTargetReverseExportMapping() { 2, {Prospero::ColorMappingRgba, Prospero::ColorMappingRgab, Prospero::ColorMappingBgra, Prospero::ColorMappingAgbr}}, + {Prospero::ChannelLayout::k11_11_10, + Prospero::ChannelType::kUNorm, + vk::Format::eR32Uint, + 4, + {Prospero::ColorMappingRgba, Prospero::ColorMappingRgab, + Prospero::ColorMappingBgra, Prospero::ColorMappingAgbr}}, {Prospero::ChannelLayout::k16_16_16_16, Prospero::ChannelType::kFloat, vk::Format::eR16G16B16A16Sfloat, @@ -13664,6 +13670,45 @@ void TestRenderTargetReverseExportMapping() { PrepareProgram(regs, sh, mappings, compiled_info); Check(sparse_slot_key == MakeStageStaticKey(compiled_info), "component-only shader mask changes introduced a duplicate shader variant"); + + ShaderPixelInputInfo packed_info{}; + packed_info.target_output_mode[0] = 9; + const auto unconverted_key = MakeStageStaticKey(packed_info); + packed_info.target_packed_unorm_mask = 1; + const auto half_up_key = MakeStageStaticKey(packed_info); + options.input_info.pixel = &packed_info; + const auto packed = RecompileForTest(shader, options); + const auto packed_source = DisassembleSpirvBinary(packed.spirv); + Check(half_up_key != unconverted_key && + packed_source.find("OpVariable %_ptr_Output_v4uint Output") != std::string::npos && + packed_source.find("RoundEven") == std::string::npos && + packed_source.find("UnpackHalf2x16") == std::string::npos, + "mode9 packed MRT export lost its cache identity, integer backing or FP32 interpretation"); + CheckSpirvBinaryValidates(packed.spirv); + packed_info.target_round_mode = 1; + const auto truncate = RecompileForTest(shader, options); + Check(MakeStageStaticKey(packed_info) != half_up_key && truncate.spirv != packed.spirv, + "packed MRT rounding was omitted from shader specialization"); + CheckSpirvBinaryValidates(truncate.spirv); + packed_info.target_round_mode = 0; + Check(MakeStageStaticKey(packed_info) == half_up_key, + "restoring packed MRT rounding did not restore the static key"); + packed_info.target_export_mapping[0] = Prospero::ColorMappingBgra; + const auto packed_reversed = RecompileForTest(shader, options); + Check(MakeStageStaticKey(packed_info) != half_up_key && + SpirvContainsVectorShuffle(packed_reversed.spirv, {2u, 1u, 0u, 3u}), + "packed MRT conversion bypassed logical component ordering"); + CheckSpirvBinaryValidates(packed_reversed.spirv); +#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS + const uint32_t partial_packed[] = { + EncodeExp0(0, 0x3), EncodeExp1(0, 1, 2, 3), 0xbf810000u}; + ExpectFatal([&] { (void)RecompileForTest(partial_packed, options); }, + "partial packed MRT export was accepted without preserving untouched fields"); + const uint32_t compressed_packed[] = { + EncodeExp0(0, 0xf, true, true), EncodeExp1(0, 1, 0, 0), 0xbf810000u}; + ExpectFatal([&] { (void)RecompileForTest(compressed_packed, options); }, + "packed MRT accepted an unsupported compressed export"); +#endif } void TestBlendMappingClassification() {