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()) {