The ocarina manager counts the length of each note and rest in whole 60 Hz frames and adds the time manager's frames
per update to that clock at four sites (main+0x32926c8, main+0x3293410, main+0x3296b08, main+0x3296da4) through
fcvtzu, which cuts the step toward zero. Above 60 fps the step is under one frame, so the clock never moved: a
recording kept its notes without their lengths (the recorder leaves a length of 0 out), the 360 and 120 frame limits
never came, and a recording ended after a fixed 60 frames of its timer instead of following the clock; at the 40 and
45 fps limits it counted one a frame. The set replaces each fcvtzu with nine words that add the fraction carried on
the patch page's +264, store what is left after the cut (NaN and negatives as 0) and convert the sum. Steps of one
and two frames give the game's count with the fraction left at 0, so 30 and 60 fps are unchanged. The words call
nothing and take no lock (one of the functions returns through an unsaved x30), and use only v16-v18, which the three
functions never name. The set is appended so that no existing set's index moves; ocarina_timing = 0 gives the game's
words.
Not reproduced before the change (read from the code). The generator's check of the original words passed against
the 1.1.1 main, the new tests/ocarina_timing_check.py passed (.venv, Unicorn), as did frame_step_check,
text_speed_check, sprint_check, movie_timeline_check, cfg_parse_check, launcher_test and cfg_rows_check.
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>
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>
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>
The physics' two water managers and the game's interactive floor simulation (the Deku Tree's ground webs) step their
surfaces in fixed steps of 1/60 s, as many whole steps as the frame's time holds, and drop the fraction, so every
frame shorter than 1/60 s ran none and the surfaces stood still above 60 fps. The new set owater (water_steps, on by
default) calls the page between the division and the game's cut, which keeps the fraction per object; at 60 fps the
count is the game's.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The patch set text_speed scales the typewriter speed of a message the game lets the player skip (fast, three times as
fast, its scripted waits as long) or runs the game's own skip on the first frame of each page (instant). Messages the
game marks unskippable, such as those that turn their page on a timer, keep the game's speed. The setting is live and
has a row on the Game tab in all four menu languages; with it at normal the set is off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stance site called page +0xf50, which the 0.10.0 line gives the water ripples above 60 fps: the second
registration ends every 1.1.1 start with "registered twice", the sprint button off included. It calls +0xf30 now, and
the comments say which slots the other sets hold. The set's switch is NX2NAVI_SPRINT_BUTTON_PATCH, not a name another
open request uses for a set of its own.
The stand-still timings count 60 Hz frames (frame_step_frames), so a toggled sprint no longer ends on a short stop at
high frame rates. A press is decided at its start for its whole length: kept from the game and the sprint's when it
begins in gameplay (B: while Link moves), else the game's alone, so a held B is not handed to the game half way and a
B that closes the pause menu starts no sprint. A sprint switched off with the button setting does not come back by
itself, Link's state is read only where the memory is readable, and the row, nx2navi.ini.example and the controls
page say that B is the jump under the game's Swap Jump Controls and name the button without assuming the layout.
tests\sprint_check.py covers the press latch, the menu press, 240 fps and the switch-off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The two sites that count a movie's event frames from the shown picture's timestamp were part of the real-time step's
set, so the only way back to the old count also turned the step off. They are set omovie now, NX2NAVI_MOVIE_PTS
(movie_pts in nx2navi.ini), on by default.
The comment says what the new count changes at the default movie cap: nothing while the decoder keeps up; in a stall
the cues wait with the picture instead of going on with the soundtrack, and step by 2 or more when a picture is
dropped. The counter's reader, main+0x3b3ec10, hands it out as a float frame through a virtual call; its callers were
not traced, so whether a cue on a dropped picture's tick can be missed is said to be unknown, which is what the
switch is for.
The movie cap's comment keeps the measurements it rests on, the narration 2.4 times early and the fade at 65 s of 154
at 144 fps, with one sentence on the new sites. The new test is described in prose with what it does not cover,
instead of a row in the lifter's table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A scalar result (an FP operation, a scalar load, FMOV, a compare, a conversion, a 64-bit vector operation) was assigned
to the register as a zero-extended 128-bit integer, which the compiler writes as two 8-byte stores, and INS and a
single-lane load wrote their lane alone. The next 16-byte load of the register then cannot take its value from the
store buffer and waits until the stores have left it.
Every such write now builds the whole value in a vector register and stores it once (runtime/simd.h): VSET puts a
scalar in the low bits with zero above it, VSET128 writes a 128-bit value built in an integer, and VINS loads the
register, replaces one lane and stores all of it. VINS passes the value through an empty asm statement, without which
the compiler turns the load, the insert and the store back into a store of the lane.
In the 360 guest functions a play profile sampled, stores of a part of a V register fall from 64322 to 4 and whole
stores rise from 10932 to 43665; the four left copy a 16-byte guest load that the compiler itself splits because the
memory was just written in halves. Host instructions fall from 1448075 to 1427163 and stores to the Cpu from 177160 to
145566; its loads rise from 141090 to 143521, as an insert loads the register it changes.
The lifted text of FP and SIMD code changes with this, so those files are compiled again.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The new patch set occlusion_off makes six reads of two parameter bytes read 0: CreateZPrePassOcclusionQuery and CreateSamplesPassedQuery, where the game first writes their options and where an update compares and stores them. The game is then left without its occlusion queries, their reports and the conditional renders that depend on them, and draws what it would have tested: more draws, and no query results to wait for.
patch_occlusion_off = 1 turns it on at the next start. The key has a row in the settings table, which gives the set its default, a section and an entry in nx2navi.ini.example. What it costs and saves in play has not been measured.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The game draws the whole picture into the 543x306 target of the save's picture, so a picture wider than 16:9 came out squeezed. The new set wide_save has one site in the viewport helper, where it loads the address of SetViewport: a viewport of exactly 0, 0, 543, 306 gets the patch page's two numbers added to its x and its width, without branches or flags. widescreen.c writes them for a picture wider than 16:9 and switches the set on only then; the scissor, set before, keeps the target's size.
C generated before the set existed keeps the other widescreen sets, and the log says that the save's picture is squeezed.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The water renderer runs trunc(step) times the water's substeps of its ripple simulation a frame, which is none while the step is under one frame: the rings stood still above 60 fps. A new site of the set ostep hands the cut to frame_rate_water_host, which adds the step to a remainder kept for the water object and returns the whole steps in it. The waters have a table of 64 slots of their own, as fewer slots than surfaces would have them take each other's in turn.
frame_step_check.py calls the water and day hooks as their sites do. It also gets the three platform stubs it has lacked since the day clock's hook, without which it did not link.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The day's update (main+0x3cdf70) adds trunc(2 x frames per update) game seconds a frame. With the set ostep's step,
the display interval in 60 Hz frames, that sum lost its fraction every frame: 1 a frame from 61 to 119 fps, 0 above
120 (the clock stood still), and at 60 fps a step a little under one frame dropped a second now and then. Its convert
now calls the patch page's +0xf60 (runtime/frame_rate.c frame_rate_day_host), which keeps the fraction per counter and
hands out the whole seconds. NX2NAVI_TRACE=perf logs, every 10 s, the seconds given beside those the cut would have
given.
Measured in Kakariko, 10 s windows: at a 60 fps limit 998 game seconds against 881 from the cut alone; at 144 fps
1,201 against 219 to 972 (the steady 1,200 a 10 s window should have).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Besides the per-frame update that the set ostep patches, the graphics system's update (main+0x3b4ea90) divides 60.0 by
the engine's nominal rate and stores the result as a global (main+0x4d41fc8): the 60 Hz frames of an update. Its one
reader is the buttons' hold-to-repeat timer (main+0x3483680), which adds it to each held button's time and repeats the
button once that passes the button's delay and period. Above 60 fps the global stayed 1.0 a rendered frame, so a held
button repeated fps / 60 times too often. Two more ostep sites load frames per update from the patch page there, so the
two steps are the same number. NX2NAVI_SPEED_PROBE=1 adds the global to the fps line.
Checked: the generator's check passes for all 34 main sites against the 1.1.1 module, and the generated C differs from
before only in the patched function. Two 60 s runs of an adult save at about 135 fps, uncapped: before, the global
step read 1.000 frames while the game's step was 0.42-0.50; after, it read 0.43-0.46, inside the game's step range;
frame rate unchanged (132-138 fps in both).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brought onto the line with FSR 4, its frame generation and the debug menu. Where both sides had added to the same
place, both stay: the patch table keeps the pickup count and the free camera, the settings list keeps both sides'
names, the menu includes the co-op page and the debug menu, and the launcher builder takes the pipeline list and the
FSR 4 folder. Both sides had added the menu's typed-character event; one definition stays, with the window code that
passes characters outside the basic plane.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
With it on, the game draws from a camera of the menu's instead of its own, in play and in cutscenes. The left stick moves it along its view and sideways, the right stick turns it, ZR and ZL raise and lower it, R makes it faster and L slower, and Link stands meanwhile; or the camera stays where it is and the pad plays the game.
It works inside the game's own camera update: a new patch site sends the instruction at which the frame's position, target and up vector are final to a host function, which replaces them for the camera that follows Link and the one the events direct, so the game draws, culls and loads detail for what the free camera sees. The camera of the interface's 3D models and the one the sound follows stay the game's. The set is off unless the menu switches it on. A teleport's arrival starts the free camera again from the game's view.
Tested in Hyrule Field with scripted stick input (turn, rise, strafe, with Link's position unchanged), with the pad playing while the camera stays, across a teleport to Kakariko Village, during the Kakariko arrival cutscene, and switched on by a click in the menu.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
mouse_mode = direct is the default, but on 1.1.1 it did nothing: only the 1.4.3 patch table had the sites, so the mouse always went through the stick emulation, which gives no turn at slow mouse speeds and tops out at about 107 degrees a second. The 1.1.1 table now has the set mouse_direct (on by default): at main+0x37c45c, where the camera's input update holds the processed stick in s1 and s0, a host call performs the displaced store and adds the raw mouse counts to both, so the mouse meets no dead zone and no curve; at main+0x37c4f0 a second call clears the idle bit for an update the mouse moved in. The counts are divided by the frames the game steps per update and by the stick camera speed, so the same hand movement turns the same angle at every frame rate. The Controls tab gets the mouse mode and mouse sensitivity rows, and mouse_mode = stick or NX2NAVI_MOUSE_DIRECT_PATCH=0 gives the old behaviour.
Checked here: the generator lifts the 1.1.1 game with the two sites (it compares each site's original word), the lifted game compiles and links with the runtime, and tests/mouse_direct_check.py passes its 1.1.1 harness (register contract, displaced store and load, equal turn at every frame rate and stick speed, the reset pause, capture and mode). Not checked here: the game was not run, so aiming with the mouse was not tried, and the differential tests against Unicorn were not run because no Python on this PC has Unicorn. The contributor measured 0.0800 degrees per count on both axes in the game.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
(cherry picked from commit bb8fecfbe43517006eafd7be9c22e8dbe9601de7)
The window that shows a pick-up above the character counts its number up to the new total by a whole step a frame and closes once they are equal. The game rounds that step down, and below one 60 Hz frame per update it was 0: above 60 fps the window stayed until the area was left, and for larger amounts the counting sound was started again every frame. The step is now floored at 1, switchable with pickup_count.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Above 60 fps players fell off moving platforms. The physics system takes its solver step count for a frame as
trunc(frames per update * 4 + 0.001): 4 at 60 fps and 8 at 30, the rates the game ships with, but 3 from 61 to 80
fps, 2 to 120, 1 to 240 (144 solver steps a second at 144 fps against 240 at 60), and 0 in a frame shorter than a
quarter of a 60 Hz frame. The solver skips its whole solve and sub-integration loop at 0, so such a frame resolves no
contact or friction and moves no rigid body. At a 240 fps limit, which "unlimited" paces at too, that was the frame
after every late one: the pacer keeps its phase, so the next display interval is shorter than the period.
The new patch set osolver rounds the count up, ceil(frames per update * 4 - 0.25): 4 and 8 as before, never fewer
steps a second than at 60 fps, at least 1. physics_steps = 0 gives the game's own count.
The step itself was the mean of the last five display intervals and changed with every frame: intervals of 5 and 30
ms in turn gave steps of 15 and 20 ms in turn, and one 60 ms frame at 200 fps took the step from 5 to 16 ms for five
frames. The game never saw a changing step on the console. The step is now the mean of the last 12 intervals without
the longest and the shortest, is the pacer's period when within 3% of it (at a 60 fps limit exactly one frame, every
frame), and repays the time the game was not given at up to a tenth of the step, so game time still follows the
clock. It is at least 1/480 s. step_steady = 0 gives the mean of five again. With trace = perf the fps lines show the
shortest and longest step and the time owed.
tests/frame_step_check.py drives frame_rate.c with made-up display times and the rest of the runtime stubbed.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The loading threads feed each chunk they read to the game's stream decoder, which is where most of the
decompression time goes. That call now reaches a host stream that writes straight into the game's output
buffer and keeps the game's state fields and results. A stream is taken from its first chunk or left to
the game; zstd_check = 1 runs both and compares the outputs. --probe-zstd checks the host streams on
frames it makes itself.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A profile of a play session showed the game's own zstd decoder, lifted from ARM64, taking 16 to 19% of all running CPU time on the loading threads: the patch set that sends decodes to the host's libzstd had sites for 1.4.3 only. recomp/patches-v111.txt now has the three calls of ZSTD_decompressMultiFrame (main+0xeb380) of this build, checked against its main by the generator; a lift with them changes three chunks. ov_zstd.c takes the guest function's address by game version.
The sites have not been through a check run on this build, and 1.4.3 has a caller that relies on the game decoder's own result, so on 1.1.1 the host function passes every call on to the game's decoder unless zstd_native = 1 is set or zstd_check = 1 runs both and logs what differs. The loading threads' stream feed (main+0xf5c80), which decodes most of the data, is not replaced yet.
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
The res_fixed patch sends the game's dynamic-resolution controller down its fixed path, which returns its scale with
state 1 or 2. The caller makes a new scene scale only on state 0, so the menu's mode reached the scene only by chance:
a mode switch in play left the scene at its old size, and a scene reload put it back at full size. The fixed path now
returns state 0, and the page's scale applies every frame. Ocarina also no longer logs that DLSS needs
nvngx_dlss.dll and follows render_scale, which was true only of the builds with a scene patch set.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The game sizes its scene each frame from a GPU-time controller. The new patch set res_fixed takes the controller's
fixed path and reads the scale from the patch page, so DLSS Quality, Balanced, Performance and Ultra Performance
render at 67, 58, 50 and 33 percent of the picture and DLAA at all of it. The set goes on once the controller has run
for a few frames, and the menu's Upscaling choice lists the modes and switches them live.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
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>
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>