diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index e592cc6c3..a7cc9238d 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -805,7 +805,8 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime, } } -void RenderExecutor::FindBuffers(PreparedBindings& prepared) { +void RenderExecutor::FindBuffers(PreparedBindings& prepared, uint64_t dispatch_threads_x, + uint32_t group_size_x) { KYTY_PROFILER_FUNCTION(); EXIT_IF(prepared.runtime == nullptr || !*prepared.runtime); const auto& program = *prepared.runtime->program; @@ -822,8 +823,23 @@ void RenderExecutor::FindBuffers(PreparedBindings& prepared) { for (const auto resource: resources) { auto descriptor = DecodeNativeDescriptor(snapshot.buffers[resource]); const auto address = descriptor.Base48(); - const auto requested_size = descriptor.GetSize(); - if (address == 0 || requested_size == 0) { + if (address == 0u) { + prepared.buffer_sources.push_back({}); + continue; + } + auto requested_size = descriptor.GetSize(); + const auto& buffer = program.info.buffers[resource]; + if (buffer.dispatch_stride != 0u) { + if (dispatch_threads_x == 0u || dispatch_threads_x > uint64_t{UINT32_MAX} + 1u || + buffer.dispatch_stride != group_size_x) { + EXIT("GPU-sized buffer requires a known matching X dispatch extent\n"); + } + requested_size = (dispatch_threads_x - 1u) * descriptor.Stride() + buffer.max_byte_extent; + if (requested_size > uint64_t{UINT32_MAX} + 1u) { + EXIT("GPU-sized buffer dispatch wraps its 32-bit byte address\n"); + } + } + if (requested_size == 0) { prepared.buffer_sources.push_back({}); continue; } diff --git a/src/graphics/host_gpu/renderer/render.h b/src/graphics/host_gpu/renderer/render.h index b8db5b081..71e83be67 100644 --- a/src/graphics/host_gpu/renderer/render.h +++ b/src/graphics/host_gpu/renderer/render.h @@ -161,7 +161,9 @@ public: uint32_t mode); void PrepareBindings(const ShaderStageRuntime& runtime, PreparedBindings& prepared); - void FindBuffers(PreparedBindings& bindings); + void FindBuffers(PreparedBindings& bindings, + uint64_t dispatch_threads_x = 0, + uint32_t group_size_x = 0); void RebindBuffers(PreparedBindings& bindings); void RebindImages(PreparedBindings& bindings); void CommitBindings(CommandBuffer& buffer, vk::PipelineBindPoint pipeline_bind_point, diff --git a/src/graphics/host_gpu/renderer/renderCompute.cpp b/src/graphics/host_gpu/renderer/renderCompute.cpp index 4f9c0673e..457184b62 100644 --- a/src/graphics/host_gpu/renderer/renderCompute.cpp +++ b/src/graphics/host_gpu/renderer/renderCompute.cpp @@ -306,7 +306,10 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer, std::copy(std::begin(input_info.dispatch_threads_num), std::end(input_info.dispatch_threads_num), bindings.shader_data.begin() + program.bindings.dispatch_thread_dword); } - FindBuffers(bindings); + const auto dispatch_threads_x = use_thread_dimensions + ? uint64_t{input_info.dispatch_threads_num[0]} + : uint64_t{thread_group_x} * input_info.threads_num[0]; + FindBuffers(bindings, dispatch_threads_x, input_info.threads_num[0]); if (program.info.uses_dma) { m_context.PrepareBda(); } diff --git a/src/graphics/shader/recompiler/ShaderRecompiler.cpp b/src/graphics/shader/recompiler/ShaderRecompiler.cpp index 9b74c9a98..a24caf876 100644 --- a/src/graphics/shader/recompiler/ShaderRecompiler.cpp +++ b/src/graphics/shader/recompiler/ShaderRecompiler.cpp @@ -641,6 +641,7 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o for (auto& inst: *block) { const auto op = inst.GetOpcode(); uint32_t first = 0; + bool native_buffer_count = false; if (op == IR::ValueOpcode::GetBufferResource) { if (std::ranges::any_of(inst.Uses(), [&](const IR::Use& use) { return ir.memory_info[use.user->Flags().index].kind == @@ -649,6 +650,8 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o continue; } const auto resource = inst.Flags(); + native_buffer_count = resource < ir.info.buffers.size() && + ir.info.buffers[resource].dispatch_stride != 0u; first = resource < ir.info.buffers.size() && ir.info.buffers[resource].indirect_root == resource ? 1u @@ -661,6 +664,7 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o continue; } for (size_t index = first; index < inst.NumArgs(); index++) { + if (native_buffer_count && index == 2u) continue; inst.SetArg(index, IR::Value(0u)); } } diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp index 66f58e480..f2e6d0068 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp @@ -506,7 +506,15 @@ void StoreLocalFlat(ValueEmitContext& ctx, const IR::Inst& inst) { void StoreWordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem, const MemoryResourceAccess& resource, uint32_t data) { const auto index = EmitMemoryElementIndex(ctx.state, resource, DwordIndex(ctx, inst, mem)); - EmitIfCondition(ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() { + auto in_bounds = EmitMemoryElementInBounds(ctx.state, resource, index); + if (mem.kind == IR::ResourceKind::Buffer && + ctx.state.program.info.buffers[mem.resource].dispatch_stride != 0u) { + const auto& handle = *inst.Arg(0).ResolveInstruction(); + in_bounds = AndCondition( + ctx.state, in_bounds, + Binary(ctx.state, spv::OpULessThan, TypeBool(ctx.state), ctx.Arg(inst, 1), ctx.Arg(handle, 2))); + } + EmitIfCondition(ctx.state, in_bounds, [&]() { StoreWordInBounds(ctx, resource, index, data); }); } diff --git a/src/graphics/shader/recompiler/ir/ShaderIR.h b/src/graphics/shader/recompiler/ir/ShaderIR.h index ea220bbef..d9a584494 100644 --- a/src/graphics/shader/recompiler/ir/ShaderIR.h +++ b/src/graphics/shader/recompiler/ir/ShaderIR.h @@ -106,6 +106,9 @@ struct BufferResource { uint32_t source = 0; uint32_t first_use_pc = 0; uint32_t max_byte_extent = 0; + // Nonzero proves index = local_x + workgroup_x * dispatch_stride. NUM_RECORDS + // stays on the GPU; host binding capacity comes from the known dispatch extent. + uint32_t dispatch_stride = 0; uint32_t packed_stride = 0; Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid; uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7); diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 72b55c1ca..e70b09f2a 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -776,8 +776,8 @@ static Value FillValue(Value value, Value guard) { // Nonnegative affine coefficients for constant, local and workgroup coordinates. Reject modular // arithmetic that could wrap; runtime coverage also bounds the largest invocation index. -static std::optional> FillIndex(Value value, uint32_t axis, Value guard, - uint32_t depth = 0) { +std::optional> AffineIndex(Value value, uint32_t axis, Value guard, + uint32_t depth) { value = FillValue(value, guard); if (depth > 32 || value.GetType() != Type::U32) { return {}; @@ -802,8 +802,8 @@ static std::optional> FillIndex(Value value, uint32_t ax op != ValueOpcode::ShiftLeftLogical32) { return {}; } - auto left = FillIndex(inst->Arg(0), axis, guard, depth + 1); - auto right = FillIndex(inst->Arg(1), axis, guard, depth + 1); + auto left = AffineIndex(inst->Arg(0), axis, guard, depth + 1); + auto right = AffineIndex(inst->Arg(1), axis, guard, depth + 1); if (!left || !right) { return {}; } @@ -870,7 +870,7 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) { const auto* address = store->Arg(1).ResolveInstruction(); if (address == nullptr || address->GetOpcode() != ValueOpcode::MakeImageAddress) return {}; for (uint32_t axis = 0; axis < 3; ++axis) { - const auto index = FillIndex(address->Arg(axis), axis, guard); + const auto index = AffineIndex(address->Arg(axis), axis, guard); if (!index || (*index)[0] != 0) return {}; if (axis < 2) { if ((*index)[1] != 1 || (*index)[2] == 0) return {}; @@ -900,7 +900,7 @@ static UniformFillPlan AnalyzeUniformFill(const Program& program) { memory.data_bits != 32 || memory.data_dwords != static_cast(store_op - stores.begin() + 1)) return {}; - const auto address = FillIndex(store->Arg(1), 0, guard); + const auto address = AffineIndex(store->Arg(1), 0, guard); if (!address || (*address)[0] != 0 || (*address)[1] != 1 || (*address)[2] == 0) return {}; result.fill.kind = UniformFillKind::Buffer; result.fill.group_stride[0] = static_cast((*address)[2]); @@ -1138,6 +1138,12 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime auto packed_stride = descriptor.PackedStride(); const auto stride = packed_stride & 0x3fffu; const bool swizzle = stride != 0u && ((packed_stride >> 14u) & 1u) != 0u; + if (base.dispatch_stride != 0u && descriptor.Base48() != 0u && + (descriptor.OutOfBounds() != 0u || stride == 0u || swizzle || + (packed_stride & (1u << 20u)) != 0u || base.max_byte_extent > stride)) { + return SpecializationFail(fmt::format( + "dispatch-sized buffer {} requires an unswizzled structured DWORD layout", i)); + } if (stride == 0u) { packed_stride &= ~((1u << 14u) | (3u << 16u)); } else if (!swizzle) { diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h index 6ad72cb36..2e2013dfb 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h @@ -43,6 +43,10 @@ struct ResourceSpecialization { // its values and is independent of the translated shader CFG. ResourcePlan ExtractResourcePlan(const Program& program); +// Nonnegative constant/local/workgroup coefficients under the instruction's active guard. +std::optional> AffineIndex(Value value, uint32_t axis, Value guard, + uint32_t depth = 0); + // Refreshes cached resources and specialization in place. A failed refresh must not be used. bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime, ResourceSnapshot& snapshot, ResourceSpecialization& specialization); diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp index c07e407e4..528dfced2 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceTracking.cpp @@ -3,6 +3,7 @@ #include "common/assert.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/recompiler/ir/passes/DeadCodeElimination.h" +#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h" #include "graphics/shader/recompiler/ir/passes/SrtWalker.h" #include @@ -1955,9 +1956,32 @@ private: } } - uint32_t AddBuffer(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) { + bool TryMakeDispatchBuffer(const Inst& inst, const Inst& handle, uint32_t& source, + uint32_t& dispatch_stride) { + const auto flags = inst.Flags(); + const auto& memory = m_program.memory_info[flags.index]; + if (m_program.stage != ShaderType::Compute || inst.GetOpcode() != ValueOpcode::StoreBufferU32 || + memory.kind != ResourceKind::Buffer || memory.typed || memory.formatted || + !memory.idxen || memory.offen || memory.data_bits != 32u || memory.data_dwords != 1u || + inst.Arg(2).Resolve() != Value(0u) || inst.Arg(3).Resolve() != Value(0u)) return false; + const auto index = AffineIndex(inst.Arg(1), 0, inst.Arg(5).Resolve()); + if (!index || (*index)[0] != 0u || (*index)[1] != 1u || (*index)[2] == 0u) return false; + DescriptorSource descriptor; + MakeSource(handle, 4u, false, false, memory.resource * 4u, descriptor, flags.pc); + // Binding capacity is derived from the dispatch. The native handle keeps its count. + descriptor.dwords[2] = Value(0u); + uint32_t bad_dword = 0; + if (!ValidateSource(descriptor, bad_dword)) return false; + source = InternSource(descriptor); + dispatch_stride = static_cast((*index)[2]); + return true; + } + + uint32_t AddBuffer(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc, + uint32_t dispatch_stride) { for (uint32_t i = 0; i < m_info.buffers.size(); i++) { - if (m_info.buffers[i].source == source) { + if (m_info.buffers[i].source == source && + m_info.buffers[i].dispatch_stride == dispatch_stride) { Merge(m_info.buffers[i], memory, op, pc); return i; } @@ -1968,6 +1992,7 @@ private: BufferResource resource; resource.source = source; resource.first_use_pc = pc; + resource.dispatch_stride = dispatch_stride; Merge(resource, memory, op, pc); m_info.buffers.push_back(resource); return static_cast(m_info.buffers.size() - 1); @@ -2123,12 +2148,14 @@ private: uint32_t resource = 0; if (buffer != BufferAccess::None) { + uint32_t dispatch_stride = 0; handle = inst.Arg(0).Resolve().TryInstruction(); const auto* indirect = handle == nullptr ? nullptr : FindIndirectDescriptor(*handle); if (indirect != nullptr) { source = indirect->source; } else if (!GetHandle(inst.Arg(0), ValueOpcode::GetBufferResource, 4, flags.pc, - memory.resource * 4u, handle, source)) { + memory.resource * 4u, handle, source) && + !TryMakeDispatchBuffer(inst, *handle, source, dispatch_stride)) { if (memory.kind != (op == ValueOpcode::ReadConstBuffer ? ResourceKind::ScalarBuffer : ResourceKind::Buffer) || !memory.SupportsIndirectBufferLoad(op)) { @@ -2140,7 +2167,7 @@ private: m_info.uses_dma = true; return; } - resource = AddBuffer(source, memory, op, flags.pc); + resource = AddBuffer(source, memory, op, flags.pc, dispatch_stride); if (resource == UINT32_MAX) { Fail(flags.pc, "buffer resource limit exceeded"); } diff --git a/tests/ResourceMaterializationTests.cpp b/tests/ResourceMaterializationTests.cpp index da4cf2780..bd2cae4ee 100644 --- a/tests/ResourceMaterializationTests.cpp +++ b/tests/ResourceMaterializationTests.cpp @@ -196,6 +196,45 @@ void TestRawScalarAddressPreservesHighBits() { } } +void TestDispatchBufferKeepsOnlyBindingLayoutOnHost() { + using namespace Libs::Graphics::ShaderRecompiler::IR; + auto plan = UserDataBufferPlan(); + plan.info.buffers[0].dispatch_stride = 64; + plan.info.buffers[0].max_byte_extent = 20; + auto &source = plan.descriptor_sources[0]; + source.dwords[1] = Value(20u << 16u); + source.dwords[3] = Value(0x16204u); + std::array user_data{0x1000u}; + uint32_t reads = 0; + const SrtRuntime runtime{.user_data = user_data, + .read_memory = RejectSpecializationRead, + .userdata = &reads, + .read_specialization_memory = RejectSpecializationRead}; + ResourceSnapshot snapshot; + ResourceSpecialization specialization; + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + snapshot.buffers[0].dwords[2] == 0 && + specialization.buffers[0].packed_stride == 20 && reads == 0, + "native record count was required to materialize its stable binding layout"); + for (const auto words : {std::array{0u, 0x16204u}, + {16u << 16u, 0x16204u}, + {(20u << 16u) | (1u << 31u), 0x16204u}, + {20u << 16u, 0x16204u | (1u << 23u)}, + {20u << 16u, 0x16204u | (1u << 28u)}}) { + source.dwords[1] = Value(words[0]); + source.dwords[3] = Value(words[1]); + Check(!MaterializeResources(plan, runtime, snapshot, specialization), + "dispatch buffer accepted a layout outside its native bounds proof"); + } + user_data[0] = 0; + source.dwords[1] = Value(0u); + source.dwords[3] = Value(0u); + Check(MaterializeResources(plan, runtime, snapshot, specialization) && + specialization.buffers[0].packed_stride == 0, + "inactive dispatch buffer did not preserve null binding semantics"); + Check(reads == 0, "dispatch buffer layout validation read GPU data on the host"); +} + void TestIntegerRuntimeValueFollowsSrtReads() { using namespace Libs::Graphics::ShaderRecompiler::IR; auto plan = SrtPlan(0x10000); @@ -488,6 +527,7 @@ void DbgExit(int) { std::abort(); } int main() { TestMappedSrtUsesDirectReaderByDefault(); TestRawScalarAddressPreservesHighBits(); + TestDispatchBufferKeepsOnlyBindingLayoutOnHost(); TestIntegerRuntimeValueFollowsSrtReads(); TestUnbasedFlatCacheHitMaterializes(); TestWrittenDescriptorUsesStrictReaderOnce(); diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 4bb03e49d..6328e62b8 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -31282,6 +31282,82 @@ void CheckComputeThreadDimensions(VulkanHarness &vulkan) { } } +void CheckGpuComputedBufferRecords(VulkanHarness &vulkan) { + using namespace ShaderRecompiler::IR; + TestCase test; + test.name = "GpuComputedBufferRecords"; + // Captured b71ef7c579835a0d: read a GPU counter, divide it into chunks, + // construct a structured descriptor, then clear each live record's last DWORD. + test.code = { + 0xbfa00003u, 0xd7460000u, 0x04010c0eu, 0x8805ff0du, 0x00040000u, 0xbe84030cu, + 0xbe860381u, 0xbe8703ffu, 0x00016204u, 0xb06a0100u, 0xe0300000u, 0x80010400u, + 0x92049281u, 0x7e0a026au, 0x996a9480u, 0xb0062000u, 0xb00a0080u, 0x380212f9u, + 0x8686066au, 0x7e040208u, 0x7e0602ffu, 0x00016204u, 0xbf8c3f70u, 0x7d8808ffu, + 0x00000200u, 0x7d8808f9u, 0x06868404u, 0x7d8808f9u, 0x06868606u, 0x020d80f9u, + 0x8686060au, 0xd5010005u, 0x00120affu, 0x00000200u, 0xd5010006u, 0x001a0d05u, + 0x7e0a0d06u, 0x7e0c0506u, 0xd76d0004u, 0x041a08c1u, 0x7e0a5705u, 0x81ea0680u, + 0x7e080504u, 0x100a0affu, 0x4f7ffffeu, 0x7e0a0f05u, 0x7ed60505u, 0x936a6b6au, + 0x9aea6b6au, 0x816a6a6bu, 0x9a87046au, 0x936a0607u, 0x4e08086au, 0x4e0a0806u, + 0x7d8608f9u, 0x06868406u, 0x7d860a06u, 0x87ea046au, 0x020902f9u, 0x86860680u, + 0xd5286a04u, 0x00120807u, 0xbeea047eu, 0x7da40080u, 0xbf880003u, 0x7e0a0280u, + 0xe0700000u, 0x80000500u, 0xbefe046au, 0x7e0a0280u, 0x7e000502u, 0x7e020501u, + 0x7e040504u, 0x7e060503u, 0xe0702010u, 0x80000500u, 0xbf810000u, + }; + constexpr u32 guest_base = 0x1000; + test.user_data[0] = guest_base + 16; + test.user_data[1] = 4u << 16; + test.user_data[2] = 1; + test.user_data[3] = 0x5204; + test.user_data[8] = guest_base + 20; + test.user_data[12] = guest_base; + test.has_user_data = true; + test.storage_buffer_offsets = {0, 16, 20}; + test.initial.assign(5 + 5 * 130, 0xdeadbeefu); + test.initial[0] = 72; + test.has_compute_info = true; + test.compute_info.threads_num[0] = 64; + test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; + test.compute_info.lds_size_dwords = 0; + test.compute_info.wave_size = 64; + test.compute_info.float_mode = 0xc0; + test.compute_info.group_id[0] = true; + test.compute_info.thread_ids_num = 1; + test.compute_info.workgroup_register = 14; + test.compute_info.dispatch_thread_dimensions = true; + test.compute_info.dispatch_threads_num[0] = 2; + test.compute_info.dispatch_threads_num[1] = test.compute_info.dispatch_threads_num[2] = 1; + test.required_spirv = {"OpFDiv", "OpUMulExtended"}; + test.forbidden_spirv = {"get_bda_pointer", "flattened_srt"}; + const auto compiled = CompileCase(test, vulkan.SubgroupSize()); + Require(test.name, "native record count", + compiled.program.info.buffers.size() == 3 && + compiled.program.info.buffers[2].dispatch_stride == 64 && + compiled.resources.buffers[2].dwords[2] == 0 && + compiled.resources.specialization_reads.empty(), + "GPU count entered the host descriptor snapshot or lost its dispatch bound"); + // Reuse the same compiled shader as both GPU data and dispatch coverage change. + for (const u32 count : {0u, 72u, 512u, 8192u, 262144u}) { + for (const u32 threads : {2u, 3u, 65u}) { + test.initial[0] = count; + test.compute_info.dispatch_threads_num[0] = threads; + test.dispatch_x = (threads + 63) / 64; + const u32 chunk = count < 512 ? 64 : count < 8192 ? 128 : count < 262144 ? 256 : 512; + const u32 records = (count + chunk - 1) / chunk; + test.expected = test.initial; + test.expected[4] = 0; + for (u32 index = 0; index < std::min(threads, records); ++index) { + test.expected[9 + index * 5] = 0; + } + auto output = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size()); + vulkan.Dispatch(test, compiled, output); + const auto actual = vulkan.ReadBuffer(test.name, output, test.expected.size()); + vulkan.DestroyBuffer(&output); + CompareWords(test, "GPU records and padded invocation bounds", test.expected, actual); + } + } + std::printf("[compute] %-32s ok\n", test.name); +} + void CheckIndirectBufferStore(VulkanHarness &vulkan) { using namespace ShaderRecompiler::IR; TestCase test; @@ -38378,6 +38454,7 @@ int main(int argc, char **argv) { RunCase(&vulkan, ScalarBufferFromLoopReadlane(32)); RunCase(&vulkan, ScalarBufferFromLoopReadlane(64)); CheckIndirectBufferStore(vulkan); + CheckGpuComputedBufferRecords(vulkan); RunCase(&vulkan, BufferLoadsGpuSelectedDescriptors()); RunCase(&vulkan, BufferLoadDwordGpuSelectedDescriptors()); RunCase(&vulkan, BufferLoadDwordx3GpuSelectedDescriptors()); @@ -38953,6 +39030,7 @@ int main(int argc, char **argv) { CheckPixelParameterAliases(); CheckRectListShaders(); CheckIndirectBufferStore(vulkan); + CheckGpuComputedBufferRecords(vulkan); CheckComputeThreadDimensions(vulkan); CheckIndirectImageKeySwitch(); CheckWave64WholeWaveResults();