SDL: Re-added balls to the SDL joystick API

From efbbafb3f15afe6e9538431937428dd6a489c1ea Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 10 Mar 2024 21:06:14 -0700
Subject: [PATCH] Re-added balls to the SDL joystick API

It turns out these were being used on Linux and at least one virtual driver was making use of them (thanks @mrfixit2001!)
---
 build-scripts/SDL_migration.cocci      | 14 +++++
 docs/README-migration.md               |  4 +-
 include/SDL3/SDL_events.h              | 24 ++++++-
 include/SDL3/SDL_joystick.h            | 51 ++++++++++++---
 include/SDL3/SDL_oldnames.h            |  6 ++
 src/dynapi/SDL_dynapi.sym              |  2 +
 src/dynapi/SDL_dynapi_overrides.h      |  2 +
 src/dynapi/SDL_dynapi_procs.h          |  2 +
 src/events/SDL_events.c                |  6 ++
 src/joystick/SDL_joystick.c            | 86 ++++++++++++++++++++++++--
 src/joystick/SDL_joystick_c.h          | 19 +++---
 src/joystick/SDL_sysjoystick.h         | 10 +++
 src/joystick/linux/SDL_sysjoystick.c   | 52 ++++++++++++++++
 src/joystick/linux/SDL_sysjoystick_c.h |  6 ++
 src/test/SDL_test_common.c             |  5 ++
 test/testautomation_joystick.c         |  1 +
 16 files changed, 262 insertions(+), 28 deletions(-)

diff --git a/build-scripts/SDL_migration.cocci b/build-scripts/SDL_migration.cocci
index 7cde786bc43b8..989b4c9eb87e8 100644
--- a/build-scripts/SDL_migration.cocci
+++ b/build-scripts/SDL_migration.cocci
@@ -2153,6 +2153,10 @@ expression e;
 + SDL_EVENT_JOYSTICK_AXIS_MOTION
 @@
 @@
+- SDL_JOYBALLMOTION
++ SDL_EVENT_JOYSTICK_BALL_MOTION
+@@
+@@
 - SDL_JOYHATMOTION
 + SDL_EVENT_JOYSTICK_HAT_MOTION
 @@
@@ -3044,3 +3048,13 @@ typedef SDL_version, SDL_Version;
 @@
 - SDL_HINT_PS2_DYNAMIC_VSYNC
 + SDL_HINT_RENDER_PS2_DYNAMIC_VSYNC
+@@
+@@
+- SDL_JoystickNumBalls
++ SDL_NumJoystickBalls
+  (...)
+@@
+@@
+- SDL_JoystickGetBall
++ SDL_GetJoystickBall
+  (...)
diff --git a/docs/README-migration.md b/docs/README-migration.md
index 18dd8decf1e8c..c5f279c8f9555 100644
--- a/docs/README-migration.md
+++ b/docs/README-migration.md
@@ -370,6 +370,7 @@ The following symbols have been renamed:
 * SDL_FINGERUP => SDL_EVENT_FINGER_UP
 * SDL_FIRSTEVENT => SDL_EVENT_FIRST
 * SDL_JOYAXISMOTION => SDL_EVENT_JOYSTICK_AXIS_MOTION
+* SDL_JOYBALLMOTION => SDL_EVENT_JOYSTICK_BALL_MOTION
 * SDL_JOYBATTERYUPDATED => SDL_EVENT_JOYSTICK_BATTERY_UPDATED
 * SDL_JOYBUTTONDOWN => SDL_EVENT_JOYSTICK_BUTTON_DOWN
 * SDL_JOYBUTTONUP => SDL_EVENT_JOYSTICK_BUTTON_UP
@@ -797,6 +798,7 @@ The following functions have been renamed:
 * SDL_JoystickGetAttached() => SDL_JoystickConnected()
 * SDL_JoystickGetAxis() => SDL_GetJoystickAxis()
 * SDL_JoystickGetAxisInitialState() => SDL_GetJoystickAxisInitialState()
+* SDL_JoystickGetBall() => SDL_GetJoystickBall()
 * SDL_JoystickGetButton() => SDL_GetJoystickButton()
 * SDL_JoystickGetFirmwareVersion() => SDL_GetJoystickFirmwareVersion()
 * SDL_JoystickGetGUID() => SDL_GetJoystickGUID()
@@ -813,6 +815,7 @@ The following functions have been renamed:
 * SDL_JoystickIsVirtual() => SDL_IsJoystickVirtual()
 * SDL_JoystickName() => SDL_GetJoystickName()
 * SDL_JoystickNumAxes() => SDL_GetNumJoystickAxes()
+* SDL_JoystickNumBalls() => SDL_GetNumJoystickBalls()
 * SDL_JoystickNumButtons() => SDL_GetNumJoystickButtons()
 * SDL_JoystickNumHats() => SDL_GetNumJoystickHats()
 * SDL_JoystickOpen() => SDL_OpenJoystick()
