85 Commits

Author SHA1 Message Date
chrg6
712568d4fb Make the release 0.11.0: the release prepared and previewed as 0.10.2 takes this number, with its notes.
The user asked for the release to go out as 0.11.0. No final 0.10.2 was published: only the previews 0.10.2-rc1,
-rc3 and -rc4 carry that name, and the launcher counts 0.11.0 as newer than each of them, so testers are offered it
on either update channel.

VERSION holds 0.11.0 and the notes move to docs/releases/v0.11.0.md, with a line for those on a preview. Every line
that named 0.10.2 as the release in which something changed names 0.11.0 now: comments in the runtime, the two log
lines that give a version (the removed low-end GPU mode in cfg.c, the page generations in vk_memory.c),
nx2navi.ini.example, docs/building.md, the labels of tests/tex_release_check.c and a comment of
tests/audio_params_check.c. AUDIT.md names the release 0.11.0 throughout and records the earlier name and the
previews once, in its status line. Nothing else changes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 09:24:25 -05:00
chrg6
1d0327acd1 Run a parallel group's pass hooks once, at its first chunk's first pass.
With Helpers take small jobs on (replay_small, with replay_pipe) the whole scene flashed for single frames under
DLSS, with the interface intact. The pass hooks keep what the last pass that ran them left: the upscaler's light
target is the one of the last scene pass before the output pass. A group's hooks run in its first chunk, and a pass
that chunk began again (after a barrier, or a texture lookup that waited for its turn) ran them a second time. With
the submitting thread already past the group, that came after the hooks of later passes and put the group's own
target back, so the upscaler took an earlier image of the frame for that frame. The log showed it: the light target
changed to another image and back at each flash.

The rule is in runtime/vk_pass_hooks.h, with tests/pass_hooks_check, which fails against the old rule.

