Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
100 changes: 85 additions & 15 deletions layersvt/device_memory_report/device_memory_report.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,20 @@
#include "device_memory_report_perfetto.h"
#include "perfetto/perfetto.h"
#include <algorithm>
#include <cassert>
#include <string>
#include <string_view>
#include <vector>

namespace {

// Returns the number of bytes of [offset, offset + size) that lie within an allocation of total_size bytes.
VkDeviceSize ClampToAllocation(VkDeviceSize offset, VkDeviceSize size, VkDeviceSize total_size) {
return offset < total_size ? std::min(size, total_size - offset) : 0;
}

} // namespace

DeviceMemoryReport& DeviceMemoryReport::Get() {
static DeviceMemoryReport instance;
return instance;
Expand Down Expand Up @@ -182,12 +192,14 @@ void DeviceMemoryReport::UpdateAllocationUnboundCounter(const AllocationKey& key
std::vector<std::pair<VkDeviceSize, VkDeviceSize>> intervals;
intervals.reserve(allocation.sub_allocations.size());
for (const auto& suballocation : allocation.sub_allocations) {
// Only the part of a binding that lies within the allocation is bound memory of it; an
// out-of-range binding must not hide unbound bytes. Clamping here, not at bind time, means
// bindings recorded against a placeholder (total_size == 0) are counted once the size is known.
if (suballocation.size > 0 && suballocation.offset < allocation.total_size) {
VkDeviceSize end = suballocation.offset + std::min(suballocation.size, allocation.total_size - suballocation.offset);
intervals.emplace_back(suballocation.offset, end);
// Sub-allocations are clamped to the allocation when bound and by ClampSubAllocationsToAllocation
// once a placeholder's size is known, so a stored binding never extends past total_size.
assert(allocation.total_size == 0 || suballocation.offset + suballocation.size <= allocation.total_size);
// Clamp anyway in release builds: a binding past the end would otherwise make the unsigned
// unbound size below wrap around.
const VkDeviceSize bound = ClampToAllocation(suballocation.offset, suballocation.size, allocation.total_size);
if (bound > 0) {
intervals.emplace_back(suballocation.offset, suballocation.offset + bound);
}
}
if (!intervals.empty()) {
Expand Down Expand Up @@ -286,7 +298,7 @@ void EmitDebugObjectName(VkObjectType object_type, uint64_t object_handle, std::

} // namespace

void DeviceMemoryReport::RemoveResourceBinding(const ObjectKey& resource_key, Resource& resource) {
void DeviceMemoryReport::RemoveResourceBinding(const ObjectKey& resource_key, Resource& resource, bool update_unbound_counter) {
if (resource.bound_memory == 0) return;

const uint64_t memory_handle = resource.bound_memory;
Expand All @@ -309,7 +321,9 @@ void DeviceMemoryReport::RemoveResourceBinding(const ObjectKey& resource_key, Re
VkDeviceSize suballocation_offset = it->offset;
SubtractCounterBytes(it->usage_track, suballocation_size);
suballocations.erase(it);
UpdateAllocationUnboundCounter(AllocationKey::App(memory_handle));
if (update_unbound_counter) {
UpdateAllocationUnboundCounter(AllocationKey::App(memory_handle));
}

EmitAllocationTraceEvent({
.operation = "DESTROY",
Expand All @@ -334,24 +348,35 @@ void DeviceMemoryReport::BindResourceMemory(VkObjectType object_type, uint64_t r

// If the same resource handle is passed more than once, remove stale bindings first.
RemoveResourceBinding(resource_key, res_it->second);
BindResourceMemoryLocked(resource_key, res_it->second, memory_handle, memory_offset);
}

void DeviceMemoryReport::BindResourceMemoryLocked(const ObjectKey& resource_key, Resource& resource, uint64_t memory_handle, VkDeviceSize memory_offset) {
const auto& [object_type, resource_handle] = resource_key;
const AllocationKey key = AllocationKey::App(memory_handle);
auto& allocation = memory_allocations_[key];
VkDeviceSize res_size = res_it->second.size;
std::string new_usage_track = GetUsageTrackName(allocation.is_driver, res_it->second.GetCluster(allocation.mem_flags));
VkDeviceSize res_size = resource.size;
// Clamp the bound resource size to the backing VkDeviceMemory allocation when its size is known,
// so drivers returning a worst-case requirement (e.g. for imported AHardwareBuffers) or out-of-range
// bindings do not inflate usage counters or snapshot BIND records beyond the physical allocation (b/578430447).
if (allocation.total_size > 0) {
Comment thread
olehkuznetsov marked this conversation as resolved.
Resolved
res_size = ClampToAllocation(memory_offset, res_size, allocation.total_size);
if (res_size == 0) return;
}
std::string new_usage_track = GetUsageTrackName(allocation.is_driver, resource.GetCluster(allocation.mem_flags));

// Suballocations represent individual resources (like buffers or images) that are bound
// to specific offset regions within a single large memory allocation.
// We add a record here to track this specific resource's footprint within the larger memory block.
allocation.sub_allocations.push_back({ object_type, resource_handle, memory_offset, res_size, new_usage_track });
res_it->second.bound_memory = memory_handle;
resource.bound_memory = memory_handle;

// Each distinct virtual resource handle adds its virtual size to its specific category track upon binding.
// Each distinct virtual resource handle adds its clamped size to its specific category track upon binding.
AddCounterBytes(new_usage_track, res_size);
UpdateAllocationUnboundCounter(key);

bool is_image = (object_type == VK_OBJECT_TYPE_IMAGE);
const char* cluster_name = res_it->second.GetCluster(allocation.mem_flags);
const char* cluster_name = resource.GetCluster(allocation.mem_flags);
EmitAllocationTraceEvent({
.operation = "BIND",
.source = is_image ? "IMAGE" : "BUFFER",
Expand All @@ -364,6 +389,37 @@ void DeviceMemoryReport::BindResourceMemory(VkObjectType object_type, uint64_t r
});
}

void DeviceMemoryReport::ClampSubAllocationsToAllocation(const AllocationKey& key) {
auto allocation_it = memory_allocations_.find(key);
if (allocation_it == memory_allocations_.end() || allocation_it->second.total_size == 0) return;
const MemoryAllocation& allocation = allocation_it->second;

struct Overhang {
ObjectKey resource_key;
Resource* resource;
VkDeviceSize offset;
};
std::vector<Overhang> overhangs;
for (const auto& suballocation : allocation.sub_allocations) {
if (ClampToAllocation(suballocation.offset, suballocation.size, allocation.total_size) == suballocation.size) continue;
const ObjectKey resource_key = {suballocation.object_type, suballocation.resource_handle};
auto res_it = resources_.find(resource_key);
// Resources are unbound before they are erased, so every binding has a tracked resource.
assert(res_it != resources_.end());
if (res_it != resources_.end()) {
overhangs.push_back({resource_key, &res_it->second, suballocation.offset});
}
}
// Unbind every overhanging resource before re-binding any, so the unbound counter is only
// recomputed once all remaining bindings lie within the allocation.
for (const auto& overhang : overhangs) {
RemoveResourceBinding(overhang.resource_key, *overhang.resource, /*update_unbound_counter=*/false);
}
for (const auto& overhang : overhangs) {
BindResourceMemoryLocked(overhang.resource_key, *overhang.resource, key.id, overhang.offset);
}
}

void DeviceMemoryReport::OnBindBufferMemory(uint64_t buffer_handle, uint64_t memory_handle, VkDeviceSize memory_offset) {
BindResourceMemory(VK_OBJECT_TYPE_BUFFER, buffer_handle, memory_handle, memory_offset);
}
Expand Down Expand Up @@ -665,7 +721,10 @@ void DeviceMemoryReport::OnMemoryReportEvent(const VkDeviceMemoryReportCallbackD
allocation.is_driver = is_driver;
allocation.object_type = pCallbackData->objectType;
allocation.object_handle = pCallbackData->objectHandle;
UpdateAllocationUnboundCounter(key);
if (is_driver) {
Comment thread
olehkuznetsov marked this conversation as resolved.
Resolved
// Also resolves cluster_name for the CREATE event. App allocations are updated below.
UpdateAllocationUnboundCounter(key);
}
operation_name = "CREATE";
memory_type = is_driver ? allocation.cluster_name : "unbound_memory";
} else if (pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_FREE_EXT ||
Expand Down Expand Up @@ -694,6 +753,14 @@ void DeviceMemoryReport::OnMemoryReportEvent(const VkDeviceMemoryReportCallbackD
});
}

if (!is_driver && (pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_ALLOCATE_EXT ||
pCallbackData->type == VK_DEVICE_MEMORY_REPORT_EVENT_TYPE_IMPORT_EXT)) {
// Bindings recorded against a placeholder before its size was known are clamped after CREATE,
// so the corrective DESTROY/BIND events follow it in the trace.
ClampSubAllocationsToAllocation(key);
UpdateAllocationUnboundCounter(key);
}

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 &&
Expand Down Expand Up @@ -732,7 +799,6 @@ void DeviceMemoryReport::OnAllocateMemory(VkDevice device, VkDeviceMemory memory
allocation.is_driver = false;
allocation.object_type = VK_OBJECT_TYPE_DEVICE_MEMORY;
allocation.object_handle = handle;
UpdateAllocationUnboundCounter(key);

EmitAllocationTraceEvent({
.operation = "CREATE",
Expand All @@ -744,6 +810,10 @@ void DeviceMemoryReport::OnAllocateMemory(VkDevice device, VkDeviceMemory memory
.object_handle = handle,
.memory_type = "unbound_memory",
});

// Clamp bindings recorded against a placeholder after CREATE (see OnMemoryReportEvent).
ClampSubAllocationsToAllocation(key);
UpdateAllocationUnboundCounter(key);
}
}

Expand Down
16 changes: 15 additions & 1 deletion layersvt/device_memory_report/device_memory_report.h
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,19 @@ class DeviceMemoryReport {
*/
void BindResourceMemory(VkObjectType object_type, uint64_t resource_handle, uint64_t memory_handle, VkDeviceSize memory_offset);

/**
* @brief Records a binding of an already-unbound resource; caller must hold counter_mutex_.
* The bound size is clamped to the allocation when its size is known, and a binding that lies
* entirely outside the allocation is dropped.
*/
void BindResourceMemoryLocked(const ObjectKey& resource_key, Resource& resource, uint64_t memory_handle, VkDeviceSize memory_offset);

/**
* @brief Re-binds sub-allocations recorded against a placeholder (before total_size was known)
* that extend past the allocation, so their sizes, counters, and trace events are clamped to it.
*/
void ClampSubAllocationsToAllocation(const AllocationKey& key);

/**
* @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).
Expand All @@ -343,8 +356,9 @@ class DeviceMemoryReport {

/**
* @brief Removes a sub-allocation of a resource from a physical memory slab if bound.
* @param update_unbound_counter Whether to recompute the slab's unbound counter afterwards.
*/
void RemoveResourceBinding(const ObjectKey& resource_key, Resource& resource);
void RemoveResourceBinding(const ObjectKey& resource_key, Resource& resource, bool update_unbound_counter = true);

/**
* @brief Removes all sub-allocations and tracking for a memory slab being freed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,46 +359,6 @@ EXPORT_FUNCTION VKAPI_ATTR VkResult VKAPI_CALL vkEnumerateDeviceExtensionPropert
}


// 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) {
auto* table = device_dispatch_table(device);
assert(table->BindBufferMemory != nullptr);
assert(buffer != VK_NULL_HANDLE);
assert(memory != VK_NULL_HANDLE);
if (DeviceMemoryReport::Get().GetRecordedResourceSize(VK_OBJECT_TYPE_BUFFER, reinterpret_cast<uint64_t>(buffer)) == 0) {
if (table->GetBufferMemoryRequirements) {
VkMemoryRequirements mem_reqs;
table->GetBufferMemoryRequirements(device, buffer, &mem_reqs);
DeviceMemoryReport::Get().OnRecordResourceSize(VK_OBJECT_TYPE_BUFFER, reinterpret_cast<uint64_t>(buffer), mem_reqs.size);
}
}
VkResult result = table->BindBufferMemory(device, buffer, memory, memoryOffset);
if (result == VK_SUCCESS) {
DeviceMemoryReport::Get().OnBindBufferMemory(reinterpret_cast<uint64_t>(buffer), reinterpret_cast<uint64_t>(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) {
auto* table = device_dispatch_table(device);
assert(table->BindImageMemory != nullptr);
assert(image != VK_NULL_HANDLE);
assert(memory != VK_NULL_HANDLE);
if (DeviceMemoryReport::Get().GetRecordedResourceSize(VK_OBJECT_TYPE_IMAGE, reinterpret_cast<uint64_t>(image)) == 0) {
if (table->GetImageMemoryRequirements) {
VkMemoryRequirements mem_reqs;
table->GetImageMemoryRequirements(device, image, &mem_reqs);
DeviceMemoryReport::Get().OnRecordResourceSize(VK_OBJECT_TYPE_IMAGE, reinterpret_cast<uint64_t>(image), mem_reqs.size);
}
}
VkResult result = table->BindImageMemory(device, image, memory, memoryOffset);
if (result == VK_SUCCESS) {
DeviceMemoryReport::Get().OnBindImageMemory(reinterpret_cast<uint64_t>(image), reinterpret_cast<uint64_t>(memory), memoryOffset);
}
return result;
}

static void RecordBufferBindings(VkDevice device, uint32_t bindInfoCount, const VkBindBufferMemoryInfo* pBindInfos) {
auto* table = device_dispatch_table(device);
for (uint32_t i = 0; i < bindInfoCount; ++i) {
Expand All @@ -413,6 +373,19 @@ static void RecordBufferBindings(VkDevice device, uint32_t bindInfoCount, const
}
}

// 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) {
auto* table = device_dispatch_table(device);
assert(table->BindBufferMemory != nullptr);
VkResult result = table->BindBufferMemory(device, buffer, memory, memoryOffset);
if (result == VK_SUCCESS) {
const VkBindBufferMemoryInfo bind_info = {
.sType = VK_STRUCTURE_TYPE_BIND_BUFFER_MEMORY_INFO, .buffer = buffer, .memory = memory, .memoryOffset = memoryOffset};
RecordBufferBindings(device, 1, &bind_info);
}
return result;
}

// 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) {
auto* table = device_dispatch_table(device);
Expand Down Expand Up @@ -459,6 +432,19 @@ static void RecordImageBinds(VkDevice device, uint32_t bindInfoCount, const VkBi
}
}

// 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) {
auto* table = device_dispatch_table(device);
assert(table->BindImageMemory != nullptr);
VkResult result = table->BindImageMemory(device, image, memory, memoryOffset);
if (result == VK_SUCCESS) {
const VkBindImageMemoryInfo bind_info = {
.sType = VK_STRUCTURE_TYPE_BIND_IMAGE_MEMORY_INFO, .image = image, .memory = memory, .memoryOffset = memoryOffset};
RecordImageBinds(device, 1, &bind_info);
}
return result;
}

// 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) {
auto* table = device_dispatch_table(device);
Expand Down Expand Up @@ -487,15 +473,10 @@ VKAPI_ATTR VkResult VKAPI_CALL vkCreateImage(VkDevice device, const VkImageCreat
VkResult result = fpCreateImage(device, pCreateInfo, pAllocator, pImage);
if (result == VK_SUCCESS && pCreateInfo != nullptr && pImage != nullptr && *pImage != VK_NULL_HANDLE) {
DeviceMemoryReport::Get().OnCreateImage(reinterpret_cast<uint64_t>(*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(VK_OBJECT_TYPE_IMAGE, reinterpret_cast<uint64_t>(*pImage), mem_reqs.size);
}
// The image size is not queried here: images created with Android hardware buffer external memory
// must be bound before vkGetImageMemoryRequirements is called (VUID-vkGetImageMemoryRequirements-image-04004),
// and otherwise report a worst-case size (b/578430447). An image whose size the application never
// queried (e.g. it used vkGetDeviceImageMemoryRequirements before creation) has it queried once bound.
}
return result;
}
Expand Down
Loading
Loading