Skip to content

get_joysticks() / get_controllers() pass device indices to SDL3 functions that expect instance IDs #181

@pinghedm

Description

@pinghedm

Describe the bug
tcod.sdl.joystick enumeration feeds a 0-based loop index into SDL3 functions that
take an SDL_JoystickID instance ID. Enumeration only works while the connected
devices happen to have instance IDs 0..N-1 (e.g. a single pad at startup). After
hot-plug/unplug cycles — where SDL3 hands out increasing instance IDs — these functions
open the wrong device or fail to find the controller.

To Reproduce
Standalone script (prints the real instance IDs vs the range(count) indices the buggy
code uses, flags divergence, and calls get_controllers()):

"""Repro for python-tcod joystick/controller enumeration bug on SDL3.

`tcod.sdl.joystick._get_number()` reads only the *count* from
`SDL_GetJoysticks(int *count)` and the enumerators then feed `range(count)` indices
into `SDL_OpenJoystick`/`SDL_OpenGamepad`/`SDL_IsGamepad` -- but SDL3 expects the
`SDL_JoystickID` *instance IDs* that `SDL_GetJoysticks` actually returns. Those are
not contiguous device indices, so once a pad is unplugged/replugged (IDs climb past
the device count) enumeration probes the wrong IDs and `get_controllers()` returns
nothing or raises.

Run with a controller connected. For the clearest demonstration, unplug and replug
the controller a few times first so its instance ID climbs above the device count.

  python repro.py
"""

import tcod.sdl.joystick
from tcod.cffi import ffi, lib


def real_instance_ids() -> list[int]:
  """The instance IDs SDL3 actually reports (what the API expects)."""
  tcod.sdl.joystick.init()
  count = ffi.new("int*")
  p = lib.SDL_GetJoysticks(count)
  if not p:
      return []
  try:
      return [int(p[i]) for i in range(int(count[0]))]
  finally:
      lib.SDL_free(p)


def main() -> None:
  ids = real_instance_ids()
  indices = list(range(len(ids)))

  print(f"connected joysticks:         {len(ids)}")
  print(f"real instance IDs (SDL3):    {ids}")
  print(f"indices the buggy code uses: {indices}")

  if ids != indices:
      print("\n>>> DIVERGENT: range(count) != instance IDs -- buggy enumeration probes wrong IDs.")
  else:
      print("\n(IDs happen to equal 0..N-1 right now; replug the pad to force divergence.)")

  print("\nis-gamepad by REAL instance id:", {i: bool(lib.SDL_IsGamepad(i)) for i in ids})
  print("is-gamepad by buggy index:     ", {i: bool(lib.SDL_IsGamepad(i)) for i in indices})

  try:
      controllers = tcod.sdl.joystick.get_controllers()
      names = [c.joystick.name for c in controllers]
      print(f"\nget_controllers() -> {len(controllers)} controller(s): {names}")
  except Exception as exc:  # noqa: BLE001 - the bug surfaces as an exception
      print(f"\nget_controllers() raised: {type(exc).__name__}: {exc}")


if __name__ == "__main__":
  main()

With one controller connected, unplug/replug it a few times so its instance ID climbs
above the device count, then run it: the IDs diverge from range(count) and
get_controllers() returns nothing / opens the wrong device.

Expected behavior
I expected to be able to use my gamepad when connected

Screenshots
If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

  • OS: Ubuntu 24.04.4 LTS
  • Python version: python 3.14.5
  • tcod version: 21.2.0
  • SDL: 3.2.16 (bundled with the tcod wheel)

Additional context

Offending code (tcod/sdl/joystick.py)

def _get_number() -> int:
    init()
    count = ffi.new("int*")
    lib.SDL_GetJoysticks(count)   # returns SDL_JoystickID* array — discarded
    return int(count[0])

def get_joysticks():   return [Joystick._open(i)       for i in range(_get_number())]
def get_controllers(): return [GameController._open(i) for i in range(_get_number()) if
lib.SDL_IsGamepad(i)]
def _get_all():        return [... for i in range(_get_number())]

_get_number() calls SDL_GetJoysticks(count) only for the count and discards the
returned instance-ID array
. The range(count) indices then go to SDL_OpenJoystick,
SDL_OpenGamepad, and SDL_IsGamepad.

Why it's wrong

In SDL3, SDL_GetJoysticks(int *count) returns an SDL_JoystickID * array of instance
IDs (free with SDL_free), and SDL_OpenJoystick / SDL_OpenGamepad / SDL_IsGamepad
all take an SDL_JoystickID — not a device index (that was the SDL2 API). This is also
inconsistent with the same module's _from_instance_id, which correctly keys off
SDL_GetJoystickID(). See https://wiki.libsdl.org/SDL3/SDL_GetJoysticks.

Metadata

Metadata

Assignees

No one assigned

    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