mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
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:
1 parent
6e9ca1ebfd
commit
c838d9692f
7 files changed
+79
-6
No files matched your search
+1
-1
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user