Skip to content

Instantly share code, notes, and snippets.

@TheNathannator
Last active April 23, 2024 21:46
Show Gist options
  • Save TheNathannator/bcebc77e653f71e77634144940871596 to your computer and use it in GitHub Desktop.
Save TheNathannator/bcebc77e653f71e77634144940871596 to your computer and use it in GitHub Desktop.
A writeup on how to interact with the Xbox One driver on Windows directly

GIP Interface

A writeup on how to directly communicate with GIP (Xbox One) devices on a basic level.

I tried Windows.Gaming.Input.Custom and was unable to get it to work, so I resorted to this. Would have liked if I could do things more legitimately with what little documentation was provided, but oh well.

This writeup is not at all comprehensive of every possibilty with the interface, otherwise there'd be far too much to go through.

Thanks to the XInputHooker project for having a bunch of function detours set up, made my life easier when doing all of this.

Table of Contents

Usage

Interface Handle

Usage of the GIP interface starts with acquiring a handle to the interface via a device path of \\.\XboxGIP:

HANDLE hFile = CreateFileW(L"\\\\.\\XboxGIP", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);

Opening the interface handle will fail if no GIP devices are connected to the system. You can either periodically try to open the handle, or only open the handle once a GIP device has been connected. The interface GUID for GIP devices is 020BC73C-0DCA-4EE3-96D5-AB006ADA5938, sourced from DJm00n's RawInputDemo. Note that GIP devices enumerated through this GUID cannot be directly interacted with, at least from what I can tell.

Reading/Writing

Once the interface handle has been acquired, use ReadFile and WriteFile to read from and write to the interface.

Every read will start with a constant-size header, which contains information such as an ID for the device that sent the message, a command ID for the type of message that was sent, and the length of the message data. Every write must contain a header of the same format as well.

You'll want to prepare a large buffer for reads (500 bytes at minimum, 1000+ bytes recommended for safety), as one of the messages sent when a new device is connected (the descriptor specifically) is a few hundred bytes large and can vary in size.

Some messages such as input reports don't get sent unless the application is focused and has a proper window (console apps (at least .NET ones) don't seem to work). In fact, there's a specific message sent to indicate when focus has changed. There may be an IOCTL transfer that bypasses this limitation, but it's currently not known if there is.

Device Re-enumeration

No driver IOCTLs or initialization messages are necessary to start receiving information, however, if you just start reading right away you aren't guaranteed to get descriptor messages for devices that are already connected to the system, meaning you can't know for sure what format the device's input reports will be in. Use the AddReenumerateCallerContext IOCTL after setting up a read buffer to have the driver restart device enumeration for your app's context:

#define GIP_ADD_REENUMERATE_CALLER_CONTEXT CTL_CODE(0x4000, 0x734, METHOD_BUFFERED, FILE_ANY_ACCESS) // 0x40001CD0
DeviceIoControl(hFile, GIP_ADD_REENUMERATE_CALLER_CONTEXT, nullptr, 0, nullptr, 0, nullptr, nullptr);

There are many other IOCTLs, but only this one is necessary for basic operation.

Data Buffer Structures

There are many different types of messages that can be sent, each with their own data structures.

Most of these structures are based off of medusalix's xone Xbox One controller driver for Linux. Some information differs between direct device communication and the GIP interface however, and most commands have been left out of here as they aren't strictly necessary and haven't shown up in the interface with regular Xbox One gamepads.

All fields are in little-endian format.

Header

All messages received start with a constant-size header:

struct GipHeader
{
    uint64_t deviceId;     // A unique identifier for the device.
    uint8_t  commandId;    // A command ID that indicates the type of message being sent.
    uint8_t  flags : 4;    // Flags for the message.
    uint8_t  client : 4;   // A sub-device identifier.
    uint8_t  sequence;     // Increases by 1 for every packet within a sequence. Wraps around from 255 to 1 when maxing out.
                           // Not all commands use this, only one I've seen use it so far is input reports,
                           // but I also haven't seen very many of the commands through the interface.
    uint8_t  unknown1;
    uint32_t length;       // The length of the data after the header.
    uint32_t unknown2;
};

enum GipHeaderFlags
{
    None = 0x00,
    Internal = 0x20  // Flag used on messages that are core to the protocol and not device-defined.
    // There are other flags that appear in direct device communication, but so far those ones haven't appeared through the interface.
};

Make sure to check the length of the header when using the data following it, even if that type of message almost certainly is a constant size. Some messages aren't a constant size, and can be different across different device types.

Message ID 0x02: Device Arrival

Sent when a device has connected to the system.