Measured by replaying a bug capture in Kakariko (DLSS Performance, 3 workers, replay_pipe = 4, replay_small = 16,
100 s a run): 51 and 52 flash frames as installed, none with replay_small = 0, none with the helpers off, 52 with the
render target reuse off. Two copies of the installed build made the same way, one with this change and one without:
no flash frame in two runs with it, 373 in one run without.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 06:44:38 -05:00
chrg6
fed7aca7fc Leave Helpers work ahead and Helpers take small jobs off by default, as in 0.10.1.
0.10.2 was to turn replay_pipe = 4 and replay_small = 16 on by default. A tester measured those two with five replay workers against the defaults on an RTX 3090, standing at eight places in Hyrule Field and at Lake Hylia with the default run repeated (within 3 %): slower at four of them (50.1 instead of 58.1 fps at one) and level or at most 7 % faster at the other four. The user decided to leave both at 0. CPU helper threads stay Automatic and Start next frame early stays on, as 0.10.1 shipped them; the notes and nx2navi.ini.example say so.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 05:18:15 -05:00
chrg6
f7a2d94813 Bring public main's video memory fix and 0.10.1's defaults into the 0.10.2 line.
Main gained three commits after 0.10.1's release commit: a render target that was drawn over gives its image to the next target of its size, such an image is handed on after 8 presents and freed after 32 while the card is past its pressure share, and 0.10.1 kept CPU helper threads on Automatic and Start next frame early on. The settings keep this line's defaults (Helpers work ahead 4, Helpers take small jobs 16), the comments that said both were off in 0.10.1 now say what 0.10.1 shipped, the texture log keeps both sides' counters, and 0.10.2's notes say which two defaults are new.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-14 04:20:32 -05:00
chrg6
69ab33d1f1 Turn the Game tab's Testing settings on by default for 0.10.2, as the user asked: CPU helper threads Automatic (replay_workers = auto, which gives none on four cores or fewer and none on AMD's driver for RX 6000 and older), Helpers work ahead 4 (replay_pipe), Helpers take small jobs 16 (replay_small) and Start next frame early On (tail_early). A value a player set in the menu or nx2navi.ini stays; the notes say how to go back to 0.10.1's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-14 03:20:37 -05:00
chrg6
51c9261db3 Close a frame's replay state under the submission lock: at the present when the lock is free, else first thing in the next submission, so another thread's replay never sees it reset halfway.
present_image ran the frame's bookkeeping on the presenting thread without a lock: vk_exec_frame_log (the trace counts
and vk_exec_cond_frame_end's frame count, which the conditional draws go by), dynres_frame, vk_exec_barrier_log,
vk_dlaa_frame (the DLSS mode and its LOD bias, the frame's points, the swap image, the idle releases), vk_mvsurvey_frame,
vk_mvprev_frame (which swaps the two motion tables the draws insert into) and vk_gpu_passes_frame. All of it is state the
replay reads and writes as it records, under sub_lock. With the replay on the submitting threads the game presents on
one thread while another may be replaying (the overlap lines of the 0.10.0 fresh session, at area changes), so the
present reset it in the middle of that replay: a DLSS frame reset halfway, the motion tables swapped halfway (with
MVPREV_CHECK=1 an index out of bounds), the conditional draws counted against the next frame.

Now the bookkeeping is frame_end, and runtime/vk_frame_owed.h decides where it runs. present_image marks its frame owed
and runs it itself only when it gets sub_lock at once (vk_sync.c vk_sync_try_locked); otherwise vk_sync.c submit runs
every owed frame's, in frame order, right after it takes the lock and before its replay (vk_present_frames_owed), on
whichever thread submits next, the present thread's blit submission included. The presenting thread never waits for
another thread's replay for it: no new wait on a drawing thread; the thread that takes the lock next does the work it
would have done at the present, tens of microseconds normally. The hand-off, vk_texture_get with its upload flush and
vk_present_drain stay outside the lock, as frame_end must not wait for the present thread or submit; its callees were
checked for that. vk_sampler_aniso_live and vk_mem_census keep their place in present_image (their own locks). In the
asynchronous replay the other holder of sub_lock is the present thread's blit, and the same applies.

NX2NAVI_PRESENT_ORDER (present_order in [diagnostics], default 1, read at start) puts it back at the present with 0, as
in 0.10.1: metas, known and sections rows and an nx2navi.ini.example entry.

Not reproduced before the change. Checks that passed: the new tests/frame_owed_check (one presenting and three
replaying threads, some replays holding the lock 40 ms, over 20,000 frames: every frame's bookkeeping once, in order,
never during a replay and before any replay that took the lock after its present; the longest present under 1 ms with
19,000 presents made during a long hold), cfg_rows_check with 0 problems, pass_cc_check on every runtime file (headers
changed), no new -Wall -Wextra warnings in vk_backend.c, vk_sync.c, vk_exec.c and cfg.c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:13:02 -05:00
chrg6
8b5ddd0865 Count the queue's submissions, the game's waits that returned with work held and the gate's waits with atomic adds, as several of the game's threads reach them at once.
nvnQueueSubmitCommands' q->submits++, vk_backend.c synced_held_count's two counts and vk_exec.c vk_exec_gate_wait's
window (GW) run on the calling guest thread without a lock, and the game submits and waits from several threads (five
in the 0.10.0 fresh session). A plain ++ there loses counts, and two threads could both reach a window's end and log
it, or reset it under each other. Now each count is an atomic add; the line is logged by the thread whose add reached
it, and the gate's window is taken with exchanges by the thread whose add made it 600, so adds that come in meanwhile
count in that window or the next. Nothing reads q->submits today; the counts only feed log lines.

Not reproduced before the change. Checks that passed: pass_cc_check of vk_exec.c, vk_backend.c and nvn_api_core.c with
no new -Wall -Wextra warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:13:02 -05:00
chrg6
7a1c220e06 Test held work against the event's value now when another thread asks without the submission lock, and read the held count and the submission count as atomics there.
vk_exec_held_ready runs without sub_lock (vk_backend.c held_resume, submit_sync). It decided from the published copy
since 688b2506, but tested the wait with vk_exec_event_met, which compares at the copy point of the submission being
recorded (vk_exec_xfer.c ev_mem, ev_sync_seq). That point is set by whichever thread holds the lock, so a thread asking
while another one recorded tested "at sync" against that other submission's copies. Its question is whether held work
can go on in a submission of its own, which takes its copies later: vk_exec_event_met_now tests the value in memory now
or the signals replayed so far. Under the lock nothing changes: evo_resume and cont_ready keep the copy-point test, and
vk_exec_record asks right after its own copy point, where the two agree.

evo_pending and vk_exec_submit_seq are written under the lock and were read as plain values by vk_exec_held,
vk_exec_held_ready and the held present's since and PH_MAX checks; they are now stored and loaded as atomics
(vk_exec_submit_seq_now), so the compiler cannot keep a stale copy in an unlocked reader.

Not reproduced before the change. Checks that passed: pass_cc_check on every runtime file (a header changed), no new
-Wall -Wextra warnings in vk_exec.c, vk_exec_xfer.c and vk_backend.c, evo_peek_check, target_frames_check and
oq_hold_check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:13:02 -05:00
chrg6
688b250673 Let held_resume ask whether held work can go on from a published copy, not from a continuation another thread frees.
vk_exec_held_ready runs without sub_lock (vk_backend.c held_resume and submit_sync). It followed evq[s].head into the
continuation and read the wait in the continuation's private copy of the recording. Meanwhile another submitting
thread, inside vk_sync_submit, could run evo_resume and cont_free that continuation. A reused block then gave
vk_exec_event_met an event address pointing anywhere, so the unlocked reader could fault, not only read a stale state.

Now the holder of sub_lock publishes what the decision needs into a per-queue sequence lock (runtime/vk_evo_peek.h),
wherever a queue's first continuation, or that continuation's wait or stop, changes: in cont_push when the pushed one
becomes the first, in evo_resume after the head moves on, and after cont_walk stops at another wait. The copy holds
the wait by value. vk_exec_held_ready decides from a copy taken from one publish. It never waits for the writer:
after three tries that overlap a publish it answers "not ready", which is what losing that race answered before.
This fixes F-1-06 without the front-end lock. F-2-01's lock is still the answer for the other unlocked front-end
state.

Not reproduced before the change. Checks that passed: the new tests/evo_peek_check (one writer publishing 1,000,000
times while three readers copy: no copy mixed two publishes, and the decision cont_ready makes, with a stand-in for
vk_exec_event_met; with the sequence check removed it reports over a million mixed copies), pass_cc_check on every
runtime file (a header was added), no new -Wall -Wextra warnings in vk_exec.c, target_frames_check, oq_hold_check,
sync_hw_check, mesh_path_check and gpa_check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:40 -05:00
chrg6
c99e9a94f8 Start every pass and dispatch mark with no mesh draws counted, where the mark is made.
The count was reset in vk_exec_pass_begin alone, so a dispatch's record kept the counts of the pass that last had its slot of the ring; the report prints them only for passes, but the record now holds what its mark was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:54:24 -05:00
chrg6
46d71da8aa Draw task and mesh programs through the mesh fallback by default on AMD's Windows driver for RDNA 1 and 2, where native mesh pipelines lost the device in play; mesh_fallback = 0 draws them natively again.
Every RX 6000 loss whose pass marks named a pass was in one that starts with a native mesh draw, on every version from 0.6.0 to 0.9.2 and with every other setting tried each way; RDNA 1 cards on the same driver draw through the fallback and RDNA 3 and 4 on AMD's other driver draw natively, and neither has such a report. That the native mesh path is the cause is not shown. The decision moves into vk_mesh_path.h with a test of each device kind and setting; has_mesh_shader keeps saying what the hardware has, so the recorder thread and the low-end card test do not change. On those cards no native mesh pipeline is built any more: the seed list and the boot prewarm skip them. The early tail stays off with the fallback, a combination never measured. The fatal line names the mesh path, and NX2NAVI_VK_NOOPT's default applies with mesh_fallback = 0 alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:54:24 -05:00
chrg6
1f4818fb6b Name in a lost device's report the mesh draws of each marked pass, a group of draws the replay workers recorded between two marks, and in the fatal line how mesh draws are drawn.
A pass mark now counts its mesh draws and those that take their count from the GPU; the gap before a mark notes a group recorded by the workers, whose passes have no marks, where the report said "barriers and state only"; the fatal line names the mesh path (native, the mesh fallback, the emulation). The comment on the RX 6700 XT report in vk_exec.c called the last marked dispatch the replay's own: it is the game's (op_dispatch, cs d7566eb1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 12:54:24 -05:00
epicmeme546
5ae55db41c Keep CPU helper threads on Automatic and Start next frame early on by default in 0.10.1, as in 0.10.0, and give 0.10.1's notes the video memory fix.
The release commit for 0.10.1 had turned both off by default (replay_workers = 0, tail_early = 0). They stay as 0.10.0 has them: replay_workers = auto, half the processor's physical cores less one (3 on 8 cores), and tail_early = 1. A player's session with all four Testing settings at 0 filled the video memory as before (bug capture 7: 15.5 GiB in use, 20.7 frames a second); what filled it were the images of moved render targets, which the two commits before this one fix.

runtime/cfg.c, runtime/ui/menu.cpp, runtime/vk_exec.c and nx2navi.ini.example are back as they were at 0.10.0. The notes of 0.10.1 lose the entry about the defaults and take the fix's entry from the draft for 0.10.2, which goes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-13 11:57:11 -05:00
chrg6
a84a3ab12d Make the release 0.10.1: every setting under the Game tab's Testing heading is off by default again, CPU helper threads (replay_workers = 0) and Start next frame early (tail_early = 0) among them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 10:49:01 -05:00
chrg6
e4df494048 Turn the early tail on by default (tail_early = 1): the game starts its next frame while the graphics card runs the last one's upscaler and interface, and a frame whose end hands the game anything else runs as before; tail_early = 0 turns it off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 08:58:03 -05:00
chrg6
482f7fdfb5 Stop logging a replay worker's deferred render target lookup as a target left out, and say why a target that is left out could not be rendered.
A chunk's lookup that needs its turn returns no image and its op is replayed in turn, but the first one was logged as "render target ... is not renderable here: left out", which read as depth and cube targets the replay could not draw. Nothing else changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 08:36:46 -05:00
chrg6
0e76baad36 Limit replay_small to 0..63, where it can match a group.
A group too small to share out has fewer than 64 draws (2 x CHUNK_DRAWS), so a threshold of 64 or more never matched
in group_start and the setting did nothing. The setting's range is now 0..63, the menu's slider ends at 60 in steps of
4, and a value of 64 or more from nx2navi.ini reads as 63 with one line in the log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 06:06:27 -05:00
chrg6
c656532cd3 Warn in the log when replay workers are set by number on AMD's driver for RX 6000 and older.
The automatic count gives that driver no workers, since a game with workers lost its device there, but a number in
nx2navi.ini or the menu starts them without a word. The number still applies; the log now says that workers on this
driver have caused device loss and that auto or 0 avoids it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 06:06:27 -05:00
chrg6
839afa32ac Add a test setting that runs DLSS or FSR 3.1 before the bloom instead of after the tonemap.
Frame dumps show the upscaler reads the bloom composite's output, which the game has already exposed, bloomed and
tonemapped to 0..1. With dlss_prebloom = 1 it runs at the bloom chain's first pass on the scene's linear light. Without
upscaling its picture goes back into the light target. With upscaling the lens flares and the bloom composite are
drawn at the picture's size and that picture replaces the game's upscaler's output. Off by default; with it off
nothing changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:44:07 -05:00
chrg6
f37d7fed6d Make replay_workers default to auto, which picks the count from the CPU's cores.
Auto gives half the physical cores less one, at most 6: 2 on 6 cores, 3 on 8 (the count measured best on an 8-core CPU), 5 on 12. Four cores or fewer get none, and so does AMD's Windows driver for RX 6000 and older, where a player's device was lost with workers on and the cause is not known. The menu's row lists Automatic first with the count it gives on this PC, and the log says how many were started and why.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:33:14 -05:00
chrg6
f2849c8b26 Merge main's tail_early, replay_small and Testing rows into the 0.10.0 line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	runtime/cfg.c
#	runtime/vk_exec_xfer.c
2026-11-13 00:26:53 -05:00
epicmeme546
cb5965d14e Name the Testing rows of the performance settings in plain words and make two of them switches.
The rows are now CPU helper threads, Helpers work ahead, Helpers take small jobs and Start next frame early, each
with a short description of what it does and what it gained. Helpers work ahead and Helpers take small jobs are
switches: on writes replay_pipe = 4 and replay_small = 16, the values measured best, and another value set in
nx2navi.ini shows as on and stays.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-13 00:24:38 -05:00
chrg6
89ed3ffd17 Read ahead for mesh draws too, and let a mesh draw wait for its fast link like a vertex draw.
The look-ahead of a submission passed over mesh draws, as their pipelines had no stage libraries. Where they have
them now, a mesh draw the scan reaches is looked up with its mesh and task stages and its state is named to the
pipeline hints, so the library of its task and mesh stages, the one that takes longest to make, is building before
the replay gets to the draw. Never with the mesh fallback, which draws through vertex pipelines of its own.

A mesh draw that waits for a fast link was cut off once its pipeline had been building for 8 ms, since no link
could come. That rule now holds only for a mesh pipeline without stage libraries: with them the draw waits as a
vertex draw does, the few milliseconds of pipe_link_draw_ms from its pipeline's first miss. A one-shot mesh draw
keeps its 8 ms: when the caches lack the task and mesh library, making it is as long a compile as the whole pipeline
was, and the frame's wait pool is not spent on that.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:31:13 -05:00
epicmeme546
86131455ed Hand a group of draws too small to cut to one replay worker, whole, with replay_small.
With replay_pipe the submitting thread goes on past the groups its workers record. With NX2NAVI_REPLAY_SMALL=N a
group of N draws or more that is too small to cut into chunks goes to one worker as it is, and the submitting thread
goes on without taking the chunk itself. Off by default. The early tail now waits for groups still out before it
cuts the part, instead of giving the frame's tail up.

Kokiri Forest standing, RX 7900 XT, FSR 4, the early tail on, 5 workers, 4 groups out: about 700 draws a frame were in
such groups, 3.6 ms of the submitting thread; with 16 the replay took 10.2 instead of 11.0 ms and the frame rate was
63.2 against 62.2, 45.4 with everything off. The screenshots matched those with everything off, standing and
through warps to five areas.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 18:58:39 -05:00
epicmeme546
aec5e72051 Let the game go on while the GPU runs the end of the last frame, with tail_early = 1.
The game waits each frame until the GPU has finished the frame before. In Hyrule Field with FSR 4 the last of that
work is one part of 4.7 ms, the temporal upscaler and the interface drawn after it, then 1.4 ms of write-backs, and
what the game reads of the frame is done before the upscaler runs. With NX2NAVI_TAIL_EARLY the replay records that
tail into a command buffer of its own, and when it hands the CPU nothing but timestamps, writes no pool memory but
inline uniform updates of mirrored blocks and reads no guest memory late, the write-backs go to the queue before it
with the executed timeline's signal. The game's waits end there, and the GPU runs the tail while the next frame is
prepared. The GPU's commands are the same; a tail that fails a test goes as before. Off by default.

RX 7900 XT, Ryzen 7 5800X3D, 3440x1440, FSR 4 Native AA, one batch, replay workers off: 50.7 against 42.5 fps
standing and 56.0 against 45.9 along the field route; with FSR 3.1, 51.8 against 47.8 and 54.9 against 52.3. With
three replay workers, FSR 4, standing: 63.5 against 49.4. The screenshots matched those without it standing, along
the route and through warps to five areas, with no black frame outside the loading screens of both.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 17:41:47 -05:00
epicmeme546
94dd340fee Turn the replay workers off by default again, and set them in the settings menu's Game tab.
Players' games crashed with three workers on by default in 0.9.1 and 0.9.2: an RX 6700 XT lost its device after
about 18,000 frames, its last mark a compute dispatch of the replay, with an occlusion query slot found unavailable
after its fence just before. The cause is not known yet, so replay_workers is 0 again unless a player sets it. The new
Replay workers row (0 to 8, experimental) writes replay_workers and waits for a restart, as the other restart-only rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 14:48:47 -05:00
chrg6
a464ffafbc Merge Tobim6's work for older graphics cards: mesh shaders emulated with compute, video memory kept within the heaps' budgets, staged mirror uploads and the submit wait that goes with them.
Conflicts: the settings key lists take both sides; frame generation keeps the camera-cut handling and logs its counters every 600 frames with the cuts among them; the pass-time mark moves into draw_viewport; mo_desc keeps the pose depth and notes the reader.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 12:14:58 -05:00
chrg6
5f10a0012b Merge remote-tracking branch 'nx2navi/main' into ra-all-test 2026-11-12 11:23:49 -05:00
epicmeme546
322d7c0933 Run DLSS or FSR where the game's own upscaler runs, and fill a jittered pass's edge rows as soon as it ends.
With the HUD hidden the screen showed the scene at its render size instead of the upscaler's picture. The upscaler ran at the pass after the game's own upscaler, taken for the pass the HUD is drawn into. Without a HUD the game draws no such pass: the next one is the frame's last, which encodes the stand-in's scaled scene into the 8-bit output. The upscaler's picture went into that target and the last draw drew over it. A player's save with the HUD off showed blocks of render pixels, a picture jumping by a render pixel between frames, and the undrawn edge row in white, at every FSR mode and at Native AA; every test save until now had the HUD on. vk_dlaa_upscaler now runs the upscaler right after the stand-in of the game's upscaler wrote its output image, and puts the picture there (NX2NAVI_DLAA_AT_UPSCALER=0: as before).

The edge fill moves to the end of each pass that took the jitter (vk_dlaa_pass_end), for the pass's colour targets and depth, in one copy of eight regions per image. Filled only before the upscaler, the undrawn row had already been read by the water, the bloom and the anti-aliasing pass.

On a copy of the player's save (HUD off), FSR 4 Ultra Performance, world slowed to 1/20, present screenshots 0.25 s apart: the picture moves 0.00 to 0.02 px between shots (old placement: a jump of one render pixel in 5 of 20), the change on edges is 1.9 against 16.9, and no shot has the white row (half had it, with HDR on). Native AA, Performance with HDR, and FSR 3.1 are steady and clean as well; the save with the HUD on is unchanged. A run through a stream: 0 of 106 shots with a bright bottom edge (20 of 102). Frame rate 58.3 fps against 58.6.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 11:14:16 -05:00
Leonardo Chappuis
82e093ef93 Fix a deadlock between the replay workers in vk_exec_pass_end.
vk_exec_pass_end took tfr_lock and then, in vk_texture_gpu_written, the texture lock. A chunk recorded by another
thread can hold the texture lock in the pool sync and end its pass there (a lookup's alias copy through
vk_exec_inline_begin), and then wait for tfr_lock. The two chunks waited for each other and the other chunks spun in
their turns, so loading after Continue never finished with replay_workers = 3. The targets are now marked written
before tfr_lock is taken, which guards only the target history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 10:30:14 -05:00
Tobim6
38a3824b8f Mesh emulation: cut the part at a pass's first emulated draw instead of splitting it, so the texture pool set stays.
A split hands the part to the queue and seals its texture pool set; the next part takes another set of the ring and
refreshes its entries first. The splits for the mesh stages made 5 parts a submission and 6 set switches a frame at
2.3 ms of replay each (Kokiri Forest on 0.7.1, an RX 580: replay 32-38 ms a frame, the replay thread blocked on fences
26-35% of the time, 79 waits for a set the GPU still used). A cut (vk_sync_cut) ends the command buffer without
submitting it: the part and its pre-pass command buffer wait in a chain and go to the queue, in order, with the next
part that does, so the set is the same one throughout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:22:37 +01:00
Tobim6
55ab90c66e Mesh emulation: the stages of a pass's emulated draws run before the pass, in a command buffer of their own.
Each emulated mesh draw ended the pass, ran its stages between four barriers and began the pass again: a pass of 20
emulated draws became 20 passes, and on an RX 580 6 to 7 ms of a frame's 30 ms of GPU time went to the gaps between
passes (gpu_passes in Kokiri Forest: 8.7-10 ms per frame outside the game's passes and dispatches, 6.3-7.7 ms of it in
273-306 gaps with barriers only; 71 emulated draws a frame, 36 of them indirect). Now the first emulated draw of a pass
ends the part there (vk_exec_split_now), so the pass begins a new part, and the stages of every emulated draw of that
pass, the indirect ones' ring entries included, go into the part's pre-pass command buffer (vk_sync_pre), which the
queue runs after the mirror prologue and before the part; the pass is not split again. NX2NAVI_MESH_EMU_BATCH=0 (Lama
patches: Mesh emulation before the pass) brings the split back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:22:35 +01:00
Tobim6
1bfe90e3e2 Pass times: log the largest viewport each kind of pass draws with.
A dynamic resolution that keeps its targets at the largest size and draws the scene into part of them shows up as a
viewport smaller than the pass; one that resizes its targets shows up in the pass size. NX2NAVI_SCENE_SCALE=33 left
the handheld scene pass at 1440x810, so this tells which of the two the game does there.

(cherry picked from commit af7abe36b0b15c5a63b354d2870543987d1f4b1a)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:21:54 +01:00
chrg6
56e142169c 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>
2026-11-12 07:29:41 -05:00
TheShinySylveon
0cb2e6f00a Reserve no core for the recording thread by default
On an i9-14900KF (8 performance and 16 efficiency cores, 32 logical processors), with the replay on its own thread,
the core NX2NAVI_RECORDER_CORE=auto reserved (logical processors 14 and 15) left the woken replay thread unscheduled
for about 200 ms roughly once a second while the game stood in Kakariko. During each stall its queue held 40 to 50
items, its lock was free, the wakes had been sent and it used no CPU time, while a game thread waited on a fence of the
work in that queue: 27 frames a minute had a 150 ms or longer frame. With NX2NAVI_RECORDER_CORE=0 the same minute had
none. The default is now 0; auto and 1 still reserve the core, and the log says when none is reserved.
2026-11-12 07:12:41 -05:00
epicmeme546
432b4e4b1c Turn the replay workers on by default: three of them record a frame's large draw groups beside the submitting thread.
On an RX 7900 XT with a Ryzen 7 5800X3D, Hyrule Field standing went from 41.2 to 50.4 fps in one batch, and the
pictures matched those with the workers off there and in Kokiri Forest, Kakariko, Goron City, a graveyard house and
Gerudo Valley. replay_workers = 0 in nx2navi.ini turns them off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 07:10:27 -05:00
epicmeme546
7eee4817f9 Split the uniform snapshot's copy over the replay workers at the start of a submission.
The workers have no chunk to record while the snapshot copies the uniform ranges a submission binds (about 700
regions, 0.7 MiB in Hyrule Field), so the replay thread now hands them slices of it through vk_exec_par, a loop the
workers take between groups. Without workers the copy runs on the replay thread as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 07:10:15 -05:00
chrg6
2f39b27fb6 Record the draw replay on three replay workers by default, and start the draft notes after 0.9.0.
replay_workers = 0 records everything on the submitting thread, as before.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 06:20:05 -05:00
epicmeme546
eb7072f90a Make NX2NAVI_REPLAY_PIPE the number of groups the submitting thread may have out at once, from 1 to 4.
Hyrule Field, standing, 3 workers: with 1, 2 or 4 groups out the frame rate stayed within one frame per second of
the 50 the mode without pipelining makes, over three batches, though the replay is shorter. A frame there is the
replay and then about 6.6 ms of GPU work after it, which the next frame waits for; the design comment in vk_exec.c
now says so. Sending a part's finished command buffers while later groups record was tried and gained nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 04:39:12 -05:00
epicmeme546
ffa599ef33 Let the submitting thread go on past the replay workers' groups, with up to four groups in flight.
With NX2NAVI_REPLAY_PIPE=1 (off by default) the submitting thread hands a group's chunks to the workers, takes chunks
of it itself until none is left to take, and goes on with the ops after the group in a new command buffer after the
chunks' places in the part (vk_sync_append_later), also into the next group's front pass.
Up to four groups are handed out and not in their part yet; their chunks wait in one queue in the order of the stream.
The submitting thread waits for them only where the order needs it: for room, before a part goes to the GPU, before
copies and texture or buffer clears, where DLAA's run or the contact shadows' hook acts, and where a lookup would make
a texture entry or take back a retired one (the op goes again afterwards, as a chunk's does in turn).

What the chunks read stays as the stream had it while the submitting thread runs ahead: uniform snapshot drops count
from their place in the stream, a chunk reads only the inline uniform updates before its group, the HDR present's
source goes by stream position, the texture pool set syncs under the chunks' lock, each group keeps its own render
conditions, occlusion segments and closed query slots, and the queries after a group's last report go into the next
segment made after it. A group's first chunk runs its pass hooks after the group before began its pass; turns wait
for every earlier chunk, of earlier groups too. Locks the chunks take, the submitting thread takes while it is behind.

Draws keep the sampled-texture descriptors they resolved per draw context and reuse them without the texture lock
while their slot is unchanged; the workers waited for that lock at nearly every draw. Texture lookups that change
nothing no longer wait for their turn. The perf line says where the submitting thread waited, what the chunks took,
and which texture lookups waited; NX2NAVI_GPU_TIMELINE also shows the chunks and the waits.

Off by default because it does not pay yet: in Hyrule Field with 3 workers it shortens the replay (11.5 against 12.1
ms a frame) but not the frame (49.2 against 49.6 fps in the same batch). The GPU gets a group's commands only once its
last chunk is recorded, so the main thread's wait for the GPU after the frame's first submission grew by 0.8 ms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 04:39:12 -05:00
epicmeme546
9fe3dd3331 Replay workers record mesh draws, and stop waiting for each other at queries and per-object motion.
Mesh draws ended a group and stayed on the submitting thread: 119 a frame in Hyrule Field at about 13 us each, and the
groups around them were cut short. They are recorded in the chunks now: the entries filled ahead for the indirect ones
are the draw context's, the ring index is taken atomically and the ring is made before the chunks record, each chunk
counts the ops that break a run of them on its own, and the lazy lookups of the mesh and shading-rate entry points store
their pointer before the flag that says it is there. With the mesh fallback, and while NX2NAVI_GPU_PASSES copies the
workgroup counts, they stay on the submitting thread.

A chunk's closed occlusion query slots go into a list of its own and into their segments once the group is done, in
chunk order, instead of under a lock per query. The per-object motion check decides alone per draw context (its
variant memo is per context) and takes its lock only to list a draw. The occlusion query ring has 16384 slots: the
chunks' blocks are sized to their draws, and with 4096 the ring came round to slots not reset yet.

The perf line splits the submitting thread's time with workers on: ops outside groups by kind, the front pass, its own
chunks and the wait for the others, and which ops ended the groups.

Hyrule Field, RX 7900 XT, 3440x1440, FSR 4 Native AA, same batch: standing 51.7 fps with 3 workers against 43.6 with
the workers off (replay 11.2 against 15.1 ms); along the field route 48.9 against 44.7. Screenshots standing and along
the route match the replay without workers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:15:35 -05:00
epicmeme546
a0a7548d30 Replay workers take their occlusion query slots in the order of the stream.
The chunks of a parallel group took query slots from the ring as they recorded, so the slots of the group's chunks
interleaved. The completion thread reads and resets slots in runs that follow the ring, and with interleaved slots it
made four times the result calls and a reset per slot: the wait after a frame's first submission ended 3.5 ms after
the GPU had run it instead of 0.6 ms, and the replay time the workers saved went into that wait (44 fps with 3 workers
as without). The front pass now gives each chunk a block of slots, as many as its draws that count samples and render,
in the order of the stream; a segment's slots are taken in ring order. Standing in Hyrule Field, 3 workers: 49.8 fps
against 43.7 with the workers off.

NX2NAVI_GPU_TIMELINE=<frame> writes four frames from that present on to the log: each replay, each timed part's submit
and GPU run, the waits for the GPU and the workers' groups, so a run shows whether the GPU waits for the replay, a wait,
or the CPU behind one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:15:35 -05:00
epicmeme546
77ae0f658e Keep samples-passed counters inside parallel groups, and lock the shared tables only while a chunk records.
Samples-passed resets and reports ended most groups in Hyrule Field (the game reports twice without a reset there, so
counting stays on): the front pass now replays them in the order of the stream, makes each occlusion segment and
queues its completion callback in that order, and each worker follows the counting from its chunk's first op and adds
the queries it closes to the segment the stream gives them. Per-object motion takes a lock instead of waiting for the
earlier chunks: its list's order does not change the picture. The locks the chunks share are taken only inside a
chunk, as outside groups the submitting thread alone replays; taken always, they cost the replay about 1 ms a frame.
The perf line counts the draws by what ended their group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:14:37 -05:00
epicmeme546
7534193b62 Record the large draw groups of a submission on several replay workers at once.
Stage 2 of NX2NAVI_REPLAY_WORKERS (default 0, the replay as before). A run of draws is cut into groups: a group ends
where the render targets change after its first draw or clear, and before an inline uniform update, a mesh draw and a
samples-passed counter op, which the submitting thread replays itself in the order of the stream. A group of 64 draws
or more is first walked by a front pass on the submitting thread: it applies the state and binds, settles the CPU side
of the timestamp reports and decides the render conditions in the order of the stream, and copies the replay state at
the first op of each chunk. The workers and the submitting thread then take the chunks in order and record each into a
command buffer of its own, which vk_sync_append puts into the part in the order of the stream.

The frame's pass hooks (contact shadows, DLSS) run in a group's first chunk, which the others start after. What still
depends on the order between chunks waits until every earlier chunk of the group is done: a texture lookup that would
make or take back an entry returns nothing and the chunk replays the op again in turn, and so do the contact shadows'
pass, per-object motion and the HDR pass's source. The tables the chunks share take locks (the shader table, the
descriptor lists, mirror use and GPU write notes, uniform snapshot drops, target frames, the DLAA camera votes and the
previous-frame motion notes); each chunk keeps its own occlusion query open, its own feedback memo and no pass marks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:14:37 -05:00
epicmeme546
4fc32e8d84 Hand the draw context down in the replay state instead of reading it from a thread-local at every call.
Reading the thread-local pointer in the functions that run per binding and per descriptor cost about 0.3 ms of
replay a frame in Hyrule Field (45.0 against 45.8 fps). VkExec.dc now names the context; the draw and look-ahead
entry points fill it from the calling thread's on first use, the functions that hold the state read it from there and
the few helpers without it take it as a parameter. A replay worker's copy of the state carries the worker's context.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:14:37 -05:00
epicmeme546
3010a637bf Let one replay worker record each long run of draws from a copy of the replay state, behind NX2NAVI_REPLAY_WORKERS.
Stage 1 of recording a submission on several threads. 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). With NX2NAVI_REPLAY_WORKERS=1 a run of 16 draws or more is recorded by a worker thread: it takes a
copy of the replay state, records the run into a command buffer of its own pool with a draw context of its own, ends
it behind a full barrier and hands the state back. vk_sync_append puts that command buffer into the part between the
replay's own pieces before and after it, so the GPU runs everything in the op stream's order. The submitting thread
waits for each run, so every side effect keeps the replay's order; this stage checks that a run recorded apart from
a copy of the state gives the same pictures. The default, 0, replays as before.

walk's handling of the ops that do not steer it moves into op_one, which the worker uses too. NX2NAVI_REPLAY_RUNS=1
logs every 600 frames how the draws fall into such runs and which ops break them. The draw replay's hot functions load
their recording context once instead of at every use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:14:36 -05:00
epicmeme546
85af8a8f7f Keep the draw replay's per-recording state in one context a thread reaches through a thread-local pointer.
vk_exec_draw.c kept what a draw, bind or dispatch carries to the next one in file statics: the state the command
buffer holds, the descriptor signatures and skips, the per-binding reuse, the shader and pipeline memos, the shader
lookup's caches and the submission's transient resources. They now live in one DrawCtx, and every thread starts with
the replay's own, so the replay behaves as before. A later stage can give each recording thread a context of its own.

vk_exec.c's inline commands (vk_exec_inline_begin) find the replay state the calling thread records with through a
thread-local pointer too, instead of the file's single state and a flag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:14:36 -05:00
chrg6
2cccdb8823 Keep the game on the largest-L3 die only when that die has 8 cores, and reserve no recorder core there.
cache_die moved the process to the logical processors of the largest L3 cache whenever the L3 sizes differed. That is
right for a two-die X3D desktop part with 8 cores a die, but it also held the game to the 4 large cores of a mobile
part whose 8 small cores share a smaller L3, and to the 6 cores of a 12-core X3D part, where the recording thread's
reserved core then left the game 5. The die is now used only when it has 8 or more physical cores (perf.c
cpu_placement logs when one is passed over). And while the process runs on that die alone, RECORDER_CORE=auto
reserves no core for the recording thread, since the game's threads already have the die to themselves; =1 still
reserves one.

Checked: all runtime files compile. This PC has a single-die processor (one L3), where neither rule applies: a run
still reserves the recording core (logical processors 14 and 15 of 16) and logs no cache die.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:18 -05:00
chrg6
6c991d628d Start a texture the CPU does not write, made over storage the CPU writes, with that storage's bytes.
Two descriptors can name one CPU-written storage: one whose content the cache takes from the CPU, and a second with a texture flag for which it does not. The second has no upload of its own and its slot shows the first one's image while that entry is live. Once the first is released, a lookup of the second's slot makes it an entry of its own, start_at destroys the released first one, and the new image starts cleared and is never uploaded; the slot shows it from then on, also after the first descriptor is registered again. The same happens when both are released and the texture pool refresh looks the second's slot up before the next texture arrives in that memory.

With tex_split_fill = 1 (the default) start_at gives such an entry the storage's bytes: by a copy from the released CPU-written entry it is about to destroy when that image is current and alias_compatible allows, else by an upload from the storage. The image then follows the storage as a CPU-written texture's does until the GPU or WriteTexels writes it; 2 fills it once, 0 is the behaviour before. Depth, stencil and multisample images, pools without CPU access and storage over 32 MiB are left out, and an entry that is to be filled gets a new image instead of a released entry's, since a reused image goes through its first transition after the upload batch. The perf trace counts the copies, the fills with their bytes and time, the fills made again after a write and the entries left empty.

--probe-vkmem makes both sequences with the three values and reads the second descriptor's image back: zeros with 0, the storage's bytes with 1, and with 2 the bytes of the texture that was in the memory before when both descriptors were let go of. One case makes the entry inside a recorded submission, through a hook that lets the probe stand in for a replay, and there the content comes by the copy.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 05:57:23 -05:00
chrg6
2ccd69ef15 Merge the crash and memory fixes: a clean stop on a lost device, a commit gate for background pipeline builds, a start-up memory warning, and a retry for a texture whose decode had no scratch memory.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 04:49:50 -05:00