Files
NX2navi/runtime/gfx_null.c
chrg6 9728424aec End the wait after a frame's first submission when its passes are through, not its write-backs.
Every frame the render thread waited about 5 ms for the GPU after the frame's first submission, with the game
CPU-bound in every area. The wait is there so that the GPU has run that submission's passes before the game
rewrites what they read. But the submission also ends with the write-backs of what its shaders wrote, 60 MiB merged
into guest memory in 3 to 4 ms of GPU time, and the wait covered those too.
The write-backs now go into a part of their own. The part before them signals a second timeline, the executed
timeline, and the first-submission wait waits on that; the write-back part signals the timeline as before, so every
fence the game asks for still covers them, in the same queue order. wb_split = 0 keeps one part and the old wait.
Not measured in the game yet.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-09 23:42:12 -05:00

159 lines
6.9 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;
}
// NX2NAVI_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.
// 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,
.exec_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,
};