73 Commits

Author SHA1 Message Date
chrg6
0131b1b263 Stop a launcher release and a build kit on an nx2navi_dlssg.dll that lacks an export runtime/dlssg/nx2_sl.h declares, before they download anything.
The review found that nothing on the release path ran the new export check: tools/make_launcher.py build_app and
tools/make_kit.py copied whatever --dlssg DLL they were given, and the release script still named the DLL of 0.8.0,
which lacks nx2sl_reflex2. Shipped that way, Reflex would never hold the frame limit while players still got this
branch's other changes. make_launcher.py check_dlssg and make_kit.py main now run build_dlssg.check on the --dlssg DLL
beside the translator's check, before anything is resolved, downloaded or deleted; build_dlssg.check also stops on a
missing file and on a file that is no DLL, with a message instead of a traceback. tests/dlssg_exports_check.py adds a
DLL with every declared export (it passes), the two-export DLL, a missing file and two files that are no DLL (they
stop, naming why), make_launcher.check_dlssg on both DLLs, and that both scripts' main makes the check before
streamline_pin.resolve and shutil.rmtree; it fails when the call is taken out of make_launcher.py main. The DLL built
from this branch passes make_launcher.check_dlssg; the DLL of 0.8.0 is refused, naming nx2sl_reflex2.
docs/launcher.md and docs/building.md "Checks" say so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:12:49 -05:00
chrg6
9f4f51f010 Fail tools/build_dlssg.py when nx2navi_dlssg.dll lacks an export nx2_sl.h declares, and let it check a DLL built earlier.
The runtime takes a missing optional export (nx2sl_reflex2, nx2sl_frames_max) for an older DLL and leaves the
feature out with one log line, so a release that ships a DLL built from older sources than its runtime loses Reflex's
frame limit without an error; the release script took the DLL of 0.8.0 from a fixed path. After the build,
build_dlssg.py now reads every PFN_nx2sl_<name> in runtime/dlssg/nx2_sl.h and the DLL's export directory (a small
PE reader of its own, PE32 and PE32+) and exits naming each export that is missing; --check <dll> makes the same
check without building. tests/dlssg_exports_check.py builds a DLL with two of three declared exports with MSYS2's
clang and checks that the third is named, and that nx2_sl.h's own declarations are read in full; docs/building.md
"Checks" and docs/launcher.md name both. The DLL built from this branch with the pinned Streamline 2.14.16 SDK passes
(15 exports); the one built before nx2sl_reflex2 fails, naming it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:12:49 -05:00
chrg6
814c10b355 Keep the Steam shortcut from starting the game while the launcher runs a job in the game folder.
The Steam shortcut's --play runs before the window's single-instance lock and checked only whether the game was running. Started while the window rebuilt the game, installed an update or built the mods' code, it ran mods.prepare into the folder the job was reading and started the old ocarina.exe, and the job then failed at the swap with "ocarina.exe is in use" (an update's install left the new launcher on the old build); during the shaders step, make_game.py's own run of the exe made the shortcut log "already running" and do nothing visible. play_only now tries the launcher folder's lock and lets go of it at once: when another process holds it and data/job-running.json names a job (jobs.running_job, which leaves the marker in place), it logs that the launcher is busy, says which job to wait for in a message box and does not start the game. A marker that a stopped launcher left does not stop it, since its lock went with its process, and neither does the window open without a job.

