XOutput icon indicating copy to clipboard operation
XOutput copied to clipboard

Bug fix: XOutputDevice.RefreshInput() is not thread safe.

Open kenh6942 opened this issue 1 year ago • 1 comments

Originally observed issue:

  • When moving 2 joysticks on 2 different input devices mapped to 1 XOutput device at the same time, occasionally one of the joystick inputs would be lost/disconnect.
  • In game, this caused the joystick to remain pressed in some last-polled direction.
  • This could be resolved in the XOutput app by hitting the "Force Refresh" button.

Debug/conclusion process:

  • Log would show a "WARN Poll failed for ..." when this occurs.
  • Log event is from DirectDevice.cs line 319 (logger.Warning($"Poll failed for {ToString()}");)
  • The exception thrown to cause the log event was a "Collection was modified ..."
  • Exception was thrown from line 313 (InputChanged?.Invoke(this, deviceInputChangedEventArgs);)
  • ...which was invoking XOutputDevice.SourceInputChange() which then calls RefreshInput()
  • Debugging calls in the function, it appeared to be the call to changes.Any() that was throwing the exception.
  • Looking online for other instances of when such an exception could occur, the quick solution is to make a copy of the Collection for iteration. Adding ToList() to line 119 (changes.GetChanges(force);) appeared to fix the issue, but is more masking the problem than fixing it.
  • How is the Collection being changed? Each DirectDevice object that eventually calls into XOutputDevice.RefreshInput() has a thread. RefreshInput() makes changes to the state variable of XOutputDevice. It is possible for more than one thread to be calling RefreshInput() and thus making asynchronous changes to the state variable.
  • Excluding it's initialization, state is only used/of-use in RefreshInput().
  • Solution: Make state a local variable of RefreshInput().

This does mean a DeviceState is instantiated for each call to RefreshInput() which is inefficient.

kenh6942 avatar Jan 14 '24 19:01 kenh6942

Originally branched off the 3.32 tag. The forced push was to rebase on top of 3.x branch.

kenh6942 avatar Jan 14 '24 19:01 kenh6942