Commit Graph

222 Commits

Author SHA1 Message Date
chrg6
34cfb1a6e9 Say in the 0.9.0 notes that Kakariko's haze no longer drops out for a frame, and narrow the known flicker to shadows and lighting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 05:43:33 -05:00
chrg6
4956ef8365 Let a finalized slot sample its own texture's image while that still holds the texture, not the stand-in.
The game finalizes texture objects on its own thread ahead of the replay, which still draws the frame the game recorded
before that; on the console the memory still holds the texels those draws read. Kakariko moves about 30 of its
textures to new addresses every four or five frames, and each move finalized the old descriptors. The replay's draws
of the frame before then met finalized CPU-written slots, and the stale rule gave them the stand-in even when their
own entry was still there with its content. The village's haze dropped out of a frame a few times a minute, and the
haze sprites that met a stand-in left hard-edged clear patches. A play session in Kakariko logged up to 4,805 stand-in
lookups in 60 frames, 1,826 in the 60 frames around one haze dropout, and 612 in the 60 frames after a run of
patched frames.

Such a slot now resolves to its own entry while that entry was uploaded or drawn by the GPU, and was neither superseded
by another descriptor at its address nor overwritten. The lookup samples the image as it is: it neither takes the
entry back nor reads its storage, so freed storage is still never read (the access violation the stale rule was made
for). NX2NAVI_TEX_STALE_KEPT=0 gives the stand-in again. The perf log's finalize line counts these lookups.

In a replay of a recorded Kakariko walk, most 60-frame windows went from stand-ins to the kept image (up to 328 a
window, stand-ins 0). In play, stand-ins remain in short bursts where the entry was already gone (up to about 2,000 in
60 frames, against the kept image in most windows). The tester still sees flicker that the 640x360 frame recorder
does not catch; that is not settled by this.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 05:42:43 -05:00
chrg6
e97c892742 Write the release notes of 0.9.0: the draft after 0.8.0 with every change since, the install steps, the known issues and the thanks.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 05:32:09 -05:00
epicmeme546
4c5855ce43 Hold ASTC textures whose every block decodes opaque as BC1 instead of BC3: the same texels in half the video memory.
The BC3 encoder's colour half is a four-colour BC1 block (c0 above c1, or every index 0 when they are equal), so a BC1 image of an opaque texture decodes to exactly the texels the BC3 one did. The encoder writes that half alone with its specialization constant BC1 set; its push constants are as before, so the BC3 pipeline is unchanged.

Every ASTC texture the CPU provides is made BC3, and its first upload checks its blocks while it copies them into staging: when all decode with alpha 255 (tex_astc.c astc_blocks_opaque, from the block header for all but the endpoint modes with alpha, which are decoded), the entry gets a BC1 image in place of the BC3 one before the decode. Later uploads into a BC1 image check again and go back to BC3 for content with alpha. A large texture's copy and check run on the ASTC decode workers, so its first upload copies faster than before. A check of its own where the texture is made cost 1.7 s of the render thread over a seven-minute teleport tour.

On that tour (Kakariko Village, the Graveyard, Death Mountain Trail, Kokiri Forest, Hyrule Field, RX 7900 XT), against BC3 alone in the same batch: the ASTC textures in Hyrule Field at the end 2,568 instead of 3,104 MiB, video memory at its peak 11,814 instead of 12,374 MiB, process commit at its peak 25,539 instead of 26,139 MiB; the first 7,200 frames in 93.7 instead of 101.3 s and the run's slowest 1 % at 24.1 instead of 21.1 fps. --probe-astc checks the header test against a decode of every generated block, the loop, its copy and the worker pool against it, and each BC1 block against the colour half of its BC3 block. NX2NAVI_ASTC_BC1=0 (astc_bc1 = 0) keeps every ASTC texture BC3; the sampling probe NX2NAVI_ASTC_BC_PROBE it replaces is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 02:11:46 -05:00
chrg6
14fbc6a0ea Make the mesh fallback's programs on up to four worker threads instead of one.
A place's task and mesh programs all arrive in its first frames, and each one's draws are left out until its compute
pipelines are built, so with one thread the foliage of a first visit filled in one program at a time.
NX2NAVI_MESH_FALLBACK_WORKERS (auto: one thread for each 4 logical processors, 1 to 4) sets how many threads make
them, and the log says how long the queue took to empty the first time.

