mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
The game waits each frame until the GPU has finished the frame before. In Hyrule Field with FSR 4 the last of that work is one part of 4.7 ms, the temporal upscaler and the interface drawn after it, then 1.4 ms of write-backs, and what the game reads of the frame is done before the upscaler runs. With NX2NAVI_TAIL_EARLY the replay records that tail into a command buffer of its own, and when it hands the CPU nothing but timestamps, writes no pool memory but inline uniform updates of mirrored blocks and reads no guest memory late, the write-backs go to the queue before it with the executed timeline's signal. The game's waits end there, and the GPU runs the tail while the next frame is prepared. The GPU's commands are the same; a tail that fails a test goes as before. Off by default. RX 7900 XT, Ryzen 7 5800X3D, 3440x1440, FSR 4 Native AA, one batch, replay workers off: 50.7 against 42.5 fps standing and 56.0 against 45.9 along the field route; with FSR 3.1, 51.8 against 47.8 and 54.9 against 52.3. With three replay workers, FSR 4, standing: 63.5 against 49.4. The screenshots matched those without it standing, along the route and through warps to five areas, with no black frame outside the loading screens of both. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
160 lines
7.0 KiB
C
160 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;
|
|
}
|
|
|
|
// 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,
|
|
.game_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,
|
|
};
|