@@ -843,7 +846,6 @@ The following functions have been removed:
 * SDL_JoystickHasRumble() - replaced with SDL_PROP_JOYSTICK_CAP_RUMBLE_BOOLEAN
 * SDL_JoystickHasRumbleTriggers() - replaced with SDL_PROP_JOYSTICK_CAP_TRIGGER_RUMBLE_BOOLEAN
 * SDL_JoystickNameForIndex() - replaced with SDL_GetJoystickInstanceName()
-* SDL_JoystickNumBalls() - API has been removed, see https://github.com/libsdl-org/SDL/issues/6766
 * SDL_JoystickPathForIndex() - replaced with SDL_GetJoystickInstancePath()
 * SDL_NumJoysticks() - replaced with SDL_GetJoysticks()
 
diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index ec7002e562c58..2f1a7d5afcbcb 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -152,11 +152,12 @@ typedef enum
 
     /* Joystick events */
     SDL_EVENT_JOYSTICK_AXIS_MOTION  = 0x600, /**< Joystick axis motion */
-    SDL_EVENT_JOYSTICK_HAT_MOTION   = 0x602, /**< Joystick hat position change */
+    SDL_EVENT_JOYSTICK_BALL_MOTION,          /**< Joystick trackball motion */
+    SDL_EVENT_JOYSTICK_HAT_MOTION,           /**< Joystick hat position change */
     SDL_EVENT_JOYSTICK_BUTTON_DOWN,          /**< Joystick button pressed */
     SDL_EVENT_JOYSTICK_BUTTON_UP,            /**< Joystick button released */
-    SDL_EVENT_JOYSTICK_ADDED,         /**< A new joystick has been inserted into the system */
-    SDL_EVENT_JOYSTICK_REMOVED,       /**< An opened joystick has been removed */
+    SDL_EVENT_JOYSTICK_ADDED,                /**< A new joystick has been inserted into the system */
+    SDL_EVENT_JOYSTICK_REMOVED,              /**< An opened joystick has been removed */
     SDL_EVENT_JOYSTICK_BATTERY_UPDATED,      /**< Joystick battery level change */
     SDL_EVENT_JOYSTICK_UPDATE_COMPLETE,      /**< Joystick update is complete */
 
@@ -336,6 +337,22 @@ typedef struct SDL_MouseMotionEvent
     float yrel;         /**< The relative motion in the Y direction */
 } SDL_MouseMotionEvent;
 
+/**
+ *  \brief Joystick trackball motion event structure (event.jball.*)
+ */
+typedef struct SDL_JoyBallEvent
+{
+    Uint32 type;        /**< ::SDL_JOYBALLMOTION */
+    Uint64 timestamp;   /**< In nanoseconds, populated using SDL_GetTicksNS() */
+    SDL_JoystickID which; /**< The joystick instance id */
+    Uint8 ball;         /**< The joystick trackball index */
+    Uint8 padding1;
+    Uint8 padding2;
+    Uint8 padding3;
+    Sint16 xrel;        /**< The relative motion in the X direction */
+    Sint16 yrel;        /**< The relative motion in the Y direction */
+} SDL_JoyBallEvent;
+
 /**
  *  Mouse button event structure (event.button.*)
  */
@@ -706,6 +723,7 @@ typedef union SDL_Event
     SDL_MouseButtonEvent button;            /**< Mouse button event data */
     SDL_MouseWheelEvent wheel;              /**< Mouse wheel event data */
     SDL_JoyAxisEvent jaxis;                 /**< Joystick axis event data */
+    SDL_JoyBallEvent jball;                 /**< Joystick ball event data */
     SDL_JoyHatEvent jhat;                   /**< Joystick hat event data */
     SDL_JoyButtonEvent jbutton;             /**< Joystick button event data */
     SDL_JoyDeviceEvent jdevice;             /**< Joystick device change event data */
