A player wrote that the settings file is overwritten on updates and that a line has to be added again each time. Nothing an update of the launcher's install does writes the file: the updater works in the launcher's own folders, and the build only reads it (the bind_ lines for the button prompts, shader_carry for the shaders step). The tests now hold that with the player's file as reported: settings the menu wrote, three lines that are no settings of the game, a comment, CRLF line ends and no line end after the last line. It comes through the launcher's part of an update (staging, the switch, the cleanup; BackgroundUpdateTest), through the rebuild under a newer kit, a rebuild with nothing to do and a build from the game files again (MakeGameTest, with the check, the ExeFS, RomFS, shaders and prompts steps real and the lift and compile as stand-ins), and through the launcher's own settings switch, which adds its one line (PlayerIniTest).
One writer did write the whole file again: installer/ini.py, the older installer's last step at every install and update. It kept every line, but turned CRLF into LF, added a line end after the last line and dropped a byte order mark, and a file saved as UTF-16 stopped the step with an error. It now changes the lang line only and keeps every other byte; a UTF-16 file is read as that and written as UTF-8, as the game's menu writes it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A player on an AMD card reported crashes in fights and that a line added to nx2navi.ini cured them. That line, async_compute = 1, is no setting of the game, and neither are two more in the same file; the one line of the file that changes how memory reaches the GPU is wb_lazy = 0. A second report, an RX 6600 on 0.7.1, lost its device 3.5 minutes into play with the lazy write-back on. Neither proves it the cause: the player changed several lines at once and sent no log, the RX 6600's last pass mark is inside a draw pass and not a fetch, cards with 8 GB lost their device the same way on NVIDIA before the lazy write-back existed, and reading the fault handler, the fetch, the arming and the buffers' lifetimes found no failure path. What depends on the driver and cannot be checked without the card: pages of guest memory are inaccessible while a host-pointer import of them is bound and written by the GPU, and Vulkan calls run inside the access-fault handler.
The default is now decided once the driver is known (wb_lazy_choice.h, vk_memory.c vk_mem_lazy_decide): on under Windows as before, except on AMD's driver branch for RDNA 1 and 2, which none of us has run it on; AMD's driver for RDNA 3 and newer keeps it on, since builds with that default were measured on an RX 7900 XT. What the player sets wins everywhere, and one line at start says the choice and its reason. On those cards open areas run at the 0.7.0 frame rate again (on an RTX 5090 that was 65 fps against 90 in Hyrule Field; not measured on AMD). tests/wb_lazy_check.c checks the choice by platform, driver and setting without a GPU.
For reports of this kind the log now holds what was missing. At start: every setting nx2navi.ini sets, and in one line the keys that are no settings of this version and do nothing; the settings menu shows those in its bottom row. At a fatal error or a crash: one line with the card, its driver, the memory mode, the lazy write-back with its reason, armed ranges, fetches so far and a fetch in progress, and the texture detail (dispatch.c rt_fatal_note_add, vk_device.c fatal_note), since a report is often the end of a log alone.
--probe-cfg loads the player's file: the three lines are named, and the menu's save keeps them and every other line it does not change byte for byte. Nothing here ran on an AMD card, and no fatal error was provoked to read the new line.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
While retired it got no use stamp, so it counted as idle the moment it came back, and a sweep over the video memory budget could evict it as idle in the same call that took it back: on the cards that are over budget, which are the ones the take-back is for.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A mark on a pool slot without an entry of its own went to the slot's own key, so a texture sampled only through a second descriptor at its address counted as unsampled, was retired after two minutes under memory pressure, and a mark on its stand-in could not take it back. Where the pool's syncs kept leaving slots behind, a retired texture was never destroyed either and its slots kept the stand-in: on a card with 6 GB the ground of the field was drawn without its textures and its height. The mark now goes to the textures the slot follows, and a retired texture that is not due after five seconds is taken back.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Each submission's first sync of the bindless texture pool walked every registered slot, 33,000 in Kokiri Forest, to find the few textures the CPU wrote and the few slots that changed. Three parts of that walk change, each with its own switch (pool_fast, pool_presnap, pool_group; on by default, 0 and ab accepted):
- The freshness check goes through a list of the entries the slots resolved to, with a check by 2 MiB region first; slots that resolved to nothing are looked up again when the game's memory map changes and every eighth submission. The use stamps the idle eviction reads are left from a count of the slots that hold each entry.
- The check takes the write-watch snapshots of its regions first, so unchanged entries pass the region test.
- The GPU-written slots of one address bucket with one descriptor form a group, which the dirty walk leaves alone while its first member is unchanged.
Measured on an RTX 5090 at 2560x1440, standing in Kokiri Forest, in-run A/B over five runs with all three switched (three of them with the barrier change switched too): 93.6 to 100.7, 96.4 to 103.1, 95.8 to 102.6, 99.3 to 107.1 and 91.9 to 99.9 frames a second, the replay thread 0.7 to 0.8 ms a frame shorter; a control's blocks differ by 0.4 %. On a walk with NX2NAVI_POOL_GROUP_CHECK=1 no group that was left alone had missed a write, and screenshots across the A and B blocks show the same picture.
From pull request #390. Not tried: the texture eviction under memory pressure (a 9 GiB budget was never reached on the walk), AMD and Intel drivers, Linux. One of the nine runs with it on ended by itself with exit code 0 and nothing in its log two seconds after the window lost the focus; three repeats of that run did not.
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>
The new row, Add to Steam (Windows), puts a non-Steam shortcut named "The Legend of Zelda Ocarina of Time" into the shortcuts.vdf of every Steam account that signed in on the PC (launcher/steam.py): the binary key-value file is read and written back as it was, with ours added or updated in place, and the file from before the first change kept beside it. Steam reads that file only at its start and writes it back when it closes, so a running Steam is asked to close (-shutdown) and started again after.
The pictures come from the player's game when the row is selected (launcher/gameart.py), as the launcher's fonts and sounds do: Navi from the HUD's icon and its glow, the logo with the Master Sword and the Hylian Shield from the title screen's texture archive (decoded with runtime/tex_astc.c, built into a library with the toolchain's clang), and a frame 12 seconds into the first title movie, the field under the full moon, decoded from its AV1 with the toolchain's dav1d.exe, as the background, the cover and the wide tile. They go into config/grid named by the shortcut's id, CRC-32 of Exe and AppName with the top bit set, which matches the id Steam gave an existing shortcut on the test PC.
Steam starts the launcher's pythonw.exe on data/steam_play.pyw, which runs the launcher of app/current.txt with the new --play: the game starts from its folder with its console hidden, as Play starts it, and the process waits for it and for the copies its own Restart starts, so Steam counts the game as running while it is. ocarina.exe is a console program, so starting it directly would open a console window beside the game, and NX2navi.exe ends six seconds after it started the launcher. Started that way on an RX 7900 XT PC, the game ran, the Python process stayed until the game was closed and ended with code 0. Tested against copies of that PC's Steam files; the real Steam library is not changed until the player selects the row.
Co-Authored-By: Claude Opus 5.5 <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>
A contributor traced the one-frame dark shadowing at the castle town gate to depth targets the game remakes at other sizes about a thousand times a minute: the new image started cleared, so everything near the camera read as in shadow for a frame. With depth_alias = 1 a depth-only image of the same format copies the live one's depth for the region both cover. Their three runs each way showed 6, 4 and 0 dark frames without it and none with it; the cost of the copies and other scenes are not measured yet, so it is off by default.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The window that shows a pick-up above the character counts its number up to the new total by a whole step a frame and closes once they are equal. The game rounds that step down, and below one 60 Hz frame per update it was 0: above 60 fps the window stayed until the area was left, and for larger amounts the counting sound was started again every frame. The step is now floored at 1, switchable with pickup_count.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The reports of black and white blocks on Navi, on Link next to her and on bright surfaces seen from inside a house come from RX 5000 and 6000 cards, whose Windows driver compiles with "AMD proprietary shader compiler" (vk_caps.amd_sc); an RX 7900 XT (LLPC) draws the same scenes clean. The game's shaders test for NaN with OpIsNan in 48,717 of an install's 54,959 translated modules, 26 tests each on average, and the translator's guard that keeps NaN out of every float colour output, so that one bad texel does not spread through blur and bloom into blocks, is such a test too. SignedZeroInfNanPreserve asks the driver to keep those tests; whether that compiler does is not known.
With that driver, vk_pipeline.c spirv_nan_bits now turns every 32-bit isnan, scalar or vector, into (bits & 0x7fffffff) > 0x7f800000 on the number's bits, and a float select with a constant whose bits are a NaN into a select of the bits (issue #156), when the module is created. NX2NAVI_VK_NAN_BITS=1 does it on every driver, 0 on none. Run over every module of an install's shader cache, the rewrite changed 1,281,675 tests and 57 selects with every output well formed (each id defined once, below the bound, the uint types declared once); --selftest checks it on a small module, and Link's house with it forced on (RX 7900 XT) draws as without it. Not yet tried on an RX 5000 or 6000 card.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a lost device the driver's fault report gives an address and nothing says what was there. With va_report = 1 (NX2NAVI_VA_REPORT, off by default) the game keeps every address range the driver binds to a buffer or image and every range it unbinds (VK_EXT_device_address_binding_report, runtime/vk_vaddr.c). The report of a lost device then names the ranges that covered each faulting page, or the nearest ones when none did, and vaddr-history.txt beside the log holds the whole history, also when the driver gives no fault report. With 2 the call stack behind each range of 1 MiB or more is kept too. A player sees nothing of this unless the setting is on: without it no extension is enabled and no callback runs.
From pull request #349. Its second part, which stops a frame dump from turning on mesh query statistics, is left out: it changes what a dump writes for everyone who uses one. Built for Windows; no device was lost to try it.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
On Linux each Restart from the menu cost the next game one more processor core on machines with 12 or more logical processors. A process started with posix_spawn begins with the mask of the thread that spawned it, and since the recording thread has a core of its own no thread has every processor: the restarted game began without that core and took the next fast one for its own recording thread. plat_restart (runtime/platform/linux.c) now widens the calling thread's mask to all of the process's processors for the time of the spawn and puts it back. A mask set from outside is kept, helper programs started with plat_spawn are unchanged, and Windows is not touched.
From pull request #435, with its test tests/platform_restart_mask_test.c, which needs Linux. Not compiled or run here: this machine has no Linux compiler.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With the mouse camera in stick mode and a camera speed above 1, the mouse could never turn faster than a full stick does at camera speed 1, however high the mouse sensitivity was set. The mouse's counts were limited to full tilt first and divided by the camera speed afterwards, so at camera speed 1.5 the most the mouse could send was two thirds of full tilt, which the game's own factor brought back to the old top speed. The division now comes before the limit (runtime/input_map.c input_mouse_stick): a slow movement turns as far as before, and a fast flick reaches full tilt, which is the game's top speed with the camera speed in it. Direct mouse mode, the default, does not use this code. A camera speed of 1 or less behaves as before.
From pull request #424. tests/mouse_stick_check.py is new here: it builds input_map.c alone and checks the deflection at each camera speed without the game.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A player who keeps the game on a mapped network drive could not finish a build. Three things change in tools/make_game.py:
- The output folder keeps its drive letter on Windows. It was resolved to the share's UNC name, which is a second connection to the same server, and some servers drop that one under the compilers' load while the mapped drive stays up.
- When a source's size cannot be read because its folder stopped answering, the build says so and asks again for up to a minute before it stops with a message that names the drive. It used to end in a traceback. A file that is gone from a folder that answers still ends the build at once.
- A finished file is put in place through a temporary file beside it instead of a file in a temporary folder. Such a folder is made for its owner alone, and some shares refuse that while they take ordinary files; the build then reported a permissions fault in the output folder after everything had compiled.
From pull request #419: the first and third change are the contributor's, the wait is written again so that a file that is really gone does not wait a minute, and the launcher tests cover all three.
Co-Authored-By: Matthias <dev@sleepyyui.com>
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The AppImage tested for zbic with PYTHONPATH and the user's site-packages in view, then ran the build with Python's isolated mode, which sees neither. A player with zbic installed for their own Python of the same minor version passed the test, never got it installed where the build looks, and was told to run the pip command that had put it in the place the build ignores. The test now uses the same isolation and the same search path as the build, so zbic is installed into the AppImage's own folder whenever the build could not import it.
From pull request #436.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The developer build stopped at undefined dav1d symbols since the AV1 movie decoder (runtime/ov_movie2.c) came in: tools/make_game.py linked dav1d, tools/build_game.py did not. Windows links libdav1d.a statically, as libvpx and zstd are, so the exe imports no libdav1d DLL; Linux links -ldav1d. The llvm-mingw link list and the Linux release's static list get dav1d too, and docs/building.md lists the package for MSYS2 and Arch.
From pull request #439, with the two other link lists added.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A player's AppImage build of 0.7.1 stopped in its compile step after eight minutes of RomFS work: four
files did not compile, because the kit builds with the system's compiler and headers and theirs were
older than the runtime needs. vk_pipeline.c and vk_device.c name VK_KHR_pipeline_binary,
VK_KHR_dynamic_rendering_local_read and VK_KHR_shader_maximal_reconvergence, which their Vulkan headers
did not declare, and ui/menu.cpp takes its font in with #embed, which clang has from version 19.
- Vulkan headers: tools/vulkan_pin.py holds the release the other builds use (1.4.363, the archive and
hash tools/make_release_linux.py already pinned, which now reads them from there). The AppImage carries
its C headers in toolchain/vulkan-headers and builds against them whatever the system has; a kit or a
source checkout uses the system's when they are at least that release and fetches the pinned ones
otherwise (build_game.py vulkan_include). Their folder goes first on the include path of the runtime's
sources only, so the lifted code's objects keep their keys, and a runtime object compiled before
against the system's headers is taken over instead of compiled again. AppRun no longer asks for the
system's Vulkan headers.
- #embed: tools/build_shaders.py also writes the font's bytes as runtime/ui/roboto_ttf.h (ignored by git,
carried by a kit), and menu.cpp includes that where the compiler has no __has_embed. With #embed
nothing changes.
- Before its long steps a Linux build that will compile now checks every runtime source that needs no
header of the install with the compile step's own command (clang -fsyntax-only) and links an empty
program against the game's libraries (make_game.py preflight): an old compiler, an old header or a
missing development package stops it in the first seconds with the compiler's own errors. A failed
compile names every file that failed, not only the three whose errors it prints.
Checked on Windows: tests/launcher_test.py passes with four new tests (117); the header of bytes and
#embed give the same 162,588 bytes, and both branches of menu.cpp compile; the early check passes on the
Windows file set with clang 23 and stops on files made to fail; the 34 local headers of release 363 are
the upstream tag's files by git blob id. Not run: anything on Linux, the pinned archive's download, a
compiler older than 23.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The automatic levels assume the ASTC textures end as BC3. Decoded on the CPU or with the BC3 pass off they stay RGBA8 and take four times the memory, which is what one mip level left out gives back. A card with 8 GB that decoded on the CPU went 560 MiB over its budget and lost its device.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The result of that read was not checked. After a device loss the value was taken as it came, the callbacks of submissions that never ran were called as if their work were done, and the game went on recording until the next checked queue call failed. That was an upload's, so two players' reports read as an upload fault.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A font file registered with Windows stays open, so writing it failed with Permission denied. When the cached title font was registered but its family could not be used, every start tried to decode it again, failed on the locked file and kept the system font. The registration is now given up before the fonts are decoded again, and an unchanged file is not rewritten.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A crash inside a recorder's Vulkan layer read like a fault of the game: the report now says that the module is not part of the game and what to do. A lost device now logs the video memory in use against the driver's budget, so a report from a card with little memory shows whether the memory ran out.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Found in the first run of the previous two commits (a first visit of Kokiri Forest from a state that had only seen Hyrule Field):
- The count of frames a target was rendered in started again whenever the guest's texture object changed, and the game initializes its render target objects anew every frame at the same storage: the log showed `seen 2129/2128 in 1 frames`, so no target ever became steady and the rule did nothing. The owner of an image is now the storage address the texture cache keys it by. An image handed on to a texture elsewhere still starts at none.
- A one-shot mesh draw waited its 8 ms in every frame that met the same pipeline while it compiled for 150 to 1100 ms: 17 waits, 170 ms, every draw skipped at the end, up to four in one frame. A mesh pipeline that has been building for longer than that wait is a compile, and the draws after the first do not wait for it (vk_pipeline_building_ms).
- The last wait of a full-screen draw with no picture to hold was 40 ms a draw, and two such draws made 86 ms in one frame. It is 40 ms a frame now.
- A one-shot vertex draw skipped at the end of its wait gets its log line however short the wait was (seven were skipped with no line, so their targets could not be named).
That run against the same cell of the earlier comparison (same exe path, cache state, route and settings; one run each): the load's worst wait of the draws in one frame 99 ms against 735 ms, no mesh wait over 13 ms against 524 and 206 ms. Two long frames remain in both runs (628 and 862 ms here, 660 and 854 ms there): two compute pipelines built at their first dispatch, about 395 and 397 ms.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A session's log gave the counts of slow pipelines and slow frames but not what they were. Now:
- Each pipeline a draw missed that took 50 ms or longer to become usable gets a line (the first 48 of a run): mesh or vertex, fast-linked or built whole, whether pipeline-keys.bin named it, its shaders' input ids and where their translations came from (the shader cache, a previous translator's, or translated in this session). The summary line counts the same: how many were mesh pipelines, how many the key file named and the prebuild had not reached, how many had a shader translated in this session.
- Each shader library that took 25 ms or longer gets a line (the first 32): its shader, the translation's origin, and whether outputs were added for its fragment shader, which is a module the preparation never built.
- The count of draw lookups that "found shader libraries missing" says what it counts: a library not made in this process yet, not one the driver's cache lacks.
- After the boot prewarm one line says how many of the recorded pipelines' shaders are a previous translator's translations, beside the pipeline cache hits that show whether any pipeline was compiled again.
- The frame-time summary's "over 25 ms by cause" took the replay's whole time as one part, so every frame that waited for a pipeline, a translation or a texture upload inside the replay counted as "replay". The replay's part is now its time without those waits.
gfx_shader_origin gives a translation's origin.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A draw whose pipeline is still building is skipped when its pass is rendered every frame and waits otherwise, so that a lookup table or a volume generated once is not left empty. "Every frame" meant "in the last four frames", so after a loading screen or a full-screen menu every pass of the first frame read as new: each draw with a new pipeline waited on the replay thread, up to 30 s each, and a mesh draw, whose pipeline has no fast link, for its whole compile. A player's 0.7.1 log shows it at three area changes: frames of 954, 1015 and 684 ms with 853, 960 and 663 ms of pipeline wait, six mesh draws waiting 52 to 660 ms in passes whose targets had been rendered in thousands of frames.
The target history (now runtime/vk_target_frames.h) also counts the frames each target was rendered in, for as long as it is the same guest texture. From eight frames on its pass is per-frame however long ago the last one was, and its draws are skipped and built by the workers like any other (pipe_skip_steady, on by default). A target rendered for the first time still waits, now from one pool of 50 ms a presented frame that the full-screen draws share (pipe_frame_wait_ms; oneshot_budget = 0 gives the old wait back), a mesh draw 8 ms at most, and the wait for a one-shot draw's shader comes from the same pool (gfx_shader_await takes a limit). A full-screen draw with no picture left to hold waits 40 ms more instead of 500. One frame so waits under 100 ms for pipelines in all.
Every 600 frames with a miss the log says how many draws found a shader or pipeline missing, how long they held the replay thread in all and in the worst frame, by the kind of wait, and how many were skipped at its end.
tests/target_frames_check.py replays the area change of that log against the header. Not yet run in the game.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The limit counted the frames on screen, so the game was paced at the limit over frame generation's multiplier: 60 at 4x drew 15 frames a second, and each step up of the multiplier took drawn frames away. Now a limit of 60 at 4x is 60 drawn and 240 shown. Only the display choice still counts the screen's frames, since a frame past the monitor's refresh is never seen, and with no limit set the default is 60 drawn rather than the monitor's rate.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A slot whose storage is unmapped has nothing to make, but with the creation budget at 0 the lookup noted it as left before asking whether its storage can be read. In a busy frame, once the frame's creation time was spent, about a thousand stale slots were noted at once, and the pool refresh took that for an area load: up to 274 times a minute in one session's log, each time lifting the count budget and the frame's creation time for the rest of the frame.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
What came in since 0.7.0: the lazy write-back on by default with the flicker's cause fixed, the pipeline read-ahead
and linking changes, the texture detail setting, the mirror share for 6 GiB cards, five pull requests and the log
queue's order. The notes say which numbers are single measurements and that the small-card changes were measured with
the budget capped on a larger card.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Eight commits: a draw that misses its pipeline has the rest of its submission read ahead, the library workers link
the pipelines of skipped draws, a pipeline whose fragment shader reads a target of its own pass is fast-linked like
any other, and diagnostics. The merge was clean. Their before and after matrix had not finished when this was merged.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The user asked for this once the flicker fix was measured. The single-frame flicker that kept it off in 0.7.0 came from frames drawn before the game's signal that their data was written, which the previous commit fixes; the lazy write-back only made the frames come faster, so the signal fell into the replay more often. wb_lazy = 0 in nx2navi.ini still turns it off, and an ini that already holds the key keeps its value. Elsewhere than Windows the default stays 0, as its fetch inside the access-fault handler has never run there.
Measured on the build of the previous commit, 1,800 presented frames a run, standing in Hyrule Field by day with DLSS Performance, frame generation 4x and HDR. With the lazy write-back on and eight busy threads beside the game: 38 and 0 single-frame spikes with the fix switched off (408 and 44 frames whose signal came during the replay), 0 and 0 with it on (667 and 724 such frames). With it off and the same load: 0 spikes in four runs, two with the fix and two without, 86 to 140 such frames each. Without load, one run each: 88.5 frames a second with the fix against 93.3 without it with the lazy write-back on (GPU 7.2 against 6.8 ms a frame, 6.53 against 6.48 submissions a frame), 71.0 against 65.6 with it off, so the fix's cost is inside the run-to-run spread; every run ended cleanly without a fatal error. This build was started once with no setting anywhere: the lazy write-back was on, and the Screen tab shows no row for it.
Not measured: play by hand with the lazy write-back on and the fix (the user's sessions so far ran with it off), other areas than Hyrule Field, and more than one run for the frame rates.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A thread that found the queue's lock taken for 4,000 spins wrote its line itself, ahead of the lines it had queued
earlier. On a busy machine the lock's holder loses the processor inside the copy and the lock stays taken for a time
slice, so one thread's lines reached the log out of order. A line now waits for the lock, and a line that finds no
room, or is larger than the queue, is written after everything queued before it.
tests/logq_check.py failed on this in 8 of 30 runs beside four busy processes and passes 100 of 100 now. It gained a
check that holds the queue's locks itself and fails on the old code every time, and it no longer depends on time: it
waits for the queue to empty, keeps the line about slow writes back except in the step that checks it, and counts the
lines that waited instead of timing the slowest.
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>
A large compute shader is built at its first dispatch, on the replay thread (470 to 720 ms each on a first visit to
Kokiri Forest), so whether its program exists long before that dispatch says whether the build could start earlier.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Shaders are looked up and pipelines built when a draw first needs them. The time between nvnProgramSetShaders and a
program's first draw is the head start a build begun at the program's creation would have; the line gives it as
counts under 17 ms, 100 ms, 1 s, 5 s, 30 s and over, mesh programs apart (vk_exec_draw.c prog_lead,
nvn_api_obj.c nvn_prog_made_ns). Off unless asked for.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
It was a constant beside the per-frame budget (pipe_link_ms). With the stage libraries in the driver's cache a fast
link takes 4 to 17 ms for a third of the pipelines a draw misses, so both values decide how many such draws are left
out of a frame; the setting lets that be measured.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The next draw with a new pipeline was only found when the replay reached it, so the draws of an area's first frame,
or of a turn that brings several new materials into view, paid for their pipelines one after another while the
workers that could have built them together sat idle. The submission is recorded before it is replayed: the first
draw of a submission that finds a shader or pipeline missing now has the ops after it read (vk_exec.c vk_exec_ahead)
with the binds that decide a draw's shaders and shader libraries applied to a copy of the replay state. Each draw
there looks its shaders up as the real draw will, which also queues a shader that is not in memory, and names its
state to vk_pipeline_hint, which queues the pre-rasterization and fragment shader libraries that are missing. No
pipeline is entered and no key recorded, so a wrong guess costs two libraries at most. The draw's own lookup then
links on its thread when both libraries are there.
On a first visit to Kokiri Forest a scan read 6,900 ops in 0.78 ms on average (the longest 5.5 ms; a scan ends
after 3 ms), and the scans of the load and the walk named 377 states without a pipeline and queued 104 libraries
for them.
pipe_ahead = 0 (NX2NAVI_PIPE_AHEAD) turns it off.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A draw that is skipped while its pipeline builds leaves the pipeline to the pool: a job is fast-linked first and goes
back in the queue for its optimised build. The pool keeps one worker out of optimised builds; with the other three
inside one (100 to 350 ms each) that worker linked every new pipeline of a burst one after another. The library
workers, which so far served only draws that wait, now take the fast link of a draw-queue job too (vk_pipeline.c
fast_take, lib_worker), and there are as many of them as pool workers (four on sixteen logical processors, two
before).
Measured on a first visit to Kokiri Forest (RTX 5090, libraries in the driver's cache): of the 444 pipelines draws
missed, 180 took over 50 ms to become usable, the longest 1.3 s, and their draws were missing from every frame until
then. With this change and the two beside it (feedback loops, the look-ahead) 23 did, 19 of them mesh pipelines,
which have no fast link.
pipe_fast_workers = 0 (NX2NAVI_PIPE_FAST_WORKERS) gives the old behaviour.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Such a pipeline (soft particles and water reading the depth buffer, full-screen passes that read the target they
write) had no fast link: gpl_link refused it, so its draws were skipped during, or waited for, the whole optimised
build. The flags belong to the fragment output state alone, so the fragment output library carries them and its key
names them (FoKey.fbl); the shader libraries are the ones the same shaders use without a loop.
Measured on a first visit to Kokiri Forest (RTX 5090, stage libraries in the driver's cache, DLSS Frame Generation
on, where no frame is held back and a full-screen draw waits on): full-screen draws of such pipelines waited 267, 42
and 61 ms in the area's first frame and 410 ms in the middle of the walk; fast-linked they wait 2 to 3 ms.
gpl_feedback = 0 in nx2navi.ini (NX2NAVI_GPL_FEEDBACK) gives the old behaviour.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
- Every pipeline a draw missed is timed from the miss to its first usable pipeline, the fast link or, without one,
the build itself (vk_pipeline.c ready_note): the gpl lines of a save give the counts under 4, 8, 17, 50 and 150 ms
and over, the average, the longest, and how many had no fast link. That time is how long the pipeline's draws were
missing from the picture or waited.
- The stage library lines add the libraries of 25 ms or more and their sum, and the pre-rasterization libraries that
had outputs added for their fragment shaders: those are another module than the one the preparation built, so the
driver's cache has none of them.
- "a draw waited" lines name a pipeline with a feedback loop.
- The "draws skipped for a pipeline" counter (overlay, perf lines, flight records) was raised by every lookup that
found the pipeline building, also when the draw then waited for its link and was drawn. It is raised where the draw
is skipped now (vk_exec_draw.c draw_skipped).
Co-Authored-By: Claude Fable 5.2 <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>
One conflict, in the build kit's notes, which keep both the shader cache item and the Texture detail text.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The probe takes the level from NX2NAVI_TEXTURE_DETAIL. It covers a 1024x1024 texture, a 256x256 array of three layers, and a 128x128 and an unmipped texture that stay whole, with the images as RGBA8 or BC3, and the sampler LOD clamp of a reduced texture. It passes at full, reduced and low on an RTX 5090, with NX2NAVI_ASTC_BC 0 and 1.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The row offers Automatic, Full, Reduced and Low, names the level Automatic picks on this card, and notes that a change takes effect after a restart.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The textures of a loaded area are what fills a 6 GiB card: with the budget taken as 4,730 MiB a walk through Castle Town held 6,907 MiB of device memory, 4,302 MiB of it textures, 201 pools were refused their mirrors and the game ran at 20 fps. With texture_detail = reduced a mipmapped 2D or 2D-array ASTC texture of 256x256 and more that the game's CPU fills is made without its largest level, with low without its two largest when it is 1024x1024 or more. The same walk then held 5,472 MiB (textures 2,357) and 4,987 MiB (textures 1,997), and the ASTC texels decoded fell from 2,589 M to 826 M and 503 M. auto, the default, picks full from 8,192 MiB of device-local budget, low under 6,144 MiB and reduced between. full changes nothing.
The image holds the game's levels from the one it starts at (VkTexImage.skip), and the upload with its ASTC regions and BC3 encoding, WriteTexels, the write-back, the views and the sampler LOD clamp of combined bindings count from there. Render targets, storage images, 3D and cube textures, small and unmipped textures and textures the device samples as ASTC itself are left whole, as is a texture made over a live one that holds GPU-written content. A shader that fetches texels by position or uses a texture's size gets the texture whole: gfx_shader.c marks such bindings from the SPIR-V (the layer count of an array, which the translator's layer clamp asks for, does not count), and the replay makes a reduced texture bound there again with every level.
Not run on a card with 6 GiB: the figures come from an RTX 5090 with NX2NAVI_VRAM_BUDGET_MB=4730.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>