From c89fed4eae488ae5f953b722facb576a22719485 Mon Sep 17 00:00:00 2001
From: Vicki Pfau <[EMAIL REDACTED]>
Date: Fri, 10 Oct 2025 18:22:48 -0700
Subject: [PATCH] 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.
---
src/joystick/hidapi/SDL_hidapi_switch2.c | 102 ++++++++++++++++++++++-
1 file changed, 100 insertions(+), 2 deletions(-)
diff --git a/src/joystick/hidapi/SDL_hidapi_switch2.c b/src/joystick/hidapi/SDL_hidapi_switch2.c
index 9b59d37952093..d3e75042af044 100644
--- a/src/joystick/hidapi/SDL_hidapi_switch2.c
+++ b/src/joystick/hidapi/SDL_hidapi_switch2.c
@@ -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) {