Skip to content

Repository files navigation

HIDMaestro

HIDMaestro

"And we talk of Christ, we rejoice in Christ, we preach of Christ, we prophesy of Christ, and we write according to our prophecies, that our children may know to what source they may look for a remission of their sins." 2 Nephi 25:26

Glory, honor, and praise to the Lord Jesus Christ, the source of all truth and salvation, forever and ever.

You are warmly invited to visit ComeUntoChrist.org and learn more about Him and The Church of Jesus Christ of Latter-day Saints.


Total downloads Discord Website Documentation GitHub followers Follow on X

Virtual game controllers that look like real hardware to Windows. No kernel driver. No network. No reboot.

HIDMaestro creates virtual controllers that present the exact identity of real hardware across the whole Windows input stack at once. Pick from 231 built-in profiles or point it at a controller you own and clone it. DirectInput, XInput, SDL3, the browser Gamepad API, and WGI/GameInput all see the VID/PID, product name, HID descriptor, axis and button layout, and bus type the profile defines.

It runs entirely in user mode (UMDF2), signed with a locally trusted self-signed certificate. No EV certificate, no testsigning boot mode, no kernel driver that can blue-screen the machine.

231 device profiles · 20+ projects shipping it · ~35 µs median single-press · 0 kernel drivers

using var ctx = new HMContext();
ctx.LoadDefaultProfiles();
ctx.InstallDriver();
using var ctrl = ctx.CreateController(ctx.GetProfile("xbox-360-wired")!);
ctrl.SubmitState(new HMGamepadState { Buttons = HMButton.A });

Quick start

Requirements: Visual Studio 2022+ with the MSVC x64 and ARM64 build tools, Windows SDK/WDK 10.0.26100.0, .NET 10. Runs on x64 and ARM64 Windows. HIDMaestro.Core.dll is one AnyCPU assembly that carries a driver payload for each and picks the one matching the machine it is on.

# Build the native driver + companion for x64 and ARM64, then the SDK (idempotent).
scripts\build_all.cmd

# Minimal SDK consumer
dotnet run --project example\SdkDemo

# Full test app: cert + build + sign + install all automatic, requires elevation
cd test
dotnet build
bin\Release\net10.0-windows10.0.26100.0\win-x64\HIDMaestroTest.exe emulate xbox-360-wired

# Several controllers at once, any mix of profiles
HIDMaestroTest.exe emulate xbox-series-xs-bt xbox-360-wired dualsense

# List or search the 231 profiles
HIDMaestroTest.exe list
HIDMaestroTest.exe search thrustmaster

# Measure input latency
HIDMaestroTest.exe latency xbox-360-wired

# Validate every API (XInput, DirectInput, HIDAPI/SDL3, browser, WGI, HID order)
python scripts\verify.py --controllers 4

The test app is self-contained. First run creates a locally trusted certificate, extracts and signs the driver, installs it, creates the controllers, and starts feeding a test pattern. One console window, no popups, requires administrator.

During emulation you can remove 2 to dispose one controller, 2 dualsense to live-swap controller 2, or quit to shut down cleanly.

If you want to use HIDMaestro through a UI instead of code, install PadForge. It wraps this SDK with a full input-mapping app.


Who ships it.

Twenty-plus independent open-source projects have adopted HIDMaestro, across six languages, for a combined installed base of about 574,000 downloads as of September 2026. Roughly 27,700 of those builds ship or fetch the runtime directly.

PadForge

PadForge is the flagship implementation. It is built on this SDK end to end and drives more of the surface than any other consumer: every profile family, live profile swapping, multi-controller slots, force feedback, controller audio and haptics, the Valve personas, and the virtual VR controllers. Its Softpedia listing is an Editor's Pick at 5.0 out of 5.

To watch the SDK work without writing any code, install PadForge. If you are building your own app, it is the worked example of what this SDK does at full stretch.

Everyone else

