Supporting more than 4 XInput-capable devices on Windows [RFC]

I discovered that XBox One controllers can be accessed in the background using Raw Input.

I’ve tested that it works using 8 XBox 360 controllers, 1 XBox One controller and a Saitek Dual Analog pad connected at the same time. The XBox controllers had the same limitaiton as accessed via DirectInput i.e. Triggers on the same axis, no guide button and probably no rumble effects.

I think this means that more than 4 (and probably more than 8) XBox One controllers can be accessed in the background via Raw Input but I only have 1 XBox One pad to test with.

Here’s the code I used to test it: https://github.com/supersmo/Using-Raw-Input-API-to-Process-Joystick-Input
(The binary is attached as a release but Avira Antivirus thinks it’s suspicious since it listens to raw input in the background.)

If this is true here’s a summary:
XInput:
Max 4 XBox 360 or XBox One controllers
Always receives input in the background (use gamePad->Suspend(); when the application looses focus to not receive input.)
Limitations:

  • Only XInput capable devices.
  • Unable to activate the extra 2 rumble motors in a XBox One controller triggers. (Only Windows.Gaming.Input can do that)

Windows.Gaming.Input:
Unlimited number of XBox 360 or XBox One controllers for Win 10? (Tested with 9 controllers now.)
Limitations:

  • Controllers can’t be accessed in the background. (Not 100% sure about this. I haven’t been able to get it to work myself and documentation says so at least regarding UWP apps. ref
  • Only works on Universal Windows Platforms. (e.g. windows 10)
  • The Gamepad class only supports Xbox One certified or Xbox 360 compatible gamepad. ref: (unable to post more than 2 links since I am a new user). Other Gamepads can be accessed via the RawGameController class.

DirectInput:
Unlimited number of controllers (?)
Configurable background input access - See limitation for XBox One controllers.
Limitations:

  • XBox 360 and XBox One controllers have triggers on the same axis, no guide button and no rumble.
  • XBox One cannot be accessed in the background.
  • Windows Store Apps can’t use DirectInput.
  • Microsoft no longer recommends using DirectInput.

Raw Input:
Unlimited number of controllers (?)
Configurable background input access.
Limitations:

  • XBox 360 and XBox One controllers have triggers on the same axis, no guide button and probably no rumble.

Looks like there are two use cases:
Accessing controllers in the foreground:
If on a UWP device:
Windows.Gaming.Input.Gamepad for all supported controllers. (tested with 9 XBox controllers)
Windows.Gaming.Input.RawGameController, Raw Input or DirectInput for the rest.

Otherwise:
XInput for 4 supported controllers.
Raw Input or DirectInput for the rest.

Accessing controllers in the background:
XInput for 4 supported controllers.
Raw Input for all controllers or just for XBox One controllers and let DirectInput handle the rest.

(EDITS:

  • Changed the post to reflect that WIndows.Gaming.Input supports more than 8 XBox controllers for WIndows 10.
  • Raw Input can get input in a background application even if the foreground application is using Raw Input.
  • Added that XInput can’t activate the 2 extra rumble motors in an XBox One controller’s triggers.
  • Added information regarding configurable background input for the different APIs.)

RawInput does seem to work well in foreground in background for XInput-capable devices. Assuming it works in parallel to the other APIs, then, maybe, we have a simpler
Fix #6 Add RawInput as a new joystick driver, in parallel to HIDAPI/DirectInput - XInput-capable devices are now handled by RawInput, with the same simple forward correlation that’s there now in SDL_hidapi for getting trigger/guide/rumble from XInput/Win10 where available. Would have a Hint available to use XInput directly (with the current limits).

Or, if we really need to keep using XInput, we could extend the XInput module to also listen to RawInput as a fallback, doing the more complicated correlation to figure out which RawInput devices aren’t getting data through XInput.

That sounds worth pursuing.

For #4, remember that you can’t surface a controller in one API if it might also be available in another API, and you can’t tell from XInput whether one of the devices it’s returning is an Xbox 360 controller.

Can you enter a bug in bugzilla for tracking?

http://bugzilla.libsdl.org/

Created an issue for tracking here: https://bugzilla.libsdl.org/show_bug.cgi?id=4477
Do you prefer we discuss this further in Bugzilla, or just use that for tracking progress/assignment?

Remember that you can’t surface a controller in one API if it might also be available in another API

Yeah, for any of the methods where we correlate backwards, we must surface a number of virtual controllers equal to what’s left (e.g. if we have 3 360s and 2 XBOnes, XInput surfaces only 2 “virtual” controllers, and by run-time correlation we figure out which physical XInput devices should be piped through to the user). With either kind of correlation, we also need to reset it and figure it out again any time a device is removed, since Windows shuffles around which devices are and are not exposed in XInput if one of the previous 4 is removed.

If any of these fixes seems more likely to be acceptable, I’ll put together a proof of concept implementation. I think any could work, and I think I’m leaning towards #5 or #6. If any maintainer has a preference, or has had more in-depth experience with any pitfalls of RawInput, I’d appreciate any additional direction.

These changes really aren’t needed for any of my company’s games, but I’d love to see other, potentially great, local multiplayer games not be limited by default ^_^. Did some data mining, and with our game Splody, 8% of all local multi-player matches involved 5 or more players. With 360 controllers being, by far, the most common, that’s a potentially significant impact on other similar games. Splody just uses DirectInput, so just works, except when Steam overrides things, but other games in my Steam library are not so functional and future ones would potentially be improved by this.

That would be awesome. :slight_smile:
So many games are limited to 4 players because of the input frameworks they use. And as you pointed out Steam Input uses SDL under the hood so this would really open up for games to have more than 4 XInput devices via the Steam Input API as well as long as the controllers can be accessed in the background.

About Raw Input, keep in mind that Raw Input can only receive the messages in the background if the foreground application didn’t consume it via Raw API. So DirectInput seems more reliable for working in the background… except that it doesn’t work in the background for XBox One controllers.

(Another thing that might be worth looking into when targetting UWP is Windows.Gaming.Input.RawGameController.)

About Raw Input, keep in mind that Raw Input can only recieve the messages in the background if the foreground application didn’t consume it via Raw API

For games, I think this is perfect. I really like it when games continue working with controller inputs while in the background, and simultaneously, if hypocritically, get upset when a game window in the background responds to controller input I’m doing for something in the foreground, and RawInput seems to exactly behave this way. Admittedly this only comes up for me when I’m multitasking (gaming + Netflix or gaming + developing, respectively), so not a big issue. For any interactions between games and things like SteamInput, this also seems fine - if the game is using SteamInput, it’s not going to be opening controllers with RawInput, so SteamInput will still get the events; if the game opens the controllers themselves, they get the events, all is well. Only possible issue would be if a game opens all controllers with SDL in RawInput mode and tries to use SteamInput, while ignoring the controllers they opened, it would fail to get any events, but that should be an easy to catch bug for the game developer.

Yikes!
Sorry for spreading some wrong information.

The 8 player limit for Windodws.Gaming.Input doesn’t seem to apply to Windows 10!

I played with Windows.Gaming.Input now and accessed all 9 XBox controllers (8XBox360 and 1 XBxox One) via that interface! I manged to get them all to vibrate too. I was so sure the limit was carried over from XBox One since I had seen that DIrectXTK has a const for MAX_PLAYER_COUNT = 8 for WIndows 10 and XBox One ref!

So my hypothesis now is that you can get all your XBox controllers via Windows.Gaming.Input and the rest either via the Windows.Gaming.Input.RawGameController, DirectInput or RawInput.
The only downside is that I have not found a way to access the controllers in the background using Windows.Gaming.Input.

Did some additional investigation and chatting with supersmo, and it seems RawInput can be configured to get events even when something else is in the foreground and is getting them (not sure that’s desired by default, but can definitely be an option that non-game things/services would want).

I think the most reasonable thing at this point seems to be the Fix #6: adding RawInput as a new joystick driver, and would look very, very similar to the HIDAPI driver in how it gets extra events/functionality from XInput or the Win10 APIs. This driver would be used for all XInput-capable controllers, would be better or equal in all cases (unless I’ve forgotten something), and the others would continue as they are now.

That was a good find!

Updated my previous posts.
Also added the XInput limitaion that it can’t activate the 2 extra rumble motors in an XBox One controller’s triggers. You need to use Windows.Gaming.Input for that.

Just my 2 cents as I had to dig into it and try to use DInput/XInput and came back to 4 XInput only.

When you unplug one of the XBOX gamepad using DInput/RawInput, one of the XInput gamepad is lost and no way to detect which or not the one unplugged.

As my application must run on Windows7, I stick on XInput only because unplug/replug is a must have.
It’s not because it works with 10 or 12 gamepads.

So please consider to the unplug/replug in your desing.

Yeah, hot-plugging is super important, especially if you’ve got 10-12 players. If you’ve got 10-12 wireless gamepads, and you play for just a half hour, you’re almost guaranteed to have 2-3 of them run out of batteries, need recharging, or just turn off because someone “dropped” it and the power pack popped out ;). I’ve got a proof of concept working that gets data from RawInput and passing it through SDL’s exising HIDAPI that seems to solve all of the problems. Needs a bit more testing and cleanup, but I should post a detailed update later today.

