diff --git a/CMakeLists.txt b/CMakeLists.txt index b80cbd2318..3787aa7618 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -95,6 +95,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Windows|Linux|BSD") if(NOT WIN32) option(BUILD_CPUTIMING "Build CPUTiming layer" ON) option(BUILD_DEBUGMARKER "Build DebugMarker layer" ON) + option(BUILD_DEVICEMEMORYREPORT "Build DeviceMemoryReport layer" OFF) endif() option(BUILD_LAYERMGR "Build Vulkan Configurator" ON) set(SDK_VERSION "" CACHE STRING "Vulkan SDK version") @@ -108,6 +109,7 @@ elseif(ANDROID) option(BUILD_SCREENSHOT "Build screenshot layer" ON) option(BUILD_CPUTIMING "Build CPUTiming layer" ON) option(BUILD_DEBUGMARKER "Build DebugMarker layer" ON) + option(BUILD_DEVICEMEMORYREPORT "Build DeviceMemoryReport layer" ON) set(BUILD_MONITOR OFF) set(BUILD_LAYERMGR OFF) @@ -127,7 +129,7 @@ if(BUILD_TESTS) add_subdirectory(tests) endif() -if(BUILD_APIDUMP OR BUILD_MONITOR OR BUILD_SCREENSHOT OR BUILD_CPUTIMING OR BUILD_DEBUGMARKER) +if(BUILD_APIDUMP OR BUILD_MONITOR OR BUILD_SCREENSHOT OR BUILD_CPUTIMING OR BUILD_DEBUGMARKER OR BUILD_DEVICEMEMORYREPORT) message(STATUS "INFO: Building Vulkan Layers") add_subdirectory(layersvt) endif() diff --git a/layersvt/CMakeLists.txt b/layersvt/CMakeLists.txt index 2e5a64324d..92538d5e3a 100644 --- a/layersvt/CMakeLists.txt +++ b/layersvt/CMakeLists.txt @@ -201,11 +201,47 @@ if(BUILD_DEBUGMARKER) target_compile_definitions(VkLayer_DebugMarker PRIVATE VK_ENABLE_BETA_EXTENSIONS) endif() +if(BUILD_DEVICEMEMORYREPORT) + add_library(VkLayer_DeviceMemoryReport MODULE) + set_target_properties(VkLayer_DeviceMemoryReport PROPERTIES FOLDER "layers/devicememoryreport") + target_sources(VkLayer_DeviceMemoryReport PRIVATE + device_memory_report/device_memory_report_handwritten_dispatch.cpp + device_memory_report/device_memory_report_handwritten_functions.h + device_memory_report/device_memory_report.h + device_memory_report/device_memory_report.cpp + device_memory_report/device_memory_report_perfetto.h + device_memory_report/device_memory_report_perfetto.cpp + perfetto/perfetto.cc + vk_layer_table.cpp + vk_layer_table.h + layer_keep_alive.cpp + device_memory_report/VkLayer_DeviceMemoryReport.json.in + ) + + target_include_directories(VkLayer_DeviceMemoryReport PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_SOURCE_DIR}/device_memory_report + ${CMAKE_CURRENT_BINARY_DIR} + ) + + if(CMAKE_SYSTEM_NAME MATCHES "Linux|BSD|DragonFly|GNU") + if (BUILD_WSI_XCB_SUPPORT) + target_compile_definitions(VkLayer_DeviceMemoryReport PRIVATE VK_USE_PLATFORM_XLIB_KHR) + endif() + + if (BUILD_WSI_WAYLAND_SUPPORT) + target_compile_definitions(VkLayer_DeviceMemoryReport PRIVATE VK_USE_PLATFORM_WAYLAND_KHR) + endif() + endif() + + target_compile_definitions(VkLayer_DeviceMemoryReport PRIVATE VK_ENABLE_BETA_EXTENSIONS) +endif() + if (BUILD_TESTS AND NOT RUN_ON_GITHUB) add_subdirectory(test) endif() -list(APPEND TOOL_LAYERS "VkLayer_api_dump" "VkLayer_screenshot" "VkLayer_monitor" "VkLayer_CPUTiming" "VkLayer_DebugMarker") +list(APPEND TOOL_LAYERS "VkLayer_api_dump" "VkLayer_screenshot" "VkLayer_monitor" "VkLayer_CPUTiming" "VkLayer_DebugMarker" "VkLayer_DeviceMemoryReport") foreach(layer ${TOOL_LAYERS}) if (NOT TARGET "${layer}") continue() @@ -217,6 +253,8 @@ foreach(layer ${TOOL_LAYERS}) set(layer_dir "cpu_timing/") elseif(layer STREQUAL "VkLayer_screenshot") set(layer_dir "screenshot/") + elseif(layer STREQUAL "VkLayer_DeviceMemoryReport") + set(layer_dir "device_memory_report/") else() set(layer_dir "") endif() @@ -265,6 +303,8 @@ foreach(layer ${TOOL_LAYERS}) set(INPUT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/cpu_timing/${layer}.json.in") elseif(layer STREQUAL "VkLayer_screenshot") set(INPUT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/screenshot/json/${layer}.json.in") + elseif(layer STREQUAL "VkLayer_DeviceMemoryReport") + set(INPUT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/device_memory_report/${layer}.json.in") else() set(INPUT_FILE "${CMAKE_CURRENT_SOURCE_DIR}/json/${layer}.json.in") endif() diff --git a/layersvt/device_memory_report/VkLayer_DeviceMemoryReport.def b/layersvt/device_memory_report/VkLayer_DeviceMemoryReport.def new file mode 100644 index 0000000000..7e64580f96 --- /dev/null +++ b/layersvt/device_memory_report/VkLayer_DeviceMemoryReport.def @@ -0,0 +1,21 @@ +; Copyright (C) 2026 Google 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. + +LIBRARY VkLayer_DeviceMemoryReport +EXPORTS +vkGetInstanceProcAddr +vkGetDeviceProcAddr +vkEnumerateInstanceExtensionProperties +vkEnumerateInstanceLayerProperties +vkEnumerateDeviceLayerProperties diff --git a/layersvt/device_memory_report/VkLayer_DeviceMemoryReport.json.in b/layersvt/device_memory_report/VkLayer_DeviceMemoryReport.json.in new file mode 100644 index 0000000000..9cbbf16db0 --- /dev/null +++ b/layersvt/device_memory_report/VkLayer_DeviceMemoryReport.json.in @@ -0,0 +1,17 @@ +{ + "file_format_version" : "1.2.0", + "layer" : { + "name": "VK_LAYER_GOOGLE_DeviceMemoryReport", + "type": "GLOBAL", + "library_path": "@JSON_LIBRARY_PATH@", + "api_version": "@JSON_VERSION@", + "implementation_version": "1", + "description": "Vulkan Device Memory Report Layer", + "device_extensions": [ + { + "name": "VK_EXT_device_memory_report", + "spec_version": "2" + } + ] + } +} diff --git a/layersvt/device_memory_report/device_memory_report.cpp b/layersvt/device_memory_report/device_memory_report.cpp new file mode 100644 index 0000000000..68fbd77cdf --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report.cpp @@ -0,0 +1,401 @@ +/* Copyright (C) 2026 Google 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 "device_memory_report.h" +#include "device_memory_report_perfetto.h" +#include "perfetto/perfetto.h" +#include +#include +#include +#include + +DeviceMemoryReport& DeviceMemoryReport::Get() { + static DeviceMemoryReport instance; + return instance; +} + +void DeviceMemoryReport::SetVkInstance(VkPhysicalDevice phys_dev, VkInstance instance) { + std::lock_guard lock(map_mutex_); + vk_instance_map_[phys_dev] = instance; +} + +VkInstance DeviceMemoryReport::GetVkInstance(VkPhysicalDevice phys_dev) { + if (phys_dev == VK_NULL_HANDLE) return VK_NULL_HANDLE; + std::lock_guard lock(map_mutex_); + auto it = vk_instance_map_.find(phys_dev); + if (it != vk_instance_map_.end()) return it->second; + + // Fallback: If only a single VkInstance exists in the process, any valid physical device + // must belong to that instance. + if (vk_instance_map_.size() == 1) { + return vk_instance_map_.begin()->second; + } + return VK_NULL_HANDLE; +} + +void DeviceMemoryReport::SetDeviceMemoryProperties(VkDevice device, const VkPhysicalDeviceMemoryProperties& props) { + std::lock_guard lock(counter_mutex_); + device_memory_properties_map_[device] = props; +} + +void DeviceMemoryReport::SetAllocationMemoryProperties(uint64_t memory_handle, VkMemoryPropertyFlags property_flags) { + std::lock_guard lock(counter_mutex_); + auto& allocation = memory_allocations_[memory_handle]; + allocation.mem_flags = property_flags; + // An allocation with total_size > 0 represents an active physical memory allocation that + // has already been recorded and contributed to Perfetto counter tracks. + // Changing its memory property flags (e.g., HOST_VISIBLE or LAZILY_ALLOCATED) can alter + // how its unbound memory headroom is classified into usage categories (such as staging + // vs general buffer or transient memoryless). We must update the unbound counter to move + // the reported bytes from the old usage track to the newly classified track. + // If total_size == 0, the allocation has not yet been instantiated (or is being configured + // prior to allocation), so no counter bytes have been emitted to Perfetto yet. + if (allocation.total_size > 0) { + UpdateAllocationUnboundCounter(memory_handle); + } +} + +void DeviceMemoryReport::OnDestroyDevice(VkDevice device) { + std::lock_guard lock(counter_mutex_); + has_callback_map_.erase(device); + device_memory_properties_map_.erase(device); +} + +void VKAPI_PTR DeviceMemoryReport::MemoryReportCallback(const VkDeviceMemoryReportCallbackDataEXT* pCallbackData, void* pUserData) { + DeviceMemoryReport::Get().OnMemoryReportEvent(pCallbackData); +} + +const char* GetBufferCluster(VkBufferUsageFlags usage, VkMemoryPropertyFlags memFlags) { + const VkBufferUsageFlags kFunctionalBufferUsageFlags = + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | + VK_BUFFER_USAGE_INDEX_BUFFER_BIT | + VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | + VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | + VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | + VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | + VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | + VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | + VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | + VK_BUFFER_USAGE_SHADER_BINDING_TABLE_BIT_KHR | + VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT | + VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_COUNTER_BUFFER_BIT_EXT | + VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT; + + // 1. Host staging: Must be host-visible and not a primary GPU functional buffer + if ((memFlags & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) && + (usage & (VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT)) && + !(usage & kFunctionalBufferUsageFlags)) { + return "staging_transfer"; + } + // 2. Ray tracing acceleration structures & shader binding tables + if (usage & (VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR | + VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR | + VK_BUFFER_USAGE_SHADER_BINDING_TABLE_BIT_KHR)) { + return "ray_tracing"; + } + // 3. Indirect draw / dispatch commands & conditional rendering + if (usage & (VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT)) { + return "indirect_gpu_driven"; + } + // 4. Compute storage buffers (SSBOs & Storage Texel Buffers) + if (usage & (VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT)) { + return "compute_storage"; + } + // 5. Uniform buffers & uniform texel buffers + if (usage & (VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT)) { + return "uniform_constants"; + } + // 6. Geometry & mesh data (Vertex + Index) + if (usage & (VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT)) { + return "geometry_mesh"; + } + // 7. Transform feedback stream-out + if (usage & (VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT | VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_COUNTER_BUFFER_BIT_EXT)) { + return "transform_feedback"; + } + return "general_buffer"; +} + +const char* GetImageCluster(VkImageUsageFlags usage, VkMemoryPropertyFlags memFlags) { + // 1. Mobile TBDR on-chip tile memoryless attachments + if ((usage & VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT) || + (memFlags & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT)) { + return "transient_memoryless"; + } + // 2. Compute storage write targets (UAVs) + if (usage & VK_IMAGE_USAGE_STORAGE_BIT) { + return "storage_compute_image"; + } + // 3. Depth / stencil render targets + if (usage & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) { + return "depth_stencil_target"; + } + // 4. Color render targets + if (usage & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) { + return "color_render_target"; + } + // 5. Sampled textures + if (usage & VK_IMAGE_USAGE_SAMPLED_BIT) { + return "static_texture"; + } + return "general_image"; +} + +// Construct a Perfetto track name for visualizing memory usage by category in the UI. +static std::string GetUsageTrackName(bool is_driver, std::string_view usage) { + std::string track = is_driver ? "vulkan.mem.driver.usage." : "vulkan.mem.app.usage."; + track += usage; + return track; +} + +void DeviceMemoryReport::AddCounterBytes(const std::string& track, uint64_t size) { + uint64_t& bytes = usage_memory_bytes_[track]; + bytes += size; + TRACE_COUNTER("VulkanDeviceMemoryReport", GetCounterTrack(track), bytes); +} + +void DeviceMemoryReport::SubtractCounterBytes(const std::string& track, uint64_t size) { + uint64_t& bytes = usage_memory_bytes_[track]; + bytes = (bytes >= size) ? (bytes - size) : 0; + TRACE_COUNTER("VulkanDeviceMemoryReport", GetCounterTrack(track), bytes); +} + +void DeviceMemoryReport::UpdateAllocationUnboundCounter(uint64_t memory_handle) { + auto allocation_it = memory_allocations_.find(memory_handle); + if (allocation_it == memory_allocations_.end()) return; + auto& allocation = allocation_it->second; + + uint64_t bound_size = 0; + std::vector> intervals; + intervals.reserve(allocation.sub_allocations.size()); + for (const auto& suballocation : allocation.sub_allocations) { + if (suballocation.size > 0) { + VkDeviceSize end = (suballocation.offset + suballocation.size < suballocation.offset) ? UINT64_MAX : (suballocation.offset + suballocation.size); + intervals.emplace_back(suballocation.offset, end); + } + } + if (!intervals.empty()) { + std::sort(intervals.begin(), intervals.end()); + + // Overlapping and adjacent intervals are merged: + // bound_size is calculated as the mathematical union (distinct physical footprint) of all intervals. + VkDeviceSize current_start = intervals[0].first; + VkDeviceSize current_end = intervals[0].second; + for (size_t i = 1; i < intervals.size(); ++i) { + if (intervals[i].first <= current_end) { + // Overlapping or adjacent interval: extend current merged range + current_end = std::max(current_end, intervals[i].second); + } else { + // Disjoint interval: add previous merged interval size and begin next range + bound_size += (current_end - current_start); + current_start = intervals[i].first; + current_end = intervals[i].second; + } + } + bound_size += (current_end - current_start); + } + + uint64_t new_unbound = (allocation.total_size > bound_size) ? (allocation.total_size - bound_size) : 0; + + std::string track_name = "unbound_memory"; + auto res_it = resources_.find(allocation.object_handle); + // If the memory object has an associated resource with a specific usage, use it as the track name. + if (res_it != resources_.end()) { + track_name = res_it->second.GetCluster(allocation.mem_flags); + } + std::string new_unbound_track = GetUsageTrackName(allocation.is_driver, track_name); + + // If the unbound memory usage track name or the number of unbound bytes has changed, + // update the global counters by subtracting the old bytes from the old track + // and adding the new bytes to the new track. + if (allocation.unbound_usage_track != new_unbound_track || allocation.applied_unbound_bytes != new_unbound) { + if (allocation.applied_unbound_bytes > 0) { + SubtractCounterBytes(allocation.unbound_usage_track, allocation.applied_unbound_bytes); + } + if (new_unbound > 0) { + AddCounterBytes(new_unbound_track, new_unbound); + } + } + allocation.unbound_usage_track = new_unbound_track; + allocation.applied_unbound_bytes = new_unbound; +} + +void DeviceMemoryReport::RemoveResourceBinding(uint64_t resource_handle) { + auto mem_it = resource_to_memory_map_.find(resource_handle); + if (mem_it == resource_to_memory_map_.end()) return; + + uint64_t memory_handle = mem_it->second; + resource_to_memory_map_.erase(mem_it); + + auto allocation_it = memory_allocations_.find(memory_handle); + if (allocation_it != memory_allocations_.end()) { + auto& suballocations = allocation_it->second.sub_allocations; + // Search by resource handle to identify which specific suballocation to remove, + // since a single memory block can have multiple resources bound to it. + for (auto it = suballocations.begin(); it != suballocations.end(); ++it) { + if (it->resource_handle == resource_handle) { + SubtractCounterBytes(it->usage_track, it->size); + suballocations.erase(it); + break; + } + } + UpdateAllocationUnboundCounter(memory_handle); + } +} + +void DeviceMemoryReport::BindResourceMemory(uint64_t resource_handle, uint64_t memory_handle, VkDeviceSize memory_offset) { + std::lock_guard lock(counter_mutex_); + auto res_it = resources_.find(resource_handle); + if (res_it == resources_.end() || res_it->second.size == 0) return; + + // If the same resource handle is passed more than once, remove stale bindings first. + RemoveResourceBinding(resource_handle); + + auto& allocation = memory_allocations_[memory_handle]; + VkDeviceSize res_size = res_it->second.size; + std::string new_usage_track = GetUsageTrackName(allocation.is_driver, res_it->second.GetCluster(allocation.mem_flags)); + + // Suballocations represent individual resources (like buffers or images) that are bound + // to specific offset regions within a single large memory allocation. + // We add a record here to track this specific resource's footprint within the larger memory block. + allocation.sub_allocations.push_back({ resource_handle, memory_offset, res_size, new_usage_track }); + resource_to_memory_map_[resource_handle] = memory_handle; + + // Each distinct virtual resource handle adds its virtual size to its specific category track upon binding. + AddCounterBytes(new_usage_track, res_size); + UpdateAllocationUnboundCounter(memory_handle); +} + +void DeviceMemoryReport::OnBindBufferMemory(uint64_t buffer_handle, uint64_t memory_handle, VkDeviceSize memory_offset) { + BindResourceMemory(buffer_handle, memory_handle, memory_offset); +} + +void DeviceMemoryReport::OnBindImageMemory(uint64_t image_handle, uint64_t memory_handle, VkDeviceSize memory_offset) { + BindResourceMemory(image_handle, memory_handle, memory_offset); +} + +void DeviceMemoryReport::RemoveAllocationTracking(uint64_t memory_handle) { + auto allocation_it = memory_allocations_.find(memory_handle); + if (allocation_it == memory_allocations_.end()) return; + + auto& allocation = allocation_it->second; + for (const auto& suballocation : allocation.sub_allocations) { + SubtractCounterBytes(suballocation.usage_track, suballocation.size); + resource_to_memory_map_.erase(suballocation.resource_handle); + } + if (allocation.applied_unbound_bytes > 0) { + SubtractCounterBytes(allocation.unbound_usage_track, allocation.applied_unbound_bytes); + } + memory_allocations_.erase(allocation_it); +} + +void DeviceMemoryReport::OnRecordResourceSize(uint64_t resource_handle, VkDeviceSize size) { + std::lock_guard lock(counter_mutex_); + resources_[resource_handle].size = size; +} + +VkDeviceSize DeviceMemoryReport::GetRecordedResourceSize(uint64_t resource_handle) { + std::lock_guard lock(counter_mutex_); + auto it = resources_.find(resource_handle); + return it != resources_.end() ? it->second.size : 0; +} + +void DeviceMemoryReport::OnCreateImage(uint64_t image_handle, VkImageUsageFlags usage) { + std::lock_guard lock(counter_mutex_); + auto& res = resources_[image_handle]; + res.is_image = true; + res.image_usage = usage; + for (const auto& pair : memory_allocations_) { + if (pair.second.object_handle == image_handle) { + UpdateAllocationUnboundCounter(pair.first); + } + } +} + +void DeviceMemoryReport::OnCreateBuffer(uint64_t buffer_handle, VkBufferUsageFlags usage, VkDeviceSize size) { + std::lock_guard lock(counter_mutex_); + auto& res = resources_[buffer_handle]; + res.is_image = false; + res.buffer_usage = usage; + res.size = size; + for (const auto& pair : memory_allocations_) { + if (pair.second.object_handle == buffer_handle) { + UpdateAllocationUnboundCounter(pair.first); + } + } +} + +void DeviceMemoryReport::OnDestroyObject(uint64_t object_handle) { + std::lock_guard lock(counter_mutex_); + RemoveResourceBinding(object_handle); + resources_.erase(object_handle); +} + +void DeviceMemoryReport::OnMemoryReportEvent(const VkDeviceMemoryReportCallbackDataEXT* pCallbackData) { + std::lock_guard lock(counter_mutex_); + // For internal driver allocations, a single object (e.g. VkImage) might have multiple distinct memory allocations. + // We must use memoryObjectId as the key so each allocation is tracked separately and can be individually freed. + // For device memory allocations, objectHandle is the VkDeviceMemory handle, which we use as the key for compatibility. + uint64_t key = (pCallbackData->objectType == VK_OBJECT_TYPE_DEVICE_MEMORY) ? pCallbackData->objectHandle : pCallbackData->memoryObjectId; + + if (pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_ALLOCATE_EXT || + pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_IMPORT_EXT) { + auto& allocation = memory_allocations_[key]; + allocation.total_size = pCallbackData->size; + allocation.is_driver = (pCallbackData->flags & VK_DEVICE_MEMORY_REPORT_FLAG_INTERNAL_OBJECT_BIT_EXT) != 0; + allocation.object_handle = pCallbackData->objectHandle; + UpdateAllocationUnboundCounter(key); + } else if (pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT || + pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_UNIMPORT_EXT) { + RemoveAllocationTracking(key); + } +} + +void DeviceMemoryReport::SetHasMemoryReportCallback(VkDevice device, bool has_callback) { + std::lock_guard lock(counter_mutex_); + has_callback_map_[device] = has_callback; +} + +void DeviceMemoryReport::OnAllocateMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize size, uint32_t memory_type_index, VkMemoryPropertyFlags property_flags) { + std::lock_guard lock(counter_mutex_); + uint64_t handle = reinterpret_cast(memory); + auto& allocation = memory_allocations_[handle]; + + VkMemoryPropertyFlags mem_flags = property_flags; + if (mem_flags == 0 && memory_type_index != UINT32_MAX) { + auto prop_it = device_memory_properties_map_.find(device); + if (prop_it != device_memory_properties_map_.end() && memory_type_index < prop_it->second.memoryTypeCount) { + mem_flags = prop_it->second.memoryTypes[memory_type_index].propertyFlags; + } + } + bool mem_flags_changed = (allocation.mem_flags != mem_flags); + allocation.mem_flags = mem_flags; + + if (has_callback_map_[device]) { + if (mem_flags_changed && allocation.total_size > 0) { + UpdateAllocationUnboundCounter(handle); + } + } else { + allocation.total_size = size; + allocation.is_driver = false; + UpdateAllocationUnboundCounter(handle); + } +} + +void DeviceMemoryReport::OnFreeMemory(VkDevice device, VkDeviceMemory memory) { + std::lock_guard lock(counter_mutex_); + if (has_callback_map_[device]) return; + RemoveAllocationTracking(reinterpret_cast(memory)); +} diff --git a/layersvt/device_memory_report/device_memory_report.h b/layersvt/device_memory_report/device_memory_report.h new file mode 100644 index 0000000000..22514516aa --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report.h @@ -0,0 +1,310 @@ +/* Copyright (C) 2026 Google 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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#ifndef VK_DEVICE_MEMORY_REPORT_FLAG_INTERNAL_OBJECT_BIT_EXT +#define VK_DEVICE_MEMORY_REPORT_FLAG_INTERNAL_OBJECT_BIT_EXT 0x00000001 +#endif + +/** + * @brief Categorizes buffer usage flags and memory property flags into a cluster category name. + * @param usage The Vulkan buffer usage flags. + * @param memFlags The physical memory property flags. + * @return The name of the cluster category for buffer memory tracking. + */ +const char* GetBufferCluster(VkBufferUsageFlags usage, VkMemoryPropertyFlags memFlags = 0); + +/** + * @brief Categorizes image usage flags and memory property flags into a cluster category name. + * @param usage The Vulkan image usage flags. + * @param memFlags The physical memory property flags. + * @return The name of the cluster category for image memory tracking. + */ +const char* GetImageCluster(VkImageUsageFlags usage, VkMemoryPropertyFlags memFlags = 0); + +/** + * The DeviceMemoryReport class is responsible for tracking Vulkan device memory + * allocations and object associations, sending live memory usage counters to Perfetto traces. + * + * It tracks allocations per object handle, categorizing memory by usage + * types (e.g., textures, surfaces, buffers). + * + * How it works: + * The layer intercepts Vulkan memory allocation and object creation events, using either + * VK_EXT_device_memory_report callbacks (when supported by the underlying driver) or falling back + * to direct allocation intercepts (vkAllocateMemory/vkFreeMemory). + * Object bindings (vkBindBufferMemory, vkBindImageMemory, vkBindBufferMemory2, vkBindImageMemory2) are tracked to attribute memory allocations to usage categories. + * + * Track Categories: + * Memory usage counters are reported to Perfetto under: + * - Driver vs Application allocations (e.g., vulkan.mem.driver.* vs vulkan.mem.app.*) + * - Usages (vulkan.mem.*.usage.) + * + * This class is a singleton and provides thread-safe access to its state. + */ +class DeviceMemoryReport { + public: + /** + * @brief Returns the singleton instance of the DeviceMemoryReport class. + * @return Reference to the DeviceMemoryReport singleton. + */ + static DeviceMemoryReport& Get(); + + /** + * @brief Associates a Vulkan physical device with its corresponding instance. + * @param phys_dev The Vulkan physical device. + * @param instance The Vulkan instance. + */ + void SetVkInstance(VkPhysicalDevice phys_dev, VkInstance instance); + + /** + * @brief Retrieves the Vulkan instance associated with a given physical device. + * @param phys_dev The Vulkan physical device. + * @return The associated Vulkan instance. + */ + VkInstance GetVkInstance(VkPhysicalDevice phys_dev); + + /** + * @brief Stores physical device memory properties for a logical device. + * @param device The Vulkan device handle. + * @param props The memory properties of the physical device. + */ + void SetDeviceMemoryProperties(VkDevice device, const VkPhysicalDeviceMemoryProperties& props); + + /** + * @brief Explicitly sets memory property flags for an allocation handle (useful for testing or overrides). + * @param memory_handle The 64-bit handle of the Vulkan device memory. + * @param property_flags The memory property flags. + */ + void SetAllocationMemoryProperties(uint64_t memory_handle, VkMemoryPropertyFlags property_flags); + + /** + * @brief Cleans up device-specific tracking state upon device destruction. + * @param device The Vulkan device handle. + */ + void OnDestroyDevice(VkDevice device); + + /** + * @brief Static callback invoked by the VK_EXT_device_memory_report extension. + * @param pCallbackData Pointer to the memory report callback data structure. + * @param pUserData User data pointer (unused). + */ + static void VKAPI_PTR MemoryReportCallback(const VkDeviceMemoryReportCallbackDataEXT* pCallbackData, void* pUserData); + + /** + * @brief Processes a device memory report event received from the Vulkan driver callback. + * @param pCallbackData Pointer to the memory report callback data structure. + */ + void OnMemoryReportEvent(const VkDeviceMemoryReportCallbackDataEXT* pCallbackData); + + /** + * @brief Sets whether a Vulkan device active callback is installed for VK_EXT_device_memory_report. + * @param device The Vulkan device handle. + * @param has_callback True if driver callback is active for the device, false otherwise. + */ + void SetHasMemoryReportCallback(VkDevice device, bool has_callback); + + /** + * @brief Handles fallback memory allocation tracking when driver callback is unavailable. + * @param device The Vulkan device handle. + * @param memory The VkDeviceMemory handle being allocated. + * @param size The size of the allocation in bytes. + * @param memory_type_index The memory type index from VkMemoryAllocateInfo. + * @param property_flags Optional explicit memory property flags (for testing or overrides). + */ + void OnAllocateMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize size, uint32_t memory_type_index = UINT32_MAX, VkMemoryPropertyFlags property_flags = 0); + + /** + * @brief Handles fallback memory free tracking when driver callback is unavailable. + * @param device The Vulkan device handle. + * @param memory The VkDeviceMemory handle being freed. + */ + void OnFreeMemory(VkDevice device, VkDeviceMemory memory); + + /** + * @brief Tracks binding of buffer memory to correlate buffer usage with memory allocations. + * @param buffer_handle The 64-bit handle of the Vulkan buffer. + * @param memory_handle The 64-bit handle of the Vulkan device memory. + * @param memory_offset Offset into device memory where buffer is bound. + */ + void OnBindBufferMemory(uint64_t buffer_handle, uint64_t memory_handle, VkDeviceSize memory_offset); + + /** + * @brief Tracks binding of image memory to correlate image usage with memory allocations. + * @param image_handle The 64-bit handle of the Vulkan image. + * @param memory_handle The 64-bit handle of the Vulkan device memory. + * @param memory_offset Offset into device memory where image is bound. + */ + void OnBindImageMemory(uint64_t image_handle, uint64_t memory_handle, VkDeviceSize memory_offset); + + /** + * @brief Records the size of a virtual resource (buffer or image) in bytes. + * @param resource_handle The 64-bit handle of the Vulkan object. + * @param size Size in bytes from memory requirements or create info. + */ + void OnRecordResourceSize(uint64_t resource_handle, VkDeviceSize size); + + /** + * @brief Retrieves the recorded size of a virtual resource (for testing). + * @param resource_handle The 64-bit handle of the Vulkan object. + * @return Size in bytes, or 0 if not tracked. + */ + VkDeviceSize GetRecordedResourceSize(uint64_t resource_handle); + + /** + * @brief Tracks creation of a Vulkan image and its usage flags. + * @param image_handle The 64-bit handle of the Vulkan image. + * @param usage Usage flags for the created image. + */ + void OnCreateImage(uint64_t image_handle, VkImageUsageFlags usage); + + /** + * @brief Tracks creation of a Vulkan buffer, its usage flags, and requested size. + * @param buffer_handle The 64-bit handle of the Vulkan buffer. + * @param usage Usage flags for the created buffer. + * @param size Size in bytes of the buffer allocation. + */ + void OnCreateBuffer(uint64_t buffer_handle, VkBufferUsageFlags usage, VkDeviceSize size); + + /** + * @brief Handles destruction of a Vulkan object, cleaning up tracked usage state. + * @param object_handle The 64-bit handle of the destroyed Vulkan object. + */ + void OnDestroyObject(uint64_t object_handle); + + private: + /** + * @brief Represents a sub-allocation of a Vulkan resource (buffer or image) bound within a physical memory allocation. + */ + struct SubAllocation { + uint64_t resource_handle; /**< Handle of the bound Vulkan resource (buffer or image). */ + VkDeviceSize offset; /**< Offset in bytes within the physical memory allocation where the resource is bound. */ + VkDeviceSize size; /**< Size in bytes of the sub-allocated resource. */ + std::string usage_track; /**< Name of the memory usage trace counter category for this sub-allocation. */ + }; + + /** + * @brief Tracks state and sub-allocations for a physical device memory allocation. + */ + struct MemoryAllocation { + VkDeviceSize total_size = 0; + VkDeviceSize applied_unbound_bytes = 0; + bool is_driver = false; + VkMemoryPropertyFlags mem_flags = 0; + std::vector sub_allocations; + std::string unbound_usage_track; + uint64_t object_handle = 0; + }; + + /** + * @brief Tracks metadata for a virtual resource (buffer or image). + */ + struct Resource { + bool is_image = false; + VkImageUsageFlags image_usage = 0; + VkBufferUsageFlags buffer_usage = 0; + VkDeviceSize size = 0; + + const char* GetCluster(VkMemoryPropertyFlags mem_flags) const { + if (is_image) { + return GetImageCluster(image_usage, mem_flags); + } + return GetBufferCluster(buffer_usage, mem_flags); + } + }; + + /** + * @brief Helper to bind resource memory (buffers and images). + */ + void BindResourceMemory(uint64_t resource_handle, uint64_t memory_handle, VkDeviceSize memory_offset); + + /** + * @brief Updates unbound memory category counter for a physical memory slab. + * Unbound memory is allocated device memory not currently bound to any active resource (e.g. buffer or image). + */ + void UpdateAllocationUnboundCounter(uint64_t memory_handle); + + /** + * @brief Removes a sub-allocation of a resource from a physical memory slab if bound. + */ + void RemoveResourceBinding(uint64_t resource_handle); + + /** + * @brief Removes all sub-allocations and tracking for a memory slab being freed. + */ + void RemoveAllocationTracking(uint64_t memory_handle); + + /** + * @brief Increments trace counter for a memory track. + */ + void AddCounterBytes(const std::string& track, uint64_t size); + + /** + * @brief Decrements trace counter for a memory track with underflow protection. + */ + void SubtractCounterBytes(const std::string& track, uint64_t size); + + /** + * @brief Mutex protecting access to the physical device to instance mapping table. + */ + std::mutex map_mutex_; + + /** + * @brief Maps a physical device handle to its corresponding Vulkan instance handle. + */ + std::unordered_map vk_instance_map_; + + /** + * @brief Mutex protecting access to memory tracking tables and counter states. + */ + std::mutex counter_mutex_; + + /** + * @brief Maps a Vulkan device handle to a boolean indicating if driver memory report callback is active. + */ + std::unordered_map has_callback_map_; + + /** + * @brief Maps a Vulkan device handle to its physical device memory properties. + */ + std::unordered_map device_memory_properties_map_; + + /** + * @brief Maps a virtual resource handle to its metadata. + */ + std::unordered_map resources_; + + /** + * @brief Maps a virtual resource handle to the physical memory handle it is bound to. + */ + std::unordered_map resource_to_memory_map_; + + /** + * @brief Maps a physical memory handle to its allocation details and sub-allocations. + */ + std::unordered_map memory_allocations_; + + /** + * @brief Maps a usage track name to its current total memory usage in bytes. + */ + std::unordered_map usage_memory_bytes_; +}; diff --git a/layersvt/device_memory_report/device_memory_report_handwritten_dispatch.cpp b/layersvt/device_memory_report/device_memory_report_handwritten_dispatch.cpp new file mode 100644 index 0000000000..0c160a209d --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report_handwritten_dispatch.cpp @@ -0,0 +1,109 @@ +/* Copyright (C) 2026 Google 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 "device_memory_report_handwritten_functions.h" +#include "vk_layer_table.h" +#include + +extern "C" { + +static PFN_vkVoidFunction devmemreport_known_instance_functions(const char* pName) { + if (strcmp(pName, "vkGetInstanceProcAddr") == 0) return reinterpret_cast(vkGetInstanceProcAddr); + if (strcmp(pName, "vkCreateInstance") == 0) return reinterpret_cast(vkCreateInstance); + if (strcmp(pName, "vkDestroyInstance") == 0) return reinterpret_cast(vkDestroyInstance); + if (strcmp(pName, "vkEnumeratePhysicalDevices") == 0) return reinterpret_cast(vkEnumeratePhysicalDevices); + if (strcmp(pName, "vkEnumeratePhysicalDeviceGroups") == 0) return reinterpret_cast(vkEnumeratePhysicalDeviceGroups); + if (strcmp(pName, "vkEnumerateInstanceExtensionProperties") == 0) return reinterpret_cast(vkEnumerateInstanceExtensionProperties); + if (strcmp(pName, "vkEnumerateInstanceLayerProperties") == 0) return reinterpret_cast(vkEnumerateInstanceLayerProperties); + return nullptr; +} + +static PFN_vkVoidFunction devmemreport_known_device_functions(const char* pName) { + if (strcmp(pName, "vkGetDeviceProcAddr") == 0) return reinterpret_cast(vkGetDeviceProcAddr); + if (strcmp(pName, "vkCreateDevice") == 0) return reinterpret_cast(vkCreateDevice); + if (strcmp(pName, "vkDestroyDevice") == 0) return reinterpret_cast(vkDestroyDevice); + if (strcmp(pName, "vkEnumerateDeviceLayerProperties") == 0) return reinterpret_cast(vkEnumerateDeviceLayerProperties); + if (strcmp(pName, "vkAllocateMemory") == 0) return reinterpret_cast(vkAllocateMemory); + if (strcmp(pName, "vkFreeMemory") == 0) return reinterpret_cast(vkFreeMemory); + if (strcmp(pName, "vkBindBufferMemory") == 0) return reinterpret_cast(vkBindBufferMemory); + if (strcmp(pName, "vkBindImageMemory") == 0) return reinterpret_cast(vkBindImageMemory); + if (strcmp(pName, "vkBindBufferMemory2") == 0) return reinterpret_cast(vkBindBufferMemory2); + if (strcmp(pName, "vkBindImageMemory2") == 0) return reinterpret_cast(vkBindImageMemory2); + if (strcmp(pName, "vkBindBufferMemory2KHR") == 0) return reinterpret_cast(vkBindBufferMemory2KHR); + if (strcmp(pName, "vkBindImageMemory2KHR") == 0) return reinterpret_cast(vkBindImageMemory2KHR); + if (strcmp(pName, "vkCreateImage") == 0) return reinterpret_cast(vkCreateImage); + if (strcmp(pName, "vkDestroyImage") == 0) return reinterpret_cast(vkDestroyImage); + if (strcmp(pName, "vkCreateBuffer") == 0) return reinterpret_cast(vkCreateBuffer); + if (strcmp(pName, "vkDestroyBuffer") == 0) return reinterpret_cast(vkDestroyBuffer); + if (strcmp(pName, "vkGetImageMemoryRequirements") == 0) return reinterpret_cast(vkGetImageMemoryRequirements); + if (strcmp(pName, "vkGetImageMemoryRequirements2") == 0) return reinterpret_cast(vkGetImageMemoryRequirements2); + if (strcmp(pName, "vkGetImageMemoryRequirements2KHR") == 0) return reinterpret_cast(vkGetImageMemoryRequirements2KHR); + if (strcmp(pName, "vkGetBufferMemoryRequirements") == 0) return reinterpret_cast(vkGetBufferMemoryRequirements); + if (strcmp(pName, "vkGetBufferMemoryRequirements2") == 0) return reinterpret_cast(vkGetBufferMemoryRequirements2); + if (strcmp(pName, "vkGetBufferMemoryRequirements2KHR") == 0) return reinterpret_cast(vkGetBufferMemoryRequirements2KHR); + return nullptr; +} + +EXPORT_FUNCTION VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetInstanceProcAddr(VkInstance instance, const char* pName) { + PFN_vkVoidFunction func = devmemreport_known_instance_functions(pName); + if (func) { + return func; + } + + // If it's a device function, we can also return it here if we want to support GIPA for device functions. + func = devmemreport_known_device_functions(pName); + if (func) { + return func; + } + + if (instance == nullptr) { + return nullptr; + } + + auto table = instance_dispatch_table(instance); + if (table == NULL || table->GetInstanceProcAddr == NULL) { + return nullptr; + } + + return table->GetInstanceProcAddr(instance, pName); +} + +EXPORT_FUNCTION VKAPI_ATTR PFN_vkVoidFunction VKAPI_CALL vkGetDeviceProcAddr(VkDevice device, const char* pName) { + if (device == nullptr) { + return nullptr; + } + + if (device_dispatch_table(device)->GetDeviceProcAddr == NULL) { + return nullptr; + } + + // We must verify that the underlying device actually supports the function. + // Returning an interceptor for an unsupported function violates the Vulkan spec and can cause + // applications to erroneously think an extension is supported, leading to crashes when called. + PFN_vkVoidFunction down_func = device_dispatch_table(device)->GetDeviceProcAddr(device, pName); + if (down_func == nullptr) { + return nullptr; + } + + // Only return the intercepted function if the device supports the command. + PFN_vkVoidFunction func = devmemreport_known_device_functions(pName); + if (func) { + return func; + } + + return down_func; +} + +} // extern "C" diff --git a/layersvt/device_memory_report/device_memory_report_handwritten_functions.h b/layersvt/device_memory_report/device_memory_report_handwritten_functions.h new file mode 100644 index 0000000000..78dc41d8d4 --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report_handwritten_functions.h @@ -0,0 +1,452 @@ +/* Copyright (C) 2026 Google 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. + */ + +#pragma once + +#include +#include +#include +#include +#include +#include "vk_layer_table.h" +#include "device_memory_report.h" +#include "device_memory_report_perfetto.h" + +// This file contains handwritten implementations for Vulkan functions intercepted by +// the VK_LAYER_GOOGLE_DeviceMemoryReport layer: +// +// Core infrastructure & lifecycle: +// - vkCreateInstance: Initializes Perfetto tracing and the instance dispatch table. +// - vkEnumeratePhysicalDevices / vkEnumeratePhysicalDeviceGroups: Tracks the mapping +// between physical devices and instances to support dispatch table lookups. +// - vkCreateDevice / vkDestroyDevice: Initializes/destroys device dispatch tables and +// injects VK_EXT_device_memory_report callback registration into device creation. +// +// Memory tracking & resource tracking intercepts: +// - vkAllocateMemory / vkFreeMemory: Tracks direct allocations/frees as fallbacks. +// - vkBindBufferMemory* / vkBindImageMemory*: Associates buffer/image handles with memory allocations. +// - vkCreateBuffer / vkDestroyBuffer: Tracks buffer creation, usage flags, and requested sizes. +// - vkCreateImage / vkDestroyImage: Tracks image creation and usage flags. +// - vkGetBufferMemoryRequirements* / vkGetImageMemoryRequirements*: Tracks resource memory requirements. +// - vkEnumerate*ExtensionProperties / vkEnumerate*LayerProperties: Advertises the layer +// and support for the VK_EXT_device_memory_report extension. + +#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0])) + +#if defined(__GNUC__) && __GNUC__ >= 4 +#define EXPORT_FUNCTION __attribute__((visibility("default"))) +#elif defined(__SUNPRO_C) && (__SUNPRO_C >= 0x590) +#define EXPORT_FUNCTION __attribute__((visibility("default"))) +#else +#define EXPORT_FUNCTION +#endif + +#define LAYER_NAME "VK_LAYER_GOOGLE_DeviceMemoryReport" +#define LAYER_DESCRIPTION "Vulkan Device Memory Report Layer" + +extern "C" { + +VKAPI_ATTR VkResult VKAPI_CALL vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, + VkInstance* pInstance) { + InitializeDeviceMemoryReportPerfetto(); + + // Get the function pointer + VkLayerInstanceCreateInfo* chain_info = get_chain_info(pCreateInfo, VK_LAYER_LINK_INFO); + assert(chain_info->u.pLayerInfo != 0); + PFN_vkGetInstanceProcAddr fpGetInstanceProcAddr = chain_info->u.pLayerInfo->pfnNextGetInstanceProcAddr; + assert(fpGetInstanceProcAddr != 0); + PFN_vkCreateInstance fpCreateInstance = (PFN_vkCreateInstance)fpGetInstanceProcAddr(NULL, "vkCreateInstance"); + if (fpCreateInstance == NULL) { + return VK_ERROR_INITIALIZATION_FAILED; + } + + // Call the function and create the dispatch table + chain_info->u.pLayerInfo = chain_info->u.pLayerInfo->pNext; + VkResult result = fpCreateInstance(pCreateInfo, pAllocator, pInstance); + if (result == VK_SUCCESS) { + initInstanceTable(*pInstance, fpGetInstanceProcAddr); + } + + return result; +} + +// Intercept physical device enumeration to store physical device to instance mapping. +VKAPI_ATTR VkResult VKAPI_CALL vkEnumeratePhysicalDevices(VkInstance instance, uint32_t* pPhysicalDeviceCount, VkPhysicalDevice* pPhysicalDevices) { + + VkResult result = instance_dispatch_table(instance)->EnumeratePhysicalDevices(instance, pPhysicalDeviceCount, pPhysicalDevices); + + if ((result == VK_SUCCESS || result == VK_INCOMPLETE) && pPhysicalDeviceCount != nullptr && pPhysicalDevices != nullptr) { + for (uint32_t i = 0; i < *pPhysicalDeviceCount; ++i) { + DeviceMemoryReport::Get().SetVkInstance(pPhysicalDevices[i], instance); + } + } + return result; +} + +// Intercept physical device group enumeration to store physical device to instance mapping. +VKAPI_ATTR VkResult VKAPI_CALL vkEnumeratePhysicalDeviceGroups(VkInstance instance, uint32_t* pPhysicalDeviceGroupCount, VkPhysicalDeviceGroupProperties* pPhysicalDeviceGroupProperties) { + + VkResult result = instance_dispatch_table(instance)->EnumeratePhysicalDeviceGroups(instance, pPhysicalDeviceGroupCount, pPhysicalDeviceGroupProperties); + + if ((result == VK_SUCCESS || result == VK_INCOMPLETE) && pPhysicalDeviceGroupCount != nullptr && pPhysicalDeviceGroupProperties != nullptr) { + for (uint32_t i = 0; i < *pPhysicalDeviceGroupCount; ++i) { + for (uint32_t j = 0; j < pPhysicalDeviceGroupProperties[i].physicalDeviceCount; ++j) { + DeviceMemoryReport::Get().SetVkInstance(pPhysicalDeviceGroupProperties[i].physicalDevices[j], instance); + } + } + } + return result; +} + +VKAPI_ATTR void VKAPI_CALL vkDestroyInstance(VkInstance instance, const VkAllocationCallbacks* pAllocator) { + dispatch_key key = get_dispatch_key(instance); + instance_dispatch_table(instance)->DestroyInstance(instance, pAllocator); + destroy_instance_dispatch_table(key); +} + +VKAPI_ATTR VkResult VKAPI_CALL vkCreateDevice(VkPhysicalDevice physicalDevice, const VkDeviceCreateInfo* pCreateInfo, + const VkAllocationCallbacks* pAllocator, VkDevice* pDevice) { + InitializeDeviceMemoryReportPerfetto(); + + // Get the function pointer + VkLayerDeviceCreateInfo* chain_info = get_chain_info(pCreateInfo, VK_LAYER_LINK_INFO); + assert(chain_info->u.pLayerInfo != 0); + PFN_vkGetInstanceProcAddr fpGetInstanceProcAddr = chain_info->u.pLayerInfo->pfnNextGetInstanceProcAddr; + PFN_vkGetDeviceProcAddr fpGetDeviceProcAddr = chain_info->u.pLayerInfo->pfnNextGetDeviceProcAddr; + VkInstance vk_instance = DeviceMemoryReport::Get().GetVkInstance(physicalDevice); + PFN_vkCreateDevice fpCreateDevice = (PFN_vkCreateDevice)fpGetInstanceProcAddr(vk_instance, "vkCreateDevice"); + if (fpCreateDevice == NULL) { + return VK_ERROR_INITIALIZATION_FAILED; + } + + // Call the function and create the dispatch table + chain_info->u.pLayerInfo = chain_info->u.pLayerInfo->pNext; + + // Check if the underlying driver supports VK_EXT_device_memory_report + bool supports_memory_report = false; + uint32_t ext_count = 0; + if (instance_dispatch_table(physicalDevice)->EnumerateDeviceExtensionProperties) { + if (instance_dispatch_table(physicalDevice)->EnumerateDeviceExtensionProperties(physicalDevice, nullptr, &ext_count, nullptr) == VK_SUCCESS && ext_count > 0) { + std::vector exts(ext_count); + if (instance_dispatch_table(physicalDevice)->EnumerateDeviceExtensionProperties(physicalDevice, nullptr, &ext_count, exts.data()) == VK_SUCCESS) { + for (const auto& ext : exts) { + if (strcmp(ext.extensionName, VK_EXT_DEVICE_MEMORY_REPORT_EXTENSION_NAME) == 0) { + supports_memory_report = true; + break; + } + } + } + } + } + + // If supported, inject VK_EXT_device_memory_report callback into pNext chain + VkDeviceCreateInfo modified_create_info = *pCreateInfo; + std::vector enabled_extensions; + for (uint32_t i = 0; i < pCreateInfo->enabledExtensionCount; ++i) { + enabled_extensions.push_back(pCreateInfo->ppEnabledExtensionNames[i]); + } + + VkDeviceDeviceMemoryReportCreateInfoEXT memory_report_ci = {}; + if (supports_memory_report) { + bool already_enabled = false; + for (const char* name : enabled_extensions) { + if (strcmp(name, VK_EXT_DEVICE_MEMORY_REPORT_EXTENSION_NAME) == 0) { + already_enabled = true; + break; + } + } + if (!already_enabled) { + enabled_extensions.push_back(VK_EXT_DEVICE_MEMORY_REPORT_EXTENSION_NAME); + } + modified_create_info.enabledExtensionCount = static_cast(enabled_extensions.size()); + modified_create_info.ppEnabledExtensionNames = enabled_extensions.data(); + + memory_report_ci.sType = VK_STRUCTURE_TYPE_DEVICE_DEVICE_MEMORY_REPORT_CREATE_INFO_EXT; + memory_report_ci.pfnUserCallback = DeviceMemoryReport::MemoryReportCallback; + memory_report_ci.pUserData = nullptr; + memory_report_ci.pNext = modified_create_info.pNext; + modified_create_info.pNext = &memory_report_ci; + } + + VkResult result = fpCreateDevice(physicalDevice, supports_memory_report ? &modified_create_info : pCreateInfo, pAllocator, pDevice); + if (result == VK_SUCCESS) { + initDeviceTable(*pDevice, fpGetDeviceProcAddr); + DeviceMemoryReport::Get().SetHasMemoryReportCallback(*pDevice, supports_memory_report); + + VkPhysicalDeviceMemoryProperties mem_props = {}; + if (instance_dispatch_table(physicalDevice)->GetPhysicalDeviceMemoryProperties) { + instance_dispatch_table(physicalDevice)->GetPhysicalDeviceMemoryProperties(physicalDevice, &mem_props); + DeviceMemoryReport::Get().SetDeviceMemoryProperties(*pDevice, mem_props); + } + } + + return result; +} + +VKAPI_ATTR void VKAPI_CALL vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* pAllocator) { + DeviceMemoryReport::Get().OnDestroyDevice(device); + dispatch_key key = get_dispatch_key(device); + device_dispatch_table(device)->DestroyDevice(device, pAllocator); + destroy_device_dispatch_table(key); +} + +// Fallback memory allocation tracking used when driver callback is unavailable. +VKAPI_ATTR VkResult VKAPI_CALL vkAllocateMemory(VkDevice device, const VkMemoryAllocateInfo* pAllocateInfo, + const VkAllocationCallbacks* pAllocator, VkDeviceMemory* pMemory) { + PFN_vkAllocateMemory fpAllocateMemory = (PFN_vkAllocateMemory)device_dispatch_table(device)->AllocateMemory; + VkResult result = fpAllocateMemory(device, pAllocateInfo, pAllocator, pMemory); + if (result == VK_SUCCESS && pAllocateInfo != nullptr && pMemory != nullptr && *pMemory != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnAllocateMemory(device, *pMemory, pAllocateInfo->allocationSize, pAllocateInfo->memoryTypeIndex); + } + return result; +} + +// Fallback memory free tracking used when driver callback is unavailable. +VKAPI_ATTR void VKAPI_CALL vkFreeMemory(VkDevice device, VkDeviceMemory memory, const VkAllocationCallbacks* pAllocator) { + if (memory != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnFreeMemory(device, memory); + } + PFN_vkFreeMemory fpFreeMemory = (PFN_vkFreeMemory)device_dispatch_table(device)->FreeMemory; + if (fpFreeMemory != NULL) { + fpFreeMemory(device, memory, pAllocator); + } +} + +EXPORT_FUNCTION VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceExtensionProperties(const char* pLayerName, + uint32_t* pPropertyCount, + VkExtensionProperties* pProperties) { + return util_GetExtensionProperties(0, nullptr, pPropertyCount, pProperties); +} + +EXPORT_FUNCTION VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateInstanceLayerProperties(uint32_t* pPropertyCount, + VkLayerProperties* pProperties) { + static const VkLayerProperties layerProperties[] = {{ + LAYER_NAME, + VK_MAKE_VERSION(1, 4, VK_HEADER_VERSION), // specVersion + VK_MAKE_VERSION(0, 1, 0), // implementationVersion + LAYER_DESCRIPTION, + }}; + + return util_GetLayerProperties(ARRAY_SIZE(layerProperties), layerProperties, pPropertyCount, pProperties); +} + +EXPORT_FUNCTION VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateDeviceLayerProperties(VkPhysicalDevice physicalDevice, + uint32_t* pPropertyCount, + VkLayerProperties* pProperties) { + static const VkLayerProperties layerProperties[] = {{ + LAYER_NAME, + VK_MAKE_VERSION(1, 4, VK_HEADER_VERSION), + VK_MAKE_VERSION(0, 1, 0), + LAYER_DESCRIPTION, + }}; + + return util_GetLayerProperties(ARRAY_SIZE(layerProperties), layerProperties, pPropertyCount, pProperties); +} + + +// Intercept memory binding to correlate buffer object handles with device memory allocations. +VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory(VkDevice device, VkBuffer buffer, VkDeviceMemory memory, VkDeviceSize memoryOffset) { + VkResult result = device_dispatch_table(device)->BindBufferMemory(device, buffer, memory, memoryOffset); + if (result == VK_SUCCESS && buffer != VK_NULL_HANDLE && memory != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnBindBufferMemory(reinterpret_cast(buffer), reinterpret_cast(memory), memoryOffset); + } + return result; +} + +// Intercept memory binding to correlate image object handles with device memory allocations. +VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory(VkDevice device, VkImage image, VkDeviceMemory memory, VkDeviceSize memoryOffset) { + VkResult result = device_dispatch_table(device)->BindImageMemory(device, image, memory, memoryOffset); + if (result == VK_SUCCESS && image != VK_NULL_HANDLE && memory != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnBindImageMemory(reinterpret_cast(image), reinterpret_cast(memory), memoryOffset); + } + return result; +} + +static void RecordBufferBindings(uint32_t bindInfoCount, const VkBindBufferMemoryInfo* pBindInfos) { + for (uint32_t i = 0; i < bindInfoCount; ++i) { + if (pBindInfos[i].buffer != VK_NULL_HANDLE && pBindInfos[i].memory != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnBindBufferMemory(reinterpret_cast(pBindInfos[i].buffer), reinterpret_cast(pBindInfos[i].memory), pBindInfos[i].memoryOffset); + } + } +} + +// Intercept memory binding via vkBindBufferMemory2 to correlate buffer object handles with device memory allocations. +VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory2(VkDevice device, uint32_t bindInfoCount, const VkBindBufferMemoryInfo* pBindInfos) { + VkResult result = device_dispatch_table(device)->BindBufferMemory2(device, bindInfoCount, pBindInfos); + if (result == VK_SUCCESS && pBindInfos != nullptr) { + RecordBufferBindings(bindInfoCount, pBindInfos); + } + return result; +} + +// Intercept memory binding via vkBindBufferMemory2KHR to correlate buffer object handles with device memory allocations. +VKAPI_ATTR VkResult VKAPI_CALL vkBindBufferMemory2KHR(VkDevice device, uint32_t bindInfoCount, const VkBindBufferMemoryInfo* pBindInfos) { + VkResult result = device_dispatch_table(device)->BindBufferMemory2KHR(device, bindInfoCount, pBindInfos); + if (result == VK_SUCCESS && pBindInfos != nullptr) { + RecordBufferBindings(bindInfoCount, pBindInfos); + } + return result; +} + +static void RecordImageBinds(uint32_t bindInfoCount, const VkBindImageMemoryInfo* pBindInfos) { + for (uint32_t i = 0; i < bindInfoCount; ++i) { + if (pBindInfos[i].image != VK_NULL_HANDLE && pBindInfos[i].memory != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnBindImageMemory(reinterpret_cast(pBindInfos[i].image), reinterpret_cast(pBindInfos[i].memory), pBindInfos[i].memoryOffset); + } + } +} + +// Intercept memory binding via vkBindImageMemory2 to correlate image object handles with device memory allocations. +VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory2(VkDevice device, uint32_t bindInfoCount, const VkBindImageMemoryInfo* pBindInfos) { + VkResult result = device_dispatch_table(device)->BindImageMemory2(device, bindInfoCount, pBindInfos); + if (result == VK_SUCCESS && pBindInfos != nullptr) { + RecordImageBinds(bindInfoCount, pBindInfos); + } + return result; +} + +// Intercept memory binding via vkBindImageMemory2KHR to correlate image object handles with device memory allocations. +VKAPI_ATTR VkResult VKAPI_CALL vkBindImageMemory2KHR(VkDevice device, uint32_t bindInfoCount, const VkBindImageMemoryInfo* pBindInfos) { + VkResult result = device_dispatch_table(device)->BindImageMemory2KHR(device, bindInfoCount, pBindInfos); + if (result == VK_SUCCESS && pBindInfos != nullptr) { + RecordImageBinds(bindInfoCount, pBindInfos); + } + return result; +} + +// Intercept image creation to track image usage category flags. +VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(VkDevice device, const VkImageCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkImage* pImage) { + PFN_vkCreateImage fpCreateImage = (PFN_vkCreateImage)device_dispatch_table(device)->CreateImage; + VkResult result = fpCreateImage(device, pCreateInfo, pAllocator, pImage); + if (result == VK_SUCCESS && pCreateInfo != nullptr && pImage != nullptr && *pImage != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnCreateImage(reinterpret_cast(*pImage), pCreateInfo->usage); + + // Query and record memory requirements right after creation. + // This is needed because Vulkan 1.3 applications might use vkGetDeviceImageMemoryRequirements + // *before* creation, and subsequently skip calling vkGetImageMemoryRequirements, which would leave the tracked size as 0. + if (device_dispatch_table(device)->GetImageMemoryRequirements && (pCreateInfo->flags & VK_IMAGE_CREATE_DISJOINT_BIT) == 0) { + VkMemoryRequirements mem_reqs; + device_dispatch_table(device)->GetImageMemoryRequirements(device, *pImage, &mem_reqs); + DeviceMemoryReport::Get().OnRecordResourceSize(reinterpret_cast(*pImage), mem_reqs.size); + } + } + return result; +} + +// Intercept image destruction to clean up tracked handle state. +VKAPI_ATTR void VKAPI_CALL vkDestroyImage(VkDevice device, VkImage image, const VkAllocationCallbacks* pAllocator) { + if (image != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnDestroyObject(reinterpret_cast(image)); + } + PFN_vkDestroyImage fpDestroyImage = (PFN_vkDestroyImage)device_dispatch_table(device)->DestroyImage; + if (fpDestroyImage != NULL) { + fpDestroyImage(device, image, pAllocator); + } +} + +// Intercept buffer creation to track buffer usage category flags and requested size. +VKAPI_ATTR VkResult VKAPI_CALL vkCreateBuffer(VkDevice device, const VkBufferCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, VkBuffer* pBuffer) { + PFN_vkCreateBuffer fpCreateBuffer = (PFN_vkCreateBuffer)device_dispatch_table(device)->CreateBuffer; + VkResult result = fpCreateBuffer(device, pCreateInfo, pAllocator, pBuffer); + if (result == VK_SUCCESS && pCreateInfo != nullptr && pBuffer != nullptr && *pBuffer != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnCreateBuffer(reinterpret_cast(*pBuffer), pCreateInfo->usage, pCreateInfo->size); + + // Query buffer memory requirements to record accurate actual bound size, + // in case the app relies on vkGetDeviceBufferMemoryRequirements. + if (device_dispatch_table(device)->GetBufferMemoryRequirements) { + VkMemoryRequirements mem_reqs; + device_dispatch_table(device)->GetBufferMemoryRequirements(device, *pBuffer, &mem_reqs); + DeviceMemoryReport::Get().OnRecordResourceSize(reinterpret_cast(*pBuffer), mem_reqs.size); + } + } + return result; +} + +// Intercept buffer destruction to clean up tracked handle state. +VKAPI_ATTR void VKAPI_CALL vkDestroyBuffer(VkDevice device, VkBuffer buffer, const VkAllocationCallbacks* pAllocator) { + if (buffer != VK_NULL_HANDLE) { + DeviceMemoryReport::Get().OnDestroyObject(reinterpret_cast(buffer)); + } + PFN_vkDestroyBuffer fpDestroyBuffer = (PFN_vkDestroyBuffer)device_dispatch_table(device)->DestroyBuffer; + if (fpDestroyBuffer != NULL) { + fpDestroyBuffer(device, buffer, pAllocator); + } +} + +// Intercept image memory requirements query to record virtual resource size. +VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements(VkDevice device, VkImage image, VkMemoryRequirements* pMemoryRequirements) { + if (device_dispatch_table(device)->GetImageMemoryRequirements) { + device_dispatch_table(device)->GetImageMemoryRequirements(device, image, pMemoryRequirements); + if (image != VK_NULL_HANDLE && pMemoryRequirements != nullptr) { + DeviceMemoryReport::Get().OnRecordResourceSize(reinterpret_cast(image), pMemoryRequirements->size); + } + } +} + +static void RecordImageRequirements(const VkImageMemoryRequirementsInfo2* pInfo, const VkMemoryRequirements2* pMemoryRequirements) { + if (pInfo != nullptr && pInfo->image != VK_NULL_HANDLE && pMemoryRequirements != nullptr) { + DeviceMemoryReport::Get().OnRecordResourceSize(reinterpret_cast(pInfo->image), pMemoryRequirements->memoryRequirements.size); + } +} + +// Intercept image memory requirements 2 query to record virtual resource size. +VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements2(VkDevice device, const VkImageMemoryRequirementsInfo2* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + if (device_dispatch_table(device)->GetImageMemoryRequirements2) { + device_dispatch_table(device)->GetImageMemoryRequirements2(device, pInfo, pMemoryRequirements); + RecordImageRequirements(pInfo, pMemoryRequirements); + } +} + +// Intercept image memory requirements 2 KHR query to record virtual resource size. +VKAPI_ATTR void VKAPI_CALL vkGetImageMemoryRequirements2KHR(VkDevice device, const VkImageMemoryRequirementsInfo2* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + if (device_dispatch_table(device)->GetImageMemoryRequirements2KHR) { + device_dispatch_table(device)->GetImageMemoryRequirements2KHR(device, pInfo, pMemoryRequirements); + RecordImageRequirements(pInfo, pMemoryRequirements); + } +} + +// Intercept buffer memory requirements query to record virtual resource size. +VKAPI_ATTR void VKAPI_CALL vkGetBufferMemoryRequirements(VkDevice device, VkBuffer buffer, VkMemoryRequirements* pMemoryRequirements) { + if (device_dispatch_table(device)->GetBufferMemoryRequirements) { + device_dispatch_table(device)->GetBufferMemoryRequirements(device, buffer, pMemoryRequirements); + if (buffer != VK_NULL_HANDLE && pMemoryRequirements != nullptr) { + DeviceMemoryReport::Get().OnRecordResourceSize(reinterpret_cast(buffer), pMemoryRequirements->size); + } + } +} + +static void RecordBufferRequirements2(const VkBufferMemoryRequirementsInfo2* pInfo, const VkMemoryRequirements2* pMemoryRequirements) { + if (pInfo != nullptr && pInfo->buffer != VK_NULL_HANDLE && pMemoryRequirements != nullptr) { + DeviceMemoryReport::Get().OnRecordResourceSize(reinterpret_cast(pInfo->buffer), pMemoryRequirements->memoryRequirements.size); + } +} + +// Intercept buffer memory requirements 2 query to record virtual resource size. +VKAPI_ATTR void VKAPI_CALL vkGetBufferMemoryRequirements2(VkDevice device, const VkBufferMemoryRequirementsInfo2* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + if (device_dispatch_table(device)->GetBufferMemoryRequirements2) { + device_dispatch_table(device)->GetBufferMemoryRequirements2(device, pInfo, pMemoryRequirements); + RecordBufferRequirements2(pInfo, pMemoryRequirements); + } +} + +// Intercept buffer memory requirements 2 KHR query to record virtual resource size. +VKAPI_ATTR void VKAPI_CALL vkGetBufferMemoryRequirements2KHR(VkDevice device, const VkBufferMemoryRequirementsInfo2* pInfo, VkMemoryRequirements2* pMemoryRequirements) { + if (device_dispatch_table(device)->GetBufferMemoryRequirements2KHR) { + device_dispatch_table(device)->GetBufferMemoryRequirements2KHR(device, pInfo, pMemoryRequirements); + RecordBufferRequirements2(pInfo, pMemoryRequirements); + } +} + +} // extern "C" diff --git a/layersvt/device_memory_report/device_memory_report_perfetto.cpp b/layersvt/device_memory_report/device_memory_report_perfetto.cpp new file mode 100644 index 0000000000..01b4dc258e --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report_perfetto.cpp @@ -0,0 +1,94 @@ +/* Copyright (C) 2026 Google 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 "device_memory_report_perfetto.h" +#include +#include +#include +#include +#include + +PERFETTO_TRACK_EVENT_STATIC_STORAGE(); + +void InitializeDeviceMemoryReportPerfetto() { + static std::once_flag init_flag; + std::call_once(init_flag, []() { + perfetto::TracingInitArgs args; + args.backends = perfetto::kSystemBackend; + perfetto::Tracing::Initialize(args); + perfetto::TrackEvent::Register(); + }); +} + +namespace { +static const std::unordered_map kStaticTracks = { + // App usage tracks + {"vulkan.mem.app.usage.staging_transfer", perfetto::CounterTrack("vulkan.mem.app.usage.staging_transfer").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.ray_tracing", perfetto::CounterTrack("vulkan.mem.app.usage.ray_tracing").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.indirect_gpu_driven", perfetto::CounterTrack("vulkan.mem.app.usage.indirect_gpu_driven").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.compute_storage", perfetto::CounterTrack("vulkan.mem.app.usage.compute_storage").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.uniform_constants", perfetto::CounterTrack("vulkan.mem.app.usage.uniform_constants").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.geometry_mesh", perfetto::CounterTrack("vulkan.mem.app.usage.geometry_mesh").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.transform_feedback", perfetto::CounterTrack("vulkan.mem.app.usage.transform_feedback").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.general_buffer", perfetto::CounterTrack("vulkan.mem.app.usage.general_buffer").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.transient_memoryless", perfetto::CounterTrack("vulkan.mem.app.usage.transient_memoryless").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.storage_compute_image", perfetto::CounterTrack("vulkan.mem.app.usage.storage_compute_image").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.depth_stencil_target", perfetto::CounterTrack("vulkan.mem.app.usage.depth_stencil_target").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.color_render_target", perfetto::CounterTrack("vulkan.mem.app.usage.color_render_target").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.static_texture", perfetto::CounterTrack("vulkan.mem.app.usage.static_texture").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.general_image", perfetto::CounterTrack("vulkan.mem.app.usage.general_image").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.app.usage.unbound_memory", perfetto::CounterTrack("vulkan.mem.app.usage.unbound_memory").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + + // Driver usage tracks + {"vulkan.mem.driver.usage.staging_transfer", perfetto::CounterTrack("vulkan.mem.driver.usage.staging_transfer").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.ray_tracing", perfetto::CounterTrack("vulkan.mem.driver.usage.ray_tracing").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.indirect_gpu_driven", perfetto::CounterTrack("vulkan.mem.driver.usage.indirect_gpu_driven").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.compute_storage", perfetto::CounterTrack("vulkan.mem.driver.usage.compute_storage").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.uniform_constants", perfetto::CounterTrack("vulkan.mem.driver.usage.uniform_constants").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.geometry_mesh", perfetto::CounterTrack("vulkan.mem.driver.usage.geometry_mesh").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.transform_feedback", perfetto::CounterTrack("vulkan.mem.driver.usage.transform_feedback").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.general_buffer", perfetto::CounterTrack("vulkan.mem.driver.usage.general_buffer").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.transient_memoryless", perfetto::CounterTrack("vulkan.mem.driver.usage.transient_memoryless").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.storage_compute_image", perfetto::CounterTrack("vulkan.mem.driver.usage.storage_compute_image").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.depth_stencil_target", perfetto::CounterTrack("vulkan.mem.driver.usage.depth_stencil_target").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.color_render_target", perfetto::CounterTrack("vulkan.mem.driver.usage.color_render_target").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.static_texture", perfetto::CounterTrack("vulkan.mem.driver.usage.static_texture").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.general_image", perfetto::CounterTrack("vulkan.mem.driver.usage.general_image").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, + {"vulkan.mem.driver.usage.unbound_memory", perfetto::CounterTrack("vulkan.mem.driver.usage.unbound_memory").set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES)}, +}; +} // namespace + +perfetto::CounterTrack GetCounterTrack(std::string_view name) { + auto it = kStaticTracks.find(name); + if (it != kStaticTracks.end()) { + return it->second; + } + static std::mutex dynamic_mutex; + static std::unordered_map dynamic_tracks; + static std::unordered_set dynamic_names; + std::lock_guard lock(dynamic_mutex); + auto dyn_it = dynamic_tracks.find(std::string(name)); + if (dyn_it != dynamic_tracks.end()) { + return dyn_it->second; + } + auto name_it = dynamic_names.insert(std::string(name)).first; + perfetto::CounterTrack track = perfetto::CounterTrack(perfetto::DynamicString(name_it->c_str())) + .set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES); + dynamic_tracks.emplace( + std::piecewise_construct, + std::forward_as_tuple(*name_it), + std::forward_as_tuple(track)); + return track; +} diff --git a/layersvt/device_memory_report/device_memory_report_perfetto.h b/layersvt/device_memory_report/device_memory_report_perfetto.h new file mode 100644 index 0000000000..b3a7645668 --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report_perfetto.h @@ -0,0 +1,30 @@ +/* Copyright (C) 2026 Google 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. + */ + +#ifndef LAYERSVT_DEVICE_MEMORY_REPORT_PERFETTO_H +#define LAYERSVT_DEVICE_MEMORY_REPORT_PERFETTO_H + +#include "perfetto/perfetto.h" +#include + +// Define categories used for Perfetto tracing in DeviceMemoryReport layer. +PERFETTO_DEFINE_CATEGORIES( + perfetto::Category("VulkanDeviceMemoryReport").SetDescription("Vulkan Device Memory Report Counters") +); + +void InitializeDeviceMemoryReportPerfetto(); +perfetto::CounterTrack GetCounterTrack(std::string_view name); + +#endif // LAYERSVT_DEVICE_MEMORY_REPORT_PERFETTO_H diff --git a/layersvt/test/CMakeLists.txt b/layersvt/test/CMakeLists.txt index ee14406ea3..6f7bdbd646 100644 --- a/layersvt/test/CMakeLists.txt +++ b/layersvt/test/CMakeLists.txt @@ -37,6 +37,9 @@ function(LayerTest NAME) if (${NAME} STREQUAL "DebugMarker") target_sources(${TEST_NAME} PRIVATE ../debug_marker/debug_marker.cpp ../debug_marker/debug_marker_perfetto.cpp ../perfetto/perfetto.cc) target_include_directories(${TEST_NAME} PRIVATE .. ../debug_marker) + elseif (${NAME} STREQUAL "DeviceMemoryReport") + target_sources(${TEST_NAME} PRIVATE ../device_memory_report/device_memory_report.cpp ../device_memory_report/device_memory_report_perfetto.cpp ../perfetto/perfetto.cc) + target_include_directories(${TEST_NAME} PRIVATE .. ../device_memory_report) endif() target_compile_definitions(${TEST_NAME} PUBLIC LAYER_BINARY_PATH="$") add_test(NAME ${TEST_NAME} COMMAND ${TEST_NAME}) @@ -48,7 +51,7 @@ function(LayerTest NAME) endif() endfunction() -set(LAYER_TEST_FILES api_dump monitor screenshot CPUTiming DebugMarker) +set(LAYER_TEST_FILES api_dump monitor screenshot CPUTiming DebugMarker DeviceMemoryReport) foreach(test_item ${LAYER_TEST_FILES}) # If the target doesn't exist continue. diff --git a/layersvt/test/test_devicememoryreport.cpp b/layersvt/test/test_devicememoryreport.cpp new file mode 100644 index 0000000000..6fc6c639b9 --- /dev/null +++ b/layersvt/test/test_devicememoryreport.cpp @@ -0,0 +1,499 @@ +/* Copyright (C) 2026 Google 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 "layer_test_helper.h" +#include "device_memory_report.h" +#include "device_memory_report_perfetto.h" + +#include + +#include + +static const char* kLayerName = "VK_LAYER_GOOGLE_DeviceMemoryReport"; + +class DeviceMemoryReportTests : public VkTestFramework { + public: + ~DeviceMemoryReportTests(){}; + + static void SetUpTestSuite() {} + static void TearDownTestSuite(){}; +}; + +TEST_F(DeviceMemoryReportTests, InitLayer) { + TEST_DESCRIPTION("Test Creating a Vulkan Instance with DeviceMemoryReport layer"); + + layer_test::VulkanInstanceBuilder inst_builder; + VkResult err = inst_builder.Init(kLayerName); + EXPECT_EQ(err, VK_SUCCESS); + + VkInstance instance = inst_builder.GetInstance(); + EXPECT_NE(instance, VK_NULL_HANDLE); + + inst_builder.Reset(); +} + +TEST_F(DeviceMemoryReportTests, PhysicalDeviceEnumeration) { + TEST_DESCRIPTION("Test physical device to instance mapping"); + + layer_test::VulkanInstanceBuilder inst_builder; + VkResult err = inst_builder.Init(kLayerName); + EXPECT_EQ(err, VK_SUCCESS); + + VkInstance instance = inst_builder.GetInstance(); + EXPECT_NE(instance, VK_NULL_HANDLE); + + VkPhysicalDevice phys_dev = reinterpret_cast(0x1234); + DeviceMemoryReport::Get().SetVkInstance(phys_dev, instance); + EXPECT_EQ(DeviceMemoryReport::Get().GetVkInstance(phys_dev), instance); + + inst_builder.Reset(); +} + +TEST_F(DeviceMemoryReportTests, ExtensionProperties) { + TEST_DESCRIPTION("Test extension enumeration for instance and device"); + + layer_test::VulkanInstanceBuilder inst_builder; + VkResult err = inst_builder.Init(kLayerName); + EXPECT_EQ(err, VK_SUCCESS); + + VkInstance instance = inst_builder.GetInstance(); + EXPECT_NE(instance, VK_NULL_HANDLE); + + // Test instance extension properties advertised by the layer + uint32_t inst_ext_count = 0; + EXPECT_EQ(vkEnumerateInstanceExtensionProperties(kLayerName, &inst_ext_count, nullptr), VK_SUCCESS); + EXPECT_EQ(inst_ext_count, 0u); + + VkPhysicalDevice phys_dev = VK_NULL_HANDLE; + inst_builder.GetPhysicalDevice(&phys_dev); + if (phys_dev != VK_NULL_HANDLE) { + // Test device extension properties advertised by the layer + uint32_t dev_ext_count = 0; + EXPECT_EQ(vkEnumerateDeviceExtensionProperties(phys_dev, kLayerName, &dev_ext_count, nullptr), VK_SUCCESS); + EXPECT_GE(dev_ext_count, 1u); + std::vector dev_exts(dev_ext_count); + EXPECT_EQ(vkEnumerateDeviceExtensionProperties(phys_dev, kLayerName, &dev_ext_count, dev_exts.data()), VK_SUCCESS); + bool found_mem_report = false; + for (const auto& ext : dev_exts) { + if (strcmp(ext.extensionName, VK_EXT_DEVICE_MEMORY_REPORT_EXTENSION_NAME) == 0) { + found_mem_report = true; + } + } + EXPECT_TRUE(found_mem_report); + } + + inst_builder.Reset(); +} + +TEST_F(DeviceMemoryReportTests, EmitEventsAndSubCounters) { + TEST_DESCRIPTION("Test calling OnMemoryReportEvent with object types and allocating/freeing memory"); + + InitializeDeviceMemoryReportPerfetto(); + + VkDeviceMemoryReportCallbackDataEXT cb_data = {}; + cb_data.sType = VK_STRUCTURE_TYPE_DEVICE_MEMORY_REPORT_CALLBACK_DATA_EXT; + cb_data.flags = 0; // Application memory + cb_data.type = VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_ALLOCATE_EXT; + cb_data.memoryObjectId = 0x1000; + cb_data.size = 1024 * 1024; + cb_data.objectType = VK_OBJECT_TYPE_IMAGE; + cb_data.objectHandle = 0x5000; + + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + + cb_data.type = VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT; + cb_data.size = 512 * 1024; + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + + cb_data.flags = VK_DEVICE_MEMORY_REPORT_FLAG_INTERNAL_OBJECT_BIT_EXT; // Driver memory + cb_data.type = VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_ALLOCATE_EXT; + cb_data.objectType = VK_OBJECT_TYPE_BUFFER; + cb_data.size = 2048; + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + + // Test direct allocate/free fallbacks + VkDevice dummy_device = reinterpret_cast(0x1234); + VkDeviceMemory dummy_memory = reinterpret_cast(0x5678); + DeviceMemoryReport::Get().OnAllocateMemory(dummy_device, dummy_memory, 4096); + DeviceMemoryReport::Get().OnFreeMemory(dummy_device, dummy_memory); + + EXPECT_TRUE(true); +} + +TEST_F(DeviceMemoryReportTests, BufferImageBindingAndCallbackSuppression) { + TEST_DESCRIPTION("Test buffer/image memory binding, IMPORT/UNIMPORT events, and fallback suppression when callback is enabled"); + + InitializeDeviceMemoryReportPerfetto(); + + // Test buffer and image memory binding + uint64_t buffer_handle = 0x8000; + uint64_t image_handle = 0x8001; + uint64_t memory_handle = 0x9000; + DeviceMemoryReport::Get().OnCreateBuffer(buffer_handle, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, 1024); + DeviceMemoryReport::Get().OnCreateImage(image_handle, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + DeviceMemoryReport::Get().OnRecordResourceSize(image_handle, 2048); + DeviceMemoryReport::Get().OnBindBufferMemory(buffer_handle, memory_handle, 0); + DeviceMemoryReport::Get().OnBindImageMemory(image_handle, memory_handle, 1024); + + // Test IMPORT_EXT and UNIMPORT_EXT events + VkDeviceMemoryReportCallbackDataEXT cb_data = {}; + cb_data.sType = VK_STRUCTURE_TYPE_DEVICE_MEMORY_REPORT_CALLBACK_DATA_EXT; + cb_data.flags = 0; + cb_data.type = VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_IMPORT_EXT; + cb_data.memoryObjectId = 0x3000; + cb_data.size = 4096; + cb_data.objectType = VK_OBJECT_TYPE_DEVICE_MEMORY; + cb_data.objectHandle = memory_handle; + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + + cb_data.type = VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_UNIMPORT_EXT; + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + + // Test callback suppression: when callback is registered on VkDevice, fallback allocations should be ignored + VkDevice test_device = reinterpret_cast(0xABCD); + VkDeviceMemory test_memory = reinterpret_cast(0xEF01); + + DeviceMemoryReport::Get().SetHasMemoryReportCallback(test_device, true); + DeviceMemoryReport::Get().OnAllocateMemory(test_device, test_memory, 8192); + DeviceMemoryReport::Get().OnFreeMemory(test_device, test_memory); + + // Enable fallback by turning off callback flag and verify allocate/free work + DeviceMemoryReport::Get().SetHasMemoryReportCallback(test_device, false); + DeviceMemoryReport::Get().OnAllocateMemory(test_device, test_memory, 8192); + DeviceMemoryReport::Get().OnFreeMemory(test_device, test_memory); + + EXPECT_TRUE(true); +} + +TEST_F(DeviceMemoryReportTests, UsageTypeBreakdown) { + TEST_DESCRIPTION("Test automatic usage type classification for granular image and buffer usage categories"); + + InitializeDeviceMemoryReportPerfetto(); + + uint64_t color_img = 0xA001; + uint64_t depth_img = 0xA002; + uint64_t sampled_img = 0xA005; + uint64_t storage_img = 0xA006; + uint64_t transient_img = 0xA007; + + uint64_t vtx_buf = 0xB001; + uint64_t idx_buf = 0xB002; + uint64_t ubo_buf = 0xB003; + uint64_t staging_buf = 0xB006; + uint64_t storage_buf = 0xB007; + uint64_t indirect_buf = 0xB008; + + uint64_t mem_handle = 0xC001; + + // Register images + DeviceMemoryReport::Get().OnCreateImage(color_img, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + DeviceMemoryReport::Get().OnCreateImage(depth_img, VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT); + DeviceMemoryReport::Get().OnCreateImage(sampled_img, VK_IMAGE_USAGE_SAMPLED_BIT); + DeviceMemoryReport::Get().OnCreateImage(storage_img, VK_IMAGE_USAGE_STORAGE_BIT); + DeviceMemoryReport::Get().OnCreateImage(transient_img, VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT); + + DeviceMemoryReport::Get().OnRecordResourceSize(color_img, 65536); + DeviceMemoryReport::Get().OnRecordResourceSize(depth_img, 65536); + DeviceMemoryReport::Get().OnRecordResourceSize(sampled_img, 65536); + DeviceMemoryReport::Get().OnRecordResourceSize(storage_img, 65536); + DeviceMemoryReport::Get().OnRecordResourceSize(transient_img, 65536); + + // Register buffers + DeviceMemoryReport::Get().OnCreateBuffer(vtx_buf, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, 4096); + DeviceMemoryReport::Get().OnCreateBuffer(idx_buf, VK_BUFFER_USAGE_INDEX_BUFFER_BIT, 4096); + DeviceMemoryReport::Get().OnCreateBuffer(ubo_buf, VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, 4096); + DeviceMemoryReport::Get().OnCreateBuffer(staging_buf, VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, 4096); + DeviceMemoryReport::Get().OnCreateBuffer(storage_buf, VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, 4096); + DeviceMemoryReport::Get().OnCreateBuffer(indirect_buf, VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, 4096); + + VkDeviceMemoryReportCallbackDataEXT cb_data = {}; + cb_data.sType = VK_STRUCTURE_TYPE_DEVICE_MEMORY_REPORT_CALLBACK_DATA_EXT; + cb_data.flags = 0; + cb_data.type = VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_ALLOCATE_EXT; + cb_data.memoryObjectId = 0x5000; + cb_data.size = 2097152; + cb_data.objectType = VK_OBJECT_TYPE_DEVICE_MEMORY; + cb_data.objectHandle = mem_handle; + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + DeviceMemoryReport::Get().SetAllocationMemoryProperties(mem_handle, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT); + + // Bind images and buffers to test usage categorization and track transitions + DeviceMemoryReport::Get().OnBindImageMemory(color_img, mem_handle, 0); + DeviceMemoryReport::Get().OnBindImageMemory(depth_img, mem_handle, 65536); + DeviceMemoryReport::Get().OnBindImageMemory(sampled_img, mem_handle, 131072); + DeviceMemoryReport::Get().OnBindImageMemory(storage_img, mem_handle, 196608); + DeviceMemoryReport::Get().OnBindImageMemory(transient_img, mem_handle, 262144); + + DeviceMemoryReport::Get().OnBindBufferMemory(vtx_buf, mem_handle, 327680); + DeviceMemoryReport::Get().OnBindBufferMemory(idx_buf, mem_handle, 331776); + DeviceMemoryReport::Get().OnBindBufferMemory(ubo_buf, mem_handle, 335872); + DeviceMemoryReport::Get().OnBindBufferMemory(staging_buf, mem_handle, 339968); + DeviceMemoryReport::Get().OnBindBufferMemory(storage_buf, mem_handle, 344064); + DeviceMemoryReport::Get().OnBindBufferMemory(indirect_buf, mem_handle, 348160); + + // Free memory + cb_data.type = VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT; + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + + // Clean up objects + DeviceMemoryReport::Get().OnDestroyObject(color_img); + DeviceMemoryReport::Get().OnDestroyObject(depth_img); + DeviceMemoryReport::Get().OnDestroyObject(sampled_img); + DeviceMemoryReport::Get().OnDestroyObject(storage_img); + DeviceMemoryReport::Get().OnDestroyObject(transient_img); + + DeviceMemoryReport::Get().OnDestroyObject(vtx_buf); + DeviceMemoryReport::Get().OnDestroyObject(idx_buf); + DeviceMemoryReport::Get().OnDestroyObject(ubo_buf); + DeviceMemoryReport::Get().OnDestroyObject(staging_buf); + DeviceMemoryReport::Get().OnDestroyObject(storage_buf); + DeviceMemoryReport::Get().OnDestroyObject(indirect_buf); + + EXPECT_TRUE(true); +} + +TEST_F(DeviceMemoryReportTests, MemoryAliasingAndOverlap) { + TEST_DESCRIPTION("Test memory aliasing where overlapping virtual resources occupy the same physical memory slab"); + + InitializeDeviceMemoryReportPerfetto(); + + uint64_t mem_handle = 0xD001; + uint64_t image_a = 0xD101; + uint64_t image_b = 0xD102; + uint64_t buffer_c = 0xD103; + + // Step 1: Allocate a 10,000-byte continuous physical memory slab (VkDeviceMemory). + // Initial state: total = 10,000 B, bound = 0 B, unbound_memory = 10,000 B. + VkDeviceMemoryReportCallbackDataEXT cb_data = {}; + cb_data.sType = VK_STRUCTURE_TYPE_DEVICE_MEMORY_REPORT_CALLBACK_DATA_EXT; + cb_data.type = VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_ALLOCATE_EXT; + cb_data.memoryObjectId = 0x6000; + cb_data.size = 10000; + cb_data.objectType = VK_OBJECT_TYPE_DEVICE_MEMORY; + cb_data.objectHandle = mem_handle; + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + + // Step 2: Bind Resource A (Color Attachment Image) to range [0, 4000) (size = 4000 B). + // - vulkan.mem.app.usage.color_render_target += 4,000 B + // - Merged intervals: [0, 4000) -> bound_size = 4,000 B, unbound_memory = 6,000 B + DeviceMemoryReport::Get().OnCreateImage(image_a, VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT); + DeviceMemoryReport::Get().OnRecordResourceSize(image_a, 4000); + DeviceMemoryReport::Get().OnBindImageMemory(image_a, mem_handle, 0); + + // Step 3: Bind Resource B (Sampled Texture Image) to range [2000, 6000) (size = 4000 B). + // This overlaps / aliases Resource A on the physical sub-range [2000, 4000). + // - Each virtual resource adds its full virtual size to its specific category track: + // vulkan.mem.app.usage.static_texture += 4,000 B (both A and B report active virtual capacity). + // - Overlapping intervals [0, 4000) and [2000, 6000) are merged into union [0, 6000). + // - Physical slab bound_size = 6,000 B (overlapping physical region is NOT double-counted). + // - Remaining unbound headroom: unbound_memory = 10,000 - 6,000 = 4,000 B. + DeviceMemoryReport::Get().OnCreateImage(image_b, VK_IMAGE_USAGE_SAMPLED_BIT); + DeviceMemoryReport::Get().OnRecordResourceSize(image_b, 4000); + DeviceMemoryReport::Get().OnBindImageMemory(image_b, mem_handle, 2000); + + // Step 4: Bind Resource C (Vertex Buffer) to disjoint range [8000, 9500) (size = 1500 B). + // - vulkan.mem.app.usage.geometry_mesh += 1,500 B + // - Interval union: [0, 6000) U [8000, 9500) -> bound_size = 6,000 + 1,500 = 7,500 B. + // - Remaining unbound headroom: unbound_memory = 10,000 - 7,500 = 2,500 B. + DeviceMemoryReport::Get().OnCreateBuffer(buffer_c, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, 1500); + DeviceMemoryReport::Get().OnBindBufferMemory(buffer_c, mem_handle, 8000); + + // Step 5: Destroy Resource A. + // - vulkan.mem.app.usage.color_render_target -= 4,000 B + // - Interval [0, 4000) is removed. Remaining intervals: [2000, 6000) U [8000, 9500). + // - Recalculated bound_size = 4,000 + 1,500 = 5,500 B. + // - Updated unbound headroom: unbound_memory = 10,000 - 5,500 = 4,500 B. + DeviceMemoryReport::Get().OnDestroyObject(image_a); + + // Step 6: Free physical memory slab. + // - All remaining sub-allocations on this slab are cleaned up and unbound counter is reset. + cb_data.type = VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT; + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + + // Step 7: Clean up remaining virtual resource object handles. + DeviceMemoryReport::Get().OnDestroyObject(image_b); + DeviceMemoryReport::Get().OnDestroyObject(buffer_c); + + EXPECT_TRUE(true); +} + +TEST_F(DeviceMemoryReportTests, ClusterClassificationFunctions) { + TEST_DESCRIPTION("Test cluster classification for buffer and image usages with memory properties"); + + // Buffer clusters + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), "staging_transfer"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT), "staging_transfer"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), "staging_transfer"); + // Staging without host-visible property falls through to general_buffer + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_TRANSFER_SRC_BIT, 0), "general_buffer"); + // Host-visible functional buffers should NOT be misclassified as staging + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), "geometry_mesh"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_INDEX_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), "geometry_mesh"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), "uniform_constants"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), "uniform_constants"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), "compute_storage"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), "compute_storage"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT), "indirect_gpu_driven"); + + // Ray tracing + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_STORAGE_BIT_KHR, 0), "ray_tracing"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_ACCELERATION_STRUCTURE_BUILD_INPUT_READ_ONLY_BIT_KHR, 0), "ray_tracing"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_SHADER_BINDING_TABLE_BIT_KHR, 0), "ray_tracing"); + + // Indirect GPU driven + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_INDIRECT_BUFFER_BIT, 0), "indirect_gpu_driven"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_CONDITIONAL_RENDERING_BIT_EXT, 0), "indirect_gpu_driven"); + + // Compute storage + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_STORAGE_BUFFER_BIT, 0), "compute_storage"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT, 0), "compute_storage"); + + // Uniform constants + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT, 0), "uniform_constants"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT, 0), "uniform_constants"); + + // Geometry mesh + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, 0), "geometry_mesh"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_INDEX_BUFFER_BIT, 0), "geometry_mesh"); + + // Transform feedback + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_BUFFER_BIT_EXT, 0), "transform_feedback"); + EXPECT_STREQ(GetBufferCluster(VK_BUFFER_USAGE_TRANSFORM_FEEDBACK_COUNTER_BUFFER_BIT_EXT, 0), "transform_feedback"); + + // General buffer + EXPECT_STREQ(GetBufferCluster(0, 0), "general_buffer"); + + // Image clusters + // Transient memoryless + EXPECT_STREQ(GetImageCluster(VK_IMAGE_USAGE_TRANSIENT_ATTACHMENT_BIT, 0), "transient_memoryless"); + EXPECT_STREQ(GetImageCluster(0, VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT), "transient_memoryless"); + + // Storage compute image + EXPECT_STREQ(GetImageCluster(VK_IMAGE_USAGE_STORAGE_BIT, 0), "storage_compute_image"); + + // Depth stencil target + EXPECT_STREQ(GetImageCluster(VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT, 0), "depth_stencil_target"); + + // Color render target + EXPECT_STREQ(GetImageCluster(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT, 0), "color_render_target"); + EXPECT_STREQ(GetImageCluster(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, 0), "color_render_target"); + + // Static texture + EXPECT_STREQ(GetImageCluster(VK_IMAGE_USAGE_SAMPLED_BIT, 0), "static_texture"); + + // General image + EXPECT_STREQ(GetImageCluster(0, 0), "general_image"); +} + +TEST_F(DeviceMemoryReportTests, StaticCounterTrackLookup) { + TEST_DESCRIPTION("Test static counter track lookup and dynamic fallback track creation"); + + perfetto::CounterTrack track_staging = GetCounterTrack("vulkan.mem.app.usage.staging_transfer"); + EXPECT_EQ(track_staging.Serialize().counter().unit(), perfetto::protos::gen::CounterDescriptor::UNIT_SIZE_BYTES); + EXPECT_NE(track_staging.uuid, 0u); + + perfetto::CounterTrack track_storage = GetCounterTrack("vulkan.mem.app.usage.compute_storage"); + EXPECT_EQ(track_storage.Serialize().counter().unit(), perfetto::protos::gen::CounterDescriptor::UNIT_SIZE_BYTES); + EXPECT_NE(track_storage.uuid, 0u); + + perfetto::CounterTrack track_unbound = GetCounterTrack("vulkan.mem.driver.usage.unbound_memory"); + EXPECT_EQ(track_unbound.Serialize().counter().unit(), perfetto::protos::gen::CounterDescriptor::UNIT_SIZE_BYTES); + EXPECT_NE(track_unbound.uuid, 0u); + + perfetto::CounterTrack dynamic_track = GetCounterTrack("vulkan.mem.app.custom_track"); + EXPECT_EQ(dynamic_track.Serialize().counter().unit(), perfetto::protos::gen::CounterDescriptor::UNIT_SIZE_BYTES); + EXPECT_NE(dynamic_track.uuid, 0u); + + // Re-query the dynamic track to ensure persistent registry returns consistent track + perfetto::CounterTrack dynamic_track_again = GetCounterTrack("vulkan.mem.app.custom_track"); + EXPECT_EQ(dynamic_track.uuid, dynamic_track_again.uuid); + EXPECT_EQ(dynamic_track_again.Serialize().counter().unit(), perfetto::protos::gen::CounterDescriptor::UNIT_SIZE_BYTES); +} + +TEST_F(DeviceMemoryReportTests, ProactiveMemoryRequirementsQuery) { + TEST_DESCRIPTION("Test that the layer proactively queries memory requirements when creating images and buffers"); + + layer_test::VulkanInstanceBuilder inst_builder; + VkResult err = inst_builder.Init(kLayerName); + EXPECT_EQ(err, VK_SUCCESS); + + VkPhysicalDevice phys_dev = VK_NULL_HANDLE; + inst_builder.GetPhysicalDevice(&phys_dev); + if (phys_dev == VK_NULL_HANDLE) { + GTEST_SKIP() << "No physical device found, skipping test."; + } + + // Create a logical device + float queue_priority = 1.0f; + VkDeviceQueueCreateInfo queue_info = {}; + queue_info.sType = VK_STRUCTURE_TYPE_DEVICE_QUEUE_CREATE_INFO; + queue_info.queueFamilyIndex = 0; + queue_info.queueCount = 1; + queue_info.pQueuePriorities = &queue_priority; + + VkDeviceCreateInfo dev_info = {}; + dev_info.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO; + dev_info.queueCreateInfoCount = 1; + dev_info.pQueueCreateInfos = &queue_info; + dev_info.enabledExtensionCount = 0; + + VkDevice device = VK_NULL_HANDLE; + err = vkCreateDevice(phys_dev, &dev_info, nullptr, &device); + if (err != VK_SUCCESS) { + GTEST_SKIP() << "Failed to create logical device, skipping test."; + } + + // Create an image + VkImageCreateInfo img_info = {}; + img_info.sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO; + img_info.imageType = VK_IMAGE_TYPE_2D; + img_info.format = VK_FORMAT_R8G8B8A8_UNORM; + img_info.extent = {64, 64, 1}; + img_info.mipLevels = 1; + img_info.arrayLayers = 1; + img_info.samples = VK_SAMPLE_COUNT_1_BIT; + img_info.tiling = VK_IMAGE_TILING_OPTIMAL; + img_info.usage = VK_IMAGE_USAGE_SAMPLED_BIT; + img_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + img_info.initialLayout = VK_IMAGE_LAYOUT_UNDEFINED; + + VkImage image = VK_NULL_HANDLE; + err = vkCreateImage(device, &img_info, nullptr, &image); + ASSERT_EQ(err, VK_SUCCESS); + + // The interceptor should have called OnRecordResourceSize. + // Verify that the recorded size is > 0. + VkDeviceSize img_size = DeviceMemoryReport::Get().GetRecordedResourceSize(reinterpret_cast(image)); + EXPECT_GT(img_size, 0); + + // Create a buffer + VkBufferCreateInfo buf_info = {}; + buf_info.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; + buf_info.size = 1024; + buf_info.usage = VK_BUFFER_USAGE_TRANSFER_SRC_BIT; + buf_info.sharingMode = VK_SHARING_MODE_EXCLUSIVE; + + VkBuffer buffer = VK_NULL_HANDLE; + err = vkCreateBuffer(device, &buf_info, nullptr, &buffer); + ASSERT_EQ(err, VK_SUCCESS); + + // The interceptor should have called OnRecordResourceSize. + VkDeviceSize buf_size = DeviceMemoryReport::Get().GetRecordedResourceSize(reinterpret_cast(buffer)); + EXPECT_GT(buf_size, 0); + + vkDestroyImage(device, image, nullptr); + vkDestroyBuffer(device, buffer, nullptr); + vkDestroyDevice(device, nullptr); +}