Callers (i.e., OEHIDEvent) now pass an expected middle value to the
scaledValue function, so scaledValue doesn't have to learn the middle. We
assume the middle is in fact the middle of the IOKit range for the HID
class.
This fixes the drift problem where the character would sometimes walk
slowly if the user had only fully trained the calibration in one direction
on an axis.
The only remaining limitation at this point is: you must run before you
can walk. If you only ever push the joystick to 25%, the scaler will
assume 25% is the max and will scale it to +1.0 instead of +0.25. I think
it's rare users will even notice (most games push the analog stick to its
extremes most of the time), and if they do, pushing the stick to its
extreme just once solves the problem.
This commit fixes the "relearn" problem from 65fcd7af27. We now won't
scale a positive value unless a (potentially non-continuous) subsequence
of 8 monotonically increasing values have been observed, and we won't
scale a negative value unless 8 decreasing values have been observed.
While we're waiting for those 8 samples, we simply use the default scaler
that assumes the device returns the full range of values expected by
IOKit.
To put it simply, we will use the default scalar for each direction of
each axis until we have 8 good samples about that direction of that axis.
This learning algorithm can still get confused about the center. If you
push an axis to the top of its range (100%) but only halfway to the bottom
(25%), then the learned middle will be 62.5% instead of 50%, so 50% will
actually map to a slightly negative value outside the dead zone. (Link
walks slowly to the left...) The problem is self correcting as soon as
the user pushes the stick to both ends of the range.
There's still room to improve that. Instead of learning the middle, we
can assume that each device's actual middle is somewhere near the IOKit
expected middle. We really only need to learn min and max.
The default OEHIDEvent class assumes that controller devices will always
use the full range specified by IOKit. For example, if IOKit maps a
control to the range 0..255, OEHIDEvent assumes that every attached device
will return 0 and 255 when pushed to its physical limits.
Many N64 controller adapters apparently do not behave this way. They
return less than the full range of values IOKit expects, leading to
characters and vehicles being unable to use their full range of motion.
Link can only tiptoe in Zelda games, for example.
This patch teaches the OEDeviceHandler class to automatically learn the
bounds of each analog axis, individually. So if the actual physical range
of the X axis of an N64 controller is 49..192, all logical values will be
scaled based on that range instead of 0..255.
This, I think, is tidier than adding a calibration interface. Why ask the
user to configure what the software can learn for itself?
One limitation is that OEDeviceHandler must relearn its range each time
you start the the emulator. That means until you've pushed the joystick
through its full range of motion, the scaling will behave in unexpected
ways. For example, if you push an X axis to the right, the learned range
might be 128..192. That rightward push will correctly be scaled to +1.0,
but when you return the stick to the center, 128 will now scale to -1.0.
Pushing the stick to the left fixes this by teaching the program that the
minimum is (e.g.) 49 and not 128. Put simply, the user must push all
analog axes through their full range of motion at least once when the
emulator starts.
There's room for improvement on this "relearn" issue, which I'll address
in a follow-up commit.
This can be used for computer system plugins as a way to ignore system media key presses, so that F1-F12 keys may only register if Fn is pressed. The downside is this does not respect OS keyboard preferences if a user has changed the default settings, but seems to be the best we can do without relying on CGEventTap to track events which will require accessibility access. Attempted app-side workarounds by overriding sendEvent: or checking NSApp.currentEvent to block NSSystemDefined type NSEvent's proved to be useless due to syncing.
Modern gamepad analogs are very sensitive with minimal dead zone. This may be a better medium between modern (MFi/PS3/PS4/Xbox/8bitdo/Steam) and legacy (Logitech era) devices. 4096/32768 = 0.125
There are two issues in this case:
1. XPC messages are processed in a private queue making OEDeviceManager calls unsafe when retrieving device handlers. We solve this problem by making thread safe operations.
2. Sometimes we may receive binding updates on the emulator process before the process has had the time to recognize a device that is connected. This is solved by using placeholder device objects that we replace with real devices in OEHIDEvent when the device is finally parsed. This is possible because if a device is added to the UI process it will be added to the emulator process eventually.