SDL: First pass at extending virtual controller functionality

From 94eeb587c1969bcfea79303273895619e9edd3c2 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Sun, 15 May 2022 20:01:12 -0700
Subject: [PATCH] First pass at extending virtual controller functionality

Added the ability to specify a name and the product VID/PID for a virtual controller

Also added a test case to testgamecontroller, if you pass --virtual as a parameter
---
 include/SDL_joystick.h                       |  38 +++
 src/dynapi/SDL_dynapi_overrides.h            |   1 +
 src/dynapi/SDL_dynapi_procs.h                |   1 +
 src/joystick/SDL_gamecontroller.c            | 102 +-----
 src/joystick/SDL_joystick.c                  |  16 +-
 src/joystick/SDL_joystick_c.h                |   5 +
 src/joystick/virtual/SDL_virtualjoystick.c   | 308 ++++++++++++++-----
 src/joystick/virtual/SDL_virtualjoystick_c.h |  15 +-
 test/testgamecontroller.c                    | 230 ++++++++++++--
 9 files changed, 511 insertions(+), 205 deletions(-)

diff --git a/include/SDL_joystick.h b/include/SDL_joystick.h
index 1aa130f1a32..5231d023abe 100644
--- a/include/SDL_joystick.h
+++ b/include/SDL_joystick.h
@@ -348,6 +348,44 @@ extern DECLSPEC int SDLCALL SDL_JoystickAttachVirtual(SDL_JoystickType type,
                                                       int nbuttons,
                                                       int nhats);
 