Okay, I’ve got a fairly robust RawInput integration with SDL up and working.

It turns out that the RawInput API feeds us exactly the same raw data as HIDAPI is getting, however we can register with a lot more options than the CreateFile method HIDAPI is using (e.g. the option to still get data when in the background), and it gets it in a different way (Windows messages instead of reading from a file), and discovery is rather different (much simpler). So, I ended up implementing it as a very thin RawInput joystick driver that calls into SDL’s HIDAPI 360 driver (SDL_HIDAPI_DriverXbox360), and let the existing code handle all of the logic going from a USB state packet to controller data. Windows does provide a bunch of APIs (just a couple of which hid.c is already using) to enumerate buttons/axes/etc on HID devices (whether from RawInput or “HidD”), which could make this work for more controllers in general, but since it seems we’re only using the raw HIDAPI for very specific controllers (and these happen to be exactly the controllers that don’t work well in current XInput/DirectInput), it seemed fine to just call the existing code with the raw state packets instead of adding yet another path.

You can view the code changes here or as a .patch.

You can ignore any changes to testgamecontroller2 and the addition of SDL_JOYSTICK_ANNOTATE_NAMES - I needed a way to easily see which driver a controller was using while testing, but this probably isn’t generally useful (would want to add a SDL_JoystickGetDeviceDriverName() or something instead).

