Files
integration/Docs/Getting-Gamepad-USB-Device-Name.md
2026-05-05 16:26:49 -04:00

5.1 KiB

Getting the Gamepad USB Device Name

Unreal exposes UInputDeviceSubsystem::GetMostRecentlyUsedHardwareDevice(), which returns an FHardwareDeviceIdentifier. In stock Unreal this is nearly useless: on Windows it toggles between "WindowsApplication" (KBM) and "XInputController" (any XInput pad); on Linux it returns nothing meaningful at all. It cannot distinguish a DualSense from an Xbox pad.

The USB-reported device name (e.g. "Sony Interactive Entertainment Wireless Controller") is what we want to surface. This document describes the changes required on each platform to put that name into FHardwareDeviceIdentifier.

How FHardwareDeviceIdentifier Gets Populated

Input drivers wrap their MessageHandler->OnControllerButton/Analog calls in an FInputDeviceScope (stack-allocated, thread-local stack of pointers). The scope carries InputDeviceName (driver class) and HardwareDeviceIdentifier (physical device string). When the input event reaches UInputDeviceSubsystem, those two fields are copied into the FHardwareDeviceIdentifier record for that user.

So the fix on each platform is the same shape: at device-connect time, cache the USB device-name string; in the per-event scope, pass it as the HardwareDeviceIdentifier.

Windows: Patch the GameInput Plugin

Location: Engine/Plugins/Runtime/GameInput/Source/GameInputBase/.

The Microsoft GameInput SDK already exposes the data we need on every device via GameInputDeviceInfo::displayName (a wchar_t*). The plugin reads it for log lines (GameInputUtils.cpp:18-23) but does not propagate it.

Steps:

  1. Cache at connect. Add FString CachedDisplayName to FGameInputDeviceContainer. In its constructor / device-info init path (GameInputDeviceContainer.cpp:44, where Info is in scope), set:

    CachedDisplayName = FString(Info->displayName);
    

    TCHAR == wchar_t on Windows, so the conversion is direct. Lifetime matches the container — cleared automatically on disconnect.

  2. Make the container reachable from FGameInputEventParams. The processor already gets a Device pointer; add a Container pointer so GetHardwareDeviceIdentifierName can read the cached string.

  3. Return it from GetHardwareDeviceIdentifierName (GameInputDeviceProcessor.cpp:61). After the existing bOverrideHardwareDeviceIdString block, before the family-bucket switch:

    if (Params.Container && !Params.Container->CachedDisplayName.IsEmpty())
    {
        return Params.Container->CachedDisplayName;
    }
    

    The override path still wins (explicit dev intent); the family-bucket fallback handles devices with empty displayName.

Total: ~10 lines plus the field. All inside #if GAME_INPUT_SUPPORT.

The user must enable the GameInput plugin and disable the default XInputDevice plugin (otherwise both drivers will dispatch events for the same pad).

Linux: Patch LinuxApplication.cpp

Location: Engine/Source/Runtime/ApplicationCore/Private/Linux/LinuxApplication.cpp.

SDL already provides the device name via SDL_GameControllerName(). The Linux path calls it once for a log line in AddGameController (line 2175) and then discards it. The Linux path also does not push an FInputDeviceScope at all around its controller events — that's the second half of why GetMostRecentlyUsedHardwareDevice is useless on Linux.

Steps:

  1. Store the name. Add FString DeviceName to SDLControllerState. In AddGameController (~line 2175), assign:

    ControllerState.DeviceName = UTF8_TO_TCHAR(SDL_GameControllerName(Controller));
    
  2. Wrap controller-event dispatch in a scope. Around the MessageHandler->OnControllerButton*/OnControllerAnalog block (starting ~line 518):

    FInputDeviceScope Scope(
        nullptr,
        FName("LinuxApplication"),
        ControllerState.DeviceId.GetId(),
        ControllerState.DeviceName);
    // ... existing dispatch ...
    

    Use "LinuxApplication" for InputDeviceName to mirror the Windows convention.

Total: ~12 lines. No third-party dependencies, no engine plugins, no per-device config tables.

What This Does Not Solve

  • Stock XInput on Windows. If we keep the default XInputDevice plugin, events continue to come through XInputInterface.cpp with a fixed "XInputController" identifier. XInput itself does not expose VID/PID or device strings. Recovering the name there requires correlating the XInput slot with a Raw Input HID via the IG_xx token in the Raw Input device path, then calling HidD_GetProductString — a separate, more involved patch. GameInput is the cleaner answer on Windows.

  • XInput-wrapped pads under GameInput. A DualSense routed through Steam Input or DS4Windows will surface as the wrapper's display name (e.g. ViGEm), not as the underlying physical device. This is a property of the wrapper, not fixable from our side.

  • displayName is not a stable identifier. It is a human-readable name whose exact text varies by OS version, driver, and USB descriptor. Use it for prompt-set selection and logging. Do not use it as a save-game key.