diff --git a/docs/building.md b/docs/building.md index f537894e..7583b96f 100644 --- a/docs/building.md +++ b/docs/building.md @@ -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 diff --git a/runtime/vk_backend.c b/runtime/vk_backend.c index 07af851c..40c18dff 100644 --- a/runtime/vk_backend.c +++ b/runtime/vk_backend.c @@ -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. diff --git a/runtime/vk_present_held.h b/runtime/vk_present_held.h new file mode 100644 index 00000000..aaa1804c --- /dev/null +++ b/runtime/vk_present_held.h @@ -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 + +#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); +} diff --git a/tests/present_held_check.c b/tests/present_held_check.c new file mode 100644 index 00000000..7584fe94 --- /dev/null +++ b/tests/present_held_check.c @@ -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 +#include +#include + +#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; +} diff --git a/tests/present_held_check.py b/tests/present_held_check.py new file mode 100644 index 00000000..406006cb --- /dev/null +++ b/tests/present_held_check.py @@ -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())