Skip to content

Epic: External controller support across backends (detection, removal, reconnection) #609

@apotema

Description

@apotema

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions