Skip to content

feat(apple): gamepad on macOS/iOS + iOS AVAudioSession#125

Open
proggeramlug wants to merge 2 commits into
mainfrom
feat/apple-gamepad-lifecycle-audio
Open

feat(apple): gamepad on macOS/iOS + iOS AVAudioSession#125
proggeramlug wants to merge 2 commits into
mainfrom
feat/apple-gamepad-lifecycle-audio

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Second batch from the 2026-07-19 audit — the Apple native work. Unlike #124, parts of this need on-device testing: this environment has only CommandLineTools (no Xcode/iOS SDK), so the iOS crate can't be compiled here (its minimp3/Jolt C deps need the SDK). Verification status is called out per-item below.

Gamepad — macOS & iOS

Two audit findings in one: the shared gamepad FFI reads engine().input, but only tvOS/visionOS ever populated it — macOS and iOS had zero gamepad support (isGamepadAvailable permanently false), despite EN-031 being marked "shipped — a pad drives the shooter on iPhone." It never did on Apple.

  • Poll the first connected controller via GameController, resolved dynamically through the ObjC runtime (no new dependency). No-op when the framework or a controller is absent. Button/axis indices match the tvOS/visionOS map so controls carry across targets.
  • Fixed a real bug found in the tvOS reference while porting it: it reads GCControllerAxisInput.value (an ObjC float) as f64 — an ABI mismatch (float vs double return in different-width registers) — and calls a bogus xAxis_value selector. Those crates aren't in CI, so it was never caught. This version uses the correct two-step xAxisvalue as f32.
  • Shared input::reset_gamepad() clears polled axes/buttons at the top of each poll so a released button reads released (begin_frame doesn't reset gamepad state — the same latent stuck-button bug the tvOS poller has).

Verification: macOS compiles and is the reference implementation. iOS is byte-identical logic.

iOS AVAudioSession

iOS/tvOS/visionOS brought up RemoteIO with no AVAudioSession, so audio defaulted to SoloAmbient — silenced by the ring/silent switch, stopped on screen-lock. Set the Playback category + activate before RemoteIO. Done via the ObjC runtime; the category constant's NSString value equals its symbol name, so no AVFAudio link needed.

Verification: the msg_send patterns (setCategory:error:, setActive:error:, NSString::from_str, Bool return) were type-checked via an equivalent snippet compiled on macOS (all dynamic AnyClass/AnyObject, so it type-checks without the class existing).

What I deliberately did NOT do, and why

  • Lifecycle / backgrounding handlers — the audit flagged a backgrounding crash risk. I traced it: the shared renderer's acquire_frame (draw2d.rs:100) already handles every surface-loss error by logging, reconfiguring, and skipping the frame — no panic. So backgrounding doesn't crash; explicit observers would be a battery/behavior refinement, not a crash fix. Not worth writing blind NSNotification/scene-delegate wiring for.
  • tvOS/visionOS gamepad ABI fix (the f64/xAxis_value bug above) — a correctness fix to code I can't compile here; lower priority since it only triggers when a controller connects. Straightforward to port from this PR's correct macOS/iOS version.
  • tvOS/visionOS AVAudioSession — same gap, but those backends don't import objc2 and tvOS has no silent switch (low value); skipped rather than add blind imports with uncertain feature availability.
  • Device-rate audio query — investigated: macOS/iOS RemoteIO and the output AudioUnit resample client→hardware, so the hardcoded 44.1k is self-consistent (no audible pitch bug). Not the bug the audit implied on native.

Test plan (needs a Mac with Xcode)

  1. Build+run macOS with a connected Xbox/PS/MFi pad → isGamepadAvailable true, sticks/buttons/triggers/d-pad drive input.
  2. Same on an iOS device/simulator.
  3. iOS: confirm game audio plays with the silent switch ON.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added GameController support on iOS and macOS for thumbsticks, triggers, buttons, and directional controls.
    • Added keyboard-repeat detection support for web and watchOS targets.
    • Improved texture-array and material data handling for more reliable asset loading.
  • Bug Fixes

    • Improved audio stability under heavy playback by limiting simultaneous sounds and music tracks.
  • Documentation

    • Updated platform backend details and clarified optional hardware ray-tracing support.
    • Expanded path-tracing roadmap coverage.

Ralph and others added 2 commits July 19, 2026 04:14
…docs)

Main CI was red on ffi-parity, and fixing that exposed a second failure
(the file-line gate) that had been hidden behind it — the EN-063 "gate
nobody reads" pattern, recurring.

CI:
- Export bloom_is_key_repeated on web (input_ffi.rs) and watchOS
  (regenerated ffi_stubs.rs via gen_stubs.js). validate-ffi green.
- Split renderer/mod.rs 13156 -> 12556 by moving ~30 GPU-uniform POD
  structs + build_card_ortho_v2 into types.rs (their documented home).
  Struct/field order is byte-identical, so this is layout-neutral; the
  golden-image suite (raster) and 128 unit tests pass. Baseline ratcheted
  to 12556. (EN-052)

