Skip to content

feat(bgfx): desktop GLFW gamepad reading (buttons + axes) - #315

Merged
apotema merged 1 commit into
mainfrom
feat/bgfx-desktop-gamepad
Jun 13, 2026
Merged

feat(bgfx): desktop GLFW gamepad reading (buttons + axes)#315
apotema merged 1 commit into
mainfrom
feat/bgfx-desktop-gamepad

Conversation

@apotema

@apotema apotema commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Implement bgfx desktop GLFW gamepad reading (buttons + axes). The bgfx desktop input path only had isGamepadAvailable (joystick presence); isGamepadButtonDown/Pressed/getGamepadAxisValue were return false/0 TODO stubs — so a bgfx game on desktop knew a pad was plugged in but received no input from it.

What

Read via glfw.getGamepadState, translating the engine's canonical raylib-compatible numbering — buttons [0,17], axes LX/LY/RX/RY/LT/RT = [0,5] (matching android_gamepad + the raylib/sdl backends) — to GLFW's standard layout:

  • canonToGlfwButton maps face/dpad/bumper/thumb/middle buttons; the digital LEFT/RIGHT_TRIGGER_2 (10/12) are derived from the analog trigger axes (> 0).
  • Axes are 1:1 (canonical 0..5 == GLFW 0..5).
  • isGamepadButtonPressed rising edges are snapshotted once per gamepad per frame in newFrame (snapshotGamepads), mirroring the keyboard/mouse *_pressed pattern — one getGamepadState per slot, not per query.

The Android path (#310) is untouched; the agp-vs-GLFW split is comptime-gated.

Verification

  • bgfx desktop zig build test + Android compile-check + example build → green.
  • On-host probe (macOS): a connected controller in slot 0 reads as present but NOT a GLFW-mapped gamepad — confirming the documented limit: GLFW can't decode a Switch-mode Nintendo Pro Controller (only SDL's HIDAPI can). An X-input/Xbox pad is the reliable GLFW target, or use the SDL backend.

Honest caveat

Live button/axis input wasn't end-to-end confirmed — the only controller paired to this machine is the Pro Controller, which GLFW doesn't map (above), and on macOS reading input also needs Input Monitoring permission for the host binary (run from a real Terminal, not a sandboxed shell). The mapping + edge logic is build-verified and matches the canonical numbering used by the other backends; confirming with an Xbox pad on a permitted Terminal is the remaining runtime check.

The bgfx desktop input path only had `isGamepadAvailable` (joystick presence);
buttons/axes were `return false`/`0` TODO stubs, so a bgfx game on desktop knew
a pad was plugged in but received no input. Implement reading via
`glfw.getGamepadState`, translating the engine's canonical raylib-compatible
numbering (buttons [0,17], axes LX/LY/RX/RY/LT/RT = [0,5]) to GLFW's standard
layout:
- `canonToGlfwButton` maps face/dpad/bumper/thumb/middle buttons; the digital
  LEFT/RIGHT_TRIGGER_2 (10/12) are derived from the analog trigger axes.
- axes are 1:1 (canonical 0..5 == GLFW 0..5).
- `isGamepadButtonPressed` rising edges are snapshotted once per gamepad per
  frame in `newFrame` (`snapshotGamepads`), mirroring the keyboard/mouse
  `*_pressed` pattern; `getGamepadState` is called once per slot, not per query.

Android path (#310) unchanged. A controller GLFW can't map (Switch-mode
Nintendo Pro Controller — only SDL HIDAPI decodes those) reports present but
yields no gamepad state (buttons/axes read released/0); use an X-input pad or
the SDL backend. On macOS, input reading also needs Input Monitoring permission.

Verified: bgfx desktop test + Android compile-check + example build green.
@cursor

cursor Bot commented Jun 13, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes per-frame input semantics on desktop bgfx only; behavior depends on GLFW/SDL mappings and can differ from joystickPresent for unsupported controllers.

Overview
Desktop bgfx games can read gamepad input again. The GLFW path no longer returns stub false/0 for button and axis queries; it pulls mapped gamepad state via glfw.getGamepadState and maps the engine’s canonical raylib-style buttons [0,17] and axes [0,5] onto GLFW’s standard layout.

Each frame, newFrame calls snapshotGamepads after pollEvents so isGamepadButtonPressed uses rising-edge state (same pattern as keyboard/mouse). Face, dpad, bumpers, and thumbs go through canonToGlfwButton; digital LEFT/RIGHT_TRIGGER_2 (10/12) come from analog trigger axes above a 0.0 threshold. isGamepadAvailable is unchanged (still joystickPresent); unmapped pads stay “present” but read as idle. Android agp behavior is untouched.

