Skip to content

ios: drive draws from CADisplayLink instead of a polling render thread#646

Open
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:pr-ios-cadisplaylink-render-loop
Open

ios: drive draws from CADisplayLink instead of a polling render thread#646
benface wants to merge 1 commit into
not-fl3:masterfrom
benface:pr-ios-cadisplaylink-render-loop

Conversation

@benface

@benface benface commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

The iOS launch path spawned a background thread that ran a hot polling loop — performSelectorOnMainThread:setNeedsDisplay + yield_now() — to drive frames into MTKView (configured in manual-redraw mode with setPaused: YES). yield_now() isn't a sleep, so the thread pinned ~100 % of one CPU core continuously. On a real iOS device that triggers thermal throttling, which clamps the GPU clock and drops effective FPS within a minute or two of use.

Switch MTKView to continuous-draw mode (setPaused: NO, setEnableSetNeedsDisplay: NO) so CADisplayLink drives drawInMTKView: at the display rate. Channel receivers move into the IosDisplay payload (main-thread access) and get drained at the start of each drawInMTKView:, with messages dispatched inline via a new dispatch_message helper — the main → channel → render thread → main round-trip through performSelectorOnMainThread:processMessage: is no longer needed since events arrive on the main thread to begin with. processMessage: and MainThreadState::cur_msg disappear with the thread.

Reproduction

Run any sample on a real iPhone for a minute or two; watch the frame rate drop as the device heats up. In Xcode's Activity Monitor, sustained ~100 % CPU usage shows up on a non-main thread with "Very High" Energy Impact, both of which disappear after the switch.

Also rolls in a Rust 2024 static_mut_refs lint fix on RUN_ARGS.take() via &raw mut.

@benface benface force-pushed the pr-ios-cadisplaylink-render-loop branch from 5788583 to eea80f9 Compare June 13, 2026 22:48
The iOS launch path spawned a background thread that ran
`loop { try_recv messages, performSelectorOnMainThread(setNeedsDisplay),
yield_now }` with MTKView in manual-redraw mode
(`setPaused: YES` + `setEnableSetNeedsDisplay: YES`). `yield_now()`
isn't a sleep, so the thread pinned ~100 % of one CPU core
continuously — iOS thermal-throttling the GPU clock down on real
devices after ~a minute of use, and pinning `setPreferredFramesPerSecond`
nowhere near its hint because frames only happened when the thread
nagged the view.

Switch MTKView to continuous-draw (`setPaused: NO`,
`setEnableSetNeedsDisplay: NO`). `CADisplayLink` drives
`drawInMTKView:` on the main thread at the display rate. Channel
receivers move into the `IosDisplay` payload (main-thread-only) and
get drained at the start of each `drawInMTKView:`, with messages
dispatched inline via a new `dispatch_message` helper — no more
main → channel → render thread → main round-trip through
`performSelectorOnMainThread:processMessage:`. The
`processMessage:` selector and `MainThreadState::cur_msg` field
disappear with the thread.

Same-app comparison on iPhone 17 Pro: CPU 140 % → 50 %, FPS held
at 60, Energy Impact "Very High" → "High", phone no longer heats
up during play.

Also rolls in a Rust 2024 `static_mut_refs` lint fix on
`RUN_ARGS.take()` via `&raw mut`.
@benface benface force-pushed the pr-ios-cadisplaylink-render-loop branch from eea80f9 to c6d90c8 Compare June 15, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant