From ee034be4b2e58c283883ca31393bb17aaa27af8e Mon Sep 17 00:00:00 2001 From: nmzik Date: Sat, 31 Oct 2026 03:04:38 +0100 Subject: [PATCH] shader: rely on robustBufferAccess2 for storage buffer bounds --- src/graphics/host_gpu/graphicContext.h | 1 + .../renderer/pipeline/descriptors.cpp | 5 +- .../renderer/pipeline/pipelineCache.cpp | 3 +- .../presentation/window/vulkanWindow.cpp | 13 +- .../recompiler/backend/spirv/SpirvEmitter.h | 2 + .../backend/spirv/spirvEmitterInternal.h | 4 + .../backend/spirv/spirvEmitterMemory.cpp | 31 ++- .../spirv/spirvEmitterMemoryHelpers.cpp | 10 + src/graphics/shader/recompiler/ir/Serialize.h | 2 +- tests/ShaderRecompilerComputeTests.cpp | 224 +++++++++++++++++- 10 files changed, 276 insertions(+), 19 deletions(-) diff --git a/src/graphics/host_gpu/graphicContext.h b/src/graphics/host_gpu/graphicContext.h index a80af2064..551430928 100644 --- a/src/graphics/host_gpu/graphicContext.h +++ b/src/graphics/host_gpu/graphicContext.h @@ -35,6 +35,7 @@ struct GraphicContext { bool attachment_feedback_loop_enabled = false; bool provoking_vertex_last_enabled = false; bool maintenance8_enabled = false; + bool robust_buffer_access_enabled = false; bool conditional_rendering_enabled = false; bool occlusion_queries_enabled = false; bool occlusion_query_precise_enabled = false; diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index e31510d5b..9409be326 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -119,7 +119,8 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource const auto& [address, size, id] = source; if (address == 0 || size == 0) { - return {context.GetBufferCache().GetBuffer(NULL_BUFFER_ID).Handle(), 0, 16}; + // Null descriptors read zero and drop writes, with or without shader bounds checks. + return {nullptr, 0, vk::WholeSize}; } const auto& graphics = context.GetGraphics(); const auto alignment = graphics.StorageMinAlignment(); @@ -1017,7 +1018,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer, case BindingKind::Buffers: EXIT_IF(descriptors.buffers.size() != write.descriptorCount); for (const auto& view: descriptors.buffers) { - EXIT_IF(view.buffer == nullptr); + EXIT_IF(view.range == 0); } write.pBufferInfo = descriptors.buffers.data(); break; diff --git a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp index 736d99e15..215f515d7 100644 --- a/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/pipelineCache.cpp @@ -239,7 +239,8 @@ ShaderRecompiler::Spirv::Profile SpirvProfile(const GraphicContext& graphics) { // signed-zero results; GLSL.std.450 alone does not order -0 below +0. return {.maintenance8 = graphics.maintenance8_enabled, .native_float_min_max = graphics.physical_device_properties.vendorID == 0x1002u, - .device_clock_period_ps = graphics.device_clock_period_ps}; + .device_clock_period_ps = graphics.device_clock_period_ps, + .robust_buffer_access = graphics.robust_buffer_access_enabled}; } std::filesystem::path ShaderStorePath() { diff --git a/src/graphics/presentation/window/vulkanWindow.cpp b/src/graphics/presentation/window/vulkanWindow.cpp index 73bfebc27..8539d175a 100644 --- a/src/graphics/presentation/window/vulkanWindow.cpp +++ b/src/graphics/presentation/window/vulkanWindow.cpp @@ -574,8 +574,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics, vk::PhysicalDeviceVulkan11Properties properties11 {}; properties11.pNext = &subgroup_size_control; + vk::PhysicalDeviceRobustness2PropertiesEXT robustness2_properties {}; + robustness2_properties.pNext = &properties11; vk::PhysicalDeviceFloatControlsProperties float_controls {}; - float_controls.pNext = &properties11; + float_controls.pNext = &robustness2_properties; vk::PhysicalDeviceProperties2 properties2 {}; properties2.pNext = &float_controls; @@ -605,6 +607,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics, feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout && feedback_dynamic.attachmentFeedbackLoopDynamicState; graphics.maintenance8_enabled = maintenance8_extension && maintenance8.maintenance8; + // A coarser alignment rounds the range up past the guest's last byte. + graphics.robust_buffer_access_enabled = + supported_robustness2.robustBufferAccess2 && + robustness2_properties.robustStorageBufferAccessSizeAlignment == 1u; // The device clock ticks at timestampPeriod nanoseconds. if (shader_clock_extension && shader_clock.shaderDeviceClock) { graphics.device_clock_period_ps = static_cast( @@ -703,9 +709,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics, features13.subgroupSizeControl = graphics.compute_subgroup_size_control_enabled ? VK_TRUE : VK_FALSE; - LOGF("Vulkan robustness: robustImageAccess=%s robustImageAccess2=%s\n", + LOGF("Vulkan robustness: robustImageAccess=%s robustImageAccess2=%s buffer bounds=%s\n", features13.robustImageAccess == VK_TRUE ? "true" : "false", - robustness2.robustImageAccess2 == VK_TRUE ? "true" : "false"); + robustness2.robustImageAccess2 == VK_TRUE ? "true" : "false", + graphics.robust_buffer_access_enabled ? "robustBufferAccess2" : "shader checks"); vk::DeviceCreateInfo create_info {}; vk::PhysicalDeviceMeshShaderFeaturesEXT mesh_features {}; diff --git a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h index fd5d6a55d..0a576714d 100644 --- a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h +++ b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.h @@ -15,6 +15,8 @@ struct Profile { bool native_float_min_max = false; // NMin/NMax give the guest's NaN and signed-zero results. // OpReadClockKHR Device tick in picoseconds; 0 without shaderDeviceClock. uint32_t device_clock_period_ps = 0; + // Storage buffer elements past the descriptor range, to the byte, read zero and drop writes. + bool robust_buffer_access = false; }; inline constexpr Profile DefaultProfile {}; diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h index 9782aac55..bcca6ba41 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h @@ -449,6 +449,10 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& st uint32_t EmitMemoryElementInBounds(EmitterState& state, const MemoryResourceAccess& access, uint32_t index); +// EmitMemoryElementInBounds, or true for storage buffers the device bounds-checks; only +// loads and plain stores may use it. +uint32_t EmitStorageElementInBounds(EmitterState& state, const MemoryResourceAccess& access, + uint32_t index); uint32_t EmitMemoryElementPointer(EmitterState& state, const MemoryResourceAccess& access, uint32_t index); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp index a15fae156..4254b2409 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemory.cpp @@ -380,7 +380,7 @@ uint32_t LoadWordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR: const MemoryResourceAccess& resource) { const auto index = DwordIndex(ctx, inst, mem, resource.byte_offset); return EmitValueOrZeroIfCondition( - ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), + ctx.state, EmitStorageElementInBounds(ctx.state, resource, index), [&]() { return LoadWordInBounds(ctx, resource, index); }); } @@ -399,7 +399,7 @@ uint32_t LoadSubwordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state), address, ConstantU32(ctx.state, std::countr_zero(resource.element_bits / 8u))); return EmitValueOrZeroIfCondition( - ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() { + ctx.state, EmitStorageElementInBounds(ctx.state, resource, index), [&]() { return LoadSubwordInBounds(ctx, resource, address, index, bits, sign_extend); }); } @@ -561,11 +561,14 @@ void StoreSubwordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR: const auto index = Binary( ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state), address, ConstantU32(ctx.state, std::countr_zero(resource.element_bits / 8u))); - EmitIfCondition( - ctx.state, BufferIndexInBounds(ctx, inst, mem, resource, - EmitMemoryElementInBounds(ctx.state, resource, index)), [&]() { - StoreSubwordInBounds(ctx, mem, resource, address, index, bits, data); - }); + // 32-bit views merge subwords in a compare-exchange loop, and out-of-range atomics return + // undefined values. + const auto in_bounds = resource.element_bits < 32u + ? EmitStorageElementInBounds(ctx.state, resource, index) + : EmitMemoryElementInBounds(ctx.state, resource, index); + EmitIfCondition(ctx.state, BufferIndexInBounds(ctx, inst, mem, resource, in_bounds), [&]() { + StoreSubwordInBounds(ctx, mem, resource, address, index, bits, data); + }); } void StoreSubword(ValueEmitContext& ctx, const IR::Inst& inst, IR::MemoryInfo mem, uint32_t bits) { @@ -632,7 +635,7 @@ void StoreWordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me if (resource.object_pointer == 0u) return; const auto index = DwordIndex(ctx, inst, mem, resource.byte_offset); const auto in_bounds = BufferIndexInBounds(ctx, inst, mem, resource, - EmitMemoryElementInBounds(ctx.state, resource, index)); + EmitStorageElementInBounds(ctx.state, resource, index)); EmitIfCondition(ctx.state, in_bounds, [&]() { StoreWordInBounds(ctx, resource, index, data); }); @@ -1604,10 +1607,18 @@ void EmitReadConstBuffer(ValueEmitContext& ctx, const IR::Inst& inst) { } const auto access = PrepareMemoryResourceAccess(state, mem); // Scalar buffer addressing aligns the base and each offset independently. - const auto element = Binary( + auto element = Binary( state, spv::OpIAdd, TypeU32(state), index, Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.byte_offset, ConstantU32(state, 2))); - const auto condition = EmitMemoryElementInBounds(state, access, element); + // The device computes byte offsets in 32 bits, so elements from 2^30 on would wrap back into + // the range; the host prefix adds at most 63. + constexpr uint32_t last_element = UINT32_MAX >> 2u; + if (state.profile.robust_buffer_access && + (!offset.IsImmediate() || (offset.U32() >> 2u) + (mem.offset >> 2u) > last_element - 63u)) { + element = EmitGlsl(state, element, + ConstantU32(state, last_element)); + } + const auto condition = EmitStorageElementInBounds(state, access, element); ctx.Define(inst, EmitValueOrZeroIfCondition(state, condition, [&]() { return EmitNative( state, EmitMemoryElementPointer(state, access, element)); diff --git a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp index 1dd1ee6a1..d7b35e835 100644 --- a/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/spirvEmitterMemoryHelpers.cpp @@ -341,6 +341,16 @@ uint32_t EmitMemoryElementInBounds(EmitterState& state, const MemoryResourceAcce return in_bounds; } +uint32_t EmitStorageElementInBounds(EmitterState& state, const MemoryResourceAccess& access, + uint32_t index) { + // The descriptor range ends where access.length does. + if (state.profile.robust_buffer_access && (access.kind == IR::ResourceKind::Buffer || + access.kind == IR::ResourceKind::ScalarBuffer)) { + return ConstantBool(state, true); + } + return EmitMemoryElementInBounds(state, access, index); +} + uint32_t EmitMemoryElementPointer(EmitterState& state, const MemoryResourceAccess& access, uint32_t index) { if (access.kind == IR::ResourceKind::Lds || access.kind == IR::ResourceKind::Scratch) { diff --git a/src/graphics/shader/recompiler/ir/Serialize.h b/src/graphics/shader/recompiler/ir/Serialize.h index 21cc78141..504d760dc 100644 --- a/src/graphics/shader/recompiler/ir/Serialize.h +++ b/src/graphics/shader/recompiler/ir/Serialize.h @@ -174,7 +174,7 @@ KYTY_IO_FIELDS(ResourceSpecialization::Sampler, a, b, c) KYTY_IO_FIELDS(ResourceSpecialization, a, b, c) KYTY_IO_FIELDS(Bindings, a, b, c, d, e) KYTY_IO_FIELDS(CompiledShaderInfo, a, b, c, d, e, f, g, h, i, j) -KYTY_IO_FIELDS(Spirv::Profile, a, b, c) +KYTY_IO_FIELDS(Spirv::Profile, a, b, c, d) KYTY_IO_FIELDS(ShaderTessellationInputInfo, a, b, c, d, e, f, g, h, i) KYTY_IO_FIELDS(RectListParameter, a, b, c) #undef KYTY_IO_FIELDS diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 2c6f62b00..c95d562cf 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -11741,6 +11741,35 @@ public: std::printf("[gpu] %-32s ok\n", name); } + // An empty binding is a null descriptor, so loads and stores without shader checks touch no memory. + void CheckNullStorageBufferDescriptor() { + constexpr const char *name = "NullStorageBufferDescriptor"; + EnsureRuntimeContext(); + RenderContext context(m_runtime_context); + auto &executor = context.GetRenderExecutor(); + ShaderModule module; + auto &program = module.info; + program.stage = ShaderType::Compute; + program.info.buffers.push_back({.packed_stride = 1, .written = true}); + program.info.buffers[0].descriptor_index = 0; + program.bindings.descriptor_counts[0] = 1; + program.bindings.descriptor_mask = 1; + ShaderRecompiler::IR::ResourceSnapshot snapshot; + snapshot.buffers.emplace_back().dword_count = 4; + ShaderStageRuntime runtime{&module, &snapshot}; + PreparedBindings bindings; + executor.PrepareBindings(runtime, bindings); + std::array stages{&bindings}; + executor.FindBuffers(stages); + executor.RebindBuffers(bindings); + const auto &view = bindings.buffers[0]; + Require(name, "null descriptor", + view.buffer == nullptr && view.offset == 0 && view.range == vk::WholeSize && + bindings.shader_data[program.bindings.MemorySizeDword()] == 0, + "an empty storage binding was not a zero-length null descriptor"); + std::printf("[host] %-32s ok\n", name); + } + void CheckZeroStrideBufferBinding() { constexpr const char *name = "ZeroStrideBufferBinding"; constexpr uintptr_t base = 0x0000000204c00000ull; @@ -18434,6 +18463,10 @@ public: pipeline_info.sType = vk::StructureType::eComputePipelineCreateInfo; pipeline_info.stage = stage; pipeline_info.layout = pipeline_layout; + // The device leaves out-of-range storage accesses to the shader; robust profiles drop the checks. + vk::PipelineRobustnessCreateInfoEXT robustness{}; + robustness.storageBuffers = vk::PipelineRobustnessBufferBehaviorEXT::eRobustBufferAccess2; + if (test.profile->robust_buffer_access) pipeline_info.pNext = &robustness; vk::Pipeline pipeline = nullptr; RequireVk(test.name, "dispatch", m_device.createComputePipelines(nullptr, 1, &pipeline_info, @@ -22165,14 +22198,25 @@ private: available_maintenance8.pNext = &available_min_lod; vk::PhysicalDeviceShaderClockFeaturesKHR available_clock{}; available_clock.pNext = &available_maintenance8; + vk::PhysicalDevicePipelineRobustnessFeaturesEXT available_pipeline_robustness{}; + available_pipeline_robustness.pNext = &available_clock; vk::PhysicalDeviceRobustness2FeaturesEXT available_robustness2{}; - available_robustness2.pNext = &available_clock; + available_robustness2.pNext = &available_pipeline_robustness; vk::PhysicalDeviceFeatures2 available_features2{}; available_features2.sType = vk::StructureType::ePhysicalDeviceFeatures2; available_features2.pNext = &available_robustness2; m_physical_device.getFeatures2(&available_features2); + vk::PhysicalDeviceRobustness2PropertiesEXT robustness2_properties{}; + vk::PhysicalDeviceProperties2 properties2{}; + properties2.pNext = &robustness2_properties; + m_physical_device.getProperties2(&properties2); Require("VulkanHarness", "dispatch", available_robustness2.nullDescriptor == true, "null descriptors are not supported"); + Require("VulkanHarness", "dispatch", + available_robustness2.robustBufferAccess2 == true && + available_pipeline_robustness.pipelineRobustness == true && + robustness2_properties.robustStorageBufferAccessSizeAlignment == 1u, + "byte-granular per-pipeline robustBufferAccess2 is not supported"); Require("VulkanHarness", "dispatch", available_features.shaderStorageImageWriteWithoutFormat == true, "shaderStorageImageWriteWithoutFormat is not supported"); @@ -22304,9 +22348,12 @@ private: shader_clock.shaderDeviceClock = true; shader_clock.pNext = &min_lod; maintenance8.pNext = &shader_clock; + vk::PhysicalDevicePipelineRobustnessFeaturesEXT pipeline_robustness{}; + pipeline_robustness.pipelineRobustness = true; + pipeline_robustness.pNext = &maintenance8; vk::PhysicalDeviceRobustness2FeaturesEXT robustness2{}; robustness2.nullDescriptor = true; - robustness2.pNext = &maintenance8; + robustness2.pNext = &pipeline_robustness; vk::PhysicalDeviceConditionalRenderingFeaturesEXT conditional_rendering{}; conditional_rendering.conditionalRendering = true; conditional_rendering.pNext = &robustness2; @@ -22333,6 +22380,7 @@ private: VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME, VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME, VK_EXT_ROBUSTNESS_2_EXTENSION_NAME, + VK_EXT_PIPELINE_ROBUSTNESS_EXTENSION_NAME, VK_KHR_MAINTENANCE_8_EXTENSION_NAME, VK_KHR_SHADER_CLOCK_EXTENSION_NAME, VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME, @@ -33952,6 +34000,162 @@ TestCase BufferSubwordStoresAtHostOffset(u32 bytes) { return test; } +constexpr ShaderProfile RobustBufferProfile{.maintenance8 = true, .robust_buffer_access = true}; + +// A 20-byte binding ends inside each access: dwords, bytes and stores past it read zero or drop, +// whether the shader checks the bounds or the pipeline does. +TestCase BufferTailAccesses(u32 wave_size, bool robust) { + using O = ShaderOpcode; + TestCase test; + constexpr const char *names[2][2] = { + {"CheckedBufferTailAccessesWave32", "CheckedBufferTailAccessesWave64"}, + {"RobustBufferTailAccessesWave32", "RobustBufferTailAccessesWave64"}}; + test.name = names[robust][wave_size == 64]; + auto &code = test.code; + AppendVMovU32(&code, 20, 8); + code.insert(code.end(), {EncodeMubuf0(0x0eu), EncodeMubuf1(0, 0, 20), + EncodeSmem0(0x0au, 24), EncodeSmem1(8, 125)}); + for (const auto [opcode, dst, offset] : {std::array{0x0au, 4u, 18u}, std::array{0x08u, 5u, 19u}, + std::array{0x0au, 6u, 20u}}) { + AppendVMovU32(&code, 21, offset); + code.insert(code.end(), {EncodeMubuf0(opcode), EncodeMubuf1(dst, 0, 21)}); + } + for (u32 i = 0; i < 4; ++i) AppendVMovLiteral(&code, 8 + i, 0x50000000u + i); + AppendVMovU32(&code, 22, 12); + code.insert(code.end(), {EncodeMubuf0(0x1eu), EncodeMubuf1(8, 0, 22)}); + AppendVMovLiteral(&code, 12, 0x123456eeu); + for (const auto [opcode, offset] : {std::array{0x18u, 4u}, std::array{0x1au, 20u}, + std::array{0x18u, 21u}}) { + AppendVMovU32(&code, 22, offset); + code.insert(code.end(), {EncodeMubuf0(opcode), EncodeMubuf1(12, 0, 22)}); + } + for (u32 i = 0; i < 7; ++i) AppendStoreVgpr(&code, i, i); + for (u32 i = 0; i < 4; ++i) AppendStoreSgpr(&code, 24 + i, 7 + i); + AppendEnd(&code); + + test.initial = {0x10101010u, 0x11111111u, 0x22222222u, 0x33333333u, 0x44332211u, + 0xc5c5c5c5u, 0xc6c6c6c6u, 0xc7c7c7c7u}; + test.initial.resize(8 + 16); + test.expected = test.initial; + test.expected[1] = 0x111111eeu; + test.expected[3] = 0x50000000u; + test.expected[4] = 0x50000001u; + const std::array outputs{0x22222222u, 0x33333333u, 0x44332211u, 0, 0x4433u, 0x44u, 0, + 0x22222222u, 0x33333333u, 0x44332211u, 0}; + std::copy(outputs.begin(), outputs.end(), test.expected.begin() + 8); + test.user_data = MakeStructuredStorageBufferData(0, 20); + test.user_data[50] = 64; + test.user_data[51] = test.user_data[3]; + test.has_user_data = true; + test.storage_buffer_ranges = {{0, 20}, {32, 64}}; + // A full wave needs no lane compare, so OpULessThan comes only from bounds checks. + test.compute_info.threads_num[0] = wave_size; + test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; + test.compute_info.wave_size = static_cast(wave_size); + test.has_compute_info = true; + // Robust pipelines bound these accesses themselves; the checked shader compares every one. + if (robust) { + test.profile = &RobustBufferProfile; + test.forbidden_spirv = {"OpULessThan"}; + } else { + test.required_spirv = {"OpULessThan"}; + } + test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORDX4, O::S_BUFFER_LOAD_DWORDX4, + O::BUFFER_LOAD_USHORT, O::BUFFER_LOAD_UBYTE, O::BUFFER_STORE_DWORDX4, + O::BUFFER_STORE_BYTE, O::BUFFER_STORE_SHORT, O::BUFFER_STORE_DWORD, + O::S_ENDPGM}; + return test; +} + +// Robust pipelines leave out-of-range atomic results undefined, so atomics keep their checks. +TestCase RobustBufferAtomicOutOfRange() { + using O = ShaderOpcode; + TestCase test; + test.name = "RobustBufferAtomicOutOfRange"; + auto &code = test.code; + AppendVMovU32(&code, 1, 5); + for (const auto [dst, offset] : {std::array{0u, 16u}, std::array{2u, 20u}}) { + AppendVMovU32(&code, 21, offset); + code.insert(code.end(), {EncodeVop1(0x01, dst, Vgpr(1)), + EncodeMubuf0(0x32u, 0, false, true, true), EncodeMubuf1(dst, 0, 21)}); + } + AppendStoreVgpr(&code, 0, 0); + AppendStoreVgpr(&code, 2, 1); + AppendEnd(&code); + test.initial = {0x10u, 0x11u, 0x12u, 0x13u, 0x14u, 0xc5u}; + test.initial.resize(8 + 16); + test.expected = test.initial; + test.expected[4] = 0x19u; + test.expected[8] = 0x14u; + test.expected[9] = 0; + test.user_data = MakeStructuredStorageBufferData(0, 20); + test.user_data[50] = 64; + test.user_data[51] = test.user_data[3]; + test.has_user_data = true; + test.storage_buffer_ranges = {{0, 20}, {32, 64}}; + // One wave32 thread needs no lane compare, so OpULessThan comes only from bounds checks. + test.compute_info.threads_num[0] = test.compute_info.threads_num[1] = + test.compute_info.threads_num[2] = 1; + test.compute_info.wave_size = 32; + test.has_compute_info = true; + test.profile = &RobustBufferProfile; + test.required_spirv = {"OpULessThan"}; + test.opcodes = {O::V_MOV_B32, O::BUFFER_ATOMIC_ADD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + return test; +} + +// Guest offsets near 2^32 index past the end of every view: they read zero and drop stores +// instead of wrapping onto the bytes before the descriptor offset or onto the host prefix. +TestCase RobustBufferWrappedAddresses(u32 prefix) { + using O = ShaderOpcode; + TestCase test; + test.name = prefix == 0 ? "RobustBufferWrappedAddresses" : "RobustBufferWrappedAddressesOffset"; + auto &code = test.code; + for (const auto [opcode, dst, offset] : + {std::array{0x0cu, 0u, 0xfffffffcu}, std::array{0x0au, 1u, 0xfffffffeu}, + std::array{0x08u, 2u, 0xffffffffu}}) { + AppendVMovLiteral(&code, 20, offset); + code.insert(code.end(), {EncodeMubuf0(opcode), EncodeMubuf1(dst, 0, 20)}); + } + // s32 holds a runtime offset, s30 one the recompiler folds. + AppendSMovLiteral(&code, 30, 0xfffffffcu); + code.insert(code.end(), {EncodeSmem0(0x08u, 24), EncodeSmem1(0, 32), EncodeSmem0(0x08u, 25), + EncodeSmem1(0, 30)}); + AppendVMovLiteral(&code, 8, 0x5a5a5a5au); + for (const auto [opcode, offset] : {std::array{0x1cu, 0xfffffffcu}, + std::array{0x1au, 0xfffffffeu}, + std::array{0x18u, 0xffffffffu}}) { + AppendVMovLiteral(&code, 20, offset); + code.insert(code.end(), {EncodeMubuf0(opcode), EncodeMubuf1(8, 0, 20)}); + } + for (u32 i = 0; i < 3; ++i) AppendStoreVgpr(&code, i, i); + for (u32 i = 0; i < 2; ++i) AppendStoreSgpr(&code, 24 + i, 3 + i); + AppendEnd(&code); + + // Outputs fill dwords 0-4 and guest bytes start at dword 16, so a 32-bit wrap would land on + // dword 15 or 16. + test.initial.resize(24); + for (u32 i = 0; i < test.initial.size(); ++i) test.initial[i] = 0x01010101u * (i + 1); + test.expected = test.initial; + std::fill_n(test.expected.begin(), 5, 0u); + test.user_data = MakeStructuredStorageBufferData(0, 16); + test.user_data[32] = 0xfffffffcu; + test.user_data[50] = 32; + test.user_data[51] = test.user_data[3]; + test.has_user_data = true; + test.storage_buffer_offsets = {prefix}; + test.storage_buffer_ranges = {{64, 16 + prefix}, {0, 32}}; + test.compute_info.threads_num[0] = test.compute_info.threads_num[1] = + test.compute_info.threads_num[2] = 1; + test.compute_info.wave_size = 32; + test.has_compute_info = true; + test.profile = &RobustBufferProfile; + test.opcodes = {O::V_MOV_B32, O::S_MOV_B32, O::BUFFER_LOAD_DWORD, O::BUFFER_LOAD_USHORT, + O::BUFFER_LOAD_UBYTE, O::S_BUFFER_LOAD_DWORD, O::BUFFER_STORE_DWORD, + O::BUFFER_STORE_SHORT, O::BUFFER_STORE_BYTE, O::S_ENDPGM}; + return test; +} + TestCase BufferLoadDwordx4SnapshotsOverlappingAddress() { using O = ShaderOpcode; @@ -46826,6 +47030,11 @@ std::vector MakeCases() { AddCase(BufferLoadDwordIdxenUsesDescriptorStride); AddCase(BufferStoreDwordIdxenUsesDescriptorStride); AddCase(BufferStoreDwordAppliesHostOffset); + for (const u32 wave : {32u, 64u}) { + for (const bool robust : {false, true}) cases.push_back(BufferTailAccesses(wave, robust)); + } + for (const u32 prefix : {0u, 4u}) cases.push_back(RobustBufferWrappedAddresses(prefix)); + AddCase(RobustBufferAtomicOutOfRange); cases.push_back(BufferOffsetsUsePackedWords(false)); cases.push_back(BufferOffsetsUsePackedWords(true)); AddCase(BufferLoadVariants); @@ -53785,6 +53994,16 @@ int main(int argc, char **argv) { vulkan.CheckDescriptorHeapLargeSet(); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--robust-buffer-only") == 0) { + VulkanHarness vulkan; + vulkan.CheckNullStorageBufferDescriptor(); + for (const u32 wave : {32u, 64u}) { + for (const bool robust : {false, true}) RunCase(&vulkan, BufferTailAccesses(wave, robust)); + } + for (const u32 prefix : {0u, 4u}) RunCase(&vulkan, RobustBufferWrappedAddresses(prefix)); + RunCase(&vulkan, RobustBufferAtomicOutOfRange()); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--push-constant-bank-only") == 0) { VulkanHarness vulkan; vulkan.CheckGraphicsPushConstantBank(); @@ -54432,6 +54651,7 @@ int main(int argc, char **argv) { CheckShaderStore(vulkan); vulkan.CheckOrderedDispatch(); vulkan.CheckZeroStrideBufferBinding(); + vulkan.CheckNullStorageBufferDescriptor(); vulkan.CheckNullTextureDescriptors(); vulkan.CheckBdaPageTableUploads(); vulkan.CheckBdaSyncUploads();