Browse Source

Fixed bug 5028 - Virtual Joysticks (new joystick backend)

David Ludwig

I have created a new driver for SDL's Joystick and Game-Controller subsystem: a Virtual driver.  This driver allows one to create a software-based joystick, which to SDL applications will look and react like a real joystick, but whose state can be set programmatically.  A primary use case for this is to help enable developers to add touch-screen joysticks to their apps.

The driver comes with a set of new, public APIs, with functions to attach and detach joysticks, set virtual-joystick state, and to determine if a joystick is a virtual-one.

Use of virtual joysticks goes as such:

1. Attach one or more virtual joysticks by calling SDL_JoystickAttachVirtual.  If successful, this returns the virtual-device's joystick-index.
2. Open the virtual joysticks (using indicies returned by SDL_JoystickAttachVirtual).
3. Call any of the SDL_JoystickSetVirtual* functions when joystick-state changes.  Please note that virtual-joystick state will only get applied on the next call to SDL_JoystickUpdate, or when pumping or polling for SDL events (via SDL_PumpEvents or SDL_PollEvent).


Here is a listing of the new, public APIs, at present and subject to change:

------------------------------------------------------------

/**
 * Attaches a new virtual joystick.
 * Returns the joystick's device index, or -1 if an error occurred.
 */
extern DECLSPEC int SDLCALL SDL_JoystickAttachVirtual(SDL_JoystickType type, int naxes, int nballs, int nbuttons, int nhats);

/**
 * Detaches a virtual joystick
 * Returns 0 on success, or -1 if an error occurred.
 */
extern DECLSPEC int SDLCALL SDL_JoystickDetachVirtual(int device_index);

/**
 * Indicates whether or not a virtual-joystick is at a given device index.
 */
extern DECLSPEC SDL_bool SDLCALL SDL_JoystickIsVirtual(int device_index);

/**
 * Set values on an opened, virtual-joystick's controls.
 * Returns 0 on success, -1 on error.
 */
extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualAxis(SDL_Joystick * joystick, int axis, Sint16 value);
extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualBall(SDL_Joystick * joystick, int ball, Sint16 xrel, Sint16 yrel);
extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualButton(SDL_Joystick * joystick, int button, Uint8 value);
extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualHat(SDL_Joystick * joystick, int hat, Uint8 value);

------------------------------------------------------------

Miscellaneous notes on the initial patch, which are also subject to change:

1. no test code is present in SDL, yet.  This should, perhaps, change.  Initial development was done with an ImGui-based app, which potentially is too thick for use in SDL-official.  If tests are to be added, what kind of tests?  Automated?  Graphical?

2. virtual game controllers can be created by calling SDL_JoystickAttachVirtual with a joystick-type of SDL_JOYSTICK_TYPE_GAME_CONTROLLER, with naxes (num axes) set to SDL_CONTROLLER_AXIS_MAX, and with nbuttons (num buttons) set to SDL_CONTROLLER_BUTTON_MAX.  When updating their state, values of type SDL_GameControllerAxis or SDL_GameControllerButton can be casted to an int and used for the control-index (in calls to SDL_JoystickSetVirtual* functions).

3. virtual joysticks' guids are mostly all-zeros with the exception of the last two bytes, the first of which is a 'v', to indicate that the guid is a virtual one, and the second of which is a SDL_JoystickType that has been converted into a Uint8.

4. virtual joysticks are ONLY turned into virtual game-controllers if and when their joystick-type is set to SDL_JOYSTICK_TYPE_GAMECONTROLLER.  This is controlled by having SDL's default list of game-controllers have a single entry for a virtual game controller (of guid, "00000000000000000000000000007601", which is subject to the guid-encoding described above).

5. regarding having to call SDL_JoystickUpdate, either directly or indirectly via SDL_PumpEvents or SDL_PollEvents, before new virtual-joystick state becomes active (as specified via SDL_JoystickSetVirtual* function-calls), this was done to match behavior found in SDL's other joystick drivers, almost all of which will only update SDL-state during SDL_JoystickUpdate.

6. the initial patch is based off of SDL 2.0.12

7. the virtual joystick subsystem is disabled by default.  It should be possible to enable it by building with SDL_JOYSTICK_VIRTUAL=1



Questions, comments, suggestions, or bug reports very welcome!
Sam Lantinga 5 năm trước cách đây
mục cha
commit
2be75c6a61

+ 9 - 0
CMakeLists.txt

@@ -387,6 +387,7 @@ set_option(VIDEO_OFFSCREEN     "Use offscreen video driver" OFF)
 option_string(BACKGROUNDING_SIGNAL "number to use for magic backgrounding signal or 'OFF'" "OFF")
 option_string(FOREGROUNDING_SIGNAL "number to use for magic foregrounding signal or 'OFF'" "OFF")
 set_option(HIDAPI              "Use HIDAPI for low level joystick drivers" ${OPT_DEF_HIDAPI})