diff --git a/include/SDL3/SDL_joystick.h b/include/SDL3/SDL_joystick.h
index 6c3fc20242ec4..4d45467b310db 100644
--- a/include/SDL3/SDL_joystick.h
+++ b/include/SDL3/SDL_joystick.h
@@ -732,6 +732,24 @@ extern DECLSPEC SDL_JoystickID SDLCALL SDL_GetJoystickInstanceID(SDL_Joystick *j
  */
 extern DECLSPEC int SDLCALL SDL_GetNumJoystickAxes(SDL_Joystick *joystick);
 
+/**
+ * Get the number of trackballs on a joystick.
+ *
+ * Joystick trackballs have only relative motion events associated with them
+ * and their state cannot be polled.
+ *
+ * Most joysticks do not have trackballs.
+ *
+ * \param joystick an SDL_Joystick structure containing joystick information
+ * \returns the number of trackballs on success or a negative error code on
+ *          failure; call SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_GetJoystickBall
+ */
+extern DECLSPEC int SDLCALL SDL_GetNumJoystickBalls(SDL_Joystick *joystick);
+
 /**
  * Get the number of POV hats on a joystick.
  *
@@ -823,8 +841,7 @@ extern DECLSPEC void SDLCALL SDL_UpdateJoysticks(void);
  *
  * \sa SDL_GetNumJoystickAxes
  */
-extern DECLSPEC Sint16 SDLCALL SDL_GetJoystickAxis(SDL_Joystick *joystick,
-                                                   int axis);
+extern DECLSPEC Sint16 SDLCALL SDL_GetJoystickAxis(SDL_Joystick *joystick, int axis);
 
 /**
  * Get the initial state of an axis control on a joystick.
@@ -840,8 +857,28 @@ extern DECLSPEC Sint16 SDLCALL SDL_GetJoystickAxis(SDL_Joystick *joystick,
  *
  * \since This function is available since SDL 3.0.0.
  */
-extern DECLSPEC SDL_bool SDLCALL SDL_GetJoystickAxisInitialState(SDL_Joystick *joystick,
-                                                   int axis, Sint16 *state);
+extern DECLSPEC SDL_bool SDLCALL SDL_GetJoystickAxisInitialState(SDL_Joystick *joystick, int axis, Sint16 *state);
+
+/**
+ * Get the ball axis change since the last poll.
+ *
+ * Trackballs can only return relative motion since the last call to
+ * SDL_GetJoystickBall(), these motion deltas are placed into `dx` and `dy`.
+ *
+ * Most joysticks do not have trackballs.
+ *
+ * \param joystick the SDL_Joystick to query
+ * \param ball the ball index to query; ball indices start at index 0
+ * \param dx stores the difference in the x axis position since the last poll
+ * \param dy stores the difference in the y axis position since the last poll
+ * \returns 0 on success or a negative error code on failure; call
+ *          SDL_GetError() for more information.
+ *
+ * \since This function is available since SDL 3.0.0.
+ *
+ * \sa SDL_GetNumJoystickBalls
+ */
+extern DECLSPEC int SDLCALL SDL_GetJoystickBall(SDL_Joystick *joystick, int ball, int *dx, int *dy);
 
 /**
  *  \name Hat positions
@@ -881,8 +918,7 @@ extern DECLSPEC SDL_bool SDLCALL SDL_GetJoystickAxisInitialState(SDL_Joystick *j
  *
  * \sa SDL_GetNumJoystickHats
  */
-extern DECLSPEC Uint8 SDLCALL SDL_GetJoystickHat(SDL_Joystick *joystick,
-                                                 int hat);
+extern DECLSPEC Uint8 SDLCALL SDL_GetJoystickHat(SDL_Joystick *joystick, int hat);
 
 /**
  * Get the current state of a button on a joystick.
@@ -896,8 +932,7 @@ extern DECLSPEC Uint8 SDLCALL SDL_GetJoystickHat(SDL_Joystick *joystick,
  *
  * \sa SDL_GetNumJoystickButtons
  */
-extern DECLSPEC Uint8 SDLCALL SDL_GetJoystickButton(SDL_Joystick *joystick,
-                                                    int button);
+extern DECLSPEC Uint8 SDLCALL SDL_GetJoystickButton(SDL_Joystick *joystick, int button);
 
 /**
  * Start a rumble effect.
diff --git a/include/SDL3/SDL_oldnames.h b/include/SDL3/SDL_oldnames.h
index 0dafc18d88792..8ca7dddd029a8 100644
--- a/include/SDL3/SDL_oldnames.h
+++ b/include/SDL3/SDL_oldnames.h
@@ -115,6 +115,7 @@
 #define SDL_JOYBUTTONUP SDL_EVENT_JOYSTICK_BUTTON_UP
 #define SDL_JOYDEVICEADDED SDL_EVENT_JOYSTICK_ADDED
 #define SDL_JOYDEVICEREMOVED SDL_EVENT_JOYSTICK_REMOVED
+#define SDL_JOYBALLMOTION SDL_EVENT_JOYSTICK_BALL_MOTION
 #define SDL_JOYHATMOTION SDL_EVENT_JOYSTICK_HAT_MOTION
 #define SDL_KEYDOWN SDL_EVENT_KEY_DOWN
 #define SDL_KEYMAPCHANGED SDL_EVENT_KEYMAP_CHANGED
@@ -302,6 +303,7 @@
 #define SDL_JoystickGetAttached SDL_JoystickConnected
 #define SDL_JoystickGetAxis SDL_GetJoystickAxis
 #define SDL_JoystickGetAxisInitialState SDL_GetJoystickAxisInitialState
+#define SDL_JoystickGetBall SDL_GetJoystickBall
 #define SDL_JoystickGetButton SDL_GetJoystickButton
 #define SDL_JoystickGetFirmwareVersion SDL_GetJoystickFirmwareVersion
 #define SDL_JoystickGetGUID SDL_GetJoystickGUID
@@ -318,6 +320,7 @@
 #define SDL_JoystickIsVirtual SDL_IsJoystickVirtual
 #define SDL_JoystickName SDL_GetJoystickName
 #define SDL_JoystickNumAxes SDL_GetNumJoystickAxes
+#define SDL_JoystickNumBalls SDL_GetNumJoystickBalls
 #define SDL_JoystickNumButtons SDL_GetNumJoystickButtons
 #define SDL_JoystickNumHats SDL_GetNumJoystickHats
 #define SDL_JoystickOpen SDL_OpenJoystick
@@ -597,6 +600,7 @@
 #define SDL_JOYBUTTONUP SDL_JOYBUTTONUP_renamed_SDL_EVENT_JOYSTICK_BUTTON_UP
 #define SDL_JOYDEVICEADDED SDL_JOYDEVICEADDED_renamed_SDL_EVENT_JOYSTICK_ADDED
 #define SDL_JOYDEVICEREMOVED SDL_JOYDEVICEREMOVED_renamed_SDL_EVENT_JOYSTICK_REMOVED
+#define SDL_JOYBALLMOTION SDL_JOYBALLMOTION_renamed_SDL_EVENT_JOYSTICK_BALL_MOTION
 #define SDL_JOYHATMOTION SDL_JOYHATMOTION_renamed_SDL_EVENT_JOYSTICK_HAT_MOTION
 #define SDL_KEYDOWN SDL_KEYDOWN_renamed_SDL_EVENT_KEY_DOWN
 #define SDL_KEYMAPCHANGED SDL_KEYMAPCHANGED_renamed_SDL_EVENT_KEYMAP_CHANGED
@@ -785,6 +789,7 @@
 #define SDL_JoystickGetAttached SDL_JoystickGetAttached_renamed_SDL_JoystickConnected
 #define SDL_JoystickGetAxis SDL_JoystickGetAxis_renamed_SDL_GetJoystickAxis
 #define SDL_JoystickGetAxisInitialState SDL_JoystickGetAxisInitialState_renamed_SDL_GetJoystickAxisInitialState
+#define SDL_JoystickGetBall SDL_JoystickGetBall_renamed_SDL_GetJoystickBall
 #define SDL_JoystickGetButton SDL_JoystickGetButton_renamed_SDL_GetJoystickButton
 #define SDL_JoystickGetFirmwareVersion SDL_JoystickGetFirmwareVersion_renamed_SDL_GetJoystickFirmwareVersion
 #define SDL_JoystickGetGUID SDL_JoystickGetGUID_renamed_SDL_GetJoystickGUID
@@ -801,6 +806,7 @@
 #define SDL_JoystickIsVirtual SDL_JoystickIsVirtual_renamed_SDL_IsJoystickVirtual
 #define SDL_JoystickName SDL_JoystickName_renamed_SDL_GetJoystickName
 #define SDL_JoystickNumAxes SDL_JoystickNumAxes_renamed_SDL_GetNumJoystickAxes
+#define SDL_JoystickNumBalls SDL_JoystickNumBalls_renamed_SDL_GetNumJoystickBalls
 #define SDL_JoystickNumButtons SDL_JoystickNumButtons_renamed_SDL_GetNumJoystickButtons
 #define SDL_JoystickNumHats SDL_JoystickNumHats_renamed_SDL_GetNumJoystickHats
 #define SDL_JoystickOpen SDL_JoystickOpen_renamed_SDL_OpenJoystick
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index 5d08979e090cd..67dec60c394a7 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -974,6 +974,8 @@ SDL3_0.0.0 {
     SDL_qsort_r;
     SDL_bsearch_r;
     SDL_AddVulkanRenderSemaphores;
+    SDL_GetNumJoystickBalls;
+    SDL_GetJoystickBall;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index c5378f1fd77fb..5ee503eb9abcc 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -999,3 +999,5 @@
 #define SDL_qsort_r SDL_qsort_r_REAL
 #define SDL_bsearch_r SDL_bsearch_r_REAL
 #define SDL_AddVulkanRenderSemaphores SDL_AddVulkanRenderSemaphores_REAL
+#define SDL_GetNumJoystickBalls SDL_GetNumJoystickBalls_REAL
+#define SDL_GetJoystickBall SDL_GetJoystickBall_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index 0c84f92d963bc..77494c653e14c 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -1024,3 +1024,5 @@ SDL_DYNAPI_PROC(SDL_CameraPosition,SDL_GetCameraDevicePosition,(SDL_CameraDevice
 SDL_DYNAPI_PROC(void,SDL_qsort_r,(void *a, size_t b, size_t c, int (SDLCALL *d)(void *, const void *, const void *), void *e),(a,b,c,d,e),)
 SDL_DYNAPI_PROC(void*,SDL_bsearch_r,(const void *a, const void *b, size_t c, size_t d, int (SDLCALL *e)(void *, const void *, const void *), void *f),(a,b,c,d,e,f),return)
 SDL_DYNAPI_PROC(int,SDL_AddVulkanRenderSemaphores,(SDL_Renderer *a, Uint32 b, Sint64 c, Sint64 d),(a,b,c,d),return)
+SDL_DYNAPI_PROC(int,SDL_GetNumJoystickBalls,(SDL_Joystick *a),(a),return)
+SDL_DYNAPI_PROC(int,SDL_GetJoystickBall,(SDL_Joystick *a, int b, int *c, int *d),(a,b,c,d),return)
diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index 01d126350ac1f..102ae52451ea4 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -383,6 +383,12 @@ static void SDL_LogEvent(const SDL_Event *event)
                            (uint)event->jaxis.axis, (int)event->jaxis.value);
         break;
 
+        SDL_EVENT_CASE(SDL_EVENT_JOYSTICK_BALL_MOTION)
+        (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u which=%d ball=%u xrel=%d yrel=%d)",
+                           (uint)event->jball.timestamp, (int)event->jball.which,
+                           (uint)event->jball.ball, (int)event->jball.xrel, (int)event->jball.yrel);
+        break;
+
         SDL_EVENT_CASE(SDL_EVENT_JOYSTICK_HAT_MOTION)
         (void)SDL_snprintf(details, sizeof(details), " (timestamp=%u which=%d hat=%u value=%u)",
                            (uint)event->jhat.timestamp, (int)event->jhat.which,
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index f68968ff0b2ad..82fb8a7991f58 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -1091,15 +1091,21 @@ SDL_Joystick *SDL_OpenJoystick(SDL_JoystickID instance_id)
     joystick->guid = driver->GetDeviceGUID(device_index);
 
     if (joystick->naxes > 0) {
-        joystick->axes = (SDL_JoystickAxisInfo *)SDL_calloc(joystick->naxes, sizeof(SDL_JoystickAxisInfo));
+        joystick->axes = (SDL_JoystickAxisInfo *)SDL_calloc(joystick->naxes, sizeof(*joystick->axes));
+    }
+    if (joystick->nballs > 0) {
+        joystick->balls = (SDL_JoystickBallData *)SDL_calloc(joystick->nballs, sizeof(*joystick->balls));
     }
     if (joystick->nhats > 0) {
-        joystick->hats = (Uint8 *)SDL_calloc(joystick->nhats, sizeof(Uint8));
+        joystick->hats = (Uint8 *)SDL_calloc(joystick->nhats, sizeof(*joystick->hats));
     }
     if (joystick->nbuttons > 0) {
-        joystick->buttons = (Uint8 *)SDL_calloc(joystick->nbuttons, sizeof(Uint8));
+        joystick->buttons = (Uint8 *)SDL_calloc(joystick->nbuttons, sizeof(*joystick->buttons));
     }
-    if (((joystick->naxes > 0) && !joystick->axes) || ((joystick->nhats > 0) && !joystick->hats) || ((joystick->nbuttons > 0) && !joystick->buttons)) {
+    if (((joystick->naxes > 0) && !joystick->axes) ||
+        ((joystick->nballs > 0) && !joystick->balls) ||
+        ((joystick->nhats > 0) && !joystick->hats) ||
+        ((joystick->nbuttons > 0) && !joystick->buttons)) {
         SDL_CloseJoystick(joystick);
         SDL_UnlockJoysticks();
         return NULL;
@@ -1324,6 +1330,16 @@ int SDL_GetNumJoystickHats(SDL_Joystick *joystick)
     return retval;
 }
 
+/*
+ * Get the number of trackballs on a joystick
+ */
+int SDL_GetNumJoystickBalls(SDL_Joystick *joystick)
+{
+    CHECK_JOYSTICK_MAGIC(joystick, -1);
+
+    return joystick->nballs;
+}
+
 /*
  * Get the number of buttons on a joystick
  */
@@ -1414,6 +1430,31 @@ Uint8 SDL_GetJoystickHat(SDL_Joystick *joystick, int hat)
     return state;
 }
 
