blob: 25b89979e2bdd1ad8694d960488933a5165913c6 [file]
/* Copyright (c) 2024-2026 LunarG, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "containers/container_utils.h"
#include "gpuav/core/gpuav.h"
#include "gpuav/resources/gpuav_state_trackers.h"
#include "gpuav/resources/gpuav_vulkan_objects.h"
#include "gpuav/shaders/gpuav_error_codes.h"
#include "gpuav/shaders/gpuav_error_header.h"
#include "gpuav/shaders/gpuav_shaders_constants.h"
#include "gpuav/spirv/vertex_attribute_fetch_oob_pass.h"
#include "state_tracker/shader_module.h"
#include "utils/action_command_utils.h"
#include "utils/image_utils.h"
namespace gpuav {
struct VertexAttributeFetchLimit {
// Default value indicates that no vertex buffer attribute fetching will be OOB
VkDeviceSize max_vertex_attributes_count = std::numeric_limits<VkDeviceSize>::max();
vvl::VertexBufferBinding binding_info{};
VkVertexInputAttributeDescription attribute{};
uint32_t instance_rate_divisor = vvl::kNoIndex32;
};
// Computes vertex attributes fetching limits based on the set of bound vertex buffers.
// Used to detect out of bounds indices in index buffers.
static std::pair<std::optional<VertexAttributeFetchLimit>, std::optional<VertexAttributeFetchLimit>> GetVertexAttributeFetchLimits(
const vvl::CommandBuffer& cb_state) {
const LastBound& last_bound = cb_state.GetLastBoundGraphics();
const vvl::Pipeline* pipeline_state = last_bound.pipeline_state;
const bool dynamic_vertex_input = last_bound.IsDynamic(CB_DYNAMIC_STATE_VERTEX_INPUT_EXT);
const auto& vertex_binding_descriptions =
dynamic_vertex_input ? cb_state.dynamic_state_value.vertex_bindings : pipeline_state->vertex_input_state->bindings;
std::optional<VertexAttributeFetchLimit> vertex_attribute_fetch_limit_vertex_input_rate;
std::optional<VertexAttributeFetchLimit> vertex_attribute_fetch_limit_instance_input_rate;
small_vector<uint32_t, 32> vertex_shader_used_locations;
{
const ::spirv::EntryPoint* vertex_entry_point = last_bound.GetVertexEntryPoint();
if (!vertex_entry_point) {
return {std::optional<VertexAttributeFetchLimit>{}, std::optional<VertexAttributeFetchLimit>{}};
}
for (const ::spirv::StageInterfaceVariable& interface_var : vertex_entry_point->stage_interface_variables) {
for (const ::spirv::InterfaceSlot& interface_slot : interface_var.interface_slots) {
const uint32_t location = interface_slot.Location();
if (std::find(vertex_shader_used_locations.begin(), vertex_shader_used_locations.end(), location) ==
vertex_shader_used_locations.end()) {
vertex_shader_used_locations.emplace_back(location);
}
}
}
}
for (const auto& [binding, vertex_binding_desc] : vertex_binding_descriptions) {
const vvl::VertexBufferBinding* vbb = vvl::Find(cb_state.current_vertex_buffer_binding_info, binding);
if (!vbb) {
// Validation error
continue;
}
for (const auto& [location, attrib] : vertex_binding_desc.locations) {
if (std::find(vertex_shader_used_locations.begin(), vertex_shader_used_locations.end(), location) ==
vertex_shader_used_locations.end()) {
continue;
}
const VkDeviceSize attribute_size = GetVertexInputFormatSize(attrib.desc.format);
VkDeviceSize vertex_buffer_remaining_size =
vbb->effective_size > attrib.desc.offset ? vbb->effective_size - attrib.desc.offset : 0;
VkDeviceSize vertex_attributes_count = 0;
if (vbb->stride > 0) {
vertex_attributes_count = vertex_buffer_remaining_size / vbb->stride;
if (vertex_buffer_remaining_size > vertex_attributes_count * vbb->stride) {
vertex_buffer_remaining_size -= vertex_attributes_count * vbb->stride;
} else {
vertex_buffer_remaining_size = 0;
}
// maybe room for one more attribute but not full stride - not having stride space does not matter for last element
if (vertex_buffer_remaining_size >= attribute_size) {
vertex_attributes_count += 1;
}
} else {
// For the current attribute type, if stride is 0, the same vertex data chunk will be accessed by all vertex shader
// instances See https://docs.vulkan.org/spec/latest/chapters/fxvertex.html#fxvertex-input-address-calculation
if (vertex_buffer_remaining_size >= attribute_size) {
// attribute won't be limiting
continue;
} else {
// Vertex attribute does not fit in vertex buffer
vertex_attributes_count = 0;
}
}
if (vertex_binding_desc.desc.inputRate == VK_VERTEX_INPUT_RATE_VERTEX) {
if (!vertex_attribute_fetch_limit_vertex_input_rate.has_value()) {
vertex_attribute_fetch_limit_vertex_input_rate = VertexAttributeFetchLimit{};
}
vertex_attribute_fetch_limit_vertex_input_rate->max_vertex_attributes_count =
std::min(vertex_attribute_fetch_limit_vertex_input_rate->max_vertex_attributes_count, vertex_attributes_count);
if (vertex_attribute_fetch_limit_vertex_input_rate->max_vertex_attributes_count == vertex_attributes_count) {
vertex_attribute_fetch_limit_vertex_input_rate->binding_info = *vbb;
vertex_attribute_fetch_limit_vertex_input_rate->attribute.location = attrib.desc.location;
vertex_attribute_fetch_limit_vertex_input_rate->attribute.binding = attrib.desc.binding;
vertex_attribute_fetch_limit_vertex_input_rate->attribute.format = attrib.desc.format;
vertex_attribute_fetch_limit_vertex_input_rate->attribute.offset = attrib.desc.offset;
}
} else if (vertex_binding_desc.desc.inputRate == VK_VERTEX_INPUT_RATE_INSTANCE) {
if (!vertex_attribute_fetch_limit_instance_input_rate.has_value()) {
vertex_attribute_fetch_limit_instance_input_rate = VertexAttributeFetchLimit{};
}
vertex_attribute_fetch_limit_instance_input_rate->max_vertex_attributes_count =
std::min(vertex_attribute_fetch_limit_instance_input_rate->max_vertex_attributes_count,
vertex_attributes_count * vertex_binding_desc.desc.divisor);
if (vertex_attribute_fetch_limit_instance_input_rate->max_vertex_attributes_count ==
(vertex_attributes_count * vertex_binding_desc.desc.divisor)) {
vertex_attribute_fetch_limit_instance_input_rate->binding_info = *vbb;
vertex_attribute_fetch_limit_instance_input_rate->attribute.location = attrib.desc.location;
vertex_attribute_fetch_limit_instance_input_rate->attribute.binding = attrib.desc.binding;
vertex_attribute_fetch_limit_instance_input_rate->attribute.format = attrib.desc.format;
vertex_attribute_fetch_limit_instance_input_rate->attribute.offset = attrib.desc.offset;
vertex_attribute_fetch_limit_instance_input_rate->instance_rate_divisor = vertex_binding_desc.desc.divisor;
}
}
}
}
return {vertex_attribute_fetch_limit_vertex_input_rate, vertex_attribute_fetch_limit_instance_input_rate};
}
void RegisterVertexAttributeFetchOobValidation(Validator& gpuav, CommandBufferSubState& cb) {
if (!gpuav.gpuav_settings.shader_instrumentation.vertex_attribute_fetch_oob) {
return;
}
struct ErrorInfo {
std::optional<VertexAttributeFetchLimit> vertex_attribute_fetch_limit_vertex_input_rate{};
std::optional<VertexAttributeFetchLimit> vertex_attribute_fetch_limit_instance_input_rate{};
std::optional<vvl::IndexBufferBinding> index_buffer_binding{};
};
// Used to communicate error info between lambdas
auto error_info = std::make_shared<ErrorInfo>();
cb.on_instrumentation_error_logger_register_functions.emplace_back([error_info](Validator& gpuav, CommandBufferSubState& cb,
const LastBound& last_bound) {
auto local_error_info = std::make_shared<ErrorInfo>();
*local_error_info = *error_info;
CommandBufferSubState::InstrumentationErrorLogger inst_error_logger = [local_error_info = std::move(local_error_info)](
Validator& gpuav, const Location& loc,
const uint32_t* error_record,
const InstrumentedShader*,
std::string& out_error_msg,
std::string& out_vuid_msg) {
if (GetErrorGroup(error_record) != glsl::kErrorGroup_InstIndexedDraw) {
return false;
}
const uint32_t error_sub_code = GetSubError(error_record);
if (error_sub_code != glsl::kErrorSubCode_IndexedDraw_OOBVertexIndex &&
error_sub_code != glsl::kErrorSubCode_IndexedDraw_OOBInstanceIndex) {
return false;
}
switch (loc.function) {
case vvl::Func::vkCmdDrawIndexed:
out_vuid_msg = "VUID-vkCmdDrawIndexed-None-02721";
break;
case vvl::Func::vkCmdDrawIndexedIndirectCount:
case vvl::Func::vkCmdDrawIndexedIndirectCountKHR:
out_vuid_msg = "VUID-vkCmdDrawIndexedIndirectCount-None-02721";
break;
case vvl::Func::vkCmdDrawIndexedIndirectCount2KHR:
out_vuid_msg = "VUID-vkCmdDrawIndexedIndirectCount2KHR-None-02721";
break;
case vvl::Func::vkCmdDrawIndexedIndirect:
out_vuid_msg = "VUID-vkCmdDrawIndexedIndirect-None-02721";
break;
case vvl::Func::vkCmdDrawIndexedIndirect2KHR:
out_vuid_msg = "VUID-vkCmdDrawIndexedIndirect2KHR-None-02721";
break;
case vvl::Func::vkCmdDrawMultiIndexedEXT:
out_vuid_msg = "VUID-vkCmdDrawMultiIndexedEXT-None-02721";
break;
default:
return false;
}
assert(local_error_info->vertex_attribute_fetch_limit_vertex_input_rate.has_value() ||
local_error_info->vertex_attribute_fetch_limit_instance_input_rate.has_value());
assert(local_error_info->index_buffer_binding.has_value());
auto add_vertex_buffer_binding_info =
[&gpuav, error_sub_code](const VertexAttributeFetchLimit& vertex_attribute_fetch_limit, std::string& out) {
out += vertex_attribute_fetch_limit.binding_info.String(*gpuav.device_state);
out += "- Vertices count: ";
out += std::to_string(vertex_attribute_fetch_limit.max_vertex_attributes_count);
out += '\n';
if (error_sub_code == glsl::kErrorSubCode_IndexedDraw_OOBInstanceIndex) {
if (vertex_attribute_fetch_limit.instance_rate_divisor != vvl::kNoIndex32) {
out += " - Instance rate divisor: ";
out += std::to_string(vertex_attribute_fetch_limit.instance_rate_divisor);
out += '\n';
}
}
};
auto add_vertex_attribute_info = [](const VertexAttributeFetchLimit& vertex_attribute_fetch_limit, std::string& out) {
out += "The following VkVertexInputAttributeDescription caused OOB access:\n";
out += " - Location: ";
out += std::to_string(vertex_attribute_fetch_limit.attribute.location);
out += '\n';
out += " - Binding: ";
out += std::to_string(vertex_attribute_fetch_limit.attribute.binding);
out += '\n';
out += " - Format: ";
out += string_VkFormat(vertex_attribute_fetch_limit.attribute.format);
out += '\n';
out += " - Offset: ";
out += std::to_string(vertex_attribute_fetch_limit.attribute.offset);
out += " bytes\n";
};
if (error_sub_code == glsl::kErrorSubCode_IndexedDraw_OOBVertexIndex) {
out_error_msg += "Vertex index ";
const uint32_t oob_vertex_index = error_record[glsl::kHeader_StageInfoOffset_0];
out_error_msg += std::to_string(oob_vertex_index);
} else if (error_sub_code == glsl::kErrorSubCode_IndexedDraw_OOBInstanceIndex) {
out_error_msg += "Instance index ";
const uint32_t oob_instance_index = error_record[glsl::kHeader_StageInfoOffset_1];
out_error_msg += std::to_string(oob_instance_index);
const uint32_t instance_rate_divisor =
local_error_info->vertex_attribute_fetch_limit_instance_input_rate->instance_rate_divisor;
if (instance_rate_divisor > 1 && instance_rate_divisor != vvl::kNoIndex32) {
out_error_msg += " (or ";
out_error_msg += std::to_string(oob_instance_index / instance_rate_divisor);
out_error_msg += " if divided by instance rate divisor of ";
out_error_msg += std::to_string(instance_rate_divisor);
out_error_msg += ")";
}
}
if (error_sub_code == glsl::kErrorSubCode_IndexedDraw_OOBVertexIndex) {
out_error_msg += ", using VK_VERTEX_INPUT_RATE_VERTEX, has caused an OOB access within a bound vertex buffer.\n";
} else if (error_sub_code == glsl::kErrorSubCode_IndexedDraw_OOBInstanceIndex) {
out_error_msg += ", using VK_VERTEX_INPUT_RATE_INSTANCE, has caused an OOB access within a bound vertex buffer.\n";
}
if (error_sub_code == glsl::kErrorSubCode_IndexedDraw_OOBVertexIndex) {
add_vertex_buffer_binding_info(*local_error_info->vertex_attribute_fetch_limit_vertex_input_rate, out_error_msg);
add_vertex_attribute_info(*local_error_info->vertex_attribute_fetch_limit_vertex_input_rate, out_error_msg);
} else if (error_sub_code == glsl::kErrorSubCode_IndexedDraw_OOBInstanceIndex) {
add_vertex_buffer_binding_info(*local_error_info->vertex_attribute_fetch_limit_instance_input_rate, out_error_msg);
add_vertex_attribute_info(*local_error_info->vertex_attribute_fetch_limit_instance_input_rate, out_error_msg);
}
if (error_sub_code == glsl::kErrorSubCode_IndexedDraw_OOBVertexIndex) {
const uint32_t index_byte_size = IndexTypeByteSize(local_error_info->index_buffer_binding->index_type);
assert(index_byte_size != 0); // Should never be VK_INDEX_TYPE_NONE_KHR
const uint32_t max_indices_in_buffer =
static_cast<uint32_t>(local_error_info->index_buffer_binding->size / index_byte_size);
out_error_msg += "Index Buffer (";
out_error_msg += gpuav.FormatHandle(local_error_info->index_buffer_binding->Buffer());
out_error_msg += ") binding info:\n";
out_error_msg += " - Type: ";
out_error_msg += string_VkIndexType(local_error_info->index_buffer_binding->index_type);
out_error_msg += '\n';
out_error_msg += " - Offset: ";
out_error_msg += std::to_string(local_error_info->index_buffer_binding->BufferOffset());
out_error_msg += " bytes\n";
out_error_msg += " - Size: ";
out_error_msg += std::to_string(local_error_info->index_buffer_binding->size);
out_error_msg += " bytes (sizeof(";
out_error_msg += string_VkIndexType(local_error_info->index_buffer_binding->index_type);
out_error_msg += ") * ";
out_error_msg += std::to_string(max_indices_in_buffer);
out_error_msg += ")\n";
}
out_error_msg +=
"Note: Vertex buffer binding size is the effective, valid one, based on how the VkBuffer was created and "
"the vkCmdBindVertexBuffers parameters. So it can be clamped up to 0 if binding was invalid.";
return true;
};
return inst_error_logger;
});
auto get_vertex_attribute_fetch_limits_buffer_range = [error_info](CommandBufferSubState& cb) {
vko::BufferRange vertex_attribute_fetch_limits_buffer_range =
cb.gpu_resources_manager.GetHostCoherentBufferRange(4 * sizeof(uint32_t));
if (vertex_attribute_fetch_limits_buffer_range.buffer == VK_NULL_HANDLE) {
return vertex_attribute_fetch_limits_buffer_range;
}
auto vertex_attribute_fetch_limits_buffer_ptr = (uint32_t*)vertex_attribute_fetch_limits_buffer_range.offset_mapped_ptr;
const auto [vertex_attribute_fetch_limit_vertex_input_rate, vertex_attribute_fetch_limit_instance_input_rate] =
GetVertexAttributeFetchLimits(cb.base);
if (vertex_attribute_fetch_limit_vertex_input_rate.has_value()) {
vertex_attribute_fetch_limits_buffer_ptr[0] = 1u;
vertex_attribute_fetch_limits_buffer_ptr[1] =
(uint32_t)vertex_attribute_fetch_limit_vertex_input_rate->max_vertex_attributes_count;
} else {
vertex_attribute_fetch_limits_buffer_ptr[0] = 0u;
}
if (vertex_attribute_fetch_limit_instance_input_rate.has_value()) {
vertex_attribute_fetch_limits_buffer_ptr[2] = 1u;
vertex_attribute_fetch_limits_buffer_ptr[3] =
(uint32_t)vertex_attribute_fetch_limit_instance_input_rate->max_vertex_attributes_count;
} else {
vertex_attribute_fetch_limits_buffer_ptr[2] = 0u;
}
error_info->vertex_attribute_fetch_limit_vertex_input_rate = vertex_attribute_fetch_limit_vertex_input_rate;
error_info->vertex_attribute_fetch_limit_instance_input_rate = vertex_attribute_fetch_limit_instance_input_rate;
error_info->index_buffer_binding = cb.base.index_buffer_binding;
return vertex_attribute_fetch_limits_buffer_range;
};
cb.on_instrumentation_common_desc_update_functions.emplace_back(
[&gpuav, get_vertex_attribute_fetch_limits_buffer_range](CommandBufferSubState& cb, const LastBound&, const Location& loc,
CommonDescriptorUpdate& out_update) {
if (!vvl::IsCommandDrawVertex(loc.function)) {
return;
}
if (vvl::IsCommandDrawVertexIndexed(loc.function)) {
vko::BufferRange buffer_range = get_vertex_attribute_fetch_limits_buffer_range(cb);
if (buffer_range.buffer == VK_NULL_HANDLE) {
return;
}
out_update.buffer = buffer_range.buffer;
out_update.offset = buffer_range.offset;
out_update.range = buffer_range.size;
out_update.address = buffer_range.offset_address;
} else {
// Point all non-indexed draws to our global buffer that will bypass the check in shader
VertexAttributeFetchOff& resource = gpuav.shared_resources_cache.GetOrCreate<VertexAttributeFetchOff>(gpuav);
if (!resource.valid) {
return;
}
out_update.buffer = resource.buffer.VkHandle();
out_update.offset = 0;
out_update.range = resource.buffer.Size();
out_update.address = resource.buffer.Address();
}
out_update.binding = glsl::kBindingInstVertexAttributeFetchLimits;
});
}
} // namespace gpuav