An updated SDL2.dll with these changes can be found here. I’ve tested it a bit with Steam.

Integration questions:

  • I re-used the DInput SDL_HelperWindow, had to add a callback there, but should I just make a new one? So many lines of code to make a window on Windows ^_^.

Additional work needed:

  • Want to correlate by axis motion and not just button presses - a lot of games start by having you just move, not press a button, would be good for those games to register triggers/guide and rumble correctly
  • Fix correlation to work when framerate is low / two controllers press a button a the same time (currently will occasionally correlate incorrectly)
  • Use Windows.Gaming.Input if available (code mostly there, just resolving compiling/linking issues without the SDK present, I think?)
  • Before button/axis correlation, need initial press of Guide button to go through (even if to a guess at the wrong controller), otherwise Steam’s big picture mode or using the controller as a mouse doesn’t work. The guide button triggers a no-op state packet, so it should still be able to get a guess with pretty high accuracy.
  • Test with lots of random controllers plugged in - I’ve got quite a slew of them, but may want someone else to do some additional compatibility verification. In theory only the behavior of XInput-capable devices should change.

If desired, we could use RawInput + the HID APIs and surface all controllers through RawInput, if for some reason that was preferable to DirectInput, but there doesn’t seem to be any advantage (for other controllers) over DirectInput (would need to, at least, figure out how to exactly map to the DInput GUID so that the game controller DB was not made obsolete). Another possibility would be to yank out the Windows innards of hid.c and use the RawInput APIs to simulate a readable stream and surface it there, to keep just one Joystick driver, but I’m not sure if this would affect the other controllers that currently use HIDAPI (PS4/Switch), and it seems it might just add a lot of unneeded complexity.

I’d love some feedback (or just a “sounds good so far”) from SDL maintainers before I dive into the more finicky details =). I’ll probably have a couple days set aside next week to work on this.

I’ve made further progress on this, and now believe I have something as good or better in all ways to the current joystick handling. I’ve expanded the XInput correlation to respond to axis movement and additionally be reliable amid ambiguous data, low framerates, and device addition/removal, as well as ensuring Guide button presses successfully get propagated even before any button or axis movement has happened to allow definite correlation (seems to 100% of the time get to the right controller in my tests, since a guide button press sends a detectable state packet to correlate with, but theoretically it could trigger on the wrong device, but the app would still receive the button press).

Code changes still here and binary DLL here. All code changes combined into a new .patch here.

The changes are really two independent sets of changes - additions to SDL_hidapi_xbox360.c to improve correlation, and the rest of the changes to allow getting HID data via RawInput instead of ReadFile(). One without the other is pretty useless though, due the to limitations discussed earlier ^_^.

I’ve got a little more cleanup to do, and would like to expand the correlation code to work the same for both XInput and Windows.Gaming.Input, and get that working as well. Also need some hardening as I think there are some edge cases with the correlation that could mis-correlate if an app does not open all of the available joysticks (does anyone do that?), so I need to look into that as well. But, all in all, this seems to be working pretty great! It’s almost to the point where I’ll update my Steam game to no longer force DirectInput and use this code path instead and push it out to my users to find any possible lingering issues.

Sorry for the delay on updates, have been busy, and ran into some snags, but I think they’re all resolved now.

Code changes here, patch here, changes included in binaries here.

This approach appears to be working pretty great, with one catch noted below, which is detectable and handled by just disabling RawInput. I believe this new approach is ready to be considered for merging. I’ve put it live in production for my game on Steam, and will update if users find any issues I did not.