+/*
+ * Get the ball axis change since the last poll
+ */
+int SDL_GetJoystickBall(SDL_Joystick *joystick, int ball, int *dx, int *dy)
+{
+    int retval;
+
+    CHECK_JOYSTICK_MAGIC(joystick, -1);
+
+    retval = 0;
+    if (ball < joystick->nballs) {
+        if (dx) {
+            *dx = joystick->balls[ball].dx;
+        }
+        if (dy) {
+            *dy = joystick->balls[ball].dy;
+        }
+        joystick->balls[ball].dx = 0;
+        joystick->balls[ball].dy = 0;
+    } else {
+        return SDL_SetError("Joystick only has %d balls", joystick->nballs);
+    }
+    return retval;
+}
+
 /*
  * Get the current state of a button on a joystick
  */
@@ -1781,6 +1822,7 @@ void SDL_CloseJoystick(SDL_Joystick *joystick)
         SDL_free(joystick->path);
         SDL_free(joystick->serial);
         SDL_free(joystick->axes);
+        SDL_free(joystick->balls);
         SDL_free(joystick->hats);
         SDL_free(joystick->buttons);
         for (i = 0; i < joystick->ntouchpads; i++) {
@@ -2101,6 +2143,41 @@ int SDL_SendJoystickAxis(Uint64 timestamp, SDL_Joystick *joystick, Uint8 axis, S
     return posted;
 }
 
+int SDL_SendJoystickBall(Uint64 timestamp, SDL_Joystick *joystick, Uint8 ball, Sint16 xrel, Sint16 yrel)
+{
+    int posted;
+
+    SDL_AssertJoysticksLocked();
+
+    /* Make sure we're not getting garbage events */
+    if (ball >= joystick->nballs) {
+        return 0;
+    }
+
+    /* We ignore events if we don't have keyboard focus. */
+    if (SDL_PrivateJoystickShouldIgnoreEvent()) {
+        return 0;
+    }
+
+    /* Update internal mouse state */
+    joystick->balls[ball].dx += xrel;
+    joystick->balls[ball].dy += yrel;
+
+    /* Post the event, if desired */
+    posted = 0;
+    if (SDL_EventEnabled(SDL_EVENT_JOYSTICK_BALL_MOTION)) {
+        SDL_Event event;
+        event.type = SDL_EVENT_JOYSTICK_BALL_MOTION;
+        event.common.timestamp = timestamp;
+        event.jball.which = joystick->instance_id;
+        event.jball.ball = ball;
+        event.jball.xrel = xrel;
+        event.jball.yrel = yrel;
+        posted = SDL_PushEvent(&event) == 1;
+    }
+    return posted;
+}
+
 int SDL_SendJoystickHat(Uint64 timestamp, SDL_Joystick *joystick, Uint8 hat, Uint8 value)
 {
     int posted;
@@ -2310,6 +2387,7 @@ void SDL_UpdateJoysticks(void)
 
 static const Uint32 SDL_joystick_event_list[] = {
     SDL_EVENT_JOYSTICK_AXIS_MOTION,
+    SDL_EVENT_JOYSTICK_BALL_MOTION,
     SDL_EVENT_JOYSTICK_HAT_MOTION,
     SDL_EVENT_JOYSTICK_BUTTON_DOWN,
     SDL_EVENT_JOYSTICK_BUTTON_UP,
diff --git a/src/joystick/SDL_joystick_c.h b/src/joystick/SDL_joystick_c.h
index 0f00d180b68b0..fd50467cbedde 100644
--- a/src/joystick/SDL_joystick_c.h
+++ b/src/joystick/SDL_joystick_c.h
@@ -161,18 +161,13 @@ extern void SDL_PrivateJoystickAdded(SDL_JoystickID instance_id);
 extern SDL_bool SDL_IsJoystickBeingAdded(void);
 extern void SDL_PrivateJoystickRemoved(SDL_JoystickID instance_id);
 extern void SDL_PrivateJoystickForceRecentering(SDL_Joystick *joystick);
-extern int SDL_SendJoystickAxis(Uint64 timestamp, SDL_Joystick *joystick,
-                                   Uint8 axis, Sint16 value);
-extern int SDL_SendJoystickHat(Uint64 timestamp, SDL_Joystick *joystick,
-                                  Uint8 hat, Uint8 value);
-extern int SDL_SendJoystickButton(Uint64 timestamp, SDL_Joystick *joystick,
-                                     Uint8 button, Uint8 state);
-extern int SDL_SendJoystickTouchpad(Uint64 timestamp, SDL_Joystick *joystick,
-                                       int touchpad, int finger, Uint8 state, float x, float y, float pressure);
-extern int SDL_SendJoystickSensor(Uint64 timestamp, SDL_Joystick *joystick,
-                                     SDL_SensorType type, Uint64 sensor_timestamp, const float *data, int num_values);
-extern void SDL_SendJoystickBatteryLevel(SDL_Joystick *joystick,
-                                            SDL_JoystickPowerLevel ePowerLevel);
+extern int SDL_SendJoystickAxis(Uint64 timestamp, SDL_Joystick *joystick, Uint8 axis, Sint16 value);
+extern int SDL_SendJoystickBall(Uint64 timestamp, SDL_Joystick *joystick, Uint8 ball, Sint16 xrel, Sint16 yrel);
+extern int SDL_SendJoystickHat(Uint64 timestamp, SDL_Joystick *joystick, Uint8 hat, Uint8 value);
+extern int SDL_SendJoystickButton(Uint64 timestamp, SDL_Joystick *joystick, Uint8 button, Uint8 state);
+extern int SDL_SendJoystickTouchpad(Uint64 timestamp, SDL_Joystick *joystick, int touchpad, int finger, Uint8 state, float x, float y, float pressure);
+extern int SDL_SendJoystickSensor(Uint64 timestamp, SDL_Joystick *joystick, SDL_SensorType type, Uint64 sensor_timestamp, const float *data, int num_values);
+extern void SDL_SendJoystickBatteryLevel(SDL_Joystick *joystick, SDL_JoystickPowerLevel ePowerLevel);
 
 /* Function to get the Steam virtual gamepad info for a joystick */
 extern const struct SDL_SteamVirtualGamepadInfo *SDL_GetJoystickInstanceVirtualGamepadInfo(SDL_JoystickID instance_id);
diff --git a/src/joystick/SDL_sysjoystick.h b/src/joystick/SDL_sysjoystick.h
index d3d8e71de5c58..68eb038e0ef90 100644
--- a/src/joystick/SDL_sysjoystick.h
+++ b/src/joystick/SDL_sysjoystick.h
@@ -32,6 +32,7 @@ extern "C" {
 #endif
 
 /* The SDL joystick structure */
+
 typedef struct SDL_JoystickAxisInfo
 {
     Sint16 initial_value;           /* Initial axis state */
@@ -43,6 +44,12 @@ typedef struct SDL_JoystickAxisInfo
     SDL_bool sending_initial_value; /* Whether we are sending the initial axis value */
 } SDL_JoystickAxisInfo;
 
+typedef struct SDL_JoystickBallData
+{
+    int dx;
+    int dy;
+} SDL_JoystickBallData;
+
 typedef struct SDL_JoystickTouchpadFingerInfo
 {
     Uint8 state;
@@ -82,6 +89,9 @@ struct SDL_Joystick
     int naxes _guarded; /* Number of axis controls on the joystick */
     SDL_JoystickAxisInfo *axes _guarded;
 
+    int nballs _guarded; /* Number of trackballs on the joystick */
+    SDL_JoystickBallData *balls _guarded; /* Current ball motion deltas */
+
     int nhats _guarded;   /* Number of hats on the joystick */
     Uint8 *hats _guarded; /* Current hat states */
 
diff --git a/src/joystick/linux/SDL_sysjoystick.c b/src/joystick/linux/SDL_sysjoystick.c
index bf1304971f962..5de8b758177a8 100644
--- a/src/joystick/linux/SDL_sysjoystick.c
+++ b/src/joystick/linux/SDL_sysjoystick.c
@@ -1147,6 +1147,16 @@ static SDL_JoystickID LINUX_JoystickGetDeviceInstanceID(int device_index)
     return GetJoystickByDevIndex(device_index)->device_instance;
 }
 
+static int allocate_balldata(SDL_Joystick *joystick)
+{
+    joystick->hwdata->balls =
+        (struct hwdata_ball *)SDL_calloc(joystick->nballs, sizeof(struct hwdata_ball));
+    if (joystick->hwdata->balls == NULL) {
+        return -1;
+    }
+    return 0;
+}
+
 static int allocate_hatdata(SDL_Joystick *joystick)
 {
     int i;
@@ -1319,6 +1329,9 @@ static void ConfigJoystick(SDL_Joystick *joystick, int fd, int fd_sensor)
                 ++joystick->naxes;
             }
         }
+        if (test_bit(REL_X, relbit) || test_bit(REL_Y, relbit)) {
+            ++joystick->nballs;
+        }
 
     } else if ((ioctl(fd, JSIOCGBUTTONS, &key_pam_size, sizeof(key_pam_size)) >= 0) &&
                (ioctl(fd, JSIOCGAXES, &abs_pam_size, sizeof(abs_pam_size)) >= 0)) {
@@ -1426,6 +1439,11 @@ static void ConfigJoystick(SDL_Joystick *joystick, int fd, int fd_sensor)
     }
 
     /* Allocate data to keep track of these thingamajigs */
+    if (joystick->nballs > 0) {
+        if (allocate_balldata(joystick) < 0) {
+            joystick->nballs = 0;
+        }
+    }
     if (joystick->nhats > 0) {
         if (allocate_hatdata(joystick) < 0) {
             joystick->nhats = 0;
@@ -1754,6 +1772,11 @@ static void HandleHat(Uint64 timestamp, SDL_Joystick *stick, int hatidx, int axi
     }
 }
 
+static void HandleBall(SDL_Joystick *stick, Uint8 ball, int axis, int value)
+{
+    stick->hwdata->balls[ball].axis[axis] += value;
+}
+
 static int AxisCorrect(SDL_Joystick *joystick, int which, int value)
 {
     struct axis_correct *correct;
@@ -1844,6 +1867,8 @@ static void PollAllValues(Uint64 timestamp, SDL_Joystick *joystick)
             }
         }
     }
+
+    /* Joyballs are relative input, so there's no poll state. Events only! */
 }
 
 static void PollAllSensors(Uint64 timestamp, SDL_Joystick *joystick)