Project What it does with HIDMaestro
foundation-sunshine Sunshine fork. Its DualSense support is built on HIDMaestro
JoystickGremlinEx Flight-sim remapper. Drives the SDK from Python over pythonnet
Nearcade Remote-play bridge. Pins the SDK as a submodule
dualsense-command Bundles the runtime in its bridge installers
DirectXInput Ships the runtime with its controller tooling
FlexInput Uses HIDMaestro as its default virtual-device backend

LizardByte's libvirtualhid, from the organization behind Sunshine, reuses this project's force-feedback descriptor byte for byte and says so in its own source:

HIDMaestro's MIT-licensed MinimumViablePidFfbBlock, byte-for-byte. The complete Output report set is required for DirectInput to enumerate the device as PID force-feedback capable.

Their header also describes the layout as "the constant-force and sine-periodic subset of the PID descriptor proven by HIDMaestro", and their alternatives table lists HIDMaestro beside ViGEmBus, inputtino and WinUHid.


Identity: exact hardware, down to the bus.

VID/PID, product string, descriptor, axis and button layout, and bus type all match the real device. A Bluetooth controller reports as Bluetooth, not as a USB device wearing its name.

  • Exact hardware identity. VID/PID, product string, HID descriptor, axis and button counts, trigger behavior, and bus type all come from the profile. SDL3's controller database matches it, Steam recognizes it, Chrome identifies it, joy.cpl shows the right name. A Bluetooth controller reports as Bluetooth, not as a USB device wearing its name.

  • Devices are JSON, not hardcoded. Add a controller by writing a data-only JSON profile or by capturing one you already own. No per-device source code, no recompile, no hardcoded device classes.

  • Data-driven profiles. Every controller is a JSON file. Adding support for a new one means writing JSON, not modifying code.

  • Sony pads answer the whole startup interrogation, not just the input reports. A game with native PlayStation support interrogates a DualSense before it will use it: firmware info, pairing info, then motion calibration. Serve a plausible-looking blob of zeros to any of them and the pad is refused, which is why a virtual controller can work in Steam Input and still be invisible to the game. Calibration is a divisor, so zeros make the consumer's sensitivity NaN. Firmware info is validated on content, and a real title abandons the device and retries every 500 ms on a zeroed reply, before it ever asks for calibration. HIDMaestro serves real payloads for all of them on both the UMDF2 and composite backends, byte-identical between them, with every field offset checked against the Linux hid-playstation driver and a second independent consumer.

  • Where hardware revisions disagree, the current one wins. A DualSense made in 2020 reports the product string Wireless Controller. A DualSense made today reports DualSense Wireless Controller. Both report bcdDevice 0x0100, so nothing on the wire distinguishes them and a profile can only serve one. As of v1.4.5 dualsense and dualsense-composite serve the current string, because a consumer keyed to the launch string is already broken against real modern hardware. The launch string stays reachable on dualsense-bt, whose dualsense-bt-full sibling carries the current one.

  • The same device on every life. A virtual controller keeps its device paths, container id and USB serial across a dispose and recreate, a process restart, a reboot and a driver upgrade, so a program that stored a binding against the path or the serial keeps it. Pass an identity key that means something to you, or pass none and the controller index is the key:

    using var pad = ctx.CreateController(profile, "slot0");   // same paths every time

    Every family is covered: plain HID parents take an explicit instance id with the child's ParentIdPrefix written before registration, the Xbox families take a fixed software-device tuple, and the composite personas serve a serial derived from the key. A different profile at the same key keeps the identity and refreshes the descriptor. Verified by a battery that measures parent, child, interface path, DirectInput GUID, SDL3 path and USB serial across nine lives per family. How this works.

  • Protocol controllers, not just passive HID. The Nintendo Switch Pro Controller is not a passive device: hosts drive a Nintendo subcommand handshake and stall without a device that answers. HIDMaestro's driver answers it over the real Bluetooth wire (the shipped descriptor is extracted byte-exact from a live Pro's SDP cache): SPI calibration reads, input-mode switch, 60 Hz full-mode streaming with gyro and accel at the 49-byte Bluetooth report size. SDL3's HIDAPI driver and Steam Input bind it as a real Bluetooth Pro Controller with motion and rumble. Before any protocol host arrives, the pad streams genuine 12-byte 0x3F simple-mode frames, the one report DirectInput can parse, so joy.cpl reads a working controller in exactly the states real hardware allows.

