Skip to content

t_nro, t_symbols: stub dm_gfx_scale, which loader.c's render scale calls - #1931

Draft
FeReuss wants to merge 3 commits into
seedds:mainfrom
FeReuss:pr/oot-launcher-render-scale
Draft

FeReuss wants to merge 3 commits into
seedds:mainfrom
FeReuss:pr/oot-launcher-render-scale

Conversation

@FeReuss

@FeReuss FeReuss commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #1304: the first commit is that PR's head (6542d07e, Pedro Nascimento's, unchanged; here on top of the current main); review the second and the third. It will be rebased when #1304 merges.

What and why

The Mac launcher had no way to choose Ocarina of Time's (0400486024DEC000, v131072) picture size, the GPU share the game sees, or a console mode whose default follows the build. This adds them as basic settings:

  • Render scale: "Game's own" (1.0, the game's full size, with its own dynamic resolution as the sub-setting) or one of the game's own dynamic-resolution levels fixed (0.75 ... 0.5 from the profile's render_levels, shown in pixels from render_base). patches.py has a render-scale stub (the graphics update's scale apply block, found by function fingerprint beside the frame step and the dynamic-resolution stubs) that takes the loader's scale every frame while it is not 1.0; loader.c divides it by the graphics tier's picture so a level names the same size at every GPU share.
  • GPU share (DM_GFX_SCALE: Whole GPU, 80 %, 67 %) and Console mode (DM_OPMODE) take Auto, the profile's tested value (hle_oe.c dm_gfx_scale, dm_opmode_value). The profile names tier 0 (gfx_scale 0), so the picture is unchanged by default; the lighter tiers are the game's own modes, which The graphics scale is tier 0 in both modes: Star Fox's docked 3D was 768x432, smaller than handheld #1304 makes reachable.
  • Dynamic resolution (the game's own climb under load) is the sub-setting of Render scale at "Game's own".
  • Frame rate offers 30 and 60. 120 is not offered: the game steps its time in whole vsyncs and fps_max 120 needs the game measured at that rate first (the host says [RED] and runs at 60 if it is forced).
  • Display mode (window / full screen) was already a basic setting.

Nothing changes unless a player picks a value: the defaults are the profile's.

What these settings do on a MacBook Air M4 (Hyrule Field view, docked, 1920x1080 as the game's full size, one 120 s run each, window): DM_RENDER_SCALE=0.5 (1280x720) 45.4 fps against 44.9 at full size: the game's cost on Apple GPUs hardly depends on the picture size (mesh, compute and the upscaler's fixed output dominate), so lowering it does not buy frames. DM_GFX_SCALE=1 (the game's 80 % mode, a 2048x1152 UI and a 683x384 dynamic-resolution floor) 56.8-57.0 fps over 20 minutes, at the cost of a smaller picture: it is offered, not the default.

Checked

  • Tests: dm/tools/tests test_launcher_settings, test_docs, test_game_profile, test_architecture, test_oot_frame_step: see the numbers below; make -C dm/host/tests GAME=0400486024DEC000 run (includes t_oe, which now checks the tier and the mode defaults, and t_nro / t_symbols, which stub dm_gfx_scale for loader.c's new call); dm/ios/tests/launcher_storage_check.swift for the iOS front end's row. The whole make -C dm/gfx/tests GAME=0400486024DEC000 run on the tree with every PR of the series: 150 suites, 72,812 checks, 0 failed.
  • Games run: Ocarina of Time 0400486024DEC000 v131072 on the Mac, through the launcher's environment (DM_RENDER_SCALE, DM_GFX_SCALE, DM_OPMODE, DM_DYNAMIC_RESOLUTION) on the tree of 2026-11-08/09 this was written on (main 7f502220); this branch is rebased on 7a7acfee and built and unit-tested, not run in the game.
  • Devices and OS: MacBook Air M4 (10-core GPU, 16 GB, no fan), macOS 27.2.
  • scripts/regress.py check: not run; a profile and launcher change that reaches one game, and only Ocarina of Time is built on this Mac.

Front ends

Front-ends: interface: a launcher.json choice row can carry "auto" (the value its Auto option stands for, the game's tested default); Ocarina of Time's render scale is a choice of 1.0 first (Dynamic) then the game's own levels, with "base" for their pixels, where it was a range. A front end that draws choices needs nothing else; Auto is its default.

Docs

  • The finding is in the game note and docs/settings.md, docs/frontends.md and docs/launcher.schema.json describe auto and the choice form; docs_patches.py and env_switches.py were run (game patches table, switch table).
  • A chain tool changed: no.
  • No game files, keys or data extracted from a game in the change or the attachments

Series: what these PRs add up to

This PR is one of a set from one effort to make Ocarina of Time (0400486024DEC000 v131072) hold its frame rate on a MacBook Air M4. The set, and how its parts combine into the last measured result:

The result (the Hyrule Field view, the heaviest of the game: 1920x1080 docked, 60 fps cap, window, 120 s runs, M4 with 10 GPU cores, 16 GB, macOS 27.2):

Tree fps frame p50 GPU p50
main 7f502220 + #1304, fps_hold at its default 30.1 33.31 ms 27.56 ms
+ fps_hold off (#1927) 33.7 29.38 ms 27.28 ms
build 3: + #1833 with the M4 window fix, border-FC (#1929), the barrier set and cut_draws 128 below, and the PRs listed as "also in the tree" 40.2 24.48 ms 22.09 ms
+ enc_thread on (#1928) 41.6 23.46 ms 21.39 ms
+ native float add (#1930) = build 4 44.6 22.39 ms 20.45 ms

(The step from 33.7 to 40.2 changes the shader chain, so it has no in-process A/B; #1833 is most of it. Kokiri Forest, walking, is at 60 fps on build 4; the Hyrule view is the worst case.)

Each part switched in one process on build 4 (DM_AB, 210 s, 600-frame phases, frame p50 with the part active against without; the noise between phases is about 0.3 ms): border-FC (#1929) 22.61 vs 23.98 ms; the barrier set (FRAG_ORDER, PASS_OVERLAP and HEAP_RELAX alternating together) 22.41 vs 22.58 ms; CUT_IDLE 22.74 vs 22.85 ms; DISPATCH_CHAIN 22.32 vs 22.48 ms.

Which PRs make that tree:

Part Where
fps_hold off #1927
enc_thread on #1928
border-FC #1929
native float add #1930
launcher's Render scale / GPU share (no frame-time effect) #1931 (draft, on #1304)
M4 address window of shader_raw_direct (without it every raw store is dropped on an M4 and the foliage is not drawn: the 48-50 fps of a build without it are false) the branch FeReuss:pr1833-m4-window, one commit on #1833's head (not yet offered on #1833 itself)
Base, other authors, open, in the measured tree #1304 (tier 0), #1714 (gpu_ts_hz), #1833 (shader_raw_direct), #1720, #1377, #1840
Base, in the measured tree, already in main or withdrawn #1844 (-fwrapv, now e4e5ff64), #1368 / #1708 (native mesh prewarm and mesh without task: main's own versions), #1707 (withdrawn)
Measured, not submitted (the net is within the noise): barrier set (FRAG_ORDER + PASS_OVERLAP + HEAP_RELAX), CUT_IDLE branches pr/pass-overlap and pr/cut-idle in FeReuss/nxnative-oot
Not ported: dispatch chain (the same idea as #1901), cut_draws 128 (64 and 128 tie on this tree)

All of it was measured on main 7f502220 plus the above; these PRs are rebased on main 8d5d4f1b; the tool and docs tests that touch them were run again there (test_env_switches, test_docs, test_nsp_chain, and each PR's own), while the host build and the graphics suite are those of 7a7acfee, and none of it has been run in the game on the newer main (a shader rebuild of the new chain takes about an hour and 10 GB of disk). This PR is #1931.

🤖 Generated with Claude Code

lunks and others added 3 commits November 9, 2026 13:40
…ystem leaves a game, not the operation mode. Star Fox rendered 768x432 docked, smaller than handheld

oe::GetCurrentGraphicsScale (am's graphics scaling parameters, 10 / 20 / 30 -> tier 0 / 1 / 2) answered 1 docked. Games size their picture and their GPU budget by it, as a console does while it needs the GPU itself (GameChat's video): Star Fox keeps only "tier 0" (byte +0xa of its mode event, main+0xba69970) and below it renders 0.8 times smaller, so docked its DLSS network was resized to 768x432 at boot (960x540 handheld, and with tier 0 docked). Donkey Kong Bananza's code scales its frame buffer by {1.0, 0.8, 2/3}[tier] and adds 0 / 9 / 13 to the GPU time its dynamic resolution counts (from the disassembly, main+0x30f840; its docked size with tier 0 was not measured on main). A docked picture smaller than the handheld one on a GPU twice as fast is not the console's, and Star Fox's console footage matches tier 0: Peppy's close-up 65 s into it has a mean luminance gradient of 5.8 (x1000 at 1080p), tier 0 here 5.3, tier 1 4.6. GPU time in that cutscene is the same, about 7 ms p50 at its 30 fps.

The host answers 0 in both modes; DM_GFX_SCALE=1 or 2 answers a reduced tier (t_oe). Docs: services.md entry, both games' notes, env_switches.

Checked: make -C dm/host/tests GAME=040074A01BF12000 run (t_oe: 57 checks). The sizes and gradients above were measured on a build of our fork, not on main; no regress run, and the change reaches every Switch 2 game that reads the tier.
…esolution are basic settings

The Mac launcher had no way to pick a picture size for Ocarina of Time, the GPU share the game sees, or the console mode with a default that follows the build. Now:

- Render scale: "Game's own" (1.0, the game's full size, its own dynamic resolution as the sub-setting) or one of the game's own dynamic-resolution levels fixed (0.75 .. 0.5 in the profile's render_levels, shown in pixels from render_base). patches.py has a render-scale stub (the graphics update's scale apply block, found by function fingerprint beside the frame step and the dynamic-resolution stubs) that takes the loader's scale every frame while it is not 1.0; loader.c divides it by the graphics tier's picture so a level names the same size at every GPU share.
- GPU share (DM_GFX_SCALE: Whole GPU, 80 %, 67 %) and Console mode (DM_OPMODE) take Auto, the profile's tested value (hle_oe.c dm_gfx_scale and dm_opmode_value). The profile names tier 0 (gfx_scale 0), so the picture is unchanged by default and the launcher offers the game's own lighter modes.
- Dynamic resolution (the game's own climb under load) is the sub-setting of Render scale at "Game's own".
- Frame rate offers 30 and 60. 120 is not offered: the game steps its time in whole vsyncs, and fps_max 120 needs the game measured at that rate first.

The new settings.tsv rows carry level=basic and needs=; a choice row may have auto=<label>, its default, which the host reads as the profile's value. Needs PR seedds#1304 (the graphics scale is tier 0 in both modes), whose commit this one is built on.

Front-ends: interface: a launcher.json choice row can carry "auto" (the value its Auto option stands for, the game's tested default); Ocarina of Time's render scale is a choice of 1.0 first (Dynamic) then the game's own levels, with "base" for their pixels, where it was a range. A front end that draws choices needs nothing else; Auto is its default.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The launcher's render scale (934b51a3) divides by the graphics tier's picture through hle_oe.c's dm_gfx_scale; the two host tests that link loader.c without hle_oe.c define it (tier 0, the whole GPU).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@FeReuss
FeReuss force-pushed the pr/oot-launcher-render-scale branch from 4e722ba to 24cbc61 Compare November 9, 2026 16:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation game: 0400E88025B1A000 A game profile game: 040074A01BF12000 Donkey Kong Bananza game: 0400486024DEC000 A game host The Horizon host: SVCs, services, threads, memory (dm/host) ios The iPhone and iPad app (dm/ios) 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.

3 participants