Commit Graph

447 Commits

Author SHA1 Message Date
epicmeme546
3fe6965df9 Give the Custom colour scheme a Panel Color for the panels and the title band, and have Button Color change the buttons alone, their orange choices included.
Button Color recoloured the list panel, the description box and the title band, and left the orange choices in the rows as they were, so the buttons themselves seemed not to change. It now takes the rows' label cells, their value cells and the orange choices, which come out a lighter shade of the picked colour; the new Panel Color takes the panels, the dividers and the title band. Text on a light button or panel turns dark, and so does the text in choices that come out bright. The colour is kept as custom_panel in launcher.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 03:11:09 -05:00
epicmeme546
4cc4663008 Show Background Color and Button Color in Customize Launcher as soon as Color Scheme is set to Custom, and hide them again when another scheme is chosen.
Changing the scheme redrew the window but kept the rows the screen was opened with, so the two rows came and went only when the screen was opened again, as after choosing a picture. The rows are now made again each time the scheme changes, with the cursor staying on Color Scheme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 03:03:11 -05:00
epicmeme546
43760d4129 Add a Custom colour scheme with a colour square and a hex code, start the player's background picture below the title band, and shorten the Add to Steam description.
Custom is the last scheme in Customize Launcher. Its Background Color and Button Color rows open a picker in the description box's place: a square of the hue's colours (saturation across, brightness up and down) with a hue bar under it to click or drag on, Hue, Saturation and Brightness rows for the arrows and the gamepad, and a Hex Code row to type a code into. Each picked colour takes the place of one colour of the first scheme, the backdrop's middle tint or the buttons' wood, and the rest of its group follows it, lighter and darker as in the first scheme; light buttons get dark text. The colours are kept as custom_background and custom_buttons in launcher.json.

The background picture used to fill the whole window, so its top was hidden under the title band. It now fills the window from where the band ends, cut to that shape around its middle and scaled evenly. A copy an older launcher kept for the whole window is cut, never squeezed, and made again from the picture file while it is there.

