Skip to content

feat(bgfx): route desktop gamepad through SDL HIDAPI source (Switch-pad support) - #318

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

apotema merged 2 commits into
mainfrom
feat/bgfx-desktop-sdl-gamepad

Conversation

@apotema

@apotema apotema commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Route bgfx desktop gamepad input through the shared sdl_gamepad source (SDL2 + HIDAPI), so bgfx-desktop reads controllers GLFW can't decode — notably Switch-mode Nintendo Pro Controllers / Joy-Cons. Mirrors the raylib/sokol pattern exactly. The GLFW path (#315) is preserved as the .gamepad = .none fallback; Android (#310) is untouched.

What (mirrors raylib/sokol)

  • src/deps_linker.zig — added .bgfx to the .raylib, .sokol => if (cfg.gamepad == .auto) arm that stages backends/sdl_gamepad (labelle_sdl_gamepad → link sdl_gamepad).
  • src/templates/build_zig.txt — backend_bgfx now takes .gamepad_enabled and replicates raylib/sokol's if (backend_input.import_table.get("sdl_gamepad")) |m| overrideImport(m, "labelle_core", core_mod) unification block.
  • src/build_files.zig — desktop bgfx renders the section with .gamepad_enabled.
  • backends/bgfx/build.zig.zon — declares .labelle_sdl_gamepad = .{ .path = "../sdl_gamepad" } (non-lazy, like raylib/sokol).
  • backends/bgfx/build.zig — a gamepad_enabled build option, desktop-gated resolution of the sdl_gamepad module (core unified onto it), a build_options module carrying the flag onto input, and the SDL2 link on desktop (Homebrew / LABELLE_SDL2_LIB).
  • backends/bgfx/src/input.zig — detection identical to raylib/sokol: build_options.gamepad_enabled + a comptime desktop predicate gate @import("sdl_gamepad") and a use_sdl_gamepad flag. The four gamepad getters route through sdl_gp.Source.* ahead of the GLFW path; newFrame pumps sdl_gp.Source.update() (lazy-init, hotplug, edge snapshot) when wired. .none → the SDL module is absent and the getters fall through to GLFW. Android (agp) branch unchanged.

Verification (independent)

  • Assembler zig build + zig build test → green.
  • bgfx backend zig build test green for: .auto (default, SDL), .none (-Dgamepad_enabled=false, GLFW), aarch64-linux-android, and x86_64-windows-gnu cross.
  • .auto generate+build (bgfx desktop): generated build.zig stages + wires sdl_gamepad, links libSDL2, references _SDL_GameController*.
  • .none generate+build: no sdl_gamepad, no SDL2 linked, GLFW (glfwGetGamepadState) present.
  • Android backend_bgfx_android untouched (no SDL).
  • Controller read: the connected USB Pro Controller is detected through the exact sdl_gamepad.Source API bgfx now routes to (slot 0 AVAILABLE + live axis reads) — same source raylib already uses to read it.

Honest notes

  • In-game live input not exercised: on macOS, SDL HIDAPI button/state reads inside a windowed app may need Input Monitoring permission. Run a generated .auto bgfx desktop game from your own Terminal with the controller to confirm in-game.
  • Linux deviation: raylib/sokol route Linux desktop to labelle-core's udev/evdev source; bgfx has no core-udev input wiring, so it routes all desktop (incl. Linux) through SDL on .auto. So bgfx-Linux desktop pulls SDL2 where raylib-Linux uses udev. Documented in code; a udev-parity path for bgfx-Linux is a possible follow-up.
  • Dependency change: bgfx desktop builds pull SDL2 when .gamepad = .auto (default), matching raylib/sokol; .none keeps the SDL-free GLFW path.

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

cursor Bot commented Jun 13, 2026 •

Copy link
Copy Markdown

PR Summary

Medium Risk
Default bgfx desktop builds now depend on SDL2 and comptime gamepad routing; mis-staged deps or missing SDL on CI/dev machines would break links, but Android and .none paths stay SDL-free.

Overview
bgfx desktop gamepad input now follows the same .gamepad = .auto path as raylib/sokol: the shared sdl_gamepad source (SDL2 + HIDAPI) replaces GLFW for availability, buttons, axes, and per-frame updates, so controllers GLFW cannot decode (e.g. Switch-mode Pro Controllers) can be read. .gamepad = .none keeps the existing GLFW snapshot path; Android (android_gamepad) is unchanged.

The assembler stages backends/sdl_gamepad for bgfx when gamepad is auto, passes gamepad_enabled into the generated backend_bgfx dependency, and overrideImports the app’s unified labelle_core onto the transitive sdl_gamepad module (same diamond fix as raylib/sokol). backends/bgfx adds the build option, optional sdl_gamepad import + build_options, desktop SDL2 link (Homebrew paths / LABELLE_SDL2_LIB on Windows), and the labelle_sdl_gamepad zon path dep.

CI: the dedicated bgfx macOS job brew install sdl2 so default desktop builds link SDL2.

