switch2: Preliminary rumble support

Fused controller support is somewhat lacking, and the scaling and frequency
on rumble is somewhat arbitrary, but otherwise it works fine.
This commit is contained in:
Vicki Pfau
2025-10-10 18:22:48 -07:00
committed by Sam Lantinga
parent ef99341691
commit c89fed4eae

View File

@@ -29,9 +29,13 @@
#include "../../misc/SDL_libusb.h"
#include "../SDL_sysjoystick.h"
#include "SDL_hidapijoystick_c.h"
#include "SDL_hidapi_rumble.h"
#ifdef SDL_JOYSTICK_HIDAPI_SWITCH2
#define RUMBLE_INTERVAL 12
#define RUMBLE_MAX 29000
// Define this if you want to log all packets from the controller
#if 0
#define DEBUG_SWITCH2_PROTOCOL
@@ -95,6 +99,13 @@ typedef struct
Uint8 out_endpoint;
Uint8 in_endpoint;
Uint64 rumble_timestamp;
Uint32 rumble_seq;
Uint16 rumble_hi_amp;
Uint16 rumble_lo_amp;
Uint32 rumble_error;
bool rumble_updated;
Switch2_StickCalibration left_stick;
Switch2_StickCalibration right_stick;
Uint8 left_trigger_zero;
@@ -552,7 +563,15 @@ static bool HIDAPI_DriverSwitch2_OpenJoystick(SDL_HIDAPI_Device *device, SDL_Joy
static bool HIDAPI_DriverSwitch2_RumbleJoystick(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
{
return SDL_Unsupported();
SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)device->context;
if (low_frequency_rumble != ctx->rumble_lo_amp || high_frequency_rumble != ctx->rumble_hi_amp) {
ctx->rumble_lo_amp = low_frequency_rumble;
ctx->rumble_hi_amp = high_frequency_rumble;
ctx->rumble_updated = true;
}
return true;
}
static bool HIDAPI_DriverSwitch2_RumbleJoystickTriggers(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, Uint16 left_rumble, Uint16 right_rumble)
@@ -563,7 +582,7 @@ static bool HIDAPI_DriverSwitch2_RumbleJoystickTriggers(SDL_HIDAPI_Device *devic
static Uint32 HIDAPI_DriverSwitch2_GetJoystickCapabilities(SDL_HIDAPI_Device *device, SDL_Joystick *joystick)
{
SDL_DriverSwitch2_Context *ctx = (SDL_DriverSwitch2_Context *)device->context;
Uint32 result = 0;
Uint32 result = SDL_JOYSTICK_CAP_RUMBLE;
if (ctx->player_lights) {
result |= SDL_JOYSTICK_CAP_PLAYER_LED;
@@ -924,6 +943,83 @@ static void HandleSwitchProState(Uint64 timestamp, SDL_Joystick *joystick, SDL_D
);
}
static bool UpdateRumble(SDL_DriverSwitch2_Context *ctx)
{
if (!ctx->rumble_updated && !ctx->rumble_lo_amp && !ctx->rumble_hi_amp) {
return true;
}
Uint64 timestamp = SDL_GetTicks();
Uint64 interval = RUMBLE_INTERVAL;
if (timestamp < ctx->rumble_timestamp) {
return true;
}
if (!SDL_HIDAPI_LockRumble()) {
return false;
}
unsigned char rumble_data[64] = {};
if (ctx->device->product_id == USB_PRODUCT_NINTENDO_SWITCH2_GAMECUBE_CONTROLLER) {
Uint16 rumble_max = SDL_max(ctx->rumble_lo_amp, ctx->rumble_hi_amp);
rumble_data[0x00] = 0x3;
rumble_data[1] = 0x50 | (ctx->rumble_seq & 0xf);
if (rumble_max == 0) {
rumble_data[2] = 2;
ctx->rumble_error = 0;
} else {
if (ctx->rumble_error < rumble_max) {
rumble_data[2] = 1;
ctx->rumble_error += UINT16_MAX - rumble_max;
} else {
rumble_data[2] = 0;
ctx->rumble_error -= rumble_max;
}
}
} else {
// Rumble can get so strong that it might be dangerous to the controller...
// This is a game controller, not a massage device, so let's clamp it somewhat
int low_amp = ctx->rumble_lo_amp * RUMBLE_MAX / UINT16_MAX;
int high_amp = ctx->rumble_hi_amp * RUMBLE_MAX / UINT16_MAX;
rumble_data[0x01] = 0x50 | (ctx->rumble_seq & 0xf);
rumble_data[0x02] = 0x87;
rumble_data[0x03] = ((high_amp >> 4) & 0xfc) | 1;
rumble_data[0x04] = (high_amp >> 12) | 0x20;
rumble_data[0x05] = (low_amp & 0xc0) | 0x11;
rumble_data[0x06] = low_amp >> 8;
switch (ctx->device->product_id) {
case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_LEFT:
case USB_PRODUCT_NINTENDO_SWITCH2_JOYCON_RIGHT:
if (ctx->device->parent) {
// FIXME: This shouldn't be necessary, but the rumble thread appears to back up if we don't do this
interval *= 2;
}
rumble_data[0] = 0x1;
break;
case USB_PRODUCT_NINTENDO_SWITCH2_PRO:
rumble_data[0] = 0x2;
SDL_memcpy(&rumble_data[0x11], &rumble_data[0x01], 6);
break;
}
}
ctx->rumble_seq++;
ctx->rumble_updated = false;
if (!ctx->rumble_lo_amp && !ctx->rumble_hi_amp) {
ctx->rumble_timestamp = 0;
} else {
if (!ctx->rumble_timestamp) {
ctx->rumble_timestamp = timestamp;
}
ctx->rumble_timestamp += interval;
}
if (SDL_HIDAPI_SendRumbleAndUnlock(ctx->device, rumble_data, sizeof(rumble_data)) != sizeof(rumble_data)) {
return SDL_SetError("Couldn't send rumble packet");
}
return true;
}
static void HIDAPI_DriverSwitch2_HandleStatePacket(SDL_HIDAPI_Device *device, SDL_Joystick *joystick, SDL_DriverSwitch2_Context *ctx, Uint8 *data, int size)
{
Uint64 timestamp = SDL_GetTicksNS();
@@ -990,6 +1086,8 @@ static bool HIDAPI_DriverSwitch2_UpdateDevice(SDL_HIDAPI_Device *device)
}
HIDAPI_DriverSwitch2_HandleStatePacket(device, joystick, ctx, data, size);
UpdateRumble(ctx);
}
if (size < 0) {