@@ -1952,6 +1977,17 @@ static void HandleInputEvents(SDL_Joystick *joystick)
                     break;
                 }
                 break;
+            case EV_REL:
+                switch (code) {
+                case REL_X:
+                case REL_Y:
+                    code -= REL_X;
+                    HandleBall(joystick, code / 2, code % 2, event->value);
+                    break;
+                default:
+                    break;
+                }
+                break;
             case EV_SYN:
                 switch (code) {
                 case SYN_DROPPED:
@@ -2121,6 +2157,8 @@ static void HandleClassicEvents(SDL_Joystick *joystick)
 
 static void LINUX_JoystickUpdate(SDL_Joystick *joystick)
 {
+    int i;
+
     SDL_AssertJoysticksLocked();
 
     if (joystick->hwdata->m_bSteamController) {
@@ -2133,6 +2171,19 @@ static void LINUX_JoystickUpdate(SDL_Joystick *joystick)
     } else {
         HandleInputEvents(joystick);
     }
+
+    /* Deliver ball motion updates */
+    for (i = 0; i < joystick->nballs; ++i) {
+        int xrel, yrel;
+
+        xrel = joystick->hwdata->balls[i].axis[0];
+        yrel = joystick->hwdata->balls[i].axis[1];
+        if (xrel || yrel) {
+            joystick->hwdata->balls[i].axis[0] = 0;
+            joystick->hwdata->balls[i].axis[1] = 0;
+            SDL_SendJoystickBall(0, joystick, (Uint8)i, xrel, yrel);
+        }
+    }
 }
 
 /* Function to close a joystick after use */
@@ -2160,6 +2211,7 @@ static void LINUX_JoystickClose(SDL_Joystick *joystick)
         SDL_free(joystick->hwdata->key_pam);
         SDL_free(joystick->hwdata->abs_pam);
         SDL_free(joystick->hwdata->hats);
+        SDL_free(joystick->hwdata->balls);
         SDL_free(joystick->hwdata->fname);
         SDL_free(joystick->hwdata);
     }
