diff --git a/CMakeLists.txt b/CMakeLists.txt index b80cbd2318..e16790f3a6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,6 +48,7 @@ if (BUILD_WERROR) endif() option(BUILD_TESTS "Build tests") +option(BUILD_DEVICEMEMORYREPORT "Build DeviceMemoryReport layer" ON) option(RUN_ON_CI "Build only tests that can run on C.I." ON) if(BUILD_TESTS) 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..12e17d232b --- /dev/null +++ b/layersvt/device_memory_report/VkLayer_DeviceMemoryReport.def @@ -0,0 +1,22 @@ +; 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 +vkEnumerateDeviceExtensionProperties +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..53550e8338 --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report.cpp @@ -0,0 +1,281 @@ +/* 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) { + std::lock_guard lock(map_mutex_); + auto it = vk_instance_map_.find(phys_dev); + if (it != vk_instance_map_.end()) return it->second; + return VK_NULL_HANDLE; +} + +void VKAPI_PTR DeviceMemoryReport::MemoryReportCallback(const VkDeviceMemoryReportCallbackDataEXT* pCallbackData, void* pUserData) { + DeviceMemoryReport::Get().OnMemoryReportEvent(pCallbackData); +} + +// Maps Vulkan image or buffer usage flags to a Perfetto memory track usage category name. +static const char* GetUsageCategoryName(bool is_image, uint32_t usage_flags) { + if (is_image) { + if (usage_flags & VK_IMAGE_USAGE_DEPTH_STENCIL_ATTACHMENT_BIT) return "depth_stencil_attachment"; + if (usage_flags & VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT) return "color_attachment"; + if (usage_flags & VK_IMAGE_USAGE_SAMPLED_BIT) return "texture"; + if (usage_flags & (VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT)) return "transfer_image"; + return "image"; + } + if (usage_flags & VK_BUFFER_USAGE_VERTEX_BUFFER_BIT) return "vertex_buffer"; + if (usage_flags & VK_BUFFER_USAGE_INDEX_BUFFER_BIT) return "index_buffer"; + if (usage_flags & VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT) return "uniform_buffer"; + if (usage_flags & (VK_BUFFER_USAGE_TRANSFER_SRC_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT)) return "staging_buffer"; + return "buffer"; +} + +// 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("vulkan", GetCounterTrack(track.c_str()), 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("vulkan", GetCounterTrack(track.c_str()), 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() && !res_it->second.usage.empty()) { + track_name = res_it->second.usage; + } + 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::OnBindBufferMemory(uint64_t buffer_handle, uint64_t memory_handle, VkDeviceSize memory_offset) { + std::lock_guard lock(counter_mutex_); + auto res_it = resources_.find(buffer_handle); + if (res_it == resources_.end() || res_it->second.usage.empty() || res_it->second.size == 0) return; + + // If the same resource handle is passed more than once, remove stale bindings first. + RemoveResourceBinding(buffer_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.usage); + + // 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({ buffer_handle, memory_offset, res_size, new_usage_track }); + resource_to_memory_map_[buffer_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::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::OnBindImageMemory(uint64_t image_handle, uint64_t memory_handle, VkDeviceSize memory_offset) { + OnBindBufferMemory(image_handle, memory_handle, memory_offset); +} + +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_); + resources_[image_handle].usage = GetUsageCategoryName(true, 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_); + resources_[buffer_handle] = { GetUsageCategoryName(false, usage), 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) { + std::lock_guard lock(counter_mutex_); + if (has_callback_map_[device]) return; + uint64_t handle = reinterpret_cast(memory); + auto& allocation = memory_allocations_[handle]; + 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..5a2ce91eaf --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report.h @@ -0,0 +1,252 @@ +/* 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 + +/** + * 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 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. + */ + void OnAllocateMemory(VkDevice device, VkDeviceMemory memory, VkDeviceSize size); + + /** + * @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; + 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 { + std::string usage; + VkDeviceSize size = 0; + }; + + /** + * @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 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..2cfa666673 --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report_handwritten_dispatch.cpp @@ -0,0 +1,110 @@ +/* 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, "vkEnumerateDeviceExtensionProperties") == 0) return reinterpret_cast(vkEnumerateDeviceExtensionProperties); + 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..42b43af7ff --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report_handwritten_functions.h @@ -0,0 +1,511 @@ +/* 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 & debugging intercepts: +// - vkAllocateMemory / vkFreeMemory: Tracks direct allocations/frees as fallbacks. +// - vkBindBufferMemory / vkBindImageMemory: Associates buffer/image handles with memory. +// - vkSetDebugUtilsObjectNameEXT / vkDebugMarkerSetObjectNameEXT: Associates debug names +// and markers with object handles for labeled memory reporting. +// - 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" + +static std::once_flag g_perfetto_init_flag; + +extern "C" { + +VKAPI_ATTR VkResult VKAPI_CALL vkCreateInstance(const VkInstanceCreateInfo* pCreateInfo, const VkAllocationCallbacks* pAllocator, + VkInstance* pInstance) { + std::call_once(g_perfetto_init_flag, []() { 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"); + + // 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) { + std::call_once(g_perfetto_init_flag, []() { 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"); + + // 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 (vk_instance != VK_NULL_HANDLE && instance_dispatch_table(vk_instance)->EnumerateDeviceExtensionProperties) { + if (instance_dispatch_table(vk_instance)->EnumerateDeviceExtensionProperties(physicalDevice, nullptr, &ext_count, nullptr) == VK_SUCCESS && ext_count > 0) { + std::vector exts(ext_count); + if (instance_dispatch_table(vk_instance)->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); + } + + return result; +} + +VKAPI_ATTR void VKAPI_CALL vkDestroyDevice(VkDevice device, const VkAllocationCallbacks* pAllocator) { + 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); + } + 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) { + static const VkExtensionProperties instanceExtensions[] = { + {VK_EXT_DEBUG_UTILS_EXTENSION_NAME, VK_EXT_DEBUG_UTILS_SPEC_VERSION}, + }; + + if (pLayerName != nullptr && strcmp(pLayerName, LAYER_NAME) == 0) { + return util_GetExtensionProperties(ARRAY_SIZE(instanceExtensions), instanceExtensions, pPropertyCount, 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); +} + +EXPORT_FUNCTION VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateDeviceExtensionProperties(VkPhysicalDevice physicalDevice, + const char* pLayerName, + uint32_t* pPropertyCount, + VkExtensionProperties* pProperties) { + static const VkExtensionProperties deviceExtensions[] = { + {VK_EXT_DEVICE_MEMORY_REPORT_EXTENSION_NAME, VK_EXT_DEVICE_MEMORY_REPORT_SPEC_VERSION}, + {VK_EXT_DEBUG_MARKER_EXTENSION_NAME, VK_EXT_DEBUG_MARKER_SPEC_VERSION}, + }; + + if (pLayerName != nullptr && strcmp(pLayerName, LAYER_NAME) == 0) { + return util_GetExtensionProperties(ARRAY_SIZE(deviceExtensions), deviceExtensions, pPropertyCount, pProperties); + } + + if (physicalDevice == nullptr) { + return VK_SUCCESS; + } + + VkInstance vk_instance = DeviceMemoryReport::Get().GetVkInstance(physicalDevice); + if (vk_instance == VK_NULL_HANDLE) { + return VK_SUCCESS; + } + + // Manually append device extension. This should not be necessary, but the Android Vulkan + // loader does not expose extensions from implicit layer. + if (pProperties == nullptr) { + VkResult res = instance_dispatch_table(vk_instance)->EnumerateDeviceExtensionProperties(physicalDevice, pLayerName, pPropertyCount, pProperties); + if (res == VK_SUCCESS) { + (*pPropertyCount) += ARRAY_SIZE(deviceExtensions); + } + return res; + } + + if (*pPropertyCount > 0) { + uint32_t requestedCount = *pPropertyCount; + VkResult res = instance_dispatch_table(vk_instance)->EnumerateDeviceExtensionProperties(physicalDevice, pLayerName, pPropertyCount, pProperties); + if (res == VK_SUCCESS) { + uint32_t originalCount = *pPropertyCount; + uint32_t additionalCount = 0; + + for (uint32_t i = 0; i < ARRAY_SIZE(deviceExtensions); ++i) { + bool found = false; + for (uint32_t j = 0; j < originalCount; ++j) { + if (strcmp(pProperties[j].extensionName, deviceExtensions[i].extensionName) == 0) { + found = true; + break; + } + } + if (!found) { + if (originalCount + additionalCount < requestedCount) { + pProperties[originalCount + additionalCount] = deviceExtensions[i]; + } + additionalCount++; + } + } + *pPropertyCount = originalCount + additionalCount; + if (*pPropertyCount > requestedCount) { + *pPropertyCount = requestedCount; + } + } + return res; + } + return VK_SUCCESS; +} + +// 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..b84eaf28bd --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report_perfetto.cpp @@ -0,0 +1,34 @@ +/* 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" + +PERFETTO_TRACK_EVENT_STATIC_STORAGE(); + +void InitializeDeviceMemoryReportPerfetto() { + static bool initialized = false; + if (initialized) return; + initialized = true; + + perfetto::TracingInitArgs args; + args.backends = perfetto::kSystemBackend; + perfetto::Tracing::Initialize(args); + perfetto::TrackEvent::Register(); +} + +perfetto::CounterTrack GetCounterTrack(const char* name) { + return perfetto::CounterTrack(perfetto::DynamicString(name)) + .set_unit(perfetto::CounterTrack::Unit::UNIT_SIZE_BYTES); +} 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..43739643de --- /dev/null +++ b/layersvt/device_memory_report/device_memory_report_perfetto.h @@ -0,0 +1,29 @@ +/* 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" + +// Define categories used for Perfetto tracing in DeviceMemoryReport layer. +PERFETTO_DEFINE_CATEGORIES( + perfetto::Category("vulkan").SetDescription("Vulkan Device Memory Report Counters") +); + +void InitializeDeviceMemoryReportPerfetto(); +perfetto::CounterTrack GetCounterTrack(const char* 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..65334959f9 --- /dev/null +++ b/layersvt/test/test_devicememoryreport.cpp @@ -0,0 +1,334 @@ +/* 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, 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 vtx_buf = 0xB001; + uint64_t idx_buf = 0xB002; + uint64_t ubo_buf = 0xB003; + uint64_t staging_buf = 0xB006; + + 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().OnRecordResourceSize(color_img, 65536); + DeviceMemoryReport::Get().OnRecordResourceSize(depth_img, 65536); + DeviceMemoryReport::Get().OnRecordResourceSize(sampled_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); + + 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 = 1048576; + cb_data.objectType = VK_OBJECT_TYPE_DEVICE_MEMORY; + cb_data.objectHandle = mem_handle; + DeviceMemoryReport::MemoryReportCallback(&cb_data, nullptr); + + // 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().OnBindBufferMemory(vtx_buf, mem_handle, 196608); + DeviceMemoryReport::Get().OnBindBufferMemory(idx_buf, mem_handle, 200704); + DeviceMemoryReport::Get().OnBindBufferMemory(ubo_buf, mem_handle, 204800); + DeviceMemoryReport::Get().OnBindBufferMemory(staging_buf, mem_handle, 208896); + + // 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(vtx_buf); + DeviceMemoryReport::Get().OnDestroyObject(idx_buf); + DeviceMemoryReport::Get().OnDestroyObject(ubo_buf); + DeviceMemoryReport::Get().OnDestroyObject(staging_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_attachment += 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.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.vertex_buffer += 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_attachment -= 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, 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); +}