Custom controllers

The HidDescriptorBuilder, HMProfileBuilder, and HMDeviceExtractor APIs let you build or modify any device:

  • Clone and modify. Take a DualSense (15 buttons) and ship a 16-button variant. Windows, Steam, and games still see "DualSense" because the VID/PID and product string are preserved.
  • Build from scratch. Define a flight stick, racing wheel, or arcade panel with arbitrary VID/PID, axis count, button count, and resolution. No hex editing.
  • Capture a real device. HMDeviceExtractor.Extract reads the HID descriptor Windows parsed from any device you have plugged in and returns a ready-to-deploy profile. Point it at the controller, get a matching virtual.
// Clone a DualSense and add a button
var custom = new HMProfileBuilder()
    .FromProfile(ctx.GetProfile("dualsense")!)
    .Id("dualsense-16btn")
    .Descriptor(new HidDescriptorBuilder()
        .Gamepad()
        .AddStick("Left", 8).AddStick("Right", 8)
        .AddTrigger("Left", 8).AddTrigger("Right", 8)
        .AddButtons(16).AddHat()
        .Build())
    .InputReportSize(9)
    .Build();
using var ctrl = ctx.CreateController(custom);

Adding a controller to HIDMaestro is a data change. Adding one to a code-per-device emulator means writing and compiling a new device implementation. That difference is the whole point of the profile system.


Every API at once.

DirectInput, XInput, SDL3, the browser Gamepad API, and WGI/GameInput all see one correct device.

  • One device, every API. DirectInput sees correct axes and buttons. XInput sees separate triggers in one slot. SDL3/HIDAPI sees the right identity and bus type. The browser sees a STANDARD GAMEPAD with separate triggers. WGI sees one Gamepad. How this works.
  • Multiple controllers at once. No hard limit. Verified with 6 mixed controllers, correct per-controller ordering across all APIs. XInput caps Xbox-family profiles at its own 4 slots.
  • Force feedback. HID PID 1.0 answers for DirectInput FFB games, plus rumble/haptic output events the consumer routes to real hardware.
  • Hot-plug. Create and remove controllers with no reboot. Live-swap a controller's profile mid-session. Warm single-controller create is ~200 ms.
  • Validated across every API and both ends of the spectrum. A 60-scenario regression battery checks DirectInput, XInput, SDL3/HIDAPI, the browser Gamepad API, and WGI on every change. It passes on a 16-core Windows 11 desktop, and the 57-scenario v1.7.3 battery also passed on a low-power Intel Atom Windows 10 fixture.

Validation

Tested on Windows 11 IoT Enterprise LTSC 2024 (build 26200) and Windows 10 IoT Enterprise LTSC (build 19044), with a self-signed certificate in the machine's Root and TrustedPublisher stores and no test-signing boot mode. Every profile is checked across all the input APIs a game can reach: DirectInput (joy.cpl and DirectInput8), XInput, SDL3/HIDAPI, the Chrome Gamepad API, WGI/GameInput, and HID enumeration order, through scripts/verify.py plus manual verification. A real Xbox Series X|S Bluetooth controller tested side by side shows byte-identical behavior across the HID class APIs.

Profile DirectInput XInput SDL3 Browser WGI
Xbox 360 Wired 5 axes, 10 btns 1 slot, separate triggers &IG_ path, USB STANDARD GAMEPAD 1 entry
Xbox Series BT 5 axes, 16 btns 1 slot, separate triggers &IG_ path, Bluetooth STANDARD GAMEPAD 1 entry
DualSense (PS5) 6 axes, 15 btns N/A USB Detected N/A
6-controller mixed All 6 visible 4 slots (XInput cap) 4 IG + 2 live 4 pads (Chrome cap) All 6 visible

The Xbox Series BT row shows 16 buttons because Windows' xinputhid synthesizes a 16-button layout over the 12-button source descriptor. Details.

