Skip to content

Run on iPhone: touch controls, mobile render profile, device build#9

Merged
proggeramlug merged 6 commits into
mainfrom
ios/touch-controls-and-mobile-profile
Jul 11, 2026
Merged

Run on iPhone: touch controls, mobile render profile, device build#9
proggeramlug merged 6 commits into
mainfrom
ios/touch-controls-and-mobile-profile

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

The shooter now builds, signs, installs and runs on a physical iPhone — landscape, ~50 fps on an iPhone 16 Pro, with the arena, grass, water, skinned aliens, physics and audio all intact.

./tools/deploy-ios.sh            # build → sign → install → launch
./tools/deploy-ios.sh --console  # ...and stream the device log

Depends on Bloom-Engine/engine#89. Three engine bugs stood in the way — a material loader that skipped the platform's asset-path hook (which is why water didn't render), a missing c++ runtime declaration that broke the link for any physics game, and a device-limit request that was also crashing this game on macOS. That PR should land first.

Touch controls

Left half is a movement stick that materialises under your thumb wherever it lands; right half is drag-to-look; FIRE / JUMP / R / GUN along the right edge. Holding FIRE still steers, so one thumb can shoot and aim instead of having to pick.

Two constraints shaped this more than taste did:

Touch slots are sparse. getTouchCount() is the number of live fingers, but touch points are addressed by slot, and slots go sparse the moment a finger lifts out of order. Lift the left thumb while aiming with the right and the live finger is at slot 1 while the count reads 1 — so the obvious 0..getTouchCount() loop reads slot 0, released but still carrying its last coordinates, as a live touch. In an FPS that means the stick stays deflected and you keep running forward while your aim goes dead. We scan 0..getMaxTouchPoints() and let isTouchActive() arbitrate (new in the engine PR), and track fingers by slot id — a look delta only means anything measured against the same finger as last frame.

The mouse path is unusable. iOS synthesises mouse button 0 from touch 0, so isMouseButtonDown(0) is true whenever any first finger is down. The existing fire check would have fired continuously while you steered with the movement stick. The mobile branch reads touch directly and never looks at the mouse.

Reload/restart and weapon-swap come back as InputState fields rather than injected key presses, because injectKeyDown() only sets keys_down and never the keys_pressed edge — isKeyPressed(Key.R) would never have seen them. (Same reason the engine's own VirtualButton.key doesn't work for edge-triggered verbs.)

Render budget

The shipped settings target a discrete desktop GPU. A phone is a tile-based deferred GPU on a battery, and the screen-space passes are both the expensive part and the part that scales worst — so SSGI, SSR, GTAO and sun shafts come off, while sun shadows and bloom stay, which is where most of the arena's look actually lives.

Lumen SW-GI is the one that has to go regardless: it re-bakes an SDF clipmap as the view moves, and there's no headroom to absorb that stall. Render scale stays at 0.5 with TSR reconstructing to native, so a 2622×1206 iPhone runs a ~1311×603 internal buffer.

HUD

iOS reports the screen in pixels where macOS reports points (engine EN-024), so every hardcoded offset in the HUD would come out a third of its intended size — a 13px status line on a 2622px-wide screen. Rather than rescale forty draw calls, the HUD is laid out in a fixed ~1000-unit logical space and the whole 2D pass is scaled through the camera's zoom. sw/sh become that logical space; swPx/shPx stay the real pixels, which is what touch coordinates arrive in.

The touch controls are drawn outside that scaled camera, in raw pixels, because they have to land on exactly the coordinates input.ts hit-tests — controls that don't press where they look are worse than no controls at all.

Keyboard-only affordances drop out on touch: the F5–F8 debug line and the full-width diagnostic bar (which sat directly under the thumbs) give way to a frame-rate readout in the corner, and the prompts say "tap" instead of "press any key".

Build

perry.toml gains an [ios] table and, critically, features = ["ios-game-loop"] — UIKit needs UIApplicationMain() to own the main thread forever and the game loop wants it too, so the feature runs the game on a spawned thread instead. Without it the app links, installs, launches, and never shows a window.

tools/deploy-ios.sh signs by hand, because Perry's own signing step fails on the extended attributes macOS leaves on the asset files (resource fork, Finder information, or similar detritus not allowed).

Testing

  • Runs on a physical iPhone 16 Pro (iOS 27): all three waves playable, water and materials rendering, spatial audio, ~50 fps.
  • Desktop is unchangedMOBILE is false, so uiScale is 1, the 2D camera wrap is skipped and drawTouchControls() no-ops. macOS build verified running.

