SDL2 is consistently dropping joystick inputs

Hi, just for some context i’m developing using an open source unreal engine 4 plugin that uses SDL2 to add better input support. The full source code is here, though it seems that the problem i’m having isn’t found there. GitHub - tsky1971/UEJoystickPlugin: Unofficial Joystick Plugin for the Unreal Engine

I’m using a while(SDL_PollEvent), (and i have also tried SDL_AddEventWatch to the same effect ), which every frame is consuming the SDL event queue. Sometimes someone will do something like release the trigger on the joystick, or center and release the joystick, and it will get stuck firing forever or get stuck off-center because the last input with trigger press or release, or the center position gets dropped.

I’ve used a debugger to trace this problem down to SDL event poller itself, when this bug happens the event for “press trigger”, “release trigger” or “joystick at 0,0” is never present in sdl’s the event queue to begin with. This happens on multiple devices on multiple testers setups in different countries so while an identical hardware failure is possible it’s very unlikely.

The problem is also framerate dependent, the more frequently we’re pumping events, the less inputs get missed by sdl, at 1 fps 10-25% of inputs get dropped, and at 100 fps about .5% of inputs get dropped which is unfortunately still quite noticable in gameplay, as the problem was noticed by a tester at a high framerate.

As a test i replaced the poll with a pump and peek to check if something else was polling the event queue and eating the events, but in this setup the event queue only increases in size and never drops, staying flat when the joystick is left untouched. Therefore something else isn’t eating the events. Additionally it’s not capping out, since during my testing i never got anywhere near the event queue limit even without polling active, generating about 100 events per second per device.

So, the only conclusion i can make at this point is that all 3 versions of sdl2 i’ve tried(2.0.8, 10, 14) are somehow broken, or something else is consuming the OS events before sdl can pull them into it’s event queue. Or i’m badly misunderstanding something.

Can anyone help figure this out? Im not sure i would even start with how to check if something else is eating the OS(windows in this case) event queue before sdl is getting to them. Unreal engine doesn’t even have directinput support without other plugins (which i have disabled) so if it’s unreal engine somehow eating the inputs from the OS queue im not sure where it would be doing that from.

I’m not surprised to see something like that happening. If you’re running inside another game engine, it thinks it owns that event queue that you’re polling! It’s competing with you for the input events. (A classic race condition.) The framerate-dependent part is most likely due to input being checked on a fixed-timestep loop rather than on the frame rendering time slice, so the lower the framerate, the higher the ratio of fixed-timestep events to frame draws, and so the more of “your” input events the game engine ends up consuming.

That makes a lot of sense and i agree that’s by far the most likely cause. For me whats annoying is that this is for directinput joysticks and UE4 has no directinput support by default. It has a (very bad) optional directinput plugin, which is disabled and isn’t being compiled for the project, so i don’t have a clue how it might be competing for the inputs!

Do you have any general methods for now to track down where the input is getting used that might help? I’m building the engine from source so i can potentially track down and fix whatever is doing it.

If you’re using Windows, look through the SDL code to see which window message codes generate the joystick events, then set a Win32 API hook to respond to those messages as a side-channel rather than competing with the engine for input.

On other OSes I don’t know how you’d want to do that.

1 Like

I don’t think this goes through normal Win32 event handling.
The DirectInput events are fetched either via GetDeviceData() or GetDeviceState(), depending on joystick->hwdata->buffered (I didn’t further look up what this all means and when what happens).

Anyway, if UE4 indeed doesn’t support DirectInput, its own event handling shouldn’t get in the way here.

If the current joystick state is only received from the device when SDL_PumpEvents() is called (usually by SDL_PollEvent()), then it’s not surprising if events that happen between two frames are lost. I don’t know if this is actually the case though, shouldn’t either SDL2 or DirectInput get the device state in a thread so this doesn’t happen?
If not, this could be your solution: In a separate thread call SDL_PumpEvents() often enough, so all the Joystick events have been generated once you get around to call SDL_PollEvent() from the (possibly laggy) main-thread

1 Like

Thanks for the pointer to GetDeviceData(), that does seem to be the way as i couldnt find any win32 hooks. The docs say that GetDeviceData() Is buffered while GetDeviceState() is not, im having trouble figuring which path SDL is using as i inspect the source code though.

Im pretty sure it’s the buffered one though, because if it was just the state then a released trigger wouldnt get stuck as pulled, or vice versa. This problem would probably be undetectable on the unbuffered version because then the state changes getting dropped would just be corrected a frame late, but instead what happens is the input device changes states, writes something to the buffer but that change seems to vanish before sdl can queue it and a trigger someone let go 10 seconds ago is still “held down” to the game.

I could break it into another thread at high poll rate, but we’re noticing the issue at high framerates like 144 already so im not sure how high i’d need to go to reduce the probability of missing an event enough. I’d rather just never miss them and not have to gamble on it! And GetDeviceData() being buffered means i dont think i can just side channel it as whatever is clearing the buffer will still be doing that, though i could drop SDL and switch to directly getting states from the the hell that is directinput joysticks but that sounds like a bad time, saving that for the last ditch effort.

The engine definitely doesnt have default directinput support, but i wouldn’t put it past epic to be consuming directinput anyways as a part of some arcane or half-implemented part of the engine, or even a different plugin than the one i mentioned being disabled. So I tried disabling all the plugins, but that didnt work. Now that i know that “GetDeviceData” is the keyword though i ran a search and i found 3 instances, one in a directX includes list for a bunch of libs, which im kinda praying isn’t doing anything since i couldnt find that function actually being called just declared in a header. And two more instances in what seems to be a different version of SDL embedded inside the engine’s third party docker support. Apparantly SDL is used for the docker GUI backend? It feels a bit like i’ve entered the third layer of hell here but i’m pretty sure that code isn’t even being compiled since it’s #if’d out by a preprocessor that isn’t enabled, so back to square one.

I just found SDL_LockJoysticks in the API, so i will work on trying to use that and maybe an exclusive lock on the joystick devices once they’re detected will do the trick. Bit worried that will mean that there are side effects like people being unable to use drivers, joystick status displays or virtual joysticks correctly though.

Edit: No luck on lock joysticks, it must not lock them the way i was hoping it would.

I found the engine’s main input loop and disabled everything on the input loop other than the sdl plugin, now im pretty certain nothing else in the engine is messing with the direct input device events, but it’s hard to be absolutely sure, so im crossing that possibility out for now.

I’ve noticed a new effect though. Specifically that event inputs almost never get dropped if you are just doing one thing, like pulling the trigger. It’s when you combine a trigger pull with a 3 axis joystick movement plus throttle that the events stop coming really consistently. So im back to “buffer overflow” thinking.

From further research, every directinput device has it’s own buffer of some length, and SDL2 is essentially acting as a middle man buffer, moving items from the directinput buffer into it’s own queue. I’ve found this in the SDL source code which is probably the real issue here:


32 is probably just way too low for a 3 axis stick with multiple buttons at a device polling rate. If SDL is running at a high polling rate and a game is running a 60 on a typical joystick with 3 axes, a throttle, a hat, and a user mashing several buttons that queue fills up the directinput buffer will overflow the buffer in just 3-5 polls, and if the internal poll rate on the device is even remotely high then it’s not hard to see how that would break pretty much immediately. The default USB polling rate for most of these directinput devices seems to be the usb default of 125hz with mice for example going up 1000+ so i don’t understand how this even works reliably for 60 fps games that might dip to begin with, from what i can see in the source sdl’s mouse support has a much bigger buffer? Definetly needs expanding for joysticks.

So, i rebuilt sdl2 from source with the directinput buffering disabled and it actually fixes the issue completely, though it generates the new issue that button presses quicker than a frame are missed by the game, which isnt great either but is arguably better than the alternative. I also tried increasing the size of the directinput buffer, but sdl is using a statically allocated array for that so i could only get it from 32 to 167 before it hit the static alloc memory limit, which did noticably improve the result but without a guarantee that inputs wont get dropped it’s not enough.

Next i tried forcing sdl to use both buffered and immediate mode events at the same time and that solved the problem for me completely. All my game code does (at least, it should) use state aware bindings so having an accidental doubled up “button down” before a “button release” is never an issue, since a user can sometimes have two buttons bound to the same thing anyways and the game already needs handling for that. The function i modified is below, i just added that one line inside the buffering side of the if statement. The order in there is important or it doesnt work, i guess the event queue add order requires it.

 void
SDL_DINPUT_JoystickUpdate(SDL_Joystick * joystick)
{
    HRESULT result;

    result = IDirectInputDevice8_Poll(joystick->hwdata->InputDevice);
    if (result == DIERR_INPUTLOST || result == DIERR_NOTACQUIRED) {
        IDirectInputDevice8_Acquire(joystick->hwdata->InputDevice);
        IDirectInputDevice8_Poll(joystick->hwdata->InputDevice);
    }

	if (joystick->hwdata->buffered) {
		UpdateDINPUTJoystickState_Polled(joystick);
        UpdateDINPUTJoystickState_Buffered(joystick);
    } else {
        UpdateDINPUTJoystickState_Polled(joystick);
    }


}

So unless i find other problems, which honestly isn’t all that unlikely (we will see when this hits the testers) this issue is solved for my project.

But, on SDL’s end. There’s no reason to ignore changes in joystick state when it’s right there to check, and if performance from checking both paths is the concern then SDL could do something like check if the buffer was at capacity and if it was, only then do the state comparison checks to produce events when it gets pumped and if it detects a different state than the last pull. This would be a big improvement on my hacky solution where im just using polls and buffers at the same time, and work for every game, not just a game written with button state kept in mind. Also that default directinput buffer of 32 really just should be 100x bigger if there’s not a good reason not to. The alternative is just allowing some events on some standard joystick hardware to get dropped at standard framerates which is never desirable.