A 60-scenario live-swap regression battery drives every create / swap / remove / force-kill sequence, the FFB round-trip, the Sony vendor-blob encode/decode, the composite USB personas end to end through the real USB stack, the battery reply a pad gives XInput, and the device identity of every family across nine lives, verifying no PnP devnodes are left behind. 60/60 PASS on a 16-core AMD Ryzen 9 Windows 11 desktop. The 57-scenario v1.7.3 battery also passed 57/57 on a 4-core Intel Atom Z8350 Windows 10 fixture, the low end of the performance and OS spectrum.

Full device-tree dumps, HIDAPI enumeration logs, per-profile results, and startup/teardown timing are in docs/INTERNALS.md.

Xbox Series X|S BT, Xbox 360 Wired, and DualSense across Device Manager, joy.cpl, Chrome Gamepad Tester, and PadForge/SDL3

Xbox Series BT across all tools Xbox 360 Wired across all tools DualSense across all tools


Latency: lower, measured.

  • Lower latency, measured. ~35 µs median single-press, more than 4x faster than the closest alternative, with no batching cap on input. Output (rumble, FFB, LED) is event-driven as of #34: ~0.15 ms median from the game's write to the consumer callback, down from the 9.4 ms poll-quantized path, and idle controllers cost zero measurable CPU.
  • No network in the path. Input travels through shared memory on the same machine. There is no socket, no USBIP stack, and no kernel transport driver between your application and the device.
  • Fast to start, fast to recover. When the installed driver already matches, InstallDriver() completes in ~40-60 ms and a consumer goes from process start to a live controller in about a second. If the consuming app is force-closed mid-session, the next launch evicts the orphaned devices and has the first controller live again in ~2.3 s, measured with an Xbox Series BT profile in the mix (the deepest teardown stack).

Measured input latency from SubmitState to the input surfacing through XInput, single button press, 10,000 iterations on the same host:

Single-press latency
HIDMaestro (measured) ~35 µs median, worst case under 1 ms
VIIPER (their published Windows figure) 168 µs

Reproduce it yourself: HIDMaestroTest.exe latency xbox-360-wired. The harness shares one clock between the writer and the reader and detects the actual button bit changing, so the number is real propagation, not poll quantization. Full methodology and per-run numbers: docs/testing/latency.md.

HIDMaestro talks to its driver through a shared-memory section on the same machine. There is no socket and no network stack in the path. Both directions are event-driven: each SubmitState signals the driver immediately with no fixed batching interval, and the driver signals each captured output packet (rumble, FFB, LED) back to the consumer at ~0.15 ms median instead of the pre-#34 8 ms poll. Idle controllers cost zero measurable CPU. Output methodology and before/after numbers: docs/testing/latency.md.

Input latency, lower is better: HIDMaestro 0.035 ms versus VIIPER at 0.168 ms localhost, 1-5 ms over wired LAN, and 10-50 ms over Wi-Fi

The network bars above use the optimistic end of each LAN range. Even granting VIIPER the best case, its Wi-Fi path is hundreds of times longer than HIDMaestro's shared-memory path.

A note on the comparison, and the layering question

A note on the comparison. VIIPER is "Virtual Input over IP", built on USBIP and driven over a TCP API or an in-process library. Its sub-millisecond figures are measured over localhost only. Its own benchmark doc says so plainly: "remote/network USBIP attachment will add network RTT and jitter which is intentionally excluded from these baseline figures." Round-trip time there is how long an input takes to travel to another machine and come back, and jitter is how much that delay varies from moment to moment. The headline latency excludes the network path the project is named for, and VIIPER batches reports every millisecond, capping the update rate at 1000 Hz.

The moment you run it over an actual network, that excluded round-trip time dominates: roughly 1 to 5 ms added over wired LAN, and 10 to 50 ms over Wi-Fi, on top of the localhost figure rather than instead of it. That is one to two orders of magnitude above the sub-millisecond number the docs lead with, and the caveat that explains the gap is a single line buried in a testing doc. Anyone who reads "well below 1 millisecond" and pictures networked play is being pointed at the wrong number.

There is also a layering question worth naming. Networking is a transport concern that belongs in the application, not in the virtual-device driver. Building USBIP-over-IP into the emulator puts a kernel USBIP driver and a listening socket in front of every user, including the ones who only ever drive a controller on the same machine. HIDMaestro keeps the device layer local: the SDK writes input to a shared-memory section, and nothing in the driver knows or cares whether that input came from the local process or was relayed by the consumer from another machine. If you want input over a network, that is the application's job to own and secure, without dragging a network stack and a kernel transport driver through every local use of the device. Consumers already do this at the right layer: PadForge, built on HIDMaestro, shares controllers across PCs over a network with its Remote Link feature (added in 3.4.0), both directions with feedback returning to the real device, with zero latency added for anyone playing locally.

And there is the matter of what USBIP can represent at all. USB/IP transports USB, so every device VIIPER creates is a USB device to Windows. The controllers people actually use often are not: an Xbox or DualSense paired over Bluetooth enumerates as a Bluetooth device, and SDL3 and Chromium parse it through different code paths because of that bus type. A USB-only emulation reports the wrong bus for those controllers. HIDMaestro sets bus type per profile, so a Bluetooth controller presents as Bluetooth (HIDAPI reports bus_type = Bluetooth), matching the hardware it stands in for.


Nothing in the kernel.

  • No kernel driver, no test-signing. Pure user-mode UMDF2, loaded by a locally trusted self-signed certificate. No test-signing boot mode, no purchased certificate, no reboot, and a bug cannot blue-screen the machine. It installs on an ordinary user's PC, not just a developer box.
  • No kernel driver. UMDF2 runs the driver in a normal user-mode process. A bug cannot blue-screen the machine. Self-signed certificate trusted by the local machine is enough.

How it works

HIDMaestro is a UMDF2 HID minidriver hosted by Windows' own mshidumdf.sys, fed input through a per-controller shared-memory section. XInput for Xbox profiles comes from a companion device that registers the XUSB interface. Bluetooth identity, the &IG_ enumerator behavior, the separate-trigger descriptor trick, the WGI admission path, and the XInput slot allocator are all documented in docs/INTERNALS.md.

Why user mode is enough: the HID class driver already lives in the kernel (mshidumdf.sys), XInput discovery uses a device interface a user-mode driver can register, GameInput reads HID reports rather than driver internals, and bus type and VID/PID are settable from user mode. See Why UMDF2 Is Enough.


Controller audio and haptics: composite USB personas

A real USB DualSense is a four-interface composite: USB Audio Class speaker/haptics out, microphone in, and HID. UMDF2 can present exactly one HID interface, so the standard dualsense profile stops there. As of v1.4.0 three additional profiles present the full composite. They are the ones named Full in a profile picker, which is the catalog's marker for the most capable profile of a given device:

  • dualsense-composite: the real pad's four interfaces, byte-for-byte from a hardware descriptor dump. The OUT stream is 4-channel 48 kHz where channels 1/2 are the speaker and channels 3/4 drive the voice-coil actuators. That stream is the only path on Windows by which a game hands a controller its authored haptic waveforms, and it surfaces on the SDK as HMController.UsbAudio.Output with per-channel roles. The microphone is UsbAudio.Microphone: feed PCM, Windows records it from a real "Headset Microphone (Wireless Controller)" endpoint.
  • dualsense-edge-composite: the Edge's four interfaces from a physical Edge's full USB probe. Same speaker/haptics stream and microphone as the base pad, the Edge's own 389-byte HID descriptor, and the Edge's real 1 ms USB input polling.
  • dualshock-4-v2-composite: the DS4 v2's composite, with headset audio and a mono microphone. The hardware has no haptics lane.

The original DS4 v1 (054C:05C4) has no composite variant for a reason worth stating: real hardware probes show it presents a single HID interface over USB with no audio class at all. USB audio arrived with the v2.

