mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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>
104 lines
5.2 KiB
C
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);
|
|
}
|