How should I get SDL to respond to XInput controllers?

Hi, my wired Xbox 360 controller is detected, but I’m not getting any input from it.

I initialise like so:

SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0");
SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
SDL_Init(SDL_INIT_GAMECONTROLLER);

Open all the attached controllers (the Xbox 360 controller is found and opened):

SDL_GameController* gamepadId[12] = { nullptr };
for (int i = 0; i < SDL_NumJoysticks(); i++)
{
	if (!SDL_IsGameController(i))
	{
		continue;
	}
	SDL_GameController* g = SDL_GameControllerOpen(i);
	gamepadId[numGamepads++] = g;
}

I have SDL_GameControllerUpdate(); in my game loop and query each controller with SDL_GameControllerGetButton and SDL_GameControllerGetAxis. With these I’m able to correctly query button and axis states for my DualSense, DualShock 4, Switch Pro Controller, Power A Pro Controller, and my Logitech Gamepad F310 in DirectInput mode, but not in XInput mode, and not my Xbox 360 controller.

The queries for XInput device states are just returning all false / 0.

I’ve only tried with one or two controllers attached at a time, so XInput’s 4 controller limit shouldn’t be affecting this. I’m using SDL 2.0.14 on Windows 10 64-bit. Is there something else I’m supposed to do to get XInput devices working properly?

Have you tried to go with SDL event based approach instead of manual call to SDL_GameControllerUpdate()?

SDL_Event e;
while (SDL_PollEvent(&e))
{
	switch (e.type)
	{
	case SDL_CONTROLLERDEVICEADDED:
	{
		const int joystickIndex = e.cdevice.which;
		SDL_GameController* gameController = SDL_GameControllerOpen(joystickIndex);
		SDL_Joystick* joystick = SDL_GameControllerGetJoystick(gameController);
		const SDL_JoystickID  = SDL_JoystickInstanceID(joystick);
		// TODO
	}
	break;
	case SDL_CONTROLLERDEVICEREMOVED:
	{
		const SDL_JoystickID joystickID = e.cdevice.which;
		// TODO
	}
	break;
	case SDL_CONTROLLERAXISMOTION:
	{
		const SDL_JoystickID joystickID = e.caxis.which;
		const SDL_GameControllerAxis axis = (SDL_GameControllerAxis)e.caxis.axis;
		const float value = e.caxis.value / (float)SDL_JOYSTICK_AXIS_MAX;
		// TODO
	}
	break;
	case SDL_CONTROLLERBUTTONDOWN:
	case SDL_CONTROLLERBUTTONUP:
	{
		const SDL_JoystickID joystickID = e.cbutton.which;
		const SDL_GameControllerButton button = (SDL_GameControllerButton)e.cbutton.button;
		const bool isPressed = e.cbutton.state == SDL_PRESSED;
		// TODO
	}
	break;
	}
}

Thanks for the suggestions! I’ve tried it just now, and I’m not getting events from XInput devices. Input events come through just fine for other devices I tried (DualSense, F310 in DirectInput mode, Power A).

Do you have SDL_CONTROLLERDEVICEREMOVED event on controller disconnection?

Seems I having somewhat related issue here.
Workaround is:

SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1");

I looked into SDL code and seems issue was in SDL_CreateDeviceNotification() - it is calling CreateWindowEx with HWND_MESSAGE param and this will not work with RegisterDeviceNotification(). So no connection status updates for pads.

See: https://stackoverflow.com/q/22016259/1795050

Wow, good catch! I wasn’t getting SDL_CONTROLLERDEVICEREMOVED events from the Xbox controller. Setting that hint fixed things:

SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1");

Now I’m able to both query specific button states and receive input events with XInput devices.

Thanks for the help! Do you know if it’s intended behaviour that that hint needs to be set for them to work?

@slouken can you check this on your side please?

1 Like

Additional explanation in Raymond Chen’s blog here.

1 Like

I investigated this issue further.
It turned our that SDL_HINT_JOYSTICK_THREAD fixes this issue only partially: HID gamepads like PS4/PS5 still not detected.

Seems WM_DEVICECHANGE is working regardless of HWND_MESSAGE flag in CreateWindowEx call.

The problem was that I called SDL_PollEvent() (and SDL_PumpEvents() indirectly) in non-main thread and thats is why no WM_DEVICECHANGE arriving into ControllerWndProc (inside SDL_hidapijoystick.c).
I fixed this with creating of a new separate thread with simple windows loop:

SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER);

while(threadStarted)
{
	MSG msg; 
	while(GetMessage(&msg, NULL, 0, 0 ))
	{ 
		TranslateMessage(&msg); 
		DispatchMessage(&msg); 
	}
	
	SDL_Event e;
    SDL_WaitEvent(&e);

    do
    {
		switch (e.type)
		{
			....
		}
    }
    while(SDL_PollEvent(&e));
}

SDL_QuitSubSystem(SDL_INIT_GAMECONTROLLER);
1 Like