Add to Steam's description and result are now one plain sentence each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 02:50:15 -05:00
epicmeme546
a7b8810d37 Add the game to Steam from Customize Launcher, with Navi as its icon and the title screen's moonlit field and logo as its pictures, made from the player's own game.
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>
2026-11-10 13:49:06 -05:00
epicmeme546
da0d0be700 Read occlusion query results in batches, and wait in the submit call for the game's signal that the frame's data is written.
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>
2026-11-10 13:37:12 -05:00
epicmeme546
e38f42a959 Test for NaN on the bits of the number on AMD's driver for RX 5000 and 6000 cards, so that no shader compiler can leave a test out.
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>
2026-11-10 13:08:03 -05:00
chrg6
005d30a29c Give the Kokiri Forest measurement in the 0.7.1 notes as it was recorded: worst frame 434 to 89 ms, frames over 50 ms 5 to 3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.7.1
2026-11-10 11:23:45 -05:00
chrg6
4bac444ad7 Set the version to 0.7.1 and write its release notes.
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>
2026-11-10 11:14:51 -05:00
chrg6
0e16e58cf3 Merge the shader stutter fixes into the combined test line.
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>
2026-11-10 11:05:17 -05:00
chrg6
381264165a Turn the lazy write-back on by default on Windows and take its row out of the settings menu.
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>
2026-11-10 10:52:11 -05:00
chrg6
c1951d2b37 Keep a thread's log lines in order when the log queue's lock is taken.
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>
2026-11-10 10:26:51 -05:00
chrg6
07721c0058 Test an event wait against the value the event had when the submission took its copies, so a frame is no longer drawn from data the game had not finished writing.
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>
2026-11-10 10:26:51 -05:00
chrg6
22cff8f1f9 The program lead diagnostic covers compute programs too: the time between a compute program's creation and its first dispatch.
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>
2026-11-10 10:11:32 -05:00
chrg6
ca843507e9 A diagnostic says how long before its first draw the game made each program (NX2NAVI_PROG_LEAD=1).
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>
2026-11-10 10:03:54 -05:00
chrg6
a38a902702 The longest wait of one draw for its fast link is a setting (NX2NAVI_PIPE_LINK_DRAW_MS), 3 ms as before.
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>
2026-11-10 10:00:20 -05:00
chrg6
fdf2fb89d3 A draw that misses its pipeline has the rest of its submission read ahead, and the stage libraries of the pipelines the later draws need are queued at once.
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>
2026-11-10 10:00:20 -05:00
chrg6
b121f90e41 The library workers link the pipelines of skipped draws, so a burst of new materials is linked on several threads instead of by the one pool worker that is not inside an optimised build.
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>
2026-11-10 10:00:19 -05:00
chrg6
1d9b0dc962 A pipeline whose fragment shader reads a target of its own pass is fast-linked like any other: the feedback loop flags are part of the fragment output library.
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>
2026-11-10 10:00:19 -05:00
chrg6
eeeeec683b The pipeline statistics say how long a missed pipeline took to become usable and which stage libraries were slow, and a draw counts as skipped for its pipeline only when it was skipped.
- 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>
2026-11-10 10:00:18 -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
chrg6
cdaab81727 Merge the texture detail setting into the combined test line.
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>
2026-11-10 09:51:33 -05:00
chrg6
e3f623942b Check the texture detail in --probe-vkmem: ASTC textures are uploaded and rewritten, and every level and layer of their images must hold the game's level it stands for.
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>
2026-11-10 09:38:08 -05:00
chrg6
364a40ae9b Add the Texture detail row to the settings menu's Screen tab, in the four menu languages.
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>
2026-11-10 09:38:08 -05:00
chrg6
8603b0e1e9 Add the texture_detail setting, which makes large ASTC textures without their largest one or two mip levels on cards with little video memory.
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>
2026-11-10 09:38:07 -05:00
chrg6
f16554cb8e Keep the game's buffers mirrored in video memory up to half the budget, even when textures have filled it.
mirror_create refused a pool whose mirror did not fit under seven eighths of the VRAM budget. It first evicts idle
mirrors and asks idle textures for room, but on a 6 GiB card a loaded area's textures in use pass the budget by
themselves, so neither helps and the pools stay imported: the GPU then reads vertices, indices and storage over PCIe
at every draw. Two reports from RTX 3060 Laptop cards (6 GiB, a budget of about 4.7 GiB) measured 70 to 250 ms of GPU
time a frame and 5 to 13 fps in that state.

Now a mirror is made although the check fails while the mirrors together stay within half the budget; the textures
are still asked for the room, and past that share a pool is refused as before. A mirror the driver cannot keep in VRAM
is paged to system memory, where the imported pool already is, so it does not read slower than its pool.

Measured with the budget capped at 4,730 MiB on a 32 GiB card (1080p window, DLSS Balanced, no frame generation,
standing in Hyrule Field): before, 4 pools of 244.6 MiB stay imported, the largest pool among them (244.4 MiB), with
634 MiB mirrored; after, none stay imported and 878 MiB are mirrored, in two runs. The area still needs 5.6 to 6.3
GiB there, 3.9 GiB of it textures, so a 6 GiB card stays over its budget: this ends the buffer starvation, not the
overflow. Not run on a 6 GiB card.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 09:31:21 -05:00
chrg6
1f2b320736 Say "after restart" in the menu only while a restart would change something, and count the waiting settings on Restart.
A setting read at the start showed "after restart" under its value always. The values the game started with are now
kept (cfg.c start_values), a row shows the note only while its value differs from them, and the Restart button counts
the settings that wait, the upscaler, frame generation and HDR among them. A value the environment sets never waits.
--probe-cfg checks the count.

From pull request #377 by mooki000. Three conflicts in the menu resolved against the frame generation rows that came
in since: the upscaling row keeps its description by what can load, and the frame generation restart test knows both
ways NVIDIA's runs.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 09:14:23 -05:00
chrg6
a1fcbb0b07 Name NVIDIA's Shader Cache Size limit when the library warm finds the driver's cache cold.
The driver keeps the prepared shaders' libraries in its shader cache, and under Windows that cache has a size limit
set in NVIDIA's settings. A limit smaller than the set drops the oldest libraries as new ones come in (a 2048 MB cache
could not hold them on the reporter's RTX 5070 Ti), so every start builds them again in the background and play
stutters until that is done. The cold lines of the library warm now say so on NVIDIA under Windows, most plainly when
the cache is cold right after a warm that finished on the same driver; the build kit's notes and the example file say
it too. tests/warm_hint_check.py checks which hint each case gets.

