Skip to content

Ocarina of Time takes the launcher's Render scale: the 3D views at that fraction of the game's own size, the dynamic resolution held at its top level (Kokiri Forest docked at 0.75: GPU p50 15.77 → 14.31 ms, 60 fps held) - #1903

Open
FifteenPen wants to merge 1 commit into
seedds:mainfrom
FifteenPen:pr/render-scale

Conversation

@FifteenPen

@FifteenPen FifteenPen commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What and why

The launcher's Render scale (DM_RENDER_SCALE, 0.5 to 1.0) fixes a game's 3D views at a fraction of their size, for builds whose rewriter stubs read it. Breath of the Wild and Tears of the Kingdom have such stubs; The Legend of Zelda: Ocarina of Time (0400486024DEC000, v131072) had none, so the setting did not appear for it.

The 3D views' sizes come from the scale the engine stores in its view-size update (v131072 main+0x31d3200; str s0,[x20,#0xe5c] at +0xd48). That store, and the size loop after it, run only in a frame where the dynamic-resolution update answers status 0, a new scale. At a steady frame rate it never does, so a stub at the store alone changes nothing.

This change adds two stubs in that function, placed by its fingerprint through the frame-step rule's others:

  • At the update's answer (+0xce4, the ubfx of its status byte), a host scale below 1.0 that is not yet applied makes the status 0, so that frame recomputes the sizes once.
  • At the store, the game's scale is multiplied by the host's, so the size is that fraction of the game's own size, as on Breath of the Wild and Tears of the Kingdom.

The existing dynamic-resolution level stub returns the top level under such a scale, so the size is fixed. 1.0, the loader's default, runs the game's own code. The rewriter declares RENDER_SCALE for the build, so the launcher offers the setting.

No tool change is needed any more: main's dry run (docs_patches.py) now starts its stub area at 0x6000000, within a B's reach of a rule's other functions, so this branch, rebased onto 04811a6, takes main's file as it is and only regenerates docs/agents/game-patches.md.

Docs: the game note's "Dynamic resolution" section (Current: Render scale, with the numbers below), the new BINDING entry in patches.py, and the loader's comment that lists the render-scale stubs.

