feat(bgfx): register AndroidBackendContext + drop sokol shims → gamepad on bgfx-Android (#310 stage 4) - #313
Conversation
…310 stage 4) Make the bgfx Android backend register a real AndroidBackendContext with core's #310 seam, reusing the existing JNI gamepad glue, and remove the inert sokol-compat shims from the generated bgfx-Android main — so gamepad detection/state works on bgfx-Android (parity with the sokol Stage 3 path). Shared glue (promote): the backend-agnostic Android gamepad STATE machine (android_gamepad_state.zig, #250) + InputManager JNI DETECTION glue (android_gamepad_jni.c, #248) move out of backends/sokol/src into a new in-tree sub-package backends/android_gamepad, consumed by BOTH sokol and bgfx via `.path = "../android_gamepad"`. The pure-Zig state module is the `android_gamepad` module; the JNI .c is pulled via `dep.path(...)` and compiled into each backend's `input` module (cross-package `b.path("..")` is rejected by Zig 0.16). Sokol updated to consume the shared package; its host + Android compile-checks stay green. bgfx adapter: backends/bgfx/src/android.zig builds the AndroidBackendContext from the shell's stored ANativeActivity* (exported as the C symbol labelle_bgfx_get_native_activity to avoid a module cycle) + the shared JNI glue (extern "c"). Re-exported Android-gated from the bgfx `input` module. The shell (android_app.zig) now routes gamepad AInputEvents — KEY events for buttons, JOYSTICK-source MOTION events for analog axes/hat — into the shared state via input.zig, alongside the existing touch path; newFrame snapshots the gamepad edge. Generated main: android.txt drops the three exported sokol-compat shims (sapp_android_get_native_activity + inert labelle_android_gamepad_init/ _shutdown) and instead registers the seam once at gameInit startup via `engine.core.registerAndroidBackend(@import("backend_input").android.backendContext())`. The generated build unifies the app core onto the bgfx `input` module (overrideImport) so the registered vtable's type matches the engine's, and the deps linker stages the android_gamepad sub-package for sokol + bgfx. Verified: assembler build+test green; bgfx/sokol host tests + android_gamepad sub-pkg tests green; desktop bgfx + sokol examples build; bgfx-android .so builds with the JNI bridge linked and NO sapp_* shim export (nm -D); on-device the .so dlopens cleanly and the NativeActivity runs (the old shim removal previously crashed the loader). Live gamepad input needs a controller paired to the tablet to confirm end-to-end.
PR SummaryMedium Risk Overview A new bgfx gains Codegen removes the three exported shims from Reviewed by Cursor Bugbot for commit b3508f1. Bugbot is set up for automated code reviews on this repo. Configure here. |
There was a problem hiding this comment.
Code Review
This pull request refactors the Android gamepad support by moving the state machine and JNI detection glue into a shared android_gamepad sub-package, which is now integrated into both the sokol and bgfx backends. Key changes include implementing the bgfx Android adapter, wiring up input event handling for touch and gamepads, and updating the build system and codegen templates to register the backend context. Feedback on these changes highlights two issues: first, unconditionally returning 0 for key events in android_app.zig can cause unexpected exits when pressing gamepad buttons that map to AKEYCODE_BACK; second, the extern "c" declaration for labelle_android_gamepad_init in android.zig has a type mismatch and should use ?*const anyopaque to correctly match the C signature.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| if (etype == AINPUT_EVENT_TYPE_KEY) { | ||
| // Controller buttons (BUTTON_A/B/X/Y, L1/R1/L2/R2, thumbs, start/ | ||
| // select/mode) and DPAD_* arrive as key events. Forward the raw | ||
| // keycode; the shared state module maps it to a canonical button | ||
| // (and ignores non-gamepad keys). We do NOT consume it (return 0) so | ||
| // system keys (BACK/HOME/volume) still reach their default handlers. | ||
| const keycode = AKeyEvent_getKeyCode(event); | ||
| const action = AKeyEvent_getAction(event); | ||
| if (action == AKEY_EVENT_ACTION_DOWN) { | ||
| input.applyGamepadKey(device_id, keycode, true); | ||
| } else if (action == AKEY_EVENT_ACTION_UP) { | ||
| input.applyGamepadKey(device_id, keycode, false); | ||
| } | ||
| return 0; | ||
| } |
There was a problem hiding this comment.
Returning 0 (unhandled) unconditionally for all key events means that system-traversed keys like AKEYCODE_BACK (value 4, which commonly maps to the "B" button on many Android gamepads) will be passed back to the OS. This will trigger the default Android back-button behavior, causing the application to unexpectedly exit/close when the user presses the "B" button. Consider implementing an interception mechanism (similar to shouldConsumeBack in the sokol backend) to conditionally return 1 (handled) for gamepad buttons or specifically for AKEYCODE_BACK when gamepad input is active.
| // Defined as no-ops off Android, so declaring them is safe everywhere — but we | ||
| // only ever wire them into a registered context on Android. `extern "c"` | ||
| // already implies the C calling convention. | ||
| extern "c" fn labelle_android_gamepad_init(activity: ?*anyopaque) void; |
There was a problem hiding this comment.
The extern "c" declaration for labelle_android_gamepad_init uses ?*anyopaque for the activity parameter, but the actual C signature (as implemented in android_gamepad_jni.c) expects const void *, which maps to ?*const anyopaque in Zig. This mismatch should be corrected to ensure type safety and consistency with the sokol backend's declaration.
extern "c" fn labelle_android_gamepad_init(activity: ?*const anyopaque) void;
…activity (review) Gemini (high): onInputEvent returned 0 for every key event, so a controller's B/"circle"/select button — which many pads emit as AKEYCODE_BACK — triggered Android back-navigation and finished the activity (the game quit) on press. Consume AKEYCODE_BACK only when it originates from a gamepad/joystick source; the genuine system BACK (touchscreen/system source) stays unhandled and still navigates. Mirrors sokol's B->BACK guard (assembler#248). (Declined the companion `?*anyopaque` vs C `const void*` finding: the extern must be `?*anyopaque` to match the seam's gamepad_init vtable field type — the merged sokol adapter uses the same — and it's ABI-compatible with `const void*`.) Verified: bgfx android compile-check + desktop test green.
|
Bot triage:
Verified: bgfx Android compile-check + desktop test green. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit b3508f1. Configure here.
| input.applyGamepadKey(device_id, keycode, true); | ||
| } else if (action == AKEY_EVENT_ACTION_UP) { | ||
| input.applyGamepadKey(device_id, keycode, false); | ||
| } |
There was a problem hiding this comment.
Key cancel leaves buttons stuck
Low Severity
Gamepad key handling in onInputEvent only updates shared state on AKEY_EVENT_ACTION_DOWN and AKEY_EVENT_ACTION_UP. Android can emit AKEY_EVENT_ACTION_CANCEL when focus is lost or input is interrupted; those events are ignored, so a held button can remain down in android_gamepad until another edge arrives.
Reviewed by Cursor Bugbot for commit b3508f1. Configure here.
Backend work since v0.39.1: - wgpu: macOS Metal surface + textured sprite rendering (#290, #291) - bgfx: macOS bring-up to on-device Android (#296 epic, #304/#305/#307/#308/#309) - #310 AndroidBackendContext adapters: sokol (#312) + bgfx (#313) register the core seam; bgfx-Android gamepad via the shared android_gamepad sub-package - bgfx desktop gamepad: GLFW (#315) + SDL HIDAPI / Switch-pad support (#318) - cached bgfx CI job (#295) Android codegen now calls core.registerAndroidBackend → requires labelle-core >= v1.17.0 + labelle-engine >= v1.50.0.


Stage 4 (final) of labelle-core#310 — the bgfx Android backend registers an
AndroidBackendContextwith core's seam and drops the sokol-compat shims, so gamepad works on bgfx-Android. Completes the seam refactor (core #1, engine #2, sokol adapter #3 merged).Reuse the JNI glue: promote to a shared sub-package
Cross-package
b.path("..")is rejected in Zig 0.16, so the backend-agnostic gamepad glue moved into a new in-tree sub-packagebackends/android_gamepad/(matching the toolkit sub-package convention):android_gamepad_jni.c(feat(sokol/android): JNI InputManager gamepad detection source — centerpiece (Phase 1) #248 InputManager JNI) — each backend compiles it viadep.path("src/android_gamepad_jni.c")into its NDK-sysroot input module.android_gamepad_state.zig(Epic (Phase 3): full analog gamepad state on sokol — Android-TV gameplay #250 button/axis state) — theandroid_gamepadmodule.Both sokol and bgfx now consume it (
.path = "../android_gamepad"). Sokol updated + stays green.bgfx adapter + shell
backends/bgfx/src/android.zig(new, Android-gated): buildscore.AndroidBackendContext.get_native_activityreads the shell's activity via the C symbollabelle_bgfx_get_native_activity(a C bridge avoids a module cycle);gamepad_init/shutdownareextern "c"to the shared glue. Re-exported Android-gated frominput.zig.android_app.zig: exportslabelle_bgfx_get_native_activity;onInputEventnow routes gamepad KEY events (buttons) + JOYSTICK-source MOTION (axes/hat) into the shared state viainput.applyGamepadKey/Motion, keeping touch working.input.zig: gamepad queries route to the shared state on Android.Codegen — shims out, registration in
templates/android.txt+main_template.zig: removed the three exportedsapp_*/labelle_android_gamepad_*shims;gameInitnow callsengine.core.registerAndroidBackend(@import("backend_input").android.backendContext())once at startup.build_zig.txt(backend_bgfx_android):overrideImports the app core ontobackend_inputso the registered vtable type unifies with the engine's;deps_linker.zigstages theandroid_gamepadsub-package.Verification (independent)
zig build+test, the newandroid_gamepadsub-package tests, sokol desktop (glue moved), and bgfx desktop + example — all green..so: generatedexamples/bgfx-android --platform android+zig build→libgame.so(aarch64 ELF).nm -D: nosapp_android_get_native_activityexport (shim gone); the real bridge is present (labelle_bgfx_get_native_activity,labelle_android_gamepad_init,labelle_android_on_device_added,ANativeActivity_onCreate,android_main). Generated main has the singleregisterAndroidBackendcall, no shims.libgame.so … okand the NativeActivity comes up with nocannot locate symbol/ native crash (the shim removal previously crashed the loader at dlopen — this is the load-bearing on-device proof).Honest scope
Build/registration/lifecycle + on-device dlopen are verified. Live gamepad input is NOT confirmed — no gamepad is paired to the tablet (
dumpsys inputshows only built-in devices). End-to-end button/axis flow needs a controller paired to the tablet, thenadb logcatfor thegamepad_connectedenumeration.