Skip to content

Frame generation: MetalFX makes a frame between each two presents, a 30 fps game shown at 60 - #2241

Open
Mars-Mah3r wants to merge 1 commit into
seedds:mainfrom
Mars-Mah3r:frame-generation
Open

Mars-Mah3r wants to merge 1 commit into
seedds:mainfrom
Mars-Mah3r:frame-generation

Conversation

@Mars-Mah3r

@Mars-Mah3r Mars-Mah3r commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

What and why

A game held at 30 fps on a 60 Hz display link shows each frame for two ticks; 60 fps doubles the CPU and GPU work and, on an iPhone, the heat. Frame generation (DM_FRAMEGEN=1, launcher: Frame generation, drawn while the frame rate is 30 fps) has MetalFX's frame interpolator (MTL4FXFrameInterpolator) make the frame halfway between each two presents, and the display link shows it between them: the game does the work of 30 frames a second and the screen shows 60.

This ports the frame generation of cheemu21's Ocarina of Time fork (kid-with-sword-remake 7979a465, builds X2-X9; co-author on the commit) onto main, without the fork's other changes:

  • It runs on the present snapshots, which it turns on with a ring of 3. After each snapshot copy, in the same command buffer, the interpolator makes the frame between the previous snapshot and this one into the slot's own texture. Depth and motion inputs are constant (far depth, no motion): the interpolator's optical flow finds the motion, so no game profile needs anything.
  • A picture shown smaller than the game draws it is copied at the shown size (a crop alone, 1:1 when shown at its size or larger), which cuts the interpolation's pixels on an iPhone.
  • It pauses while frames drop, until three presents in a row come in time; the picture after a generated frame takes a present allocator of its own (3 to 5), so the display link thread does not wait for the GPU; with it on, the 30 fps cap keeps the 60 Hz link.
  • Left out: the fork's pipeline-script capture (DM_PSO_CAPTURE). The offline archive's .mtl4x unwrap (pso_bundle_unwrap, t_pso_unwrap) comes along because it shares the same nvn_mtl.m hunks; build_ipa.sh does not pack .mtl4x, so it is idle until an app carries one.
  • Reference entry: Frame generation between held presents; player docs: Frame generation.

Off by default; with DM_FRAMEGEN unset, the only path that changes is the present allocator index (still ticket % 3) and two bookkeeping fields.

Checked

  • Tests (M1 Pro, Metal validation on): make -C dm/gfx/tests GAME=0100F2C0115B6000 check_t_framegen 29 checks, check_t_present_snap 14, check_t_present_alloc 13, check_t_link_ticks 43, check_t_pso_unwrap 9, all pass; dm/tools/tests test_docs, test_env_switches, test_launcher_settings, test_settings.
  • Games run: The Legend of Zelda: Tears of the Kingdom (0100F2C0115B6000, 1.4.3) on the Mac, Great Sky Island lookout, 30 fps cap with frame generation on: generated frames shown at 1920x1080 for the whole run, the interpolator's history never started again over 1,800 generated frames, interpolation GPU time p50 7.8-8.5 ms / p95 14.6-16.4 ms (M1 Pro), one slow display-link callback (77 ms).
  • Picture, after a Mac shader build for this chain: the same lookout, a 40 s camera pan, 30 fps cap with frame generation against 60 fps: screenshots taken mid-pan show the islands, Link and the HUD (hearts, the Jump prompt) without visible smearing or ghosting; interpolation p50 6.6-7.9 ms, p95 7.3-17.8 ms of GPU at 1920x1080 over 3,600 generated frames, the history started again 3 times; game GPU p50 11.7 ms (60 fps run: 13.3 ms).
  • Not checked: an iPhone run of this port (the fork measured it on an iPhone 18 Pro Max with Ocarina of Time: 30 fps cap shown at 60 with 0-1 of 300 frames dropped), fast camera turns frame by frame.
  • regress.py check --games: not run. The feature is off by default and its paths run only with DM_FRAMEGEN=1; the present and display-link tests above cover the shared paths.

