mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
Merge main into the 0.9.1 release: the launcher's animations stopped behind another window, the uniform snapshot's copy split over the replay workers, and the replay workers' default of three with its measurements.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -55,7 +55,8 @@ Customize Launcher (on the home screen) makes the look the player's own:
|
||||
scheme's backdrop back.
|
||||
- Animations (on by default): the cursor glides from row to row, a new screen slides into place, sparks drift up
|
||||
over the backdrop and Navi bobs in the title band with a pulsing halo (`App.animate`). They move by the time that
|
||||
passed, stand still while the window is hidden or minimised, and Off stops them all.
|
||||
passed, stand still while the window is hidden or minimised or another window is in front (the game, when Hide
|
||||
Launcher While Playing is off), and Off stops them all.
|
||||
- Sounds (Low by default; Off, Medium or High): two of the game's own interface sounds, the decide and cancel sounds
|
||||
of its menus, when a row is chosen or a choice changes and when the player goes back. The launcher decodes them
|
||||
from the player's built game or dump into `data/cache/sounds` (`gamefiles.extract_sounds`:
|
||||
|
||||
@@ -1996,14 +1996,16 @@ class App:
|
||||
def animate(self):
|
||||
"""One step of everything that moves: the cursor on its way to the focused row and its breath, the screen
|
||||
sliding into place, the sparks and Navi. Each moves by the time since the last step (the timer comes when
|
||||
Tk gets to it, 16 to 50 ms apart), at most a tenth of a second's worth. With Animations off, and while the
|
||||
window is hidden or minimised (the game is running), nothing moves and the timer runs slowly."""
|
||||
Tk gets to it, 16 to 50 ms apart), at most a tenth of a second's worth. With Animations off, while the
|
||||
window is hidden or minimised, and while another window is in front, nothing moves and the timer runs slowly.
|
||||
The last covers a launcher left open behind the game (Hide Launcher While Playing off): drawing every 15 ms
|
||||
took about 0.6 of a CPU core from the game for a window nobody saw."""
|
||||
moving = False
|
||||
try:
|
||||
now = time.perf_counter()
|
||||
dt = min(0.1, max(0.0, now - self.anim_last))
|
||||
self.anim_last = now
|
||||
moving = self.animated() and self.root.state() == 'normal'
|
||||
moving = self.animated() and self.root.state() == 'normal' and winsys.in_front(self.root)
|
||||
if moving:
|
||||
self.anim_t += dt
|
||||
self.step_slide(dt)
|
||||
|
||||
@@ -550,6 +550,10 @@ int vk_exec_in_chunk(void); // the calling thread records a chunk of a paralle
|
||||
// waits until the group's first chunk ran the pass hooks.
|
||||
int vk_exec_shared(void);
|
||||
int vk_exec_behind(void);
|
||||
// The replay thread, while no group of the replay workers is out (vk_exec.c "side work"): fn(arg, lo, hi) over items
|
||||
// [0, n) in slices of grain, on the workers and the calling thread at once; returns when every slice is done. Without
|
||||
// workers, or for one slice, the caller runs it alone.
|
||||
void vk_exec_par(void (*fn)(void *arg, int lo, int hi), void *arg, int n, int grain);
|
||||
void vk_exec_hooks_wait(void);
|
||||
// A pass hook (vk_contact_pass_begin, vk_dlaa_pass) about to change what draws read or to record: the submitting
|
||||
// thread past a group waits for the group first; a group's hooks run in its first chunk, in turn already.
|
||||
|
||||
@@ -1761,6 +1761,9 @@ static void op_one(VkExec *ex, const op_hdr *h) {
|
||||
}
|
||||
|
||||
// ---- replay workers (NX2NAVI_REPLAY_WORKERS, default 3; 0: the replay records every op on the submitting thread)
|
||||
// Default 3: on an RX 7900 XT with a Ryzen 7 5800X3D, Hyrule Field standing went from 41.2 to 50.4 fps (2026-11-12, one
|
||||
// batch), with the same pictures there and in Kokiri Forest, Kakariko, Goron City, a graveyard house and Gerudo
|
||||
// Valley; 5 or 7 workers measured the same as 3.
|
||||
// A submission's draws come in runs between ops that have to stay in the replay's order on its own thread (dispatches,
|
||||
// copies, texture and buffer clears, events, the game's barriers, calls). Hyrule Field, standing (NX2NAVI_REPLAY_RUNS,
|
||||
// 2026-11-11): about 3,500 draws a frame in 82 runs, 96% of them in runs of 16 draws or more and 67% in runs of 256 or
|
||||
@@ -1879,12 +1882,66 @@ static int ser_kind(uint32_t op) {
|
||||
static Chunk *chunk_take(void);
|
||||
static void chunk_record(Chunk *c, int pool);
|
||||
|
||||
// ---- side work (vk_exec_par): the replay thread hands the workers a loop over n items while they have no chunk to
|
||||
// record, at a submission's start (vk_usnap.c copy_regions). One job at a time, in a static slot: seq is odd while it
|
||||
// runs. A worker counts itself in (PJ.in) and checks seq again before it reads the job, and the caller, once every
|
||||
// slice is done, makes seq even and waits until no worker is in, so none reads a job that has ended.
|
||||
static struct {
|
||||
void (*fn)(void *, int, int);
|
||||
void *arg;
|
||||
int n, grain;
|
||||
volatile int32_t next, left, in;
|
||||
volatile uint32_t seq;
|
||||
} PJ;
|
||||
static PlatMutex pj_lock = PLAT_MUTEX_INIT;
|
||||
|
||||
static void par_slices(void) {
|
||||
for (;;) {
|
||||
int lo = __atomic_fetch_add(&PJ.next, PJ.grain, __ATOMIC_ACQ_REL);
|
||||
if (lo >= PJ.n) return;
|
||||
int hi = lo + PJ.grain < PJ.n ? lo + PJ.grain : PJ.n;
|
||||
PJ.fn(PJ.arg, lo, hi);
|
||||
__atomic_sub_fetch(&PJ.left, hi - lo, __ATOMIC_ACQ_REL);
|
||||
}
|
||||
}
|
||||
|
||||
static int par_pending(void) { return __atomic_load_n(&PJ.seq, __ATOMIC_ACQUIRE) & 1; }
|
||||
|
||||
static void par_help(void) {
|
||||
uint32_t s = __atomic_load_n(&PJ.seq, __ATOMIC_ACQUIRE);
|
||||
if (!(s & 1)) return;
|
||||
__atomic_add_fetch(&PJ.in, 1, __ATOMIC_SEQ_CST);
|
||||
if (__atomic_load_n(&PJ.seq, __ATOMIC_SEQ_CST) == s) par_slices();
|
||||
__atomic_sub_fetch(&PJ.in, 1, __ATOMIC_SEQ_CST);
|
||||
}
|
||||
|
||||
void vk_exec_par(void (*fn)(void *, int, int), void *arg, int n, int grain) {
|
||||
if (grain < 1) grain = 1;
|
||||
if (RW.n <= 0 || n <= grain || Q.drained != Q.out) {
|
||||
if (n > 0) fn(arg, 0, n);
|
||||
return;
|
||||
}
|
||||
plat_mutex_lock(&pj_lock);
|
||||
PJ.fn = fn, PJ.arg = arg, PJ.n = n, PJ.grain = grain;
|
||||
__atomic_store_n(&PJ.next, 0, __ATOMIC_RELAXED);
|
||||
__atomic_store_n(&PJ.left, n, __ATOMIC_RELAXED);
|
||||
__atomic_add_fetch(&PJ.seq, 1, __ATOMIC_SEQ_CST); // odd: running
|
||||
for (int k = 0; k < RW.n; k++)
|
||||
if (__atomic_load_n(&RW.w[k]->sleeping, __ATOMIC_SEQ_CST)) plat_event_set(&RW.w[k]->wake);
|
||||
par_slices();
|
||||
while (__atomic_load_n(&PJ.left, __ATOMIC_ACQUIRE) > 0) plat_pause();
|
||||
__atomic_add_fetch(&PJ.seq, 1, __ATOMIC_SEQ_CST); // even: ended
|
||||
while (__atomic_load_n(&PJ.in, __ATOMIC_SEQ_CST)) plat_pause();
|
||||
plat_mutex_unlock(&pj_lock);
|
||||
}
|
||||
|
||||
static void rw_main(void *arg) {
|
||||
RwWorker *w = arg;
|
||||
plat_thread_priority(PLAT_PRIO_HIGHEST); // as the recording thread (rec_claim)
|
||||
perf_lock_timing_here(); // NX2NAVI_LOCK_TIME, as on the recording thread
|
||||
vk_exec_draw_ctx_use(w->dc);
|
||||
for (;;) {
|
||||
par_help();
|
||||
Chunk *c = chunk_take();
|
||||
if (c) {
|
||||
chunk_record(c, w->idx);
|
||||
@@ -1892,13 +1949,16 @@ static void rw_main(void *arg) {
|
||||
}
|
||||
// spins about 100 us after a chunk (the next group of the frame is usually closer), then sleeps
|
||||
uint64_t t0 = kern_now_ns();
|
||||
for (unsigned i = 0; __atomic_load_n(&Q.take, __ATOMIC_SEQ_CST) >= __atomic_load_n(&Q.put, __ATOMIC_SEQ_CST); i++) {
|
||||
for (unsigned i = 0; __atomic_load_n(&Q.take, __ATOMIC_SEQ_CST) >= __atomic_load_n(&Q.put, __ATOMIC_SEQ_CST);
|
||||
i++) {
|
||||
if (par_pending()) break;
|
||||
if ((i & 63) || kern_now_ns() - t0 < 100000) {
|
||||
plat_pause();
|
||||
continue;
|
||||
}
|
||||
__atomic_store_n(&w->sleeping, 1, __ATOMIC_SEQ_CST);
|
||||
if (__atomic_load_n(&Q.take, __ATOMIC_SEQ_CST) >= __atomic_load_n(&Q.put, __ATOMIC_SEQ_CST))
|
||||
if (__atomic_load_n(&Q.take, __ATOMIC_SEQ_CST) >= __atomic_load_n(&Q.put, __ATOMIC_SEQ_CST) &&
|
||||
!par_pending())
|
||||
plat_event_wait_ns(&w->wake, 2000000);
|
||||
__atomic_store_n(&w->sleeping, 0, __ATOMIC_RELAXED);
|
||||
t0 = kern_now_ns();
|
||||
|
||||
@@ -264,15 +264,39 @@ static void summary(void) {
|
||||
U.subs = U.off_subs = U.regions = U.bytes = U.copy_ns = U.hits = U.drops = U.inflight = U.refused = 0;
|
||||
}
|
||||
|
||||
// The bytes of regions reg[0..nreg) (total bytes in all) into a's device copy, through its host copy with DLSS.
|
||||
static void copy_regions(Arena *a, const Reg *reg, int nreg, uint64_t total) {
|
||||
// copy_regions's work, sliced for the replay workers (vk_exec_par): regions of the game's memory into the arena, then
|
||||
// the host copy into the device copy in COPY_SLICE pieces.
|
||||
#define COPY_SLICE (64u << 10)
|
||||
typedef struct { uint8_t *dst, *map; const Reg *reg; uint64_t total; } CopyJob;
|
||||
static void copy_some(void *arg, int lo, int hi) {
|
||||
const CopyJob *j = arg;
|
||||
for (int i = lo; i < hi; i++)
|
||||
memcpy(j->dst + j->reg[i].off, (const void *)(uintptr_t)j->reg[i].lo, j->reg[i].hi - j->reg[i].lo);
|
||||
}
|
||||
static void copy_down(void *arg, int lo, int hi) {
|
||||
const CopyJob *j = arg;
|
||||
uint64_t a = (uint64_t)lo * COPY_SLICE, b = (uint64_t)hi * COPY_SLICE;
|
||||
if (b > j->total) b = j->total;
|
||||
memcpy(j->map + a, j->dst + a, b - a);
|
||||
}
|
||||
|
||||
// The bytes of regions reg[0..nreg) (total bytes in all) into a's device copy, through its host copy with DLSS. With
|
||||
// par (the replay thread at a submission's start), split over the replay workers: about 700 regions and 0.7 MiB in
|
||||
// Hyrule Field, half a millisecond of the submitting thread's time on one thread.
|
||||
static void copy_regions(Arena *a, const Reg *reg, int nreg, uint64_t total, int par) {
|
||||
uint8_t *dst = a->map;
|
||||
if (vk_dlaa_available()) {
|
||||
if (!a->cpu && !(a->cpu = malloc(a->size))) rt_fatal("vk_usnap: out of memory");
|
||||
dst = a->cpu;
|
||||
}
|
||||
for (int i = 0; i < nreg; i++) memcpy(dst + reg[i].off, (const void *)(uintptr_t)reg[i].lo, reg[i].hi - reg[i].lo);
|
||||
if (dst != a->map) memcpy(a->map, dst, total); // one sequential write into the device copy
|
||||
CopyJob j = {dst, a->map, reg, total};
|
||||
if (par) vk_exec_par(copy_some, &j, nreg, 48);
|
||||
else copy_some(&j, 0, nreg);
|
||||
if (dst != a->map) { // sequential writes into the device copy
|
||||
int slices = (int)((total + COPY_SLICE - 1) / COPY_SLICE);
|
||||
if (par) vk_exec_par(copy_down, &j, slices, 2);
|
||||
else copy_down(&j, 0, slices);
|
||||
}
|
||||
vmaFlushAllocation(vk_ctx()->vma, a->alloc, 0, total); // a no-op on coherent memory
|
||||
}
|
||||
|
||||
@@ -307,7 +331,7 @@ void vk_usnap_take(const uint64_t *handles, int n) {
|
||||
t->total += (hi - lo + ALIGN - 1) & ~(uint64_t)(ALIGN - 1);
|
||||
}
|
||||
plat_mutex_unlock(&gather_lock);
|
||||
if (t->nreg && (t->arena = arena_get(t->total))) copy_regions(t->arena, t->reg, t->nreg, t->total);
|
||||
if (t->nreg && (t->arena = arena_get(t->total))) copy_regions(t->arena, t->reg, t->nreg, t->total, 0);
|
||||
t->ns = kern_now_ns() - t0;
|
||||
plat_mutex_lock(&tq_lock);
|
||||
if (tq_tail) tq_tail->next = t;
|
||||
@@ -393,7 +417,7 @@ void vk_usnap_begin(const uint64_t *handles, int n, uint64_t fence) {
|
||||
plat_mutex_unlock(&gather_lock);
|
||||
if (!U.nreg) return;
|
||||
if (!(U.arena = arena_get(total))) return;
|
||||
copy_regions(U.arena, U.reg, U.nreg, total);
|
||||
copy_regions(U.arena, U.reg, U.nreg, total, 1); // the replay thread at the submission's start
|
||||
U.on = 1;
|
||||
U.subs++, U.regions += (uint64_t)U.nreg, U.bytes += total, U.copy_ns += kern_now_ns() - t0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user