Count system memory in the device-local budget where device memory takes host memory

The paths that give memory back under pressure (headroom's spare block, released textures, pool mirrors) watched the
Vulkan heap budget only. On a UMA GPU that budget is gigabytes of the same RAM the game runs in, and on Windows the
AMD driver charges device-local allocations to the process commit one to one (RX 9070: VRAM use and private bytes
grew by the same 1164, 256 and 256 MiB). Crash logs show the result: the process at 4.4-10.5 GB of commit, 16 GB
machines at 80-94% RAM, std::bad_alloc inside amdvlk64.dll (#285), allocation failures on Intel UHD.

vk_mem_host_room (vk_memory.c) is what the system can still give beyond a reserve (TOTK_MEM_RESERVE_MIB, 1536):
commit, and on a UMA GPU physical memory too; Linux overcommits, so there only a UMA GPU's available memory counts.
NVIDIA is left out, not measured. vk_mem_vram caps the budget at use plus that room, and headroom keeps its spare
block only while four blocks fit in it.

RX 9070, field save, 60 s with the room near zero from the start (TOTK_MEM_RESERVE_MIB=21000): commit 7124 MB instead
of 8965, VRAM 2631 MiB instead of 3781, released textures 153 instead of 1866; mirrors refused for 423 MiB of pools,
GPU 4.45 ms a frame instead of 3.34 ms, 60 fps either way. Without pressure nothing changes. The census logs the
room, the device class line the mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Valiantsin DzerakhandClaude Opus 5.5 committed 2026-11-09 09:02:36 +03:00
1 parent 6e9ca1ebfd
commit c838d9692f
7 files changed
+79 -6

No files matched your search

+1 -1
View File
@@ -47,7 +47,7 @@ static const char *const known[] = {
"FULLSCREEN", "FXAA", "GPL", "GPL_DRAW_BUILD", "GPU", "GPU_INDEX", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_HUE",
"HDR_PEAK_NITS", "HDR_UI_NITS", "HIGHFPS_FIX", "INPUT", "INPUT_RECORD", "INPUT_SCRIPT", "KSPIN_US", "LANG", "LIB_PREBUILD",
"LIB_PREBUILD_KEEP", "LIB_PREBUILD_THREADS", "LOCALE", "LOD_GUARD", "LSTICK_DEADZONE", "MEMORY", "MENU_LANG", "MENU_TEST",
"MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_AIM_SCALE", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_ASPECT", "MOVIE_HOST", "MOVIE_PACE",
"MEM_RESERVE_MIB", "MENU_CURSOR", "MENU_CURSOR_LOG", "MENU_TEST_DIR", "MENU_TEST_SET", "MINIMAL_LAYOUT", "MIRROR_GAP", "MODS_DIR", "MODS_DISABLED", "MODS_ORDER", "MODS_REBUILD", "MOTION", "MOUSE_AIM_SCALE", "MOUSE_MODE", "MOUSE_DIRECT_PATCH", "MOUSE_SENS", "MOVIE_ASPECT", "MOVIE_HOST", "MOVIE_PACE",
"MVOBJ_ALL", "MVOBJ_BANKS", "MVOBJ_EQUAL", "MVOBJ_EXACT", "MVOBJ_EXEC", "MVOBJ_NOC4", "MVOBJ_ONLY", "MVOBJ_TIMES", "MVPREV_CHECK", "MVPREV_GROW", "MVPREV_SKIP", "MVPREV_TRUST", "MV_CHECK",
"MV_SURVEY",
"ONE_PER_CORE", "OPERATION_MODE", "OUTPUT_RES", "PAD_LAYOUT", "PASS_CRUMBS", "PATCH_OUTPUT", "PATCH_SCENE", "PCORE", "PCSS", "PCSS_K", "PERF_OVERLAY", "PERF_SLOW_MS", "PIPELINE_CACHE", "PIPE_MEMO", "PIPE_PREBUILD", "PIPE_SKIP", "PIPE_SYNC", "PIPE_WAIT",
+11
View File
@@ -1673,6 +1673,17 @@ void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
if (private_bytes) *proc_mb = (double)(private_bytes >> 20);
}
void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room) {
*commit_room = *phys_room = UINT64_MAX;
char line[128];
unsigned long long k;
FILE *f = fopen("/proc/meminfo", "r");
if (!f) return;
while (fgets(line, sizeof line, f))
if (sscanf(line, "MemAvailable: %llu kB", &k) == 1) *phys_room = (uint64_t)k << 10;
fclose(f);
}
// ---- child processes
struct PlatChild {
+3
View File
@@ -433,6 +433,9 @@ void plat_mem_info(float *used_mb, float *total_mb, float *proc_mb);
// Commit charge in MB: this process's private bytes, the system's commit in use and its limit (RAM plus page files).
// An allocation fails when the system's reaches the limit, however much physical memory is free. -1 when unknown.
void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb);
// What the system can still give, in bytes: commit (Windows: limit minus charge; Linux, which overcommits: UINT64_MAX)
// and physical memory available without paging. UINT64_MAX when unknown.
void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room);
// This process's memory in bytes: resident (the working set) and private (Windows: committed private bytes; Linux: the
// resident anonymous pages). 0 when unknown.
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes);
+6
View File
@@ -1784,6 +1784,12 @@ void plat_commit_info(double *proc_mb, double *used_mb, double *limit_mb) {
*proc_mb = (double)(pmc.PrivateUsage >> 20);
}
void plat_mem_room(uint64_t *commit_room, uint64_t *phys_room) {
*commit_room = *phys_room = UINT64_MAX;
MEMORYSTATUSEX ms = {.dwLength = sizeof ms};
if (GlobalMemoryStatusEx(&ms)) *commit_room = ms.ullAvailPageFile, *phys_room = ms.ullAvailPhys;
}
void plat_process_memory(uint64_t *resident, uint64_t *private_bytes) {
PROCESS_MEMORY_COUNTERS_EX pm = {.cb = sizeof pm};
*resident = *private_bytes = 0;
+5 -1
View File
@@ -300,8 +300,12 @@ int vk_mem_class_of(uint64_t addr); // VK_MCLS_* of the pool at ad
void vk_mem_census(uint64_t frame); // at each present: pool census and traffic, logged at frame 1800 and,
// with TOTK_TRACE=perf, every 1800 frames
// VRAM in use and its budget, summed over the DEVICE_LOCAL heaps. exact 1: VK_EXT_memory_budget (the whole process),
// 0: this allocator's own allocations against the heap sizes. 0 when unknown.
// 0: this allocator's own allocations against the heap sizes. 0 when unknown. Where device memory takes host memory
// (vk_mem_host_room) the budget is at most the use plus that room.
int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact);
// Bytes the system can still give device memory beyond a reserve (TOTK_MEM_RESERVE_MIB) where device memory takes host
// memory: a UMA GPU (commit and physical memory), or a discrete non-NVIDIA GPU on Windows (commit). UINT64_MAX elsewhere.
uint64_t vk_mem_host_room(void);
// VRAM headroom (vk_headroom.c): the VMA callbacks that see new device memory, and the thread that allocates the next
// block of each memory type in use ahead of the render thread.
const VmaDeviceMemoryCallbacks *vk_headroom_callbacks(void);
+5 -2
View File
@@ -12,7 +12,7 @@
// a block per memory type is held back.
// Under VRAM pressure no headroom is kept: the spare block counts against the budget that decides whether a pool gets
// its VRAM mirror (vk_memory.c), so while use plus two blocks passes 85% of the budget the anchor goes and nothing new
// is allocated.
// is allocated. The same holds when the system runs short of memory (room).
// TOTK_VRAM_HEADROOM=0 turns it off.
#include "cfg.h"
@@ -58,11 +58,14 @@ static VkDeviceSize block_size(const VkCtx *c, uint32_t type) {
return heap <= HR_SMALL_HEAP ? heap / 8 : 256ull << 20; // VMA's default preferredLargeHeapBlockSize
}
// The spare block is memory the game does not use yet; where device memory takes host memory it is kept only while the
// system has four blocks to spare beyond the reserve (vk_mem_host_room). On the RX 9070 the spare blocks held 1.4 GB
// of commit.
static int room(const VkCtx *c, uint32_t type, VkDeviceSize block) {
VmaBudget b[VK_MAX_MEMORY_HEAPS];
vmaGetHeapBudgets(c->vma, b);
const VmaBudget *h = &b[c->mem_props.memoryTypes[type].heapIndex];
return h->budget && h->usage + 2 * block <= h->budget / 100 * 85;
return h->budget && h->usage + 2 * block <= h->budget / 100 * 85 && vk_mem_host_room() >= 4 * block;
}
static void drop_anchor(const VkCtx *c, uint32_t type) {
+48 -2
View File
@@ -574,8 +574,10 @@ void vk_mem_census(uint64_t frame) {
// The baseline for the commit a crash report prints: a crash far above it points at growth during play.
double proc, used, limit;
plat_commit_info(&proc, &used, &limit);
rt_log("mem: commit at frame %llu: this process %.0f MB, the system %.0f of %.0f MB", (unsigned long long)frame, proc,
used, limit);
uint64_t room = vk_mem_host_room();
rt_log("mem: commit at frame %llu: this process %.0f MB, the system %.0f of %.0f MB; room for device memory %s%.0f MiB",
(unsigned long long)frame, proc, used, limit, room == UINT64_MAX ? "not counted " : "",
room == UINT64_MAX ? 0.0 : room / 1048576.0);
// Per heap, what VMA holds (blocks, and the allocations inside them) beside what the driver counts for the process:
// usage above VMA's blocks is the driver's own memory (pipelines, command buffers, descriptor pools).
VkCtx *vc = vk_ctx();
@@ -619,6 +621,42 @@ static uint64_t vram_limit(void) {
return (uint64_t)limit;
}
// Device memory that takes host memory. A UMA GPU's memory is the machine's RAM. On Windows the AMD driver charges
// device-local allocations to the process commit one to one (RX 9070: VRAM use and private bytes grew by the same
// 1164, 256 and 256 MiB), so on a 16 GB machine the commit limit is reached while the VRAM budget shows gigabytes free,
// and the driver then fails inside a call (#285: std::bad_alloc in amdvlk64.dll under gpl_link). Intel's Windows driver
// is taken to do the same; NVIDIA's is not measured and left out. Linux overcommits, so there only a UMA GPU's physical
// memory counts (plat_mem_room).
enum { HB_NONE, HB_COMMIT, HB_UMA };
static int host_backed;
static uint64_t room_reserve;
static volatile uint64_t room_cached, room_ms;
uint64_t vk_mem_host_room(void) {
if (!host_backed) return UINT64_MAX;
uint64_t now = plat_now_ms(), at = __atomic_load_n(&room_ms, __ATOMIC_ACQUIRE);
if (at && now - at < 250) return __atomic_load_n(&room_cached, __ATOMIC_RELAXED); // a query costs a system call
uint64_t commit, phys;
plat_mem_room(&commit, &phys);
uint64_t r = host_backed == HB_UMA && phys < commit ? phys : commit;
r = r == UINT64_MAX ? r : r > room_reserve ? r - room_reserve : 0;
__atomic_store_n(&room_cached, r, __ATOMIC_RELAXED);
__atomic_store_n(&room_ms, now | 1, __ATOMIC_RELEASE);
return r;
}
// The budget is at most the use plus what the system can still give, so the paths that react to VRAM pressure (the
// mirror refusal, released textures, the headroom block) react to a system short of memory too.
static void host_cap(uint64_t used, uint64_t *budget) {
uint64_t room = vk_mem_host_room();
if (room == UINT64_MAX || used + room >= *budget) return;
*budget = used + room ? used + room : 1;
static volatile int32_t logged;
if (!plat_xchg32(&logged, 1))
rt_log("mem: system memory limits the device-local budget: %.0f MiB in use, %.0f MiB more beyond a reserve of "
"%.0f MiB (logged once)", used / 1048576.0, room / 1048576.0, room_reserve / 1048576.0);
}
int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
VkCtx *c = vk_ctx();
*used = *budget = 0, *exact = 0;
@@ -634,6 +672,7 @@ int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
*used += b.heapUsage[h], *budget += b.heapBudget[h];
*exact = 1;
if (limit && *budget > limit) *budget = limit;
if (*budget) host_cap(*used, budget);
return *budget != 0;
}
VmaBudget vb[VK_MAX_MEMORY_HEAPS];
@@ -642,6 +681,7 @@ int vk_mem_vram(uint64_t *used, uint64_t *budget, int *exact) {
if (mp->memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT)
*used += vb[h].statistics.blockBytes, *budget += mp->memoryHeaps[h].size;
if (limit && *budget > limit) *budget = limit;
if (*budget) host_cap(*used, budget);
return *budget != 0;
}
@@ -2243,6 +2283,9 @@ void vk_mem_class_log(const VkPhysicalDeviceProperties *p, const VkPhysicalDevic
else if (host_heap < 0) cls = "uma", why = "every heap is device-local";
else cls = "discrete", why = "device-local heap separate from host memory";
int discrete = cls[0] == 'd';
host_backed = !discrete ? HB_UMA : p->vendorID != 0x10DE ? HB_COMMIT : HB_NONE;
const char *rs = cfg_get("TOTK_MEM_RESERVE_MIB");
room_reserve = (rs && *rs ? strtoull(rs, NULL, 10) : 1536) << 20; // Windows, other programs, our own heap
char line[2048];
size_t k = (size_t)snprintf(line, sizeof line, "mem: class %s (%s; device type %d %s, %s)", cls, why, t, tn, p->deviceName);
for (uint32_t h = 0; h < m->memoryHeapCount; h++)
@@ -2262,6 +2305,9 @@ void vk_mem_class_log(const VkPhysicalDeviceProperties *p, const VkPhysicalDevic
f & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT ? " LAZY" : "");
}
rt_log("%s", line);
rt_log("mem: device memory against system memory: %s, reserve %llu MiB (TOTK_MEM_RESERVE_MIB)",
host_backed == HB_UMA ? "commit and physical memory (uma)" : host_backed == HB_COMMIT ? "commit" : "not counted",
(unsigned long long)(room_reserve >> 20));
mirror_mode(discrete);
}