From pull request #403 by tarballz; two conflicts resolved (an include and the build kit's notes).

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 09:13:35 -05:00
chrg6
25e31815fd Keep a texture's finalization on its registered slots, so a stale slot stays stale after its image was evicted.
Whether a slot was stale was read from the released image's entry, which eviction, the released cap or a new image
at the address can destroy before the slot is looked up again; the slot then revived the texture from storage the
game had freed (the access violation in the block copy during a pool refresh). The flag now lives on the slot: a
release sets it on the slots registered with that descriptor, a registration clears it, and a finalized slot of a
CPU-written texture follows only live GPU content at its address.

From pull request #348 by TophC7, taken as it is.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 09:13:08 -05:00
chrg6
87bf554637 Render the scene of a picture of 1440 lines or fewer at the share its DLSS or FSR mode names.
The game sizes its scene against its own 1440-line picture only while the picture is taller; under a smaller picture
it measures against that picture itself. The share was scaled by the picture's height over 1440 in both cases, so a
1080-line picture got three quarters of the scene its mode names: Quality drew 960x540 instead of 1280x720. A picture
of 1440 lines is unchanged; a 1080-line one now draws 1.78 times the pixels in every mode, which costs frame rate on
slow cards and is what the mode asks for.

From pull request #393 by mooki000, taken as it is.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 09:12:33 -05:00
chrg6
bbcce968b0 Run DLAA on the pass the HUD is drawn into, so its picture reaches the screen in gameplay.
In DLAA the pass the HUD is drawn into has the scene's size and the light format, and the game's upscaler has just
written the picture there. The frame used to end at the 8-bit output pass instead, which put DLSS's picture back into
a light target nothing reads any more: gameplay showed no DLAA at all. With an upscaling share nothing changes.

From pull request #382 by mooki000, taken as it is.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 09:12:33 -05:00
chrg6
f747c4e43d Skip the runtime shader step in make_game.py when the release does not carry build_shaders.py, so a player's update compiles again.
The launcher release carries the runtime's shader headers built and none of the tools that build them, and
make_game.py's compile step imported build_shaders unconditionally: every update to the 0.7.0 build stopped at
"Compile Changed Files" with ModuleNotFoundError. The import is now tried, and without it the step that rebuilds stale
headers is left out, as its comment intended. A launcher test checks that every tool make_game.py imports is in the
launcher release unless the import is guarded or only runs on Linux.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v0.7.0
2026-11-10 07:44:54 -05:00
chrg6
d6b4eab004 Describe the Linux changes, the event wakes and the log thread in the 0.7.0 notes, and thank their authors.
The Linux section says plainly that none of it was run here. The AppImage is described as built and attached by an
automated job after the release is published.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:40:20 -05:00
chrg6
fa2e5026a4 Merge GitHub main into the 0.7.0 release line: main's two newest commits are the event wake and log thread changes this line already holds.
The same two changes under main's hashes, so the tree does not change; main is an ancestor of the release line again.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:35:29 -05:00
chrg6
d0b15e4578 Merge the two performance changes into the 0.7.0 release line: a signalled event wakes only its own waiters, and a thread of its own writes the log.
One conflict, the list of setting names, which keeps both sides' additions.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:32:34 -05:00
chrg6
449b5f73b0 Merge the Linux pull requests into the 0.7.0 release line.
Eighteen commits, one per Linux pull request that was not in yet: the build fix for a source checkout, NVIDIA's shader
disk cache only with NVIDIA's driver, tools/run_game.py for a Linux game folder, a memory limit the low-memory handling
can read, the recording thread's core on Linux, shadow storage on Linux and Proton, the Steam Deck profile and prompts,
FSR 3.1 offered only where its library loads, DLSS frame generation through NGX on Linux, the AppImage with its window
and its workflow, and the processor level from /proc/cpuinfo. Two conflicts: the settings table keeps the area-load
settings under the new comment, and the example file keeps the lazy write-back as off by default and adds that it
stays off with shadow storage.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:31:55 -05:00
chrg6
5da5a48a90 Write the log from a thread of its own, so a stalled write no longer holds the thread that logged.
Every log line was two writes on the thread that logged it, to stderr and to the log file. Such a write can stall for
tens or hundreds of milliseconds (a busy disk, a virus scanner, a console or a pipe that is slow to take it), and the
render thread logs too: a frame it spent in a write was a hitch with nothing else to show for it.