struct GipArrival
{
    GipHeader header;
    uint64_t deviceId;  // yes, this is provided in addition to the device ID in the header.
    uint16_t vendorId;
    uint16_t productId;
    struct {
        uint16_t major;
        uint16_t minor;
        uint16_t build;
        uint16_t revision;
    } firmwareVersion;
    uint8_t unknown[13];  // This one differs from direct communication
};

Message ID 0x03: Device Status

Sent periodically to report a device's current status (such as battery state).

struct GipStatus
{
    GipHeader header;
    uint8_t batteryLevel : 2;
    uint8_t batteryType : 2;
    uint8_t unknown : 3;
    bool    connected : 1;
    uint8_t unknown3[3];
};

enum GipBatteryType
{
    Wired = 0,
    Standard = 1,
    ChargeKit = 2
};

enum GipBatteryLevel
{
    Low = 0,
    Medium = 1,
    High = 2,
    Full = 3
};

Message ID 0x04: Device Descriptor

A descriptor for the device sent when a new device connects to the system.

This one starts with its own header:

struct GipDescriptorHeader
{
    uint16_t vendorId;
    uint16_t productId;

    uint8_t  unknown[14];
    uint16_t length; // This length is relative to unknown[0], *not* the start of the following offsets.
                     // Subtract 16 to get the length relative to the offsets.

    // These are all offsets into the following data buffer that point to a block of a specific type of data.
    // An offset of 0 is a null offset and means there is no data for that offset.
    // The offsets are relative to offset_externalCommands, *not* unknown[0].
    uint16_t offset_externalCommands;
    uint16_t offset_firmwareVersions;
    uint16_t offset_audioFormats;
    uint16_t offset_inputCommands;
    uint16_t offset_outputCommands;
    uint16_t offset_classNames;
    uint16_t offset_interfaceGuids;
    uint16_t offset_hidDescriptor;
};

Following the header is a variable-size buffer, of size length - 32. The offsets in the header point to locations within this buffer where the start of a block of metadata can be found.

Each metadata block starts with a single-byte count of how many elements are in the block, following which are the elements themselves. If this count is 0, there are no elements and the block should be ignored.

template <class T>
struct GipDescriptorBlock
{
    uint8_t count;
    T elements[];
};

In short, this is the overall format of the message:

struct GipDescriptor
{
    GipHeader header;
    GipDescriptorHeader descriptorHeader;
    byte buffer[]; // where sizeof(buffer) == (descriptorHeader.length - 32)
};

This message is typically a few hundred bytes large, so you'll want to prepare a large buffer for it (500 bytes at minimum).

External Commands

This block describes non-core commands that the device supports.

struct GipExternalCommand
{
    uint16_t unknown1;
    uint8_t  command;    // The command ID of the message.
    uint16_t maxLength;  // The maximum size of the command data.
    uint16_t unknown2;
    uint8_t  flags;      // Flags that describe the command.
    uint8_t  unknown3[15];
};
typedef GipDescriptorBlock<GipExternalCommand> GipExternalCommandBlock;

enum GipExternalCommandFlags
{
    Unknown = 0x04, // Found in the Xbox One Rock Band 4 Stratocaster descriptor.
    Output = 0x08,  // This command is sent from the device.
    Input = 0x10    // This command is sent to the device.
};

Firmware Versions

This block indicates which firmware versions a device supports.

struct GipFirmwareVersion
{
    uint16_t major;
    uint16_t minor;
};
typedef GipDescriptorBlock<GipFirmwareVersion> GipFirmwareVersionBlock;

Audio Formats

This block indicates the audio formats that a device supports.

struct GipAudioFormat
{
    uint8_t unknown[2];
};
typedef GipDescriptorBlock<GipAudioFormat> GipAudioFormatBlock;

Input/Output Commands

These blocks indicate the commands that a device can send (input) and receive (output).

typedef GipDescriptorBlock<uint8_t> GipSupportedCommandBlock;

Class Name Strings

This block contains the class names of a device.

struct GipClassName
{
    uint16_t length;
    char_t buffer[];
};
typedef GipDescriptorBlock<GipClassName> GipClassNameBlock;

Interface GUIDs

This block contains GUIDs that indicate the interfaces a device supports.

typedef GipDescriptorBlock<GUID> GipInterfaceGuidBlock;

These GUIDs can be used to identify the device's type and what its input report is like.

Some notable GUIDs (note that these might not be correct, could only verify gamepad and navigation):

// Device is capable of navigating menus.
// {B8F31FE7-7386-40E9-A9F8-2F21263ACFB7}
DEFINE_GUID(GUID_GIP_INTERFACE_NAVIGATION,    0xB8F31FE7, 0x7386, 0x40E9, 0xA9, 0xF8, 0x2F, 0x21, 0x26, 0x3A, 0xCF, 0xB7);

