Blog Platform 8 min read

Windows GameInput vs XInput — picking a stack for MIDI

GameInput vs XInput vs Raw Input on Windows for a gamepad-to-MIDI bridge. Latency, controller coverage, and the call to make in 2026.

By Aidxn Design

Three Windows APIs read a gamepad: XInput (2005, Xbox 360-era, dying), GameInput (2022, the replacement, formerly "Project Athens"), and Raw Input (1995, low-level HID, niche but useful). If you are building a gamepad-to-MIDI bridge today, the gameinput xinput gamepad decision shapes your support matrix, your latency budget, and how many controllers you can hold open at once. This post is the call: GameInput first, XInput fallback, Raw Input for the weird hardware.

TL;DR
  • Use GameInput on Windows 10 build 19041+ and Windows 11. Supports DualSense, Switch Pro, MFi, up to 16 devices, ~2 ms USB latency.
  • Fall back to XInput only for Windows 10 < 19041. Caps at 4 controllers, Xbox-style only, no DualSense touchpad or adaptive triggers.
  • Add Raw Input as a third path for niche HID hardware (third-party Switch pads, arcade sticks).
  • SDL3 is a fine cross-platform wrapper, but you pay 8 MB and lose adaptive-trigger access on the DualSense.

The state of play in 2026

XInput is now legacy. Microsoft has not added a feature to it in over a decade — no DualSense support, no adaptive triggers, no touchpad, four-controller hard cap, eleven buttons + two triggers + two sticks, that's it. GameInput is Microsoft's actively developed input stack, ships as part of the Game Development Kit (GDK), and is the only Windows API that exposes DualSense adaptive triggers and the touchpad correctly. The full GameInput reference lives in Microsoft's GameInput documentation.

Side-by-side comparison

FeatureGameInputXInputRaw Input
Released202220051995
Max controllers~164Unlimited
DualSense touchpadYesNoYes (raw HID)
Adaptive triggers (read/write)YesNoRead-only without effort
Typical USB latency1.8–2.5 ms3–4 ms1.5–2 ms
Switch Pro / Joy-ConYes (BT)NoYes (HID)
API surface~30 functions~7 functions~10 functions
Min Windows version10 build 1904172000

XInput — the minimum viable bridge

XInput is the easiest API to write. Two functions, one struct, polled at your frame rate. If you only care about Xbox controllers and you ship to old Windows, write this and go home.

#include <Xinput.h>
#pragma comment(lib, "Xinput.lib")

XINPUT_STATE prev = {0};
void poll_xinput(void) {
    XINPUT_STATE state;
    if (XInputGetState(0, &state) != ERROR_SUCCESS) return;

    // Buttons changed?
    WORD diff = state.Gamepad.wButtons ^ prev.Gamepad.wButtons;
    if (diff & XINPUT_GAMEPAD_A)
        send_note(60, (state.Gamepad.wButtons & XINPUT_GAMEPAD_A) ? 127 : 0);

    // Left stick X → CC 1
    int x = state.Gamepad.sThumbLX;             // -32768..32767
    int cc = (x + 32768) * 127 / 65535;          // 0..127
    if (cc != prev_cc) { send_cc(1, cc); prev_cc = cc; }

    prev = state;
}

Polled at 1 kHz this gets you ~3 ms latency end-to-end. Good enough for Logic-style transport mapping. Useless for adaptive triggers, MPE, or anything fancy.

GameInput — the modern path

GameInput is COM-flavoured and event-driven, not polled. You get an IGameInput, register a callback for device connect/disconnect, and read snapshots via IGameInputReading. It is more code than XInput but the model maps cleanly to a MIDI bridge — you only do work when something changes.

#include <GameInput.h>

IGameInput* g_input = nullptr;
GameInputCallbackToken token;

void on_device(GameInputCallbackToken, void*, IGameInputDevice* dev,
               uint64_t, GameInputDeviceStatus current, GameInputDeviceStatus) {
    if (current & GameInputDeviceConnected) {
        // Read snapshots in a worker thread, push to MIDI.
    }
}

HRESULT init(void) {
    HRESULT hr = GameInputCreate(&g_input);
    if (FAILED(hr)) return hr;
    return g_input->RegisterDeviceCallback(
        nullptr, GameInputKindGamepad,
        GameInputDeviceConnected, GameInputBlockingEnumeration,
        nullptr, on_device, &token);
}

The key win: IGameInputDevice->GetDeviceInfo() returns a typed struct describing vendor, product, and capability bits. You know at runtime whether the connected device supports adaptive triggers, an integrated touchpad, motion sensors, or rumble — without VID/PID lookup tables. That is the cleanup XInput should have had in 2010.

Raw Input — the escape hatch

Some controllers refuse to behave. Third-party Switch Pro clones, off-brand fighting sticks, the Hori Mini, fitness bikes pretending to be HID gamepads. They show up under WM_INPUT on Raw Input but get filtered by GameInput's "is this really a gamepad" heuristics. Keep a Raw Input fallback that registers for the HID usage page 0x01 usage 0x05 and parses report descriptors directly. It is a pain — but it is the path of last resort. We cover the underlying choice in our hidapi vs native driver comparison.

Wiring the MIDI side

Windows still has no built-in virtual MIDI port. You need teVirtualMIDI (used by loopMIDI), or you ship a kernel driver. Our bridge embeds the teVirtualMIDI runtime, so users do not install anything separately — first-run handshake creates the port. The full virtual MIDI story across all three OSes is in our virtual MIDI port explainer.

// Send MIDI via teVirtualMIDI
#include "teVirtualMIDI.h"
LPVM_MIDI_PORT port = virtualMIDICreatePortEx2(
    L"UniversalControllerMIDI", nullptr, nullptr, 65535,
    TE_VM_FLAGS_INSTANTIATE_TX_ONLY);

void send_cc(BYTE ch, BYTE cc, BYTE val) {
    BYTE msg[3] = { (BYTE)(0xB0 | ch), cc, val };
    virtualMIDISendData(port, msg, sizeof msg);
}

Gotchas worth knowing

  • GameInput needs the GDK SDK headers — they are not in the base Windows SDK. Install via the Game Development Kit installer.
  • Steam Input on Windows hides DualSense behind a virtual Xbox controller when active. Detect Steam and either ask the user to exit, or unhook from Steam Input for non-Steam apps.
  • HKLM\\SOFTWARE\\Microsoft\\GameInput — flags here let users disable GameInput globally. Respect the flag; do not silently override.
  • XInput's "battery level" call is async and may return stale data for ~2 sec after connect. Do not trust the first read.
  • Bluetooth LE DualSense pairing on Windows still has packet loss above 1.5 m. USB-C is the only acceptable cable for a paid product.

Our recommendation for any new Windows gamepad-MIDI work in 2026: GameInput as the primary, XInput as a Win10-legacy fallback, Raw Input behind an "advanced" toggle. That is how Universal Controller MIDI ships, and the result is a bridge that handles DualSense, DualShock 4, Xbox Series, Switch Pro, MFi, and the long tail of HID weirdness without forcing the user to think about which stack is running.

Keep reading

More setup walkthroughs