In a game run the lines now go into a queue that a log thread writes out in order (runtime/logq.c). A fatal error,
the crash report and the end of the process write the queue out on their own thread first and write their own lines
directly, so nothing is lost to an exit. A thread that finds the queue's lock held for long, or the queue full, writes
its line itself. NX2NAVI_LOG_ASYNC=0 writes every line where it is logged, as before.

Every write is timed in both modes: a "log: ... writes of the log took over 2 ms" line counts the slow ones, gives
the longest and says whose time they took.

tests/logq_check.py checks order and completeness with eight threads, a slow destination, a stopped destination with
the queue full, and the synchronous flush.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:30:44 -05:00
chrg6
b9ccc5e7a5 Wake only the threads that wait for the event that was signalled.
A signalled kernel event, or a thread that ended, woke every thread blocked in WaitSynchronization. Each woken thread
takes a virtual core before it looks at its handles, asking a running thread of lower priority for its core when all
are held, finds nothing signalled and blocks again. The audio renderer signals its event 200 times a second, so every
thread that waits for a message, a timer or another thread's end did this 200 times a second at the cost of the
threads that were working.

A wait now records its handle list, and a signal wakes the waits that hold a handle of the signalled event or thread.
NX2NAVI_SYNC_WAKE_ALL=1 (or ab) wakes them all as before, and the sched line counts the waits woken and left asleep.

tests/sync_wake_check.py builds kernel.c with the rest of the runtime stubbed and checks the wakes, the timeouts, the
cancel and that none of 4,000 signals to eight looping waiters is lost, in both modes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:30:40 -05:00
chrg6
a03e059d2b Write the log from a thread of its own, so a stalled write no longer holds the thread that logged.
Every log line was two writes on the thread that logged it, to stderr and to the log file. Such a write can stall for
tens or hundreds of milliseconds (a busy disk, a virus scanner, a console or a pipe that is slow to take it), and the
render thread logs too: a frame it spent in a write was a hitch with nothing else to show for it.

In a game run the lines now go into a queue that a log thread writes out in order (runtime/logq.c). A fatal error,
the crash report and the end of the process write the queue out on their own thread first and write their own lines
directly, so nothing is lost to an exit. A thread that finds the queue's lock held for long, or the queue full, writes
its line itself. NX2NAVI_LOG_ASYNC=0 writes every line where it is logged, as before.

Every write is timed in both modes: a "log: ... writes of the log took over 2 ms" line counts the slow ones, gives
the longest and says whose time they took.

tests/logq_check.py checks order and completeness with eight threads, a slow destination, a stopped destination with
the queue full, and the synchronous flush.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:22:41 -05:00
chrg6
c2a51a334a Merge GitHub main into the 0.7.0 release line: uniform blocks copied at the submit call with the replay on its own thread, the overlay's frame times measured where the pictures go out, and early pictures kept back.
The two commits main gained since the last merge. docs/releases/v0.6.2.md stays removed; the two notes they added to
it are in docs/releases/v0.7.0.md. Checked on a dev build of the merged tree: the settings, self-test, graphics ABI,
input and kernel probes pass, and a scripted run with six DLSS mode changes presents no dark or black frame, makes the
area's textures in one frame at the load and ends without a fatal error.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:17:01 -05:00
chrg6
094573bd26 Wake only the threads that wait for the event that was signalled.
A signalled kernel event, or a thread that ended, woke every thread blocked in WaitSynchronization. Each woken thread
takes a virtual core before it looks at its handles, asking a running thread of lower priority for its core when all
are held, finds nothing signalled and blocks again. The audio renderer signals its event 200 times a second, so every
thread that waits for a message, a timer or another thread's end did this 200 times a second at the cost of the
threads that were working.

