libsdl-org / SDL

Simple Directmedia Layer
https://libsdl.org
zlib License
9.79k stars 1.82k forks source link

Windows.Gaming.Input registers duplicate joystick #7948

Closed TempoLabGames closed 10 months ago

TempoLabGames commented 1 year ago

Under some circumstances, Windows.Gaming.Input detects a duplicate joystick for an XBox controller. I have reproduced the bug on both Windows 10 and Windows 11, but it manifests differently.

Encountered in SDL version 2.28.1 (from https://github.com/libsdl-org/SDL/releases/download/release-2.28.1/SDL2-devel-2.28.1-VC.zip).

Test program:

#include <stdio.h>

#define SDL_MAIN_HANDLED
#include <SDL.h>

#pragma comment(lib, "SDL2.lib")

#define MAX_JOYSTICKS 256

void log_joystick_operation(SDL_Joystick* joystick, SDL_JoystickID id, const char* operation)
{
    const char* name = SDL_JoystickName(joystick);
    const char* path = SDL_JoystickPath(joystick);
    SDL_JoystickGUID guid = SDL_JoystickGetGUID(joystick);
    char guidstr[33] = { 0 };
    SDL_JoystickGetGUIDString(guid, guidstr, sizeof(guidstr));
    printf("Joystick %s\n  Instance ID: %d\n  Name: %s\n  Path: %s\n  GUID: %s\n",
        operation, id, name, path, guidstr);
}

int main(int argc, char* argv[])
{
    SDL_SetHint(SDL_HINT_JOYSTICK_THREAD, "1");
    // Uncomment to disable these backends, or set the corresponding environment variables.
    // SDL_SetHint(SDL_HINT_JOYSTICK_RAWINPUT, "0");
    // SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "0");
    // SDL_SetHint(SDL_HINT_DIRECTINPUT_ENABLED, "0");
    SDL_Init(SDL_INIT_JOYSTICK);
    SDL_Joystick* joysticks[MAX_JOYSTICKS] = { 0 };
    while (1)
    {
        SDL_Event event;
        if (SDL_WaitEvent(&event) != 1)
        {
            printf("Error waiting for event\n");
            return 1;
        }
        switch (event.type)
        {
        case SDL_JOYDEVICEADDED:
        {
            int device_index = event.jdevice.which;
            printf("Joystick added [index %d] @ %d\n", device_index, event.jdevice.timestamp);
            SDL_Joystick* joystick = SDL_JoystickOpen(device_index);
            SDL_JoystickID id = SDL_JoystickInstanceID(joystick);
            if (id >= MAX_JOYSTICKS)
            {
                printf("Joystick instance id too large: %d\n", id);
                return 1;
            }
            log_joystick_operation(joystick, id, "opened");
            if (joysticks[id] != NULL)
            {
                printf("Duplicate joystick instance id: %d\n", id);
                return 1;
            }
            joysticks[id] = joystick;
            break;
        }
        case SDL_JOYDEVICEREMOVED:
        {
            SDL_JoystickID id = event.jdevice.which;
            printf("Joystick removed @ %d\n", event.jdevice.timestamp);
            if (id >= MAX_JOYSTICKS)
            {
                printf("Joystick instance id too large: %d\n", id);
                return 1;
            }
            SDL_Joystick* joystick = joysticks[id];
            if (joystick == NULL)
            {
                printf("Unknown joystick instance id removed: %d\n", id);
                return 1;
            }
            log_joystick_operation(joystick, id, "closed");
            SDL_JoystickClose(joystick);
            joysticks[id] = NULL;
            break;
        }
        default:
            continue;
        }
        printf("\n");
    }
}

On Windows 11, the duplicate controller is not detected on startup, but appears if the controller is unplugged and reconnected.

(For all below tests, the controller starts plugged in, then is unplugged, then reconnected.)

>SDLBug.exe
Joystick added [index 0] @ 5
Joystick opened
  Instance ID: 0
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200

Joystick removed @ 3381
Joystick closed
  Instance ID: 0
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200

Joystick added [index 0] @ 8064
Joystick opened
  Instance ID: 1
  Name: Xbox One Game Controller
  Path: (null)
  GUID: 03004d6e5e040000120b000000007701

Joystick added [index 0] @ 8080
Joystick opened
  Instance ID: 2
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200

If all other backends are disabled, the Windows.Gaming.Input instance is detected on startup and functions as expected.

>set SDL_DIRECTINPUT_ENABLED=0

>set SDL_XINPUT_ENABLED=0

>set SDL_JOYSTICK_RAWINPUT=0

>SDLBug.exe
Joystick added [index 0] @ 78
Joystick opened
  Instance ID: 0
  Name: Xbox One Game Controller
  Path: (null)
  GUID: 03004d6e5e040000120b000000007701

Joystick removed @ 2653
Joystick closed
  Instance ID: 0
  Name: Xbox One Game Controller
  Path: (null)
  GUID: 03004d6e5e040000120b000000007701

Joystick added [index 0] @ 6548
Joystick opened
  Instance ID: 1
  Name: Xbox One Game Controller
  Path: (null)
  GUID: 03004d6e5e040000120b000000007701

On Windows 10, this functions correctly with default settings:

>SDLBug.exe
Joystick added [index 0] @ 8
Joystick opened
  Instance ID: 0
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&2&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200

Joystick removed @ 2875
Joystick closed
  Instance ID: 0
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&2&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200

Joystick added [index 0] @ 7495
Joystick opened
  Instance ID: 1
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&2&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200

However, disabling DirectInput appears to manifest the duplicate device, which sometimes collides with the existing device:

>set SDL_DIRECTINPUT_ENABLED=0

>SDLBug.exe
Joystick added [index 0] @ 20
Joystick opened
  Instance ID: 0
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&2&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200

Joystick removed @ 2983
Joystick closed
  Instance ID: 0
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&2&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200

Joystick added [index 0] @ 6418
Joystick opened
  Instance ID: 2
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&2&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200

Joystick added [index 0] @ 6427
Joystick opened
  Instance ID: 2
  Name: Controller (Xbox One For Windows)
  Path: \\?\HID#VID_045E&PID_02FF&IG_00#7&51dab6b&2&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}
  GUID: 0300938d5e040000ff02000000007200
Duplicate joystick instance id: 2

I also observed this collision problem rarely on Windows 11, but it does not reproduce as readily.

(Note that extending this sample to display input events won't show duplicated inputs because the Windows.Gaming.Input joystick doesn't appear to receive button presses when run from a console application. However, in my actual application, each button press is reported once per joystick object.)

This appears to show three separate problems:

  1. The Windows.Gaming.Input driver can return duplicate devices when used in conjunction with other drivers. I've skimmed through the code but can't see how SDL ordinarily deduplicates controllers that are visible via multiple input drivers.

  2. The joystick device_index may occasionally reference the wrong joystick object based on what seems to be a race condition.

  3. There is no SDL hint to disable the Windows.Gaming.Input subsystem. This was mentioned in #7068 but seems to have been missed amongst the other info. Currently the only way to remove these devices is to filter out devices without a path, or remove the subsystem entirely and recompile.

TempoLabGames commented 1 year ago

Point 3 has been addressed in 2.28.2 with SDL_HINT_JOYSTICK_WGI. Thanks!

slouken commented 11 months ago

Are you still seeing this in the latest version of SDL?

TempoLabGames commented 11 months ago

I've just retested on both my Windows 11 and Windows 10 machines with SDL 2.28.5 and reproduced issues 1 and 2 from my original report.

However, since the introduction of SDL_HINT_JOYSTICK_WGI, I've found disabling that by default using SDL_SetHintWithPriority(SDL_HINT_JOYSTICK_WGI, "0", SDL_HINT_DEFAULT) to be an acceptable workaround.

slouken commented 10 months ago

Normally WGI will ignore controllers that are handled by XInput and raw input, but it looks like on your system WGI is seeing the controller before it's available to raw input.

The issue with events and device indices referring to the same device is inherent in referencing devices by index, which is redesigned in SDL3.

Given that you have a reasonable workaround and future versions of SDL will hopefully use GDK, I'll go ahead and close this for now.

Thanks!