diff --git a/src/joystick/linux/SDL_sysjoystick_c.h b/src/joystick/linux/SDL_sysjoystick_c.h
index 98bee2042583d..8e441c0a9aa70 100644
--- a/src/joystick/linux/SDL_sysjoystick_c.h
+++ b/src/joystick/linux/SDL_sysjoystick_c.h
@@ -43,6 +43,12 @@ struct joystick_hwdata
     struct ff_effect effect;
     Uint32 effect_expiration;
 
+    /* The current Linux joystick driver maps balls to two axes */
+    struct hwdata_ball
+    {
+        int axis[2];
+    } *balls;
+
     /* The current Linux joystick driver maps hats to two axes */
     struct hwdata_hat
     {
diff --git a/src/test/SDL_test_common.c b/src/test/SDL_test_common.c
index 952ac60bc1768..c447e14f1e433 100644
--- a/src/test/SDL_test_common.c
+++ b/src/test/SDL_test_common.c
@@ -1719,6 +1719,11 @@ static void SDLTest_PrintEvent(const SDL_Event *event)
         SDL_Log("SDL EVENT: Joystick %" SDL_PRIu32 " removed",
                 event->jdevice.which);
         break;
+    case SDL_EVENT_JOYSTICK_BALL_MOTION:
+        SDL_Log("SDL EVENT: Joystick %" SDL_PRIs32 ": ball %d moved by %d,%d",
+                event->jball.which, event->jball.ball, event->jball.xrel,
+                event->jball.yrel);
+        break;
     case SDL_EVENT_JOYSTICK_HAT_MOTION:
     {
         const char *position = "UNKNOWN";
diff --git a/test/testautomation_joystick.c b/test/testautomation_joystick.c
index 4f191047e1d51..b78770fee9464 100644
--- a/test/testautomation_joystick.c
+++ b/test/testautomation_joystick.c
@@ -50,6 +50,7 @@ static int TestVirtualJoystick(void *arg)
             SDLTest_AssertCheck(SDL_GetJoystickSerial(joystick) == NULL, "SDL_GetJoystickSerial()");
             SDLTest_AssertCheck(SDL_GetJoystickType(joystick) == desc.type, "SDL_GetJoystickType()");
             SDLTest_AssertCheck(SDL_GetNumJoystickAxes(joystick) == desc.naxes, "SDL_GetNumJoystickAxes()");
+            SDLTest_AssertCheck(SDL_GetNumJoystickBalls(joystick) == 0, "SDL_GetNumJoystickBalls()");
             SDLTest_AssertCheck(SDL_GetNumJoystickHats(joystick) == desc.nhats, "SDL_GetNumJoystickHats()");
             SDLTest_AssertCheck(SDL_GetNumJoystickButtons(joystick) == desc.nbuttons, "SDL_GetNumJoystickButtons()");