Skip to content

Epic (Phase 3): full analog gamepad state on sokol — Android-TV gameplay #250

@apotema

Description

@apotema

Epic (Phase 3): Full analog gamepad state on sokol — Android-TV gameplay

Parent: labelle-toolkit/labelle-engine#609. The expensive part. Builds on Phase 1 sokol detection (#248, #249), which delivered presence/identity only.

Goal

Read live button and axis state (sticks, triggers, all buttons, D-pad) on the sokol backend, so Android-TV games — which are sokol-only (verified) — get full-gameplay controller input. Today sokol's gamepad methods are hard stubs (backends/sokol/src/input.zig:91).

The core problem

sokol_app owns the AInputQueue and exposes no hook to intercept raw native input events (verified). Button presses (KeyEvent) and sticks (MotionEvent SOURCE_JOYSTICK) flow into sokol and are dropped — gamepad button keycodes almost certainly aren't in sokol's Android keymap. So unlike detection, state genuinely needs the event stream sokol consumes.

Approach — DECIDED (see prior-art research below)

sokol here is a labelle-maintained fork (commit 887b30f), which makes patching viable:

  1. Patch the sokol fork to forward unhandled AInputEvents (passthrough callback for events sokol doesn't consume). At the NDK level these arrive as Android semantic constants (AKEYCODE_BUTTON_A, AMOTION_EVENT_AXIS_LTRIGGER, …) — not raw HID. This is exactly the input Unreal's FAndroidInputInterface consumes.
  2. Mapping = Android semantic constants + a small quirk table (the Unreal/Unity model), not Paddleboat and not a from-scratch SDL DB. Android's HID/KeyLayout layer already normalizes most pads to semantic codes; we only need a handful of per-device axis-routing overrides (UE ships ~5, Unity ships 2). Optionally seed the quirk table from SDL gamecontrollerdb.txt later (Godot model) if coverage demands — not required to ship.
  3. Wire results into the existing poll methods (isGamepadButtonDown/Pressed, getGamepadAxisValue), replacing the stubs.
  4. Back-button trap: controller B often maps to KEYCODE_BACK on Android TV — intercept so it doesn't exit the app.

Also in scope (other sokol platforms)

Flow surface (currently missing)

  • flow-codegen has no gamepad reporter nodes — add IsGamepadButtonDown/Pressed/Released, GetGamepadAxisValue so flows can poll state (parallels the existing keyboard/mouse reporters).

Prior-art research — Paddleboat vs. hand-rolled (RESOLVED → hand-rolled)

Checked how Godot, Unreal, and Unity handle Android gamepads (primary sources):

Engine Native source Mapping Identity Paddleboat
Godot Java onKeyDown/onGenericMotionEvent → JNI semantic codes + SDL gamecontrollerdb.txt + own DB device name (weak) No
Unreal GameActivity extends NativeActivity + native AInputEvent semantic AKEYCODE_BUTTON_* + small per-device quirk table getDescriptor() No
Unity Java UnityPlayerActivity.injectEvent semantic constants + layout matcher + 2 vendor cases deviceDescriptor No

Conclusions:

  • None use Paddleboat. It postdates their input stacks and would mean a second mapping system; it targets projects with no input layer. Not adopting it.
  • "Hand-rolled" is cheap here, because Android pre-normalizes to semantic constants — the feared "maintain a gamecontrollerdb.txt" tarpit does not apply; a small quirk table suffices (UE/Unity proof).
  • Unreal is our exact analog (NativeActivity + native AInputEvent + quirk table + getDescriptor + InputManager hotplug). We mirror it: state via native AInputEvent (this issue), hotplug/identity via JNI InputManager/getDescriptor (feat(sokol/android): JNI InputManager gamepad detection source — centerpiece (Phase 1) #248).
  • Use getDescriptor() for identity (beats Godot's name-based scheme for free).

Acceptance criteria

  • Android TV: full button/axis state readable during gameplay via the standard poll methods.
  • Logical mapping correct for common pads (Xbox/DualSense/generic) via semantic constants + quirk table; no Paddleboat dependency.
  • B-button does not exit the app.
  • sokol Linux + wasm state reads working.
  • flow gamepad reporter nodes available.

Non-goals

  • Detection/identity (Phase 1). Player mapping (Phase 2).

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