# Code patches for Ocarina of Time (Switch 2) 1.1.1 main (recomp/patches.py). The sites were found in this build's
# main; their original words are checked by the generator. runtime/widescreen.c writes the patch page and switches the
# sets: +96 the game image size and +104 the scene size as {w, h} in one 64-bit word, +112 the 16:9 ratio a camera is
# matched against, +116 the ratio that replaces it and +120 the tolerance of the match, as floats; +144 the bit
# patterns of the UI's full-screen orthographic projection (m00 = 2 / 3840, m11 = 2 / 2160), +160..+175 and +208..+223
# the four screen vtables that keep it, +176 the UI's horizontal squeeze (16:9 over the picture's ratio) in four lanes, +192 the
# movie's margin on each side as a share of the width.
#
# The matrix-update, UI and movie sites: Copyright (c) 2026 Ivan0xFF (MIT; see THIRD_PARTY_NOTICES.md). Each is
# written without branches or flags, as the generator's patch sites need. The set names differ from the other patch
# files' (output, scene, aspect), whose page layout runtime/resolution.c owns.

set wide_out    NX2NAVI_PATCH_WIDE_OUT    0
set wide_scene  NX2NAVI_PATCH_WIDE_SCENE  0
set wide_aspect NX2NAVI_PATCH_WIDE_ASPECT 0
set wide_hud    NX2NAVI_PATCH_WIDE_HUD    0
set wide_movie  NX2NAVI_PATCH_WIDE_MOVIE  0
set res_fixed   NX2NAVI_PATCH_RES_FIXED   0
set ostep       NX2NAVI_OCARINA_REALTIME  1
set zstd        NX2NAVI_ZSTD_NATIVE       1
set osolver     NX2NAVI_PHYSICS_STEPS     1
set opickup     NX2NAVI_PICKUP_COUNT      1
set freecam     NX2NAVI_PATCH_FREECAM     0

# Real-time step: the framework's per-frame update (main+0x6e6be0) gives the time manager **[main+0x4c7e890] its
# nominal frame through main+0x3b4e300 (+0x10 frames per update, +0x14 the inverse, +0x18 seconds, +0x1c the rate),
# as 60 (or 30) over the target refresh rate it keeps, 60. Gameplay steps base (+0x1c0, 1.0) times frames per update,
# the interface and the frame counter at update +0xd58 step frames per update, so above 60 fps all of it ran fast.
# The update takes frames per update from the patch page instead: +256, the display interval in 60 Hz frames
# (runtime/frame_rate.c). x10 is rewritten before it is read again, and the tst's flags feed only the fcsel.
site ostep main 6e6cc0 1e220c21 @page:x10   # fcsel s1, s1, s2, eq -> adrp x10, page
site ostep main 6e6cc4 1e201828 bd410148    # fdiv s8, s1, s0 -> ldr s8, [x10, #256]
# The engine's second step: the graphics system's update (main+0x3b4ea90) also divides 60.0 by the same nominal rate
# (its +0x2c8) and stores the result as a global, main+0x4d41fc8, the 60 Hz frames of the last update. Its reader is the
# hold-to-repeat timer of the buttons (main+0x3483680), which adds it to each held button's time and repeats the button
# once that passes the button's delay and period in 60 Hz frames; above 60 fps it added 1.0 a rendered frame, so a held
# button repeated fps / 60 times too often. The global takes frames per update from the patch page too, so the two steps
# never differ. x9 is not read in the function and is free across the call that follows; the store keeps x8.
site ostep main 3b4eaf4 5e21d800 @page:x9   # scvtf s0, s0 -> adrp x9, page
site ostep main 3b4eaf8 1e201820 bd410120   # fdiv s0, s1, s0 -> ldr s0, [x9, #256]
# The time of day: its update (main+0x3cdf70) adds trunc(2 * frames per update) game seconds a frame to the seconds of
# the day (+0x3c of its object, carried into the day count by the code after it). With the step above below one frame
# per update the sum was 1 from 61 to 119 fps (the day at 83% of its pace at 100 fps) and 0 above 120, where the clock
# stood still and the sun swung back and forth around a frozen hour; at 60 fps a step a little under one frame lost a
# second too. The cut calls the page's +0xf60 instead (runtime/frame_rate.c frame_rate_day_host): the frame's seconds
# with the fraction kept for the next frame, so the day keeps the game's pace at any frame rate. x30 is the function's
# own saved one, and the call at main+0x3cdfc8 sets it again.
site ostep main 3cdfc0 1e380008 952b4be8   # fcvtzs w8, s0 -> bl page+0xf60

# Physics solver steps: the physics system's step setup (main+0x38aeb80, and the same code in main+0x38afc10) gives the
# rigid body world its solver step count for the frame, trunc(frames per update * count + 0.001) with the count at
# system +0x244 (4, set by the constructor main+0x38ae800), and the world keeps the result's inverse beside it (world
# vtable +0x88, main+0x3a24c20: world +0xc48 and +0xcb8). That is 4 at 60 fps and 8 at 30, the two rates the game ships
# with: 240 solver steps a second. Below one frame per update it truncates to fewer a second (3 a frame from 61 to 80
# fps, 2 to 120, 1 to 240: 144 a second at 144 fps), and to 0 under a quarter frame, with an infinite inverse. The
# solver (main+0x180b0a0) runs its solve and sub-integration loop that many times and skips it at 0 (main+0x180b380):
# such a frame solves no contact or friction and moves no rigid body, so whatever stands on a moving body is left
# behind in it. At a 240 fps limit that was the frame after a late one, whose display interval is shorter than the
# period (runtime/frame_rate.c ocarina_step). The count rounds up instead, ceil(frames per update * count - 0.25): 4 and 8 as before at 60 and 30
# fps, also with a step a few percent off them, never fewer steps a second than at 60 fps, and at least 1 down to a
# 16th of a frame. s2 is loaded only for this sum, and none of the words reads or writes the flags.
site osolver main 38aebe4 bd440d02 1e2a1002   # ldr s2, [x8, #0x40c] (0.001) -> fmov s2, #0.25
site osolver main 38aebf0 1f010800 1f218800   # fmadd s0, s0, s1, s2 -> fnmsub s0, s0, s1, s2 (s0 * s1 - s2)
site osolver main 38aebf4 1e380001 1e280001   # fcvtzs w1, s0 -> fcvtps w1, s0 (round up)
site osolver main 38afd10 bd440d02 1e2a1002   # the same three words in the second copy
site osolver main 38afd1c 1f010800 1f218800
site osolver main 38afd20 1e380001 1e280001

# Pick-up window count: the window that shows a pick-up above the character (its update is main+0xa67e50) moves the
# number it shows (+0x360) to the new total (+0x35c) by trunc(frames per update * max(1, difference / 50)) a frame, and
# closes only once the two are equal. Below one frame per update that is 0: above 60 fps, and at 60 while the step runs
# a little under one frame, the count never ends, so the window stayed until the area was left, and for an amount over
# 9 the counting sound was started again every frame and its closing sound never came. The increment is floored at 1
# without the flags (clz is 32 only for 0, so increment += clz >> 5); at one and two frames per update it is what the
# game computes. Above 60 fps a small amount then counts one a rendered frame, faster than at 60. w13 held the time
# manager, last read at main+0xa682ec, and every path from the site reaches a call before it reads x13 again.
site opickup main a68300 1e38000c 1e38000c 5ac0118d 53057dad 0b0d018c   # fcvtzs w12, s0; clz w13, w12; lsr w13, w13, #5; add w12, w12, w13

# Mouse camera: the camera's stick input update (main+0x37c300) puts the right stick through a radial dead zone of
# 0.005, scales the radius to one, clamps it and raises it to StickExponent (2, the default of
# game__component__PlayerCameraParam); then come the reset timer at +0x5e0 (the stick reads zero while it runs), the X/Y
# inversion (w21 bits 1 and 2) and the idle test (flags +0x530 bit 4: the processed stick under 10%). At 37c45c s1 and
# s0 hold the processed x and y for the stores at +0x538 and +0x53c: the host callback performs the displaced store of
# s1 and adds the raw mouse counts to both (runtime/mouse_direct.c camera_input_oot), so the mouse never becomes a stick
# value and meets no dead zone or curve. At 37c4f0, the displaced ldp d9, d8, [sp, #0x10] before the update's last
# `str w8, [x20, #0x530]`, w8 holds the flags: a second callback clears the idle bit for an update the mouse moved in
# (camera_activity_oot). x30 is the function's own saved one at both sites. The callbacks' addresses are page +0xfd0
# and +0xfc0; the page is main+0x4ea0000 on this build.
set mouse_direct NX2NAVI_MOUSE_DIRECT_PATCH 1
site mouse_direct main 37c45c bd053a81 952c92dd   # str s1, [x20, #0x538] -> bl page+0xfd0
site mouse_direct main 37c4f0 6d4123e9 952c92b4   # ldp d9, d8, [sp, #0x10] -> bl page+0xfc0

