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>
Every report adds a line for video memory outside VMA (the driver's usage and budget per heap against VMA's blocks, and counts of pipelines by kind and by last use, stage libraries, compute pipelines, layouts and descriptor pools, with the swapchain, DLSS and FSR sizes), a line that splits the process's commit by owner (the game's heap and maps, device-local memory, VMA's host blocks, the C runtime heap with the shader caches, pipeline tables, texture cache, log queue and flight recorder in it, the driver's pipeline cache data and the rest), and a line naming what grew by more than 64 MiB since the report before and since play began. Every ten minutes of play and at exit a ranking line orders the categories by growth and marks those that only climbed; tests/mem_growth_check.py checks that bookkeeping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
F8 now always writes a folder under logs/captures: the area, Link's position and rotation, the camera of the frame on screen and the game's own, the presented frame as a PNG, frame times around the press, the machine, the settings that differ from the defaults, the end of the log, the save, and the game's flags and texts. A note on screen names the capture. The flight recorder's F8 dump goes into the capture's folder.
NX2NAVI_CAPTURE_REPLAY starts a run on a capture's settings, teleports to its area, walks Link to where he stood and puts the free camera where the camera was; tools/capture_run.py prepares the profile from the capture's save, runs it and compares the two captures. docs/bug-capture.md has the files and the steps.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The window's close ends the process under the detached threads, so a dump file the flight recorder's thread was
writing stayed cut short under its own name. Its three files are now written as .part and renamed when complete
(runtime/part_file.h, with a check that kills a writer in the middle), and the allocator's map goes through
plat_file_write_cached. The shader identities and the other cache files already went through an atomic write.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With flight = 1 and its thumbnails kept, the recorder now also writes a dump when a frame's picture differs strongly from the frame before and the frame after while those two agree, which is what a one-frame flicker looks like. Each thumbnail is reduced to 16x9 cells of mean brightness where the present thread already sees it complete, and a frame is a flash when at least three cells are more than 24 of 255 brighter or darker than in both neighbours, the neighbours being alike in those cells, around them and over the picture as a whole. A camera cut, a fade, a pan and shapes crossing the picture are no flash, and a frame beside one without a thumbnail is not judged. The dump is the one F8 writes, named "flash" with the frame's number in its header line, at most one every 30 seconds. flight_flash = 0 turns it off; flight_flash_diff and flight_flash_cells set the two thresholds.
On the thumbnails of earlier dumps the defaults flag the frames whose scene was white once and nothing in the runs without that fault. tests/flight_flash_check.py checks the test itself on drawn pictures.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
In towns, kerb, stair and wall-top edges are drawn untextured for one frame when the game registers a round of streamed textures, and no pool entry of those textures goes to the stand-in in such a frame. A binding written into a draw's own set, and a slot that is unregistered when its marks are read, left no event until now.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Ten slots a frame are noted as having no picture when their submission ends: the 128x72 and 64x36 volumes the game registers under new slot numbers in every frame. The events now tell a slot whose entry cannot be viewed in any kind a shader reads from one whose lookup found nothing.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
At 320x180 a frame of a 2560x1440 picture kept one texel in eight each way, and a flicker of a small object or of fine shading while the camera stands still did not show in a dump. The ring of 600 thumbnails takes 553 MB of host-visible memory instead of 138 MB while the recorder is on.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Ocarina streams its textures by detail level: while the player moves it registers about 32 pool slots with their next level every four or five frames. A pool refresh made 16 of them and wrote the stand-in into the set for the rest until its next round; the set is read as it is when its submission goes to the queue, so an object whose slot was among the rest could be drawn grey for a frame. A player's recording shows 1,525 such slots in 600 frames, each with its texture made later in the same frame. A slot whose entry in the set is a texture's view when it is looked up now has its texture made by that lookup, beside the count budget and the frame's creation time, 64 of them a present; pool_shown_first = 0 leaves them to the budget. The flight recorder also notes a slot that still had no picture when its submission ended.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
When the pool's entry of a slot goes from a texture's view to the stand-in, events.tsv gets a line with the slot and what its own texture was at that moment: finalized, released, superseded, overwritten, uploaded, its storage readable, held out. The set is written with update-after-bind, so every draw of the submission that samples the slot reads the stand-in from then on; the check at the marks' read-back comes a frame later and finds the slot resolved again.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Three more texture events for a dump's events.tsv: after each upload a hash of 64 probes of the storage and how many of them were all zero, so a second upload a frame later tells new bytes from the same ones and a first upload of unfilled storage shows; a slot registered with a CPU-written texture, with what the slot named before; and a slot a shader sampled that has no image of its own and follows none, so its stand-in was on screen. They are for telling why a texture shows wrong for one frame while the game streams textures in.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
frames.tsv gains step_frames, game_step_frames and guest_presents. The game writes its step at its own present, a frame or two before the replay presents that frame, so the present count is there to line the two up.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each texture made (with how its content started), uploaded, uploaded again, released, taken back or destroyed is one line with its frame, address, format, size, layers and flags, so a one-frame flicker in the thumbnails can be matched to the textures of that frame. events.tsv is written first: the thumbnails take about five seconds, and a game closed meanwhile kept no frames.tsv.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With n under the ring's length each dump overlaps the one before, and a run's frame records can be read back whole
except for its last n frames. NX2NAVI_FLIGHT_AT takes sixteen frame numbers at most, which covered a walk at 60 fps
and lost most of one at 240.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>