55 Commits

Author SHA1 Message Date
chrg6
27a034e946 Keep the 240 ceiling while RivaTuner Statistics Server holds the limit, and take the limit back after 200 ms with no host present.
frame_step_cap_ns returned 0 once RTSS held the limit, so pace() set each slot to now and the acquire never waited.
RTSS holds frames inside the host present, and a minimised window (gfx_present.c present_image skips the present) or
a frame hold gives it none to hold: the game then ran with no limiter but pace_smooth's floor, which follows the
intervals down, and the quick menu is where 640-700 fps read address 0 (frame_rate.c LIMIT_FREE_MAX_HZ).
gfx_present.c flip_locked now stores the time of each host present that took its picture (gfx_window_hosted_ns), and
rtss_decide.h rtss_pace_period gives the pacer the limit's period before the first host present and once none went
out for 200 ms (as vk_lowlat.c HOLD_IDLE_NS), and the 240 ceiling's period, never 0, while RTSS holds. The frames RTSS
lets through come further apart than the ceiling, so pace() waits for none of them; no new wait site.

Not reproduced before the change. frame_step_check (new cases: holding with a host present 10 and 199 ms ago, 250 ms
ago, none yet, not holding, 144 minimised, unlimited) and rtss_limit_check (rtss_pace_period) pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 20:12:25 -05:00
chrg6
cd3f015732 Log the guest's ExitProcess line after the exit guard, and bring the comments on gfx_window_closed's return up to date.
The review of the exit sequence found that svc.c case 0x07 logged "guest ExitProcess at ...; exiting" before
rt_exit_begin. When a close had already begun, the guest thread that the guard then parks still claimed to exit, and
past the log cap the line fell outside the end's reserve, which rt_exit_begin opens. It also found that host.h and
gfx_wsys.h still said gfx_window_closed does not return, though it returns for its own thread coming back, and that
rt_exit_begin's comment spoke of the window thread handling a message inside the sequence, which since the close
runs on a thread of its own happens only in the fallback.

svc.c logs the line after rt_exit_begin returns 1. The two header comments say gfx_window_closed returns only for its
own thread coming back (gfx_present.c rt_exit_begin), and rt_exit_begin's comment says the window thread runs the
sequence itself only when gfx_window_win.c exit_start could make no thread.

tests/exit_order_check.py checks statically that 0x07's rt_log comes after rt_exit_begin; it fails on the code
before this change and passes after it. exit_order_check and the compile check of every runtime file (a header
changed) pass, with no new -Wall warnings in svc.c and gfx_present.c.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:43:24 -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
153295b64c Check cfg_meta's result before reading a row's default in default_number and gfx_cfg_window_size.
cfg.c default_number, which cfg_int and cfg_double use for a set value that does not parse, read cfg_meta(name)->def
without a check, so a key without a row whose value was set and unparsable crashed there (the verifier's probe: a
segmentation fault inside default_number). gfx_present.c gfx_cfg_window_size did the same for WINDOW_SIZE. Neither
is reachable today: every unguarded value read has a row (tests/cfg_rows_check.py holds it so), the three guarded
ones use cfg_bool, and WINDOW_SIZE has a row.

default_number gives 0 for a key without a row; gfx_cfg_window_size falls back to 1920x1080 when the row or its
default is missing. Taking the device profile's default there (1280x800 on a Steam Deck) would change what players
get and is left for approval.

