| /* 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 <vulkan/vulkan_core.h> |
| #include "descriptor_checks_classic.h" |
| #include "drawdispatch/drawdispatch_vuids.h" |
| #include "gpuav/core/gpuav.h" |
| #include "gpuav/resources/gpuav_shader_resources.h" |
| #include "gpuav/resources/gpuav_state_trackers.h" |
| #include "gpuav/shaders/gpuav_error_codes.h" |
| #include "gpuav/shaders/gpuav_error_header.h" |
| #include "gpuav/shaders/gpuav_shaders_constants.h" |
| |
| namespace gpuav { |
| |
| // Returns the number of bytes to hold 32 bit aligned array of bits. |
| static uint32_t BitBufferSize(uint32_t num_bits) { |
| static constexpr uint32_t kBitsPerWord = 32; |
| return (((num_bits + (kBitsPerWord - 1)) & ~(kBitsPerWord - 1)) / kBitsPerWord) * sizeof(uint32_t); |
| } |
| |
| DescriptorIdPool::DescriptorIdPool(Validator& gpuav, uint32_t max_descriptors) : max_descriptors_(max_descriptors), buffer_(gpuav) { |
| // If max_descriptors_ is 0, GPU-AV aborted during vkCreateDevice(). We still need to |
| // support calls into this class as no-ops if this happens. |
| if (max_descriptors_ == 0) { |
| return; |
| } |
| |
| VkBufferCreateInfo buffer_info = vku::InitStruct<VkBufferCreateInfo>(); |
| buffer_info.size = BitBufferSize(max_descriptors_ + 1); // add extra entry since 0 is the invalid id. |
| buffer_info.usage = VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_SHADER_DEVICE_ADDRESS_BIT; |
| |
| VmaAllocationCreateInfo alloc_info{}; |
| alloc_info.requiredFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT; |
| const VkResult result = buffer_.Create(&buffer_info, &alloc_info); |
| if (result != VK_SUCCESS) { |
| return; |
| } |
| |
| gpu_id_pool_state_ = (uint32_t*)buffer_.GetMappedPtr(); |
| memset(gpu_id_pool_state_, 0, static_cast<size_t>(buffer_info.size)); |
| } |
| |
| DescriptorIdPool::~DescriptorIdPool() { |
| if (max_descriptors_ > 0) { |
| buffer_.Destroy(); |
| gpu_id_pool_state_ = nullptr; |
| } |
| } |
| |
| DescriptorId DescriptorIdPool::NextId(const VulkanTypedHandle& handle) { |
| if (max_descriptors_ == 0) { |
| return 0; |
| } |
| DescriptorId result; |
| |
| // NOTE: valid ids are in the range [1, max_descriptors_] (inclusive) |
| // 0 is the invalid id. |
| std::lock_guard guard(lock_); |
| if (alloc_map_.size() >= max_descriptors_) { |
| return 0; |
| } |
| do { |
| result = next_id_++; |
| if (next_id_ > max_descriptors_) { |
| next_id_ = 1; |
| } |
| } while (alloc_map_.count(result) > 0); |
| alloc_map_[result] = handle; |
| gpu_id_pool_state_[result / 32] |= 1u << (result & 31); |
| return result; |
| } |
| |
| void DescriptorIdPool::DeleteId(DescriptorId id) { |
| if (max_descriptors_ > 0) { |
| std::lock_guard guard(lock_); |
| // Note: We don't mess with next_id_ here because ids should be assigned in LRU order. |
| gpu_id_pool_state_[id / 32] &= ~(1u << (id & 31)); |
| alloc_map_.erase(id); |
| } |
| } |
| |
| struct DescriptorChecksClassicCbState { |
| vko::BufferRange last_bound_desc_sets_ssbo; |
| }; |
| |
| void DescriptorChecksClassicOnFinishDeviceSetup(Validator& gpuav) { |
| if (!gpuav.gpuav_settings.shader_instrumentation.descriptor_checks) { |
| gpuav.shared_resources_cache.GetOrCreate<DescriptorIdPool>(gpuav, 0); |
| return; |
| } |
| |
| VkPhysicalDeviceDescriptorIndexingProperties desc_indexing_props = vku::InitStructHelper(); |
| VkPhysicalDeviceProperties2 props2 = vku::InitStructHelper(&desc_indexing_props); |
| DispatchGetPhysicalDeviceProperties2Helper(gpuav.api_version, gpuav.physical_device, &props2); |
| |
| uint32_t num_descs = desc_indexing_props.maxUpdateAfterBindDescriptorsInAllPools; |
| if (num_descs == 0 || num_descs > glsl::kDebugInputBindlessMaxDescriptors) { |
| num_descs = glsl::kDebugInputBindlessMaxDescriptors; |
| } |
| |
| gpuav.shared_resources_cache.GetOrCreate<DescriptorIdPool>(gpuav, num_descs); |
| } |
| |
| void RegisterDescriptorChecksClassicValidation(Validator& gpuav, CommandBufferSubState& cb) { |
| if (!gpuav.gpuav_settings.shader_instrumentation.descriptor_checks) { |
| return; |
| } |
| |
| cb.on_instrumentation_error_logger_register_functions.emplace_back([](Validator& gpuav, CommandBufferSubState& cb, |
| const LastBound& last_bound) { |
| // We want to grab the last (current) element in descriptor_binding_commands, but as a std::vector, the reference might be |
| // garbage later, so just hold the index for later. It is possible to have no descriptor sets bound, for example if using |
| // push constants. |
| uint32_t descriptor_binding_index = vvl::kNoIndex32; |
| DescriptorSetBindings* desc_set_bindings = cb.shared_resources_cache.TryGet<DescriptorSetBindings>(); |
| if (desc_set_bindings && !desc_set_bindings->descriptor_set_binding_commands.empty()) { |
| descriptor_binding_index = uint32_t(desc_set_bindings->descriptor_set_binding_commands.size() - 1); |
| } |
| |
| CommandBufferSubState::InstrumentationErrorLogger inst_error_logger = [&cb, descriptor_binding_index]( |
| Validator& gpuav, const Location& loc, |
| const uint32_t* error_record, |
| const InstrumentedShader*, |
| std::string& out_error_msg, |
| std::string& out_vuid_msg) { |
| using namespace glsl; |
| |
| bool error_found = false; |
| if (GetErrorGroup(error_record) != kErrorGroup_InstDescriptorIndexingOOB) { |
| return error_found; |
| } |
| error_found = true; |
| |
| std::ostringstream strm; |
| |
| if (descriptor_binding_index == vvl::kNoIndex32) { |
| assert(false); // This means we have hit a situtation where there are no descriptors bound |
| return false; |
| } |
| const DescriptorSetBindings& desc_set_bindings = cb.shared_resources_cache.Get<DescriptorSetBindings>(); |
| const auto& descriptor_sets = |
| desc_set_bindings.descriptor_set_binding_commands[descriptor_binding_index].bound_descriptor_sets; |
| |
| // Currently we only encode the descriptor index here and save the binding in a parameter slot |
| // The issue becomes if the user has kErrorSubCode_DescriptorIndexing_Bounds then we can't back track to the exact |
| // binding because they have gone over it |
| const uint32_t encoded_set_index = error_record[kInst_LogError_ParameterOffset_0]; |
| const uint32_t set_num = encoded_set_index >> kInst_DescriptorIndexing_SetShift; |
| const uint32_t descriptor_index = encoded_set_index & kInst_DescriptorIndexing_IndexMask; |
| const uint32_t binding_num = error_record[kInst_LogError_ParameterOffset_2]; |
| |
| const uint32_t array_length = error_record[kInst_LogError_ParameterOffset_1]; |
| |
| const uint32_t error_sub_code = GetSubError(error_record); |
| switch (error_sub_code) { |
| case kErrorSubCode_DescriptorIndexing_Bounds: { |
| strm << "(set = " << set_num << ", binding = " << binding_num << ") Index of " << descriptor_index |
| << " used to index descriptor array of length " << array_length << "."; |
| out_vuid_msg = vvl::CreateActionVuid(loc.function, vvl::ActionVUID::DESCRIPTOR_INDEX_OOB_10068); |
| error_found = true; |
| } break; |
| |
| case kErrorSubCode_DescriptorIndexing_Uninitialized: { |
| const auto& dsl = descriptor_sets[set_num]->Layout(); |
| strm << "(set = " << set_num << ", binding = " << binding_num << ") Descriptor index " << descriptor_index |
| << " is uninitialized."; |
| |
| if (descriptor_index == 0 && array_length == 1) { |
| strm << " (There is no array, but descriptor is viewed as having an array of length 1)"; |
| } |
| |
| const VkDescriptorBindingFlags binding_flags = dsl.GetDescriptorBindingFlagsFromBinding(binding_num); |
| if (binding_flags & VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT) { |
| strm << " VK_DESCRIPTOR_BINDING_VARIABLE_DESCRIPTOR_COUNT_BIT was used and the original descriptorCount (" |
| << dsl.GetDescriptorCountFromBinding(binding_num) |
| << ") could have been reduced during AllocateDescriptorSets."; |
| } else if (gpuav.enabled_features.nullDescriptor) { |
| strm << " nullDescriptor feature is on, but vkUpdateDescriptorSets was not called with VK_NULL_HANDLE for " |
| "this " |
| "descriptor."; |
| } |
| |
| out_vuid_msg = vvl::CreateActionVuid(loc.function, vvl::ActionVUID::INVALID_DESCRIPTOR_08114); |
| error_found = true; |
| } break; |
| |
| case kErrorSubCode_DescriptorIndexing_Destroyed: { |
| strm << "(set = " << set_num << ", binding = " << binding_num << ") Descriptor index " << descriptor_index |
| << " references a resource that was destroyed."; |
| |
| if (descriptor_index == 0 && array_length == 1) { |
| strm << " (There is no array, but descriptor is viewed as having an array of length 1)"; |
| } |
| |
| out_vuid_msg = "UNASSIGNED-Descriptor destroyed"; |
| error_found = true; |
| } break; |
| } |
| out_error_msg += strm.str(); |
| return error_found; |
| }; |
| |
| return inst_error_logger; |
| }); |
| |
| cb.on_instrumentation_error_logger_register_functions.emplace_back([](Validator& gpuav, CommandBufferSubState& cb, |
| const LastBound& last_bound) { |
| // We want to grab the last (current) element in descriptor_binding_commands, but as a std::vector, the reference might be |
| // garbage later, so just hold the index for later. It is possible to have no descriptor sets bound, for example if using |
| // push constants. |
| uint32_t descriptor_binding_index = vvl::kNoIndex32; |
| DescriptorSetBindings* desc_set_bindings = cb.shared_resources_cache.TryGet<DescriptorSetBindings>(); |
| if (desc_set_bindings && !desc_set_bindings->descriptor_set_binding_commands.empty()) { |
| descriptor_binding_index = uint32_t(desc_set_bindings->descriptor_set_binding_commands.size() - 1); |
| } |
| |
| bool uses_shader_object = last_bound.pipeline_state == nullptr; |
| |
| CommandBufferSubState::InstrumentationErrorLogger inst_error_logger = [&cb, descriptor_binding_index, uses_shader_object]( |
| Validator& gpuav, const Location& loc, |
| const uint32_t* error_record, |
| const InstrumentedShader*, |
| std::string& out_error_msg, |
| std::string& out_vuid_msg) { |
| using namespace glsl; |
| bool error_found = false; |
| if (GetErrorGroup(error_record) != kErrorGroup_InstDescriptorClass) { |
| return error_found; |
| } |
| error_found = true; |
| std::ostringstream strm; |
| |
| if (descriptor_binding_index == vvl::kNoIndex32) { |
| assert(false); // This means we have hit a situtation where there are no descriptors bound |
| return false; |
| } |
| const DescriptorSetBindings& desc_set_bindings = cb.shared_resources_cache.Get<DescriptorSetBindings>(); |
| const auto& descriptor_sets = |
| desc_set_bindings.descriptor_set_binding_commands[descriptor_binding_index].bound_descriptor_sets; |
| |
| const uint32_t encoded_set_index = error_record[kInst_LogError_ParameterOffset_0]; |
| const uint32_t set_num = encoded_set_index >> kInst_DescriptorIndexing_SetShift; |
| const uint32_t global_descriptor_index = encoded_set_index & kInst_DescriptorIndexing_IndexMask; |
| |
| const auto descriptor_set_state = descriptor_sets[set_num]; |
| auto [binding_num, desc_index] = descriptor_set_state->GetBindingAndIndex(global_descriptor_index); |
| |
| const auto* binding_state = descriptor_set_state->GetBinding(binding_num); |
| |
| strm << "(set = " << set_num << ", binding = " << binding_num << ", index " << desc_index << ") "; |
| |
| const uint32_t error_sub_code = GetSubError(error_record); |
| switch (error_sub_code) { |
| case kErrorSubCode_DescriptorClass_GeneralBufferBounds: |
| case kErrorSubCode_DescriptorClass_GeneralBufferCoopMatBounds: { |
| if (binding_state->descriptor_class != vvl::DescriptorClass::GeneralBuffer) { |
| assert(false); |
| return false; |
| } |
| const vvl::Buffer* buffer_state = |
| static_cast<const vvl::BufferBinding*>(binding_state)->descriptors[desc_index].GetBufferState(); |
| if (buffer_state) { |
| const uint32_t byte_offset = error_record[kInst_LogError_ParameterOffset_1]; |
| const uint32_t resource_size = error_record[kInst_LogError_ParameterOffset_2]; |
| strm << " access out of bounds. The descriptor buffer (" << gpuav.FormatHandle(buffer_state->Handle()) |
| << ") size is " << buffer_state->GetSize() << " bytes, " << resource_size |
| << " bytes were bound, and the highest out of bounds access was at [" << byte_offset << "] bytes"; |
| } else { |
| // This will only get called when using nullDescriptor without bindless |
| strm << "Trying to access a null descriptor, but vkUpdateDescriptorSets was not called with VK_NULL_HANDLE " |
| "for " |
| "this descriptor. "; |
| } |
| if (error_sub_code == kErrorSubCode_DescriptorClass_GeneralBufferCoopMatBounds) { |
| strm << "\nFor VK_KHR_cooperative_matrix this is invalid unless cooperativeMatrixRobustBufferAccess is " |
| "enabled."; |
| } |
| |
| vvl::ActionVUID action_vuid = vvl::ActionVUID::UNKNOWN; |
| if (binding_state->type == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER || |
| binding_state->type == VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER_DYNAMIC) { |
| action_vuid = uses_shader_object ? vvl::ActionVUID::UNIFORM_ACCESS_OOB_08612 |
| : vvl::ActionVUID::UNIFORM_ACCESS_OOB_06935; |
| } else { |
| action_vuid = uses_shader_object ? vvl::ActionVUID::STORAGE_ACCESS_OOB_08613 |
| : vvl::ActionVUID::STORAGE_ACCESS_OOB_06936; |
| } |
| out_vuid_msg = vvl::CreateActionVuid(loc.function, action_vuid); |
| } break; |
| |
| case kErrorSubCode_DescriptorClass_TexelBufferBounds: { |
| if (binding_state->descriptor_class != vvl::DescriptorClass::TexelBuffer) { |
| assert(false); |
| return false; |
| } |
| |
| const vvl::BufferView* buffer_view_state = |
| static_cast<const vvl::TexelBinding*>(binding_state)->descriptors[desc_index].GetBufferViewState(); |
| if (buffer_view_state) { |
| const uint32_t byte_offset = error_record[kInst_LogError_ParameterOffset_1]; |
| const uint32_t resource_size = error_record[kInst_LogError_ParameterOffset_2]; |
| |
| strm << " access out of bounds. The descriptor texel buffer (" |
| << gpuav.FormatHandle(buffer_view_state->Handle()) << ") size is " << resource_size |
| << " texels and the highest out of bounds access was at [" << byte_offset << "] bytes"; |
| } else { |
| // This will only get called when using nullDescriptor without bindless |
| strm << "Trying to access a null descriptor, but vkUpdateDescriptorSets was not called with VK_NULL_HANDLE " |
| "for " |
| "this descriptor. "; |
| } |
| |
| // https://gitlab.khronos.org/vulkan/vulkan/-/issues/3977 |
| out_vuid_msg = "UNASSIGNED-Descriptor Texel Buffer texel out of bounds"; |
| } break; |
| |
| default: |
| error_found = false; |
| assert(false); // other OOB checks are not implemented yet |
| } |
| |
| out_error_msg += strm.str(); |
| return error_found; |
| }; |
| |
| return inst_error_logger; |
| }); |
| |
| DescriptorSetBindings& desc_set_bindings = cb.shared_resources_cache.GetOrCreate<DescriptorSetBindings>(); |
| |
| desc_set_bindings.on_update_bound_descriptor_sets.emplace_back([](Validator& gpuav, CommandBufferSubState& cb, |
| DescriptorSetBindings::BindingCommand& desc_binding_cmd) { |
| DescriptorChecksClassicCbState& dc_cb_state = cb.shared_resources_cache.GetOrCreate<DescriptorChecksClassicCbState>(); |
| dc_cb_state.last_bound_desc_sets_ssbo = |
| cb.gpu_resources_manager.GetHostCoherentBufferRange(sizeof(glsl::BoundDescriptorSetsSSBO)); |
| dc_cb_state.last_bound_desc_sets_ssbo.Clear(); |
| auto desc_sets_ssbo = static_cast<glsl::BoundDescriptorSetsSSBO*>(dc_cb_state.last_bound_desc_sets_ssbo.offset_mapped_ptr); |
| desc_sets_ssbo->descriptor_init_status = gpuav.shared_resources_cache.Get<DescriptorIdPool>().GetDeviceAddress(); |
| |
| for (size_t bound_ds_i = 0; bound_ds_i < desc_binding_cmd.bound_descriptor_sets.size(); ++bound_ds_i) { |
| auto& bound_ds = desc_binding_cmd.bound_descriptor_sets[bound_ds_i]; |
| // Account for gaps in descriptor sets bindings |
| if (!bound_ds) { |
| continue; |
| } |
| |
| if (bound_ds->IsUpdateAfterBind()) { |
| continue; |
| } |
| desc_sets_ssbo->descriptor_encodings[bound_ds_i] = SubState(*bound_ds).GetDescriptorEncodingsAddress(gpuav); |
| } |
| |
| desc_binding_cmd.bound_desc_sets_ssbo = dc_cb_state.last_bound_desc_sets_ssbo; |
| }); |
| |
| cb.on_instrumentation_common_desc_update_functions.emplace_back( |
| [dummy_buffer_range = vko::BufferRange{}](CommandBufferSubState& cb, const LastBound&, const Location&, |
| CommonDescriptorUpdate& out_update) mutable { |
| DescriptorChecksClassicCbState* dc_cb_state = cb.shared_resources_cache.TryGet<DescriptorChecksClassicCbState>(); |
| if (dc_cb_state) { |
| const vko::BufferRange& buffer_range = dc_cb_state->last_bound_desc_sets_ssbo; |
| 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 { |
| // TODO - This will always hit the case for Buffer/Heap mode, this is wrong and just an issue with the fact |
| // on_update_bound_descriptor_sets is never called |
| |
| // [For Classic mode] If no descriptor set was bound in command buffer, we still need "something" to be in the slot |
| // or else it will be marked as invalid for not being updated |
| if (dummy_buffer_range.buffer == VK_NULL_HANDLE) { |
| // Caputre the dummy_buffer_range so if found multiple time, only allocate it once |
| dummy_buffer_range = cb.gpu_resources_manager.GetDeviceLocalBufferRange(64); |
| } |
| out_update.buffer = dummy_buffer_range.buffer; |
| out_update.offset = dummy_buffer_range.offset; |
| out_update.range = dummy_buffer_range.size; |
| out_update.address = dummy_buffer_range.offset_address; |
| } |
| |
| out_update.binding = glsl::kBindingInstDescriptorIndexingOOB; |
| }); |
| |
| // For every descriptor binding command, and for each set bound by this command, update a GPU buffer holding its |
| // corresponding descriptor states |
| cb.on_pre_cb_submission_functions.emplace_back([](Validator& gpuav, CommandBufferSubState& cb, VkCommandBuffer) { |
| DescriptorSetBindings& desc_set_bindings = cb.shared_resources_cache.Get<DescriptorSetBindings>(); |
| for (DescriptorSetBindings::BindingCommand& desc_binding_cmd : desc_set_bindings.descriptor_set_binding_commands) { |
| auto desc_sets_ssbo_ptr = |
| static_cast<glsl::BoundDescriptorSetsSSBO*>(desc_binding_cmd.bound_desc_sets_ssbo.offset_mapped_ptr); |
| for (size_t bound_ds_i = 0; bound_ds_i < desc_binding_cmd.bound_descriptor_sets.size(); ++bound_ds_i) { |
| auto& bound_ds = desc_binding_cmd.bound_descriptor_sets[bound_ds_i]; |
| // Account for gaps in descriptor sets bindings |
| if (!bound_ds) { |
| continue; |
| } |
| |
| DescriptorSetSubState& desc_set_state = SubState(*bound_ds); |
| desc_sets_ssbo_ptr->descriptor_encodings[bound_ds_i] = desc_set_state.GetDescriptorEncodingsAddress(gpuav); |
| } |
| } |
| }); |
| } |
| |
| } // namespace gpuav |