mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
With the replay on the submitting thread, a present that waits for event-ordered work (vk_backend.c PH) goes out after
the submission that replays the last of that work, at the next present, or after PH_MAX submissions. The game submits
from several threads while one of them presents, so that submission may be another thread's. PH had no lock: two
submitting threads could both pass ph_flush's check of PH.on and put one frame out twice; a thread reading PH's fields
while the presenting thread armed the next frame could put out a job mixed from two frames; and a held frame put out on
a submitting thread could reach the present thread after the presenting thread's next frame, a picture back in time.
runtime/vk_present_held.h now holds PH's job, its arming, putting out and counts behind PH.lock. Submitting threads
only try the lock (ph_flush_try) and go on when another thread holds it: that thread is putting a present out or arming
one, and the next submission, the next present or PH_MAX checks again, the bound there was before. The presenting
thread takes it for every synchronous present while held presents are on (ph_flush_next, ph_present), so the
synchronous present_image calls run one at a time and in the game's order. That is the one new wait, and only the
presenting thread makes it: only while another thread puts a held present out, for at most that present_image. It is
counted (lock_waits, lock_wait_ns, lock_wait_max) and timed as PERF_DRAIN. No path holds the lock across held_resume,
which is a replay. Neither 0.10.0 fresh log has a held present at all ("went out" and "presents that waited" lines),
so the wait is not expected in normal play. present_held = 0 turns held presents off, and with them every use of the
lock.
Not reproduced before the change. Checks that passed: the new tests/present_held_check (one presenting and three
submitting threads over 200,000 frames: every frame out once, in order and never two at once; the unlocked 0.10.1
shape through the same harness gave about 205,000 presents for 200,000 frames, 6,000 to 8,000 out of order or twice and
8,000 to 12,000 at the same time; step-by-step cases; a submitting thread gives up on a held lock at once and the
presenting thread's wait is counted), pass_cc_check on every runtime file, no new -Wall -Wextra warnings in
vk_backend.c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
103 lines
5.1 KiB
C
103 lines
5.1 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
|
|
} 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);
|
|
}
|