Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
2dfaa2a
device_memory_report: publish object names from the memory layer
jimblacklercorp Sep 21, 2026
f9e551b
device_memory_report: require VkObjectType in OnDestroyObject
jimblacklercorp Sep 22, 2026
984a1c4
device_memory_report: remove redundant name cleanup in RemoveAllocati…
jimblacklercorp Sep 22, 2026
0042524
device_memory_report: use structured bindings in EmitAllDebugObjectNames
jimblacklercorp Sep 22, 2026
41130cb
device_memory_report: assert non-null pNameInfo in object naming inte…
jimblacklercorp Sep 22, 2026
be5c52b
device_memory_report: support standalone VK_EXT_debug_utils and VK_EX…
jimblacklercorp Sep 22, 2026
6cb9fc3
device_memory_report: add dispatch unit tests for debug object naming
jimblacklercorp Sep 22, 2026
d48b857
device_memory_report: document standalone debug extension support in …
jimblacklercorp Sep 22, 2026
77e2dd9
device_memory_report: deduplicate and unify vkEnumerateDeviceExtensio…
jimblacklercorp Sep 22, 2026
0ccafa6
device_memory_report: initialize *pMessenger in vkCreateDebugUtilsMes…
jimblacklercorp Sep 22, 2026
dccb929
device_memory_report: remove vkEnumerateDeviceExtensionProperties fro…
jimblacklercorp Sep 22, 2026
20da836
device_memory_report: keep default hidden visibility on DeviceMemoryR…
jimblacklercorp Sep 22, 2026
ca6d634
device_memory_report: deduplicate DeviceMemoryReportTestPeer into sha…
jimblacklercorp Sep 22, 2026
9f39424
device_memory_report: emit empty-name clear event on object destroy a…
jimblacklercorp Sep 22, 2026
a31caf5
device_memory_report: retire VkDeviceMemory debug names after DESTROY…
jimblacklercorp Sep 22, 2026
df1f87d
device_memory_report: resize downstream_extensions after fill query
jimblacklercorp Sep 22, 2026
5000cac
device_memory_report: expose vkEnumerateDeviceLayerProperties in NULL…
jimblacklercorp Sep 22, 2026
586864a
device_memory_report: inline devmemreport_EnumerateDeviceExtensionPro…
jimblacklercorp Sep 22, 2026
1a43369
device_memory_report: guard and clamp the downstream device extension…
jimblacklercorp Sep 23, 2026
864f268
device_memory_report: honour LLP_LAYER_15 in vkEnumerateInstanceExten…
jimblacklercorp Sep 23, 2026
86c0e94
device_memory_report: accept VK_INCOMPLETE in the vkCreateDevice exte…
jimblacklercorp Sep 23, 2026
a683d06
device_memory_report: make FakeInstance non-copyable and expand test …
jimblacklercorp Sep 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions layersvt/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ if(BUILD_DEVICEMEMORYREPORT)
endif()

target_compile_definitions(VkLayer_DeviceMemoryReport PRIVATE VK_ENABLE_BETA_EXTENSIONS)
if(UNIX AND NOT APPLE)
target_link_options(VkLayer_DeviceMemoryReport PRIVATE "-Wl,-Bsymbolic-functions")
endif()
endif()

if (BUILD_TESTS AND NOT RUN_ON_GITHUB)
Expand Down
10 changes: 10 additions & 0 deletions layersvt/device_memory_report/VkLayer_DeviceMemoryReport.json.in
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,20 @@
"api_version": "@JSON_VERSION@",
"implementation_version": "1",
"description": "Vulkan Device Memory Report Layer",
"instance_extensions": [
{
"name": "VK_EXT_debug_utils",
"spec_version": "2"
}
],
"device_extensions": [
{
"name": "VK_EXT_device_memory_report",
"spec_version": "2"
},
{
"name": "VK_EXT_debug_marker",
"spec_version": "4"
}
]
}
Expand Down
142 changes: 122 additions & 20 deletions layersvt/device_memory_report/device_memory_report.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,23 @@ void EmitAllocationTraceEvent(const AllocationTraceEvent& event) {
"memory_type", event.memory_type);
}

