mirror of
https://github.com/KytyPS5/KytyPS5.git
synced 2026-10-31 09:28:29 +00:00
shader: rely on robustBufferAccess2 for storage buffer bounds
This commit is contained in:
1 parent
ec746622c2
commit
ee034be4b2
10 files changed
+274
-17
No files matched your search
@@ -35,6 +35,7 @@ struct GraphicContext {
|
|||||||
bool attachment_feedback_loop_enabled = false;
|
bool attachment_feedback_loop_enabled = false;
|
||||||
bool provoking_vertex_last_enabled = false;
|
bool provoking_vertex_last_enabled = false;
|
||||||
bool maintenance8_enabled = false;
|
bool maintenance8_enabled = false;
|
||||||
|
bool robust_buffer_access_enabled = false;
|
||||||
bool conditional_rendering_enabled = false;
|
bool conditional_rendering_enabled = false;
|
||||||
bool occlusion_queries_enabled = false;
|
bool occlusion_queries_enabled = false;
|
||||||
bool occlusion_query_precise_enabled = false;
|
bool occlusion_query_precise_enabled = false;
|
||||||
|
|||||||
@@ -119,7 +119,8 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
|
|||||||
|
|
||||||
const auto& [address, size, id] = source;
|
const auto& [address, size, id] = source;
|
||||||
if (address == 0 || size == 0) {
|
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& graphics = context.GetGraphics();
|
||||||
const auto alignment = graphics.StorageMinAlignment();
|
const auto alignment = graphics.StorageMinAlignment();
|
||||||
@@ -1017,7 +1018,7 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
|
|||||||
case BindingKind::Buffers:
|
case BindingKind::Buffers:
|
||||||
EXIT_IF(descriptors.buffers.size() != write.descriptorCount);
|
EXIT_IF(descriptors.buffers.size() != write.descriptorCount);
|
||||||
for (const auto& view: descriptors.buffers) {
|
for (const auto& view: descriptors.buffers) {
|
||||||
EXIT_IF(view.buffer == nullptr);
|
EXIT_IF(view.range == 0);
|
||||||
}
|
}
|
||||||
write.pBufferInfo = descriptors.buffers.data();
|
write.pBufferInfo = descriptors.buffers.data();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -239,7 +239,8 @@ ShaderRecompiler::Spirv::Profile SpirvProfile(const GraphicContext& graphics) {
|
|||||||
// signed-zero results; GLSL.std.450 alone does not order -0 below +0.
|
// signed-zero results; GLSL.std.450 alone does not order -0 below +0.
|
||||||
return {.maintenance8 = graphics.maintenance8_enabled,
|
return {.maintenance8 = graphics.maintenance8_enabled,
|
||||||
.native_float_min_max = graphics.physical_device_properties.vendorID == 0x1002u,
|
.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() {
|
std::filesystem::path ShaderStorePath() {
|
||||||
|
|||||||
@@ -574,8 +574,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
|||||||
vk::PhysicalDeviceVulkan11Properties properties11 {};
|
vk::PhysicalDeviceVulkan11Properties properties11 {};
|
||||||
properties11.pNext = &subgroup_size_control;
|
properties11.pNext = &subgroup_size_control;
|
||||||
|
|
||||||
|
vk::PhysicalDeviceRobustness2PropertiesEXT robustness2_properties {};
|
||||||
|
robustness2_properties.pNext = &properties11;
|
||||||
vk::PhysicalDeviceFloatControlsProperties float_controls {};
|
vk::PhysicalDeviceFloatControlsProperties float_controls {};
|
||||||
float_controls.pNext = &properties11;
|
float_controls.pNext = &robustness2_properties;
|
||||||
vk::PhysicalDeviceProperties2 properties2 {};
|
vk::PhysicalDeviceProperties2 properties2 {};
|
||||||
properties2.pNext = &float_controls;
|
properties2.pNext = &float_controls;
|
||||||
|
|
||||||
@@ -605,6 +607,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
|||||||
feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout &&
|
feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout &&
|
||||||
feedback_dynamic.attachmentFeedbackLoopDynamicState;
|
feedback_dynamic.attachmentFeedbackLoopDynamicState;
|
||||||
graphics.maintenance8_enabled = maintenance8_extension && maintenance8.maintenance8;
|
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.
|
// The device clock ticks at timestampPeriod nanoseconds.
|
||||||
if (shader_clock_extension && shader_clock.shaderDeviceClock) {
|
if (shader_clock_extension && shader_clock.shaderDeviceClock) {
|
||||||
graphics.device_clock_period_ps = static_cast<uint32_t>(
|
graphics.device_clock_period_ps = static_cast<uint32_t>(
|
||||||
@@ -703,9 +709,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
|
|||||||
features13.subgroupSizeControl =
|
features13.subgroupSizeControl =
|
||||||
graphics.compute_subgroup_size_control_enabled ? VK_TRUE : VK_FALSE;
|
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",
|
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::DeviceCreateInfo create_info {};
|
||||||
vk::PhysicalDeviceMeshShaderFeaturesEXT mesh_features {};
|
vk::PhysicalDeviceMeshShaderFeaturesEXT mesh_features {};
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ struct Profile {
|
|||||||
bool native_float_min_max = false; // NMin/NMax give the guest's NaN and signed-zero results.
|
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.
|
// OpReadClockKHR Device tick in picoseconds; 0 without shaderDeviceClock.
|
||||||
uint32_t device_clock_period_ps = 0;
|
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 {};
|
inline constexpr Profile DefaultProfile {};
|
||||||
|
|||||||
@@ -449,6 +449,10 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& st
|
|||||||
|
|
||||||
uint32_t EmitMemoryElementInBounds(EmitterState& state, const MemoryResourceAccess& access,
|
uint32_t EmitMemoryElementInBounds(EmitterState& state, const MemoryResourceAccess& access,
|
||||||
uint32_t index);
|
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 EmitMemoryElementPointer(EmitterState& state, const MemoryResourceAccess& access,
|
||||||
uint32_t index);
|
uint32_t index);
|
||||||
|
|||||||
@@ -380,7 +380,7 @@ uint32_t LoadWordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR:
|
|||||||
const MemoryResourceAccess& resource) {
|
const MemoryResourceAccess& resource) {
|
||||||
const auto index = DwordIndex(ctx, inst, mem, resource.byte_offset);
|
const auto index = DwordIndex(ctx, inst, mem, resource.byte_offset);
|
||||||
return EmitValueOrZeroIfCondition(
|
return EmitValueOrZeroIfCondition(
|
||||||
ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index),
|
ctx.state, EmitStorageElementInBounds(ctx.state, resource, index),
|
||||||
[&]() { return LoadWordInBounds(ctx, 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,
|
ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state), address,
|
||||||
ConstantU32(ctx.state, std::countr_zero(resource.element_bits / 8u)));
|
ConstantU32(ctx.state, std::countr_zero(resource.element_bits / 8u)));
|
||||||
return EmitValueOrZeroIfCondition(
|
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);
|
return LoadSubwordInBounds(ctx, resource, address, index, bits, sign_extend);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -561,9 +561,12 @@ void StoreSubwordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR:
|
|||||||
const auto index = Binary(
|
const auto index = Binary(
|
||||||
ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state), address,
|
ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state), address,
|
||||||
ConstantU32(ctx.state, std::countr_zero(resource.element_bits / 8u)));
|
ConstantU32(ctx.state, std::countr_zero(resource.element_bits / 8u)));
|
||||||
EmitIfCondition(
|
// 32-bit views merge subwords in a compare-exchange loop, and out-of-range atomics return
|
||||||
ctx.state, BufferIndexInBounds(ctx, inst, mem, resource,
|
// undefined values.
|
||||||
EmitMemoryElementInBounds(ctx.state, resource, index)), [&]() {
|
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);
|
StoreSubwordInBounds(ctx, mem, resource, address, index, bits, data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -632,7 +635,7 @@ void StoreWordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me
|
|||||||
if (resource.object_pointer == 0u) return;
|
if (resource.object_pointer == 0u) return;
|
||||||
const auto index = DwordIndex(ctx, inst, mem, resource.byte_offset);
|
const auto index = DwordIndex(ctx, inst, mem, resource.byte_offset);
|
||||||
const auto in_bounds = BufferIndexInBounds(ctx, inst, mem, resource,
|
const auto in_bounds = BufferIndexInBounds(ctx, inst, mem, resource,
|
||||||
EmitMemoryElementInBounds(ctx.state, resource, index));
|
EmitStorageElementInBounds(ctx.state, resource, index));
|
||||||
EmitIfCondition(ctx.state, in_bounds, [&]() {
|
EmitIfCondition(ctx.state, in_bounds, [&]() {
|
||||||
StoreWordInBounds(ctx, resource, index, data);
|
StoreWordInBounds(ctx, resource, index, data);
|
||||||
});
|
});
|
||||||
@@ -1604,10 +1607,18 @@ void EmitReadConstBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
|
|||||||
}
|
}
|
||||||
const auto access = PrepareMemoryResourceAccess(state, mem);
|
const auto access = PrepareMemoryResourceAccess(state, mem);
|
||||||
// Scalar buffer addressing aligns the base and each offset independently.
|
// Scalar buffer addressing aligns the base and each offset independently.
|
||||||
const auto element = Binary(
|
auto element = Binary(
|
||||||
state, spv::OpIAdd, TypeU32(state), index,
|
state, spv::OpIAdd, TypeU32(state), index,
|
||||||
Binary(state, spv::OpShiftRightLogical, TypeU32(state), access.byte_offset, ConstantU32(state, 2)));
|
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<GLSLstd450UMin, IR::Type::U32>(state, element,
|
||||||
|
ConstantU32(state, last_element));
|
||||||
|
}
|
||||||
|
const auto condition = EmitStorageElementInBounds(state, access, element);
|
||||||
ctx.Define(inst, EmitValueOrZeroIfCondition(state, condition, [&]() {
|
ctx.Define(inst, EmitValueOrZeroIfCondition(state, condition, [&]() {
|
||||||
return EmitNative<spv::OpLoad, IR::Type::U32>(
|
return EmitNative<spv::OpLoad, IR::Type::U32>(
|
||||||
state, EmitMemoryElementPointer(state, access, element));
|
state, EmitMemoryElementPointer(state, access, element));
|
||||||
|
|||||||
@@ -341,6 +341,16 @@ uint32_t EmitMemoryElementInBounds(EmitterState& state, const MemoryResourceAcce
|
|||||||
return in_bounds;
|
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 EmitMemoryElementPointer(EmitterState& state, const MemoryResourceAccess& access,
|
||||||
uint32_t index) {
|
uint32_t index) {
|
||||||
if (access.kind == IR::ResourceKind::Lds || access.kind == IR::ResourceKind::Scratch) {
|
if (access.kind == IR::ResourceKind::Lds || access.kind == IR::ResourceKind::Scratch) {
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ KYTY_IO_FIELDS(ResourceSpecialization::Sampler, a, b, c)
|
|||||||
KYTY_IO_FIELDS(ResourceSpecialization, a, b, c)
|
KYTY_IO_FIELDS(ResourceSpecialization, a, b, c)
|
||||||
KYTY_IO_FIELDS(Bindings, a, b, c, d, e)
|
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(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(ShaderTessellationInputInfo, a, b, c, d, e, f, g, h, i)
|
||||||
KYTY_IO_FIELDS(RectListParameter, a, b, c)
|
KYTY_IO_FIELDS(RectListParameter, a, b, c)
|
||||||
#undef KYTY_IO_FIELDS
|
#undef KYTY_IO_FIELDS
|
||||||
|
|||||||
@@ -11741,6 +11741,35 @@ public:
|
|||||||
std::printf("[gpu] %-32s ok\n", name);
|
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<PreparedBindings *, 1> 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() {
|
void CheckZeroStrideBufferBinding() {
|
||||||
constexpr const char *name = "ZeroStrideBufferBinding";
|
constexpr const char *name = "ZeroStrideBufferBinding";
|
||||||
constexpr uintptr_t base = 0x0000000204c00000ull;
|
constexpr uintptr_t base = 0x0000000204c00000ull;
|
||||||
@@ -18434,6 +18463,10 @@ public:
|
|||||||
pipeline_info.sType = vk::StructureType::eComputePipelineCreateInfo;
|
pipeline_info.sType = vk::StructureType::eComputePipelineCreateInfo;
|
||||||
pipeline_info.stage = stage;
|
pipeline_info.stage = stage;
|
||||||
pipeline_info.layout = pipeline_layout;
|
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;
|
vk::Pipeline pipeline = nullptr;
|
||||||
RequireVk(test.name, "dispatch",
|
RequireVk(test.name, "dispatch",
|
||||||
m_device.createComputePipelines(nullptr, 1, &pipeline_info,
|
m_device.createComputePipelines(nullptr, 1, &pipeline_info,
|
||||||
@@ -22165,14 +22198,25 @@ private:
|
|||||||
available_maintenance8.pNext = &available_min_lod;
|
available_maintenance8.pNext = &available_min_lod;
|
||||||
vk::PhysicalDeviceShaderClockFeaturesKHR available_clock{};
|
vk::PhysicalDeviceShaderClockFeaturesKHR available_clock{};
|
||||||
available_clock.pNext = &available_maintenance8;
|
available_clock.pNext = &available_maintenance8;
|
||||||
|
vk::PhysicalDevicePipelineRobustnessFeaturesEXT available_pipeline_robustness{};
|
||||||
|
available_pipeline_robustness.pNext = &available_clock;
|
||||||
vk::PhysicalDeviceRobustness2FeaturesEXT available_robustness2{};
|
vk::PhysicalDeviceRobustness2FeaturesEXT available_robustness2{};
|
||||||
available_robustness2.pNext = &available_clock;
|
available_robustness2.pNext = &available_pipeline_robustness;
|
||||||
vk::PhysicalDeviceFeatures2 available_features2{};
|
vk::PhysicalDeviceFeatures2 available_features2{};
|
||||||
available_features2.sType = vk::StructureType::ePhysicalDeviceFeatures2;
|
available_features2.sType = vk::StructureType::ePhysicalDeviceFeatures2;
|
||||||
available_features2.pNext = &available_robustness2;
|
available_features2.pNext = &available_robustness2;
|
||||||
m_physical_device.getFeatures2(&available_features2);
|
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,
|
Require("VulkanHarness", "dispatch", available_robustness2.nullDescriptor == true,
|
||||||
"null descriptors are not supported");
|
"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",
|
Require("VulkanHarness", "dispatch",
|
||||||
available_features.shaderStorageImageWriteWithoutFormat == true,
|
available_features.shaderStorageImageWriteWithoutFormat == true,
|
||||||
"shaderStorageImageWriteWithoutFormat is not supported");
|
"shaderStorageImageWriteWithoutFormat is not supported");
|
||||||
@@ -22304,9 +22348,12 @@ private:
|
|||||||
shader_clock.shaderDeviceClock = true;
|
shader_clock.shaderDeviceClock = true;
|
||||||
shader_clock.pNext = &min_lod;
|
shader_clock.pNext = &min_lod;
|
||||||
maintenance8.pNext = &shader_clock;
|
maintenance8.pNext = &shader_clock;
|
||||||
|
vk::PhysicalDevicePipelineRobustnessFeaturesEXT pipeline_robustness{};
|
||||||
|
pipeline_robustness.pipelineRobustness = true;
|
||||||
|
pipeline_robustness.pNext = &maintenance8;
|
||||||
vk::PhysicalDeviceRobustness2FeaturesEXT robustness2{};
|
vk::PhysicalDeviceRobustness2FeaturesEXT robustness2{};
|
||||||
robustness2.nullDescriptor = true;
|
robustness2.nullDescriptor = true;
|
||||||
robustness2.pNext = &maintenance8;
|
robustness2.pNext = &pipeline_robustness;
|
||||||
vk::PhysicalDeviceConditionalRenderingFeaturesEXT conditional_rendering{};
|
vk::PhysicalDeviceConditionalRenderingFeaturesEXT conditional_rendering{};
|
||||||
conditional_rendering.conditionalRendering = true;
|
conditional_rendering.conditionalRendering = true;
|
||||||
conditional_rendering.pNext = &robustness2;
|
conditional_rendering.pNext = &robustness2;
|
||||||
@@ -22333,6 +22380,7 @@ private:
|
|||||||
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
|
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
|
||||||
VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME,
|
VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME,
|
||||||
VK_EXT_ROBUSTNESS_2_EXTENSION_NAME,
|
VK_EXT_ROBUSTNESS_2_EXTENSION_NAME,
|
||||||
|
VK_EXT_PIPELINE_ROBUSTNESS_EXTENSION_NAME,
|
||||||
VK_KHR_MAINTENANCE_8_EXTENSION_NAME,
|
VK_KHR_MAINTENANCE_8_EXTENSION_NAME,
|
||||||
VK_KHR_SHADER_CLOCK_EXTENSION_NAME,
|
VK_KHR_SHADER_CLOCK_EXTENSION_NAME,
|
||||||
VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME,
|
VK_EXT_IMAGE_VIEW_MIN_LOD_EXTENSION_NAME,
|
||||||
@@ -33952,6 +34000,162 @@ TestCase BufferSubwordStoresAtHostOffset(u32 bytes) {
|
|||||||
return test;
|
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<u32, 11> 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<uint8_t>(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() {
|
TestCase BufferLoadDwordx4SnapshotsOverlappingAddress() {
|
||||||
using O = ShaderOpcode;
|
using O = ShaderOpcode;
|
||||||
|
|
||||||
@@ -46826,6 +47030,11 @@ std::vector<TestCase> MakeCases() {
|
|||||||
AddCase(BufferLoadDwordIdxenUsesDescriptorStride);
|
AddCase(BufferLoadDwordIdxenUsesDescriptorStride);
|
||||||
AddCase(BufferStoreDwordIdxenUsesDescriptorStride);
|
AddCase(BufferStoreDwordIdxenUsesDescriptorStride);
|
||||||
AddCase(BufferStoreDwordAppliesHostOffset);
|
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(false));
|
||||||
cases.push_back(BufferOffsetsUsePackedWords(true));
|
cases.push_back(BufferOffsetsUsePackedWords(true));
|
||||||
AddCase(BufferLoadVariants);
|
AddCase(BufferLoadVariants);
|
||||||
@@ -53785,6 +53994,16 @@ int main(int argc, char **argv) {
|
|||||||
vulkan.CheckDescriptorHeapLargeSet();
|
vulkan.CheckDescriptorHeapLargeSet();
|
||||||
return 0;
|
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) {
|
if (argc == 2 && std::strcmp(argv[1], "--push-constant-bank-only") == 0) {
|
||||||
VulkanHarness vulkan;
|
VulkanHarness vulkan;
|
||||||
vulkan.CheckGraphicsPushConstantBank();
|
vulkan.CheckGraphicsPushConstantBank();
|
||||||
@@ -54432,6 +54651,7 @@ int main(int argc, char **argv) {
|
|||||||
CheckShaderStore(vulkan);
|
CheckShaderStore(vulkan);
|
||||||
vulkan.CheckOrderedDispatch();
|
vulkan.CheckOrderedDispatch();
|
||||||
vulkan.CheckZeroStrideBufferBinding();
|
vulkan.CheckZeroStrideBufferBinding();
|
||||||
|
vulkan.CheckNullStorageBufferDescriptor();
|
||||||
vulkan.CheckNullTextureDescriptors();
|
vulkan.CheckNullTextureDescriptors();
|
||||||
vulkan.CheckBdaPageTableUploads();
|
vulkan.CheckBdaPageTableUploads();
|
||||||
vulkan.CheckBdaSyncUploads();
|
vulkan.CheckBdaSyncUploads();
|
||||||
|
|||||||
Reference in new issue
Block a user