Reproduced before the change with the new case in tests/cfg_parse_check.py (cfg_int and cfg_double of an unset row's
key set to "abc" crashed the program); with the change both give 0 and the check passes, as do the cfg rows checks
and the compile checks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:42:34 -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
e39f71df39 Run every orderly exit through one sequence that puts RivaTuner's profile back before the slow pipeline save.
The window's close, the guest's ExitProcess and the end of a --seconds run each ran their own list. All three
waited for the pipeline save (up to 5 s for running builds, then the write) before they put RivaTuner Statistics
Server's profile back, stopped HD rumble, wrote the frame-time summary and flushed the save, so a console close,
a session end or an "End task" that killed the process in that wait left the game's limit in RTSS's profile (the
next run takes it for the player's own), a buzzing controller and no exit summary. ExitProcess also lacked
cfg_save and the rumble stop, and it and the window's close had no shared guard, so both could run at once and
write the summary twice while the present thread still added to it.

gfx_present.c rt_exit_begin is the one guard (the first caller runs; another thread waits for good; the same
thread again returns, and svc.c ends the process then rather than resume guest code). rt_exit_steps runs, in
order: fs_save_wait (a Commit under way finishes, none is refused), rtss_limit_restore, input_rumble_hd_stop,
cfg_save (not for --seconds), the prewarm marker clear, prof_write, vk_mem_exit_log, a non-final log flush
(the new logq.c logq_flush, bounded trylocks, queue left on), vk_pipeline_save_exit, and fs_save_flush last.
perf.c ft_stop keeps the present thread's frame-time updates out of the exit's summary and waits at most 50 ms,
on the exiting thread, for one under way. The restore relies on rtss_limit.c's closed flag to stay restored
while frames go on during the pipeline save. The log order changes: "frametime: exit" and the stutter-map exit
lines now come before the pipeline save's lines.

Not reproduced before the change. tests/exit_order_check.py (new: the step order, the once-guard with a second
thread, ft_stop's wait and bound, and statically that the three exits use the helper with the RTSS restore
before the pipeline save), tests/logq_check.py (with a logq_flush case), tests/save_share_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
0672655c96 Let a destroyed vsync event give its slot when the vsync list is full, instead of ending the process on the 17th.
gfx_vsync_bind kept every SystemEventType address the game ever passed to GetDisplayVsyncEvent, never dropped one, and
called rt_fatal on the 17th distinct address. A destroyed event also stayed on the list the vsync thread signals 60
times a second. kernel.c now exports kern_system_event_live (mapped guest memory with a nonzero state byte), and with
the list full gfx_vsync_bind gives the first slot whose event is no longer live to the new one, which also takes the
destroyed event off the vsync thread's list; it stays fatal only when all 16 are live. Each new listing logs
"gfx: vsync event ... bound in slot N, M listed", so a long session's log shows how often new addresses come.

Not reproduced before the change: the fresh 0.10.0 session's system event count stayed at 9-10 across its area loads,
and no log here covers returns to the title. tests/vsync_bind_check.py cuts gfx_vsync_bind from gfx_present.c with a
stub liveness check: a 17th event takes the first destroyed slot, an 18th the next, and with 16 live events the 17th
still ends with the fatal. system_event_check now also checks kern_system_event_live. The compile check of every
runtime file passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:46 -05:00
chrg6
0ffac80222 Name the queue lock's long holders under NX2NAVI_LOCK_TIME, and time each swapchain rebuild's hold of it.
With lock_time = 1 the 0.10.0 session showed flip_locked waiting up to 48.61 ms for the queue lock, and the replay's
queue_submit up to 8.05 ms. Nothing named who held it: only vk_texture.c's lock records its holders. A swapchain
rebuild holds the lock across vkDeviceWaitIdle and the create, and that hold was not timed either.

vk_queue_lock, vk_queue_lock_raw and vk_queue_trylock_raw are no longer inlined. Each takes the lock with
plat_rmutex_lock_at and its own return address, so the wait report keeps naming the caller (before, the inlined call
named a site inside flip_locked). At the outermost take, with lock_time on, the holder's site and the time are noted.
vk_queue_unlock passes a hold of 5 ms or more to perf.c perf_qlock_hold. The frame-time summary then prints "queue lock
held 5 ms or more" beside the texture lock's holds, by site, as the waits are. create_swapchain logs how long a
successful rebuild took: the wait for the lock, the hold, and the part spent waiting for the device to idle. It logs
the first 16 rebuilds and each doubling of their count.

Diagnostics only: the counters are behind NX2NAVI_LOCK_TIME, which is off by default, plus one capped line per
rebuild. Checks that passed: pass_cc_check on every runtime file (host.h changed), no new -Wall -Wextra warnings in
gfx_present.c and perf.c, stutter_hitch_check, pgo_build_test, sync_hw_check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:40 -05:00
chrg6
68bc197393 Route present_image's fence-wait results through the lost-device check, cap their lines, and skip a capture whose wait failed.
present_image's 5 s wait for the slot's fence passed its result to nothing but a "present fence wait timed out" line.
A lost device was reported as a timeout, and since a lost device's wait returns at once, the line repeated at the
game's present rate until another thread noticed the loss. A GPU hung but not lost repeated the line every 5 s with no
cap. The result now goes to vk_device_lost_check first. The line says whether the wait timed out or failed, with its
code and count, for the first 8 times and at each doubling.

An acquire answering an error other than OUT_OF_DATE (VK_ERROR_SURFACE_LOST_KHR, say) skipped the frame without a
word. Each such code is now logged once. Making the surface again would be presentation and device work and is left
alone.

The capture path waited for the slot's fence and then read the read-back buffer whatever the wait returned:
save_capture and grab_deliver could copy a frame the GPU had not finished. A failed wait now goes to
vk_device_lost_check, marks no capture valid, and ends a pending grab with no pixels, as the no-buffer path does.

Not reproduced before the change: the fresh logs have no such line. Checks that passed: pass_cc_check on
gfx_present.c, no new -Wall -Wextra warnings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 18:41:40 -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
d9429b0c00 Break the memory report down past VMA, by owner in system memory, and by growth over a session.
Every report adds a line for video memory outside VMA (the driver's usage and budget per heap against VMA's blocks, and counts of pipelines by kind and by last use, stage libraries, compute pipelines, layouts and descriptor pools, with the swapchain, DLSS and FSR sizes), a line that splits the process's commit by owner (the game's heap and maps, device-local memory, VMA's host blocks, the C runtime heap with the shader caches, pipeline tables, texture cache, log queue and flight recorder in it, the driver's pipeline cache data and the rest), and a line naming what grew by more than 64 MiB since the report before and since play began. Every ten minutes of play and at exit a ranking line orders the categories by growth and marks those that only climbed; tests/mem_growth_check.py checks that bookkeeping.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 06:46:31 -05:00
epicmeme546
4fd70e398f Make F8 write a bug capture and add a test run that starts from one.
F8 now always writes a folder under logs/captures: the area, Link's position and rotation, the camera of the frame on screen and the game's own, the presented frame as a PNG, frame times around the press, the machine, the settings that differ from the defaults, the end of the log, the save, and the game's flags and texts. A note on screen names the capture. The flight recorder's F8 dump goes into the capture's folder.

