Files
NX2navi/runtime/vk_present_held.h
chrg6 0e918d4846 Log at exit, and with trace = perf every 600 presents when they moved, how often the game's threads met at the present: frames whose bookkeeping went to the next submission, held presents another thread put out, and the presenting thread's waits for one.
The submit-path changes leave three numbers a play-test needs: how many presents left their frame's bookkeeping to the
next submission because another thread held the submission lock (fo_owed_n) and how many frames a submission caught
up (fo_late_n); how many held presents another thread's submission put out (ph_out compares the thread with the one
that presented, now kept in PhJob.tid); and how often the presenting thread waited for PH.lock while one went out,
with the time in all and the longest (vk_present_held.h's counts). vk_backend.c order_log writes them on one line:
with the synchronous replay at exit (gfx_exit_log, "vk: over the run to frame N: frame bookkeeping: ..."), and with
NX2NAVI_TRACE=perf at every 600th present when a count moved since the last such line ("perf: since the last such
line, at frame N: ..."). With present_order = 0 the bookkeeping counts stay 0. The presenting thread's wait is also in
the slow-frame lines' "drain" since the lock commit. No timing change.

Checks that passed: pass_cc_check of vk_backend.c with no new -Wall -Wextra warnings, present_held_check,
perf_summary_check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:13:02 -05:00

104 lines
5.2 KiB
C

// A present that waits for held work (vk_backend.c "a present that waits for held work"): its state and who may put it
// out. Header only, so that tests/present_held_check.c checks it without a device.
//
// With the replay on the submitting thread the game presents from one guest thread while others submit. A present
// held for event-ordered work goes out after the submission that replays the last of that work, on whichever thread
// made it, at the next present, or after PH_MAX submissions; only the presenting thread arms one. The lock keeps the
// held job whole and lets one thread put it out once. It also keeps the synchronous presents one at a time and in the
// game's order: the present thread takes one picture in waiting (vk_backend.c present_image), so a held present another
// thread puts out has to reach it before the next present does. A submitting thread only tries the lock: when another
// thread holds it, that thread is putting a present out or arming one, and the submitting thread goes on without
// waiting; its next submission, the next present or PH_MAX checks again. The presenting thread waits for the lock
// (ph_lock, counted) only while another thread puts a held present out: at most that one present_image.
#pragma once
#include <stdint.h>
#include "platform/platform.h"
typedef struct {
const void *t; // the game's texture (tex_t)
int crop[4], has_crop;
uint64_t at_ns, queued, frame, since; // since: the replay's submission count when it began to wait
uint32_t tid; // the thread that presented it (for the counts of who put it out)
} PhJob;
enum { PH_AFTER, PH_CAPPED, PH_NEXT, PH_WHY_N }; // why a held present went out: work replayed, PH_MAX, next present
#define PH_NONE (-1) // nothing goes out; for the present callback, one that did not wait
typedef struct {
PlatMutex lock; // job, on, waited and out; the order of the synchronous presents
int32_t on; // a present waits: changed under lock, read without it as a hint (atomic)
PhJob job;
uint64_t waited, out[PH_WHY_N]; // presents that waited; held presents put out, by why
// The presenting thread's waits for the lock: how many, their sum and the longest, in ns. Written under the lock,
// read anywhere (atomics). on_wait (may be NULL) is told when such a wait began (plat_now_ns), once it has ended.
uint64_t lock_waits, lock_wait_ns, lock_wait_max;
void (*on_wait)(uint64_t t0);
} PhState;
// Puts a present out (vk_backend.c present_image), with the lock held. why: PH_AFTER, PH_CAPPED or PH_NEXT for a held
// one, PH_NONE for a present that did not wait.
typedef void (*PhPresent)(const PhJob *j, int why, void *arg);
// Whether a held present goes out after a submission, asked with the lock held: PH_AFTER, PH_CAPPED or PH_NONE.
typedef int (*PhWhy)(const PhJob *j, void *arg);
static inline int ph_armed(const PhState *s) { return __atomic_load_n(&s->on, __ATOMIC_ACQUIRE); }
// The presenting thread's lock: at once when it is free, else a counted wait.
static inline void ph_lock(PhState *s) {
if (plat_mutex_trylock(&s->lock)) return;
uint64_t t0 = plat_now_ns();
plat_mutex_lock(&s->lock);
uint64_t d = plat_now_ns() - t0;
__atomic_add_fetch(&s->lock_waits, 1, __ATOMIC_RELAXED);
__atomic_add_fetch(&s->lock_wait_ns, d, __ATOMIC_RELAXED);
if (d > s->lock_wait_max) __atomic_store_n(&s->lock_wait_max, d, __ATOMIC_RELAXED);
if (s->on_wait) s->on_wait(t0);
}
// A submitting thread, before or after its submission. Never waits; 1 when it put the held present out.
static inline int ph_flush_try(PhState *s, PhWhy why_now, PhPresent present, void *arg) {
if (!ph_armed(s) || !plat_mutex_trylock(&s->lock)) return 0;
int why = s->on ? why_now(&s->job, arg) : PH_NONE;
if (why != PH_NONE) {
__atomic_store_n(&s->on, 0, __ATOMIC_RELAXED);
s->out[why]++;
present(&s->job, why, arg);
}
plat_mutex_unlock(&s->lock);
return why != PH_NONE;
}
// The presenting thread at its next present: a held present goes out now. Waits while another thread puts it out.
static inline int ph_flush_next(PhState *s, PhPresent present, void *arg) {
if (!ph_armed(s)) return 0;
ph_lock(s);
int out = s->on;
if (out) {
__atomic_store_n(&s->on, 0, __ATOMIC_RELAXED);
s->out[PH_NEXT]++;
present(&s->job, PH_NEXT, arg);
}
plat_mutex_unlock(&s->lock);
return out;
}
// The presenting thread: j waits for held work (hold_now(arg), asked with the lock held) or goes out now, in either case
// behind a held present another thread is putting out.
static inline void ph_present(PhState *s, const PhJob *j, int (*hold_now)(void *arg), PhPresent present, void *arg) {
ph_lock(s);
if (s->on) { // only a presenting thread arms: a second presenting thread's held present goes first
__atomic_store_n(&s->on, 0, __ATOMIC_RELAXED);
s->out[PH_NEXT]++;
present(&s->job, PH_NEXT, arg);
}
if (hold_now && hold_now(arg)) {
s->job = *j;
s->waited++;
__atomic_store_n(&s->on, 1, __ATOMIC_RELEASE);
} else {
present(j, PH_NONE, arg);
}
plat_mutex_unlock(&s->lock);
}