A composite persona is a real Sony pad at every level a filter can inspect, which is what the audio class driver requires and is not negotiable. That leaves a host with nothing of its own to recognise, so the emulated host controller these personas sit behind carries a second hardware ID, ROOT\HIDMAESTRO_UDE, alongside its upstream one. An application that already excludes its own virtual pads by looking for HIDMAESTRO in a device's hardware IDs keeps working if it walks far enough up: from the persona's HID interface that node is four parents away. Nothing is added to the persona itself.

That same invisibility is why cleanup has to reach the personas by a different route. The device sweep behind RemoveAllVirtualControllers walks the ROOT and SWD enumerators for the HIDMAESTRO token, and a composite carries none, so before v1.4.5 a consumer that created one and exited left a USB DualSense enumerated on the machine with nothing left running to feed it. The sweep now detaches every persona this SDK owns from the emulated host controller first, then walks the enumerators as before. Personas belonging to another live process are detached too, which is the intent: a consumer asking for a clean machine gets one.

using var ctrl = ctx.CreateController(ctx.GetProfile("dualsense-composite")!);
ctrl.UsbAudio!.Output.FramesReceived += (out_, pcm) => { /* speaker + haptic PCM */ };
ctrl.UsbAudio.Microphone.Submit(micPcm);

That is the whole setup. Composite personas create like any other profile, because the USB transport they need ships inside HIDMaestro.Core.dll and installs itself the first time one is created, exactly the way the UMDF2 driver already does. No second package, no separate download, nothing for a user to go find. The bundled component is usbip-win2 0.9.7.5, BSD-2-Clause, Microsoft-signed for x64 and ARM64, redistributed unmodified with its notice, and verified against the upstream release's published SHA256 both when the SDK is built and again before it is ever executed. A machine that HIDMaestro itself put on usbip-win2 0.9.7.7, which is what releases through v1.8.1 installed, is moved to 0.9.7.5 automatically: the SDK replaces the host controller driver through Windows' own driver update, leaves the root-hub filter alone so no USB device drops, and does it only in a Windows session where nothing has used the controller yet. Until such a session comes, 0.9.7.7 keeps working. Any other usbip-win2 version was put there by another program and is left as it is. Set HIDMAESTRO_KEEP_TRANSPORT=1 to keep 0.9.7.7. Windows re-enumerates the USB root hubs once during that one-time install, so devices blink for a moment on the very first composite controller a machine ever creates.

Every device behavior stays in HIDMaestro's own user-mode code: the SDK runs an in-process USB/IP device server on loopback, including the 1 ms isochronous audio pacing. The version pin is deliberate. 0.9.7.5 is the last release to publish an ARM64 build before 0.9.8.0, 0.9.7.8 has two open kernel-pool-corruption reports (usbip-win2#180, usbip-win2#181), and nothing between 0.9.7.5 and 0.9.7.7 touches the interface HIDMaestro uses.

Measured on the Atom Z8350 floor machine: full 4-channel render and live microphone capture through usbaudio.sys with no frame starvation, attach in ~316 ms, and idle cost with the transport installed but no device attached indistinguishable from baseline (0.35% vs 0.24% CPU). The composite path runs as scenario S45 of the battery, so a broken persona fails the release gate like anything else.

Valve-recognized Steam devices

The same machinery answers a different problem. The plain steam-deck and steam-controller profiles carry Valve's real ids over standard gamepad descriptors, so Steam files them under Generic DirectInput and none of Steam Input's Valve-device treatment applies: no gyro lane, no trackpads, no HD haptics, no Valve button prompts. Recognition follows the device a real unit presents, not the ids alone. Three personas present those devices.

steam-deck-composite (28DE:1205) reproduces a real unit's whole USB identity from its lsusb dump, because that identity is what Steam inspects: bcdDevice 3.00, product string Steam Controller, a serial string, wTotalLength 150 and five interfaces: mouse on 0, keyboard on 1, the vendor-page controller on 2, then an Interface Association Descriptor and the CDC ACM pair on 3 and 4. Input is the 64-byte Neptune frame (ID_CONTROLLER_DECK_STATE, header 01 00 09 40), packed from SteamDeckStatePacket_t and cross-checked against a 25,000-frame capture of real hardware.

