mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-11 01:41:02 +00:00
Each frame the game waits in one nvnSyncWait (1.2.1 lr 0x7000b23dfc) 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, and on Great Sky Island the wait is about 4.6 ms of a 13 ms frame. With TOTK_SYNC_AHEAD=1 that one wait waits for the frame before it instead, once the replay thread submitted the asked value, so the game builds the next frame while the GPU runs the last one. The game then writes guest memory that GPU work still in flight reads. Shadow storage makes that safe: a shadow update of a pool with a reader in flight is queued and copied by the GPU in the next part's prologue, in queue order, instead of memcpy'd under that reader (TOTK_SHADOW_DEFER=1, the default with the switch; 0 makes the replay thread wait for the reader instead). The guard is read under the shadow lock and the wait takes that lock once after raising it, so no sync that read the old guard is still copying when the game writes again. The switch needs shadow storage (Linux and Wine by default, TOTK_SHADOW=1 on native Windows) and the 1.2.1 call site; otherwise it stays off with a log line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
11 KiB
C
127 lines
11 KiB
C
// Internal header of the NVN front end, shared by nvn_api_core.c and the other
|
|
// slices (textures, samplers, programs, windows, state objects, command buffers). Adapted from MacNativeGames games/totk/third_party/d2r_gfx/gfx/nvn/hle_nvn.c and nvn_impl.h @ b75be8cf
|
|
// (MIT, Copyright (c) 2026 jhaizhou; origin d2r-mobile).
|
|
#pragma once
|
|
#include <stdint.h>
|
|
|
|
#include "gfx.h"
|
|
#include "host.h"
|
|
#include "nvn_obj.h"
|
|
|
|
// NVN_API(nvnX) { body }: the strong nvnp_nvnX(Cpu *c); counts the call in gfx_counters, TOTK_TRACE=gfx logs the first.
|
|
#define NVN_API(n) \
|
|
static void nvn_impl_##n(Cpu *c); \
|
|
void nvnp_##n(Cpu *c) { \
|
|
static uint8_t seen; \
|
|
nvn_called(&seen, #n); \
|
|
nvn_impl_##n(c); \
|
|
} \
|
|
static void nvn_impl_##n(Cpu *c)
|
|
void nvn_called(uint8_t *seen, const char *name); // the counting prologue of NVN_API
|
|
#define NVN_LOG(...) do { if (rt_trace & RT_TRACE_GFX) rt_log("nvn: " __VA_ARGS__); } while (0) // TOTK_TRACE=gfx
|
|
// A wait for the GPU, the display or another thread gives the virtual core up until the end of the enclosing scope.
|
|
#define NVN_BLOCKING() __attribute__((cleanup(kern_core_back))) int nvn_away_ = kern_core_away()
|
|
static inline int nvn_arg_bool(const Cpu *c, int i) { return (int)(arg_x(c, i) & 0xff); } // NVNboolean: low 8 bits only
|
|
|
|
#define NVN_POOL_MAGIC MAGIC('P', 'O', 'O', 'L') // pool_t.magic of an initialized pool
|
|
#define NVN_VPOOL_PAGE 0x10000ull // NVN virtual mapping granularity (64 KiB)
|
|
static inline int nvn_gpu_only(uint64_t a) { return a - RT_GPU_VA_BASE < RT_GPU_VA_SIZE; } // address in the GPU window
|
|
// A CPU-invisible physical or virtual pool lives in the GPU window: mem is its GPU address (never mapped on the host),
|
|
// game_mem the game's storage (never touched), span the reserved window bytes, host NULL. Every other pool: mem is the
|
|
// game's storage, the GPU address equals the guest address.
|
|
|
|
typedef struct { NVNdevice *dev; pool_t *pool; int64_t off; uint64_t size; } buf_builder_t; FITS(buf_builder_t, NVNbufferBuilder);
|
|
typedef struct { uint32_t magic; pool_t *pool; int64_t off; uint64_t size; } buf_t; FITS(buf_t, NVNbuffer);
|
|
typedef struct { // NVNmappingRequest as main passes it (0x28 bytes)
|
|
const NVNmemoryPool *physical_pool; // NULL unmaps
|
|
int64_t physical_offset, virtual_offset;
|
|
uint64_t size;
|
|
int32_t storage_class;
|
|
uint32_t padding;
|
|
} nvn_mapping_t;
|
|
_Static_assert(sizeof(nvn_mapping_t) == 0x28, "NVNmappingRequest stride");
|
|
|
|
const pool_t *nvn_pool_find(uint64_t addr, uint64_t len); // GPU-visible pool holding [addr, addr+len], NULL if none
|
|
uint64_t nvn_new_serial(void); // next tex_t / prog_t serial, from 1
|
|
// TOTK_SYNC_AHEAD (nvn_api_core.c): nvnQueuePresentTexture notes the newest fence at each present; the newest
|
|
// previous-frame value the game was told is done although it may not be (0: none). The back end's shadow copies
|
|
// wait for it or are queued for the GPU (vk_sync.c prologue), so the GPU never reads CPU writes the game made under
|
|
// that assumption too early.
|
|
void nvn_sync_ahead_present(void);
|
|
uint64_t nvn_sync_ahead_guard(void);
|
|
int vk_mem_shadow_on(void); // vk_memory.c: 1 with shadow storage on (valid once the Vulkan back end is up)
|
|
void vk_mem_shadow_fence(void); // vk_memory.c: returns once every shadow sync that read the old guard is done
|
|
void gfx_replay_wait_value(uint64_t v); // vk_backend.c: the replay thread submitted value v
|
|
uint64_t nvn_fence_last(void); // fence value of the newest submit (0 before the first)
|
|
int nvn_depth_minus_w(void); // 1 after DeviceSetDepthMode(NEAR_IS_MINUS_W), else 0
|
|
void nvn_guest_written(uint64_t addr, uint64_t size); // the host or the game wrote pool storage: back end re-reads
|
|
// TOTK_TRACE=perf: MemoryPoolFlushMappedRange [0] and BufferFlushMappedRange [1] calls [0] and bytes [1] by the pool's
|
|
// CPU class (0 CPU_CACHED, 1 CPU_UNCACHED, 2 other) since the previous call.
|
|
void nvn_flush_stats(uint64_t out[2][3][2]);
|
|
void nvn_submit_prepare(const uint64_t *handles, int n); // weak no-op; the command-buffer slice reads deferred binds
|
|
// Asynchronous replay: as nvn_submit_prepare, writing the handles to replay to out (a clone where a still queued
|
|
// submission uses the same recording with other deferred binds)
|
|
void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n);
|
|
int gfx_replay_async(void); // 1: the back end replays submissions on its own thread
|
|
uint64_t gfx_replayed_submits(void); // newest submit number (nvn_submit_count) replayed
|
|
#define NVN_HANDLE_TEX(h) ((int)((h) & 0xfffff)) // combined texture handle = texture id | sampler id << 20
|
|
#define NVN_HANDLE_SAMP(h) ((int)(((h) >> 20) & 0xfff))
|
|
extern GfxStats gfx_counters; // behind gfx_stats(); add with __atomic_fetch_add. frames:
|
|
// the back end's present; nvn_calls/nvn_distinct: NVN_API
|
|
|
|
// ---- nvn_api_obj.c: textures, samplers, programs, windows
|
|
#define NVN_TEX_MAGIC MAGIC('T', 'E', 'X', 0) // tex_t.magic of an initialized texture
|
|
#define NVN_SAMP_MAGIC MAGIC('S', 'M', 'P', 0) // samp_t.magic
|
|
#define NVN_PROG_MAGIC MAGIC('P', 'R', 'O', 'G') // prog_t.magic
|
|
#define NVN_WIN_MAGIC MAGIC('W', 'I', 'N', 'D') // win_t.magic
|
|
// The live texture whose storage starts at mem, copied into *out; 0 if none. The game re-initializes transient textures
|
|
// at the same pool memory without registering them again, so a registered serial that was finalized since resolves here.
|
|
int nvn_tex_live_at(const void *mem, tex_t *out);
|
|
// Byte offset of a view's first level and layer inside the texture storage (nvnTextureGetViewOffset); v may be NULL.
|
|
uint64_t nvn_tex_view_offset(const tex_t *t, const view_t *v);
|
|
|
|
// ---- nvn_api_cmd.c, nvn_ext.c: command buffers and their replay contract
|
|
// A command handle (what QueueSubmitCommands receives) points at a host copy of one recording: a flat sequence of
|
|
// 8-byte-aligned records {op_hdr, payload}, OP_BEGIN first and OP_END last. Called recordings are inlined, so a copy
|
|
// holds no OP_CALL or OP_JUMP. Texture pointers in payloads (p_set_rt, p_copy_b2t, p_copy_t2t, p_clear_tex) are
|
|
// immutable host copies ("pins"), one per tex_t.serial, that live while a recording copy names them and a while after
|
|
// (nvn_pins_sweep). Deferred binds (p_bind_handles.src != 0) hold the
|
|
// handles read at submit (nvn_submit_prepare). Payloads of the ops nvn_obj.h does not define:
|
|
typedef struct { volatile uint32_t *val; uint32_t mode, value; } p_signal_event; // OP_SIGNAL_EVENT; mode: NVNeventSignalMode
|
|
typedef struct { uint64_t type, addr; } p_report_counter; // OP_REPORT_COUNTER: NVNcounterType, NVNcounterData address
|
|
typedef struct { uint64_t type; } p_reset_counter; // OP_RESET_COUNTER
|
|
typedef struct { uint64_t src, dst, size; } p_copy_b2b; // OP_COPY_B2B: GPU addresses
|
|
typedef struct { uint64_t dst, size; uint8_t data[]; } p_update_ub; // OP_UPDATE_UB: size bytes written at dst
|
|
typedef struct { int32_t bits, pad; } p_barrier; // OP_BARRIER: NVNbarrierBits
|
|
typedef struct { uint64_t target; } p_jump; // OP_JUMP / OP_CALL: next record / called recording
|
|
// OP_DRAW_IDX_IND: DrawElementsIndirect; ind = GPU address of {count, instances, first index, base vertex, base
|
|
// instance} (VkDrawIndexedIndirectCommand's layout), read by the GPU when the draw runs
|
|
typedef struct { int32_t prim, itype; uint64_t ibuf, ind; } p_draw_idx_ind;
|
|
typedef struct { uint64_t ind, caller; } p_dispatch_ind; // OP_DISPATCH_IND: ind = GPU address of the 3 group counts
|
|
// OP_CLEAR_BUF: size bytes at addr filled with the 32-bit value in GPU order (ClearBuffer; ReportValue is a 4-byte one)
|
|
typedef struct { uint64_t addr, size; uint32_t value, pad; } p_clear_buf;
|
|
_Static_assert(sizeof(p_signal_event) == 16 && sizeof(p_report_counter) == 16 && sizeof(p_copy_b2b) == 24 &&
|
|
sizeof(p_update_ub) == 16, "op payload layouts of hle_nvn.c");
|
|
static inline const op_hdr *nvn_op_first(uint64_t handle) { return (const op_hdr *)(uintptr_t)handle; } // OP_BEGIN
|
|
static inline const op_hdr *nvn_op_next(const op_hdr *h) { return (const op_hdr *)(const void *)((const uint8_t *)h + h->size); }
|
|
static inline const void *nvn_op_payload(const op_hdr *h) { return h + 1; }
|
|
int nvn_handle_live(uint64_t handle); // 1 while the handle's copy exists (its control memory was
|
|
// not added again)
|
|
uint64_t nvn_handle_bytes(uint64_t handle); // bytes of the copy, OP_BEGIN to OP_END; 0 if not live
|
|
uint64_t nvn_submit_count(void); // QueueSubmitCommands calls so far
|
|
// Submits a texture pin outlives its last tex_pin and the release of the last recording copy naming it: about 20 s at
|
|
// one submit a frame (MacNativeGames frees after two 600-submit windows).
|
|
#define NVN_PIN_AGE 1200
|
|
// Frees the texture pins no recording copy has named for NVN_PIN_AGE submits (nvn_api_cmd.c, "texture pins"), except keep[0..nkeep):
|
|
// the replay's current render targets, which carry over to the next submission. Called between submissions by the
|
|
// thread that replays them; now = nvn_submit_count().
|
|
void nvn_pins_sweep(uint64_t now, const tex_t *const *keep, int nkeep);
|
|
void nvn_pins_stats(uint64_t *live, uint64_t *freed); // pins that exist, pins freed so far
|
|
const char *nvn_op_name(uint32_t op); // "DRAW_IDX", ...; "?" past OP_COUNT_
|
|
// MemoryPoolInvalidateMappedRange: the CPU is about to read pool storage the GPU wrote. Weak no-op in nvn_ext.c; a back
|
|
// end that renders into host-side copies defines it and writes them back.
|
|
void nvn_pool_invalidate(const pool_t *p, uint64_t off, uint64_t size);
|
|
// MemoryPoolFlushMappedRange: the CPU wrote [mem + off, mem + off + size). Weak no-op in nvn_ext.c; a back end that keeps
|
|
// device-side copies of pool memory (vk_memory.c's VRAM mirror) defines it and copies the range again.
|
|
void nvn_pool_flushed(const pool_t *p, uint64_t off, uint64_t size);
|