Skip to content

OoT-fps A-1: the queue replay and encode threads declare the frame's deadline, so iOS no longer runs them on efficiency cores at 30 fps - #2569

Closed
stan69b wants to merge 1 commit into
seedds:mainfrom
stan69b:pr/oot-fps-a-perf-cores
Closed

stan69b wants to merge 1 commit into
seedds:mainfrom
stan69b:pr/oot-fps-a-perf-cores

Conversation

@stan69b

@stan69b stan69b commented Nov 13, 2026

Copy link
Copy Markdown
Contributor

Batch OoT-fps, session A, item A-1.

Problem

At the 30 fps cap on an iPhone, Ocarina of Time kept its frame's CPU work on efficiency cores:

  • 1-25 % of CPU time on performance cores;
  • the submit thread at 1.55-1.66 GHz, with 16-24 ms of CPU a frame;
  • 25-26 fps, where the GPU needs 20-27 ms a frame.

Cause

Only the thread that acquires the window declared a time-constraint policy.

  • iOS's performance controller sizes the clock so that each declared thread fits its declared period.
  • That thread, the guest's main thread in this game, fits 33 ms on efficiency cores.
  • The threads that make the frame late, the queue replay thread and the encode thread, had no deadline at all.

The summary's thread policy line also checked the wrong thread. It read the policy of the thread that presents, which in this game is the game's own presentation thread. So it reported "not in effect" while the main thread held the policy (priority 97 in a spindump).

Fix

The fix is in nvn_rt.h (pure logic, tested by t_thread_rt) and hle_nvn.c, and applies to every game.

  • Per-thread declaration: the queue replay and encode threads each sample their own CPU time once per presented frame. After 120 frames they declare the frame's period with their own computation: the larger of 1.25 × p90 and 1.1 × peak, at most 0.9 of the period. They declare again when that moves by more than 15 %.
  • Diagnostics: the thread policy line now lists each declaring thread with its priority, how many frames it was demoted below the real-time band, and its CPU time and mean clock a frame.
  • Switches: DM_RT=0 turns every declaration off. The new DM_RT_FRAME=0 keeps only the acquire thread's declaration.
  • Docs: a frame-rate reference entry, the symptom index, the game note, development.md and env_switches.tsv.

Phone, before and after

iPhone 17 (iOS 27.2.1), Kakariko Village, the unattended camera route (iphone_drive_batch.sh), 7 minutes each, back to back. Builds: oot-build-vis (6725bc14) as the baseline, and the same build with this commit.

gameplay windows baseline this PR
fps (median) 26.4 29.1
frame p50 / p95 37.0 / 45.4 ms 33.7 / 38.1 ms
GPU p50 32.1 ms 30.8 ms
submit thread CPU per frame 15.2 ms at 1.67 GHz 13.2 ms at 1.78 GHz
share on performance cores 48 % 52 %
CPU power 0.78 W 0.87 W
real-time threads main thread only acquire, replay and encode threads at priority 97, demoted in 0 frames

The phone was at thermal state 2 for both runs.

Open: a slow GPU state. An earlier pair on the same route, with either build, went into another state of the phone:

  • the GPU took 66-80 ms for the same draws, against 27-32 ms;
  • the CPU ran on performance cores at 3.4-3.8 GHz, drawing 1.7 W for about one core of work;
  • the game ran at 9-13 fps.

The baseline went into it as well, so this change does not cause the state. Whether the CPU clock takes the GPU's share of the power budget there, and whether more declared deadlines make that worse, is not known. It is recorded as open in the reference entry.

Tests

  • dm/gfx/tests: t_thread_rt 8/8, t_fstep 86/0, t_nvn 314/0, t_gfx_suspend 22/0.
  • dm/tools/tests/test_docs.py and test_env_switches.py: OK.

Regression check

Pending. The change is host-only and does not touch the shader chain. Its effect is on iOS scheduling, which the Mac harness does not measure. The combined phone run of batch OoT-fps (item A-2) will add numbers here.

…deadline, so iOS no longer runs them on efficiency cores at 30 fps

Only the thread that acquires the window declared a time-constraint policy. iOS's performance
controller sizes the clock so that each declared thread fits its period, and that thread fits
33 ms on efficiency cores; the threads that make a frame late (queue replay, encode) had no
deadline. They now take one sample of their own CPU time per presented frame (nvn_rt.h) and
declare the frame's period with their own computation after a first window of 120 frames.

The thread policy line names each declaring thread with its priority, its demoted frames and
its CPU time and clock a frame (the old line read the presenting thread, which in Ocarina of
Time is the game's own). DM_RT=0 turns every declaration off, DM_RT_FRAME=0 keeps only the
acquire thread's.

iPhone 17, Kakariko, 7 minutes each back to back: 26.4 -> 29.1 fps, frame p95 45.4 -> 38.1 ms,
submit 15.2 ms at 1.67 GHz -> 13.2 ms at 1.78 GHz. Open: a slow GPU state of the phone (9-13 fps
with or without the change), in docs/reference/frame-rate.md.
@github-actions github-actions Bot added nvn NVN on Metal and its tests (dm/gfx/nvn, dm/gfx/tests) docs Documentation game: 0400486024DEC000 A game labels Nov 13, 2026
seedds added a commit that referenced this pull request Nov 13, 2026
Only the thread that acquires the window declared a time-constraint policy. iOS's performance
controller sizes the clock so that each declared thread fits its period, and that thread fits
33 ms on efficiency cores; the threads that make a frame late (queue replay, encode) had no
deadline, and at Ocarina of Time's 30 fps cap on an iPhone they ran on efficiency cores. They
now take one sample of their own CPU time per presented frame (nvn_rt.h) and declare the
frame's period with their own computation after a first window of 120 frames.

The thread policy line names each declaring thread with its priority, its demoted frames and
its CPU time and clock a frame (the old line read the presenting thread, which in Ocarina of
Time is the game's own). DM_RT=0 turns every declaration off, DM_RT_FRAME=0 keeps only the
acquire thread's.

iPhone 17, Kakariko, 7 minutes each back to back: 26.4 -> 29.1 fps, frame p95 45.4 -> 38.1 ms,
submit 15.2 ms at 1.67 GHz -> 13.2 ms at 1.78 GHz, CPU 0.78 -> 0.87 W. Open: a slow GPU state
of the phone (9-13 fps with or without the change), in docs/reference/frame-rate.md.

Co-authored-by: Stan Bono <3304418+stan69b@users.noreply.github.com>
@seedds

seedds commented Nov 13, 2026

Copy link
Copy Markdown
Owner

Landed on main as 4c71083, with a follow-up b06f4e0 that folds in PR #2137: each thread that acquires the window now has its own time-constraint slot (its own CPU clock, policy and port), and an acquire thread that stops acquiring for 120 frames goes back to the standard policy. OCTOPATH TRAVELER II acquires from two threads, so this was reachable. You are credited as co-author.

Mac regression check: Mario Kart World and OCTOPATH TRAVELER II pass. Octopath had GPU stalls (one command buffer taking 120-160 ms of GPU time against 3-9 ms of encoder work) in 2 of 9 runs with the frame threads declaring. Both came within half an hour of each other, and none came in the next 7 runs nor in 18 runs without the declarations. Whether the declarations play a part is open in docs/reference/frame-rate.md; DM_RT_FRAME=0 is the comparison switch if it shows up again. The iPhone slow-GPU state also stays open there.

Still open: the combined phone run of batch OoT-fps (item A-2) for a second iPhone measurement.

@stan69b

stan69b commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

OoT-fps combined phone build, against the baseline (session A, A-2)

iPhone 17, Ocarina of Time, Kakariko Village by day, unattended drive script, 7 minutes each, back to back (2026-11-13, 22:20 and 22:36). Medians over the gameplay windows (more than 1,000 draws a frame).

baseline (512 ring) combined change
fps 28.6 29.4 +0.8
frame time p50 / p95 34.3 / 40.6 ms 33.6 / 37.1 ms p95 −3.5 ms
NVN submit+present p50 14.13 ms 12.25 ms −13 %
GPU p50 31.2 ms 30.4 ms −3 %
submit thread CPU a frame 13.47 ms at 2.05 GHz 12.00 ms at 1.82 GHz −11 % at a lower clock
instructions per draw 26,790 24,614 −8.1 %
draws per frame 2,680 2,770
share of CPU time on performance cores 49 % 51 %
CPU energy 0.99 W 0.84 W −15 %
residency set 3,499-3,561 MB 3,014-3,174 MB −0.4 GB
system wired 5,230-5,397 MB 4,808-4,950 MB −0.4 GB
memory-pressure warns 1 0
thermal state (max) 1 2
  • Both runs show the same screenshots and the same RED line: the stale pipeline archive message, which both builds print.
  • After each run, env.txt was put back byte for byte, and the save was not touched.
  • The game's real-time threads in the combined run were never demoted: 0 of 300 frames on the acquire, queue replay and encode threads.

Against A-1's targets:

  • Submit ≤ 15 ms: reached.
  • A steady 30 fps: nearly; 29.4 median, p95 37 ms.
  • Performance-core share ≥ 60 %: not reached (51 %).

An earlier pair that cannot be compared: the first pair (21:04-21:19) gave 11.4 fps for the baseline and 29.2 fps for the combined build. That gap is the time of day, not the change: the baseline's save loaded Kakariko at night (GPU p50 60 ms) and the combined build's by day, as B found (#2582). Every run's end.png was checked for time of day, and only day runs are compared above.

The combined build is the one installed on the phone.

iDams pushed a commit to iDams/nxnative that referenced this pull request Nov 13, 2026
…dds#2569)

Only the thread that acquires the window declared a time-constraint policy. iOS's performance
controller sizes the clock so that each declared thread fits its period, and that thread fits
33 ms on efficiency cores; the threads that make a frame late (queue replay, encode) had no
deadline, and at Ocarina of Time's 30 fps cap on an iPhone they ran on efficiency cores. They
now take one sample of their own CPU time per presented frame (nvn_rt.h) and declare the
frame's period with their own computation after a first window of 120 frames.

The thread policy line names each declaring thread with its priority, its demoted frames and
its CPU time and clock a frame (the old line read the presenting thread, which in Ocarina of
Time is the game's own). DM_RT=0 turns every declaration off, DM_RT_FRAME=0 keeps only the
acquire thread's.

iPhone 17, Kakariko, 7 minutes each back to back: 26.4 -> 29.1 fps, frame p95 45.4 -> 38.1 ms,
submit 15.2 ms at 1.67 GHz -> 13.2 ms at 1.78 GHz, CPU 0.78 -> 0.87 W. Open: a slow GPU state
of the phone (9-13 fps with or without the change), in docs/reference/frame-rate.md.

Co-authored-by: Stan Bono <3304418+stan69b@users.noreply.github.com>
Kayyluhh added a commit to Kayyluhh/nxnative that referenced this pull request Nov 14, 2026
…dds#2367 (5b0f137) replaces the batch's; the encode thread declares the frame deadline (seedds#2569) beside replay_ahead's hold check
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 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.

2 participants