# Game image: the graphics setup (main+0x1202ce0) fills the renderer's configuration with the size of the picture the
# game presents, 2560x1440 at every console mode (config +0x14, copied to renderer +0x9c, which main+0x346be50 reads
# for the main render target set, main+0xf49040), and the display settings object (main+0x31f3890) stores the same size
# at +0xb68.
site wide_out main 1202ee0 d2814008 @page:x8   # mov x8, #0xa00 -> adrp x8, page
site wide_out main 1202ee4 f2c0b408 f9403108   # movk x8, #0x5a0, lsl #32 -> ldr x8, [x8, #96]
site wide_out main 31f39f4 d2814008 @page:x8   # mov x8, #0xa00 -> adrp x8, page
site wide_out main 31f39f8 f2c0b408 f9403108   # movk x8, #0x5a0, lsl #32 -> ldr x8, [x8, #96]

# Scene: the display settings object's scene size (+0xb70), 1920x1080, the picture the game's upscaler takes in.
site wide_scene main 31f3a00 d280f008 @page:x8  # mov x8, #0x780 -> adrp x8, page
site wide_scene main 31f3a04 f2c08708 f9403508  # movk x8, #0x438, lsl #32 -> ldr x8, [x8, #104]

# Render scale: the game sizes its 3D scene by a dynamic-resolution controller that follows its GPU-time reports (3/4
# of the picture here). Bit 0 of an engine config word at +0x94 picks it (main+0x805bd0); the other path takes a fixed
# scale of 1.0 (main+0x806420). res_fixed always takes the fixed path and reads its scale from +240 (a float, written by
# runtime/widescreen.c from the DLSS mode). It is switched on only once the controller has run for a while, as its
# fields would otherwise stay unset and the scene stay black.
# The controller returns its scale with a state in bits 32..39. Its caller (main+0x31d3ee0) makes a new scene scale
# from the returned one only on state 0 ("changed": the bit 3 request of renderer +0xe2c, consumed at main+0x31d40b4);
# the fixed path returns 1 or 2, so the page's scale reached the scene only by chance and a scene reload put it back at
# full size. The fixed path returns state 0 instead: the caller applies the page's scale every frame, so a mode
# picked in the menu takes effect at once and survives a reload.
site res_fixed main 805bd0 360041e8 1400020f            # tbz w8, #0, #0x80640c -> b #0x80640c
site res_fixed main 80641c 9a888128 aa1f03e8            # csel x8, x9, x8, hi -> mov x8, xzr (state 0)
site res_fixed main 806420 52a7f009 @page:x9 b940f129   # mov w9, #0x3f800000 -> adrp x9, page; ldr w9, [x9, #240]

# Camera aspect ratio: the camera update (main+0x4c48c0, the same code as the camera update site in
# recomp/patches-v143.txt) copies the camera's near, far, vertical angle and ratio (camera +0x134..+0x140) into the
# sead perspective projection the renderer reads (+0x98..+0xb0; the projection's own matrix update, main+0x3485cc0, does
# not run in play: the renderer builds its matrices from these fields). The replacement loads the ratio as before and
# puts the page's ratio in its place when it is within the page's tolerance of 16:9 (the camera's value is not the bit
# pattern of the 16/9 constant, and the tolerance is 0.01); other ratios stay the camera's. fabd + fcmge + bit
# write no flags, so the alternatives agree. s1..s4 and x9 are free there: nothing reads them before the call at
# main+0x4c534c.
site wide_aspect main 4c5334 bd414260 bd414260 @page:x9 2d4e0921 bd407923 7ea1d404 7e24e464 2ea41c40   # ldr s0, [x19, #0x140]; ldp s1, s2, [x9, #112]; ldr s3, [x9, #120]; fabd s4, s0, s1; fcmge s4, s3, s4; bit v0, v2, v4

# The same replacement where sead's perspective projection builds its matrix (doUpdateMatrix, main+0x3485cc0) and in
# the three inlined copies of it the game mostly uses; the ratio goes back to the projection (+0xb0), so culling sees
# the wider view. Ratios that are not 16:9 (cube maps, square views) are kept. Scratch: x9 and three s registers that
# the next instructions overwrite before they read them.
site wide_aspect main 3485cc0 2d558400 2d558400 @page:x9 2d4e0d22 bd407924 7ea2d422 7e22e482 2ea21c61 bd00b001   # doUpdateMatrix: ldp s0, s1, [x0, #0xac]
site wide_aspect main 4f1e8 2d558ea0 2d558ea0 @page:x9 2d4e1524 bd407922 7ea4d464 7e24e444 2ea41ca3 bd00b2a3   # inlined matrix update: ldp s0, s3, [x21, #0xac]
site wide_aspect main 23b9a8 2d558c00 2d558c00 @page:x9 2d4e1524 bd407922 7ea4d464 7e24e444 2ea41ca3 bd00b003   # inlined matrix update: ldp s0, s3, [x0, #0xac]
site wide_aspect main 33b82c 2d558440 2d558440 @page:x9 2d4e1123 bd407922 7ea3d423 7e23e443 2ea31c81 bd00b041   # inlined matrix update: ldp s0, s1, [x2, #0xac]

