Skip to content

iPhone: background pause, cheaper overlay, fps cap, haptics, MetalFX - #18

Merged
kanapitsas merged 10 commits into
mainfrom
feat/ios-polish
Nov 7, 2026
Merged

kanapitsas merged 10 commits into
mainfrom
feat/ios-polish

Conversation

@kanapitsas

Copy link
Copy Markdown
Owner

iPhone: lifecycle fixes, a cheaper overlay, and new settings.

  • The copied game data (game/, prep/, linux/, ~11.5 GB) is excluded from iCloud backups; saves stay backed up.
  • The game pauses while the app is not active: no present, OpenAL suspended, sensors stopped; P2_IOS_BACKGROUND_PAUSE=0 turns it off.
  • The touch overlay uses fixed layers instead of a full-screen drawRect flushed on the game thread.
  • Back-tap motion updates stop after 2 s in a menu or with a controller.
  • New settings: vibration when a portal is fired; 30/40/60/120 fps cap; auto 30 fps when hot or in Low Power Mode; MetalFX upscaling (Off / Quality 0.75 / Performance 0.5, through mtld3d's render.scale).

Limit: mtld3d reads present.maxFps at init only, so on Metal the power-saving cap applies at the next launch.

Tested: make check; every runtime/ios file and main_ios.c compiles for the device. Not tested on the phone yet: checklist in docs/ios.md.

🤖 Generated with Claude Code

kanapitsas and others added 10 commits November 6, 2026 23:10
Documents/game, prep and linux hold ~11.5 GB copied from the Mac, which every
iPhone backup uploaded again. They now carry NSURLIsExcludedFromBackupKey,
set at launch only when missing; portal2-userdata (saves) stays backed up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing stopped the game loop when the app left the screen: it kept rendering
and presenting, which gets an OpenGL ES app killed in the background and makes
Metal submissions fail. On UIApplicationWillResignActive the main thread (the
game's) now stops at its next SDL_PollEvent: glFinish, OpenAL context
suspended and detached, then it serves UIKit's run loop until
DidBecomeActive. The UIKit pump runs with the guest lock held (SDL forwards
app events to the game's event watchers, guest code); the lock is released
around each 20 ms sleep and around the ALC calls, as the gate rules require.
P2_IOS_BACKGROUND_PAUSE=0 restores the old behavior, P2_IOS_BACKGROUND_AUDIO=0
leaves OpenAL alone.

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

The overlay was a full-screen, non-opaque view drawn at the native x3 scale:
every stick move, button press and FPS change re-rasterized and uploaded a
2796x1290 bitmap, synchronously on the game thread (displayIfNeeded plus
CATransaction flush per touchesMoved). Sticks and buttons are now one
CAShapeLayer each (icon as a pre-rendered sublayer), moved or recolored with
actions disabled; the FPS counter is a bitmap the size of its box, redrawn
only when its text changes. Changes mark the overlay and are pushed once per
run-loop pass (observer, step timer as a fallback) with a flush that now only
carries a few layer properties. Same shapes, colors, sizes and hit areas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CoreMotion ran at 200 Hz for the whole session, in menus and with a physical
controller (overlay hidden), where every candidate is rejected anyway. Updates
now stop after 2 s without gameplay (menu, overlay concealed, app inactive) and
restart with the same handler and rate when play resumes. The first 150 ms
after a restart are fed as menu time, since the touch that closed the menu
went to SDL and escapes the touch guard. Detector thresholds and rate are
unchanged; recordings and the dataset keep working, with gaps in menus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Back taps and the portal buttons gave no feedback beyond the shot itself, so
a missed or doubled tap was hard to notice. A light UIImpactFeedbackGenerator
impact now plays for each portal fired through the overlay; the generator is
prepared ahead (at most once a second in game, and after each shot) so the
first impact is not late. New setting "Vibrate when firing" (P2Haptics ->
P2_HAPTICS), on by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frames per second only offered 60 and 120. It now also offers 30 and 40:
Metal gets them through present.maxFps, OpenGL through a wait before each
present (the display only paces 60 and 120), and 40 asks the ProMotion panel
for 120 Hz so frames land on every third refresh instead of alternating.
New setting "Auto power saving" (P2PowerSave -> P2_IOS_POWER_SAVE, on by
default) lowers the cap to 30 in Low Power Mode or at a serious or critical
thermal state, with a log line. mtld3d reads present.maxFps once at
initialization (rev 101f37d3 has no way to change it later), so with Metal
the check happens at launch only; with OpenGL the cap follows changes in play.
A new check ties the Settings choices and defaults to what the code reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The pinned mtld3d can render below the presented size and upscale with the
MetalFX spatial scaler (render.scale), but nothing exposed it. New setting
"Upscaling (MetalFX)": Off, Quality (0.75), Performance (0.5), mapped to
P2_IOS_UPSCALE and then to render.scale in the MTLD3D_CONFIG keys the app puts
in front of the user's. It multiplies on top of Resolution, which the setting
footer and docs/ios.md explain; with OpenGL it is ignored with a log line.
docs/usage.md shows the same key for the Mac Metal renderer (env only: the Mac
app has no launch-options file to put it in).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The branch is not tested on the phone yet, so the roadmap line stays in
progress until it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review findings on the pause: the overlay's step timer still ran during the
UIKit pump, so a held stick kept sending mouse motion; the 2 s sensor delay
could outlast the time iOS leaves before suspending the app; and a frame
presented from another thread in the background still reached glFlush. The
overlay now treats an inactive app like a menu (fingers released), sensors
stop at once when the app is inactive, and a background swap returns before
any GL call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kanapitsas
kanapitsas merged commit a3d2433 into main Nov 7, 2026
3 checks passed
@kanapitsas
kanapitsas deleted the feat/ios-polish branch November 7, 2026 22:08
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