Run on iPhone: touch controls, mobile render profile, device build#9
Conversation
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.
📝 WalkthroughWalkthroughAdded iOS build and deployment support, mobile touch controls, mobile-specific rendering and HUD behavior, artifact handling, and documentation for signing, provisioning, and device deployment. ChangesiOS Mobile Support
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
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
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.
|
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.
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 The profiler can't help here. |
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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
README.md (1)
38-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSpecify a language for the fenced code block.
The code block at line 38 has no language specifier. markdownlint flags this as MD040. Add
bashto 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.mdaround 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 -->
| 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; | ||
| } |
There was a problem hiding this comment.
🎯 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 -nRepository: 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.
| 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); | ||
| } |
There was a problem hiding this comment.
📐 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
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.
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 obvious0..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 scan0..getMaxTouchPoints()and letisTouchActive()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
InputStatefields rather than injected key presses, becauseinjectKeyDown()only setskeys_downand never thekeys_pressededge —isKeyPressed(Key.R)would never have seen them. (Same reason the engine's ownVirtualButton.keydoesn'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/shbecome that logical space;swPx/shPxstay 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.tshit-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.tomlgains an[ios]table and, critically,features = ["ios-game-loop"]— UIKit needsUIApplicationMain()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.shsigns 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
MOBILEis false, souiScaleis 1, the 2D camera wrap is skipped anddrawTouchControls()no-ops. macOS build verified running.Summary by CodeRabbit
New Features
Documentation
Chores