Automatically learn analog-control calibration

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 commit is contained in:
Patrick Reynolds
2019-10-11 01:14:39 -04:00
parent 5c0e228f29
commit 65fcd7af27
+49 -1
View File
@@ -49,6 +49,8 @@ static NSString *const OEDeviceHandlerUniqueIdentifierKey = @"OEDeviceHandlerUni
{
OEDeviceDescription *_deviceDescription;
NSMutableDictionary *_deadZones;
NSMutableDictionary *_autoCalMins;
NSMutableDictionary *_autoCalMaxs;
}
@property(readwrite) NSUInteger deviceNumber;
@@ -71,6 +73,8 @@ static NSString *const OEDeviceHandlerUniqueIdentifierKey = @"OEDeviceHandlerUni
FIXME("Save default dead zones in user defaults based on device description.");
_defaultDeadZone = 0.125;
_deadZones = [[NSMutableDictionary alloc] init];
_autoCalMins = [[NSMutableDictionary alloc] init];
_autoCalMaxs = [[NSMutableDictionary alloc] init];
}
return self;
@@ -177,6 +181,20 @@ static NSString *const OEDeviceHandlerUniqueIdentifierKey = @"OEDeviceHandlerUni
return deadZone != nil ? [deadZone doubleValue] : _defaultDeadZone;
}
- (CGFloat)autoCalMinForControlCookie:(NSUInteger)controlCookie;
{
NSNumber *autoCalMin = _autoCalMins[@(controlCookie)];
return autoCalMin != nil ? [autoCalMin doubleValue] : 100000;
}
- (CGFloat)autoCalMaxForControlCookie:(NSUInteger)controlCookie;
{
NSNumber *autoCalMax = _autoCalMaxs[@(controlCookie)];
return autoCalMax != nil ? [autoCalMax doubleValue] : -100000;
}
- (CGFloat)deadZoneForControlDescription:(OEControlDescription *)controlDesc;
{
return [self deadZoneForControlCookie:[[controlDesc genericEvent] cookie]];
@@ -193,7 +211,37 @@ static NSString *const OEDeviceHandlerUniqueIdentifierKey = @"OEDeviceHandlerUni
- (CGFloat)scaledValue:(CGFloat)rawValue forAxis:(OEHIDEventAxis)axis controlCookie:(NSUInteger)cookie
{
FIXME("move all scaling logic here from OEHIDEvent in a *clean* way");
return -100;
NSInteger min = [self autoCalMinForControlCookie:cookie];
NSInteger max = [self autoCalMaxForControlCookie:cookie];
BOOL log = NO;
if (rawValue < min)
{
_autoCalMins[@(cookie)] = @(rawValue);
log = YES;
}
if (rawValue > max)
{
_autoCalMaxs[@(cookie)] = @(rawValue);
log = YES;
}
if (log)
NSLog(@"AutoCal: cookie=%lu rawValue=%f min=%ld max=%ld", cookie, rawValue, min, max);
if (min == max)
return -100; // first sample
NSInteger middleValue = (max + min) / 2 + 1;
if(min >= 0)
{
min -= middleValue;
rawValue -= middleValue;
max -= middleValue;
}
if(rawValue < 0) return -rawValue / (CGFloat)min;
else if(rawValue > 0) return rawValue / (CGFloat)max;
return 0.0;
}
@end