/**
* @brief Writes one object name to the trace, for consumers to join onto memory events by handle.
*
* Emits a "VulkanObjectName" instant event under the "VulkanDeviceMemoryReport" category with
* three debug annotations:
* - "object_type" (int32_t): the VkObjectType value of the named object.
* - "object_handle" (uint64_t): the raw Vulkan handle of the object.
* - "object_name" (string): the debug name assigned to the object, or an empty string when a
* previously assigned name has been cleared.
*/
void EmitDebugObjectName(VkObjectType object_type, uint64_t object_handle, std::string_view name) {
TRACE_EVENT_INSTANT("VulkanDeviceMemoryReport", "VulkanObjectName",
"object_type", static_cast<int32_t>(object_type),
"object_handle", object_handle,
"object_name", name);
}

} // namespace

void DeviceMemoryReport::RemoveResourceBinding(uint64_t resource_handle) {
Expand Down Expand Up @@ -402,6 +419,7 @@ void DeviceMemoryReport::Reset() {
resource_to_memory_map_.clear();
memory_allocations_.clear();
usage_memory_bytes_.clear();
debug_object_names_.clear();
}
void DeviceMemoryReport::OnCreateImage(uint64_t image_handle, VkImageUsageFlags usage) {
std::lock_guard<std::mutex> lock(counter_mutex_);
Expand All @@ -428,10 +446,77 @@ void DeviceMemoryReport::OnCreateBuffer(uint64_t buffer_handle, VkBufferUsageFla
}
}

void DeviceMemoryReport::OnDestroyObject(uint64_t object_handle) {
void DeviceMemoryReport::OnDestroyObject(uint64_t object_handle, VkObjectType object_type) {
std::lock_guard<std::mutex> lock(counter_mutex_);
RemoveResourceBinding(object_handle);
resources_.erase(object_handle);
if (debug_object_names_.erase(std::make_pair(object_type, object_handle)) > 0) {
EmitDebugObjectName(object_type, object_handle, "");
}
}
Comment thread
jimblacklercorp marked this conversation as resolved.

void DeviceMemoryReport::SetDebugObjectName(VkObjectType object_type, uint64_t object_handle, const char* name) {
// Other types would be trace volume that nothing reads; VK_LAYER_GOOGLE_DebugMarker names them
// all for consumers that need it.
switch (object_type) {
case VK_OBJECT_TYPE_BUFFER:
case VK_OBJECT_TYPE_IMAGE:
case VK_OBJECT_TYPE_DEVICE_MEMORY:
break;
default:
return;
}

// A null or empty name clears the name, and the clear still has to be published. A view, not a
// string: the overwhelmingly common call is an application re-applying a name that has not
// changed, some do it every frame, and that path must not allocate.
const std::string_view new_name = name ? std::string_view(name) : std::string_view();
const auto key = std::make_pair(object_type, object_handle);

std::lock_guard<std::mutex> lock(counter_mutex_);

auto existing = debug_object_names_.find(key);
if (existing == debug_object_names_.end()) {
// Nothing stored and nothing to store: no state change to publish.
if (new_name.empty()) return;
debug_object_names_.emplace(key, new_name);
} else if (existing->second == new_name) {
// Applications re-apply the same name routinely, so only a change is worth publishing.
return;
} else if (new_name.empty()) {
debug_object_names_.erase(existing);
} else {
// Assigning in place reuses the capacity already allocated for the previous name.
existing->second.assign(new_name);
}
EmitDebugObjectName(object_type, object_handle, new_name);
}

void DeviceMemoryReport::SetDebugObjectName(VkDebugReportObjectTypeEXT object_type, uint64_t object_handle, const char* name) {
// VK_EXT_debug_marker names objects with the legacy VkDebugReportObjectTypeEXT enum. Only the
// types this layer attributes memory to are mapped.
VkObjectType mapped_type = VK_OBJECT_TYPE_UNKNOWN;
switch (object_type) {
case VK_DEBUG_REPORT_OBJECT_TYPE_BUFFER_EXT:
mapped_type = VK_OBJECT_TYPE_BUFFER;
break;
case VK_DEBUG_REPORT_OBJECT_TYPE_IMAGE_EXT:
mapped_type = VK_OBJECT_TYPE_IMAGE;
break;
case VK_DEBUG_REPORT_OBJECT_TYPE_DEVICE_MEMORY_EXT:
mapped_type = VK_OBJECT_TYPE_DEVICE_MEMORY;
break;
default:
return;
}
SetDebugObjectName(mapped_type, object_handle, name);
}

void DeviceMemoryReport::EmitAllDebugObjectNames() {
Comment thread
jimblacklercorp marked this conversation as resolved.
for (const auto& [key, name] : debug_object_names_) {
const auto& [object_type, object_handle] = key;
EmitDebugObjectName(object_type, object_handle, name);
}
}

void DeviceMemoryReport::DumpCurrentCountersAndAllocations() {
Expand Down Expand Up @@ -476,6 +561,11 @@ void DeviceMemoryReport::DumpCurrentCountersAndAllocations() {
});
}
}

