I’ve been debugging some weird axis behavior under SDL with the “8BitDo N30 Pro 2” game controller connected via USB on Windows. This device reports 6 axes (but only actually uses 4).
Instead of the expected four axes:
0:leftx, 1:lefty, 2:rightx, 3:righy, 4:n/a 5:n/a
Both of the sticks’ y-axes are duplicated, basically like this:
0: leftx, 1:lefty, 2:lefty, 3:rightx, 4:righty, 5:righty
This does not happen under Linux or macOS, and I also verified that the HID device actually just sends single axis updates as expected (it does).
I’ve done some debugging in the directinput joystick driver in SDL under Windows 10, as I suspected there could be an issue with the axis enumeration. I added some debugging code, and here is the output of SDL’s enumeration of axes with IDirectInputDevice8_EnumObjects:
EnumDevObjectsCallback dwType=1282 DIDFT_AXIS INSTANCE=5
- wUsagePage=0x01, wUsage=0x35 tszName=Z
- GUID_RzAxis
- SDL 2.0.10 wants in->ofs = 20
- sizeof(LONG) * DIDFT_GETINSTANCE(dev->dwType) = 20
EnumDevObjectsCallback dwType=514 DIDFT_AXIS INSTANCE=2
- wUsagePage=0x01, wUsage=0x32 tszName=Z
- GUID_ZAxis
- SDL 2.0.10 wants in->ofs = 8
- sizeof(LONG) * DIDFT_GETINSTANCE(dev->dwType) = 8
EnumDevObjectsCallback dwType=258 DIDFT_AXIS INSTANCE=1
- wUsagePage=0x01, wUsage=0x31 tszName=Y
- GUID_YAxis
- SDL 2.0.10 wants in->ofs = 4
- sizeof(LONG) * DIDFT_GETINSTANCE(dev->dwType) = 4
EnumDevObjectsCallback dwType=2 DIDFT_AXIS INSTANCE=0
- wUsagePage=0x01, wUsage=0x30 tszName=X
- GUID_XAxis
- SDL 2.0.10 wants in->ofs = 0
- sizeof(LONG) * DIDFT_GETINSTANCE(dev->dwType) = 0
EnumDevObjectsCallback dwType=1538 DIDFT_AXIS INSTANCE=6
- wUsagePage=0x02, wUsage=0xc5 tszName=B
- GUID_RzAxis
- SDL 2.0.10 wants in->ofs = 20
- sizeof(LONG) * DIDFT_GETINSTANCE(dev->dwType) = 24
EnumDevObjectsCallback dwType=1794 DIDFT_AXIS INSTANCE=7
- wUsagePage=0x02, wUsage=0xc4 tszName=A
- GUID_YAxis
- SDL 2.0.10 wants in->ofs = 4
- sizeof(LONG) * DIDFT_GETINSTANCE(dev->dwType) = 28
As shown seen here, the underlying issue is that DirectInput reuses GUID_YAxis
and GUID_RzAxis
for the final two axes. And SDL uses the GUID’s to calculate the offset into the DIJOYSTATE structure. So, two and two SDL axes get the same ofs, and both will report a change when the corresponding single axis on the device changes.
I can’t really claim that there is a bug in SDL as such, it looks more like the bug is really in DirectInput/Windows (it would make sense for Windows to report guid type slider here). However, extracing the instance number of the axis via DIDFT_GETINSTANCE(dev->dwType) yields the correct axis index. For example, the last reported axis has guidType GUID_YAxis
but DIDFT_GETINSTANCE(dev->dwType) is 7
.
So it seems to me that using the guidType is unreliable in certain cases. One alternative is to just ditch the GUID comparisons, and instead for axes simply do:
in->ofs = sizeof(LONG) * DIDFT_GETINSTANCE(dev->dwType);
That certain works well for this device, and sounds sane. But on the other not hand, not being a Windows/DirectInput developer really, I cannot say for certain that using the DIDFT_GETINSTANCE(dev->dwType) is more reliable overall on different Windows versions (etc), so I also suggest an alternative fix: Keep the guidType comparisons as they are today, and also add the following code afterwards, to override the offset when these two instance numbers are seen:
if (DIDFT_GETINSTANCE(dev->dwType) == 6) {
in->ofs = DIJOFS_SLIDER(0);
} else if (DIDFT_GETINSTANCE(dev->dwType) == 7) {
in->ofs = DIJOFS_SLIDER(1);
}
With either of these changes, the device reports axes as expected, and it also matches the behavior of the device on Linux and macOS.