Measured with the fallback forced on, no pipeline cache and new exe names (cold driver cache), the adult save's
first 75 s: with 1 worker the first 5 programs were ready after 420 ms, 3,266 draws were skipped while a pipeline or
program was pending, 1% low 16.1 fps and 145 frames over 50 ms; with 4 workers 256 ms, 956 draws, 55.7 fps and 35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:18 -05:00
chrg6
677a69378f Fill the fragment inputs a mesh program does not write, with the point sprite's coordinate for point meshes.
A mesh pipeline whose fragment stage reads a location the mesh program never writes now gets a fragment module where
that input is a private variable set to (0, 0, 0, 1) at entry, or to (s, t, 0, 1) of gl_PointCoord when the mesh
program draws points. The fragment shaders recorded with two of the game's point-drawing mesh programs read location
5, which those programs never write; Vulkan leaves such an input undefined.

Checks: tests/fs_fill_inputs_check.py passes; 237 recorded fragment modules rewritten offline pass spirv-val; a
60 s Kakariko run with the change shows the area as before with no Vulkan errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:18 -05:00
chrg6
c60abba2e5 Count a gamepad axis only once it has been seen at rest.
A device that reports an axis held from the start (a virtual pad nobody holds, a trigger or pedal mapped onto a stick,
a stick resting far off its middle) turned the camera or moved Link on its own, and since pad_active counted it as
pressed, it could take the gamepad role from the controller in the player's hands whenever that one was idle, and keep
it. input.c now reads every axis through pad_axis: 0 until the axis has been within a quarter of its range of rest
(a trigger's rest is 0), then the device's value. The game's sticks and triggers, the menu's stick readout and the
choice of gamepad all use it. A stick the player holds at the start counts once it has been let go.

Checked: the runtime compiles; new tests/pad_rest_check.py (pad_axis cut from input.c, SDL stood in for) passes: a
held axis reads 0 for 100 polls, a released one reads the device, and axes and gamepads keep separate states;
tests/mouse_stick_check.py still passes. Not run with a real device.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:18 -05:00
chrg6
ec2e05e3a3 Leave F11 to overlays when Ctrl, Shift or Alt is held with it.
F11 switched fullscreen whatever modifier was held, so an overlay or capture tool's Ctrl+F11 or Shift+F11 shortcut also
switched the game's window. The Win32 window now ignores F11 with Ctrl or Shift held (Alt+F11 already arrives as a
system key it does not handle), and the SDL window with any modifier. docs/controls.md says so.

Checked: the runtime compiles; gfx_window_sdl.c passes a syntax check against the SDL3 headers. Not run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:18 -05:00
chrg6
01164679d4 Give Mesa's shader cache beside the game a limit of 8 GB instead of its default 1 GB.
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>
2026-11-11 19:38:18 -05:00
chrg6
e8b4d29f91 Give mesh output components outside the program's output map Maxwell's default of (0, 0, 0, 1).
A mesh program's header lists the components of each output vector it has; the fragment stage may read the others.
The native mesh entry function never wrote them, so they read as whatever the output held, and the fallback's vertex
stage wrote 0 for all of them, w included. Maxwell gives an unwritten component (0, 0, 0, 1), as the pipeline code
already pads a missing vertex output. Both paths now write that default: MeshSpirv.cs after copying each vertex's lines,
mesh_fb.c meshfb_vs with w as the bits of 1.0f whatever the type.

