Epic: External controller support across backends
Cross-repo effort to make controller detection, removal, and reconnection work uniformly across all render backends, with full-gameplay input on Android TV as the eventual target.
Verified constraints (drive the whole design)
- Android & iOS are sokol-only in
labelle-assembler (no lifecycle template for other backends → error.TemplateNotFound, see root.zig:851, codegen/lifecycle/loop.zig:220). So Android-TV controllers must be built on sokol — no SDL/raylib fallback exists there.
- SDL2 is desktop-only here → great for desktop Linux, irrelevant to Android.
- sokol has zero gamepad support today (hard stubs in
backends/sokol/src/input.zig:91); sapp_android_get_native_activity() is exposed but unused — the JNI escape hatch. sokol here is a labelle-maintained fork (commit 887b30f), so patching its Android input path is feasible.
- raylib is poll-only, no connection callback, no stable GUID (only
getGamepadName), MAX_GAMEPADS=4. Its existing per-frame poll in game.zig is the only correct approach for it. It does expose setGamepadMappings (SDL_GameControllerDB) + setGamepadVibration.
Design
A backend-keyed event source with a comptime platform-native fallback:
InputInterface gains an optional pollGamepadEvents(out) -> usize (and describeGamepads() diagnostic). Backends with a native gamepad layer (raylib, sdl) provide it; backends without (sokol, null) fall back to a platform gamepad source selected by target OS (evdev+udev / JNI-InputManager / HTML5 Gamepad API).
- The engine drains a uniform event queue each tick instead of the fixed 4-slot scan. This hides poll-vs-callback as a per-backend detail and removes the 4-slot cap where backends support more.
- Never double-open: when the backend has its own layer, the platform source is not compiled in (avoids duplicate events / device contention).
Phases
- Phase 0 — Foundation (no behavior change): event type + interface + engine queue-drain refactor + payload enrichment.
- Phase 1 — Detection everywhere: backend impls (raylib delegate, sdl native+GUID) + sokol platform sources (Android JNI, Linux evdev/udev, wasm Gamepad API). Connect/remove/reconnect-by-id across the board.
- Phase 2 —
ControllerManager: player↔controller mapping, debounced-lost (BT sleep churn on TV), descriptor/GUID/input_id resume, auto-pause hook, higher-level player_* events. (separate epic)
- Phase 3 — Full analog state on sokol: patch the sokol fork to forward
AInputEvents (or integrate Paddleboat/AGDK) + semantic button/axis mapping (mapping-DB). Required for Android-TV full gameplay. (separate epic — the expensive part)
Reconnection identity (verified)
| Combo |
Detect/remove |
Stable key |
| sdl (desktop) |
✅ |
✅ GUID |
| sokol+Android (JNI) |
✅ |
✅ InputDevice.getDescriptor() |
| sokol+Linux (evdev) |
✅ |
✅ input_id |
| raylib (any) |
⚠️ poll |
❌ name only |
Sub-issues
Phase 0 — Foundation:
Phase 1 — Detection everywhere:
Phase 2 — ControllerManager: #611 — player mapping, debounced-lost, reconnect resume, auto-pause.
Phase 3 — full analog state on sokol: labelle-toolkit/labelle-assembler#250 — sokol-fork AInputEvent forwarding / Paddleboat + mapping DB; required for Android-TV full gameplay. The expensive part.
Epic: External controller support across backends
Cross-repo effort to make controller detection, removal, and reconnection work uniformly across all render backends, with full-gameplay input on Android TV as the eventual target.
Verified constraints (drive the whole design)
labelle-assembler(no lifecycle template for other backends →error.TemplateNotFound, seeroot.zig:851,codegen/lifecycle/loop.zig:220). So Android-TV controllers must be built on sokol — no SDL/raylib fallback exists there.backends/sokol/src/input.zig:91);sapp_android_get_native_activity()is exposed but unused — the JNI escape hatch. sokol here is a labelle-maintained fork (commit887b30f), so patching its Android input path is feasible.getGamepadName),MAX_GAMEPADS=4. Its existing per-frame poll ingame.zigis the only correct approach for it. It does exposesetGamepadMappings(SDL_GameControllerDB) +setGamepadVibration.Design
A backend-keyed event source with a comptime platform-native fallback:
InputInterfacegains an optionalpollGamepadEvents(out) -> usize(anddescribeGamepads()diagnostic). Backends with a native gamepad layer (raylib, sdl) provide it; backends without (sokol, null) fall back to a platform gamepad source selected by target OS (evdev+udev / JNI-InputManager / HTML5 Gamepad API).Phases
ControllerManager: player↔controller mapping, debounced-lost (BT sleep churn on TV), descriptor/GUID/input_id resume, auto-pause hook, higher-levelplayer_*events. (separate epic)AInputEvents (or integrate Paddleboat/AGDK) + semantic button/axis mapping (mapping-DB). Required for Android-TV full gameplay. (separate epic — the expensive part)Reconnection identity (verified)
InputDevice.getDescriptor()input_idSub-issues
Phase 0 — Foundation:
GamepadEvent+pollGamepadEvents/describeGamepadsonInputInterfacePhase 1 — Detection everywhere:
pollGamepadEvents(poll relocate)Phase 2 —
ControllerManager: #611 — player mapping, debounced-lost, reconnect resume, auto-pause.Phase 3 — full analog state on sokol: labelle-toolkit/labelle-assembler#250 — sokol-fork
AInputEventforwarding / Paddleboat + mapping DB; required for Android-TV full gameplay. The expensive part.