Merge task/memcommit: count device memory against system memory where it takes process commit, BC3 for ASTC on integrated GPUs, cap the released-texture cache by bytes

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:58:10 +03:00
commit a79ff17314
13 files changed
+193 -23

No files matched your search

+2
View File
@@ -181,6 +181,7 @@ Many rows below are developer tools that only act together with another setting
| --- | --- | --- | --- |
| `app_pool` | Size in bytes of the guest application memory pool; accepts decimal or `0x` hex. | The default depends on the resolution settings; `0` stops the start with a message, and too small a pool leaves the game without memory. | Risky |
| `audio_off` | Any value mixes audio without opening an output device. | The game runs in silence and reports two channels; same as `audio = none`. | Caution |
| `mem_reserve_mib` (default `1536`) | MiB of system memory kept free for Windows, other programs and the game's own heap when the renderer sizes what it keeps in device memory, where device memory takes system memory (integrated GPUs, and discrete GPUs whose driver charges it to the process). | A larger value keeps fewer released textures and spare blocks when the system runs short; a smaller one lets the game use more before it holds back. | Caution |
| `one_per_core` (default `0`) | `1` keeps the process on the first logical processor of each physical core; `ab` switches it with the `ab` blocks. | Busy threads stop sharing a core with a sibling thread, which can help or hurt on the CPU; it only applies when the CPU has hyperthreads. | Caution |
| `reexec` | Counter of address-space retries that the runtime sets itself when it starts the process again. | The counter is cleared when the window is recreated; a value of 20 or more stops the start with a fatal error. | Risky |
| `replay_chunks` (default `0`) | Most chunks a submission is cut into when `replay_workers` is set; `0` uses twice the number of recording threads. | Fewer chunks leave threads idle at the end of a submission, more add scan and submit work per chunk; no effect while `replay_workers` is `0`. | Caution |
@@ -190,6 +191,7 @@ Many rows below are developer tools that only act together with another setting
| `restart_wait` | Set by the runtime when it restarts itself; names the old process by id and creation time so the new process waits for it to exit. | Meant for the runtime's own use; the code does not describe a manual value. | Unknown |
| `spin` (default `1`) | `0` makes a guest thread that polls memory keep its virtual core. | Polling threads can starve the thread whose item they wait for, so frames can stall. | Caution |
| `spin_handoff_us` (default `50`) | Microseconds a polling wait lasts before it gives its virtual core to a waiting thread. | A shorter value hands cores over sooner; a longer one keeps them longer. | Caution |
| `tex_released_mib` (default `256`) | MiB of textures the game released that are kept for reuse; older ones beyond it are dropped. `0` keeps up to 2048 of them, whatever their size. | A larger value keeps more device memory and process commit; a smaller one makes the game create more textures again. | Caution |
## Generated-code patches
+5 -4
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",
@@ -59,7 +59,7 @@ static const char *const known[] = {
"SHADER_MEMO", "SHADOW_RANGE", "SHADOW_RES", "SHADOW_SCALE", "SHADOW_SNAP", "SHARED_FONTS", "SHARPNESS", "SHOTS", "SHOTS_DIR",
"SHOTS_FULL", "SPEED_PROBE", "SPIN", "SPIN_HANDOFF_US", "SPLIT_DRAWS", "SPLIT_US", "STICK_DEADZONE", "STICK_SENS",
"SWKBD_ANSWER",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_HEAP_EXTRA_MB", "TEX_MEMO", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UB_OVERLAY", "UB_OVERLAY_OUT", "UPSCALER", "VB_USED", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"SWKBD_TEST", "TEST_PAD", "TEST_PREPARE_SHADERS", "TEX_ALIAS", "TEX_HEAP_EXTRA_MB", "TEX_MEMO", "TEX_RELEASED_MIB", "TEX_VRAM_PRESSURE", "TIMER_1MS", "TRACE", "TXD_TRANSPOSE", "UBO_SNAPSHOT", "UB_OVERLAY", "UB_OVERLAY_OUT", "UPSCALER", "VB_USED", "VK_DYN_A2C", "VK_DYN_VERTEX", "VK_NO_FLOAT64",
"VK_PRESENT", "VK_RECORDER", "VK_SSBO_ALIGN", "VK_SUBGROUP", "VK_SUBGROUP_PIN", "VK_VALIDATION", "VOLUME", "VRAM_HEADROOM", "VRAM_LIMIT_MIB", "VS_OUTS",
"WALK_KEY", "WALK_SCALE", "WB_EXACT", "WB_FENCE_LIMIT", "WB_MERGE", "WINDOW_SIZE", "XLATE", "XLATE_ID", "XLATE_KEEP", "XLATE_PROCS",
"ZSTD_CHECK", "ZSTD_NATIVE",
@@ -99,7 +99,7 @@ static const struct { const char *key, *section; } sections[] = {
{"TIMER_1MS", "performance"}, {"PIPE_WAIT", "performance"}, {"PIPE_SKIP", "performance"},
{"LIB_PREBUILD", "performance"}, {"LIB_PREBUILD_THREADS", "performance"}, {"PIPE_PREBUILD", "performance"},
{"PREWARM_XLATE", "performance"}, {"GPL", "performance"}, {"ASTC", "performance"}, {"MEMORY", "performance"},
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
{"TEX_ALIAS", "performance"}, {"TEX_VRAM_PRESSURE", "performance"}, {"TEX_RELEASED_MIB", "performance"}, {"ASTC_BC", "performance"}, {"LOD_GUARD", "performance"}, {"FIX_C0", "performance"}, {"FIX_ZERO", "performance"},
{"BIND_SKIP", "performance"}, {"SHADER_MEMO", "performance"}, {"PIPE_MEMO", "performance"}, {"TEX_MEMO", "performance"}, {"VB_USED", "performance"}, {"DESC_TEMPLATE", "performance"},
{"PUSH_SKIP", "performance"}, {"SPLIT_DRAWS", "performance"}, {"SPLIT_US", "performance"}, {"VK_VALIDATION", "performance"},
{"VK_DYN_VERTEX", "performance"}, {"VK_DYN_A2C", "performance"}, {"VK_SUBGROUP_PIN", "performance"},
@@ -173,7 +173,8 @@ static const CfgMeta metas[] = {
{"PIPE_PREBUILD", "1", "0|1", RESTART}, {"PREWARM_XLATE", "", "1..64", RESTART}, {"GPL", "1", "0|1", RESTART},
// textures and memory
{"ASTC", "auto", "auto|gpu|native|cpu", RESTART}, {"MEMORY", "auto", "auto|import|mirror|verify", RESTART},
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
{"TEX_ALIAS", "1", "0|1", RESTART}, {"TEX_VRAM_PRESSURE", "90", "0..100", RESTART},
{"TEX_RELEASED_MIB", "256", "0..65536", RESTART}, {"ASTC_BC", "", "0|1", RESTART}, {"ZSTD_NATIVE", "1", "0|1", RESTART}, {"ZSTD_CHECK", "0", "0|1", RESTART},
// render fixes
{"LOD_GUARD", "1", "0|1", RESTART}, {"FIX_C0", "1", "0|1", RESTART}, {"FIX_ZERO", "1", "0|1", RESTART},
{"BIND_SKIP", "1", "0|1", RESTART}, {"SHADER_MEMO", "1", "0|1", RESTART}, {"PIPE_MEMO", "1", "0|1", RESTART}, {"TEX_MEMO", "1", "0|1", RESTART},
+3 -1
View File
@@ -756,7 +756,9 @@ static void test_crash_report(void) {
const char *g = report_line(out, "*5ca1ab1e0c0de002", line, sizeof line);
CHECK(g && strstr(g, "x19"), "crash report: no memory line for x19 (%s)", g ? g : "none");
CHECK(strstr(out, "guest stack sp+0x00:"), "crash report: no guest stack words");
printf("crash report: exit code %d, host registers, code bytes, memory at rbx and x19, guest stack %s\n", code,
CHECK(report_line(out, " memory commit: this process ", line, sizeof line) && !strstr(line, "process -1 MB"),
"crash report: no memory commit line");
printf("crash report: exit code %d, host registers, code bytes, memory at rbx and x19, guest stack, commit %s\n", code,
failures ? "checked with failures" : "present");
}
+5
View File
@@ -298,6 +298,11 @@ static void crash_handler(const PlatCrash *x) {
rt_log_raw("\n", 1);
if (x->cxx_type)
rt_logf(" C++ type %s%s%s\n", x->cxx_type, x->cxx_what ? ", what(): " : "", x->cxx_what ? x->cxx_what : "");
// A driver that runs out of memory faults or throws (std::bad_alloc) instead of returning an error: the commit
// tells that apart from a driver bug, as rt_fatal's line does for our own failed allocations.
double proc, used, limit;
plat_commit_info(&proc, &used, &limit);
rt_logf(" memory commit: this process %.0f MB, the system %.0f of %.0f MB\n", proc, used, limit);
uintptr_t fstart = 0;
uint64_t f = x->module ? 0 : lifted_function_at(rip, &fstart);
if (f)
+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;
+8 -1
View File
@@ -300,8 +300,15 @@ 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 GPU whose device-local memory took process commit in
// vk_mem_host_probe (commit). UINT64_MAX elsewhere.
uint64_t vk_mem_host_room(void);
void vk_mem_host_probe(void); // vk_device.c, after the device: measures that for a discrete GPU, once
int vk_mem_uma(void); // 1: the device class is uma (vk_mem_class_log), its memory is RAM
// 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);
+6 -4
View File
@@ -300,17 +300,19 @@ static int create_pipeline(VkCtx *c) {
// TOTK_ASTC_BC=1: the BC3 encoder, when the device samples BC3 (every desktop GPU does); 0 leaves ASTC as RGBA8.
// Unset: on when the largest device-local heap is under 5 GiB. There RGBA8 ASTC, four times the size of BC3, fills
// video memory (a GTX 1650 4 GiB went from 10-23 to 3-12 ms of GPU time a frame, issue #87); BC3 loses some precision
// (normal maps), so GPUs with more memory keep RGBA8.
// (normal maps), so GPUs with more memory keep RGBA8. A UMA GPU's heap is the machine's RAM however large it reads
// (10.6 GiB on an ROG Ally X, 14.9 GiB on a Radeon 880M), so there BC3 is on too: on the RX 9070, BC3 took 737 MiB off
// VMA's device-local allocations after a minute in the field, at the same GPU time.
static int create_bc_pipeline(VkCtx *c) {
const char *e = cfg_get("TOTK_ASTC_BC");
VkDeviceSize vram = 0;
for (uint32_t h = 0; h < c->mem_props.memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
if (c->mem_props.memoryHeaps[h].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT && c->mem_props.memoryHeaps[h].size > vram)
vram = c->mem_props.memoryHeaps[h].size;
int on = e && *e ? *e != '0' : vram < (5ull << 30);
int on = e && *e ? *e != '0' : vram < (5ull << 30) || vk_mem_uma();
if (!(e && *e))
rt_log("vk: TOTK_ASTC_BC unset: BC3 %s (largest device-local heap %llu MiB)", on ? "on" : "off",
(unsigned long long)(vram >> 20));
rt_log("vk: TOTK_ASTC_BC unset: BC3 %s (largest device-local heap %llu MiB%s)", on ? "on" : "off",
(unsigned long long)(vram >> 20), vk_mem_uma() ? ", uma: system memory" : "");
if (!on) return 0;
VkFormatProperties p;
vkGetPhysicalDeviceFormatProperties(c->phys, VK_FORMAT_BC3_SRGB_BLOCK, &p);
+2 -1
View File
@@ -475,7 +475,8 @@ int vk_init(void) {
plat_mutex_lock(&g_lock);
if (!g_ready) {
memset(&g_vk, 0, sizeof g_vk);
if (create_instance() && pick_device() && create_device()) g_ready = 1, vk_upload_pools_init(), vk_headroom_start(), vk_dlaa_init();
if (create_instance() && pick_device() && create_device())
g_ready = 1, vk_mem_host_probe(), vk_upload_pools_init(), vk_headroom_start(), vk_dlaa_init();
else destroy_all();
}
int ok = g_ready;
+10 -3
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) {
@@ -72,7 +75,11 @@ static void drop_anchor(const VkCtx *c, uint32_t type) {
static void top_up(const VkCtx *c, uint32_t type) {
VkDeviceSize block = block_size(c, type);
if (!room(c, type, block)) {
// Device-local types only: textures and mirrors grow there. Host memory types are woken by the staging pool's own
// blocks (vk_memory.c vk_staging), while their default pool held 4 MiB: on the RX 9070 the spare blocks of types 1
// and 3 held 512 MiB of commit for nothing.
if (!(c->mem_props.memoryTypes[type].propertyFlags & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) ||
!room(c, type, block)) {
drop_anchor(c, type);
return;
}
+100
View File
@@ -571,6 +571,25 @@ void vk_mem_census(uint64_t frame) {
n_import_failures, (unsigned long long)count[POOL_PHYS], total[POOL_PHYS] / 1048576.0,
(unsigned long long)count[POOL_VIRT], total[POOL_VIRT] / 1048576.0,
(unsigned long long)__atomic_load_n(&n_cross_defer, __ATOMIC_RELAXED));
// 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);
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();
if (vc && vc->vma) {
VmaBudget vb[VK_MAX_MEMORY_HEAPS];
vmaGetHeapBudgets(vc->vma, vb);
for (uint32_t h = 0; h < vc->mem_props.memoryHeapCount && h < VK_MAX_MEMORY_HEAPS; h++)
rt_log("mem: heap %u (flags %#x) at frame %llu: vma %u blocks %.0f MiB, %u allocations %.0f MiB; driver usage "
"%.0f of %.0f MiB", h, vc->mem_props.memoryHeaps[h].flags, (unsigned long long)frame,
vb[h].statistics.blockCount, vb[h].statistics.blockBytes / 1048576.0, vb[h].statistics.allocationCount,
vb[h].statistics.allocationBytes / 1048576.0, vb[h].usage / 1048576.0, vb[h].budget / 1048576.0);
}
static const char *const kinds[] = {"", "import", "physical", "virtual"};
for (int j = 0; j < ng; j++) {
char f[160];
@@ -602,6 +621,79 @@ 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). Whether a discrete
// GPU's driver does so is measured once (vk_mem_host_probe), not assumed per vendor: an RTX 5080 log (#398) showed
// 10.4 GB of process commit at frame 1200, about what the RX 9070 holds with its VRAM counted. Linux overcommits, so
// there only a UMA GPU's physical memory counts (plat_mem_room).
enum { HB_NONE, HB_COMMIT, HB_UMA, HB_PROBE };
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;
}
int vk_mem_uma(void) { return host_backed == HB_UMA; }
// A discrete GPU: one device-local allocation of HB_PROBE_SIZE, and the process commit before and after it. The driver
// charges it at allocation or not at all as far as this sees; one that charged only at first use would read as not
// counted, as before the probe. Another thread's allocation in between could only add, and half the size is the line.
#define HB_PROBE_SIZE (128ull << 20)
void vk_mem_host_probe(void) {
VkCtx *c = vk_ctx();
if (!c || host_backed != HB_PROBE) return;
host_backed = HB_NONE;
uint64_t commit, phys;
plat_mem_room(&commit, &phys);
if (commit == UINT64_MAX) return; // no commit limit (Linux)
uint32_t type = UINT32_MAX;
for (uint32_t i = 0; i < c->mem_props.memoryTypeCount && type == UINT32_MAX; i++)
if ((c->mem_props.memoryTypes[i].propertyFlags & (VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT)) ==
VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT)
type = i;
VkMemoryAllocateInfo ai = {.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO, .allocationSize = HB_PROBE_SIZE,
.memoryTypeIndex = type};
uint64_t r0, p0, r1, p1;
VkDeviceMemory m;
plat_process_memory(&r0, &p0);
if (type == UINT32_MAX || !p0 || vkAllocateMemory(c->device, &ai, NULL, &m) != VK_SUCCESS) {
rt_log("mem: device memory against system memory: not measured (no device-local type or allocation failed): not counted");
return;
}
plat_process_memory(&r1, &p1);
vkFreeMemory(c->device, m, NULL);
int64_t d = (int64_t)(p1 - p0);
host_backed = d >= (int64_t)HB_PROBE_SIZE / 2 ? HB_COMMIT : HB_NONE;
rt_log("mem: device memory against system memory: %s (a %llu MiB device-local allocation took %.0f MiB of process "
"commit), reserve %llu MiB (TOTK_MEM_RESERVE_MIB)", host_backed == HB_COMMIT ? "commit" : "not counted",
(unsigned long long)(HB_PROBE_SIZE >> 20), d / 1048576.0, (unsigned long long)(room_reserve >> 20));
}
// 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;
@@ -617,6 +709,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];
@@ -625,6 +718,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;
}
@@ -2226,6 +2320,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 : HB_PROBE; // a discrete GPU is measured once the device exists
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++)
@@ -2245,6 +2342,9 @@ void vk_mem_class_log(const VkPhysicalDeviceProperties *p, const VkPhysicalDevic
f & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT ? " LAZY" : "");
}
rt_log("%s", line);
if (host_backed == HB_UMA)
rt_log("mem: device memory against system memory: commit and physical memory (uma), reserve %llu MiB "
"(TOTK_MEM_RESERVE_MIB)", (unsigned long long)(room_reserve >> 20));
mirror_mode(discrete);
}
+32 -9
View File
@@ -64,6 +64,7 @@ typedef struct Tex {
uint32_t wt_n; // WriteTexels into the storage since the last upload
struct Tex *rel_prev, *rel_next; // while released: the list of released entries, oldest first
uint64_t rel_ms; // while released: when (coarse_ms)
uint64_t rel_bytes; // while released: its own memory, counted in rel_bytes_total
uint64_t use_seq; // order of the last creation or take-back among all entries
uint64_t rel_seq; // while released: use_seq of the newest entry placed before this release; an entry
// with a later use_seq on overlapping storage replaced this one's bytes
@@ -95,6 +96,7 @@ static SampSlot *sslots;
static int nsslots;
static nvn_hmap samplers = {.klen = sizeof(samp_par)};
static int nreleased;
static uint64_t rel_bytes_total; // own memory of the released entries
static Tex *rel_head, *rel_tail; // released entries in release order (sweep_released drops from the head)
static uint64_t n_uploads, n_reuploads;
// Re-uploads since the last perf line (tex_lock): all, and those of textures a WriteTexels wrote since their last upload.
@@ -538,7 +540,11 @@ static void own_remove(Tex *e) {
plat_mutex_unlock(&owned_lock);
}
static uint64_t own_bytes(const Tex *e);
static void rel_append(Tex *e) {
e->rel_bytes = own_bytes(e);
rel_bytes_total += e->rel_bytes;
e->rel_next = NULL;
e->rel_prev = rel_tail;
if (rel_tail) rel_tail->rel_next = e;
@@ -547,6 +553,8 @@ static void rel_append(Tex *e) {
}
static void rel_unlink(Tex *e) {
rel_bytes_total -= e->rel_bytes;
e->rel_bytes = 0;
if (e->rel_prev) e->rel_prev->rel_next = e->rel_next;
else rel_head = e->rel_next;
if (e->rel_next) e->rel_next->rel_prev = e->rel_prev;
@@ -644,12 +652,19 @@ static void sweep_released(void) {
// CLEAN_AGE_MS: without the wait a fight re-created about 500 of them per 10 s. Any other render target lives only in its
// image: dropping one that was merely old brought the texture back empty (black flashes in a Depths boss fight, ~170
// "left uninitialised" aliases a minute).
//
// TOTK_TEX_RELEASED_MIB (default 256; 0 = no cap): the same drops run while the released entries hold more own memory than
// that, under pressure or not. A 10-minute field run on the RX 9070 kept 818 MiB of them, 730 MiB with their storage
// taken by a later texture, while 23579 of 23660 take-backs came within a second and none after 120 s. With the
// cap the same route ended with 216 MiB of them, 1.1 GB less process commit and 1 GB less in VMA blocks; 15 dropped
// textures were created again, and the slow frames stayed the same.
#define PRESSURE_STEP 32
#define CLEAN_AGE_MS 5000u
#define DEAD_AGE_MS 30000u
#define DEAD_QUIET_SUBMITS 600u
#define DEAD_RETRY_QUERIES 8 // budget queries to skip after a search that found nothing
static int vram_pressure = -1;
static uint64_t released_cap;
static uint64_t n_pressure_drops, n_pressure_overwritten;
static int tex_by_addr(const void *a, const void *b) {
@@ -741,16 +756,23 @@ static void sweep_pressure(void) { // tex_lock held
const char *s = cfg_get("TOTK_TEX_VRAM_PRESSURE");
vram_pressure = s && *s ? atoi(s) : 90;
if (vram_pressure < 0 || vram_pressure > 100) vram_pressure = 90;
rt_log("vk: released textures under VRAM pressure: %s%d%%", vram_pressure ? "dropped above " : "off", vram_pressure);
const char *m = cfg_get("TOTK_TEX_RELEASED_MIB");
released_cap = (m && *m ? strtoull(m, NULL, 10) : 256) << 20;
rt_log("vk: released textures under VRAM pressure: %s%d%%; above %llu MiB of them: %s (TOTK_TEX_RELEASED_MIB)",
vram_pressure ? "dropped above " : "off", vram_pressure, (unsigned long long)(released_cap >> 20),
released_cap ? "dropped" : "no cap");
}
if (!vram_pressure || !rel_head || submit_epoch & 7) return; // the budget query every 8 submits
if (!rel_head || submit_epoch & 7) return; // the budget query every 8 submits
uint64_t need = released_cap && rel_bytes_total > released_cap ? rel_bytes_total - released_cap : 0;
uint64_t used, budget;
int exact;
if (!vk_mem_vram(&used, &budget, &exact)) return;
uint64_t line = budget / 100 * (uint64_t)vram_pressure;
if (used <= line) return;
if (vram_pressure && vk_mem_vram(&used, &budget, &exact)) {
uint64_t line = budget / 100 * (uint64_t)vram_pressure;
if (used > line && used - line > need) need = used - line;
}
if (!need) return;
int dropped = 0;
uint64_t need = used - line, freed = drop_overwritten(need, &dropped);
uint64_t freed = drop_overwritten(need, &dropped);
for (Tex *e = rel_head, *next; e && freed < need && dropped < PRESSURE_STEP && coarse_ms - e->rel_ms >= CLEAN_AGE_MS; e = next) {
next = e->rel_next;
if (!e->pub.mirrored || e->pub.gpu_owned || e->gpu_dirty) continue;
@@ -1740,8 +1762,9 @@ void vk_texture_new_submit(void) {
(unsigned long long)f * 60, (unsigned long long)ck, (unsigned long long)n_checks, (unsigned long long)calls,
(unsigned long long)untr);
n_checks = 0;
rt_log("perf: frame %llu: released textures kept %d, dropped under VRAM pressure %llu (%llu overwritten)",
(unsigned long long)f * 60, nreleased, (unsigned long long)n_pressure_drops,
rt_log("perf: frame %llu: released textures kept %d (%.0f MiB), dropped under VRAM pressure or the cap %llu "
"(%llu overwritten)", (unsigned long long)f * 60, nreleased, rel_bytes_total / 1048576.0,
(unsigned long long)n_pressure_drops,
(unsigned long long)n_pressure_overwritten);
n_pressure_drops = n_pressure_overwritten = 0;
}
@@ -2533,7 +2556,7 @@ void vk_texture_shutdown(void) {
by_addr = (nvn_hmap){.klen = 8};
samplers = (nvn_hmap){.klen = sizeof(samp_par)};
free(tslots), free(sslots);
tslots = NULL, sslots = NULL, ntslots = nsslots = 0, nreleased = 0;
tslots = NULL, sslots = NULL, ntslots = nsslots = 0, nreleased = 0, rel_bytes_total = 0;
rel_head = rel_tail = NULL;
if (sinfo) memset(sinfo, 0, sizeof *sinfo << 20), memset(sepoch, 0, sizeof *sepoch << 20); // no slot is registered: tokens 0
unlock();