Files
ProSepandClaude Opus 5.5 a3d876e2d2 Let sync_ahead lag the 1.4.3 frame wait too
On 1.4.3 the per-frame wait is the second of the two nvnSyncWait calls in f_70002db8ac (return address 0x70002db99c). Like on 1.2.1, its sync comes from a two-entry ring indexed per frame; the first call waits on one object's sync behind a mode check. wdiasjunior found the site and measured it on an RX 7800 XT (#676): about 67-70 to 81-83 fps on Great Sky Island.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:55:56 +01:00

703 lines
35 KiB
C

// NVN front end, first slice: device, queue, memory pool, buffer, sync and
// event objects. Ported from MacNativeGames games/totk/third_party/d2r_gfx/gfx/nvn/hle_nvn.c @ b75be8cf (MIT,
// Copyright (c) 2026 jhaizhou; origin d2r-mobile), lines 137-253, 534-645 and 1975-2001; Metal, mach, the translation
// thread and Apple diagnostics dropped. Calls that reached Metal go to gfx_backend() (gfx.h). nvnQueuePresentTexture
// belongs to the window slice; nvnDeviceGetProcAddress lives in nvn_procs.c.
//
// Objects are stored in place in the NVN object memory the game provides (nvn_obj.h); every argument is read from the
// AAPCS64 registers (host.h arg_x / arg_p) and every result returned with ret_x.
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "cfg.h"
#include "gfx.h"
#include "host.h"
#include "nvn_api.h"
#include "nvn_pool_index.h"
#include "platform/platform.h"
#define DEV_MAGIC MAGIC('D', 'E', 'V', 0)
#define QUEUE_MAGIC MAGIC('Q', 'U', 'E', 'U')
#define BUF_MAGIC MAGIC('B', 'U', 'F', 0)
#define SYNC_MAGIC MAGIC('S', 'Y', 'N', 'C')
#define EVENT_MAGIC MAGIC('E', 'V', 'N', 'T')
// ---------- shared helpers (nvn_api.h)
// The call count goes to the shared counter 1024 at a time: the main thread and the job workers all record command
// buffers, and one locked add per call kept the counter's cache line moving between their cores (0.8k samples a
// fight). The count lags by at most 1023 calls per thread.
void nvn_called(uint8_t *seen, const char *name) {
static _Thread_local uint32_t calls;
if (++calls == 1024) {
__atomic_fetch_add(&gfx_counters.nvn_calls, calls, __ATOMIC_RELAXED);
calls = 0;
}
if (__atomic_load_n(seen, __ATOMIC_RELAXED) || __atomic_exchange_n(seen, 1, __ATOMIC_RELAXED)) return;
uint64_t n = __atomic_add_fetch(&gfx_counters.nvn_distinct, 1, __ATOMIC_RELAXED);
NVN_LOG("first call %llu: %s", (unsigned long long)n, name);
}
uint64_t nvn_new_serial(void) {
static uint64_t n;
return __atomic_add_fetch(&n, 1, __ATOMIC_RELAXED);
}
static uint64_t last_fence;
uint64_t nvn_fence_last(void) { return __atomic_load_n(&last_fence, __ATOMIC_ACQUIRE); }
static int depth_minus_w;
int nvn_depth_minus_w(void) { return __atomic_load_n(&depth_minus_w, __ATOMIC_RELAXED); }
void nvn_guest_written(uint64_t addr, uint64_t size) {
if (size) gfx_backend()->guest_written(addr, size);
}
// memcpy of pool storage the GPU rendered into must copy what the GPU wrote: TotK moves its generated grass maps
// between pools with memcpy. Copies below 1 KiB skip the check; the copy itself is memmove, as overlapping
// arguments are undefined for memcpy anyway.
static void ov_memcpy(Cpu *c) {
uint64_t dst = arg_x(c, 0), src = arg_x(c, 1), n = arg_x(c, 2);
if (n >= 1024) gfx_backend()->guest_read(src, n);
memmove((void *)(uintptr_t)dst, (const void *)(uintptr_t)src, n);
ret_x(c, dst);
}
HOST_OVERRIDE("memcpy", ov_memcpy);
__attribute__((weak)) void nvn_submit_prepare(const uint64_t *handles, int n) { (void)handles, (void)n; }
__attribute__((weak)) void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
memcpy(out, handles, sizeof *out * (size_t)n);
}
__attribute__((weak)) int gfx_replay_async(void) { return 0; }
__attribute__((weak)) uint64_t gfx_replayed_submits(void) { return nvn_submit_count(); }
// ---------- pools: GPU window (section 3.1) and the address index
static PlatMutex pool_lock = PLAT_MUTEX_INIT; // guards gva and index
typedef struct { uint64_t lo, hi; } Span;
enum { GVA_MAX = 1024 };
static Span gva[GVA_MAX]; // ranges handed out in the GPU window, sorted by lo
static int ngva;
static pidx pool_index;
// First fit in [RT_GPU_VA_BASE, +RT_GPU_VA_SIZE); size is a multiple of 64 KiB, so every range stays 64 KiB aligned.
static uint64_t gva_alloc(uint64_t size) {
plat_mutex_lock(&pool_lock);
uint64_t at = RT_GPU_VA_BASE, r = 0;
int i = 0;
for (; i < ngva && gva[i].lo - at < size; i++) at = gva[i].hi;
if (ngva < GVA_MAX && size <= RT_GPU_VA_BASE + RT_GPU_VA_SIZE - at) {
memmove(gva + i + 1, gva + i, (size_t)(ngva - i) * sizeof *gva);
gva[i] = (Span){at, at + size};
ngva++;
r = at;
}
plat_mutex_unlock(&pool_lock);
return r;
}
static void gva_free(uint64_t lo) {
plat_mutex_lock(&pool_lock);
for (int i = 0; i < ngva; i++)
if (gva[i].lo == lo) {
memmove(gva + i, gva + i + 1, (size_t)(ngva - i - 1) * sizeof *gva);
ngva--;
break;
}
plat_mutex_unlock(&pool_lock);
}
static void index_add(const pool_t *o) {
uintptr_t lo = (uintptr_t)o->mem;
pidx_ent e = {.lo = lo, .hi = lo + o->size, .base = lo, .gpu = lo, .key = o};
plat_mutex_lock(&pool_lock);
pidx_remove(&pool_index, o, NULL); // a pool object initialized again without Finalize
int ok = pidx_insert(&pool_index, &e);
plat_mutex_unlock(&pool_lock);
if (!ok) rt_fatal("nvn: out of memory for the pool index");
}
static void index_del(const pool_t *o) {
plat_mutex_lock(&pool_lock);
pidx_remove(&pool_index, o, NULL);
plat_mutex_unlock(&pool_lock);
}
const pool_t *nvn_pool_find(uint64_t addr, uint64_t len) {
plat_mutex_lock_shared(&pool_lock);
const pidx_ent *e = pidx_find(&pool_index, (uintptr_t)addr, len);
const pool_t *p = e ? e->key : NULL;
plat_mutex_unlock_shared(&pool_lock);
return p;
}
// Physical and virtual pools (MacNativeGames totk_vpool.c checks). A CPU-invisible one gets a GPU window range.
// A CPU-visible physical pool (1.4.3: flags 0x18c, GPU_NO_ACCESS) keeps the game's storage for the CPU; the back end
// gives it device memory as any physical pool (vk_memory.c vk_pool_init), which the GPU reaches through virtual pools.
static int pool_place(pool_t *o) {
int phys = (o->flags & NVN_MEMORY_POOL_FLAGS_PHYSICAL) != 0, virt = (o->flags & NVN_MEMORY_POOL_FLAGS_VIRTUAL) != 0;
if (!phys && !virt) return 1;
const char *kind = phys ? "physical" : "virtual";
if (phys && virt) { rt_log("nvn: pool flags %#x are both PHYSICAL and VIRTUAL", o->flags); return 0; }
if (!o->size || o->size % NVN_VPOOL_PAGE) {
rt_log("nvn: %s pool size %#llx is not a multiple of 64 KiB", kind, (unsigned long long)o->size);
return 0;
}
if (virt && o->mem) { rt_log("nvn: virtual pool given storage %p", (void *)o->mem); return 0; }
if (phys && !o->mem) { rt_log("nvn: physical pool without storage"); return 0; }
if (!(o->flags & NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS)) {
if (virt) {
rt_log("nvn: CPU-visible virtual pool (flags %#x, size %#llx) refused", o->flags, (unsigned long long)o->size);
return 0;
}
rt_log("nvn: CPU-visible physical pool %p (flags %#x, size %#llx) keeps the game's storage", (void *)o->mem,
o->flags, (unsigned long long)o->size);
return 1;
}
uint64_t a = gva_alloc(o->size);
if (!a) {
rt_log("nvn: GPU address window full, %s pool of %#llx bytes refused", kind, (unsigned long long)o->size);
return 0;
}
o->mem = (uint8_t *)(uintptr_t)a;
o->span = o->size;
return 1;
}
static int pool_gpu_visible(const pool_t *o) { return !(o->flags & NVN_MEMORY_POOL_FLAGS_GPU_NO_ACCESS); }
// ---------- Device
static const int devinfo[] = { // Tegra X1 values (nnheaders nvn_DeviceConstantsNX.h, measured on SMO)
[0x00] = 55, [0x01] = 15, // ABI: TOTK main requires major 55, minor >= 15
[0x02] = 14, [0x03] = 65536, [0x04] = 256, [0x05] = 8, [0x06] = 16, [0x07] = 4, [0x08] = 16, [0x09] = 32,
[0x0A] = 16, [0x0B] = 4, [0x0C] = 4, [0x0D] = 4, [0x0E] = 16, [0x0F] = 32, [0x10] = 32, [0x11] = 256, [0x12] = 256,
[0x13] = 4, [0x14] = 8, [0x15] = 512, [0x16] = 256, [0x17] = 2, [0x18] = 256, [0x19] = 4096, [0x1A] = 131072,
[0x1B] = 16, [0x1C] = 1536, [0x1D] = 1024, [0x1E] = 64, [0x1F] = 1536, [0x20] = 65535, [0x21] = 65535, [0x22] = 65535,
[0x23] = 8, [0x24] = 1048576, [0x25] = 4096, [0x26] = 16, [0x27] = 4096, [0x28] = 1, [0x29] = 1, [0x2A] = 1,
[0x2B] = 262144, [0x2C] = 15, [0x2D] = 2048, [0x2E] = 1, [0x2F] = 1, [0x30] = 14, [0x31] = 5, [0x32] = 1, [0x33] = 8,
[0x34] = 8, [0x35] = 4, [0x36] = 32, [0x37] = 128, [0x38] = 32, [0x39] = 128, [0x3A] = 65536, [0x3B] = 1, [0x3C] = 4,
[0x3D] = 16384, [0x3E] = 134217728, [0x3F] = 2048, [0x40] = 16384, [0x41] = 16384, [0x42] = 1, [0x43] = 1, [0x44] = 1,
[0x45] = 1, [0x46] = 5, [0x47] = 1, [0x48] = 1, [0x49] = 1, [0x4A] = 1, [0x4B] = 1, [0x4C] = 1, [0x4D] = 16,
[0x4E] = 1024, [0x4F] = 32, [0x50] = 4096, [0x51] = 65536, [0x52] = 65536, [0x53] = 256, [0x54] = 16384,
[0x55] = 262144, [0x56] = 4096, [0x57] = 1, [0x58] = 4, [0x59] = 2, [0x5A] = 0, [0x5B] = 4096, [0x5C] = 16384,
[0x5D] = 4096, [0x5E] = 128, [0x5F] = 4096, // 0x5F (separate sampler bindings) from the sampler pool limit, unverified
};
// void nvnDeviceBuilderSetDefaults(NVNdeviceBuilder *b)
NVN_API(nvnDeviceBuilderSetDefaults) { memset(arg_p(c, 0), 0, sizeof(dev_builder_t)); }
// void nvnDeviceBuilderSetFlags(NVNdeviceBuilder *b, int flags)
NVN_API(nvnDeviceBuilderSetFlags) { ((dev_builder_t *)arg_p(c, 0))->flags = (uint32_t)arg_x(c, 1); }
// NVNboolean nvnDeviceInitialize(NVNdevice *d, const NVNdeviceBuilder *b)
NVN_API(nvnDeviceInitialize) {
dev_t_ *o = arg_p(c, 0);
const dev_builder_t *b = arg_p(c, 1);
memset(o, 0, sizeof *o);
o->magic = DEV_MAGIC;
o->flags = b->flags;
NVN_LOG("DeviceInitialize %p flags %#x, back end %s", (void *)o, o->flags, gfx_backend()->name);
ret_x(c, 1);
}
// void nvnDeviceFinalize(NVNdevice *d)
NVN_API(nvnDeviceFinalize) { ((dev_t_ *)arg_p(c, 0))->magic = 0; }
// void nvnDeviceGetInteger(const NVNdevice *d, NVNdeviceInfo what, int *out)
NVN_API(nvnDeviceGetInteger) {
unsigned w = (unsigned)arg_x(c, 1);
int *out = arg_p(c, 2);
if (w >= sizeof devinfo / sizeof *devinfo) rt_fatal("nvn: unsupported NVN device property %#x", w);
*out = devinfo[w];
NVN_LOG("DeviceGetInteger %#x -> %d", w, *out);
}
// NVNboolean nvnDeviceRegisterFastClearColor{,i,ui}(NVNdevice *d, const T *color, NVNformat format), ...Depth(d, float):
// fast-clear registration is an optimisation hint; "not registered" keeps the normal clear path.
NVN_API(nvnDeviceRegisterFastClearColor) { ret_x(c, 0); }
NVN_API(nvnDeviceRegisterFastClearColori) { ret_x(c, 0); }
NVN_API(nvnDeviceRegisterFastClearColorui) { ret_x(c, 0); }
NVN_API(nvnDeviceRegisterFastClearDepth) { ret_x(c, 0); }
// NVNimageHandle nvnDeviceGetImageHandle(const NVNdevice *d, int id)
NVN_API(nvnDeviceGetImageHandle) { ret_x(c, (uint64_t)(int64_t)(int)arg_x(c, 1)); }
// NVNtextureHandle nvnDeviceGetTextureHandle(const NVNdevice *d, int texture_id, int sampler_id):
// texture id (20 bits) | sampler id << 20, the encoding the replay decodes (NVN_HANDLE_TEX / NVN_HANDLE_SAMP)
NVN_API(nvnDeviceGetTextureHandle) {
int tid = (int)arg_x(c, 1), sid = (int)arg_x(c, 2);
if (tid < 0 || tid >= (1 << 20) || sid < 0 || sid >= (1 << 12)) rt_fatal("nvn: texture handle ids out of range %d/%d", tid, sid);
ret_x(c, (uint64_t)tid | (uint64_t)sid << 20);
}
// NVNtextureHandle nvnDeviceGetTexelFetchHandle(const NVNdevice *d, int texture_id)
NVN_API(nvnDeviceGetTexelFetchHandle) {
int tid = (int)arg_x(c, 1);
if (tid < 0 || tid >= (1 << 20)) rt_fatal("nvn: texel fetch id out of range %d", tid);
ret_x(c, (uint64_t)tid);
}
// NVNseparateTextureHandle / NVNseparateSamplerHandle nvnDeviceGetSeparate{Texture,Sampler}Handle(const NVNdevice *d,
// int id): the descriptor index; a sampler index sits at bit 20 as in combined handles. One-word structs return in x0.
NVN_API(nvnDeviceGetSeparateTextureHandle) { ret_x(c, (uint64_t)(int64_t)(int)arg_x(c, 1)); }
NVN_API(nvnDeviceGetSeparateSamplerHandle) { ret_x(c, (uint64_t)(int64_t)(int)arg_x(c, 1) << 20); }
// void nvnDeviceInstallDebugCallback(NVNdevice *d, PFNNVNDEBUGCALLBACKPROC cb, void *data, NVNboolean enable)
NVN_API(nvnDeviceInstallDebugCallback) {
dev_t_ *o = arg_p(c, 0);
o->dbg_cb = nvn_arg_bool(c, 3) ? arg_p(c, 1) : NULL;
o->dbg_data = arg_p(c, 2);
}
// void nvnDeviceSetWindowOriginMode(NVNdevice *d, NVNwindowOriginMode mode)
NVN_API(nvnDeviceSetWindowOriginMode) {
uint32_t m = (uint32_t)arg_x(c, 1);
((dev_t_ *)arg_p(c, 0))->origin = m;
NVN_LOG("window origin %s", m ? "upper left (as D3D and Vulkan)" : "lower left (GL)");
}
// void nvnDeviceSetDepthMode(NVNdevice *d, NVNdepthMode mode)
NVN_API(nvnDeviceSetDepthMode) {
uint32_t m = (uint32_t)arg_x(c, 1);
((dev_t_ *)arg_p(c, 0))->depth_mode = m;
__atomic_store_n(&depth_minus_w, m == NVN_DEPTH_MODE_NEAR_IS_MINUS_W, __ATOMIC_RELAXED);
NVN_LOG("depth mode %s", m ? "near is 0 (as D3D and Vulkan)" : "near is -w (GL): vertex shaders map z to (z+w)/2");
}
// void nvnDeviceFinalizeCommandHandle(NVNdevice *d, NVNcommandHandle h)
NVN_API(nvnDeviceFinalizeCommandHandle) { (void)c; }
// ---------- Memory pools
// void nvnMemoryPoolBuilderSetDefaults(NVNmemoryPoolBuilder *b)
NVN_API(nvnMemoryPoolBuilderSetDefaults) { memset(arg_p(c, 0), 0, sizeof(pool_builder_t)); }
// void nvnMemoryPoolBuilderSetDevice(NVNmemoryPoolBuilder *b, NVNdevice *d)
NVN_API(nvnMemoryPoolBuilderSetDevice) { ((pool_builder_t *)arg_p(c, 0))->dev = arg_p(c, 1); }
// void nvnMemoryPoolBuilderSetStorage(NVNmemoryPoolBuilder *b, void *memory, size_t size)
NVN_API(nvnMemoryPoolBuilderSetStorage) {
pool_builder_t *b = arg_p(c, 0);
b->mem = arg_p(c, 1);
b->size = arg_x(c, 2);
}
// void nvnMemoryPoolBuilderSetFlags(NVNmemoryPoolBuilder *b, int flags)
NVN_API(nvnMemoryPoolBuilderSetFlags) { ((pool_builder_t *)arg_p(c, 0))->flags = (uint32_t)arg_x(c, 1); }
// NVNboolean nvnMemoryPoolInitialize(NVNmemoryPool *p, const NVNmemoryPoolBuilder *b)
// Pool setup imports, mirrors and maps memory in the back end (the driver, VirtualAlloc): up to 20 ms on the game's
// loader threads, which held a virtual core through it while job workers waited (TOTK_CORE_DIAG). The thread gives
// the core up, as for any blocking host call.
NVN_API(nvnMemoryPoolInitialize) {
NVN_BLOCKING();
pool_t *o = arg_p(c, 0);
const pool_builder_t *s = arg_p(c, 1);
memset(o, 0, sizeof *o);
o->magic = NVN_POOL_MAGIC;
o->flags = s->flags;
o->mem = o->game_mem = s->mem;
o->size = s->size;
int ok = pool_place(o);
if (ok && !o->mem) {
rt_log("nvn: MemoryPoolInitialize: pool without storage, flags %#x size %#llx", o->flags,
(unsigned long long)o->size);
ok = 0;
}
if (ok && !gfx_backend()->pool_init(o)) {
rt_log("nvn: MemoryPoolInitialize: the %s back end refused pool %p (flags %#x, size %#llx)", gfx_backend()->name,
(void *)o->mem, o->flags, (unsigned long long)o->size);
ok = 0;
}
if (ok && pool_gpu_visible(o)) index_add(o);
if (!ok) {
if (nvn_gpu_only((uintptr_t)o->mem)) gva_free((uintptr_t)o->mem);
o->magic = 0;
}
NVN_LOG("MemoryPool %p mem %p size %#llx flags %#x storage %p%s", (void *)o, (void *)o->mem,
(unsigned long long)o->size, o->flags, (void *)o->game_mem, ok ? "" : " failed");
ret_x(c, ok);
}
// void nvnMemoryPoolFinalize(NVNmemoryPool *p)
NVN_API(nvnMemoryPoolFinalize) {
NVN_BLOCKING(); // as nvnMemoryPoolInitialize
pool_t *o = arg_p(c, 0);
if (o->magic != NVN_POOL_MAGIC) return;
if (pool_gpu_visible(o)) index_del(o);
gfx_backend()->pool_fini(o);
if (nvn_gpu_only((uintptr_t)o->mem)) gva_free((uintptr_t)o->mem);
o->mem = o->game_mem;
o->span = 0;
o->magic = 0;
}
static uint64_t flush_stat[2][3][2]; // nvn_flush_stats
static void flush_note(int kind, const pool_t *p, uint64_t size) {
uint32_t f = p ? p->flags : 0;
int k = f & NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS ? 2 : f & NVN_MEMORY_POOL_FLAGS_CPU_CACHED ? 0
: f & NVN_MEMORY_POOL_FLAGS_CPU_UNCACHED ? 1 : 2;
__atomic_fetch_add(&flush_stat[kind][k][0], 1, __ATOMIC_RELAXED);
__atomic_fetch_add(&flush_stat[kind][k][1], size, __ATOMIC_RELAXED);
}
void nvn_flush_stats(uint64_t out[2][3][2]) {
for (int i = 0; i < 2; i++)
for (int k = 0; k < 3; k++)
for (int j = 0; j < 2; j++) out[i][k][j] = __atomic_exchange_n(&flush_stat[i][k][j], 0, __ATOMIC_RELAXED);
}
// void nvnMemoryPoolFlushMappedRange(const NVNmemoryPool *p, ptrdiff_t offset, size_t size): storage is coherent; a
// back end with device-side copies hears of it (nvn_pool_flushed)
NVN_API(nvnMemoryPoolFlushMappedRange) {
const pool_t *p = arg_p(c, 0);
int64_t off = (int64_t)arg_x(c, 1);
uint64_t size = arg_x(c, 2);
if (rt_trace & RT_TRACE_PERF) flush_note(0, p, size);
if (p && off >= 0 && (uint64_t)off < p->size) nvn_pool_flushed(p, (uint64_t)off, size < p->size - (uint64_t)off ? size : p->size - (uint64_t)off);
}
// NVNmemoryPoolFlags nvnMemoryPoolGetFlags(const NVNmemoryPool *p)
NVN_API(nvnMemoryPoolGetFlags) { ret_x(c, ((const pool_t *)arg_p(c, 0))->flags); }
// void *nvnMemoryPoolMap(const NVNmemoryPool *p): NULL for CPU_NO_ACCESS
NVN_API(nvnMemoryPoolMap) {
const pool_t *o = arg_p(c, 0);
if (o->magic != NVN_POOL_MAGIC || (o->flags & NVN_MEMORY_POOL_FLAGS_CPU_NO_ACCESS)) { ret_x(c, 0); return; }
ret_x(c, (uintptr_t)o->mem);
}
// MacNativeGames totk_vpool.c: every request is checked before any mapping changes.
static int check_request(const pool_t *v, const nvn_mapping_t *r, int i) {
uint64_t end = (uint64_t)r->virtual_offset + r->size;
if (r->virtual_offset < 0 || r->physical_offset < 0 || !r->size) {
rt_log("nvn: MapVirtual request %d has a negative or empty range", i);
return 0;
}
if (((uint64_t)r->virtual_offset | (uint64_t)r->physical_offset | r->size) % NVN_VPOOL_PAGE) {
rt_log("nvn: MapVirtual request %d not 64 KiB aligned (v %#llx p %#llx n %#llx)", i,
(unsigned long long)r->virtual_offset, (unsigned long long)r->physical_offset, (unsigned long long)r->size);
return 0;
}
if (end < r->size || end > v->size) {
rt_log("nvn: MapVirtual request %d exceeds the virtual pool (%#llx > %#llx)", i, (unsigned long long)end,
(unsigned long long)v->size);
return 0;
}
if (!r->physical_pool) return 1; // unmap
const pool_t *p = (const pool_t *)(const void *)r->physical_pool;
if (p->magic != NVN_POOL_MAGIC || !(p->flags & NVN_MEMORY_POOL_FLAGS_PHYSICAL)) {
rt_log("nvn: MapVirtual request %d source %p is not a physical pool", i, (const void *)p);
return 0;
}
uint64_t pend = (uint64_t)r->physical_offset + r->size;
if (pend < r->size || pend > p->size) {
rt_log("nvn: MapVirtual request %d exceeds the physical pool (%#llx > %#llx)", i, (unsigned long long)pend,
(unsigned long long)p->size);
return 0;
}
return 1;
}
// NVNboolean nvnMemoryPoolMapVirtual(NVNmemoryPool *p, int n, const NVNmappingRequest *requests)
NVN_API(nvnMemoryPoolMapVirtual) {
NVN_BLOCKING(); // as nvnMemoryPoolInitialize
pool_t *v = arg_p(c, 0);
int n = (int)arg_x(c, 1);
const nvn_mapping_t *r = arg_p(c, 2);
if (v->magic != NVN_POOL_MAGIC || !(v->flags & NVN_MEMORY_POOL_FLAGS_VIRTUAL) || !v->mem) {
rt_log("nvn: MapVirtual target %p is not a virtual pool", (void *)v);
ret_x(c, 0);
return;
}
if (n < 0 || (n && !r)) { rt_log("nvn: MapVirtual with %d requests at %p", n, (const void *)r); ret_x(c, 0); return; }
for (int i = 0; i < n; i++)
if (!check_request(v, &r[i], i)) { ret_x(c, 0); return; }
if (!gfx_backend()->pool_map_virtual(v, n, r))
rt_fatal("nvn: MapVirtual of %d requests failed after validation; virtual pool %p is inconsistent", n, (void *)v);
NVN_LOG("MapVirtual pool %p, %d requests", (void *)v->mem, n);
ret_x(c, 1);
}
// NVNbufferAddress nvnMemoryPoolGetBufferAddress(const NVNmemoryPool *p)
NVN_API(nvnMemoryPoolGetBufferAddress) { ret_x(c, (uintptr_t)((const pool_t *)arg_p(c, 0))->mem); }
// ---------- Buffers: they reference their pool and own no storage
// void nvnBufferBuilderSetDevice(NVNbufferBuilder *b, NVNdevice *d)
NVN_API(nvnBufferBuilderSetDevice) { ((buf_builder_t *)arg_p(c, 0))->dev = arg_p(c, 1); }
// void nvnBufferBuilderSetDefaults(NVNbufferBuilder *b): keeps the device
NVN_API(nvnBufferBuilderSetDefaults) {
buf_builder_t *b = arg_p(c, 0);
NVNdevice *d = b->dev;
memset(b, 0, sizeof *b);
b->dev = d;
}
// void nvnBufferBuilderSetStorage(NVNbufferBuilder *b, NVNmemoryPool *p, ptrdiff_t offset, size_t size)
NVN_API(nvnBufferBuilderSetStorage) {
buf_builder_t *b = arg_p(c, 0);
b->pool = arg_p(c, 1);
b->off = (int64_t)arg_x(c, 2);
b->size = arg_x(c, 3);
}
// NVNboolean nvnBufferInitialize(NVNbuffer *b, const NVNbufferBuilder *builder)
NVN_API(nvnBufferInitialize) {
buf_t *o = arg_p(c, 0);
const buf_builder_t *s = arg_p(c, 1);
if (!s->pool || s->pool->magic != NVN_POOL_MAGIC || s->off < 0 || (uint64_t)s->off > s->pool->size ||
s->size > s->pool->size - (uint64_t)s->off) {
ret_x(c, 0);
return;
}
*o = (buf_t){BUF_MAGIC, s->pool, s->off, s->size};
ret_x(c, 1);
}
// void nvnBufferFinalize(NVNbuffer *b)
NVN_API(nvnBufferFinalize) { memset(arg_p(c, 0), 0, sizeof(buf_t)); }
static uint64_t buf_address(const buf_t *o) { return o->pool && o->pool->mem ? (uintptr_t)o->pool->mem + (uint64_t)o->off : 0; }
// void *nvnBufferMap(const NVNbuffer *b): NULL in a GPU-window pool, which has no CPU view
NVN_API(nvnBufferMap) {
uint64_t a = buf_address(arg_p(c, 0));
ret_x(c, nvn_gpu_only(a) ? 0 : a);
}
// NVNbufferAddress nvnBufferGetAddress(const NVNbuffer *b)
NVN_API(nvnBufferGetAddress) { ret_x(c, buf_address(arg_p(c, 0))); }
// NVNmemoryPool *nvnBufferGetMemoryPool(const NVNbuffer *b)
NVN_API(nvnBufferGetMemoryPool) { ret_x(c, (uintptr_t)((const buf_t *)arg_p(c, 0))->pool); }
// ptrdiff_t nvnBufferGetMemoryOffset(const NVNbuffer *b)
NVN_API(nvnBufferGetMemoryOffset) { ret_x(c, (uint64_t)((const buf_t *)arg_p(c, 0))->off); }
// size_t nvnBufferGetSize(const NVNbuffer *b)
NVN_API(nvnBufferGetSize) { ret_x(c, ((const buf_t *)arg_p(c, 0))->size); }
// void nvnBufferFlushMappedRange(const NVNbuffer *b, ptrdiff_t offset, size_t size): the back end re-reads the range
NVN_API(nvnBufferFlushMappedRange) {
const buf_t *o = arg_p(c, 0);
int64_t off = (int64_t)arg_x(c, 1);
uint64_t size = arg_x(c, 2);
if (off < 0 || (uint64_t)off > o->size || size > o->size - (uint64_t)off)
rt_fatal("nvn: BufferFlushMappedRange %#llx+%#llx outside the buffer (%#llx bytes)", (unsigned long long)off,
(unsigned long long)size, (unsigned long long)o->size);
if (rt_trace & RT_TRACE_PERF) flush_note(1, o->pool, size);
uint64_t a = buf_address(o);
if (a && !nvn_gpu_only(a)) nvn_guest_written(a + (uint64_t)off, size);
}
// ---------- Sync and events
// A sync is a fence value: fenced = 1 after QueueFenceSync (the newest submit), 3 after WindowAcquireTexture (fence 0).
// NVNboolean nvnSyncInitialize(NVNsync *s, NVNdevice *d)
NVN_API(nvnSyncInitialize) {
*(sync_t *)arg_p(c, 0) = (sync_t){SYNC_MAGIC, 0, 0};
ret_x(c, 1);
}
// void nvnSyncFinalize(NVNsync *s)
NVN_API(nvnSyncFinalize) { ((sync_t *)arg_p(c, 0))->magic = 0; }
// void nvnQueueFenceSync(NVNqueue *q, NVNsync *s, NVNsyncCondition condition, int flags): "every command submitted to
// the queue so far has completed"
NVN_API(nvnQueueFenceSync) {
sync_t *s = arg_p(c, 1);
s->fenced = 1;
s->fence = nvn_fence_last();
}
// TOTK_SYNC_AHEAD=1: one more frame in flight. Each frame the game waits (one nvnSyncWait, frame_wait_lr) for
// the frame it presented last; on the Switch that work is long done by then, here it is still in the replay and on the
// GPU (about 11 ms from the submit), and the wait sets the frame time. With the switch on, that wait, and only that
// one, waits for the frame before it instead. What the game reads back of GPU results after the wait can then be a
// frame old. The game's CPU writes to memory the GPU still reads reach the GPU only through shadow copies, which are
// queued for the GPU or wait (nvn_sync_ahead_guard). The wait still lasts until the replay thread submitted the asked
// value, so no copy of that value itself picks up those writes. pa_fence: the newest fence at the latest present and
// the one before, present thread only for writes. pa_guard: the newest value the game was told is done early.
static uint64_t pa_fence[2], pa_guard;
// The return address of that wait. 1.2.1: 0x7000b23dfc. 1.4.3: 0x70002db99c, the second of the two nvnSyncWait calls
// in f_70002db8ac; like on 1.2.1 its sync comes from a two-entry ring indexed per frame, and the first call waits on
// one object's sync behind a mode check. 0: no site known, the switch stays off.
static uint64_t frame_wait_lr(void) {
return rt_game_version == 121 ? 0x7000b23dfcull : rt_game_version == 143 ? 0x70002db99cull : 0;
}
static int sync_ahead(void) {
static int on = -1;
if (on < 0) {
on = cfg_bool("TOTK_SYNC_AHEAD");
// Without shadow storage the GPU reads the game's own memory, which the game writes ahead: nothing guards
// that. The Vulkan back end sets the mode while it starts, before the game's first present or wait; the null
// back end never does.
if (on && !vk_mem_shadow_on()) {
rt_log("nvn: TOTK_SYNC_AHEAD needs shadow storage (TOTK_SHADOW=1 on native Windows); "
"the game waits for the GPU as before");
on = 0;
}
if (on && !frame_wait_lr()) {
rt_log("nvn: TOTK_SYNC_AHEAD: no frame wait site is known for this game version; the game waits for the "
"GPU as before");
on = 0;
}
if (on) rt_log("nvn: TOTK_SYNC_AHEAD: a wait for the frame presented last waits for the frame before it");
}
return on;
}
void nvn_sync_ahead_present(void) {
if (!sync_ahead()) return;
__atomic_store_n(&pa_fence[1], __atomic_load_n(&pa_fence[0], __ATOMIC_RELAXED), __ATOMIC_RELEASE);
__atomic_store_n(&pa_fence[0], nvn_fence_last(), __ATOMIC_RELEASE);
}
uint64_t nvn_sync_ahead_guard(void) { return __atomic_load_n(&pa_guard, __ATOMIC_ACQUIRE); }
// NVNsyncWaitResult nvnSyncWait(const NVNsync *s, uint64_t timeout_ns): a sync never fenced reports a timeout at once;
// a fenced one is released only after the back end completed that fence value
NVN_API(nvnSyncWait) {
const sync_t *s = arg_p(c, 0);
uint64_t timeout = arg_x(c, 1);
if (!s->fenced) { ret_x(c, NVN_SYNC_WAIT_RESULT_TIMEOUT_EXPIRED); return; }
const GfxBackend *b = gfx_backend();
uint64_t want = s->fence;
// Only the game's per-frame wait lags, and not a poll (timeout 0).
if (timeout && c->x[30] == frame_wait_lr() && sync_ahead() && b->fence_done() < want) {
uint64_t p0 = __atomic_load_n(&pa_fence[0], __ATOMIC_ACQUIRE);
uint64_t p1 = __atomic_load_n(&pa_fence[1], __ATOMIC_ACQUIRE);
if (want <= p0 && want > p1) want = p1;
}
if (want != s->fence) { // the replay of the asked value is done first: the guard then covers every copy of it
{
NVN_BLOCKING();
gfx_replay_wait_value(s->fence);
}
uint64_t g = __atomic_load_n(&pa_guard, __ATOMIC_RELAXED);
while (s->fence > g &&
!__atomic_compare_exchange_n(&pa_guard, &g, s->fence, 1, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {}
vk_mem_shadow_fence(); // a shadow sync that read the old guard finishes before the game writes again
}
if (b->fence_done() >= want) {
b->cpu_synced(want == s->fence ? s->fence : b->fence_done());
ret_x(c, NVN_SYNC_WAIT_RESULT_ALREADY_SIGNALED);
return;
}
int ok;
{
NVN_BLOCKING();
ok = b->fence_wait(want, timeout);
}
if (ok) b->cpu_synced(want == s->fence ? s->fence : b->fence_done());
ret_x(c, ok ? NVN_SYNC_WAIT_RESULT_CONDITION_SATISFIED : NVN_SYNC_WAIT_RESULT_TIMEOUT_EXPIRED);
}
// void nvnEventBuilderSetDefaults(NVNeventBuilder *b)
NVN_API(nvnEventBuilderSetDefaults) { memset(arg_p(c, 0), 0, sizeof(ev_builder_t)); }
// void nvnEventBuilderSetStorage(NVNeventBuilder *b, const NVNmemoryPool *p, int64_t offset)
NVN_API(nvnEventBuilderSetStorage) { *(ev_builder_t *)arg_p(c, 0) = (ev_builder_t){arg_p(c, 1), (int64_t)arg_x(c, 2)}; }
// NVNboolean nvnEventInitialize(NVNevent *e, const NVNeventBuilder *b): the value lives in pool storage
NVN_API(nvnEventInitialize) {
const ev_builder_t *s = arg_p(c, 1);
if (!s->pool || !s->pool->mem || nvn_gpu_only((uintptr_t)s->pool->mem)) {
rt_log("nvn: EventInitialize without CPU-visible storage (pool %p)", (const void *)s->pool);
ret_x(c, 0);
return;
}
*(ev_t *)arg_p(c, 0) = (ev_t){EVENT_MAGIC, (volatile uint32_t *)(s->pool->mem + s->off)};
ret_x(c, 1);
}
// void nvnEventFinalize(NVNevent *e)
NVN_API(nvnEventFinalize) { ((ev_t *)arg_p(c, 0))->magic = 0; }
// uint32_t nvnEventGetValue(const NVNevent *e)
NVN_API(nvnEventGetValue) { ret_x(c, *((const ev_t *)arg_p(c, 0))->val); }
// void nvnEventSignal(NVNevent *e, NVNeventSignalMode mode, uint32_t value)
NVN_API(nvnEventSignal) {
const ev_t *e = arg_p(c, 0);
uint32_t v = (uint32_t)arg_x(c, 2);
if ((uint32_t)arg_x(c, 1) == NVN_EVENT_SIGNAL_MODE_ADD) *e->val += v;
else *e->val = v;
}
// ---------- Queue
// void nvnQueueBuilderSetDefaults(NVNqueueBuilder *b): keeps the device; the device's default memory sizes
NVN_API(nvnQueueBuilderSetDefaults) {
queue_builder_t *b = arg_p(c, 0);
NVNdevice *d = b->dev;
memset(b, 0, sizeof *b);
b->dev = d;
b->cmd_sz = 65536;
b->comp_sz = 262144;
b->ctl_sz = 16384;
b->flush = 4096;
}
// void nvnQueueBuilderSetDevice(NVNqueueBuilder *b, NVNdevice *d)
NVN_API(nvnQueueBuilderSetDevice) { ((queue_builder_t *)arg_p(c, 0))->dev = arg_p(c, 1); }
// void nvnQueueBuilderSetFlags(NVNqueueBuilder *b, int flags)
NVN_API(nvnQueueBuilderSetFlags) { ((queue_builder_t *)arg_p(c, 0))->flags = (uint32_t)arg_x(c, 1); }
// void nvnQueueBuilderSet{Command,Compute,Control}MemorySize / SetCommandFlushThreshold(NVNqueueBuilder *b, size_t n)
NVN_API(nvnQueueBuilderSetCommandMemorySize) { ((queue_builder_t *)arg_p(c, 0))->cmd_sz = arg_x(c, 1); }
NVN_API(nvnQueueBuilderSetComputeMemorySize) { ((queue_builder_t *)arg_p(c, 0))->comp_sz = arg_x(c, 1); }
NVN_API(nvnQueueBuilderSetControlMemorySize) { ((queue_builder_t *)arg_p(c, 0))->ctl_sz = arg_x(c, 1); }
NVN_API(nvnQueueBuilderSetCommandFlushThreshold) { ((queue_builder_t *)arg_p(c, 0))->flush = arg_x(c, 1); }
// size_t nvnQueueBuilderGetQueueMemorySize(const NVNqueueBuilder *b): command + compute + control rounded to 4 KiB (the
// host does not use it; the game only allocates it)
NVN_API(nvnQueueBuilderGetQueueMemorySize) {
const queue_builder_t *b = arg_p(c, 0);
ret_x(c, (b->cmd_sz + b->comp_sz + b->ctl_sz + 4095) & ~(uint64_t)4095);
}
// void nvnQueueBuilderSetQueueMemory(NVNqueueBuilder *b, void *memory, size_t size)
NVN_API(nvnQueueBuilderSetQueueMemory) {
queue_builder_t *b = arg_p(c, 0);
b->mem = arg_p(c, 1);
b->mem_sz = arg_x(c, 2);
}
// NVNboolean nvnQueueInitialize(NVNqueue *q, const NVNqueueBuilder *b)
NVN_API(nvnQueueInitialize) {
queue_t *q = arg_p(c, 0);
*q = (queue_t){QUEUE_MAGIC, ((const queue_builder_t *)arg_p(c, 1))->dev, 0};
NVN_LOG("QueueInitialize %p", (void *)q);
ret_x(c, 1);
}
// void nvnQueueFinalize(NVNqueue *q)
NVN_API(nvnQueueFinalize) { ((queue_t *)arg_p(c, 0))->magic = 0; }
// NVNqueueGetErrorResult nvnQueueGetError(NVNqueue *q, NVNqueueErrorInfo *info)
NVN_API(nvnQueueGetError) {
NVNqueueErrorInfo *info = arg_p(c, 1);
if (info) memset(info, 0, sizeof *info);
ret_x(c, NVN_QUEUE_GET_ERROR_RESULT_GPU_NO_ERROR);
}
// void nvnQueueFlush(NVNqueue *q): submits are already handed to the back end
NVN_API(nvnQueueFlush) { (void)c; }
// void nvnQueueFinish(NVNqueue *q)
NVN_API(nvnQueueFinish) {
{
NVN_BLOCKING();
gfx_backend()->finish();
}
gfx_backend()->cpu_synced(gfx_backend()->fence_done());
}
// ---------- Submit latency guard for TotK 1.2.1's per-object uniform double buffer
// NVN starts the GPU when QueueSubmitCommands returns, within microseconds; here the replay into Vulkan takes 15-30 ms
// first, and the GPU reads uniform memory only after it. The game's producer f_70008c6460 (thread 0x103e, p56)
// rewrites the half of its per-object uniform blocks that was front until the last flip, 14-23 ms after the submit
// that still reads it, with no fence: on hardware the GPU has read that half by then. Here the rewrite lands inside
// the replay, and that frame draws other objects' matrices (phantom rocks, flat far field).
// The guard makes the producer wait, at its commit point, until the newest submit has been replayed and completed.
// lod_entered / lod_issued count submits that entered QueueSubmitCommands / reached the back end; lod_fence holds
// the fence of the last LOD_RING. TOTK_LOD_GUARD=0 turns the wait off.
#define LOD_RING 64
static uint64_t lod_entered, lod_issued, lod_fence[LOD_RING];
// void nvnQueueSubmitCommands(NVNqueue *q, int n, const NVNcommandHandle *handles): replayed on this thread with its
// core given up (section 2.2), or queued for the replay thread (TOTK_REPLAY_ASYNC, vk_backend.c); the newest fence
// value is what QueueFenceSync records. With the queue the fence covers replay and GPU, so the LOD guard below waits
// for both.
NVN_API(nvnQueueSubmitCommands) {
queue_t *q = arg_p(c, 0);
int n = (int)arg_x(c, 1);
const uint64_t *h = arg_p(c, 2);
if (n < 0 || (n && !h)) rt_fatal("nvn: QueueSubmitCommands with %d handles at %p", n, (const void *)h);
NVN_BLOCKING();
uint64_t no = __atomic_add_fetch(&lod_entered, 1, __ATOMIC_SEQ_CST);
q->submits++;
uint64_t f;
if (gfx_replay_async()) {
// Prepare and queue under one lock: submit numbers follow queue order (gfx_replayed_submits).
static PlatMutex submit_lock = PLAT_MUTEX_INIT;
uint64_t local[32], *hs = n <= 32 ? local : malloc(sizeof *hs * (size_t)n);
if (!hs) rt_fatal("nvn: out of memory (submit of %d handles)", n);
plat_mutex_lock(&submit_lock);
nvn_submit_prepare_async(h, hs, n);
f = gfx_backend()->submit(hs, n);
plat_mutex_unlock(&submit_lock);
if (hs != local) free(hs);
} else {
nvn_submit_prepare(h, n);
f = gfx_backend()->submit(h, n);
}
uint64_t prev = __atomic_load_n(&last_fence, __ATOMIC_RELAXED);
while (f > prev && !__atomic_compare_exchange_n(&last_fence, &prev, f, 1, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {}
__atomic_store_n(&lod_fence[no % LOD_RING], f, __ATOMIC_RELAXED);
__atomic_store_n(&lod_issued, no, __ATOMIC_SEQ_CST);
}
// The producer (1.2.1 main f_70008c6460) releases the read lock on obj+0x538 at lr 0x70008c70c8; when
// [obj+0x529] == [obj+0x528] (x19 = obj) it goes on to rewrite the back half. Waits off the core, at most ~400 ms.
static void obs_release_read(Cpu *c) {
uint64_t lr = c->x[30], obj = c->x[19];
rt_original(c);
if (lr != 0x70008c70c8ull) return;
static int on = -1;
if (on < 0) {
on = cfg_bool("TOTK_LOD_GUARD");
}
if (!on || *(volatile uint8_t *)(uintptr_t)(obj + 0x528) != *(volatile uint8_t *)(uintptr_t)(obj + 0x529)) return;
uint64_t e = __atomic_load_n(&lod_entered, __ATOMIC_SEQ_CST);
if (!e) return;
NVN_BLOCKING();
for (int i = 0; i < 200 && __atomic_load_n(&lod_issued, __ATOMIC_SEQ_CST) < e; i++) plat_sleep_ms(1);
uint64_t f = __atomic_load_n(&lod_fence[e % LOD_RING], __ATOMIC_RELAXED);
if (gfx_backend()->fence_done() < f) gfx_backend()->fence_wait(f, 200000000ull);
}
HOST_OBSERVER("_ZN2nn2os15ReleaseReadLockEPNS0_20ReaderWriterLockTypeE", obs_release_read);