NX2NAVI_CAPTURE_REPLAY starts a run on a capture's settings, teleports to its area, walks Link to where he stood and puts the free camera where the camera was; tools/capture_run.py prepares the profile from the capture's save, runs it and compares the two captures. docs/bug-capture.md has the files and the steps.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-13 06:22:18 -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
691d41a026 Free the images of HDR, FSR 1 and SMAA once they are no longer used, instead of keeping them for the process.
Each made its images at the picture's or the scene's size the first time it ran and kept them until the process
ended, also after the menu switched it off:
- the HDR encode's picture and menu image (28 and 14 MiB at 2560x1440, 63 and 32 MiB at 2160 lines) now go when the
  swapchain is made without HDR, after a wait for the presents that may still read them;
- FSR 1's two images (56 MiB, 127 MiB at 2160 lines) go after 300 presents it did not scale, with the same wait;
- SMAA's three work images (35 MiB, 79 MiB at 2160 lines) go after 600 frames of another anti-aliasing, deferred past
  the submission being recorded.
Each makes them anew when it runs again; one log line says when they go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:46:26 -05:00
chrg6
fdaa3e2a01 Take a failed frame generator's swapchain and contexts down at the next present.
A frame generation failure turns it off for the run, but the present compared what is wanted with what runs only when
a setting changed, so AMD's swapchain and both of its contexts stayed until some other setting moved. A frame
generator that runs and is no longer wanted is now noticed at every present, and the swapchain is made again without
it, as when it is switched off in the menu. The other way round, wanted and not made, is still tried only at a
settings change, so a swapchain that cannot be made is not tried at every frame.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-13 00:44:33 -05:00
chrg6
c37fb7dc40 Hold each frame's start in the driver while low latency is on, and let that keep the frame limit.
The driver's sleep sits where the game's frame starts: at its first pad read or window image acquire after a present,
whichever comes first, before the input is sampled. The frame's number, the count of the game's presents, goes with
its work: the guest thread marks the simulation and the first submission, the replaying thread tags its queue
submissions and marks their end, and the present thread marks the host present, which carries the number too. The
window's swapchain is made with the latency mode while NVIDIA's path is on, and remade when the setting changes.