Reviewed by Cursor Bugbot for commit 1f37804. 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 implements desktop gamepad support using GLFW in the bgfx backend. The review feedback identifies two critical compilation issues: first, snapshotGamepads() will fail to compile on Android targets because glfw is an empty struct, which requires a comptime guard; second, referencing glfw.Gamepad.Axis.count will cause a compilation error on desktop platforms because standard zglfw enums do not expose a count field, and the length of state.axes should be used instead.

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 +320 to +322
fn snapshotGamepads() void {
var g: u32 = 0;
while (g < MAX_GAMEPADS) : (g += 1) {

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

On Android targets, glfw is defined as an empty struct (struct {}), which does not contain getGamepadState. Since snapshotGamepads() is referenced in newFrame(), the compiler will perform semantic analysis on its body and fail to compile on Android.

Adding a comptime check at the beginning of snapshotGamepads() ensures that the compiler discards the rest of the function body during semantic analysis on Android, preventing compilation errors.

fn snapshotGamepads() void {
    if (comptime is_android) return;
    var g: u32 = 0;

Comment on lines +363 to +365
if (gamepad >= MAX_GAMEPADS or axis >= glfw.Gamepad.Axis.count) return 0;
const state = glfw.getGamepadState(@enumFromInt(gamepad)) catch return 0;
return state.axes[axis];

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

Using glfw.Gamepad.Axis.count will cause a compilation error on desktop platforms because standard zglfw enums do not expose a count field. Instead, you should fetch the gamepad state first and guard the axis index using the actual length of the state.axes array (state.axes.len). This is more robust and idiomatic Zig.

    if (gamepad >= MAX_GAMEPADS) return 0;
    const state = glfw.getGamepadState(@enumFromInt(gamepad)) catch return 0;
    if (axis >= state.axes.len) return 0;
    return state.axes[axis];

@apotema

apotema commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

Bot triage — both declined as false positives (each contradicted by a passing build):

  • snapshotGamepads() breaks the Android build (Gemini, high) — declined. snapshotGamepads() is referenced only in newFrame's desktop path, after the comptime if (is_android) { ...; return; }. With is_android comptime-true, Zig does not analyze the post-return tail (this is the exact pattern the pre-existing glfw.pollEvents() on the line above already relies on). Proven: zig build test -Dtarget=aarch64-linux-android compiles clean with this code.
  • glfw.Gamepad.Axis.count is not exposed (Gemini, high) — declined. This zglfw fork does define it: pub const Axis = enum(u8) { ...; pub const count = std.meta.fields(@This()).len; }. The desktop CI build (bgfx-build macOS + build-and-test) is green, which compiles this expression.

Both are the generic-knowledge-vs-this-fork mismatch; CI + the local Android compile-check are authoritative.

@apotema
apotema merged commit 701220c into main Jun 13, 2026
5 checks passed
@apotema
apotema deleted the feat/bgfx-desktop-gamepad branch June 13, 2026 16:44
apotema added a commit that referenced this pull request Jun 13, 2026
…ad support) (#318)

* feat(bgfx): route desktop gamepad through shared SDL HIDAPI source

Mirror the raylib/sokol pattern so bgfx desktop reads controllers GLFW
can't decode (e.g. Switch-mode Nintendo Pro Controllers) through the
shared `backends/sdl_gamepad` source. Keep the #315 GLFW path as the
`.gamepad = .none` fallback. Android (#310, android_gamepad) untouched.

- deps_linker: add `.bgfx` to the `.auto`-gated sdl_gamepad staging arm.
- build template: forward `gamepad_enabled` to the bgfx dep and add the
  core↔sdl_gamepad unification block in the `backend_bgfx` section.
- build_files: render `backend_bgfx` with the gamepad_enabled flag.
- backends/bgfx/build.zig.zon: declare `../sdl_gamepad` path dep.
- backends/bgfx/build.zig: gamepad_enabled option, resolve+unify the
  sdl_gamepad module on desktop, import build_options, link SDL2.
- backends/bgfx/src/input.zig: comptime `use_sdl_gamepad` (build_options +
  desktop predicate) routes the gamepad getters through sdl_gp.Source and
  pumps it in newFrame; GLFW path stays as the opt-out fallback.

Desktop bgfx builds now pull SDL2 when `.gamepad = .auto` (default),
matching raylib/sokol.

* fix(bgfx-sdl-gamepad): install SDL2 in bgfx CI job + build.zig review fixes

- CI: the dedicated `bgfx-build` (macOS) job didn't install SDL2, but bgfx
  desktop now links it when `.gamepad = .auto` (default) — add `brew install
  sdl2`, matching the build-and-test job. (This was the CI failure.)
- build.zig (Gemini review): `dirExists`/`sdlLibPath` now take the build
  graph's `Io` (`b.graph.io`) instead of spinning up a fresh `std.Io.Threaded`
  thread pool per probe.
- LABELLE_SDL2_LIB is gated on the TARGET os only (was target AND host), so
  cross-compiling to Windows from a non-Windows host honors it.

Verified: bgfx `.auto` + `.none` tests green; ci.yml parses.
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