steam-controller-composite (28DE:1102) is the wired 2015 controller. Every descriptor is verbatim from a real unit: the device and configuration blobs, the 63-byte keyboard and 56-byte mouse report descriptors its lizard mode drives, and the 33-byte vendor-page controller descriptor. Three interfaces, because SDL binds the pad only on interface 2. Its frame is ValveControllerStatePacket_t; the pad has one stick and no separate stick field, so with the finger-down bit clear sLeftPadX/Y is the joystick, which is how the hardware reports it.

steam-controller-2 (28DE:1302) is the 2026 controller, the one SDL calls Triton. One HID interface addressing everything by report id, with the 372-byte descriptor and the attribute values Steam validates taken from two independent reads of real hardware. Its frame is the 54-byte TritonMTUFull_t on report 0x42.

All three answer the GET_REPORT interrogation Steam performs before it will claim a device, and all three are verified end to end by battery scenarios S51 and S52: S51 pins descriptors, endpoints and feature answers with no device; S52 creates each persona, drives it through SubmitState, and reads the frame back off the real HID stack to confirm input reaches a consumer.

Virtual VR controllers

A VR controller is not an OS device: games ask the VR runtime "where is the left hand, and what is its trigger doing." So this subsystem is a native OpenVR driver that SteamVR's own vrserver loads, embedded in HIDMaestro.Core.dll and registered with one call. One driver covers native OpenVR games and OpenXR games running on SteamVR, which is the default PCVR configuration.

HMVR.EnsureDriverRegistered();          // one-time, content-hash idempotent
using var vr = new HMVRController();    // both hands appear in SteamVR
vr.SubmitState(in state);               // buttons, trigger/grip, stick, optional full pose - per frame
vr.HapticReceived += (_, e) => ...;     // every haptic pulse a VR app plays, with hand attribution
var head = vr.GetHmdPose();             // the real headset pose, for head-as-input mapping

The hands hold real SteamVR hand roles, serve the modern input system through a full input profile, and serve legacy GetControllerState readers through a shipped legacy binding. Controllers exist only while a consumer is live, so an idle machine shows no phantom devices. SteamVR is the one dependency, and it installs Steam-client-free and account-free via Valve's own steamcmd +login anonymous +app_update 250820. The whole loop is machine-verified on headless rigs with no headset (battery scenario S50): enumeration, roles, input, haptics, and a 90 Hz legacy state stream read back through Valve's own client API. Full docs.


How it compares

Tool by tool
HIDMaestro VIIPER ViGEmBus vJoy WinUHid
Kernel driver required No (UMDF2 user mode) Yes (USBIP) Yes Yes No (UMDF2 on VHF)
Installs without test-signing mode Yes Yes Yes Yes No (ships test-signed)
EV certificate for new builds No No (uses signed usbip-win2) Yes ($300+/yr) Yes No (OV cert for x64)
Network play App layer via consumers (PadForge Remote Link), zero local penalty In the driver: +1-5 ms wired, +10-50 ms Wi-Fi No No No
Identity per controller Exact, 231 profiles 6 fixed device types 2 fixed types Fixed "vJoy Device" 4 presets, or raw descriptor
Bus type fidelity Per-profile, incl. Bluetooth USB only (USBIP) USB only USB only USB only
Add a new device JSON file, or capture one you own Write Go (a few hundred lines/device) N/A N/A Write C, or raw descriptor
Local single-press latency ~35 µs measured 168 µs published (localhost) N/A N/A Not published
Input update rate Event-driven, no fixed cap 1000 Hz (1 ms batching) N/A N/A Event-driven
License MIT GPL-3.0 (clients MIT) N/A N/A MIT
Status Active Active Retired Stale Active

