Gyro aiming for Switch controllers on SDL < 2.0.14

dkms-hid-nintendo can expose the IMU sensors of a Switch controller
as a "second joystick", which makes possible to use gyro aiming on a
system without SDL 2.0.14 available (the minimum required to read
controller sensors like gyro and accelerometer).

This commit makes both "sensor" and "joystick reading" to coexist.
"Sensor" is still the preferred method when available.
This commit is contained in:
Jaime Moreira 2023-03-28 23:06:34 -03:00
parent c2d80c64ee
commit 61df6a74d5

View file

@ -167,12 +167,25 @@ static qboolean gyro_active = false;
// Gyro calibration
static float gyro_accum[3];
static unsigned int num_samples;
static cvar_t *gyro_calibration_x;
static cvar_t *gyro_calibration_y;
static cvar_t *gyro_calibration_z;
#if SDL_VERSION_ATLEAST(2, 0, 14) // support for controller sensors (gyro, accelerometer)
static unsigned int num_samples;
#define NATIVE_SDL_GYRO 1 // uses SDL_CONTROLLERSENSORUPDATE to read gyro
#else // for SDL < 2.0.14, gyro can be read as a "secondary joystick" exposed by dkms-hid-nintendo
static unsigned int num_samples[3];
static SDL_Joystick *imu_joystick = NULL; // gyro "joystick"
#define IMU_JOY_AXIS_GYRO_ROLL 3
#define IMU_JOY_AXIS_GYRO_PITCH 4
#define IMU_JOY_AXIS_GYRO_YAW 5
#endif
// To ignore SDL_JOYDEVICEADDED at game init. Allows for hot plugging of game controller afterwards.
static qboolean first_init = true;
@ -766,7 +779,7 @@ IN_Update(void)
break;
}
#if SDL_VERSION_ATLEAST(2, 0, 16) // support for controller sensors (gyro, accelerometer)
#ifdef NATIVE_SDL_GYRO // controller sensors' reading supported (gyro, accelerometer)
case SDL_CONTROLLERSENSORUPDATE:
if (event.csensor.sensor != SDL_SENSOR_GYRO)
{
@ -781,9 +794,39 @@ IN_Update(void)
break;
}
#else // gyro read as "secondary joystick"
case SDL_JOYAXISMOTION:
if ( !imu_joystick || event.cdevice.which != SDL_JoystickInstanceID(imu_joystick) )
{
break; // controller axes handled by SDL_CONTROLLERAXISMOTION
}
int axis_value = event.caxis.value;
if (countdown_reason == REASON_GYROCALIBRATION && updates_countdown)
{
switch (event.caxis.axis)
{
case IMU_JOY_AXIS_GYRO_PITCH:
gyro_accum[0] += axis_value;
num_samples[0]++;
break;
case IMU_JOY_AXIS_GYRO_YAW:
gyro_accum[1] += axis_value;
num_samples[1]++;
break;
case IMU_JOY_AXIS_GYRO_ROLL:
gyro_accum[2] += axis_value;
num_samples[2]++;
}
break;
}
#endif // NATIVE_SDL_GYRO
if (gyro_active && gyro_mode->value &&
!cl_paused->value && cls.key_dest == key_game)
{
#ifdef NATIVE_SDL_GYRO
if (!gyro_turning_axis->value)
{
gyro_yaw = event.csensor.data[1] - gyro_calibration_y->value; // yaw
@ -793,13 +836,31 @@ IN_Update(void)
gyro_yaw = -(event.csensor.data[2] - gyro_calibration_z->value); // roll
}
gyro_pitch = event.csensor.data[0] - gyro_calibration_x->value;
#else // old "joystick" gyro
switch (event.caxis.axis) // inside "case SDL_JOYAXISMOTION" here
{
case IMU_JOY_AXIS_GYRO_PITCH:
gyro_pitch = -(axis_value - gyro_calibration_x->value);
break;
case IMU_JOY_AXIS_GYRO_YAW:
if (!gyro_turning_axis->value)
{
gyro_yaw = axis_value - gyro_calibration_y->value;
}
break;
case IMU_JOY_AXIS_GYRO_ROLL:
if (gyro_turning_axis->value)
{
gyro_yaw = axis_value - gyro_calibration_z->value;
}
}
#endif // NATIVE_SDL_GYRO
}
else
{
gyro_yaw = gyro_pitch = 0;
}
break;
#endif // SDL_VERSION_ATLEAST(2, 0, 16)
case SDL_CONTROLLERDEVICEREMOVED:
if (!controller)
@ -889,10 +950,23 @@ IN_Update(void)
case REASON_GYROCALIBRATION: // finish and save calibration
{
#ifdef NATIVE_SDL_GYRO
const float inverseSamples = 1.f / num_samples;
Cvar_SetValue("gyro_calibration_x", gyro_accum[0] * inverseSamples);
Cvar_SetValue("gyro_calibration_y", gyro_accum[1] * inverseSamples);
Cvar_SetValue("gyro_calibration_z", gyro_accum[2] * inverseSamples);
#else
if (!num_samples[0] || !num_samples[1] || !num_samples[2])
{
Com_Printf("Calibration failed, please retry inside a level after having moved your controller a little.\n");
}
else
{
Cvar_SetValue("gyro_calibration_x", gyro_accum[0] / num_samples[0]);
Cvar_SetValue("gyro_calibration_y", gyro_accum[1] / num_samples[1]);
Cvar_SetValue("gyro_calibration_z", gyro_accum[2] / num_samples[2]);
}
#endif
Com_Printf("Calibration results:\n X=%f Y=%f Z=%f\n",
gyro_calibration_x->value, gyro_calibration_y->value, gyro_calibration_z->value);
CalibrationFinishedCallback();
@ -1269,7 +1343,11 @@ IN_Move(usercmd_t *cmd)
//
// For movement this is not needed, as those are absolute values independent of framerate
float joyViewFactor = cls.rframetime/0.01666f;
#ifdef NATIVE_SDL_GYRO
float gyroViewFactor = (1.0f / M_PI) * joyViewFactor;
#else
float gyroViewFactor = (1.0f / 2560.0f) * joyViewFactor; // normalized for Switch gyro
#endif
if (joystick_yaw)
{
@ -1915,10 +1993,14 @@ Controller_Rumble(const char *name, vec3_t source, qboolean from_player,
void
StartCalibration(void)
{
#ifdef NATIVE_SDL_GYRO
num_samples = 0;
#else
num_samples[0] = num_samples[1] = num_samples[2] = 0;
#endif
gyro_accum[0] = 0.0;
gyro_accum[1] = 0.0;
gyro_accum[2] = 0.0;
num_samples = 0;
updates_countdown = 300;
countdown_reason = REASON_GYROCALIBRATION;
}
@ -2033,9 +2115,26 @@ IN_Controller_Init(qboolean notify_user)
// Ugly hack to detect IMU-only devices - works for Switch controllers at least
if (name_len > 4 && !strncmp(joystick_name + name_len - 4, " IMU", 4))
{
Com_Printf ("Skipping IMU device.\n");
SDL_JoystickClose(joystick);
joystick = NULL;
#ifdef NATIVE_SDL_GYRO
Com_Printf ("Skipping IMU device.\n");
#else // if it's not a Left JoyCon, use it as Gyro
Com_Printf ("IMU device found.\n");
if ( !imu_joystick && name_len > 16 && strncmp(joystick_name + name_len - 16, "Left Joy-Con IMU", 16) != 0 )
{
imu_joystick = SDL_JoystickOpen(i);
if (imu_joystick)
{
show_gyro = true;
Com_Printf ("Using this device as Gyro sensor.\n");
}
else
{
Com_Printf ("Couldn't open IMU: %s.\n", SDL_GetError());
}
}
#endif
continue;
}
@ -2059,7 +2158,7 @@ IN_Controller_Init(qboolean notify_user)
SDL_JoystickClose(joystick);
joystick = NULL;
if (is_controller)
if (is_controller && !controller)
{
controller = SDL_GameControllerOpen(i);
if (!controller)
@ -2071,7 +2170,7 @@ IN_Controller_Init(qboolean notify_user)
show_gamepad = true;
Com_Printf("Enabled as Game Controller, settings:\n%s\n", SDL_GameControllerMapping(controller));
#if SDL_VERSION_ATLEAST(2, 0, 16) // support for controller sensors
#ifdef NATIVE_SDL_GYRO
if ( SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO)
&& !SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE) )
@ -2090,7 +2189,7 @@ IN_Controller_Init(qboolean notify_user)
SDL_GameControllerSetLED(controller, 0, 80, 0); // green light
}
#endif // SDL_VERSION_ATLEAST(2, 0, 16)
#endif // NATIVE_SDL_GYRO
joystick_haptic = SDL_HapticOpenFromJoystick(SDL_GameControllerGetJoystick(controller));
@ -2124,7 +2223,9 @@ IN_Controller_Init(qboolean notify_user)
Com_Printf("Controller doesn't support rumble.\n");
}
#ifdef NATIVE_SDL_GYRO // "native" exits when finding a single working controller
break;
#endif
}
}
}
@ -2236,6 +2337,14 @@ IN_Controller_Shutdown(qboolean notify_user)
show_gamepad = show_gyro = show_haptic = false;
joystick_left_x = joystick_left_y = joystick_right_x = joystick_right_y = 0;
gyro_yaw = gyro_pitch = 0;
#ifndef NATIVE_SDL_GYRO
if (imu_joystick)
{
SDL_JoystickClose(imu_joystick);
imu_joystick = NULL;
}
#endif
}
/*