# HUD and menus: the 2D canvas setup (main+0x774330) multiplies a layout's projection by its view. When the projection
# is the full-screen orthographic one of the 3840x2160 layout grid, its first row is scaled by 16:9 over the picture's
# ratio, so the UI keeps its shape in the middle 16:9 of the picture, its edge elements at that area's edges, and the
# markers that follow the 3D world stay on their targets. Screens whose canvas (+0x1b8 into the screen object) belongs
# to the item-wheel background (vtable main+0x4bed098) or the pause and system menu backgrounds (main+0x4be4fb0) keep
# it: they redraw a capture of the whole picture. So do the event and loading fades (UIEventFadeScreen
# main+0x4be61c8, UILoadingFadeScreen main+0x4be7b30; the vtables that hold those names at +0x378): a fade is one colour,
# and in the middle 16:9 alone it would leave the picture showing at the sides. Without branches: the two projection
# words are compared in vector lanes, the screen's vtable is read from the canvas' screen only when they match (else from
# the canvas itself, a readable address) and compared with the four on the page, and bit selects the scaled row. x16, x17 and v16..v19 are free (a leaf; v16..v19 are written next).
site wide_hud main 774368 ad401041 ad401041 @page:x16 fd404a10 6e040431 6e0c2491 0ea0fa31 2eb08e31 2eb1ae31 0e040631 9e660231 d106e010 8a110210 8a310011 aa110210 fd400210 @page:x17 3dc02a32 3dc03633 4e080610 6ef28e12 6ef38e13 4eb31e52 6e124253 0eb31e52 0e721e31 4e040631 3dc02e33 6e33dc33 6eb11e61   # 2D canvas: ldp q1, q4, [x2]

# Pre-rendered movies: the movie layer's draw (main+0x3b5c71c) builds a viewport over the whole picture (sead viewport
# at x29 - 40: min x +8, max x +16); it moves in by the page's margin on each side, the middle 16:9 of the picture.
site wide_movie main 3b5c758 a941a269 d100a3b0 bd400a10 bd401211 1e303a32 @page:x17 bd40c233 1e330a52 1e322a10 1e323a31 bd000a10 bd001211 a941a269   # movie layer viewport: ldp x9, x8, [x19, #0x18]

# The game's zstd decoder (BSD zstd, statically linked into main) through the host's libzstd (runtime/ov_zstd.c): the
# three calls of ZSTD_decompressMultiFrame (main+0xeb380: dctx, dst, capacity, src, size, dict, dict size, ddict) go to
# the host function on the patch page (+0xff0). main+0xe910c is the file loader's call for a file that fits one read
# chunk. The loading threads decode most of the data through the game's stream feed (main+0xf5c80: state, src, size,
# ddict), called once per chunk read at main+0xf5c2c: that call goes to the host's stream decoder on the patch page
# (+0xf80), which takes a stream from its first chunk or leaves it to the game (ov_zstd.c host_stream). Until a check
# run has compared the two decoders on this build, the host functions pass every call on to the game's decoder unless
# zstd_native = 1 is set (ov_zstd.c trusted).
site zstd main e910c  9400089d 9536dfb9   # bl ZSTD_decompressMultiFrame -> page+0xff0
site zstd main bf72e0 97d3d028 950aa744   # bl ZSTD_decompressMultiFrame -> page+0xff0
site zstd main f50164 97c66c87 94fd43a3   # bl ZSTD_decompressMultiFrame -> page+0xff0
site zstd main f5c2c  94000015 9536acd5   # bl the stream feed -> page+0xf80

# The debug menu's free camera (runtime/debug_tools.c dbg_freecam_host; switched on by the menu, never by a setting). The
# camera update (main+0x4c48c0) chooses the frame's position and target (doubles) and up vector between the camera's
# own and an override; every path of that choice joins at main+0x4c4e50 with them in d0-d2, d3-d5 and s7, s16, s6,
# before they are stored into the look-at camera and its matrix is made. That instruction goes to the host function
# on the patch page (+0xf70), which puts the free camera's vectors into those registers and does the instruction's
# own work.
site freecam main 4c4e50 1e624011 95277048   # fcvt s17, d0 -> bl page+0xf70
