Files
Valiantsin DzerakhandClaude Opus 5.5 a1964a0c31 Free texture pin copies once no recording names them
Every texture initialization got a 128-byte host copy for recordings to point at, kept for the whole run. The game
initializes its transient targets again every frame (about 29 new copies a frame on 1.4.3), so host memory grew by
about 44 MiB per minute of play, standing still too, until the commit limit ended long sessions (#670).

Each pin now counts the recording copies whose ops name it: counted in flatten_recording, uncounted when a copy or a
per-submission clone is freed. A pin with no copy left is freed NVN_PIN_AGE (1200) submits after its last use, so a
recording still in progress keeps its pins, and never while the replay holds it as a current render target, which
carries over to the next submission and is compared by pointer. TOTK_PIN_SWEEP=0 keeps every copy.

A 300 s standing-still run on the field save: host memory without VRAM grew 5 MiB, against about 220 MiB before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 15:00:36 +03:00

158 lines
7.0 KiB
C

// The null graphics back end: no GPU. Every submit completes at
// once and advances the fence value, pools always initialise, presents are only counted, texel reads return zeros.
// A submit performs the CPU-visible side effects of its op streams in order: event signals, counter reports (timestamp
// ticks, counter 0), buffer-to-buffer copies and uniform updates.
// It is the CPU-side gate for the NVN front end. Also the back-end choice (gfx_backend) and the statistics (gfx_stats).
#include <stdint.h>
#include <string.h>
#include "gfx.h"
#include "host.h"
#include "nvn_api.h"
GfxStats gfx_counters;
GfxStats gfx_stats(void) {
GfxStats s;
s.frames = __atomic_load_n(&gfx_counters.frames, __ATOMIC_RELAXED);
s.nvn_calls = __atomic_load_n(&gfx_counters.nvn_calls, __ATOMIC_RELAXED);
s.nvn_distinct = __atomic_load_n(&gfx_counters.nvn_distinct, __ATOMIC_RELAXED);
s.unimplemented = __atomic_load_n(&gfx_counters.unimplemented, __ATOMIC_RELAXED);
s.validation_errors = __atomic_load_n(&gfx_counters.validation_errors, __ATOMIC_RELAXED);
s.shader_failures = __atomic_load_n(&gfx_counters.shader_failures, __ATOMIC_RELAXED);
s.draws_skipped = __atomic_load_n(&gfx_counters.draws_skipped, __ATOMIC_RELAXED);
return s;
}
// TOTK_GPU=null|vulkan, default vulkan (rt_config); chosen once.
const GfxBackend *gfx_backend(void) {
static const GfxBackend *chosen;
const GfxBackend *b = __atomic_load_n(&chosen, __ATOMIC_ACQUIRE);
if (b) return b;
const char *gpu = rt_config()->gpu;
b = gpu && !strcmp(gpu, "null") ? &gfx_null_backend : &gfx_vulkan_backend;
__atomic_store_n(&chosen, b, __ATOMIC_RELEASE);
return b;
}
static uint64_t fence;
static int null_pool_init(pool_t *p) { (void)p; return 1; }
static void null_pool_fini(pool_t *p) { (void)p; }
static int null_pool_map_virtual(pool_t *v, int n, const void *requests) { (void)v, (void)n, (void)requests; return 1; }
static void null_texture_register(int id, const tex_t *t, const view_t *v) { (void)id, (void)t, (void)v; }
static void null_sampler_register(int id, const samp_par *s) { (void)id, (void)s; }
static void null_texture_release(const tex_t *t) { (void)t; }
static void null_program_release(const prog_t *p) { (void)p; }
// The CPU-visible side effects of one op stream, in stream order (MNG nvn_mtl.m mtl_exec and the commit handler:
// 10618-10642, 1677-1678, 1960-1985). Everything else (draws, dispatches, clears, texture copies) has no effect here.
// Addresses of CPU-invisible pools (the GPU window) hold no host memory, so those writes are dropped.
static void *null_cpu_addr(uint64_t addr, uint64_t size, const char *what) {
if (nvn_gpu_only(addr)) return NULL;
if (!nvn_pool_find(addr, size ? size : 1)) rt_fatal("gfx: null back end: %s at %#llx (%llu bytes) is in no memory pool", what,
(unsigned long long)addr, (unsigned long long)size);
return (void *)(uintptr_t)addr;
}
// NVNcounterData = {counter, timestamp}. Timestamps are Tegra 19.2 MHz ticks, the base of GetSystemTick; every counter
// type the null GPU cannot measure (all but the timestamps, samples passed included) reports 0.
static void null_report(uint64_t addr) {
NVNcounterData *d = null_cpu_addr(addr, sizeof *d, "counter report");
if (!d) return;
uint64_t rec[2] = {0, (uint64_t)((unsigned __int128)kern_now_ns() * 12 / 625)};
memcpy(d, rec, sizeof rec);
}
static void null_replay(uint64_t handle) {
for (const op_hdr *h = nvn_op_first(handle);; h = nvn_op_next(h)) {
const void *a = nvn_op_payload(h);
switch (h->op) {
case OP_END: return;
case OP_SIGNAL_EVENT: {
const p_signal_event *e = a;
if (e->mode == NVN_EVENT_SIGNAL_MODE_ADD) __atomic_fetch_add(e->val, e->value, __ATOMIC_RELEASE);
else __atomic_store_n(e->val, e->value, __ATOMIC_RELEASE);
break;
}
case OP_REPORT_COUNTER: null_report(((const p_report_counter *)a)->addr); break;
case OP_COPY_B2B: {
const p_copy_b2b *b = a;
void *s = null_cpu_addr(b->src, b->size, "buffer copy source"), *d = null_cpu_addr(b->dst, b->size, "buffer copy target");
if (s && d) memmove(d, s, b->size);
break;
}
case OP_CLEAR_BUF: {
const p_clear_buf *f = a;
uint32_t *d = null_cpu_addr(f->addr, f->size, "buffer fill");
for (uint64_t i = 0; d && i < f->size / 4; i++) d[i] = f->value;
break;
}
case OP_UPDATE_UB: {
const p_update_ub *u = a;
void *d = null_cpu_addr(u->dst, u->size, "uniform update");
if (d) memcpy(d, u->data, u->size);
break;
}
default: break;
}
}
}
static uint64_t null_submit(const uint64_t *handles, int n) {
nvn_pins_sweep(nvn_submit_count(), NULL, 0); // no replay state outlives a submission here
for (int i = 0; i < n; i++) null_replay(handles[i]);
return __atomic_add_fetch(&fence, 1, __ATOMIC_ACQ_REL);
}
static int null_fence_wait(uint64_t value, uint64_t timeout_ns) {
(void)timeout_ns;
return value <= __atomic_load_n(&fence, __ATOMIC_ACQUIRE); // a value never submitted is never reached
}
static uint64_t null_fence_done(void) { return __atomic_load_n(&fence, __ATOMIC_ACQUIRE); }
static void null_present(const tex_t *t, const int crop[4], uint64_t at_ns) {
(void)t, (void)crop, (void)at_ns;
__atomic_fetch_add(&gfx_counters.frames, 1, __ATOMIC_RELAXED);
}
static void null_finish(void) {}
static void null_read_texels(const tex_t *t, const view_t *v, const NVNcopyRegion *r, uint8_t *dst, uint64_t row,
uint64_t img) {
(void)t, (void)v;
int h = r->height > 0 ? r->height : 1, d = r->depth > 0 ? r->depth : 1;
for (int z = 0; z < d; z++) memset(dst + (uint64_t)z * img, 0, row * (uint64_t)h);
}
static void null_write_texels(const tex_t *t, const view_t *v, const NVNcopyRegion *r, const uint8_t *src,
uint64_t row, uint64_t img) {
(void)t, (void)v, (void)r, (void)src, (void)row, (void)img;
}
static void null_guest_written(uint64_t addr, uint64_t size) { (void)addr, (void)size; }
static void null_guest_read(uint64_t addr, uint64_t size) { (void)addr, (void)size; }
static void null_cpu_synced(uint64_t fence) { (void)fence; }
const GfxBackend gfx_null_backend = {
.name = "null",
.pool_init = null_pool_init,
.pool_fini = null_pool_fini,
.pool_map_virtual = null_pool_map_virtual,
.texture_register = null_texture_register,
.sampler_register = null_sampler_register,
.texture_release = null_texture_release,
.program_release = null_program_release,
.submit = null_submit,
.fence_wait = null_fence_wait,
.fence_done = null_fence_done,
.present = null_present,
.finish = null_finish,
.read_texels = null_read_texels,
.write_texels = null_write_texels,
.guest_written = null_guest_written,
.guest_read = null_guest_read,
.cpu_synced = null_cpu_synced,
};