PS4 controller is detected as joystick but not as gamepad
SDL version: 3.2.26 OS: Windows 10 x64
My PS4 controller (connected using wired USB) is detected as a joystick but not as a gamepad by SDL.
This issue happens only when the controller is in "rest mode" (yellow pulsing controller LED, or LED is off). The controller seems to go into this state when it hasn't been used for like a day.
SDL_GetJoysticks returns 1 joysticks, but SDL_GetGamepads returns 0 gamepads. The joystick id can be opened and read (e.g. SDL_GetJoystickAxis works and returns valid values).
Replugging the controller (LED turns blue) fixes the issue, but it's not ideal.
Repro:
#include <cstdio>
#include <SDL3/SDL.h>
int main() {
SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD);
printf("SDL version: %d\n", SDL_GetVersion());
int numJoysticks = 0;
SDL_JoystickID *jidPtr = SDL_GetJoysticks(&numJoysticks);
printf("Num joysticks: %d\n", numJoysticks);
int numGamepads = 0;
SDL_JoystickID *gidPtr = SDL_GetGamepads(&numGamepads);
printf("Num gamepads: %d\n", numGamepads);
SDL_Joystick *jp = SDL_OpenJoystick(jidPtr[0]);
printf("Joystick name: %s\n", SDL_GetJoystickName(jp));
while (1) {
SDL_PumpEvents();
printf("Joystick axis 0: %d\n", SDL_GetJoystickAxis(jp, 0));
SDL_Delay(100);
}
}
Prints:
SDL version: 3002026
Num joysticks: 1
Num gamepads: 0
Joystick name: Controller
Joystick axis 0: 521
etc...
After replugging:
SDL version: 3002026
Num joysticks: 1
Num gamepads: 1
Joystick name: PS4 Controller
Joystick axis 0: 0
Joystick axis 0: 642
etc...
Can you print out the GUID as well?
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#define printf SDL_Log
int main(int argc, char *argv[])
{
SDL_Init(SDL_INIT_JOYSTICK | SDL_INIT_GAMEPAD);
printf("SDL version: %d\n", SDL_GetVersion());
int numJoysticks = 0;
SDL_JoystickID *jidPtr = SDL_GetJoysticks(&numJoysticks);
printf("Num joysticks: %d\n", numJoysticks);
int numGamepads = 0;
SDL_JoystickID *gidPtr = SDL_GetGamepads(&numGamepads);
printf("Num gamepads: %d\n", numGamepads);
SDL_Joystick *jp = SDL_OpenJoystick(jidPtr[0]);
printf("Joystick name: %s\n", SDL_GetJoystickName(jp));
char str[33];
SDL_GUID guid = SDL_GetJoystickGUID(jp);
SDL_GUIDToString(guid, str, sizeof(str));
printf("Joystick guid: %s\n", str);
while (1) {
SDL_PumpEvents();
//printf("Joystick axis 0: %d\n", SDL_GetJoystickAxis(jp, 0));
SDL_Delay(100);
}
}
I can't seem to trigger the "rest mode" state anymore. Since yesterday the controller just works as expected (and there's no pulsing yellow LED). I don't really understand the controller state logic to be honest, perhaps the behavior is related to charging/battery level. I'll keep an eye out. If I can trigger the state again, I'll definitely post the GUID info here.
Okay, thanks!
I finally was able to reproduce this again:
SDL version: 3002026
Num joysticks: 1
Num gamepads: 0
Joystick name: Controller
Joystick guid: 05000000000000000000000000000000
I did some digging and was able to trace this back to EnumJoystickDetectCallback in SDL_dinputjoystick.c. LPCDIDEVICEINSTANCE pDeviceInstance:
guidProduct = {00000000-0000-0000-0000-504944564944} dwDevType = 66328 tszProductName = 0x000000d7972ff5b0 L"6 axis 14 button device with hat switch"
And in QueryDeviceInfo, the call to IDirectInputDevice8_GetProperty(device, DIPROP_VIDPID, &dipdw.diph) "succeeds", but returns 0 in dipdw.dwData, which causes vendor and product id to be 0.
After replugging the controller, we get different values:
guidProduct = {05C4054C-0000-0000-0000-504944564944} dwDevType = 66069 tszProductName = 0x000000d3778ff500 L"Wireless Controller"
IDirectInputDevice8_GetProperty now works as expected too, returning correct vendor and product id.
It's worth noting that the device path (obtained in QueryDevicePath) is the same in both scenarios and does always contain the correct vendor and product id:
\?\HID#VID_054C&PID_05C4#6&<instance_id>&0&0000#{4D1E55B2-F16F-11CF-88CB-001111000030}
(where <instance_id> varies depending on which USB port I plug the controller into)
So this looks like a weird DirectInput?/driver? issue.
It could be worked around by trying to parse the vendor and product ids from the device path as a fallback, not sure if it would fully fix everything though, and perhaps there is a better way.
Do you actually get the correct inputs if you hack in the correct VID/PID? Also, SDL should be using the HIDAPI driver here, which talks directly to the hardware and shouldn't have this issue. Does testcontroller work when it's in this state? If not, what's the output?
Great suggestions, thanks! I just confirmed that SDL (with the gamepad now in normal "working" state) is indeed using the HIDAPI driver. This is happening for both my simple test program as well as testcontroller.
I'm not sure about before. I vaguely recall seeing the HID driver returning GetCount() 0, but definitely not certain about this, I will need to re check. Unfortunately, I can't reproduce the broken state at will, I'll need to wait until it happens again. Then, I'll also try to hack the VID/PID manually and see how that goes. Will report back when I know more.
Managed to reproduce this once more.
In hipapi/windows/hid.c, in hid_enumerate/hid_internal_get_device_info: HidD_GetAttributes seems broken. It returns TRUE but sets both VendorID and ProductID to 0 (despite the device path containing the correct vendor and product id).
Stepping further, HIDAPI_JoystickGetCount ends up returning 0 (the device is in the SDL_HIDAPI_devices linked list, but SDL_HIDAPI_numjoysticks == 0).
Stepping further again, WINDOWS_JoystickGetCount returns 1, which allows SDL to at least use the gamepad as a joystick via that driver.
Manually setting the vendor_id/product_id in hid_internal_get_device_info (hipapi/windows/hid.c), after HidD_GetAttributes call:
wchar_t *prefix = L"\\\\?\\HID#VID_054C&PID_05C4";
if (dev->vendor_id == 0 && dev->product_id == 0 && strncmp(path, prefix, strlen(prefix)) == 0) {
dev->vendor_id = 0x054c;
dev->product_id = 0x05c4;
}
This hack seems to work. In testcontroller the gamepad now shows up as a PS4 controller. However, there is also a second joystick detected by SDL, detected via the windows backend. Also, not sure if haptics work (didn't get to test this yet).
PS. Perhaps worth noting that some other HidD_ functions fail too, e.g. in hid.c: HidD_GetManufacturerString, HidD_GetProductString return "". After replugging I get "Sony Computer Entertainment" and "Wireless Controller".
What happens in SDL_hidapijoystick.c? Is it detected at all in this state?
The device is detected and added to SDL_HIDAPI_devices (in SDL_hidapijoystick.c, in HIDAPI_AddDevice). However, it's not recognized as a HIDAPI joystick.
I didn't step through this in detail (e.g. HIDAPI_SetupDeviceDriver), but at a high level this made sense to me. The only way for SDL_HIDAPI_numjoysticks to increase is via HIDAPI_JoystickConnected. But that function is normally called in SDL_hidapi_ps4.c, and because the device is not recognized as a PS4 controller in the first place, that code never fires.
The device is detected and added to SDL_HIDAPI_devices (in SDL_hidapijoystick.c, in HIDAPI_AddDevice). However, it's not recognized as a HIDAPI joystick.
I didn't step through this in detail (e.g. HIDAPI_SetupDeviceDriver), but at a high level this made sense to me. The only way for SDL_HIDAPI_numjoysticks to increase is via HIDAPI_JoystickConnected. But that function is normally called in SDL_hidapi_ps4.c, and because the device is not recognized as a PS4 controller in the first place, that code never fires.
This makes sense. This sounds like a windows bug. If the Windows HID driver is so broken that it doesn't recognize the VID/PID of the device, I don't know that we should be trying to hack around it.
Agreed, looks like a Windows bug. It's probably not a coincidence that both the HidD_ and DirectInput_ paths fail in the same way, could be some race condition deep in the usb driver code.
If we did want to work around it, that would probably be best done in libusb/hidapi, and SDL would have to update it's deduplication logic, but it all seems very fragile, especially given other HidD_ calls are failing too. Might even introduce new problems.
From an end user perspective, it's just not a great experience though. Boot up a game, and realize input is broken. Funny thing is that some other non-SDL games do in fact work, but that's probably because they are fine with just reading the joystick. That does make SDL games that use the gamepad API look worse in comparison though.
I really wonder what the impact of this bug is. How many people are affected? How often does it happen? It it just PS4 controllers, or also other gamepads? Perhaps it is just my controller, and this is just some weird HW issue? (that seems unlikely though, given that things can be made to work using hacks). Do most gamers just know to reconnect their controller if it doesn't work, and move on? Or do most users use the controller in wireless mode, side stepping the issue? I don't recall seeing many similar bug reports from other SDL users, which does suggest low impact.