Summary by CodeRabbit

  • New Features

    • Added iPhone support with touch controls for movement, aiming, firing, reloading, jumping, and weapon switching.
    • Added mobile-optimized HUD scaling, prompts, diagnostics, and rendering performance settings.
    • Added an iOS build, signing, installation, and launch workflow, including optional console output.
    • Added iOS deployment and performance guidance to the documentation.
  • Documentation

    • Added setup requirements, controls, graphics notes, and deployment instructions for iPhone.
  • Chores

    • Updated ignored files for build output and iOS app bundles.
    • Standardized generated asset path formatting across platforms.

Left half of the screen is a movement stick whose origin appears wherever
the thumb lands; the right half is drag-to-look; FIRE / JUMP / R / GUN sit
along the right edge. Holding FIRE keeps steering, so a single thumb can
shoot and aim rather than having to choose.

Two things drive the shape of this:

Touch slots are sparse. getTouchCount() is the number of live fingers, but
touch points are addressed by slot, and slots go sparse the moment a finger
lifts out of order — lift the left thumb while aiming with the right and the
live finger is at slot 1 while the count reads 1. Walking 0..getTouchCount()
then reads slot 0, released but still holding its last coordinates, as a live
touch: the stick stays deflected and the player keeps running. So we scan
0..getMaxTouchPoints() and let isTouchActive() arbitrate, and we track fingers
by slot id — a look delta only means anything against the same finger as last
frame.

The mouse path is unusable here. iOS synthesises mouse button 0 from touch 0,
so isMouseButtonDown(0) is true whenever *any* first finger is down — the
existing fire check would have fired continuously while steering with the
movement stick. The mobile branch reads the touch API directly and never looks
at the mouse.

Reload/restart and weapon-swap come back as InputState fields rather than
injected key presses: injectKeyDown() only sets keys_down, never the
keys_pressed edge, so isKeyPressed(Key.R) would never see them.
Render budget. The shipped settings are tuned for a discrete desktop GPU; a
phone is a tile-based deferred GPU on a battery, and the screen-space passes
are both the expensive part and the part that scales worst. Drop SSGI, SSR,
GTAO and sun shafts; keep sun shadows and bloom, which carry most of the
arena's look. Lumen SW-GI is the one that has to go: it re-bakes an SDF
clipmap as the view moves, and there is no headroom to absorb that stall.
Render scale stays 0.5 with TSR reconstructing to native, so a 2622x1206
iPhone runs a ~1311x603 internal buffer. ~50 fps on an iPhone 16 Pro.

HUD. iOS reports the screen in pixels where macOS reports points (engine
EN-024), so every hardcoded offset here would come out a third of its intended
size — a 13px status line on a 2622px-wide screen. Rather than rescale forty
draw calls, lay the HUD out in a fixed ~1000-unit logical space and scale the
whole 2D pass through the camera. sw/sh become that logical space; swPx/shPx
stay the real pixels, which is what touch coordinates arrive in.

The touch controls are drawn outside that scaled camera, in raw pixels,
because they have to land on exactly the coordinates input.ts hit-tests —
controls that don't press where they look are worse than no controls.

The keyboard-only affordances go away on touch: the F5-F8 debug line and the
full-width diagnostic bar (which sat directly under the thumbs) are replaced
by a frame-rate readout in the corner, and the prompts say "tap" rather than
"press any key" / "press R".
perry.toml grows an [ios] table and, critically, features = ["ios-game-loop"]:
UIKit needs UIApplicationMain() to own the main thread forever, and the game
loop wants it too. The feature makes Perry emit _perry_user_main and run the
game on a spawned thread. Without it the app links, installs, launches, and
never shows a window.

Orientation is pinned to landscape. The engine already locks the root view
controller at runtime from initWindow's width > height, but the plist still
advertised portrait and the app could come up rotated for a frame first.

