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 <noreply@anthropic.com>
The review of cd2fd454 found that vk_sync.c submit calls vk_present_frames_owed before its replay timing starts
(t0 = perf_begin), so the owed bookkeeping a submitting thread runs was timed nowhere: a DLSS release in
vk_dlaa_frame (16 to 45 ms, F-4-02) would have shown as unattributed time in the frame-time and slow-frame lines, where
the design counts it as that thread's replay time. vk_present_frames_owed now times the frames it runs and adds them
with perf_replay. The present thread's catch-up before a blit (present_frame_closed) is not replay: it shows in the
present thread's own blit time and in the frame bookkeeping line.
Checks that passed: pass_cc_check of vk_backend.c with no new -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of cd2fd454 found that with present_order = 1 the present thread could record and queue frame N's blit
before frame_end(N) ran: when the presenting thread's try of the submission lock failed, frame_end(N) waited for the
next submission, and that could be the present thread's own blit submission, which present_main makes only after
vk_present_blit. The blit reads what vk_dlaa_frame resets when the upscaler or frame generation is switched on or off
(gfx_present.c hudless_record through vk_dlaa_fg_hudless; vk_fg.cpp, vk_fg4.cpp and vk_dlssg.c through
vk_dlaa_fg_inputs, vk_dlaa_fg_views and vk_dlaa_fg_camera). vk_dlaa_frame drops those inputs at a switch because
handing them to frame generation ended an AMD process before, and in 0.10.1 vk_dlaa_frame(N) always ran before blit
N. So a switch frame inside an overlap window could hand frame generation the inputs the switch should have dropped.
Now present_image tells the present thread when it left the frame owed (PresentJob.owed), and present_main first
makes sure that frame's bookkeeping ran (present_frame_closed, vk_frame_owed.h fo_close): when a submission did it
meanwhile, done says so (it is now stored with release after each frame's bookkeeping and read with acquire, fo_ran)
and no lock is taken; otherwise the present thread takes the submission lock, waiting for a replay in progress
(vk_sync.c vk_sync_locked), and runs what is owed. frame_end(N) therefore always comes before blit N, as in 0.10.1.
The present thread is not one of the game's threads, and it waits for the same lock for the blit's submission a
moment later anyway. It waited for that lock with its busy flag set before too, and nothing that holds the lock waits
for the present thread, so no deadlock is added. The cost, only for a frame still owed when the
present thread takes it: the blit's recording no longer overlaps the replay it waits for, which can lengthen a drawing
thread's existing wait in vk_present_drain by that recording. The common frame, whose present got the lock, takes no
lock here. The log's frame bookkeeping line (exit, and trace = perf every 600 presents) counts these frames and the
time they took the present thread, the wait included.
tests/frame_owed_check now has a present thread that takes each frame from the presenting thread (one in waiting at
most), blits it and submits: no blit comes before its frame's bookkeeping. Its "old" argument leaves the catch-up out
and fails, as the branch had it: 2608 and 2706 blits before their frame's bookkeeping in two runs of 20,000 frames.
docs/building.md, docs/architecture.md and nx2navi.ini.example say where the bookkeeping runs.
Not reproduced before the change. Checks that passed: frame_owed_check (20,000 frames; 12,695 owed, 1,888 of them
caught up by the present thread, 0 blits before their bookkeeping, the longest present 0.003 ms), pass_cc_check of
every runtime file (headers changed), no new -Wall -Wextra warnings in vk_backend.c and vk_sync.c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_sub_in_frame says which of its frame's submissions the one being recorded is; vk_memory.c sub_place files the mirror
pre-snapshot's statistics by it during the replay, on the replaying thread and its workers. vk_be_submit wrote it on
every submitting thread before submit_sync, outside sub_lock, so with the replay on the submitting threads another
thread entering a submit changed it under a replay in progress, and the replay filed its work at the other
submission's place. Each thread now notes the place in a thread-local (vk_be_submit, and replay_main for the
asynchronous replay), and vk_sync.c submit publishes it right after taking the lock for a submission with commands
(vk_sub_in_frame_publish, a weak no-op without the back end). Submissions without commands of their own (held work, the
present thread's blit) leave it as it was, as before. Performance heuristics only; no timing change.
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_backend.c and vk_sync.c.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The submit-path changes leave three numbers a play-test needs: how many presents left their frame's bookkeeping to the
next submission because another thread held the submission lock (fo_owed_n) and how many frames a submission caught
up (fo_late_n); how many held presents another thread's submission put out (ph_out compares the thread with the one
that presented, now kept in PhJob.tid); and how often the presenting thread waited for PH.lock while one went out,
with the time in all and the longest (vk_present_held.h's counts). vk_backend.c order_log writes them on one line:
with the synchronous replay at exit (gfx_exit_log, "vk: over the run to frame N: frame bookkeeping: ..."), and with
NX2NAVI_TRACE=perf at every 600th present when a count moved since the last such line ("perf: since the last such
line, at frame N: ..."). With present_order = 0 the bookkeeping counts stay 0. The presenting thread's wait is also in
the slow-frame lines' "drain" since the lock commit. No timing change.
Checks that passed: pass_cc_check of vk_backend.c with no new -Wall -Wextra warnings, present_held_check,
perf_summary_check.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>
The replay asks whether a frame that skips a draw would be held (vk_exec_draw.c, vk_frame_hold_left) on whatever
thread holds the submission lock, and with the replay on the submitting thread that need not be the thread that
presents. vk_frame_hold_left refilled FH itself (frame_hold_refill), so a refill on a replaying thread beside the one in
frame_hold on the presenting thread wrote left_ns and last_ns from two threads: an interval counted twice or not at all,
which decides whether a frame with skipped draws is held. Now the reader computes the same arithmetic from the stored
fields (fh_left_at) and writes nothing; the limit is read once with a compare-and-swap whose winner logs it; left_ns,
last_ns and holding are stored and loaded as atomics. Refilling in two steps or one gives the same allowance, so the
single-thread case is unchanged.
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 <noreply@anthropic.com>
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>
With DLSS-G, Reflex's sleep held each frame's start with no limit while the game's pacer still held the limit on the
present thread and at the window image's acquire (A-16, P-16): the pacer's wait came after the picture was drawn, so
the pad was read up to a period before the picture left, on top of Reflex's own wait for the GPU.
vk_dlssg.c vk_dlssg_pace, on the presenting guest thread in nvnQueuePresentTexture, asks dlssg_pace.h which limit Reflex
should hold and stores it; vk_dlssg_frame on the present thread hands a changed limit to Streamline before its present
(reflex_apply, under the present lock, which keeps Streamline's options calls on one thread at a time), as does each
new swapchain, which has neither mode nor limit. GfxBackend.frame_paced now returns the period the pacer still holds:
vk_backend.c vk_be_frame_paced gives the game's ceiling (frame_step_ceiling_ns, 240) while Reflex holds the limit,
0 where a driver's low latency holds it (vk_lowlat_pace, unchanged), else the period; during a movie the back ends get
0 and the pacer keeps the movie's period and catch-up slots as before. The pacer takes the limit back before the first
host present, after 200 ms without one, with a DLL older than nx2sl_reflex2 (one log line at start), for a limit
Streamline refused, and for the run when the frames show Reflex does not hold it; the "frame rate:" lines name who
holds it, the "dlssg: present N" line counts the frame starts Reflex held, and Reflex's sleep now counts as pacing
(perf_end_any, on whichever thread acquires). No wait is added on a drawing thread: the limiter's wait moves from the
acquire's sleep_until and the present thread's slot to Reflex's sleep a few lines later in the same acquire, and the
present thread's options call returns at once. docs/architecture.md names Reflex as a limiter. Not reproduced before
the change (no RTX 40 or 50 here); vk_dlssg.c, vk_backend.c and nvn_api_obj.c compile clean with the dev build's
flags, and dlssg_pace_check, fg_reset_check and frame_step_check pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review found that the comment in vk_backend.c present_main and the message of 3236dc3c gave a cause that cannot
happen: the menu cannot turn trace=perf on between the two reads. rt_trace is set once at start (main.c parse_trace),
TRACE is a RESTART row in cfg.c metas, no menu code changes it, and the only other writer, ipc.c probe_sdk_client,
runs in a probe mode and changes only the IPC bit. The single read stays, since the stamps and both lines then follow
the same flags; the comment now says only that.
Comment only. vk_backend.c compiles with the dev flags and with -Wall -Wextra without new warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
present_main (d84aa5fb) takes the blit and present-call stamps only while trace=present or trace=perf is on, then
read rt_trace again to decide whether to log. The menu's performance log can turn trace=perf on between the two
reads; the first window would then add the difference of a stamp and 0, a wrapped figure. The flags are now read once
at the start of each present and used for the stamps and both lines.
Diagnostics only. vk_backend.c compiles with the dev flags and with -Wall -Wextra without new warnings;
perf_summary_check passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_backend.c synced_held_count counts the game's fence waits that return while a submission stopped at an event wait
is still held, to decide whether the value should be signalled only after the held remainder (F-2-05). The window
acquire's sync is fenced with value 0 (nvn_api_obj.c nvnWindowAcquireTexture), so a game's wait on it reached the
counter as a wait for 0, which no held submission signals: 6 of the 12 "a game's wait for N returned with K
event-ordered submission(s) still held" lines in the two 0.10.0 fresh sessions were "wait for 0". The counter now
returns at once for value 0, before counting.
Not done here: counting only the waits for values at or after the oldest held submission, which needs its value kept
in vk_exec.c's continuations. Diagnostic only. Not reproduced before the change (it needs a game run).
vk_backend.c compiles with the dev flags and with -Wall -Wextra without new warnings; frame_step_check passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The perf lines could not see pacing. In the 0.10.0 fresh session (nx2navi.1.log, 7,974,927 bytes) all 666 windows read
"pacing 0%" and "bound: cpu" and all 70 slow-frame lines "pacing 0.0 drain 0.0", while 610 windows had a 16.x ms
interval at fps_limit 60 under the game's own limiter. The pacer's wait happened where perf does not count it:
vk_backend.c present_image's wait for the present thread to take the previous picture was untimed, perf_end drops
waits on any thread but the presenting and replay ones (a game may acquire its window image on another), and the
acquire's GPU wait was counted twice, as pacing around b->fence_wait and as a fence wait inside vk_sync_fence_wait
(F-7-04).
- host.h: PERF_HANDOFF, the presenting thread's wait to hand the present thread the next picture, timed in
present_image from under pl (where pending is read) and kept in PresentJob.handoff_ns. perf_end_any counts a wait
on any other guest thread into a sum that perf_present adds to the frame before ft_frame reads its parts.
- nvn_api_obj.c nvnWindowAcquireTexture: the pacer's sleep is timed with perf_end_any; the GPU wait counts only as
the fence wait it is; one line names the threads that acquire and present once the first present is known.
- vk_backend.c present_main: the TRACE=present line adds the present call's time (vkQueuePresentKHR, under the queue
lock, where an overlay's frame limiter sleeps) and the job's hand-off wait; with trace=perf, a line per 60 presents
gives the present thread's blit and submit, paced wait, present call (avg, max), time past target and the hand-off.
- perf.c: "handoff" in the window line's shares and the slow-frame line, "hand-off" in the worst-frame lines and as a
cause of its own for frames over 25 ms and the stutter map. The "bound:" rule stays as it is until play-test numbers
exist.
- The cause list changes together: perf.c FT_C_* and ft_cause_name, stutter_map.c SM_CAUSES and cause_short,
tests/stutter_hitch_check.c names[] (now sized from SM_CAUSES), and tools/stutter_map.py, whose cause and hitch
patterns now take a hyphen. flight.c's frames.tsv gets a handoff_ms column (blocked_names checked against PERF_N),
and tools/flight_view.py lists it.
- tools/perf_summary.py reads the hand-off share, the present-thread lines and the TRACE=present lines, and prints
percentiles of the present interval, paced wait, time past target, present call and hand-off.
New tests/perf_summary_check.py makes lines with the runtime's format strings (checked against the C sources) and
checks the percentiles, the window averages and the hand-off share, and that older lines without these fields still
parse; tests/stutter_map_check.py gets a hand-off hitch and area cause, which the old patterns dropped. Diagnostics
only: no wait, pacing or presentation change. Not reproduced before the change (it needs a game run with trace=perf).
perf_summary_check, stutter_map_check, stutter_hitch_check, flight_flash_check and frame_step_check pass; every
runtime file compiles with the dev flags after the host.h change, and the changed files add no -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The driver's sleep sits where the game's frame starts: at its first pad read or window image acquire after a present,
whichever comes first, before the input is sampled. The frame's number, the count of the game's presents, goes with
its work: the guest thread marks the simulation and the first submission, the replaying thread tags its queue
submissions and marks their end, and the present thread marks the host present, which carries the number too. The
window's swapchain is made with the latency mode while NVIDIA's path is on, and remade when the setting changes.
While the mode is in effect the pacer holds nothing, so a picture goes out as soon as it is drawn and the limit is
kept by the driver before the pad is read; RivaTuner Statistics Server is kept out as with frame generation. The pacer
takes the limit back while a movie plays, while no host present goes out, and for the rest of the run when the frames
show the driver does not hold it. With the mode off every hook returns at its first check.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
A registration the game made while a submission was being replayed changed the slot table under that submission,
whose texture pool set is written as the replay goes and read when it runs. Such a registration now waits and takes
effect once the submission has gone to the queue; with the replay on its own thread it goes into the replay queue
ahead of the next item. NX2NAVI_REG_ORDER=0 restores the old behaviour. In a town session the white kerb, stair and
wall-top edges of single frames were gone with it; the log counts the registrations that waited.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
On 0.9.1 this is the release's own mirror capture (NX2NAVI_MIRROR_CAPTURE_KB) turned on: with
NX2NAVI_MIRROR_STAGE=1 (the default) the pages the CPU wrote are copied out of guest memory, up to 64 MiB a
part, when the part is handed to the queue. A submission whose prologue copied everything that way reads no
guest memory late, so the game's wait after a frame's first submission ends once the replay has submitted it
(vk_sync_recorded_wait). One with a range left to the GPU-time copy (a pool with a write-back in flight or a
twin) or a draw that reads a pool without a mirror is noted (vk_mem_note_live), and its wait is the GPU's.
Also 8 texture pool sets.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
A submission stopped at an event wait returns its value with its remainder held for a later replay, so the game may
take its wait as the whole submission done. The log now says how often that happens, which decides whether the value
should be signalled only after the remainder. Audit finding F-2-05, first step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a lost count the frame's first submission could go without its wait for the GPU, which is there against frames drawn with the next frame's data.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Every pass begin and every compute dispatch recorded a full barrier, which makes the GPU finish everything before each of them. With barrier_game = 1 (NX2NAVI_BARRIER_GAME) a pass after an earlier pass gets a barrier for its attachments only and a dispatch after other dispatches gets none; a change between drawing and compute keeps the full barrier, and so do the game's own barriers and the replay's copies, clears, fills and uniform updates. On an RTX 5090 at 2560x1440 the GPU's time for a frame fell from 5.4 to 4.9 ms standing in Kokiri Forest (two in-run A/B runs, the control's blocks differ by 0.7 %) and from 5.1 to 4.4 ms on a walk through it. The frame rate did not change there, because the replay thread is the limit on that machine; it should rise where the GPU is.
The default is 1 on NVIDIA graphics cards only, and 0 on every other vendor's: the game placed its barriers for an NVIDIA GPU, Vulkan asks for more of them than the game recorded, and no AMD or Intel driver has run this. 34 screenshots across the A and B blocks of one run show the same picture on NVIDIA. NX2NAVI_TRACE=perf logs the barriers a frame by kind.
From pull request #389, which had it on for every vendor; the default by vendor and the settings text are added here. The validation layer's synchronization check could not be run on this machine.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Both are about the time between two frames. Ocarina waits for the previous frame's fence before it lets the next frame's GPU work start: its main thread signals an event from the CPU, and the main queue's commands begin with a wait for it.
The completion thread ran a submission's callbacks before it let the submission's waiters go, and the occlusion counts were among them: one vkGetQueryPoolResults and one vkResetQueryPool per reported query, about a hundred queries a frame in Hyrule Field. On an RX 7900 XT (26.10.1) each call goes into the kernel, 3.9 ms of every frame, so the fence was reached 4 to 6 ms after the GPU had finished with every game thread waiting. Slots come from the ring in order, so one call now reads up to 256 slots from the one asked for, the available results are kept for the segments that follow, and the slots taken are reset in runs (vk_exec_xfer.c oq_take, NX2NAVI_OQ_BATCH). Standing in Hyrule Field at 3440x1440 with FSR: 52.7 fps against 44.6, the longest frame of the measured minute 46 ms against 55; the completion thread spends 0.3 ms a frame in the callback.
When the CPU's signal came a millisecond or two after the main queue's submit call, the submission took its copies, stopped at the wait and was held; the render thread polled the event, presented, and the present replayed the frame under a value the game's fence does not stand for. The next frame then skipped its wait for the GPU and was replayed inside its submit call, and frames of 23 and 15 ms took turns. A submission that begins with a wait for an event the CPU has signalled before now waits for the signal in the submit call, before its copies are taken, 10 ms at most (vk_exec.c vk_exec_gate_wait, NX2NAVI_GATE_WAIT_MS): 54.5 fps and a longest frame of 33 ms in the same place, 58.6 fps with the recorder thread on. With a camera sweep the pictures went out 21.0 ms apart on average with a 99th percentile of 55 ms, against 25.4 and 64. A village went from 48.6 to 53.7 fps and a graveyard from 56.5 to 64.2. 900 consecutive frames of a still scene show no dark or changed frame, with the recorder thread and without.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Each frame the game sets one of two events to 0 from the CPU, submits its worker queues and the main queue, whose first command waits for that event to be 1, and signals 1 with nvnEventSignal once it has written what the frame's commands read. The recording of a submission takes the VRAM mirror sync and the uniform snapshot at its start and tested the wait a moment later, so a signal that arrived in between passed the wait and the frame was drawn from copies taken before the signal: single frames with the whole scene's shadowing missing (6 to 7 of 255 brighter, the sky and the interface unchanged) or a black scene, more of them the busier the CPU.
Every nvnEventSignal is now numbered and keeps the value it replaced, the recording notes the number before it takes its copies, and a signal with a later number does not count for that submission. The submission stops at the wait like any wait reached before its signal and goes on at once in a submission of its own, whose copies are taken after the signal. NX2NAVI_EVENT_AT_SYNC=0 gives the earlier test. NX2NAVI_EVO_TRACE=1 logs the order in which every frame's submissions, waits and signals were replayed, which is how the frames were found.
Measured with the flight recorder's thumbnails, 1,800 presented frames a run, standing in Hyrule Field by day with the lazy write-back on, DLSS Performance, frame generation 4x and HDR: the build before this change showed 34 single-frame spikes with eight busy threads beside the game, 20 with four and 8 with six, most of them in frames whose signal came during the replay; with this change two runs with eight busy threads showed 0 and 0, with 667 and 724 such frames. A session of the user's with the lazy write-back off ran 25,200 frames without a spike in its three automatic dumps while the change stopped 4,744 wait tests.
Not measured: runs of this same build with the switch off (the two tried failed for other reasons, a device loss during the boot beside another game and a load with cold caches), any run with the lazy write-back off under load, the frame rate and GPU time against the switch off, and the differential tests, which this change does not touch. The flicker with the lazy write-back off that was reported has not been reproduced in a capture; its mechanism is the same signal and occurs there too.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The two commits main gained since the last merge. docs/releases/v0.6.2.md stays removed; the two notes they added to
it are in docs/releases/v0.7.0.md. Checked on a dev build of the merged tree: the settings, self-test, graphics ABI,
input and kernel probes pass, and a scripted run with six DLSS mode changes presents no dark or black frame, makes the
area's textures in one frame at the load and ends without a fatal error.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The texture pool creates 16 entries a refresh and stops after 6 ms a frame, so that textures streaming in do not make
long frames. An area load leaves thousands of slots at once (4,936 loading the test save), and at that rate they came
in over the frames after the load screen: the scene's first frames showed a quarter of the picture as black stand-ins,
for 22 presented frames on a free machine and for about three seconds with the CPU busy.
While a refresh leaves pool_load_backlog slots or more (512; 0 never), the pool is loading: the count limit is off, a
frame's refreshes may take pool_load_ms (50), and frame_hold leaves out the presents of frames drawn meanwhile (up to
2 s), so the picture before the load stays until fewer than 64 slots are left. Measured with the flight recorder's
thumbnails of every present: the 4,847 textures are made in one frame of 345 ms, 30 frames before the scene fades in,
and the black share of the picture goes from the fade straight to its final value. The streaming limits are as before
below the backlog.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The overlay's rate, lows and graph were fed from the game's present calls. With the replay on the submitting thread, work held for an event is replayed with the next frame, so the calls come in uneven steps although each frame costs about the same: 36 and 9 ms apart in turn in a village, a sawtooth in the graph and a 1% low of 14 fps, while the pictures went out 19.7 ms apart on average and changed by 3.9 ms from one to the next. The present thread now reports each picture (perfmon_shown) and the overlay takes its intervals from that; the game's calls are used only where no present thread reports.
A picture that arrives early now waits until seven eighths of the recent interval have passed since the one before (vk_backend.c pace_smooth, NX2NAVI_PACE_SMOOTH, on by default; not with frame generation). In that village the change from one picture to the next went from 3.9 to 3.0 ms at 51 fps both ways, in a graveyard from 3.1 to 2.6 ms at 54.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Picking a mode in the settings menu changes the scene's size. The game makes its scene targets again at that size and
they start empty, and the first frames drawn into them are wrong: the first frame of the new size is lit without what
the frame before it left (a dark scene), and two frames later the scene can be black with the interface intact (seen
going to Ultra Performance and to DLAA). Captured over six scripted mode changes with every present saved: bad frames 3
and 5 presents after the change, the same with the lazy write-back on or off and with the frame hold off.
vk_dlaa.c notes a scene of another size within 60 presents of a settings change, and vk_backend.c frame_hold then
leaves out 6 presents from that frame on, once per change and beside the allowance for frames with skipped draws. This
hold also runs with DLSS Frame Generation on (the other one does not): a present left out is a longer gap between two
rendered frames to it, and a dark frame it is given comes out as several generated ones. frame_hold_ms = 0 turns both
off. Checked on a dev build with frame generation at 4x: the hold starts at the first frame of the new size at all six
changes and covers every bad frame of the capture, with no new Streamline error.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With NX2NAVI_REPLAY_ASYNC the uniform snapshot was taken when the replay thread started recording a submission, up to a frame after the game submitted it. The game rewrites that memory without waiting for the GPU: in a graveyard the settings block of the shadow pass held another block's values in the frame its per-frame allocator moved on to the next stretch of memory, about every 364 frames. The submitting thread now takes the copy before it queues the submission (vk_usnap_take), and the replay thread takes it over and leaves out what GPU writes in flight touch (vk_usnap_begin adopt). A frame dump of such a frame shows the block as the game submitted it.
That frame still comes out dark with the option on: vertex, storage and index memory reaches the VRAM mirrors when the replay uses it, as late as the uniform blocks did, and the snapshot covers uniform bindings only. With the option off, 3,000 presents in a row were free of dark and odd frames in that graveyard and in a village; with it on, one frame in about 364 was dark. It gave 65.6 against 56.1 and 64.1 against 54.3 frames per second in those two places. The option stays off by default, and its menu text, the example settings file and the release notes now say that it can show a dark or flickering frame.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A draw whose shader or pipeline is still building is left out of its frame, and the frame was shown without it: a black or unlit picture with the interface intact when it was a lighting pass, a light going out, or ground missing for a frame. In a player's session a full-screen draw was skipped after its 50 ms and the frame came out black as a boulder hit.
The frame's present is now left out when draws were skipped (vk_backend.c frame_hold, NX2NAVI_FRAME_HOLD_MS, 120 by default, 0 off). The game runs on, so nothing waits. The hold comes from an allowance that fills again at an eighth of the clock's rate, so a long build or a run of them shows its frames as before and does not freeze the picture; a full-screen draw that would be skipped with the allowance spent waits on instead, 500 ms at most. Not with DLSS Frame Generation.
On a first walk through a village with no pipeline recorded, 21 of the 23 frames with skipped draws were held back; in the Lost Woods, 7 of 8.
The performance overlay: the VRAM line is the card's dedicated memory in use, which counts every program on it: a player read 14.5 GB as the game's while the game held 7.3 GB. The line's third cell now gives what this process holds on the device-local heaps (the driver's budget figure), in place of the GPU clock, which stays where the driver gives no budget.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
NX2NAVI_ANISO was read once, when the first sampler was made (vk_texture.c sampler_features), and make_sampler raised the level of the surface textures' samplers as it created them. Samplers are looked up by the game's parameters and live until shutdown, so a new level could only reach them after a restart, and the setting had no row in the menu.
The decision which samplers get the level moves from make_sampler into aniso_raised, and sampler_for puts the raised level into the sampler key, as it does with the DLSS texture bias. vk_sampler_aniso_live reads the setting again after a settings change, at each present beside vk_dlaa_frame's bias; on a new level it clears the slots' samplers and bumps the sampler generation, as vk_sampler_dlss_bias does, so the slots and the descriptor caches take samplers of the new level from the next draw. The old level's samplers stay until shutdown like every sampler. The setting is marked live in cfg.c, and the Screen tab gets a Texture filtering row (Game, 2x, 4x, 8x, 16x) with the description the catalog already had.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pre-snap of a submission took the pools the submission before it had synced. In Ocarina those are another submission's pools: the frame's first and fourth submissions use no pool and queried the pools of the ones before them, and the submissions after them then queried their own pools one at a time at first use. The pools are now remembered by the submission's place in its frame.
NX2NAVI_PRESNAP_PLACE=0 gives the old choice back and =ab switches it in the B blocks.
Co-Authored-By: Claude Fable 5.2 <[EMAIL]>
Every frame the render thread waited about 5 ms for the GPU after the frame's first submission, with the game
CPU-bound in every area. The wait is there so that the GPU has run that submission's passes before the game
rewrites what they read. But the submission also ends with the write-backs of what its shaders wrote, 60 MiB merged
into guest memory in 3 to 4 ms of GPU time, and the wait covered those too.
The write-backs now go into a part of their own. The part before them signals a second timeline, the executed
timeline, and the first-submission wait waits on that; the write-back part signals the timeline as before, so every
fence the game asks for still covers them, in the same queue order. wb_split = 0 keeps one part and the old wait.
Not measured in the game yet.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Performance: draws reuse the bindings that did not change and record only the dynamic states that differ from what
the command buffer holds; small uploads come from a staging arena; finished submissions are held briefly and handed to
the queue in batches; the recorder thread can keep a CPU core of its own; ASTC block-linear textures are deswizzled on
the GPU; dynamic resolution steps the upscaler mode to hold a frame-time target. Each has a setting in nx2navi.ini
(bind_reuse, dyn_delta, stg_arena, submit_batch, recorder_core, recorder_prio, astc_gpu_deswizzle, dyn_res) and the
staging cache now frees what it holds at shutdown.
Capture: NX2NAVI_DUMP=key saves the frame after each press of Insert, with mesh draws listed by task, mesh and fragment
shader, depth test and face culling, and mesh_prims.txt giving each mesh draw's primitive, clipping and fragment counts.
All of it is off unless NX2NAVI_DUMP is set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The event order (vk_exec.c) holds a submission at a wait whose signal has not been replayed. In Kokiri Forest dialogue several presents a second came with the frame's whole submission held (4 held submissions, nothing drawn since the last present): the present showed the picture from before, a flash of the whole picture, and the frame was drawn afterwards in the present thread's empty submission, next to the following frame. With the event order off (NX2NAVI_EVENT_ORDER=0) the same spot had no such frame in 3600.
present_held (vk_backend.c, synchronous replay only): at a present with held work, what can go on runs first in a submission of its own; if work is still held, the present waits and goes out after the submission that replays the last of it, at the next present, or after 8 submissions. present_held = 0 (NX2NAVI_PRESENT_HELD=0) turns it off.
held_sync (vk_exec.c vk_exec_record): a submission without commands of its own that resumes held work now gets the VRAM mirror sync before and the write-back after, and a uniform copy, like any submission. Before, that work read mirrors that had missed the CPU's latest writes, and its GPU writes were dropped from the write-back list by the next submission's vk_mem_mirror_begin. held_sync = 0 (NX2NAVI_HELD_SYNC=0) turns it off.
Not yet measured in play.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The foliage's sun shadows took most of the GPU time: one mesh draw shaded about 1,000 million fragments in the far cascade. The far cascade now shades them at a 2x2 rate, and the two far cascades drop one side of the double-sided cards. At the forest entrance the frame rate went from 18 to about 43 fps on an RX 7900 XT.
FSR 3.1, its frame generation and the console mode now take effect without a restart. The full performance overlay lists the settings shown in the options menu.
NX2NAVI_GPU_PASSES times each pass and counts its fragments, and NX2NAVI_SET_AT changes settings at a given frame for test runs. Both are off by default.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reflex's markers sat at the host present with frame numbers unlike the frame's tags, slReflexSleep was never called, and the low-latency mode was set before any swapchain existed, so it never applied. GfxBackend.frame_start, called from nvnWindowAcquireTexture, now sets the simulation and submit-start markers after Reflex's sleep (skipped once nothing was presented for 200 ms); submit end is set when the frame's submissions went out; the mode is set again after each swapchain (NX2NAVI_REFLEX: 1 on, 0 off, 2 boost); frames are numbered by the game's presents; and the fps counter counts what was shown. NX2SL_VERSION is 3, so nx2navi_dlssg.dll must be rebuilt. From a tester's fixes: 42-45 fps game, 84-90 on screen (was 24).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments say what the code does and why, docs/maxwell.md keeps the Maxwell facts, and fetch_refs.sh clones only the checkout the build reads. Code is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The texture pool descriptors are now a ring of sets, one per submission, brought up to date from a change log, and the first submission of each frame waits for its GPU work before the game goes on: the GPU ran late and part of a frame read data of the next one, and distant characters drew NaN that bloom spread over the frame. The window acquire also waits for the image's previous frame. Function locals start at zero unless DLAA is on, which removes the per-warp light tiles on the Great Fairy Fountain floor and Link's dark, speckled look. Texture slots follow only live entries of their own target, so the volumetric fog gets its 3D volume. The pool refresh walks only the slots that changed.
The translator implements PRMT (patch 0021), which it only logged before; the button prompts render again. HFMA2.RELU no longer depends on it, HMMA.F16 rounds once and HMMA.F32 is lowered, checked against GPU vectors with a new Maxwell warp emulator.
The frame dump gains a re-arming white-frame trigger, a trigger on a depth target's first pass, NaN pixels in magenta, image handles and view targets, and depth copies of sampled depth textures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>