Repository navigation
Replies: 26 comments 19 replies
|
Measured answers from our Ocarina of Time work. Two machines: Mac = M4 Pro (20-core GPU), iPad = iPad Pro M5. All runs Docked 1536x864, Kokiri Forest. Results
Mesh shaders: why the vertex stage is expensive
Pitfalls if you build the pre-pass
Upscaler (fixed cost)
Compute: float atomics
iPad specifics
Profiling that worked
BranchesEach change is a commit on our tree ( |
|
Two things from an M4 Pro MacBook Pro (Mac16,8, 24 GB, macOS 27.0.1) that may affect how the fps figures read:
I'll post fps with Dynamic resolution on and off once I've rebuilt on 04419bc. |
|
your Mac model and memory: the frame rate and whether Dynamic resolution was on: c78d7375bea9c862679ec294a6a69ff3e22e18d0.mp4//EDIT: Actually have to correct myself. With these settings after playing more than a minute the game starts eventually stuttering badly. If you pause the game and then contiue the FPS recover and the same scene runs smoothly again. Perhaps some memory issue. However, if I use
the game runs pretty great. Very good frame pacing and no longer stutters or slowdown even after playing for a few minutes. Example: c78d7375bea9c862679ec294a6a69ff3e22e18d0_30fps_nodyn.mp4 |
|
Mac: MacBook Pro 14", M1 Pro, 16 GB, macOS 26.6.1. Fullscreen on the built-in ProMotion screen (the host switched it to 120 Hz). Build: main 04419bc plus #1331, #1453, #1377, #1379 and an SDK guard; chain 89f44f9b6bb0, all 42,848 shaders. v131072, Handheld, Settings: 60 fps target, hold frames on, Dynamic resolution off (full 1536x864), Metal 4 pipelines off. Frame rate: about 30 fps most of the time, smooth to play.
Two sessions, 111 min in all, both ended by quitting:
Dynamic resolution on (a third session, same build): no gain here.
I can send a |
|
Update: the flickering dark specks in the mist are fixed (877c7ec) The ground mist tests each pixel against a depth its own earlier pass stored for the same surface. On the Switch's NVIDIA GPU those two values are always equal. Apple's GPU sometimes stores the depth one rounding step lower, at 0.2–3.8% of pixels, so the mist rejected its own surface there. The fix is general, in the shader translator: a shader comparing a pixel's depth with a depth it read treats them as equal when they're within that gap. It touches 39 of Ocarina's shaders (the mist and some foliage). The shaders rebuild on your next build (new chain). If you still see specks or anything new in the mist or grass, please post a screenshot and your Mac's chip. |
|
Why dynamic resolution is off by default now Since 04419bc the launcher's Dynamic resolution setting ( The game's controller got stuck at its lowest resolution (683x384) on the Mac and never climbed, even with GPU time to spare. Two host bugs made its timer read high: a stale frame-start timestamp, and event waits that idled the GPU. Those are fixed, and it still stayed at the bottom. The rest is in the game's own cost model:
Since the low resolution saved only about 0.4 ms and looked much worse, full resolution is the better default. A controller that steps one level at a time on the measured load, instead of the extrapolation, is being worked on but isn't on main yet. |
|
MacBook Pro M2 Pro (19-core GPU, 32 GB), macOS 27.2: Kokiri Forest on main Frame rate, on my own build. It is not plain main: main
Hangs on this build: 1 of 6 full runs (no overlay, from 119 s, GPU access revoked), plus a 200 s run that hung at 193 s. Details on #1326: #1326 (comment) |
|
MacBook Pro 14" (M4 Pro, 20-core GPU), 48 GB, macOS 26.7.2, Xcode 27.2 Build: main at Window (Composited), Dynamic resolution on: 60 fps, GPU 11.8–13.5 ms. The resolution drops on every short hitch, so the picture is low resolution most of the time, although the GPU has room to spare. Fullscreen (Direct), Dynamic resolution off:
Full resolution in fullscreen feels smooth and is the setup preferred here. No visual errors and no crashes in this session; saves load fine. Screenshots below. |
|
M5 MacBook Air (10-core GPU, 24 GB), a 45,300-frame run, zero hangs Adding the first M5 laptop data point to the thread. It is not a full-resolution run: this build predates Machine: MacBook Air Build: main Session: one continuous run, 45,300 frames, about 24 minutes of play after start-up. Route: Kokiri Forest → Lost Woods → Sacred Forest Meadow (beat the Wolfos there), then quit normally ( Frame timing151 reports of 300 frames each:
Memory
Flat. It settles by about frame 18,000 and does not creep after that, which I hope is useful next to #1831 (a 16 GB M4 Air reaching a 15 GB GPU allocation on a full-resolution build). 12.2 GB resident on 24 GB was never under pressure here, so this build does not reproduce that climb. Stability
That is one run, so it is not evidence that the hangs in #1326 / #1690 / #1830 are gone, only that a 45,300-frame session through the Lost Woods did not hit one. Audio
Two notesResolution. I cannot state the main-view scale from this log, so please read the GPU figure with that in mind. The log shows a 1536x864 attachment sampled in a feedback pass and a Start-up. Time to frame 58 was 586 s; frame 169, 772 s; frame 1031, 851 s. So about 14 minutes before the route starts, which is well past the ~5 minutes in #1747. On this app the shader library came from a partial build and the host says so twice at start: 288 distinct keys then failed to load at runtime ( |
|
Hey, I’m edge inference expert and this project nerd sniped me. In this PR: I have re-implemented NVDE movie network in pure metal kernels. This takes the title screen to 36ms a frame to 10ms a frame on an M1 Pro. The output of the reimplemented network is exact to the original network run on CPU. |
|
Dynamic resolution update (7de11da) and a request for tests on other Macs Thanks for the reports. @PaulEins, what you saw with Dynamic resolution on (low resolution most of the time with GPU to spare) was the game's own controller. It predicts each resolution's cost from the pixel count, which on a Mac overstates full resolution four to five times. The launcher's Dynamic resolution setting is still off by default (full resolution). Turned on, the game's controller is now replaced by one that climbs on what it measures. It tries each higher resolution, keeps it while the measured GPU load fits the game's own budget, and drops a level only when the load stays over for 8 frames. On an M4 Pro it reaches 1536x864 within seconds and holds 60 fps. One weakness is still open. On an M4 Pro the GPU time in heavier scenes hardly depends on resolution: at 683x384 the load was about the same as at full size. Even so, the controller still steps all the way down, and the picture goes soft for no gain. The next change will make it step down only to a resolution that measured cheaper. Until then, full resolution stays the default. Could you test it? Slower Macs are what this is for. @Amost-, your M2 Pro (30 fps at full resolution, GPU 21–23 ms) is exactly the case. On a build from current main:
A base M4 or an M1–M3 would help as well. If it brings a slower Mac from about 30–45 fps up to 60, it can become the default there. |
|
Bright band on Deku Tree walls / pale patch at the Kokiri exit: cause found, fix in #1844 M4 Pro (20-core GPU), 24 GB, macOS 27.2; v131072, Docked, 1536x864 (tested on penis6's In Gohma's room one section of the dark wall is lit brightly, and a pale patch sits on the ground at the Kokiri Forest exit; both shimmer while the camera moves and settle when it stops. Both are task/mesh-shader models; the vertex-shader models next to them are fine. The mesh shaders decode packed 16-bit vertex data by hand, and mesh #1844 adds |
|
iPad Pro 11" M5 (iPad17,1, 12 GB, iPadOS 27.2): measured data, Ocarina of Time v131072 Setup. Engine pin 1. TBDR on this iPad (Encode thread off in both runs; fresh launch each, same save, Link standing still, the first 5,400 frames after the title discarded; 24 and 25 windows):
A slightly different cut (fixed frames >= 5,400) gives 48.9 -> 51.8 fps and GPU 18.4 -> 15.6 ms, so about +3 to +4 fps. No crash. The README says TBDR did not help on the M4 Pro; here it did. One eyes-on observation, not measured: with TBDR on there is a water transparency glitch. In every water area I have looked at, the water is not transparent and the bottom is not visible. I did not compare screenshots. 2. Encode thread on iOS (#1664). 39,600 frames (~20 min) on 3. Memory limit. Without 4. Cold vs warm caches (same app, same settings, same scene):
GPU and submit time did not change; the lost frames are compile time. This fits #1368's description (mesh pipelines are not in the prewarm lists). 5. Time before the first frame, on iOS.
That reproduces #1735 on an iPad (engine without 6. Panics. Besides the one above, a Not measured: resolution on this iPad (no controlled A/B), Dynamic resolution, the 1.3.0 engine, the grass pre-pass. I can run any A/B or Drafted with Claude; reviewed by me. |
|
The 1536x864 main pass that main calls full resolution is the top of the game's own level list, and that list is data, so the game can render above it without a host change. The file is
With every level set to 1.0 in a copy of that file, loaded through In a 13.6-minute play session at 2048x1152 (Kakariko, its graveyard and Death Mountain Trail, HDR on) it held 100 % in every window, GPU p50 12.2 ms, frame p95 19.1 ms (median of the 300-frame windows). I haven't separated GPU from CPU in the slower windows; the longest stalls were first-sight mesh pipeline builds. Scaling the levels instead (×1.333, so the top is 1.0 and the rest keep their ratios) lets the game's controller move between 100 % and about 71 % under load (headless, with a larger output from our own render-size change), which might suit the climbing controller in 7de11da: as it stands its top is the game's 0.75. This was on our own build (main of 7 November plus later fixes from main), not current main. The overlay is made with a small BYML editor adapted from our Tears of the Kingdom work; happy to share it. |
|
iPad Pro 11" M5 (12 GB): history of my Ocarina of Time sessions, in one table Follow-up to my earlier comment. Same iPad, always Kokiri Forest, from autosaves of the same playthrough. Every row is one session, from the host log:
What can and cannot be read from it
Not measured on this iPad: resolution (no controlled A/B), Dynamic resolution, and anything built on the 1.3.0 engine. I will add the next build as a new row. Drafted with Claude from the host logs. |
|
Hello, I have a 120fps build on my branch I've added a ton of performance fixes, I'm getting 90+fps on my m4 max in kakariko / square. I'm getting 120fps in most other locations, however. Should I make a stacked PR ? |
|
M5 (10-core GPU), 16 GB: Kakariko Village and Hyrule Field drop to 12–20 fps and slow motion while GPU memory sits near 14 GB; the rest of the game holds 60 Setup. Mac17,2, Apple M5 (10-core GPU), 16 GB, macOS 27.2, built-in Retina display.
What I see in B and C:
Is this the render-target churn listed as open under Memory in the game note, or GPU memory pressure on a 16 GB Mac (the same churn shows with dynamic resolution off)? I can run Kakariko again with any switches or extra logging you suggest. |
|
M6 MacBook Pro, 32 GB, macOS 27.2: Kokiri Forest holds 60, Castle Town 20–25 fps and looks CPU-bound Setup. Built from main as of 2026-11-08 with Frame rate:
In Castle Town the GPU finishes well inside a 60 Hz frame, but frames still take 40–49 ms. So it looks bound by the CPU or the translation step rather than the GPU, and I'd expect lowering the resolution not to help there. It matches what @sleepyeyedjoe described for the 30 fps stretches (frame step near 100% on the performance cores). I can run exFAT saves, in case it helps someone: with the game on an exFAT drive, saves failed because exFAT can't do the game's save transactions. Pointing |
|
Another data point for the 10-core M5, from a build a couple of days behind main (host e9c2f7e, shader chain dc815e0f38ca), 32 GB, MetalFX, handheld, dynamic resolution left to the game. A 27-minute stretch of heavy scenes averaged about 37 fps. Windows with no shader compilation in them still had a GPU median around 34 ms, so this is not a cold pipeline cache. That matches the same-day 10-core M5 report above: dungeons can hold 60, and Kakariko / Hyrule Field stay in the 17–34 ms band. In the settled heavy section the attachment traffic sat at about 710 MB loaded and 710 MB stored per frame. Every 300 frames the host still created roughly 5,000 Metal textures (2–6 GB) and committed the residency set for 200–600 ms, while the set itself stayed near 12,000 allocations and about 15 GB. By frame 72,900 storage takeover had run 131,003 times, shared textures were about 1,200 (about 140 of them unreferenced), and idle retirement was still 0. The create-and-replace cost is real and it is about 1–2 ms a frame, not the rest of the GPU time. Two host findings from this tree, in case they are not on main yet:
|
|
Hello, to help with debugging and checking how different locations render/work, I created some save checkpoints. Below the details, hope it helps: Test saves for v131072: the same spots for everyone (10 checkpoints, child and adult, plus 9 more spots) Many reports here and on #1326 come from each player's own autosaves, so the scenes differ from Mac to Mac. These saves (zip, 11.6 MB) load Link at fixed places instead:
The save-format work builds on Traiver's OOT-SW2SE save editor (MIT): its parser and field catalog showed the file layout and many field names, and its catalog's hashes let me confirm the hash function. |
|
MacBook Pro 14" (M3 Pro, 11-core CPU, 14-core GPU), 36 GB, macOS 27.0.1, Xcode 27.2 Build: main Benchmark:
Metal 4 pipelines off ( First start with Metal 4 pipelines on: the bundled archive was found ( A 75 Hz external screen (2560x1080, it also offers 60 Hz): with |
|
M1 Pro (16 GB), macOS 26.6.1: two long sessions with no GPU hang, and the vanishing ground fixed by #1920 Builds: current main plus #1774 (the shader loop guard) and #1377 / #1379 (AV1 movies). Session 1 was on main 2a6f9b3, session 2 on main 04811a6. All 42,848 shaders built. Handheld, 60 fps target, dynamic resolution off, Metal 4 pipelines off.
|
|
Data point from an M4 Pro MacBook Pro (20-core GPU, 24 GB, macOS 27.0.1, AC power): main 04811a6 (chain 39eb2f781d07) plus eight open PRs stacked as host code only, no new shader image. Stacked on 04811a6: #1927, #2085, #2077, #2079, #1888, #2065 (host part), #1901, #1951. All apply cleanly; #1901 needs a 3-way apply for nvn_mtl.m. ABBA against an older build (from before the move to main), Kokiri Forest route from a fixed save with scripted inputs, docked, window, 190 s per run, stats from 60–120 s:
Most of the gain over plain main on this route is #1927: plain main dropped to 30 fps for stretches of the walk (one run never came back to 60 in a later series). #1901 accounts for about -0.8 ms of GPU time. No crashes, no GPU hangs, no [RED] lines in any run. One warning: #2065's host part without an archive rebuilt by its pso_offline.py makes the bundled archive fail its probe ( |
|
Pushed a fix to Symptom: in the Deku Tree (1F), ferns and vine leaves intermittently render as black silhouettes and flip back to green as the camera moves. Cause: when the pre-pass draws get a render pass of their own (the next pass has other targets, about once a frame in the Deku Tree), Fix: If you are on an older build of the branch, |
|
M2 profile, as asked: MacBook Air M2 (8-core CPU, 10-core GPU, 16 GB, fanless), macOS 27.0 Ocarina of Time v131072, handheld at 1440x810 (dynamic resolution off), MetalFX, frame-rate hold off. The route walks Kokiri Forest. Build: our local OoT build on base 7f50222, plus #1833 ( Measured with Where a frame goes on the M2
By pass shape (66 frames, GPUPROF):
So the M2 is GPU-bound at full size, unlike the M4 Pro. What the idle is (trace, ~9.9 ms a frame)
Two hardware facts that may help everyoneFrom a ~100-line standalone Metal 3 benchmark on this M2:
Results on this Mac
Encoder boundaries in one frame (LKG, frame 2400): 142 encoders
A dependency graph of the frame schedules in 109 encoders (−23 %). Its critical path is 79 % of node time. What this suggests for the M2The biggest levers here look structural, not per-shader:
We're testing these next on this machine and will add results. |










Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The Legend of Zelda: Ocarina of Time (
0400486024DEC000, the Switch 2 release, update v131072): where it stands onmainas of 04419bc (2026-11-08).Status
89f44f9b6bb0): rebuild the game's shader libraries (run the NSP script again) to get the fixes below.What's new in 04419bc
Full resolution by default. The game renders its 3D view at 1536x864. A new launcher setting, Dynamic resolution (
DM_DYNAMIC_RESOLUTION=1), hands the choice back to the game's own controller. That runs at 60 fps on more Macs, at a lower resolution that follows the load.The resolution no longer stays at its lowest after a loading screen. Two host bugs made the game's frame timer read more GPU time than the frame took:
With them fixed, the game's own dynamic resolution climbs back to full size.
Less GPU work per frame. The mesh shaders' 128-bit memory loads are now one load instead of four, and occlusion-culled draws are skipped once their query result has landed.
Measured on an M4 Pro (20-core GPU), Kokiri Forest:
At full size the GPU needs almost a whole 60 Hz frame on that Mac, so it often holds 30 for stretches. On slower GPUs, turn Dynamic resolution on.
Fixed earlier
Known open reports
Ideas wanted: performance
At full resolution the GPU needs about 15 ms a frame on an M4 Pro, just short of 60 fps, and slower Macs fall well below. Suggestions are welcome: Metal techniques, profiling approaches, or things you've seen work in other translation layers.
Where the time goes (M4 Pro, Kokiri Forest, Metal System Trace; measured at 683x384 before the latest fixes, so the shares shift at full size):
Already tried, no gain:
DM_NVN_TBDR=1Open questions:
If you want to dig in:
scripts/bench.pyruns a fixed route (kokiri-deckin the profile'sbench.tsv).DM_NVN_GPUPROF=<first>-<last>logs every encoder's GPU start and duration over those frames.How to help
If you play on the latest build, reply with:
log.txtand, if you can, the save from just before itAll reactions