tools/deploy-ios.sh is build -> sign -> install -> launch. It signs by hand
because Perry's own signing step fails on the extended attributes macOS leaves
on the asset files ("resource fork, Finder information, or similar detritus
not allowed"), so it strips them with xattr -cr first.

The .app carries a full copy of assets/ (~160 MB), hence the gitignore entry.
Records the four things about this target that are load-bearing and
non-obvious: the mandatory ios-game-loop feature, Perry's signing step failing
on asset xattrs, the screen being reported in pixels rather than points, and
the sparse touch-slot contract.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added iOS build and deployment support, mobile touch controls, mobile-specific rendering and HUD behavior, artifact handling, and documentation for signing, provisioning, and device deployment.

Changes

iOS Mobile Support

Layer / File(s) Summary
iOS build and deployment pipeline
.gitignore, ent.plist, perry.toml, tools/deploy-ios.sh, tools/build-world.ts
Adds iOS project metadata, entitlements, build/sign/install/launch automation, normalized generated-file paths, and ignored app/build artifacts.
Touch input and controls
src/input.ts
Adds mobile input state, touch movement and aiming, touch actions, weapon switching, reload detection, touch-slot tracking, and touch HUD rendering.
Mobile gameplay and HUD behavior
src/main.ts
Adds mobile render settings, scaled HUD coordinates, touch reload and weapon switching, mobile prompts, simplified diagnostics, and touch-control integration.
iOS setup documentation
CLAUDE.md, README.md
Documents iOS deployment commands, signing and provisioning, touch behavior, rendering differences, and device requirements.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Developer
  participant deploy-ios.sh
  participant perry
  participant codesign
  participant iOSDevice
  Developer->>deploy-ios.sh: run deployment script
  deploy-ios.sh->>perry: compile iOS target
  deploy-ios.sh->>codesign: sign app with ent.plist
  deploy-ios.sh->>iOSDevice: install app
  deploy-ios.sh->>iOSDevice: launch app
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main iPhone support additions: touch controls, mobile rendering, and device build/deploy.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ios/touch-controls-and-mobile-profile

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

GTAO was cut alongside SSGI and SSR on the assumption that all three
screen-space passes were unaffordable. Measured on an iPhone 16 Pro, that's
wrong for GTAO: mid-wave the frame sits at 25.0 ms with it and 25.5-27.2 ms
without, and the title screen holds 60 fps either way. Same frame-rate tier,
and it buys back the contact shadows that seat the grass, trees and aliens on
the ground rather than leaving them pasted over it.

Read the "free" precisely. Present mode is Fifo on a 120 Hz panel, so every
frame snaps to a multiple of 8.33 ms — GTAO is being absorbed by slack inside
a bucket, not costing nothing. It eats margin, and it's the first thing to put
back if a heavier wave starts tipping frames from the 25 ms bucket into the
33 ms one.

Per-pass GPU timings would have settled this exactly, but the profiler reports
-1 µs for every pass on iOS (the Metal backend never gets TIMESTAMP_QUERY), so
wall-clock sampled at a fixed point in the wave is the honest instrument.
@proggeramlug

Copy link
Copy Markdown
Contributor Author

Update: GTAO is back on (51cea81). I'd cut it with SSGI and SSR on the assumption all three screen-space passes were unaffordable. Measured on the device, that assumption was wrong for GTAO.

title screen mid-wave (steady)
GTAO off 16.67 ms (60 fps) 25.5–27.2 ms (~37–39 fps)
GTAO on 16.67 ms (60 fps) 25.0 ms (40 fps)

Same frame-rate tier either way, so it's contact shadows for no measurable cost — the grass, trees and aliens now sit on the ground instead of looking pasted over it.

Two caveats worth recording, because they change how you'd read these numbers:

"Free" means there was slack in the bucket, not that the pass is free. Present mode is Fifo on a 120 Hz panel, so every frame snaps to a multiple of 8.33 ms — 16.67 / 25.0 / 33.3. GTAO is being absorbed inside the 25 ms bucket. It eats margin, and it's the first thing to put back if a heavier wave starts tipping frames into the 33 ms bucket.

The profiler can't help here. getProfilerOverlay() reports gpuUs = -1 for every pass on iOS — the Metal backend never gets TIMESTAMP_QUERY — so per-pass GPU timing, which would have settled the cost exactly, isn't available on device. Wall-clock sampled at a fixed point in the wave is the honest instrument. That's arguably a gap worth filing against the engine separately.

path.relative() returns the host's separator, so regenerating on Windows and
on macOS produced headers differing only in `\` vs `/`. src/generated/world.ts
came back dirty in git after every build on whichever machine hadn't produced
it last — pure churn, and easy to sweep into an unrelated commit by accident.
@proggeramlug proggeramlug merged commit 9748368 into main Jul 11, 2026
1 check passed
@proggeramlug proggeramlug deleted the ios/touch-controls-and-mobile-profile branch July 11, 2026 12:54

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
README.md (1)

38-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Specify a language for the fenced code block.

The code block at line 38 has no language specifier. markdownlint flags this as MD040. Add bash to the opening fence.

📝 Proposed fix
-```
+```bash
 ./tools/deploy-ios.sh           # build, sign, install, launch
 ./tools/deploy-ios.sh --console # ...and stream the device log
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @README.md around lines 38 - 41, Update the fenced code block containing the
deploy-ios.sh commands in README.md to specify bash on its opening fence,
preserving the existing commands and closing fence.


</details>

<!-- cr-comment:v1:17725b32f22a68eb1778bed3 -->

_Source: Linters/SAST tools_

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @src/input.ts:

  • Around line 148-156: Update the touch-steering logic in the loop containing
    lookSlot and fireSlot so each finger maintains independent last-position state.
    Add and initialize fire-specific coordinates, use them when i === fireSlot, and
    update them after processing; keep lookLastX/lookLastY exclusively for lookSlot
    to prevent baseline resets when both touches steer.

In @src/main.ts:

  • Around line 235-246: Reconcile the MOBILE profile’s GTAO setting with the
    documented guideline and PR decision: either change setSsaoEnabled(true) to
    disable GTAO while preserving the other mobile settings, or update the relevant
    guideline to explicitly document the measured re-enable decision. Ensure the
    code and documentation consistently describe the intended mobile behavior.

Nitpick comments:
In @README.md:

  • Around line 38-41: Update the fenced code block containing the deploy-ios.sh
    commands in README.md to specify bash on its opening fence, preserving the
    existing commands and closing fence.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: defaults

**Review profile**: CHILL

**Plan**: Pro Plus

**Run ID**: `311685ec-1590-4af1-94b5-77ff9590a47f`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 7720415e212b50a6b5c8107368f0f61681014c8c and 016926b9830ef10cda0681fbe13a6f10680c62b9.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `src/generated/world.ts` is excluded by `!**/generated/**`

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `.gitignore`
* `CLAUDE.md`
* `README.md`
* `ent.plist`
* `perry.toml`
* `src/input.ts`
* `src/main.ts`
* `tools/build-world.ts`
* `tools/deploy-ios.sh`

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread src/input.ts
Comment on lines +148 to +156
if (i === lookSlot || i === fireSlot) {
// The trigger finger steers too, so you can hold fire and keep aiming
// with the same thumb instead of having to choose.
lookX = lookX + ((tx - lookLastX) / sw) * TOUCH_YAW_PER_SCREEN;
lookY = lookY + ((ty - lookLastY) / sh) * TOUCH_PITCH_PER_SCREEN;
lookLastX = tx;
lookLastY = ty;
continue;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant declarations and the cited section with line numbers.
grep -nE 'lookSlot|fireSlot|lookLastX|lookLastY|fireLastX|fireLastY|TOUCH_YAW_PER_SCREEN|TOUCH_PITCH_PER_SCREEN|hits\(' src/input.ts || true

echo '--- context around cited lines ---'
nl -ba src/input.ts | sed -n '100,230p'

Repository: Bloom-Engine/shooter

Length of output: 1502


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- src/input.ts 120-205 ---'
sed -n '120,205p' src/input.ts | cat -n

Repository: Bloom-Engine/shooter

Length of output: 3610


Track fireSlot with its own last coordinates

fireSlot reuses lookLastX/lookLastY, so a moving fire finger can reset the look baseline and produce yaw/pitch jumps when both touches are steering. Use separate last-position state for the fire finger.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/input.ts` around lines 148 - 156, Update the touch-steering logic in the
loop containing lookSlot and fireSlot so each finger maintains independent
last-position state. Add and initialize fire-specific coordinates, use them when
i === fireSlot, and update them after processing; keep lookLastX/lookLastY
exclusively for lookSlot to prevent baseline resets when both touches steer.

Comment thread src/main.ts
Comment on lines +235 to +246
if (MOBILE) {
setSsgiEnabled(false);
setSsrEnabled(false);
setSsaoEnabled(true);
setShadowsEnabled(true);
setBloomEnabled(true);
setTaaEnabled(true);
setRenderScale(0.5);
// Sun shafts are a full-screen radial blur — pure cost for a pass the player
// only sees when looking near the sun. Strength 0 is off.
setSunShafts(0, 0.90, 1.0, 0.95, 0.7);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

GTAO/SSAO stays enabled, but the guideline says the MOBILE profile drops it.

setSsaoEnabled(true) (GTAO, per the comment) keeps the pass on. The repo guideline states the mobile profile should drop SSGI, SSR, GTAO, and sun shafts while retaining shadows and bloom. The PR notes document a deliberate re-enable after device measurements, so the code and the guideline now disagree. Reconcile them — either disable GTAO here or update the guideline to reflect the measured decision — so future reviews don't treat this as a regression.

As per coding guidelines: "On mobile, keep Lumen software GI disabled and use the MOBILE render profile that drops SSGI, SSR, GTAO, and sun shafts while retaining shadows and bloom."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main.ts` around lines 235 - 246, Reconcile the MOBILE profile’s GTAO
setting with the documented guideline and PR decision: either change
setSsaoEnabled(true) to disable GTAO while preserving the other mobile settings,
or update the relevant guideline to explicitly document the measured re-enable
decision. Ensure the code and documentation consistently describe the intended
mobile behavior.

Source: Coding guidelines

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