How to query GPU memory budget and live heap usage in Vulkan with VK_EXT_memory_budget

Example: VulkanExamples/p42_budget

Post 42 in the series (post 28: capability queries, post: memory types).

VkPhysicalDeviceMemoryProperties tells you heap sizes — the 16GB printed on the box. What it does NOT tell you: how much of that heap is already gone (other apps, the compositor, your own earlier allocations) or how much the driver is willing to give you. VK_EXT_memory_budget closes both gaps with one chained struct:

main.cpp (excerpt)
vk::PhysicalDeviceMemoryBudgetPropertiesEXT budget{};
vk::PhysicalDeviceMemoryProperties2 mp{};
mp.pNext = &budget;
phys.getMemoryProperties2(&mp);

for (uint32_t i = 0; i < mp.memoryProperties.memoryHeapCount; ++i)
    printf("heap%u size=%lluMB usage=%lluMB budget=%lluMB\n", i,
           (unsigned long long)(mp.memoryProperties
                                    .memoryHeaps[i].size >> 20),
           (unsigned long long)(budget.heapUsage[i] >> 20),
           (unsigned long long)(budget.heapBudget[i] >> 20));
  • heapBudget[i] — what the driver currently lets you allocate (heap size minus driver reservations minus other processes).
  • heapUsage[i] — what is actually allocated right now.

On the test Radeon:

example.sh
--- before allocations ---
heap0 size=16128MB usage=0MB budget=15124MB flags=0x1   # VRAM
heap1 size=15540MB usage=0MB budget=15339MB flags=0x0   # GTT/host
heap2 size=256MB   usage=0MB budget=54MB   flags=0x1    # BAR carve-out
--- after 3x256MB device-local ---
heap0 size=16128MB usage=768MB budget=15124MB flags=0x1

Two instructive details:

  • budget ≠ size: heap0’s 16GB shows a 15.1GB budget — the driver keeps ~1GB for itself (page tables, firmware, other clients). heap2 is the resizable-BAR window: a 256MB heap with only a 54MB budget, i.e. most of it is permanently reserved.
  • usage counts more than you: heapUsage is system-wide on shared heaps — another app allocating VRAM shows up here, which is exactly why an app should re-check the budget when evicting or growing caches instead of trusting heap size.

Check

check.py
# SPDX-License-Identifier: CC0-1.0
import re, subprocess

o = subprocess.run(["./app"], check=True, capture_output=True,
                   text=True).stdout

def parse(text):
    return {i: (int(u), int(b)) for i, u, b in re.findall(
        r"heap(\d+) size=\d+MB usage=(\d+)MB budget=(\d+)MB "
        r"flags=0x1", text)}

before, _, after = o.partition("--- after 3x256MB device-local ---")
heaps_b = parse(before)
heaps_a = parse(after)
assert heaps_b and heaps_a, o
grew = [i for i in heaps_a
        if heaps_a[i][0] - heaps_b.get(i, (0, 0))[0] >= 700]
assert grew, f"no heap grew by ~768MB: {o}"
print("OK: heap budget/usage reported, device-local usage +768MB")
example.sh
$ python3 check.py
OK: heap budget/usage reported, device-local usage +768MB

How NOT to use it

  • Don’t treat budget as a promise — it can shrink between the query and your vkAllocateMemory (another app grabbed memory). Still handle eOutOfDeviceMemory; the budget is a policy input, not a reservation.
  • Don’t sum budgets — heaps overlap in weird ways (the BAR heap is a window into VRAM); pick the right heap via its flags.
  • Don’t re-query every frame — it is a syscall-adjacent query; poll on allocation pressure or a timer, not per-draw.

All source code in this series is released under CC0 1.0 Universal (CC0-1.0) — public domain dedication, no attribution required.


Check out similar posts by category: C++ Vulkan