While the mode is in effect the pacer holds nothing, so a picture goes out as soon as it is drawn and the limit is
kept by the driver before the pad is read; RivaTuner Statistics Server is kept out as with frame generation. The pacer
takes the limit back while a movie plays, while no host present goes out, and for the rest of the run when the frames
show the driver does not hold it. With the mode off every hook returns at its first check.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:29:59 -05:00
chrg6
df8431da7b Send the game's vibration values as they are to a Switch Pro Controller or Joy-Con, behind rumble_hd.
With NX2NAVI_RUMBLE_HD=1 (live, off by default) the gamepad the game reads, when it is a Switch Pro Controller, a
single Joy-Con or a pair of them, gets each side's low and high band with its own frequency and amplitude in the
controller's rumble report, and no dual-motor call any more. A Pro Controller and a single Joy-Con take the report
through SDL's effect call, over USB and Bluetooth; SDL refuses that call for a combined pair, so a pair's reports are
written to its two HID devices directly. Every other gamepad, the Switch 2 controllers included, keeps the motors.

The game's thread posts its newest values and a thread of input.c writes the reports: at most one every 15 ms
(rumble_hd_ms), a held value again every 30 ms, and the neutral value when the game stops sending for 250 ms, when
another gamepad takes over, when the setting is turned off and before the process ends. The rumble strength scales the
amplitude and rumble = 0 silences this path as well. With the setting off nothing is written to the controller: the
log line at the take names it by its ids, and the first report is the trial.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 22:28:36 -05:00
chrg6
202fd47ceb Let the flight recorder dump on its own when one frame flashes.
With flight = 1 and its thumbnails kept, the recorder now also writes a dump when a frame's picture differs strongly from the frame before and the frame after while those two agree, which is what a one-frame flicker looks like. Each thumbnail is reduced to 16x9 cells of mean brightness where the present thread already sees it complete, and a frame is a flash when at least three cells are more than 24 of 255 brighter or darker than in both neighbours, the neighbours being alike in those cells, around them and over the picture as a whole. A camera cut, a fade, a pan and shapes crossing the picture are no flash, and a frame beside one without a thumbnail is not judged. The dump is the one F8 writes, named "flash" with the frame's number in its header line, at most one every 30 seconds. flight_flash = 0 turns it off; flight_flash_diff and flight_flash_cells set the two thresholds.

