mirror of
https://github.com/valentderah/totk-pc-recomp.git
synced 2026-11-12 18:31:05 +00:00
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>
118 lines
10 KiB
C
118 lines
10 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
|
|
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);
|