SDL: Rotate the sensor axes to match gamepad orientation when using the device sensors for game controllers

From 8de6ce7e92907c5f509c6b33c5ee2a5f7b3c1c0c Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 16 Jun 2023 17:48:34 -0700
Subject: [PATCH] Rotate the sensor axes to match gamepad orientation when
 using the device sensors for game controllers

---
 include/SDL3/SDL_sensor.h  | 10 ++++------
 src/joystick/SDL_gamepad.c | 26 ++++++++++++++++++++++++--
 2 files changed, 28 insertions(+), 8 deletions(-)

diff --git a/include/SDL3/SDL_sensor.h b/include/SDL3/SDL_sensor.h
index 9b4a48290b21..dda75f036505 100644
--- a/include/SDL3/SDL_sensor.h
+++ b/include/SDL3/SDL_sensor.h
@@ -89,13 +89,12 @@ typedef enum
  * values[1]: Acceleration on the y axis
  * values[2]: Acceleration on the z axis
  *
- * For phones held in portrait mode and game controllers held in front of you,
- * the axes are defined as follows:
+ * For phones and tablets held in natural orientation and game controllers held in front of you, the axes are defined as follows:
  * -X ... +X : left ... right
  * -Y ... +Y : bottom ... top
  * -Z ... +Z : farther ... closer
  *
- * The axis data is not changed when the phone is rotated.
+ * The axis data is not changed when the device is rotated.
  *
  * \sa SDL_GetDisplayOrientation()
  */
@@ -114,13 +113,12 @@ typedef enum
  * values[1]: Angular speed around the y axis (yaw)
  * values[2]: Angular speed around the z axis (roll)
  *
- * For phones held in portrait mode and game controllers held in front of you,
- * the axes are defined as follows:
+ * For phones and tablets held in natural orientation and game controllers held in front of you, the axes are defined as follows:
  * -X ... +X : left ... right
  * -Y ... +Y : bottom ... top
  * -Z ... +Z : farther ... closer
  *
- * The axis data is not changed when the phone or controller is rotated.
+ * The axis data is not changed when the device is rotated.
  *
  * \sa SDL_GetDisplayOrientation()
  */
diff --git a/src/joystick/SDL_gamepad.c b/src/joystick/SDL_gamepad.c
index 01b133561885..88e6efee1ef4 100644
--- a/src/joystick/SDL_gamepad.c
+++ b/src/joystick/SDL_gamepad.c
@@ -370,6 +370,24 @@ static void RecenterGamepad(SDL_Gamepad *gamepad)
     }
 }
 
+/* SDL defines sensor orientation for phones relative to the natural
+   orientation, and for gamepads relative to being held in front of you.
+   When a phone is being used as a gamepad, its orientation changes,
+   so adjust sensor axes to match.
+ */
+static void AdjustSensorOrientation(float *src, float *dst)
+{
+    /* When a phone is rotated left and laid flat, the axes change
+       orientation as follows:
+        -X to +X becomes +Z to -Z
+        -Y to +Y becomes +X to -X
+        -Z to +Z becomes -Y to +Y
+    */
+    dst[0] = -src[1];
+    dst[1] = src[2];
+    dst[2] = -src[0];
+}
+
 /*
  * Event filter to fire gamepad events from joystick ones
  */
@@ -449,10 +467,14 @@ static int SDLCALL SDL_GamepadEventWatcher(void *userdata, SDL_Event *event)
         SDL_LockJoysticks();
         for (gamepad = SDL_gamepads; gamepad; gamepad = gamepad->next) {
             if (gamepad->joystick->accel && gamepad->joystick->accel_sensor == event->sensor.which) {
-                SDL_SendJoystickSensor(event->common.timestamp, gamepad->joystick, SDL_SENSOR_ACCEL, event->sensor.sensor_timestamp, event->sensor.data, SDL_arraysize(event->sensor.data));
+                float data[3];
+                AdjustSensorOrientation(event->sensor.data, data);
+                SDL_SendJoystickSensor(event->common.timestamp, gamepad->joystick, SDL_SENSOR_ACCEL, event->sensor.sensor_timestamp, data, SDL_arraysize(data));
             }
             if (gamepad->joystick->gyro && gamepad->joystick->gyro_sensor == event->sensor.which) {
-                SDL_SendJoystickSensor(event->common.timestamp, gamepad->joystick, SDL_SENSOR_GYRO, event->sensor.sensor_timestamp, event->sensor.data, SDL_arraysize(event->sensor.data));
+                float data[3];
+                AdjustSensorOrientation(event->sensor.data, data);
+                SDL_SendJoystickSensor(event->common.timestamp, gamepad->joystick, SDL_SENSOR_GYRO, event->sensor.sensor_timestamp, data, SDL_arraysize(data));
             }
         }
         SDL_UnlockJoysticks();