Checked

  • Tests:
    • python3 -m unittest dm/tools/tests/test_patches.py dm/tools/tests/test_nso_match.py: 48 tests OK, among them the function-matched frame-step checks (sites inside their functions, nothing written without them).
    • python3 dm/tools/docs_patches.py --check, dm/tools/tests/test_docs.py, python3 dm/tools/docs_symptoms.py --check.
    • dm/tools/lint_python.py (ruff 0.17.2, pyright 1.1.414): 0 new findings.
    • make -C dm/host GAME=0400486024DEC000 builds.
    • The rewrite of the v131072 main on the branch's earlier base (7479adf; the rebase onto 04811a6 changes no line of patches.py): the matcher finds the view-size update at 0x31d3200, all three sites are written (0x8065e0, 0x31d3ee4, 0x31d3f48), and the fixups carry RENDER_SCALE.
    • The emitted stubs were disassembled (capstone) and their branch targets checked. At the store, s0 is computed from the update's answer (fmov s0,w0 at +0xd34), never reread from +0xe5c, so the factor does not compound.
  • Games run, with title ID and version: The Legend of Zelda: Ocarina of Time (0400486024DEC000, v131072).
    • Setup: a --platform mac build of main d8b2b64, with its host and shader library and the module rewritten by this change. scripts/bench.py, route kokiri-deck, docked, two runs each, the pipeline archive warm (0 bases built in every run). The main pass's size comes from a pass dump in each run.

      Render scale main pass GPU p50 / p95 fps frames over budget
      1.0 (the game's) 1536x864 15.77 / 16.94 ms 46 588
      0.75 1171x659 14.31 / 15.19 ms 59.6 19
      0.5 781x439 13.25 / 14.34 ms 59.7 18
    • At the game's own size the GPU's p95 sits just over a vsync on this Mac, so the frame step holds 2 vsyncs for stretches of the route; at 0.75 it holds 60 fps.

    • The screenshots after the window are clean at each scale, through the game's own upscaler.

    • The main pass sizes include the engine's own normalization and rounding (×[x20+0xe58] / [x20+0xe50]), hence 1171x659 rather than exactly 1152x648.

  • Devices and OS: Apple M4 Pro (20-core GPU, 24 GB), macOS 27.2.
  • regress.py impact: Breath of the Wild, Pokémon Scarlet, Tears of the Kingdom and Hades, through loader.c. That change is a comment only. Nothing in another game's rewritten modules or host behaviour changes, so regress.py check was not run on other games.
  • Not checked: iOS; DM_DYNAMIC_RESOLUTION=1 together with a render scale below 1.0 (the scale wins: the level stub returns the top level); the handheld output.

Docs

  • The finding is in its game note or its docs/reference/<subsystem>.md entry, and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • A chain tool changed: not applicable
  • No game files, keys or data extracted from a game in the change or the attachments

@github-actions github-actions Bot added host The Horizon host: SVCs, services, threads, memory (dm/host) rewriter The NSO rewriter and per-build patches tools Tools and scripts (dm/tools, scripts) docs Documentation game: 0400486024DEC000 A game labels Nov 9, 2026
ivan-leschinsky pushed a commit to ivan-leschinsky/nxnative that referenced this pull request Nov 9, 2026
…at fraction of the game's own size, the dynamic resolution held at its top level (Kokiri Forest docked at 0.75: GPU p50 15.77 → 14.31 ms, 60 fps held)

Ported from seedds#1903 (open upstream) onto this branch: its own changes from its merge base with main, conflicts resolved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U35V38c6UYLbzyyPx6Up7P
…at fraction of the game's own size, the dynamic resolution held at its top level, as on Breath of the Wild and Tears of the Kingdom (Kokiri Forest docked at 0.75: GPU p50 15.77 -> 14.31 ms, 60 fps held)

The 3D views' sizes come from the scale the engine stores in its view-size update (v131072 main+0x31d3200, now found by its fingerprint), but that store and the size loop after it run only in a frame where the dynamic-resolution update answers "new scale", which at a steady frame rate it never does. Two stubs in that function: at the update's answer, a host scale below 1.0 not yet applied makes the frame recompute the sizes once; at the store, the game's scale is multiplied by the host's. The dynamic-resolution level stub returns the top level under such a scale, so the size is fixed. 1.0, the loader's default, runs the game's own code. The rewriter marks the build RENDER_SCALE, so the launcher offers the setting.

docs_patches.py's dry run puts a rule's other functions 8 MB apart from 0xa000000 instead of 16 MB apart from 0x10000000: a stub that returns into one of them (here the view-size update) needs a B within +-128 MB of the stub area.

Measured with scripts/bench.py on a --platform mac build of main d8b2b64 with the module rewritten by this change, kokiri-deck, docked, Apple M4 Pro, two runs each: 1.0 1536x864, GPU p50 15.77 ms, 46 fps (the frame step holds 2 vsyncs for stretches); 0.75 1171x659, 14.31 ms, 59.6 fps; 0.5 781x439, 13.25 ms, 59.7 fps; the pictures clean at each.
@todd866

todd866 commented Nov 10, 2026

Copy link
Copy Markdown

A build confirmation on current main, in case it helps review.

I applied this PR's code files on top of main at cfbdd93 (9 Nov) and again at 04811a6 (10 Nov), and ran a full --platform mac-app build of v131072 from a dump folder each time. Machine: MacBook Air M5 (10-core GPU), 32 GB. Both times the patch applied without conflicts and the build completed (42,848 shaders, none failed).

On 04811a6 the rewrite log shows the three sites matched:

frame step "frame ratio from the host's step": dynamic resolution at 0x8065e0
frame step "frame ratio from the host's step": dynamic resolution update at 0x805aa0
frame step "frame ratio from the host's step": render scale at 0x31d3200

and the packed app's Info.plist carries the DM_RENDER_SCALE setting.

Not tested yet: I have not played either build, so I have no frame-time numbers for the slider on this GPU. I'll add them after a session.

(Written with an AI coding assistant; the builds and logs are from my own machine.)

Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…nds once to half), seedds#1909 (fragment uniform reads bound to the binding length), seedds#1901 (dispatch_overlap on for Ocarina), seedds#1459 (MetalFX takes agl_ndsr_vr, no-curve variants linear), seedds#1903 (render scale stubs for Ocarina; replaces the seedds#1497 ceiling we carried)