A wait now records its handle list, and a signal wakes the waits that hold a handle of the signalled event or thread.
NX2NAVI_SYNC_WAKE_ALL=1 (or ab) wakes them all as before, and the sched line counts the waits woken and left asleep.

tests/sync_wake_check.py builds kernel.c with the rest of the runtime stubbed and checks the wakes, the timeouts, the
cancel and that none of 4,000 signals to eight looping waiters is lost, in both modes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:15:30 -05:00
chrg6
9dbfed207f Turn the lazy write-back off by default and offer it in the settings menu as Experimental FPS boost.
With it on, single frames come out with the whole scene lit differently (brighter, shadows off). The flight recorder's
thumbnails of 1,800 presents standing in Hyrule Field show none with it off in every run, and with it on 43 to 181
while four other threads keep the CPU busy (10 to 97 in earlier runs, 0 on an idle machine); the cause is not found.
Two candidate fixes (arming only against a sync whose copies were handed over, and snapshots waiting for GPU writes in
flight) did not change the count, and fetching at every release made it far worse.

So wb_lazy is 0 unless set, on every platform (mem_init loses its special case), and the Screen tab has the row
Experimental FPS boost for it on Windows, with its text in the four menu languages; like every setting read at the
start it notes "after restart". The 0.7.0 notes describe it as experimental and off, say about 1.6 GiB for the video
memory a full shader cache no longer holds (2,484 to 846 MiB at the title screen in the measured run), and list the
two fixes before this one.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:12:23 -05:00
chrg6
71eb5fbe26 Make an area's textures at once after a load instead of over the first seconds, which showed the ground and walls black.
The texture pool creates 16 entries a refresh and stops after 6 ms a frame, so that textures streaming in do not make
long frames. An area load leaves thousands of slots at once (4,936 loading the test save), and at that rate they came
in over the frames after the load screen: the scene's first frames showed a quarter of the picture as black stand-ins,
for 22 presented frames on a free machine and for about three seconds with the CPU busy.

While a refresh leaves pool_load_backlog slots or more (512; 0 never), the pool is loading: the count limit is off, a
frame's refreshes may take pool_load_ms (50), and frame_hold leaves out the presents of frames drawn meanwhile (up to
2 s), so the picture before the load stays until fewer than 64 slots are left. Measured with the flight recorder's
thumbnails of every present: the 4,847 textures are made in one frame of 345 ms, 30 frames before the scene fades in,
and the black share of the picture goes from the fade straight to its final value. The streaming limits are as before
below the backlog.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:55:13 -05:00
chrg6
75b0dca907 Add Steam Deck button prompts beside the Xbox and PlayStation ones (prompts = steamdeck).
The Controls tab's Button prompts row and nx2navi.ini take steamdeck: the Deck's own labels in the game's
text, action prompt and ocarina notes, which are A B X Y where an Xbox pad has them, L1 R1 L2 R2 and L3 R3,
Menu and View. The style is drawn by tools/pad_prompts.py like the others, into romfs-patches-steamdeck
and romfs-patches-steamdeck-label, and every build with button art writes them.

