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>
A chunk's lookup that needs its turn returns no image and its op is replayed in turn, but the first one was logged as "render target ... is not renderable here: left out", which read as depth and cube targets the replay could not draw. Nothing else changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vulkan has no quad or line-loop topology. Draws without indices were rewritten already; an indexed quad list went to the GPU with its indices under the triangle list topology, three at a time, joining corners of neighbouring quads, and an indexed loop lacked its closing line. Off by default and read at start; no log of Ocarina has shown such a draw so far, and the new line says when one comes. tests/index_rewrite_check.py checks the index lists.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A draw that finds every query slot still waiting for the completion thread goes uncounted, and the report after it said only what the other draws counted, at least 1: the game takes the object it tested for all but hidden for that frame. With the setting the report writes the larger of that and the last complete count reported into the same record. Off by default and read at start; the count of such reports is logged at the 1st, 10th, 100th and so on either way. tests/oq_hold_check.py checks the rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Through a nearest filter each of the game's shadow compares tests one texel, so shadow edges are stair steps of shadow map texels that crawl over the ground as the sun cascades follow the camera. The setting is off by default and read at start; with shadow_scale above 1 the samplers were already linear.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The wait stays shader_compress_idle_s (30) at the roomy and normal levels; every level compresses losslessly, and a
shorter wait only means a shader read again soon after is decoded once more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A thread samples every two seconds the video memory budget against this process's use (vk_mem_vram) and the system's
commit headroom and free physical memory (plat_commit_info, plat_mem_info), and turns each side into a level: roomy,
normal, tight or critical, with five points of hysteresis upward and a 256 MiB floor. Changes are logged, and the
memory report's system line ends with both levels. MEM_BUDGET_TEST_VRAM_MB and MEM_BUDGET_TEST_RAM_MB pretend a side
is that small, so that what gives memory back can be tried on a large machine; the texture cache's rules are unchanged.
tests/mem_budget_check.py checks the levels and the hysteresis.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The shader memory cache held 777 MiB of SPIR-V for 7115 shaders for the whole session. A background thread now
compresses the SPIR-V of each shader no reader pinned for shader_compress_idle_s seconds (zstd level 1, 26% of the
bytes on the test install's 44,151 modules) and frees the plain copy; readers pin it through gfx_shader_spirv_pin and
get it decoded back. What the drawing thread compares at pipeline misses and draws, the module's FNV-1a and its Binding
decorations, is kept with the shader, so module_binding_missing, the water corrections and the DLSS, crumbs and skip
lists decode nothing. A compute pipeline created at a dispatch and a mesh shader's first pipeline key still decode on
that thread, and the memory report counts those decodes apart, with the SPIR-V as held against uncompressed and the
shaders held compressed. shader_compress = 0 keeps it plain.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Readers pin the store and get the plain words, decoded from the frame when the plain copy is gone; one packer makes the
frame of a store unused for a while, checks it by decoding it, and frees the plain copy. The lock is held for a few
loads and stores only, never across a decode, an encode or a free. tests/spv_store_check.py walks a store through its
states and runs four reader threads against a packer for three seconds, comparing every word they read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
floor_watch.c takes up to twelve of the floor scan's survivors (the platform's rigid body controller unit first, then one per object at the pivot's height), watches them four at a time through the debug registers on every thread (platform.h plat_watch_set, Windows only), counts each access's call chain in a lock-free table and logs the chains with the executable's symbol names after each pass. prof_syms gains a callback form for that.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The character controller's three follow-surface modules kept only a tenth of the floor's velocity whenever the physics' inverse step passed 100, which every update above 100 fps did. The gate now compares with 100 over frames per update from the patch page above 60 fps, and with the game's 100 at 60 and 30 fps.
Co-Authored-By: Claude Opus 5.5 <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>
Link is carried by the Deku Tree's floating platform fully at 60 fps, about a tenth at 160 and not at all at 240,
which follows the solver steps an update (4, 2, 1 with physics_steps = 1) rather than those a second. The new set
osolverfull raises the rounded-up count to the 60 Hz frame's 4 in every update; physics_steps = 2 switches it on and
osolver off at start, and the mode is logged. 60 and 30 fps keep the game's 4 and 8. It costs more physics time above
60 fps: 576 solver steps a second at 144 fps and 960 at 240, against 288 and 240 with 1, the default, which stays
until a run confirms the carry. frame_step_check runs the lifted words of all three counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Released textures go once they have gone unused for the hold, and what a switched-off upscaler or SMAA held is freed, with no setting to keep them. A vram_use line left in a settings file is known and read by nothing. Memory use is to be decided by the card's and the system's budget instead.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
Main renamed the switch so that an old "coop = 1" turns nothing on; the launcher's check for Steam's library still read the old name.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hd_tick picks a report under the locks and writes it to the pair after letting them go. When input_rumble_hd_stop
wrote the neutral value and closed the pair in between, that write opened the two devices again and left the
Joy-Con vibrating after the exit. The stop now ends the pair (hd_pair_end), and hd_pair_write checks that under the
lock it holds for the write, so nothing is written after the stop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
vk_aa_mode is asked at every compute dispatch, on whichever thread records it, and the replay workers record at once.
The frame was checked and claimed without atomics, so two threads could both free the idle work images. The first
call of a frame now claims it with a compare-and-swap, and only that call frees them and takes the setting.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Play ran steamlib.ensure on the Tk thread, co-op on or off. Without the file that walks up to 60,000 folders
of the player's Steam libraries and checks signatures, and the window stood still meanwhile. Play and the Steam
shortcut's start now call it only when co-op is on in the game's settings (steamlib.wanted, read as the game reads
them), and Play runs it on a thread of its own and starts the game from the window's thread when it is done.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A group too small to share out has fewer than 64 draws (2 x CHUNK_DRAWS), so a threshold of 64 or more never matched
in group_start and the setting did nothing. The setting's range is now 0..63, the menu's slider ends at 60 in steps of
4, and a value of 64 or more from nx2navi.ini reads as 63 with one line in the log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stage calls ran on guest threads and the present thread without the lock the NVIDIA path holds, and a stage call
with the mode on could come after the switch-off on another thread and leave the driver on. The stage calls now hold
the lock shared and make none once the path is off; the switch-off holds it exclusively and waits for a stage call in
flight. Shared, because the input stage waits inside the call while the present thread makes the present stage of the
frame before, which must not wait for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The automatic count gives that driver no workers, since a game with workers lost its device there, but a number in
nx2navi.ini or the menu starts them without a word. The number still applies; the log now says that workers on this
driver have caused device loss and that auto or 0 avoids it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With frame generation and no upscaler, idle_release freed every DLSS size other than the last one at each present
from 600 presents after the switch, without waiting for the GPU. The scene's size can change from frame to frame, so a
size the frame before used could still be in flight. A size now goes only after it went unused for as many
evaluations; its DLSS feature, unused since the upscaler went off, is released as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
On a laptop with the vendor's processor graphics and a card of the same vendor, both installed drivers list the
card, and pick_device took the first listing of the best device type. That listing can come from the older driver
installed for the processor graphics, which was reported to crash in its shader compiler with the card. Among
listings of equal rank with the same vendor and device ID, the one with the higher driver version is taken now, and
the log says so. A GPU chosen by index or name is unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The job (main+0x415300) hands the time manager's step to the sound system in its phase 0 and runs the sound objects'
and the system's updates in phases 1 and 2. With the set ostep that step is a fraction of a frame above 60 fps, and fed
fractions the game has been reported to keep sounds held after an area unloaded until no voice was free, new sounds
and then the music going silent until a restart. The new set osound (sound_steps, on by default) calls the page's
+0xf10 after the job's prologue: phase 0 adds the step to what the job is owed and runs once a whole step has built
up, phases 1 and 2 follow that choice, and a job that does not run gets phase 3, which returns through its own
epilogue. After phase 0's load of the step the page's +0xf00 makes a fraction one whole step. At 60 fps and below
the job runs every frame with the game's step. Both sites were checked against the module; frame_step_check covers
the gate at 60, 30, 144 and 240 fps. Needs the game code generated again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every interactive floor of a place and both water managers ask frame_rate_sim_host once an update, each for its own
object. With 8 slots, a place with more such objects than that had them take each other's slot in turn, start
without a remainder every update and stand still above 60 fps, as before the set owater. The table now has 64 slots,
as the ripples' has, and frame_step_check runs 32 surfaces at 144 fps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Steam co-op took the setting name of the old beta's switch, coop. A player whose nx2navi.ini still had coop = 1 from that beta got the new co-op switched on at the first start of the new build, unasked: the game signed in to Steam as its test app. The switch is now coop_steam (NX2NAVI_COOP_STEAM), off by default, and coop is a retired key like the old co-op's other settings: known, read by nothing.
--probe-cfg now loads a file of the old co-op and checks that its keys are no unknown keys, that coop = 1 leaves the new co-op off, and that coop_steam = 1 turns it on; it passes on a build of this commit. The game itself was not started with the change.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Scan run 2 showed the platform moving at full speed above 60 fps while Link stays behind, so the fault is in what
carries him. Once the scan has its survivors, it logs the 0x500 bytes around the one deepest below Link, names the words
there that point into main's data (vtables), and walks the heap once more for the words that point at the starts of
the nearest objects, each logged with its own object's vtable, so one run names the classes to trace.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an RX 580 the second device-local heap (2 GiB) takes what the first has no room for. On a GPU with one device-local
heap (an RTX 3050 Laptop, 4 GiB, 16 GiB of RAM) the budget wrapper and the texture placement sent everything past the
heap's reserve to system memory instead: 4096 and more texture images and every other allocation, held in the
process's commit (21 GiB). The system's commit ran out (31730 of 32023 MB), host memory imports failed, a pool was
left without a GPU buffer and the GPU wrote through its address: device lost. Now an allocation leaves out a full heap
only when another device-local heap has room, and a texture's second step allocates as before, past the budget, where
the driver keeps it in video memory or moves it out itself, and NX2navi's own VRAM pressure drops textures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scan run 1 armed on a floor that only sinks and found points 3.4 m above Link that follow him (the camera's). The scan
now arms on a straight horizontal drift at a steady height, snapshots from 4 m below Link to 0.5 m above, keeps the
triplets that moved with him and lie more than 0.3 m below him or more than 0.5 m from his feet, and logs each of them
(64 at most) once a second with its move over that second.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Deku Tree's floating platform is not one of the engine's ground groups, and where its position lives is not known.
With floor_scan set (off by default), once Link has been carried at 60 fps for 30 frames with the left stick at rest,
one snapshot of the game's heap and alias regions keeps every float triplet near him; 30 frames later the triplets
that moved as far as he did are kept, and from then on each frame logs the twelve of them farthest from him with their
addresses, positions and moves. The snapshot logs how long it took.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run 5 named only the four nearest groups, and the still map pieces nearer to Link may have crowded out the platform's
group, whose translation is its pivot rather than its surface. A line now names up to six groups within 12 m: those
that moved since the last present first, then the nearest still ones.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game derives every time category from the patched frames per update (main+0x3b4e300 and main+0x2c84c make the
inverse, seconds and rate from it), so the lines can show whether what a floor's motion reads matches the step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-op loads steam_api64.dll from beside ocarina.exe, and the repository may not hold that file. Before Play and before a start from the Steam shortcut, when the file is not there, launcher/steamlib.py now copies one: one a release ships in its bin folder if it has one, else the newest one in the games of the player's own Steam libraries that Valve signed (WinVerifyTrust and the signer's name) and that has every call runtime/steam.c uses. Nothing is downloaded, the search takes a second or two once, and with no Steam or no suitable file the game starts as before and the Co-op tab says what is missing. The tab's two texts about the file say so in the four languages.
Checked: four new tests on made-up library folders (too old, unsigned, too deep, a second library, a shipped file first, none said once) and the launcher's 190 tests pass; on this PC the search found 40 files in 1.2 s, 25 suitable, and copied a Valve-signed one; that file's library version, an older one than the game was run with, signs in as app 480 and makes a lobby in a program without the game. Not run: Play in the launcher itself with this change.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The last run found no moving group near the drifting platform, which leaves open whether its drift is a ground group
at all. Each line now gives up to four groups within 8 m with their translation and move since the last present, and
with none that near, one line in 30 presents still gives Link's position and move.
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 manager there holds 5345 entries, which the 512 cap skipped. The probe now checks the entries' range once a present
and copies them and the rows of the groups they name (those in the guest heap) with mem_copy_readable, which takes one
lock and no system call per read; the cap is 16,384 again.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>