// Names are replayed after the allocations for the same reason they are replayed at all: a
// session that attaches mid-run never saw the naming calls, and a name with no allocation to
// attach to is meaningless.
EmitAllDebugObjectNames();
}

void DeviceMemoryReport::OnMemoryReportEvent(const VkDeviceMemoryReportCallbackDataEXT* pCallbackData) {
Expand Down Expand Up @@ -504,12 +594,12 @@ void DeviceMemoryReport::OnMemoryReportEvent(const VkDeviceMemoryReportCallbackD
} else if (pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT ||
pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_UNIMPORT_EXT) {
auto allocation_iterator = memory_allocations_.find(key);
if (allocation_iterator == memory_allocations_.end()) return;

memory_type = is_driver ? allocation_iterator->second.cluster_name : "unbound_memory";
event_size = allocation_iterator->second.total_size;
RemoveAllocationTracking(key);
operation_name = "DESTROY";
if (allocation_iterator != memory_allocations_.end()) {
memory_type = is_driver ? allocation_iterator->second.cluster_name : "unbound_memory";
event_size = allocation_iterator->second.total_size;
RemoveAllocationTracking(key);
operation_name = "DESTROY";
}
}

if (operation_name != nullptr) {
Expand All @@ -523,6 +613,13 @@ void DeviceMemoryReport::OnMemoryReportEvent(const VkDeviceMemoryReportCallbackD
.memory_type = memory_type,
});
}

const bool is_free = pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT ||
pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_UNIMPORT_EXT;
if (is_free && !is_driver && pCallbackData->objectType == VK_OBJECT_TYPE_DEVICE_MEMORY &&
debug_object_names_.erase(std::make_pair(VK_OBJECT_TYPE_DEVICE_MEMORY, key)) > 0) {
EmitDebugObjectName(VK_OBJECT_TYPE_DEVICE_MEMORY, key, "");
}
}

