Files
totk-pc-recomp/runtime/nvn_api_cmd.c
T
Valiantsin DzerakhandClaude Opus 5.5 ce150130df Make code comments say what they mean instead of pointing at things a reader cannot follow
Comments cited codes from local notes (port-lessons P1, backlog), task labels (T6c1-T6c3, M4), bare upstream commit
hashes and issue numbers, a file that no longer exists (nvn_api.c, now nvn_api_*.c), a generated file (main_0103.c)
and line numbers into upstream files that refs/ has moved past, so they pointed at unrelated code. Those now name the
file, function or project and say what the cited code does. Attribution of copied code keeps its pinned commits.

Corrected facts: TOTK_REPLAY_ASYNC defaults to 0, and the shader cache, pipeline cache and pipeline keys live next to
totk.exe, not in build/v121.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:58:01 +03:00

961 lines
51 KiB
C

// NVN front end, command-buffer slice: command buffers, their memory, recording
// as op records (nvn_obj.h), recording copies behind command handles, and the submit-time resolution of deferred binds
// (nvn_submit_prepare, called by nvnQueueSubmitCommands in nvn_api_core.c). Ported from MacNativeGames
// games/totk/third_party/d2r_gfx/gfx/nvn/hle_nvn.c @ b75be8cf (MIT, Copyright (c) 2026 jhaizhou; origin d2r-mobile),
// lines 647-1062, 1261-1286, 1486-1760 and 1975-1990, and the *Deferred and memory-callback entry points of
// totk_nvn_ext.c in the same directory. Metal, mach, the translation thread, Apple diagnostics, the per-frame
// statistics and the long-session sweeps (copy marks in control memory, texture pin sweep) dropped.
//
// Records are written into a growable host buffer per command buffer, never into the game's command memory: games hand
// the same command memory to several command buffers recording at once (budgeted for real NVN encodings), and our
// records are larger. The game's command and control memory is accounted only. EndRecording copies the finished
// recording into host memory; the handle is that copy (a flat OP_BEGIN ... OP_END sequence) and lives until the game
// adds the same control memory again. CallCommands and CopyCommands inline the called recording, so copies hold no
// OP_CALL or OP_JUMP.
#include <windows.h>
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "gfx.h"
#include "host.h"
#include "nvn_api.h"
#include "nvn_hmap.h"
#define CMD_MAGIC MAGIC('C', 'M', 'D', 'B')
#define COPY_MAGIC 0x59504f43444d43ull // "CMDCOPY": prefix of a live recording copy
#define OP_RESERVE 24 // room always kept at the end of the recording buffer
#define MIN_COMMAND_SIZE 512 // NVN_DEVICE_INFO_COMMAND_BUFFER_MIN_COMMAND_SIZE (nvn_api_core.c devinfo)
#define MIN_CONTROL_SIZE 256 // NVN_DEVICE_INFO_COMMAND_BUFFER_MIN_CONTROL_SIZE
#define CTL_PER_RECORDING 64 // control memory one recording is reported to use
#define COPY_PAGE_SHIFT 16 // recording copy index granularity (release_copies)
static cmd_t *cmd_of(const Cpu *c) { return arg_p(c, 0); }
// ---------- memory callbacks
// NVN calls the game's memory callback when a recording needs command or control memory and none is left. Records
// live in host memory here, so the callback is called only where the hardware certainly would: a recording starts
// without any command memory, or ends without any control memory. It runs on the calling guest thread through
// rt_call; whatever memory it adds is accounted. Set{Command,Control}MemoryCallbackEnabled(false) suppress it.
enum { CB_NO_COMMAND = 1, CB_NO_CONTROL = 2 };
static SRWLOCK cb_lock = SRWLOCK_INIT;
static nvn_hmap cb_off = {.klen = 8}; // command buffer -> disabled callback kinds | 0x100
static unsigned cb_disabled(const cmd_t *o) {
AcquireSRWLockShared(&cb_lock);
uintptr_t v = (uintptr_t)nvn_hm_get(&cb_off, &o);
ReleaseSRWLockShared(&cb_lock);
return (unsigned)v & 0xff;
}
static void cb_set_disabled(const cmd_t *o, unsigned kind, int disabled) {
AcquireSRWLockExclusive(&cb_lock);
unsigned v = (unsigned)(uintptr_t)nvn_hm_get(&cb_off, &o) & 0xff;
v = disabled ? v | kind : v & ~kind;
if (v) nvn_hm_put(&cb_off, &o, (void *)(uintptr_t)(v | 0x100));
else nvn_hm_del(&cb_off, &o);
ReleaseSRWLockExclusive(&cb_lock);
}
static void memory_event(Cpu *c, cmd_t *o, int event, uint64_t min) {
if (!o->cb || (cb_disabled(o) & (event == NVN_COMMAND_BUFFER_MEMORY_EVENT_OUT_OF_COMMAND_MEMORY ? CB_NO_COMMAND
: CB_NO_CONTROL)))
return;
NVN_LOG("command buffer %p: memory callback %p (%s memory, %llu bytes)", (void *)o, (void *)o->cb,
event == NVN_COMMAND_BUFFER_MEMORY_EVENT_OUT_OF_COMMAND_MEMORY ? "command" : "control",
(unsigned long long)min);
rt_call(c, (uint64_t)(uintptr_t)o->cb, 4, (uint64_t)(uintptr_t)o, (uint64_t)event, min,
(uint64_t)(uintptr_t)o->cb_data);
}
// ---------- recording buffer
static void put_hdr(uint8_t *p, uint32_t op, uint32_t size) {
op_hdr h = {op, size};
memcpy(p, &h, sizeof h);
}
static void cmd_reserve(cmd_t *o, size_t need) {
if (o->seg && o->cur + need + OP_RESERVE <= o->end) return;
size_t used = o->seg ? (size_t)(o->cur - o->seg) : 0;
size_t cap = o->seg ? (size_t)(o->end - o->seg) : 0;
if (!cap) cap = 64 << 10;
while (used + need + OP_RESERVE > cap) cap *= 2;
uint8_t *b = realloc(o->seg, cap);
if (!b) rt_fatal("nvn: command recording buffer of %zu bytes", cap);
o->seg = b;
o->cur = b + used;
o->end = b + cap;
}
// Space for one record (header included); the header's op is filled by the caller.
static uint8_t *cmd_alloc(cmd_t *o, uint32_t size) {
size = (size + 7) & ~7u;
cmd_reserve(o, size);
uint8_t *p = o->cur;
o->cur += size;
o->cmd_used += size;
o->hw_used += size;
o->last_rec = size;
put_hdr(p, 0, size);
return p;
}
// The game sums nvnCommandBufferGetCommandMemoryUsed over recordings to size resource heaps, so usage must be what the
// real NVN push-buffer encoding takes, not our record size. Known encodings (Maxwell methods, from a real recording):
// DrawElements 12 words, DrawElementsBaseVertex 11, BindVertexAttribState 1+16, BindVertexStreamState 16+n,
// BindVertexBuffer 6 words each.
static void hw_fix(cmd_t *o, size_t bytes) {
o->hw_used += bytes;
o->hw_used -= o->last_rec;
o->last_rec = bytes;
}
// A zeroed payload of one record.
static void *rec(cmd_t *o, uint32_t op, size_t payload) {
uint32_t sz = (uint32_t)((8 + payload + 7) & ~(size_t)7);
uint8_t *p = cmd_alloc(o, sz);
put_hdr(p, op, sz);
memset(p + 8, 0, sz - 8);
return p + 8;
}
#define REC(o, OP, T) ((T *)rec((o), (OP), sizeof(T)))
// Array-carrying binds take the prefix fields plus n elements; the replay reads only the first n.
#define RECN(o, OP, T, ARR, N) ((T *)rec((o), (OP), offsetof(T, ARR) + sizeof(((T *)0)->ARR[0]) * (size_t)(N)))
static void copy_view(view_t *dst, uint8_t *has, const NVNtextureView *v) {
*has = v != NULL;
if (v) *dst = *(const view_t *)v;
}
// ---------- texture pins
// Recordings refer to textures by pointer, but the game may finalize or reuse an NVNtexture after recording and before
// the submit (NVN itself captured only GPU state). Records therefore point at an immutable host copy per texture
// initialization (tex_t.serial). The copies are kept for the whole run (MacNativeGames sweeps them for long sessions).
static SRWLOCK pin_lock = SRWLOCK_INIT;
static nvn_hmap pins = {.klen = 8}; // serial -> tex_t copy
static const tex_t *tex_pin(const NVNtexture *nt) {
const tex_t *t = (const tex_t *)nt;
if (!t) return NULL;
uint64_t serial = t->serial;
if (!serial) rt_fatal("nvn: a recording references the uninitialized texture object %p", (const void *)t);
AcquireSRWLockShared(&pin_lock);
const tex_t *r = nvn_hm_get(&pins, &serial);
ReleaseSRWLockShared(&pin_lock);
if (r) return r;
AcquireSRWLockExclusive(&pin_lock);
r = nvn_hm_get(&pins, &serial);
if (!r) {
tex_t *copy = malloc(sizeof *copy);
if (!copy) rt_fatal("nvn: texture pin copy");
*copy = *t;
nvn_hm_put(&pins, &serial, copy);
r = copy;
}
ReleaseSRWLockExclusive(&pin_lock);
return r;
}
// ---------- recording copies
// A handle points just past a copy_prefix. Copies are indexed by the 64 KiB block of their control-memory position, so
// adding control memory again releases the copies recorded there without walking every live copy. 4 KiB pages cost
// about 500 lookups per AddControlMemory under the exclusive lock (1.4.3 field: 53 calls a frame, 2 MiB ranges, 3.7
// copies per page), and the recording threads waited on it.
typedef struct { uint64_t magic, bytes, made_at, used_at, deferred; } copy_prefix; // deferred: 0 unknown, 1 none, 2 some
_Static_assert(sizeof(copy_prefix) % 8 == 0, "records stay 8-byte aligned");
typedef struct { uintptr_t lo, hi; uint8_t *copy; } cmd_copy;
typedef struct { cmd_copy *v; uint32_t n, cap; } copy_bucket;
static SRWLOCK copy_lock = SRWLOCK_INIT; // guards the two maps, the counters and every prefix
static nvn_hmap copy_pages = {.klen = 8}; // page -> copy_bucket
static nvn_hmap copy_live = {.klen = 8}; // handle -> copy_prefix of a live copy
static uint64_t ncopies, copy_bytes, copies_made, nsubmits;
// Asynchronous replay: copies released by the game, and per-submission clones, that a queued submission still reads.
// Freed once gfx_replayed_submits() reaches their used_at. Empty with synchronous replay.
static copy_prefix **zombies;
static uint32_t nzombies, cap_zombies;
static void zombie_add(copy_prefix *pf) { // copy_lock held
if (nzombies == cap_zombies) {
cap_zombies = cap_zombies ? cap_zombies * 2 : 64;
zombies = realloc(zombies, sizeof *zombies * cap_zombies);
if (!zombies) rt_fatal("nvn: out of memory (released recording copies)");
}
zombies[nzombies++] = pf;
}
static void zombie_sweep(uint64_t replayed) { // copy_lock held
for (uint32_t i = 0; i < nzombies;) {
if (zombies[i]->used_at <= replayed) {
free(zombies[i]);
zombies[i] = zombies[--nzombies];
} else {
i++;
}
}
}
static copy_prefix *copy_find(uint64_t handle) { // copy_lock held
return nvn_hm_get(&copy_live, &handle);
}
int nvn_handle_live(uint64_t handle) {
AcquireSRWLockShared(&copy_lock);
int live = copy_find(handle) != NULL;
ReleaseSRWLockShared(&copy_lock);
return live;
}
uint64_t nvn_handle_bytes(uint64_t handle) {
AcquireSRWLockShared(&copy_lock);
const copy_prefix *pf = copy_find(handle);
uint64_t n = pf ? pf->bytes : 0;
ReleaseSRWLockShared(&copy_lock);
return n;
}
// Marks a live copy used by submission now; its size from OP_BEGIN to OP_END inclusive.
static uint64_t copy_touch(uint64_t handle, uint64_t now, const char *what) {
AcquireSRWLockShared(&copy_lock);
copy_prefix *pf = copy_find(handle);
uint64_t bytes = 0;
if (pf) __atomic_store_n(&pf->used_at, now, __ATOMIC_RELAXED), bytes = pf->bytes;
ReleaseSRWLockShared(&copy_lock);
if (!pf) rt_fatal("nvn: %s: command handle %#llx is not a live recording (released or never recorded)", what,
(unsigned long long)handle);
return bytes;
}
// The recording [p, p + n), OP_BEGIN to OP_END (the recording buffer is one block: recording emits no OP_JUMP), copied
// into one host block of its exact size. One allocation and one copy: growing the copy record by record cost the
// game's main thread about 1 ms a frame in reallocations and small copies (1.4.3, field).
static uint8_t *flatten_recording(const uint8_t *p, size_t n) {
uint8_t *out = malloc(sizeof(copy_prefix) + n);
if (!out) rt_fatal("nvn: command copy allocation");
memcpy(out + sizeof(copy_prefix), p, n);
for (size_t at = 0;;) { // the records stay checked: a corrupt one stops here, not in the replay
op_hdr h;
memcpy(&h, p + at, sizeof h);
if (h.size < 8 || (h.size & 7) || h.op >= OP_COUNT_ || h.op == OP_JUMP || h.op == OP_CALL || at + h.size > n ||
(!at && h.op != OP_BEGIN))
rt_fatal("nvn: corrupt recording at %p (op %u size %u)", (const void *)(p + at), h.op, h.size);
at += h.size;
if (h.op == OP_END) {
if (at != n) rt_fatal("nvn: recording at %p ends at %zu of %zu bytes", (const void *)p, at, n);
break;
}
}
*(copy_prefix *)out = (copy_prefix){COPY_MAGIC, n, 0, 0};
return out + sizeof(copy_prefix);
}
static void copy_add(uintptr_t lo, uintptr_t hi, uint8_t *copy) {
copy_prefix *pf = (copy_prefix *)copy - 1;
uint64_t handle = (uintptr_t)copy;
uintptr_t page = lo >> COPY_PAGE_SHIFT;
AcquireSRWLockExclusive(&copy_lock);
pf->made_at = nsubmits;
copy_bucket *b = nvn_hm_get(&copy_pages, &page);
if (!b) {
b = calloc(1, sizeof *b);
if (!b) rt_fatal("nvn: command copy index");
nvn_hm_put(&copy_pages, &page, b);
}
if (b->n == b->cap) {
b->cap = b->cap ? b->cap * 2 : 8;
b->v = realloc(b->v, b->cap * sizeof *b->v);
if (!b->v) rt_fatal("nvn: command copy index");
}
b->v[b->n++] = (cmd_copy){lo, hi, copy};
nvn_hm_put(&copy_live, &handle, pf);
ncopies++;
copy_bytes += pf->bytes;
if (++copies_made % 20000 == 0)
NVN_LOG("recording copies: %llu made, %llu live, %.1f MiB, submit %llu", (unsigned long long)copies_made,
(unsigned long long)ncopies, copy_bytes / 1048576.0, (unsigned long long)nsubmits);
ReleaseSRWLockExclusive(&copy_lock);
}
// Adding control memory again retires every recording copy made in it (the driver's handle lived there).
static void release_copies(uintptr_t lo, uintptr_t hi) {
if (hi <= lo) return;
uint64_t replayed = gfx_replayed_submits();
AcquireSRWLockExclusive(&copy_lock);
if (nzombies) zombie_sweep(replayed);
for (uintptr_t pg = lo >> COPY_PAGE_SHIFT; pg <= (hi - 1) >> COPY_PAGE_SHIFT; pg++) {
copy_bucket *b = nvn_hm_get(&copy_pages, &pg);
if (!b) continue;
for (uint32_t i = 0; i < b->n;) {
cmd_copy *k = &b->v[i];
if (!(k->lo < hi && lo < k->hi)) { i++; continue; }
uint64_t handle = (uintptr_t)k->copy;
copy_prefix *pf = (copy_prefix *)k->copy - 1;
nvn_hm_del(&copy_live, &handle);
ncopies--;
copy_bytes -= pf->bytes;
pf->magic = 0;
if (pf->used_at > replayed) zombie_add(pf); // a queued submission still reads it
else free(pf);
b->v[i] = b->v[--b->n];
}
if (!b->n) {
nvn_hm_del(&copy_pages, &pg);
free(b->v);
free(b);
}
}
ReleaseSRWLockExclusive(&copy_lock);
}
// ---------- CommandBuffer: object and memory
// NVNboolean nvnCommandBufferInitialize(NVNcommandBuffer *cmd, NVNdevice *device): the inline fast-path fields are kept
// at ~0 / 0 so NVN 55 inline code never finds room and calls the API (nvn_obj.h cmd_t)
NVN_API(nvnCommandBufferInitialize) {
cmd_t *o = cmd_of(c);
memset(o, 0, sizeof *o);
o->magic = CMD_MAGIC;
o->dev = arg_p(c, 1);
o->inline_cur = ~0ull;
o->inline_end = 0;
cb_set_disabled(o, CB_NO_COMMAND | CB_NO_CONTROL, 0);
ret_x(c, 1);
}
// void nvnCommandBufferFinalize(NVNcommandBuffer *cmd): handles outlive their recorder
NVN_API(nvnCommandBufferFinalize) {
cmd_t *o = cmd_of(c);
free(o->seg);
o->seg = o->cur = o->end = NULL;
o->magic = 0;
cb_set_disabled(o, CB_NO_COMMAND | CB_NO_CONTROL, 0);
}
// void nvnCommandBufferSetMemoryCallback(NVNcommandBuffer *cmd, PFNNVNCOMMANDBUFFERMEMORYCALLBACKPROC callback)
NVN_API(nvnCommandBufferSetMemoryCallback) { cmd_of(c)->cb = arg_p(c, 1); }
// void nvnCommandBufferSetMemoryCallbackData(NVNcommandBuffer *cmd, void *data)
NVN_API(nvnCommandBufferSetMemoryCallbackData) { cmd_of(c)->cb_data = arg_p(c, 1); }
// PFNNVNCOMMANDBUFFERMEMORYCALLBACKPROC nvnCommandBufferGetMemoryCallback(const NVNcommandBuffer *cmd)
NVN_API(nvnCommandBufferGetMemoryCallback) { ret_x(c, (uintptr_t)cmd_of(c)->cb); }
// void *nvnCommandBufferGetMemoryCallbackData(const NVNcommandBuffer *cmd) (nvn.h declares it void)
NVN_API(nvnCommandBufferGetMemoryCallbackData) { ret_x(c, (uintptr_t)cmd_of(c)->cb_data); }
// void nvnCommandBufferSetCommandMemoryCallbackEnabled / SetControlMemoryCallbackEnabled(NVNcommandBuffer *cmd,
// NVNboolean enabled) (NVN 55, no prototype in nvn.h)
NVN_API(nvnCommandBufferSetCommandMemoryCallbackEnabled) { cb_set_disabled(cmd_of(c), CB_NO_COMMAND, !nvn_arg_bool(c, 1)); }
NVN_API(nvnCommandBufferSetControlMemoryCallbackEnabled) { cb_set_disabled(cmd_of(c), CB_NO_CONTROL, !nvn_arg_bool(c, 1)); }
// void nvnCommandBufferAddCommandMemory(NVNcommandBuffer *cmd, const NVNmemoryPool *pool, ptrdiff_t offset, size_t size):
// accounting only; command memory can be shared by independent recordings with distinct control memory
NVN_API(nvnCommandBufferAddCommandMemory) {
cmd_t *o = cmd_of(c);
const pool_t *p = arg_p(c, 1);
o->cmd_base = (uintptr_t)(p ? p->mem : NULL) + (uintptr_t)arg_x(c, 2);
o->game_size = arg_x(c, 3);
o->cmd_used = 0;
o->hw_used = 0;
}
// void nvnCommandBufferAddControlMemory(NVNcommandBuffer *cmd, void *memory, size_t size): the SDK encodes handles in
// control memory (sdk+0x444530), so reusing that storage retires the copies recorded there
NVN_API(nvnCommandBufferAddControlMemory) {
cmd_t *o = cmd_of(c);
void *m = arg_p(c, 1);
size_t n = arg_x(c, 2);
release_copies((uintptr_t)m, (uintptr_t)m + n);
o->ctl = m;
o->ctl_size = n;
o->ctl_used = 0;
}
// size_t nvnCommandBufferGet{Control,Command}Memory{Size,Used,Free}(const NVNcommandBuffer *cmd): usage is bounded by
// the memory the game gave, so the game never sees more used than it provided
static size_t min_size(size_t a, size_t b) { return a < b ? a : b; }
NVN_API(nvnCommandBufferGetControlMemorySize) { ret_x(c, cmd_of(c)->ctl_size); }
NVN_API(nvnCommandBufferGetControlMemoryUsed) { const cmd_t *o = cmd_of(c); ret_x(c, min_size(o->ctl_used, o->ctl_size)); }
NVN_API(nvnCommandBufferGetControlMemoryFree) { const cmd_t *o = cmd_of(c); ret_x(c, o->ctl_size - min_size(o->ctl_used, o->ctl_size)); }
NVN_API(nvnCommandBufferGetCommandMemorySize) { ret_x(c, cmd_of(c)->game_size); }
NVN_API(nvnCommandBufferGetCommandMemoryUsed) { const cmd_t *o = cmd_of(c); ret_x(c, min_size(o->hw_used, o->game_size)); }
NVN_API(nvnCommandBufferGetCommandMemoryFree) { const cmd_t *o = cmd_of(c); ret_x(c, o->game_size - min_size(o->hw_used, o->game_size)); }
// ---------- CommandBuffer: recording
// void nvnCommandBufferBeginRecording(NVNcommandBuffer *cmd)
NVN_API(nvnCommandBufferBeginRecording) {
cmd_t *o = cmd_of(c);
if (o->recording) rt_log("nvn: BeginRecording on command buffer %p while it records", (void *)o);
o->recording = 1;
o->cur = o->seg; // the previous recording was copied at EndRecording
o->rec_start = o->cmd_used;
put_hdr(cmd_alloc(o, 8), OP_BEGIN, 8);
o->hw_used -= 8; // BEGIN and END have no hardware equivalent
o->hw_start = o->hw_used;
if (!o->game_size) memory_event(c, o, NVN_COMMAND_BUFFER_MEMORY_EVENT_OUT_OF_COMMAND_MEMORY, MIN_COMMAND_SIZE);
}
// NVNcommandHandle nvnCommandBufferEndRecording(NVNcommandBuffer *cmd)
NVN_API(nvnCommandBufferEndRecording) {
cmd_t *o = cmd_of(c);
if (!o->recording || !o->seg) rt_fatal("nvn: EndRecording on command buffer %p, which does not record", (void *)o);
if (!o->ctl_size) memory_event(c, o, NVN_COMMAND_BUFFER_MEMORY_EVENT_OUT_OF_CONTROL_MEMORY, MIN_CONTROL_SIZE);
put_hdr(cmd_alloc(o, 8), OP_END, 8);
o->hw_used -= 8;
o->recording = 0;
uint8_t *copy = flatten_recording(o->seg, (size_t)(o->cur - o->seg));
// Finalizing the recorder does not retire its handle; only reusing the control memory it was recorded in does.
uintptr_t lo = (uintptr_t)o->ctl + (o->ctl_used < o->ctl_size ? o->ctl_used : 0);
uintptr_t hi = o->ctl ? lo + 1 : 0;
o->ctl_used += CTL_PER_RECORDING;
copy_add(lo, hi, copy);
ret_x(c, (uintptr_t)copy);
}
// NVNboolean nvnCommandBufferIsRecording(const NVNcommandBuffer *cmd)
NVN_API(nvnCommandBufferIsRecording) { ret_x(c, cmd_of(c)->recording != 0); }
// The called recording is copied into the caller at the call position: the game recycles command memory in small steps,
// so a handle may be gone by the time the caller is submitted. Deferred binds stay deferred. A copy is OP_BEGIN, its
// records and OP_END (flatten_recording checked them), so the records go over as one block; usage grows by the same
// bytes as record by record.
static void inline_recording(cmd_t *o, uint64_t handle, const char *what) {
if (!handle) rt_fatal("nvn: %s with a null handle", what);
uint64_t bytes = copy_touch(handle, __atomic_load_n(&nsubmits, __ATOMIC_RELAXED), what);
if (bytes <= 16) return;
size_t body = (size_t)bytes - 16;
cmd_reserve(o, body);
memcpy(o->cur, (const uint8_t *)(uintptr_t)handle + 8, body);
o->cur += body;
o->cmd_used += body;
o->hw_used += body;
o->last_rec = 0;
}
// void nvnCommandBuffer{Call,Copy}Commands(NVNcommandBuffer *cmd, int n, const NVNcommandHandle *handles)
NVN_API(nvnCommandBufferCallCommands) {
cmd_t *o = cmd_of(c);
int n = (int)arg_x(c, 1);
const uint64_t *h = arg_p(c, 2);
for (int i = 0; i < n; i++) inline_recording(o, h[i], "CallCommands");
}
NVN_API(nvnCommandBufferCopyCommands) {
cmd_t *o = cmd_of(c);
int n = (int)arg_x(c, 1);
const uint64_t *h = arg_p(c, 2);
for (int i = 0; i < n; i++) inline_recording(o, h[i], "CopyCommands");
}
// ---------- CommandBuffer: commands with CPU-visible results (performed by the back end in GPU order)
// void nvnCommandBufferSignalEvent(NVNcommandBuffer *cmd, const NVNevent *event, NVNeventSignalMode mode,
// NVNeventSignalLocation location, int flags, uint32_t value)
NVN_API(nvnCommandBufferSignalEvent) {
const ev_t *e = arg_p(c, 1);
*REC(cmd_of(c), OP_SIGNAL_EVENT, p_signal_event) = (p_signal_event){e->val, (uint32_t)arg_x(c, 2), (uint32_t)arg_x(c, 5)};
}
// void nvnCommandBufferResetCounter(NVNcommandBuffer *cmd, NVNcounterType type)
NVN_API(nvnCommandBufferResetCounter) { REC(cmd_of(c), OP_RESET_COUNTER, p_reset_counter)->type = (uint32_t)arg_x(c, 1); }
// void nvnCommandBufferReportCounter(NVNcommandBuffer *cmd, NVNcounterType type, NVNbufferAddress address)
NVN_API(nvnCommandBufferReportCounter) {
*REC(cmd_of(c), OP_REPORT_COUNTER, p_report_counter) = (p_report_counter){(uint32_t)arg_x(c, 1), arg_x(c, 2)};
}
// void nvnCommandBufferCopyBufferToBuffer(NVNcommandBuffer *cmd, NVNbufferAddress src, NVNbufferAddress dst,
// size_t size, int flags)
NVN_API(nvnCommandBufferCopyBufferToBuffer) {
*REC(cmd_of(c), OP_COPY_B2B, p_copy_b2b) = (p_copy_b2b){arg_x(c, 1), arg_x(c, 2), arg_x(c, 3)};
}
// void nvnCommandBufferUpdateUniformBuffer(NVNcommandBuffer *cmd, NVNbufferAddress buffer, size_t buffer_size,
// ptrdiff_t offset, size_t size, const void *data): the data is copied at record time (the caller may change it
// afterwards) and written into the buffer at replay
NVN_API(nvnCommandBufferUpdateUniformBuffer) {
uint64_t dst = arg_x(c, 1) + arg_x(c, 3), n = arg_x(c, 4);
const void *data = arg_p(c, 5);
if (n > (16u << 20)) rt_fatal("nvn: UpdateUniformBuffer of %#llx bytes", (unsigned long long)n);
p_update_ub *r = rec(cmd_of(c), OP_UPDATE_UB, offsetof(p_update_ub, data) + n);
r->dst = dst;
r->size = n;
memcpy(r->data, data, n);
}
// ---------- CommandBuffer: render state, payloads of nvn_obj.h
// void nvnCommandBufferSetRenderTargets(NVNcommandBuffer *cmd, int n, const NVNtexture *const *colors,
// const NVNtextureView *const *color_views, const NVNtexture *depth, const NVNtextureView *depth_view)
NVN_API(nvnCommandBufferSetRenderTargets) {
int n = (int)arg_x(c, 1);
const NVNtexture *const *ct = arg_p(c, 2);
const NVNtextureView *const *cv = arg_p(c, 3);
const NVNtexture *dt = arg_p(c, 4);
const NVNtextureView *dv = arg_p(c, 5);
if (n < 0 || n > 8) {
rt_log("nvn: SetRenderTargets with %d colour targets", n);
n = n < 0 ? 0 : 8;
}
p_set_rt *r = REC(cmd_of(c), OP_SET_RT, p_set_rt);
r->n = n;
for (int i = 0; i < n; i++) {
r->color[i] = ct ? tex_pin(ct[i]) : NULL;
copy_view(&r->cview[i], &r->has_cview[i], cv ? cv[i] : NULL);
}
r->depth = tex_pin(dt);
copy_view(&r->dview, &r->has_dview, dv);
}
// void nvnCommandBufferClearColor(NVNcommandBuffer *cmd, int index, const float *rgba, int mask)
NVN_API(nvnCommandBufferClearColor) {
p_clear_color *r = REC(cmd_of(c), OP_CLEAR_COLOR, p_clear_color);
r->index = (int)arg_x(c, 1);
r->mask = (int)arg_x(c, 3);
memcpy(r->rgba, arg_p(c, 2), 16);
}
// void nvnCommandBufferClearDepthStencil(NVNcommandBuffer *cmd, float depth, NVNboolean depth_mask, int stencil,
// int stencil_mask)
NVN_API(nvnCommandBufferClearDepthStencil) {
*REC(cmd_of(c), OP_CLEAR_DS, p_clear_ds) = (p_clear_ds){arg_s(c, 0), nvn_arg_bool(c, 1), (int)arg_x(c, 2), (int)arg_x(c, 3)};
}
static void set_viewports(cmd_t *o, int first, int n, const float *v) {
if (first < 0 || n < 0 || first + n > 16) {
rt_log("nvn: SetViewports first %d n %d", first, n);
return;
}
p_viewports *r = REC(o, OP_VIEWPORTS, p_viewports);
r->first = first;
r->n = n;
for (int i = 0; i < n; i++) memcpy(r->v[i], v + 4 * i, 16);
}
// void nvnCommandBufferSetViewports(NVNcommandBuffer *cmd, int first, int n, const float *xywh)
NVN_API(nvnCommandBufferSetViewports) { set_viewports(cmd_of(c), (int)arg_x(c, 1), (int)arg_x(c, 2), arg_p(c, 3)); }
// void nvnCommandBufferSetViewport(NVNcommandBuffer *cmd, int x, int y, int w, int h)
NVN_API(nvnCommandBufferSetViewport) {
float v[4] = {(float)(int)arg_x(c, 1), (float)(int)arg_x(c, 2), (float)(int)arg_x(c, 3), (float)(int)arg_x(c, 4)};
set_viewports(cmd_of(c), 0, 1, v);
}
static void set_scissor(cmd_t *o, int x, int y, int w, int h) { *REC(o, OP_SCISSOR, p_scissor) = (p_scissor){x, y, w, h}; }
// void nvnCommandBufferSetScissor(NVNcommandBuffer *cmd, int x, int y, int w, int h)
NVN_API(nvnCommandBufferSetScissor) {
set_scissor(cmd_of(c), (int)arg_x(c, 1), (int)arg_x(c, 2), (int)arg_x(c, 3), (int)arg_x(c, 4));
}
// void nvnCommandBufferSetScissors(NVNcommandBuffer *cmd, int first, int n, const int *xywh): the replay keeps one
// scissor (index 0); other indices matter only to shaders that select a viewport index, so differing ones are reported
NVN_API(nvnCommandBufferSetScissors) {
int first = (int)arg_x(c, 1), n = (int)arg_x(c, 2);
const int *r = arg_p(c, 3);
if (first < 0 || n < 0 || first + n > 16) rt_fatal("nvn: SetScissors first %d n %d", first, n);
for (int i = 1; i < n; i++)
if (memcmp(r + 4 * i, r, 16)) {
static int once;
if (!once++) rt_log("nvn: SetScissors: differing scissor %d ignored (single-scissor replay)", first + i);
}
if (first == 0 && n > 0) set_scissor(cmd_of(c), r[0], r[1], r[2], r[3]);
}
static void set_depth_range(cmd_t *o, float n, float f) { *REC(o, OP_DEPTH_RANGE, p_depth_range) = (p_depth_range){n, f}; }
// void nvnCommandBufferSetDepthRange(NVNcommandBuffer *cmd, float near, float far)
NVN_API(nvnCommandBufferSetDepthRange) { set_depth_range(cmd_of(c), arg_s(c, 0), arg_s(c, 1)); }
// void nvnCommandBufferSetDepthRanges(NVNcommandBuffer *cmd, int first, int n, const float *ranges)
NVN_API(nvnCommandBufferSetDepthRanges) {
int first = (int)arg_x(c, 1), n = (int)arg_x(c, 2);
const float *r = arg_p(c, 3);
if (first < 0 || n < 0 || first + n > 16) rt_fatal("nvn: SetDepthRanges first %d n %d", first, n);
for (int i = 1; i < n; i++)
if (memcmp(r + 2 * i, r, 8)) {
static int once;
if (!once++) rt_log("nvn: SetDepthRanges: differing range %d ignored (single-range replay)", first + i);
}
if (first == 0 && n > 0) set_depth_range(cmd_of(c), r[0], r[1]);
}
// void nvnCommandBufferSetDepthClamp / SetRenderEnable / SetRasterizerDiscard(NVNcommandBuffer *cmd, NVNboolean enable)
NVN_API(nvnCommandBufferSetDepthClamp) { REC(cmd_of(c), OP_DEPTH_CLAMP, p_flag)->value = nvn_arg_bool(c, 1) != 0; }
NVN_API(nvnCommandBufferSetRenderEnable) { REC(cmd_of(c), OP_RENDER_ENABLE, p_flag)->value = nvn_arg_bool(c, 1) != 0; }
// Rasterizer discard runs vertex work without rasterising; with no transform feedback that equals not drawing.
NVN_API(nvnCommandBufferSetRasterizerDiscard) { REC(cmd_of(c), OP_RASTER_DISCARD, p_flag)->value = nvn_arg_bool(c, 1) != 0; }
// void nvnCommandBufferSetPrimitiveRestart(NVNcommandBuffer *cmd, NVNboolean enable, int index): TOTK draws its
// terrain and cliff strips with 16-bit indices and restart at 0xffff; the replay applies it per indexed draw (op_draw_idx)
NVN_API(nvnCommandBufferSetPrimitiveRestart) {
*REC(cmd_of(c), OP_PRIM_RESTART, p_prim_restart) = (p_prim_restart){nvn_arg_bool(c, 1) != 0, (uint32_t)arg_x(c, 2)};
}
// void nvnCommandBufferSetRenderEnableConditional(NVNcommandBuffer *cmd, NVNconditionalRenderMode mode,
// NVNbufferAddress counters): rendering is enabled when the two NVNcounterData.counter values at addr / addr+16
// compare equal (mode 0) or not equal (mode 1). TOTK gates most of its ClearColor commands with it; the replay
// evaluates it in GPU order.
NVN_API(nvnCommandBufferSetRenderEnableConditional) {
int m = (int)arg_x(c, 1);
if (m != 0 && m != 1) rt_fatal("nvn: SetRenderEnableConditional: unknown mode %d", m);
p_render_cond *r = REC(cmd_of(c), OP_RENDER_COND, p_render_cond);
r->mode = m;
r->addr = arg_x(c, 2);
}
// void nvnCommandBufferBindProgram(NVNcommandBuffer *cmd, const NVNprogram *program, int stages)
NVN_API(nvnCommandBufferBindProgram) {
*REC(cmd_of(c), OP_BIND_PROGRAM, p_bind_program) = (p_bind_program){arg_p(c, 1), (int)arg_x(c, 2)};
}
// void nvnCommandBufferBind{Blend,Color,ChannelMask,Multisample,Polygon,DepthStencil}State(NVNcommandBuffer *cmd,
// const NVN*State *state): the state object's bytes are copied (p_bind_state.kind order)
static void bind_state(const Cpu *c, int kind, size_t n) {
p_bind_state *r = REC(cmd_of(c), OP_BIND_STATE, p_bind_state);
r->kind = kind;
memcpy(r->bytes, arg_p(c, 1), n);
}
NVN_API(nvnCommandBufferBindBlendState) { bind_state(c, 0, sizeof(blend_st)); }
NVN_API(nvnCommandBufferBindColorState) { bind_state(c, 1, sizeof(color_st)); }
NVN_API(nvnCommandBufferBindChannelMaskState) { bind_state(c, 2, sizeof(chmask_st)); }
NVN_API(nvnCommandBufferBindMultisampleState) { bind_state(c, 3, sizeof(ms_st)); }
NVN_API(nvnCommandBufferBindPolygonState) { bind_state(c, 4, sizeof(poly_st)); }
NVN_API(nvnCommandBufferBindDepthStencilState) { bind_state(c, 5, sizeof(ds_st)); }
// void nvnCommandBufferSetPointSize(NVNcommandBuffer *cmd, float size): recorded as bind-state kind 6 with the float as payload
NVN_API(nvnCommandBufferSetPointSize) {
p_bind_state *r = REC(cmd_of(c), OP_BIND_STATE, p_bind_state);
float s = arg_s(c, 0);
r->kind = 6;
memcpy(r->bytes, &s, sizeof s);
}
// void nvnCommandBufferBindVertexAttribState(NVNcommandBuffer *cmd, int n, const NVNvertexAttribState *states)
NVN_API(nvnCommandBufferBindVertexAttribState) {
cmd_t *o = cmd_of(c);
int n = (int)arg_x(c, 1);
if (n < 0 || n > 16) { rt_log("nvn: BindVertexAttribState n %d", n); return; }
p_bind_vattr *r = RECN(o, OP_BIND_VATTR, p_bind_vattr, st, n);
r->first = 0;
r->n = n;
memcpy(r->st, arg_p(c, 2), 4 * (size_t)n);
hw_fix(o, 68); // the driver always writes all 16 attribute formats: 17 words
}
// void nvnCommandBufferBindVertexStreamState(NVNcommandBuffer *cmd, int n, const NVNvertexStreamState *states)
NVN_API(nvnCommandBufferBindVertexStreamState) {
cmd_t *o = cmd_of(c);
int n = (int)arg_x(c, 1);
if (n < 0 || n > 16) { rt_log("nvn: BindVertexStreamState n %d", n); return; }
p_bind_vstream *r = RECN(o, OP_BIND_VSTREAM, p_bind_vstream, st, n);
r->first = 0;
r->n = n;
memcpy(r->st, arg_p(c, 2), 8 * (size_t)n);
hw_fix(o, 4 * (16 + (size_t)n));
}
static void bind_vertex_buffers(cmd_t *o, int first, int n, const NVNbufferRange *rg) {
if (first < 0 || n < 0 || first + n > 16) { rt_log("nvn: BindVertexBuffers first %d n %d", first, n); return; }
p_bind_vbuf *r = RECN(o, OP_BIND_VBUF, p_bind_vbuf, r, n);
r->first = first;
r->n = n;
memcpy(r->r, rg, sizeof *rg * (size_t)n);
hw_fix(o, 24 * (size_t)n);
}
// void nvnCommandBufferBindVertexBuffers(NVNcommandBuffer *cmd, int first, int n, const NVNbufferRange *ranges)
NVN_API(nvnCommandBufferBindVertexBuffers) { bind_vertex_buffers(cmd_of(c), (int)arg_x(c, 1), (int)arg_x(c, 2), arg_p(c, 3)); }
// void nvnCommandBufferBindVertexBuffer(NVNcommandBuffer *cmd, int index, NVNbufferAddress address, size_t size)
NVN_API(nvnCommandBufferBindVertexBuffer) {
NVNbufferRange r = {arg_x(c, 2), arg_x(c, 3)};
bind_vertex_buffers(cmd_of(c), (int)arg_x(c, 1), 1, &r);
}
// Uniform and storage buffers: one record per binding (CB_SIZE/ADDRESS 4 words + CB_BIND 1 on hardware).
static void bind_buffers(const Cpu *c, uint32_t op) { // (cmd, stage, first, n, const NVNbufferRange *ranges)
cmd_t *o = cmd_of(c);
int st = (int)arg_x(c, 1), first = (int)arg_x(c, 2), n = (int)arg_x(c, 3);
const NVNbufferRange *rg = arg_p(c, 4);
for (int i = 0; i < n; i++) {
*REC(o, op, p_bind_buf) = (p_bind_buf){st, first + i, rg[i].address, rg[i].size};
hw_fix(o, 20);
}
}
static void bind_buffer(const Cpu *c, uint32_t op) { // (cmd, stage, index, NVNbufferAddress address, size_t size)
cmd_t *o = cmd_of(c);
*REC(o, op, p_bind_buf) = (p_bind_buf){(int)arg_x(c, 1), (int)arg_x(c, 2), arg_x(c, 3), arg_x(c, 4)};
hw_fix(o, 20);
}
NVN_API(nvnCommandBufferBindUniformBuffer) { bind_buffer(c, OP_BIND_UBO); }
NVN_API(nvnCommandBufferBindUniformBuffers) { bind_buffers(c, OP_BIND_UBO); }
NVN_API(nvnCommandBufferBindStorageBuffer) { bind_buffer(c, OP_BIND_SSBO); }
NVN_API(nvnCommandBufferBindStorageBuffers) { bind_buffers(c, OP_BIND_SSBO); }
// Texture, image and separate handle binds. A deferred bind (NVN 55 *Deferred entry points, the fallback of the inline
// fast paths) keeps the pointer to the game's handle array: the original driver (sdk+0x46cc00 BindTexturesDeferred)
// reads the handles when the command buffer is submitted, and the game fills its material handle tables after
// recording and before the submit (nvn_submit_prepare). hw_bytes: the hardware size of the plain variant.
static void bind_handles(cmd_t *o, uint32_t op, int st, int first, int n, const uint64_t *h, int deferred, size_t hw_bytes) {
if (n < 0 || n > 32) { rt_log("nvn: bind of %d handles", n); return; }
p_bind_handles *r = RECN(o, op, p_bind_handles, h, n);
r->stage = st;
r->first = first;
r->n = n;
r->src = deferred ? (uint64_t)(uintptr_t)h : 0;
memcpy(r->h, h, 8 * (size_t)n);
hw_fix(o, hw_bytes ? hw_bytes : 16 + 4 * (size_t)n); // macro 7 + method 0x1a2c + CB_POS + one word per binding
}
static void bind_handle_array(const Cpu *c, uint32_t op, int deferred, int wide) { // (cmd, stage, first, n, handles)
int n = (int)arg_x(c, 3);
bind_handles(cmd_of(c), op, (int)arg_x(c, 1), (int)arg_x(c, 2), n, arg_p(c, 4), deferred,
wide && n >= 0 && n <= 32 ? 16 + 8 * (size_t)n : 0);
}
static void bind_one_handle(const Cpu *c, uint32_t op, size_t hw_bytes) { // (cmd, stage, index, handle)
uint64_t v = arg_x(c, 3);
bind_handles(cmd_of(c), op, (int)arg_x(c, 1), (int)arg_x(c, 2), 1, &v, 0, hw_bytes);
}
// Combined texture handles share the texture slots with separate textures (sampler id in bits 20 and up). Images
// (writable textures): handle = texture pool index; the game does not call RegisterImage, images and textures share
// their registration.
NVN_API(nvnCommandBufferBindTexture) { bind_one_handle(c, OP_BIND_TEX, 0); }
NVN_API(nvnCommandBufferBindTextures) { bind_handle_array(c, OP_BIND_TEX, 0, 0); }
NVN_API(nvnCommandBufferBindImage) { bind_one_handle(c, OP_BIND_IMG, 24); }
NVN_API(nvnCommandBufferBindImages) { bind_handle_array(c, OP_BIND_IMG, 0, 1); }
NVN_API(nvnCommandBufferBindSeparateTexture) { bind_one_handle(c, OP_BIND_TEX, 24); }
NVN_API(nvnCommandBufferBindSeparateTextures) { bind_handle_array(c, OP_BIND_TEX, 0, 1); }
NVN_API(nvnCommandBufferBindSeparateSampler) { bind_one_handle(c, OP_BIND_SAMP, 24); }
NVN_API(nvnCommandBufferBindSeparateSamplers) { bind_handle_array(c, OP_BIND_SAMP, 0, 1); }
// void nvnCommandBufferBind{Textures,Images,SeparateTextures,SeparateSamplers}Deferred(NVNcommandBuffer *cmd,
// NVNshaderStage stage, int first, int n, const T *handles) (NVN 55, no prototype in nvn.h)
NVN_API(nvnCommandBufferBindTexturesDeferred) { bind_handle_array(c, OP_BIND_TEX, 1, 0); }
NVN_API(nvnCommandBufferBindImagesDeferred) { bind_handle_array(c, OP_BIND_IMG, 1, 1); }
NVN_API(nvnCommandBufferBindSeparateTexturesDeferred) { bind_handle_array(c, OP_BIND_TEX, 1, 1); }
NVN_API(nvnCommandBufferBindSeparateSamplersDeferred) { bind_handle_array(c, OP_BIND_SAMP, 1, 1); }
// void nvnCommandBufferBind{Uniform,Storage}BuffersDeferred(NVNcommandBuffer *cmd, NVNshaderStage stage, int first,
// int n, const NVNbufferRange *ranges): buffer ranges still bind at record time
NVN_API(nvnCommandBufferBindUniformBuffersDeferred) {
static int once;
if (!once++) NVN_LOG("BindUniformBuffersDeferred: buffer ranges bind at record time");
bind_buffers(c, OP_BIND_UBO);
}
NVN_API(nvnCommandBufferBindStorageBuffersDeferred) {
static int once;
if (!once++) NVN_LOG("BindStorageBuffersDeferred: buffer ranges bind at record time");
bind_buffers(c, OP_BIND_SSBO);
}
// void nvnCommandBufferSetStencil{ValueMask,Mask,Ref}(NVNcommandBuffer *cmd, NVNface face, int value)
NVN_API(nvnCommandBufferSetStencilValueMask) {
*REC(cmd_of(c), OP_STENCIL, p_stencil) = (p_stencil){(int)arg_x(c, 1), (int)arg_x(c, 2), 0, 0, 0};
}
NVN_API(nvnCommandBufferSetStencilMask) {
*REC(cmd_of(c), OP_STENCIL, p_stencil) = (p_stencil){(int)arg_x(c, 1), 0, (int)arg_x(c, 2), 0, 1};
}
NVN_API(nvnCommandBufferSetStencilRef) {
*REC(cmd_of(c), OP_STENCIL, p_stencil) = (p_stencil){(int)arg_x(c, 1), 0, 0, (int)arg_x(c, 2), 2};
}
// void nvnCommandBufferSetPolygonOffsetClamp(NVNcommandBuffer *cmd, float factor, float units, float clamp)
NVN_API(nvnCommandBufferSetPolygonOffsetClamp) {
*REC(cmd_of(c), OP_POLY_OFFSET, p_poly_offset) = (p_poly_offset){arg_s(c, 0), arg_s(c, 1), arg_s(c, 2)};
}
// void nvnCommandBufferDrawArrays(NVNcommandBuffer *cmd, NVNdrawPrimitive prim, int first, int count)
NVN_API(nvnCommandBufferDrawArrays) {
*REC(cmd_of(c), OP_DRAW, p_draw) = (p_draw){(int)arg_x(c, 1), (int)arg_x(c, 2), (int)arg_x(c, 3), 0, 1};
}
// void nvnCommandBufferDrawArraysInstanced(NVNcommandBuffer *cmd, NVNdrawPrimitive prim, int first, int count,
// int base_instance, int instances): zero instances draw nothing
NVN_API(nvnCommandBufferDrawArraysInstanced) {
int inst = (int)arg_x(c, 5);
if (inst <= 0) return;
*REC(cmd_of(c), OP_DRAW, p_draw) = (p_draw){(int)arg_x(c, 1), (int)arg_x(c, 2), (int)arg_x(c, 3), (int)arg_x(c, 4), inst};
}
// void nvnCommandBufferDrawElements(NVNcommandBuffer *cmd, NVNdrawPrimitive prim, NVNindexType type, int count,
// NVNbufferAddress indices)
NVN_API(nvnCommandBufferDrawElements) {
cmd_t *o = cmd_of(c);
*REC(o, OP_DRAW_IDX, p_draw_idx) = (p_draw_idx){(int)arg_x(c, 1), (int)arg_x(c, 2), (int)arg_x(c, 3), 0, 0, 1, arg_x(c, 4)};
hw_fix(o, 48);
}
// void nvnCommandBufferDrawElementsBaseVertex(..., NVNbufferAddress indices, int base_vertex)
NVN_API(nvnCommandBufferDrawElementsBaseVertex) {
cmd_t *o = cmd_of(c);
*REC(o, OP_DRAW_IDX, p_draw_idx) =
(p_draw_idx){(int)arg_x(c, 1), (int)arg_x(c, 2), (int)arg_x(c, 3), (int)arg_x(c, 5), 0, 1, arg_x(c, 4)};
hw_fix(o, 44);
}
// void nvnCommandBufferDrawElementsInstanced(..., NVNbufferAddress indices, int base_vertex, int base_instance,
// int instances): assumed to use the 6-parameter draw macro of the base-vertex form (unverified)
NVN_API(nvnCommandBufferDrawElementsInstanced) {
cmd_t *o = cmd_of(c);
*REC(o, OP_DRAW_IDX, p_draw_idx) = (p_draw_idx){(int)arg_x(c, 1), (int)arg_x(c, 2), (int)arg_x(c, 3),
(int)arg_x(c, 5), (int)arg_x(c, 6), (int)arg_x(c, 7), arg_x(c, 4)};
hw_fix(o, 44);
}
// void nvnCommandBufferDrawElementsIndirect(NVNcommandBuffer *cmd, NVNdrawPrimitive prim, NVNindexType type,
// NVNbufferAddress indices, NVNbufferAddress indirect): the arguments at indirect are often GPU-written, so the replay
// draws with vkCmdDrawIndexedIndirect from the pool buffer (nxnative hle_nvn.c, SavageWindRecomp cmdbuf.c)
NVN_API(nvnCommandBufferDrawElementsIndirect) {
*REC(cmd_of(c), OP_DRAW_IDX_IND, p_draw_idx_ind) = (p_draw_idx_ind){(int)arg_x(c, 1), (int)arg_x(c, 2), arg_x(c, 3), arg_x(c, 4)};
}
// void nvnCommandBufferDispatchCompute(NVNcommandBuffer *cmd, int x, int y, int z): caller = the game's return address
NVN_API(nvnCommandBufferDispatchCompute) {
*REC(cmd_of(c), OP_DISPATCH, p_dispatch) = (p_dispatch){(int)arg_x(c, 1), (int)arg_x(c, 2), (int)arg_x(c, 3), c->x[30]};
}
// void nvnCommandBufferDispatchComputeIndirect(NVNcommandBuffer *cmd, NVNbufferAddress indirect): vkCmdDispatchIndirect
NVN_API(nvnCommandBufferDispatchComputeIndirect) {
*REC(cmd_of(c), OP_DISPATCH_IND, p_dispatch_ind) = (p_dispatch_ind){arg_x(c, 1), c->x[30]};
}
// void nvnCommandBufferClearBuffer(NVNcommandBuffer *cmd, NVNbufferAddress addr, size_t size, uint32_t value)
NVN_API(nvnCommandBufferClearBuffer) {
*REC(cmd_of(c), OP_CLEAR_BUF, p_clear_buf) = (p_clear_buf){arg_x(c, 1), arg_x(c, 2), (uint32_t)arg_x(c, 3), 0};
}
// void nvnCommandBufferReportValue(NVNcommandBuffer *cmd, uint32_t value, NVNbufferAddress addr): the value lands when
// the GPU reaches this point. Recorded as a 4-byte ClearBuffer, so it stays in GPU order with the ClearBuffer resets of
// the same counter (main+0x7b9274 modes 1-3) and works in CPU-invisible pools; the CPU sees it after the fence, as a
// counter report.
NVN_API(nvnCommandBufferReportValue) {
*REC(cmd_of(c), OP_CLEAR_BUF, p_clear_buf) = (p_clear_buf){arg_x(c, 2), 4, (uint32_t)arg_x(c, 1), 0};
}
// Colour reduction is a Maxwell bandwidth hint for blending into compressed targets: no effect on the result.
// void nvnCommandBufferSetColorReductionEnable(cmd, NVNboolean enable), SetColorReductionThresholds(cmd, class, const int *)
NVN_API(nvnCommandBufferSetColorReductionEnable) { (void)c; }
NVN_API(nvnCommandBufferSetColorReductionThresholds) { (void)c; }
// void nvnCommandBufferBarrier(NVNcommandBuffer *cmd, int bits)
NVN_API(nvnCommandBufferBarrier) { REC(cmd_of(c), OP_BARRIER, p_barrier)->bits = (int)arg_x(c, 1); }
// void nvnCommandBufferSetTexturePool / SetSamplerPool(NVNcommandBuffer *cmd, const NVNtexturePool / NVNsamplerPool *)
NVN_API(nvnCommandBufferSetTexturePool) { REC(cmd_of(c), OP_SET_TEX_POOL, p_set_pool)->pool = arg_p(c, 1); }
NVN_API(nvnCommandBufferSetSamplerPool) { REC(cmd_of(c), OP_SET_SAMP_POOL, p_set_pool)->pool = arg_p(c, 1); }
// void nvnCommandBufferCopyBufferToTexture(NVNcommandBuffer *cmd, NVNbufferAddress src, const NVNtexture *dst,
// const NVNtextureView *view, const NVNcopyRegion *region, int flags)
NVN_API(nvnCommandBufferCopyBufferToTexture) {
p_copy_b2t *o = REC(cmd_of(c), OP_COPY_B2T, p_copy_b2t);
o->src = arg_x(c, 1);
o->tex = tex_pin(arg_p(c, 2));
copy_view(&o->view, &o->has_view, arg_p(c, 3));
o->r = *(const NVNcopyRegion *)arg_p(c, 4);
o->flags = (int)arg_x(c, 5);
}
// void nvnCommandBufferCopyTextureToBuffer(NVNcommandBuffer *cmd, const NVNtexture *src, const NVNtextureView *view,
// const NVNcopyRegion *region, NVNbufferAddress dst, int flags): p_copy_b2t with src = the destination address
NVN_API(nvnCommandBufferCopyTextureToBuffer) {
p_copy_b2t *o = REC(cmd_of(c), OP_COPY_T2B, p_copy_b2t);
o->src = arg_x(c, 4);
o->tex = tex_pin(arg_p(c, 1));
copy_view(&o->view, &o->has_view, arg_p(c, 2));
o->r = *(const NVNcopyRegion *)arg_p(c, 3);
o->flags = (int)arg_x(c, 5);
}
// void nvnCommandBufferClearTexture{,ui}(NVNcommandBuffer *cmd, const NVNtexture *tex, const NVNtextureView *view,
// const NVNcopyRegion *region, const float / uint32_t *rgba, int mask)
static void clear_texture(const Cpu *c, int is_uint) {
p_clear_tex *o = REC(cmd_of(c), OP_CLEAR_TEX, p_clear_tex);
o->tex = tex_pin(arg_p(c, 1));
copy_view(&o->view, &o->has_view, arg_p(c, 2));
o->r = *(const NVNcopyRegion *)arg_p(c, 3);
o->mask = (int)arg_x(c, 5);
o->is_uint = (uint8_t)is_uint;
memcpy(o->c.u, arg_p(c, 4), 16);
}
NVN_API(nvnCommandBufferClearTexture) { clear_texture(c, 0); }
NVN_API(nvnCommandBufferClearTextureui) { clear_texture(c, 1); }
// void nvnCommandBufferCopyTextureToTexture(NVNcommandBuffer *cmd, const NVNtexture *src, const NVNtextureView *src_view,
// const NVNcopyRegion *src_region, const NVNtexture *dst, const NVNtextureView *dst_view,
// const NVNcopyRegion *dst_region, int flags)
NVN_API(nvnCommandBufferCopyTextureToTexture) {
p_copy_t2t *o = REC(cmd_of(c), OP_COPY_T2T, p_copy_t2t);
o->src = tex_pin(arg_p(c, 1));
o->dst = tex_pin(arg_p(c, 4));
copy_view(&o->sv, &o->has_sv, arg_p(c, 2));
copy_view(&o->dv, &o->has_dv, arg_p(c, 5));
o->sr = *(const NVNcopyRegion *)arg_p(c, 3);
o->dr = *(const NVNcopyRegion *)arg_p(c, 6);
o->flags = (int)arg_x(c, 7);
}
// void nvnCommandBufferDownsample(NVNcommandBuffer *cmd, const NVNtexture *src, const NVNtexture *dst): level 0 of src
// into level 0 of dst, whole extents, filtered. Recorded as a linear-filter texture copy: copy_t2t resolves a
// multisample source into an equal-size target and blits (2x2 average at half size) between sizes. As nxnative
// hle_nvn.c and SavageWindRecomp cmdbuf.c record it; docs/maxwell.md 1.19.
NVN_API(nvnCommandBufferDownsample) {
p_copy_t2t *o = REC(cmd_of(c), OP_COPY_T2T, p_copy_t2t);
o->src = tex_pin(arg_p(c, 1));
o->dst = tex_pin(arg_p(c, 2));
if (o->src) o->sr = (NVNcopyRegion){0, 0, 0, (int)o->src->d.width, (int)o->src->d.height, 1};
if (o->dst) o->dr = (NVNcopyRegion){0, 0, 0, (int)o->dst->d.width, (int)o->dst->d.height, 1};
o->flags = NVN_COPY_FLAGS_LINEAR_FILTER_BIT;
}
// void nvnCommandBufferSetCopyRowStride(NVNcommandBuffer *cmd, ptrdiff_t stride)
NVN_API(nvnCommandBufferSetCopyRowStride) { REC(cmd_of(c), OP_COPY_STRIDE, p_copy_stride)->stride = (int64_t)arg_x(c, 1); }
// void nvnCommandBufferSetBlendColor(NVNcommandBuffer *cmd, const float *rgba)
NVN_API(nvnCommandBufferSetBlendColor) { memcpy(REC(cmd_of(c), OP_BLEND_COLOR, p_blend_color)->rgba, arg_p(c, 1), 16); }
// Commands without an effect on this replay (hle_nvn.c g_nops): WaitSync and WaitEvent order work against other queues
// or the CPU, and every submit replays in order on one queue; shader scratch memory and the tessellation patch size
// have no host counterpart the replay needs.
// void nvnCommandBufferWaitSync(NVNcommandBuffer *cmd, const NVNsync *sync), WaitEvent(cmd, const NVNevent *event,
// NVNeventWaitMode mode, uint32_t value), SetShaderScratchMemory(cmd, const NVNmemoryPool *pool, ptrdiff_t offset,
// size_t size), SetPatchSize(cmd, int size)
NVN_API(nvnCommandBufferWaitSync) { (void)c; }
NVN_API(nvnCommandBufferWaitEvent) { (void)c; }
NVN_API(nvnCommandBufferSetShaderScratchMemory) { (void)c; }
NVN_API(nvnCommandBufferSetPatchSize) { (void)c; }
// ---------- submit
// What the console reads at the submit call is read here, on the game thread: every handle must be a live recording,
// and deferred binds take the handles the game's arrays hold now (the game fills its tables for the next frame as soon
// as the submit returns). The result is written into the recording copy.
static uint64_t def_reads, def_changed, n_clones;
static int resolve_deferred(const uint8_t *p, int depth) { // returns the number of deferred binds
int nd = 0;
for (;;) {
op_hdr h;
memcpy(&h, p, sizeof h);
if (h.size < 8 || (h.size & 7) || h.op >= OP_COUNT_)
rt_fatal("nvn: corrupt recording at %p while resolving deferred binds (op %u size %u)", (const void *)p,
h.op, h.size);
if (h.op == OP_END) return nd;
if (h.op == OP_JUMP) {
memcpy(&p, p + 8, 8);
continue;
}
if (h.op == OP_CALL) {
uint64_t t;
memcpy(&t, p + 8, 8);
if (depth >= 16) rt_fatal("nvn: deferred bind resolution: call depth over 16");
nd += resolve_deferred((const uint8_t *)(uintptr_t)t, depth + 1);
} else if (h.op == OP_BIND_TEX || h.op == OP_BIND_IMG || h.op == OP_BIND_SAMP) {
p_bind_handles *b = (p_bind_handles *)(uintptr_t)(p + 8);
if (b->src && b->n > 0 && b->n <= 32) {
nd++;
uint64_t cur[32];
memcpy(cur, (const void *)(uintptr_t)b->src, 8 * (size_t)b->n);
__atomic_fetch_add(&def_reads, 1, __ATOMIC_RELAXED);
if (memcmp(cur, b->h, 8 * (size_t)b->n)) {
if (__atomic_fetch_add(&def_changed, 1, __ATOMIC_RELAXED) < 6)
NVN_LOG("deferred bind read new handles at submit: stage %d slot %d, %#llx at record -> %#llx",
b->stage, b->first, (unsigned long long)b->h[0], (unsigned long long)cur[0]);
memcpy(b->h, cur, 8 * (size_t)b->n);
}
}
}
p += h.size;
}
}
void nvn_submit_prepare(const uint64_t *handles, int n) {
uint64_t now = __atomic_add_fetch(&nsubmits, 1, __ATOMIC_RELAXED);
for (int i = 0; i < n; i++) copy_touch(handles[i], now, "QueueSubmitCommands");
for (int i = 0; i < n; i++) resolve_deferred((const uint8_t *)(uintptr_t)handles[i], 0);
}
// Asynchronous replay. A copy a queued submission still reads keeps that submission's deferred binds: when it holds
// deferred binds (or is not known to hold none), this submission gets a clone, resolved here and freed after its replay.
void nvn_submit_prepare_async(const uint64_t *handles, uint64_t *out, int n) {
uint64_t now = __atomic_add_fetch(&nsubmits, 1, __ATOMIC_RELAXED);
uint64_t replayed = gfx_replayed_submits();
AcquireSRWLockExclusive(&copy_lock);
if (nzombies) zombie_sweep(replayed);
for (int i = 0; i < n; i++) {
copy_prefix *pf = copy_find(handles[i]);
if (!pf) {
ReleaseSRWLockExclusive(&copy_lock);
rt_fatal("nvn: QueueSubmitCommands: command handle %#llx is not a live recording (released or never recorded)",
(unsigned long long)handles[i]);
}
out[i] = handles[i];
if (pf->used_at > replayed && pf->used_at != now && pf->deferred != 1) {
size_t sz = sizeof *pf + pf->bytes;
copy_prefix *cl = malloc(sz);
if (!cl) rt_fatal("nvn: out of memory (recording clone, %zu bytes)", sz);
memcpy(cl, pf, sz);
cl->magic = 0; // not a live handle: the game never sees it
cl->used_at = now;
zombie_add(cl);
out[i] = (uintptr_t)(cl + 1);
__atomic_fetch_add(&n_clones, 1, __ATOMIC_RELAXED);
} else {
pf->used_at = now;
}
}
ReleaseSRWLockExclusive(&copy_lock);
for (int i = 0; i < n; i++) {
int nd = resolve_deferred((const uint8_t *)(uintptr_t)out[i], 0);
if (out[i] == handles[i]) ((copy_prefix *)(uintptr_t)out[i] - 1)->deferred = nd ? 2 : 1;
}
if (now % 7200 == 0)
rt_log("nvn: asynchronous replay at submit %llu: %llu recording clones so far, %u copies waiting for the replay",
(unsigned long long)now, (unsigned long long)__atomic_load_n(&n_clones, __ATOMIC_RELAXED), nzombies);
}
uint64_t nvn_submit_count(void) { return __atomic_load_n(&nsubmits, __ATOMIC_RELAXED); }
const char *nvn_op_name(uint32_t op) {
static const char *const names[] = {
"END", "JUMP", "BEGIN", "CALL", "SIGNAL_EVENT", "REPORT_COUNTER", "COPY_B2B", "UPDATE_UB", "SET_RT",
"CLEAR_COLOR", "CLEAR_DS", "VIEWPORTS", "SCISSOR", "DEPTH_RANGE", "BIND_PROGRAM", "BIND_STATE", "BIND_VATTR",
"BIND_VSTREAM", "BIND_VBUF", "BIND_UBO", "BIND_SSBO", "BIND_TEX", "BIND_SAMP", "STENCIL", "POLY_OFFSET", "DRAW",
"DRAW_IDX", "DISPATCH", "BARRIER", "SET_TEX_POOL", "SET_SAMP_POOL", "COPY_B2T", "COPY_STRIDE", "BLEND_COLOR",
"COPY_T2B", "BIND_IMG", "COPY_T2T", "RESET_COUNTER", "CLEAR_TEX", "DEPTH_CLAMP", "RENDER_ENABLE",
"RASTER_DISCARD", "RENDER_COND", "PRIM_RESTART", "DRAW_IDX_IND", "DISPATCH_IND", "CLEAR_BUF",
};
_Static_assert(sizeof names / sizeof *names == OP_COUNT_, "one name per op");
return op < OP_COUNT_ ? names[op] : "?";
}