58 Commits

Author SHA1 Message Date
chrg6
80b96a748d Describe RivaTuner Statistics Server's hand-off, the pacer's fallbacks and rtss-profile.txt as they behave now.
docs/architecture.md said the pacer holds nothing while RTSS holds the limit, through movies and minimised windows,
that the pacer stands aside once RTSS lists the process, that RTSS fails at 1.2 times the limit over six polls, and
that a killed run leaves its limit for the next run to take as the player's. The one-limiter and RTSS paragraphs now
name the pacer's fallbacks (before the first host present, a movie, 200 ms with no host present, a limiter seen
letting frames through, any failure) and its 240 ceiling beside RTSS, the settle rule, a movie's profile, the proof
RTSS must give before the pacer stands aside, the 1.05 rule over three new periods, the rounding down, and the state
file with rtss_limit_crash. nx2navi.ini.example's rtss lines, README.md's frame generation item, docs/building.md's
rtss_limit_check and frame_step_check paragraphs and rtss_limit.c's header follow, and the comments at main.c
crash_handler's and dispatch.c fatal_end's rtss_limit_crash say when RTSS's library is still called there (no line in
the state file, never on RTSS's own thread). No code changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:12:26 -05:00
chrg6
04f4d44747 Keep RivaTuner Statistics Server's profile as it was in rtss-profile.txt, so a start after a kill or crash puts it back, and leave it to that start at a crash.
An exit that skipped rtss_limit_restore (Task Manager, a hang, lost_wait_exit, a kill during the pipeline save) left
the game's limit in Profiles\<exe>.cfg, and the next run's first apply took it for the player's own: that run then
put the stale limit back at its close, and a profile the game had made stayed for good (lane A's probe: kill1, then
kill2 'held a limit of 60/1'). The crash handler and the fatal error's end also called into RTSSHooks64.dll from a
process that had failed (P-13).

rtss_limit.c now keeps one line per profile in rtss-profile.txt next to the executable (rtss_decide.h RtssSaved:
whether the profile existed, its old limit and denominator, the value the run wrote), written atomically before the
run's first change of the profile, updated with each value written, and removed once the profile is back (put_back;
also when the first write did not stick). On the first poll with the hooks library a start that finds a line of its
profile puts the profile back first (restore_left, rtss_restore_plan): the old limit, or no profile, unless the player
changed or removed it since, which the written value tells. main.c crash_handler and dispatch.c fatal_end call the
new rtss_limit_crash, which closes the run and leaves the profile to that start with no call into RTSS's library; only
when the file could not be written does it put the profile back as before, and never from the worker's own thread. A
file that cannot be written leaves the run as it was, with one line in the log. Profiles frozen by earlier versions
cannot be told from the player's own.

Not reproduced before the change. rtss_limit_check (T12: the line's format and parse, the file's rewrite, each plan;
the static check that both crash paths go through rtss_limit_crash), rtss_worker_check (state_record, state_player,
state_unwritable, crash_recorded, and kill_restart with its player and edited variants, each a killed run and the next
start in processes of their own), fatal_exit_check and exit_order_check pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:12:25 -05:00
chrg6
66c607414b End a hung fatal error's report with its own code when a thread waiting beside it gives up, not with the crash code.
The review of the shared failure guard found that a thread parked in dispatch.c rt_ending_enter ended the process
with 71 after 30 s even when the first report was a FATAL (70, or 72 for a lost device), so a fatal error whose report
hung read as a crash to the launcher.

rt_ending_enter now takes the code its caller ends with: fatal_end passes its own, main.c crash_handler 71. The first
caller stores it next to ending_tid, and a waiting thread that gives up after 30 s ends the process with it. Before
the store the value is 71, as before.

Reproduced before the change with a one-off build of tests/fatal_exit_check.c given a fatal note that never returns
and a fault on another thread: the process ended with 71 after 30.5 s; with this change it ends with 70 after 30.5 s.
That case takes 30 s, so the matrix gets a static check instead: tests/fatal_exit_check.py holds rt_ending_enter to
storing the first caller's code and ending with it, fatal_end to passing its code, and crash_handler to its first
line `if (!rt_ending_enter(71)) plat_exit(71);`. fatal_exit_check and the compile check of every runtime file (a
header changed) pass, with no new -Wall warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:24 -05:00
chrg6
313ef09081 Open the log's end reserve only when the process is ending, not at a bug capture's log sync in play.
The review of the log tail change found that main.c log_sink opened the 4 MiB reserve past the cap on g_log_final as
well as on g_log_ending. bugcap.c write_log_tail calls rt_log_sync in play, which sets g_log_final, so after a bug
capture in a trace = perf session past the cap, ordinary play lines filled the reserve (about 6 minutes at 12 KB/s),
the end's lines were dropped again, and the "end of the run follows" line came mid-run.

The reserve is now keyed on g_log_ending alone, as E-10's design had it. The ends that relied on rt_log_sync now call
rt_log_ending first: dispatch.c fatal_end and main.c crash_handler right after their guard, vk_device.c
lost_wait_exit's watchdog before its sync, and plat_exit through the new main.c log_sync_ending (plat_exit_hook in a
game run), so every path that ends through plat_exit marks the end too. rt_exit_begin and perf_summary_flush call it
as before.

Not reproduced in the game before the change. tests/log_cap_check.py gains a capture case (rt_log_sync in play, 1000
more play lines, then the end): on the code before this change 58 play lines took the reserve, the end line came at
the sync and no end line reached the file; now no play line passes the cap and the end's lines fill the reserve. A
fatal case runs rt_log_ending and then rt_log_sync, and static checks hold crash_handler, log_sync_ending, fatal_end
and lost_wait_exit to calling rt_log_ending before rt_log_sync. log_cap_check, fatal_exit_check (with a stand-in for
rt_log_ending) and the compile checks of main.c, dispatch.c and vk_device.c pass, with no new -Wall warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:23 -05:00
chrg6
b23acff301 Leave a line in the log when a module ends the process with ExitProcess, and write the gathered lines at a focus loss.
Two runs of 2026-11-13 (the 0.10.0 fresh install's nx2navi.1.log, 7,974,927 bytes, and the 0.9.2 test's
nx2navi.3.log) ended seconds after "window lost the focus" with exit code 0, no WER event and no line of their own:
an ExitProcess or exit() called by a module in the process and a kill from outside could not be told apart, and
both lose the up to 250 ms of lines the log thread was gathering. A direct ExitProcess skips perf.c's atexit sync.

(a) ExitProcess stops every other thread and then calls each module's detach, the executable's TLS callbacks
included; TerminateProcess calls none. win32.c exit_note_tls, in the .CRT$XLB list beside _tls_used, writes on
DLL_PROCESS_DETACH what the log queue holds (logq.c logq_peek: the bytes as they stand, no lock) and one line,
"exit: the process is ending through ExitProcess, not one of the game's own ends", with the return addresses that
led there. It runs under the loader lock with the other threads gone, so it uses no C runtime (hex written by hand)
and does not ask the loader: the addresses are named from a module list noted at start and at each focus loss
(plat_exit_note_modules: EnumProcessModules, two tables and a writers' lock the callback never takes), and
RtlCaptureStackBackTrace and WriteFile take no lock of the process. plat_exit, plat_terminate and plat_reexec set
the orderly flag (plat_exit_orderly), and main does once main_run has returned, so a tool mode's exit() leaves no
line; a library's exit() during a game run does, naming ucrtbase and its caller. main.c log_open sets it up for a
game run that writes a log file. Linux has empty stand-ins.

(b) gfx_window_focus_changed calls the new main.c rt_log_focus_lost at a focus loss: logq_nudge makes the log thread
write what it gathers at once (a flag log_main's gathering loop checks) and the module list is noted again.

Not reproduced before the change (the two ends are unexplained). The new tests/exit_tls_check.py links win32.c and
logq.c with a queued line still being gathered: ExitProcess from the program and exit() write the line and the note
naming exit_tls_check.exe; TerminateProcess, plat_exit and a return after plat_exit_orderly write no note.
tests/logq_check.c gains a nudge case, which fails with the flag left unset (252 ms) and passes with it.
thread_end_check, fatal_exit_check, crash_stack_check, exit_order_check, save_share_check and the compile checks of
every runtime file pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:23 -05:00
chrg6
dd78132e6d Tell a crash on the RTSS thread as the game's own call of RivaTuner's library, and skip the restore there.
main.c foreign_hook matches "hook", so a crash in RTSSHooks64.dll got the line that the library is not part of the
game and that a recording, overlay or capture program put it there, also when the game's own thread that sets the
frame limit (rtss_limit.c's "rtss" worker) had loaded and called it. The one RTSS crash on record (issue #231)
faulted in ntdll on that thread, where no note applied. And the crash handler then called rtss_limit_restore from
that thread, whose own lock it holds (SRW locks are not recursive), so the restore's trylocks spent a second.

The platform layer keeps the name a thread was made with in the thread itself (plat_thread_name, copied at its
start: a detached thread's struct may go before it ends). main.c crash_module_note picks the note: on the "rtss"
thread, whatever the module, the line says the fault is in the game's calls of RTSS's library or around them and
that rtss = 0 keeps the game from calling it; on other threads RTSSHooks64.dll stays foreign, as it is RTSS's
overlay there. The restore is skipped on the rtss thread. rtss_limit.c is unchanged: the thread is known by the name
it is spawned with. Crash text only, besides the skipped restore.

Not reproduced before the change. tests/foreign_hook_check.py checks the note's choice (RTSSHooks64.dll and ntdll on
the RTSS thread: the RTSS note; RTSSHooks64.dll elsewhere: foreign; a driver: none), and tests/crash_stack_check.py
that the crash report reads the faulting thread's name. They, thread_end_check, fatal_exit_check and the compile
checks of every runtime file pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:23 -05:00
chrg6
4a36e96411 Name Vulkan layers called VkLayer_*, a recorder's Vulkan hook and a driver's capture tool as foreign in the crash report.
main.c foreign_hook matched "vk_layer", "hook", "overlay" and "reshade" in the lower-cased module name, so a crash
inside a layer named in the usual VkLayer_<name>.dll way (lower-cased "vklayer_") or inside obs-vulkan64.dll or
nvspcap64.dll lacked the line that the module is not part of the game (the audit's probe on these names).

It also matches "vklayer", "obs-vulkan" and "nvspcap" now, and leaves out "vklayer_khronos": the validation layer is
one the game loads itself when NX2NAVI_VK_VALIDATION asks, and its crash should not tell a developer to close a
recording program. Crash text only.

Reproduced before the change from the probe's names; the new tests/foreign_hook_check.py cuts foreign_hook out of
main.c and checks nine foreign names and ten of the game's own and the drivers' (the validation layer among them).
It and the compile checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:23 -05:00
chrg6
b4556957c2 Add a hang watch that logs a report when no frame is shown with the window in front, or the GPU does not finish, for 20 s.
A present, a submission or a completion that never came back (a deadlock, a driver wait that does not return, a
GPU hang without a reset) left a log that just stopped: kernel.c wait_watch covers guest lock and thread-end waits
only and is off by default, and nothing watched the present, submit or completion threads.

hang_watch.c runs a thread of its own in a game run (main.c starts it before the boot; never in the tool modes).
Once a second it reads, without a lock, gfx_counters.frames and the submissions issued and done (the new
vk_sync.c vk_sync_progress, no device call). It reports a stall of the frames while the guest runs, the window is
in front and not minimized (gfx_window_in_front) and no orderly end has begun (rt_exit_started): the game stops
presenting without the focus, so a focus change starts the count again, and the boot screen is not judged. It
reports submissions issued and not done, "done" unchanged for N s, whatever the focus. One report per stall, a
new stall needs progress, at most 3 a run. A report names the stall with the counts, the focus and the world
clock; who holds the present, queue and window-start locks (gfx_present.c gfx_lock_holders, read from
PlatRMutex.owner and depth); the guest threads' lock and thread-end waits (kernel.c kern_wait_report, wait_watch's
walk factored out as wait_pass, run with limit 0 under ten 10 ms trylocks of the kernel lock); and then writes the
log out with logq_flush, never rt_log_sync, which would put every later line on the thread that logs it in a run
that may recover. It never ends the process and adds no wait to a drawing thread. The per-thread wait sites and
lock owner wrappers are left for a later change.

NX2NAVI_HANG_WATCH_S sets the seconds (default 20, 0..600, read at start, 0 off): a metas row, a known[] name, a
diagnostics sections[] entry and an nx2navi.ini.example entry.

Not reproduced before the change (a diagnostic). The new tests/hang_watch_check.py drives hang_watch.c's decisions
on made-up readings: no report while things move, unfocused, minimized or ending; one per stall of the frames with
the window in front and one again after progress; one for submissions not done whatever the focus; none for an
idle GPU; at most 3; logq_flush called and logq_sync never. tests/cfg_rows_check.py (0 problems), the scratchpad
rows check ("missing []") and the compile checks of every runtime file pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:42:34 -05:00
chrg6
bbea3ce2a0 Write the end of the run to the log file past its 64 MiB cap, up to a 4 MiB reserve counted on its own.
main.c log_sink wrote nothing to the file once g_log_bytes passed LOG_CAP: every later line, the exit summary,
"frametime: exit", the stutter map's exit lines and FATAL and crash lines included, went to stderr only, which a
game started from the launcher or Explorer does not keep. A trace = perf session reaches the cap in about 90
minutes (the fresh 0.10.0 log grew 12 KB/s), and the play-test asks for an hour or more of exactly that. A reserve
compared against g_log_bytes would not help: that counter adds every line, dropped ones too, so 4 MiB of dropped
lines (under 6 minutes at that rate) would use it up before the end came.

The new rt_log_ending marks the process as ending; rt_exit_begin (the orderly ends) and perf_summary_flush (the
frame-time summary, also on the cheats rebuild exit) call it, and rt_log_sync (a fatal error, the crash report)
counts as ending too. The exit summary and the stutter map are written before rt_log_sync, so keying on
g_log_final alone would have missed them. While ending, a line past the cap goes to the file through
g_log_tail, a counter of the bytes written past the cap, up to LOG_END_RESERVE (4 MiB), after one line saying the
end of the run follows. The cut line says the end will still come. The file can end 4 MiB past the cap.

perf_summary_flush now calls rt_log_ending before ft_stop, so tests/exit_order_check.py's static check of it wants
ft_stop before ft_report rather than first. The check also prints one closing line for the static checks and the
program together: the program's own "ok" line had hidden a failing static check.

Not reproduced before the change (no log here reached the cap). The new tests/log_cap_check.py builds log_sink
with a 4 KiB cap and a 2 KiB reserve: past the cap ordinary lines are dropped with one cut line, and after
rt_log_ending or rt_log_sync the end's lines fill the reserve after one "end follows" line, however much was
dropped before. exit_order_check (rt_exit_begin marks the end once; exit code 0 at this commit), logq_check and the
compile checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:42:10 -05:00
chrg6
87fd385b7b Stop HD rumble at a fatal error and a crash too, on a thread of its own the end waits for 300 ms at most.
input_rumble_hd_stop ran only in the orderly ends, so a FATAL or a crash while a controller vibrated on HD rumble
left it buzzing: nothing renews or stops the value after the process is gone (input.c says so). It also took
hd_lock with a blocking lock while it only tried the other two.

dispatch.c rt_rumble_stop_bounded runs input_rumble_hd_stop (weak in dispatch.c, as fs_save_crash_wait is, for
builds without input.c) on a new "rumble stop" thread and waits at most 300 ms for it, never joining it: the stop
goes through SDL, which takes its own joystick lock, and a crash on SDL's thread while it held that lock would
otherwise keep the ending thread there and the process would never end. fatal_end and main.c crash_handler call
it right after rtss_limit_restore. input.c tries hd_lock 50 times 1 ms apart, as it does `lock` and sdl_lock, and
skips the stop when it is not taken.

Not reproduced before the change. tests/fatal_exit_check.py (now linking dispatch.c as its own unit: the stop runs
at a fatal error; a stop that hangs leaves the end at code 70 after 0.3 s; statically both failed ends call the
bounded stop after the RTSS restore), tests/rumble_hd_check.py and the compile checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:42:10 -05:00
chrg6
722a5dd1b0 Keep 32 KiB of every thread's and fiber's stack back for a stack overflow's crash report.
A stack overflow leaves the faulting thread only a guard page's worth of stack for the handler, and the crash
report needs more: main.c crash_handler's rt_log buffers are 2 KiB each, it walks the frames, and while the boot
builds pipelines its first call writes a file (plat_file_write's frame is about 4 KiB). Nothing in the runtime set
a stack guarantee, so the report faulted again; the verifier's probe ended with 0xC0000005 and no line once the
report's first call was the file write. The launcher starts the game with stderr on DEVNULL, so raw_note's stderr
line was all there was, and nobody saw it.

The new platform plat_stack_guarantee calls SetThreadStackGuarantee with PLAT_STACK_GUARANTEE (32 KiB) and runs
at the start of every thread win32.c thread_main starts, in fiber_start (a guest fiber's stack is its own and the
guarantee belongs to the running fiber), and on the main thread in main. Each thread and fiber commits that much
more of its stack. Linux's crash handling already runs on its signal stack (linux.c alt_stack), so it is empty
there.

Reproduced before the change by the new tests/crash_stack_check.py: a plat_thread_spawn thread and a plat_fiber_new
fiber recurse until their stacks run out, and the handler writes a file and three 2 KiB log lines. Without the
guarantee both ended with 0xC0000005 and no line; with it both write every line and end with 71.
thread_end_check, fatal_exit_check and the compile checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:42:10 -05:00
chrg6
1352b14f64 Let the first failure report end the run whole when another thread crashes or hits a fatal error beside it.
The platform layer calls the crash handler on every thread that faults, without waiting for the others
(win32.c crash_veh, crash_filter), and main.c crash_handler ended any second entry with plat_exit(71) at once.
So a second fault during the first report cut it after its first lines, together with its wait for a save Commit
(fs_save_crash_wait) and RivaTuner's restore; the audit's probe showed it in 3 of 3 runs. fatal_end had no guard
at all and none shared with the crash report: a crash during a FATAL's end wrote a whole crash report whose
"FATAL: access violation" became the log's last FATAL line (the one the launcher shows) and replaced code 70 with
71, and a FATAL during a crash report cut it with code 70.

dispatch.c rt_ending_enter is the one guard for both ends: the first caller reports and ends the process; the
same thread again (a failure inside its own report) ends at once with its own code (crash_handler keeps
plat_exit(71), fatal_end plat_terminate(code)); another thread waits in 10 ms sleeps, logging nothing since it
may hold the log queue's lock, until the process ends, and after 30 s ends it with 71 should the first report
hang. fatal_end calls it first; crash_handler calls it in place of its own busy flag.

Reproduced before the change by tests/fatal_exit_check.py's new cases, which failed on the old guards: two
faults at once (report lines 2 and 3 missing), a fault during rt_fatal (a crash report after the FATAL) and
rt_fatal during a crash report (exit 70, the report cut). They pass now, with the earlier cases and the static
check that crash_handler starts with the shared guard; the compile checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:42:10 -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
e682934bd0 Write the log file before stderr, and keep a blocked console from stopping the log and the exit.
A selection in the console window of a game started directly held back the write to stderr, and with it the log
file, the exit hook and the crash report. The log file now comes first, stderr goes past the C runtime's stream lock,
and the lines of an exit, a fatal error or a crash skip stderr while another thread is stuck in it. A game run turns
the console's QuickEdit mode off, and a closed console, Ctrl+C or Ctrl+Break ends the game the way a closed window
does, with the settings and saves written and the exit logged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 06:06:27 -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
cd2f9f12be Add an experimental co-op through Steam: rooms, invites, names in the world, chat and shared progress.
Up to four players, each in their own game and save, meet in a room. Rooms are Steam lobbies and the games talk through Steam's networking, so nobody forwards a port and nobody runs a server. The game signs in to Steam as its test app (480), so Steam shows the player as playing Spacewar. Off by default; the menu has a Co-op tab with Create a room, Join a room by its ID, Invite a friend, the players, chat and Leave, in English, Russian, Brazilian Portuguese and Simplified Chinese.

runtime/steam.c loads steam_api64.dll from beside the exe at run time and declares the C functions it calls itself; nothing of the Steamworks SDK is in the repository, and a player copies the file from a Steam game they have. runtime/coop.c is the room: the lobby's owner is the host, lets games of the same protocol and game build in and lists them, and every game takes positions, chat and progress only from the players on that list. runtime/coop_game.c reads Link from the Player actor and shares progress, items, songs and quest items through the game's own flag routines, each value only ever raised; small keys, rupees, ammunition and hearts stay each player's own, and the save is copied to coop-backup before anything of a room reaches it. The other players show as a name with a distance where their Link stands, projected with the upscaler's camera.

One Steam account cannot be in a lobby twice, and this PC has not the memory for two games, so the room and the game side were run with one game and tests/coop_peer.py as the second player over runtime/coop_local.c, a stand-in for Steam on 127.0.0.1 that only the environment turns on: the peer was let in, saw the game's Link in Hyrule Field, chat went both ways, the save was copied, one flag the save lacked was set and a flag the game does not share was refused. On Steam itself one game signed in as app 480, made a room, showed its ID and the player's Steam name on the Co-op tab and left. Not run: two Steam accounts in one room, an invite, a join by room ID over Steam, a host that leaves, a removal, and the menu's text probe on this build.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-13 04:16:17 -05:00
epicmeme546
c84c035939 Remove the co-op of 0.8.0: its runtime layer, relay, menu tab, settings, tests and docs.
The hidden beta shared story progress through a relay a player had to run and never showed the other players. A new co-op replaces it. Gone are runtime/coop*.c and their headers, the menu's Co-op tab and its texts in four languages, tools/coop_relay.py, the co-op tests, docs/coop.md, the [coop] section of nx2navi.ini.example, the --probe-coop mode, the coop trace item, and Monocypher, which only this layer used.

The old settings stay known as retired keys, so an nx2navi.ini that still sets them logs no unknown key, and they stay on the list of private settings, so a bug report keeps a room's secret, a relay's address and a name out. The platform layer's UDP sockets and fs_save_snapshot stay for the new co-op; cfg_ini_now and fs_profile_path, which nothing else called, are removed.

Checked: the game builds and starts, the menu shows its four tabs, --probe-cfg and --probe-fs pass, and the launcher's private-settings test passes. tests/save_share_check.py builds without its co-op stub; it cannot run from this worktree because the folder's path is longer than Windows allows, on main too.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-13 03:37:26 -05:00
chrg6
4a6ba66115 Tell a start that was closed or stopped during the boot's builds from a driver crash, and limit the whole warm.
A marker or a journal left by a start that died in the boot's builds was always read as a driver crash. Since the
whole warm those builds run for minutes, so a start ended from outside in that time (the task manager, a program that
stops the game, the power) switched the pipeline prebuild off for that driver for good, or put up to 32 variants on
the skip list that the driver had merely been building. Closing the window was already an orderly end and still is.

The crash handler now leaves a note while the boot's builds run. With the note the next start does what it did: it
skips the prebuild, or the variants the journal names. Without it the end is taken for a stopped start: the pipelines
are built again and the journal's variants are blamed on nothing. Three such ends in a row still skip the prebuild,
for a driver that ends the process without a fault the handler sees. The boot's journal has a file of its own, so the
preparation's keeps its meaning.

The whole warm waits prewarm_full_s seconds at most (600): when they are up the game starts, the builders step down
to the background's thread counts and priority, and the file says "done", so the boot screen has one turn per driver.

tests\boot_warm_check.py covers what a leftover marker means with and without the note.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:24:22 -05:00
chrg6
d48a3d59f8 End a helper run with its own exit code when a library faults while the process is ending.
The shader preparation modes and the probes return from main, and the C runtime's exit then calls the detach of every library in the process, those other programs put there included. On a player's PC a post-processing injector's Vulkan layer read a null pointer there at the end of every --warm-shaders and --prepare-shaders, four runs over three versions: each printed "variants built into the driver cache", then a FATAL, and ended with code 71, so the launcher logged a finished preparation as one that did not finish. The crash handler now ends such a process with the code its mode returned and one "exit:" line; a fault while the mode still runs, or in the executable's own code on the way out, is reported as before. A crash inside that injector's library also gets the note that the library is not part of the game.

--probe-crash-exit makes the fault from an atexit handler, and tests/exit_fault_check.py checks both cases.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 14:57:13 -05:00
Tobim6
bb654470b2 Crash report: the log names what the crashing thread was building, and a library link names its case.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:22:24 +01: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
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
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
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
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
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
1b89c0227b Let --probe-movie test this title's AV1 movies, which it rejected before testing anything.
The probe checked for VP9 first and stopped on anything else, and every movie of this game is AV1, so the decoder the game actually uses had no test short of a game run. For an AV1 movie the probe now drives the host's movie::VideoDecoder as the game does: it feeds the samples, takes the pictures at 30 and at 60 updates a second, and checks that every picture is delivered once, in time-stamp order, with the end of the stream after the last one, and how far behind real time the pictures are. It starts no window and no device. A run that never reaches the end of the stream fails after a bounded number of updates instead of hanging.

On the game's files here: before, "FAILED: WebM: codec V_AV1"; now 240 of 240 pictures at 30 updates a second and 180 of 180 at 60, none late, the last 1 ms behind real time at 60, "probe-movie: passed". It does not look at the pictures' content.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit 2543b07117c638e4e3bf7df01e16f6801f9cbcfb)
2026-11-11 00:39:58 -05:00
chrg6
ba9d6bac06 Keep co-op's private settings out of the log and out of bug reports.
This line logs every setting nx2navi.ini sets, and a bug report takes a copy of nx2navi.ini along: with co-op both would have carried a room's secret, the relay's address and the player's name to whoever reads the report, and the secret lets anyone into the room. One list (runtime/cfg_private.h: the room's secret, the offer number, the relay, the name, the key a maker removes, the menu's test hook) now serves the log's line, "Copy system info" and the report's copy of the file, in the game and in the launcher: such a key reads "(set, not shown)", every other byte of the file stays.

Also: the last leftovers of the restorations that were left out with the puppets (their settings prefix and section), main.c's byte order mark as it was, and two comments that cited files this tree does not have.

Checked: with a room, a relay and a name in nx2navi.ini the log's line shows all three as set and none of their values; the same text gives the same bytes through the game's scrub and the launcher's (a launcher test, and the C function built alone); the launcher tests, the co-op probe, the relay tests and the tab check pass.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 00:20:22 -05:00
chrg6
7e07e13bac Add co-op, off by default: rooms through a relay, progress sync between the players' own saves, and a Co-op tab.
Each player keeps their own game and save. One player makes a room, the others join it with its secret, and story flags and, by the room's rules, items, songs, quest items, small keys (a shared pool), rupees and ammunition reach every save. The layer is a host thread with one UDP socket to a relay the players run themselves (tools/coop_relay.py, Python's standard library only); the messages between the players are sealed with a key from the room's secret (XChaCha20-Poly1305, Monocypher 4.0.2 vendored with its licence and a row in THIRD_PARTY_NOTICES.md), every change is signed by the player who made it, a player accepts a room by its members' keys and the key of its rules, the save is copied to coop-backup before anything is applied, and values are set through the game's own routines between frames. The Co-op tab makes, joins, accepts, removes and leaves, and the menu takes typed text for the name, the relay and the secret. This is the contributor's pull request #518 on this line.

Nothing reaches the network while co-op is off: the socket is opened, and the relay's name looked up, only after co-op is on, a relay is set and a room is made or joined. There is no default relay.

Left out of the pull request: the other players drawn in the world (the "puppets") with the copied engine they need. They were unfinished, added two patch sites to the game's per-frame update that could not be put through the differential tests here, and their file helpers clashed with the ones this line already has. So no position is sent: the POS message's codec stays, nothing sends it.

Changed against the pull request:
- The menu's test hook drives the Co-op tab only when it comes from the environment. From nx2navi.ini it could turn co-op on, join a room and accept its offer.
- The log's label for a room is hashed from the secret instead of being its first three characters.
- A game that has not saved yet has no save to copy, and takes nothing from the room until it has saved once; that used to count as a copy made.
- A PING's token no longer carries the PC's uptime.
- On Windows the maker's key file and its folder get an access list with the player's account alone.
- The tests that link the platform layer get the socket library, and the save test a stand-in for the title screen's hook.

Checked on Windows, where the pull request had never been compiled: the runtime builds and links; --probe-coop passes (sockets on 127.0.0.1, the codec against its byte vectors, a fuzz of every parser, two sessions through a hostile mock relay, a relay restart, the sync engine); tests/coop_relay_test.py passes its 61 tests, with the probe through the real relay on 127.0.0.1 at 0 and 30 % loss; tests/coop_page_check.py passes and its deliberate fault fails 48 checks. Not checked: two games in one room (one game runs at a time here), any real network, the tab in the game.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 00:11:41 -05:00
chrg6
7ccb958c47 Let a save Commit that is under way finish before the crash report or a fatal error ends the process, for at most two seconds.
The game's Commit returns once the commit is queued, so the game may already have reported the save as done while its thread still copied; ending then left an unfinished copy that the next start rolled back.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 23:26:01 -05:00
chrg6
fc71691e91 Merge branch 'claude/amd-fight-crash-ef03a9' into test-all-fixes 2026-11-10 15:22:07 -05:00
chrg6
caf50ecf1c Turn the lazy write-back off by default on AMD's driver for RX 5000 and 6000 cards, and say in the log which settings a run had.
A player on an AMD card reported crashes in fights and that a line added to nx2navi.ini cured them. That line, async_compute = 1, is no setting of the game, and neither are two more in the same file; the one line of the file that changes how memory reaches the GPU is wb_lazy = 0. A second report, an RX 6600 on 0.7.1, lost its device 3.5 minutes into play with the lazy write-back on. Neither proves it the cause: the player changed several lines at once and sent no log, the RX 6600's last pass mark is inside a draw pass and not a fetch, cards with 8 GB lost their device the same way on NVIDIA before the lazy write-back existed, and reading the fault handler, the fetch, the arming and the buffers' lifetimes found no failure path. What depends on the driver and cannot be checked without the card: pages of guest memory are inaccessible while a host-pointer import of them is bound and written by the GPU, and Vulkan calls run inside the access-fault handler.

The default is now decided once the driver is known (wb_lazy_choice.h, vk_memory.c vk_mem_lazy_decide): on under Windows as before, except on AMD's driver branch for RDNA 1 and 2, which none of us has run it on; AMD's driver for RDNA 3 and newer keeps it on, since builds with that default were measured on an RX 7900 XT. What the player sets wins everywhere, and one line at start says the choice and its reason. On those cards open areas run at the 0.7.0 frame rate again (on an RTX 5090 that was 65 fps against 90 in Hyrule Field; not measured on AMD). tests/wb_lazy_check.c checks the choice by platform, driver and setting without a GPU.

For reports of this kind the log now holds what was missing. At start: every setting nx2navi.ini sets, and in one line the keys that are no settings of this version and do nothing; the settings menu shows those in its bottom row. At a fatal error or a crash: one line with the card, its driver, the memory mode, the lazy write-back with its reason, armed ranges, fetches so far and a fetch in progress, and the texture detail (dispatch.c rt_fatal_note_add, vk_device.c fatal_note), since a report is often the end of a log alone.

--probe-cfg loads the player's file: the three lines are named, and the menu's save keeps them and every other line it does not change byte for byte. Nothing here ran on an AMD card, and no fatal error was provoked to read the new line.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 15:14:25 -05:00
chrg6
918d399d55 Add the shared pipeline list: mesh pipelines players met are built at the shader preparation, before the first start.
The preparation translates every shader and builds each one's stage library, so a pipeline a draw meets for the first time is linked in 2 to 6 ms. A mesh pipeline has no stage libraries: on an install that never drew it, it compiles whole in play, 0.2 to 1.1 s with its draws left out of the picture (18 of them on a first visit of Kokiri Forest). Whole pipelines were built ahead only from the folder's own pipeline-keys.bin.

tools/pipeline_list.py merge folds the pipeline-keys.bin and shader-answers.bin of game folders that were played into one list file: every key record once, with the answers record of each shader it names. A record names its shaders by input id, which depends on the game's code and the answers alone, and holds the game's pipeline state; what depends on the GPU is derived when the pipeline is built, so a record made on one machine is good on another with the same game version and runtime. Four folders on this PC give 5,432 records, 317 of them mesh pipelines (147 KB of keys).

tools/prepare_shaders.py reads the list (tools/data/pipeline_list.bin, data/pipeline_list.bin in a release folder, or --pipeline-list), adds its shader variants to the seed and writes its records as seed-pipeline-keys.bin. nx2navi.exe --prepare-shaders builds the mesh pipelines among them after the seed is translated and saves the pipeline cache before the stage libraries are built (NX2NAVI_PREPARE_PIPELINES=all builds the vertex pipelines too); --warm-shaders does it again after an update or a new driver, and --prepare-pipelines is that step alone. The pipelines go into the caches and are not kept or added to the folder's own key file, so they cost no start-up time or video memory until a draw meets them. A marker file keeps a driver that crashes on one of them from failing every preparation.

The list is derived from game data and stays out of the repository: check_content.py refuses files recorded from a game folder by their first bytes, and make_kit.py, make_launcher.py and make_release.py carry a list given with --pipeline-list. All three now also carry pipeline_list.py, which prepare_shaders.py imports.

Checked without the game: tests/pipeline_list_test.py, and prepare_shaders.py --no-run on the game files with a list of 317 mesh pipelines (all 266 shader variants found in this game version, 177 new to the seed). The build step in nx2navi.exe compiles and links and has not been run.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 14:52:46 -05:00
chrg6
0a9bbed34a Say in the crash report when the crash is in another program's module, and report video memory when the device is lost.
A crash inside a recorder's Vulkan layer read like a fault of the game: the report now says that the module is not part of the game and what to do. A lost device now logs the video memory in use against the driver's budget, so a report from a card with little memory shows whether the memory ran out.

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

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

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

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

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

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

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

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 07:22:41 -05:00
chrg6
c117cd8999 Set NVIDIA's shader disk cache on Linux only with NVIDIA's driver, in a folder that could be made and written, before any thread of the game starts.
The first part of this pull request (the cache beside the exe with an 8 GiB limit) was already in. This
is the rest: nothing is set without NVIDIA's kernel driver, neither variable is set when the folder cannot
be made or written (the driver caches nothing in such a folder), the log says which folder and limit hold,
and a game run sets the drivers' cache variables in main before any of its threads can load a Vulkan
driver (vk_device.c vk_driver_cache_env); the other modes still set them in create_instance. On Windows
that only moves the MESA_SHADER_CACHE_DIR setenv earlier.

Pull request #369 by mooki000, taken as it is. The Linux part is not compiled or run here.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:17:13 -05:00
chrg6
61a13c62b9 Keep only the compute pipelines of recently dispatched shaders from the boot, and build the others into the pipeline cache without keeping them.
The boot prebuild built the pipeline of every compute shader play ever recorded and kept all of them for the process. A pipeline that exists holds driver memory whether or not anything dispatches it: with a history of 4,066 compute shaders they held 2.3 GiB of device memory, and its share of the process's commit, from the title screen on (RTX 5090, NVIDIA 621.01), while a boot into the first village dispatched 95 of them.

The boot now keeps the pipelines of the 512 compute shaders dispatched most recently (compute-used.bin next to pipeline-keys.bin records which run last dispatched each; the setting is compute_keep, with "all" for the earlier behaviour). Every other one is still built at boot, so that the pipeline cache holds it after an update or a driver update, but destroyed at once, and created from the cache at its first dispatch. A dispatch that comes while such a build runs gets that build's pipeline.

Each boot stage now logs the device memory and the process commit it leaves ("mem: boot stage"), and the log says what the first dispatches cost. nx2navi.exe --probe-prewarm runs these stages without the game from the caches next to the executable and times first dispatches of pipelines the boot did not keep: 256 of them took 0.19 ms on average and 3 ms at most, all read from the pipeline cache file, and 372 pipelines built and destroyed at boot left the device memory where it was.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit 16a796d623686bca5304eaf1b29981247453a413)
2026-11-10 03:34:03 -05:00
chrg6
0cebadcfac Leave the shader preparation modes on every processor the process was given.
The large-cache die's placement is for the game's own threads. --prepare-shaders and --warm-shaders translate and build
on as many processors as they find, so on a two-die processor the placement would have halved them.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 02:02:11 -05:00
ordinarybob
662a6e88ee Run the game on the die with the larger L3 cache on processors whose dies differ (AMD's two-die X3D processors).
Windows places a game on the V-Cache die only when it recognizes the exe as a game, and it does not know this one, so the threads were spread over both dies by frequency preference. cache_die = 1 (the default) narrows the process to the logical processors that share the largest L3; it never widens the set the process was given, and does nothing where every L3 has the same size. The recorder's reserved core is then chosen inside that set.

Co-Authored-By: Claude Fable 5.2 <[EMAIL]>
2026-11-10 02:01:35 -05:00
chrg6
202fc247d0 Name the gamepad probe probe_gamepad, so it does not clash with ov_probe.c's own static probe_input.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 23:54:29 -05:00
chrg6
be8c2620e5 Run the gamepad update on a thread of its own, so a controller that comes or goes no longer stalls the game.
When a device came or went, SDL's gamepad update first found every device again, 100 ms and more, on the game's
polling thread with the input lock held: a controller disconnecting gave five frames of 120 to 240 ms in a row, with
the game's other pollers queued on the lock. The pad thread now makes SDL's initialisation and every update. A poll
asks for an update and waits at most 2 ms for it; an ordinary one is done in microseconds and the poll reads fresh
state as before, and during a device search the poll goes on with the state it read last. Rumble, the motion samples
and the menu's stick reading skip a beat then instead of waiting.
--probe-input checks it on a virtual gamepad, with an update made to take 300 ms.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-09 23:51:58 -05:00
chrg6
f63b6796ee Decode Ocarina 1.1.1's loading streams on the host decoder, behind the same zstd_native = 1 switch.
The loading threads feed each chunk they read to the game's stream decoder, which is where most of the
decompression time goes. That call now reaches a host stream that writes straight into the game's output
buffer and keeps the game's state fields and results. A stream is taken from its first chunk or left to
the game; zstd_check = 1 runs both and compares the outputs. --probe-zstd checks the host streams on
frames it makes itself.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-09 21:21:48 -05:00
Michael Schienbein
a103673341 Make the SDL window honour monitor, --monitor and --list-monitors.
On Linux the window ignored NX2NAVI_MONITOR (the log said it had no effect) and --list-monitors only printed that monitor selection is Win32-only. The SDL window now picks a monitor with the same rules as the Win32 one: auto (the system places it), primary, a 1-based index in listing order, or part of the monitor's name, ignoring case, the first match in list order winning. The monitors are SDL's displays; --list-monitors prints their index, name (the make and model, with the connector on Wayland and X11), pixel size and position, primary flag and display scale, then what NX2NAVI_MONITOR would pick, and the log says the same when the window opens.

The window is created centred on the chosen monitor and centred again at its final size, and that position also names the display the fullscreen setting covers, which is how a Wayland window, which a client cannot move, opens fullscreen on the chosen output. On Wayland a windowed window opens where the compositor puts it; the log says so once. With a chosen monitor, the size widescreen.c reads for fullscreen before the window exists is that monitor's.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 20:51:09 -05:00
chrg6
66927b8f94 Ask every driver but NVIDIA's to keep NaN and infinity in the game's shaders, which should bring Navi's orb back on AMD GPUs.
The game's shaders rely on IEEE NaN and infinity results: unordered comparisons compile to x > c || isnan(x), a
zero-length normalisation gives an infinity that a later test catches, integer masks travel through float selects,
and the translator's own NaN guards are isnan tests. Our SPIR-V never declared SignedZeroInfNanPreserve, which lets a
driver assume floats are never NaN or infinite. NVIDIA's driver keeps the IEEE results anyway; AMD's compilers do not
(issue #156: a NaN-bit select read as a signed integer gave 2^32; #41 and #110: black and white blocks on Navi and on
Link next to her). The player report: AMD GPUs draw Navi's sparkles but not her orb, whose light sits at her own
centre.

vk_pipeline.c spirv_nan_preserve now adds the capability, SPV_KHR_float_controls and the mode for 32-bit floats (16
and 64 where the module declares them and the driver can) to every game shader module. NX2NAVI_VK_NAN_PRESERVE: unset
on every driver except NVIDIA's own, 1 everywhere, 0 off. --selftest checks the rewrite; the rewritten modules of 600
translated game shaders pass spirv-val for Vulkan 1.3. Not yet seen on an AMD GPU.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 11:17:08 -05:00
Tyler
2270664be2 Total each allocation once in the commit census, add a size breakdown, and write the profile on a crash
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HtayeqEG2x5woYzWuEQBGV
2026-11-09 08:20:01 -05:00
xyzzyy
a4ee74b74e Stop flushing the shader cache files every read checks, so shader preparation translates twice as fast.
Each translation flushed its .spv and .res.json to the disk, wrote its .use mask with a flushed, written-through
rename and renamed its outputs written through: about six waits for the drive per shader. An NVMe drive does about
1,100 flushed files a second, a SATA SSD or a hard drive far fewer, so the translators sat mostly idle. Every read
already checks these files (spirv_complete, reflection_valid, the .use magic and size) and a torn one is translated
again, so they are now written whole but not flushed (plat_file_write_cached, plat_file_replace without durable).
The .in files stay flushed: they hold the guest's code and only their header and size are checked.

4,683 shaders of the install's preparation on a Ryzen 7 7700X and a Samsung 990 PRO: translation 36.4 and 37.2 s
before, 18.4 and 18.9 s after; every .spv, .res.json, .use and .in file and shader-prepared.bin are the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:18:54 -05:00