Pull request #107 by loteran asked for PlayStation and Steam Deck prompts; it was closed, and the tree
took the PlayStation half in another form, drawn from button art instead of redrawn font glyphs. This is
the Steam Deck half in that form, written here: the art has no Deck style, so its entry in
tools/prompt_art.py names the Xbox pictures for the face buttons, Menu, View, the d-pad and the sticks
and the PlayStation pictures for the shoulders and stick clicks. It needs four pictures of the Xbox set
that no other style used (the white letters on a light button, for the ocarina's notes); a release made
with --prompt-art copies them with the rest.

The Windows files compile and tests/launcher_test.py passes, with the prompts step's test following the
number of folders (seven now); every picture the style names is in the art used for releases. The style
was not drawn here (that needs the game's font), and --probe-cfg, which has a new check, was not run.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:53:30 -05:00
chrg6
bc4ea7574f Read the processor's level from /proc/cpuinfo on Linux when clang's own detection finds none.
Where a sandbox or a compiler wrapper hides the processor from clang -march=native, tools/make_game.py found
no x86-64 level and built the lifted code as generic x86-64. It now falls back to the features every
logical processor lists in /proc/cpuinfo and takes a level only when they prove it. cpu_tag reads the same
list through the new cpu_flags.

From pull request #295 by repparw, whose three build fixes came with its FSR 3.1 work. The other two were
in already (the processor in the compile key, the shaders step keyed by the translator in use), and the
wrapper case that prompted this one is also handled by the probe's own environment; this is the fallback
for what that does not reach. The pull request's main part, FSR 3.1 upscaling on Linux from vendored
sources, is not taken: 366 of its 494 vendored files are compiled SPIR-V shaders as C headers, which this
repository does not hold.

Checked on Windows with made-up cpuinfo files (v3 on every core, one core at v2, v2, no flags line);
tests/launcher_test.py passes.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:48:09 -05:00
chrg6
eb2608296b Generate frames with DLSS on Linux through NGX itself: 2x to 6x or dynamic, switched while the game runs.
DLSS Frame Generation went through Streamline, which only the Windows build has, so framegen = dlss did
nothing on Linux. There it now calls NGX's frame generation feature directly (vk_dlaa.c vk_ngxfg_*), with
the depth, motion vectors, jitter, camera and HUD-less picture the Streamline path gets. With it on, each
frame is composed into an image of its own instead of a swapchain image, the generated frames are made at
the end of the frame's command buffer, and a presenter thread shows them and then the rendered frame, each
copied into a swapchain image it acquires itself, spaced over the time between rendered frames
(gfx_present.c ng_*). fg_mult is the multiplier: 2 to 6, or dynamic for only the frames the display's
refresh rate needs. NVIDIA's libnvidia-ngx-dlssg.so.<version> goes beside the game; it is not in the
repository or any kit.

Pull request #124 by spidercraft. Its base was the present path of 0.5, so it is ported by hand:
- taken as written: the NGX calls and the presenter (the block is lifted from the pull request's diff);
- wired again into present_image, create_swapchain, the output rate and the HUD-less picture as they
  are now, with every new branch behind vk_ngxfg_wanted, which is 0 in the Windows build: the code is
  compiled there and never reached;
- added: frame generation's feature and parameters are released before NGX shuts down, the presenter's
  images, semaphores and command pool go when the device is released and are made again for a new one,
  and a failed submit on this path settles its fence without an acquire semaphore;
- the menu: the Frame generation row follows the setting at once on Linux and says "after restart" only
  when the game started without DLSS upscaling; the multiplier row lists what the GPU allows and Dynamic;
- the overlay row of the pull request it builds on (#123) is not part of this.
It touches vk_dlaa.c, which another session is changing: one field in N, the FG state after it, one FN
line, the instance extension query, the capability read in vk_dlaa_init, the release, and a block appended
at the end.

Known gap, as in the pull request: on this path fps_limit counts the frames the game draws. The Windows
files compile without new warnings; nothing was run, and the Linux path is not compiled here.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:45:59 -05:00
chrg6
d01f0d7052 Give the Linux AppImage a window to install, update and play.
Started without arguments on a desktop (a double-click), the AppImage showed nothing. It now opens a
window (tools/appimage_gui.py, standard library and Pillow): the dump and the game folder through the
desktop's own file choosers, Install or Update with a progress bar per step, Play, the last run's output,
and an applications-menu entry for play.sh. The window runs AppRun --events, which does what the command
line does and prints make_game.py's progress lines plus its own for the package check and DLSS. Its text
is drawn with Pillow in the desktop's font and sized by the screen's DPI, in English or French by locale.
The command line is unchanged.

Pull request #291 by loteran. The window is taken as it is, with one change: the menu entry's Exec line
quotes the game folder by the desktop entry rules, so a folder name with a quote, a dollar or a percent
sign gives a working entry. The AppRun changes are applied by hand onto the script as it is now.

The script passes bash -n and the window a syntax check; neither was run, on Linux or elsewhere.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:39:36 -05:00
chrg6
7d3185fca6 Let an AppImage without NVIDIA's DLSS library download it beside the game on machines with NVIDIA's driver.
An AppImage made with --no-dlss carries no NVIDIA file, and its builds had no DLSS. After a build its
AppRun now downloads the pinned libnvidia-ngx-dlss.so.<version> from NVIDIA's DLSS SDK repository when
/proc/driver/nvidia exists and the game folder holds no such library, checks its SHA-256 and writes the
notice that names its licence. NX2NAVI_NO_DLSS=1 skips it, and a failed download is a warning.

Pull request #290 by loteran, written again on top of the pin that came with the bundled library:
- the version, file name, checksum and address come from tools/dlss_pin.py when the AppImage is made,
  instead of a second copy in the script, and the notice is dlss_pin's own text, carried in the kit;
- it runs only when no library is beside the game, so it adds nothing to a build that placed the kit's
  and leaves a player's own version alone;
- the download goes to a name that does not start like the library's: the game takes any file with that
  prefix, and an interrupted download would have been one.

The generated AppRun passes bash -n; nothing was run on Linux here.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:38:45 -05:00
chrg6
32ee021905 Include NVIDIA's pinned DLSS library for Linux in the AppImage, and place it beside the built game.
On Linux the game looks for libnvidia-ngx-dlss.so.<version> beside the executable, and the driver brings
only NGX's core, so the DLSS modes did nothing in every Linux build. tools/dlss_pin.py pins the Linux
library of the DLSS version it pins for Windows, with its own size and SHA-256; tools/make_appimage.py
puts it in the kit's bin/ with its notice (downloaded from NVIDIA's DLSS SDK repository and checked, or
--dlss; --no-dlss leaves it out), and tools/make_game.py copies it beside the game on Linux as it copies
the DLL on Windows, keeping a library the player put there.

Pull request #297 by mooki000, taken as it is; the builder's description is merged with the button art
option that came with the release workflow. On Windows place_dlss does what it did. No NVIDIA file is in
the repository.

tests/launcher_test.py passes with its two new tests (112 tests, Python 3.12 on Windows); the AppImage
was not built here.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:38:02 -05:00
epicmeme546
a710c6193d Measure the overlay's frame times where the pictures go out, and keep an early picture back to seven eighths of the recent interval.
The overlay's rate, lows and graph were fed from the game's present calls. With the replay on the submitting thread, work held for an event is replayed with the next frame, so the calls come in uneven steps although each frame costs about the same: 36 and 9 ms apart in turn in a village, a sawtooth in the graph and a 1% low of 14 fps, while the pictures went out 19.7 ms apart on average and changed by 3.9 ms from one to the next. The present thread now reports each picture (perfmon_shown) and the overlay takes its intervals from that; the game's calls are used only where no present thread reports.

A picture that arrives early now waits until seven eighths of the recent interval have passed since the one before (vk_backend.c pace_smooth, NX2NAVI_PACE_SMOOTH, on by default; not with frame generation). In that village the change from one picture to the next went from 3.9 to 3.0 ms at 51 fps both ways, in a graveyard from 3.1 to 2.6 ms at 54.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:35:28 -05:00
chrg6
ff4b6772f9 Build the Linux AppImage on a GitHub Actions Linux runner and attach it to each release.
The release is assembled on Windows, and the AppImage needs Linux (the translator's native build and
appimagetool), so releases carried no Linux download. .github/workflows/linux-appimage.yml runs when a
release is published, or by hand for a tag: it builds the translator, takes the button art from the
release's own app zip, runs tools/make_appimage.py, checks that the AppImage starts and uploads it with
its SHA-256 to that release. tools/make_appimage.py takes --prompt-art as make_kit.py does and bundles
pillow, so an AppImage build can draw the Xbox, PlayStation and keyboard prompts.

Pull request #273 by loteran, taken as it is. Added: the workflow installs glslc and spirv-tools, which
make_appimage.py needs since the runtime's shaders are compiled at build time.

The workflow does nothing until this file is on the repository's default branch on GitHub; from then on
every published release starts it, with write access to that release. It was not run here, and
make_appimage.py only passes a syntax check on Windows.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:34:28 -05:00