From 93e9434869025fb906f9fe0cdd4f1070b644d31a Mon Sep 17 00:00:00 2001 From: chrg6 Date: Fri, 13 Nov 2026 19:45:55 -0500 Subject: [PATCH] Read the frame hold's allowance and its time as one pair in vk_frame_hold_left, under a sequence word the refill at the present writes. The review of d2014496 found that vk_frame_hold_left loaded left_ns, last_ns and holding as three separate relaxed loads while frame_hold_refill could store between them, so a reader on a replaying thread could pair a new left_ns with an old last_ns and count one present interval twice (or, the other way round, not at all) for its decision, the effect d2014496 meant to remove, now on the read side only; its comment presented the read as exact. Now frame_hold_refill writes the pair between two steps of FH.seq (odd while it writes), and vk_frame_hold_left reads it again when a read overlapped a refill. After FH_READ_TRIES such reads it goes with the last one, as vk_evo_peek.h does, so a drawing thread never waits for the present; the comment says what such a read can get wrong. holding is still stored after the refill, and a read between the two applies the previous frame's value to the microseconds since that refill, which the comment says too. The decision is asked only for a full-screen draw whose pipeline is still pending (vk_exec_draw.c). Not reproduced before the change. Checks that passed: pass_cc_check of vk_backend.c with no new -Wall -Wextra warnings. Co-Authored-By: Claude Opus 5.5 --- runtime/vk_backend.c | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/runtime/vk_backend.c b/runtime/vk_backend.c index 83d1385b..88ed0867 100644 --- a/runtime/vk_backend.c +++ b/runtime/vk_backend.c @@ -192,12 +192,15 @@ void vk_present_drain(void) { // (vk_frame_hold_left, vk_exec_draw.c), on whatever thread holds the submission lock, which need not be the one that // presents: so that reader works out the allowance from limit_ns, left_ns, last_ns and holding (atomics) and writes // nothing. When it refilled FH itself, a refill on the replaying thread beside one at the present on another thread -// counted the same interval twice or not at all. +// counted the same interval twice or not at all. A refill writes left_ns and last_ns as a pair under a sequence word +// (seq, odd while it writes), so the reader does not pair a new left_ns with an old last_ns either. static struct { int64_t limit_ns, left_ns; uint64_t last_ns, held, runs, shown, load_t0, load_held; int holding, resize_left; + uint32_t seq; } FH = {.limit_ns = -1}; +#define FH_READ_TRIES 3 // NX2NAVI_FRAME_HOLD_MS, read once by whichever thread asks first; that one logs it. static int64_t fh_limit(void) { @@ -220,17 +223,32 @@ static int64_t fh_left_at(int64_t lim, int64_t left, uint64_t last, int holding, } static int64_t frame_hold_refill(uint64_t now) { // frame_hold only; returns the limit - int64_t lim = fh_limit(); - __atomic_store_n(&FH.left_ns, fh_left_at(lim, FH.left_ns, FH.last_ns, FH.holding, now), __ATOMIC_RELAXED); + int64_t lim = fh_limit(), left = fh_left_at(lim, FH.left_ns, FH.last_ns, FH.holding, now); + uint32_t s = FH.seq; + __atomic_store_n(&FH.seq, s + 1, __ATOMIC_RELAXED); + __atomic_thread_fence(__ATOMIC_RELEASE); // a reader that sees either new field also sees the odd sequence + __atomic_store_n(&FH.left_ns, left, __ATOMIC_RELAXED); __atomic_store_n(&FH.last_ns, now, __ATOMIC_RELAXED); + __atomic_store_n(&FH.seq, s + 2, __ATOMIC_RELEASE); return lim; } // Whether a frame that skipped draws now would be held (vk_exec_draw.c: a full-screen draw is skipped only then). Any -// thread; reads FH only. The fields are loaded before the clock, so now is not before last. +// thread; reads FH only. left_ns and last_ns come from one refill: a read that overlaps a refill is made again, and +// after FH_READ_TRIES such reads the last one is used (it can count one interval twice or not at all, for this one +// decision), so the reader never waits for the present. holding is stored at the present just after its refill; a +// read between the two applies the previous frame's holding to the few microseconds since that refill. The fields are +// loaded before the clock, so now is not before last. int vk_frame_hold_left(void) { - int64_t lim = fh_limit(), left = __atomic_load_n(&FH.left_ns, __ATOMIC_RELAXED); - uint64_t last = __atomic_load_n(&FH.last_ns, __ATOMIC_RELAXED); + int64_t lim = fh_limit(), left = 0; + uint64_t last = 0; + for (int k = 0; k < FH_READ_TRIES; k++) { + uint32_t s0 = __atomic_load_n(&FH.seq, __ATOMIC_ACQUIRE); + left = __atomic_load_n(&FH.left_ns, __ATOMIC_RELAXED); + last = __atomic_load_n(&FH.last_ns, __ATOMIC_RELAXED); + __atomic_thread_fence(__ATOMIC_ACQUIRE); // the fields are read before the sequence is read again + if (!(s0 & 1) && __atomic_load_n(&FH.seq, __ATOMIC_RELAXED) == s0) break; + } int holding = __atomic_load_n(&FH.holding, __ATOMIC_RELAXED); return fh_left_at(lim, left, last, holding, kern_now_ns()) > 0 && !vk_dlssg_active(); }