Front ends

Front-ends: new: the launcher setting DM_FRAMEGEN (Frame generation), a toggle in Game drawn while DM_FPS_TARGET is 30 (needs=DM_FPS_TARGET=30), NVN games

Docs

  • The finding is in its docs/reference/frame-rate.md entry, and python3 dm/tools/docs_symptoms.py was run
  • A chain tool changed: no
  • No game files, keys or data extracted from a game in the change or the attachments

🤖 Generated with Claude Code

…th the 30 fps frame rate): MetalFX's frame interpolator makes the frame halfway between each two presents and the display link shows it between them, so a game held at 30 fps is shown at 60

Ported from cheemu21's Ocarina of Time fork (kid-with-sword-remake 7979a46, X2-X9), the frame generation part only; its pipeline-script capture (DM_PSO_CAPTURE) is left out. The offline archive's .mtl4x unwrap (pso_bundle_unwrap, t_pso_unwrap) comes with it, since the same nvn_mtl.m hunks carry it; build_ipa.sh does not pack .mtl4x yet, so it is idle until an app carries one.

- After each present's snapshot copy, in the same command buffer, MTL4FXFrameInterpolator makes the frame between the previous snapshot and this one into the slot's own texture; the display link shows it on the present's first tick and the present's own picture half an interval later. Depth and motion inputs are constant (far depth, no motion): the interpolator's optical flow finds the motion, so it needs nothing from a game's profile.
- A picture shown smaller than the game draws it is copied at the shown size, a crop alone 1:1; generation pauses while frames drop until three presents in a row come in time; the picture after a generated frame takes a present allocator of its own (3 to 5); the 30 fps cap keeps the 60 Hz link with frame generation on.
- RED_ONCE keys 164-167 (main uses up to 163).