+set_option(JOYSTICK_VIRTUAL    "Enable the virtual-joystick driver" ON)
 
 set(SDL_SHARED ${SDL_SHARED_ENABLED_BY_DEFAULT} CACHE BOOL "Build a shared version of the library")
 set(SDL_STATIC ${SDL_STATIC_ENABLED_BY_DEFAULT} CACHE BOOL "Build a static version of the library")
@@ -905,6 +906,14 @@ if(SDL_DLOPEN)
   endif()
 endif()
 
+if(SDL_JOYSTICK)
+  if(JOYSTICK_VIRTUAL)
+    set(SDL_JOYSTICK_VIRTUAL 1)
+    file(GLOB JOYSTICK_VIRTUAL_SOURCES ${SDL2_SOURCE_DIR}/src/joystick/virtual/*.c)
+    set(SOURCE_FILES ${SOURCE_FILES} ${JOYSTICK_VIRTUAL_SOURCES})
+  endif()
+endif()
+
 if(SDL_VIDEO)
   if(VIDEO_DUMMY)
     set(SDL_VIDEO_DRIVER_DUMMY 1)

+ 1 - 0
include/SDL_config.h.cmake

@@ -293,6 +293,7 @@
 #cmakedefine SDL_JOYSTICK_USBHID_MACHINE_JOYSTICK_H @SDL_JOYSTICK_USBHID_MACHINE_JOYSTICK_H@
 #cmakedefine SDL_JOYSTICK_HIDAPI @SDL_JOYSTICK_HIDAPI@
 #cmakedefine SDL_JOYSTICK_EMSCRIPTEN @SDL_JOYSTICK_EMSCRIPTEN@
+#cmakedefine SDL_JOYSTICK_VIRTUAL @SDL_JOYSTICK_VIRTUAL@
 #cmakedefine SDL_HAPTIC_DUMMY @SDL_HAPTIC_DUMMY@
 #cmakedefine SDL_HAPTIC_LINUX @SDL_HAPTIC_LINUX@
 #cmakedefine SDL_HAPTIC_IOKIT @SDL_HAPTIC_IOKIT@

+ 2 - 1
include/SDL_gamecontroller.h

@@ -64,7 +64,8 @@ typedef enum
     SDL_CONTROLLER_TYPE_XBOXONE,
     SDL_CONTROLLER_TYPE_PS3,
     SDL_CONTROLLER_TYPE_PS4,
-    SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO
+    SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO,
+    SDL_CONTROLLER_TYPE_VIRTUAL
 } SDL_GameControllerType;
 
 typedef enum

+ 31 - 0
include/SDL_joystick.h

@@ -105,6 +105,7 @@ typedef enum
     SDL_JOYSTICK_POWER_MAX
 } SDL_JoystickPowerLevel;
 
+
 /* Function prototypes */
 
 /**
@@ -199,6 +200,36 @@ extern DECLSPEC SDL_Joystick *SDLCALL SDL_JoystickFromInstanceID(SDL_JoystickID
  */
 extern DECLSPEC SDL_Joystick *SDLCALL SDL_JoystickFromPlayerIndex(int player_index);
 
+/**
+ * Attaches a new virtual joystick.
+ * Returns the joystick's device index, or -1 if an error occurred.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickAttachVirtual(SDL_JoystickType type,
+                                                      int naxes,
+                                                      int nballs,
+                                                      int nbuttons,
+                                                      int nhats);
+
+/**
+ * Detaches a virtual joystick
+ * Returns 0 on success, or -1 if an error occurred.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickDetachVirtual(int device_index);
+
+/**
+ * Indicates whether or not a virtual-joystick is at a given device index.
+ */
+extern DECLSPEC SDL_bool SDLCALL SDL_JoystickIsVirtual(int device_index);
+
+/**
+ * Set values on an opened, virtual-joystick's controls.
+ * Returns 0 on success, -1 on error.
+ */
+extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualAxis(SDL_Joystick * joystick, int axis, Sint16 value);
+extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualBall(SDL_Joystick * joystick, int ball, Sint16 xrel, Sint16 yrel);
+extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualButton(SDL_Joystick * joystick, int button, Uint8 value);
+extern DECLSPEC int SDLCALL SDL_JoystickSetVirtualHat(SDL_Joystick * joystick, int hat, Uint8 value);
+
 /**
  *  Return the name for this currently opened joystick.
  *  If no name can be found, this function returns NULL.

+ 7 - 0
src/dynapi/SDL_dynapi_overrides.h

@@ -749,3 +749,10 @@
 #define SDL_GetAndroidSDKVersion SDL_GetAndroidSDKVersion_REAL
 #define SDL_isupper SDL_isupper_REAL
 #define SDL_islower SDL_islower_REAL
+#define SDL_JoystickAttachVirtual SDL_JoystickAttachVirtual_REAL
+#define SDL_JoystickDetachVirtual SDL_JoystickDetachVirtual_REAL
+#define SDL_JoystickIsVirtual SDL_JoystickIsVirtual_REAL
+#define SDL_JoystickSetVirtualAxis SDL_JoystickSetVirtualAxis_REAL
+#define SDL_JoystickSetVirtualBall SDL_JoystickSetVirtualBall_REAL
+#define SDL_JoystickSetVirtualButton SDL_JoystickSetVirtualButton_REAL
+#define SDL_JoystickSetVirtualHat SDL_JoystickSetVirtualHat_REAL

+ 7 - 0
src/dynapi/SDL_dynapi_procs.h

@@ -809,3 +809,10 @@ SDL_DYNAPI_PROC(int,SDL_GetAndroidSDKVersion,(void),(),return)
 #endif
 SDL_DYNAPI_PROC(int,SDL_isupper,(int a),(a),return)
 SDL_DYNAPI_PROC(int,SDL_islower,(int a),(a),return)
+SDL_DYNAPI_PROC(int,SDL_JoystickAttachVirtual,(SDL_JoystickType a, int b, int c, int d, int e),(a,b,c,d,e),return)
+SDL_DYNAPI_PROC(int,SDL_JoystickDetachVirtual,(int a),(a),return)
+SDL_DYNAPI_PROC(SDL_bool,SDL_JoystickIsVirtual,(int a),(a),return)
+SDL_DYNAPI_PROC(int,SDL_JoystickSetVirtualAxis,(SDL_Joystick *a, int b, Sint16 c),(a,b,c),return)
+SDL_DYNAPI_PROC(int,SDL_JoystickSetVirtualBall,(SDL_Joystick *a, int b, Sint16 c, Sint16 d),(a,b,c,d),return)
+SDL_DYNAPI_PROC(int,SDL_JoystickSetVirtualButton,(SDL_Joystick *a, int b, Uint8 c),(a,b,c),return)
+SDL_DYNAPI_PROC(int,SDL_JoystickSetVirtualHat,(SDL_Joystick *a, int b, Uint8 c),(a,b,c),return)

+ 3 - 0
src/joystick/SDL_gamecontrollerdb.h

@@ -707,6 +707,9 @@ static const char *s_ControllerMappings [] =
     "05000000de2800000611000001000000,Steam Controller,a:b0,b:b1,back:b6,guide:b8,leftshoulder:b4,leftstick:b9,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,righttrigger:a3,start:b7,x:b2,y:b3,",
     "050000005e040000e0020000df070000,Xbox Wireless Controller,a:b0,b:b1,back:b8,dpdown:h0.4,dpleft:h0.8,dpright:h0.2,dpup:h0.1,leftshoulder:b4,leftstick:b6,lefttrigger:a2,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b7,righttrigger:a5,rightx:a3,righty:a4,start:b9,x:b2,y:b3,",
 #endif
+#if defined(SDL_JOYSTICK_VIRTUAL)
+    "00000000000000000000000000007601,Virtual Joystick,a:b0,b:b1,x:b2,y:b3,back:b4,guide:b5,start:b6,leftstick:b7,rightstick:b8,leftshoulder:b9,rightshoulder:b10,dpup:b11,dpdown:b12,dpleft:b13,dpright:b14,leftx:a0,lefty:a1,rightx:a2,righty:a3,lefttrigger:a4,righttrigger:a5",
+#endif
 #if defined(SDL_JOYSTICK_EMSCRIPTEN)
     "default,Standard Gamepad,a:b0,b:b1,back:b8,dpdown:b13,dpleft:b14,dpright:b15,dpup:b12,guide:b16,leftshoulder:b4,leftstick:b10,lefttrigger:b6,leftx:a0,lefty:a1,rightshoulder:b5,rightstick:b11,righttrigger:b7,rightx:a2,righty:a3,start:b9,x:b2,y:b3,",
 #endif

+ 128 - 0
src/joystick/SDL_joystick.c

@@ -46,6 +46,10 @@
 #include <tlhelp32.h>
 #endif
 
+#if SDL_JOYSTICK_VIRTUAL
+#include "./virtual/SDL_sysjoystick_c.h"
+#endif
+
 static SDL_JoystickDriver *SDL_joystick_drivers[] = {
 #if defined(SDL_JOYSTICK_DINPUT) || defined(SDL_JOYSTICK_XINPUT)
     &SDL_WINDOWS_JoystickDriver,
@@ -74,6 +78,9 @@ static SDL_JoystickDriver *SDL_joystick_drivers[] = {
 #ifdef SDL_JOYSTICK_HIDAPI
     &SDL_HIDAPI_JoystickDriver,
 #endif
+#ifdef SDL_JOYSTICK_VIRTUAL
+    &SDL_VIRTUAL_JoystickDriver,
+#endif
 #if defined(SDL_JOYSTICK_DUMMY) || defined(SDL_JOYSTICK_DISABLED)
     &SDL_DUMMY_JoystickDriver
 #endif
@@ -456,6 +463,115 @@ SDL_JoystickOpen(int device_index)
 }
 
 
+int
+SDL_JoystickAttachVirtual(SDL_JoystickType type,
+                          int naxes,
+                          int nballs,
+                          int nbuttons,
+                          int nhats)
+{
+#if SDL_JOYSTICK_VIRTUAL
+    return SDL_JoystickAttachVirtualInner(type,
+                                          naxes,
+                                          nballs,
+                                          nbuttons,
+                                          nhats);
+#else
+    return SDL_SetError("SDL not built with virtual-joystick support");
+#endif
+}
+
+
+int
+SDL_JoystickDetachVirtual(int device_index)
+{
+#if SDL_JOYSTICK_VIRTUAL
+    SDL_JoystickDriver *driver;
+
+    SDL_LockJoysticks();
+    if (SDL_GetDriverAndJoystickIndex(device_index, &driver, &device_index)) {
+        if (driver == &SDL_VIRTUAL_JoystickDriver) {
+            const int result = SDL_JoystickDetachVirtualInner(device_index);
+            SDL_UnlockJoysticks();
+            return result;
+        }
+    }
+    SDL_UnlockJoysticks();
+
+    return SDL_SetError("Virtual joystick not found at provided index");
+#else
+    return SDL_SetError("SDL not built with virtual-joystick support");
+#endif
+}
+
+
+SDL_bool
+SDL_JoystickIsVirtual(int device_index)
+{
+#if SDL_JOYSTICK_VIRTUAL
+    SDL_JoystickDriver *driver;
+    int driver_device_index;
+    SDL_bool is_virtual = SDL_FALSE;
+
+    SDL_LockJoysticks();
+    if (SDL_GetDriverAndJoystickIndex(device_index, &driver, &driver_device_index)) {
+        if (driver == &SDL_VIRTUAL_JoystickDriver) {
+            is_virtual = SDL_TRUE;
+        }
+    }
+    SDL_UnlockJoysticks();
+
+    return is_virtual;
+#else
+    return SDL_FALSE;
+#endif
+}
+
+
+int
+SDL_JoystickSetVirtualAxis(SDL_Joystick * joystick, int axis, Sint16 value)
+{
+#if SDL_JOYSTICK_VIRTUAL
+    return SDL_JoystickSetVirtualAxisInner(joystick, axis, value);
+#else
+    return SDL_SetError("SDL not built with virtual-joystick support");
+#endif
+}
+
+
+int
+SDL_JoystickSetVirtualBall(SDL_Joystick * joystick, int axis, Sint16 xrel, Sint16 yrel)
+{
+#if SDL_JOYSTICK_VIRTUAL
+    return SDL_JoystickSetVirtualBallInner(joystick, axis, xrel, yrel);
+#else
+    return SDL_SetError("SDL not built with virtual-joystick support");
+#endif
+}
+
+
+int
+SDL_JoystickSetVirtualButton(SDL_Joystick * joystick, int button, Uint8 value)
+{
+#if SDL_JOYSTICK_VIRTUAL
+    return SDL_JoystickSetVirtualButtonInner(joystick, button, value);
+#else
+    return SDL_SetError("SDL not built with virtual-joystick support");
+#endif
+}
+
+
+int
+SDL_JoystickSetVirtualHat(SDL_Joystick * joystick, int hat, Uint8 value)
+{
+#if SDL_JOYSTICK_VIRTUAL
+    return SDL_JoystickSetVirtualHatInner(joystick, hat, value);
+#else
+    return SDL_SetError("SDL not built with virtual-joystick support");
+#endif
+}
+
+
 /*
  * Checks to make sure the joystick is valid.
  */
@@ -1563,6 +1679,8 @@ SDL_GetJoystickGameControllerType(const char *name, Uint16 vendor, Uint16 produc
                 SDL_strcmp(name, "Wireless Gamepad") == 0) {
                 /* HORI or PowerA Switch Pro Controller clone */
                 type = SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO;
+            } else if (SDL_strcmp(name, "Virtual Joystick") == 0) {
+                type = SDL_CONTROLLER_TYPE_VIRTUAL;
             } else {
                 type = SDL_CONTROLLER_TYPE_UNKNOWN;
             }
@@ -1624,6 +1742,12 @@ SDL_IsJoystickHIDAPI(SDL_JoystickGUID guid)
     return (guid.data[14] == 'h') ? SDL_TRUE : SDL_FALSE;
 }
 
