Skip to content

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 - #1459

Open
PromethOps wants to merge 5 commits into
seedds:mainfrom
PromethOps:upscaler-ndsr-vr-metalfx

Conversation

@PromethOps

@PromethOps PromethOps commented Nov 6, 2026 •

Copy link
Copy Markdown

What and why

The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000 v131072) upscales 1536x864 to 2048x1152 with agl's NDSR network from agl_ndsr_vr.pck: f8965bffbdf51d78, fa3033d05c0a41d6 and the other variants. It has two siblings, agl_ndsr.pck and agl_ndsr_vr_light.pck. DM_UPSCALER=metalfx (the launcher's "Game's upscaler: MetalFX") is meant to draw that network with MetalFX's spatial scaler instead. On this game it did nothing, for two reasons:

  • UPSCALER_SOURCE in dm/games/game.py matched only /agl_ndsr.pck#, so the profile named none of the shaders the game runs. It now also matches agl_ndsr_vr.pck and agl_ndsr_vr_light.pck.
  • Once listed, every variant was refused as "neither of the colour shapes MetalFX draws". Their code holds no sRGB curve: they neither encode nor decode, R11G11B10F in and out.

A variant whose code has no curve constant and no MUFU.EX2 / MUFU.LG2 (up_mfx_has_pow) is now drawn as the linear shape. Every curve takes a power, whichever buffer its constants come from, so the power check covers curves whose constants are not in the code. Such a variant converts nothing, so its input and output are in one colour space, whichever it is. The linear shape converts nothing either, so the colour passes as it is. The washed-out picture of An upscaler's colour read from its code came from a decode the shader does and the swap did not; there is none here. Code with a power but no curve constant still runs the game's shader.

With the change the option works on this game: the log reads upscaler f8965bffbdf51d78_b: input and output in one colour space …, then first dispatch of fa3033d05c0a41d6_b drawn by MetalFX, 1536x864 … → 2048x1152.

An earlier version of this description presented MetalFX as the cure for dotted horizontal lines the network's output showed in fog and light shafts. Main's dead-code removal fix, which covers two NDSR variants, removed them at their source: on main the game's own upscaler draws the same scene clean. This PR only makes the player's MetalFX choice work on these packs.

