The 2024 Wheel Reinvention Jam just concluded. See the results.

Using microsoft GameInput API with multiple controllers

Hi, I wanted to add support for gamepads in a game, and started to look at XInput, and the doc mentioned a newer API that should support all gamepads, not just xbox ones.

https://learn.microsoft.com/en-us/gaming/gdk/_content/gc/input/overviews/input-overview

I tried it even if I don't like that it most likely requires the user to install some redists, and got it to work. But I just don't get how to have two controllers working.

The examples I found in the doc use GetCurrentReading to discover a controller. By asking for a gamepad reading and passing 0 as the device, the function will return an input that matches gamepad and we can get the device from the reading. But when we want to detect another controller, if we pass 0 as the device it will (or can I don't know if it will do it 100% of the time) return a reading for the first controller. I though that I could use GetNextReading or GetPreviousReading but passing 0 for the device in those function makes the function return NOT IMPLEMENTED.

Does anyone knows how to get several controller to work ? Is GameInput any good ? In the doc several function are labeled as not implemented, which doesn't inspire confidence.

Here is a sample application if anyone wants to try stuff (all the code for GameInput comes from the doc). Note that you'll need Microsoft GDK, and call C:\Program Files (x86)\Microsoft GDK\Command Prompts\GamingDesktopVars.cmd GamingDesktopVS2022

/* cl main.c -Zi -Od -Fegameinput -link -subsystem:windows user32.lib gameinput.lib */

#include <windows.h>
#include <stdbool.h>
#define COBJMACROS
#include <gameinput.h>

int running = 1;

IGameInput* g_gameInput = 0;
IGameInputDevice* g_gamepad = 0;

HRESULT InitializeInput( void ) {
    HRESULT result = GameInputCreate( &g_gameInput );
    return result;
}

void ShutdownInput( void ) {
    
    if ( g_gamepad ) {
        IGameInputDevice_Release( g_gamepad );
    }
    
    if ( g_gameInput ) {
        IGameInput_Release( g_gameInput );
    }
}

void PollGamepadInput( void ) {
    
    // Ask for the latest reading from devices that provide fixed-format
    // gamepad state. If a device has been assigned to g_gamepad, filter
    // readings to just the ones coming from that device. Otherwise, if
    // g_gamepad is null, it will allow readings from any device.
    IGameInputReading * reading;
    HRESULT success = IGameInput_GetCurrentReading( g_gameInput, GameInputKindGamepad, g_gamepad, &reading );
    
    if ( SUCCEEDED( success ) ) {
        // If no device has been assigned to g_gamepad yet, set it
        // to the first device we receive input from. (This must be
        // the one the player is using because it's generating input.)
        if ( !g_gamepad ) {
            IGameInputReading_GetDevice( reading, &g_gamepad );
        }
        
        // Retrieve the fixed-format gamepad state from the reading.
        GameInputGamepadState state;
        IGameInputReading_GetGamepadState( reading, &state );
        IGameInputReading_Release( reading );
        
        // Application-specific code to process the gamepad state goes here.
        OutputDebugStringW( L"got state\n" );
        
        if ( state.buttons /* & GameInputGamepadA */ ) {
            OutputDebugStringW( L"A pressed\n" );
            running = 0;
        }
    }
    
    // If an error is returned from GetCurrentReading(), it means the
    // gamepad we were reading from has disconnected. Reset the
    // device pointer, and go back to looking for an active gamepad.
    else if (g_gamepad)
    {
        IGameInputDevice_Release( g_gamepad );
        g_gamepad = 0;
    }
}

LRESULT CALLBACK window_proc( HWND window, UINT message, WPARAM wParam, LPARAM lParam ) {
    
    LRESULT result = 0;
    
    switch ( message ) {
        case WM_DESTROY:
        case WM_CLOSE: {
            running = 0;
        } break;
        
        default: {
            result = DefWindowProcW( window, message, wParam, lParam );
        } break;
    }
    
    return result;
}

int APIENTRY WinMain( HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow ) {
    
    WNDCLASSW window_class = { 0 };
    window_class.lpfnWndProc = window_proc;
    window_class.hInstance = hInst;
    window_class.lpszClassName = L"gameinput";
    
    RegisterClassW( &window_class );
    
    HWND window = CreateWindowW( L"gameinput", L"gameinput", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, 0, 0 );
    ShowWindow( window, SW_SHOW );
    
    InitializeInput( );
    
    while ( running ) {
        
        MSG message;
        
        // GetMessage( &message, 0, 0, 0 );
        // TranslateMessage( &message );
        // DispatchMessageW( &message );
        
        message = ( MSG ) { 0 };
        
        while ( PeekMessageW( &message, 0, 0, 0, PM_REMOVE ) ) {
            TranslateMessage( &message );
            DispatchMessageW( &message );
        }
        
        PollGamepadInput( );
    }
    
    ShutdownInput( );
    
    return 0;
}

Using the following function I got it to work. Not sure I like how it works.

void PollGamepadInput_2( void ) {
    
    if ( !g_gamepad_1 ) {
        
        IGameInputReading * reading;
        HRESULT success = IGameInput_GetCurrentReading( g_gameInput, GameInputKindGamepad, 0, &reading );
        
        if ( SUCCEEDED( success ) ) {
            IGameInputDevice* gamepad = 0;
            IGameInputReading_GetDevice( reading, &gamepad );
            
            if ( gamepad != g_gamepad_2 ) {
                g_gamepad_1 = gamepad;
                OutputDebugString( "gamepad 1\n" );
            } else {
                IGameInputDevice_Release( gamepad );
            }
            
            IGameInputReading_Release( reading );
        }
    }
    
    if ( !g_gamepad_2 ) {
        
        IGameInputReading* reading;
        HRESULT success = IGameInput_GetCurrentReading( g_gameInput, GameInputKindGamepad, 0, &reading );
        
        if ( SUCCEEDED( success ) ) {
            IGameInputDevice* gamepad = 0;
            IGameInputReading_GetDevice( reading, &gamepad );
            
            if ( gamepad != g_gamepad_1 ) {
                g_gamepad_2 = gamepad;
                OutputDebugString( "gamepad 2\n" );
            } else {
                IGameInputDevice_Release( gamepad );
            }
            
            IGameInputReading_Release( reading );
        }
    }
    
    if ( g_gamepad_1 ) {
        
        IGameInputReading* reading;
        HRESULT success = IGameInput_GetCurrentReading( g_gameInput, GameInputKindGamepad, g_gamepad_1, &reading );
        
        if ( SUCCEEDED( success ) ) {
            
            GameInputGamepadState state;
            IGameInputReading_GetGamepadState( reading, &state );
            IGameInputReading_Release( reading );
            
            if ( state.buttons & GameInputGamepadA ) {
                OutputDebugStringW( L"pad 1 A\n" );
            }
        } else {
            IGameInputDevice_Release( g_gamepad_1 );
            g_gamepad_1 = 0;
        }
    }
    
    if ( g_gamepad_2 ) {
        
        IGameInputReading* reading;
        HRESULT success = IGameInput_GetCurrentReading( g_gameInput, GameInputKindGamepad, g_gamepad_2, &reading );
        
        if ( SUCCEEDED( success ) ) {
            
            GameInputGamepadState state;
            IGameInputReading_GetGamepadState( reading, &state );
            IGameInputReading_Release( reading );
            reading = 0;
            
            if ( state.buttons & GameInputGamepadA ) {
                OutputDebugStringW( L"pad 2 A\n" );
            }
        } else {
            IGameInputDevice_Release( g_gamepad_2 );
            g_gamepad_2 = 0;
        }
    }
}

Can you use IGameInput::RegisterDeviceCallback method to setup callback to know whenever new device is connected or disconnected? Then you'll know exactly for which devices you can call GetCurrentReading to get reading.

I'll test that when I've got some time.


Replying to mmozeiko (#29359)

Here is an example with the callback. The 4th arguments of the register callback function allows to receive connection "notification" for pads that are already connected, otherwise you'd need to disconnect and reconnect the gamepad.

If anyone could test if a PlayStation 4 / 5 controller is detected would be helpful. I don't have one myself, but a friend tested it and it didn't work. The application creates a log file (and logs to the output panel of debuggers) and the window doesn't display anything, only the A button (X on PlayStation I guess) closes the application.

/* cl -nologo main.c -Zi -Od -Fegameinput -link -subsystem:windows user32.lib gameinput.lib */

#include <stdio.h>
#include <windows.h>
#include <stdbool.h>
#define COBJMACROS
#include <gameinput.h>

#define USE_CALLBACK

int running = 1;

FILE* log = 0;
#define log_l( literal ) fwrite( ( literal ), sizeof( literal ) - 1, 1, log ); \
OutputDebugStringW( L##literal );

IGameInput* g_game_input = 0;
IGameInputDevice* g_gamepad_1 = 0;
IGameInputDevice* g_gamepad_2 = 0;
GameInputCallbackToken g_callback_token;

LRESULT CALLBACK window_proc( HWND window, UINT message, WPARAM wParam, LPARAM lParam ) {
    
    LRESULT result = 0;
    
    switch ( message ) {
        case WM_DESTROY:
        case WM_CLOSE: {
            running = 0;
        } break;
        
        default: {
            result = DefWindowProcW( window, message, wParam, lParam );
        } break;
    }
    
    return result;
}

void device_callback( GameInputCallbackToken callback_token, void* context, IGameInputDevice* device, uint64_t timestamp, GameInputDeviceStatus current_status, GameInputDeviceStatus previous_status )  {
    
    log_l( "callback\n" );
    bool is_gamepad_1 = device == g_gamepad_1;
    bool is_gamepad_2 = device == g_gamepad_2;
    
    if ( current_status & GameInputDeviceConnected ) {
        
        log_l( "connected\n" );
        
        if ( ( !g_gamepad_1 || !g_gamepad_2 ) && ( !is_gamepad_1 && !is_gamepad_2 ) ) {
            
            if ( !g_gamepad_1 ) {
                log_l( "gamepad 1\n" );
                g_gamepad_1 = device;
            } else {
                log_l( "gamepad 2\n" );
                g_gamepad_2 = device;
            }
        }
        
    } else {
        
        log_l( "disconnected\n" );
        
        if ( is_gamepad_1 ) {
            log_l( "gamepad 1\n" );
            g_gamepad_1 = 0;
        } else if ( is_gamepad_2 ) {
            log_l( "gamepad 2\n" );
            g_gamepad_2 = 0;
        }
    }
}

int APIENTRY WinMain( HINSTANCE hInst, HINSTANCE hInstPrev, PSTR cmdline, int cmdshow ) {
    
    log = fopen( "log.txt", "wb" );
    
    WNDCLASSW window_class = { 0 };
    window_class.lpfnWndProc = window_proc;
    window_class.hInstance = hInst;
    window_class.lpszClassName = L"gameinput";
    
    RegisterClassW( &window_class );
    
    HWND window = CreateWindowW( L"gameinput", L"gameinput", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, 0, 0 );
    ShowWindow( window, SW_SHOW );
    
    HRESULT hr = GameInputCreate( &g_game_input );
    
    if ( !SUCCEEDED( hr ) ) {
        log_l( "GameInputCreate failed.\n" );
    }
    
#if defined( USE_CALLBACK )
    /* NOTE simon: Use GameInputBlockingEnumeration or GameInputAsyncEnumeration to get call for pads that are already connected. */
    hr = IGameInput_RegisterDeviceCallback( g_game_input, 0, GameInputKindGamepad, GameInputDeviceConnected, GameInputBlockingEnumeration, 0, device_callback, &g_callback_token );
    
    if ( !SUCCEEDED( hr ) ) {
        log_l( "RegisterDeviceCallback failed.\n" );
    }
#endif
    
    while ( running ) {
        
        MSG message;
        
        // GetMessage( &message, 0, 0, 0 );
        // TranslateMessage( &message );
        // DispatchMessageW( &message );
        
        message = ( MSG ) { 0 };
        
        while ( PeekMessageW( &message, 0, 0, 0, PM_REMOVE ) ) {
            TranslateMessage( &message );
            DispatchMessageW( &message );
        }
        
        bool pad_1_a_pressed = false;
        bool pad_2_a_pressed = false;
        
#if !defined( USE_CALLBACK )
        
        if ( !g_gamepad_1 ) {
            
            log_l( "gamepad 1 detection\n" );
            
            IGameInputReading * reading;
            HRESULT success = IGameInput_GetCurrentReading( g_game_input, GameInputKindGamepad, 0, &reading );
            
            if ( SUCCEEDED( success ) ) {
                IGameInputDevice* gamepad = 0;
                IGameInputReading_GetDevice( reading, &gamepad );
                
                if ( gamepad != g_gamepad_2 ) {
                    g_gamepad_1 = gamepad;
                    log_l( "gamepad 1 found\n" );
                } else {
                    IGameInputDevice_Release( gamepad );
                }
                
                IGameInputReading_Release( reading );
            }
        }
        
        if ( !g_gamepad_2 ) {
            
            log_l( "gamepad 2 detection\n" );
            
            IGameInputReading* reading;
            HRESULT success = IGameInput_GetCurrentReading( g_game_input, GameInputKindGamepad, 0, &reading );
            
            if ( SUCCEEDED( success ) ) {
                IGameInputDevice* gamepad = 0;
                IGameInputReading_GetDevice( reading, &gamepad );
                
                if ( gamepad != g_gamepad_1 ) {
                    g_gamepad_2 = gamepad;
                    log_l( "gamepad 2 found\n" );
                } else {
                    IGameInputDevice_Release( gamepad );
                }
                
                IGameInputReading_Release( reading );
            }
        }
        
#endif
        
        if ( g_gamepad_1 ) {
            
            log_l( "gamepad 1 input\n" );
            IGameInputReading* reading;
            HRESULT success = IGameInput_GetCurrentReading( g_game_input, GameInputKindGamepad, g_gamepad_1, &reading );
            
            if ( SUCCEEDED( success ) ) {
                
                log_l( "got reading.\n" );
                GameInputGamepadState state;
                IGameInputReading_GetGamepadState( reading, &state );
                IGameInputReading_Release( reading );
                
                if ( state.buttons & GameInputGamepadA ) {
                    log_l( "gamepad 1: A is pressed\n" );
                    pad_1_a_pressed = true;
                }
            } else {
#if !defined( USE_CALLBACK )
                log_l( "gamepad 1 disconnected.\n" );
                IGameInputDevice_Release( g_gamepad_1 );
                g_gamepad_1 = 0;
#endif
            }
        }
        
        if ( g_gamepad_2 ) {
            
            log_l( "gamepad 2 input\n" );
            IGameInputReading* reading;
            HRESULT success = IGameInput_GetCurrentReading( g_game_input, GameInputKindGamepad, g_gamepad_2, &reading );
            
            if ( SUCCEEDED( success ) ) {
                
                log_l( "got reading.\n" );
                GameInputGamepadState state;
                IGameInputReading_GetGamepadState( reading, &state );
                IGameInputReading_Release( reading );
                reading = 0;
                
                if ( state.buttons & GameInputGamepadA ) {
                    log_l( "gamepad 2: A is pressed\n" );
                    pad_2_a_pressed = true;
                }
            } else {
#if !defined( USE_CALLBACK )
                log_l( "gamepad 2 disconnected.\n" );
                IGameInputDevice_Release( g_gamepad_2 );
                g_gamepad_2 = 0;
#endif
            }
        }
        
        if ( pad_1_a_pressed || pad_2_a_pressed ) {
            running = false;
        }
        
        Sleep( 16 );
    }
    
#if defined( USE_CALLBACK )
    
    IGameInput_UnregisterCallback( g_game_input, g_callback_token, 5000 );
    
#else
    
    if ( g_gamepad_1 ) {
        IGameInputDevice_Release( g_gamepad_1 );
    }
    
    if ( g_gamepad_2 ) {
        IGameInputDevice_Release( g_gamepad_2 );
    }
    
#endif
    
    if ( g_game_input ) {
        IGameInput_Release( g_game_input );
    }
    
    fclose( log );
    
    return 0;
}

Here is the exe to avoid having to compile it.

gameinput_230508.7z

It took me a while to figure out how to get gameinput.h & lib files - the documentation pages are not helpful with that. Turns out I needed to go to https://github.com/microsoft/GDK and run that PGDK.exe to install SDK.

I tried Switch Pro & PS4 controllers - both worked, callback was called and button A press detected. PS5 did not work (not detected by GameInput), but windows sees it in control panel joystick test thingy.


Edited by Mārtiņš Možeiko on

Thanks for testing it out.

My friend that tested it didn't install the GDK but he had the gameinput dlls in C:\Program Files (x86)\Microsoft GameInput. I don't know if it comes with Windows or he installed them before from some other game...

Anyway on his machine, functions from GameInput are called, but the PS4 controller is not detected. If he use DS4 to emulate an xbox controller the pad is detected (the callback works) but there are no reading when trying to poll gamepads.

I made another version using XInput and when he uses DS4 his gamepad works.

Any idea what could be wrong ?

Can I use the header you posted in another thread for PS4/5 controllers ? Is there any license ? Are there any issues you're aware off ?


Replying to mmozeiko (#29368)

I was not using any emulation software for PS4 controller (or xinput whatever hook dlls that translate it to xinput api). Just whatever Windows natively detected. It just worked.

My header using rawinput for PS4/5 should be fine to use - I mean that's exactly how SDL or linux kernel accesses PS4/5 controllers. Exactly same protocol & same byte parsing. There should be no issues with that (aside if I have any bugs in code, I have not used it for anything shippable). It's in public domain, I don't care about any licenses for it.


Edited by Mārtiņš Možeiko on
Replying to mrmixer (#29370)

Thanks.


Replying to mmozeiko (#29371)

I used your header and managed to get my friend's controller working, but not out of the box.

His controller product id isn't the same as the one in the header (0x05c4), it's 0x09cc. According to this page it's the product id for "DualShock 4 (2nd Gen)". After adding that id in the header it worked.

There was a warning while compiling the header: in ps_crc32 the variable i is redefined in the inner loop.

Do you know if there is some documentation freely accessible about the PS4/5 controller dead zones and maximum stick values ? After a bit of testing we were able to get stick value where X was 1.0f and Y was 0.3f which I wouldn't expect to be possible. The raw value for sticks can be 127 on x and 68 on y which makes a vector of about 144 unit, that's 13% more than 127 which should be the max value.

I don't think there is anything officially documented about it. Only whatever people have reverse engineered.

What do you mean with "vector of about 144 unit" ? Are you doing sqrt(x*x+y*y)? That would mean you are assuming hardware reports coordinates for circle. But I don't know if that's the case. I mean - I don't know how PS5 controller hardware is made. But it could be square, not circle. Thus on diagonal it could be sqrt(2) larger than just max vertical or horizontal direction.

It could be also that even if hardware is circle, it remaps the output to square. Not sure. Anyways - this is in general a pain to deal with controllers, not a PS specific thing.

Maybe just clamp the shape to circle? Basically get angle & magnitude. Clamp the magnitude to one, and remap angle&magnitude back to x&y if you need x&y.


Replying to mrmixer (#29376)

What do you mean with "vector of about 144 unit" ? Are you doing sqrt(xx+yy)? That would mean you are assuming hardware reports coordinates for circle.

Thus on diagonal it could be sqrt(2) larger than just max vertical or horizontal direction.

That was what I was trying to figure out, and nothing seemed right, so I though there might be some recommended way of using the values in the PlayStation doc or something like that.

Maybe just clamp the shape to circle? Basically get angle & magnitude. Clamp the magnitude to one, and remap angle&magnitude back to x&y if you need x&y.

That's what I ended up doing.

Once again, thanks for your help.


Replying to mmozeiko (#29377)