graphics: render packed 11-11-10 UNorm targets with exact rounding

Co-authored-by: fxpw <fxpw@outlook.com>
This commit is contained in:
nmzik
2026-10-15 04:35:56 +02:00
parent fb213e8ae5
commit b4406b0467
11 changed files with 291 additions and 10 deletions
+1 -1
View File
@@ -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},
@@ -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 =
@@ -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<uint32_t>(blend));
}
pixel_info.target_packed_unorm_mask |= 1u << export_index;
pixel_info.target_round_mode |= static_cast<uint8_t>(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;
@@ -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<uint32_t>(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) {
@@ -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);
@@ -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<GLSLstd450FClamp, IR::Type::F32>(
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));
@@ -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());
+1
View File
@@ -675,6 +675,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
key.push_back(static_cast<uint32_t>(info.dual_source_blending));
key.push_back(static_cast<uint32_t>(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) {
+3
View File
@@ -175,6 +175,9 @@ struct ShaderPixelInputInfo {
uint8_t target_output_mode[8] = {};
uint32_t target_shader_mask = UINT32_MAX;
std::array<Prospero::ColorComponentMapping, 8> 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;
+169
View File
@@ -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<std::vector<u32>, 2> result;
constexpr std::array<std::array<u32, 3>, 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<u32>(packed_address >> 8u),
(static_cast<u32>(Prospero::BufferFormat::k11_11_10UNorm) << 20u) | (3u << 30u),
((extent - 1u) >> 2u) | ((extent - 1u) << 14u),
packed_read.image_descriptor_swizzle |
(static_cast<u32>(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<u32, 7> 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<uint8_t>(Prospero::BlendFactor::kOne);
registers.SetBlendControl(float_slot, additive);
const auto &code = packed_pixels[test.clamp];
const auto ps_address = reinterpret_cast<uint64_t>(code.data());
ShaderMapUserData(ps_address, {.type = Prospero::ShaderBinaryType::kPs,
.code_size_bytes = static_cast<u32>(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<float, 4>{-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<u32, 3> channels = test.clamp ? std::array<u32, 3>{0, 2047, 256}
: test.truncate ? std::array<u32, 3>{2, 3, 10} : std::array<u32, 3>{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<u32>(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<float, 4> 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<u32>(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<float>(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;
+45
View File
@@ -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() {