+SDL_bool
+SDL_IsJoystickVirtual(SDL_JoystickGUID guid)
+{
+    return (guid.data[14] == 'v') ? SDL_TRUE : SDL_FALSE;
+}
+
 static SDL_bool SDL_IsJoystickProductWheel(Uint32 vidpid)
 {
     static Uint32 wheel_joysticks[] = {
@@ -1715,6 +1839,10 @@ static SDL_JoystickType SDL_GetJoystickGUIDType(SDL_JoystickGUID guid)
         }
     }
 
+    if (SDL_IsJoystickVirtual(guid)) {
+        return (SDL_JoystickType)guid.data[15];
+    }
+
     SDL_GetJoystickGUIDInfo(guid, &vendor, &product, NULL);
     vidpid = MAKE_VIDPID(vendor, product);
 

+ 3 - 0
src/joystick/SDL_joystick_c.h

@@ -73,6 +73,9 @@ extern SDL_bool SDL_IsJoystickXInput(SDL_JoystickGUID guid);
 /* Function to return whether a joystick guid comes from the HIDAPI driver */
 extern SDL_bool SDL_IsJoystickHIDAPI(SDL_JoystickGUID guid);
 
+/* Function to return whether a joystick guid comes from the Virtual driver */
+extern SDL_bool SDL_IsJoystickVirtual(SDL_JoystickGUID guid);
+
 /* Function to return whether a joystick should be ignored */
 extern SDL_bool SDL_ShouldIgnoreJoystick(const char *name, SDL_JoystickGUID guid);
 