Reviewed by Cursor Bugbot for commit 88869c2. 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 integrates the shared windowless-SDL desktop gamepad source into the bgfx backend, mirroring the implementation in the raylib and sokol backends. This allows the bgfx backend to route gamepad getters through SDL's HIDAPI drivers on desktop targets to decode controllers (like Switch-mode Nintendo Pro Controllers) that GLFW cannot. Feedback on the changes highlights an inefficiency in dirExists where a new std.Io.Threaded instance is initialized on every call instead of reusing the existing b.graph.io context. Additionally, the cross-compilation check for Windows incorrectly gates the LABELLE_SDL2_LIB search on the host OS being Windows, which breaks cross-compilation from other platforms.

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 thread backends/bgfx/build.zig Outdated
Comment on lines +21 to +34
fn sdlLibPath(target_os: std.Target.Os.Tag, host_os: std.Target.Os.Tag) ?[]const u8 {
if (target_os != .macos or host_os != .macos) return null;
if (dirExists("/opt/homebrew/lib")) return "/opt/homebrew/lib";
if (dirExists("/usr/local/lib")) return "/usr/local/lib";
return null;
}

fn dirExists(path: []const u8) bool {
var threaded = std.Io.Threaded.init(std.heap.page_allocator, .{});
defer threaded.deinit();
const io = threaded.io();
std.Io.Dir.accessAbsolute(io, path, .{}) catch return false;
return true;
}

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

In dirExists, initializing a new std.Io.Threaded instance via std.Io.Threaded.init(std.heap.page_allocator, .{}) on every call is highly inefficient. It spawns OS worker threads and allocates memory on the heap just to check if a directory exists.

Since b.graph.io (of type std.Io) is already initialized and available in the build function, we should pass it down to sdlLibPath and dirExists to perform the directory checks using the existing I/O context.

fn sdlLibPath(io: std.Io, target_os: std.Target.Os.Tag, host_os: std.Target.Os.Tag) ?[]const u8 {
    if (target_os != .macos or host_os != .macos) return null;
    if (dirExists(io, "/opt/homebrew/lib")) return "/opt/homebrew/lib";
    if (dirExists(io, "/usr/local/lib")) return "/usr/local/lib";
    return null;
}

fn dirExists(io: std.Io, path: []const u8) bool {
    std.Io.Dir.accessAbsolute(io, path, .{}) catch return false;
    return true;
}

Comment thread backends/bgfx/build.zig Outdated
// link + (on macOS Homebrew) the library path matters. Mirrors raylib/sokol.
if (sdl_gp_mod != null) {
input_mod.link_libc = true;
if (sdlLibPath(target.result.os.tag, builtin.target.os.tag)) |p| {

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

Update the call to sdlLibPath to pass b.graph.io as the first argument, matching the updated signature.

        if (sdlLibPath(b.graph.io, target.result.os.tag, builtin.target.os.tag)) |p| {

Comment thread backends/bgfx/build.zig Outdated
// (`windows-gnu`) toolchain, so honor `LABELLE_SDL2_LIB` — the dir
// holding the import lib (`libSDL2.dll.a`). `SDL2.dll` must be on PATH
// (or beside the exe) at runtime.
if (target.result.os.tag == .windows and builtin.target.os.tag == .windows) {

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 check for LABELLE_SDL2_LIB is gated on builtin.target.os.tag == .windows. However, when cross-compiling to Windows from a non-Windows host (e.g., Linux or macOS), builtin.target.os.tag will be the host OS, which prevents LABELLE_SDL2_LIB from being honored.

Since LABELLE_SDL2_LIB is specifically used to provide the path to the Windows SDL2 import library when cross-compiling or compiling for Windows, we should only check if the target OS is Windows (target.result.os.tag == .windows), regardless of the host OS.

        if (target.result.os.tag == .windows) {

… 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

apotema commented Jun 13, 2026

Copy link
Copy Markdown
Contributor Author

CI failure + bot triage — fixed in 88869c2:

  • CI: bgfx backend build + test (macOS) failed — root cause: that dedicated job didn't brew install sdl2, but bgfx desktop now links SDL2 when .gamepad = .auto (the default). Added the sdl2 install step, matching the build-and-test job. (Not a bot finding — the actual red check.)
  • std.Io.Threaded per dirExists call (Gemini, medium) — fixed. dirExists/sdlLibPath now take the build graph's Io (b.graph.io) instead of spinning up a fresh thread pool on every probe.
  • sdlLibPath signature (Gemini, medium) — fixed; call site passes b.graph.io.
  • LABELLE_SDL2_LIB gated on host==windows (Gemini, medium) — fixed. Now gated on the target os only, so cross-compiling to Windows from a non-Windows host honors it.

Verified: bgfx .auto + .none tests green; ci.yml parses.

@apotema
apotema merged commit a00cc45 into main Jun 13, 2026
5 checks passed
@apotema
apotema deleted the feat/bgfx-desktop-sdl-gamepad branch June 13, 2026 17:42
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