Silent-failure FFI bugs (TS API):
- loadMaterial called bloom_set_material_params, which is NOT in the
  manifest, so Perry silently no-op'd it and every material's params
  array was dropped. Reroute through the working _scratch path.
- createTextureArray/createTextureArrayEx were exported but uncallable
  from Perry (number[] into an i64 pointer). Reroute through the same
  scratch path so they work; non-breaking (no Perry caller could reach
  them before).

Audio RT-safety (audio/render.rs):
- The 65th concurrent voice reallocated the voices Vec ON the audio
  thread. Add voice-stealing (drop the quietest) so the render thread
  never grows past its preallocated cap; same for music. Add 2
  regression tests to a file that had none.

Docs:
- README: Windows audio XAudio2 -> WASAPI, Linux PulseAudio -> ALSA,
  drop the Wayland claim. CLAUDE.md: same ALSA fix.
- lumen-roadmap platform matrix: HW ray-query is opt-in, not auto.
- pt-roadmap ticket table: add the shipped PT-3b/6/7/8 rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two audit findings: the shared gamepad FFI reads engine().input, but only
tvOS/visionOS ever populated it — macOS and iOS had NO gamepad support
(isGamepadAvailable permanently false), despite the EN-031 ticket claiming
"a pad drives the shooter on iPhone." And iOS/tvOS/visionOS bring up
RemoteIO with no AVAudioSession, so audio defaults to SoloAmbient (silenced
by the ring/silent switch, stops on screen lock).

Gamepad (macOS compile-verified; iOS identical code, blind):
- Poll the first connected controller via the GameController framework,
  resolved dynamically through the ObjC runtime (no GameController link
  dep). No-op when the framework or a controller is absent. Button/axis
  indices match the tvOS/visionOS map so controls carry across targets.
- Wrote the CORRECT GameController API: GCControllerAxisInput.value and
  GCControllerButtonInput.value are ObjC `float`, read as f32. The tvOS
  reference reads them as f64 (an ABI mismatch — float and double return
  in different-width registers) and uses a bogus `xAxis_value` selector;
  those crates aren't in CI so it was never caught. This version is right.
- Shared input::reset_gamepad() clears polled axes/buttons at the top of a
  poll so a released button reads released (begin_frame does not reset
  gamepad state — the same latent stuck-button bug the tvOS poller has).

iOS AVAudioSession (blind; the msg_send syntax was type-checked via an
equivalent macOS snippet):
- Set the Playback category + activate before RemoteIO so game audio isn't
  killed by the silent switch. Done via the ObjC runtime; the category
  constant's NSString value equals its symbol name, so no AVFAudio link.

VERIFICATION STATUS: macOS gamepad compiles + is the reference. iOS gamepad
is byte-identical logic to the compiled macOS version; iOS AVAudioSession
msg_send patterns type-check on macOS. The iOS crate itself cannot be
compiled in this environment (its minimp3/Jolt C deps need the iOS SDK),
so the iOS paths need an on-device build+run to fully confirm. tvOS/visionOS
gamepad ABI fix and the lifecycle/backgrounding handlers are NOT in this
commit — see the PR description.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The pull request centralizes renderer uniform types, adds native controller and audio handling, migrates TypeScript FFI operations to scratch buffers, exports key-repeat queries, and updates architecture and rendering roadmap documentation.

Changes

Renderer type centralization

Layer / File(s) Summary
Shared renderer contracts
native/shared/src/renderer/types.rs
Adds POD uniform layouts, GI and path-tracing parameters, bake-job state, card projection construction, and post-processing types.
Renderer module migration
native/shared/src/renderer/mod.rs, tools/file-lines-baseline.json
Imports shared renderer types, removes local definitions, and updates the recorded line-count baseline.

Native input and audio runtime

Layer / File(s) Summary
Polled gamepad state
native/shared/src/input.rs, native/ios/src/lib.rs, native/macos/src/lib.rs
Adds gamepad reset support and polls extended controllers before frame processing on iOS and macOS.
iOS audio session setup
native/ios/src/lib.rs
Configures and activates the playback audio session before RemoteIO initialization.
Bounded audio playback
native/shared/src/audio/render.rs
Caps voice and music collections, evicts entries at capacity, and adds coverage for capacity and quietest-voice eviction.
Key-repeat exports
native/web/src/input_ffi.rs, native/watchos/src/ffi_stubs.rs
Adds the web key-repeat query and a watchOS FFI stub.

Scratch-buffer FFI paths

Layer / File(s) Summary
Scratch FFI declarations and calls
src/core/index.ts, src/models/index.ts
Removes direct material-parameter bindings and routes material parameters and texture-array data through scratch buffers.

Architecture and roadmap documentation

Layer / File(s) Summary
Platform architecture labels
CLAUDE.md, README.md
Updates Linux and Windows backend labels.
Ray-query and path-tracing roadmaps
docs/perf/lumen-roadmap.md, docs/pt/pt-roadmap.md
Documents opt-in hardware ray-query behavior and adds denoising and skinned-mesh roadmap tickets.

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

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately highlights the main Apple gamepad and iOS audio-session changes.
Description check ✅ Passed The description follows the template with Summary, Test plan, and reviewer notes, and it provides substantive details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 feat/apple-gamepad-lifecycle-audio

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.

@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

🤖 Prompt for all review comments with 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.

Inline comments:
In `@docs/perf/lumen-roadmap.md`:
- Around line 41-44: Align the as-built correction with the platform behavior
documented in the PT roadmap: either narrow the opt-in statement to platforms
gated by BLOOM_HW_GI, BLOOM_PT, or --pt, or update the platform defaults so
macOS/Linux and Windows share the same contract. Ensure the “HW path”
capability-versus-default clarification remains accurate across both roadmap
documents.

In `@src/models/index.ts`:
- Around line 588-590: Update both texel packing expressions in
src/models/index.ts at lines 588-590 and 630-632 to wrap the bitwise combination
in parentheses and apply an unsigned right shift (>>> 0), ensuring values with
the high bit set remain positive unsigned 32-bit values before being passed to
bloom_mesh_scratch_push_u32.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 74a871e4-d305-47c7-8f1a-8ef98b45c51f

📥 Commits

Reviewing files that changed from the base of the PR and between e498433 and b306ab0.

📒 Files selected for processing (15)
  • CLAUDE.md
  • README.md
  • docs/perf/lumen-roadmap.md
  • docs/pt/pt-roadmap.md
  • native/ios/src/lib.rs
  • native/macos/src/lib.rs
  • native/shared/src/audio/render.rs
  • native/shared/src/input.rs
  • native/shared/src/renderer/mod.rs
  • native/shared/src/renderer/types.rs
  • native/watchos/src/ffi_stubs.rs
  • native/web/src/input_ffi.rs
  • src/core/index.ts
  • src/models/index.ts
  • tools/file-lines-baseline.json
💤 Files with no reviewable changes (1)
  • src/core/index.ts

Comment on lines +41 to +44
> **As-built correction (2026-07):** the "HW path" column below describes
> *capability*, not the default. The HW ray-query path is **opt-in**
> everywhere (`BLOOM_HW_GI=1` / `BLOOM_PT` / `--pt`); SW GI is the shipping
> default on every platform. See the as-built note under the table.

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

Align the opt-in claim with the actual platform behavior.

Line 43 says HW ray-query is opt-in everywhere, but docs/pt/pt-roadmap.md Lines 105-110 says macOS/Linux request ray query by default and only Windows uses the explicit opt-in gates. Narrow this note to the platforms that implement opt-in gating, or update the platform behavior so both roadmaps describe the same contract.

🤖 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 `@docs/perf/lumen-roadmap.md` around lines 41 - 44, Align the as-built
correction with the platform behavior documented in the PT roadmap: either
narrow the opt-in statement to platforms gated by BLOOM_HW_GI, BLOOM_PT, or
--pt, or update the platform defaults so macOS/Linux and Windows share the same
contract. Ensure the “HW path” capability-versus-default clarification remains
accurate across both roadmap documents.

Comment thread src/models/index.ts
Comment on lines +588 to +590
bloom_mesh_scratch_push_u32(
bytes[b] | (bytes[b + 1] << 8) | (bytes[b + 2] << 16) | (bytes[b + 3] << 24),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Prevent texel data corruption by using unsigned bitwise shift.

In JavaScript and TypeScript, bitwise operators return signed 32-bit integers. If the fourth byte (e.g., an alpha channel of 128 or greater) sets the highest bit, the packed result evaluates as a negative integer. When this negative float is passed across the FFI to Rust and cast to u32, it saturates to 0 (since Rust 1.45+), resulting in completely corrupted texels. Appending >>> 0 forces the result to an unsigned 32-bit integer, preserving the bit pattern correctly as a positive f64.

  • src/models/index.ts#L588-L590: wrap the bitwise packing expression in parentheses and apply >>> 0.
  • src/models/index.ts#L630-L632: apply the same >>> 0 unsigned right shift to the texel packing logic.
🐛 Proposed fix
-      bytes[b] | (bytes[b + 1] << 8) | (bytes[b + 2] << 16) | (bytes[b + 3] << 24),
+      ((bytes[b] | (bytes[b + 1] << 8) | (bytes[b + 2] << 16) | (bytes[b + 3] << 24)) >>> 0),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
bloom_mesh_scratch_push_u32(
bytes[b] | (bytes[b + 1] << 8) | (bytes[b + 2] << 16) | (bytes[b + 3] << 24),
);
bloom_mesh_scratch_push_u32(
((bytes[b] | (bytes[b + 1] << 8) | (bytes[b + 2] << 16) | (bytes[b + 3] << 24)) >>> 0),
);
📍 Affects 1 file
  • src/models/index.ts#L588-L590 (this comment)
  • src/models/index.ts#L630-L632
🤖 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/models/index.ts` around lines 588 - 590, Update both texel packing
expressions in src/models/index.ts at lines 588-590 and 630-632 to wrap the
bitwise combination in parentheses and apply an unsigned right shift (>>> 0),
ensuring values with the high bit set remain positive unsigned 32-bit values
before being passed to bloom_mesh_scratch_push_u32.

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