On the thumbnails of earlier dumps the defaults flag the frames whose scene was white once and nothing in the runs without that fault. tests/flight_flash_check.py checks the test itself on drawn pictures.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-12 15:16:30 -05:00
Tobim6
81cdd67086 Frame generation: the HUD-less picture also where the upscaler's output is smaller than the presented picture
In handheld mode the scene is 75% of the picture (1440x810 in 1920x1080) and the game's last passes scale it up with
the HUD. hudless_record took the upscaler's output only at the presented picture's size: no HUD-less picture, and no
frame generated (5 of 3600 on an RX 580). The crop is now scaled to the smaller picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:23:15 +01:00
Tobim6
6de2be9ed5 GPU timeline (NX2NAVI_TRACE=perf): every 60 frames, what the GPU's idle time was spent waiting for.
The perf line's "idle with it queued" was the gap before each replay part handed over before the previous one ended:
13-17 ms a frame on an RX 580 in Kokiri Forest on 0.7.1, unexplained. Uploads and presents were not timed, a held
submission (submit batching) counted as handed over while it still waited in vk_sync, and with cuts and pre-pass
command buffers a submission closed more than the four timestamp pairs ts_unsent had room for: the rest went to the
completion queue without a submit time and out of the GPU's order. Now every timed piece of GPU work (replay parts,
mirror prologues, pre-pass command buffers, upload batches, presents) is noted with when it was handed to vk_sync, when
it really went to the queue and its GPU start and end, and every 60 frames two lines split the idle time: starved
(nothing in the queue, and how much of that a held submission waited in the batching) or with work in the queue, by
the kind of piece that started after the gap, with the average delay from hand-over to start on an idle GPU per kind.
ts_unsent has room for every command buffer of a submission, and the pairs are read in queue order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-12 16:22:42 +01:00
automata.
27f93a314a Save completed pipeline cache data during NVIDIA background builds 2026-11-11 18:10:38 -05:00
chrg6
358ad54105 Log the luminance of HDR10 shots and keep their raw pixels, and show an HDR shot as a 2.2 display would.
A shot taken from an HDR10 swapchain (the menu's test hook, NX2NAVI_SHOT_EVERY) now writes the picture's luminance in nits to the log, the darkest pixel, four percentiles and the brightest, and its pixels as the swapchain holds them beside the PNG. The PNG of such a shot shows the HDR display's light at the HUD's white through the power 2.2, not the sRGB curve, so a dark tone the HDR display lifts looks lifted in it too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-11 15:06:30 -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
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
e887a1d18b Delete the boot prebuild's running marker at every orderly exit, so closing the game during boot is not taken for a driver crash.
The marker was deleted only when the boot's builds ended. A window closed before that left it, the next start read it as a start that died in the driver's compiler, and every later start on that GPU and driver skipped the prebuild and built each pipeline at its first draw.

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

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

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

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 06:45:59 -05:00
epicmeme546
41eb0e635b Apply the camera's stick sensitivity and the speaker layout during a run.
NX2NAVI_STICK_SENS was served to the game as a multiplied camera pack at start only. A change during the run is now written into the game's memory: the StickSensitivity array as the game holds it, at the factor in effect, is looked for in the guest's heap and every copy is overwritten at the new factor. The game reads the array each time it steps the camera, so the speed follows at once. The copies found are remembered, so a later step of the menu's slider takes no search, and a search runs again a moment after the game opens the pack anew. On Ocarina 1.1.1 the first change of a run found 4 copies in about 2 s on its own thread, and the same stick push for a third of a second turned the camera about 30 degrees at 1, 90 at 3, 10 at 0.3 and 30 again at 1, all in one run.

NX2NAVI_AUDIO_CHANNELS was decided once. A change that gives another channel count now signals the device change events the game holds, as a console does when its output moves: the game asks GetActiveChannelCount again and mixes for the new count, and the mixer makes its SDL stream again with that count at its next render. On Ocarina 1.1.1 the centre input of the device sink went from 0.0006 rms to 0.013 within the 5 s after stereo became 5.1 and back to 0.0006 after stereo was set again, with no underrun. NX2NAVI_TRACE=audio logs the six inputs' levels with its level line.

Both rows lose their "after restart" note. The graphics card, widescreen, the resolution, the game's language, the button prompts and the game's native HDR still take a restart: the game or the Vulkan device reads them once while starting.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 00:21:31 -05:00
epicmeme546
9ae7764ab5 Change a setting with the mouse only on the two framed arrows beside its value, and switch HDR without a restart.
In the settings menu a click anywhere on a row stepped its setting to the next value, and the left arrow stood left of the value's text, so it moved whenever a value of another width came up: clicking the left arrow through a list landed on the row at the second click and stepped the other way. Each stepper now has its arrows in white frames at fixed places, the left one the width of the row's widest value from the right one, and the mouse changes the value on those frames only; a click on the rest of the row selects it. On and off rows have the same arrows. An arrow at the end of its values is drawn faint and does nothing, and a button held on an arrow repeats as a held key does. A, Enter and left and right work as before.

NX2NAVI_MENU_TEST_CLICK clicks rows by their setting's name during a menu test run. On an RX 7900 XT at 3232x1352 the arrows stepped the performance overlay one value a click and stopped at both ends, and clicks on the body of four rows changed nothing.

HDR between off and the tone mapper now follows the menu. The HDR path is set up at every start (vk_hdr_armed): the swapchain colour space and metadata extensions, the tonemap splice and the settings in the support buffer, with the spliced tonemap returning the game's own SDR result while the swapchain is SDR. A change of the setting makes the swapchain again in the other format. On the same card a run that started with HDR off went to an HDR10 swapchain and back, and one that started with HDR on went to SDR and back, with the picture right in each state. The game's native HDR still waits for a restart, as the game asks about the display once, and the row says so. NX2NAVI_HDR_LIVE = 0 leaves the HDR path out of a run that starts with HDR off.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-09 23:27:02 -05:00
tangerineturtle1
589c9e8654 Stop the replay from searching the environment on every draw, remember setting lookups, and spread texture streaming over more frames.
The draw path called getenv("NX2NAVI_SKIP_PROG") for each of ~2,000 draws a frame, and getenv walks the whole environment block: in a replay-thread profile it was a sixth of the samples. That switch, the polygon offset log and the ASTC alpha probe now read the environment once. The replay's cost per draw went from 3.7-5.0 us to 1.2-2.8 us in play, and the mean frame rate by minute from 60-131 fps to 103-243 fps (different places in each run, so the figures are approximate).

cfg_get and cfg_value keep each thread's answers per name until the settings version changes (cfg_set, cfg_reset, cfg_load, and cfg_setenv, which the code that sets NX2NAVI_ variables in the process's own environment now uses); NX2NAVI_CFG_MEMO=0 asks every time.

