16 Commits

Author SHA1 Message Date
chrg6
0472734008 Time the hand-off to the present thread, the present call and other threads' acquire waits, and count the acquire's GPU wait once.
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>
2026-11-13 18:41:39 -05:00
chrg6
26d26babe5 Bring public main's bug capture on F8 and its removal of the Steam co-op into the 0.10.0 line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	docs/architecture.md
#	launcher/steamlib.py
#	launcher/ui/app.py
#	runtime/boot.c
#	runtime/cfg.c
#	runtime/flight.c
#	runtime/flight.h
#	runtime/gfx_present.c
#	tests/launcher_test.py
2026-11-13 09:52:43 -05:00
chrg6
d9429b0c00 Break the memory report down past VMA, by owner in system memory, and by growth over a session.
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>
2026-11-13 06:46:31 -05:00
epicmeme546
4fd70e398f Make F8 write a bug capture and add a test run that starts from one.
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>
2026-11-13 06:22:18 -05:00
chrg6
c5cb67645e Write the flight dump and the memory map through a temporary file and a rename.
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>
2026-11-12 22:34:22 -05:00
chrg6
202fd47ceb Let the flight recorder dump on its own when one frame flashes.
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>
2026-11-12 15:16:30 -05:00
chrg6
e3cf0710c5 Record a draw's own texture binding that got the stand-in, and a sampled pool slot that is not registered.
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>
2026-11-12 13:08:16 -05:00
chrg6
f45273bfe5 Say in the flight recorder's blank-slot events what the slot resolves to, and whether any shader reads the pool as a volume.
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>
2026-11-12 11:28:22 -05:00
chrg6
0cc247b513 Record the flight recorder's thumbnails at 640x360.
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>
2026-11-12 10:43:10 -05:00
chrg6
fe468f7ec6 Make a slot's new texture at once when the slot is showing another one.
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>
2026-11-12 10:19:19 -05:00
chrg6
95fb707bcb Record a pool slot losing its picture in the flight recorder.
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>
2026-11-12 10:12:23 -05:00
chrg6
bbaf732b81 Record what an upload read, slot registrations and sampled stand-ins in the flight recorder.
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>
2026-11-12 10:04:25 -05:00
chrg6
0c343e04a4 Record the game's time step at every present in the flight recorder: the patch page's step, the time manager's and the game's present count.
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>
2026-11-12 02:54:32 -05:00
chrg6
7f5906c121 Keep the texture events of the recorded frames in the flight recorder and write them with each dump as events.tsv, before the thumbnails.
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>
2026-11-12 02:42:22 -05:00
chrg6
4467f73ead The flight recorder can dump every n-th frame (NX2NAVI_FLIGHT_EVERY), so the dumps of a scripted run hold all of its frames.
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>
2026-11-10 10:00:18 -05:00
Valiantsin Dzerakh
0478c13e45 Import the base tree (MIT, see LICENSE) 2026-11-07 05:04:38 -05:00