VIIPER describes itself as running entirely in userspace. On Windows that holds only for its device code: the USB/IP transport is a third-party kernel-mode driver, and VIIPER makes you go install it. HIDMaestro's own driver is user-mode UMDF2, and it rides the host that already ships with Windows, so the "no kernel driver" row above is literal for everything the standard profiles do. The composite USB personas are the one deliberate exception, and they use the same signed usbip-win2 transport VIIPER does, because a Windows audio endpoint requires a driver-backed USB device and no user-mode API can create one. The difference is what the user has to do about it: HIDMaestro ships that transport inside its own DLL and deploys it on demand, so a composite persona is a profile you pick, not a prerequisite you chase. Use any other profile and no kernel-mode component is ever installed.

HIDMaestro is Windows optimized and focused on game controllers and HID game devices. Within that scope it gives you exact hardware identity with no kernel driver, no network layer, and no per-device code. That combination is what HIDMaestro is built for, and nothing else on this list delivers it.

What it replaces

  • VIIPER: needs a kernel-mode USB/IP driver on Windows despite the userspace billing, makes the user install it, and presents every controller as USB so Bluetooth devices report the wrong bus type. Its headline latency is localhost-only and already 4 to 5 times higher than HIDMaestro's even there. The network it is named for adds another 1 to 50 ms on top.
  • vJoy: kernel driver, no longer actively maintained, shows up as "vJoy Device" instead of real hardware.
  • ViGEmBus: kernel driver, retired, new builds need an EV code-signing certificate.
  • DsHidMini: user-mode, but translates a physically connected DualShock 3 rather than arbitrary input.

Known Limitations

  • Windows optimized. HIDMaestro is built on UMDF2, mshidumdf, and the Windows HID/XInput/WGI stack. There is no Linux or macOS build.
  • Output is delivered, not routed to hardware. The driver accepts rumble/haptic/FFB writes and raises OutputReceived to the consumer. Sending those to a physical controller is the consumer's job (PadForge does this).
  • Auth-chip controllers. PS4/PS5 online and Nintendo Switch Online require cryptographic authentication from real controller hardware. HIDMaestro cannot replicate authentication chips.
  • Vendor-specific feature reports. LED control, calibration, and firmware-update reports vary per device and need per-controller work.
  • Anti-cheat. Virtual devices are detectable by kernel-level anti-cheat. HIDMaestro does not hide that it is virtual.

Security and Scope

HIDMaestro replicates the public-facing identity and input/output behavior of game controllers. It does not replicate cryptographic authentication, implement vendor-private protocols unless a profile adds them, bypass anti-cheat, or modify data from physical controllers.

Credits

  • DsHidMini by Nefarius Software Solutions. HIDMaestro builds on the UMDF2 + xinputhid approach Nefarius pioneered in DsHidMini, which demonstrated that a user-mode driver framework can replace kernel-mode drivers for controller emulation on Windows. The mshidumdf HID proxy, WUDFRd reflector, and xinputhid XInput bridge are the foundation of HIDMaestro's stack.
  • HIDAPI: bus-type detection behavior informed the BTHLEDEVICE spoofing technique.
  • SDL3: multi-backend fallback behavior informed the &IG_ enumerator trick. SDL3 is not a dependency. HIDMaestro is validated against it.

Donations

There is no tip jar. Knowing HIDMaestro is useful is reward enough. If you insist on giving something, give it to a charity instead and bless humanity. Short of one you already trust, Humanitarian Services of The Church of Jesus Christ of Latter-day Saints puts it to work directly, and give.org grades charities against twenty standards if you would rather choose your own. The upstream projects listed above have earned it as much as anyone. They made all of this possible.

Anonymously, preferably. Not at all is fine too, and nothing here changes either way.

My promise: HIDMaestro will never become paid, freemium, or Patreon early-access paywalled. Free means free.

License

MIT License. See LICENSE for details.

About

Virtual game controllers for Windows that show up as real hardware to every API: DirectInput, XInput, SDL3, browser Gamepad, WGI/GameInput. Byte-exact VID/PID and HID descriptor per profile. 228 profiles across 32 vendors. HID PID 1.0 force feedback, hot-plug, multi-controller. Pure user mode: no kernel driver, no EV cert, no reboots.

Topics

Resources

Stars

104 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages