From 81f312638fc4534b6046df066bb36460f56ead0a Mon Sep 17 00:00:00 2001 From: nmzik Date: Sat, 26 Sep 2026 23:30:21 +0400 Subject: [PATCH] graphics: encode packed UNorm buffer stores before writing skinning normals --- .../backend/spirv/spirvEmitterMemory.cpp | 89 ++++++++++++------- tests/ShaderRecompilerComputeTests.cpp | 80 ++++++++++++++--- 2 files changed, 128 insertions(+), 41 deletions(-) diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp index fec3e3be3..736d5514b 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp @@ -706,26 +706,66 @@ uint32_t FormattedOutOfBoundsValue(ValueEmitContext& ctx, const IR::MemoryInfo& return ConstructU32Composite(ctx.state, components, values); } -void StoreFormattedInBounds(ValueEmitContext& ctx, const IR::MemoryInfo& mem, - const PreparedFormattedMemory& plan, uint32_t component, - uint32_t data) { - if (component >= plan.info.component_count) return; - const auto bits = plan.info.component_bits[component]; - if (bits == 16u && (plan.info.type == Format::ComponentType::Snorm || - plan.info.type == Format::ComponentType::Float)) { +uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx, + const Format::BufferFormatInfo& info, + uint32_t component, uint32_t data) { + const auto bits = info.component_bits[component]; + if (info.type == Format::ComponentType::Unorm) { + const auto value = EmitBitCastF32U32(ctx.state, data); + const auto clamped = EmitFPMin32( + ctx.state, EmitFPMax32(ctx.state, value, ConstantF32Value(ctx.state, 0.0f)), + ConstantF32Value(ctx.state, 1.0f)); + const auto scaled = EmitFPMul32( + ctx.state, clamped, ConstantF32Value(ctx.state, float((1u << bits) - 1u))); + return Unary(ctx.state, spv::OpConvertFToU, TypeU32(ctx.state), + EmitFPRoundEven32(ctx.state, scaled)); + } + if (bits == 16u && (info.type == Format::ComponentType::Snorm || + info.type == Format::ComponentType::Float)) { const auto value = EmitBitCastF32U32(ctx.state, data); const auto pair = EmitCompositeConstructF32x2(ctx.state, value, ConstantF32Value(ctx.state, 0.0f)); - data = plan.info.type == Format::ComponentType::Float - ? EmitPackHalf2x16(ctx.state, pair) - : EmitPackSnorm2x16(ctx.state, pair); - } - if (bits == 8u || bits == 16u) { - StoreSubwordInBounds(ctx, mem, plan.resource, plan.addresses[component], - plan.indices[component], bits, data); - } else { - StoreWordInBounds(ctx, plan.resource, plan.indices[component], data); + return info.type == Format::ComponentType::Float ? EmitPackHalf2x16(ctx.state, pair) + : EmitPackSnorm2x16(ctx.state, pair); } + return data; +} + +void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst, + const IR::MemoryInfo& mem, const MemoryResourceAccess& resource, + const Format::BufferFormatInfo& info, uint32_t data, + uint32_t components) { + // RDNA2 formatted stores transfer the entire format, zero-filling missing VGPRs. + const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info, info.component_count, + FormattedAccess::Store); + EmitIfCondition(ctx.state, plan.in_bounds, [&]() { + auto packed = ConstantU32(ctx.state, 0); + for (uint32_t component = 0; component < info.component_count; component++) { + auto value = ConstantU32(ctx.state, 0); + if (component < components) { + value = data; + if (components != 1u) { + value = ctx.state.builder.AllocateId(); + ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), + value, data, component); + } + } + value = EncodeFormattedStoreComponent(ctx, info, component, value); + const auto bits = info.component_bits[component]; + if (info.packed_bitfield) { + packed = EmitBitFieldInsert( + ctx.state, packed, value, + ConstantU32(ctx.state, info.component_bit_offset[component]), + ConstantU32(ctx.state, bits)); + } else if (bits == 8u || bits == 16u) { + StoreSubwordInBounds(ctx, mem, resource, plan.addresses[component], + plan.indices[component], bits, value); + } else { + StoreWordInBounds(ctx, resource, plan.indices[component], value); + } + } + if (info.packed_bitfield) StoreWordInBounds(ctx, resource, plan.indices[0], packed); + }); } void FormattedStore(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) { @@ -737,11 +777,7 @@ void FormattedStore(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memor StoreWordPrepared(ctx, inst, RebaseRawComponent(mem, 0u), resource, data); return; } - const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info, 1u, - FormattedAccess::Store); - EmitIfCondition(ctx.state, plan.in_bounds, [&]() { - StoreFormattedInBounds(ctx, mem, plan, 0u, data); - }); + StoreFormattedPrepared(ctx, inst, mem, resource, info, data, 1u); }); } @@ -859,16 +895,7 @@ void StoreWideBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t compo const auto info = Format::GetFormatInfo( mem.formatted ? BufferFormat(ctx, mem) : Prospero::BufferFormat::kInvalid); if (info.type != Format::ComponentType::Unknown) { - const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info, components, - FormattedAccess::Store); - EmitIfCondition(state, plan.in_bounds, [&]() { - for (uint32_t component = 0; component < components; component++) { - const auto data = state.builder.AllocateId(); - state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), data, - composite, component); - StoreFormattedInBounds(ctx, mem, plan, component, data); - } - }); + StoreFormattedPrepared(ctx, inst, mem, resource, info, composite, components); return; } for (uint32_t component = 0; component < components; component++) { diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index e97879caf..6366dd3b2 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -23770,7 +23770,7 @@ TestCase BufferLoadFormatXChecksOnlyTransferredComponent() { return test; } -TestCase BufferStoreFormatXChecksOnlyTransferredComponent() { +TestCase BufferStoreFormatXRejectsPartialRecord() { using O = ShaderOpcode; std::vector code; @@ -23781,10 +23781,10 @@ TestCase BufferStoreFormatXChecksOnlyTransferredComponent() { AppendEnd(&code); TestCase test; - test.name = "BufferStoreFormatXChecksOnlyTransferredComponent"; + test.name = "BufferStoreFormatXRejectsPartialRecord"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; - test.expected = {0x11111111u, 0x22222222u, 0x33333333u, 0xaaaaaaaau}; + test.expected = test.initial; test.storage_buffer_range_dwords = 4; test.user_data = MakeStructuredStorageBufferData( 0, 4, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); @@ -23817,7 +23817,7 @@ TestCase BufferLoadFormatXyChecksOnlyTransferredComponents() { return test; } -TestCase BufferStoreFormatXyChecksOnlyTransferredComponents() { +TestCase BufferStoreFormatXyRejectsPartialRecord() { using O = ShaderOpcode; std::vector code; @@ -23829,10 +23829,10 @@ TestCase BufferStoreFormatXyChecksOnlyTransferredComponents() { AppendEnd(&code); TestCase test; - test.name = "BufferStoreFormatXyChecksOnlyTransferredComponents"; + test.name = "BufferStoreFormatXyRejectsPartialRecord"; test.code = std::move(code); test.initial = {0x11111111u, 0x22222222u, 0x33333333u, 0x44444444u}; - test.expected = {0x11111111u, 0x22222222u, 0xaaaaaaaau, 0xbbbbbbbbu}; + test.expected = test.initial; test.storage_buffer_range_dwords = 4; test.user_data = MakeStructuredStorageBufferData( 0, 4, false, BufferFormat(Prospero::BufferFormat::k32_32_32_32Float)); @@ -24147,6 +24147,60 @@ TestCase BufferStoreFormatXResource16UintPreservesAdjacentLanes() { return test; } +TestCase BufferStoreFormatXyzwPackedUnormSkinningVectors() { + using O = ShaderOpcode; + + // The captured skinning dispatch stores a compressed quaternion with W = 1. + constexpr std::array values = { + 0.25f, 0.5f, 0.75f, 1.0f, + -1.0f, 0.0f, 2.0f, 1.0f, + 0.5f, 0.5f, 0.5f, 0.5f}; + std::vector code; + for (u32 record = 0; record < 3; record++) { + for (u32 component = 0; component < 4; component++) { + AppendVMovLiteral(&code, component, + std::bit_cast(values[record * 4 + component])); + } + AppendVMovU32(&code, 20, record); + code.push_back(EncodeMubuf0(0x07u, 0, true, false)); + code.push_back(EncodeMubuf1(0, 0, 20)); + } + AppendEnd(&code); + + TestCase test; + test.name = "BufferStoreFormatXyzwPackedUnormSkinningVectors"; + test.code = std::move(code); + test.initial = std::vector(4, 0xdeadbeefu); + test.expected = {0xeff80100u, 0xfff00000u, 0xa0080200u, 0xdeadbeefu}; + test.user_data = MakeStructuredStorageBufferData( + 4, 3, false, BufferFormat(Prospero::BufferFormat::k10_10_10_2UNorm)); + test.has_user_data = true; + test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_XYZW, O::S_ENDPGM}; + return test; +} + +TestCase BufferStoreFormatXZeroFillsPackedRecord() { + using O = ShaderOpcode; + + std::vector code; + AppendVMovLiteral(&code, 0, std::bit_cast(0.5f)); + AppendVMovU32(&code, 20, 4); + code.push_back(EncodeMubuf0(0x04u)); + code.push_back(EncodeMubuf1(0, 0, 20)); + AppendEnd(&code); + + TestCase test; + test.name = "BufferStoreFormatXZeroFillsPackedRecord"; + test.code = std::move(code); + test.initial = {0x11111111u, 0xffffffffu, 0x33333333u}; + test.expected = {0x11111111u, 0x00000200u, 0x33333333u}; + test.user_data = MakeStructuredStorageBufferData( + 0, 12, false, BufferFormat(Prospero::BufferFormat::k10_10_10_2UNorm)); + test.has_user_data = true; + test.opcodes = {O::V_MOV_B32, O::BUFFER_STORE_FORMAT_X, O::S_ENDPGM}; + return test; +} + TestCase BufferStoreFormatXyzwFloat16ConvertsComponents() { using O = ShaderOpcode; @@ -29090,9 +29144,9 @@ std::vector MakeCases() { AddCase(BufferLoadFormatXyzwRejectsPartialRecord); AddCase(BufferStoreFormatXyzwDropsPartialRecord); AddCase(BufferLoadFormatXChecksOnlyTransferredComponent); - AddCase(BufferStoreFormatXChecksOnlyTransferredComponent); + AddCase(BufferStoreFormatXRejectsPartialRecord); AddCase(BufferLoadFormatXyChecksOnlyTransferredComponents); - AddCase(BufferStoreFormatXyChecksOnlyTransferredComponents); + AddCase(BufferStoreFormatXyRejectsPartialRecord); AddCase(BufferStoreVariants); AddCase(BufferFormatVariants); AddCase(BufferLoadFormatXyzwSnapshotsOverlappingAddress); @@ -29103,6 +29157,8 @@ std::vector MakeCases() { AddCase(BufferFormatStoreVariants); AddCase(BufferStoreFormatXResource16UintWritesHalfword); AddCase(BufferStoreFormatXResource16UintPreservesAdjacentLanes); + AddCase(BufferStoreFormatXyzwPackedUnormSkinningVectors); + AddCase(BufferStoreFormatXZeroFillsPackedRecord); AddCase(BufferStoreFormatXyzwFloat16ConvertsComponents); AddCase(BufferStoreFormatXyzwSnorm16CapturedSkinningVectors); AddCase(BufferStoreFormatXSnorm16ClampsRoundsAndPreservesHalfwords); @@ -33836,15 +33892,19 @@ int main(int argc, char **argv) { RunCase(&vulkan, VectorVopcCmpxOrderedCapturedExecMask()); return 0; } - if (argc == 2 && std::strcmp(argv[1], "--buffer-snorm-store-only") == 0) { + if (argc == 2 && std::strcmp(argv[1], "--buffer-format-store-only") == 0) { VulkanHarness vulkan; + RunCase(&vulkan, BufferStoreFormatXyzwPackedUnormSkinningVectors()); + RunCase(&vulkan, BufferStoreFormatXZeroFillsPackedRecord()); + RunCase(&vulkan, BufferStoreFormatXyzwFloat16ConvertsComponents()); RunCase(&vulkan, BufferStoreFormatXyzwSnorm16CapturedSkinningVectors()); RunCase(&vulkan, BufferStoreFormatXSnorm16ClampsRoundsAndPreservesHalfwords()); RunCase(&vulkan, BufferStoreFormatXResource16UintWritesHalfword()); RunCase(&vulkan, BufferStoreFormatXResource16UintPreservesAdjacentLanes()); RunCase(&vulkan, BufferStoreFormatXyResource88UintWritesBytes()); RunCase(&vulkan, BufferStoreFormatXyzwDropsPartialRecord()); - RunCase(&vulkan, BufferStoreFormatXChecksOnlyTransferredComponent()); + RunCase(&vulkan, BufferStoreFormatXRejectsPartialRecord()); + RunCase(&vulkan, BufferStoreFormatXyRejectsPartialRecord()); RunCase(&vulkan, BufferFormatStoreVariants()); return 0; }