+ 1 - 0
src/joystick/SDL_sysjoystick.h

@@ -152,6 +152,7 @@ extern SDL_JoystickDriver SDL_HAIKU_JoystickDriver;
 extern SDL_JoystickDriver SDL_HIDAPI_JoystickDriver;
 extern SDL_JoystickDriver SDL_IOS_JoystickDriver;
 extern SDL_JoystickDriver SDL_LINUX_JoystickDriver;
+extern SDL_JoystickDriver SDL_VIRTUAL_JoystickDriver;
 extern SDL_JoystickDriver SDL_WINDOWS_JoystickDriver;
 
 #endif /* SDL_sysjoystick_h_ */

+ 455 - 0
src/joystick/virtual/SDL_sysjoystick.c

@@ -0,0 +1,455 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2020 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "../../SDL_internal.h"
+
+#if defined(SDL_JOYSTICK_VIRTUAL)
+
+/* This is the virtual implementation of the SDL joystick API */
+
+#include "SDL_sysjoystick_c.h"
+#include "../SDL_sysjoystick.h"
+#include "../SDL_joystick_c.h"
+
+extern SDL_JoystickDriver SDL_VIRTUAL_JoystickDriver;
+
+static joystick_hwdata * g_VJoys = NULL;
+
+
+static joystick_hwdata *
+VIRTUAL_HWDataForIndex(int device_index)
+{
+    joystick_hwdata *vjoy = g_VJoys;
+    while (vjoy) {
+        if (device_index == 0)
+            break;
+        --device_index;
+        vjoy = vjoy->next;
+    }
+    return vjoy;
+}
+
+
+static void
+VIRTUAL_FreeHWData(joystick_hwdata *hwdata)
+{
+    if (!hwdata) {
+        return;
+    }
+    if (hwdata->axes) {
+        SDL_free((void *)hwdata->axes);
+        hwdata->axes = NULL;
+    }
+    if (hwdata->balls) {
+        SDL_free((void *)hwdata->balls);
+        hwdata->balls = 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 */
+    joystick_hwdata * cur = g_VJoys;
+    joystick_hwdata * prev = NULL;
+    while (cur) {
+        if (hwdata == cur) {
+            if (prev) {
+                prev->next = cur->next;
+            } else {
+                g_VJoys = cur->next;
+            }
+            break;
+        }
+        prev = cur;
+        cur = cur->next;
+    }
+
+    SDL_free(hwdata);
+}
+
+
+int
+SDL_JoystickAttachVirtualInner(SDL_JoystickType type,
+                               int naxes,
+                               int nballs,
+                               int nbuttons,
+                               int nhats)
+{
+    joystick_hwdata *hwdata = NULL;
+    int device_index = -1;
+
+    hwdata = SDL_calloc(1, sizeof(joystick_hwdata));
+    if (!hwdata) {
+        VIRTUAL_FreeHWData(hwdata);
+        return SDL_OutOfMemory();
+    }
+
+    hwdata->naxes = naxes;
+    hwdata->nballs = nballs;
+    hwdata->nbuttons = nbuttons;
+    hwdata->nhats = nhats;
+    hwdata->name = "Virtual Joystick";
+
+    /* Note that this is a Virtual device and what subtype it is */
+    hwdata->guid.data[14] = 'v';
+    hwdata->guid.data[15] = (Uint8)type;
+
+    /* Allocate fields for different control-types */
+    if (naxes > 0) {
+        hwdata->axes = SDL_calloc(naxes, sizeof(Sint16));
+        if (!hwdata->axes) {
+            VIRTUAL_FreeHWData(hwdata);
+            return SDL_OutOfMemory();
+        }
+    }
+    if (nballs > 0) {
+        hwdata->balls = SDL_calloc(nballs, sizeof(hwdata->balls[0]));
+        if (!hwdata->balls) {
+            VIRTUAL_FreeHWData(hwdata);
+            return SDL_OutOfMemory();
+        }
+    }
+    if (nbuttons > 0) {
+        hwdata->buttons = SDL_calloc(nbuttons, sizeof(Uint8));
+        if (!hwdata->buttons) {
+            VIRTUAL_FreeHWData(hwdata);
+            return SDL_OutOfMemory();
+        }
+    }
+    if (nhats > 0) {
+        hwdata->hats = SDL_calloc(nhats, sizeof(Uint8));
+        if (!hwdata->hats) {
+            VIRTUAL_FreeHWData(hwdata);
+            return SDL_OutOfMemory();
+        }
+    }
+
+    /* Allocate an instance ID for this device */
+    hwdata->instance_id = SDL_GetNextJoystickInstanceID();
+
+    /* Add virtual joystick to SDL-global lists */
+    hwdata->next = g_VJoys;
+    g_VJoys = hwdata;
+    SDL_PrivateJoystickAdded(hwdata->instance_id);
+
+    /* Return the new virtual-device's index */
+    device_index = SDL_JoystickGetDeviceIndexFromInstanceID(hwdata->instance_id);
+    return device_index;
+}
+
+
+int
+SDL_JoystickDetachVirtualInner(int device_index)
+{
+    SDL_JoystickID instance_id;
+    joystick_hwdata *hwdata = VIRTUAL_HWDataForIndex(device_index);
+    if (!hwdata) {
+        return SDL_SetError("Virtual joystick data not found");
+    }
+    instance_id = hwdata->instance_id;
+    VIRTUAL_FreeHWData(hwdata);
+    SDL_PrivateJoystickRemoved(instance_id);
+    return 0;
+}
+
+
+int
+SDL_JoystickSetVirtualAxisInner(SDL_Joystick * joystick, int axis, Sint16 value)
+{
+    joystick_hwdata *hwdata;
+
+    SDL_LockJoysticks();
+
+    if (!joystick || !joystick->hwdata) {
+        SDL_UnlockJoysticks();
+        return SDL_SetError("Invalid joystick");
+    }
+
+    hwdata = (joystick_hwdata *)joystick->hwdata;
+    if (axis < 0 || axis >= hwdata->nbuttons) {
+        SDL_UnlockJoysticks();
+        return SDL_SetError("Invalid axis index");
+    }
+
+    hwdata->axes[axis] = value;
+
+    SDL_UnlockJoysticks();
+    return 0;
+}
+
+
+int
+SDL_JoystickSetVirtualBallInner(SDL_Joystick * joystick, int ball, Sint16 xrel, Sint16 yrel)
+{
+    joystick_hwdata *hwdata;
+
+    SDL_LockJoysticks();
+
+    if (!joystick || !joystick->hwdata) {
+        SDL_UnlockJoysticks();
+        return SDL_SetError("Invalid joystick");
+    }
+
+    hwdata = (joystick_hwdata *)joystick->hwdata;
+    if (ball < 0 || ball >= hwdata->nbuttons) {
+        SDL_UnlockJoysticks();
+        return SDL_SetError("Invalid ball index");
+    }
+
+    hwdata->balls[ball].xrel = xrel;
+    hwdata->balls[ball].yrel = yrel;
+
+    SDL_UnlockJoysticks();
+    return 0;
+}
+
+
+int
+SDL_JoystickSetVirtualButtonInner(SDL_Joystick * joystick, int button, Uint8 value)
+{
+    joystick_hwdata *hwdata;
+
+    SDL_LockJoysticks();
+
+    if (!joystick || !joystick->hwdata) {
+        SDL_UnlockJoysticks();
+        return SDL_SetError("Invalid joystick");
+    }
+
+    hwdata = (joystick_hwdata *)joystick->hwdata;
+    if (button < 0 || button >= hwdata->nbuttons) {
+        SDL_UnlockJoysticks();
+        return SDL_SetError("Invalid button index");
+    }
+
+    hwdata->buttons[button] = value;
+
+    SDL_UnlockJoysticks();
+    return 0;
+}
+
+
+int
+SDL_JoystickSetVirtualHatInner(SDL_Joystick * joystick, int hat, Uint8 value)
+{
+    joystick_hwdata *hwdata;
+
+    SDL_LockJoysticks();
+
+    if (!joystick || !joystick->hwdata) {
+        SDL_UnlockJoysticks();
+        return SDL_SetError("Invalid joystick");
+    }
+
+    hwdata = (joystick_hwdata *)joystick->hwdata;
+    if (hat < 0 || hat >= hwdata->nbuttons) {
+        SDL_UnlockJoysticks();
+        return SDL_SetError("Invalid hat index");
+    }
+
+    hwdata->hats[hat] = value;
+
+    SDL_UnlockJoysticks();
+    return 0;
+}
+
+
+static int
+VIRTUAL_JoystickInit(void)
+{
+    return 0;
+}
+
+
+static int
+VIRTUAL_JoystickGetCount(void)
+{
+    int count = 0;
+    joystick_hwdata *cur = g_VJoys;
+    while (cur) {
+        ++count;
+        cur = cur->next;
+    }
+    return count;
+}
+
+
+static void
+VIRTUAL_JoystickDetect(void)
+{
+}
+
+
+static const char *
+VIRTUAL_JoystickGetDeviceName(int device_index)
+{
+    joystick_hwdata *hwdata = VIRTUAL_HWDataForIndex(device_index);
+    if (!hwdata) {
+        return NULL;
+    }
+    return hwdata->name ? hwdata->name : "";
+}
+
+
+static int
+VIRTUAL_JoystickGetDevicePlayerIndex(int device_index)
+{
+    return -1;
+}
+
+
+static void
+VIRTUAL_JoystickSetDevicePlayerIndex(int device_index, int player_index)
+{
+}
+
+
+static SDL_JoystickGUID
+VIRTUAL_JoystickGetDeviceGUID(int device_index)
+{
+    joystick_hwdata *hwdata = VIRTUAL_HWDataForIndex(device_index);
+    if (!hwdata) {
+        SDL_JoystickGUID guid;
+        SDL_zero(guid);
+        return guid;
+    }
+    return hwdata->guid;
+}
+
+
+static SDL_JoystickID
+VIRTUAL_JoystickGetDeviceInstanceID(int device_index)
+{
+    joystick_hwdata *hwdata = VIRTUAL_HWDataForIndex(device_index);
+    if (!hwdata) {
+        return -1;
+    }
+    return hwdata->instance_id;
+}
+
+
+static int
+VIRTUAL_JoystickOpen(SDL_Joystick * joystick, int device_index)
+{
+    joystick_hwdata *hwdata = VIRTUAL_HWDataForIndex(device_index);
+    if (!hwdata) {
+        return SDL_SetError("No such device");
+    }
+    if (hwdata->opened) {
+        return SDL_SetError("Joystick already opened");
+    }
+    joystick->instance_id = hwdata->instance_id;
+    joystick->hwdata = hwdata;
+    joystick->naxes = hwdata->naxes;
+    joystick->nballs = hwdata->nballs;
+    joystick->nbuttons = hwdata->nbuttons;
+    joystick->nhats = hwdata->nhats;
+    hwdata->opened = SDL_TRUE;
+    return 0;
+}
+
+
+static int
+VIRTUAL_JoystickRumble(SDL_Joystick * joystick, Uint16 low_frequency_rumble, Uint16 high_frequency_rumble)
+{
+    return SDL_Unsupported();
+}
+
+
+static void
+VIRTUAL_JoystickUpdate(SDL_Joystick * joystick)
+{
+    joystick_hwdata *hwdata;
+
+    if (!joystick) {
+        return;
+    }
+    if (!joystick->hwdata) {
+        return;
+    }
+
+    hwdata = (joystick_hwdata *)joystick->hwdata;
+
+    for (int i = 0; i < hwdata->naxes; ++i) {
+        SDL_PrivateJoystickAxis(joystick, i, hwdata->axes[i]);
+    }
+    for (int i = 0; i < hwdata->nballs; ++i) {
+        SDL_PrivateJoystickBall(joystick, i, hwdata->balls[i].xrel, hwdata->balls[i].yrel);
+    }
+    for (int i = 0; i < hwdata->nbuttons; ++i) {
+        SDL_PrivateJoystickButton(joystick, i, hwdata->buttons[i]);
+    }
+    for (int i = 0; i < hwdata->nhats; ++i) {
+        SDL_PrivateJoystickHat(joystick, i, hwdata->hats[i]);
+    }
+}
+
+
+static void
+VIRTUAL_JoystickClose(SDL_Joystick * joystick)
+{
+    joystick_hwdata *hwdata;
+
+    if (!joystick) {
+        return;
+    }
+    if (!joystick->hwdata) {
+        return;
+    }
+
+    hwdata = (joystick_hwdata *)joystick->hwdata;
+    hwdata->opened = SDL_FALSE;
+}
+
+
+static void
+VIRTUAL_JoystickQuit(void)
+{
+    while (g_VJoys) {
+        VIRTUAL_FreeHWData(g_VJoys);
+    }
+}
+
+
+SDL_JoystickDriver SDL_VIRTUAL_JoystickDriver =
+{
+    VIRTUAL_JoystickInit,
+    VIRTUAL_JoystickGetCount,
+    VIRTUAL_JoystickDetect,
+    VIRTUAL_JoystickGetDeviceName,
+    VIRTUAL_JoystickGetDevicePlayerIndex,
+    VIRTUAL_JoystickSetDevicePlayerIndex,
+    VIRTUAL_JoystickGetDeviceGUID,
+    VIRTUAL_JoystickGetDeviceInstanceID,
+    VIRTUAL_JoystickOpen,
+    VIRTUAL_JoystickRumble,
+    VIRTUAL_JoystickUpdate,
+    VIRTUAL_JoystickClose,
+    VIRTUAL_JoystickQuit,
+};
+
+#endif /* SDL_JOYSTICK_VIRTUAL || SDL_JOYSTICK_DISABLED */
+
+/* vi: set ts=4 sw=4 expandtab: */

+ 69 - 0
src/joystick/virtual/SDL_sysjoystick_c.h

@@ -0,0 +1,69 @@
+/*
+  Simple DirectMedia Layer
+  Copyright (C) 1997-2020 Sam Lantinga <slouken@libsdl.org>
+
+  This software is provided 'as-is', without any express or implied
+  warranty.  In no event will the authors be held liable for any damages
+  arising from the use of this software.
+
+  Permission is granted to anyone to use this software for any purpose,
+  including commercial applications, and to alter it and redistribute it
+  freely, subject to the following restrictions:
+
+  1. The origin of this software must not be misrepresented; you must not
+     claim that you wrote the original software. If you use this software
+     in a product, an acknowledgment in the product documentation would be
+     appreciated but is not required.
+  2. Altered source versions must be plainly marked as such, and must not be
+     misrepresented as being the original software.
+  3. This notice may not be removed or altered from any source distribution.
+*/
+#include "../../SDL_internal.h"
+
+#ifndef SDL_JOYSTICK_VIRTUAL_H
+#define SDL_JOYSTICK_VIRTUAL_H
+
+#if SDL_JOYSTICK_VIRTUAL
+
+#include "SDL_joystick.h"
+
+/**
+ * Data for a virtual, software-only joystick.
+ */
+typedef struct joystick_hwdata
+{
+    SDL_JoystickType type;
+    SDL_bool attached;
+    const char *name;
+    SDL_JoystickGUID guid;
+    int naxes;
+    Sint16 *axes;
+    int nballs;
+    struct {
+        Sint16 xrel;
+        Sint16 yrel;
+    } *balls;
+    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 nballs,
+                                   int nbuttons,
+                                   int nhats);
+
+int SDL_JoystickDetachVirtualInner(int device_index);
+
+int SDL_JoystickSetVirtualAxisInner(SDL_Joystick * joystick, int axis, Sint16 value);
+int SDL_JoystickSetVirtualBallInner(SDL_Joystick * joystick, int ball, Sint16 xrel, Sint16 yrel);
+int SDL_JoystickSetVirtualButtonInner(SDL_Joystick * joystick, int button, Uint8 value);
+int SDL_JoystickSetVirtualHatInner(SDL_Joystick * joystick, int hat, Uint8 value);
+
+#endif  /* SDL_JOYSTICK_VIRTUAL */
+#endif  /* SDL_JOYSTICK_VIRTUAL_H */

+ 3 - 0
test/testgamecontroller.c

@@ -310,6 +310,9 @@ main(int argc, char *argv[])
             case SDL_CONTROLLER_TYPE_NINTENDO_SWITCH_PRO:
                 description = "Nintendo Switch Pro Controller";
                 break;
+            case SDL_CONTROLLER_TYPE_VIRTUAL:
+                description = "Virtual Game Controller";
+                break;
             default:
                 description = "Game Controller";
                 break;