Files
Valiantsin DzerakhandClaude Opus 5.5 213bc6af6a Wait out a short out-of-memory spell instead of ending the game
Crash logs ended in fatal errors when the system's commit ran out for a moment: a staging buffer (-2), a guest commit
with the system at 24442 of 24447 MB, image creation (-2). Windows grows a system-managed page file and some drivers
charge device memory to the process commit, so the same request often succeeds a moment later.

oom.c holds the wait nvn_api_cmd.c realloc_wait already used: it requests a release, retries every 10 ms for up to
30 s, logs the first 8 with the commit numbers. rt_oom_request sets a flag that vk_texture_new_submit takes (one atomic
load per submit): it drops up to 256 released textures of the two kinds sweep_pressure drops (overwritten, clean
mirrored), never GPU-written content, and frees the headroom blocks. vk_oom_release does at once what needs no submit:
headroom blocks, finished upload batches and deferred objects.

- memory.c commit: waits and retries without frz_lock between tries; every caller waits, boot ones too.
- vk_staging and the draw-data chunks (vk_exec_draw.c scratch): release, then wait; the open upload batch is not
  submitted, its caller still records into it.
- create_tex: release and one retry; no wait under the texture lock, the texture is asked for again at its next use.
- the texture write-back buffer: release, then wait.
- the headroom thread keeps no spare block for 30 s after a failure.

TOTK_TEST_OOM=<site>:<n> (commit, staging, image, chunk) fails the first n attempts; probe-kernel forces 3 refused
commits, probe-vkexec 2 staging buffers, 1 image and 1 chunk, and both pass through them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 09:53:30 +03:00

107 lines
4.5 KiB
C

// Short out-of-memory spells: a failed allocation waits for memory instead of ending the game.
//
// Windows refuses commit beyond the current limit while it grows a system-managed page file, and on some drivers device
// memory counts as process commit (vk_memory.c vk_mem_host_probe), so for a moment every allocation of the process can
// fail and the same request succeeds a little later. The failure sites (memory.c commit, vk_memory.c vk_staging,
// vk_texture.c create_tex, vk_exec_draw.c transient chunks, nvn_api_cmd.c realloc_wait) ask the Vulkan back end to give
// memory back (rt_oom_request: released textures and headroom blocks, at its next submit, where its locks are free) and
// retry for up to OOM_WAIT_MS before they end the game.
//
// TOTK_TEST_OOM=<site>:<n>[,<site>:<n>...] (sites commit, staging, image, chunk) fails the first n attempts at a site, so
// the wait can be tested without running out of memory; the probes arm it with rt_oom_force.
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "host.h"
#include "platform/platform.h"
#define OOM_WAIT_MS 30000u
#define OOM_STEP_MS 10u
#define OOM_REPORTS 8
#define OOM_REQUEST_EVERY 100 // retries between two release requests: a second, since a release only frees what is old enough
volatile int32_t rt_oom_req;
static const char *const site_name[RT_OOM_SITES] = {"commit", "staging", "image", "chunk"};
static volatile int32_t force_left[RT_OOM_SITES];
static volatile int32_t forced_parsed;
static PlatOnce force_once = PLAT_ONCE_INIT;
static void force_parse(void *arg) {
(void)arg;
const char *s = cfg_get("TOTK_TEST_OOM");
while (s && *s) {
const char *colon = strchr(s, ':'), *end = strchr(s, ',');
if (!end) end = s + strlen(s);
if (colon && colon < end) {
for (int i = 0; i < RT_OOM_SITES; i++)
if ((size_t)(colon - s) == strlen(site_name[i]) && !strncmp(s, site_name[i], (size_t)(colon - s))) {
int n = atoi(colon + 1);
__atomic_store_n(&force_left[i], n > 0 ? n : 0, __ATOMIC_RELAXED);
rt_log("oom: TOTK_TEST_OOM fails the first %d %s attempts", n, site_name[i]);
}
}
s = *end ? end + 1 : end;
}
__atomic_store_n(&forced_parsed, 1, __ATOMIC_RELEASE);
}
int rt_oom_forced(int site) {
if (!__atomic_load_n(&forced_parsed, __ATOMIC_ACQUIRE)) plat_once(&force_once, force_parse, NULL);
if (__atomic_load_n(&force_left[site], __ATOMIC_RELAXED) <= 0) return 0;
return plat_dec32(&force_left[site]) >= 0;
}
void rt_oom_force(int site, int n) {
if (!__atomic_load_n(&forced_parsed, __ATOMIC_ACQUIRE)) plat_once(&force_once, force_parse, NULL);
__atomic_store_n(&force_left[site], n, __ATOMIC_RELAXED);
}
int rt_oom_forced_left(int site) {
int n = __atomic_load_n(&force_left[site], __ATOMIC_RELAXED);
return n > 0 ? n : 0;
}
void rt_oom_request(void) { __atomic_store_n(&rt_oom_req, 1, __ATOMIC_RELEASE); }
int rt_oom_wait(const char *what, uint64_t bytes, int (*retry)(void *arg), void *arg) {
rt_oom_request();
static volatile int32_t reports;
int report = plat_inc32(&reports) <= OOM_REPORTS;
double proc, used, limit;
if (report) {
plat_commit_info(&proc, &used, &limit);
rt_log("%s of %llu bytes failed (memory commit: this process %.0f MB, the system %.0f of %.0f MB); waiting up "
"to %u s for memory", what, (unsigned long long)bytes, proc, used, limit, OOM_WAIT_MS / 1000);
}
uint64_t t0 = plat_now_ms();
for (int n = 1; plat_now_ms() - t0 < OOM_WAIT_MS; n++) {
plat_sleep_ms(OOM_STEP_MS);
if (retry(arg)) {
if (report)
rt_log("%s of %llu bytes succeeded after %llu ms", what, (unsigned long long)bytes,
(unsigned long long)(plat_now_ms() - t0));
return 1;
}
if (n % OOM_REQUEST_EVERY == 0) rt_oom_request();
}
plat_commit_info(&proc, &used, &limit);
rt_log("%s of %llu bytes still failed after %u s (memory commit: this process %.0f MB, the system %.0f of %.0f MB)",
what, (unsigned long long)bytes, OOM_WAIT_MS / 1000, proc, used, limit);
return 0;
}
typedef struct { void *old, *p; size_t n; } Realloc;
static int realloc_try(void *arg) {
Realloc *r = arg;
return (r->p = realloc(r->old, r->n)) != NULL;
}
void *rt_realloc_wait(void *old, size_t n, const char *what) {
void *p = realloc(old, n);
if (p || !n) return p;
Realloc r = {old, NULL, n};
return rt_oom_wait(what, n, realloc_try, &r) ? r.p : NULL;
}