// Device is a gamepad.
// {082E402C-07DF-45E1-A5AB-A3127AF197B5}
DEFINE_GUID(GUID_GIP_INTERFACE_GAMEPAD,       0x082E402C, 0x07DF, 0x45E1, 0xA5, 0xAB, 0xA3, 0x12, 0x7A, 0xF1, 0x97, 0xB5);

// Device is an arcade stick.
// {332054CC-A34B-41D5-A34A-A6A6711EC4B3}
DEFINE_GUID(GUID_GIP_INTERFACE_ARCADE_STICK,  0x332054CC, 0xA34B, 0x41D5, 0xA3, 0x4A, 0xA6, 0xA6, 0x71, 0x1E, 0xC4, 0xB3);

// Device is a flight stick.
// {03F1A011-EFE9-4CC1-969C-38DC55F404D0}
DEFINE_GUID(GUID_GIP_INTERFACE_FLIGHT_STICK,  0x03F1A011, 0xEFE9, 0x4CC1, 0x96, 0x9C, 0x38, 0xDC, 0x55, 0xF4, 0x04, 0xD0);

// Device is a racing wheel.
// {646979CF-6B71-4E96-8DF9-59E398D7420C}
DEFINE_GUID(GUID_GIP_INTERFACE_RACING_WHEEL,  0x646979CF, 0x6B71, 0x4E96, 0x8D, 0xF9, 0x59, 0xE3, 0x98, 0xD7, 0x42, 0x0C);

HID Descriptor

This block contains the device's HID descriptor.

typedef GipDescriptorBlock<uint8_t> GipHidDescriptorBlock;

Message ID 0x09: Set Device State

Send this to set the state of the device (force-feedback or vibration).

This likely differs between device types, the structure here is for standard gamepads.

struct GipForceFeedback
{
    GipHeader header;
    uint8_t unknown1;
    uint8_t flags;
    uint8_t leftTrigger;
    uint8_t rightTrigger;
    uint8_t leftMotor;
    uint8_t rightMotor;
    uint8_t duration;
    uint8_t delay;
    uint8_t repeat;
};

enum GipForceFeedbackFlags
{
    RightMotor = 0x01,
    LeftMotor = 0x02,
    RightTrigger = 0x04,
    LeftTrigger = 0x08
};

Message ID 0x20: Device Input Report

Sent when the input state of a device changes.

This can vary between devices, for simplicity only the standard gamepad's report is described here.

struct GipGamepadInputReport
{
    GipHeader header;
    uint16_t buttons;
    uint16_t leftTrigger;   // Ranges from 0x000 to 0x3FF
    uint16_t rightTrigger;  // Ranges from 0x000 to 0x3FF
    int16_t leftStickX;
    int16_t leftStickY;
    int16_t rightStickX;
    int16_t rightStickY;
};

enum GipGamepadButtons
{
    Sync = 0x0001, // Yes, this is exposed through the interface! Guide button isn't though.
    Menu = 0x0004,
    View = 0x0008,
    A = 0x0010,
    B = 0x0020,
    X = 0x0040,
    Y = 0x0080,
    DpadUp = 0x0100,
    DpadDown = 0x0200,
    DpadLeft = 0x0400,
    DpadRight = 0x0800,
    LeftShoulder = 0x1000,
    RightShoulder = 0x2000,
    LeftThumbClick = 0x4000,
    RightThumbClick = 0x8000
};

Message ID 0xE0: System Focus Change

Sent when the application gains or loses focus.

struct GipFocusChange
{
    GipHeader header;
    uint8_t status;
    uint8_t unknown[7];
};

enum GipFocusStatus
{
    Unfocused = 0,
    Focused = 1
};

Simple Proof-of-Concept Example

#include <Windows.h>

#define GIP_ADD_REENUMERATE_CALLER_CONTEXT CTL_CODE(0x4000, 0x734, METHOD_BUFFERED, FILE_ANY_ACCESS) // 0x40001CD0

// For conciseness, error checking is not included in this example.
void GipInterfaceExample()
{
    // Get the GIP interface handle.
    HANDLE gipHandle = CreateFileW(L"\\\\.\\XboxGIP", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
    // Request a re-enumeration of connected devices.
    DeviceIoControl(gipHandle, GIP_ADD_REENUMERATE_CALLER_CONTEXT, nullptr, 0, nullptr, 0, nullptr, nullptr);

    // Read from the interface.
    byte buffer[1000];
    DWORD bytesRead;
    while (ReadFile(gipHandle, &buffer, sizeof(buffer), &bytesRead, nullptr))
    {
        // Process messages here.
    }

    // Close the handle once done, or when reading fails and you need to exit
    CloseHandle(gipHandle);
}

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment