From a34be87715ee4229e2b2a29fe18f3f7c4c3a1835 Mon Sep 17 00:00:00 2001 From: epicmeme546 <178368684+epicmeme546@users.noreply.github.com> Date: Wed, 11 Nov 2026 18:31:42 -0500 Subject: [PATCH] Replay workers take their occlusion query slots in the order of the stream. The chunks of a parallel group took query slots from the ring as they recorded, so the slots of the group's chunks interleaved. The completion thread reads and resets slots in runs that follow the ring, and with interleaved slots it made four times the result calls and a reset per slot: the wait after a frame's first submission ended 3.5 ms after the GPU had run it instead of 0.6 ms, and the replay time the workers saved went into that wait (44 fps with 3 workers as without). The front pass now gives each chunk a block of slots, as many as its draws that count samples and render, in the order of the stream; a segment's slots are taken in ring order. Standing in Hyrule Field, 3 workers: 49.8 fps against 43.7 with the workers off. NX2NAVI_GPU_TIMELINE= writes four frames from that present on to the log: each replay, each timed part's submit and GPU run, the waits for the GPU and the workers' groups, so a run shows whether the GPU waits for the replay, a wait, or the CPU behind one. Co-Authored-By: Claude Opus 5.5 --- runtime/cfg.c | 2 +- runtime/gfx.h | 4 +++ runtime/nvn_api_core.c | 6 +++- runtime/vk_backend.c | 1 + runtime/vk_exec.c | 12 +++++-- runtime/vk_exec.h | 6 +++- runtime/vk_exec_xfer.c | 34 ++++++++++++++---- runtime/vk_sync.c | 78 ++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 132 insertions(+), 11 deletions(-) diff --git a/runtime/cfg.c b/runtime/cfg.c index 5891c043..a06a2265 100644 --- a/runtime/cfg.c +++ b/runtime/cfg.c @@ -45,7 +45,7 @@ static const char *const known[] = { "BIND_ZL", "BIND_ZR", "BLOCK_VIEWS_3D", "CACHE_DIE", "CLOTH_REALTIME", "COLOR_BRIGHTNESS", "COMPUTE_KEEP", "COLOR_CONTRAST", "COLOR_GAMMA", "COLOR_PRESET", "COLOR_SATURATION", "CONTACT_DEBUG", "CONTACT_SHADOWS", "CORE_DIAG", "CRIT", "CRIT_OUT", "DESC_TEMPLATE", "DEVICE_PROFILE", "DISCORD_APP_ID", "DISCORD_IMAGE", "DISCORD_RPC", "DLAA", "DLAA_CAM_KEEP", "DLSS_IDLE_FREE", "DLAA_JITTER", "DLAA_MV", "DLAA_MASKDEBUG", "DLAA_MVDEBUG", "DLAA_OBJMV", "DLAA_SURVEY", "DLAA_TARGET_KEEP", "DLAA_TIMES", "DLSS", "DLSS_CHAR_MASK", "DLSS_PRESET", "DLSS_REACTIVE", "DUMP", "DUMP_BUF", "DUMP_DIR", - "DUMP_LOG", "DUMP_RAW", "DUMP_SHADERS", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", + "DUMP_LOG", "DUMP_RAW", "DUMP_SHADERS", "DUMP_FILE", "DS3", "LOG_CS_BINDS", "LOG_TEX_FLAGS", "LOG_MESH", "MESH", "POOL", "NATIVE_TENSOR", "LOG_RCOND", "LOG_RT_SIZES", "DUMP_WHITE", "DUMP_AFTER", "DUMP_MOVIE", "SUBMIT_FINISH", "ACQUIRE_WAIT", "CRASH_FILTER_GUARD", "CS_FALLBACK", "EXEFS", "FEEDBACK", "FIX_C0", "FIX_ZERO", "FLIGHT", "FLIGHT_AT", "FLIGHT_DIR", "FLIGHT_FRAMES", "GPU_TIMELINE", "FFX_PIPE_CACHE", "FLIGHT_SPIKE_MS", "FLIGHT_THUMBS", "FLIGHT_EVERY", "FLUSH_SPAN", "FLUSH_UPLOAD", "FOG_CULL", "FOLIAGE_SHADOW_CULL", "FOLIAGE_SHADOW_VRS", "FOV", "FOV_PATCH", "FG_HUDLESS_SHOW", "FG_MULT", "FPS60", "FPS_LIMIT", "FRAMEGEN", "FRAMEGEN_MULTIPLIER", "FREEZE_IDLE", "FREEZE_BUDGET_US", "FREE_CORES", "FULLSCREEN", "FXAA", "GAME_ASPECT", "GAME_RES", "GPA", "GPA_EMPTY", "GPA_TWIN", "GPA_TWIN_MAX_KB", "GPL", "GPL_DRAW_BUILD", "GPL_FEEDBACK", "PIPE_AHEAD", "PIPE_FAST_WORKERS", "PIPE_FRAME_WAIT_MS", "PIPE_LINK_DRAW_MS", "PIPE_SKIP_STEADY", "PROG_LEAD", "GPU", "GPU_INDEX", "GPU_PASSES", "GPU_NAME", "HDR", "HDR_GAME_NITS", "HDR_LIVE", "HEAP_LAZY", "HDR_PEAK_NITS", "HDR_UI_NITS", diff --git a/runtime/gfx.h b/runtime/gfx.h index 21250f95..a07d0e7a 100644 --- a/runtime/gfx.h +++ b/runtime/gfx.h @@ -18,6 +18,10 @@ const OvNvState *ov_nv_state(void); // ov_gfx.c // ---- window, vsync, focus (gfx_present.c and the window back end, gfx_wsys.h; vk_device.c). Host threads only; never run guest code. void gfx_window_start(void); // window thread, Vulkan device, swapchain; idempotent +// NX2NAVI_GPU_TIMELINE= (vk_sync.c "frame timeline"): an event of the frames it records, at CPU time t (0: now) +// with two values; gfx_tl_present counts the presented frames and writes the record to the log when it is complete. +void gfx_tl_note(int kind, uint64_t t, uint64_t a, uint64_t b); +void gfx_tl_present(void); void gfx_vsync_bind(uint64_t system_event); // nn::vi vsync event; signalled at 60 Hz uint64_t gfx_vsync_ticks(void); // ticks since start uint64_t gfx_vsync_time_ns(uint64_t tick); // kern_now_ns() time of a tick (pacing) diff --git a/runtime/nvn_api_core.c b/runtime/nvn_api_core.c index 83000367..1a7e46ee 100644 --- a/runtime/nvn_api_core.c +++ b/runtime/nvn_api_core.c @@ -712,7 +712,11 @@ NVN_API(nvnQueueSubmitCommands) { // several guest threads submit at once (the pause menu's first frame on): a plain ++ lost counts, and with one the // frame's first submission went without its wait, or a later one waited int idx = __atomic_fetch_add(&nvn_submits_in_frame, 1, __ATOMIC_RELAXED); - if (finish_mask && f && (finish_mask == -1 || (idx < 63 && (finish_mask >> idx & 1)))) gfx_backend()->exec_wait(f, UINT64_MAX); + if (finish_mask && f && (finish_mask == -1 || (idx < 63 && (finish_mask >> idx & 1)))) { + gfx_tl_note('W', 0, f, 0); + gfx_backend()->exec_wait(f, UINT64_MAX); + gfx_tl_note('w', 0, 0, 0); + } nvn_call_note(0, (uintptr_t)q, 0); } diff --git a/runtime/vk_backend.c b/runtime/vk_backend.c index 063c008d..1893f6fc 100644 --- a/runtime/vk_backend.c +++ b/runtime/vk_backend.c @@ -536,6 +536,7 @@ void gfx_exit_log(void) { static void vk_be_present(const tex_t *t, const int crop[4], uint64_t at_ns) { vk_sync_start(); + gfx_tl_present(); vk_dlssg_game_present(); if (!gfx_replay_async()) { uint64_t frame = __atomic_add_fetch(&gfx_counters.frames, 1, __ATOMIC_RELAXED); diff --git a/runtime/vk_exec.c b/runtime/vk_exec.c index 4abc67ee..f8ad25ee 100644 --- a/runtime/vk_exec.c +++ b/runtime/vk_exec.c @@ -56,6 +56,7 @@ typedef struct { uint32_t nops; // the ops it records (JUMPs not counted) uint32_t cond0, ncond; // its render conditions' decisions in G.cond uint64_t oq; // the occlusion counting at its first op (vk_exec_xfer_chunk_state) + uint32_t slot0, nslots; // its block of occlusion query slots (vk_exec_xfer_slots) int idx; // in its group: 0 runs the group's pass hooks VkCommandBuffer cb; int recorded; @@ -1883,6 +1884,7 @@ static Chunk *chunk_open(const VkExec *ex, const op_hdr *h, int idx) { Chunk *c = &G.c[idx]; memcpy(&c->ex, ex, sizeof *ex); c->h = h, c->nops = 0, c->cond0 = G.ncond, c->ncond = 0, c->idx = idx, c->oq = vk_exec_xfer_chunk_state(); + c->nslots = 0; return c; } @@ -1893,7 +1895,7 @@ static void chunk_record(Chunk *c, int pool) { VkExec *ex = &c->ex, *was = x_cur; if (pool < RW.n) ex->dc = RW.w[pool]->dc; // the submitting thread records with its own (the state's) x_cur = ex, rw_chunk = c, rw_ordered = !c->idx; - vk_exec_xfer_worker(1, G.cond + c->cond0, c->ncond, c->oq); + vk_exec_xfer_worker(1, G.cond + c->cond0, c->ncond, c->oq, c->slot0, c->nslots); ex->cb = vk_sync_worker_begin(pool); // the replay's command buffer before the group ended behind a full barrier, with every image in GENERAL ex->recorded = 0, ex->barrier_at = 0, ex->in_pass = 0, ex->nfbl = 0, ex->mark = NULL; @@ -1925,7 +1927,7 @@ static void chunk_record(Chunk *c, int pool) { free(ex->views); ex->views = NULL; c->cb = ex->cb, c->recorded = ex->recorded; - vk_exec_xfer_worker(0, NULL, 0, 0); + vk_exec_xfer_worker(0, NULL, 0, 0, 0, 0); x_cur = was, rw_chunk = NULL; __atomic_store_n(&c->done, 1, __ATOMIC_RELEASE); } @@ -1955,6 +1957,7 @@ static const op_hdr *group_start(VkExec *ex, const op_hdr *h, int depth) { return h; } uint64_t t0 = kern_now_ns(); + gfx_tl_note('g', 0, draws, (uint64_t)n); vk_exec_barrier(ex); // the command buffer so far ends behind a full barrier, every image back in GENERAL vk_exec_draw_group_begin(ex); // the front pass @@ -1972,6 +1975,7 @@ static const op_hdr *group_start(VkExec *ex, const op_hdr *h, int depth) { if (cut) c = chunk_open(ex, o, nc++), cd = 0, cut = 0; switch (o->op) { case OP_DRAW: case OP_DRAW_IDX: case OP_DRAW_IDX_IND: + if (vk_exec_xfer_counting() && !ex->render_disabled) c->nslots++; // at most one query each if (++cd == per && nc < n) cut = 1; break; case OP_CLEAR_COLOR: case OP_CLEAR_DS: break; // recorded by the chunk @@ -1993,6 +1997,7 @@ static const op_hdr *group_start(VkExec *ex, const op_hdr *h, int depth) { o = nvn_op_next(o); } vk_exec_xfer_group(0); // the segments stay listed for the workers until the next group + for (int i = 0; i < nc; i++) G.c[i].slot0 = vk_exec_xfer_slots(G.c[i].nslots); // in the order of the stream // handed out: the workers and this thread take the chunks in order for (int i = 0; i < nc; i++) G.c[i].done = 0; G.hooks = 0; @@ -2010,6 +2015,7 @@ static const op_hdr *group_start(VkExec *ex, const op_hdr *h, int depth) { int rec = 0; for (int i = 0; i < nc; i++) cbs[i] = G.c[i].cb, rec += G.c[i].recorded; ex->cb = vk_sync_append(cbs, nc); + gfx_tl_note('h', 0, 0, 0); ex->recorded += rec, ex->barrier_at = ex->recorded; // the last chunk ended with a full barrier ex->in_pass = 0, ex->nfbl = 0, ex->mark = NULL, ex->dirty = VK_EXEC_DIRTY_ALL, ex->rt_resolved = 0, ex->fb_fs = NULL; vk_exec_draw_new_cb(); @@ -2100,6 +2106,7 @@ static void rec_claim(void) { int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t fence) { if (vk_device_is_lost()) return 0; // the run is ending (vk_device.c vk_device_lost_check): nothing is recorded uint64_t in_ns = kern_now_ns(); + gfx_tl_note('R', 0, (uint64_t)n, fence); perf_lock_timing_here(); // NX2NAVI_LOCK_TIME if (prof_replay_tid != plat_thread_id()) perf_replay_thread(); // the profiler follows the thread replaying now perf_rec_waits = rec_wait_totals; @@ -2231,5 +2238,6 @@ int vk_exec_record(VkCommandBuffer cb, const uint64_t *handles, int n, uint64_t } rec_spent_ns += kern_now_ns() - in_ns; if (rec_spent_ns >= REC_CLAIM_NS) rec_claim(); + gfx_tl_note('r', 0, (uint64_t)(ex->recorded - part_start), 0); return ex->recorded - part_start; // what the last part holds } diff --git a/runtime/vk_exec.h b/runtime/vk_exec.h index 6aba301d..4cf4f25e 100644 --- a/runtime/vk_exec.h +++ b/runtime/vk_exec.h @@ -163,7 +163,11 @@ void vk_exec_draw_group_begin(VkExec *ex); void vk_exec_xfer_group(int on); int vk_exec_xfer_front(VkExec *fx, uint32_t op, const void *payload); uint64_t vk_exec_xfer_chunk_state(void); -void vk_exec_xfer_worker(int on, const uint8_t *cond, uint32_t ncond, uint64_t state); +void vk_exec_xfer_worker(int on, const uint8_t *cond, uint32_t ncond, uint64_t state, uint32_t slot0, uint32_t nslots); +// The front pass: whether a draw here counts samples (a query slot), and n slots of the ring in the order of the stream +// for one chunk (the first is returned; vk_exec_xfer_worker hands them to the chunk's worker). +int vk_exec_xfer_counting(void); +uint32_t vk_exec_xfer_slots(uint32_t n); // vk_exec_draw.c: forget what the command buffer being recorded has bound (pipeline, sets, dynamic state, vertex // buffers): the replay continues in a new one (vk_sync_split). void vk_exec_draw_new_cb(void); diff --git a/runtime/vk_exec_xfer.c b/runtime/vk_exec_xfer.c index 47b519e2..df63b238 100644 --- a/runtime/vk_exec_xfer.c +++ b/runtime/vk_exec_xfer.c @@ -228,11 +228,16 @@ typedef struct { uint64_t addr, seq; int reset; volatile int32_t lost; uint32_t // 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 atomically and adds the slots it closes under oq_seg_lock. XW.cond: the decisions of its chunk's render -// conditions, made by the front pass in the order of the stream. +// ring slots from its chunk's block and adds the slots it closes under oq_seg_lock. 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; - uint32_t seg; + uint32_t seg, qnext, qend; const uint8_t *cond; uint32_t ncond, i; } XW = {.open = -1}; @@ -244,12 +249,21 @@ void vk_exec_xfer_group(int on) { // on: a new group's front pass begins; off: GS.on = on; } +int vk_exec_xfer_counting(void) { return oq_counting; } + +uint32_t vk_exec_xfer_slots(uint32_t n) { + uint32_t first = oq_next; + oq_next += n; + return first; +} + uint64_t vk_exec_xfer_chunk_state(void) { return (uint64_t)GS.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) { +void vk_exec_xfer_worker(int on, const uint8_t *cond, uint32_t ncond, uint64_t state, uint32_t slot0, uint32_t nslots) { XW.on = on, XW.open = -1, XW.cond = cond, XW.ncond = ncond, XW.i = 0; + 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); } @@ -319,8 +333,9 @@ static void oq_close(VkExec *ex) { 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 its slot at once (a busy one is lost, not tried again): the others take theirs meanwhile - uint32_t q = (XW.on ? __atomic_fetch_add(&oq_next, 1, __ATOMIC_RELAXED) : oq_next) % OQ_N; + // a worker takes the next slot of its chunk's block, past it one from the ring at once; a busy one is lost, not + // tried again + uint32_t q = (!XW.on ? oq_next : 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.n) __atomic_store_n(&GS.s[XW.seg]->lost, 1, __ATOMIC_RELAXED); @@ -415,6 +430,13 @@ 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 diff --git a/runtime/vk_sync.c b/runtime/vk_sync.c index 62e24591..37a5591d 100644 --- a/runtime/vk_sync.c +++ b/runtime/vk_sync.c @@ -405,6 +405,7 @@ static void ts_read_cb(void *arg); // idle time with work queued, 7-8 ms a frame over PCIe 3.0 in Kokiri Forest, and the frame as CPU-bound. static void ts_submitted(void) { uint64_t now = plat_now_ns(); + gfx_tl_note('S', now, cur_fence, 0); for (int pass = 0; pass < 2; pass++) for (int i = 0; i < ts_n_unsent; i++) { TsRead *t = ts_unsent[i]; @@ -432,6 +433,7 @@ static void ts_read_cb(void *arg) { // completion thread, in submission order max64(&ts_max_gap_ns, gap); } if (sub && sn > sub) plat_add64(&ts_queued_ns, sn - sub); + gfx_tl_note('G', (uint64_t)sn, (uint64_t)en, (uint64_t)sub); if (en > ts_last_end_ns) ts_last_end_ns = en; } uint64_t from = s > ts_last_end ? s : ts_last_end; @@ -450,6 +452,82 @@ static void ts_read_cb(void *arg) { // completion thread, in submission order free(t); } +// ---- frame timeline +// NX2NAVI_GPU_TIMELINE=: from that presented frame on, TL_FRAMES frames of what the CPU and the GPU did go to the +// log once they are complete, in milliseconds from the first of those presents: each replay's begin and end and its +// thread, each timed part's submit, GPU start and end (calibrated timestamps, so with NX2NAVI_TRACE=perf), the waits for +// the GPU after a frame's first submission, the replay workers' groups and the presents. It tells whether the GPU waits +// for the replay, for a wait, or for the CPU behind one. Off (and one load per note) without the setting. +#define TL_CAP 16384 +#define TL_FRAMES 4 +typedef struct { uint64_t t, a, b; uint32_t tid; int kind; } TlEv; +static TlEv *tl_ev; +static volatile int32_t tl_n, tl_rec; +static int tl_on = -1; +static uint64_t tl_from, tl_frames; + +void gfx_tl_note(int kind, uint64_t t, uint64_t a, uint64_t b) { + if (!__atomic_load_n(&tl_rec, __ATOMIC_ACQUIRE)) return; + int32_t i = plat_inc32(&tl_n) - 1; + if (i < TL_CAP) tl_ev[i] = (TlEv){t ? t : plat_now_ns(), a, b, plat_thread_id(), kind}; +} + +static int tl_cmp(const void *x, const void *y) { + uint64_t a = ((const TlEv *)x)->t, b = ((const TlEv *)y)->t; + return a < b ? -1 : a > b; +} + +static void tl_dump(void) { + int n = tl_n < TL_CAP ? tl_n : TL_CAP; + qsort(tl_ev, (size_t)n, sizeof *tl_ev, tl_cmp); + uint64_t t0 = 0, t1 = UINT64_MAX; + for (int i = 0; i < n; i++) + if (tl_ev[i].kind == 'P') { + if (tl_ev[i].a == tl_from) t0 = tl_ev[i].t; + if (tl_ev[i].a == tl_from + TL_FRAMES) t1 = tl_ev[i].t; + } + rt_log("vk: timeline of frames %llu-%llu (%d events), ms from the first present:", (unsigned long long)tl_from, + (unsigned long long)(tl_from + TL_FRAMES), n); + for (int i = 0; i < n; i++) { + const TlEv *e = &tl_ev[i]; + if (e->t < t0 || e->t > t1) continue; + double at = (double)(int64_t)(e->t - t0) / 1e6; + switch (e->kind) { + case 'P': rt_log("vk: tl %8.3f t%-5u present, frame %llu", at, e->tid, (unsigned long long)e->a); break; + case 'R': rt_log("vk: tl %8.3f t%-5u replay begins: %llu handles, value %llu", at, e->tid, + (unsigned long long)e->a, (unsigned long long)e->b); break; + case 'r': rt_log("vk: tl %8.3f t%-5u replay ends: %llu commands in the last part", at, e->tid, + (unsigned long long)e->a); break; + case 'W': rt_log("vk: tl %8.3f t%-5u waits for the GPU to run value %llu", at, e->tid, (unsigned long long)e->a); break; + case 'w': rt_log("vk: tl %8.3f t%-5u wait ends", at, e->tid); break; + case 'g': rt_log("vk: tl %8.3f t%-5u group of %llu draws in %llu chunks", at, e->tid, (unsigned long long)e->a, + (unsigned long long)e->b); break; + case 'h': rt_log("vk: tl %8.3f t%-5u group recorded", at, e->tid); break; + case 'S': rt_log("vk: tl %8.3f t%-5u part submitted, value %llu", at, e->tid, (unsigned long long)e->a); break; + case 'G': rt_log("vk: tl %8.3f gpu part runs %.3f ms (submitted %.3f ms before)", at, + (double)(e->a - e->t) / 1e6, (double)(int64_t)(e->t - e->b) / 1e6); break; + default: break; + } + } +} + +void gfx_tl_present(void) { + if (tl_on < 0) { + const char *e = cfg_get("NX2NAVI_GPU_TIMELINE"); + tl_from = e && *e ? strtoull(e, NULL, 0) : 0; + tl_on = tl_from && (tl_ev = calloc(TL_CAP, sizeof *tl_ev)) != NULL; + } + if (!tl_on) return; + uint64_t f = ++tl_frames; + if (f == tl_from) __atomic_store_n(&tl_rec, 1, __ATOMIC_RELEASE); + gfx_tl_note('P', 0, f, 0); + if (f == tl_from + TL_FRAMES + 3) { // the parts of the last frame have been read back by now + __atomic_store_n(&tl_rec, 0, __ATOMIC_SEQ_CST); + tl_dump(); + tl_on = 0; + } +} + // At the start of a command buffer (sub_lock held): the pair it times, -1 untimed. static int ts_open(VkCommandBuffer cb) { if (ts_state != 1 || !perf_gpu_timing()) {