Not reproduced before the change in a game run: found by reading the code. Checks that passed: tests/launcher_test.py (216 tests, a new one for a held lock with the job's marker, a leftover marker with the lock free, and the window open without a job, which fails before the change) and tools/check_content.py --staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:30 -05:00
chrg6
8f42883a74 Tell the end of a game started from the Steam shortcut as Play tells it, with a message box for a fatal one.
play_only, the --play path of the Steam shortcut, logged only "the game ended (code N)" in launcher.log: it read no FATAL line, added no note to the game's log and showed nothing, so a game that ended with 70, 71, 72 or a Windows status left the player with no word of why (as when Steam starts it while the window replaces a save, and it ends at once with "another copy of the game is running"). The 7b0dbeb1 change that logs every end touched the window's watch only. play_only now follows the run with GameRun, the Restart's copies too, and takes the same game_end as the window: the line in launcher.log, the note in the game's log, and a message box only for an end worth one (a FATAL line, 70, 71 or 72, a Windows status; not 0, nor 1 from Task Manager's End task). The logs are written before the box, which waits for OK while Steam still counts the game as running.

Not reproduced before the change in a game run: the audit's probe showed it with a stand-in process. Checks that passed: tests/launcher_test.py (214 tests, a new one for the Steam shortcut's ends with codes 70, 0xC0000005, 0 and 1, which fails before the change) and tools/check_content.py --staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:30 -05:00
chrg6
8de99df6eb Explain a lost device's exit code 72 with the game's FATAL line, and follow the copy the game's own Restart starts for the code it really ends with.
The launcher read the game's FATAL line only after codes 70 and 71, so a run that ended through vk_device.c vk_device_lost_check (code 72, as in the AMD device losses of 2026-11-12) showed a bare "code 72" with no cause. 72 now joins 70 and 71 in FATAL_CODES and has a text of its own, which stands without a FATAL line because the loss's watchdog (vk_device.c lost_wait_exit) can end the run before that line is written.

The game's Restart (gfx_present.c gfx_window_closed) starts the exe again and ends the process the launcher started with code 0, and the launcher took that 0 as the run's code. A restarted copy that ended with 70, 71 or 72 was logged as an end with code 0 and its FATAL line was never shown; one that was killed or hung got a note saying code 0 in its log. Once the process it follows has ended with 0 while the exe still runs, the launcher now finds the copy by its image file (winsys.game_copy, compared as a file so that another install's game is not taken), holds a handle to it and takes its exit code; a further Restart is followed the same way. A copy it could not find ends with its code logged as not known. After code 0 or an unknown code, a FATAL line in a log written since the start can only be such a copy's, and it is shown as one; a log older than the start (a run that ended before it opened its own) is not read. A log cut at its 64 MiB cap (main.c log_sink) wrote its end lines to stderr only, and no longer gets a note saying it has no line of its own end. The end's handling is one function, game_end, for the Steam shortcut's play to share.

Not reproduced before the change in a game run: the audit's probes showed each of these ends with stand-in processes and logs. Checks that passed: tests/launcher_test.py (213 tests, new ones for code 72 with and without its FATAL line, the followed copy, a copy not found, a FATAL line after code 0 and a log older than the start, the cut log, and game_copy on a sleeper compiled with the ucrt64 clang) and tools/check_content.py --staged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:30 -05:00
chrg6
7b0dbeb18d Write the session's reports when the window loses the focus in play, more often in play, and log every end of the game in the launcher.
The session of 2026-11-13 ended 8 s after its window lost the focus, after 7 minutes of play, with none of the game's exit lines: no frame-time summary of its last period, no stutter map of its last minute and no growth ranking at all, as the ranking came every 10 minutes. When the window loses the focus in play, the present thread now writes the frame-time summary so far (starting a new period), the stutter map and the growth ranking at its next present, at most once a minute; formatting only, nothing asks the device. The growth ranking comes every 2 minutes of play instead of 10, the stutter map every 5 instead of 10.

A process that ends through exit() rather than plat_exit now writes the log thread's queue last, after the frame-time summary's exit handler, instead of losing it.

The launcher logged a game's end only for a nonzero code, so the session's end with code 0 left no line anywhere. It now logs every end of a game it started, and when the game's log has no line of the game's own end (the window's close, the game's ExitProcess, a timed run's end, a FATAL or exit line), it notes the end there too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 15:57:52 -05:00
chrg6
26d26babe5 Bring public main's bug capture on F8 and its removal of the Steam co-op into the 0.10.0 line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

# Conflicts:
#	docs/architecture.md
#	launcher/steamlib.py
#	launcher/ui/app.py
#	runtime/boot.c
#	runtime/cfg.c
#	runtime/flight.c
#	runtime/flight.h
#	runtime/gfx_present.c
#	tests/launcher_test.py
2026-11-13 09:52:43 -05:00
epicmeme546
f5da269add Remove the experimental co-op through Steam.
This takes back the three commits that added it (cd2f9f1, e60677e, a01910d): its runtime layer and Steam binding, the Co-op tab and what it drew over the game, the launcher's copy of Steam's library file, its settings, tests and docs. The tree is as it was before them, with the bug capture that came after kept: its note on screen no longer makes room for co-op's lines, and dbg_area, which the co-op had brought and the capture uses, stays.

The four settings of that co-op join the retired names, so a settings file that still has them logs no unknown key. A steam_api64.dll that the launcher put beside the game is left where it is: nothing loads it any more.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-13 09:08:40 -05:00
chrg6
da65aa6099 Bring public main's Steam co-op, the removal of the old co-op and the launcher's Steam library file into the 0.10.0 line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 04:37:01 -05:00
epicmeme546
e60677ed87 Have the launcher put Steam's library file beside the game, so no player fetches it for co-op.
Co-op loads steam_api64.dll from beside ocarina.exe, and the repository may not hold that file. Before Play and before a start from the Steam shortcut, when the file is not there, launcher/steamlib.py now copies one: one a release ships in its bin folder if it has one, else the newest one in the games of the player's own Steam libraries that Valve signed (WinVerifyTrust and the signer's name) and that has every call runtime/steam.c uses. Nothing is downloaded, the search takes a second or two once, and with no Steam or no suitable file the game starts as before and the Co-op tab says what is missing. The tab's two texts about the file say so in the four languages.

Checked: four new tests on made-up library folders (too old, unsigned, too deep, a second library, a shipped file first, none said once) and the launcher's 190 tests pass; on this PC the search found 40 files in 1.2 s, 25 suitable, and copied a Valve-signed one; that file's library version, an older one than the game was run with, signs in as app 480 and makes a lobby in a program without the game. Not run: Play in the launcher itself with this change.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-13 04:24:56 -05:00
chrg6
13e6563236 Build the prepared shaders into the driver's cache at an update only when a sample says the driver lost them.
After an update that keeps the prepared shaders, --warm-shaders built all 42,000 of them into the graphics driver's
cache again: 10 to 20 s at every update on a player's PC while the driver gave every one back at once, and the full
build when it no longer had them (380 s there once, after a change of what they are built with). It now builds twelve
fragment libraries spread over the list first, into the driver's cache alone, as the game's check at every start
does, and builds the whole list only when fewer than three quarters of them came from that cache.

On copies of an install's caches (RTX 5090): with the driver's cache partly cold after a relink, 5 of 12 came from it
and the whole list was built (313 s); the run after it found 12 of 12 and built nothing more, the step taking 3 s.
lib_warm = force builds the whole list whatever the sample says.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 03:40:21 -05:00
chrg6
e0870b87b4 Resume a part file only for the file it was begun for.
A part left by a download of other bytes under the same name was resumed, and the result failed its sha256 check
as if the download were damaged. A note beside the part now names the file it belongs to by the expected sha256;
a part of another file is deleted with a log line and the download starts over, and the same file is still resumed
from another server.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:34:22 -05:00
chrg6
9dcd73eebc Refuse a mod archive that holds a name Windows cannot keep.
A file called after a device, ending in a dot or a space, or holding a colon was unpacked into the device, under
another name or as a hidden stream, and the game missed it without a word. On Windows the archive is now refused
with the file's name and the reason; elsewhere such names pass as before.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:34:22 -05:00
chrg6
8c32aedabb Refuse a game folder whose path leaves no room for the game's files.
Windows and the build tools stop at 260 characters for a file's whole path, and a built game needs up to 128 below
its folder. A deeper folder made the build stop in the middle with a file "not found"; Begin Installation and the
install job now refuse it first and name both lengths.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:34:22 -05:00
chrg6
0a85e33e0f Hold the game's profile lock while an import replaces the save.
The window asked whether the game runs before the player picked the save, so the game could start during the dialog
or during the backup's copy and mount a save that was half replaced. The import now takes profile/instance.lock as
the game does: a running game refuses it, and a game started in the middle is turned away by its own lock.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:34:22 -05:00
chrg6
d0e6cd695e Ask the disk for room before a download or an unpack starts.
A disk that filled in the middle ended a download after four tries with an error that named no cause, and an unpack
with half a folder. The rest of a download, the build tools with their unpacked size, an update's archive and a
mod's zip are now measured against the free space first, and the refusal says how much is needed and how much is
free; a disk that fills during a download is named as full at once.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:34:22 -05:00
chrg6
cc0e2c4bf4 Say at the next start which job the launcher was stopped in.
A job that writes into the game folder leaves data/job-running.json while it runs and removes it when it ends. A
launcher closed or killed in the middle leaves it, and the next start logs it and tells the player which job did not
finish and that Rebuild the Game or Begin Installation goes on from the steps that were done.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:34:22 -05:00
chrg6
2a7fda9506 Put a picked mod together beside the mods folder and move it in once it is complete.
Add a Mod copied straight into the mods folder, so a copy that stopped halfway left a half mod there, switched on
at the game's next start. It now goes into a folder beside the mods folder and moves in with one rename; a failed or
interrupted copy removes what it made.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:34:22 -05:00
chrg6
3ca1541e0e Let one process work in a launcher folder at a time, and one job on the same thing.
The window already locked data/launcher.lock, but a second one closed without a word, the command line did not take
the lock, and nothing kept two jobs apart. The lock is now launcher/locks.py: the window, every command but check and
every job that changes files hold it, the one refused says who holds it, and a job names what it changes so that a
second one on the same thing fails before its first step.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:34:22 -05:00
chrg6
0b974d04f1 Carry an execution profile in a launcher release.
make_launcher.py --pgo FILE puts the profile and its record into app/<version>/data, where make_game.py looks for it,
before the prebuilt runtime is built, so the runtime library is compiled with it. The profile is part of the app zip
the update feed serves, so the build after an update compiles with it. The release stops before it downloads or
builds anything when the record names another recomp/ than the checkout's, and after the runtime library when the
release's compiler, the one players build with, left the profile out.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:30:46 -05:00
chrg6
ef3466ce4f Merge main into the 0.9.1 release: the launcher's Mods screen, the anti-aliasing choices beside FSR and DLSS, and the scene's last row and column filled before an upscaler reads it.
The 0.9.1 notes keep the release branch's text and take main's sections: New, the edge line under Fixed, and Changed.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 09:00:37 -05:00
epicmeme546
89c453d7bb Add a Mods screen to the launcher and build the mods' code patches into the launcher's game.
Mods, on the home screen, opens the game's mods folder, adds a downloaded mod from its .zip, .7z or .rar file or its
folder, switches each mod on or off, and sets the load order (mods_disabled and mods_order in nx2navi.ini, written as
the game writes them). Each mod's description says what it changes, which other mod changes the same files and which
of the two wins, and why the game leaves it off when it does.

Before the game starts, Play puts the mods folder in order (launcher/mods.py prepare): an archive in it is unpacked into
a folder of its name and kept, a mod whose files sit a folder or more down has them moved up, and a romfs, exefs or
cheats folder put straight into the mods folder becomes a mod of its own. An archive that would write outside its
folder is refused, and nothing is deleted.

The launcher's build (tools/make_game.py) left the mods' code cheats and ExeFS patch files out, so those mods stayed
off. The lift now turns them into patch sets first, as the setup does, with the files in the lift's key and
build-cache/mod-code.json recording what it took up. Play builds the game again before it starts when an enabled mod
brings code no build has seen. The shaders' key names the lift and the compile without the mods' code, so such a build
compiles only what the patches change and leaves the shaders prepared. tools/mod_files.py is the one place the tools,
the build and the launcher find a mod's parts.

Tested: a zipped mod holding its own folder, put into a mods folder, was unpacked and moved up by the launcher's code,
and the game served its file in place of the game's. The lifter took a code cheat and a pchtxt patch made for the game's
main as patch sets. The launcher's and the setup's tests pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 08:11:57 -05:00
chrg6
56e142169c Merge main into the 0.9.1 release: the launcher's animations stopped behind another window, the uniform snapshot's copy split over the replay workers, and the replay workers' default of three with its measurements.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 07:29:41 -05:00
chrg6
d2ab9a00be Check at startup that the launcher's Python can read zstd files, and say how to fix it when it cannot.
Run from a checkout, the launcher uses the Python that started it. A Python older than 3.14 without the zstandard
package got through the first-run screen and stopped at Gather Build Tools with only "No module named 'zstandard'",
after the packages were downloaded. toolchain.zstd_missing now looks for compression.zstd or zstandard when the
launcher starts. Without either, the log and the first screen name the Python and give the pip line for
tools/requirements-launcher.txt, and Gather Build Tools stops with the same message before it unpacks anything. A
release's own Python always has the package; if it ever lacks it, the message says to reinstall NX2navi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 07:18:10 -05:00
epicmeme546
bd94adc9ec Stop the launcher's animations while another window is in front, so a launcher left open behind the game no longer draws every 15 ms.
With Hide Launcher While Playing off, the launcher stayed in its normal state behind the game and kept stepping its
animations: about 0.6 of a CPU core went to a window nobody saw, beside the game's render thread. The animations now
also stand still while the window is not in front, the same check the gamepad polling uses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 07:09:55 -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
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
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
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
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
4af930d0c2 Ask for the disk space the build really needs, and say how long an install really takes.
The launcher asked for 31 GB where the build's own check now asks for about 34, so a disk between the two passed the first check and was refused seconds later. Both say 34 GB now. The build no longer asks for the whole shader preparation's space when the game folder already holds translations. The texts said 10 to 25 minutes for an install that took players 27 to 41, and 5 GB for a build cache that measures 1.6. A build error's message goes into the launcher's log as one line.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:12:52 -05:00
chrg6
92e3cc0adb Merge the launcher fixes: updates that stop keep the game, honest progress and errors, and a faster file check.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:08:15 -05:00
chrg6
e36305d346 List the launcher's new tests in its guide.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:02:26 -05:00
chrg6
62a02da381 Unpack the texture archive in pieces when drawing the button prompts.
pad_prompts.py gave each packed game file to the decompressor in one
call, which returns one bytes object grown by copying. On made-up data
of the size of the interface texture archive, 69 MB packed and 244 MB
unpacked, that takes 21 s on a fast PC; players' builds waited 44 to 52 s
before the step's first folder, most of its minute. In 64 KiB pieces
joined once the same bytes take 0.3 s.

The prompts step's key held the digest of the three scripts' bytes, so
this edit would have drawn every install's prompts again, as two of the
last four releases did. It now holds a constant changed by hand, whose
value is the scripts' digest in 0.7.0 and 0.7.1, and a test fails when
the scripts' logic changes, to ask whether the constant must change.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 16:00:37 -05:00
chrg6
e678e1f38e Check the disk space and the build tools before the long steps.
Only the RomFS image's space was looked at, and only when that step
began: a disk that held the image but not the build cache or the shaders
stopped the build in the compile or, half an hour in, in the shader
preparation. The build now adds up what the steps it will run write into
the game folder (the image and 1 GiB, 2 GiB for the build cache, 4 GiB
for a full shader preparation, the least each is known to need) and
stops at once when the disk has less, naming each part and how much is
missing.

On Windows a build whose compile step will run first compiles and links
an empty program with the installed tools, against the libraries the
game links. Tools that are incomplete or blocked stopped the build only
in its compile step, after the image and the lift; the message says what
failed and how to get the tools back.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:55:26 -05:00
chrg6
856944002e Hold repeated update checks back, and say when GitHub takes them again.
Every selection of the Updates row started a request to GitHub, which
allows an address 60 an hour: a player who selected it 25 times in 90
seconds had every check of the next hour refused. The row and F5 now
start no check within a minute of the last one.

A refused check said "try again in an hour" whatever the answer held. It
now names the time from the answer's X-RateLimit-Reset or Retry-After.
A failed check wrote two tracebacks, 22 lines, into launcher.log each
time; a FeedError is now one line with its message and its cause.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:36:20 -05:00
chrg6
c02b62826d Cap the default number of compiles by the free memory.
make_game.py ran one compile per logical processor whatever the memory.
Without --jobs it now runs as many as 80 % of the free memory holds at
768 MiB a compile, the peak build_game.py records for one, never more
than the processors and never fewer than one, on Windows and Linux alike.
The build's first lines say the number and what decided it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:31:52 -05:00
chrg6
18a86dd652 Show what the check of the game files found.
The check printed its counts into the build's lines only, so a player
whose build ended with "4 damaged, 3 given a stand-in, 1 left as they
are" saw nothing of it. A job now reads romfs-check.json after the build:
the count goes into the step's row, and when the check ran in that job, a
sentence goes onto the home screen and into launcher.log.

The same four files are flagged on every copy of the game, and three of
them get a stand-in. For those the sentence says so and that nothing
needs doing; other files are counted and romfs-check.json is named.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:27:55 -05:00
chrg6
752c172ccf Say how long an update really takes, and fill the warm-up's bar from zero.
The Updates row promised that an update rebuilds the game "in a few
minutes". The code cannot keep that: a version that changes the lifter
compiles the game's code again, and one that changes what the shaders are
built with builds all of them into the driver's cache again. Players'
updates took 24, 34 and 45 minutes. The row now says it takes a few
minutes when little changed and half an hour or more otherwise.

The warm-up of the driver's cache after an update only builds, but its
count was drawn in the second half of the bar as in a full preparation,
so a 20 minute run read 50 % in its first second. Its count now fills the
whole bar, and the row says so while the shader cache is packed before it.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:22:48 -05:00
chrg6
31256a5015 Merge branch 'claude/pr-intake-4-947f9e' into test-all-fixes 2026-11-10 15:22:06 -05:00
chrg6
e69a84fe59 Roll a version that cannot start back without waiting for a click.
start.pyw showed the import failure in a message box before it ended with
code 86, and the stub waits six seconds for that code: unless the player
closed the box in time, the broken version stayed the current one. When
app/previous.txt names another complete release, the failure now goes to
crash.log only and the process ends at once, so the stub rolls back and
the previous version says on its home screen which version did not start.
The box stays when there is nothing to go back to, when Steam started the
launcher without the stub, and when the stub's wait is already over.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:13:40 -05:00
chrg6
88495c296c Keep the game installed when an update stops in the compile.
An update whose lift ran again drops the compile step's record before the
compile runs, so a compile that then stopped left state.json without it,
build.built_version said '' and the launcher's next start showed the
first-run screen and asked for the dump. build.installed now also counts a
folder that holds the exe of the build before and game files a rebuild can
start from: the window opens on home, where Rebuild the Game reads "Not
Finished" and Play says which build it starts, and an update's rebuild and
the command line's go on with the game. Begin Installation into a folder
whose game files are already unpacked builds on from them instead of
reading the dump again.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:11:06 -05:00
chrg6
006bdfa4b2 Let the launcher's bug report export write the game's log and settings when the game has no save yet.
A game that stops before its first save is the one its player most wants to report, and Export Save refused it: "the game has no save yet". With Add Log and Settings on (the default) the launcher now writes NX2navi-log-<date>_<time>-bug-report.zip with the game's latest log, nx2navi.ini and a manifest that counts no save files, and says so on the Saves screen. Without a log there is still nothing to export, an export without the log and settings still needs a save, no account is made for the export, and an import refuses such a zip as one without a save.

This is the first of the two faults pull request #364 names, written anew inside the existing export; its Export Diagnostics screen and its separate crash log are not taken.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 14:52:13 -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
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
2a9efee4c5 Merge GitHub main into the test line: the launcher's customisation, the settings and sound changes, and the warm stage-library cache.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 03:32:30 -05:00
epicmeme546
ddc4d92812 Give the launcher's colour schemes plain names.
The six schemes were named after places in the game. They are now Forest, Volcano, Lake, Desert, Temple and Shadow, in the same order and colours, and their ids in launcher.json follow (forest is the default). A launcher.json that names one of the old ids gets the default scheme, as with any name it does not know.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 02:05:21 -05:00
epicmeme546
5f4bbd2084 Play the launcher's sounds at Low by default.
A new launcher.json starts with sounds at low instead of off, and a value the file cannot hold falls back to low too (launcher/ui/sound.py DEFAULT_LEVEL). The player can still pick Off, Medium or High in Customize Launcher. The sounds are still only the game's select and back sounds, read from the player's own game.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 02:02:45 -05:00