Checked

  • Games run, with title ID and version: The Legend of Zelda: Ocarina of Time (Switch 2, 0400486024DEC000, update v131072, SDK 22.2.8), on main 75c1a8c with this PR (and A GPU-only pool's render targets share one placement heap: Ocarina of Time's 3 GB of render targets fit in 0.7 GB #1453, Shared textures no live object resolves to are retired when GPU memory runs short: Ocarina of Time no longer freezes after the talk with Saria #1455). Kokiri Forest in fog and light shafts, a fixed scene, DM_UPSCALER=metalfx against the game's upscaler, compared on screenshots and log lines. The player confirmed the picture with both of their MetalFX launchers.
  • Devices and OS: MacBook Pro M5 Pro, 24 GB, macOS 27.2.
  • t_upscaler_mfx with Donkey Kong Bananza's (040074A01BF12000) and Splatoon Raiders' (0400380020056000) profiles, DM_UPSCALER game and metalfx, both encoding modes: 15 and 94 checks. One expectation changed: no curve and no power is now one colour space, not "the shader runs". New checks:
    • no curve constant but a power (EX2, LG2): the shader runs;
    • a MUFU that is no power (RCP) does not count.
  • test_docs.py, docs_symptoms.py. The failing checks (host unit tests, tool tests, sanitizers, clang-tidy) fail the same way on main 75c1a8c; the build check passes.
  • Not checked: scripts/regress.py check --games. Bananza's and Splatoon Raiders' variants have curve constants, so their classification is unchanged.

Docs

  • The finding is in its docs/reference/rendering.md entries (the upscaler variants, and the no-curve shape), and python3 dm/tools/docs_symptoms.py was run (the symptom index)
  • A chain tool changed: not the case
  • No game files, keys or data extracted from a game in the change or the attachments

…ariant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's

Ocarina of Time (Switch 2) upscales 1536x864 to 2048x1152 with agl_ndsr_vr.pck's network (f8965bffbdf51d78 and others). game.py listed only agl_ndsr.pck's shaders, so the MetalFX option left it running; listed, its variants were refused, as their code holds no sRGB curve (R11G11B10F in and out). A variant with no curve constant and no MUFU.EX2 / MUFU.LG2 (which every curve takes, wherever its constants come from) neither encodes nor decodes, and is drawn as the linear shape, which converts nothing either. With MetalFX in its place, the dotted horizontal lines the network's output showed in fog and light shafts are gone.

Tests: t_upscaler_mfx (Bananza's and Splatoon Raiders' profiles, game and metalfx, both encoding modes): no curve and no power -> one colour space; a power with no curve constant -> the shader runs; a MUFU that is no power does not count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation labels Nov 6, 2026
AnkMister added a commit to AnkMister/nxnative that referenced this pull request Nov 6, 2026
Port PR seedds#1459 and PR seedds#1461 onto origin/main 0cefa86. Keep the game upscaler and ordinary window placement as defaults. Select a requested screen before layer setup so backing-scale and local HDR capability reads use that display.

Front-ends: DM_SCREEN accepts a display-name substring. The existing Game upscaler choice now recognizes agl_ndsr_vr and agl_ndsr_vr_light networks. Both options remain opt-in.
…MetalFX option draws its agl_ndsr_vr.pck upscale (main's dead-code fix removed the dotted lines at their source)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dinadur

dinadur commented Nov 6, 2026 •

Copy link
Copy Markdown

Tested this PR on top of main 0c7e88b (chain 844bf1a5ea8f) with The Legend of Zelda: Ocarina of Time (0400486024DEC000, v131072), Mac app, Mac Studio M5 Ultra, macOS 27.2.

With DM_UPSCALER=metalfx, the dotted horizontal lines from #1537 are gone, apart from a little flicker. But the picture is clearly too bright and washed out in Kokiri Forest: low contrast and pale, as if the colours went through one conversion too many. With DM_UPSCALER=game the colours are right, but the faint lines come back.

The log classifies every variant as linear and runs MetalFX in HDR mode:


[nvn-mtl] upscaler (DM_UPSCALER): metalfx, the game's upscaler shaders (59, from the shader map) are drawn by MetalFX's spatial scaler
[nvn-mtl] upscaler f8965bffbdf51d78_b: input and output in one colour space (linear, or no sRGB curve in the code), MetalFX in HDR mode
[nvn-mtl] upscaler fa3033d05c0a41d6_b: input and output in one colour space (linear, or no sRGB curve in the code), MetalFX in HDR mode
(same for 04135bb812a0d66e_b, a335c1db6270c355_b, aad7ea7e5c02f41d_b, b106afba1a18faa9_b, d17eefaff4f0bc23_b)
[nvn-mtl] upscaler: MetalFX spatial scaler (HDR) 1195x672 → 2048x1152, 0.7 ms

DM_PRESENT_UPSCALE=metalfx was also on. Screenshot attached below.

Screenshot 2026-11-06 at 1 56 53 PM

…ted colour: MetalFX gets its input squared into linear colour, and the picture is no longer washed out

Ocarina of Time's agl_ndsr_vr and agl_ndsr_vr_light networks have no sRGB curve but square their output, keeping its sign (FMUL / HMUL2 of a register by itself with |x| on one side). Drawn as one colour space, the result stayed root-encoded: about the square root of the game's picture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@PromethOps

Copy link
Copy Markdown
Author

Thanks @dinadur, reproduced here (M5 Pro) and fixed in d715366.

Cause: these networks have no sRGB curve, but they aren't in one colour space either. A small pass before them square-roots the scene colour (gamma 2, into RGB9E5F), and the network writes its result squared with its sign kept, x * |x| (FMUL in agl_ndsr_vr.pck, HMUL2 in agl_ndsr_vr_light.pck). So their input is root-encoded and their output is linear. MetalFX left the result root-encoded, which gave about the square root of the right picture. On the same frame, the picture's mean was 146/255 against 92 with the game's upscaler, and the darkest 5 % sat at 63 against 10.

Fix: the host now detects that square in the shader's code, an FMUL or HMUL2 of a register by itself with |x| on one side. It needs 8 of them; the real variants have 32 to 108. It then squares the input on its copy into the scaler's RGBA16F texture, so MetalFX runs in HDR mode on linear colour and values above 1 keep their range. On the same scene, the result is within 1.3/255 of the game's upscaler on every channel, and the darkest 5 % is at 9.7 against 10. t_upscaler_mfx covers the classifier and a square-rooted input end to end.

The log now says square-rooted input, linear output (the shader squares): squared into linear, then MetalFX in HDR mode and MetalFX spatial scaler (HDR, input squared).

@bryanjeal

Copy link
Copy Markdown

This branch's game.py regex covers the _vr packs but its test change stops at agl_ndsr.pck's rows; test_game_profile.py still pins DM_GAME_UPSCALER_KEYS at 2 keys, so nothing would catch a VR-pack row dropping out again. One row fixes that, and it happens to be the pack Ocarina of Time's own upscaler key lives in:

'cs_other.bin\t6\tcompute\tfa3033d05c0a41d6\t/Lib/agl/agl_resource.Nin_Ounce_NVN2.release.sarc.zs/agl_ndsr_vr_light.pck#NDSR@0x84900\n'

after the agl_common.sharcb row in the MAP fixture, and the expectation becomes

self.assertEqual(macros(MAP), ['#define DM_GAME_UPSCALER_KEYS "7629d857a0a93833,c56db3d5a0f1b5af,fa3033d05c0a41d6"', '#define DM_GAME_UPSCALER_N 3'])

Defined outside MAP the fixture's escape could bite: horizontal tab and newline as above, and the file treats MAP as one string split on 'cs_other.bin' elsewhere, so a new key in a pixel row or a source outside the three packs would break the split. Without the regex line this row turns the fixture green only while main's UPSCALER_SOURCE still holds, which is why it rides on this branch rather than standing alone (PR #1559 has it as a standalone; if this PR takes it I will close that).

…ture, so the VR packs' upscaler keys stay covered (Ocarina of Time's fa3033d05c0a41d6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the tools Tools and scripts (dm/tools, scripts) label Nov 6, 2026
@PromethOps

Copy link
Copy Markdown
Author

Thanks @bryanjeal, taken as you wrote it: the agl_ndsr_vr_light.pck row now sits after the agl_common.sharcb row in MAP, and the expectation is 3 keys. test_game_profile.py passes. #1559 can be closed if this lands.

@FifteenPen

Copy link
Copy Markdown
Contributor

From Ocarina of Time on our tree, where MetalFX's spatial scaler replaced the agl_ndsr_vr chain:

  • Speed: Kokiri Forest 24.5 → 22.5 ms docked (Peak8D's MetalFX path, ported to our tree).
  • Picture: the grass shimmered with hard, aliased blade edges in Hyrule Field (handheld, render scale 0.80); the game's own network anti-aliases them. We went back to the game's network as the default and kept MetalFX as a choice.
  • Networks by ratio: the network the game picks depends on the render-to-output ratio (we saw five keys, among them 1.1x, 1.33x and 2x). A replacement that follows the chain's dispatches has to take whichever one is used.

@dpadr

dpadr commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

Tested on Ocarina of Time (0400486024DEC000, v131072) on a 16 GB M2 MacBook Air, macOS 27, applied on top of a current main-based host (7f50222 plus a few host PRs). Host only: no shader rebuild needed, chain unchanged.

  • Before: DM_UPSCALER=metalfx drew nothing. No "drawn by MetalFX" line appeared, and metalfx and game measured the same.
  • With this PR:
    • 59 upscaler keys, up from 22.
    • Takeover logged: fa3033d05c0a41d6_b 1440x810 → 1920x1080, squared into RGBA16F. The agl_ndsr_vr_light.pck variants are read as square-rooted input.
  • Handheld, scripted Kokiri Forest route, 2 runs each, submit thread at about 3 GHz:
    • Upscale span (from the upscaler's segment to the UI pass, including MetalFX's encoder): 2.50 ms → 1.31 / 1.38 ms.
    • Frame p50: 27.5–28.2 ms → 25.5–26.7 ms. GPU span: 24.9–25.8 ms → 23.4–24.1 ms.
  • Colour matches the game's own network in screenshots, with no wash-out. Edges are slightly softer, as expected from the spatial scaler.
  • No hangs, GPU faults or red lines.
  • t_upscaler_mfx: 20 checks OK. test_game_profile: OK.

Details and the comparison table: #1911.

@PromethOps

Copy link
Copy Markdown
Author

Thanks @FifteenPen and @dpadr.

@FifteenPen: the game's network stays the default. DM_UPSCALER is game unless the player picks MetalFX in the launcher, so the harder grass edges you saw are a trade the player opts into, as you chose on your tree. On ratios: the profile now takes every pack the game's networks come from (agl_ndsr.pck, agl_ndsr_vr.pck, agl_ndsr_vr_light.pck, 59 keys), and each takeover reads its input and output sizes from the dispatch it replaces. In one Ocarina of Time session on an M2 Pro (16 GB), four of them were drawn by MetalFX: fa3033d05c0a41d6 (square-rooted input, 1536x864 → 2048x1152), f8965bffbdf51d78, a335c1db6270c355 and d17eefaff4f0bc23. If a key stays on the game's network with this branch, its log line would help.

@dpadr: thank you for the measurements and for #1911, which describes the cause this branch fixes.

Fasen24-AI added a commit to Fasen24-AI/swordboiMacNative that referenced this pull request Nov 10, 2026
…tworks too, and a variant with no curve and no power is drawn as one colour space: Ocarina of Time's upscaler is MetalFX's

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
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
seedds#1901 seedds#1459 seedds#1928 seedds#1942 seedds#1940 seedds#1952 seedds#1611 seedds#2065 seedds#2083 seedds#2008 seedds#2079 seedds#2022 seedds#2075 seedds#2077 seedds#2020 seedds#2021 onto main 04811a6

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#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#1611 by Esteban-V (open): An argument block whose only change is a uniform buffer is patched instead of encoded again
- seedds#1774 by uncraftablexd (closed): Compute threads that exit before a barrier keep arriving at it, and every shader loop has a cap (GPU hang defences), chain change
- seedds#1824 by cocoman93 (open): Clamp texel fetches past a sampled 2D texture's edge: no hard edge in Ocarina of Time's mist
- 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#1928 by FeReuss (open): Ocarina of Time: enc_thread on, the replay encodes on a thread of its own
- seedds#1929 by FeReuss (open): Unused border corrections are compiled out of pipelines that bind no sampler with a twin (function constant DMSH_BORDER_FC)
- seedds#1930 by FeReuss (open): A float add to storage memory with no result read is one native atomic add instead of a compare-and-swap loop
- seedds#1940 by uncraftablexd (open): A prewarm list's lines after its #@after_first_frame line are built after the first frame, in the background, instead of in the prepare step (issue seedds#1747)
- seedds#1942 by uncraftablexd (open): Frame step: late frames with the GPU idle do not step up, and a longer hold the GPU does not explain is probed
- seedds#1952 by AdyaGMD (open): Compute moves ahead of scene passes past occlusion queries and conditional rendering; clears wait across dispatches
- seedds#2008 by bquilty25 (open): nvn: reactive memory watchdog, 16-way view cache, and texture upload batching
- 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#2022 by sleepyeyedjoe (closed): Ocarina of Time: large models no longer go black for one frame twice, 4–5 frames apart, when the game streams new textures into them (a texture ID a shader has read keeps its read mark when it gets another texture)
- seedds#2030 by humanperzeus (open): A NaN colour at alpha 0 is returned as 0, as NVIDIA's blender takes a zero factor: Ocarina of Time's white clouds over the ferns are gone
- seedds#2035 by Know-It-All-Brother (open): The shader build runs as worker processes, and no shader waits for a batch (Ocarina of Time's cold Mac map: 3,002 s to 1,298 s)
- seedds#2065 by Raymon82777 (closed): A vertex library with layer entry points of its own gets bases for its main0 pairs: Ocarina of Time's 230 ms camera-turn hitch in Hyrule Field goes to 39 ms
- 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)
- seedds#2077 by roandegraaf (closed): Ocarina of Time: fix the "plaid" terrain / missing texture tiles (GPU-drawn textures now reach the storage the game copies)
- seedds#2079 by uncraftablexd (open): Render pipelines that share a vertex function are taken from the archive or compiled one at a time: the AGX driver crashes in createVertexProgramVariant's completion block
- seedds#2083 by FifteenPen (closed): Ocarina of Time's prewarm list gets 41 more task / mesh pipelines from later play
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs Documentation nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) tools Tools and scripts (dm/tools, scripts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants