mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
vk_exec_held_ready runs without sub_lock (vk_backend.c held_resume, submit_sync). It decided from the published copy
since 688b2506, but tested the wait with vk_exec_event_met, which compares at the copy point of the submission being
recorded (vk_exec_xfer.c ev_mem, ev_sync_seq). That point is set by whichever thread holds the lock, so a thread asking
while another one recorded tested "at sync" against that other submission's copies. Its question is whether held work
can go on in a submission of its own, which takes its copies later: vk_exec_event_met_now tests the value in memory now
or the signals replayed so far. Under the lock nothing changes: evo_resume and cont_ready keep the copy-point test, and
vk_exec_record asks right after its own copy point, where the two agree.
evo_pending and vk_exec_submit_seq are written under the lock and were read as plain values by vk_exec_held,
vk_exec_held_ready and the held present's since and PH_MAX checks; they are now stored and loaded as atomics
(vk_exec_submit_seq_now), so the compiler cannot keep a stale copy in an unlocked reader.
Not reproduced before the change. Checks that passed: pass_cc_check on every runtime file (a header changed), no new
-Wall -Wextra warnings in vk_exec.c, vk_exec_xfer.c and vk_backend.c, evo_peek_check, target_frames_check and
oq_hold_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1453 lines
78 KiB
C
1453 lines
78 KiB
C
// Transfers of the Vulkan op-stream replay: buffer and texture copies, texture
|
|
// clears, event signals, counter reports and conditional rendering. vk_exec.c calls vk_exec_op_xfer for these ops.
|
|
//
|
|
// Buffers are guest memory: vk_mem_resolve maps an address to the VkBuffer behind its pool, textures are the vk_texture.c images
|
|
// (linear texel layout, GENERAL layout; block-linear storage is only converted when a mirrored texture is uploaded). So a
|
|
// copy is one Vulkan transfer command and the pitch of a buffer side comes from the payload (OP_COPY_STRIDE or the
|
|
// tight row). Every copy ends the open pass and starts with a full barrier. Destinations: an image is marked
|
|
// GPU-written at record time; a CPU-visible buffer range is reported to vk_gpu_written once the GPU is done (write
|
|
// watch does not see GPU writes, textures stored there re-upload).
|
|
//
|
|
// Events and counters are CPU-visible side effects. Their values go into guest memory on the completion thread through
|
|
// vk_completion_add, in submit order, as gfx_null.c does: event WRITE / ADD, counter {0, timestamp} for every type but
|
|
// samples passed. A timestamp report puts 0 in the counter word: NX2NAVI gates its clears on two such words being unequal.
|
|
//
|
|
// Samples passed (NVN_COUNTER_TYPE_SAMPLES_PASSED) is a running count on the queue: RESET_COUNTER zeroes it, every
|
|
// draw after it adds the samples that passed the depth and stencil tests, REPORT_COUNTER writes the running value.
|
|
// NX2NAVI resets and reports it 3 to 8 times a frame in the field and its CPU reads the results back (f_70010c3298); a 0
|
|
// there culls what the query tested. The replay counts with Vulkan occlusion queries ("occlusion counts" below).
|
|
//
|
|
// Conditional rendering is a CPU-side skip, not VK_EXT_conditional_rendering: the extension compares one 32-bit word
|
|
// with zero, NVN compares the counter words at addr and addr + 16. The condition is evaluated when the op is recorded
|
|
// from guest memory; a counter reported by an earlier submission that is not done yet counts as 0 if it is a timestamp,
|
|
// as unknown if it is samples passed. An unknown count renders: the report usually sits in the command buffer being
|
|
// recorded, so its value cannot be waited for, and the word in memory still holds an older query's count. Rendering
|
|
// costs at most the work the condition would have saved; in the field
|
|
// NX2NAVI conditions only on timestamp reports. The condition sets VkExec.render_disabled, which vk_exec.c honours for
|
|
// clears and vk_exec_draw.c for draws and dispatches. A condition that cannot be read (address outside every pool)
|
|
// renders.
|
|
// Copies and CLEAR_TEX are not conditional.
|
|
//
|
|
// Contains code Copyright (c) 2026 jhaizhou, Copyright (c) 2026 Peiikai (MIT; see THIRD_PARTY_NOTICES.md).
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "cfg.h"
|
|
#include "platform/platform.h"
|
|
#include "host.h"
|
|
#include "nvn_api.h"
|
|
#include "nvn_tex.h"
|
|
#include "tex.h"
|
|
#include "vk.h"
|
|
#include "vk_exec.h"
|
|
#include "vk_oq_hold.h"
|
|
|
|
enum { W_FORMAT, W_REGION, W_BUFFER, W_OVERLAP, W_COPY, W_CLEAR, W_QUERY, W_COUNT };
|
|
|
|
static void warn_once(int key, const char *fmt, ...) {
|
|
static volatile int32_t seen[W_COUNT];
|
|
if (__atomic_exchange_n(&seen[key], 1, __ATOMIC_RELAXED)) return;
|
|
char msg[256];
|
|
va_list ap;
|
|
va_start(ap, fmt);
|
|
vsnprintf(msg, sizeof msg, fmt, ap);
|
|
va_end(ap);
|
|
rt_log("vk: %s (logged once)", msg);
|
|
}
|
|
|
|
static uint32_t max1(uint32_t v) { return v ? v : 1; }
|
|
|
|
// ---- completion callbacks (completion thread)
|
|
|
|
typedef struct { uint64_t addr, size; int back; } Range; // back: written into a VRAM mirror (vk_mem_mirrored)
|
|
|
|
// What the GPU work of the submission being recorded hands to the CPU at its completion without a fault to fetch it:
|
|
// event values, counter and occlusion reports, GPU writes of memory no VRAM mirror holds (vk_sync_recorded_wait waits
|
|
// for the newest such submission). Write-backs of the mirrors note it in vk_memory.c; the lazily written ones fetch on
|
|
// the CPU's first access.
|
|
static volatile int32_t ex_results; // VK_RES_* bits
|
|
static int tail_results; // those noted while the submission's tail records (vk_exec.c "the early tail")
|
|
int vk_exec_tail_results(void) { int r = tail_results; tail_results = 0; return r; }
|
|
void vk_exec_note_results(int kind) {
|
|
if (vk_exec_tail()) tail_results |= kind;
|
|
__atomic_fetch_or(&ex_results, kind, __ATOMIC_RELAXED);
|
|
vk_gpu_passes_result(kind); // NX2NAVI_GPU_PASSES: where in the submission it is done
|
|
}
|
|
int vk_exec_take_results(void) { return __atomic_exchange_n(&ex_results, 0, __ATOMIC_RELAXED); }
|
|
|
|
// An inline uniform update in a submission's tail (vk_exec_draw.c op_update_ub, vk_exec.c "the early tail"): the GPU
|
|
// copies the bytes into the pool's mirror where the stream has the update, and guest memory gets them here, with the
|
|
// results the game reads, in place of a write-back.
|
|
typedef struct { uint64_t addr; uint32_t size; uint8_t data[]; } UbWrite;
|
|
static void ub_write_cb(void *arg) {
|
|
UbWrite *w = arg;
|
|
memcpy((void *)(uintptr_t)w->addr, w->data, w->size);
|
|
free(w);
|
|
}
|
|
void vk_exec_tail_ub(uint64_t fence, uint64_t addr, const void *data, uint32_t size) {
|
|
UbWrite *w = malloc(sizeof *w + size);
|
|
if (!w) rt_fatal("vk: out of memory");
|
|
w->addr = addr, w->size = size;
|
|
memcpy(w->data, data, size);
|
|
vk_completion_add_result(fence, ub_write_cb, w);
|
|
}
|
|
|
|
static void guest_written_cb(void *arg) {
|
|
Range *r = arg;
|
|
if (r->back) vk_gpu_written_back(r->addr, r->size);
|
|
else vk_gpu_written(r->addr, r->size);
|
|
free(r);
|
|
}
|
|
|
|
// The GPU wrote [addr, addr+size) of CPU-visible memory: tell the texture cache once the fence is reached.
|
|
static void note_gpu_write(VkExec *ex, uint64_t addr, uint64_t size) {
|
|
vk_usnap_drop(addr, size);
|
|
if (nvn_gpu_only(addr)) return;
|
|
Range *r = malloc(sizeof *r);
|
|
if (!r) rt_fatal("vk: out of memory");
|
|
*r = (Range){addr, size, vk_mem_mirrored(addr, size)};
|
|
if (!r->back) vk_exec_note_results(VK_RES_WRITE);
|
|
vk_mem_gpu_pending(addr, size);
|
|
vk_completion_add(ex->fence, guest_written_cb, r);
|
|
}
|
|
|
|
typedef struct { volatile uint32_t *val; uint32_t mode, value; } EventWrite;
|
|
|
|
// The side effects below that ran after a waiter on the executed timeline had gone on (vk_sync.c vk_sync_effect_late):
|
|
// the game was already past the wait after its submit when these reached its memory.
|
|
static volatile uint64_t late_events, late_counters, late_counts;
|
|
|
|
void vk_exec_late_stats(uint64_t *events, uint64_t *counters, uint64_t *counts) {
|
|
*events = __atomic_exchange_n(&late_events, 0, __ATOMIC_RELAXED);
|
|
*counters = __atomic_exchange_n(&late_counters, 0, __ATOMIC_RELAXED);
|
|
*counts = __atomic_exchange_n(&late_counts, 0, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
static void event_cb(void *arg) {
|
|
EventWrite *e = arg;
|
|
if (vk_sync_effect_late()) __atomic_fetch_add(&late_events, 1, __ATOMIC_RELAXED);
|
|
if (e->mode == NVN_EVENT_SIGNAL_MODE_ADD) __atomic_fetch_add(e->val, e->value, __ATOMIC_RELEASE);
|
|
else __atomic_store_n(e->val, e->value, __ATOMIC_RELEASE);
|
|
free(e);
|
|
}
|
|
|
|
// NVNcounterData = {counter, timestamp}; timestamps are Tegra 19.2 MHz ticks (the base of GetSystemTick).
|
|
// keep: the 4-byte quarters of the record the write leaves as they are (bit n: bytes 4n to 4n + 3), 0 none.
|
|
// ahead_ns: the timestamp is that much past now (a tail timestamp the GPU has not reached yet, counter_cb), else 0.
|
|
static void write_counter(uint64_t addr, uint64_t counter, unsigned keep, uint64_t ahead_ns) {
|
|
uint64_t rec[2] = {counter, (uint64_t)((unsigned __int128)(kern_now_ns() + ahead_ns) * 12 / 625)};
|
|
if (!keep) {
|
|
memcpy((void *)(uintptr_t)addr, rec, sizeof rec);
|
|
return;
|
|
}
|
|
for (unsigned q = 0; q < 4; q++)
|
|
if (!(keep >> q & 1)) memcpy((uint8_t *)(uintptr_t)addr + 4 * q, (const uint8_t *)rec + 4 * q, 4);
|
|
}
|
|
|
|
// Reports other than samples passed (timestamps; the statistics counters are not modelled) write a counter of 0. The
|
|
// counter word matters: Ocarina writes {1, 0} into a record from the CPU, reports TIMESTAMP_TOP into it and gates
|
|
// clears on it differing from a zero record (mode 1), clears that must not run.
|
|
static void report_write(uint64_t addr, uint64_t seq, uint64_t counter, uint64_t ahead_ns); // below, with the pending table
|
|
|
|
// tail: the submission's value for a timestamp reported in its tail (vk_exec.c "the early tail"), else 0
|
|
typedef struct { uint64_t addr, seq, tail; } CounterWrite;
|
|
|
|
static void counter_cb(void *arg) {
|
|
CounterWrite *w = arg;
|
|
if (vk_sync_effect_late()) __atomic_fetch_add(&late_counters, 1, __ATOMIC_RELAXED);
|
|
// A timestamp of the tail, written while the GPU still runs the tail: the time the tail is expected to end at
|
|
report_write(w->addr, w->seq, 0, w->tail ? vk_sync_tail_pending_ns(w->tail) : 0);
|
|
free(w);
|
|
}
|
|
|
|
// Reports queued by the replay that the GPU has not finished. The condition of RENDER_COND reads a timestamp one as 0
|
|
// and a samples-passed one as unknown.
|
|
// The newest report per address, in a table probed over PENDING_PROBE slots from the address's hash: Ocarina issues
|
|
// hundreds of occlusion reports a frame and conditions draws on them in the same frame, so a ring of the last 256
|
|
// reports had dropped the report a condition referred to, which then read an older query's count (often 0) and culled
|
|
// visible ground and trees. It also rotates through tens of thousands of record addresses, so an entry whose value is
|
|
// in memory (written >= seq: a condition reads memory anyway) gives its slot to a new address; a table that only grew
|
|
// filled up after a minute and conditions on the newer records read the CPU's 1 again.
|
|
#define PENDING (1u << 16)
|
|
#define PENDING_PROBE 32
|
|
// seq: the newest report to addr; written: the newest whose value the completion thread has put in guest memory. A
|
|
// report counts as pending until then, not until its fence passes: the callback runs after the fence, and in between
|
|
// the record still held what the CPU wrote (Ocarina re-arms its condition records with 1 for the next frame), so a
|
|
// condition read 1 where the GPU had written 0, a depth clear ran after the depth copy and frames showed only sky.
|
|
// frame: the present count (vk_exec_cond_frame_end, in replay order) when the report was replayed. The game arms its
|
|
// condition records from the CPU between frames, so within a frame a timestamp report decides the record's value even
|
|
// after it reached memory: Ocarina's CPU, running a frame ahead, had re-armed a record with 1 by the time a later depth
|
|
// clear of the frame read it, the clear ran after the depth copy and the frame showed only sky (white flashes).
|
|
// clr_seq, clr_mask: the quarters of the record a small buffer clear filled after the report clr_seq, the newest
|
|
// replayed before the clear ("small buffer clears over counter records" below). Written by the replay thread under
|
|
// clr_lock; the report writes read them under it.
|
|
typedef struct { uint64_t addr, seq, frame; volatile uint64_t written; int unknown, type; uint64_t clr_seq; uint8_t clr_mask; } Pending;
|
|
static Pending pending[PENDING];
|
|
static uint64_t report_seq;
|
|
static uint64_t cond_frame;
|
|
|
|
// Per-frame counts of conditional work (NX2NAVI_LOG_RCOND=1, vk_exec_cond_frame_end logs them): clears and
|
|
// draws issued under a render condition, run or skipped.
|
|
unsigned vk_cond_clears_run, vk_cond_clears_skip, vk_cond_draws_skip, vk_cond_depth_run;
|
|
|
|
void vk_exec_cond_frame_end(uint64_t frame) {
|
|
static int on = -1;
|
|
if (on < 0) on = rt_knob("LOG_RCOND", "0");
|
|
if (on && (vk_cond_clears_run || vk_cond_clears_skip || vk_cond_draws_skip))
|
|
rt_log("vk: frame %llu conditions: clears run %u skipped %u, draws skipped %u, depth clears run %u",
|
|
(unsigned long long)frame, vk_cond_clears_run, vk_cond_clears_skip, vk_cond_draws_skip, vk_cond_depth_run);
|
|
vk_cond_clears_run = vk_cond_clears_skip = vk_cond_draws_skip = vk_cond_depth_run = 0;
|
|
if (on == 2) rt_log("SEQ present %llu", (unsigned long long)frame);
|
|
cond_frame++;
|
|
}
|
|
|
|
static Pending *pending_slot(uint64_t addr, int insert);
|
|
|
|
// Completion thread: the value of report seq to addr is in guest memory.
|
|
static void pending_written(uint64_t addr, uint64_t seq) {
|
|
Pending *e = pending_slot(addr, 0);
|
|
if (!e) return;
|
|
uint64_t w = __atomic_load_n(&e->written, __ATOMIC_RELAXED);
|
|
while (w < seq && !__atomic_compare_exchange_n(&e->written, &w, seq, 0, __ATOMIC_RELEASE, __ATOMIC_RELAXED)) {}
|
|
}
|
|
|
|
// Lookups run on the replay thread and the completion thread; only the replay thread inserts.
|
|
static Pending *pending_slot(uint64_t addr, int insert) {
|
|
if (!addr) return NULL;
|
|
uint32_t i = (uint32_t)((addr * 0x9E3779B97F4A7C15ull) >> 48) & (PENDING - 1);
|
|
Pending *free_ = NULL, *victim = NULL;
|
|
for (unsigned n = 0; n < PENDING_PROBE; n++, i = (i + 1) & (PENDING - 1)) {
|
|
Pending *e = &pending[i];
|
|
uint64_t a = __atomic_load_n(&e->addr, __ATOMIC_ACQUIRE);
|
|
if (a == addr) return e;
|
|
if (!insert) continue;
|
|
if (!a) { if (!free_) free_ = e; continue; }
|
|
if (__atomic_load_n(&e->written, __ATOMIC_ACQUIRE) >= e->seq && e->frame != cond_frame && (!victim || e->seq < victim->seq))
|
|
victim = e; // done, and not from this frame (whose reports still decide their records)
|
|
}
|
|
Pending *e = free_ ? free_ : victim;
|
|
if (!e) return NULL; // every probed entry still has a report in flight
|
|
e->seq = 0, e->frame = 0, e->unknown = 0, e->type = -1, e->clr_seq = 0, e->clr_mask = 0;
|
|
__atomic_store_n(&e->written, 0, __ATOMIC_RELEASE);
|
|
__atomic_store_n(&e->addr, addr, __ATOMIC_RELEASE);
|
|
return e;
|
|
}
|
|
|
|
// ---- small buffer clears over counter records (NX2NAVI_CLEAR_SMALL_CPU, default 1; 0: the GPU's fill alone)
|
|
//
|
|
// A ClearBuffer or ReportValue is a fill the GPU makes where it stands in the stream (clear_buf): guest memory has the
|
|
// bytes once the GPU got there, through the imported pool, a mirror's twin or the mirror's write-back. A counter
|
|
// report reaches guest memory another way, as a CPU write after its submission's fence (report_write), and a render
|
|
// condition reads the record when it is replayed (counter_word). For a record that gets both, the two ways do not
|
|
// keep the order of the stream:
|
|
// - a report followed by a clear ends as the report: its write comes after the fence, the fill ran before it. When
|
|
// the clear is in a later submission the completion thread races the GPU for the last word;
|
|
// - a condition replayed after the clear reads what memory held before the GPU ran the fill, or the 0 of a report
|
|
// that the clear has since overwritten.
|
|
// So a fill of at most CLEAR_SMALL bytes is also applied here, at its place in the replay, to the records it covers
|
|
// that the pending table holds, which are the records a counter report went into: the covered bytes get the value in
|
|
// guest memory at once, the entry notes which quarters of the record were filled and after which report, reports up
|
|
// to that one leave those quarters alone when their write comes, and a condition takes them from memory. A report
|
|
// replayed after the clear writes the whole record as before. The GPU's fill is recorded all the same; it writes the
|
|
// value a second time and is what a shader or a mirror sees.
|
|
// Only those records, not every small fill: the write here is ahead of the GPU, and the GPU reads guest memory too,
|
|
// at once in an imported pool and through the next upload of the page the write watch then lists in a mirrored one.
|
|
// A draw count or an atomic counter a shader adds to, reset by such a fill, would read the reset before the passes
|
|
// that still count on the old value have run. Nothing on the GPU reads a counter record (conditions are decided on
|
|
// the CPU), a report's own write already lands on the same page, and whatever this write puts a value over, the GPU's
|
|
// fill puts the same value over later.
|
|
// The counts (vk_exec_clear_stats) are kept whatever the setting is: the small fills into CPU-visible memory, how
|
|
// many covered such a record, and the reports that went into bytes one of the last CLR_RING fills had covered.
|
|
#define CLEAR_SMALL 256
|
|
#define CLR_RING 8
|
|
static PlatMutex clr_lock = PLAT_MUTEX_INIT; // a report's write against a clear's note and write
|
|
static volatile int32_t clr_any; // an entry holds a note: the report writes take clr_lock from here on
|
|
static int clr_cpu = -1; // the setting
|
|
// A flag per 4 KiB page a counter report went into, by a hash of the page (a page that shares another's flag only
|
|
// costs a clear its table lookups). Replay thread.
|
|
static uint8_t rep_pages[1u << 16];
|
|
static struct {
|
|
uint64_t n, bytes, hit, recs, after, frame0; // since the last line: fills, their bytes, fills over a record, such
|
|
// records, reports into filled bytes; the frame of the last line
|
|
volatile uint64_t kept; // report writes that left filled quarters alone
|
|
uint64_t addr; // the last fill
|
|
uint32_t size, value;
|
|
struct { uint64_t lo, hi; } ring[CLR_RING]; // the last fills, and the range that holds them all
|
|
uint64_t lo, hi;
|
|
int next;
|
|
} CS;
|
|
|
|
static uint8_t *rep_page(uint64_t addr) { return &rep_pages[(uint32_t)(((addr >> 12) * 0x9E3779B97F4A7C15ull) >> 48)]; }
|
|
|
|
// Replay thread, at a report into the record at addr.
|
|
static void clear_note_report(uint64_t addr) {
|
|
*rep_page(addr) = 1, *rep_page(addr + sizeof(NVNcounterData) - 1) = 1;
|
|
if (addr >= CS.hi || addr + sizeof(NVNcounterData) <= CS.lo) return;
|
|
for (int i = 0; i < CLR_RING; i++)
|
|
if (addr < CS.ring[i].hi && addr + sizeof(NVNcounterData) > CS.ring[i].lo) { CS.after++; return; }
|
|
}
|
|
|
|
// Replay thread: the fill of [a, a + n) with value, 4-byte aligned, backed and CPU-visible, at its place in the stream.
|
|
static void clear_small(uint64_t a, uint64_t n, uint32_t value) {
|
|
if (clr_cpu < 0) {
|
|
clr_cpu = cfg_bool("NX2NAVI_CLEAR_SMALL_CPU");
|
|
rt_log("vk: buffer clears of at most %d bytes %s (NX2NAVI_CLEAR_SMALL_CPU)", CLEAR_SMALL,
|
|
clr_cpu ? "are also applied at their place in the replay to the counter records they cover"
|
|
: "are the GPU's fill alone, also over counter records");
|
|
}
|
|
CS.n++, CS.bytes += n, CS.addr = a, CS.size = (uint32_t)n, CS.value = value;
|
|
CS.ring[CS.next].lo = a, CS.ring[CS.next].hi = a + n, CS.next = (CS.next + 1) % CLR_RING;
|
|
CS.lo = UINT64_MAX, CS.hi = 0;
|
|
for (int i = 0; i < CLR_RING; i++) {
|
|
if (CS.ring[i].lo == CS.ring[i].hi) continue;
|
|
if (CS.ring[i].lo < CS.lo) CS.lo = CS.ring[i].lo;
|
|
if (CS.ring[i].hi > CS.hi) CS.hi = CS.ring[i].hi;
|
|
}
|
|
if (!*rep_page(a) && !*rep_page(a + n - 1)) return; // no report ever went into these pages
|
|
int hit = 0;
|
|
for (uint64_t k = a - 12; k < a + n; k += 4) { // every record address whose 16 bytes reach into the fill
|
|
Pending *e = pending_slot(k, 0);
|
|
if (!e) continue;
|
|
unsigned m = 0;
|
|
for (unsigned q = 0; q < 4; q++)
|
|
if (k + 4 * q >= a && k + 4 * q < a + n) m |= 1u << q;
|
|
hit = 1, CS.recs++;
|
|
if (!clr_cpu) continue;
|
|
__atomic_store_n(&clr_any, 1, __ATOMIC_RELEASE);
|
|
plat_mutex_lock(&clr_lock);
|
|
// the quarters filled since the newest report: an earlier fill's stay when no report came in between
|
|
e->clr_mask = (uint8_t)(e->clr_seq < e->seq ? m : e->clr_mask | m);
|
|
e->clr_seq = report_seq; // every report replayed so far
|
|
for (unsigned q = 0; q < 4; q++)
|
|
if (m >> q & 1) memcpy((void *)(uintptr_t)(k + 4 * q), &value, 4);
|
|
plat_mutex_unlock(&clr_lock);
|
|
}
|
|
CS.hit += (uint64_t)hit;
|
|
}
|
|
|
|
// The quarters of e's record that a small clear filled after its newest report: memory holds them.
|
|
static unsigned clear_quarters(const Pending *e) { return e->clr_seq >= e->seq ? e->clr_mask : 0; }
|
|
|
|
// Completion thread, or the thread whose wait runs the results: report seq to addr goes into guest memory, but for
|
|
// the quarters a small clear replayed after it has filled.
|
|
static void report_write(uint64_t addr, uint64_t seq, uint64_t counter, uint64_t ahead_ns) {
|
|
int lk = __atomic_load_n(&clr_any, __ATOMIC_ACQUIRE);
|
|
unsigned keep = 0;
|
|
if (lk) {
|
|
plat_mutex_lock(&clr_lock);
|
|
const Pending *e = pending_slot(addr, 0);
|
|
if (e && seq <= e->clr_seq) keep = e->clr_mask;
|
|
if (keep) __atomic_fetch_add(&CS.kept, 1, __ATOMIC_RELAXED);
|
|
}
|
|
write_counter(addr, counter, keep, ahead_ns);
|
|
if (lk) plat_mutex_unlock(&clr_lock);
|
|
pending_written(addr, seq);
|
|
}
|
|
|
|
// The counts since the last call as a clause for a periodic log line (vk_usnap.c summary). Replay thread.
|
|
void vk_exec_clear_stats(char *out, size_t n) {
|
|
uint64_t kept = __atomic_exchange_n(&CS.kept, 0, __ATOMIC_RELAXED);
|
|
snprintf(out, n, "buffer clears of at most %d bytes into CPU-visible memory: %llu in %llu frames, %llu bytes (the last "
|
|
"%u bytes of %#x at %#llx), %llu of them over a record a counter report went into before (%llu records), "
|
|
"%llu counter reports into bytes one of the last %d had cleared, %llu report writes left cleared bytes alone",
|
|
CLEAR_SMALL, (unsigned long long)CS.n, (unsigned long long)(cond_frame - CS.frame0),
|
|
(unsigned long long)CS.bytes, CS.size, CS.value, (unsigned long long)CS.addr, (unsigned long long)CS.hit,
|
|
(unsigned long long)CS.recs, (unsigned long long)CS.after, CLR_RING, (unsigned long long)kept);
|
|
CS.n = CS.bytes = CS.hit = CS.recs = CS.after = 0, CS.frame0 = cond_frame;
|
|
}
|
|
|
|
// ---- occlusion counts (samples passed)
|
|
//
|
|
// After a SAMPLES_PASSED reset, the first draw of a pass begins an occlusion query (precise when the device has
|
|
// occlusionQueryPrecise); the query ends with the pass, or at the next report or reset of the counter, and the next
|
|
// draw begins another. A Vulkan query begun inside a render pass must end inside it. NX2NAVI always resets, draws the
|
|
// tested proxies, reports, so counting stops at a report and restarts at the next reset: queries cover only the draws
|
|
// between the two, not every pass of the frame. Once the game reports twice without a reset in between, counting stays
|
|
// on for good (cumulative NVN semantics; that one report misses the draws since the previous one). The queries closed since the last
|
|
// report or reset form a segment, handed to the completion thread with that report or reset: in submit order it sums
|
|
// the segment's results into the running count (a reset drops the sum and zeroes the count) and writes the count of a
|
|
// report into guest memory. Slots come from one ring; the completion thread reads and host-resets them
|
|
// (hostQueryReset, Vulkan 1.2), so the replay records no reset commands. A full ring (no slot read back yet) leaves the
|
|
// draw uncounted and the segment's count at least 1, as does a query still unavailable after its fence. Without hostQueryReset the counts stay 0.
|
|
// The ring holds about four frames of Hyrule Field's queries (about 3,500 a frame): the replay workers' chunks take
|
|
// blocks of slots sized to their draws (vk_exec_xfer_slots), more than they use, and with 4096 the ring came round to
|
|
// slots the completion thread had not reset yet, leaving draws uncounted.
|
|
#define OQ_N 16384
|
|
#define LOG_ONCE(...) do { static volatile char once_; if (!__atomic_exchange_n(&once_, 1, __ATOMIC_RELAXED)) rt_log(__VA_ARGS__); } while (0)
|
|
static VkQueryPool oq_pool;
|
|
static int oq_state; // 0 not set up, 1 counting available, -1 unavailable
|
|
static VkQueryControlFlags oq_flags;
|
|
static volatile uint8_t oq_busy[OQ_N]; // from begin until the completion thread read and reset the slot
|
|
static uint32_t oq_next; // replay: next ring slot
|
|
static int oq_counting; // replay: draws are counted (a reset since the last report, or oq_keep)
|
|
static int oq_reported; // replay: a samples-passed report since the last reset
|
|
static int oq_keep; // replay: the game reported twice without a reset: count every draw
|
|
static int oq_open = -1; // replay: the slot whose query is active in the open pass
|
|
static uint32_t *oq_seg; // replay: slots closed since the last report or reset
|
|
static uint32_t oq_nseg, oq_capseg;
|
|
static int oq_lost; // replay: the segment missed a draw (ring full)
|
|
static uint64_t oq_count; // completion thread: samples since the last reset
|
|
|
|
typedef struct { uint64_t addr, seq, fence; int reset; volatile int32_t lost; uint32_t n, cap; uint32_t *q; } OqSegment;
|
|
|
|
// The replay workers (vk_exec.c "parallel groups") record the draws of one group at once. The front pass replays a
|
|
// group's samples-passed resets and reports in the order of the stream before the workers record (vk_exec_xfer_front):
|
|
// each makes its segment, queued for the completion thread in that order, and they are listed in GS. A worker follows
|
|
// the counting as the stream does from its chunk's first op (vk_exec_xfer_chunk_state): XW.counting and the segment its
|
|
// closed queries belong to, the next one at or after its position in the stream (XW.seg; past the group's last, the
|
|
// pending list oq_seg, which a later reset or report takes). It keeps the query open in its own pass (XW.open), takes
|
|
// ring slots from its chunk's block and keeps the slots it closes with their segment in its chunk's list (CP), which
|
|
// vk_exec_xfer_group_merge puts into the segments once the group is done, chunk after chunk: under a lock per slot, the
|
|
// workers waited for each other at every query. XW.cond: the decisions of its chunk's render conditions, made by the
|
|
// front pass in the order of the stream.
|
|
// The blocks (vk_exec_xfer_slots): the front pass gives each chunk as many slots as it has draws that count samples and
|
|
// render, one after the other, so the slots follow the stream as the replay's own do and the completion thread reads
|
|
// and resets them in long runs (oq_take). Taken from the ring at once, the slots of a group's chunks interleaved: four
|
|
// times the result calls and a reset per slot, 3.5 ms more of the wait after a frame's first submission (RX 7900 XT,
|
|
// Hyrule Field, 3 workers). A chunk that needs more than its block takes them from the ring.
|
|
static _Thread_local struct {
|
|
int on, open, counting, reported, keep, chunk, g;
|
|
uint32_t seg, qnext, qend;
|
|
const uint8_t *cond;
|
|
uint32_t ncond, i;
|
|
} XW = {.open = -1};
|
|
// GS: per group slot (vk_exec.c LIVE_GROUPS), the front pass's segments of the group. tail: with NX2NAVI_REPLAY_PIPE
|
|
// the submitting thread replays on past the group before its chunks are done: the first segment made after the
|
|
// group's front pass (tail_wait), by the next group's front pass or the replay, takes the queries the chunks close
|
|
// after the group's last report or reset, which in the stream come before that one's own.
|
|
#define GS_GROUPS 4
|
|
static struct { int tail_wait; OqSegment **s, *tail; uint32_t n, cap; } GS[GS_GROUPS];
|
|
static int gs_on = -1; // the group slot whose front pass runs
|
|
#define CP_CHUNKS 32
|
|
static struct { uint32_t *seg, *q, n, cap; } CP[GS_GROUPS][CP_CHUNKS]; // per chunk: closed slots and their segment
|
|
|
|
void vk_exec_xfer_group(int group) { // a group's front pass begins (its slot), or it ended (-1): the list stays
|
|
if (group >= GS_GROUPS) rt_fatal("vk: parallel group slot %d of %d", group, GS_GROUPS);
|
|
if (group >= 0) GS[group].n = 0, GS[group].tail_wait = 0, GS[group].tail = NULL;
|
|
else if (gs_on >= 0) GS[gs_on].tail_wait = 1; // until vk_exec_xfer_group_merge
|
|
gs_on = group;
|
|
}
|
|
|
|
int vk_exec_xfer_counting(void) { return oq_counting; }
|
|
|
|
uint32_t vk_exec_xfer_slots(uint32_t n) { // the workers of a group the submitting thread went on past take some too
|
|
return __atomic_fetch_add(&oq_next, n, __ATOMIC_RELAXED);
|
|
}
|
|
|
|
uint64_t vk_exec_xfer_chunk_state(void) {
|
|
return (uint64_t)GS[gs_on].n << 8 | (uint64_t)(oq_counting & 1) | (uint64_t)(oq_reported & 1) << 1 | (uint64_t)(oq_keep & 1) << 2;
|
|
}
|
|
|
|
void vk_exec_xfer_worker(int on, const uint8_t *cond, uint32_t ncond, uint64_t state, uint32_t slot0, uint32_t nslots,
|
|
int chunk, int group) {
|
|
if (chunk < 0 || chunk >= CP_CHUNKS) rt_fatal("vk: replay worker chunk %d of at most %d", chunk, CP_CHUNKS);
|
|
if (group < 0 || group >= GS_GROUPS) rt_fatal("vk: parallel group slot %d of %d", group, GS_GROUPS);
|
|
XW.on = on, XW.open = -1, XW.cond = cond, XW.ncond = ncond, XW.i = 0, XW.chunk = chunk, XW.g = group;
|
|
XW.qnext = slot0, XW.qend = slot0 + nslots;
|
|
XW.counting = (int)(state & 1), XW.reported = (int)(state >> 1 & 1), XW.keep = (int)(state >> 2 & 1);
|
|
XW.seg = (uint32_t)(state >> 8);
|
|
}
|
|
|
|
// Adds closed slot q to the segment a worker's queries belong to now, or the pending list.
|
|
static void seg_add(uint32_t q) {
|
|
__typeof__(CP[0][0]) *l = &CP[XW.g][XW.chunk];
|
|
if (l->n == l->cap) {
|
|
l->cap = l->cap ? l->cap * 2 : 256;
|
|
if (!(l->seg = realloc(l->seg, sizeof *l->seg * l->cap)) || !(l->q = realloc(l->q, sizeof *l->q * l->cap)))
|
|
rt_fatal("vk: out of memory");
|
|
}
|
|
l->seg[l->n] = XW.seg, l->q[l->n++] = q;
|
|
}
|
|
|
|
void vk_exec_xfer_group_merge(int group, int nchunks) {
|
|
__typeof__(GS[0]) *gs = &GS[group];
|
|
for (int c = 0; c < nchunks && c < CP_CHUNKS; c++) {
|
|
__typeof__(CP[0][0]) *l = &CP[group][c];
|
|
for (uint32_t i = 0; i < l->n; i++) {
|
|
uint32_t q = l->q[i];
|
|
if (l->seg[i] < gs->n) {
|
|
OqSegment *s = gs->s[l->seg[i]];
|
|
if (s->n == s->cap) {
|
|
s->cap = s->cap ? s->cap * 2 : 16;
|
|
if (!(s->q = realloc(s->q, sizeof *s->q * s->cap))) rt_fatal("vk: out of memory");
|
|
}
|
|
s->q[s->n++] = q;
|
|
} else if (gs->tail) {
|
|
OqSegment *s = gs->tail;
|
|
if (s->n == s->cap) {
|
|
s->cap = s->cap ? s->cap * 2 : 16;
|
|
if (!(s->q = realloc(s->q, sizeof *s->q * s->cap))) rt_fatal("vk: out of memory");
|
|
}
|
|
s->q[s->n++] = q;
|
|
} else {
|
|
if (oq_nseg == oq_capseg) {
|
|
oq_capseg = oq_capseg ? oq_capseg * 2 : 64;
|
|
if (!(oq_seg = realloc(oq_seg, sizeof *oq_seg * oq_capseg))) rt_fatal("vk: out of memory");
|
|
}
|
|
oq_seg[oq_nseg++] = q;
|
|
}
|
|
}
|
|
l->n = 0;
|
|
}
|
|
gs->tail_wait = 0, gs->tail = NULL;
|
|
}
|
|
|
|
static int oq_ready(void) {
|
|
if (oq_state) return oq_state > 0;
|
|
VkCtx *v = vk_ctx();
|
|
// vk_device.c enables every feature the device reports, so supported means enabled.
|
|
VkPhysicalDeviceVulkan12Features f12 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_1_2_FEATURES};
|
|
VkPhysicalDeviceFeatures2 f2 = {.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2, .pNext = &f12};
|
|
vkGetPhysicalDeviceFeatures2(v->phys, &f2);
|
|
oq_state = -1;
|
|
if (!f12.hostQueryReset) {
|
|
rt_log("vk: no hostQueryReset: samples-passed counters report 0");
|
|
return 0;
|
|
}
|
|
VkQueryPoolCreateInfo qi = {.sType = VK_STRUCTURE_TYPE_QUERY_POOL_CREATE_INFO, .queryType = VK_QUERY_TYPE_OCCLUSION,
|
|
.queryCount = OQ_N};
|
|
VkResult r = vkCreateQueryPool(v->device, &qi, NULL, &oq_pool);
|
|
if (r != VK_SUCCESS) {
|
|
rt_log("vk: occlusion query pool creation failed (%d): samples-passed counters report 0", (int)r);
|
|
return 0;
|
|
}
|
|
vkResetQueryPool(v->device, oq_pool, 0, OQ_N);
|
|
oq_flags = f2.features.occlusionQueryPrecise ? VK_QUERY_CONTROL_PRECISE_BIT : 0;
|
|
oq_state = 1;
|
|
return 1;
|
|
}
|
|
|
|
static void oq_close(VkExec *ex) {
|
|
int *open = XW.on ? &XW.open : &oq_open;
|
|
if (*open < 0) return;
|
|
vkCmdEndQuery(ex->cb, oq_pool, (uint32_t)*open);
|
|
ex->recorded++;
|
|
if (XW.on) seg_add((uint32_t)*open);
|
|
else {
|
|
if (oq_nseg == oq_capseg) {
|
|
oq_capseg = oq_capseg ? oq_capseg * 2 : 64;
|
|
oq_seg = realloc(oq_seg, sizeof *oq_seg * oq_capseg);
|
|
if (!oq_seg) rt_fatal("vk: out of memory");
|
|
}
|
|
oq_seg[oq_nseg++] = (uint32_t)*open;
|
|
}
|
|
*open = -1;
|
|
}
|
|
|
|
void vk_exec_oq_draw(VkExec *ex) {
|
|
int *open = XW.on ? &XW.open : &oq_open;
|
|
if (!(XW.on ? XW.counting : oq_counting) || *open >= 0) return;
|
|
// a worker takes the next slot of its chunk's block, past it one from the ring at once (as the replay does, while
|
|
// a group's workers may still take some); a busy one is lost, not tried again
|
|
uint32_t q = (XW.on && XW.qnext != XW.qend ? XW.qnext++ : __atomic_fetch_add(&oq_next, 1, __ATOMIC_RELAXED)) % OQ_N;
|
|
if (__atomic_load_n(&oq_busy[q], __ATOMIC_ACQUIRE)) {
|
|
warn_once(W_QUERY, "occlusion query ring full: draws left uncounted");
|
|
if (XW.on && XW.seg < GS[XW.g].n) __atomic_store_n(&GS[XW.g].s[XW.seg]->lost, 1, __ATOMIC_RELAXED);
|
|
else __atomic_store_n(&oq_lost, 1, __ATOMIC_RELAXED);
|
|
return;
|
|
}
|
|
__atomic_store_n(&oq_busy[q], 1, __ATOMIC_RELAXED);
|
|
vkCmdBeginQuery(ex->cb, oq_pool, q, oq_flags);
|
|
ex->recorded++;
|
|
*open = (int)q;
|
|
}
|
|
|
|
void vk_exec_oq_pass_end(VkExec *ex) { oq_close(ex); }
|
|
|
|
static void oq_batch_drop(void); // the results kept by the completion thread (oq_take)
|
|
static void oq_hold_drop(void); // the counts remembered by the completion thread (oq_report_count)
|
|
|
|
void vk_exec_xfer_shutdown(void) {
|
|
if (oq_state > 0) vkDestroyQueryPool(vk_ctx()->device, oq_pool, NULL);
|
|
oq_pool = VK_NULL_HANDLE, oq_state = 0, oq_next = 0, oq_counting = oq_reported = oq_keep = 0, oq_open = -1;
|
|
oq_nseg = 0, oq_lost = 0, oq_count = 0;
|
|
memset((void *)oq_busy, 0, sizeof oq_busy);
|
|
oq_batch_drop();
|
|
oq_hold_drop();
|
|
}
|
|
|
|
// The completion thread reads the results in batches (NX2NAVI_OQ_BATCH, default 1; 0: every segment by itself, as
|
|
// before). The game tests one proxy per report, so a segment holds one query, and a frame of Hyrule Field has about a
|
|
// hundred of them: one vkGetQueryPoolResults and one vkResetQueryPool each. On an RX 7900 XT (26.10.1) each of those
|
|
// calls goes into the kernel, and the two hundred took 3.9 ms of every frame on the completion thread, which runs a
|
|
// submission's callbacks before it lets the submission's waiters go: the game's main thread waits for the previous
|
|
// frame's fence before it lets the next frame's GPU work start, so the frame after one replayed inside its submit
|
|
// call began 4 to 6 ms late with every thread idle. Slots come from the ring in order, so the slots of the segments
|
|
// that follow lie behind the one asked for: one call reads OQ_BATCH slots from there on with their availability, the
|
|
// available ones are kept (the results stay in the pool until the reset), and the segments after it take theirs from
|
|
// what is kept. A slot not available in the batch (its part had not run yet) is read again when its segment comes.
|
|
// The slots taken are reset in runs, one vkResetQueryPool for each: a run ends when a slot that does not continue it
|
|
// is taken or a new batch is read. Until then its slots stay busy for the replay, at most OQ_BATCH of the ring's
|
|
// OQ_N, also while the game reports nothing.
|
|
#define OQ_BATCH 256
|
|
static struct {
|
|
uint32_t first, n; // the slots kept: [first, first + n)
|
|
uint64_t v[OQ_BATCH * 2]; // per slot: samples, available
|
|
uint8_t taken[OQ_BATCH];
|
|
uint32_t r0, rn; // the run of taken slots still to be reset
|
|
uint64_t calls, slots, segs; // since the last perf line: result calls, slots they read, segments served
|
|
} OQB;
|
|
static int oq_batch_mode = -1;
|
|
|
|
static void oq_batch_drop(void) { OQB.n = OQB.rn = 0; }
|
|
|
|
static void oq_reset_flush(VkDevice dev) {
|
|
if (!OQB.rn) return;
|
|
vkResetQueryPool(dev, oq_pool, OQB.r0, OQB.rn);
|
|
for (uint32_t k = 0; k < OQB.rn; k++) __atomic_store_n(&oq_busy[OQB.r0 + k], 0, __ATOMIC_RELEASE);
|
|
OQB.rn = 0;
|
|
}
|
|
|
|
static void oq_read(VkDevice dev, uint32_t first, uint32_t n) {
|
|
VkResult r = vkGetQueryPoolResults(dev, oq_pool, first, n, sizeof OQB.v[0] * 2 * n, OQB.v, 2 * sizeof OQB.v[0],
|
|
VK_QUERY_RESULT_64_BIT | VK_QUERY_RESULT_WITH_AVAILABILITY_BIT);
|
|
if (r != VK_SUCCESS && r != VK_NOT_READY) {
|
|
vk_device_lost_check(r, "the read of occlusion query results");
|
|
rt_fatal("vk: occlusion query results failed (%d)", (int)r);
|
|
}
|
|
OQB.first = first, OQB.n = n;
|
|
memset(OQB.taken, 0, n);
|
|
OQB.calls++, OQB.slots += n;
|
|
}
|
|
|
|
// The samples of slot q, whose submission has completed; *avail 0 when the query has no result.
|
|
static uint64_t oq_take(VkDevice dev, uint32_t q, int *avail) {
|
|
uint32_t k = q - OQB.first;
|
|
if (!(q >= OQB.first && k < OQB.n && !OQB.taken[k] && OQB.v[2 * k + 1])) {
|
|
// not kept, or kept as unavailable (read before its part ran): from this slot on, up to the replay's next slot
|
|
oq_reset_flush(dev); // before the kept results go: the run's slots are among them
|
|
uint32_t next = __atomic_load_n(&oq_next, __ATOMIC_RELAXED) % OQ_N, n = OQ_N - q;
|
|
if (next > q && next - q < n) n = next - q;
|
|
if (n > OQ_BATCH) n = OQ_BATCH;
|
|
if (oq_batch_mode == 0) n = 1;
|
|
oq_read(dev, q, n);
|
|
k = 0;
|
|
}
|
|
OQB.taken[k] = 1;
|
|
if (OQB.rn && q != OQB.r0 + OQB.rn) oq_reset_flush(dev);
|
|
if (!OQB.rn) OQB.r0 = q;
|
|
OQB.rn++;
|
|
*avail = OQB.v[2 * k + 1] != 0;
|
|
return OQB.v[2 * k];
|
|
}
|
|
|
|
// NX2NAVI_OQ_HOLD (default 0): a report whose segment left draws uncounted (a full ring, vk_exec_oq_draw) writes the
|
|
// larger of what was counted and the last complete count of its record (vk_oq_hold.h); off, what was counted, at least
|
|
// 1. Such reports are counted either way and logged at the 1st, 10th, 100th... so a log shows how often the ring ran
|
|
// full. Completion thread only, as oq_count.
|
|
static OqHold oq_held;
|
|
static int oq_hold_on = -1;
|
|
static uint64_t oq_lossy;
|
|
|
|
static void oq_hold_drop(void) { memset(&oq_held, 0, sizeof oq_held), oq_lossy = 0, oq_hold_on = -1; }
|
|
|
|
static uint64_t oq_report_count(const OqSegment *s, uint64_t counted) {
|
|
if (oq_hold_on < 0) {
|
|
oq_hold_on = cfg_bool("NX2NAVI_OQ_HOLD");
|
|
if (oq_hold_on) rt_log("vk: samples-passed reports that miss draws repeat their record's last complete count (NX2NAVI_OQ_HOLD)");
|
|
}
|
|
if (!s->lost) {
|
|
if (oq_hold_on) oqh_note(&oq_held, s->addr, counted);
|
|
return counted;
|
|
}
|
|
uint64_t v = oq_hold_on ? oqh_lost(&oq_held, s->addr, counted) : counted;
|
|
uint64_t n = ++oq_lossy, p = 1;
|
|
while (p < n) p *= 10;
|
|
if (p == n)
|
|
rt_log("vk: %llu samples-passed reports so far missed draws the full occlusion query ring left uncounted (this "
|
|
"one: %llu counted, %llu reported%s)", (unsigned long long)n, (unsigned long long)counted,
|
|
(unsigned long long)v, oq_hold_on ? ", NX2NAVI_OQ_HOLD" : "");
|
|
return v;
|
|
}
|
|
|
|
static void oq_cb(void *arg) {
|
|
OqSegment *s = arg;
|
|
VkDevice dev = vk_ctx()->device;
|
|
if (oq_batch_mode < 0) oq_batch_mode = cfg_bool("NX2NAVI_OQ_BATCH");
|
|
// a segment the replay workers filled lists its slots in the order the chunks closed them: in ring order, they are
|
|
// taken in runs (oq_take)
|
|
for (uint32_t i = 1; i < s->n; i++) {
|
|
uint32_t v = s->q[i], j = i;
|
|
for (; j && s->q[j - 1] > v; j--) s->q[j] = s->q[j - 1];
|
|
s->q[j] = v;
|
|
}
|
|
uint64_t sum = 0;
|
|
for (uint32_t i = 0; i < s->n; i++) {
|
|
// The fence has passed, so a query that is still unavailable will not become available: waiting would block the
|
|
// completion thread, and with it every later callback, for good. It counts as visible (one sample).
|
|
int avail;
|
|
uint64_t v = oq_take(dev, s->q[i], &avail);
|
|
if (avail) sum += v;
|
|
else {
|
|
sum += 1;
|
|
// A query its submission never wrote. Every RX 6000 report of a lost device (0.8.0 to 0.9.2) had this line
|
|
// first, its callback run for work the GPU had not finished. A timeline read above every value its signals
|
|
// carry, then the device itself, end the run here when it is lost (vk_sync.c "a lost device in the
|
|
// timelines' values", vk_device.c vk_device_lost_poll). Otherwise the slot counts as visible and the game
|
|
// goes on, as a ring that came round too early would leave it. The first such slot is logged with the
|
|
// submissions' state and the GPU's breadcrumbs: whether the GPU had finished this submission, and whether
|
|
// the timeline read above what was signalled.
|
|
vk_sync_check_timeline("an occlusion query its finished submission never wrote (a timeline value no "
|
|
"submission signals)");
|
|
vk_device_lost_poll();
|
|
static volatile char logged;
|
|
if (!__atomic_exchange_n(&logged, 1, __ATOMIC_RELAXED)) {
|
|
char st[512];
|
|
vk_sync_state_text(st, sizeof st);
|
|
rt_log("vk: occlusion query slot %u of submission %llu not available after its fence, counted as visible; "
|
|
"%s", s->q[i], (unsigned long long)s->fence, st);
|
|
vk_crumbs_report();
|
|
}
|
|
}
|
|
}
|
|
if (s->n && ++OQB.segs >= 12000) {
|
|
if (rt_trace & RT_TRACE_PERF)
|
|
rt_log("perf: occlusion queries: %llu segments read with %llu result calls of %.1f slots on average",
|
|
(unsigned long long)OQB.segs, (unsigned long long)OQB.calls,
|
|
OQB.calls ? (double)OQB.slots / (double)OQB.calls : 0.0);
|
|
OQB.calls = OQB.slots = OQB.segs = 0;
|
|
}
|
|
if (s->reset) oq_count = 0;
|
|
else {
|
|
oq_count += sum;
|
|
if (s->lost && !oq_count) oq_count = 1;
|
|
if (s->addr) {
|
|
if (vk_sync_effect_late()) __atomic_fetch_add(&late_counts, 1, __ATOMIC_RELAXED);
|
|
report_write(s->addr, s->seq, oq_report_count(s, oq_count), 0);
|
|
}
|
|
}
|
|
free(s->q), free(s);
|
|
}
|
|
|
|
// Hands the segment to the completion thread with a report (addr; 0: no CPU-visible memory) or a reset.
|
|
static void oq_segment(VkExec *ex, uint64_t addr, int reset) {
|
|
oq_close(ex);
|
|
OqSegment *s = calloc(1, sizeof *s);
|
|
if (!s || (oq_nseg && !(s->q = malloc(sizeof *s->q * oq_nseg)))) rt_fatal("vk: out of memory");
|
|
s->cap = oq_nseg;
|
|
s->addr = addr, s->seq = report_seq, s->fence = ex->fence, s->reset = reset, s->lost = oq_lost, s->n = oq_nseg;
|
|
if (addr) vk_exec_note_results(VK_RES_OQ);
|
|
if (oq_nseg) memcpy(s->q, oq_seg, sizeof *s->q * oq_nseg);
|
|
if (gs_on >= 0) { // the front pass: the workers add the queries of the group's ops before it
|
|
__typeof__(GS[0]) *gs = &GS[gs_on];
|
|
if (gs->n == gs->cap) {
|
|
gs->cap = gs->cap ? gs->cap * 2 : 64;
|
|
if (!(gs->s = realloc(gs->s, sizeof *gs->s * gs->cap))) rt_fatal("vk: out of memory");
|
|
}
|
|
gs->s[gs->n++] = s;
|
|
}
|
|
for (int g = 0; g < GS_GROUPS; g++) // the first made after a group's front pass
|
|
if (GS[g].tail_wait && !GS[g].tail) GS[g].tail = s;
|
|
oq_nseg = 0, oq_lost = 0;
|
|
vk_completion_add_result(ex->fence, oq_cb, s);
|
|
}
|
|
|
|
static void reset_counter(VkExec *ex, const p_reset_counter *p) {
|
|
if (p->type != NVN_COUNTER_TYPE_SAMPLES_PASSED || !oq_ready()) return;
|
|
oq_segment(ex, 0, 1);
|
|
oq_counting = 1, oq_reported = 0;
|
|
}
|
|
|
|
// A samples-passed report: the segment goes with it (addr 0: no CPU-visible memory) and counting stops until the next
|
|
// reset, unless reports came without resets before.
|
|
static void oq_report(VkExec *ex, uint64_t addr) {
|
|
oq_segment(ex, addr, 0);
|
|
if (oq_reported && !oq_keep) {
|
|
oq_keep = 1;
|
|
rt_log("vk: samples passed reported twice without a reset: counting every draw from now on");
|
|
}
|
|
oq_reported = 1;
|
|
if (!oq_keep) oq_counting = 0;
|
|
}
|
|
|
|
// ---- event order check
|
|
// The game orders work between its queues with events: a command buffer waits (WaitEvent) until another's SignalEvent
|
|
// has set the value. Every submission replays here on one queue in the order it came, and a wait records nothing, so a
|
|
// wait whose signal comes in a later submission does not hold its work back: that work runs before what it waits for.
|
|
// The replay keeps the value each event reaches in replay order (ev_seen, the signals recorded so far, on top of the
|
|
// value in memory) and counts the waits it reaches unmet; NX2NAVI_TRACE=perf logs the count, and the first few are
|
|
// logged as they happen. Replay thread only, apart from vk_exec_event_met_now's unlocked read of ev_seen.
|
|
#define EV_SEEN 64
|
|
static struct { volatile uint32_t *val; uint32_t v; } ev_seen[EV_SEEN];
|
|
static int ev_n, ev_next;
|
|
static uint64_t ev_waits, ev_unmet, ev_late, ev_late_subs;
|
|
// Waits reached unmet, until a replayed signal meets them (ev_late: the signal came that many submissions later, so
|
|
// the work after the wait ran before the work it waited for) or they are pushed out by newer ones.
|
|
#define EV_OPEN 64
|
|
static struct { volatile uint32_t *val; uint32_t mode, value; uint64_t sub; } ev_open[EV_OPEN];
|
|
static int ev_open_n;
|
|
|
|
static uint32_t *ev_slot(volatile uint32_t *val, int create) {
|
|
for (int i = 0; i < ev_n; i++)
|
|
if (ev_seen[i].val == val) return &ev_seen[i].v;
|
|
if (!create) return NULL;
|
|
int i = ev_n < EV_SEEN ? ev_n++ : ev_next++ % EV_SEEN;
|
|
ev_seen[i].val = val, ev_seen[i].v = __atomic_load_n(val, __ATOMIC_ACQUIRE);
|
|
return &ev_seen[i].v;
|
|
}
|
|
|
|
static int ev_holds(uint32_t have, uint32_t mode, uint32_t want) {
|
|
return mode == NVN_EVENT_WAIT_MODE_GEQUAL_WRAP ? (int32_t)(have - want) >= 0 : have == want;
|
|
}
|
|
|
|
// ---- events the CPU signals
|
|
// Ocarina gates its main queue on the CPU: each frame the game sets one of two events to 0 (they take turns frame by
|
|
// frame), submits the worker queues' command buffers and the main queue's, whose first command waits for the event
|
|
// to be 1, and signals 1 from the CPU (nvnEventSignal) once it has finished writing what the frame's commands read.
|
|
// The signal comes before the main queue's submit call, after it, or while that call is replaying. On the console
|
|
// the GPU starts the queue at the signal and reads memory from then on.
|
|
// Here a submission's recording begins with the copies of what it reads: the VRAM mirrors' sync and the uniform
|
|
// snapshot (vk_exec_record), and the wait is tested when the walk reaches it, some hundred microseconds later. A
|
|
// signal that came in between passed the wait, and the frame was drawn from copies taken before the game had
|
|
// finished writing: single frames with the scene's shadowing missing or the whole scene black, the interface as it
|
|
// was, most of them when other work keeps the CPU busy, since the game's threads then finish later and the signal
|
|
// falls into the replay more often.
|
|
// So a wait is tested against the value the event had when the submission took its copies: every nvnEventSignal is
|
|
// numbered and keeps the value it replaced (vk_exec_event_cpu, before the write), vk_exec_record notes the number at
|
|
// its start (vk_exec_event_sync), and a signal with a later number does not count for that submission. The wait then
|
|
// stops the submission like any wait reached before its signal, and vk_backend.c submit_sync lets it go on at once
|
|
// in a submission of its own, whose copies are taken after the signal.
|
|
// NX2NAVI_EVENT_AT_SYNC=0: the value in memory when the walk reaches the wait, as before.
|
|
#define CPU_EV 16
|
|
#define CPU_EV_HIST 8
|
|
static struct { const void *val; uint64_t seq[CPU_EV_HIST]; uint32_t before[CPU_EV_HIST]; unsigned n; } cpu_ev[CPU_EV];
|
|
static int cpu_ev_n; // cpu_ev_lock
|
|
static uint64_t cpu_ev_seq; // cpu_ev_lock: nvnEventSignal calls so far
|
|
static uint64_t ev_sync_seq = UINT64_MAX; // cpu_ev_lock: cpu_ev_seq at the recording's start; UINT64_MAX outside one
|
|
static PlatMutex cpu_ev_lock = PLAT_MUTEX_INIT;
|
|
static uint64_t ev_after_sync; // replay: waits only a signal after the copies would have passed
|
|
|
|
int vk_exec_event_at_sync(void) {
|
|
static int on = -1;
|
|
if (on < 0) on = rt_knob("EVENT_AT_SYNC", "1");
|
|
return on;
|
|
}
|
|
|
|
void vk_exec_event_cpu(const void *event) {
|
|
plat_mutex_lock(&cpu_ev_lock);
|
|
int i = 0;
|
|
while (i < cpu_ev_n && cpu_ev[i].val != event) i++;
|
|
if (i == cpu_ev_n && cpu_ev_n < CPU_EV) cpu_ev[cpu_ev_n++].val = event, cpu_ev[i].n = 0;
|
|
cpu_ev_seq++;
|
|
if (i < cpu_ev_n) {
|
|
unsigned k = cpu_ev[i].n++ % CPU_EV_HIST;
|
|
cpu_ev[i].seq[k] = cpu_ev_seq, cpu_ev[i].before[k] = __atomic_load_n((const volatile uint32_t *)event, __ATOMIC_ACQUIRE);
|
|
}
|
|
plat_mutex_unlock(&cpu_ev_lock);
|
|
}
|
|
|
|
// 1 when the CPU has signalled this event before (nvnEventSignal): its waits are gates the game opens from the CPU.
|
|
int vk_exec_event_cpu_known(const void *event) {
|
|
plat_mutex_lock(&cpu_ev_lock);
|
|
int i = 0;
|
|
while (i < cpu_ev_n && cpu_ev[i].val != event) i++;
|
|
int known = i < cpu_ev_n;
|
|
plat_mutex_unlock(&cpu_ev_lock);
|
|
return known;
|
|
}
|
|
|
|
// The wait's condition on the value in memory now, whatever the replay has seen. Any thread.
|
|
int vk_exec_event_holds_now(const void *payload) {
|
|
const p_wait_event *p = payload;
|
|
return ev_holds(__atomic_load_n(p->val, __ATOMIC_ACQUIRE), p->mode, p->value);
|
|
}
|
|
|
|
void vk_exec_event_sync(int begin) {
|
|
plat_mutex_lock(&cpu_ev_lock);
|
|
ev_sync_seq = begin ? cpu_ev_seq : UINT64_MAX;
|
|
plat_mutex_unlock(&cpu_ev_lock);
|
|
}
|
|
|
|
uint64_t vk_exec_event_late_take(void) {
|
|
uint64_t n = ev_after_sync;
|
|
ev_after_sync = 0;
|
|
return n;
|
|
}
|
|
|
|
// The event's value in memory, and in *at_sync the value it had when the submission being recorded took its copies
|
|
// (the same outside a recording, and for an event the CPU has not signalled since).
|
|
static uint32_t ev_mem(volatile uint32_t *val, uint32_t *at_sync) {
|
|
uint32_t mem = __atomic_load_n(val, __ATOMIC_ACQUIRE);
|
|
*at_sync = mem;
|
|
plat_mutex_lock(&cpu_ev_lock);
|
|
if (ev_sync_seq != UINT64_MAX)
|
|
for (int i = 0; i < cpu_ev_n; i++) {
|
|
if (cpu_ev[i].val != (const void *)val) continue;
|
|
uint64_t first = UINT64_MAX;
|
|
unsigned n = cpu_ev[i].n < CPU_EV_HIST ? cpu_ev[i].n : CPU_EV_HIST;
|
|
for (unsigned k = 0; k < n; k++) // the earliest signal after the copies replaced the value they saw
|
|
if (cpu_ev[i].seq[k] > ev_sync_seq && cpu_ev[i].seq[k] < first) first = cpu_ev[i].seq[k], *at_sync = cpu_ev[i].before[k];
|
|
break;
|
|
}
|
|
plat_mutex_unlock(&cpu_ev_lock);
|
|
return mem;
|
|
}
|
|
|
|
// Whether the wait holds. *late: it holds only by a CPU signal that came after the submission's copies.
|
|
static int ev_met(const p_wait_event *p, uint32_t *mem, const uint32_t **seen, int *late) {
|
|
uint32_t at_sync;
|
|
*mem = ev_mem(p->val, &at_sync);
|
|
*seen = ev_slot(p->val, 0);
|
|
int by_seen = *seen && ev_holds(**seen, p->mode, p->value);
|
|
int now = ev_holds(*mem, p->mode, p->value) || by_seen, then = ev_holds(at_sync, p->mode, p->value) || by_seen;
|
|
*late = now && !then;
|
|
return vk_exec_event_at_sync() ? then : now;
|
|
}
|
|
|
|
int vk_exec_event_met(const void *payload) {
|
|
uint32_t mem;
|
|
const uint32_t *seen;
|
|
int late;
|
|
return ev_met(payload, &mem, &seen, &late);
|
|
}
|
|
|
|
// The value in memory now, or the signals replayed so far, whatever submission is being recorded: for vk_exec.c
|
|
// vk_exec_held_ready, which asks without the submission lock whether held work can go on in a submission of its own.
|
|
// That submission takes its copies later, so a CPU signal given before now counts for it; testing at the copy point
|
|
// (ev_mem) would take the copy point of the recording another thread may be making. ev_seen belongs to the replay and
|
|
// is read here without its lock, so the answer is a decision only: a wrong "ready" makes an empty submission, which
|
|
// decides again under the lock (vk_exec.c evo_resume).
|
|
int vk_exec_event_met_now(const void *payload) {
|
|
const p_wait_event *p = payload;
|
|
if (ev_holds(__atomic_load_n(p->val, __ATOMIC_ACQUIRE), p->mode, p->value)) return 1;
|
|
int n = __atomic_load_n(&ev_n, __ATOMIC_RELAXED);
|
|
for (int i = 0; i < n && i < EV_SEEN; i++)
|
|
if (__atomic_load_n(&ev_seen[i].val, __ATOMIC_RELAXED) == p->val)
|
|
return ev_holds(__atomic_load_n(&ev_seen[i].v, __ATOMIC_RELAXED), p->mode, p->value);
|
|
return 0;
|
|
}
|
|
|
|
int vk_exec_signalled;
|
|
|
|
int vk_exec_wait_event(const void *payload) {
|
|
const p_wait_event *p = payload;
|
|
ev_waits++;
|
|
uint32_t mem;
|
|
const uint32_t *seen;
|
|
int late;
|
|
int met = ev_met(p, &mem, &seen, &late);
|
|
if (late) {
|
|
ev_after_sync++;
|
|
vk_exec_evt('L', (const void *)p->val, 0);
|
|
}
|
|
if (met) return 1;
|
|
ev_unmet++;
|
|
if (ev_open_n == EV_OPEN) memmove(ev_open, ev_open + 1, sizeof ev_open - sizeof *ev_open), ev_open_n--;
|
|
ev_open[ev_open_n].val = p->val, ev_open[ev_open_n].mode = p->mode, ev_open[ev_open_n].value = p->value;
|
|
ev_open[ev_open_n++].sub = vk_exec_submit_seq;
|
|
static int logged;
|
|
if (logged++ < 8)
|
|
rt_log("vk: event wait reached before its signal: event %p, mode %u, wants %#x, memory %#x, replayed signals %s%#x",
|
|
(void *)p->val, p->mode, p->value, mem, seen ? "" : "none ", seen ? *seen : 0);
|
|
return 0;
|
|
}
|
|
|
|
void vk_exec_event_stats(uint64_t *waits, uint64_t *unmet, uint64_t *late, uint64_t *late_subs) {
|
|
*waits = ev_waits, *unmet = ev_unmet, *late = ev_late, *late_subs = ev_late_subs;
|
|
ev_waits = ev_unmet = ev_late = ev_late_subs = 0;
|
|
}
|
|
|
|
static void signal_event(VkExec *ex, const p_signal_event *p) {
|
|
uint32_t *seen = ev_slot(p->val, 1);
|
|
*seen = p->mode == NVN_EVENT_SIGNAL_MODE_ADD ? *seen + p->value : p->value;
|
|
vk_exec_evt('s', (const void *)p->val, *seen);
|
|
vk_exec_signalled = 1;
|
|
for (int i = 0; i < ev_open_n;)
|
|
if (ev_open[i].val == p->val && ev_holds(*seen, ev_open[i].mode, ev_open[i].value)) {
|
|
ev_late++, ev_late_subs += vk_exec_submit_seq - ev_open[i].sub;
|
|
ev_open[i] = ev_open[--ev_open_n];
|
|
} else i++;
|
|
vk_exec_note_results(VK_RES_EVENT);
|
|
EventWrite *e = malloc(sizeof *e);
|
|
if (!e) rt_fatal("vk: out of memory");
|
|
*e = (EventWrite){p->val, p->mode, p->value};
|
|
vk_completion_add_result(ex->fence, event_cb, e);
|
|
}
|
|
|
|
static void report_counter(VkExec *ex, const p_report_counter *p) {
|
|
int samples = p->type == NVN_COUNTER_TYPE_SAMPLES_PASSED && oq_ready();
|
|
report_seq++; // before oq_report / counter_cb take it
|
|
if (nvn_gpu_only(p->addr)) { // CPU-invisible pool: no host memory behind it
|
|
if (samples) oq_report(ex, 0);
|
|
return;
|
|
}
|
|
VkMemSpan m;
|
|
if (!vk_mem_resolve(p->addr, sizeof(NVNcounterData), &m))
|
|
rt_fatal("vk: counter report at %#llx is in no memory pool", (unsigned long long)p->addr);
|
|
vk_mem_count(m.cls, VK_MUSE_GPU_WRITE, sizeof(NVNcounterData));
|
|
clear_note_report(p->addr);
|
|
if (samples) oq_report(ex, p->addr);
|
|
else {
|
|
// a timestamp in the tail is the one result a tail may hand the CPU: its time is written ahead (counter_cb)
|
|
int ts_tail = vk_exec_tail() && p->type == NVN_COUNTER_TYPE_TIMESTAMP, keep = tail_results;
|
|
vk_exec_note_results(VK_RES_COUNTER);
|
|
if (ts_tail) tail_results = keep;
|
|
CounterWrite *a = malloc(sizeof *a);
|
|
if (!a) rt_fatal("vk: out of memory");
|
|
*a = (CounterWrite){p->addr, report_seq, ts_tail ? ex->fence : 0};
|
|
vk_completion_add_result(ex->fence, counter_cb, a);
|
|
}
|
|
Pending *e = pending_slot(p->addr, 1);
|
|
if (e) e->seq = report_seq, e->frame = cond_frame, e->unknown = samples, e->type = (int)p->type;
|
|
else LOG_ONCE("vk: a counter report's table neighbourhood is all in flight: conditions on %#llx read memory",
|
|
(unsigned long long)p->addr);
|
|
}
|
|
|
|
// ---- conditional rendering
|
|
|
|
// 1: *v read, 0: not readable, 2: a samples-passed report not in memory yet (unknown)
|
|
static int counter_word(uint64_t addr, uint64_t *v) {
|
|
const Pending *e = pending_slot(addr, 0); // the last report to addr decides
|
|
// but for the halves of the word a small clear filled after it (clear_small): memory holds those
|
|
unsigned filled = e ? clear_quarters(e) & 3 : 0;
|
|
int zero = 0; // the report decides the halves that were not filled: 0
|
|
if (e && filled != 3) {
|
|
if (!e->unknown && e->frame == cond_frame) zero = 1; // a timestamp/statistics report of this frame
|
|
else if (__atomic_load_n(&e->written, __ATOMIC_ACQUIRE) < e->seq) {
|
|
if (e->unknown) return *v = 0, 2;
|
|
zero = 1;
|
|
}
|
|
}
|
|
if (zero && !filled) return *v = 0, 1;
|
|
VkMemSpan m;
|
|
if (nvn_gpu_only(addr) || !vk_mem_resolve(addr, sizeof *v, &m)) return 0;
|
|
memcpy(v, (const void *)(uintptr_t)addr, sizeof *v);
|
|
if (zero) *v &= filled == 1 ? 0xffffffffull : 0xffffffff00000000ull;
|
|
vk_mem_count(m.cls, VK_MUSE_CPU_READ, sizeof *v);
|
|
return 1;
|
|
}
|
|
|
|
// The counter type of the newest report to addr the ring still holds, -1 when there is none (NX2NAVI_LOG_RCOND).
|
|
static int last_type(uint64_t addr) {
|
|
const Pending *e = pending_slot(addr, 0);
|
|
return e ? e->type : -1;
|
|
}
|
|
|
|
// NX2NAVI_LOG_RCOND=ab (diagnosis): the words a condition compares and where each came from: "f" a report replayed
|
|
// this frame (0), "p" a pending report (0 or unknown), "m" memory, "c" memory after a small clear that came after the
|
|
// report (clear_small), with the newest report's type and frame.
|
|
void vk_exec_cond_why(const p_render_cond *c, char *buf, size_t cap) {
|
|
int o = snprintf(buf, cap, "%#llx mode %d:", (unsigned long long)c->addr, c->mode);
|
|
for (int k = 0; k < 2 && o < (int)cap; k++) {
|
|
uint64_t a = c->addr + 16 * (uint64_t)k;
|
|
const Pending *e = pending_slot(a, 0);
|
|
const char *src = !e ? "m" : (clear_quarters(e) & 3) == 3 ? "c" : !e->unknown && e->frame == cond_frame ? "f"
|
|
: __atomic_load_n(&e->written, __ATOMIC_ACQUIRE) < e->seq ? "p" : "m";
|
|
o += snprintf(buf + o, cap - (size_t)o, " %llu %s(type %d, frame %lld of %llu)",
|
|
(unsigned long long)*(const uint64_t *)(uintptr_t)a, src, e ? e->type : -1,
|
|
e ? (long long)e->frame : -1LL, (unsigned long long)cond_frame);
|
|
}
|
|
}
|
|
|
|
static int cond_enabled(const p_render_cond *c) {
|
|
uint64_t x, y;
|
|
int rx = counter_word(c->addr, &x), ry = counter_word(c->addr + 16, &y);
|
|
if (!rx || !ry) {
|
|
warn_once(W_BUFFER, "conditional render counters at %#llx are not readable: rendering", (unsigned long long)c->addr);
|
|
return 1;
|
|
}
|
|
if (rx == 2 || ry == 2) return 1; // a samples-passed count not done yet
|
|
int on = c->mode == 0 ? x == y : x != y;
|
|
// NX2NAVI_LOG_RCOND=1: one condition in 499 (up to 300 lines), on or off, with the counter types last reported there
|
|
static int log_on = -1, nlog;
|
|
static unsigned ncond;
|
|
if (log_on < 0) log_on = rt_knob("LOG_RCOND", "0");
|
|
if (log_on && ncond++ % 499 == 0 && nlog < 300) {
|
|
nlog++;
|
|
// (pending: the report value has not reached guest memory yet)
|
|
int px = 0, py = 0; // the word came from a report not done yet (forced to 0) rather than memory
|
|
const Pending *ex_ = pending_slot(c->addr, 0), *ey_ = pending_slot(c->addr + 16, 0);
|
|
px = ex_ && ex_->written < ex_->seq, py = ey_ && ey_->written < ey_->seq;
|
|
const uint64_t *m = (const uint64_t *)(uintptr_t)c->addr;
|
|
rt_log("vk: render condition %s at %#llx mode %d: %llu (type %d%s) vs %llu (type %d%s); memory %llx %llx %llx %llx",
|
|
on ? "on" : "off", (unsigned long long)c->addr, c->mode, (unsigned long long)x, last_type(c->addr), px ? ", pending" : "",
|
|
(unsigned long long)y, last_type(c->addr + 16), py ? ", pending" : "", (unsigned long long)m[0],
|
|
(unsigned long long)m[1], (unsigned long long)m[2], (unsigned long long)m[3]);
|
|
}
|
|
return on;
|
|
}
|
|
|
|
// ---- textures
|
|
|
|
typedef struct {
|
|
const VkTexImage *img;
|
|
VkImageAspectFlags aspect;
|
|
uint32_t level, layer0;
|
|
int bpb, bw, bh;
|
|
int32_t x, y, z; // region origin
|
|
uint32_t w, h, nz; // region extent; nz: slices (3D) or layers
|
|
} Sub;
|
|
|
|
// Resolves tex, its view and a copy region to image coordinates. 0 (warned) when the copy cannot be replayed.
|
|
static int sub_resolve(const tex_t *t, const view_t *v, int has_v, const NVNcopyRegion *r, int defaults_to_level,
|
|
Sub *s) {
|
|
memset(s, 0, sizeof *s);
|
|
if (!t) return 0;
|
|
TexVkFormat f;
|
|
if (!tex_vk_format(t->d.format, &f) || f.vk != f.ideal || f.expand_rgb || f.astc_bw) {
|
|
warn_once(W_FORMAT, "copy of format %#x texture (decoded or expanded on upload): skipped", t->d.format);
|
|
return 0;
|
|
}
|
|
s->bpb = nvn_fmt_bpb(t->d.format, &s->bw, &s->bh);
|
|
s->img = vk_texture_get(t);
|
|
if (!s->img || !s->bpb) {
|
|
warn_once(W_FORMAT, "copy of format %#x texture without an image: skipped", t->d.format);
|
|
return 0;
|
|
}
|
|
const VkTexImage *img = s->img;
|
|
if (img->aspect & VK_IMAGE_ASPECT_COLOR_BIT) s->aspect = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
else if (img->aspect == VK_IMAGE_ASPECT_DEPTH_BIT) s->aspect = VK_IMAGE_ASPECT_DEPTH_BIT;
|
|
else {
|
|
warn_once(W_FORMAT, "copy of depth-stencil format %#x texture: skipped", t->d.format);
|
|
return 0;
|
|
}
|
|
s->level = has_v && v->set_levels ? v->base_level : 0;
|
|
s->layer0 = has_v && v->set_layers ? v->min_layer : 0;
|
|
uint32_t lw = max1(img->width >> s->level), lh = max1(img->height >> s->level);
|
|
int k = img->scale > 1 ? img->scale : 1; // a larger shadow map: the game's region in its texels
|
|
s->x = r->xoffset * k, s->y = r->yoffset * k, s->z = r->zoffset;
|
|
s->nz = r->depth > 0 ? (uint32_t)r->depth : 1;
|
|
s->w = r->width > 0 ? (uint32_t)r->width * k : defaults_to_level ? (s->x < (int32_t)lw ? lw - (uint32_t)s->x : 0) : 0;
|
|
s->h = r->height > 0 ? (uint32_t)r->height * k : defaults_to_level ? (s->y < (int32_t)lh ? lh - (uint32_t)s->y : 0) : 0;
|
|
int is3d = img->type == VK_IMAGE_TYPE_3D;
|
|
uint32_t layers = is3d ? 1 : (uint32_t)s->z + s->nz, first = is3d ? 0 : s->layer0;
|
|
if (s->level >= img->levels || s->x < 0 || s->y < 0 || s->z < 0 || !s->w || !s->h ||
|
|
(uint64_t)s->x + s->w > lw || (uint64_t)s->y + s->h > lh || first + layers > img->layers ||
|
|
(is3d && (uint64_t)s->z + s->nz > max1(img->depth >> s->level))) {
|
|
warn_once(W_REGION, "copy region %d,%d,%d %ux%ux%u outside level %u of a %ux%u texture: skipped", s->x, s->y,
|
|
s->z, s->w, s->h, s->nz, s->level, img->width, img->height);
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
static VkImageSubresourceLayers sub_layers(const Sub *s) {
|
|
int is3d = s->img->type == VK_IMAGE_TYPE_3D;
|
|
return (VkImageSubresourceLayers){s->aspect, s->level, is3d ? 0 : s->layer0 + (uint32_t)s->z, is3d ? 1 : s->nz};
|
|
}
|
|
|
|
static VkOffset3D sub_offset(const Sub *s) {
|
|
return (VkOffset3D){s->x, s->y, s->img->type == VK_IMAGE_TYPE_3D ? s->z : 0};
|
|
}
|
|
|
|
static VkExtent3D sub_extent(const Sub *s) {
|
|
return (VkExtent3D){s->w, s->h, s->img->type == VK_IMAGE_TYPE_3D ? s->nz : 1};
|
|
}
|
|
|
|
// Buffer side of a texture <-> buffer copy: bytes covered, pitches, Vulkan buffer span (a VRAM mirror when the pool
|
|
// has one).
|
|
static int buffer_side(const VkExec *ex, const Sub *s, uint64_t addr, uint64_t *size, VkBufferImageCopy *c,
|
|
VkMemSpan *span) {
|
|
uint32_t wb = (s->w + (uint32_t)s->bw - 1) / (uint32_t)s->bw, hb = (s->h + (uint32_t)s->bh - 1) / (uint32_t)s->bh;
|
|
uint64_t row = ex->copy_stride > 0 ? (uint64_t)ex->copy_stride : (uint64_t)wb * (uint64_t)s->bpb;
|
|
if (row % (uint64_t)s->bpb || row < (uint64_t)wb * (uint64_t)s->bpb) {
|
|
warn_once(W_REGION, "buffer copy row stride %llu does not fit %u blocks of %d bytes: skipped",
|
|
(unsigned long long)row, wb, s->bpb);
|
|
return 0;
|
|
}
|
|
uint64_t img = row * hb;
|
|
*size = img * (s->nz - 1) + row * (hb - 1) + (uint64_t)wb * (uint64_t)s->bpb;
|
|
VkMemSpan m;
|
|
if (!addr || !vk_mem_resolve(addr, *size, &m) || m.avail < *size) {
|
|
warn_once(W_BUFFER, "texture copy buffer %#llx (%llu bytes) is not one backed span: skipped",
|
|
(unsigned long long)addr, (unsigned long long)*size);
|
|
return 0;
|
|
}
|
|
VK_MEM_PICK(m);
|
|
*span = m;
|
|
*c = (VkBufferImageCopy){.bufferOffset = m.offset, .bufferRowLength = (uint32_t)(row / (uint64_t)s->bpb) * (uint32_t)s->bw,
|
|
.bufferImageHeight = hb * (uint32_t)s->bh, .imageSubresource = sub_layers(s),
|
|
.imageOffset = sub_offset(s), .imageExtent = sub_extent(s)};
|
|
return 1;
|
|
}
|
|
|
|
static void copy_buffer_image(VkExec *ex, const p_copy_b2t *p, int to_image) {
|
|
Sub s;
|
|
if (!sub_resolve(p->tex, &p->view, p->has_view, &p->r, 1, &s)) return;
|
|
vk_gpu_gap_note(to_image ? VK_GAP_COPY_B2T : VK_GAP_COPY_T2B, (uint64_t)s.w * s.h);
|
|
if (s.img->scale > 1) { // the buffer holds the game's texel count, the image more
|
|
warn_once(W_REGION, "buffer copy of a shadow map enlarged by NX2NAVI_SHADOW_SCALE: skipped");
|
|
return;
|
|
}
|
|
uint64_t size;
|
|
VkBufferImageCopy c;
|
|
VkMemSpan m;
|
|
if (!buffer_side(ex, &s, p->src, &size, &c, &m)) return;
|
|
vk_mem_count(m.cls, to_image ? VK_MUSE_COPY_SRC : VK_MUSE_GPU_WRITE, size);
|
|
vk_exec_pass_end(ex);
|
|
vk_exec_barrier(ex);
|
|
if (to_image) vkCmdCopyBufferToImage(ex->cb, m.buffer, s.img->image, VK_IMAGE_LAYOUT_GENERAL, 1, &c);
|
|
else vkCmdCopyImageToBuffer(ex->cb, s.img->image, VK_IMAGE_LAYOUT_GENERAL, m.buffer, 1, &c);
|
|
ex->recorded++;
|
|
if (to_image) vk_texture_gpu_written(p->tex);
|
|
else vk_mem_gpu_wrote(&m, size), note_gpu_write(ex, p->src, size);
|
|
}
|
|
|
|
static void copy_t2t(VkExec *ex, const p_copy_t2t *p) {
|
|
Sub s, d;
|
|
if (!sub_resolve(p->src, &p->sv, p->has_sv, &p->sr, 1, &s)) return;
|
|
if (!sub_resolve(p->dst, &p->dv, p->has_dv, &p->dr, 1, &d)) return;
|
|
d.nz = s.nz;
|
|
vk_gpu_gap_note(VK_GAP_COPY_T2T, (uint64_t)s.w * s.h);
|
|
int same_size = s.w == d.w && s.h == d.h;
|
|
int same_format = s.img->format == d.img->format;
|
|
if (s.img->samples != d.img->samples && !(s.img->samples > 1 && d.img->samples == 1 && same_size && same_format)) {
|
|
warn_once(W_COPY, "texture copy between %u and %u samples: skipped", s.img->samples, d.img->samples);
|
|
return;
|
|
}
|
|
if (s.aspect != d.aspect || (s.aspect != VK_IMAGE_ASPECT_COLOR_BIT && !same_format)) {
|
|
warn_once(W_COPY, "texture copy between formats %#x and %#x: skipped", p->src->d.format, p->dst->d.format);
|
|
return;
|
|
}
|
|
VkImageSubresourceLayers sl = sub_layers(&s), dl = sub_layers(&d);
|
|
vk_exec_pass_end(ex);
|
|
vk_exec_barrier(ex);
|
|
if (s.img->samples > 1 && d.img->samples == 1) {
|
|
VkImageResolve rv = {sl, sub_offset(&s), dl, sub_offset(&d), sub_extent(&s)};
|
|
vkCmdResolveImage(ex->cb, s.img->image, VK_IMAGE_LAYOUT_GENERAL, d.img->image, VK_IMAGE_LAYOUT_GENERAL, 1, &rv);
|
|
} else if (same_size && same_format) {
|
|
VkImageCopy cp = {sl, sub_offset(&s), dl, sub_offset(&d), sub_extent(&s)};
|
|
vkCmdCopyImage(ex->cb, s.img->image, VK_IMAGE_LAYOUT_GENERAL, d.img->image, VK_IMAGE_LAYOUT_GENERAL, 1, &cp);
|
|
} else { // scaled or converting copy: nearest, or linear when asked for (colour only)
|
|
int is3d = s.img->type == VK_IMAGE_TYPE_3D;
|
|
VkImageBlit bl = {.srcSubresource = sl, .dstSubresource = dl};
|
|
bl.srcOffsets[0] = sub_offset(&s);
|
|
bl.srcOffsets[1] = (VkOffset3D){s.x + (int32_t)s.w, s.y + (int32_t)s.h, is3d ? s.z + (int32_t)s.nz : 1};
|
|
bl.dstOffsets[0] = sub_offset(&d);
|
|
bl.dstOffsets[1] = (VkOffset3D){d.x + (int32_t)d.w, d.y + (int32_t)d.h, is3d ? d.z + (int32_t)d.nz : 1};
|
|
int linear = (p->flags & NVN_COPY_FLAGS_LINEAR_FILTER_BIT) && s.aspect == VK_IMAGE_ASPECT_COLOR_BIT;
|
|
vkCmdBlitImage(ex->cb, s.img->image, VK_IMAGE_LAYOUT_GENERAL, d.img->image, VK_IMAGE_LAYOUT_GENERAL, 1, &bl,
|
|
linear ? VK_FILTER_LINEAR : VK_FILTER_NEAREST);
|
|
}
|
|
ex->recorded++;
|
|
vk_texture_gpu_written(p->dst);
|
|
static int lg = -1;
|
|
if (lg < 0) lg = rt_knob("LOG_RCOND", "0");
|
|
if (lg == 2 && (d.img->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) && d.w >= 1600)
|
|
rt_log("SEQ depth copy %#llx -> %#llx", (unsigned long long)s.img->addr, (unsigned long long)d.img->addr);
|
|
}
|
|
|
|
// ---- buffer to buffer
|
|
|
|
static void copy_b2b(VkExec *ex, const p_copy_b2b *p) {
|
|
uint64_t src = p->src, dst = p->dst, left = p->size;
|
|
vk_gpu_gap_note(VK_GAP_COPY_B2B, p->size);
|
|
int started = 0;
|
|
while (left) {
|
|
VkMemSpan s, d;
|
|
if (!vk_mem_resolve(src, 1, &s) || !vk_mem_resolve(dst, 1, &d)) {
|
|
warn_once(W_BUFFER, "buffer copy %#llx -> %#llx (%llu bytes) leaves backed memory: rest skipped",
|
|
(unsigned long long)p->src, (unsigned long long)p->dst, (unsigned long long)p->size);
|
|
break;
|
|
}
|
|
uint64_t n = left < s.avail ? left : s.avail;
|
|
if (n > d.avail) n = d.avail;
|
|
VK_MEM_PICK(s);
|
|
VK_MEM_PICK(d);
|
|
if (s.buffer == d.buffer && s.offset < d.offset + n && d.offset < s.offset + n) {
|
|
warn_once(W_OVERLAP, "buffer copy %#llx -> %#llx overlaps within one buffer: skipped",
|
|
(unsigned long long)p->src, (unsigned long long)p->dst);
|
|
break;
|
|
}
|
|
if (!started) vk_exec_pass_end(ex), vk_exec_barrier(ex), started = 1;
|
|
VkBufferCopy bc = {s.offset, d.offset, n};
|
|
vkCmdCopyBuffer(ex->cb, s.buffer, d.buffer, 1, &bc);
|
|
vk_mem_gpu_wrote(&d, n);
|
|
ex->recorded++;
|
|
vk_mem_count(s.cls, VK_MUSE_COPY_SRC, n);
|
|
vk_mem_count(d.cls, VK_MUSE_GPU_WRITE, n);
|
|
src += n, dst += n, left -= n;
|
|
}
|
|
if (started) note_gpu_write(ex, p->dst, p->size - left);
|
|
}
|
|
|
|
// ---- buffer fills (ClearBuffer, and ReportValue as a 4-byte one): vkCmdFillBuffer in GPU order after a full
|
|
// barrier, so the value lands after earlier work, as NVN's semaphore release does; CPU-visible ranges go to
|
|
// vk_guest_written at the fence, as copies do. Works in CPU-invisible pools. A small fill of CPU-visible memory is
|
|
// also applied to the counter records it covers, here in the replay (clear_small).
|
|
|
|
static void clear_buf(VkExec *ex, const p_clear_buf *p) {
|
|
if ((p->addr | p->size) & 3) {
|
|
warn_once(W_BUFFER, "buffer fill at %#llx of %llu bytes is not 4-byte aligned: skipped", (unsigned long long)p->addr,
|
|
(unsigned long long)p->size);
|
|
return;
|
|
}
|
|
uint64_t dst = p->addr, left = p->size;
|
|
vk_gpu_gap_note(VK_GAP_CLEAR_BUF, p->size);
|
|
VkMemSpan whole;
|
|
if (p->size && p->size <= CLEAR_SMALL && !nvn_gpu_only(p->addr) && vk_mem_resolve(p->addr, p->size, &whole))
|
|
clear_small(p->addr, p->size, p->value);
|
|
int started = 0;
|
|
while (left) {
|
|
VkMemSpan d;
|
|
if (!vk_mem_resolve(dst, 1, &d) || d.offset % 4) {
|
|
warn_once(W_BUFFER, "buffer fill at %#llx (%llu bytes) leaves backed memory: rest skipped",
|
|
(unsigned long long)p->addr, (unsigned long long)p->size);
|
|
break;
|
|
}
|
|
uint64_t n = left < d.avail ? left : d.avail & ~3ull;
|
|
if (!n) break;
|
|
if (!started) vk_exec_pass_end(ex), vk_exec_barrier(ex), started = 1;
|
|
VK_MEM_PICK(d);
|
|
vkCmdFillBuffer(ex->cb, d.buffer, d.offset, n, p->value);
|
|
// A VRAM mirror's guest copy is filled too, at once: atomics through the device address table count in both
|
|
// copies (translator patch 0036), and a counter reset here must start from the same value in both.
|
|
VkBuffer twin = vk_mem_guest_twin(&d);
|
|
if (twin) vkCmdFillBuffer(ex->cb, twin, d.offset, n, p->value), ex->recorded++;
|
|
vk_mem_gpu_wrote(&d, n);
|
|
ex->recorded++;
|
|
vk_mem_count(d.cls, VK_MUSE_GPU_WRITE, n);
|
|
dst += n, left -= n;
|
|
}
|
|
if (started) note_gpu_write(ex, p->addr, p->size - left);
|
|
}
|
|
|
|
// ---- texture clears
|
|
|
|
static int int_format(VkFormat f) {
|
|
switch (f) {
|
|
case VK_FORMAT_R8_UINT: case VK_FORMAT_R8G8_UINT: case VK_FORMAT_R8G8B8A8_UINT: case VK_FORMAT_R8_SINT:
|
|
case VK_FORMAT_R8G8_SINT: case VK_FORMAT_R8G8B8A8_SINT: case VK_FORMAT_R16_UINT: case VK_FORMAT_R16G16_UINT:
|
|
case VK_FORMAT_R16G16B16A16_UINT: case VK_FORMAT_R16_SINT: case VK_FORMAT_R16G16_SINT:
|
|
case VK_FORMAT_R16G16B16A16_SINT: case VK_FORMAT_R32_UINT: case VK_FORMAT_R32G32_UINT:
|
|
case VK_FORMAT_R32G32B32A32_UINT: case VK_FORMAT_R32_SINT: case VK_FORMAT_R32G32_SINT:
|
|
case VK_FORMAT_R32G32B32A32_SINT: case VK_FORMAT_A2B10G10R10_UINT_PACK32:
|
|
return 1;
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
// Whole levels only (vkCmdClear*Image has no rectangle); partial regions, channel masks and a view that reinterprets
|
|
// the format are skipped.
|
|
static void clear_tex(VkExec *ex, const p_clear_tex *p) {
|
|
const tex_t *t = p->tex;
|
|
if (!t) return;
|
|
Sub s;
|
|
if (!sub_resolve(t, &p->view, p->has_view, &p->r, 1, &s)) return;
|
|
const VkTexImage *img = s.img;
|
|
int is3d = img->type == VK_IMAGE_TYPE_3D;
|
|
uint32_t lw = max1(img->width >> s.level), lh = max1(img->height >> s.level);
|
|
vk_gpu_gap_note(VK_GAP_CLEAR_TEX, (uint64_t)lw * lh);
|
|
int reformat = p->has_view && p->view.set_format && p->view.format != t->d.format;
|
|
if (s.x || s.y || s.w != lw || s.h != lh || (p->mask & 0xf) != 0xf || reformat ||
|
|
(is3d && (s.z || s.nz != max1(img->depth >> s.level)))) {
|
|
warn_once(W_CLEAR, "texture clear of region %d,%d %ux%u mask %#x (level %ux%u, view format change %d): skipped",
|
|
s.x, s.y, s.w, s.h, p->mask, lw, lh, reformat);
|
|
return;
|
|
}
|
|
VkImageSubresourceRange rng = {img->aspect, s.level, 1, is3d ? 0 : s.layer0 + (uint32_t)s.z, is3d ? 1 : s.nz};
|
|
if (img->aspect & VK_IMAGE_ASPECT_COLOR_BIT) {
|
|
rng.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT;
|
|
VkClearColorValue cv;
|
|
memcpy(&cv, &p->c, sizeof cv);
|
|
if (int_format(img->format) != (p->is_uint != 0)) {
|
|
// A value of the other kind that is all zero bits (0.0f is) is the same clear either way: Ocarina clears its
|
|
// rg16ui edge map (480x270) with a float 0 each frame, and skipped, the post chain's edge classification
|
|
// read the previous frames' edges. Other values stay skipped.
|
|
if (cv.uint32[0] | cv.uint32[1] | cv.uint32[2] | cv.uint32[3]) {
|
|
warn_once(W_CLEAR, "texture clear value kind (uint %d) does not match format %d: skipped", p->is_uint,
|
|
(int)img->format);
|
|
return;
|
|
}
|
|
}
|
|
vk_exec_pass_end(ex);
|
|
vk_exec_barrier(ex);
|
|
vkCmdClearColorImage(ex->cb, img->image, VK_IMAGE_LAYOUT_GENERAL, &cv, 1, &rng);
|
|
} else {
|
|
int both = (img->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) && (img->aspect & VK_IMAGE_ASPECT_STENCIL_BIT);
|
|
VkClearDepthStencilValue dv = {0, 0};
|
|
if (img->aspect & VK_IMAGE_ASPECT_DEPTH_BIT) dv.depth = p->c.f[0];
|
|
if (img->aspect & VK_IMAGE_ASPECT_STENCIL_BIT) dv.stencil = both ? p->c.u[1] : p->c.u[0];
|
|
vk_exec_pass_end(ex);
|
|
vk_exec_barrier(ex);
|
|
vkCmdClearDepthStencilImage(ex->cb, img->image, VK_IMAGE_LAYOUT_GENERAL, &dv, 1, &rng);
|
|
}
|
|
ex->recorded++;
|
|
vk_texture_gpu_written(t);
|
|
}
|
|
|
|
// ---- parallel groups (vk_exec.c): the CPU side in the front pass, the rest in the workers
|
|
|
|
// The front pass, in the order of the stream, before the workers record (vk_exec_xfer_group on): a counter report or
|
|
// reset (completion callbacks, pending entries, the occlusion segments and counting), a render enable and a render
|
|
// condition (decided here, on the reports replayed so far). Returns the condition's decision: 1 renders.
|
|
int vk_exec_xfer_front(VkExec *fx, uint32_t op, const void *payload) {
|
|
switch (op) {
|
|
case OP_REPORT_COUNTER: report_counter(fx, payload); return 1;
|
|
case OP_RESET_COUNTER: reset_counter(fx, payload); return 1;
|
|
case OP_RENDER_ENABLE: fx->render_disabled = !((const p_flag *)payload)->value; return 1;
|
|
case OP_RENDER_COND:
|
|
fx->render_cond = *(const p_render_cond *)payload;
|
|
fx->has_render_cond = 1;
|
|
fx->render_disabled = !cond_enabled(payload);
|
|
return !fx->render_disabled;
|
|
default: rt_fatal("vk: op %s has no front pass", nvn_op_name(op));
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
// A worker: what the front pass left, in its chunk. The contact shadows' pass (vk_contact_flush) is recorded where
|
|
// the replay would, once every earlier chunk of the group is done.
|
|
static void xfer_worker(VkExec *ex, uint32_t op, const void *payload) {
|
|
if (op != OP_RENDER_ENABLE && vk_contact_pending()) vk_exec_turn(), vk_contact_flush(ex);
|
|
switch (op) {
|
|
case OP_REPORT_COUNTER: case OP_RESET_COUNTER: {
|
|
int type = op == OP_REPORT_COUNTER ? (int)((const p_report_counter *)payload)->type
|
|
: (int)((const p_reset_counter *)payload)->type;
|
|
if (type != NVN_COUNTER_TYPE_SAMPLES_PASSED || !oq_ready()) break;
|
|
oq_close(ex); // into the segment this op ends; the next queries go to the next one
|
|
XW.seg++;
|
|
if (op == OP_RESET_COUNTER) XW.counting = 1, XW.reported = 0;
|
|
else {
|
|
if (XW.reported) XW.keep = 1; // as oq_report
|
|
XW.reported = 1;
|
|
if (!XW.keep) XW.counting = 0;
|
|
}
|
|
break;
|
|
}
|
|
case OP_RENDER_ENABLE: ex->render_disabled = !((const p_flag *)payload)->value; break;
|
|
case OP_RENDER_COND:
|
|
ex->render_cond = *(const p_render_cond *)payload;
|
|
ex->has_render_cond = 1;
|
|
if (XW.i >= XW.ncond) rt_fatal("vk: a replay worker met more render conditions than its front pass decided");
|
|
ex->render_disabled = !XW.cond[XW.i++];
|
|
break;
|
|
default: rt_fatal("vk: op %s in a parallel group", nvn_op_name(op));
|
|
}
|
|
}
|
|
|
|
static void op_xfer_impl(VkExec *ex, uint32_t op, const void *payload) {
|
|
if (XW.on) return xfer_worker(ex, op, payload);
|
|
if (op != OP_RENDER_ENABLE && op != OP_SIGNAL_EVENT) vk_contact_flush(ex);
|
|
switch (op) {
|
|
case OP_COPY_B2B: copy_b2b(ex, payload); break;
|
|
case OP_COPY_B2T: copy_buffer_image(ex, payload, 1); break;
|
|
case OP_COPY_T2B: copy_buffer_image(ex, payload, 0); break; // p_copy_b2t: src holds the destination address
|
|
case OP_COPY_T2T: copy_t2t(ex, payload); break;
|
|
case OP_CLEAR_TEX: clear_tex(ex, payload); break;
|
|
case OP_SIGNAL_EVENT: signal_event(ex, payload); break;
|
|
case OP_REPORT_COUNTER: report_counter(ex, payload); break;
|
|
case OP_RESET_COUNTER: reset_counter(ex, payload); break;
|
|
case OP_CLEAR_BUF: clear_buf(ex, payload); break;
|
|
case OP_RENDER_ENABLE: ex->render_disabled = !((const p_flag *)payload)->value; break;
|
|
case OP_RENDER_COND:
|
|
ex->render_cond = *(const p_render_cond *)payload;
|
|
ex->has_render_cond = 1;
|
|
ex->render_disabled = !cond_enabled(payload);
|
|
break;
|
|
default: rt_fatal("vk: op %s is not a transfer", nvn_op_name(op));
|
|
}
|
|
}
|
|
|
|
// NX2NAVI_REPLAY_TIME: see the note over vk_exec_op_draw in vk_exec_draw.c; transfers are the third kind.
|
|
void vk_exec_op_xfer(VkExec *ex, uint32_t op, const void *payload) {
|
|
static int on = -1;
|
|
if (on < 0) on = cfg_bool("NX2NAVI_REPLAY_TIME");
|
|
if (!on) { op_xfer_impl(ex, op, payload); return; }
|
|
uint64_t t0 = plat_now_ns();
|
|
op_xfer_impl(ex, op, payload);
|
|
perf_replay_op_at(PERF_RP_XFER, op, plat_now_ns() - t0);
|
|
}
|