The texture pool creates at most pool_create_budget (NX2NAVI_POOL_CREATE_BUDGET, default 16, was a fixed 64) new texture entries per refresh. The refresh holds the texture lock throughout, and streaming new textures in made 20-40 ms frames; at 16 the lock was held 5 ms per slow frame instead of 24 in a dense area, and the frames over 25 ms in a heavy minute fell from 111-188 to 89, with no pop-in seen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:09:26 -05:00
chrg6
d13ec37ced Drop the old swapchain when a new one cannot be made, instead of acquiring from the retired one every frame.
A create with oldSwapchain retires it even when it fails. The old one, its views and its semaphores now go at once, on
the frame generation paths too, the next present makes a swapchain from nothing, and the failure line is throttled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 08:09:25 -05:00
chrg6
1f7e9c975b Speed up the replay and fix what the perf pass found, and add a frame capture key for chasing rendering faults.
Performance: draws reuse the bindings that did not change and record only the dynamic states that differ from what
the command buffer holds; small uploads come from a staging arena; finished submissions are held briefly and handed to
the queue in batches; the recorder thread can keep a CPU core of its own; ASTC block-linear textures are deswizzled on
the GPU; dynamic resolution steps the upscaler mode to hold a frame-time target. Each has a setting in nx2navi.ini
(bind_reuse, dyn_delta, stg_arena, submit_batch, recorder_core, recorder_prio, astc_gpu_deswizzle, dyn_res) and the
staging cache now frees what it holds at shutdown.

Capture: NX2NAVI_DUMP=key saves the frame after each press of Insert, with mesh draws listed by task, mesh and fragment
shader, depth test and face culling, and mesh_prims.txt giving each mesh draw's primitive, clipping and fragment counts.
All of it is off unless NX2NAVI_DUMP is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 03:34:46 -05:00
epicmeme546
754ded99e0 Raise the frame rate in Kokiri Forest and apply more settings during a run.
The foliage's sun shadows took most of the GPU time: one mesh draw shaded about 1,000 million fragments in the far cascade. The far cascade now shades them at a 2x2 rate, and the two far cascades drop one side of the double-sided cards. At the forest entrance the frame rate went from 18 to about 43 fps on an RX 7900 XT.

FSR 3.1, its frame generation and the console mode now take effect without a restart. The full performance overlay lists the settings shown in the options menu.

NX2NAVI_GPU_PASSES times each pass and counts its fragments, and NX2NAVI_SET_AT changes settings at a given frame for test runs. Both are off by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 02:40:51 -05:00
chrg6
ac5c196f3a Pace DLSS Frame Generation with Reflex from the frame's start, so it no longer halves the game's frame rate.
Reflex's markers sat at the host present with frame numbers unlike the frame's tags, slReflexSleep was never called, and the low-latency mode was set before any swapchain existed, so it never applied. GfxBackend.frame_start, called from nvnWindowAcquireTexture, now sets the simulation and submit-start markers after Reflex's sleep (skipped once nothing was presented for 200 ms); submit end is set when the frame's submissions went out; the mode is set again after each swapchain (NX2NAVI_REFLEX: 1 on, 0 off, 2 boost); frames are numbered by the game's presents; and the fps counter counts what was shown. NX2SL_VERSION is 3, so nx2navi_dlssg.dll must be rebuilt. From a tester's fixes: 42-45 fps game, 84-90 on screen (was 24).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 15:51:48 -05:00
chrg6
b8d7eede08 Keep presenting after a present submit fails, and ignore a crop outside the image.
- The present resets its slot's fence after the acquire. When the submit then failed (out of device or host
  memory), the fence stayed unsignalled and the acquire's semaphore signalled: the slot's next fence wait timed out
  after 5 s, the frame counter no longer moved on, and every later present waited on the same slot, so the picture
  stopped for good while each game submit waited behind it. An empty batch now takes the semaphore and signals the
  fence, and the swapchain is made again, which gives the acquired image back.
