mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
Put a held present out under a lock of its own that submitting threads only try, so that it goes out once, whole and before the next present.
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>
This commit is contained in:
@@ -767,6 +767,13 @@ A change to the copy of each event-ordered queue's first held wait (`runtime/vk_
|
||||
`vk_exec_held_ready`) passes `python tests\evo_peek_check.py`, which builds the header alone: one writer republishes
|
||||
while three readers copy, no copy mixes two publishes, and no reader waits for the writer. No GPU, no game files.
|
||||
|
||||
A change to the present that waits for held work (`runtime/vk_present_held.h`, `runtime/vk_backend.c` `submit_sync`,
|
||||
`vk_be_present`, `ph_out`) passes `python tests\present_held_check.py`, which builds the header with the platform layer:
|
||||
one presenting and three submitting threads over 200,000 frames, every frame out once, in order and never two at once,
|
||||
each reason a held present goes out seen; step by step what keeps and what puts out a held present; and with the lock
|
||||
held elsewhere a submitting thread gives up at once while the presenting thread waits and counts its wait. No GPU, no
|
||||
game files.
|
||||
|
||||
A change to how `runtime/memory.c` `mem_copy_readable` reads the lazy heap (`NX2NAVI_HEAP_LAZY=1`) passes
|
||||
`python tests\heap_lazy_copy_check.py`, which builds `memory.c` with the Windows platform layer and its fault handler:
|
||||
every copy returns, a heap page nothing touched yet reads zero and stays reserved, and pages past the heap's end are
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "vk_dlssg.h"
|
||||
#include "vk_fg.h"
|
||||
#include "vk_lowlat.h"
|
||||
#include "vk_present_held.h"
|
||||
|
||||
#define WEAK __attribute__((weak))
|
||||
|
||||
@@ -490,15 +491,17 @@ uint64_t gfx_replayed_submits(void) {
|
||||
// first, in a submission of its own (held_resume); if work is still held, the present waits and goes out right after
|
||||
// the submission that replays the last of it, at the next present, or after PH_MAX submissions at the latest. Only with
|
||||
// the replay on the submitting thread, as the event order. NX2NAVI_PRESENT_HELD=0: every present goes out at once.
|
||||
// The game presents from one thread while others submit, so the submission that puts a held present out may be another
|
||||
// thread's, while the presenting thread goes on to its next present. vk_present_held.h keeps that whole: one lock
|
||||
// (PH.lock) around arming, putting out and every synchronous present while held presents are on, so a held frame goes
|
||||
// out once, in one piece and before the next one. A submitting thread only tries it and never waits; the presenting
|
||||
// thread waits for it only while another thread is putting a held present out (counted, and timed as PERF_DRAIN). No
|
||||
// path holds it across held_resume, which is a replay; present_image's callees take sub_lock only by trying it.
|
||||
#define PH_MAX 8
|
||||
static struct {
|
||||
int on; // a present waits
|
||||
const tex_t *t;
|
||||
int crop[4], has_crop;
|
||||
uint64_t at_ns, queued, frame, since; // since: vk_exec_submit_seq when it began to wait
|
||||
uint64_t waited, ran, after, capped, next, logged; // presents that waited; held work run first at a present;
|
||||
// presents out after the held work, at PH_MAX, at the next present
|
||||
} PH;
|
||||
static void ph_lock_waited(uint64_t t0) { perf_end(PERF_DRAIN, t0); } // perf_begin's clock is plat_now_ns's
|
||||
static PhState PH = {.lock = PLAT_MUTEX_INIT, .on_wait = ph_lock_waited};
|
||||
static uint64_t ph_ran; // held work run at a present (held_resume), on any thread: atomic
|
||||
static uint64_t ph_logged; // PH.lock: "went out" lines so far
|
||||
|
||||
static int present_held_on(void) {
|
||||
static int on = -1;
|
||||
@@ -510,37 +513,53 @@ static uint64_t submit_sync(const uint64_t *handles, int n);
|
||||
|
||||
// The held work that can go on now, in a submission of its own (vk_exec_record syncs it like any other: held_sync).
|
||||
// The present thread first finishes what it was handed: its empty submission replays held work and frees what it
|
||||
// replayed (vk_exec_record), which this walk reads. A present ph_flush handed it a moment ago is such a case.
|
||||
// replayed (vk_exec_record), which this walk reads. A present ph_out handed it a moment ago is such a case.
|
||||
static int held_resume(void) {
|
||||
vk_present_drain();
|
||||
if (!vk_exec_held() || !vk_exec_held_ready()) return 0;
|
||||
submit_sync(NULL, 0);
|
||||
PH.ran++;
|
||||
__atomic_add_fetch(&ph_ran, 1, __ATOMIC_RELAXED);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void ph_flush(uint64_t *why) {
|
||||
if (!PH.on) return;
|
||||
PH.on = 0, (*why)++;
|
||||
if (PH.logged < 16)
|
||||
PH.logged++, rt_log("vk: the present of frame %llu went out %llu submission(s) after the game's present call, %d "
|
||||
"held submissions left (%s)", (unsigned long long)PH.frame,
|
||||
(unsigned long long)(vk_exec_submit_seq_now() - PH.since), vk_exec_held(),
|
||||
why == &PH.after ? "the held work was replayed" : why == &PH.capped ? "PH_MAX reached"
|
||||
// vk_present_held.h's present callback, with PH.lock held: a held present (why PH_AFTER, PH_CAPPED, PH_NEXT) or one
|
||||
// that did not wait (PH_NONE) goes to present_image.
|
||||
static void ph_out(const PhJob *j, int why, void *arg) {
|
||||
(void)arg;
|
||||
if (why != PH_NONE && ph_logged < 16)
|
||||
ph_logged++, rt_log("vk: the present of frame %llu went out %llu submission(s) after the game's present call, %d "
|
||||
"held submissions left (%s)", (unsigned long long)j->frame,
|
||||
(unsigned long long)(vk_exec_submit_seq_now() - j->since), vk_exec_held(),
|
||||
why == PH_AFTER ? "the held work was replayed" : why == PH_CAPPED ? "PH_MAX reached"
|
||||
: "the next present came");
|
||||
present_image(PH.t, PH.has_crop ? PH.crop : NULL, PH.at_ns, PH.queued, PH.frame, 0);
|
||||
if (PH.waited % 600 == 0)
|
||||
present_image(j->t, j->has_crop ? j->crop : NULL, j->at_ns, j->queued, j->frame, 0);
|
||||
if (why != PH_NONE && PH.waited % 600 == 0)
|
||||
rt_log("vk: presents that waited for held work: %llu (held work run at the present %llu times; out after it %llu, "
|
||||
"at %d submissions %llu, at the next present %llu)", (unsigned long long)PH.waited,
|
||||
(unsigned long long)PH.ran, (unsigned long long)PH.after, PH_MAX, (unsigned long long)PH.capped,
|
||||
(unsigned long long)PH.next);
|
||||
(unsigned long long)__atomic_load_n(&ph_ran, __ATOMIC_RELAXED), (unsigned long long)PH.out[PH_AFTER],
|
||||
PH_MAX, (unsigned long long)PH.out[PH_CAPPED], (unsigned long long)PH.out[PH_NEXT]);
|
||||
}
|
||||
|
||||
// Asked with PH.lock held, after a submission that ran held work first (before) or after this thread's submission:
|
||||
// whether the held present goes out now.
|
||||
static int ph_why_before(const PhJob *j, void *arg) {
|
||||
(void)j, (void)arg;
|
||||
return vk_exec_held() ? PH_NONE : PH_AFTER;
|
||||
}
|
||||
static int ph_why_after(const PhJob *j, void *arg) {
|
||||
(void)arg;
|
||||
return !vk_exec_held() ? PH_AFTER : vk_exec_submit_seq_now() - j->since >= PH_MAX ? PH_CAPPED : PH_NONE;
|
||||
}
|
||||
static int ph_hold(void *arg) { // the presenting thread, PH.lock held: work is still held, so its present waits
|
||||
(void)arg;
|
||||
return vk_exec_held() != 0;
|
||||
}
|
||||
|
||||
static uint64_t submit_sync(const uint64_t *handles, int n) {
|
||||
vk_sync_start();
|
||||
if (PH.on && n) { // a present waits: what can go on goes first, then the present, then this submission
|
||||
if (n && ph_armed(&PH)) { // a present waits: what can go on goes first, then the present, then this submission
|
||||
held_resume();
|
||||
if (!vk_exec_held()) ph_flush(&PH.after);
|
||||
ph_flush_try(&PH, ph_why_before, ph_out, NULL);
|
||||
}
|
||||
vk_present_drain();
|
||||
vk_exec_gate_wait(handles, n); // a submission that begins with a wait for the CPU's signal: before its copies
|
||||
@@ -550,10 +569,7 @@ static uint64_t submit_sync(const uint64_t *handles, int n) {
|
||||
// the CPU signals"): it goes on now, in a submission of its own with copies taken after the signal, and that
|
||||
// submission's value is the one the game's fence stands for.
|
||||
if (n && vk_exec_event_at_sync() && vk_exec_held() && vk_exec_held_ready()) f = submit_sync(NULL, 0);
|
||||
if (PH.on && n) {
|
||||
if (!vk_exec_held()) ph_flush(&PH.after);
|
||||
else if (vk_exec_submit_seq_now() - PH.since >= PH_MAX) ph_flush(&PH.capped);
|
||||
}
|
||||
if (n) ph_flush_try(&PH, ph_why_after, ph_out, NULL); // at once unless a present waits
|
||||
return f;
|
||||
}
|
||||
|
||||
@@ -732,17 +748,16 @@ static void vk_be_present(const tex_t *t, const int crop[4], uint64_t at_ns) {
|
||||
perf_present(frame); // before vk_exec_frame_log, which resets the frame's exec counts the flight recorder reads
|
||||
uint64_t queued = kern_now_ns();
|
||||
if (present_held_on()) { // present_held
|
||||
PhJob j = {.t = t, .has_crop = crop != NULL, .at_ns = at_ns, .queued = queued, .frame = frame};
|
||||
if (crop) memcpy(j.crop, crop, sizeof j.crop);
|
||||
vk_present_drain(); // the present thread's empty submission may run held work: done before it is read
|
||||
if (PH.on) held_resume(), ph_flush(&PH.next); // the last present still waits: out now
|
||||
if (ph_armed(&PH)) held_resume(), ph_flush_next(&PH, ph_out, NULL); // the last present still waits: out now
|
||||
if (vk_exec_held()) held_resume();
|
||||
if (vk_exec_held()) {
|
||||
PH.on = 1, PH.t = t, PH.has_crop = crop != NULL, PH.at_ns = at_ns, PH.queued = queued, PH.frame = frame;
|
||||
if (crop) memcpy(PH.crop, crop, sizeof PH.crop);
|
||||
PH.since = vk_exec_submit_seq_now(), PH.waited++;
|
||||
return;
|
||||
}
|
||||
j.since = vk_exec_submit_seq_now();
|
||||
ph_present(&PH, &j, ph_hold, ph_out, NULL); // waits for the held work, or goes out now
|
||||
return;
|
||||
}
|
||||
present_image(t, crop, at_ns, queued, frame, 0);
|
||||
present_image(t, crop, at_ns, queued, frame, 0); // only this thread presents: no lock
|
||||
return;
|
||||
}
|
||||
// Frame bound: the replay thread handled the previous present before this one is queued.
|
||||
|
||||
102
runtime/vk_present_held.h
Normal file
102
runtime/vk_present_held.h
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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);
|
||||
}
|
||||
221
tests/present_held_check.c
Normal file
221
tests/present_held_check.c
Normal file
@@ -0,0 +1,221 @@
|
||||
// tests/present_held_check.py builds this: runtime/vk_present_held.h, the present that waits for held work, with one
|
||||
// presenting thread and three submitting threads. Every frame must reach the present callback exactly once and in
|
||||
// frame order, never two callbacks at once, and a submitting thread must never wait for the lock. "old" runs the
|
||||
// unlocked check-then-clear of 0.10.1 through the same harness and only prints what it sees, to show that the harness
|
||||
// catches the race.
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "vk_present_held.h"
|
||||
|
||||
#define FRAMES 200000
|
||||
#define SUBMITTERS 3
|
||||
#define PH_MAX 8
|
||||
|
||||
static int fails;
|
||||
#define CHECK(c, ...) do { if (!(c)) { printf("present_held_check: FAIL: " __VA_ARGS__); printf("\n"); fails++; } } while (0)
|
||||
|
||||
static int old_mode;
|
||||
static volatile int32_t inside, stop;
|
||||
static volatile uint64_t last_frame, presents, dup_or_order, overlap, held_presents, seq;
|
||||
static volatile int32_t held;
|
||||
static uint64_t rng_state = 0x9E3779B97F4A7C15ull;
|
||||
|
||||
static uint32_t rnd(uint64_t *s) {
|
||||
*s ^= *s << 13, *s ^= *s >> 7, *s ^= *s << 17;
|
||||
return (uint32_t)*s;
|
||||
}
|
||||
|
||||
static void spin(uint32_t n) {
|
||||
for (volatile uint32_t i = 0; i < n; i++) {}
|
||||
}
|
||||
|
||||
// vk_backend.c ph_out's stand-in: present_image's work is the spin, the window a second caller would fall into.
|
||||
static void present_cb(const PhJob *j, int why, void *arg) {
|
||||
(void)arg;
|
||||
if (__atomic_exchange_n(&inside, 1, __ATOMIC_ACQ_REL)) __atomic_add_fetch(&overlap, 1, __ATOMIC_RELAXED);
|
||||
uint64_t f = j->frame;
|
||||
uint64_t was = __atomic_load_n(&last_frame, __ATOMIC_ACQUIRE);
|
||||
if (f != was + 1) __atomic_add_fetch(&dup_or_order, 1, __ATOMIC_RELAXED);
|
||||
spin(200);
|
||||
__atomic_store_n(&last_frame, f, __ATOMIC_RELEASE);
|
||||
__atomic_add_fetch(&presents, 1, __ATOMIC_RELAXED);
|
||||
if (why != PH_NONE) __atomic_add_fetch(&held_presents, 1, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&inside, 0, __ATOMIC_RELEASE);
|
||||
}
|
||||
|
||||
// vk_backend.c ph_why_after: out when nothing is held any more, or PH_MAX submissions after it began to wait.
|
||||
static int why_after(const PhJob *j, void *arg) {
|
||||
(void)arg;
|
||||
if (!__atomic_load_n(&held, __ATOMIC_ACQUIRE)) return PH_AFTER;
|
||||
return __atomic_load_n(&seq, __ATOMIC_RELAXED) - j->since >= PH_MAX ? PH_CAPPED : PH_NONE;
|
||||
}
|
||||
|
||||
static int hold_now(void *arg) {
|
||||
(void)arg;
|
||||
return __atomic_load_n(&held, __ATOMIC_ACQUIRE) > 0;
|
||||
}
|
||||
|
||||
static PhState S = {.lock = PLAT_MUTEX_INIT};
|
||||
|
||||
// ---- the 0.10.1 shape (vk_backend.c ph_flush before vk_present_held.h), for the harness's own check
|
||||
static struct { int on; PhJob job; } OLD;
|
||||
static void old_flush(int why) {
|
||||
if (!OLD.on) return;
|
||||
OLD.on = 0;
|
||||
present_cb(&OLD.job, why, NULL);
|
||||
}
|
||||
|
||||
static void submitter(void *arg) {
|
||||
uint64_t r = (uint64_t)(uintptr_t)arg * 0x2545F4914F6CDD1Dull + 1;
|
||||
while (!__atomic_load_n(&stop, __ATOMIC_ACQUIRE)) {
|
||||
uint64_t q = __atomic_add_fetch(&seq, 1, __ATOMIC_RELAXED);
|
||||
uint32_t x = rnd(&r);
|
||||
if (x % 5 == 0) __atomic_add_fetch(&held, 1, __ATOMIC_RELEASE);
|
||||
if (x % 3 == 0) __atomic_store_n(&held, 0, __ATOMIC_RELEASE);
|
||||
if (old_mode) {
|
||||
if (OLD.on) {
|
||||
if (!__atomic_load_n(&held, __ATOMIC_ACQUIRE)) old_flush(PH_AFTER);
|
||||
else if (q - OLD.job.since >= PH_MAX) old_flush(PH_CAPPED);
|
||||
}
|
||||
} else {
|
||||
ph_flush_try(&S, why_after, present_cb, NULL);
|
||||
}
|
||||
spin(rnd(&r) % 400);
|
||||
}
|
||||
}
|
||||
|
||||
static void stress(void) {
|
||||
PlatThread *t[SUBMITTERS];
|
||||
for (int i = 0; i < SUBMITTERS; i++) t[i] = plat_thread_new(submitter, (void *)(uintptr_t)(i + 1), "submit");
|
||||
for (uint64_t f = 1; f <= FRAMES; f++) {
|
||||
uint32_t x = rnd(&rng_state);
|
||||
PhJob j = {.frame = f};
|
||||
if (old_mode) {
|
||||
if (OLD.on) old_flush(PH_NEXT);
|
||||
if (x % 2) __atomic_store_n(&held, 0, __ATOMIC_RELEASE); // held_resume ran what it could
|
||||
if (__atomic_load_n(&held, __ATOMIC_ACQUIRE)) {
|
||||
OLD.job = j, OLD.job.since = __atomic_load_n(&seq, __ATOMIC_RELAXED);
|
||||
OLD.on = 1;
|
||||
} else {
|
||||
present_cb(&j, PH_NONE, NULL);
|
||||
}
|
||||
} else { // vk_backend.c vk_be_present's synchronous branch
|
||||
ph_flush_next(&S, present_cb, NULL);
|
||||
if (x % 2) __atomic_store_n(&held, 0, __ATOMIC_RELEASE);
|
||||
j.since = __atomic_load_n(&seq, __ATOMIC_RELAXED);
|
||||
ph_present(&S, &j, hold_now, present_cb, NULL);
|
||||
}
|
||||
spin(x % 600);
|
||||
}
|
||||
__atomic_store_n(&stop, 1, __ATOMIC_RELEASE);
|
||||
for (int i = 0; i < SUBMITTERS; i++) plat_thread_join(t[i]);
|
||||
if (old_mode) {
|
||||
old_flush(PH_NEXT);
|
||||
printf("present_held_check: old (prints only): %llu presents for %d frames, %llu of them held first; %llu out of "
|
||||
"order or twice, %llu while another one ran\n", (unsigned long long)presents, FRAMES,
|
||||
(unsigned long long)held_presents, (unsigned long long)dup_or_order, (unsigned long long)overlap);
|
||||
return;
|
||||
}
|
||||
ph_flush_next(&S, present_cb, NULL);
|
||||
printf("present_held_check: new: %llu presents for %d frames, %llu of them held first (waited %llu; out after the "
|
||||
"work %llu, at PH_MAX %llu, at the next present %llu); %llu out of order or twice, %llu while another one "
|
||||
"ran; the presenting thread waited for the lock %llu times\n", (unsigned long long)presents, FRAMES,
|
||||
(unsigned long long)held_presents, (unsigned long long)S.waited, (unsigned long long)S.out[PH_AFTER],
|
||||
(unsigned long long)S.out[PH_CAPPED], (unsigned long long)S.out[PH_NEXT], (unsigned long long)dup_or_order,
|
||||
(unsigned long long)overlap, (unsigned long long)S.lock_waits);
|
||||
CHECK(presents == FRAMES, "%llu presents for %d frames", (unsigned long long)presents, FRAMES);
|
||||
CHECK(last_frame == FRAMES, "the last frame out is %llu", (unsigned long long)last_frame);
|
||||
CHECK(!dup_or_order, "%llu presents out of order or twice", (unsigned long long)dup_or_order);
|
||||
CHECK(!overlap, "%llu presents ran while another one did", (unsigned long long)overlap);
|
||||
CHECK(S.out[PH_AFTER] && S.out[PH_CAPPED] && S.out[PH_NEXT], "a reason never occurred (after %llu, PH_MAX %llu, "
|
||||
"next %llu)", (unsigned long long)S.out[PH_AFTER], (unsigned long long)S.out[PH_CAPPED],
|
||||
(unsigned long long)S.out[PH_NEXT]);
|
||||
}
|
||||
|
||||
// ---- one thread, step by step
|
||||
static uint64_t seen[16], nseen;
|
||||
static int seen_why[16];
|
||||
static void record_cb(const PhJob *j, int why, void *arg) {
|
||||
(void)arg;
|
||||
if (nseen < 16) seen[nseen] = j->frame, seen_why[nseen] = why;
|
||||
nseen++;
|
||||
}
|
||||
static int hold_yes(void *arg) { (void)arg; return 1; }
|
||||
static int hold_no(void *arg) { (void)arg; return 0; }
|
||||
static int why_none(const PhJob *j, void *arg) { (void)j, (void)arg; return PH_NONE; }
|
||||
static int why_out(const PhJob *j, void *arg) { (void)j, (void)arg; return PH_AFTER; }
|
||||
|
||||
static void steps(void) {
|
||||
PhState s = {.lock = PLAT_MUTEX_INIT};
|
||||
PhJob a = {.frame = 1}, b = {.frame = 2};
|
||||
ph_present(&s, &a, hold_no, record_cb, NULL);
|
||||
CHECK(nseen == 1 && seen[0] == 1 && seen_why[0] == PH_NONE && !ph_armed(&s), "a present with nothing held goes out");
|
||||
nseen = 0;
|
||||
ph_present(&s, &a, hold_yes, record_cb, NULL);
|
||||
CHECK(nseen == 0 && ph_armed(&s) && s.waited == 1, "a present with work held waits");
|
||||
CHECK(!ph_flush_try(&s, why_none, record_cb, NULL) && nseen == 0 && ph_armed(&s),
|
||||
"a submission after which work is still held keeps the present");
|
||||
CHECK(ph_flush_try(&s, why_out, record_cb, NULL) && nseen == 1 && seen[0] == 1 && seen_why[0] == PH_AFTER &&
|
||||
!ph_armed(&s) && s.out[PH_AFTER] == 1, "a submission that replayed the held work puts the present out");
|
||||
CHECK(!ph_flush_try(&s, why_out, record_cb, NULL) && nseen == 1, "a held present goes out once");
|
||||
nseen = 0;
|
||||
ph_present(&s, &a, hold_yes, record_cb, NULL);
|
||||
CHECK(ph_flush_next(&s, record_cb, NULL) && nseen == 1 && seen_why[0] == PH_NEXT && !ph_armed(&s),
|
||||
"the next present puts the held one out");
|
||||
CHECK(!ph_flush_next(&s, record_cb, NULL) && nseen == 1, "nothing goes out twice at the next present");
|
||||
nseen = 0;
|
||||
ph_present(&s, &a, hold_yes, record_cb, NULL);
|
||||
ph_present(&s, &b, hold_no, record_cb, NULL);
|
||||
CHECK(nseen == 2 && seen[0] == 1 && seen_why[0] == PH_NEXT && seen[1] == 2 && seen_why[1] == PH_NONE,
|
||||
"a present behind a held one puts the earlier one out first");
|
||||
}
|
||||
|
||||
// ---- the lock held elsewhere: a submitting thread does not wait, the presenting thread waits and is counted
|
||||
static PhState L = {.lock = PLAT_MUTEX_INIT};
|
||||
static volatile uint64_t try_ns;
|
||||
static volatile int32_t try_out = -1, waits_told;
|
||||
static void on_wait(uint64_t t0) { (void)t0; __atomic_add_fetch(&waits_told, 1, __ATOMIC_RELAXED); }
|
||||
static void try_thread(void *arg) {
|
||||
(void)arg;
|
||||
uint64_t t0 = plat_now_ns();
|
||||
try_out = ph_flush_try(&L, why_out, record_cb, NULL);
|
||||
try_ns = plat_now_ns() - t0;
|
||||
}
|
||||
static void present_thread(void *arg) {
|
||||
(void)arg;
|
||||
PhJob c = {.frame = 3};
|
||||
ph_present(&L, &c, hold_no, record_cb, NULL);
|
||||
}
|
||||
|
||||
static void contended(void) {
|
||||
PhJob a = {.frame = 1};
|
||||
L.on_wait = on_wait;
|
||||
ph_present(&L, &a, hold_yes, record_cb, NULL); // armed
|
||||
nseen = 0;
|
||||
plat_mutex_lock(&L.lock); // another thread puts a held present out
|
||||
PlatThread *t = plat_thread_new(try_thread, NULL, "try");
|
||||
plat_thread_join(t);
|
||||
CHECK(try_out == 0 && nseen == 0 && ph_armed(&L), "a submitting thread put the present out past a held lock");
|
||||
CHECK(try_ns < 50000000ull, "a submitting thread took %.1f ms to give up on a held lock", try_ns / 1e6);
|
||||
PlatThread *p = plat_thread_new(present_thread, NULL, "present");
|
||||
plat_sleep_ms(30);
|
||||
CHECK(nseen == 0, "the presenting thread went past a held lock");
|
||||
__atomic_store_n(&L.on, 0, __ATOMIC_RELAXED); // what the lock's holder did: the held present went out
|
||||
plat_mutex_unlock(&L.lock);
|
||||
plat_thread_join(p);
|
||||
CHECK(nseen == 1 && seen[0] == 3, "the presenting thread's present after the lock was let go");
|
||||
CHECK(L.lock_waits == 1 && waits_told == 1 && L.lock_wait_ns >= 20000000ull && L.lock_wait_max == L.lock_wait_ns,
|
||||
"the presenting thread's wait was not counted: %llu waits, %.1f ms, longest %.1f ms, told %d",
|
||||
(unsigned long long)L.lock_waits, L.lock_wait_ns / 1e6, L.lock_wait_max / 1e6, waits_told);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
old_mode = argc > 1 && !strcmp(argv[1], "old");
|
||||
if (!old_mode) steps(), contended();
|
||||
stress();
|
||||
if (old_mode) return 0;
|
||||
printf("present_held_check: %s\n", fails ? "FAILED" : "passed");
|
||||
return fails ? 1 : 0;
|
||||
}
|
||||
58
tests/present_held_check.py
Normal file
58
tests/present_held_check.py
Normal file
@@ -0,0 +1,58 @@
|
||||
"""Build and run tests/present_held_check.c: runtime/vk_present_held.h, the present that waits for held work
|
||||
(vk_backend.c "a present that waits for held work"). One presenting thread and three submitting threads over 200,000
|
||||
frames: every frame reaches the present exactly once and in order, never two at once, and each reason a held present
|
||||
goes out occurs. Step by step: a held present stays while work is held, goes out after the work, at the next present
|
||||
or before a later present, and only once. With the lock held elsewhere a submitting thread gives up at once, and the
|
||||
presenting thread waits and counts its wait. Then the unlocked 0.10.1 shape runs through the same harness and only
|
||||
prints what it saw, to show that the harness catches the race.
|
||||
|
||||
No GPU, no game files, any Python.
|
||||
|
||||
Usage: present_held_check.py
|
||||
"""
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MSYS_BIN = Path(r'C:\msys64\ucrt64\bin')
|
||||
|
||||
|
||||
def compiler():
|
||||
"""$CC, else MSYS2 UCRT64's clang, else clang on PATH."""
|
||||
if os.environ.get('CC'):
|
||||
return os.environ['CC']
|
||||
if os.name == 'nt' and (MSYS_BIN / 'clang.exe').exists():
|
||||
return str(MSYS_BIN / 'clang.exe')
|
||||
return shutil.which('clang') or 'clang'
|
||||
|
||||
|
||||
def main():
|
||||
work = ROOT / 'build' / 'test' / 'present_held'
|
||||
work.mkdir(parents=True, exist_ok=True)
|
||||
win = os.name == 'nt'
|
||||
exe = work / ('present_held_check.exe' if win else 'present_held_check')
|
||||
env = dict(os.environ)
|
||||
if win and MSYS_BIN.exists():
|
||||
env['PATH'] = str(MSYS_BIN) + os.pathsep + env.get('PATH', '')
|
||||
rt = ROOT / 'runtime'
|
||||
plat = [rt / 'platform' / ('win32.c' if win else 'linux.c'), rt / 'platform' / 'common.c']
|
||||
libs = ['-lbcrypt', '-lws2_32', '-lpsapi', '-lwinmm', '-ldbghelp', '-lshell32', '-lole32', '-luser32', '-ladvapi32'] if win \
|
||||
else ['-lpthread']
|
||||
subprocess.check_call([compiler(), '-std=gnu11', '-O1', '-g', '-Wall', '-Wno-unused-function', '-fno-strict-aliasing',
|
||||
'-march=x86-64-v3', '-I', str(rt), str(ROOT / 'tests' / 'present_held_check.c'),
|
||||
*map(str, plat), '-o', str(exe), '-lm', *libs], env=env)
|
||||
try:
|
||||
rc = subprocess.run([str(exe)], env=env, timeout=300).returncode
|
||||
if rc == 0:
|
||||
subprocess.run([str(exe), 'old'], env=env, timeout=300)
|
||||
return rc
|
||||
except subprocess.TimeoutExpired:
|
||||
print('present_held_check: FAIL: the check did not end within 300 s')
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user