Checked: all 520 task and mesh programs of an installed shader cache translate and pass spirv-val (vulkan1.3); 500
mesh modules gained about 730 bytes each on average, so many of the game's mesh programs have partly listed vectors.
--probe-meshfb passes (fallback pictures equal the mesh pipeline's), and its module part converts all 520 new
translations with 0 failures, all 1,020 modules pass spirv-val and their compute pipelines build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 19:38:17 -05:00
chrg6
bfe88e503a Add one anti-aliasing setting: the game's SMAA, off, FXAA, SMAA at its Ultra preset, or TAA.
Ocarina anti-aliases every frame with its own SMAA in three compute passes after the bloom composite. The new aa
setting (nx2navi.ini aa, the Screen tab's Anti-aliasing row) keeps that by default. off, fxaa and smaa leave out the
game's edge and weight passes and replace its composition with a pass of our own that writes the same square-rooted
RGB9E5 words into the scene target: a plain copy, an edge smoothing written here, or the SMAA reference implementation
with its area and search textures (MIT). taa is DLSS or FSR 3.1 at the scene's size, which the row sets through the
upscaler; while either runs, the game's SMAA stays whatever aa says. The game's passes are known by a hash of their
guest code. The shader build now also rebuilds a header when a file its source includes changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:09:16 -05:00
chrg6
3529eff949 Offer both kinds of HDR in the menu again: the tone mapped picture and the game's own.
The HDR row lists Off, RenoDX (the game's picture tone mapped to the display, the one fixed look) and Native (the game's own HDR, taken up at the next start), with the peak, game and HUD whites under either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:06:30 -05:00
chrg6
309e412f1f Make HDR one fixed look with true blacks, and keep only its on switch and the three whites as settings.
HDR on is the game's scene through the observer-response mapper, anchored to the game's own mid grey and tone slope, with half of the hue bend of the game's clamp: the defaults the menu had. The tone mapper choice, the mid grey, the tone slope and its match, the hue clip, exposure, highlights, shadows, contrast, saturation, highlight saturation, blowout, flare, hue, grade and gamma are gone from the menu, the settings table and the example ini; a file that still sets them works as before, the keys read by nothing and logging nothing. The menu's HDR row is Off and On, with the peak, game and HUD whites under it. The game's own HDR is still there for a file that sets hdr = native, and the row lists it only then. The luminance mapper and its grading code, which no setting reaches any more, are removed from the tonemap library, and the support buffer carries one vec4 for it.

The dark tones of a dark scene sat far above black: the present showed the game's sRGB-encoded picture as that encode decodes, linear near black, while the game is graded for a display of gamma about 2.2, and since the observer-response mapper became the default the 2.2 correction ran with the luminance mapper only. A scene value of 0.003 came out at 0.29 nits at a white of 350 nits, where a 2.2 display shows the game at 0.015. The present now always shows the picture as a 2.2 display would, and the tonemap aims its peak lower by that correction's inverse, so the brightest light still reaches the display's peak; the same value comes out at 0.017 nits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:06:30 -05:00
Kevin Parnell
3752b8d157 Say in the launcher's docs that downloads follow the PC's HTTP proxy, from the environment or Windows' settings.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 14:37:11 -05:00
epicmeme546
06bc977ca9 Add the traversal stutter fix, the one Resolution row and the FSR 4.1 name to the draft release notes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 14:06:17 -05:00
epicmeme546
c490bfec68 Start the draft release notes of 0.8.1 with the fixes for vanished ground, white flicker, texture churn on smaller cards, FSR 4 turning into FSR 3.1, and the debug menu's teleports and free camera.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 13:19:24 -05:00
epicmeme546
8afca0922a Merge the fix for sunken terrain chunks from pull request #535: a finalize marks stale only the slots its own texture object registered, so a streamed heightmap registered at the storage of the one before it keeps its slot.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 11:28:41 -05:00
chrg6
d55f7e763d Make co-op a hidden beta in 0.8.0: the menu shows its Co-op tab only with coop_beta = 1.
No menu row sets coop_beta; it lives in nx2navi.ini under [coop] (NX2NAVI_COOP_BETA) and takes effect at once. Co-op
itself is unchanged: coop = 1 still runs a session with or without the tab. The notes and the co-op guide say how to
show the tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 09:47:42 -05:00
chrg6
7f01711087 Make the release 0.8.0, with its notes.
The draft notes for the release after 0.7.1 become the notes of 0.8.0: every change since 0.7.1, the install and
update steps, the requirements (mesh shaders are no longer required, with the fallback untested on such a card), the
known issues and the thanks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 09:16:23 -05:00
chrg6
a516025760 Add the two flicker fixes to the draft release notes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 08:59:01 -05:00
chrg6
4f85f3d59a Leave the pools as imports for NVIDIA's own driver on Linux in the game itself.
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>
2026-11-11 08:58:48 -05:00
chrg6
cd2a13062e Add the fixes line's changes to the draft release notes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 08:21:36 -05:00
chrg6
bdc67cebfc Take out the GPU address history of a lost device's report.
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>
2026-11-11 08:18:55 -05:00
chrg6
3d394dd84e Merge the fixes line: the stutter, memory, flicker and crash fixes, co-op, the mesh shader fallback and the launcher fixes.
Brought onto the line with FSR 4, its frame generation and the debug menu. Where both sides had added to the same
place, both stay: the patch table keeps the pickup count and the free camera, the settings list keeps both sides'
names, the menu includes the co-op page and the debug menu, and the launcher builder takes the pipeline list and the
FSR 4 folder. Both sides had added the menu's typed-character event; one definition stays, with the window code that
passes characters outside the basic plane.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 08:18:01 -05:00
epicmeme546
b59c0d40a8 Add a free camera to the debug menu.
With it on, the game draws from a camera of the menu's instead of its own, in play and in cutscenes. The left stick moves it along its view and sideways, the right stick turns it, ZR and ZL raise and lower it, R makes it faster and L slower, and Link stands meanwhile; or the camera stays where it is and the pad plays the game.

It works inside the game's own camera update: a new patch site sends the instruction at which the frame's position, target and up vector are final to a host function, which replaces them for the camera that follows Link and the one the events direct, so the game draws, culls and loads detail for what the free camera sees. The camera of the interface's 3D models and the one the sound follows stay the game's. The set is off unless the menu switches it on. A teleport's arrival starts the free camera again from the game's view.

Tested in Hyrule Field with scripted stick input (turn, rise, strafe, with Link's position unchanged), with the pad playing while the camera stays, across a teleport to Kakariko Village, during the Kakariko arrival cutscene, and switched on by a click in the menu.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 08:04:19 -05:00
Martin
2765a752b0 Finalize only the slots a texture object registered itself, so a finalize no longer touches another live texture's slots at the same storage, and drop the early return that skipped the whole release when such a texture was live, which also kept textures the game expected released (Kakariko fog lines).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 09:30:37 -03:00
epicmeme546
fe3126e9a7 Add teleporting and cutscene starting to the debug menu.
The Teleport tab lists every area of the game with its named start positions, read from the player's own game files: the warp songs' pads, both sides of the loading zones and the boss rooms among them. A click changes area the way the game's own doors do: during a short event of the game that only waits, the menu reserves the next area and start position in the game's data and asks the game's sequence for the change, so the area unloads and loads as in play, a first visit's cutscene plays and a boss room starts its fight.

The Cutscenes tab lists the game's event scripts by name and starts one through the game's event manager.

Tested from Hyrule Field to Kakariko Village, the Forest Temple, Kokiri Forest in the child's time, the bottom of Lake Hylia, Volvagia's room, and the pads of two warp songs, four of them in one run, with Link walking afterwards; the Kakariko arrival cutscene started by name.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 07:29:17 -05:00
epicmeme546
5a69bdc91f Add a debug menu for testing Ocarina of Time: the game's flags by name, the game's speed, and a memory search, on F9.
With debug_menu = 1 (off by default; the Game tab has the switch) F9 opens a panel on the left edge in the
performance overlay's look, with the game in view beside it. ui/menu.cpp lays it out in the settings menu's place,
so its input and the game's blocked pad are the menu's own. Typed characters now reach the menu for its text fields.

Flags: the game's 11,292 on/off flags are read from its game data manager, and a click changes one through the
game's own function on the game's thread at its pad read (runtime/debug_tools.c dbg_tick), so the game applies it
at its next update as it does its own changes. The game knows a flag by the hash of its name only; the names shown
are found in the player's own event flow files, hashed and matched when the panel first opens (2,250 of them from
915 files in 0.2 s), and none is kept in the code. World: Link's position, and a speed factor from 0.05 to 8 that
multiplies the step the runtime gives the game each frame (measured 0.50 at 0.5x). Memory: a search of the heap
for a value of 1, 2 or 4 bytes or a float (about 2 s), narrowed by equals, changed, unchanged, went up or went
down, with a value to set at a place or to hold there every frame.

Tested through the window's own messages: F9 opened it, a click on a flag set that flag in the game, text typed
into the filter narrowed the list, and F9 closed it; a scripted run set and held a value and put it back.

Teleporting, a free camera and starting cutscenes are not in it: writing Link's position, or every copy of it in
the heap, moved him back within three frames, so they need the game's own warp, scene-change and event functions.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 06:19:47 -05:00
chrg6
295922c224 Sync the device address table's shadows once per guest submission that binds the table, and take out the port of pull request #353.
The author of pull request #353 closed it on 2026-11-11 with the words that they give no permission to use its code. Its port (3134f524) goes: the mark of a pipeline that declares the table (VkPipeInfo.gpa, vk_pipeline.c pipe_add) and of the part that draws with it (vk_mem_gpa_use in vk_exec_draw.c bind_resources and mo_emit), the sync of the table's pools in every such part's prologue (vk_mem_gpa_sync), the submission with a callback in the replay's place as it stood (vk_sync_probe_submit), and --probe-vkmem's read through the table with its shader (test_gpa_read, runtime/shaders/gpa_read.spvasm).

In its place, after pull request #551, which was written without that code: a shadow pool goes into the table and onto a list of the table's shadows (vk_memory.c shadow_table_add); the record of a binding of the table notes the guest submission (vk_exec_draw.c ssbo_info, vk_mem_shadow_table_read); and vk_mem_shadow_sync brings the CPU's writes into every shadow on the list once for that guest submission, before the part is submitted. Once is enough, because the copy-ins read the guest submission's write-watch snapshots, which a second sync would find unchanged. Measured by #551's author on Linux (RTX 4090, Kokiri Forest): 113 and 76 frames over 25 ms in 75 s against 217 and 429 with the port, 43 against 41 fps, the picture complete in both.

What this line had built on the port stays, on the new sync: the test that passes over a pool with nothing written (shadow_pool_clean), the upload of the same pages into a table pool's VRAM mirror, the write-back of stores through the table (NX2NAVI_SHADOW_TABLE_STORES, table_wb_cb, now asked once per guest submission that binds the table) and the counts of NX2NAVI_SHADOW_CHECK. The probes that record their own commands (the lost-device probe, --probe-vkmem's table parts) call vk_sync_submit_own, written anew. vk_mem_shadow_storage stays: one line that returns the mode. Finalized pools in the table were already kept for vk_mem_shutdown here.

--probe-vkmem has a check of its own for the sync (test_table_sync): the shadow of a pool that is never bound keeps its old word through a CPU write and a guest submission that does not bind the table, and holds the new one after a guest submission that does. It passes with NX2NAVI_SHADOW=1 on Windows (RTX 5090), as do the store check with NX2NAVI_SHADOW_TABLE_STORES=1 and the probe without shadow storage; --probe-devlost ends as before. Not run: the game, with or without shadow storage, and anything on Linux.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 06:10:09 -05:00
chrg6
31e0a02901 Merge the mesh shader fallback: task and mesh programs drawn through compute passes and an ordinary draw where the card has no mesh shaders.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 06:01:39 -05:00
chrg6
8bb9189250 Describe the mesh fallback in the architecture notes.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 05:58:43 -05:00
chrg6
b1408ee371 Add --probe-meshfb, which draws mesh programs through the mesh pipeline and through the fallback and compares the pictures.
The probe's programs are written in the shape the translator gives a task or mesh program and need no game files.
With a folder of game translations it converts those instead and builds their compute pipelines.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 05:58:43 -05:00
chrg6
0a535ff853 Add a tool that checks the translated shaders of a shader cache offline, with the game's own load checks and spirv-val.
tools/validate_shader_cache.py reads every module and its reflection from the pack and the loose files, says how many the game would refuse and translate again, runs spirv-val on the others and groups what it refuses by message. It writes nothing to the cache. tests/validate_shader_cache_test.py checks it on a made-up cache.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 05:29:06 -05:00
chrg6
f9219180b9 Try a pipeline cache save again when it did not happen, up to six times at the save interval.
A save that found no memory for its copy, could not write its file or met a build that had just begun was not tried again until the next new pipeline was built, so a run that built none and then ended without its orderly exit lost what it had compiled since the last file. The autosave thread now repeats the try, and the same bound holds for the save owed after the play file was folded in, which before was tried at every interval for as long as it failed. tests/save_retry_check.py checks the rule.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 05:25:38 -05:00
chrg6
d073d80e70 Create FSR 3.1's pipelines through the pipeline caches kept across runs, so a context is not compiled again at every start the driver's own cache does not serve.
The library creates its pipelines itself and names no pipeline cache. It is now handed pipeline creates of the runtime's in the place of the device's two, and a create without a cache is asked of the pipeline cache first and compiled into the play cache otherwise. ffx_pipe_cache = 0 turns it off. --probe-ffx-cache makes one upscaling context without a window or the game and says where its pipelines came from, and tests/ffx_cache_check.py runs it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 05:22:56 -05:00
epicmeme546
45d0c87423 List the FSR 4 library's frame generation in the Frame generation row as a choice of its own.
The row showed AMD FSR 3.1 alone, and the frame generation of AMD's FSR SDK 2.4 could only be set in nx2navi.ini.
It is now listed after AMD FSR 3.1 as "AMD FSR 4 library" with the version the library runs on the card (3.1.6 on
an RX 7900 XT, the machine-learned 4.x on a Radeon RX 9000), where its libraries are there and FSR 4 runs. Choosing
it sets fsr4_fg and, since it generates only beside FSR 4 upscaling, FSR 4 as the upscaler; choosing AMD FSR 3.1
clears fsr4_fg. The performance overlay names the one whose swapchain is in place. In a test run the row went from
AMD FSR 3.1 to the new choice, back and to it again, and the swapchain followed each time.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 05:18:48 -05:00
chrg6
2e28ba12df Merge the stutter fixes: a pipeline cache past 2 GB is read, the prepared shader list knows its seed, the pipeline cache is written only when it changed with a small file for what play adds, and a queued build a draw waits for goes first.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 04:59:05 -05:00
chrg6
719ee94e67 Write a pipeline cache only when its data grew, and give what a run compiles a cache file of its own.
pipeline-cache.bin was copied out of the driver and written whole at every save after any pipeline was created, also one the cache had held: tonight's logs show three saves of 358.1 MB in a run that added 0.3 MB, and 302 to 306 MB written in each of seven sessions that added about 0.5 MB each. The library warm in play built its 37,572 stage libraries into the same cache, which grew it by 1.6 GB: a save of 2026.7 MB in play, a file over pipeline_cache_max_mb, and the next start dropped it whole and compiled the pipelines it had held.

cache_save now asks a cache for the size of its data first and neither copies nor writes one whose size is what its file holds. What a run compiles goes into a second cache with its own file, pipeline-cache-play.bin (pipeline_cache_play, default 1; needs pipelineCreationCacheControl): pipe_create_in asks the first cache with VK_PIPELINE_CREATE_FAIL_ON_PIPELINE_COMPILE_REQUIRED_BIT and builds into the play cache only when that says a compile is needed, and in play only the play file is saved. At a start the play file is read back and merged into the first cache in memory; once it reached pipeline_cache_play_mb (64) it is folded in for good: pipeline-cache.bin is written once, before play, and the play file removed. The preparation modes fold it at any size and build into the one file as before. The library warm's threads create with no cache of ours, so their libraries reach the driver's own cache alone.

Checked with --probe-prewarm on a copy of a dev folder's caches (3,212 recorded pipelines, 333 compute shaders), no game run: with a filled pipeline-cache.bin every pipeline came from it and neither file was written; with none and a cold driver cache the compiles went into pipeline-cache-play.bin (75.9 MB written, pipeline-cache.bin not created); the next start merged it and wrote pipeline-cache.bin once (191.1 MB) and removed it; kept below the fold size, a play file of 68.8 MB gave 1,445 pipelines at 0.3 ms from the first cache where 470 hit without the merge, and was not written again. Not checked in play: the saves during a session, the library warm without a cache, and drivers other than NVIDIA's.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 04:56:26 -05:00
epicmeme546
5bdf583331 Add frame generation through AMD's FSR SDK 2.4 as a setting to try with FSR 4, and never generate a frame without the HUD-less picture.
runtime/vk_fg4.cpp makes the frame generation swapchain and context with the libraries FSR 4 comes with
(amd_fidelityfx_loader_vk.dll and amd_fidelityfx_framegeneration_vk.dll beside the executable). vk_fg.cpp takes
that path only with fsr4_fg = 1 while FSR 4 is the upscaler; the default stays the FSR 3.1 library. The loader
picks the version for the card: the machine-learned FSR Frame Generation 4 on Radeon RX 9000 cards with Windows 11,
which is untested, and 3.1.6 elsewhere. On an RX 7900 XT (driver 26.10.1) it lists frame generation 3.1.6 and the
swapchains 4.0.0, which the driver brings and the loader chooses, and 3.1.7. The swapchain in place is wanted only
while its library is the one to use, so a change of the setting or of the upscaler, or a failure of this path,
takes it down as switching frame generation off does and the next present makes it with the other library.

The preparation gives this library the camera's position and axes, which FSR Frame Generation 4 requires, and a
scene scale of one metre a unit: with the 0.01 the FSR 3.1 path uses, version 3.1.6 drew grey translucent shapes
into the sky of its frames while the camera turned (160 captures of the screen during a camera sweep; clean with 1).

Both paths now generate a frame only when the picture without the HUD is there for it: generated from the
presented picture alone, the HUD would be warped with the scene behind it. In four configurations (21:9 with either
library, a 16:9 picture in a 21:9 window, a 1080 picture without HDR) every generated frame had it, and the log
counts the frames without. On screen, of the 6,404 pixels of the HUD's item button that stayed fixed through a
camera sweep with the FSR 3.1 library, 6,162 stayed fixed with this one.

Standing in Hyrule Field at 3440x1440 with FSR 4 Quality the game draws 43.0 frames a second with this path and
42.2 with the FSR 3.1 library, each shown twice over. One run changed the present mode, HDR, fullscreen, frame
generation, FSR 4, this setting and the upscaler in turn: 216 screenshots without a bad frame, and no failure.

The SDK's frame generation headers join runtime/third_party/ffx_sdk, unmodified. tools/make_kit.py and
tools/make_launcher.py take the third library from the --ffx4 folder when it is there, and tools/make_game.py
copies it beside the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 04:49:01 -05:00
chrg6
147e8a02e3 Warn in the log at the start when the machine has less memory free than the game takes in play.
The game held 23.9 to 24.4 GB of commit in three sessions on an RTX 5090 at 3840x2160, flat over the session, on a
machine whose commit stood at 66 of 73 GB with it. A machine that cannot give it that much pages, drops textures or
loses the device, and its log said nothing about memory until then.

mem_start_note reads the system's commit when a game starts and writes one warning line when what is free, plus what
the process already holds, is below mem_need_mb (24576 by default, 0 for no warning). The menu has no place for a
start-up notice (its only toasts are the co-op page's), so the line is in the log alone.

tests/mem_need_check.py checks the rule and, through --probe-memneed, the answer and the warning line with the
machine's own commit.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 04:48:36 -05:00
chrg6
39fa0be78c Make every pipeline built ahead of its first use pass a memory gate that counts the builds already running.
Only the large compute shaders' background builder looked at the system's commit before it started a build, and it
saw nothing of what the builds beside it were about to take. The boot's recorded pipelines (up to 64 builders), the
pool's background builds during play, the boot's compute pipelines and stage libraries and the library warm asked
nobody.

Each of them now asks the gate before a build. A build that starts reserves an estimate of the driver compiler's
working memory until it ends (build_reserve_mb, 64 MB; 4096 MB for a large compute shader), and the gate admits a
build only while the commit headroom holds the part kept free (build_free_mb, 4096 MB), the reservations and its own.
A refused boot builder ends and leaves its pipelines to the pool; the pool leaves a background build queued and asks
again every two seconds; the stage library prebuild and the library warm wait up to 30 seconds and then end, and the
warm goes on at the next start. A pipeline a draw or a dispatch waits for is never refused. The preparation of the
pipeline list runs without the gate: no game runs beside it.

tests/build_gate_check.py checks the gate's arithmetic and, through --probe-buildgate, its answers with the machine's
own commit. What the builders do when refused is not exercised by a test.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 04:42:07 -05:00
chrg6
cba95f8d0a Record in shader-prepared.bin which seed it was prepared from, and prepare again at an update when the seed changed.
The prepared list named its shader variants and nothing about where they came from, so an update that keeps the prepared shaders (shader_carry) kept the old list whatever seed the new kit writes: variants only the new seed names, from a newer pipeline list or a change to which variants are prepared, were translated when play first drew them. The existing identities do not cover this: nx2navi_xlate.id and identities.txt name translators, which decide cache keys and what is carried over, not which variants are listed.

The list is now version 2 (runtime/prp_file.h): after the head come the sha256 of seed-programs.bin, the sha256 of seed-answers.bin and the translator identity, written by gfx_shader_prepare_seed; the three readers go through gfx_shader_prepared_read and still read a version 1 list. At an update step_shaders writes the seed first (prepare_shaders.py --no-run, about 4 s) and keeps the list only when its digests are that seed's (make_game.py seed_changed); another seed, or a list that does not say, is prepared again. Another translator alone still carries over: input ids do not depend on the translator, and preparing everything again for it is what carrying was made to avoid.

Checked with tests/prp_file_check.py (the C layout against make_game's reader), three launcher tests, and the exe's --prepare-shaders on a seed without records in a folder of its own: the digests it wrote are those of the seed files, one byte more in the seed reads as changed, and after preparing again it does not. A version 1 list of 42,142 variants and the same list as version 2 are both read whole by --probe-prewarm.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 04:36:33 -05:00
chrg6
534591be04 End a run whose GPU device is lost in one place: one report, no callbacks of unfinished submissions, and exit code 72.
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>
2026-11-11 04:35:30 -05:00
chrg6
871b06e2e7 Read a pipeline cache larger than 2 GB on Windows, and log every refused file with its size.
cache_load measured pipeline-cache.bin with ftell into a long, which is 32 bits on Windows: a file past 2 GB measured as -1, so the run started with an empty cache and the log said nothing, not even that the file was over pipeline_cache_max_mb. The read now lives in runtime/vk_cache_file.h: the length comes from the 64-bit tell, the read runs in 64 MB pieces, and a refusal returns its reason and the file's size, which cache_load logs. The Windows atomic write no longer cuts a block of 4 GB or more to a 32-bit count.

tests/pipeline_cache_file_check.py reads a sparse file just over 2 GB through the header. With --probe-prewarm and such a file carrying this GPU's header, the log reads "holds 2048 MB, over NX2NAVI_PIPELINE_CACHE_MAX_MB 512" under the default limit and "2048.0 MB read" with the limit off; the exe before this change logged nothing for the same file.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 04:29:31 -05:00
epicmeme546
4d5454d380 Add AMD FSR 4 upscaling through the libraries of AMD's FSR SDK 2.4, as an Upscaling choice beside FSR 3.1.
runtime/vk_ffx4.cpp loads amd_fidelityfx_loader_vk.dll from beside the executable and asks it which upscaler
versions the device can run. Where a 4.x version is listed (Radeon RX 6000, RX 7000 and RX 9000 cards with AMD's
driver 26.10.1 or newer) the menu's Upscaling row gains AMD FSR 4 Native AA, Quality, Balanced, Performance and
Ultra Performance, and fsr4 = 1 runs it in FSR 3.1's place with the same inputs. Its contexts are made with that
version asked for by its id and the provider is asked afterwards which version it runs, so no other upscaler can
be shown under the name. When a context or a dispatch fails, FSR 3.1 makes the frame, and after three failures
FSR 4 is off for the run. Each upscaler starts its history anew on the frame after a change from one to the other.
The setting is off by default and the Linux build compiles stubs.

On an RX 7900 XT (driver 26.10.1, 3440x1440, HDR) the library runs FSR 4.1.2: a context takes 54 ms to make and
217 MiB of video memory. Standing in Hyrule Field: Quality 47.8 fps where FSR 3.1 gives 51.5, Native AA 41.4.
Screenshots of 601 consecutive frames standing, 801 during a camera sweep, 701 each in Kakariko and the
graveyard, and 1,301 across changes between FSR 3.1 and FSR 4, their modes and off show no flicker that FSR 3.1
does not have, and it runs together with FSR 3.1 frame generation.

The SDK's API headers are in runtime/third_party/ffx_sdk, unmodified, with the licence. tools/make_kit.py and
tools/make_launcher.py take AMD's two signed libraries with --ffx4, and tools/make_game.py copies them beside the
game when the kit has both.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 03:22:08 -05:00
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
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
chrg6
861357b2a4 Make the direct mouse camera work on game 1.1.1.
mouse_mode = direct is the default, but on 1.1.1 it did nothing: only the 1.4.3 patch table had the sites, so the mouse always went through the stick emulation, which gives no turn at slow mouse speeds and tops out at about 107 degrees a second. The 1.1.1 table now has the set mouse_direct (on by default): at main+0x37c45c, where the camera's input update holds the processed stick in s1 and s0, a host call performs the displaced store and adds the raw mouse counts to both, so the mouse meets no dead zone and no curve; at main+0x37c4f0 a second call clears the idle bit for an update the mouse moved in. The counts are divided by the frames the game steps per update and by the stick camera speed, so the same hand movement turns the same angle at every frame rate. The Controls tab gets the mouse mode and mouse sensitivity rows, and mouse_mode = stick or NX2NAVI_MOUSE_DIRECT_PATCH=0 gives the old behaviour.

Checked here: the generator lifts the 1.1.1 game with the two sites (it compares each site's original word), the lifted game compiles and links with the runtime, and tests/mouse_direct_check.py passes its 1.1.1 harness (register contract, displaced store and load, equal turn at every frame rate and stick speed, the reset pause, capture and mode). Not checked here: the game was not run, so aiming with the mouse was not tried, and the differential tests against Unicorn were not run because no Python on this PC has Unicorn. The contributor measured 0.0800 degrees per count on both axes in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit bb8fecfbe43517006eafd7be9c22e8dbe9601de7)
2026-11-11 02:36:54 -05:00
chrg6
acc1ae25bf Merge co-op, off by default: rooms through a relay, progress sync between the players' own saves, and a Co-op tab.
Without the other players drawn in the world, which are unfinished. No relay address is built in, and with co-op off nothing opens a socket. Built on Windows and tested against a relay on this PC only; two games in one room and any real network have not been tried.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 00:40:20 -05:00
chrg6
8e19b653f5 Show the walk key's rows on the Controls tab again.
The walk key (walk_key, walk_scale) works and the settings example says the Controls tab sets both, but the tab had no row for either: the row's function was left without a caller, and a player asked for a walk key because he could not find one. Walk key and Walk scale are back under the analog stick rows, with the texts that were already there, and the guide names the setting. The second half of the contributor's change, a toggle mode, is not taken: it is a new feature that nobody has pressed a key against on Windows.

Built; the rows were not looked at in the menu, since no game could be started.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit 02fab940a1bb79d8867f5e764ee597ea8eb662e8)
2026-11-11 00:39:50 -05:00