- A crop from the game's nvnWindowSetCrop that starts past the image gave a blit with an inverted source rectangle;
  the whole image is shown then.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 15:51:48 -05:00
chrg6
9bb3c6c55a Hand the frame limit to RivaTuner Statistics Server where it runs, and take it back when RTSS does not hold to it.
Where RTSS is installed and running, the frame limit from the menu goes into RTSS's profile for the game's executable
through RTSS's hooks library, loaded at run time, and the game's own pacer stands aside while RTSS has hooked the game.
Menu changes apply live, unlimited clears the limit, and closing the game (or any exit path) puts back what the profile
held, or removes a profile the game created. Each change is read back from the saved profile: without administrator
rights RTSS's profile folder cannot be written, and the game then keeps its own limiter. While RTSS limits, RTSS's own
frame counter for the game is checked against the limit; well past it for three seconds, the game takes the limiting
back for the run. The log says which limiter is in use and why, and a note under the frame-rate row says when RTSS
limits, when its profile is read-only, and when it does not hold the game to the limit. rtss = 0 and frame generation
keep RTSS out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 01:25:12 -05:00
chrg6
c327dc07d8 Generate frames inside the picture only, so a window of another shape keeps its black bars clean, and use the Triforce as the icon.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 17:36:21 -05:00
chrg6
501d5bd084 Release pass: fix crashes, freezes and races, cut VRAM use, and translate more shaders.
Crashes: the render-target heap (HeapDef DynamicTextureAllocatorMem and its parent) grows with the widescreen
picture, so the pause menu no longer reads a null block at 5120x2160; texture uploads keep the game's memory mapped
while they copy it, so UnmapPhysicalMemory no longer pulls it away mid-copy; DLSS Frame Generation no longer crashes
in Streamline's set-up (physical devices and the window surface go through its proxies, its swapchain images are
counted first, resources are tagged per frame); a stale core-wait target is cleared before its thread is freed.

Freezes and races: a resize during a swapchain rebuild is kept, a failed frame generation swapchain is retried
instead of replaced by a plain one, three acquire timeouts in a row rebuild the swapchain, device waits take the
queue lock, a translator child that stops answering times out, an in-flight compute pipeline builds at normal
priority, a save Commit started before the window closes finishes and none starts after, a settings change just
before closing is saved, and a movie sample taken before a flush is dropped.

HDR: Enhanced HDR extends the game's own tonemap so highlights go above the picture's white (the hue slider,
which no longer applies, is gone from the menu); frame generation gets a HUD-less picture, the HDR flag and the HDR10
metadata under HDR too.

Memory: ASTC textures are encoded to BC3 unless the PC has 20 GiB of VRAM and 48 GiB of RAM, as Windows charges video
memory to the commit charge (a long session held 17 GiB of RGBA8 textures and 45.7 GiB of commit on a 32 GiB PC).

Shaders: the translator lowers spill-mode programs with more than eight uniform register groups in a wider spill
layout, and local loads and stores with a uniform-register address (36029 of the game's 36031 variants translate,
20 failed before).

