Skip to content

feat(bgfx): register AndroidBackendContext + drop sokol shims → gamepad on bgfx-Android (#310 stage 4) - #313

Merged
apotema merged 2 commits into
mainfrom
feat/310-bgfx-android-gamepad
Jun 13, 2026
Merged

feat(bgfx): register AndroidBackendContext + drop sokol shims → gamepad on bgfx-Android (#310 stage 4)#313
apotema merged 2 commits into
mainfrom
feat/310-bgfx-android-gamepad

Conversation

@apotema

@apotema apotema commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Stage 4 (final) of labelle-core#310 — the bgfx Android backend registers an AndroidBackendContext with 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-package backends/android_gamepad/ (matching the toolkit sub-package convention):

bgfx adapter + shell

  • backends/bgfx/src/android.zig (new, Android-gated): builds core.AndroidBackendContext. get_native_activity reads the shell's activity via the C symbol labelle_bgfx_get_native_activity (a C bridge avoids a module cycle); gamepad_init/shutdown are extern "c" to the shared glue. Re-exported Android-gated from input.zig.
  • android_app.zig: exports labelle_bgfx_get_native_activity; onInputEvent now routes gamepad KEY events (buttons) + JOYSTICK-source MOTION (axes/hat) into the shared state via input.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 exported sapp_*/labelle_android_gamepad_* shims; gameInit now calls engine.core.registerAndroidBackend(@import("backend_input").android.backendContext()) once at startup.
  • build_zig.txt (backend_bgfx_android): overrideImports the app core onto backend_input so the registered vtable type unifies with the engine's; deps_linker.zig stages the android_gamepad sub-package.

Verification (independent)

  • Desktop unchanged: assembler zig build+test, the new android_gamepad sub-package tests, sokol desktop (glue moved), and bgfx desktop + example — all green.
  • bgfx-Android .so: generated examples/bgfx-android --platform android + zig buildlibgame.so (aarch64 ELF). nm -D: no sapp_android_get_native_activity export (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 single registerAndroidBackend call, no shims.
  • On-device: APK installs + launches on the tablet; logcat shows libgame.so … ok and the NativeActivity comes up with no cannot 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 input shows only built-in devices). End-to-end button/axis flow needs a controller paired to the tablet, then adb logcat for the gamepad_connected enumeration.

…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.
@cursor

cursor Bot commented Jun 13, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Touches Android JNI, native activity lifecycle, and generated build wiring; wrong core module unification or missing symbols would break bgfx-Android .so load at runtime.

Overview
Stage 4 of labelle-core#310 completes the bgfx Android path: core/engine no longer depend on fake sapp_* exports or no-op gamepad stubs.

A new backends/android_gamepad/ sub-package holds the shared Zig state machine (android_gamepad_state.zig) and InputManager JNI glue (android_gamepad_jni.c). Sokol and bgfx both depend on it via .path = "../android_gamepad"; each backend compiles the C file into its own NDK-wired input module. Sokol switches its import from a local path to @import("android_gamepad") and drops duplicated unit tests (they live in the sub-package).

bgfx gains src/android.zig, which builds AndroidBackendContext from labelle_bgfx_get_native_activity and the real JNI init/shutdown symbols. The NativeActivity shell exports that C accessor (replacing the old sapp_android_get_native_activity shim), and onInputEvent now forwards gamepad keys and joystick motion into input.applyGamepad*. input.zig routes Android gamepad queries to the shared state and re-exports android.backendContext() when built for Android.

Codegen removes the three exported shims from templates/android.txt and injects registerAndroidBackend(...) at the start of gameInit (main_template.zig). Generated build.zig overrideImports app core onto bgfx input so the vtable type matches the engine; deps_linker.zig stages android_gamepad for sokol/bgfx projects.

Reviewed by Cursor Bugbot for commit b3508f1. Bugbot is set up for automated code reviews on this repo. Configure here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +351 to +365
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.
@apotema

apotema commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Bot triage:

  • Gamepad BACK quits the activity (Gemini, high) — fixed in b3508f1. onInputEvent returned 0 for every key event, so a controller's B/select (which many pads emit as AKEYCODE_BACK) triggered Android back-navigation and finished the activity — the game quit on press. Now AKEYCODE_BACK is consumed only when it comes from a gamepad/joystick source; the genuine system BACK (touchscreen/system source) stays unhandled and still navigates. Mirrors sokol's B→BACK guard (feat(sokol/android): JNI InputManager gamepad detection source — centerpiece (Phase 1) #248).
  • ?*anyopaque vs C const void* (Gemini, medium) — declined. The extern must be ?*anyopaque to match the seam's gamepad_init vtable field type (*const fn(?*anyopaque) callconv(.c) void) — &labelle_android_gamepad_init is assigned to that field, and the merged sokol adapter declares it identically. ?*anyopaque is ABI-compatible with the C const void* (const is a source-level annotation, same pointer ABI); changing it to ?*const anyopaque would break the seam type match.

Verified: bgfx Android compile-check + desktop test green.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b3508f1. Configure here.

@apotema
apotema merged commit 57a58da into main Jun 13, 2026
5 checks passed
@apotema
apotema deleted the feat/310-bgfx-android-gamepad branch June 13, 2026 15:57
apotema added a commit that referenced this pull request Jun 13, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant