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

After seeing threads about people disabling Steam’s self-updating and replacing Steam’s SDL2.dll with versions with that have XInput disabled, in order to fix games that get artificially capped at 4 controllers, I think it’s time to fix SDL to support more than 4 XInput-capable controllers by default.

Obviously, any game can just disable XInput and, for the most part, things will just be better (especially on low-end systems where, at low framerates, the evented DirectInput will miss fewer button presses than the polling XInput) - unless they want to use both triggers at once or rumble. But, I’ve got a lot of games in my Steam library that don’t do that, and have to be coerced into supporting more than 4 XInput-capable devices by disabling XInput via DLL patches or environment variables. These games usually use SDL directly, but apparently newer games (Super Bomberman R, for instance) are using SteamInput which is using SDL in XInput-mode, and not allowing more than 4 players by default (unless you use non-XInput-compatible controllers).

So, I propose a fix!

Fix #1: There’s a relatively simple, fool-proof solution - if, when iterating DirectInput controllers, more than 4 are found, disable XInput as if SDL_XINPUT_ENABLED=0 was set.

Fix #2: I’d rather get the best of both worlds though - XInput for the first 4 controllers, DirectInput for the rest. The way to do this is, when there are more than 4 devices, to correlate inputs between DirectInput and XInput devices to determine which DirectInput devices do not have a paired XInput device. In order to do this within SDL, I would need to make the following changes:

  • When updating the list of devices, if there are more than 4 DirectInput-visible XInput-capable devices, add some virtual “XInput Controller #5”, etc, devices
  • Open all XInput-capable DirectInput devices and XInput devices (either immediately, or when the user opens their first joystick), and keep them open (to keep a few bytes of state for correlation), though not emitting events unless they’ve been opened by the user
  • If we still have some virtual devices, after each joystick update in which relevant state changed, do a correlation check to see if any of the 5+ open DirectInput devices are definitively not correlated to an XInput device, and if so, swap them into one of the virtual devices, and emit their current state (presumably an axis change or button press that was used for correlation) as events

Caveats I can see:

  • When a player has 5 or more XInput devices plugged in and unplugs one, one of the extra DirectInput controllers will now be upgraded to XInput, and, I believe, there is no way for us to know which one, so we would need to do the correlation logic again whenever an XInput device is removed, and it will likely cause controllers to swap players. The simple “disable XInput if more than 4 controllers” solution would not have this problem (except, perhaps when adding a 5th controller at run-time). Maybe there’s something that could be done about this, but it may be complicated.

Fix #3: Or, to throw out a (possibly?) crazy idea, we go the other way around, and always primarily use DirectInput for all devices, and do the correlation the other way around - detect which XInput devices are correlated to DInput devices - and only ever use the data from the XInput APIs for overriding the trigger axis values (once correlated) and triggering rumble. This would probably be a much cleaner solution than #2, but perhaps there’s some other advantage of the XInput API I’m not aware of.

Would a patch to make this kind of change (probably fix #2) be acceptable? Would the “simple” (Fix #1) solution be preferred? Fix #2 or #3, but with a HINT to turn it off and behave like it does now? I’m just asking for feedback on the architectural changes, not on correlation methods, would be happy to discuss those after implementing some and having a room full of monkeys (err, my friends) bang on and power on and off 15 or so controllers repeatedly ;).

I’d love to work on fixing this, but it appears it’s complicated enough that it will be a lot of work, and involve some potentially contentious changes, so I’d love to get some feedback on the most likely to be accepted change to submit before starting making (potentially significant) architectural changes.


For those interested in Steam games “supporting” more than 4 local players (or reading about issues/workarounds people go through to get some of these to work when all you own is Xbox controllers), check out the More Than 4 Local Multiplayer group on Steam.

I think we have code sitting around somewhere to use the new Win10 APIs that allow 8 XInput devices, but my understanding is that the libhidapi code is meant to talk to XInput devices directly instead of through Microsoft’s XInput API (which is where the artificial 4-controller limit actually is). In theory, libhidapi should be able to talk to as many controllers as you can connect to your computer.

There’s a simple reason Steam is limited to 4 XInput controllers. Microsoft has limited the other APIs so that they don’t provide controller events when Steam doesn’t have focus. If Steam is processing the input, it can only use the XInput API.

The correlation you’re describing has already been done in SDL_hidapi_xbox360.c, and works well - as long as the application has focus. :slight_smile:

Hmm… digging into the code and my experiments, that seems to be half true - it’s only on Xbox One controllers that it seems that limitation is there - if I enable SDL’s HIDAPI for just 360 controllers, SDL works way better - I can plug in 4 Xbox One controllers and any number of Xbox 360 controllers, and they all work flawlessly regardless of which app has focus.

If you have less than 4 Xbox One controllers, or they weren’t plugged in first, the Xbox One controllers may or may not work because no reliable correlation is done (GuessXInputDevice() seems wrong most of the time), and additionally XInput may not choose the Xbox One controllers as the first 4 (nothing we can do about that, it seems).

If reliable correlation was done, we could use guarantee the Xbox One controllers work as often as they do now (whenever they’re chosen by XInput) and still support more than 4 of other controllers using HIDAPI.

The Windows 10 APIs are also an interesting idea. 8 controller support isn’t perfect, but the set of games that support more than 8 local players almost all already use DInput (or have that as an option), so that would still, I think, greatly improve things. However, it seems the Windows 10 APIs do not get controller events for any controllers when not in the foreground, and doesn’t seem to provide any options to change this (but I could be missing something there), so that wouldn’t solve the problem for SteamInput.

I’m sorry I didn’t see the hidapi stuff in SDL before, but, I guess because it’s disabled for Xbox controllers, my debugger never went in there. It does seem like it was trying to accomplish some of this. The correlation is the opposite of what I’m generally proposing though (we need to look for devices in one API which are not seen in the other API, which may need to be more robust to protect against false negatives around polling timing issues).

So, with my new understanding, I’d propose one of the following (luckily, all simpler than previous proposals ;):

Fix #4: Enable HIDAPI on Windows for Xbox 360 controllers (list of GUIDs of controllers known to provide background events?), but not Xbox One (or future) controllers, and continue using XInput for Xbox One controllers. If the HIDAPI sees we have unsupported controllers, this would entail having SDL_xinputjoystick watching all XInput devices for events not seen in HIDAPI to determine which XInput devices are relevant (and filtering/mapping so only the Xbox One controllers’ data goes out to the user).

Fix #5: Enable HIDAPI on Windows for both, and expand the correlation in SDL_hidapi_xbox360 to additionally poll XInput and correlate XInput data back to HID devices that are not receiving data. This would probably just call into a bunch of code already in SDL_xinputjoystick.

Either of these would mean that, when in the foreground (the common case, even if SteamInput is making it less common), everything just works. When in the background, any number of Xbox 360 controllers just work, and up to 4 (if they were plugged in first, or are the only controllers) Xbox One controllers would just work. Either way, it’s better than the current limit of 4 controllers whether in foreground or background.

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.