Checked on the Mac (M1 Pro): t_framegen 29 checks, t_present_snap 14, t_present_alloc 13, t_link_ticks 43, t_pso_unwrap 9, all pass with Metal validation; test_docs, test_launcher_settings, test_settings. Tears of the Kingdom 1.4.3 at the Great Sky Island lookout, 30 fps cap with frame generation: interpolation p50 7.8-8.5 ms, p95 14.6-16.4 ms of GPU at 1920x1080 over 1,800 generated frames, the history never started again, one slow display-link callback (77 ms); the picture itself was not checked (the Mac's shader libraries for this chain were incomplete and every run drew black, frame generation on or off).

Front-ends: new: the launcher setting DM_FRAMEGEN (Frame generation), a toggle in Game drawn while DM_FPS_TARGET is 30 (needs=DM_FPS_TARGET=30), NVN games

Co-authored-by: cheemu21 <130556833+cheemu21@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devMlGUE

Copy link
Copy Markdown
Contributor

Numbers from a slower Mac: M1 Pro, 16 GB (macOS 26.6.2), Ocarina of Time v131072, this branch merged onto main at 8b55ebb3. Handheld, windowed, scripts/bench.py --route field-run --fps 30, a save in Hyrule Castle Town's surroundings, pipeline archive warm. One run of 45 s per variant, so treat them as a first reading.

variant game GPU p50 / p95 interpolation GPU p50 / p95 generated frames shown presents shown without theirs (late)
off 27.4 / 29.6 ms – – –
DM_FRAMEGEN=1 (1920x1080) 28.3 / 31.0 ms 5.60 / 5.9 ms 1,800 362
DM_FRAMEGEN=1 DM_GFX_SCALE=2 (1280x720) 22.3 / 24.3 ms 2.62 / 2.8 ms 1,800 12
  • At full size this Mac has no room for it: the game's frame already takes about 27 ms of the 33.3, the interpolation adds 5.6, and generation pauses often (362 late presents; frame p95 33.6 → 38.2 ms, fps 29.6 → 28.4).
  • With the graphics scale at tier 2 it fits: 12 late presents, frame p50 / p95 33.33 / 33.65 ms, 2 dropped frames, no [RED] line. So on this class of Mac the feature seems to need the reduced tier (or dynamic resolution) beside it.

Merging onto 8b55ebb3 had three small conflicts, in case it saves time on the rebase: g_red_once's size, with key 164 now used on both sides (I moved one to a free key); the ticket line of the snapshot ring, where main added if (copy) *copy = g_snap[slot].ev; (I kept both changes); and dm/gfx/tests/Makefile, where both sides add lines after t_present_snap queued.

Not checked: how the picture looks in play (artifacts, the HUD), latency, t_framegen, more than one run, docked mode.

@devMlGUE

Copy link
Copy Markdown
Contributor

Follow-up after actually playing it on the M1 Pro (tier 2 + frame generation, a few minutes around the castle): the pacing was fine, but I didn't like the picture and went back to plain 30 fps.

What I saw: buildings and other structures shimmer while standing still, and everything turns blurry as soon as the camera moves. Overall the image looked "generated" rather than drawn.

The log agrees the feature itself was doing its job: 3,600 generated frames shown, 71 late presents, interpolation at 2.6 ms p50.

One caveat: I only played it with DM_GFX_SCALE=2, since that's what fits on this Mac, so part of the low detail is surely the 1280x720 scene and not the interpolation. I didn't try the two apart.

dpadr pushed a commit to dpadr/nxnative that referenced this pull request Nov 11, 2026
… the scene before the HUD, its depth and the game's camera (DM_FRAMEGEN, off by default)

A prototype for evaluation, not for merging as it is. Between two frames the game draws, the host shows one made by
MetalFX's frame interpolator (Metal 4) from the 1440x810 scene before the HUD, the scene's depth and motion vectors
reprojected from the game's camera block; the generated scene goes through the game's spatial upscale a second time
and is composed under the real frame's HUD, which is never interpolated. The generated frame is presented ahead of
its real frame on the present thread (DM_PRESENT_LINK=0), each for half the period; the pacing follows seedds#2241, the
presented-time shift and the delayed first engagement an outside patch (framegen-stage1-v4) read as reference.

Specific to Ocarina of Time: the camera block's layout and the far-plane test that picks the scene camera, read from
one build and not keyed by build ID yet.

Played on the Ocarina performance line (cut6) on an M2 MacBook Air: 45-53 shown frames a second from a 30 fps cap,
the game's frame rising from about 30 ms to about 37 ms. On main: the host compiles for 0400486024DEC000; no game
run and no regress run. With DM_FRAMEGEN unset the hooks return at once.

Co-Authored-By: Claude <noreply@anthropic.com>
@AbnerVictor

Copy link
Copy Markdown

iPhone run of this port (the "Not checked" item): iPhone Duo (iPhone19,4, Apple A20 Pro GPU), OoT v131072, NX2Apple 1.4.0 shared-app build on engine ab41d6a, shader chain cb2e91c138e3. I applied this PR's single commit to that engine snapshot: 25 of 26 hunks applied cleanly; the two remaining conflicts were local context differences (an extra DM_PRESENT_INTERVAL settings row and dm_prof()->present_snap) and were resolved by hand. clang -fsyntax-only with the iOS 27.2 SDK passed, chain V unchanged.

Settings: DM_FPS_TARGET=30, DM_FRAMEGEN=1, DM_PRESENT_SNAP=0 (auto-promoted to a ring of 3), DM_PRESENT_UPSCALE=off, DM_UPSCALER=metalfx, Metal HUD on.

Result over ~204 s: 5,400 generated frames shown, history restarted 0 times, 3 skipped for a newer present, 31 presents shown without theirs while frames dropped. Interpolator 1920x1080 (depth/motion 960x540), created in 320.8 ms. Interpolation GPU p50 4.1-8.0 ms (steady 5.6-6.6), p95 6.6-13.8 ms @1920x1080. Game frame p50 33.31-33.37 ms, dropped 0 in the last 11 windows, 30 drawn / 60 shown, thermal 0 throughout, no crash. CPU time on performance cores stayed 53-59 %.

Two slow display-link callbacks at startup (377.5 ms, 82.6 ms), none afterwards -- the fork's X11 fix (not committing the residency set for generated pictures) is not part of this port, which may explain those two.

For contrast with the A18 Pro numbers in #2269: 1920x1080 frame generation cost p50 11.7 ms there and did not fit beside a 30 ms frame; on this A20 Pro it is 4-8 ms and fits without a resolution preset. So the headroom is quite SoC-dependent.

Limitation: light/medium scenes, cold start, one session; heavy areas and fast camera turns not measured. Full log available. More context and the 60 fps vs 30 fps comparison in #2269 (comment posted there).

Thanks for the port -- it made 30 fps + 60 shown work on this device without doubling the CPU/GPU load.

stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 12, 2026
…ree to the Kokiri Emerald at the 30 fps cap, with the eight runs behind it, what did not help, frame generation and what is open

A dated entry under "For an iPhone or iPad build": the build (smaller textures, staging released,
MetalFX for the upscaler, DM_SCREEN_PIXELS=1; branch iphone-exp, chain 39eb2f781d07, PR seedds#2241's
frame generation for part of the play), a table of r1_base, r1_reuse0, r1_chunk256, r1_nocomp0,
r2_mfx_game, r2_mfx_on, b2_play1 and b2_play2 over frames 6,000-22,500, made from the logs. r2_mfx_on
is marked as what it was: MetalFX refused every variant before the square-root shape.

Not this: DM_GUEST_FILE_CHUNK=256M (noise) and DM_NVN_FAMILY_NOCOMPRESS=0 (no gain; the rule
protects the M1 iPad from a GPU hang). Frame generation: 9,000 frames generated with no restart, the
boss at 29.3-29.9 fps shown at 60. Open: the movie cutscenes at 15-18 fps (nn::ndve's network
35-70 ms of GPU), the freeze at a scene state's first visit, the empty CPU profile (seedds#2197) and
DM_MEMSTAT on iOS.
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 12, 2026
…ree to the Kokiri Emerald at the 30 fps cap, with the eight runs behind it, what did not help, frame generation and what is open

A dated entry under "For an iPhone or iPad build": the build (smaller textures, staging released,
MetalFX for the upscaler, DM_SCREEN_PIXELS=1; branch iphone-exp, chain 39eb2f781d07, PR seedds#2241's
frame generation for part of the play), a table of r1_base, r1_reuse0, r1_chunk256, r1_nocomp0,
r2_mfx_game, r2_mfx_on, b2_play1 and b2_play2 over frames 6,000-22,500, made from the logs. r2_mfx_on
is marked as what it was: MetalFX refused every variant before the square-root shape.

Not this: DM_GUEST_FILE_CHUNK=256M (noise) and DM_NVN_FAMILY_NOCOMPRESS=0 (no gain; the rule
protects the M1 iPad from a GPU hang). Frame generation: 9,000 frames generated with no restart, the
boss at 29.3-29.9 fps shown at 60. Open: the movie cutscenes at 15-18 fps (nn::ndve's network
35-70 ms of GPU), the freeze at a scene state's first visit, the empty CPU profile (seedds#2197) and
DM_MEMSTAT on iOS.
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 12, 2026
…ree to the Kokiri Emerald at the 30 fps cap, with the eight runs behind it, what did not help, frame generation and what is open

A dated entry under "For an iPhone or iPad build": the build (smaller textures, staging released,
MetalFX for the upscaler, DM_SCREEN_PIXELS=1; branch iphone-exp, chain 39eb2f781d07, PR seedds#2241's
frame generation for part of the play), a table of r1_base, r1_reuse0, r1_chunk256, r1_nocomp0,
r2_mfx_game, r2_mfx_on, b2_play1 and b2_play2 over frames 6,000-22,500, made from the logs. r2_mfx_on
is marked as what it was: MetalFX refused every variant before the square-root shape.

Not this: DM_GUEST_FILE_CHUNK=256M (noise) and DM_NVN_FAMILY_NOCOMPRESS=0 (no gain; the rule
protects the M1 iPad from a GPU hang). Frame generation: 9,000 frames generated with no restart, the
boss at 29.3-29.9 fps shown at 60. Open: the movie cutscenes at 15-18 fps (nn::ndve's network
35-70 ms of GPU), the freeze at a scene state's first visit, the empty CPU profile (seedds#2197) and
DM_MEMSTAT on iOS.
stan69b pushed a commit to stan69b/nxnative that referenced this pull request Nov 12, 2026
…ree to the Kokiri Emerald at the 30 fps cap, with the eight runs behind it, what did not help, frame generation and what is open

A dated entry under "For an iPhone or iPad build": the build (smaller textures, staging released,
MetalFX for the upscaler, DM_SCREEN_PIXELS=1; branch iphone-exp, chain 39eb2f781d07, PR seedds#2241's
frame generation for part of the play), a table of r1_base, r1_reuse0, r1_chunk256, r1_nocomp0,
r2_mfx_game, r2_mfx_on, b2_play1 and b2_play2 over frames 6,000-22,500, made from the logs. r2_mfx_on
is marked as what it was: MetalFX refused every variant before the square-root shape.

Not this: DM_GUEST_FILE_CHUNK=256M (noise) and DM_NVN_FAMILY_NOCOMPRESS=0 (no gain; the rule
protects the M1 iPad from a GPU hang). Frame generation: 9,000 frames generated with no restart, the
boss at 29.3-29.9 fps shown at 60. Open: the movie cutscenes at 15-18 fps (nn::ndve's network
35-70 ms of GPU), the freeze at a scene state's first visit, the empty CPU profile (seedds#2197) and
DM_MEMSTAT on iOS.
dontpokethebear3893 pushed a commit to dontpokethebear3893/nxnative that referenced this pull request Nov 13, 2026
…X's frame interpolator makes the frame between each two presents; a game at 60 fps is shown at 120 on a 120 Hz screen, one at 30 at 60

Ported from seedds#2241 (Mars-Mah3r; from cheemu21's Ocarina of Time fork), which shows a 30 fps game at 60. On top of it:
- at 60 fps on a screen that shows 120 Hz the display link runs at 120 Hz, so each present has a tick for its generated frame and one for its own picture
- a 120 fps target becomes 60 drawn by the game, shown as 120 (dm_fps_state_apply)
- variable refresh stays off with it: generated frames go out on the link's fixed ticks
- the launcher row is a choice (Off / 2x) offered at every frame rate

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@stan69b

stan69b commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

A follow-up for this PR is in #2582. With frame generation on, the game's acquire of a snapshot ring slot waited until the present after the slot's own had run on the GPU. That present sits behind the game's next frame on the same queue, so the game lost its frame of overlap with the GPU. #2582 frees the slot once the display link has taken that present.

Ocarina of Time, 30 fps cap, frame generation on:

Before After
Mac (M1 Pro), Hyrule Field: frame time p95 36-39 ms 33.5-33.8 ms (33.5 with frame generation off)
Mac: snapshot ring blocked 68-89 % of frames, 8-16 ms 0 % in most windows
iPhone 17, Kakariko Village by day: fps 26.2 24.9 (within noise)

The 4-7 fps seen on the iPhone with frame generation was the scene: Kakariko Village at night runs at 8.5 fps with frame generation off too.

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)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants