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>
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>
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>
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>
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>
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>
A start that finds the pipeline cache written by another device or driver has nothing built, and everything was
compiled beside the game: an 8-core integrated GPU drew fewer than 600 frames in its first 11 minutes. Such a start
now waits on the "Preparing shaders" screen, with counts, for every recorded pipeline, for the compute pipelines on
every core at normal priority, and, when a sample says the driver's own cache lacks the prepared stage libraries, for
all of them on every core. The memory gate still holds for the builds, and the library builds write the preparation's
journal while the start waits, so a variant the driver's compiler dies on is skipped from the next start on.
boot-warm-owed.txt beside the caches names the GPU and driver: a start closed during the warm is followed by one that
goes on with it, three at most, and "done" keeps later starts ordinary while the cache file still carries the earlier
driver's header. When the boot stops waiting (prewarm_wait_ms, or a minute without progress) the builders step down
to the background's thread counts and idle priority. prewarm_full = 0 turns the warm off.
With nothing in the driver's cache the probe took 425 s on an RTX 5090 with 16 CPU threads: 87 s for 5,775 recorded
pipelines, 19 s for 4,075 compute pipelines and 317 s for 42,231 prepared variants.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
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>
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>
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>
On 1.1.1 the SDK's nn::oe::InitializeApplet calls SetFocusHandlingMode itself, before DT_INIT, so the first call reached the boot checkpoint from sdk+0x598840 and --boot and --seconds stopped with a FATAL before the game started. Only a call from main is the checkpoint now. 1.1.1 also gets its own return addresses (nnMain's SetFocusHandlingMode at main+0x1202d44, the first nvnBootstrapLoader at main+0x346bf8c, read from x30 in the two host functions during a normal start) and its init_array counts, so a 1.1.1 run is no longer checked against the 1.2.1 values.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The one-game-per-profile lock was taken at the first storage use, after the boot prewarm. A second copy started
while the game ran (a second Play, a shortcut) spent 14 to 30 s translating and building on every core and committed
about 10.9 GB before it ended with "another copy of the game is running", enough to bring a 48 GB machine to its
commit limit and stall the copy being played. rt_boot now takes the lock first; storage_init takes it again, which
plat_lock_file answers from the handle it already holds. A restart from the settings menu is unaffected: main waits
for the previous process to exit before rt_boot.
Every shader cache key names the translator, so a release with a new translator had no cached translation at all:
the launcher re-translated about 42,000 programs before play, and play waited for any program not prepared.
The cache now lists the translator identities it holds translations of in shader-cache/identities.txt, newest first.
A shader the current translator has no translation of uses a previous one's, looked up with that identity mixed with
this machine's host options and read with the usual checks; it keeps that translation for the rest of the session, so
no pipeline is built twice. After the boot prewarm a background pass translates every stored input that has a previous
translator's entry again under the current one, on its own translator child at idle priority that waits while the
game translates. A program the new translator refuses keeps its old translation and is logged. Once the pass has gone
through every such input, the old entries of what it covered are dropped: loose files are deleted, and a pruned copy
of shader-cache.pack replaces the pack at the next start when the pack is still the one it was copied from.
--prepare-shaders still translates everything under the current translator. NX2NAVI_SHADER_CARRY=0 keeps the earlier
behaviour.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Small ranges the game invalidates and reads back (the exposure meter) are copied from guest memory into the mirror at every submission that uses their pool, so a CPU write made between recording and the GPU's run is not lost; NX2NAVI_READBACK_SYNC=0 turns it off.
While the boot builds the recorded pipelines a marker file exists next to the pipeline cache. A start that finds it left over records the GPU and driver in pipeline-prewarm-skip.txt, and starts on them build those pipelines at their first draw instead.
CommandBufferWaitEvent is now recorded as OP_WAIT_EVENT. The replay waits for nothing but counts the waits it reaches before their signal, and how many submissions later that signal comes, in the NX2NAVI_TRACE=gfx frame line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
Each file that contains copied code names its copyright holder and license and points at THIRD_PARTY_NOTICES.md; zlib code keeps its full notice and the mark that it was altered. Comments and probe text only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments say what the code does and why, docs/maxwell.md keeps the Maxwell facts, and fetch_refs.sh clones only the checkout the build reads. Code is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The patch sets of recomp/patches-v111.txt read the game's 2560x1440 picture and 1920x1080 scene sizes from the patch page, give every 16:9 camera the picture's ratio, squeeze the UI's full-screen projection into the middle 16:9 (fades and the menu backgrounds that redraw a capture stay full width) and pillarbox movies. runtime/widescreen.c sets them up from the new Screen tab row Widescreen (NX2NAVI_GAME_ASPECT: 16:9, window, 21:9 or 32:9, at the next start).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ocarina steps its game by the clock (the same frame-numbered walk covers half the distance at 120 fps), so in a build
without the fps60 and realtime patch sets the pacer holds frames to the frame limit alone: any rate from 30 to 1000,
the monitor's rate, or unlimited (164 fps average in Kokiri Forest). The menu always shows the row, with 165, 240 and
Unlimited added. NX2NAVI_PACE_HZ sets the pacer's rate for tests.
A compute pipeline is compiled outside the pipeline lock, and one compile serves the replay and the boot prebuild
alike; the prebuild uses half the CPUs. The guest waits at most NX2NAVI_PREWARM_WAIT_MS (3000) for the boot prewarm,
which then goes on in the background (a cold start waited 9.8 s). NX2NAVI_TEST_PAD_AT attaches the test gamepad late
and presses it, to test a controller switched on in play.
README, docs/architecture.md, docs/controls.md and AGENTS.md describe the Ocarina build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The NVN2 front end and the SM86 to Maxwell shader lowering (tools/xlate/AmpereLowering.cs, mesh and task stages,
bindless texture pools, BRX jump tables, the NVN2 compute c0 layout) with four translator patches (0017-0020) bring the
opening of the game to 58-60 fps on the Vulkan renderer.
This round fixes the haze: SM86 IPA is lowered as a plain Maxwell IPA.PASS (texture coordinates were corrected for
perspective twice); LDSM, TXD, both F2IP byte forms, HMMA, MOVM and HFMA2.RELU are lowered and spill mode has eight
uniform register groups; conditional rendering keeps the newest counter report per address and treats a report as
pending until its value is in guest memory, so occlusion tests and the depth clears around the depth copy behave; and
the upscaler stand-in decodes the post pass's sRGB picture before writing the linear target. Tensor-core programs keep
the stand-in unless NX2NAVI_NATIVE_TENSOR=1, because their lowering does not match the hardware yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>