+/**
+ * The structure that defines an extended virtual joystick description
+ *
+ * The caller must zero the structure and then initialize the version with `SDL_VIRTUAL_JOYSTICK_DESC_VERSION` before passing it to SDL_JoystickAttachVirtualEx()
+ *
+ * \sa SDL_JoystickAttachVirtualEx
+ */
+typedef struct SDL_VirtualJoystickDesc
+{
+    Uint16 version;     /**< `SDL_VIRTUAL_JOYSTICK_DESC_VERSION` */
+    Uint16 type;        /**< `SDL_JoystickType` */
+    Uint16 naxes;       /**< the number of axes on this joystick */
+    Uint16 nbuttons;    /**< the number of buttons on this joystick */
+    Uint16 nhats;       /**< the number of hats on this joystick */
+    Uint16 vendor_id;   /**< the USB vendor ID of this joystick */
+    Uint16 product_id;  /**< the USB product ID of this joystick */
+    Uint16 padding;     /**< unused */
+    const char *name;   /**< the name of the joystick */
+
+    void *userdata;     /**< User data pointer passed to callbacks */
+    void (*Update)(void *userdata); /**< Called when the joystick state should be updated */
+
+} SDL_VirtualJoystickDesc;
+
+/**
+ * \brief The current version of the SDL_VirtualJoystickDesc structure
+ */
+#define SDL_VIRTUAL_JOYSTICK_DESC_VERSION   1
+
+/**
+ * Attach a new virtual joystick with extended properties.
+ *
+ * \returns the joystick's device index, or -1 if an error occurred.
+ *
+ * \since This function is available since SDL 2.24.0.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickAttachVirtualEx(const SDL_VirtualJoystickDesc *desc);
+
 /**
  * Detach a virtual joystick.
  *
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index 90b2254edfb..15577b51e4b 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -870,3 +870,4 @@
 #define SDL_GameControllerPath SDL_GameControllerPath_REAL
 #define SDL_JoystickPathForIndex SDL_JoystickPathForIndex_REAL
 #define SDL_JoystickPath SDL_JoystickPath_REAL
+#define SDL_JoystickAttachVirtualEx SDL_JoystickAttachVirtualEx_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index f78c0e4236b..45ee57547b2 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -941,3 +941,4 @@ SDL_DYNAPI_PROC(const char*,SDL_GameControllerPathForIndex,(int a),(a),return)
 SDL_DYNAPI_PROC(const char*,SDL_GameControllerPath,(SDL_GameController *a),(a),return)
 SDL_DYNAPI_PROC(const char*,SDL_JoystickPathForIndex,(int a),(a),return)
 SDL_DYNAPI_PROC(const char*,SDL_JoystickPath,(SDL_Joystick *a),(a),return)
+SDL_DYNAPI_PROC(int,SDL_JoystickAttachVirtualEx,(const SDL_VirtualJoystickDesc *a),(a),return)
diff --git a/src/joystick/SDL_gamecontroller.c b/src/joystick/SDL_gamecontroller.c
index 50eb190a53c..f0f932e5174 100644
--- a/src/joystick/SDL_gamecontroller.c
+++ b/src/joystick/SDL_gamecontroller.c
@@ -464,100 +464,6 @@ static int SDLCALL SDL_GameControllerEventWatcher(void *userdata, SDL_Event * ev
     return 1;
 }
 
-/*
- * Helper function to guess at a mapping for virtual controllers
- */
-static ControllerMapping_t *SDL_CreateMappingForVirtualController(SDL_JoystickGUID guid)
-{
-    const int face_button_mask = ((1 << SDL_CONTROLLER_BUTTON_A) |
-                                  (1 << SDL_CONTROLLER_BUTTON_B) |
-                                  (1 << SDL_CONTROLLER_BUTTON_X) |
-                                  (1 << SDL_CONTROLLER_BUTTON_Y));
-    SDL_bool existing;
-    char mapping_string[1024];
-    int button_mask;
-    int axis_mask;
-
-    button_mask = SDL_SwapLE16(*(Uint16*)(&guid.data[sizeof(guid.data)-4]));
-    axis_mask = SDL_SwapLE16(*(Uint16*)(&guid.data[sizeof(guid.data)-2]));
-    if (!button_mask && !axis_mask) {
-        return NULL;
-    }
-    if (!(button_mask & face_button_mask)) {
-        /* We don't know what buttons or axes are supported, don't make up a mapping */
-        return NULL;
-    }
-
-    SDL_strlcpy(mapping_string, "none,*,", sizeof(mapping_string));
-
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_A)) {
-        SDL_strlcat(mapping_string, "a:b0,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_B)) {
-        SDL_strlcat(mapping_string, "b:b1,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_X)) {
-        SDL_strlcat(mapping_string, "x:b2,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_Y)) {
-        SDL_strlcat(mapping_string, "y:b3,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_BACK)) {
-        SDL_strlcat(mapping_string, "back:b4,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_GUIDE)) {
-        SDL_strlcat(mapping_string, "guide:b5,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_START)) {
-        SDL_strlcat(mapping_string, "start:b6,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_LEFTSTICK)) {
-        SDL_strlcat(mapping_string, "leftstick:b7,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_RIGHTSTICK)) {
-        SDL_strlcat(mapping_string, "rightstick:b8,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_LEFTSHOULDER)) {
-        SDL_strlcat(mapping_string, "leftshoulder:b9,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_RIGHTSHOULDER)) {
-        SDL_strlcat(mapping_string, "rightshoulder:b10,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_DPAD_UP)) {
-        SDL_strlcat(mapping_string, "dpup:b11,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_DPAD_DOWN)) {
-        SDL_strlcat(mapping_string, "dpdown:b12,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_DPAD_LEFT)) {
-        SDL_strlcat(mapping_string, "dpleft:b13,", sizeof(mapping_string));
-    }
-    if (button_mask & (1 << SDL_CONTROLLER_BUTTON_DPAD_RIGHT)) {
-        SDL_strlcat(mapping_string, "dpright:b14,", sizeof(mapping_string));
-    }
-    if (axis_mask & (1 << SDL_CONTROLLER_AXIS_LEFTX)) {
-        SDL_strlcat(mapping_string, "leftx:a0,", sizeof(mapping_string));
-    }
-    if (axis_mask & (1 << SDL_CONTROLLER_AXIS_LEFTY)) {
-        SDL_strlcat(mapping_string, "lefty:a1,", sizeof(mapping_string));
-    }
-    if (axis_mask & (1 << SDL_CONTROLLER_AXIS_RIGHTX)) {
-        SDL_strlcat(mapping_string, "rightx:a2,", sizeof(mapping_string));
-    }
-    if (axis_mask & (1 << SDL_CONTROLLER_AXIS_RIGHTY)) {
-        SDL_strlcat(mapping_string, "righty:a3,", sizeof(mapping_string));
-    }
-    if (axis_mask & (1 << SDL_CONTROLLER_AXIS_TRIGGERLEFT)) {
-        SDL_strlcat(mapping_string, "lefttrigger:a4,", sizeof(mapping_string));
-    }
-    if (axis_mask & (1 << SDL_CONTROLLER_AXIS_TRIGGERRIGHT)) {
-        SDL_strlcat(mapping_string, "righttrigger:a5,", sizeof(mapping_string));
-    }
-
-    return SDL_PrivateAddMappingForGUID(guid, mapping_string,
-                      &existing, SDL_CONTROLLER_MAPPING_PRIORITY_DEFAULT);
-}
-
 #ifdef __ANDROID__
 /*
  * Helper function to guess at a mapping based on the elements reported for this controller
@@ -790,9 +696,6 @@ static ControllerMapping_t *SDL_PrivateGetControllerMappingForGUID(SDL_JoystickG
             return s_pXInputMapping;
         }
 #endif
-        if (!mapping && SDL_IsJoystickVirtual(guid)) {
-            mapping = SDL_CreateMappingForVirtualController(guid);
-        }
 #ifdef __ANDROID__
         if (!mapping && !SDL_IsJoystickHIDAPI(guid)) {
             mapping = SDL_CreateMappingForAndroidController(guid);
@@ -1355,6 +1258,11 @@ static ControllerMapping_t *SDL_PrivateGenerateAutomaticControllerMapping(const
     SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "dpdown", &raw_map->dpdown);
     SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "dpleft", &raw_map->dpleft);
     SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "dpright", &raw_map->dpright);
+    SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "misc1", &raw_map->misc1);
+    SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "paddle1", &raw_map->paddle1);
+    SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "paddle2", &raw_map->paddle2);
+    SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "paddle3", &raw_map->paddle3);
+    SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "paddle4", &raw_map->paddle4);
     SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "leftx", &raw_map->leftx);
     SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "lefty", &raw_map->lefty);
     SDL_PrivateAppendToMappingString(mapping, sizeof(mapping), "rightx", &raw_map->rightx);
diff --git a/src/joystick/SDL_joystick.c b/src/joystick/SDL_joystick.c
index e5ec5b7f855..914f29e2d8e 100644
--- a/src/joystick/SDL_joystick.c
+++ b/src/joystick/SDL_joystick.c
@@ -522,9 +522,23 @@ SDL_JoystickOpen(int device_index)
 int
 SDL_JoystickAttachVirtual(SDL_JoystickType type,
                           int naxes, int nbuttons, int nhats)
+{
+    SDL_VirtualJoystickDesc desc;
+
+    SDL_zero(desc);
+    desc.version = SDL_VIRTUAL_JOYSTICK_DESC_VERSION;
+    desc.type = (Uint16)type;
+    desc.naxes = (Uint16)naxes;
+    desc.nbuttons = (Uint16)nbuttons;
+    desc.nhats = (Uint16)nhats;
+    return SDL_JoystickAttachVirtualEx(&desc);
+}
+
+int
+SDL_JoystickAttachVirtualEx(const SDL_VirtualJoystickDesc *desc)
 {
 #if SDL_JOYSTICK_VIRTUAL
-    return SDL_JoystickAttachVirtualInner(type, naxes, nbuttons, nhats);
+    return SDL_JoystickAttachVirtualInner(desc);
 #else
     return SDL_SetError("SDL not built with virtual-joystick support");
 #endif
diff --git a/src/joystick/SDL_joystick_c.h b/src/joystick/SDL_joystick_c.h
index 312e113375f..a7aa6b7145d 100644
--- a/src/joystick/SDL_joystick_c.h
+++ b/src/joystick/SDL_joystick_c.h
@@ -168,6 +168,11 @@ typedef struct _SDL_GamepadMapping
     SDL_InputMapping dpdown;
     SDL_InputMapping dpleft;
     SDL_InputMapping dpright;
+    SDL_InputMapping misc1;
+    SDL_InputMapping paddle1;
+    SDL_InputMapping paddle2;
+    SDL_InputMapping paddle3;
+    SDL_InputMapping paddle4;
     SDL_InputMapping leftx;
     SDL_InputMapping lefty;
     SDL_InputMapping rightx;
diff --git a/src/joystick/virtual/SDL_virtualjoystick.c b/src/joystick/virtual/SDL_virtualjoystick.c
index 9be879b30aa..a2c290b422a 100644
--- a/src/joystick/virtual/SDL_virtualjoystick.c
+++ b/src/joystick/virtual/SDL_virtualjoystick.c
@@ -56,18 +56,6 @@ VIRTUAL_FreeHWData(joystick_hwdata *hwdata)
     if (!hwdata) {
         return;
     }
-    if (hwdata->axes) {
-        SDL_free((void *)hwdata->axes);
-        hwdata->axes = NULL;
-    }
-    if (hwdata->buttons) {
-        SDL_free((void *)hwdata->buttons);
-        hwdata->buttons = NULL;
-    }
-    if (hwdata->hats) {
-        SDL_free(hwdata->hats);
-        hwdata->hats = NULL;
-    }
 
     /* Remove hwdata from SDL-global list */
     while (cur) {
@@ -83,81 +71,103 @@ VIRTUAL_FreeHWData(joystick_hwdata *hwdata)
         cur = cur->next;
     }
 
+    if (hwdata->name) {
+        SDL_free(hwdata->name);
+        hwdata->name = NULL;
+    }
+    if (hwdata->axes) {
+        SDL_free((void *)hwdata->axes);
+        hwdata->axes = NULL;
+    }
+    if (hwdata->buttons) {
+        SDL_free((void *)hwdata->buttons);
+        hwdata->buttons = NULL;
+    }
+    if (hwdata->hats) {
+        SDL_free(hwdata->hats);
+        hwdata->hats = NULL;
+    }
     SDL_free(hwdata);
 }
 
 
 int
-SDL_JoystickAttachVirtualInner(SDL_JoystickType type,
-                               int naxes,
-                               int nbuttons,
-                               int nhats)
+SDL_JoystickAttachVirtualInner(const SDL_VirtualJoystickDesc *desc)
 {
     joystick_hwdata *hwdata = NULL;
     int device_index = -1;
-    const Uint16 vendor_id = 0;
-    const Uint16 product_id = 0;
+    const char *name = NULL;
     Uint16 button_mask = 0;
     Uint16 axis_mask = 0;
     Uint16 *guid16;
 
+    if (!desc) {
+        return SDL_InvalidParamError("desc");
+    }
+    if (desc->version != SDL_VIRTUAL_JOYSTICK_DESC_VERSION) {
+        /* Is this an old version that we can support? */
+        return SDL_SetError("Unsupported virtual joystick description version %d", desc->version);
+    }
+
     hwdata = SDL_calloc(1, sizeof(joystick_hwdata));
     if (!hwdata) {
         VIRTUAL_FreeHWData(hwdata);
         return SDL_OutOfMemory();
     }
+    SDL_memcpy(&hwdata->desc, desc, sizeof(*desc));
+
+    if (desc->name) {
+        name = desc->name;
+    } else {
+        switch (desc->type) {
+        case SDL_JOYSTICK_TYPE_GAMECONTROLLER:
+            name = "Virtual Controller";
+            break;
+        case SDL_JOYSTICK_TYPE_WHEEL:
+            name = "Virtual Wheel";
+            break;
+        case SDL_JOYSTICK_TYPE_ARCADE_STICK:
+            name = "Virtual Arcade Stick";
+            break;
+        case SDL_JOYSTICK_TYPE_FLIGHT_STICK:
+            name = "Virtual Flight Stick";
+            break;
+        case SDL_JOYSTICK_TYPE_DANCE_PAD:
+            name = "Virtual Dance Pad";
+            break;
+        case SDL_JOYSTICK_TYPE_GUITAR:
+            name = "Virtual Guitar";
+            break;
+        case SDL_JOYSTICK_TYPE_DRUM_KIT:
+            name = "Virtual Drum Kit";
+            break;
+        case SDL_JOYSTICK_TYPE_ARCADE_PAD:
+            name = "Virtual Arcade Pad";
+            break;
+        case SDL_JOYSTICK_TYPE_THROTTLE:
+            name = "Virtual Throttle";
+            break;
+        default:
+            name = "Virtual Joystick";
+            break;
+        }
+    }
+    hwdata->name = SDL_strdup(name);
 
-    hwdata->naxes = naxes;
-    hwdata->nbuttons = nbuttons;
-    hwdata->nhats = nhats;
-
-    switch (type) {
-    case SDL_JOYSTICK_TYPE_GAMECONTROLLER:
-        hwdata->name = "Virtual Controller";
-        break;
-    case SDL_JOYSTICK_TYPE_WHEEL:
-        hwdata->name = "Virtual Wheel";
-        break;
-    case SDL_JOYSTICK_TYPE_ARCADE_STICK:
-        hwdata->name = "Virtual Arcade Stick";
-        break;
-    case SDL_JOYSTICK_TYPE_FLIGHT_STICK:
-        hwdata->name = "Virtual Flight Stick";
-        break;
-    case SDL_JOYSTICK_TYPE_DANCE_PAD:
-        hwdata->name = "Virtual Dance Pad";
-        break;
-    case SDL_JOYSTICK_TYPE_GUITAR:
-        hwdata->name = "Virtual Guitar";
-        break;
-    case SDL_JOYSTICK_TYPE_DRUM_KIT:
-        hwdata->name = "Virtual Drum Kit";
-        break;
-    case SDL_JOYSTICK_TYPE_ARCADE_PAD:
-        hwdata->name = "Virtual Arcade Pad";
-        break;
-    case SDL_JOYSTICK_TYPE_THROTTLE:
-        hwdata->name = "Virtual Throttle";
-        break;
-    default:
-        hwdata->name = "Virtual Joystick";
-        break;
-    }
-
-    if (type == SDL_JOYSTICK_TYPE_GAMECONTROLLER) {
+    if (desc->type == SDL_JOYSTICK_TYPE_GAMECONTROLLER) {
         int i;
 
-        if (naxes >= 2) {
+        if (desc->naxes >= 2) {
             axis_mask |= ((1 << SDL_CONTROLLER_AXIS_LEFTX) | (1 << SDL_CONTROLLER_AXIS_LEFTY));
         }
-        if (naxes >= 4) {
+        if (desc->naxes >= 4) {
             axis_mask |= ((1 << SDL_CONTROLLER_AXIS_RIGHTX) | (1 << SDL_CONTROLLER_AXIS_RIGHTY));
         }
-        if (naxes >= 6) {
+        if (desc->naxes >= 6) {
             axis_mask |= ((1 << SDL_CONTROLLER_AXIS_TRIGGERLEFT) | (1 << SDL_CONTROLLER_AXIS_TRIGGERRIGHT));
         }
 
-        for (i = 0; i < nbuttons && i < sizeof(Uint16)*8; ++i) {
+        for (i = 0; i < desc->nbuttons && i < sizeof(Uint16)*8; ++i) {
             button_mask |= (1 << i);
         }
     }
@@ -167,34 +177,41 @@ SDL_JoystickAttachVirtualInner(SDL_JoystickType type,
     guid16 = (Uint16 *)hwdata->guid.data;
     *guid16++ = SDL_SwapLE16(SDL_HARDWARE_BUS_VIRTUAL);
     *guid16++ = 0;
-    *guid16++ = SDL_SwapLE16(vendor_id);
+    *guid16++ = SDL_SwapLE16(desc->vendor_id);
     *guid16++ = 0;
-    *guid16++ = SDL_SwapLE16(product_id);
+    *guid16++ = SDL_SwapLE16(desc->product_id);
     *guid16++ = 0;
     *guid16++ = SDL_SwapLE16(button_mask);
     *guid16++ = SDL_SwapLE16(axis_mask);
 
     /* Note that this is a Virtual device and what subtype it is */
     hwdata->guid.data[14] = 'v';
-    hwdata->guid.data[15] = (Uint8)type;
+    hwdata->guid.data[15] = (Uint8)desc->type;
 
     /* Allocate fields for different control-types */
-    if (naxes > 0) {
-        hwdata->axes = SDL_calloc(naxes, sizeof(Sint16));
+    if (desc->naxes > 0) {
+        hwdata->axes = SDL_calloc(desc->naxes, sizeof(Sint16));
         if (!hwdata->axes) {
             VIRTUAL_FreeHWData(hwdata);
             return SDL_OutOfMemory();
         }
+
+        /* Trigger axes are at minimum value at rest */
+        if (desc->type == SDL_JOYSTICK_TYPE_GAMECONTROLLER &&
+            desc->naxes > SDL_CONTROLLER_AXIS_TRIGGERRIGHT) {
+            hwdata->axes[SDL_CONTROLLER_AXIS_TRIGGERLEFT] = SDL_JOYSTICK_AXIS_MIN;
+            hwdata->axes[SDL_CONTROLLER_AXIS_TRIGGERRIGHT] = SDL_JOYSTICK_AXIS_MIN;
+        }
     }
-    if (nbuttons > 0) {
-        hwdata->buttons = SDL_calloc(nbuttons, sizeof(Uint8));
+    if (desc->nbuttons > 0) {
+        hwdata->buttons = SDL_calloc(desc->nbuttons, sizeof(Uint8));
         if (!hwdata->buttons) {
             VIRTUAL_FreeHWData(hwdata);
             return SDL_OutOfMemory();
         }
     }
-    if (nhats > 0) {
-        hwdata->hats = SDL_calloc(nhats, sizeof(Uint8));
+    if (desc->nhats > 0) {
+        hwdata->hats = SDL_calloc(desc->nhats, sizeof(Uint8));
         if (!hwdata->hats) {
             VIRTUAL_FreeHWData(hwdata);
             return SDL_OutOfMemory();
@@ -243,7 +260,7 @@ SDL_JoystickSetVirtualAxisInner(SDL_Joystick *joystick, int axis, Sint16 value)
     }
 
     hwdata = (joystick_hwdata *)joystick->hwdata;
-    if (axis < 0 || axis >= hwdata->naxes) {
+    if (axis < 0 || axis >= hwdata->desc.naxes) {
         SDL_UnlockJoysticks();
         return SDL_SetError("Invalid axis index");
     }
@@ -268,7 +285,7 @@ SDL_JoystickSetVirtualButtonInner(SDL_Joystick *joystick, int button, Uint8 valu
     }
 
     hwdata = (joystick_hwdata *)joystick->hwdata;
-    if (button < 0 || button >= hwdata->nbuttons) {
+    if (button < 0 || button >= hwdata->desc.nbuttons) {
         SDL_UnlockJoysticks();
         return SDL_SetError("Invalid button index");
     }
@@ -293,7 +310,7 @@ SDL_JoystickSetVirtualHatInner(SDL_Joystick *joystick, int hat, Uint8 value)
     }
 
     hwdata = (joystick_hwdata *)joystick->hwdata;
-    if (hat < 0 || hat >= hwdata->nhats) {
+    if (hat < 0 || hat >= hwdata->desc.nhats) {
         SDL_UnlockJoysticks();
         return SDL_SetError("Invalid hat index");
     }
@@ -338,7 +355,7 @@ VIRTUAL_JoystickGetDeviceName(int device_index)
     if (!hwdata) {
         return NULL;
     }
-    return hwdata->name ? hwdata->name : "";
+    return hwdata->name;
 }
 
 
@@ -399,9 +416,9 @@ VIRTUAL_JoystickOpen(SDL_Joystick *joystick, int device_index)
     }
     joystick->instance_id = hwdata->instance_id;
     joystick->hwdata = hwdata;
-    joystick->naxes = hwdata->naxes;
-    joystick->nbuttons = hwdata->nbuttons;
-    joystick->nhats = hwdata->nhats;
+    joystick->naxes = hwdata->desc.naxes;
+    joystick->nbuttons = hwdata->desc.nbuttons;
+    joystick->nhats = hwdata->desc.nhats;
     hwdata->opened = SDL_TRUE;
     return 0;
 }
@@ -461,13 +478,17 @@ VIRTUAL_JoystickUpdate(SDL_Joystick *joystick)
 
     hwdata = (joystick_hwdata *)joystick->hwdata;
 
-    for (i = 0; i < hwdata->naxes; ++i) {
+    if (hwdata->desc.Update) {
+        hwdata->desc.Update(hwdata->desc.userdata);
+    }
+
+    for (i = 0; i < hwdata->desc.naxes; ++i) {
         SDL_PrivateJoystickAxis(joystick, i, hwdata->axes[i]);
     }
-    for (i = 0; i < hwdata->nbuttons; ++i) {
+    for (i = 0; i < hwdata->desc.nbuttons; ++i) {
         SDL_PrivateJoystickButton(joystick, i, hwdata->buttons[i]);
     }
-    for (i = 0; i < hwdata->nhats; ++i) {
+    for (i = 0; i < hwdata->desc.nhats; ++i) {
         SDL_PrivateJoystickHat(joystick, i, hwdata->hats[i]);
     }
 }
@@ -501,7 +522,134 @@ VIRTUAL_JoystickQuit(void)
 static SDL_bool
 VIRTUAL_JoystickGetGamepadMapping(int device_index, SDL_GamepadMapping *out)
 {
-    return SDL_FALSE;
+    joystick_hwdata *hwdata = VIRTUAL_HWDataForIndex(device_index);
+
+    if (hwdata->desc.type != SDL_JOYSTICK_TYPE_GAMECONTROLLER) {
+        return SDL_FALSE;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_A) {
+        out->a.kind = EMappingKind_Button;
+        out->a.target = SDL_CONTROLLER_BUTTON_A;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_B) {
+        out->b.kind = EMappingKind_Button;
+        out->b.target = SDL_CONTROLLER_BUTTON_B;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_X) {
+        out->x.kind = EMappingKind_Button;
+        out->x.target = SDL_CONTROLLER_BUTTON_X;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_Y) {
+        out->y.kind = EMappingKind_Button;
+        out->y.target = SDL_CONTROLLER_BUTTON_Y;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_BACK) {
+        out->back.kind = EMappingKind_Button;
+        out->back.target = SDL_CONTROLLER_BUTTON_BACK;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_GUIDE) {
+        out->guide.kind = EMappingKind_Button;
+        out->guide.target = SDL_CONTROLLER_BUTTON_GUIDE;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_START) {
+        out->start.kind = EMappingKind_Button;
+        out->start.target = SDL_CONTROLLER_BUTTON_START;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_LEFTSTICK) {
+        out->leftstick.kind = EMappingKind_Button;
+        out->leftstick.target = SDL_CONTROLLER_BUTTON_LEFTSTICK;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_RIGHTSTICK) {
+        out->rightstick.kind = EMappingKind_Button;
+        out->rightstick.target = SDL_CONTROLLER_BUTTON_RIGHTSTICK;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_LEFTSHOULDER) {
+        out->leftshoulder.kind = EMappingKind_Button;
+        out->leftshoulder.target = SDL_CONTROLLER_BUTTON_LEFTSHOULDER;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_RIGHTSHOULDER) {
+        out->rightshoulder.kind = EMappingKind_Button;
+        out->rightshoulder.target = SDL_CONTROLLER_BUTTON_RIGHTSHOULDER;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_DPAD_UP) {
+        out->dpup.kind = EMappingKind_Button;
+        out->dpup.target = SDL_CONTROLLER_BUTTON_DPAD_UP;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_DPAD_DOWN) {
+        out->dpdown.kind = EMappingKind_Button;
+        out->dpdown.target = SDL_CONTROLLER_BUTTON_DPAD_DOWN;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_DPAD_LEFT) {
+        out->dpleft.kind = EMappingKind_Button;
+        out->dpleft.target = SDL_CONTROLLER_BUTTON_DPAD_LEFT;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_DPAD_RIGHT) {
+        out->dpright.kind = EMappingKind_Button;
+        out->dpright.target = SDL_CONTROLLER_BUTTON_DPAD_RIGHT;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_MISC1) {
+        out->misc1.kind = EMappingKind_Button;
+        out->misc1.target = SDL_CONTROLLER_BUTTON_MISC1;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_PADDLE1) {
+        out->paddle1.kind = EMappingKind_Button;
+        out->paddle1.target = SDL_CONTROLLER_BUTTON_PADDLE1;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_PADDLE2) {
+        out->paddle2.kind = EMappingKind_Button;
+        out->paddle2.target = SDL_CONTROLLER_BUTTON_PADDLE2;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_PADDLE3) {
+        out->paddle3.kind = EMappingKind_Button;
+        out->paddle3.target = SDL_CONTROLLER_BUTTON_PADDLE3;
+    }
+
+    if (hwdata->desc.nbuttons > SDL_CONTROLLER_BUTTON_PADDLE4) {
+        out->paddle4.kind = EMappingKind_Button;
+        out->paddle4.target = SDL_CONTROLLER_BUTTON_PADDLE4;
+    }
+
+    if (hwdata->desc.naxes > SDL_CONTROLLER_AXIS_LEFTY) {
+        out->leftx.kind = EMappingKind_Axis;
+        out->lefty.kind = EMappingKind_Axis;
+        out->leftx.target = SDL_CONTROLLER_AXIS_LEFTX;
+        out->lefty.target = SDL_CONTROLLER_AXIS_LEFTY;
+    }
+
+    if (hwdata->desc.naxes > SDL_CONTROLLER_AXIS_RIGHTY) {
+        out->rightx.kind = EMappingKind_Axis;
+        out->righty.kind = EMappingKind_Axis;
+        out->rightx.target = SDL_CONTROLLER_AXIS_RIGHTX;
+        out->righty.target = SDL_CONTROLLER_AXIS_RIGHTY;
+    }
+
+    if (hwdata->desc.naxes > SDL_CONTROLLER_AXIS_TRIGGERRIGHT) {
+        out->lefttrigger.kind = EMappingKind_Axis;
+        out->righttrigger.kind = EMappingKind_Axis;
+        out->lefttrigger.target = SDL_CONTROLLER_AXIS_TRIGGERLEFT;
+        out->righttrigger.target = SDL_CONTROLLER_AXIS_TRIGGERRIGHT;
+    }
+
+    return SDL_TRUE;
 }
 
 SDL_JoystickDriver SDL_VIRTUAL_JoystickDriver =
diff --git a/src/joystick/virtual/SDL_virtualjoystick_c.h b/src/joystick/virtual/SDL_virtualjoystick_c.h
index b251c2d14af..a12d6c84a9c 100644
--- a/src/joystick/virtual/SDL_virtualjoystick_c.h
+++ b/src/joystick/virtual/SDL_virtualjoystick_c.h
@@ -34,24 +34,18 @@ typedef struct joystick_hwdata
 {
     SDL_JoystickType type;
     SDL_bool attached;
-    const char *name;
+    char *name;
     SDL_JoystickGUID guid;
-    int naxes;
+    SDL_VirtualJoystickDesc desc;
     Sint16 *axes;
-    int nbuttons;
     Uint8 *buttons;
-    int nhats;
     Uint8 *hats;
     SDL_JoystickID instance_id;
     SDL_bool opened;
     struct joystick_hwdata *next;
 } joystick_hwdata;
 
-int SDL_JoystickAttachVirtualInner(SDL_JoystickType type,
-                                   int naxes,
-                                   int nbuttons,
-                                   int nhats);
-
+int SDL_JoystickAttachVirtualInner(const SDL_VirtualJoystickDesc *desc);
 int SDL_JoystickDetachVirtualInner(int device_index);
 
 int SDL_JoystickSetVirtualAxisInner(SDL_Joystick * joystick, int axis, Sint16 value);
@@ -59,4 +53,7 @@ int SDL_JoystickSetVirtualButtonInner(SDL_Joystick * joystick, int button, Uint8
 int SDL_JoystickSetVirtualHatInner(SDL_Joystick * joystick, int hat, Uint8 value);
 
 #endif  /* SDL_JOYSTICK_VIRTUAL */
+
 #endif  /* SDL_VIRTUALJOYSTICK_C_H */
+
+/* vi: set ts=4 sw=4 expandtab: */
diff --git a/test/testgamecontroller.c b/test/testgamecontroller.c
index 82b6b6c980e..ef0838cb294 100644
--- a/test/testgamecontroller.c
+++ b/test/testgamecontroller.c
@@ -28,6 +28,10 @@
 #define SCREEN_WIDTH    512
 #define SCREEN_HEIGHT   320
 
+#define BUTTON_SIZE     50
+#define AXIS_SIZE       50
+
+
 /* This is indexed by SDL_GameControllerButton. */
 static const struct { int x; int y; } button_positions[] = {
     {387, 167},  /* SDL_CONTROLLER_BUTTON_A */
@@ -50,7 +54,9 @@ static const struct { int x; int y; } button_positions[] = {
     {330, 135},  /* SDL_CONTROLLER_BUTTON_PADDLE2 */
     {132, 175},  /* SDL_CONTROLLER_BUTTON_PADDLE3 */
     {330, 175},  /* SDL_CONTROLLER_BUTTON_PADDLE4 */
+    {0, 0},      /* SDL_CONTROLLER_BUTTON_TOUCHPAD */
 };
+SDL_COMPILE_TIME_ASSERT(button_positions, SDL_arraysize(button_positions) == SDL_CONTROLLER_BUTTON_MAX);
 
 /* This is indexed by SDL_GameControllerAxis. */
 static const struct { int x; int y; double angle; } axis_positions[] = {
@@ -61,6 +67,7 @@ static const struct { int x; int y; double angle; } axis_positions[] = {
     {91,  -20,   0.0},  /* TRIGGERLEFT */
     {375, -20,   0.0},  /* TRIGGERRIGHT */
 };
+SDL_COMPILE_TIME_ASSERT(axis_positions, SDL_arraysize(axis_positions) == SDL_CONTROLLER_AXIS_MAX);
 
 static SDL_Window *window = NULL;
 static SDL_Renderer *screen = NULL;
@@ -72,6 +79,11 @@ static SDL_Texture *background_front, *background_back, *button, *axis;
 static SDL_GameController *gamecontroller;
 static SDL_GameController **gamecontrollers;
 static int num_controllers = 0;
+static SDL_Joystick *virtual_joystick = NULL;
+static SDL_GameControllerAxis virtual_axis_active = SDL_CONTROLLER_AXIS_INVALID;
+static int virtual_axis_start_x;
+static int virtual_axis_start_y;
+static SDL_GameControllerButton virtual_button_active = SDL_CONTROLLER_BUTTON_INVALID;
 
 static void UpdateWindowTitle()
 {
@@ -280,12 +292,170 @@ static void CyclePS5TriggerEffect()
     SDL_GameControllerSendEffect(gamecontroller, &state, sizeof(state));
 }
 
+static SDL_bool ShowingFront()
+{
+    SDL_bool showing_front = SDL_TRUE;
+    int i;
+
+    if (gamecontroller) {
+        /* Show the back of the controller if the paddles are being held */
+        for (i = SDL_CONTROLLER_BUTTON_PADDLE1; i <= SDL_CONTROLLER_BUTTON_PADDLE4; ++i) {
+            if (SDL_GameControllerGetButton(gamecontroller, (SDL_GameControllerButton)i) == SDL_PRESSED) {
+                showing_front = SDL_FALSE;
+                break;
+            }
+        }
+    }
+    if ((SDL_GetModState() & KMOD_SHIFT) != 0) {
+        showing_front = SDL_FALSE;
+    }
+    return showing_front;
+}
+
+static int OpenVirtualController()
+{
+    SDL_VirtualJoystickDesc desc;
+
+    SDL_zero(desc);
+    desc.version = SDL_VIRTUAL_JOYSTICK_DESC_VERSION;
+    desc.type = SDL_JOYSTICK_TYPE_GAMECONTROLLER;
+    desc.naxes = SDL_CONTROLLER_AXIS_MAX;
+    desc.nbuttons = SDL_CONTROLLER_BUTTON_MAX;
+    return SDL_JoystickAttachVirtualEx(&desc);
+}
+
+static SDL_GameControllerButton FindButtonAtPosition(int x, int y)
+{
+    SDL_Point point;
+    int i;
+    SDL_bool showing_front = ShowingFront();
+
+    point.x = x;
+    point.y = y;
+    for (i = 0; i < SDL_CONTROLLER_BUTTON_TOUCHPAD; ++i) {
+        SDL_bool on_front = (i < SDL_CONTROLLER_BUTTON_PADDLE1 || i > SDL_CONTROLLER_BUTTON_PADDLE4);
+        if (on_front == showing_front) {
+            SDL_Rect rect;
+            rect.x = button_positions[i].x;
+            rect.y = button_positions[i].y;
+            rect.w = BUTTON_SIZE;
+            rect.h = BUTTON_SIZE;
+            if (SDL_PointInRect(&point, &rect)) {
+                return (SDL_GameControllerButton)i;
+            }
+        }
+    }
+    return SDL_CONTROLLER_BUTTON_INVALID;
+}
+

(Patch may be truncated, please check the link at the top of this post.)