void DeviceMemoryReport::SetHasMemoryReportCallback(VkDevice device, bool has_callback) {
Expand Down Expand Up @@ -552,6 +649,7 @@ void DeviceMemoryReport::OnAllocateMemory(VkDevice device, VkDeviceMemory memory
} else {
allocation.total_size = size;
allocation.is_driver = false;
allocation.object_type = VK_OBJECT_TYPE_DEVICE_MEMORY;
allocation.object_handle = handle;
UpdateAllocationUnboundCounter(handle);

Expand All @@ -570,20 +668,24 @@ void DeviceMemoryReport::OnAllocateMemory(VkDevice device, VkDeviceMemory memory
void DeviceMemoryReport::OnFreeMemory(VkDevice device, VkDeviceMemory memory) {
std::lock_guard<std::mutex> lock(counter_mutex_);
if (has_callback_map_[device]) return;

uint64_t handle = reinterpret_cast<uint64_t>(memory);
auto allocation_iterator = memory_allocations_.find(handle);
if (allocation_iterator == memory_allocations_.end()) return;

VkDeviceSize freed_size = allocation_iterator->second.total_size;
RemoveAllocationTracking(handle);
if (allocation_iterator != memory_allocations_.end()) {
VkDeviceSize freed_size = allocation_iterator->second.total_size;
RemoveAllocationTracking(handle);

EmitAllocationTraceEvent({
.operation = "DESTROY",
.source = "DEVICE_MEMORY",
.memory_object_id = handle,
.size = freed_size,
.offset = 0,
.object_handle = handle,
.memory_type = "unbound_memory",
});
EmitAllocationTraceEvent({
.operation = "DESTROY",
.source = "DEVICE_MEMORY",
.memory_object_id = handle,
.size = freed_size,
.offset = 0,
.object_handle = handle,
.memory_type = "unbound_memory",
});
}
if (debug_object_names_.erase(std::make_pair(VK_OBJECT_TYPE_DEVICE_MEMORY, handle)) > 0) {
EmitDebugObjectName(VK_OBJECT_TYPE_DEVICE_MEMORY, handle, "");
}
Comment thread
jimblacklercorp marked this conversation as resolved.
}
65 changes: 62 additions & 3 deletions layersvt/device_memory_report/device_memory_report.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
#pragma once

#include <vulkan/vulkan.h>
#include <map>
#include <mutex>
#include <unordered_map>
#include <string>
#include <utility>
#include <vector>

#ifndef VK_DEVICE_MEMORY_REPORT_FLAG_INTERNAL_OBJECT_BIT_EXT
Expand Down Expand Up @@ -52,12 +54,18 @@ const char* GetImageCluster(VkImageUsageFlags usage, VkMemoryPropertyFlags memFl
* 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.
* Object bindings (vkBindBufferMemory, vkBindImageMemory, vkBindBufferMemory2, vkBindImageMemory2)
* are tracked to attribute memory allocations to usage categories. The layer also advertises and
* intercepts VK_EXT_debug_utils and VK_EXT_debug_marker (stripping VK_EXT_debug_marker at device
* creation when unsupported downstream) so debug object names for buffers, images, and device
* memory are published standalone without requiring VK_LAYER_GOOGLE_DebugMarker.
*
* 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.<category>)
* - Instant events under the "VulkanDeviceMemoryReport" category ("VulkanMemoryAllocation" and
* "VulkanObjectName")
*
* This class is a singleton and provides thread-safe access to its state.
*/
Expand Down Expand Up @@ -205,10 +213,43 @@ class DeviceMemoryReport {
void DumpCurrentCountersAndAllocations();

/**
* @brief Handles destruction of a Vulkan object, cleaning up tracked usage state.
* @brief Handles destruction of a Vulkan object, cleaning up tracked usage state and any
* recorded debug name.
*
* Handles are only unique within an object type, so @p object_type is required to clear the
* destroyed object's name without dropping the name of an unrelated live object that shares
* the handle value.
*
* @param object_handle The 64-bit handle of the destroyed Vulkan object.
* @param object_type The type of the destroyed object, as a VkObjectType.
*/
void OnDestroyObject(uint64_t object_handle);
void OnDestroyObject(uint64_t object_handle, VkObjectType object_type);

/**
* @brief Records the debug name an application gave to a Vulkan object and publishes it.
*
* Names arrive through VK_EXT_debug_utils or VK_EXT_debug_marker, typically well after the
* object was created, so they are published as their own event stream rather than attached to
* the memory events. Only the object types this layer attributes memory to are kept.
*
* @param object_type The type of the object, as a VkObjectType.
* @param object_handle The 64-bit handle of the object.
* @param name The name given by the application. A null or empty name clears the stored name.
*/
void SetDebugObjectName(VkObjectType object_type, uint64_t object_handle, const char* name);

/**
* @brief Records the debug name an application gave to a Vulkan object via the legacy
* VK_EXT_debug_marker extension and publishes it.
*
* Maps the legacy VkDebugReportObjectTypeEXT enum to VkObjectType for the object types this
* layer attributes memory to, and ignores all other types.
*
* @param object_type The type of the object, as a VkDebugReportObjectTypeEXT.
* @param object_handle The 64-bit handle of the object.
* @param name The name given by the application. A null or empty name clears the stored name.
*/
void SetDebugObjectName(VkDebugReportObjectTypeEXT object_type, uint64_t object_handle, const char* name);

private:
friend class DeviceMemoryReportTestPeer;
Expand Down Expand Up @@ -276,6 +317,11 @@ class DeviceMemoryReport {
*/
void RemoveAllocationTracking(uint64_t memory_handle);

/**
* @brief Republishes every known object name. Called while counter_mutex_ is held.
*/
void EmitAllDebugObjectNames();

/**
* @brief Increments trace counter for a memory track.
*/
Expand Down Expand Up @@ -330,4 +376,17 @@ class DeviceMemoryReport {
* @brief Maps a usage track name to its current total memory usage in bytes.
*/
std::unordered_map<std::string, uint64_t> usage_memory_bytes_;

/**
* @brief Maps a pair of (object_type, object_handle) to the name the application gave the object.
*
* Handles are only unique within an object type, hence the pair key. Like resources_ and
* memory_allocations_, keys are not scoped by VkDevice, and entries persist across
* OnDestroyDevice until the object itself is destroyed or Reset() is called. Objects not
* destroyed through vkDestroyImage, vkDestroyBuffer, or vkFreeMemory (such as presentable
* VkImages owned by a VkSwapchainKHR) retain their entries until Reset() or until a recycled
* handle is renamed. Guarded by counter_mutex_. std::map is used instead of unordered_map for
* pair key support and deterministic replay order.
*/
std::map<std::pair<VkObjectType, uint64_t>, std::string> debug_object_names_;
};
Loading
Loading