Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Duplicated joystick axes with SDL_dinputjoystick #3561

SDLBugzilla opened this issue Feb 11, 2021 · 2 comments

Duplicated joystick axes with SDL_dinputjoystick #3561

SDLBugzilla opened this issue Feb 11, 2021 · 2 comments
waiting Waiting on user response


Copy link

SDLBugzilla commented Feb 11, 2021

This bug report was migrated from our old Bugzilla tracker.

These attachments are available in the static archive:

Reported in version: 2.0.10
Reported for operating system, platform: Windows 10, x86_64

Comments on the original bug report:

On 2020-02-22 17:07:49 +0000, Frode Solheim wrote:

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.

On 2020-02-22 17:09:09 +0000, Frode Solheim wrote:

(Originally reported here:

On 2020-02-24 19:22:55 +0000, Frode Solheim wrote:

Note: I've done quite a bit more testing, and concluded that the previous suggested fix is not correct. I've updated the discord thread with my preliminary findings. I plan to write a patch and will update this ticket when done.

On 2020-02-24 21:48:59 +0000, Frode Solheim wrote:

I wasn't quite satisfied with my previous investigation. Since the device in question did not actually send data for the extra conflicting axes, I couldn't verify that the suggested solution was correct. So I created my own test device (based on Arduino Pro Micro) where I created a similar HID device (using the same HID usages for axes) so I could verify that each axis was getting data from the HID device as expected. As it turned out, it didn't.

When updating the joystick state, the extra axis data did not get stored in offsets 24 and 28, but instead in offsets 180 and 196 which is outside of the DIJOYSTATE struct. This in turn led me to discover the DIJOYSTATE2 struct, where offsets 180 and 196 are lVY and lVRz, respectively. I also then noticed IDirectInputDevice_SetDataFormat which specifies that DIJOYSTATE2 should be used.

So the question is then, how to calculate these offsets from the data given while enumerating the objects on the DirectInput Device? At first, the best way seemed be to check wUsage and wUsagePage. I created a HID device with the following "HID axes" (X, Y, Z, Rx, Ry, Rz, Rudder, Throttle, Accelerator, Brake, Steering) and checked at what offset in DIJOYSTATE2 I got tthe axis data at. I also looked up what the corresponding entry in DIJOYSTATE2 was, and logged some of the assocated data from the corresponding DIDEVICEOBJECTINSTANCE struct:

HID Usage      (Page) | OFS | MACRO                           | tszName | guidType     | dwFlags |
X            0x30 (1) |   0 | DIJOFS_X                        | X       | GUID_XAxis   | 0x100   |
Y            0x31 (1) |   4 | DIJOFS_Y                        | Y       | GUID_YAxis   | 0x100   |
Z            0x32 (1) |   8 | DIJOFS_Z                        | Z       | GUID_Zaxis   | 0x100   |
Rx           0x33 (1) |  12 | DIJOFS_RX                       | X       | GUID_RxAxis  | 0x100   |
Ry           0x34 (1) |  16 | DIJOFS_RY                       | Y       | GUID_RyAxis  | 0x100   |
Rz           0x35 (1) |  20 | DIJOFS_RZ                       | Z       | GUID_RzAxis  | 0x100   |
Rudder       0xbA (2) | 228 | FIELD_OFFSET(DIJOYSTATE2, lARz) | R       | GUID_RzAxis  | 0x100   |
Throttle     0xbB (2) |  24 | DIJOFS_SLIDER(0)                | T       | GUID_Slider  | 0x100   |
Accelerator  0xc4 (2) | 180 | FIELD_OFFSET(DIJOYSTATE2, lVY)  | A       | GUID_Yaxis   | 0x100   |
Brake        0xc5 (2) | 196 | FIELD_OFFSET(DIJOYSTATE2, lVRz) | B       | GUID_RzAxis  | 0x100   |
Steering     0xc8 (2) | 176 | FIELD_OFFSET(DIJOYSTATE2, lVX)  | S       | GUID_Xaxis   | 0x100   |

At first glance, it may seem that DirectInput internally just associates HID usage/page pairs with specific struct entries. For example Accelerator => DIJOYSTATE2.lVY. I wanted to verify this (and if so, create a table of this mapping) and so I tested with a variety of axis combinations on my test HID device. However, it wasn't that simple. What offset a given HID usage/page were mapped to, depended also on the presence or absence of other HID usages. I'll skip the details of the investigation and skip to the conclusion.

Based on my observations, DirectInput seems to assign axis entries in DIJOYSTATE2 like this:

  • Each HID usage/page pair is associated with one of the following "classes" of axes; X, Y, Z, RX, RY, RZ, SLIDER (corresponding to the diffent guidTypes). Note that I said classes (as in groups).
  • The axes are assigned in order, so for example if only one axis in a class is present - say the X axis, that is assigned DIJOFS_X.
  • If more than one axis in a class is present, the next "set" of axes are used, in this order: The old axis entries from DIJOYSTATE, then the velocity variants (lVX...), then the acceleration variants (IAX...), and (presumably finally, I did get to test these ones yet), the force variants (IFX...)
  • The sliders are also assigned in order. Both slots of rglSlider ar assigned before rglVSlider is used, etc.

Looking back on the example with the "HID axes" (X, Y, Z, Rx, Ry, Rz, Rudder, Throttle, Accelerator, Brake, Steering), There are three axes with guidType GUID_RzAxis, and they are assigned offsets RZ => lRZ, Brake => lVRz, Rudder => lARz.

I created a test HID device with only the Rudder control usage, no RZ or brake, and verified that the offset I got in DIJOYSTATE was then as expected lRZ, and not lARZ (since no other GUID_RzAxis was in use).

I've also did similar tests with sliders. HID usages "slider", "dial" and "trottle": all get guidType = GUID_SLIDER, and rglSlider[0] is assigned first, then rglSlider[1], and then rglVSlider[0]. If only throttle is available, only rglSlider[0] is assigned.

A patch based on these findings is under way.

On 2020-02-24 22:56:22 +0000, Frode Solheim wrote:

Created attachment 4227
New calculation of axis and slider offsets into DIJOYSTATE2

Added a patch which keeps a tally on NumXAxes, NumYAxes, NumZAxes, NumRXAxes, NumRYAxes, NumRZAxes in addition to NumSliders, and use these to calculate the correct offset into DIJOYSTATE2. Also updated the slider offset calculation to support more than two sliders.

On 2020-03-02 02:05:53 +0000, Sam Lantinga wrote:

It looks like that will work with the buffered update, but the polled update won't correctly query the correct state. Can you check that?

On 2020-03-10 14:39:22 +0000, Frode Solheim wrote:

I will get back with an updated patch!

@SDLBugzilla SDLBugzilla added bug waiting Waiting on user response labels Feb 11, 2021
@slouken slouken self-assigned this Nov 17, 2021
@slouken slouken added this to the 2.0.20 milestone Nov 17, 2021
Copy link

slouken commented Mar 17, 2022

@FrodeSolheim, did you ever update your patch to handle polled updates?

@slouken slouken removed this from the 2.0.22 milestone Mar 17, 2022
Copy link

FrodeSolheim commented Mar 28, 2022

@slouken No, something came up and I had to put the patch on hold. And now it's been a while... -Let me see if I can resurrect the patch, it might not be too much work to handle polling correctly also.

@slouken slouken removed the bug label May 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
waiting Waiting on user response
None yet

No branches or pull requests

3 participants