Hand-resolved: the mesh draw keeps the task isolation of seedds#1842 and the copy paths our nvn_tex_level_range / DM_SHARPEN; seedds#1497's stub, test and BINDING entry go; the build-keyed rule names the render scale function (main+0x31d3200). Chain e906c7467690.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1459 by PromethOps (open): DM_UPSCALER=metalfx takes agl's agl_ndsr_vr.pck networks too, and a variant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's
- seedds#1497 by givethesourceplox (open): Add a render-scale ceiling for Ocarina v131072
- seedds#1842 by ycarogabriel-lgtm (open): Ocarina of Time: opt-in task isolation (DM_NVN_ISOLATE_TASK), the Hi-Z task a5288 in command buffers of its own, a workaround for its GPU hangs on an M1
- seedds#1897 by Know-It-All-Brother (closed): HMMA with an f16 accumulator rounds once per 8 products, as Ampere does (chain change)
- seedds#1901 by FifteenPen (open): The game's compute dispatches with no game barrier between them run side by side (profile key dispatch_overlap, on for Ocarina of Time): Kokiri Forest GPU p50 15.63 → 14.76 ms
- seedds#1902 by fxd0h (open): HFMA2 rounds once to half, as Ampere does (chain change)
- seedds#1903 by FifteenPen (open): Ocarina of Time takes the launcher's Render scale: the 3D views at that fraction of the game's own size, the dynamic resolution held at its top level (Kokiri Forest docked at 0.75: GPU p50 15.77 → 14.31 ms, 60 fps held)
- seedds#1909 by oz-rinkliff (open): Bound translated fragment uniform reads to the NVN binding length
Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…a 23 was built from

Ours kept where main has not taken the change: the pool test of image_texture (virtual pools), need_make with seedds#2075's stand-in,
the copy absorb beside main's gsb_mark, DM_SHARPEN's copy_t2t_draw_x, the render scale (seedds#1903) and fixed resolution stubs beside
seedds#2020's native output and seedds#2021's render size. Main's own taken for the depth rule (fb_depth_rule), the family textures and the
movie network: the opt-in shortcut of PR seedds#1331 (DM_NVN_NDVE_MOVIE_DIRECT) is removed, main runs the network as host kernels.
Generated files (chain header, lock, translator binaries, symptoms, game-patches) are main's here and rebuilt in the next commit.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014eD437mxrMnW2UwLZhpwc7

Upstream pull requests applied in this commit (their authors' work, brought onto this tree with a three-way merge; the description is in each PR):
- seedds#1331 by givethesourceplox (closed): Scale decoded NDVE movies on Metal and skip matched reconstruction
- seedds#1903 by FifteenPen (open): Ocarina of Time takes the launcher's Render scale: the 3D views at that fraction of the game's own size, the dynamic resolution held at its top level (Kokiri Forest docked at 0.75: GPU p50 15.77 → 14.31 ms, 60 fps held)
- seedds#2020 by padsiwono (open): Ocarina of Time v131072: Output Native draws the full output size instead of the game's 80 % frame (`DM_OUTPUT`)
- seedds#2021 by padsiwono (open): Ocarina of Time v131072 renders at the launcher's Resolution (`DM_RENDER_SIZE`), docked
- seedds#2075 by sleepyeyedjoe (closed): Ocarina of Time: a patch of scenery no longer draws dark for one frame when the game gives it a sharper texture (a pool texture the walk does not make holds a stand-in of its own colours until its first read)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400486024DEC000 A game host The Horizon host: SVCs, services, threads, memory (dm/host) rewriter The NSO rewriter and per-build patches tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants