Phase 1 + 3 (combined) of controller-support epic: labelle-toolkit/labelle-engine#609.
Goal
Controller support on iOS and tvOS (Apple TV) via Apple's GameController.framework (GCController) — both detection/removal and full button/axis state in one self-contained source. iOS is sokol-only (verified, same as Android), so it rides the same Phase 0 pollGamepadEvents abstraction.
Why this is combined (and cheap)
Unlike Android, GameController.framework is fully independent of sokol's event pipeline — it delivers events via NotificationCenter and exposes pollable state objects, so it never touches the AInputQueue sokol owns. That means:
- No sokol-fork patch required (the expensive Android Phase-3 blocker doesn't exist here).
- No mapping/quirk table — Apple normalizes every supported pad to the
GCExtendedGamepad profile.
So Phase 1 (detect) and Phase 3 (state) collapse into a single issue for this platform.
Platform facts
GCController since iOS 7; iOS 13 / tvOS 13 added native Xbox Wireless + PlayStation (DualShock 4 → DualSense) over Bluetooth, alongside MFi.
- Apple TV (tvOS) is the iOS-family analog of Android TV; Siri Remote surfaces as
GCMicroGamepad.
Scope
Add an iOS/tvOS impl to the gamepad_source sub-package (the Phase 0 comptime fallback for sokol):
- Detection/removal: observe
GCControllerDidConnect/GCControllerDidDisconnect notifications → push GamepadEvents into the ring buffer drained by the engine. Enumerate GCController.controllers() at startup. Populate name (vendorName), source_class (extended gamepad vs GCMicroGamepad/Siri Remote), type_hint.
- State: implement the poll methods (
isGamepadButtonDown/Pressed, getGamepadAxisValue) against the connected controller's GCExtendedGamepad profile — buttons A/B/X/Y, shoulders, triggers, both thumbsticks, dpad. No semantic mapping needed (profile is already logical).
- Wire the sokol backend on iOS/tvOS to this source (engine fallback path from Phase 0).
- tvOS: handle Siri Remote as
dpad_remote source class; respect the menu/play-pause buttons so they don't fight system navigation.
Known limitation
Acceptance criteria
Non-goals
Phase 1 + 3 (combined) of controller-support epic: labelle-toolkit/labelle-engine#609.
Goal
Controller support on iOS and tvOS (Apple TV) via Apple's GameController.framework (
GCController) — both detection/removal and full button/axis state in one self-contained source. iOS is sokol-only (verified, same as Android), so it rides the same Phase 0pollGamepadEventsabstraction.Why this is combined (and cheap)
Unlike Android, GameController.framework is fully independent of sokol's event pipeline — it delivers events via
NotificationCenterand exposes pollable state objects, so it never touches theAInputQueuesokol owns. That means:GCExtendedGamepadprofile.So Phase 1 (detect) and Phase 3 (state) collapse into a single issue for this platform.
Platform facts
GCControllersince iOS 7; iOS 13 / tvOS 13 added native Xbox Wireless + PlayStation (DualShock 4 → DualSense) over Bluetooth, alongside MFi.GCMicroGamepad.Scope
Add an iOS/tvOS impl to the
gamepad_sourcesub-package (the Phase 0 comptime fallback for sokol):GCControllerDidConnect/GCControllerDidDisconnectnotifications → pushGamepadEvents into the ring buffer drained by the engine. EnumerateGCController.controllers()at startup. Populatename(vendorName),source_class(extended gamepad vsGCMicroGamepad/Siri Remote),type_hint.isGamepadButtonDown/Pressed,getGamepadAxisValue) against the connected controller'sGCExtendedGamepadprofile — buttons A/B/X/Y, shoulders, triggers, both thumbsticks, dpad. No semantic mapping needed (profile is already logical).dpad_remotesource class; respect the menu/play-pause buttons so they don't fight system navigation.Known limitation
getDescriptorequivalent). Within-session identity is fine; cross-session reconnect-by-hardware-id is weaker than Android/Linux. TheControllerManager(Phase 2 / Epic (Phase 2): ControllerManager — player↔controller mapping, debounced-lost, reconnect resume labelle-engine#611) reconnect-resume falls back to its heuristic path here — document it.Acceptance criteria
GamepadEvents (incl. pre-connected at startup); no polling loop needed for detection.dpad_remote; extended pads asgamepad.Non-goals