The user asked for it: the mode no longer gains frame rate, and the mesh shader fallback covers the cards it was written for. Its switch in the settings menu's Game tab is gone with the rows under it, and so are their 57 text ids and strings.
low_end_gpu itself does nothing any more. It is still a known key, so an older nx2navi.ini gives no unknown-key warning, and while it is on the log says once that the mode was removed in 0.10.2. The preset that gave nine keys other defaults is gone, and so is the rule that read the mode's keys only while it was on: each of them (mesh_emulation, tex_clobber, foliage, vram_heap_limit, vram_reserve_mb, upload_batch_mb, mirror_stage, mirror_hot, submit_finish_gpu and the others) is an ordinary setting with the default of its row or of its reader, so a line a player wrote by hand keeps counting and a file with only the switch runs the game as it runs for everyone else. guest_opt in nx2navi.ini counts for the next rebuild without the switch too (tools/make_game.py).
The recorder thread read the switch itself: on an AMD card without mesh shaders the mode turned it on. It is off on every AMD card now unless vk_recorder = 1 is set.
nx2navi.ini.example lists the former mode's settings as single settings, the 0.10.2 notes have a Removed entry and name the foliage fix of the commit before this one, and the comments that said what the mode sets say so in the past tense. tests/cfg_parse_check has the new reading of the switch and of the keys without it; the test matrix is at 90 ok with the same two not ok as before (installer_test, worktree_test), and a clean build compiles 169 runtime files and links. Not run in the game.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Each decision moves into a header the runtime and a test both read, as vk_mesh_path.h does: vk_tex_detail.h (tex_detail_auto), vk_dlssg_device.h (dlssg_card_skipped, dlssg_device_create) and mesh_fb_hold.h (the hold's table). The runtime's behaviour is the same as before the move.
tests/tex_detail_check has the budgets from players' logs of 0.10.1 (a handheld's integrated GPU at 10,361 MiB now gets reduced, a 12 GiB card at 11,313 MiB full). tests/dlssg_device_check plays the laptop of the report with stand-ins for the interposer and the loader: the chosen Intel card is skipped, the NVIDIA card is made through the interposer, a card of another vendor is made by the loader without the interposer being asked, and a refusal falls through to the loader. tests/mesh_fb_hold_check holds a list for two presents after its colour draw was skipped, through a wait of 600 frames, and no longer once it is drawn or gone. Built against the rule 0.10.1 had, the first two and mesh_path_check fail (6, 13 and 3 checks).
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The low-end GPU mode sets mesh_emulation = 1, and on a card without mesh shaders the emulation then draws the game's task and mesh programs in place of the mesh fallback. On NVIDIA's cards without mesh shaders that ended the game at the first mesh draws of an area with an access violation inside the driver, at nvoglv64.dll+0x10cdffe on a GTX 1050 Ti and on a GTX 1070 (both 0.10.1), and a Titan X (Pascal) report names the same mode. The GTX 1050 Ti's owner set mesh_emulation = 0 and played on through the fallback with the rest of the mode on.
The decision (vk_mesh_path.h) takes the card's vendor: an NVIDIA card is drawn for as if the emulation were not asked for, and the boot line says so. The replay and the pipeline code read the decided path from the device context where each read the setting itself. tests/mesh_path_check.c has the new cases.
Not run in the game: no GTX 10 card here. mesh_path_check passes and every file that includes the changed headers compiles.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Four players of 0.10.1 could not start the game again after changing settings: "no usable Vulkan device", exit code 70, until nx2navi.ini was edited or removed by hand. One log shows the case: a laptop with an RTX 3050 and Intel Iris Xe graphics, framegen = dlss and gpu_index = 1, the Intel one. With frame generation on, the device is made through Streamline's interposer, which asks it for VK_NV_low_latency2 and failed with VK_ERROR_EXTENSION_NOT_PRESENT, and nothing else was tried.
A chosen card of another vendor now counts as not usable while the interposer is in use, so the best usable card is taken, as for a chosen card that is gone. Where the card in use is still not an NVIDIA one, or the interposer fails to make the device for another reason, the loader makes it and frame generation is off for the run: the log says why and the settings menu shows it as unavailable, as it does when Streamline's files are missing.
Not run: no PC here has two cards of different vendors. The changed files compile.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The review of the log tail change found that main.c log_sink opened the 4 MiB reserve past the cap on g_log_final as
well as on g_log_ending. bugcap.c write_log_tail calls rt_log_sync in play, which sets g_log_final, so after a bug
capture in a trace = perf session past the cap, ordinary play lines filled the reserve (about 6 minutes at 12 KB/s),
the end's lines were dropped again, and the "end of the run follows" line came mid-run.
The reserve is now keyed on g_log_ending alone, as E-10's design had it. The ends that relied on rt_log_sync now call
rt_log_ending first: dispatch.c fatal_end and main.c crash_handler right after their guard, vk_device.c
lost_wait_exit's watchdog before its sync, and plat_exit through the new main.c log_sync_ending (plat_exit_hook in a
game run), so every path that ends through plat_exit marks the end too. rt_exit_begin and perf_summary_flush call it
as before.
Not reproduced in the game before the change. tests/log_cap_check.py gains a capture case (rt_log_sync in play, 1000
more play lines, then the end): on the code before this change 58 play lines took the reserve, the end line came at
the sync and no end line reached the file; now no play line passes the cap and the end's lines fill the reserve. A
fatal case runs rt_log_ending and then rt_log_sync, and static checks hold crash_handler, log_sync_ending, fatal_end
and lost_wait_exit to calling rt_log_ending before rt_log_sync. log_cap_check, fatal_exit_check (with a stand-in for
rt_log_ending) and the compile checks of main.c, dispatch.c and vk_device.c pass, with no new -Wall warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dispatch.c fatal_end and vk_device.c lost_wait_exit ended with _Exit, which in the C runtime is ExitProcess: it
stops the other threads wherever they are and then runs every library's DLL_PROCESS_DETACH on the ending thread
(the audit's probes show the exe's TLS callback getting it after _Exit and not after TerminateProcess). That is
the detach plat_exit already avoids, after NGX's faulted once at the end of a clean run. After a FATAL such a
fault would enter the crash handler and replace code 70 or 72 with a crash report and code 71; after a device
loss a driver detach that hangs would hold the process for good, since ExitProcess had stopped the watchdog too.
The new platform plat_terminate ends the process with the code and nothing else (no exit hook, no stream flush:
the failing paths have written their lines and flushed). fatal_end calls it after its existing log sync and
flushes. lost_wait_exit tells its watchdog (spawned with a non-NULL argument) from the threads that stop there:
after LOST_EXIT_MS the watchdog alone writes "vk: the end of the run did not finish in 10 s; ending it" through
rt_log_sync's bounded trylocks, and the others end 2 s later only should the watchdog not have started. Exit
codes stay 70 and 72.
Not reproduced before the change (no detach fault on a fatal path has been seen). tests/fatal_exit_check.py
(new: dispatch.c and the platform layer with a TLS callback; rt_fatal ends with 70, its FATAL line and no detach
call, which it showed with _Exit; and statically, no _Exit in runtime outside the platform layer, and both ends
call plat_terminate) and the compile checks pass.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The review of b4e12f1c found that vk_device.c timed_alloc calls perf_mark_end after a failed vkAllocateMemory too, with
0 bytes, so the "device memory allocations" mark's count now takes in failed attempts, which vk_headroom.c on_allocate's
old mark (VMA's callback after an allocation was made) never did. The time of a failed attempt held the frame as well,
and VMA may try again with a smaller block after it, so the mark keeps it; the comment now says that failed attempts
count with their time and 0 bytes and that the count can pass the number of allocations made.
Comment only. vk_device.c compiles with the dev flags and adds no -Wall -Wextra warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An area's first visit (0.10.0 fresh session, nx2navi.1.log, frame 10113) took 421 ms, and the stutter line left 192 ms
of it as replay and 27.5 ms unattributed after the pipeline, translation and texture waits. The same frame made 78
device memory allocations of 4442.7 MiB, but the "device memory allocations" mark carried only a count and bytes:
vk_headroom.c on_allocate added it with zero time, since VMA's allocate callback runs after the allocation. At the
8-19 ms per 256 MiB block that vk_headroom.c measured, those allocations could be most of the frame's unexplained
time.
vk_device.c create_device now hands VMA a VmaVulkanFunctions whose only member is vkAllocateMemory = timed_alloc;
VMA takes the others from its static table (vk_vma.cpp VMA_STATIC_VULKAN_FUNCTIONS 1; its ImportVulkanFunctions
copies only the non-null members). timed_alloc brackets vkAllocateMemory with perf_mark_begin and perf_mark_end, which
cost one branch while the trace is off. on_allocate's zero-time mark is gone, and perf.c mark_text prints the
allocations' time, count, bytes and longest like the other kinds. The texture statistics' count of large allocations
is left as it is (adding the time there would have counted every allocation a second time).
Diagnostics only, no change to play. Not reproduced before the change (it needs an area load with trace=perf).
stutter_map_check, stutter_hitch_check, frame_step_check and mem_growth_check pass; every runtime file compiles with
the dev flags after the host.h comment change, and vk_device.c, vk_headroom.c and perf.c add no -Wall -Wextra
warnings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every RX 6000 loss whose pass marks named a pass was in one that starts with a native mesh draw, on every version from 0.6.0 to 0.9.2 and with every other setting tried each way; RDNA 1 cards on the same driver draw through the fallback and RDNA 3 and 4 on AMD's other driver draw natively, and neither has such a report. That the native mesh path is the cause is not shown. The decision moves into vk_mesh_path.h with a test of each device kind and setting; has_mesh_shader keeps saying what the hardware has, so the recorder thread and the low-end card test do not change. On those cards no native mesh pipeline is built any more: the seed list and the boot prewarm skip them. The early tail stays off with the fallback, a combination never measured. The fatal line names the mesh path, and NX2NAVI_VK_NOOPT's default applies with mesh_fallback = 0 alone.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pass mark now counts its mesh draws and those that take their count from the GPU; the gap before a mark notes a group recorded by the workers, whose passes have no marks, where the report said "barriers and state only"; the fatal line names the mesh path (native, the mesh fallback, the emulation). The comment on the RX 6700 XT report in vk_exec.c called the last marked dispatch the replay's own: it is the game's (op_dispatch, cs d7566eb1).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The RX 6000 reports of 0.9.1 and 0.9.2 ran the completion callbacks of submissions the GPU never finished before any call returned VK_ERROR_DEVICE_LOST, although the completion thread polled the device first. Every value a queue hand-over or a host signal gives the submissions' timeline or the executed timeline is now noted before the signal (vk_sync_hw.h); the completion thread and the wait after a submit end the run when a timeline reads above that note. The first occlusion query found unwritten after its fence logs its submission, the submissions' values, both timelines' readings and the GPU breadcrumbs, and the lost device's report starts with the same line, so the next report shows whether the driver gave a value above the note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A thread samples every two seconds the video memory budget against this process's use (vk_mem_vram) and the system's
commit headroom and free physical memory (plat_commit_info, plat_mem_info), and turns each side into a level: roomy,
normal, tight or critical, with five points of hysteresis upward and a 256 MiB floor. Changes are logged, and the
memory report's system line ends with both levels. MEM_BUDGET_TEST_VRAM_MB and MEM_BUDGET_TEST_RAM_MB pretend a side
is that small, so that what gives memory back can be tried on a large machine; the texture cache's rules are unchanged.
tests/mem_budget_check.py checks the levels and the hysteresis.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On a laptop with the vendor's processor graphics and a card of the same vendor, both installed drivers list the
card, and pick_device took the first listing of the best device type. That listing can come from the older driver
installed for the processor graphics, which was reported to crash in its shader compiler with the card. Among
listings of equal rank with the same vendor and device ID, the one with the higher driver version is taken now, and
the log says so. A GPU chosen by index or name is unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With the setting off the device is made exactly as before the mode existed, so the mode cannot change anything for a player who never turns it on. Turning it on in the menu then takes a restart, and the row says so; in a run that started with it on, changes still apply at once.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
NX2NAVI_LOW_LATENCY (0 off, 1 on, 2 on with boost; live) is for low latency without frame generation: NVIDIA's mode
through VK_NV_low_latency2 and AMD's through VK_AMD_anti_lag, each enabled on the device only where the driver lists
it, NVIDIA's with VK_KHR_present_id, which it needs, and AMD's with its feature. A device made by the DLSS Frame
Generation proxies gets neither: that path keeps its own low latency. vk_lowlat.c holds the calls, the frame numbering
and the statistics line; nothing calls into it yet beyond the start and the end of the device.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
On Linux every CPU-visible pool had a host-visible shadow copy, because amdgpu treats a host-pointer import as a userptr object that it looks up again at every submission and invalidates whenever the write watch or a freeze re-protects its pages. Now the watched reservations are shared mappings of one memfd, /dev/udmabuf makes dma-bufs of page ranges of it, and the device imports them (VK_KHR_external_memory_fd, VK_EXT_external_memory_dma_buf). A dma-buf pins its pages once and is not tied to the process's page tables, so protection changes leave the GPU's mapping alone, and nothing is copied.
RADV puts every imported memory object into every submission, so pools share their imports: guest addresses are cut into 64 MiB spans (NX2NAVI_DMABUF_MB), and each pool is a buffer bound into its span's memory at its offset. A new span is cut back at the spans already there, so spans overlap only where a pool itself lies across an older span's edge, since amdgpu counts every span's pages in its GTT use. A decommit punches the pages out of the memfd, or zeroes them in place where a dma-buf pins them, and the pages a freed span pinned that nothing committed go back to the system.
It is on by default where /dev/udmabuf opens, udmabuf's size_limit_mb is 1024 or more, the device has both extensions and NVIDIA's kernel module is not loaded. A start-up probe imports one page as the pools will be; any refusal keeps the shadow copies, and the log says why. NX2NAVI_DMABUF=0 keeps the copies. tests/platform_dmabuf_test.c checks the shared mapping without a GPU, and docs/building.md ("Memory") describes the imports and what they need.
RX 6800 XT (RADV, Mesa 26.2.6), one build, interleaved pairs with NX2NAVI_DMABUF=1 and 0: Kokiri Forest 60.7 against 40.3 fps (3 pairs, +51%, standard error 0.4), 1% low 50.9 against 34.0, frames over 25 ms 5 against 768; Hyrule Field 55.7 against 42.4 fps (2 pairs, +31%, standard error 0.4). No pool was left without a span, no span was refused, and --probe-vkmem passes with and without the imports.
On a card with mesh shaders and 8 GiB of video memory or more the Game tab's switch cannot turn the mode on; it stays usable while the mode is on, to turn it off again. A player with every setting at its highest had it on on a GeForce RTX 5090, where the mode moved the sun shadow map and the scene depth to memory of their own and the ground's shadow became a dark patch with straight edges that turned with the camera.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The settings had a tab of their own; they are rows of the Game tab now, listed under the switch once the mode is on after a restart, and the tab is gone. With the mode off every one of them reads as unset, whatever nx2navi.ini or the environment says, so nothing of the mode acts unless a player turned it on; the build reads guest_opt the same way. The notes describe that, and take the white edges in towns and the dropped textures from the known issues to the fixes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Mesh shaders as compute, the texture that gets memory of its own, staged mirror uploads and the shorter wait for the GPU, widened write-watch queries, the video memory limits and reserve, early upload batches, the asynchronous replay, the hot-pool rule and the recording thread on AMD cards without mesh shaders were measured on a Radeon RX 580 and run on no other card. Each keeps what 0.9.1 did unless the key or its own setting says otherwise; the key gives the defaults the way the device profile does. The notes describe them as that option, hold main's two new entries in one Changed section, and list the white edges in towns as a known issue.
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>
Mesa's Vulkan drivers keep timeline semaphores as kernel sync objects, and their queues submit on the calling thread unless a submission waits on a value not submitted yet. The replay hands a part of the frame to the queue each time it splits one off (vk_exec.c maybe_split), so every such vkQueueSubmit2, with the command stream's checks and the kernel's ioctl, ran inside the replay, the thread that limits the frame rate on an RX 6800 XT under RADV.
MESA_VK_ENABLE_SUBMIT_THREAD=1 sends every submission to a thread of the queue's own. vk_device.c mesa_submit_thread sets it before any thread can load the driver, unless the environment sets it already. submit_thread in nx2navi.ini (NX2NAVI_SUBMIT_THREAD) is 1 by default on Linux and 0 on Windows; 0 leaves the driver's own choice. On a Mesa driver the log says which one applies.
Kokiri Forest, 0.9.1, RX 6800 XT, Mesa 26.2.6, 3 pairs of 45 s runs of one build with the setting on and off: the replay took 13.9 ms a frame against 15.0, lower in every pair; frames over 25 ms fell from 910 to 406; the frame rate went from 40.0 to 40.8 fps, within the runs' noise.
AMD's Windows driver gives the graphics family a single queue. vk_device.c then made no frame generation queues, and
vk_fg_create_swapchain returned without a log line: AMD FSR 3.1 frame generation set in the menu did nothing on an
RX 580. The present queue (which nothing else may submit to) and the image acquire queue now come from a family that
can present, compute families first, with their families passed to AMD's swapchain; the log names them, or says that
there are none, and the menu offers FSR 3.1 frame generation only where they exist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Released textures kept, top texture levels left out, hot pools in system memory, the heap limit, the upload batch
limit, the kept ASTC buffer, mesh emulation and its arena and task records (greyed where the GPU has mesh shaders),
the overwritten-texture fix, the vector index fix, -O2 game code, pass times and the frame dump key. Each key gets a
row in the settings table; make_game.py reads guest_opt from nx2navi.ini when the environment does not set it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An RX 580 (8 GiB, AMD 23.10) shows its memory as a 6 GiB device-local heap and a 2 GiB device-local, host-visible
one, and Windows granted the game 5.1 GiB of the first. A save load put 5.6 GiB of textures, VRAM mirrors and pools
into the first heap while the second held 69 MiB, and the next submit failed with VK_ERROR_OUT_OF_DEVICE_MEMORY;
budget flags on the texture allocations alone did not change that. VMA now gets heap limits (pHeapSizeLimit) when
the device has two or more device-local heaps: each heap's budget at device creation less a tenth (at least 256 MiB)
for memory VMA does not see. Every allocation past a limit goes to the next memory type that allows it.
NX2NAVI_VRAM_HEAP_LIMIT: auto, 0 (off) or MiB for the largest heap. A physical pool that finds no video memory left
takes any memory type it may live in instead of failing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
NX2NAVI_VRAM_BUDGET_MB changes only the number the decisions compare with. The ballast takes that much device-local
memory right after the allocator is made, in dedicated buffers of 256 MiB, and holds it until the allocator goes, so the
driver's own behaviour at its limit (slow allocations, memory moved to system RAM, refusals) shows as on an 8 GB card.
Checked on an RTX 5090: 24000 of 24000 MiB taken and held; through eight reloads of Kakariko the use went past the card
(32,218 MiB at the end) and Kakariko fell from 118-151 fps in the first four rounds to 10-24 fps in the last four. Texture
detail was still chosen as full: the choice reads the driver's budget, which the ballast does not lower.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux with RADV (and the Steam Deck) the game points MESA_SHADER_CACHE_DIR at mesa-cache beside the exe, a folder
nothing else writes, but left Mesa's limit of 1 GB. The stage libraries the library warm builds in play are kept only
in the driver's cache, and a preparation of every shader variant is larger than that limit, so what did not fit was
compiled again in play. The limit is now 8 GB in that folder unless MESA_SHADER_CACHE_MAX_SIZE is set, as NVIDIA's
cache beside the game already has.
Checked: all runtime files compile. Not run on Linux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Linux every driver got shadow storage (copies of guest memory beside it), which exists for amdgpu, where importing guest memory costs the write protection of its pages. NVIDIA's own driver imports without that cost, so there the copies are only the slower path: in Kokiri Forest on 0.7.1 (RTX 4090) imports ran at 51.9 frames a second and the copies at 27.4. Until now only the AppImage's play.sh knew, by setting NX2NAVI_SHADOW=0 when NVIDIA's kernel driver was present at build time, so a game started any other way got the copies, and a PC with that kernel driver whose game ran on another card got imports where it should not. The game now chooses by the driver of the device it runs on, play.sh no longer sets the variable, and NX2NAVI_SHADOW=1 or 0 still decides. The driver is asked when the device is picked, before the storage is chosen.
Windows is unchanged: the device probe here reads "mem: mode mirror" with no shadow storage line, and the shadow line with NX2NAVI_SHADOW=1. The Linux path itself was not run: there is no Linux machine here.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The author of the pull request it was ported from closed that request on 2026-11-11 with the words that they give no
permission to use its code. The history (NX2NAVI_VA_REPORT, runtime/vk_vaddr.c, the stack walk it used and its notes
in the docs and the example settings) goes; the lost device's report keeps the breadcrumbs, the pass marks, the video
memory and the driver's fault information.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
auto turns the fallback on for a device without VK_EXT_mesh_shader, 1 for every device (testing), 0 never.
mesh_fallback_mb sizes the buffers the passes write. Nothing draws through it yet.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A lost device used to end the process from whichever submit or wait noticed it first, while the present only logged it
and went on and the other threads kept calling the driver. The completion thread could take the submissions' timeline
as reached and run the callbacks of submissions that never finished.
vk_device_lost_check now takes the result of every submit, present, acquire and wait. The first thread to see the loss
sets a flag, writes the report and ends the process as a fatal error does (the log written, a save commit under way
finished), with an exit code of its own; later threads stop in that call, and a watchdog ends the process after ten
seconds should the first thread hang. The replay, its submit, the uploads, the present and the lazy fetch read the flag
and return without touching the device. The completion thread asks the device with a call of its own before a new
value counts and runs no callback once the flag is set.
The executable's --probe-devlost and tests/device_lost_check.py check this without a lost device, and
device_lost_at in nx2navi.ini ends a real run the same way.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
When Streamline loaded but DLSS Frame Generation was not available, the menu's row read "in use: off, needs an RTX 40 GPU or newer", also on an RTX 5080: the same check fails when the plugin did not load or a function it needs is missing. The row now points to the dlssg lines of the log, where the bridge says whether the plugin loaded and which of its functions it found, and then asks Streamline for its support result on the selected Vulkan device and logs it by name. The query is an optional export, so a bridge DLL of an earlier build keeps working.
The runtime and the bridge (tools/build_dlssg.py, MSVC, Streamline's headers) both build here. The failing case itself was not reproduced: on this RTX 5090 frame generation is available.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The default was off only on AMD's driver branch for RDNA 1 and 2, on the grounds that builds with the default on had been measured on an RX 7900 XT. Those measurements were standing in Hyrule Field and walking through a village; the report this is about is of a game that ended in fights, on an AMD card the player did not name, and fights are where the lazy path is busiest. Nobody has played one on an AMD card under our eyes on either branch, so the exception is now AMD's own driver as a whole (wb_lazy_choice.h, by VkDriverId), until a card of each branch has played fights with wb_lazy = 1 and a log to show. The cost is the open-area frame rate of 0.7.0 on every AMD card under Windows; the RX 7900 XT figures in the draft notes come from builds that had it on by default, which the notes now say. wb_lazy = 1 still turns it on.
--probe-vkdevice prints the line a fatal error or a crash adds to the log, so its wording can be read without a failing run. On an RTX 5090: "fatal: gpu NVIDIA GeForce RTX 5090, driver NVIDIA "621.01" version 621.1.0.0; memory mode mirror, lazy write-back on (the default on Windows (wb_lazy = 0 turns it off)); texture detail not chosen yet (no texture made)", and with the reporting player's nx2navi.ini beside the exe "lazy write-back off (wb_lazy = 0 is set)", after a start whose log names async_compute, vram_safe_mode and exception_handler as no settings of this version.
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>
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>
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>
The first part of this pull request (the cache beside the exe with an 8 GiB limit) was already in. This
is the rest: nothing is set without NVIDIA's kernel driver, neither variable is set when the folder cannot
be made or written (the driver caches nothing in such a folder), the log says which folder and limit hold,
and a game run sets the drivers' cache variables in main before any of its threads can load a Vulkan
driver (vk_device.c vk_driver_cache_env); the other modes still set them in create_instance. On Windows
that only moves the MESA_SHADER_CACHE_DIR setenv earlier.
Pull request #369 by mooki000, taken as it is. The Linux part is not compiled or run here.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
NVIDIA's Linux driver compiles into a disk cache in ~/.cache/nvidia, shared with every program, and a process adds at most
1 GiB to it (__GL_SHADER_DISK_CACHE_SIZE); a start that finds more than that in several files deletes the later ones.
nx2navi.exe --prepare-shaders builds the 42,142 variants of game 1.1.1 in one process, about 1.5 GB: on the default limit it
stored about 30,700 of them, and the first start after the preparation compiled the rest (and the pipelines built from
them) again.
Unless the environment sets them, the game now points __GL_SHADER_DISK_CACHE_PATH at nvidia-cache beside the exe (made
here, the driver only makes GLCache inside it) and __GL_SHADER_DISK_CACHE_SIZE at 8 GiB before its first Vulkan call, as it
does for Mesa's cache. The docs name both.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
In the settings menu a click anywhere on a row stepped its setting to the next value, and the left arrow stood left of the value's text, so it moved whenever a value of another width came up: clicking the left arrow through a list landed on the row at the second click and stepped the other way. Each stepper now has its arrows in white frames at fixed places, the left one the width of the row's widest value from the right one, and the mouse changes the value on those frames only; a click on the rest of the row selects it. On and off rows have the same arrows. An arrow at the end of its values is drawn faint and does nothing, and a button held on an arrow repeats as a held key does. A, Enter and left and right work as before.
NX2NAVI_MENU_TEST_CLICK clicks rows by their setting's name during a menu test run. On an RX 7900 XT at 3232x1352 the arrows stepped the performance overlay one value a click and stopped at both ends, and clicks on the body of four rows changed nothing.
HDR between off and the tone mapper now follows the menu. The HDR path is set up at every start (vk_hdr_armed): the swapchain colour space and metadata extensions, the tonemap splice and the settings in the support buffer, with the spliced tonemap returning the game's own SDR result while the swapchain is SDR. A change of the setting makes the swapchain again in the other format. On the same card a run that started with HDR off went to an HDR10 swapchain and back, and one that started with HDR on went to SDR and back, with the picture right in each state. The game's native HDR still waits for a restart, as the game asks about the display once, and the row says so. NX2NAVI_HDR_LIVE = 0 leaves the HDR path out of a run that starts with HDR off.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Main brought the fixes of eleven pull requests (the texture pool set sealed when an early replay part is submitted, every location reflected in interface arrays, RGB9E5 texels recognised for integer storage views, the upscaler a frame started with kept, GPU pass timestamps without vendor breadcrumbs, the Linux build, window and NGX fixes), render targets released when drawn over at another address, the vertex layout kept in the pipeline on newer AMD drivers, and the upscaler choice read once per evaluation.
Four hunks conflicted and take both sides: pool_sync returns for a sealed set before the per-frame creation budget's clock (vk_exec_draw.c); make_module keeps the test line's early_ok argument and main's module hash (vk_pipeline.c); a texture taken back counts as sampled again and records how long it was away, and the texture line logs the disuse counters and the overwritten render targets (vk_texture.c).
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
VMA's blocks on large heaps are 64 MiB instead of 256 (vma_block_mb): one session held 2.1 GiB free inside the
device blocks beside 8.2 GiB of allocations after a few area changes, and the driver counts a block whole until it
empties. The headroom thread keeps its spare block only for the device-local types textures and mirrors grow in; the
spare blocks it made for the host types and the PCIe BAR type at boot stayed empty for the whole run. The ASTC
decoder's scratch is 32 MiB, so it stays inside a block, and an area load that makes one per upload batch holds half
as much at once. A DLSS feature no frame used for 3600 evaluations is freed with its work images, and a model change
now frees all five sizes.
The perf log gets a "vram by use" line (textures, released textures, mirrors, physical pools, ASTC scratch, headroom
spare, free space inside the blocks, memory outside VMA and DLSS's own share), the free ranges per memory type, the
addresses whose duplicate texture entries hold the most, and a line when memory outside VMA moves by 256 MiB.
astc_bc_probe in nx2navi.ini turns on the ASTC opacity probe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 99cfc2fa14ae208b7b23e057ff9b87986cd6a5e0)
NVIDIA 621.01 reuses a pipeline-cache.bin entry only in the executable that wrote it, so every renamed or updated exe
compiled all of its pipelines again (23 s boots, 17 % read from a 470 MB file). With NX2NAVI_PIPE_BINARIES=1, where the
driver has VK_KHR_pipeline_binary, every pipeline, stage library and compute pipeline is looked up by its driver
pipeline key in pipeline-binaries.bin and made from the stored binaries; anything not stored is compiled once with its
data captured and appended. Each binary is stored once under its own key: nearly every graphics pipeline carries the
same 2 MiB binary, which made a first version of the file 7 GB. pipeline-cache.bin is not used while this is on. The
knob is off by default while it is being tested; NX2NAVI_PIPE_BINARIES_MAX_MB caps the file.
NX2NAVI_ONESHOT_WAIT_MS sets how long a draw into a target the previous frame did not render waits for a pipeline built
for the first time before it is skipped; the default keeps the old 30 s.
Co-Authored-By: Claude Opus 5.5 <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>
Guest code runs ballots and shuffles where the guest compiler had the warp reconverged, for example right after a loop
each lane leaves at a different iteration. Vulkan only promises that with VK_KHR_shader_maximal_reconvergence, so
the pipeline code adds the extension and the MaximallyReconvergesKHR mode to every module that declares subgroup
capabilities. vk_reconverge = 0 in nx2navi.ini turns it off.
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>
- A GPU index or name that is not usable, or no longer there, falls back to the best usable GPU with a log line instead
of stopping the game before the menu can change it (vk_device.c pick_device).
- A held Esc no longer opens the menu and closes it again on its first key repeat (menu_win.cpp, menu_sdl.cpp).
- The menu does not open on the boot screen, where it was not drawn while its keys queued up and changed settings once
the game ran.
- The DLSS render scale follows the upscaler that runs: a DLSS or FSR 3.1 mode picked in the menu, which applies after
a restart, or DLSS where NGX is not available, no longer shrinks the scene with nothing to scale it back up.
- Upscaling without DLSS or FSR 3.1 also turns frame generation off, which needs their motion vectors.
- The HDR peak brightness slider reaches the setting's 10000 nits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crashes: the render-target heap (HeapDef DynamicTextureAllocatorMem and its parent) grows with the widescreen
picture, so the pause menu no longer reads a null block at 5120x2160; texture uploads keep the game's memory mapped
while they copy it, so UnmapPhysicalMemory no longer pulls it away mid-copy; DLSS Frame Generation no longer crashes
in Streamline's set-up (physical devices and the window surface go through its proxies, its swapchain images are
counted first, resources are tagged per frame); a stale core-wait target is cleared before its thread is freed.
Freezes and races: a resize during a swapchain rebuild is kept, a failed frame generation swapchain is retried
instead of replaced by a plain one, three acquire timeouts in a row rebuild the swapchain, device waits take the
queue lock, a translator child that stops answering times out, an in-flight compute pipeline builds at normal
priority, a save Commit started before the window closes finishes and none starts after, a settings change just
before closing is saved, and a movie sample taken before a flush is dropped.
HDR: Enhanced HDR extends the game's own tonemap so highlights go above the picture's white (the hue slider,
which no longer applies, is gone from the menu); frame generation gets a HUD-less picture, the HDR flag and the HDR10
metadata under HDR too.
Memory: ASTC textures are encoded to BC3 unless the PC has 20 GiB of VRAM and 48 GiB of RAM, as Windows charges video
memory to the commit charge (a long session held 17 GiB of RGBA8 textures and 45.7 GiB of commit on a 32 GiB PC).
Shaders: the translator lowers spill-mode programs with more than eight uniform register groups in a wider spill
layout, and local loads and stores with a uniform-register address (36029 of the game's 36031 variants translate,
20 failed before).
The launcher release ships the shader tools and AMD's FidelityFX library (make_launcher.py --ffx); windres there gets
the toolchain on PATH.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frame generation now gets the picture without the HUD each frame: the upscaler's output before the HUD is drawn over
it, encoded to sRGB as the game's last pass encodes its picture and put through the same colour, scaling and borders
as the presented frame, so it tells the HUD apart and leaves it unwarped in the generated frames
(NX2NAVI_FG_HUDLESS_SHOW=1 shows that picture, a test). The performance overlay gains an FG row and the fps line the
display's rate, generated frames included. The device enables VK_KHR_get_memory_requirements2 whenever AMD's frame
generation is on, which crashed at start with DLSS upscaling.
DLSS Frame Generation through NVIDIA Streamline (vk_dlssg.c, and nx2navi_dlssg.dll from runtime/dlssg, built with
MSVC by tools/build_dlssg.py against the SDK kept outside the repository) creates the instance and device through
Streamline's proxies and calls its swapchain hooks; Streamline still fails in its plugin start-up here, so the menu
lists it only while it is set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>