From c914435f207c145fb522a3226e5ced8341ba772e Mon Sep 17 00:00:00 2001 From: nmzik Date: Wed, 14 Oct 2026 19:13:16 +0200 Subject: [PATCH] shader: plan finite GPU-selected RGBA16 buffer stores --- .../ir/passes/ResourceMaterialization.cpp | 16 +- .../recompiler/ir/passes/ResourceTracking.cpp | 11 +- tests/ResourceTrackingTests.cpp | 142 ++++++++++++++++-- tests/ShaderRecompilerComputeTests.cpp | 81 +++++++--- 4 files changed, 207 insertions(+), 43 deletions(-) diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 4d9724e04..c3a6875a1 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -253,12 +253,16 @@ bool NormalizeIndirectBuffer(DescriptorValue& value, bool indexed, const BufferR const auto format = Format::GetFormatInfo(descriptor.Format()); if (!indexed || format.type == Format::ComponentType::Unknown) return SpecializationFail("indirect formatted buffers require a supported indexed format"); - if (!formatted_read && - ((descriptor.Format() != Prospero::BufferFormat::k32UInt && - descriptor.Format() != Prospero::BufferFormat::k8SInt) || - descriptor.DstSelXYZW() != DstSel(4, 0, 0, 0) || - descriptor.Stride() != format.byte_size)) - return SpecializationFail("indirect formatted X stores require tight SInt8 or UInt32 identity records"); + if (!formatted_read) { + const bool scalar = resource.max_byte_extent == 4u && + (descriptor.Format() == Prospero::BufferFormat::k32UInt || + descriptor.Format() == Prospero::BufferFormat::k8SInt); + const bool rgba16 = resource.max_byte_extent == 16u && + descriptor.Format() == Prospero::BufferFormat::k16_16_16_16Float; + if ((!scalar && !rgba16) || descriptor.Stride() != format.byte_size || + descriptor.DstSelXYZW() != (rgba16 ? DstSel(4, 5, 6, 7) : DstSel(4, 0, 0, 0))) + return SpecializationFail("indirect formatted stores require tight identity SInt8, UInt32 or RGBA16Float records"); + } if (formatted_read && (descriptor.Base48() == 0u || descriptor.Stride() == 0u || descriptor.NumRecords() == 0u)) { // Every index is out of bounds. Preserve the format and swizzle constants. diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index 17e771286..fc48fe5c5 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -2618,7 +2618,7 @@ private: bool IndirectBufferAccess(const Inst& inst) const { const auto opcode = inst.GetOpcode(); if (opcode != ValueOpcode::StoreBufferU32 && opcode != ValueOpcode::StoreBufferU8 && - opcode != ValueOpcode::LoadBufferU32x2) return false; + opcode != ValueOpcode::StoreBufferU32x4 && opcode != ValueOpcode::LoadBufferU32x2) return false; const auto index = inst.Flags().index; if (index >= m_program.memory_info.size()) return false; const auto& memory = m_program.memory_info[index]; @@ -2630,6 +2630,9 @@ private: if (!memory.idxen) return false; if (opcode == ValueOpcode::StoreBufferU8) return !memory.formatted && memory.data_bits == 8u; + if (opcode == ValueOpcode::StoreBufferU32x4) + return memory.formatted && memory.data_bits == 32u && memory.data_dwords == 4u && + memory.component_count == 4u; return memory.formatted && memory.data_bits == 16u && memory.data_dwords == 2u && memory.component_count == 4u; } @@ -3023,6 +3026,12 @@ private: return finish(); } if (!legacy_store) { + if (table_source.dword_count == 2u && !plan.split_offsets) { + const auto maximum = SelectorMaximum(key); + if (maximum >= MaxIndirectDescriptorProbes) return false; + indirect.key_count = Value(maximum + 1u); + return finish(); + } // A bounded V# supplies every reachable descriptor without reading the GPU key. // Separate U32 offsets could wrap each DWORD to a different table position. if (table_source.dword_count != 4u || plan.split_offsets) return false; diff --git a/tests/ResourceTrackingTests.cpp b/tests/ResourceTrackingTests.cpp index f2767f1f4..e098213e8 100644 --- a/tests/ResourceTrackingTests.cpp +++ b/tests/ResourceTrackingTests.cpp @@ -5092,7 +5092,8 @@ void TestGpuSelectedBufferRecords() { void TestGpuSelectedFormattedRecordStore() { namespace Prospero = Libs::Graphics::Prospero; - const auto make = [](uint32_t stride = 592u, uint32_t shift = 16u, bool split = false) { + const auto make = [](uint32_t stride = 592u, uint32_t shift = 16u, bool split = false, + bool wide = false) { auto fixture = std::make_unique(); auto& f = *fixture; std::array table_words, selector_words; @@ -5100,29 +5101,39 @@ void TestGpuSelectedFormattedRecordStore() { table_words[word] = f.UserData(word); selector_words[word] = f.UserData(4u + word); } - const auto table = f.Buffer(table_words); + const auto table = wide ? f.Address(table_words[0], table_words[1]) : f.Buffer(table_words); const auto lane = f.Emit(ValueOpcode::LaneId); - const auto packed = f.Emit(ValueOpcode::LoadBufferU32, + const auto packed = wide ? f.Emit(ValueOpcode::ReadConstBuffer, + {f.Buffer(selector_words), f.Emit(ValueOpcode::IMul32, {lane, Value(8u)})}, + f.AddMemory({.kind = ResourceKind::ScalarBuffer, .offset = 4u}, 0x1c4cu)) : + f.Emit(ValueOpcode::LoadBufferU32, {f.Buffer(selector_words), lane, Value(0u), Value(0u), Value(true)}, f.AddMemory({.kind = ResourceKind::Buffer, .idxen = true}, 0x1bcu)); - const auto key = f.Emit(ValueOpcode::ReadLane, + const auto key = wide ? (shift == 0u ? packed : f.Emit(ValueOpcode::BitFieldUExtract, + {packed, Value(16u), Value(5u)})) : f.Emit(ValueOpcode::ReadLane, {f.Emit(ValueOpcode::ShiftRightLogical32, {packed, Value(shift)}), Value(0u)}); const auto offset = f.Emit(ValueOpcode::IMul32, {key, Value(stride)}); std::array words; for (uint32_t word = 0; word < 4u; ++word) { const auto dynamic = split ? f.Emit(ValueOpcode::IAdd32, {offset, Value(word * 4u)}) : offset; - words[word] = f.Emit(ValueOpcode::ReadConstBuffer, {table, dynamic}, - f.AddMemory({.kind = ResourceKind::ScalarBuffer, - .offset = 448u + (split ? 0u : word * 4u), - .component_index = word, .component_count = 4u}, 0x2e8u)); + const auto flags = f.AddMemory({.kind = wide ? ResourceKind::ScalarAddress : ResourceKind::ScalarBuffer, + .offset = (wide ? 48u : 448u) + (split ? 0u : word * 4u), + .component_index = word, .component_count = 4u}, wide ? 0x1d88u : 0x2e8u); + words[word] = wide ? f.Emit(ValueOpcode::LoadAddressU32, + {table, dynamic, Value(0u), Value(true)}, flags) : + f.Emit(ValueOpcode::ReadConstBuffer, {table, dynamic}, flags); } f.Emit(ValueOpcode::LoadBufferU32, {f.Buffer(words), lane, Value(0u), Value(0u), Value(true)}, f.AddMemory({.kind = ResourceKind::Buffer, .formatted = true, .idxen = true}, 0x2f8u)); + const auto data = wide ? f.Emit(ValueOpcode::CompositeConstructU32x4, + {Value(0x3f800000u), Value(0xc0000000u), Value(0x3f000000u), Value(0x3e800000u)}) : + Value(uint32_t(-127)); for (uint32_t use = 0; use < 2u; ++use) - f.Emit(ValueOpcode::StoreBufferU32, - {f.Buffer(words), lane, Value(0u), Value(0u), Value(uint32_t(-127)), Value(true)}, - f.AddMemory({.kind = ResourceKind::Buffer, .formatted = true, + f.Emit(wide ? ValueOpcode::StoreBufferU32x4 : ValueOpcode::StoreBufferU32, + {f.Buffer(words), lane, Value(0u), Value(0u), data, Value(true)}, + f.AddMemory({.kind = ResourceKind::Buffer, .data_dwords = wide ? 4u : 1u, + .component_count = wide ? 4u : 1u, .formatted = true, .idxen = true, .coherent = true}, 0x1618u + use * 8u)); f.PlanAndTrack(); return fixture; @@ -5250,6 +5261,115 @@ void TestGpuSelectedFormattedRecordStore() { {std::tuple{592u, 0u, false}, std::tuple{65540u, 16u, false}, std::tuple{592u, 16u, true}}) CheckFatal([&] { make(stride, shift, split); }, "not a valid runtime value", "exact record store accepted an unbounded, wrapping or split descriptor offset"); + + auto wide = make(16u, 16u, false, true); + auto wide_plan = ExtractResourcePlan(wide->program); + uint32_t wide_root = UINT32_MAX; + for (uint32_t resource = 0; resource < wide_plan.info.buffers.size(); ++resource) { + const auto& source = wide_plan.descriptor_sources[wide_plan.info.buffers[resource].source]; + if (!source.indirect_descriptor) continue; + const auto& indirect = *source.indirect_descriptor; + Check(wide_root == UINT32_MAX && !indirect.table_scalar && !indirect.selector && + !indirect.table_maximum && !indirect.pointer_offset && + indirect.key_count == Value(32u) && indirect.table_stride == 16u && + indirect.table_offset == 0u && indirect.table_immediate == 48u && + wide_plan.descriptor_sources[indirect.table_source].dword_count == 2u && + wide_plan.info.buffers[resource].max_byte_extent == 16u, + "RGBA16 store lost its finite raw-pointer rows or captured the GPU selector"); + wide_root = resource; + } + Check(wide_root != UINT32_MAX, "RGBA16 store has no native candidate root"); + selection = nullptr; + uint32_t address_reads = 0u; + stores = 0u; + for (const auto* block : wide->program.blocks) + for (const auto& inst : *block) { + if (inst.GetOpcode() == ValueOpcode::GetBufferResourceIndex) { + Check(selection == nullptr && inst.Flags() == wide_root && + inst.Arg(0).ResolveInstruction()->GetOpcode() == ValueOpcode::BitFieldUExtract, + "RGBA16 stores duplicated selection or replaced the runtime finite key"); + selection = &inst; + } + if (inst.GetOpcode() == ValueOpcode::LoadAddressU32) { + Check(!wide->program.memory_info[inst.Flags().index].planning_only, + "RGBA16 store consumed descriptor words still used by the BDA read"); + ++address_reads; + } + if (inst.GetOpcode() == ValueOpcode::StoreBufferU32x4) { + Check(selection && inst.Arg(0).ResolveInstruction()->Arg(0).ResolveInstruction() == selection, + "RGBA16 store lost its dominating shared selection"); + ++stores; + } + } + Check(address_reads == 4u && stores == 2u, "RGBA16 fixture lost shared descriptor uses"); + std::fill_n(memory.words.begin() + table_offset / 4u, 32u * 4u, 0u); + constexpr uint32_t rgba = (uint32_t(Prospero::BufferFormat::k16_16_16_16Float) << 12u) | 0xfacu; + user_data[0] = uint32_t(memory.base) + table_offset - 48u; + user_data[1] = 0u; + const auto wide_materialize = [&] { + return MaterializeResources(wide_plan, runtime, snapshot, specialization); + }; + for (const bool populated : {false, true, false, true}) { + for (const uint32_t row : {0u, 3u}) { + const std::array descriptor{0x100000u + row * 256u, 8u << 16u, 20u, rgba}; + for (uint32_t word = 0; word < 4u; ++word) + memory.words[table_offset / 4u + row * 4u + word] = populated ? descriptor[word] : 0u; + } + memory.reads = 0u; + Check(wide_materialize() && memory.reads == 32u && + snapshot.buffers.size() == wide_plan.info.buffers.size() + (populated ? 2u : 0u), + "finite RGBA16 table read GPU keys or retained stale null/active candidates"); + } + const auto wide_mapping = specialization.buffers[wide_root].indirect_mapping_offset; + Check(snapshot.flattened_srt[wide_mapping] == 32u, + "finite RGBA16 table lost its complete key map"); + for (uint32_t row = 0; row < 32u; ++row) + Check(snapshot.flattened_srt[wide_mapping + 1u + row * 2u] == row && + snapshot.flattened_srt[wide_mapping + 2u + row * 2u] == + (row == 0u ? 0u : row == 3u ? 2u : 1u), + "finite RGBA16 table selected the wrong active or null candidate"); + const auto wide_original = specialization; + memory.words[table_offset / 4u] += 4u; + Check(wide_materialize() && specialization == wide_original && + snapshot.buffers[wide_root].dwords[0] == memory.words[table_offset / 4u], + "RGBA16 destination refresh froze the address or changed format specialization"); + for (const auto [word, value] : std::array, 6>{{ + {0u, memory.words[table_offset / 4u] | 1u}, {1u, 16u << 16u}, + {1u, (8u << 16u) | (1u << 31u)}, {3u, rgba | (1u << 28u)}, + {3u, (uint32_t(Prospero::BufferFormat::k32UInt) << 12u) | 4u}, {3u, rgba ^ 1u}}}) { + auto& field = memory.words[table_offset / 4u + word]; + const auto saved = field; + field = value; + Check(!wide_materialize(), "RGBA16 store accepted unproved format, alignment, stride, bounds or swizzle"); + field = saved; + } + wide_plan.info.buffers[wide_root].max_byte_extent = 4u; + Check(!wide_materialize(), "scalar formatted store incorrectly admitted an RGBA16 candidate"); + wide_plan.info.buffers[wide_root].max_byte_extent = 16u; + auto& wide_indirect = *wide_plan.descriptor_sources[ + wide_plan.info.buffers[wide_root].source].indirect_descriptor; + for (const bool overflow : {false, true}) { + wide_indirect.key_count = Value(overflow ? 32u : MaxIndirectDescriptorProbes + 1u); + wide_indirect.table_stride = overflow ? 0x10000000u : 16u; + memory.reads = 0u; + Check(!wide_materialize() && memory.reads == 0u, + "finite RGBA16 table read memory before rejecting its cap or wrapped span"); + } + wide_indirect.key_count = Value(32u); + wide_indirect.table_stride = 16u; + Check(wide_materialize(), "RGBA16 candidates did not recover after invalid metadata"); + ApplyResourceSpecialization(wide->program, specialization); + for (const auto* block : wide->program.blocks) + for (const auto& inst : *block) + if (inst.GetOpcode() == ValueOpcode::StoreBufferU32x4) { + const auto& info = wide->program.memory_info[inst.Flags().index]; + Check(info.kind == ResourceKind::Buffer && info.resource == wide_root && info.formatted && + info.coherent && info.data_bits == 32u && info.component_count == 4u, + "RGBA16 specialization lost native wide conversion, selection or GLC"); + } + for (const auto [shift, split] : {std::pair{0u, false}, std::pair{16u, true}}) + CheckFatal([&] { make(16u, shift, split, true); }, "not a valid runtime value", + "finite RGBA16 store accepted an unbounded key or independently wrapping DWORDs"); } void TestFiniteBufferSelection() { diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 18a407715..a2ba0f052 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -37059,26 +37059,35 @@ void CheckGpuSelectedRecordBufferAccess(VulkanHarness &vulkan) { void CheckGpuSelectedFormattedRecordStore(VulkanHarness &vulkan) { constexpr u32 selectors = 128, table = 256, stride = 592, member = 448; - constexpr std::array keys{5u, 0u, 3u, 1u, 4u, 2u, 6u}; const u32 alignment = vulkan.RuntimeContext().GetPhysicalDeviceProperties() .limits.minStorageBufferOffsetAlignment; const u32 slot = (4096u + alignment - 1u) / alignment * alignment; + for (const bool wide : {false, true}) for (const u32 wave : {32u, 64u}) { + const std::array keys{wide ? 31u : 5u, 0u, 3u, 1u, 4u, 2u, 6u}; TestCase test; - test.name = "GpuSelectedFormattedRecordStore"; + test.name = wide ? "GpuSelectedFormattedRgba16Store" : "GpuSelectedFormattedRecordStore"; test.initial.assign((slot * 7u + keys.size() * wave * 4u) / 4u, 0xdeadbeefu); auto bytes = std::as_writable_bytes(std::span(test.initial)); + if (wide) std::fill_n(test.initial.begin() + table / 4u, 32u * 4u, 0u); for (u32 row = 0; row < 6u; ++row) { - const std::array descriptor{slot * (row + 1u), 1u << 16u, 20u, - (u32(Prospero::BufferFormat::k8SInt) << 12u) | DstSel(4, 0, 0, 0)}; + if (wide && row != 0u && row != 3u) continue; + const std::array descriptor{slot * (row + 1u), (wide ? 8u : 1u) << 16u, 20u, + wide ? (u32(Prospero::BufferFormat::k16_16_16_16Float) << 12u) | DstSel(4, 5, 6, 7) + : (u32(Prospero::BufferFormat::k8SInt) << 12u) | DstSel(4, 0, 0, 0)}; std::copy(descriptor.begin(), descriptor.end(), - test.initial.begin() + (table + row * stride + member) / 4u); - for (u32 element = 0; element < 20u; ++element) - bytes[slot * (row + 1u) + element] = std::byte(0x80u + row + element); + test.initial.begin() + (table + (wide ? row * 16u : row * stride + member)) / 4u); + for (u32 element = 0; element < 20u; ++element) { + if (wide) { + test.initial[slot * (row + 1u) / 4u + element * 2u] = 0x40003c00u; + test.initial[slot * (row + 1u) / 4u + element * 2u + 1u] = 0x38003400u; + } else bytes[slot * (row + 1u) + element] = std::byte(0x80u + row + element); + } } auto records = MakeStructuredStorageBufferData(stride, 6u); records[0] = table; std::copy_n(records.begin(), 4, test.user_data.begin() + 32u); + if (wide) { test.user_data[32] = table - 48u; test.user_data[33] = 0u; } auto selector_buffer = MakeStructuredStorageBufferData(0u, keys.size() * 4u); selector_buffer[0] = selectors; std::copy_n(selector_buffer.begin(), 4, test.user_data.begin() + 4u); @@ -37095,24 +37104,40 @@ void CheckGpuSelectedFormattedRecordStore(VulkanHarness &vulkan) { test.compute_info.workgroup_register = 44u; test.dispatch_x = keys.size(); auto &code = test.code; - code.insert(code.end(), {EncodeSop2(0x1e, 23, 44, InlineU32(2)), - EncodeVop1(0x01, 30, 23), EncodeMubuf0(0x0c), EncodeMubuf1(4, 1, 30), - EncodeVop2(0x16, 6, InlineU32(16), 4)}); - AppendVop3(&code, 0x360, 22, Vgpr(6), InlineU32(0)); - code.insert(code.end(), {EncodeSop2(0x26, 23, 22, 255), stride, - EncodeSmem0(0x0a, 8, 16), EncodeSmem1(member, 23), - EncodeMubuf0(0x00, 0, true, false, true), EncodeMubuf1(4, 2, 0), + code.push_back(EncodeSop2(0x1e, 23, 44, InlineU32(2))); + if (wide) { + code.insert(code.end(), {EncodeSmem0(0x08, 22, 2), EncodeSmem1(0, 23), + EncodeSop2(0x27, 22, 22, 255), (5u << 16u) | 16u, + EncodeSop2(0x1e, 23, 22, InlineU32(4)), + EncodeSmem0(0x02, 8, 16), EncodeSmem1(48, 23)}); + } else { + code.insert(code.end(), {EncodeVop1(0x01, 30, 23), EncodeMubuf0(0x0c), + EncodeMubuf1(4, 1, 30), EncodeVop2(0x16, 6, InlineU32(16), 4)}); + AppendVop3(&code, 0x360, 22, Vgpr(6), InlineU32(0)); + code.insert(code.end(), {EncodeSop2(0x26, 23, 22, 255), stride, + EncodeSmem0(0x0a, 8, 16), EncodeSmem1(member, 23)}); + } + code.insert(code.end(), {EncodeMubuf0(0x00, 0, true, false, true), EncodeMubuf1(4, 2, 0), EncodeSop2(0x1e, 34, 44, InlineU32(std::countr_zero(wave))), EncodeVop2(0x25, 30, 34, 0), EncodeVop2(0x1a, 30, InlineU32(2), 30)}); AppendBufferStoreDword(&code, 4, 30); - code.push_back(EncodeVop2(0x1a, 10, InlineU32(5), 0)); - code.insert(code.end(), {EncodeVop2(0x25, 10, 255, 10), u32(-200)}); + if (wide) { + // The existing direct RGBA16 fixture proves these rounded FP32-to-FP16 values. + for (const auto [vgpr, bits] : std::array, 4>{{ + {10u, 0x3f807e63u}, {11u, 0xc0000000u}, {12u, 0x3f000000u}, {13u, 0x3e800000u}}}) + AppendVMovLiteral(&code, vgpr, bits); + } else { + code.push_back(EncodeVop2(0x1a, 10, InlineU32(5), 0)); + code.insert(code.end(), {EncodeVop2(0x25, 10, 255, 10), u32(-200)}); + } AppendSMovLiteral(&code, 126, 0x55555555u); AppendSMovLiteral(&code, 127, wave == 64u ? 0x55555555u : 0u); - code.insert(code.end(), {EncodeMubuf0(0x04, 0, true, false, true), + // Captured e01c6000/80010f01, adjusted only to v10:13/v0 and s8:11. + code.insert(code.end(), {EncodeMubuf0(wide ? 0x07 : 0x04, 0, true, false, true), EncodeMubuf1(10, 2, 0)}); AppendEnd(&code); - test.decoded_counts = {{"V_LSHLREV_B32", 2u}, {"BUFFER_STORE_FORMAT_X", 1u}}; + test.decoded_counts = {{"V_LSHLREV_B32", wide ? 1u : 2u}, + {wide ? "BUFFER_STORE_FORMAT_XYZW" : "BUFFER_STORE_FORMAT_X", 1u}}; test.required_spirv = {"get_bda_pointer", "convert_indirect_buffer_format"}; test.forbidden_spirv = {"OpAtomicCompareExchange"}; const auto compiled = CompileCase(test, vulkan.SubgroupSize()); @@ -37121,7 +37146,7 @@ void CheckGpuSelectedFormattedRecordStore(VulkanHarness &vulkan) { for (const auto &inst : *block) { using namespace ShaderRecompiler::IR; if (inst.GetOpcode() != ValueOpcode::LoadBufferU32 && - inst.GetOpcode() != ValueOpcode::StoreBufferU32) continue; + inst.GetOpcode() != (wide ? ValueOpcode::StoreBufferU32x4 : ValueOpcode::StoreBufferU32)) continue; const auto &memory = compiled.program.memory_info[inst.Flags().index]; if (inst.GetOpcode() == ValueOpcode::LoadBufferU32) bda_loads += memory.kind == ResourceKind::IndirectBuffer && memory.formatted; @@ -37133,8 +37158,9 @@ void CheckGpuSelectedFormattedRecordStore(VulkanHarness &vulkan) { for (const auto &value : compiled.resources.buffers) { ShaderBufferResource descriptor{}; std::copy_n(value.dwords.begin(), 4u, descriptor.fields); - test.storage_buffer_ranges.emplace_back(descriptor.Base48(), - std::max(4u, u32(descriptor.GetSize()))); + auto range = std::max(4u, u32(descriptor.GetSize())); + if (wide && descriptor.Base48() == slot * 4u) range -= 11u; // Last active record is partial. + test.storage_buffer_ranges.emplace_back(descriptor.Base48(), range); } TestCase producer; producer.name = "FormattedRecordSelectorProducer"; @@ -37156,17 +37182,22 @@ void CheckGpuSelectedFormattedRecordStore(VulkanHarness &vulkan) { const auto row = keys[group]; test.expected[selectors / 4u + group] = (row << 16u) | 0xbeefu; for (u32 lane = 0; lane < wave; ++lane) { + const bool valid = wide ? row == 0u || row == 3u : row < 6u; test.expected[slot * 7u / 4u + group * wave + lane] = - row < 6u && lane < 20u ? u32(int32_t(0x80u + row + lane) - 256) : 0u; - if (row < 6u && lane < 20u && (lane & 1u) == 0u) - expected_bytes[slot * (row + 1u) + lane] = + valid && lane < 20u ? (wide ? 0x3f800000u : u32(int32_t(0x80u + row + lane) - 256)) : 0u; + if (valid && lane < (wide && row == 3u ? 18u : 20u) && (lane & 1u) == 0u) { + if (wide) { + test.expected[slot * (row + 1u) / 4u + lane * 2u] = 0xc0003c04u; + test.expected[slot * (row + 1u) / 4u + lane * 2u + 1u] = 0x34003800u; + } else expected_bytes[slot * (row + 1u) + lane] = std::byte(u32(std::clamp(int32_t(lane * 32u) - 200, -128, 127)) & 255u); + } } } auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size(), true); vulkan.Dispatch(producer, producer_shader, buffer); vulkan.Dispatch(test, compiled, buffer); - CompareWords(test, "shared BDA reads, signed byte saturation, EXEC, OOB and neighboring canaries", + CompareWords(test, "shared BDA reads, formatted conversion, EXEC, OOB and neighboring canaries", test.expected, vulkan.ReadBuffer(test.name, buffer, test.expected.size())); vulkan.DestroyBuffer(&buffer); std::printf("[compute] %s wave=%u ok\n", test.name, wave);