The launcher release ships the shader tools and AMD's FidelityFX library (make_launcher.py --ffx); windres there gets
the toolchain on PATH.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:02:30 -05:00
chrg6
a42648e27a Add the game's native HDR, render at the monitor's real size, and refresh movie frames on the GPU.
HDR is now a menu row: Off, Enhanced (the runtime's own HDR tonemap) or Native. Native reports an HDR10 display to
the game with the settings' peak and paper white; the game then renders its own HDR picture and presents it as PQ in
A2B10G10R10, which goes to the HDR10 swapchain as it is. The peak, game, HUD and (for Enhanced) hue rows follow it.

The build kit's link left out runtime/nx2navi.rc, so the game had no manifest and no icon: without per-monitor DPI
awareness, a 5120x2160 monitor at 150 % gave a 3413x1440 window that Windows stretched. It is compiled with windres
and linked again.

The movie decoder's frames are copied into the game's buffer by the host; the back end is now told of that write, so
the GPU's copy of the buffer does not keep older or empty bytes (green stripes in NV12).

A longjmp out of the game's JPEG decoder now logs the guest functions that called it, to trace a later crash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:31:07 -05:00
chrg6
b9a042969e Keep the HUD out of frame generation, show the frame generation rate, and start DLSS Frame Generation.
Frame generation now gets the picture without the HUD each frame: the upscaler's output before the HUD is drawn over
it, encoded to sRGB as the game's last pass encodes its picture and put through the same colour, scaling and borders
as the presented frame, so it tells the HUD apart and leaves it unwarped in the generated frames
(NX2NAVI_FG_HUDLESS_SHOW=1 shows that picture, a test). The performance overlay gains an FG row and the fps line the
display's rate, generated frames included. The device enables VK_KHR_get_memory_requirements2 whenever AMD's frame
generation is on, which crashed at start with DLSS upscaling.

DLSS Frame Generation through NVIDIA Streamline (vk_dlssg.c, and nx2navi_dlssg.dll from runtime/dlssg, built with
MSVC by tools/build_dlssg.py against the SDK kept outside the repository) creates the instance and device through
Streamline's proxies and calls its swapchain hooks; Streamline still fails in its plugin start-up here, so the menu
lists it only while it is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:31:07 -05:00
chrg6
7c11ff6816 Add AMD FSR 3.1 frame generation: a generated frame between every two the game draws.
With Frame generation set to AMD FSR 3.1 (after a restart), AMD's frame generation swapchain takes the place of the
present path's own: the device creates two more graphics queues for its presents and image acquires, the present
path acquires, gets images and presents through its replacement functions, and every frame configures generation
and records its preparation from the depth and camera motion vectors the DLSS or FSR 3.1 upscaling pass made (one of
them must be on). Generation pauses while the menu is open. On an RTX 5090 at a 60 fps game, about one frame was
generated for every frame drawn in play.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 13:31:06 -05:00
chrg6
1d7bc90848 Fix shadows, colour writes and present pacing on drivers that read Vulkan's undefined cases differently.
A depth-compare read through a sampler whose compare is off now gets the slot's comparing sampler, as the Switch
compares there and only NVIDIA's driver did so on its own. Colour components a fragment shader does not write are
masked out, as Maxwell leaves them alone. Without mailbox presentation the swapchain takes relaxed FIFO, or
immediate, so a late frame no longer halves the frame rate. NX2NAVI_VK_NOOPT=1 builds pipelines without the
driver's optimiser, for a driver whose compiler fails on one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:59:59 -05:00
chrg6
375ecf27e2 Wait for the whole boot prewarm and show its progress on a boot screen.
The game used to start after at most 3 s of prewarm, so after a translator, GPU or driver change it met the rest of
its shaders in play and stalled on each one. It now waits for all of it: a warm start is ready in about half a second
and shows nothing, while a cold one shows a "Preparing shaders" screen in the menu's colours and the game's fonts, with
the shader and then the pipeline count and a bar. The shader and pipeline prewarm report their numbers every 100 ms.
The present path takes a frame without a game image for the screen. The RomFS opens on the first host-side read, so
the screen's fonts load before the guest's first file access. NX2NAVI_PREWARM_WAIT_MS now defaults to 0 (no cap). With
no progress for 60 s the game starts anyway and the prewarm finishes in the background.

Measured with an emptied cache: 2,988 shaders translated and 2,399 pipelines built in 9.4 s behind the screen, then
0 ms shader and pipeline waits in Kokiri Forest. Warm: the game starts 0.47 s after launch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:26:51 -05:00
chrg6
020fe0eeb8 Give Ocarina's own frames-per-update the display interval, so its menus and interface keep their speed above 60 fps.
The base-step write fixed gameplay, but the interface and the frame counter step by the time manager's nominal frame, which the framework update (main+0x6e6be0) sets every frame as 60 over a target refresh rate it keeps at 60 (main+0x3b4e300 writes frames per update, its inverse, seconds and the rate). The new patch set ostep (recomp/patches-v111.txt, on by default) makes that update read frames per update from patch page +256, which the runtime writes at each present as the 5-frame average of the display interval in 60 Hz frames. The game then makes the whole nominal frame itself and the base step stays 1.0. At 144 fps the Plus menu opened in 0.17 s against 0.35 s at 60; now in 0.25 s, within the screenshot capture's noise. Builds without the set keep writing the base step. NX2NAVI_SHOT_AFTER delays the first NX2NAVI_SHOT_EVERY shot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 09:26:51 -05:00