I did the work to make the correlation also work with the Windows.Gaming.Input APIs, however since this API does not get data from controllers in background, it’s not super useful, and only provides a little more than what we get from XInput. The biggest thing it would add would be the ability to rumble all controllers, not just the first 4. To be brought to a production-ready level, it would need to be extended to listen to device add/remove events (unfortunately, Windows.Gaming.Input seems to see devices a second later than when we get device notifications from RawInput, so we can’t piggy-back on those notifications like for XInput), and we’d probably need to get it compiling/running without the Windows 10 SDK. I think that should be tackled separately from this, once this is confirmed to be working well.

The only catch is when a Valve virtual controller exists, these controllers show up in XInput, but do not send RawInput messages (though, interestingly, show up in the RawInput polling APIs). Because these devices show up in XInput alongside the devices we are now handling with RawInput, we have the same kind of correlation issue (but in the other direction), so I simply disable RawInput if there are RawInput-unsupported XInput-capable devices. We could theoretically do correlation in the other direction as well, and make the RawInput/HIDAPI joystick driver expose any XInput devices it finds that are not correlated to a HIDAPI joystick, but I do not have the time to work on that now. Additionally adding to the complexity - if the Valve virtual controller is masking a locally connected controller, the local controller still sends RawInput messages, though they show up with a different VendorID/ProductID than the virtual controller. I think this is, perhaps, a bug on Steam’s part, so after I submit a bug report with them, maybe that’ll be resolved and will simplify things. But for now, we identify there’s a set of controllers that can’t work with RawInput + XInput combining, and fall back to just XInput.

Great work Jimbly!

Would be good if Valve acknowledged it as a bug so all controllers can be handled uniformly but if the Steam Controller Raw Input issue won’t be fixed on Valve’s end, would it help to access the Steam Controllers via DirectInput instead? Thinking of various scenarios where the max 4 controller cap on XInput could pose a problem.

Another super useful thing would be if it could also be used to access triggers on separate axes. Granted it wouldn’t work via Steam Input but if a game used SDL directly it should work :slight_smile:

That would probably help. Right now if you disable XInput with a hint, you’ll get Steam Controllers through DirectInput and XBox through RawInput, however the XInput-correlation stuff gets disabled, so it probably makes sense to split that behavior a bit so you can have XInput-correlation on while still defaulting other controllers to non-XInput methods. But, default probably still needs to be XInput so rumble and triggers work. In theory we could do three-way correlation between RawInput, XInput, and DirectInput - but I’m not going down that rabbit hole!

That might be nice to have, but I’m not sure how many games there are that support more than 4 players and require more than just a couple face buttons, so I think it’s not particularly important. You may have a better idea how many of those are. Maybe some twin-stick shooters use both triggers, but I don’t recall any supporting more than 4 players.

Sorry for getting away from the original topic now but off the top of my head:

  • “Carrotting Brain” supports 4 gamepad players + 2 keyboard players and didn’t go for direct input because of triggers being on the same axis (ref)
  • Swordy: 8 players - Doesn’t work as intented since you can’t extend both arms at the same time. There’s a warning about switching to DirectInput because having triggers on the same axis removes some of the game’s features.(ref: unable to post ref because I’m a new user limited to 2 urls per post)
  • Team Racing League: 6 player racing game didn’t add an option for disabling XInput because of the issue with triggers being on the same axis. (ref)

These are the top of my head ones that didn’t add direct input support because of the same axis issue, or did add support but was negatively impacted because of it. I remember there being several games that had to alter their control scheme for Direct Input becaues of the axis issue as well but that would take some searching for me to dig out. An example of a type of game for more than 4 players that could want to have separate axis trigger control is top down racing games (or 6 player split screen like “The Karters” has) with gas, brake on triggers and combined gas and brake to drift.

This looks good, but the patch doesn’t apply cleanly to the code in Mercurial.

Can you create a bug in bugzilla (http://bugzilla.libsdl.org), and attach a tested patch that applies cleanly?

Thanks!

Will do! I’ve been working with Electron to resolve some issues with GCC cross-compiling from Mac and other minor quality things, and he’s been testing the patch with his Steam game as well. I will update the bug with a .patch in a couple days. Looks like quite a few changes were made since I pulled from upstream a week ago, so hopefully it should generate a clean patch after I merge those in.

Okay, apparently the HIDAPI code underwent a big refactor while I was working on this! I think I’ve got it all merged and settled now, and even found a couple minor bugs while merging. I also updated the hints so “xinput” is split out from “hidapi correlated data from xinput”, so it should be safe for games to set SDL_HINT_XINPUT_ENABLED=0 and they’ll still get great data from (RawInput+XInput) when available, and fall back to DirectInput when not available (because of Valve virtual controllers), while avoiding the XInput-only mode that limits the number of controllers. That’s what I’m doing in Splody now.

New code here patch here. Bug has been updated with the proposed patch.

I’ll strongly consider finishing the Windows.Gaming.Input side of things in a separate patch, once this is merged - sounds like enough games would benefit from that.