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:
heapUsageis 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 handleeOutOfDeviceMemory; 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.
If this post helped you, please consider buying me a coffee or donating via PayPal to support research & publishing of new posts on TechOverflow