From d09b9fedc6e3b0b6304e27ae34b559f190c061f0 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 11 Sep 2026 16:55:16 -0400
Subject: [PATCH 1/6] feat(windows): add user-mode Xbox 360 gamepads
Create broker-owned software devices backed by a dedicated UMDF2 XUSB companion while retaining a correlated VHF HID child for DirectInput compatibility.
Route token-authenticated native XInput state and rumble through the Windows backend, extend the driver package and wire protocols, and add installed-consumer coverage and documentation for the XUSB compatibility boundary.
---
README.md | 20 +-
docs/maintainer/store-review-validation.md | 7 +-
docs/platform-support.md | 40 +-
docs/windows-driver.md | 106 +-
gh-pages-template/virtual-hid-driver.html | 8 +-
scripts/windows/test-browser-gamepad.ps1 | 3 -
scripts/windows/test-installed-driver.ps1 | 33 +-
src/platform/windows/broker/CMakeLists.txt | 5 +-
.../windows/broker/libvirtualhid_broker.cpp | 105 +-
.../broker/xbox_360_software_device.cpp | 509 +++++++++
.../broker/xbox_360_software_device.hpp | 51 +
src/platform/windows/control_protocol.hpp | 51 +
src/platform/windows/driver/CMakeLists.txt | 41 +-
.../windows/driver/libvirtualhid.inf.in | 76 ++
.../windows/driver/libvirtualhid_umdf.cpp | 3 +-
.../driver/libvirtualhid_xbox360_umdf.cpp | 973 ++++++++++++++++++
.../shared/lvh_windows_broker_protocol.h | 2 +-
.../windows/shared/lvh_windows_protocol.h | 10 +-
.../windows/shared/xbox_360_protocol.hpp | 118 +++
src/platform/windows/windows_backend.cpp | 206 +++-
tests/CMakeLists.txt | 1 +
.../fixtures/windows_backend_test_hooks.hpp | 2 +-
tests/fixtures/windows_backend_test_hooks.cpp | 7 +-
tests/unit/test_license.cpp | 4 +-
tests/unit/test_runtime.cpp | 6 +-
tests/unit/test_windows_backend.cpp | 2 +-
tests/unit/test_windows_consumers.cpp | 69 +-
tests/unit/test_windows_protocol.cpp | 46 +-
28 files changed, 2395 insertions(+), 109 deletions(-)
create mode 100644 src/platform/windows/broker/xbox_360_software_device.cpp
create mode 100644 src/platform/windows/broker/xbox_360_software_device.hpp
create mode 100644 src/platform/windows/driver/libvirtualhid_xbox360_umdf.cpp
create mode 100644 src/platform/windows/shared/xbox_360_protocol.hpp
diff --git a/README.md b/README.md
index 655a27d..531a5cc 100644
--- a/README.md
+++ b/README.md
@@ -38,8 +38,8 @@ types.
Consumers work with portable concepts such as runtimes, device profiles,
normalized gamepad state, output callbacks, and device nodes. Platform-specific
-details such as Linux `uhid`/`uinput` or the Windows UMDF/VHF driver package stay
-behind backend implementations.
+details such as Linux `uhid`/`uinput`, Windows VHF, or the Windows Xbox 360 XUSB
+companion stay behind backend implementations.
## 🎮 Capabilities
@@ -49,9 +49,10 @@ behind backend implementations.
through Linux `uhid`; Generic and Xbox 360 gamepads plus keyboard, mouse,
touchscreen, trackpad, and pen tablet devices through `uinput`. Xbox One and
Xbox Series fall back to `uinput` when `uhid` is unavailable.
-- Windows gamepads, keyboards, and Raw Input-visible mice through a user-mode
- UMDF2 control driver backed by Virtual HID Framework, with Win32 keyboard and
- mouse fallbacks when the licensed driver path is unavailable.
+- Windows gamepads, keyboards, and Raw Input-visible mice through user-mode
+ UMDF2 drivers. Xbox 360 uses a broker-owned XUSB software-device personality;
+ the other profiles use Virtual HID Framework. Win32 keyboard and mouse
+ fallbacks remain available when the licensed driver path is unavailable.
- Output callbacks for profile-specific feedback such as ordinary and trigger
rumble, RGB and player LEDs, adaptive triggers, and raw HID output reports
when available.
@@ -111,8 +112,9 @@ devices from the OS, or ship a Windows kernel-mode driver.
Linux and Windows provide virtual-device backends. Linux uses standard
user-space kernel interfaces. Windows remains user-mode: the C++ library talks
-to a UMDF2 control driver, and the driver publishes HID gamepads, keyboards,
-and mice through VHF.
+to a broker and UMDF2 package. The package publishes Xbox 360 through XUSB and
+VHF together, and publishes the other gamepads, keyboards, and mice through
+VHF.
macOS currently provides a limited CoreGraphics synthetic-input backend for
keyboard and mouse only. It is not a virtual-HID backend and does not yet
support gamepads; native macOS virtual-HID gamepad support is planned.
@@ -125,6 +127,10 @@ same API where the backend exposes them.
- The published Windows driver installer is AMD64-only. Windows ARM64 release
packages require a different Microsoft driver-signing path.
+- Xbox 360 support implements the undocumented XUSB wire contract used by the
+ inbox XInput stack. It must be regression-tested on each supported Windows
+ release; Microsoft does not provide a supported public virtual-XInput driver
+ API.
## 🔁 Alternatives
diff --git a/docs/maintainer/store-review-validation.md b/docs/maintainer/store-review-validation.md
index e06da45..a8eaa23 100644
--- a/docs/maintainer/store-review-validation.md
+++ b/docs/maintainer/store-review-validation.md
@@ -89,8 +89,11 @@ C:\ProgramData\libvirtualhid\install-driver.log
## Scope Notes
-The `x360` profile is not used for Store review. The Windows UMDF/VHF backend is
-HID-only and intentionally does not emulate the Xbox 360 XUSB stack.
+The default Store-review path still uses Xbox Series and does not exercise the
+`x360` profile. The package also installs the separate Xbox 360 XUSB companion;
+validate that path with `test-installed-driver.ps1 -GamepadProfile x360` and the
+installed-driver `Xbox360PublishesXInputStateAndRumble` integration test before
+claiming Xbox 360 compatibility for a release.
The reviewer-visible success signal is the installed `ROOT\LIBVIRTUALHID`
control device, the `\\.\LibVirtualHid` control path, the running
diff --git a/docs/platform-support.md b/docs/platform-support.md
index d9f4c33..b15d869 100644
--- a/docs/platform-support.md
+++ b/docs/platform-support.md
@@ -26,20 +26,23 @@ Use capability queries for behavior such as:
The Windows backend keeps the normal C++ library buildable with MSVC and
MinGW/UCRT64. Gamepad creation, Raw Input-visible keyboard input, and Raw
-Input-visible relative mouse input use a user-mode UMDF2 control driver and
-Windows Virtual HID Framework. Keyboard text input, absolute mouse input, and
-the keyboard and mouse fallbacks use Win32 APIs.
+Input-visible relative mouse input use a user-mode UMDF2 package. Xbox 360 uses
+a per-controller XUSB software device plus a correlated VHF child; other
+profiles use the root control driver and Windows Virtual HID Framework.
+Keyboard text input, absolute mouse input, and the keyboard and mouse fallbacks
+use Win32 APIs.
The C++ library communicates with the driver through fixed-size protocol
structures and `DeviceIoControl`, not C++ STL types. This keeps the public API
compiler-neutral and preserves the boundary between the MinGW/MSVC client
library and the WDK/MSVC driver package.
-When the driver is installed and licensed, the backend publishes HID gamepads,
-keyboards, and mice that standard HID and Raw Input consumers can enumerate.
-Gamepad consumers include SDL/HIDAPI, DirectInput,
-Windows.Gaming.Input/GameInput, and browser Gamepad API clients. XInput is not
-a direct target of the HID backend.
+When the driver is installed and licensed, the backend publishes gamepads,
+keyboards, and mice that standard Windows consumers can enumerate. Xbox 360 is
+a direct XInput/XUSB target while retaining a HID/DirectInput view; the other
+gamepads are descriptor-driven VHF devices. Consumers include XInput,
+SDL/HIDAPI, DirectInput, Windows.Gaming.Input/GameInput, and browser Gamepad API
+clients.
Driver-backed keyboard key transitions use a standard keyboard-page HID report
with modifier state and sixteen simultaneous non-modifier usages. Unicode text
@@ -77,6 +80,16 @@ mask and duration field, and reports the body motors as normalized
low/high-frequency rumble and the independent trigger motors as trigger-rumble
output.
+Xbox 360 uses a broker-owned System-class software devnode with an explicit
+container ID. Its dedicated UMDF2 companion publishes the XUSB interface used
+by `xinput1_4.dll`, while its VHF child preserves the public
+`0x045E:0x028E&IG_00` HID identity. Input state is delivered at native XInput
+precision and `XInputSetState` feedback is normalized into the public two-motor
+rumble callback. This is a private Windows implementation detail; the public C++
+profile and API remain platform-neutral. Because Microsoft does not document
+XUSB as a third-party virtual-driver API, this compatibility layer requires
+release-by-release installed-driver regression testing.
+
The VHF driver answers the calibration, pairing, and firmware feature reports
used to initialize DualShock 4 and DualSense HIDAPI output. It also answers the
Switch Pro USB and subcommand initialization sequence and accepts the native
@@ -97,16 +110,17 @@ lets Windows HID consumers retrieve the current battery state for Xbox One,
Xbox Series, DualShock 4, DualSense, and Switch Pro instead of relying only on
the asynchronous input stream.
-That HID report does not change the XInput battery classification of the VHF
-device. On a Windows desktop where XInput enumerated the virtual Xbox
-controller, `XInputGetBatteryInformation` returned `BATTERY_TYPE_DISCONNECTED`
-and `BATTERY_LEVEL_EMPTY` even while its input was available through
+That HID report does not change the XInput battery classification of the Xbox
+One and Xbox Series VHF devices. On a Windows desktop where XInput enumerated
+one of those virtual Xbox controllers, `XInputGetBatteryInformation` returned
+`BATTERY_TYPE_DISCONNECTED` and `BATTERY_LEVEL_EMPTY` even while its input was available through
`XInputGetState`. Headless Windows CI did not expose an XInput slot for the same
device. Neither path exposes the remote battery through XInput. Consumers that
prefer XInput, including SDL's correlated Windows Xbox path and Windows Game
Bar, therefore do not receive the remote Xbox battery value. DualShock 4,
DualSense, and Switch Pro battery state is independently covered through SDL's
-HID path.
+HID path. The Xbox 360 XUSB personality reports the fixed wired-controller
+battery state and does not accept remote battery input.
The current Steam client displays its controller battery indicator only when it
classifies the device as Bluetooth or wireless. Because VHF exposes a wired
diff --git a/docs/windows-driver.md b/docs/windows-driver.md
index 3a84e8a..49f053d 100644
--- a/docs/windows-driver.md
+++ b/docs/windows-driver.md
@@ -1,10 +1,12 @@
# Windows Driver Package
-Windows virtual HID gamepad, keyboard, and Raw Input mouse support uses a
-user-mode UMDF2 control driver backed by Virtual HID Framework. The driver
-package is separate from the normal C++ library build: the library remains
-consumable from MSVC and MinGW/UCRT64, while the driver package is built with
-the Microsoft SDK/WDK toolchain.
+Windows virtual gamepad, keyboard, and Raw Input mouse support uses a user-mode
+UMDF2 package. Most devices are backed by Virtual HID Framework. The Xbox 360
+profile uses a second, per-controller UMDF2 XUSB personality and also publishes
+a VHF HID child for HID/DirectInput compatibility. The driver package is
+separate from the normal C++ library build: the library remains consumable from
+MSVC and MinGW/UCRT64, while the driver package is built with the Microsoft
+SDK/WDK toolchain.
Windows 11 version 21H2 and later is the supported driver target. The INF also
provides a best-effort compatibility path for Windows 10 version 2004 and later
@@ -42,9 +44,11 @@ through a local named pipe, while input reports stay on the direct driver path
after creation. This keeps license and active-device checks outside the input hot
path.
-The UMDF service runs in a dedicated high-priority host process, as recommended
-for response-sensitive input drivers. This isolates its VHF input work from
-normal-priority UMDF device pools while keeping the driver entirely user-mode.
+Each UMDF service runs in a dedicated high-priority host process. This isolates
+its input work from normal-priority UMDF device pools while keeping all
+libvirtualhid driver code in user mode. The package relies only on Microsoft's
+inbox UMDF reflector and VHF lower filter in kernel mode; it does not install a
+libvirtualhid `.sys` driver.
The broker pipe explicitly grants local authenticated users generic read access
plus the individual data-write and attribute-write rights needed to exchange
@@ -79,6 +83,35 @@ submit/destroy requests include that token so stale or unrelated clients cannot
control devices they did not create. Input reports are submitted through VHF,
and HID output writes are normalized back to the C++ output callback path.
+Xbox 360 creation takes a separate path because an authentic wired Xbox 360
+controller is XUSB rather than a standard HID-only device. For every requested
+Xbox 360 controller, the broker calls `SwDeviceCreate` with a unique instance
+ID and an explicit non-null container ID. Windows binds the package's
+`libvirtualhid_xbox360_umdf.dll` to that System-class software devnode. The
+companion publishes exactly one XUSB interface using
+`{EC87F1E3-C13B-4100-B5F7-8B84D54260CB}`, and creates a VHF child with the
+profile's `VID_045E&PID_028E&IG_00` identity. The shared container and ancestor
+metadata let Windows correlate the XUSB and HID views as one controller while
+retaining DirectInput/HID compatibility.
+
+The broker opens the XUSB interface, authenticates an initialization request by
+its SCM-registered process ID, and duplicates that per-device handle into the
+requesting client. Subsequent input and feedback operations require both the
+broker-assigned device ID and a random 256-bit session token. The client submits
+native 16-bit sticks, independent 8-bit triggers, and XInput button bits through
+that handle. The companion answers XInput state/capability requests, completes
+the asynchronous XUSB input wait at an 8-millisecond cadence, and returns
+XInput rumble as the normal platform-neutral output callback. Only the broker
+retains the `HSWDEVICE`; destroying the API gamepad, losing the owning client,
+or stopping the broker closes it and removes both device views.
+
+`SwDeviceCreate`, UMDF2, and VHF are documented Windows facilities. The XUSB
+interface GUID, IOCTL numbers, and byte layouts are not a supported public
+Microsoft driver contract. They are an explicitly isolated compatibility layer
+based on observed inbox XInput behavior and HIDMaestro's independently
+documented companion design. Keep that wire protocol private to the Windows
+backend and regression-test it on every supported Windows release.
+
The driver owns the VHF input buffering policy instead of allowing VHF to build
the default HID report backlog. VHF readiness notifications permit one report at
a time; while a gamepad consumer is not ready, the driver replaces superseded
@@ -99,15 +132,17 @@ can therefore query the current controller and battery state even when they do
not consume the streaming read queue. Unnumbered reports are returned with the
leading zero report-ID byte expected by Windows HID APIs.
-For Xbox profiles, this HID input value is separate from the battery result
-returned by XInput. On a Windows desktop where XInput enumerated the VHF Xbox
-device, `XInputGetBatteryInformation` returned `BATTERY_TYPE_DISCONNECTED` and
-`BATTERY_LEVEL_EMPTY` even while `XInputGetState` received its input and
+For the Xbox One and Xbox Series VHF profiles, this HID input value is separate
+from the battery result returned by XInput. On a Windows desktop where XInput
+enumerated one of those VHF Xbox devices, `XInputGetBatteryInformation` returned
+`BATTERY_TYPE_DISCONNECTED` and `BATTERY_LEVEL_EMPTY` even while `XInputGetState` received its input and
`GetInputReport` contained the submitted value. Headless Windows CI did not
expose an XInput slot for the same device. Neither path exposes the remote
battery through XInput. SDL's Windows Xbox path and Windows Game Bar therefore
have no XInput battery value to display. VHF does not expose a
wireless-transport or XInput battery-type setting in `VHF_CONFIG`.
+The Xbox 360 XUSB personality reports the fixed wired-controller battery state;
+its public profile does not advertise remote battery input.
The current Steam client also renders its controller battery indicator only for
devices it classifies as Bluetooth or wireless. All Windows VHF profiles use a
@@ -126,15 +161,17 @@ they are not a separate runtime bypass for creating or destroying virtual
devices.
The library and installed driver must use the same control-protocol version.
-Control protocol version 4 adds the canonical keyboard device type and report
-contract to the common create request. It retains the explicit device type and
-2048-byte report-descriptor capacity introduced by version 3. A version mismatch
-is rejected rather than interpreting a request with different semantics.
-
-Each backend runtime uses one control-file handle for commands and its pending
-output read. Broker protocol version 4 carries the generalized device create
-request while continuing to duplicate the handle only for the authorized create
-IOCTL. The driver associates output events with that file object, so feedback
+Control protocol version 5 adds an optional broker-duplicated per-device
+transport handle to the create response; it is required for Xbox 360 and zero
+for ordinary VHF devices. It retains the canonical keyboard device type from
+version 4 and the explicit device type and 2048-byte report-descriptor capacity
+from version 3. A version mismatch is rejected rather than interpreting a
+request with different semantics.
+
+Each backend runtime uses one root control-file handle for ordinary VHF commands
+and its pending output read. Broker protocol version 5 additionally carries the
+Xbox 360 transport handle in the create response. The root driver associates
+ordinary VHF output events with its control file object, so feedback
from a virtual gamepad is delivered only to the runtime that created it instead
of being consumed by another libvirtualhid client. Because the shared handle is
opened for overlapped I/O, command IOCTLs also supply a valid `OVERLAPPED` event
@@ -149,7 +186,7 @@ control device returns to D0. This allows sleep to complete without waiting for
controller feedback and keeps the runtime's control handle and virtual devices
valid across resume.
-The driver opens a separate VHF source target for each virtual HID device and
+The root driver opens a separate VHF source target for each ordinary virtual HID device and
parents that target to the control-file handle that created it. If the creating
process exits or crashes, Windows cleans up devices that were not explicitly
destroyed. In brokered driver packages, the broker owns that control-file handle.
@@ -157,7 +194,8 @@ The broker tracks the requesting client process for each created device and
destroys broker-owned devices when that client process exits unexpectedly. A new
broker process first asks the driver to remove every device left by the previous
broker instance and refuses new creation until that reset succeeds. Clients must
-recreate their devices after the broker service restarts.
+recreate their devices after the broker service restarts. Xbox 360 devices use
+the same ownership rule through their broker-held software-device handles.
The backend reports `requires_installed_driver = true` and only advertises
gamepad/output-report support when the broker is reachable and the control
@@ -449,9 +487,11 @@ and continues through `SendInput`. If the driver, broker, or license is
unavailable, mouse creation retains the existing `SendInput` fallback; malformed
requests and unexpected driver failures are returned to the caller.
-The Windows backend publishes HID gamepads through VHF. DirectInput, SDL/HIDAPI,
-Windows.Gaming.Input/GameInput, and browser Gamepad API clients should see
-standard HID devices after the driver is installed.
+The Windows backend publishes most gamepads through VHF. DirectInput,
+SDL/HIDAPI, Windows.Gaming.Input/GameInput, and browser Gamepad API clients
+should see standard HID devices after the driver is installed. Xbox 360 instead
+publishes a native XUSB-facing interface for XInput and a correlated VHF child
+for HID/DirectInput consumers.
The built-in Xbox One profile uses its XboxGIP-shaped HID descriptor. The public
Xbox Series profile remains `VID_045E&PID_0B12`; the Windows transport presents
@@ -460,11 +500,12 @@ observed from physical Xbox Series USB and Xbox Wireless Adapter connections.
The VHF child preserves the native 17-byte GIP-shaped input report, and the
last byte carries battery strength for both Xbox One and Xbox Series. The report
parser accepts the native eight-byte four-motor Xbox payload when a consumer
-delivers it. The Windows backend submits Xbox input only when the packed state
-changes. State transitions still reach VHF, while raw HID consumers are not
-asked to reinterpret the same unchanged Xbox state as fresh input. The Xbox 360
-profile is rejected by the UMDF/VHF backend because a
-real Xbox 360 controller is an XUSB device rather than a VHF HID gamepad.
+delivers it. The Windows backend submits Xbox One and Xbox Series input only
+when the packed state changes. State transitions still reach VHF, while raw HID
+consumers are not asked to reinterpret the same unchanged Xbox state as fresh
+input. The Xbox 360 companion exposes the classic `0x045E:0x028E` wired
+identity, preserves native XInput button/axis precision, and normalizes the two
+XInput motors into the ordinary rumble callback.
DualShock 4 and DualSense answer the calibration, pairing, and firmware feature
requests used by their Windows HIDAPI initialization paths. Switch Pro answers
@@ -492,6 +533,9 @@ label because VHF does not provide a product/manufacturer string callback.
- The published Windows driver installer is AMD64-only. Windows ARM64 release
packages require a Microsoft dashboard signing path that is not part of the
current Azure Trusted Signing workflow.
+- Xbox 360 support depends on an undocumented XUSB compatibility contract and
+ therefore requires installed-driver XInput, browser/WGI, and rumble validation
+ on each supported Windows release before shipping.
- A temporary Polar outage limits a previously activated machine to one active
licensed virtual HID device. Yearly subscriptions must reconnect within 25
hours of their last successful validation; lifetime licenses can retain one
diff --git a/gh-pages-template/virtual-hid-driver.html b/gh-pages-template/virtual-hid-driver.html
index 179d9c4..922f78b 100644
--- a/gh-pages-template/virtual-hid-driver.html
+++ b/gh-pages-template/virtual-hid-driver.html
@@ -108,12 +108,12 @@
Compatibility notes
support does not require this package.
- Xbox 360 is not available through this UMDF/VHF backend because a real Xbox 360
- controller uses XUSB rather than standard HID.
+ Xbox 360 uses a dedicated user-mode XUSB companion plus a correlated VHF child;
+ other gamepad profiles use the VHF path directly.
- Xbox-style HID profiles do not guarantee an XInput slot. Applications that require
- XInput-specific behavior should review the profile compatibility documentation.
+ Xbox 360 targets XInput directly. Xbox One and Xbox Series remain HID profiles and
+ do not guarantee an XInput slot; review the profile compatibility documentation.
diff --git a/scripts/windows/test-browser-gamepad.ps1 b/scripts/windows/test-browser-gamepad.ps1
index 848c5d6..be22b7f 100644
--- a/scripts/windows/test-browser-gamepad.ps1
+++ b/scripts/windows/test-browser-gamepad.ps1
@@ -359,9 +359,6 @@ $expectedPattern = if ($ExpectedIdPattern) {
} else {
Get-ExpectedGamepadIdPattern -ProfileName $GamepadProfile
}
-if ($GamepadProfile -eq "x360") {
- throw "The Windows UMDF/VHF backend does not expose Xbox 360 XUSB gamepads. Use the consumer's XUSB fallback for x360."
-}
$remoteDebuggingPort = Get-FreeTcpPort
$browserUserDataDir = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-browser-gamepad-$([Guid]::NewGuid())"
$adapterStdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-browser-gamepad-adapter.out"
diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1
index 751f6b6..7ea4d74 100644
--- a/scripts/windows/test-installed-driver.ps1
+++ b/scripts/windows/test-installed-driver.ps1
@@ -268,6 +268,32 @@ function Wait-ForStartedGamepadChild {
}
}
+function Wait-ForStartedXbox360Companion {
+ param([int] $TimeoutSeconds)
+
+ $hardwareId = "SWD\LIBVIRTUALHID_XBOX360"
+ $deadline = (Get-Date).AddSeconds($TimeoutSeconds)
+ $latestRecords = @()
+ do {
+ $latestRecords = @(Get-PnPUtilDevicesByDeviceId -DeviceId $hardwareId)
+ $started = $latestRecords |
+ Where-Object { $_.Status -eq "Started" } |
+ Select-Object -First 1
+ if ($started) {
+ Write-Information "Xbox 360 XUSB companion started: $($started.InstanceId) ($($started.DriverName))" -InformationAction Continue
+ return
+ }
+ Start-Sleep -Milliseconds 500
+ } while ((Get-Date) -lt $deadline)
+
+ if (-not $latestRecords) {
+ throw "No Xbox 360 XUSB companion was found for $hardwareId."
+ }
+ foreach ($record in $latestRecords) {
+ Assert-StartedPnPRecord -Record $record -Description "Xbox 360 XUSB companion $($record.InstanceId)"
+ }
+}
+
function Invoke-GamepadAdapterSmoke {
param(
[string] $Path,
@@ -280,10 +306,6 @@ function Invoke-GamepadAdapterSmoke {
return
}
- if ($ProfileName -eq "x360") {
- throw "The Windows UMDF/VHF backend does not expose Xbox 360 XUSB gamepads. Use the consumer's XUSB fallback for x360."
- }
-
$resolvedGamepadAdapterPath = (Resolve-Path -LiteralPath $Path).Path
$stdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-gamepad-adapter.out"
$stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-gamepad-adapter.err"
@@ -306,6 +328,9 @@ function Invoke-GamepadAdapterSmoke {
}
Wait-ForStartedGamepadChild -ProfileName $ProfileName -TimeoutSeconds $DeviceStartTimeoutSeconds
+ if ($ProfileName -eq "x360") {
+ Wait-ForStartedXbox360Companion -TimeoutSeconds $DeviceStartTimeoutSeconds
+ }
} finally {
if (-not $process.HasExited) {
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
diff --git a/src/platform/windows/broker/CMakeLists.txt b/src/platform/windows/broker/CMakeLists.txt
index 2b94fe8..8d944b2 100644
--- a/src/platform/windows/broker/CMakeLists.txt
+++ b/src/platform/windows/broker/CMakeLists.txt
@@ -7,7 +7,8 @@ CPMUsePackageLock("${PROJECT_SOURCE_DIR}/package-lock.cmake")
CPMGetPackage(nlohmann_json)
add_executable(libvirtualhid_broker
- "${CMAKE_CURRENT_SOURCE_DIR}/libvirtualhid_broker.cpp")
+ "${CMAKE_CURRENT_SOURCE_DIR}/libvirtualhid_broker.cpp"
+ "${CMAKE_CURRENT_SOURCE_DIR}/xbox_360_software_device.cpp")
target_compile_features(libvirtualhid_broker PRIVATE cxx_std_23)
target_include_directories(libvirtualhid_broker
@@ -25,6 +26,8 @@ target_link_libraries(libvirtualhid_broker
advapi32
bcrypt
crypt32
+ ole32
+ setupapi
winhttp)
if(MSVC AND LIBVIRTUALHID_BUILD_WINDOWS_DRIVER)
diff --git a/src/platform/windows/broker/libvirtualhid_broker.cpp b/src/platform/windows/broker/libvirtualhid_broker.cpp
index 916724a..abb31ab 100644
--- a/src/platform/windows/broker/libvirtualhid_broker.cpp
+++ b/src/platform/windows/broker/libvirtualhid_broker.cpp
@@ -28,6 +28,7 @@
#include "lvh_windows_broker_config.hpp"
#include "lvh_windows_broker_protocol.h"
#include "lvh_windows_github_actions_evaluation.hpp"
+#include "xbox_360_software_device.hpp"
// lib includes
#include
@@ -36,6 +37,7 @@
// standard includes
#include
#include
+#include
#include
#include
#include
@@ -1087,6 +1089,15 @@ namespace lvh::detail::windows_broker_service {
return std::ranges::equal(lhs.bytes, rhs.bytes);
}
+ bool generate_session_token(LvhWindowsSessionToken &session_token) {
+ if (::BCryptGenRandom(nullptr, session_token.bytes.data(), static_cast(session_token.bytes.size()), BCRYPT_USE_SYSTEM_PREFERRED_RNG) != 0) {
+ return false;
+ }
+ return !std::ranges::all_of(session_token.bytes, [](std::uint8_t value) {
+ return value == 0U;
+ });
+ }
+
LvhWindowsDestroyDeviceRequest make_destroy_device_request(
std::uint64_t driver_device_id,
const LvhWindowsSessionToken &session_token
@@ -1322,6 +1333,7 @@ namespace lvh::detail::windows_broker_service {
LvhWindowsSessionToken session_token {};
DWORD owner_process_id {};
UniqueHandle owner_process {nullptr, &::CloseHandle};
+ std::unique_ptr xbox360_device;
bool github_actions_evaluation = false;
};
@@ -1396,17 +1408,6 @@ namespace lvh::detail::windows_broker_service {
return response;
}
- auto client_control_handle = duplicate_client_handle(
- owner_process.get(),
- request.client_control_handle
- );
- if (!client_control_handle) {
- response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument);
- copy_c_string(response.message, "Unable to duplicate the requesting client control handle.");
- fill_license_status(response.license);
- return response;
- }
-
const auto [authorization_status, github_actions_evaluation] = authorize_device_create(
response.license,
response.message
@@ -1417,11 +1418,60 @@ namespace lvh::detail::windows_broker_service {
}
auto status = LvhWindowsBrokerStatusCode::success;
- if (std::string message; !driver_.create_device(client_control_handle.get(), request.device, response.device, status, message)) {
- response.status = std::to_underlying(status);
- fill_license_status(response.license);
- copy_c_string(response.message, message);
- return response;
+ std::unique_ptr xbox360_device;
+ if (
+ request.device.device_type == LVH_WINDOWS_DEVICE_GAMEPAD &&
+ request.device.gamepad_kind == LVH_WINDOWS_GAMEPAD_XBOX_360
+ ) {
+ const auto driver_device_id = next_xbox360_driver_device_id_.fetch_add(1U);
+ auto session_token = LvhWindowsSessionToken {};
+ std::string message;
+ if (!generate_session_token(session_token)) {
+ response.status = std::to_underlying(LvhWindowsBrokerStatusCode::backend_failure);
+ fill_license_status(response.license);
+ copy_c_string(response.message, "Unable to generate an Xbox 360 device session token.");
+ return response;
+ }
+ xbox360_device = lvh::windows::Xbox360SoftwareDevice::create(
+ request.device,
+ driver_device_id,
+ session_token,
+ owner_process.get(),
+ response.device,
+ status,
+ message
+ );
+ if (!xbox360_device) {
+ response.status = std::to_underlying(status);
+ fill_license_status(response.license);
+ copy_c_string(response.message, message);
+ return response;
+ }
+ } else {
+ auto client_control_handle = duplicate_client_handle(
+ owner_process.get(),
+ request.client_control_handle
+ );
+ if (!client_control_handle) {
+ response.status = std::to_underlying(LvhWindowsBrokerStatusCode::invalid_argument);
+ copy_c_string(response.message, "Unable to duplicate the requesting client control handle.");
+ fill_license_status(response.license);
+ return response;
+ }
+
+ std::string message;
+ if (!driver_.create_device(
+ client_control_handle.get(),
+ request.device,
+ response.device,
+ status,
+ message
+ )) {
+ response.status = std::to_underlying(status);
+ fill_license_status(response.license);
+ copy_c_string(response.message, message);
+ return response;
+ }
}
{
@@ -1432,6 +1482,7 @@ namespace lvh::detail::windows_broker_service {
.session_token = response.device.session_token,
.owner_process_id = client_process_id,
.owner_process = std::move(owner_process),
+ .xbox360_device = std::move(xbox360_device),
.github_actions_evaluation = github_actions_evaluation,
}
);
@@ -1480,7 +1531,7 @@ namespace lvh::detail::windows_broker_service {
}
auto status = LvhWindowsBrokerStatusCode::success;
- if (std::string message; !driver_.destroy_device(request.device, status, message)) {
+ if (std::string message; !destroy_device_transport(request.device, status, message)) {
response.status = std::to_underlying(status);
fill_license_status(response.license);
copy_c_string(response.message, message);
@@ -1549,7 +1600,7 @@ namespace lvh::detail::windows_broker_service {
[this](const auto &request) {
auto status = LvhWindowsBrokerStatusCode::success;
std::string message;
- return driver_.destroy_device(request, status, message);
+ return destroy_device_transport(request, status, message);
},
[this](const auto &request) {
std::lock_guard lock {mutex_};
@@ -1798,6 +1849,23 @@ namespace lvh::detail::windows_broker_service {
backend_failure,
};
+ bool destroy_device_transport(
+ const LvhWindowsDestroyDeviceRequest &request,
+ LvhWindowsBrokerStatusCode &status,
+ std::string &message
+ ) {
+ {
+ std::lock_guard lock {mutex_};
+ const auto device = devices_.find(request.driver_device_id);
+ if (device != devices_.end() && device->second.xbox360_device) {
+ status = LvhWindowsBrokerStatusCode::success;
+ message.clear();
+ return true;
+ }
+ }
+ return driver_.destroy_device(request, status, message);
+ }
+
bool reconcile_driver_state(
LvhWindowsBrokerStatusCode &status,
std::string &message
@@ -2267,6 +2335,7 @@ namespace lvh::detail::windows_broker_service {
const std::string boot_session_marker_ {load_or_create_boot_session_marker()};
std::map devices_;
DriverChannel driver_;
+ std::atomic next_xbox360_driver_device_id_ {UINT64_C(0x8000000000000000)};
std::optional license_state_ {load_license_state()};
std::optional next_license_validation_attempt_;
std::optional license_validation_unavailable_since_;
diff --git a/src/platform/windows/broker/xbox_360_software_device.cpp b/src/platform/windows/broker/xbox_360_software_device.cpp
new file mode 100644
index 0000000..61a8438
--- /dev/null
+++ b/src/platform/windows/broker/xbox_360_software_device.cpp
@@ -0,0 +1,509 @@
+// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC
+// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0
+
+/**
+ * @file src/platform/windows/broker/xbox_360_software_device.cpp
+ * @brief Xbox 360 software-device creation and private-channel handoff.
+ */
+
+#include "xbox_360_software_device.hpp"
+
+#include "xbox_360_protocol.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace lvh::windows {
+ namespace {
+
+ using UniqueHandle = std::unique_ptr;
+ using UniqueDeviceInfo = std::unique_ptr<
+ std::remove_pointer_t,
+ decltype(&::SetupDiDestroyDeviceInfoList)>;
+ using UniqueModule = std::unique_ptr, decltype(&::FreeLibrary)>;
+
+ struct SoftwareDeviceHandleTag;
+ using SoftwareDeviceHandle = SoftwareDeviceHandleTag *;
+
+ constexpr auto software_device_capability_silent_install = 0x00000002U;
+ constexpr auto software_device_capability_no_display_in_ui = 0x00000004U;
+ constexpr auto software_device_capability_driver_required = 0x00000008U;
+ constexpr auto interface_poll_interval_ms = 25U;
+ constexpr auto interface_start_timeout_ms = 30000U;
+
+ struct SoftwareDeviceCreateInfo {
+ ULONG size;
+ PCWSTR instance_id;
+ PCWSTR hardware_ids;
+ PCWSTR compatible_ids;
+ const GUID *container_id;
+ ULONG capability_flags;
+ PCWSTR device_description;
+ PCWSTR device_location;
+ const SECURITY_DESCRIPTOR *security_descriptor;
+ };
+
+ struct CreationCallbackContext {
+ std::mutex mutex;
+ UniqueHandle event {nullptr, &::CloseHandle};
+ HRESULT result = E_PENDING;
+ std::wstring instance_id;
+ bool completed {};
+ bool abandoned {};
+ };
+
+ using SoftwareDeviceCreateCallback = void(WINAPI *)(
+ SoftwareDeviceHandle,
+ HRESULT,
+ CreationCallbackContext *,
+ PCWSTR
+ );
+ using SoftwareDeviceCreateFunction = HRESULT(WINAPI *)(
+ PCWSTR,
+ PCWSTR,
+ const SoftwareDeviceCreateInfo *,
+ ULONG,
+ const void *,
+ SoftwareDeviceCreateCallback,
+ CreationCallbackContext *,
+ SoftwareDeviceHandle *
+ );
+ using SoftwareDeviceCloseFunction = void(WINAPI *)(SoftwareDeviceHandle);
+
+ struct SoftwareDeviceApi {
+ UniqueModule library {nullptr, &::FreeLibrary};
+ SoftwareDeviceCreateFunction create {};
+ SoftwareDeviceCloseFunction close {};
+
+ bool load() {
+ library.reset(::LoadLibraryW(L"cfgmgr32.dll"));
+ if (!library) {
+ return false;
+ }
+ create = std::bit_cast(::GetProcAddress(library.get(), "SwDeviceCreate"));
+ close = std::bit_cast(::GetProcAddress(library.get(), "SwDeviceClose"));
+ return create != nullptr && close != nullptr;
+ }
+ };
+
+ void WINAPI software_device_created(
+ SoftwareDeviceHandle,
+ HRESULT result,
+ CreationCallbackContext *context,
+ PCWSTR instance_id
+ ) {
+ std::unique_ptr abandoned_context;
+ {
+ std::lock_guard lock {context->mutex};
+ context->result = result;
+ context->instance_id = instance_id == nullptr ? L"" : instance_id;
+ context->completed = true;
+ if (context->abandoned) {
+ abandoned_context.reset(context);
+ } else {
+ static_cast(::SetEvent(context->event.get()));
+ }
+ }
+ }
+
+ std::string windows_error_message(DWORD error_code) {
+ std::array buffer {};
+ const auto size = ::FormatMessageA(
+ FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
+ nullptr,
+ error_code,
+ MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ buffer.data(),
+ static_cast(buffer.size()),
+ nullptr
+ );
+ if (size == 0U) {
+ return std::format("Windows error {}", error_code);
+ }
+ std::string message {buffer.data(), size};
+ while (!message.empty() && (message.back() == '\r' || message.back() == '\n')) {
+ message.pop_back();
+ }
+ return message;
+ }
+
+ bool random_guid(GUID &guid) {
+ std::array random_bytes {};
+ if (const auto status = ::BCryptGenRandom(nullptr, std::bit_cast(random_bytes.data()), static_cast(random_bytes.size()), BCRYPT_USE_SYSTEM_PREFERRED_RNG); status != 0) {
+ return false;
+ }
+
+ constexpr auto variant_byte_offset = offsetof(GUID, Data4);
+ random_bytes[variant_byte_offset] =
+ (random_bytes[variant_byte_offset] & std::byte {0x3FU}) | std::byte {0x80U};
+ guid = std::bit_cast(random_bytes);
+ guid.Data3 = static_cast((guid.Data3 & 0x0FFFU) | 0x4000U);
+ return true;
+ }
+
+ std::wstring guid_string(const GUID &guid) {
+ std::array buffer {};
+ if (::StringFromGUID2(guid, buffer.data(), static_cast(buffer.size())) == 0) {
+ return {};
+ }
+ return buffer.data();
+ }
+
+ std::wstring make_multi_string(std::span values) {
+ std::wstring result;
+ for (const auto value : values) {
+ result.append(value);
+ result.push_back(L'\0');
+ }
+ result.push_back(L'\0');
+ return result;
+ }
+
+ std::wstring hardware_id(const LvhWindowsCreateDeviceRequest &request) {
+ return std::format(
+ L"USB\\VID_{:04X}&PID_{:04X}&XI_00",
+ request.hardware_ids.vendor_id,
+ request.hardware_ids.product_id
+ );
+ }
+
+ UniqueDeviceInfo make_unique_device_info(HDEVINFO value) {
+ if (value == INVALID_HANDLE_VALUE) {
+ value = nullptr;
+ }
+ return {value, &::SetupDiDestroyDeviceInfoList};
+ }
+
+ std::wstring find_interface_path(const GUID &interface_guid, std::wstring_view instance_id) {
+ auto device_info = make_unique_device_info(
+ ::SetupDiGetClassDevsW(&interface_guid, nullptr, nullptr, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE)
+ );
+ if (!device_info) {
+ return {};
+ }
+
+ for (DWORD index = 0;; ++index) {
+ SP_DEVICE_INTERFACE_DATA interface_data {};
+ interface_data.cbSize = sizeof(interface_data);
+ if (::SetupDiEnumDeviceInterfaces(device_info.get(), nullptr, &interface_guid, index, &interface_data) == FALSE) {
+ break;
+ }
+
+ DWORD required_size = 0;
+ static_cast(::SetupDiGetDeviceInterfaceDetailW(
+ device_info.get(),
+ &interface_data,
+ nullptr,
+ 0,
+ &required_size,
+ nullptr
+ ));
+ if (required_size < sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W)) {
+ continue;
+ }
+
+ const auto element_count =
+ (required_size + sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W) - 1U) /
+ sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_W);
+ std::vector storage(element_count);
+ auto *detail = storage.data();
+ detail->cbSize = sizeof(*detail);
+ SP_DEVINFO_DATA device_data {};
+ device_data.cbSize = sizeof(device_data);
+ if (::SetupDiGetDeviceInterfaceDetailW(device_info.get(), &interface_data, detail, required_size, nullptr, &device_data) == FALSE) {
+ continue;
+ }
+
+ std::array candidate_instance {};
+ if (::SetupDiGetDeviceInstanceIdW(device_info.get(), &device_data, candidate_instance.data(), static_cast(candidate_instance.size()), nullptr) == FALSE) {
+ continue;
+ }
+ if (_wcsicmp(candidate_instance.data(), std::wstring {instance_id}.c_str()) == 0) {
+ return detail->DevicePath;
+ }
+ }
+ return {};
+ }
+
+ std::wstring wait_for_interface_path(const GUID &interface_guid, std::wstring_view instance_id) {
+ for (auto elapsed_ms = 0U; elapsed_ms < interface_start_timeout_ms; elapsed_ms += interface_poll_interval_ms) {
+ if (auto path = find_interface_path(interface_guid, instance_id); !path.empty()) {
+ return path;
+ }
+ ::Sleep(interface_poll_interval_ms);
+ }
+ return {};
+ }
+
+ std::string narrow_string(std::wstring_view value) {
+ if (value.empty()) {
+ return {};
+ }
+ const auto size = ::WideCharToMultiByte(
+ CP_UTF8,
+ WC_ERR_INVALID_CHARS,
+ value.data(),
+ static_cast(value.size()),
+ nullptr,
+ 0,
+ nullptr,
+ nullptr
+ );
+ if (size <= 0) {
+ return {};
+ }
+ std::string result(static_cast(size), '\0');
+ static_cast(::WideCharToMultiByte(
+ CP_UTF8,
+ WC_ERR_INVALID_CHARS,
+ value.data(),
+ static_cast(value.size()),
+ result.data(),
+ size,
+ nullptr,
+ nullptr
+ ));
+ return result;
+ }
+
+ template
+ void copy_string(std::array &target, std::string_view source) {
+ std::ranges::fill(target, '\0');
+ const auto count = std::min(source.size(), Size - 1U);
+ std::memcpy(target.data(), source.data(), count);
+ }
+
+ bool send_initialize(
+ HANDLE handle,
+ const LvhWindowsCreateDeviceRequest &request,
+ std::uint64_t driver_device_id,
+ const LvhWindowsSessionToken &session_token,
+ std::string &message
+ ) {
+ LvhWindowsXbox360InitializeRequest initialize {};
+ initialize.version = LVH_WINDOWS_XBOX360_PROTOCOL_VERSION;
+ initialize.size = sizeof(initialize);
+ initialize.driver_device_id = driver_device_id;
+ initialize.session_token = session_token;
+ initialize.device = request;
+
+ auto event = UniqueHandle {::CreateEventW(nullptr, TRUE, FALSE, nullptr), &::CloseHandle};
+ if (!event) {
+ message = "Unable to create the Xbox 360 initialization event: " +
+ windows_error_message(::GetLastError());
+ return false;
+ }
+ OVERLAPPED overlapped {};
+ overlapped.hEvent = event.get();
+ DWORD bytes_returned = 0;
+ if (::DeviceIoControl(handle, LVH_WINDOWS_IOCTL_XBOX360_INITIALIZE, &initialize, sizeof(initialize), nullptr, 0, &bytes_returned, &overlapped) == FALSE && ::GetLastError() != ERROR_IO_PENDING) {
+ message = "Initialize Xbox 360 UMDF device: " + windows_error_message(::GetLastError());
+ return false;
+ }
+ if (::GetOverlappedResult(handle, &overlapped, &bytes_returned, TRUE) == FALSE) {
+ message = "Initialize Xbox 360 UMDF device: " + windows_error_message(::GetLastError());
+ return false;
+ }
+ return true;
+ }
+
+ } // namespace
+
+ struct Xbox360SoftwareDevice::Implementation {
+ std::unique_ptr api;
+ SoftwareDeviceHandle handle {};
+
+ ~Implementation() {
+ if (handle != nullptr && api && api->close != nullptr) {
+ api->close(handle);
+ }
+ }
+ };
+
+ Xbox360SoftwareDevice::Xbox360SoftwareDevice(std::unique_ptr implementation):
+ implementation_ {std::move(implementation)} {}
+
+ Xbox360SoftwareDevice::Xbox360SoftwareDevice(Xbox360SoftwareDevice &&) noexcept = default;
+ Xbox360SoftwareDevice &Xbox360SoftwareDevice::operator=(Xbox360SoftwareDevice &&) noexcept = default;
+
+ Xbox360SoftwareDevice::~Xbox360SoftwareDevice() {
+ implementation_.reset();
+ }
+
+ std::unique_ptr Xbox360SoftwareDevice::create(
+ const LvhWindowsCreateDeviceRequest &request,
+ std::uint64_t driver_device_id,
+ const LvhWindowsSessionToken &session_token,
+ HANDLE client_process,
+ LvhWindowsCreateDeviceResponse &response,
+ LvhWindowsBrokerStatusCode &status,
+ std::string &message
+ ) {
+ status = LvhWindowsBrokerStatusCode::backend_failure;
+ auto implementation = std::make_unique();
+ implementation->api = std::make_unique();
+ if (!implementation->api->load()) {
+ message = "Windows software-device API is unavailable: " + windows_error_message(::GetLastError());
+ return nullptr;
+ }
+
+ GUID container_id {};
+ GUID instance_guid {};
+ if (!random_guid(container_id) || !random_guid(instance_guid)) {
+ message = "Unable to generate Xbox 360 software-device identities.";
+ return nullptr;
+ }
+ const auto instance_id = guid_string(instance_guid);
+ if (instance_id.empty()) {
+ message = "Unable to format the Xbox 360 software-device identity.";
+ return nullptr;
+ }
+
+ const auto vendor_hardware_id = hardware_id(request);
+ const std::array hardware_ids {
+ LVH_WINDOWS_XBOX360_HARDWARE_ID,
+ vendor_hardware_id,
+ };
+ const std::array compatible_ids {
+ L"USB\\MS_COMP_XUSB10",
+ L"USB\\Class_FF&SubClass_5D&Prot_01",
+ L"USB\\Class_FF&SubClass_5D",
+ L"USB\\Class_FF",
+ };
+ const auto hardware_multi_string = make_multi_string(hardware_ids);
+ const auto compatible_multi_string = make_multi_string(compatible_ids);
+
+ SoftwareDeviceCreateInfo create_info {};
+ create_info.size = sizeof(create_info);
+ create_info.instance_id = instance_id.c_str();
+ create_info.hardware_ids = hardware_multi_string.c_str();
+ create_info.compatible_ids = compatible_multi_string.c_str();
+ create_info.container_id = &container_id;
+ create_info.capability_flags = software_device_capability_silent_install |
+ software_device_capability_no_display_in_ui |
+ software_device_capability_driver_required;
+ create_info.device_description = L"libvirtualhid Xbox 360 Controller";
+
+ auto callback_context = std::make_unique();
+ callback_context->event.reset(::CreateEventW(nullptr, TRUE, FALSE, nullptr));
+ if (!callback_context->event) {
+ message = "Unable to create the Xbox 360 software-device event: " +
+ windows_error_message(::GetLastError());
+ return nullptr;
+ }
+
+ const auto create_result = implementation->api->create(
+ LVH_WINDOWS_XBOX360_ENUMERATOR,
+ L"HTREE\\ROOT\\0",
+ &create_info,
+ 0U,
+ nullptr,
+ software_device_created,
+ callback_context.get(),
+ &implementation->handle
+ );
+ if (FAILED(create_result)) {
+ message = std::format("SwDeviceCreate failed with HRESULT 0x{:08X}.", static_cast(create_result));
+ return nullptr;
+ }
+
+ if (::WaitForSingleObject(callback_context->event.get(), 35000U) != WAIT_OBJECT_0) {
+ auto callback_completed = false;
+ {
+ std::lock_guard lock {callback_context->mutex};
+ callback_completed = callback_context->completed;
+ if (!callback_completed) {
+ callback_context->abandoned = true;
+ }
+ }
+ if (!callback_completed) {
+ static_cast(callback_context.release());
+ }
+ message = "Timed out while Windows installed the Xbox 360 software device.";
+ return nullptr;
+ }
+
+ auto callback_result = E_FAIL;
+ std::wstring full_instance_id;
+ {
+ std::lock_guard lock {callback_context->mutex};
+ callback_result = callback_context->result;
+ full_instance_id = callback_context->instance_id;
+ }
+ if (FAILED(callback_result) || full_instance_id.empty()) {
+ message = std::format(
+ "Windows failed to start the Xbox 360 software device (HRESULT 0x{:08X}).",
+ static_cast(callback_result)
+ );
+ return nullptr;
+ }
+
+ const auto xusb_path = wait_for_interface_path(LVH_WINDOWS_XUSB_INTERFACE_GUID, full_instance_id);
+ if (xusb_path.empty()) {
+ message = "The Xbox 360 UMDF driver started without publishing its XUSB interface.";
+ return nullptr;
+ }
+
+ auto transport_handle = UniqueHandle {
+ ::CreateFileW(
+ xusb_path.c_str(),
+ GENERIC_READ | GENERIC_WRITE,
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr,
+ OPEN_EXISTING,
+ FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
+ nullptr
+ ),
+ &::CloseHandle,
+ };
+ if (transport_handle.get() == INVALID_HANDLE_VALUE) {
+ transport_handle.release();
+ message = "Unable to open the Xbox 360 broker transport: " +
+ windows_error_message(::GetLastError());
+ return nullptr;
+ }
+ if (!send_initialize(
+ transport_handle.get(),
+ request,
+ driver_device_id,
+ session_token,
+ message
+ )) {
+ return nullptr;
+ }
+
+ HANDLE client_handle = nullptr;
+ if (::DuplicateHandle(::GetCurrentProcess(), transport_handle.get(), client_process, &client_handle, 0U, FALSE, DUPLICATE_SAME_ACCESS) == FALSE) {
+ message = "Unable to duplicate the Xbox 360 transport into the requesting client: " +
+ windows_error_message(::GetLastError());
+ return nullptr;
+ }
+
+ response.status = LVH_WINDOWS_STATUS_SUCCESS;
+ response.driver_device_id = driver_device_id;
+ response.transport_handle = std::bit_cast(client_handle);
+ response.session_token = session_token;
+ copy_string(response.device_path, narrow_string(xusb_path));
+ status = LvhWindowsBrokerStatusCode::success;
+ message.clear();
+ return std::make_unique(std::move(implementation));
+ }
+
+} // namespace lvh::windows
diff --git a/src/platform/windows/broker/xbox_360_software_device.hpp b/src/platform/windows/broker/xbox_360_software_device.hpp
new file mode 100644
index 0000000..535cf25
--- /dev/null
+++ b/src/platform/windows/broker/xbox_360_software_device.hpp
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC
+// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0
+
+/**
+ * @file src/platform/windows/broker/xbox_360_software_device.hpp
+ * @brief Broker-owned lifetime for one Xbox 360 software device.
+ */
+#pragma once
+
+#include "lvh_windows_broker_protocol.h"
+
+#include
+#include
+#include
+
+#ifndef NOMINMAX
+ #define NOMINMAX
+#endif
+#ifndef WIN32_LEAN_AND_MEAN
+ #define WIN32_LEAN_AND_MEAN
+#endif
+#include
+
+namespace lvh::windows {
+
+ class Xbox360SoftwareDevice {
+ struct Implementation;
+
+ public:
+ explicit Xbox360SoftwareDevice(std::unique_ptr implementation);
+ Xbox360SoftwareDevice(const Xbox360SoftwareDevice &) = delete;
+ Xbox360SoftwareDevice &operator=(const Xbox360SoftwareDevice &) = delete;
+ Xbox360SoftwareDevice(Xbox360SoftwareDevice &&) noexcept;
+ Xbox360SoftwareDevice &operator=(Xbox360SoftwareDevice &&) noexcept;
+ ~Xbox360SoftwareDevice();
+
+ static std::unique_ptr create(
+ const LvhWindowsCreateDeviceRequest &request,
+ std::uint64_t driver_device_id,
+ const LvhWindowsSessionToken &session_token,
+ HANDLE client_process,
+ LvhWindowsCreateDeviceResponse &response,
+ LvhWindowsBrokerStatusCode &status,
+ std::string &message
+ );
+
+ private:
+ std::unique_ptr implementation_;
+ };
+
+} // namespace lvh::windows
diff --git a/src/platform/windows/control_protocol.hpp b/src/platform/windows/control_protocol.hpp
index 77c6077..23f488e 100644
--- a/src/platform/windows/control_protocol.hpp
+++ b/src/platform/windows/control_protocol.hpp
@@ -19,9 +19,11 @@
// driver includes
#include "generic_pid_protocol.hpp"
#include "lvh_windows_protocol.h"
+#include "xbox_360_protocol.hpp"
// local includes
#include
+#include
#include
namespace lvh::detail::windows {
@@ -288,4 +290,53 @@ namespace lvh::detail::windows {
return make_submit_input_report_request(driver_device_id, empty_session_token, report);
}
+ inline std::uint16_t xbox_360_button_bits(const ButtonSet &buttons) {
+ using enum GamepadButton;
+
+ auto result = std::uint16_t {};
+ const auto add = [&](GamepadButton button, std::uint16_t flag) {
+ if (buttons.test(button)) {
+ result = static_cast(result | flag);
+ }
+ };
+
+ add(dpad_up, LVH_WINDOWS_XINPUT_DPAD_UP);
+ add(dpad_down, LVH_WINDOWS_XINPUT_DPAD_DOWN);
+ add(dpad_left, LVH_WINDOWS_XINPUT_DPAD_LEFT);
+ add(dpad_right, LVH_WINDOWS_XINPUT_DPAD_RIGHT);
+ add(start, LVH_WINDOWS_XINPUT_START);
+ add(back, LVH_WINDOWS_XINPUT_BACK);
+ add(left_stick, LVH_WINDOWS_XINPUT_LEFT_THUMB);
+ add(right_stick, LVH_WINDOWS_XINPUT_RIGHT_THUMB);
+ add(left_shoulder, LVH_WINDOWS_XINPUT_LEFT_SHOULDER);
+ add(right_shoulder, LVH_WINDOWS_XINPUT_RIGHT_SHOULDER);
+ add(guide, LVH_WINDOWS_XINPUT_GUIDE);
+ add(a, LVH_WINDOWS_XINPUT_A);
+ add(b, LVH_WINDOWS_XINPUT_B);
+ add(x, LVH_WINDOWS_XINPUT_X);
+ add(y, LVH_WINDOWS_XINPUT_Y);
+ return result;
+ }
+
+ inline LvhWindowsXbox360SubmitInputRequest make_xbox_360_submit_input_request(
+ std::uint64_t driver_device_id,
+ const LvhWindowsSessionToken &session_token,
+ const GamepadState &state
+ ) {
+ const auto normalized = reports::normalize_state(state);
+ LvhWindowsXbox360SubmitInputRequest request {};
+ request.version = LVH_WINDOWS_XBOX360_PROTOCOL_VERSION;
+ request.size = sizeof(request);
+ request.driver_device_id = driver_device_id;
+ request.session_token = session_token;
+ request.state.buttons = xbox_360_button_bits(normalized.buttons);
+ request.state.left_trigger = reports::normalize_trigger(normalized.left_trigger);
+ request.state.right_trigger = reports::normalize_trigger(normalized.right_trigger);
+ request.state.left_thumb_x = reports::normalize_axis(normalized.left_stick.x);
+ request.state.left_thumb_y = reports::normalize_axis(normalized.left_stick.y);
+ request.state.right_thumb_x = reports::normalize_axis(normalized.right_stick.x);
+ request.state.right_thumb_y = reports::normalize_axis(normalized.right_stick.y);
+ return request;
+ }
+
} // namespace lvh::detail::windows
diff --git a/src/platform/windows/driver/CMakeLists.txt b/src/platform/windows/driver/CMakeLists.txt
index 51a50bf..8abe562 100644
--- a/src/platform/windows/driver/CMakeLists.txt
+++ b/src/platform/windows/driver/CMakeLists.txt
@@ -248,6 +248,35 @@ set_target_properties(libvirtualhid_umdf PROPERTIES
OUTPUT_NAME libvirtualhid_umdf
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/package")
+add_library(libvirtualhid_xbox360_umdf SHARED
+ "${CMAKE_CURRENT_SOURCE_DIR}/libvirtualhid_xbox360_umdf.cpp")
+target_include_directories(libvirtualhid_xbox360_umdf
+ PRIVATE
+ "${LIBVIRTUALHID_WDF_INCLUDE_DIR}"
+ "${LIBVIRTUALHID_VHF_INCLUDE_DIR}"
+ "${PROJECT_SOURCE_DIR}/src/platform/windows/shared")
+target_compile_definitions(libvirtualhid_xbox360_umdf
+ PRIVATE
+ NOMINMAX
+ WIN32_LEAN_AND_MEAN
+ UMDF_USING_NTSTATUS
+ NTDDI_VERSION=0x0A000000
+ _WIN32_WINNT=0x0A00)
+target_compile_options(libvirtualhid_xbox360_umdf PRIVATE /W4)
+if(LIBVIRTUALHID_WARNINGS_AS_ERRORS)
+ target_compile_options(libvirtualhid_xbox360_umdf PRIVATE /WX)
+endif()
+target_link_libraries(libvirtualhid_xbox360_umdf
+ PRIVATE
+ advapi32
+ "${LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY}"
+ "${LIBVIRTUALHID_VHF_UM_LIBRARY}"
+ "${LIBVIRTUALHID_NTDLL_LIBRARY}")
+set_target_properties(libvirtualhid_xbox360_umdf PROPERTIES
+ MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>"
+ OUTPUT_NAME libvirtualhid_xbox360_umdf
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/package")
+
add_custom_target(libvirtualhid_windows_inf
COMMAND "${CMAKE_COMMAND}" -E make_directory
"$"
@@ -261,6 +290,7 @@ add_custom_target(libvirtualhid_windows_inf
-v "${LIBVIRTUALHID_DRIVER_VERSION}"
DEPENDS
libvirtualhid_umdf
+ libvirtualhid_xbox360_umdf
"${CMAKE_CURRENT_BINARY_DIR}/libvirtualhid.inf"
COMMENT "Preparing libvirtualhid driver INF"
VERBATIM)
@@ -281,17 +311,26 @@ if(LIBVIRTUALHID_INSTALL)
install(TARGETS libvirtualhid_umdf
RUNTIME DESTINATION "drivers/windows"
COMPONENT driver)
+ install(TARGETS libvirtualhid_xbox360_umdf
+ RUNTIME DESTINATION "drivers/windows"
+ COMPONENT driver)
install(CODE
"
set(lvh_driver_dll \"$\")
+ set(lvh_xbox360_driver_dll \"$\")
set(lvh_driver_inf \"$/libvirtualhid.inf\")
set(lvh_driver_cat \"$/libvirtualhid.cat\")
- foreach(lvh_driver_file IN ITEMS \"\${lvh_driver_dll}\" \"\${lvh_driver_inf}\" \"\${lvh_driver_cat}\")
+ foreach(lvh_driver_file IN ITEMS
+ \"\${lvh_driver_dll}\"
+ \"\${lvh_xbox360_driver_dll}\"
+ \"\${lvh_driver_inf}\"
+ \"\${lvh_driver_cat}\")
if(NOT EXISTS \"\${lvh_driver_file}\")
message(FATAL_ERROR \"Windows driver package file is missing: \${lvh_driver_file}\")
endif()
endforeach()
if(\"\${lvh_driver_dll}\" IS_NEWER_THAN \"\${lvh_driver_cat}\"
+ OR \"\${lvh_xbox360_driver_dll}\" IS_NEWER_THAN \"\${lvh_driver_cat}\"
OR \"\${lvh_driver_inf}\" IS_NEWER_THAN \"\${lvh_driver_cat}\")
message(FATAL_ERROR
\"Windows driver catalog is stale; build libvirtualhid_windows_catalog \"
diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in
index fcfaa34..7b86278 100644
--- a/src/platform/windows/driver/libvirtualhid.inf.in
+++ b/src/platform/windows/driver/libvirtualhid.inf.in
@@ -23,9 +23,11 @@ PnpLockdown=1
[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...19041]
%DeviceName%=DeviceInstall,ROOT\LIBVIRTUALHID
+%Xbox360DeviceName%=Xbox360Install,SWD\LIBVIRTUALHID_XBOX360
[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...22000]
%DeviceName%=DeviceInstall_Win11,ROOT\LIBVIRTUALHID
+%Xbox360DeviceName%=Xbox360Install_Win11,SWD\LIBVIRTUALHID_XBOX360
[DestinationDirs]
UMDriverCopy=13
@@ -35,6 +37,7 @@ UMDriverCopy=13
[SourceDisksFiles]
libvirtualhid_umdf.dll=1
+libvirtualhid_xbox360_umdf.dll=1
[DeviceInstall.NT]
CopyFiles=UMDriverCopy
@@ -44,6 +47,47 @@ AddReg=DeviceInstall_Device_AddReg,DeviceInstall_Vhf_AddReg
[UMDriverCopy]
libvirtualhid_umdf.dll
+libvirtualhid_xbox360_umdf.dll
+
+[Xbox360Install.NT]
+CopyFiles=UMDriverCopy
+
+[Xbox360Install.NT.HW]
+AddReg=Xbox360_Device_AddReg,Xbox360_Vhf_AddReg
+
+[Xbox360_Device_AddReg]
+; The XUSB endpoint must be openable by normal game processes. Private
+; libvirtualhid IOCTLs on that interface require a broker-created session token.
+HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGWGX;;;BU)(A;;GRGWGX;;;AU)(A;;GRGWGX;;;WD)(A;;GR;;;RC)"
+; Windows.Gaming.Input uses this ancestor value as an XUSB classifier marker.
+; xinputhid.sys does not attach to this System-class function device.
+HKR,,"UpperFilters",0x00010008,"xinputhid"
+
+[Xbox360_Vhf_AddReg]
+HKR,,"LowerFilters",0x00010008,"vhf"
+HKR,,VhfMode,0x00010001,0x1
+
+[Xbox360Install.NT.Interfaces]
+AddInterface={EC87F1E3-C13B-4100-B5F7-8B84D54260CB},,Xbox360_Xusb_Interface
+
+[Xbox360_Xusb_Interface]
+AddReg=Xbox360_Xusb_Interface_AddReg
+
+[Xbox360_Xusb_Interface_AddReg]
+HKR,,DeviceEnabled,0x00010001,1
+
+[Xbox360Install.NT.Services]
+AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall
+
+[Xbox360Install.NT.Wdf]
+UmdfService=libvirtualhid_xbox360_umdf,libvirtualhid_xbox360_umdf_Install
+UmdfServiceOrder=libvirtualhid_xbox360_umdf
+UmdfHostProcessSharing=ProcessSharingDisabled
+UmdfHostPriority=PriorityHigh
+UmdfKernelModeClientPolicy=AllowKernelModeClients
+UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects
+UmdfMethodNeitherAction=Copy
+UmdfFsContextUsePolicy=CanUseFsContext2
[DeviceInstall_Device_AddReg]
HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GRGWGX;;;BU)(A;;GRGWGX;;;AU)(A;;GRGW;;;WD)(A;;GR;;;RC)"
@@ -88,12 +132,44 @@ UmdfServiceOrder=libvirtualhid_umdf
UmdfHostProcessSharing=ProcessSharingDisabled
UmdfHostPriority=PriorityHigh
+[Xbox360Install_Win11.NT]
+CopyFiles=UMDriverCopy
+Include=wudfrd.inf
+Needs=WUDFRD.NT
+
+[Xbox360Install_Win11.NT.HW]
+AddReg=Xbox360_Device_AddReg,Xbox360_Vhf_AddReg
+Include=wudfrd.inf
+Needs=WUDFRD.NT.HW
+
+[Xbox360Install_Win11.NT.Interfaces]
+AddInterface={EC87F1E3-C13B-4100-B5F7-8B84D54260CB},,Xbox360_Xusb_Interface
+
+[Xbox360Install_Win11.NT.Services]
+Include=wudfrd.inf
+Needs=WUDFRD.NT.Services
+
+[Xbox360Install_Win11.NT.Wdf]
+UmdfService=libvirtualhid_xbox360_umdf,libvirtualhid_xbox360_umdf_Install
+UmdfServiceOrder=libvirtualhid_xbox360_umdf
+UmdfHostProcessSharing=ProcessSharingDisabled
+UmdfHostPriority=PriorityHigh
+UmdfKernelModeClientPolicy=AllowKernelModeClients
+UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects
+UmdfMethodNeitherAction=Copy
+UmdfFsContextUsePolicy=CanUseFsContext2
+
[libvirtualhid_umdf_Install]
UmdfLibraryVersion=@LIBVIRTUALHID_UMDF_LIBRARY_VERSION@
ServiceBinary=%13%\libvirtualhid_umdf.dll
+[libvirtualhid_xbox360_umdf_Install]
+UmdfLibraryVersion=@LIBVIRTUALHID_UMDF_LIBRARY_VERSION@
+ServiceBinary=%13%\libvirtualhid_xbox360_umdf.dll
+
[Strings]
ManufacturerName="LizardByte"
DiskName="libvirtualhid UMDF Driver Install Disk"
DeviceName="libvirtualhid Virtual HID Control Device"
+Xbox360DeviceName="libvirtualhid Xbox 360 Controller"
WudfRdDisplayName="Windows Driver Foundation - User-mode Driver Framework Reflector"
diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp
index 992e480..70d1562 100644
--- a/src/platform/windows/driver/libvirtualhid_umdf.cpp
+++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp
@@ -603,7 +603,8 @@ namespace {
request.bus_type == LVH_WINDOWS_BUS_BLUETOOTH;
const auto known_profile = request.gamepad_kind <= LVH_WINDOWS_GAMEPAD_DUALSHOCK4;
const auto valid_driver_descriptor =
- request.device_type == LVH_WINDOWS_DEVICE_GAMEPAD ||
+ (request.device_type == LVH_WINDOWS_DEVICE_GAMEPAD &&
+ request.gamepad_kind != LVH_WINDOWS_GAMEPAD_XBOX_360) ||
(request.device_type == LVH_WINDOWS_DEVICE_KEYBOARD &&
descriptor_size == lvh::detail::windows::keyboard_report_descriptor.size() &&
std::equal(
diff --git a/src/platform/windows/driver/libvirtualhid_xbox360_umdf.cpp b/src/platform/windows/driver/libvirtualhid_xbox360_umdf.cpp
new file mode 100644
index 0000000..ab785e7
--- /dev/null
+++ b/src/platform/windows/driver/libvirtualhid_xbox360_umdf.cpp
@@ -0,0 +1,973 @@
+// SPDX-FileCopyrightText: 2026 LIZARDBYTE LLC
+// SPDX-License-Identifier: LicenseRef-LizardByte-SAL-1.0
+
+/**
+ * @file src/platform/windows/driver/libvirtualhid_xbox360_umdf.cpp
+ * @brief Per-controller UMDF2 XUSB personality for Xbox 360 emulation.
+ */
+
+#ifndef NOMINMAX
+ #define NOMINMAX
+#endif
+#ifndef WIN32_LEAN_AND_MEAN
+ #define WIN32_LEAN_AND_MEAN
+#endif
+
+#define WIN32_NO_STATUS
+#include
+#undef WIN32_NO_STATUS
+
+#if defined(_MSC_VER)
+ #pragma warning(push)
+ #pragma warning(disable : 4324 4471)
+#endif
+#include
+// VHF uses UMDF WDM types declared by wdf.h.
+#include
+#if defined(_MSC_VER)
+ #pragma warning(pop)
+#endif
+
+#include "windows_device_identity.hpp"
+#include "xbox_360_protocol.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using VhfContext = PVOID; // NOSONAR(cpp:S5008): required by the VHF callback ABI.
+
+extern "C" DRIVER_INITIALIZE DriverEntry;
+EVT_WDF_DRIVER_DEVICE_ADD LvhXbox360EvtDeviceAdd;
+EVT_WDF_FILE_CLEANUP LvhXbox360EvtFileCleanup;
+EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL LvhXbox360EvtIoDeviceControl;
+EVT_WDF_OBJECT_CONTEXT_CLEANUP LvhXbox360EvtDeviceCleanup;
+EVT_WDF_TIMER LvhXbox360EvtInputPump;
+EVT_VHF_ASYNC_OPERATION LvhXbox360EvtVhfGetInputReport;
+EVT_VHF_READY_FOR_NEXT_READ_REPORT LvhXbox360EvtVhfReadyForNextReadReport;
+
+namespace {
+
+ constexpr auto xbox_version = std::uint16_t {0x0103U};
+ constexpr auto xbox_input_pump_period_ms = 8U;
+
+ struct VhfSubmission {
+ VHFHANDLE handle {};
+ std::shared_ptr> report;
+ std::uint8_t report_id {};
+ };
+
+ struct VirtualHidState {
+ WDFIOTARGET target {};
+ VHFHANDLE handle {};
+ std::vector report_descriptor;
+ std::wstring hardware_ids;
+ std::vector latest_report;
+ std::optional> pending_report;
+ std::shared_ptr> in_flight_report;
+ std::size_t active_submissions {};
+ bool ready {};
+ };
+
+ struct Xbox360DriverState {
+ std::mutex mutex;
+ std::condition_variable submissions_drained;
+ WDFDEVICE device {};
+ WDFQUEUE wait_input_queue {};
+ WDFQUEUE output_queue {};
+ WDFTIMER input_pump {};
+ LvhWindowsCreateDeviceRequest create_request {};
+ LvhWindowsSessionToken session_token {};
+ LvhWindowsXbox360InputState input {};
+ VirtualHidState virtual_hid;
+ std::optional buffered_output;
+ std::uint64_t driver_device_id {};
+ std::uint32_t packet_number {};
+ std::uint8_t low_frequency_rumble {};
+ std::uint8_t high_frequency_rumble {};
+ bool initialized {};
+ bool shutting_down {};
+ };
+
+ Xbox360DriverState &driver_state() {
+ static Xbox360DriverState state;
+ return state;
+ }
+
+ template
+ void write_little_endian(std::span output, std::size_t offset, Value value) {
+ static_assert(std::is_integral_v);
+ using Unsigned = std::make_unsigned_t;
+ const auto raw = static_cast(value);
+ for (std::size_t byte = 0; byte < sizeof(Value); ++byte) {
+ output[offset + byte] = static_cast(raw >> (byte * 8U));
+ }
+ }
+
+ bool token_matches(const LvhWindowsSessionToken &left, const LvhWindowsSessionToken &right) {
+ return std::ranges::equal(left.bytes, right.bytes);
+ }
+
+ bool valid_private_header(std::uint32_t version, std::uint32_t size, std::size_t expected_size) {
+ return version == LVH_WINDOWS_XBOX360_PROTOCOL_VERSION && size == expected_size;
+ }
+
+ bool initialized_request(
+ std::uint64_t driver_device_id,
+ const LvhWindowsSessionToken &session_token
+ ) {
+ auto &state = driver_state();
+ std::lock_guard lock {state.mutex};
+ return state.initialized && !state.shutting_down && state.driver_device_id == driver_device_id &&
+ token_matches(state.session_token, session_token);
+ }
+
+ void complete_request(WDFREQUEST request, NTSTATUS status, ULONG_PTR information = 0U) {
+ WdfRequestCompleteWithInformation(request, status, information);
+ }
+
+ template
+ NTSTATUS retrieve_input(WDFREQUEST request, Value *&value) {
+ void *buffer = nullptr;
+ size_t size = 0;
+ const auto status = WdfRequestRetrieveInputBuffer(request, sizeof(Value), &buffer, &size);
+ if (!NT_SUCCESS(status)) {
+ return status;
+ }
+ if (size < sizeof(Value)) {
+ return STATUS_BUFFER_TOO_SMALL;
+ }
+ value = static_cast(buffer);
+ return STATUS_SUCCESS;
+ }
+
+ NTSTATUS copy_to_request(WDFREQUEST request, std::span bytes) {
+ void *buffer = nullptr;
+ size_t size = 0;
+ const auto status = WdfRequestRetrieveOutputBuffer(request, bytes.size(), &buffer, &size);
+ if (!NT_SUCCESS(status) || size < bytes.size()) {
+ complete_request(request, STATUS_BUFFER_TOO_SMALL);
+ return STATUS_BUFFER_TOO_SMALL;
+ }
+ std::memcpy(buffer, bytes.data(), bytes.size());
+ complete_request(request, STATUS_SUCCESS, bytes.size());
+ return STATUS_SUCCESS;
+ }
+
+ NTSTATUS copy_event_to_request(WDFREQUEST request, const LvhWindowsOutputReportEvent &event) {
+ void *buffer = nullptr;
+ size_t size = 0;
+ const auto status = WdfRequestRetrieveOutputBuffer(request, sizeof(event), &buffer, &size);
+ if (!NT_SUCCESS(status) || size < sizeof(event)) {
+ complete_request(request, STATUS_BUFFER_TOO_SMALL);
+ return STATUS_BUFFER_TOO_SMALL;
+ }
+ std::memcpy(buffer, &event, sizeof(event));
+ complete_request(request, STATUS_SUCCESS, sizeof(event));
+ return STATUS_SUCCESS;
+ }
+
+ std::array make_xusb_state(bool wait_for_input) {
+ auto &state = driver_state();
+ std::array output {};
+ LvhWindowsXbox360InputState input {};
+ std::uint32_t packet_number = 0;
+ {
+ std::lock_guard lock {state.mutex};
+ input = state.input;
+ packet_number = state.packet_number;
+ }
+
+ write_little_endian(output, 0U, xbox_version);
+ output[2] = wait_for_input ? 0x03U : 0x01U;
+ write_little_endian(output, 5U, packet_number);
+ if (wait_for_input) {
+ output[9] = 0x00U;
+ output[10] = 0x14U;
+ }
+ write_little_endian(output, 11U, input.buttons);
+ output[13] = input.left_trigger;
+ output[14] = input.right_trigger;
+ write_little_endian(output, 15U, input.left_thumb_x);
+ write_little_endian(output, 17U, input.left_thumb_y);
+ write_little_endian(output, 19U, input.right_thumb_x);
+ write_little_endian(output, 21U, input.right_thumb_y);
+ return output;
+ }
+
+ void complete_one_wait_for_input() {
+ auto &state = driver_state();
+ WDFQUEUE queue = nullptr;
+ {
+ std::lock_guard lock {state.mutex};
+ if (state.shutting_down) {
+ return;
+ }
+ queue = state.wait_input_queue;
+ }
+ if (queue == nullptr) {
+ return;
+ }
+
+ WDFREQUEST request = nullptr;
+ if (!NT_SUCCESS(WdfIoQueueRetrieveNextRequest(queue, &request))) {
+ return;
+ }
+ const auto output = make_xusb_state(true);
+ static_cast(copy_to_request(request, output));
+ }
+
+ std::uint8_t signed_axis_to_hid(std::int16_t value, bool invert) {
+ const auto adjusted = invert ? -static_cast(value) : static_cast(value);
+ const auto clamped = std::clamp(adjusted, -32768, 32767);
+ return static_cast(
+ (static_cast(clamped + 32768) * 255U + 32767U) / 65535U
+ );
+ }
+
+ std::uint16_t xinput_to_standard_buttons(std::uint16_t buttons) {
+ auto result = std::uint16_t {};
+ const auto add = [&](std::uint16_t source, std::uint16_t destination) {
+ if ((buttons & source) != 0U) {
+ result = static_cast(result | destination);
+ }
+ };
+ add(LVH_WINDOWS_XINPUT_A, 1U << 0U);
+ add(LVH_WINDOWS_XINPUT_B, 1U << 1U);
+ add(LVH_WINDOWS_XINPUT_X, 1U << 2U);
+ add(LVH_WINDOWS_XINPUT_Y, 1U << 3U);
+ add(LVH_WINDOWS_XINPUT_LEFT_SHOULDER, 1U << 4U);
+ add(LVH_WINDOWS_XINPUT_RIGHT_SHOULDER, 1U << 5U);
+ add(LVH_WINDOWS_XINPUT_BACK, 1U << 6U);
+ add(LVH_WINDOWS_XINPUT_START, 1U << 7U);
+ add(LVH_WINDOWS_XINPUT_LEFT_THUMB, 1U << 8U);
+ add(LVH_WINDOWS_XINPUT_RIGHT_THUMB, 1U << 9U);
+ add(LVH_WINDOWS_XINPUT_GUIDE, 1U << 10U);
+ add(LVH_WINDOWS_XINPUT_DPAD_UP, 1U << 12U);
+ add(LVH_WINDOWS_XINPUT_DPAD_DOWN, 1U << 13U);
+ add(LVH_WINDOWS_XINPUT_DPAD_LEFT, 1U << 14U);
+ add(LVH_WINDOWS_XINPUT_DPAD_RIGHT, 1U << 15U);
+ return result;
+ }
+
+ std::vector make_hid_report(
+ const LvhWindowsCreateDeviceRequest &create_request,
+ const LvhWindowsXbox360InputState &input
+ ) {
+ std::vector report(create_request.report_sizes.input_report_size, 0U);
+ if (report.size() < 9U) {
+ return {};
+ }
+ report[0] = create_request.hardware_ids.report_id;
+ const auto buttons = xinput_to_standard_buttons(input.buttons);
+ report[1] = static_cast(buttons & 0xFFU);
+ report[2] = static_cast(buttons >> 8U);
+ report[3] = signed_axis_to_hid(input.left_thumb_x, false);
+ report[4] = signed_axis_to_hid(input.left_thumb_y, true);
+ report[5] = input.left_trigger;
+ report[6] = signed_axis_to_hid(input.right_thumb_x, false);
+ report[7] = signed_axis_to_hid(input.right_thumb_y, true);
+ report[8] = input.right_trigger;
+ return report;
+ }
+
+ std::optional prepare_vhf_submission() {
+ auto &state = driver_state();
+ std::lock_guard lock {state.mutex};
+ if (state.shutting_down || state.virtual_hid.handle == nullptr || !state.virtual_hid.ready || !state.virtual_hid.pending_report.has_value()) {
+ return std::nullopt;
+ }
+
+ auto report = std::make_shared>(std::move(*state.virtual_hid.pending_report));
+ state.virtual_hid.pending_report.reset();
+ state.virtual_hid.in_flight_report = report;
+ state.virtual_hid.ready = false;
+ ++state.virtual_hid.active_submissions;
+ return VhfSubmission {
+ .handle = state.virtual_hid.handle,
+ .report = std::move(report),
+ .report_id = state.create_request.hardware_ids.report_id,
+ };
+ }
+
+ NTSTATUS submit_pending_hid_report() {
+ auto submission = prepare_vhf_submission();
+ if (!submission.has_value()) {
+ return STATUS_SUCCESS;
+ }
+
+ HID_XFER_PACKET packet {};
+ packet.reportBuffer = submission->report->data();
+ packet.reportBufferLen = static_cast(submission->report->size());
+ packet.reportId = submission->report_id;
+ const auto status = VhfReadReportSubmit(submission->handle, &packet);
+
+ auto &state = driver_state();
+ {
+ std::lock_guard lock {state.mutex};
+ --state.virtual_hid.active_submissions;
+ if (!NT_SUCCESS(status) && state.virtual_hid.in_flight_report == submission->report) {
+ state.virtual_hid.in_flight_report.reset();
+ }
+ }
+ state.submissions_drained.notify_all();
+ return status;
+ }
+
+ NTSTATUS queue_hid_report(std::vector report) {
+ auto &state = driver_state();
+ {
+ std::lock_guard lock {state.mutex};
+ if (state.shutting_down || state.virtual_hid.handle == nullptr) {
+ return STATUS_SUCCESS;
+ }
+ state.virtual_hid.latest_report = report;
+ state.virtual_hid.pending_report = std::move(report);
+ }
+ return submit_pending_hid_report();
+ }
+
+ NTSTATUS initialize_vhf() {
+ auto &state = driver_state();
+ WDF_OBJECT_ATTRIBUTES target_attributes;
+ WDF_OBJECT_ATTRIBUTES_INIT(&target_attributes);
+ target_attributes.ParentObject = state.device;
+
+ WDFIOTARGET target = nullptr;
+ auto status = WdfIoTargetCreate(state.device, &target_attributes, &target);
+ if (!NT_SUCCESS(status)) {
+ return status;
+ }
+
+ WDF_IO_TARGET_OPEN_PARAMS open_params;
+ WDF_IO_TARGET_OPEN_PARAMS_INIT_OPEN_BY_FILE(&open_params, nullptr);
+ status = WdfIoTargetOpen(target, &open_params);
+ if (!NT_SUCCESS(status)) {
+ WdfObjectDelete(target);
+ return status;
+ }
+
+ const auto descriptor_size = state.create_request.report_sizes.report_descriptor_size;
+ state.virtual_hid.report_descriptor.assign(
+ state.create_request.report_descriptor.begin(),
+ state.create_request.report_descriptor.begin() + descriptor_size
+ );
+ state.virtual_hid.hardware_ids = lvh::detail::windows::make_hardware_ids(state.create_request);
+
+ VHF_CONFIG config;
+ VHF_CONFIG_INIT(
+ &config,
+ WdfIoTargetWdmGetTargetFileHandle(target),
+ static_cast(state.virtual_hid.report_descriptor.size()),
+ state.virtual_hid.report_descriptor.data()
+ );
+ config.VhfClientContext = &state;
+ config.VendorID = state.create_request.hardware_ids.vendor_id;
+ config.ProductID = state.create_request.hardware_ids.product_id;
+ config.VersionNumber = state.create_request.hardware_ids.device_version;
+ config.HardwareIDsLength = static_cast(state.virtual_hid.hardware_ids.size() * sizeof(wchar_t));
+ config.HardwareIDs = state.virtual_hid.hardware_ids.data();
+ config.EvtVhfReadyForNextReadReport = LvhXbox360EvtVhfReadyForNextReadReport;
+ config.EvtVhfAsyncOperationGetInputReport = LvhXbox360EvtVhfGetInputReport;
+
+ VHFHANDLE vhf_handle = nullptr;
+ status = VhfCreate(&config, &vhf_handle);
+ if (!NT_SUCCESS(status)) {
+ WdfObjectDelete(target);
+ return status;
+ }
+ status = VhfStart(vhf_handle);
+ if (!NT_SUCCESS(status)) {
+ VhfDelete(vhf_handle, TRUE);
+ WdfObjectDelete(target);
+ return status;
+ }
+
+ {
+ std::lock_guard lock {state.mutex};
+ state.virtual_hid.target = target;
+ state.virtual_hid.handle = vhf_handle;
+ }
+ return STATUS_SUCCESS;
+ }
+
+ void cleanup_driver_state() {
+ auto &state = driver_state();
+ WDFQUEUE wait_queue = nullptr;
+ WDFQUEUE output_queue = nullptr;
+ WDFIOTARGET target = nullptr;
+ VHFHANDLE vhf_handle = nullptr;
+ {
+ std::unique_lock lock {state.mutex};
+ state.shutting_down = true;
+ wait_queue = state.wait_input_queue;
+ output_queue = state.output_queue;
+ vhf_handle = state.virtual_hid.handle;
+ state.virtual_hid.handle = nullptr;
+ state.virtual_hid.ready = false;
+ state.virtual_hid.pending_report.reset();
+ state.submissions_drained.wait(lock, [&state] {
+ return state.virtual_hid.active_submissions == 0U;
+ });
+ target = state.virtual_hid.target;
+ state.virtual_hid.target = nullptr;
+ }
+
+ if (wait_queue != nullptr) {
+ WdfIoQueuePurgeSynchronously(wait_queue);
+ }
+ if (output_queue != nullptr) {
+ WdfIoQueuePurgeSynchronously(output_queue);
+ }
+ if (vhf_handle != nullptr) {
+ VhfDelete(vhf_handle, TRUE);
+ }
+ if (target != nullptr) {
+ WdfObjectDelete(target);
+ }
+
+ std::lock_guard lock {state.mutex};
+ state.virtual_hid.in_flight_report.reset();
+ state.buffered_output.reset();
+ }
+
+ bool request_is_broker(WDFREQUEST request) {
+ const auto requestor_process_id = WdfRequestGetRequestorProcessId(request);
+ if (requestor_process_id == 0U) {
+ return false;
+ }
+
+ using UniqueServiceHandle = std::unique_ptr<
+ std::remove_pointer_t,
+ decltype(&::CloseServiceHandle)>;
+ auto service_manager = UniqueServiceHandle {
+ ::OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT),
+ &::CloseServiceHandle,
+ };
+ if (!service_manager) {
+ return false;
+ }
+ auto service = UniqueServiceHandle {
+ ::OpenServiceW(service_manager.get(), L"libvirtualhid_broker", SERVICE_QUERY_STATUS),
+ &::CloseServiceHandle,
+ };
+ if (!service) {
+ return false;
+ }
+
+ SERVICE_STATUS_PROCESS service_status {};
+ DWORD bytes_needed = 0;
+ auto service_status_bytes = std::as_writable_bytes(std::span {&service_status, 1U});
+ return ::QueryServiceStatusEx(
+ service.get(),
+ SC_STATUS_PROCESS_INFO,
+ std::bit_cast(service_status_bytes.data()),
+ static_cast(service_status_bytes.size()),
+ &bytes_needed
+ ) != FALSE &&
+ service_status.dwCurrentState == SERVICE_RUNNING &&
+ service_status.dwProcessId == requestor_process_id;
+ }
+
+ void handle_initialize(WDFREQUEST request) {
+ if (!request_is_broker(request)) {
+ complete_request(request, STATUS_ACCESS_DENIED);
+ return;
+ }
+
+ auto *initialize = static_cast(nullptr);
+ const auto status = retrieve_input(request, initialize);
+ if (!NT_SUCCESS(status)) {
+ complete_request(request, status);
+ return;
+ }
+ const auto &device = initialize->device;
+ if (!valid_private_header(initialize->version, initialize->size, sizeof(*initialize)) || device.version != LVH_WINDOWS_CONTROL_PROTOCOL_VERSION || device.size != sizeof(device) || device.device_type != LVH_WINDOWS_DEVICE_GAMEPAD || device.gamepad_kind != LVH_WINDOWS_GAMEPAD_XBOX_360 || initialize->driver_device_id == 0U || device.report_sizes.report_descriptor_size == 0U || device.report_sizes.report_descriptor_size > LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE || device.report_sizes.input_report_size < 9U || device.report_sizes.input_report_size > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE) {
+ complete_request(request, STATUS_INVALID_PARAMETER);
+ return;
+ }
+
+ {
+ auto &state = driver_state();
+ std::lock_guard lock {state.mutex};
+ if (state.initialized) {
+ complete_request(request, STATUS_DEVICE_BUSY);
+ return;
+ }
+ state.driver_device_id = initialize->driver_device_id;
+ state.session_token = initialize->session_token;
+ state.create_request = initialize->device;
+ }
+
+ const auto vhf_status = initialize_vhf();
+ if (!NT_SUCCESS(vhf_status)) {
+ complete_request(request, vhf_status);
+ return;
+ }
+
+ auto initial_report = std::vector {};
+ {
+ auto &state = driver_state();
+ std::lock_guard lock {state.mutex};
+ state.initialized = true;
+ initial_report = make_hid_report(state.create_request, state.input);
+ }
+ static_cast(queue_hid_report(std::move(initial_report)));
+ complete_request(request, STATUS_SUCCESS);
+ }
+
+ bool same_input(
+ const LvhWindowsXbox360InputState &left,
+ const LvhWindowsXbox360InputState &right
+ ) {
+ return std::memcmp(&left, &right, sizeof(left)) == 0;
+ }
+
+ void handle_submit_input(WDFREQUEST request) {
+ auto *submit = static_cast(nullptr);
+ const auto status = retrieve_input(request, submit);
+ if (!NT_SUCCESS(status)) {
+ complete_request(request, status);
+ return;
+ }
+ if (!valid_private_header(submit->version, submit->size, sizeof(*submit)) || !initialized_request(submit->driver_device_id, submit->session_token)) {
+ complete_request(request, STATUS_ACCESS_DENIED);
+ return;
+ }
+
+ LvhWindowsCreateDeviceRequest create_request {};
+ bool changed = false;
+ {
+ auto &state = driver_state();
+ std::lock_guard lock {state.mutex};
+ changed = !same_input(state.input, submit->state);
+ if (changed) {
+ state.input = submit->state;
+ ++state.packet_number;
+ }
+ create_request = state.create_request;
+ }
+ if (changed) {
+ const auto hid_status = queue_hid_report(make_hid_report(create_request, submit->state));
+ if (!NT_SUCCESS(hid_status)) {
+ complete_request(request, hid_status);
+ return;
+ }
+ complete_one_wait_for_input();
+ }
+ complete_request(request, STATUS_SUCCESS);
+ }
+
+ void handle_read_output(WDFREQUEST request) {
+ auto *read = static_cast(nullptr);
+ const auto status = retrieve_input(request, read);
+ if (!NT_SUCCESS(status)) {
+ complete_request(request, status);
+ return;
+ }
+ if (!valid_private_header(read->version, read->size, sizeof(*read)) || !initialized_request(read->driver_device_id, read->session_token)) {
+ complete_request(request, STATUS_ACCESS_DENIED);
+ return;
+ }
+
+ auto &state = driver_state();
+ std::optional event;
+ WDFQUEUE queue = nullptr;
+ {
+ std::lock_guard lock {state.mutex};
+ event = std::move(state.buffered_output);
+ state.buffered_output.reset();
+ queue = state.output_queue;
+ }
+ if (event.has_value()) {
+ static_cast(copy_event_to_request(request, *event));
+ return;
+ }
+
+ const auto forward_status = WdfRequestForwardToIoQueue(request, queue);
+ if (!NT_SUCCESS(forward_status)) {
+ complete_request(request, forward_status);
+ }
+ }
+
+ void queue_rumble_output(std::uint8_t low_frequency, std::uint8_t high_frequency) {
+ auto &state = driver_state();
+ LvhWindowsOutputReportEvent event {};
+ WDFQUEUE queue = nullptr;
+ {
+ std::lock_guard lock {state.mutex};
+ if (!state.initialized || state.shutting_down || (state.low_frequency_rumble == low_frequency && state.high_frequency_rumble == high_frequency)) {
+ return;
+ }
+ state.low_frequency_rumble = low_frequency;
+ state.high_frequency_rumble = high_frequency;
+ event.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION;
+ event.size = sizeof(event);
+ event.driver_device_id = state.driver_device_id;
+ event.report_size = 5U;
+ event.report[0] = state.create_request.hardware_ids.report_id;
+ const auto low = static_cast(low_frequency) * 257U;
+ const auto high = static_cast(high_frequency) * 257U;
+ event.report[1] = static_cast(low & 0xFFU);
+ event.report[2] = static_cast(low >> 8U);
+ event.report[3] = static_cast(high & 0xFFU);
+ event.report[4] = static_cast(high >> 8U);
+ queue = state.output_queue;
+ }
+
+ WDFREQUEST request = nullptr;
+ if (queue != nullptr && NT_SUCCESS(WdfIoQueueRetrieveNextRequest(queue, &request))) {
+ static_cast(copy_event_to_request(request, event));
+ return;
+ }
+ std::lock_guard lock {state.mutex};
+ state.buffered_output = event;
+ }
+
+ void handle_xusb_information(WDFREQUEST request) {
+ std::array information {};
+ write_little_endian(information, 0U, xbox_version);
+ information[2] = 0x01U;
+ std::uint16_t vendor_id = 0x045EU;
+ std::uint16_t product_id = 0x028EU;
+ {
+ auto &state = driver_state();
+ std::lock_guard lock {state.mutex};
+ if (state.initialized) {
+ vendor_id = state.create_request.hardware_ids.vendor_id;
+ product_id = state.create_request.hardware_ids.product_id;
+ }
+ }
+ write_little_endian(information, 8U, vendor_id);
+ write_little_endian(information, 10U, product_id);
+ static_cast(copy_to_request(request, information));
+ }
+
+ void handle_xusb_capabilities(WDFREQUEST request, size_t output_buffer_length) {
+ constexpr std::array capabilities_v1 {
+ 0x03U,
+ 0x01U,
+ 0x00U,
+ 0x01U,
+ 0xFFU,
+ 0xF7U,
+ 0xFFU,
+ 0xFFU,
+ 0xC0U,
+ 0xFFU,
+ 0xC0U,
+ 0xFFU,
+ 0xC0U,
+ 0xFFU,
+ 0xC0U,
+ 0xFFU,
+ 0xFFU,
+ 0xFFU,
+ 0xFFU,
+ 0xFFU,
+ 0x00U,
+ 0x00U,
+ 0xFFU,
+ 0xFFU,
+ };
+ if (output_buffer_length < 36U) {
+ static_cast(copy_to_request(request, capabilities_v1));
+ return;
+ }
+
+ std::array capabilities_v2 {
+ 0x03U,
+ 0x01U,
+ 0x01U,
+ 0x01U,
+ 0x0CU,
+ 0x00U,
+ 0x5EU,
+ 0x04U,
+ 0x8EU,
+ 0x02U,
+ 0x10U,
+ 0x01U,
+ 0x00U,
+ 0xFAU,
+ 0x34U,
+ 0x22U,
+ 0xFFU,
+ 0xF7U,
+ 0xFFU,
+ 0xFFU,
+ 0xC0U,
+ 0xFFU,
+ 0xC0U,
+ 0xFFU,
+ 0xC0U,
+ 0xFFU,
+ 0xC0U,
+ 0xFFU,
+ 0xFFU,
+ 0xFFU,
+ 0xFFU,
+ 0xFFU,
+ 0x00U,
+ 0x00U,
+ 0xFFU,
+ 0xFFU,
+ };
+ {
+ auto &state = driver_state();
+ std::lock_guard lock {state.mutex};
+ if (state.initialized) {
+ capabilities_v2[6] = static_cast(state.create_request.hardware_ids.vendor_id & 0xFFU);
+ capabilities_v2[7] = static_cast(state.create_request.hardware_ids.vendor_id >> 8U);
+ capabilities_v2[8] = static_cast(state.create_request.hardware_ids.product_id & 0xFFU);
+ capabilities_v2[9] = static_cast(state.create_request.hardware_ids.product_id >> 8U);
+ }
+ }
+ static_cast(copy_to_request(request, capabilities_v2));
+ }
+
+ void handle_xusb_set_state(WDFREQUEST request) {
+ void *buffer = nullptr;
+ size_t size = 0;
+ if (NT_SUCCESS(WdfRequestRetrieveInputBuffer(request, 4U, &buffer, &size)) && size >= 4U) {
+ const auto bytes = static_cast(buffer);
+ if (size >= 5U) {
+ if (bytes[4] == 0x02U) {
+ queue_rumble_output(bytes[2], bytes[3]);
+ }
+ } else {
+ queue_rumble_output(bytes[1], bytes[3]);
+ }
+ }
+ complete_request(request, STATUS_SUCCESS);
+ }
+
+} // namespace
+
+extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT driver_object, PUNICODE_STRING registry_path) {
+ WDF_DRIVER_CONFIG config;
+ WDF_DRIVER_CONFIG_INIT(&config, LvhXbox360EvtDeviceAdd);
+ return WdfDriverCreate(driver_object, registry_path, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE);
+}
+
+NTSTATUS LvhXbox360EvtDeviceAdd(WDFDRIVER driver, PWDFDEVICE_INIT device_init) {
+ UNREFERENCED_PARAMETER(driver);
+
+ WDF_FILEOBJECT_CONFIG file_config;
+ WDF_FILEOBJECT_CONFIG_INIT(
+ &file_config,
+ WDF_NO_EVENT_CALLBACK,
+ WDF_NO_EVENT_CALLBACK,
+ LvhXbox360EvtFileCleanup
+ );
+ WdfDeviceInitSetFileObjectConfig(device_init, &file_config, WDF_NO_OBJECT_ATTRIBUTES);
+
+ WDF_OBJECT_ATTRIBUTES device_attributes;
+ WDF_OBJECT_ATTRIBUTES_INIT(&device_attributes);
+ device_attributes.EvtCleanupCallback = LvhXbox360EvtDeviceCleanup;
+ WDFDEVICE device = nullptr;
+ auto status = WdfDeviceCreate(&device_init, &device_attributes, &device);
+ if (!NT_SUCCESS(status)) {
+ return status;
+ }
+
+ auto &state = driver_state();
+ state.device = device;
+
+ status = WdfDeviceCreateDeviceInterface(device, &LVH_WINDOWS_XUSB_INTERFACE_GUID, nullptr);
+ if (!NT_SUCCESS(status)) {
+ return status;
+ }
+ WDF_IO_QUEUE_CONFIG default_queue_config;
+ WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&default_queue_config, WdfIoQueueDispatchParallel);
+ default_queue_config.EvtIoDeviceControl = LvhXbox360EvtIoDeviceControl;
+ status = WdfIoQueueCreate(device, &default_queue_config, WDF_NO_OBJECT_ATTRIBUTES, WDF_NO_HANDLE);
+ if (!NT_SUCCESS(status)) {
+ return status;
+ }
+
+ WDF_IO_QUEUE_CONFIG manual_queue_config;
+ WDF_IO_QUEUE_CONFIG_INIT(&manual_queue_config, WdfIoQueueDispatchManual);
+ status = WdfIoQueueCreate(device, &manual_queue_config, WDF_NO_OBJECT_ATTRIBUTES, &state.wait_input_queue);
+ if (!NT_SUCCESS(status)) {
+ return status;
+ }
+ status = WdfIoQueueCreate(device, &manual_queue_config, WDF_NO_OBJECT_ATTRIBUTES, &state.output_queue);
+ if (!NT_SUCCESS(status)) {
+ return status;
+ }
+
+ WDF_TIMER_CONFIG timer_config;
+ WDF_TIMER_CONFIG_INIT_PERIODIC(&timer_config, LvhXbox360EvtInputPump, xbox_input_pump_period_ms);
+ WDF_OBJECT_ATTRIBUTES timer_attributes;
+ WDF_OBJECT_ATTRIBUTES_INIT(&timer_attributes);
+ timer_attributes.ParentObject = device;
+ status = WdfTimerCreate(&timer_config, &timer_attributes, &state.input_pump);
+ if (!NT_SUCCESS(status)) {
+ return status;
+ }
+ WdfTimerStart(state.input_pump, WDF_REL_TIMEOUT_IN_MS(xbox_input_pump_period_ms));
+ return STATUS_SUCCESS;
+}
+
+void LvhXbox360EvtDeviceCleanup(WDFOBJECT device_object) {
+ UNREFERENCED_PARAMETER(device_object);
+ cleanup_driver_state();
+}
+
+void LvhXbox360EvtFileCleanup(WDFFILEOBJECT file_object) {
+ UNREFERENCED_PARAMETER(file_object);
+}
+
+void LvhXbox360EvtInputPump(WDFTIMER timer) {
+ UNREFERENCED_PARAMETER(timer);
+ complete_one_wait_for_input();
+}
+
+void LvhXbox360EvtVhfReadyForNextReadReport(VhfContext vhf_client_context) {
+ auto *state = static_cast(vhf_client_context);
+ if (state == nullptr) {
+ return;
+ }
+ {
+ std::lock_guard lock {state->mutex};
+ if (state->shutting_down || state->virtual_hid.handle == nullptr) {
+ return;
+ }
+ state->virtual_hid.in_flight_report.reset();
+ state->virtual_hid.ready = true;
+ }
+ static_cast(submit_pending_hid_report());
+}
+
+void LvhXbox360EvtVhfGetInputReport(
+ VhfContext vhf_client_context,
+ VHFOPERATIONHANDLE operation_handle,
+ VhfContext operation_context,
+ PHID_XFER_PACKET packet
+) {
+ UNREFERENCED_PARAMETER(operation_context);
+ auto *state = static_cast(vhf_client_context);
+ if (state == nullptr || packet == nullptr || packet->reportBuffer == nullptr) {
+ static_cast(VhfAsyncOperationComplete(operation_handle, STATUS_INVALID_PARAMETER));
+ return;
+ }
+
+ std::vector report;
+ {
+ std::lock_guard lock {state->mutex};
+ report = state->virtual_hid.latest_report;
+ }
+ const auto leading_report_id =
+ packet->reportId != 0U && packet->reportBufferLen > report.size() ? 1U : 0U;
+ if (report.empty() || packet->reportBufferLen < report.size() + leading_report_id) {
+ static_cast(VhfAsyncOperationComplete(operation_handle, STATUS_BUFFER_TOO_SMALL));
+ return;
+ }
+ std::fill_n(packet->reportBuffer, packet->reportBufferLen, UCHAR {});
+ std::copy(report.begin(), report.end(), packet->reportBuffer + leading_report_id);
+ static_cast(VhfAsyncOperationComplete(operation_handle, STATUS_SUCCESS));
+}
+
+void LvhXbox360EvtIoDeviceControl(
+ WDFQUEUE queue,
+ WDFREQUEST request,
+ size_t output_buffer_length,
+ size_t input_buffer_length,
+ ULONG io_control_code
+) {
+ UNREFERENCED_PARAMETER(queue);
+ UNREFERENCED_PARAMETER(input_buffer_length);
+
+ switch (io_control_code) {
+ case LVH_WINDOWS_IOCTL_XBOX360_INITIALIZE:
+ handle_initialize(request);
+ return;
+ case LVH_WINDOWS_IOCTL_XBOX360_SUBMIT_INPUT:
+ handle_submit_input(request);
+ return;
+ case LVH_WINDOWS_IOCTL_XBOX360_READ_OUTPUT:
+ handle_read_output(request);
+ return;
+ case LVH_WINDOWS_IOCTL_XUSB_GET_INFORMATION:
+ handle_xusb_information(request);
+ return;
+ case LVH_WINDOWS_IOCTL_XUSB_GET_CAPABILITIES:
+ handle_xusb_capabilities(request, output_buffer_length);
+ return;
+ case LVH_WINDOWS_IOCTL_XUSB_GET_STATE:
+ {
+ const auto state = make_xusb_state(false);
+ static_cast(copy_to_request(request, state));
+ return;
+ }
+ case LVH_WINDOWS_IOCTL_XUSB_SET_STATE:
+ handle_xusb_set_state(request);
+ return;
+ case LVH_WINDOWS_IOCTL_XUSB_GET_LED_STATE:
+ {
+ constexpr std::array led {0U, 0U, 0x06U};
+ static_cast(copy_to_request(request, led));
+ return;
+ }
+ case LVH_WINDOWS_IOCTL_XUSB_GET_BATTERY_INFO:
+ {
+ constexpr std::array battery {0U, 0x01U, 0x03U, 0U};
+ static_cast(copy_to_request(request, battery));
+ return;
+ }
+ case LVH_WINDOWS_IOCTL_XUSB_WAIT_FOR_INPUT:
+ {
+ auto &state = driver_state();
+ const auto status = WdfRequestForwardToIoQueue(request, state.wait_input_queue);
+ if (!NT_SUCCESS(status)) {
+ complete_request(request, status);
+ }
+ return;
+ }
+ case LVH_WINDOWS_IOCTL_XUSB_GET_INFORMATION_EX:
+ {
+ std::array information {};
+ write_little_endian(information, 0U, xbox_version);
+ information[2] = 0x01U;
+ information[3] = 0x01U;
+ std::uint16_t vendor_id = 0x045EU;
+ std::uint16_t product_id = 0x028EU;
+ {
+ auto &state = driver_state();
+ std::lock_guard lock {state.mutex};
+ if (state.initialized) {
+ vendor_id = state.create_request.hardware_ids.vendor_id;
+ product_id = state.create_request.hardware_ids.product_id;
+ }
+ }
+ write_little_endian(information, 8U, vendor_id);
+ write_little_endian(information, 10U, product_id);
+ const auto size = std::min(output_buffer_length, information.size());
+ static_cast(copy_to_request(request, std::span {information}.first(size)));
+ return;
+ }
+ case LVH_WINDOWS_IOCTL_XUSB_POWER_DOWN:
+ case LVH_WINDOWS_IOCTL_XUSB_POWER_INFO:
+ complete_request(request, STATUS_SUCCESS);
+ return;
+ case LVH_WINDOWS_IOCTL_XUSB_WAIT_GUIDE:
+ case LVH_WINDOWS_IOCTL_XUSB_WAIT_FOR_SYSTEM_BUTTONS:
+ case LVH_WINDOWS_IOCTL_XUSB_GET_AUDIO_INFO:
+ default:
+ complete_request(request, STATUS_INVALID_DEVICE_REQUEST);
+ return;
+ }
+}
diff --git a/src/platform/windows/shared/lvh_windows_broker_protocol.h b/src/platform/windows/shared/lvh_windows_broker_protocol.h
index 83e7d0d..aff89ab 100644
--- a/src/platform/windows/shared/lvh_windows_broker_protocol.h
+++ b/src/platform/windows/shared/lvh_windows_broker_protocol.h
@@ -10,7 +10,7 @@
#include
#include
-inline constexpr uint32_t LVH_WINDOWS_BROKER_PROTOCOL_VERSION = 4u;
+inline constexpr uint32_t LVH_WINDOWS_BROKER_PROTOCOL_VERSION = 5u;
inline constexpr uint32_t LVH_WINDOWS_BROKER_MAX_MESSAGE_SIZE = 512u;
inline constexpr uint32_t LVH_WINDOWS_BROKER_MAX_LICENSE_KEY_SIZE = 128u;
inline constexpr uint32_t LVH_WINDOWS_BROKER_MAX_INSTANCE_NAME_SIZE = 128u;
diff --git a/src/platform/windows/shared/lvh_windows_protocol.h b/src/platform/windows/shared/lvh_windows_protocol.h
index 512e99b..7d6be24 100644
--- a/src/platform/windows/shared/lvh_windows_protocol.h
+++ b/src/platform/windows/shared/lvh_windows_protocol.h
@@ -8,7 +8,7 @@
#include
#include
-inline constexpr uint32_t LVH_WINDOWS_CONTROL_PROTOCOL_VERSION = 4u;
+inline constexpr uint32_t LVH_WINDOWS_CONTROL_PROTOCOL_VERSION = 5u;
inline constexpr char LVH_WINDOWS_CONTROL_DEVICE_PATH[] = R"(\\.\LibVirtualHid)";
inline constexpr char LVH_WINDOWS_GLOBAL_CONTROL_DEVICE_PATH[] = R"(\\.\Global\LibVirtualHid)";
@@ -209,6 +209,14 @@ struct LvhWindowsCreateDeviceResponse {
uint32_t status;
uint32_t reserved0;
uint64_t driver_device_id;
+ /**
+ * Broker-duplicated per-device transport handle.
+ *
+ * This is zero for VHF devices hosted by the control driver. Xbox 360
+ * software devices use it for their private, token-authenticated UMDF
+ * channel. The value is valid only in the requesting client process.
+ */
+ uint64_t transport_handle;
LvhWindowsSessionToken session_token;
std::array device_path;
};
diff --git a/src/platform/windows/shared/xbox_360_protocol.hpp b/src/platform/windows/shared/xbox_360_protocol.hpp
new file mode 100644
index 0000000..b7bc8f0
--- /dev/null
+++ b/src/platform/windows/shared/xbox_360_protocol.hpp
@@ -0,0 +1,118 @@
+/**
+ * @file src/platform/windows/shared/xbox_360_protocol.hpp
+ * @brief Private and XUSB wire protocol for the Windows Xbox 360 personality.
+ *
+ * @note The XUSB device-interface GUID and IOCTL layouts are compatibility
+ * wire formats used by the inbox XInput stack. Microsoft does not
+ * publish them as a supported third-party driver contract.
+ */
+#pragma once
+
+#include "lvh_windows_protocol.h"
+
+#include
+#include
+
+#if defined(_WIN32)
+ #include
+#endif
+
+inline constexpr std::uint32_t LVH_WINDOWS_XBOX360_PROTOCOL_VERSION = 1U;
+inline constexpr wchar_t LVH_WINDOWS_XBOX360_ENUMERATOR[] = L"LibVirtualHid";
+inline constexpr wchar_t LVH_WINDOWS_XBOX360_HARDWARE_ID[] = L"SWD\\LIBVIRTUALHID_XBOX360";
+
+#if defined(_WIN32)
+inline constexpr GUID LVH_WINDOWS_XUSB_INTERFACE_GUID {
+ 0xEC87F1E3,
+ 0xC13B,
+ 0x4100,
+ {0xB5, 0xF7, 0x8B, 0x84, 0xD5, 0x42, 0x60, 0xCB}
+};
+#endif
+
+// XUSB wire operations consumed by xinput1_*.dll and Windows.Gaming.Input.
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_GET_INFORMATION = 0x80006000U;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_GET_CAPABILITIES = 0x8000E004U;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_GET_LED_STATE = 0x8000E008U;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_GET_STATE = 0x8000E00CU;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_SET_STATE = 0x8000A010U;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_WAIT_GUIDE = 0x8000E014U;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_GET_BATTERY_INFO = 0x8000E018U;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_POWER_DOWN = 0x8000A01CU;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_GET_AUDIO_INFO = 0x8000E020U;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_POWER_INFO = 0x80006380U;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_WAIT_FOR_SYSTEM_BUTTONS = 0x8000E384U;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_WAIT_FOR_INPUT = 0x8000E3ACU;
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XUSB_GET_INFORMATION_EX = 0x8000E3FCU;
+
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XBOX360_INITIALIZE = lvh_windows_ctl_code(
+ LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID,
+ 0x900U,
+ LVH_WINDOWS_METHOD_BUFFERED,
+ LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS
+);
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XBOX360_SUBMIT_INPUT = lvh_windows_ctl_code(
+ LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID,
+ 0x901U,
+ LVH_WINDOWS_METHOD_BUFFERED,
+ LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS
+);
+inline constexpr std::uint32_t LVH_WINDOWS_IOCTL_XBOX360_READ_OUTPUT = lvh_windows_ctl_code(
+ LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID,
+ 0x902U,
+ LVH_WINDOWS_METHOD_BUFFERED,
+ LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS
+);
+
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_DPAD_UP = 0x0001U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_DPAD_DOWN = 0x0002U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_DPAD_LEFT = 0x0004U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_DPAD_RIGHT = 0x0008U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_START = 0x0010U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_BACK = 0x0020U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_LEFT_THUMB = 0x0040U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_RIGHT_THUMB = 0x0080U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_LEFT_SHOULDER = 0x0100U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_RIGHT_SHOULDER = 0x0200U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_GUIDE = 0x0400U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_A = 0x1000U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_B = 0x2000U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_X = 0x4000U;
+inline constexpr std::uint16_t LVH_WINDOWS_XINPUT_Y = 0x8000U;
+
+#pragma pack(push, 1)
+
+struct LvhWindowsXbox360InitializeRequest {
+ std::uint32_t version;
+ std::uint32_t size;
+ std::uint64_t driver_device_id;
+ LvhWindowsSessionToken session_token;
+ LvhWindowsCreateDeviceRequest device;
+};
+
+struct LvhWindowsXbox360InputState {
+ std::uint16_t buttons;
+ std::uint8_t left_trigger;
+ std::uint8_t right_trigger;
+ std::int16_t left_thumb_x;
+ std::int16_t left_thumb_y;
+ std::int16_t right_thumb_x;
+ std::int16_t right_thumb_y;
+};
+
+struct LvhWindowsXbox360SubmitInputRequest {
+ std::uint32_t version;
+ std::uint32_t size;
+ std::uint64_t driver_device_id;
+ LvhWindowsSessionToken session_token;
+ LvhWindowsXbox360InputState state;
+};
+
+struct LvhWindowsXbox360ReadOutputRequest {
+ std::uint32_t version;
+ std::uint32_t size;
+ std::uint64_t driver_device_id;
+ LvhWindowsSessionToken session_token;
+};
+
+#pragma pack(pop)
diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp
index e8a8c7f..0d4dfbe 100644
--- a/src/platform/windows/windows_backend.cpp
+++ b/src/platform/windows/windows_backend.cpp
@@ -193,6 +193,15 @@ namespace lvh::detail {
return {handle, &::CloseHandle};
}
+ UniqueHandle take_transport_handle(LvhWindowsCreateDeviceResponse &response) {
+ const auto raw_value = static_cast(response.transport_handle);
+ if (raw_value == 0U || raw_value == std::bit_cast(INVALID_HANDLE_VALUE) || static_cast(raw_value) != response.transport_handle) {
+ return make_unique_handle(nullptr);
+ }
+ response.transport_handle = 0U;
+ return make_unique_handle(std::bit_cast(raw_value));
+ }
+
std::string windows_error_message(DWORD error_code) {
std::array message_buffer {};
const auto message_size = ::FormatMessageA(
@@ -533,6 +542,94 @@ namespace lvh::detail {
return OperationStatus::success();
}
+ OperationStatus submit_xbox360_input(
+ HANDLE handle,
+ std::uint64_t driver_device_id,
+ const LvhWindowsSessionToken &session_token,
+ const GamepadState &state
+ ) {
+ auto request = windows::make_xbox_360_submit_input_request(
+ driver_device_id,
+ session_token,
+ state
+ );
+ DWORD bytes_returned = 0;
+ return run_overlapped_device_io(
+ "submit Windows Xbox 360 input",
+ &bytes_returned,
+ [handle, &request](OVERLAPPED &overlapped, DWORD *result_size) {
+ return ::DeviceIoControl(
+ handle,
+ LVH_WINDOWS_IOCTL_XBOX360_SUBMIT_INPUT,
+ &request,
+ sizeof(request),
+ nullptr,
+ 0,
+ result_size,
+ &overlapped
+ );
+ },
+ [handle](OVERLAPPED &overlapped, DWORD *result_size, BOOL wait) {
+ return ::GetOverlappedResult(handle, &overlapped, result_size, wait);
+ }
+ );
+ }
+
+ std::optional read_xbox360_output(
+ HANDLE handle,
+ std::uint64_t driver_device_id,
+ const LvhWindowsSessionToken &session_token,
+ HANDLE stop_event
+ ) {
+ LvhWindowsXbox360ReadOutputRequest request {};
+ request.version = LVH_WINDOWS_XBOX360_PROTOCOL_VERSION;
+ request.size = sizeof(request);
+ request.driver_device_id = driver_device_id;
+ request.session_token = session_token;
+ LvhWindowsOutputReportEvent event {};
+
+ auto operation_event = make_unique_handle(::CreateEventW(nullptr, TRUE, FALSE, nullptr));
+ if (!operation_event) {
+ return std::nullopt;
+ }
+ OVERLAPPED overlapped {};
+ overlapped.hEvent = operation_event.get();
+ DWORD bytes_returned = 0;
+ if (::DeviceIoControl(handle, LVH_WINDOWS_IOCTL_XBOX360_READ_OUTPUT, &request, sizeof(request), &event, sizeof(event), &bytes_returned, &overlapped) == FALSE) {
+ if (::GetLastError() != ERROR_IO_PENDING) {
+ return std::nullopt;
+ }
+ const std::array wait_handles {operation_event.get(), stop_event};
+ const auto wait_result = ::WaitForMultipleObjects(
+ static_cast(wait_handles.size()),
+ wait_handles.data(),
+ FALSE,
+ INFINITE
+ );
+ if (wait_result != WAIT_OBJECT_0) {
+ cancel_and_drain_overlapped_io(
+ overlapped,
+ &bytes_returned,
+ [handle](OVERLAPPED &pending) {
+ return ::CancelIoEx(handle, &pending);
+ },
+ [handle](OVERLAPPED &pending, DWORD *result_size, BOOL wait) {
+ return ::GetOverlappedResult(handle, &pending, result_size, wait);
+ }
+ );
+ return std::nullopt;
+ }
+ }
+ if (::GetOverlappedResult(handle, &overlapped, &bytes_returned, FALSE) == FALSE || bytes_returned < sizeof(event) || event.version != LVH_WINDOWS_CONTROL_PROTOCOL_VERSION || event.size != sizeof(event) || event.driver_device_id != driver_device_id) {
+ return std::nullopt;
+ }
+ event.report_size = std::min(
+ event.report_size,
+ static_cast(LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE)
+ );
+ return event;
+ }
+
bool driver_input_fallback_allowed(ErrorCode code) {
switch (code) {
using enum ErrorCode;
@@ -929,13 +1026,15 @@ namespace lvh::detail {
std::uint64_t driver_device_id,
const LvhWindowsSessionToken &session_token,
DeviceProfile device_profile,
- std::string device_path
+ std::string device_path,
+ UniqueHandle xbox360_transport_handle = make_unique_handle(nullptr)
):
client_id {client_device_id},
driver_id {driver_device_id},
token {session_token},
profile {std::move(device_profile)},
- path {std::move(device_path)} {
+ path {std::move(device_path)},
+ xbox360_transport {std::move(xbox360_transport_handle)} {
if (profile.gamepad_kind == GamepadProfileKind::generic && profile.capabilities.supports_rumble) {
uses_generic_pid = !windows::make_generic_pid_report_descriptor(profile.report_descriptor).empty();
}
@@ -954,6 +1053,7 @@ namespace lvh::detail {
LvhWindowsSessionToken token {};
DeviceProfile profile;
std::string path;
+ UniqueHandle xbox360_transport {make_unique_handle(nullptr)};
bool open = true;
OutputCallback output_callback;
bool uses_generic_pid = false;
@@ -985,6 +1085,11 @@ namespace lvh::detail {
std::condition_variable switch_pro_report_ready_;
std::mutex switch_pro_report_mutex_;
std::jthread switch_pro_report_thread_;
+ UniqueHandle xbox360_output_stop_event_ {make_unique_handle(nullptr)};
+ std::jthread xbox360_output_thread_;
+
+ void stream_xbox360_output(std::stop_token stop_token);
+ void stop_xbox360_output_stream();
};
class WindowsBackendContext: public std::enable_shared_from_this {
@@ -1051,12 +1156,28 @@ namespace lvh::detail {
return {status, nullptr};
}
+ auto xbox360_transport = take_transport_handle(response);
+ if (options.profile.gamepad_kind == GamepadProfileKind::xbox_360 && !xbox360_transport) {
+ static_cast(command_channel_->destroy_device(
+ response.driver_device_id,
+ response.session_token
+ ));
+ return {
+ OperationStatus::failure(
+ ErrorCode::backend_failure,
+ "Windows broker did not return the Xbox 360 transport handle"
+ ),
+ nullptr,
+ };
+ }
+
auto state = std::make_shared(
id,
response.driver_device_id,
response.session_token,
options.profile,
- response.device_path[0] == '\0' ? command_channel_->path() : std::string {response.device_path.data()}
+ response.device_path[0] == '\0' ? command_channel_->path() : std::string {response.device_path.data()},
+ std::move(xbox360_transport)
);
{
@@ -1146,6 +1267,7 @@ namespace lvh::detail {
state->open = false;
driver_id = state->driver_id;
token = state->token;
+ state->xbox360_transport.reset();
}
{
@@ -1315,10 +1437,19 @@ namespace lvh::detail {
stream_switch_pro_reports(stop_token);
}};
}
+ if (state_->xbox360_transport) {
+ xbox360_output_stop_event_ = make_unique_handle(::CreateEventW(nullptr, TRUE, FALSE, nullptr));
+ if (xbox360_output_stop_event_) {
+ xbox360_output_thread_ = std::jthread {[this](std::stop_token stop_token) {
+ stream_xbox360_output(stop_token);
+ }};
+ }
+ }
}
WindowsGamepad::~WindowsGamepad() {
stop_switch_pro_report_stream();
+ stop_xbox360_output_stream();
}
void WindowsGamepad::stream_switch_pro_reports(std::stop_token stop_token) {
@@ -1364,8 +1495,56 @@ namespace lvh::detail {
switch_pro_report_thread_.join();
}
+ void WindowsGamepad::stream_xbox360_output(std::stop_token stop_token) {
+ while (!stop_token.stop_requested()) {
+ auto event = read_xbox360_output(
+ state_->xbox360_transport.get(),
+ state_->driver_id,
+ state_->token,
+ xbox360_output_stop_event_.get()
+ );
+ if (!event.has_value()) {
+ if (stop_token.stop_requested() || ::WaitForSingleObject(xbox360_output_stop_event_.get(), 10U) == WAIT_OBJECT_0) {
+ return;
+ }
+ continue;
+ }
+
+ std::unique_lock dispatch_lock {state_->output_dispatch_mutex_};
+ OutputCallback callback;
+ DeviceProfile profile;
+ {
+ std::lock_guard lock {state_->mutex_};
+ if (!state_->open || !state_->output_callback) {
+ continue;
+ }
+ callback = state_->output_callback;
+ profile = state_->profile;
+ }
+ const std::vector report {
+ event->report.begin(),
+ event->report.begin() + std::min(
+ event->report_size,
+ static_cast(LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE)
+ ),
+ };
+ for (const auto &output : reports::parse_output_reports(profile, report)) {
+ callback(output);
+ }
+ }
+ }
+
+ void WindowsGamepad::stop_xbox360_output_stream() {
+ if (!xbox360_output_thread_.joinable()) {
+ return;
+ }
+ static_cast(::SetEvent(xbox360_output_stop_event_.get()));
+ xbox360_output_thread_.request_stop();
+ xbox360_output_thread_.join();
+ }
+
OperationStatus WindowsGamepad::submit(
- const GamepadState & /*state*/,
+ const GamepadState &state,
const std::vector &report
) {
using enum ErrorCode;
@@ -1391,6 +1570,15 @@ namespace lvh::detail {
return context_->submit_device_report(state_, windows::make_generic_windows_input_report(report));
}
+ if (state_->xbox360_transport) {
+ return submit_xbox360_input(
+ state_->xbox360_transport.get(),
+ state_->driver_id,
+ state_->token,
+ state
+ );
+ }
+
suppress_unchanged_report =
state_->profile.gamepad_kind == GamepadProfileKind::xbox_one ||
state_->profile.gamepad_kind == GamepadProfileKind::xbox_series;
@@ -1430,6 +1618,7 @@ namespace lvh::detail {
OperationStatus WindowsGamepad::close() {
stop_switch_pro_report_stream();
+ stop_xbox360_output_stream();
return context_->close_device(state_);
}
@@ -2835,15 +3024,6 @@ namespace lvh::detail {
};
}
- if (options.profile.gamepad_kind == GamepadProfileKind::xbox_360) {
- return {
- unsupported_profile_status(
- "Windows UMDF/VHF backend cannot expose Xbox 360 XUSB gamepads; use an XUSB fallback for this profile"
- ),
- nullptr,
- };
- }
-
auto effective_options = options;
effective_options.profile = windows::effective_vhf_gamepad_profile(options.profile);
auto result = context_->create_gamepad(id, effective_options);
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 73ff209..0c83d51 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -70,6 +70,7 @@ elseif(WIN32)
list(APPEND LIBVIRTUALHID_TEST_SOURCES
"${PROJECT_SOURCE_DIR}/src/platform/windows/driver/rotating_trace_log.cpp"
+ "${PROJECT_SOURCE_DIR}/src/platform/windows/broker/xbox_360_software_device.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/fixtures/windows_broker_client_test_hooks.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/fixtures/windows_broker_service_test_hooks.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/fixtures/windows_backend_test_hooks.cpp"
diff --git a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp
index a16245a..55a1dd8 100644
--- a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp
+++ b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp
@@ -57,7 +57,7 @@ namespace lvh::detail::test {
OperationStatus backend_failure_status;
OperationStatus transport_failure_status;
OperationStatus unavailable_status;
- OperationStatus xbox_360_unsupported_status;
+ OperationStatus xbox_360_create_status;
OperationStatus oversized_descriptor_status;
OperationStatus oversized_input_report_status;
OperationStatus oversized_output_report_status;
diff --git a/tests/fixtures/windows_backend_test_hooks.cpp b/tests/fixtures/windows_backend_test_hooks.cpp
index bb6c204..8ab75eb 100644
--- a/tests/fixtures/windows_backend_test_hooks.cpp
+++ b/tests/fixtures/windows_backend_test_hooks.cpp
@@ -57,6 +57,11 @@ namespace lvh::detail {
response.driver_device_id = next_driver_id_++;
response.session_token = session_token_;
windows::copy_string(response.device_path, response_device_path_);
+ if (response.status == LVH_WINDOWS_STATUS_SUCCESS && request.device_type == LVH_WINDOWS_DEVICE_GAMEPAD && request.gamepad_kind == LVH_WINDOWS_GAMEPAD_XBOX_360) {
+ response.transport_handle = reinterpret_cast(
+ ::CreateEventW(nullptr, TRUE, FALSE, nullptr)
+ );
+ }
return protocol_status(response.status, "Windows driver rejected virtual HID device creation");
}
@@ -921,7 +926,7 @@ namespace lvh::detail {
CreateGamepadOptions options;
options.profile = profiles::xbox_360();
- result.xbox_360_unsupported_status = backend->create_gamepad(20, options).status;
+ result.xbox_360_create_status = backend->create_gamepad(20, options).status;
}
{
diff --git a/tests/unit/test_license.cpp b/tests/unit/test_license.cpp
index c3fb322..6be152f 100644
--- a/tests/unit/test_license.cpp
+++ b/tests/unit/test_license.cpp
@@ -79,7 +79,7 @@ TEST(WindowsBrokerClientTest, BuildsVersionedRequestHeader) {
EXPECT_EQ(header.size, sizeof(LvhWindowsBrokerLicenseRequest));
EXPECT_EQ(header.type, static_cast(LvhWindowsBrokerRequestType::validate_license));
EXPECT_EQ(header.reserved0, 0U);
- EXPECT_EQ(LVH_WINDOWS_BROKER_PROTOCOL_VERSION, 4U);
+ EXPECT_EQ(LVH_WINDOWS_BROKER_PROTOCOL_VERSION, 5U);
}
TEST(WindowsBrokerClientTest, PreservesFixedWireLayout) {
@@ -88,7 +88,7 @@ TEST(WindowsBrokerClientTest, PreservesFixedWireLayout) {
EXPECT_EQ(sizeof(LvhWindowsBrokerStatusRequest), 16U);
EXPECT_EQ(sizeof(LvhWindowsBrokerStatusResponse), 1324U);
EXPECT_EQ(sizeof(LvhWindowsBrokerCreateDeviceRequest), 2528U);
- EXPECT_EQ(sizeof(LvhWindowsBrokerCreateDeviceResponse), 1640U);
+ EXPECT_EQ(sizeof(LvhWindowsBrokerCreateDeviceResponse), 1648U);
EXPECT_EQ(sizeof(LvhWindowsBrokerDestroyDeviceRequest), 64U);
EXPECT_EQ(sizeof(LvhWindowsBrokerDestroyDeviceResponse), 1324U);
EXPECT_EQ(sizeof(LvhWindowsBrokerLicenseRequest), 272U);
diff --git a/tests/unit/test_runtime.cpp b/tests/unit/test_runtime.cpp
index 38aa924..631276a 100644
--- a/tests/unit/test_runtime.cpp
+++ b/tests/unit/test_runtime.cpp
@@ -87,7 +87,11 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) {
EXPECT_FALSE(created);
EXPECT_EQ(created.status.code(), lvh::ErrorCode::backend_unavailable);
} else {
- EXPECT_EQ(runtime->create_gamepad(lvh::profiles::xbox_360()).status.code(), lvh::ErrorCode::unsupported_profile);
+ auto xbox_360 = runtime->create_gamepad(lvh::profiles::xbox_360());
+ ASSERT_TRUE(xbox_360) << xbox_360.status.message();
+ ASSERT_NE(xbox_360.gamepad, nullptr);
+ EXPECT_FALSE(xbox_360.gamepad->device_nodes().empty());
+ EXPECT_TRUE(xbox_360.gamepad->close().ok());
auto created = runtime->create_gamepad(lvh::profiles::xbox_series());
ASSERT_TRUE(created) << created.status.message();
diff --git a/tests/unit/test_windows_backend.cpp b/tests/unit/test_windows_backend.cpp
index 9834bfa..64000d1 100644
--- a/tests/unit/test_windows_backend.cpp
+++ b/tests/unit/test_windows_backend.cpp
@@ -265,7 +265,7 @@ TEST_F(WindowsBackendTest, FakeChannelCoversCreateFailureBranches) {
EXPECT_EQ(result.backend_failure_status.code(), lvh::ErrorCode::backend_failure);
EXPECT_EQ(result.transport_failure_status.code(), lvh::ErrorCode::backend_failure);
EXPECT_EQ(result.unavailable_status.code(), lvh::ErrorCode::backend_unavailable);
- EXPECT_EQ(result.xbox_360_unsupported_status.code(), lvh::ErrorCode::unsupported_profile);
+ EXPECT_TRUE(result.xbox_360_create_status.ok()) << result.xbox_360_create_status.message();
EXPECT_EQ(result.oversized_descriptor_status.code(), lvh::ErrorCode::invalid_argument);
EXPECT_EQ(result.oversized_input_report_status.code(), lvh::ErrorCode::invalid_argument);
EXPECT_EQ(result.oversized_output_report_status.code(), lvh::ErrorCode::invalid_argument);
diff --git a/tests/unit/test_windows_consumers.cpp b/tests/unit/test_windows_consumers.cpp
index bca7273..41bd596 100644
--- a/tests/unit/test_windows_consumers.cpp
+++ b/tests/unit/test_windows_consumers.cpp
@@ -120,12 +120,14 @@ namespace {
class XInputApi {
public:
using GetState = DWORD(WINAPI *)(DWORD, XINPUT_STATE *);
+ using SetState = DWORD(WINAPI *)(DWORD, XINPUT_VIBRATION *);
using GetBatteryInformation = DWORD(WINAPI *)(DWORD, BYTE, XINPUT_BATTERY_INFORMATION *);
XInputApi():
module_ {LoadLibraryW(L"xinput1_4.dll")} {
if (module_ != nullptr) {
get_state_ = std::bit_cast(GetProcAddress(module_, "XInputGetState"));
+ set_state_ = std::bit_cast(GetProcAddress(module_, "XInputSetState"));
get_battery_information_ = std::bit_cast(
GetProcAddress(module_, "XInputGetBatteryInformation")
);
@@ -142,13 +144,17 @@ namespace {
}
explicit operator bool() const {
- return get_state_ != nullptr && get_battery_information_ != nullptr;
+ return get_state_ != nullptr && set_state_ != nullptr && get_battery_information_ != nullptr;
}
DWORD state(DWORD slot, XINPUT_STATE *state) const {
return get_state_(slot, state);
}
+ DWORD set_state(DWORD slot, XINPUT_VIBRATION *vibration) const {
+ return set_state_(slot, vibration);
+ }
+
DWORD battery_information(
DWORD slot,
BYTE device_type,
@@ -160,6 +166,7 @@ namespace {
private:
HMODULE module_ = nullptr;
GetState get_state_ = nullptr;
+ SetState set_state_ = nullptr;
GetBatteryInformation get_battery_information_ = nullptr;
};
@@ -818,6 +825,66 @@ TEST_F(WindowsConsumerTest, XInputDoesNotExposeSubmittedXboxBattery) {
ASSERT_TRUE(created.adapter->close().ok());
}
+TEST_F(WindowsConsumerTest, Xbox360PublishesXInputStateAndRumble) {
+ XInputApi xinput;
+ ASSERT_TRUE(xinput);
+ const auto previous_slots = current_xinput_slots(xinput);
+
+ lvh::RuntimeOptions runtime_options;
+ runtime_options.backend = lvh::BackendKind::platform_default;
+ auto runtime = lvh::Runtime::create(runtime_options);
+ ASSERT_NE(runtime, nullptr);
+ ASSERT_TRUE(runtime->capabilities().supports_gamepad)
+ << "The installed libvirtualhid Windows driver is required for this integration test";
+
+ lvh::CreateGamepadOptions options;
+ options.profile = lvh::profiles::xbox_360();
+ options.metadata.stable_id = "xinput-xbox360-test";
+ auto created = lvh::GamepadStateAdapter::create(*runtime, options);
+ ASSERT_TRUE(created) << created.status.message();
+ ASSERT_NE(created.adapter->gamepad(), nullptr);
+
+ GamepadOutputCapture output_capture;
+ output_capture.attach(*created.adapter);
+
+ const auto slot = wait_for_new_xinput_slot(xinput, previous_slots);
+ ASSERT_TRUE(slot.has_value()) << "XInput did not enumerate the Xbox 360 software device";
+
+ ASSERT_TRUE(created.adapter->set_button(lvh::GamepadButton::a, true).ok());
+ ASSERT_TRUE(created.adapter->set_button(lvh::GamepadButton::dpad_right, true).ok());
+ ASSERT_TRUE(created.adapter->set_left_trigger(0.5F).ok());
+ ASSERT_TRUE(created.adapter->set_right_stick({.x = 0.25F, .y = -0.75F}).ok());
+
+ XINPUT_STATE state {};
+ const auto input_deadline = std::chrono::steady_clock::now() + 5s;
+ while (std::chrono::steady_clock::now() < input_deadline) {
+ ASSERT_EQ(xinput.state(*slot, &state), ERROR_SUCCESS);
+ if ((state.Gamepad.wButtons & (XINPUT_GAMEPAD_A | XINPUT_GAMEPAD_DPAD_RIGHT)) == (XINPUT_GAMEPAD_A | XINPUT_GAMEPAD_DPAD_RIGHT) && state.Gamepad.bLeftTrigger >= 127U && state.Gamepad.sThumbRX > 0 && state.Gamepad.sThumbRY < 0) {
+ break;
+ }
+ std::this_thread::sleep_for(20ms);
+ }
+ EXPECT_EQ(
+ state.Gamepad.wButtons & (XINPUT_GAMEPAD_A | XINPUT_GAMEPAD_DPAD_RIGHT),
+ XINPUT_GAMEPAD_A | XINPUT_GAMEPAD_DPAD_RIGHT
+ );
+ EXPECT_GE(state.Gamepad.bLeftTrigger, 127U);
+ EXPECT_GT(state.Gamepad.sThumbRX, 0);
+ EXPECT_LT(state.Gamepad.sThumbRY, 0);
+
+ XINPUT_VIBRATION vibration {
+ .wLeftMotorSpeed = 0x5678U,
+ .wRightMotorSpeed = 0x1234U,
+ };
+ ASSERT_EQ(xinput.set_state(*slot, &vibration), ERROR_SUCCESS);
+ const auto rumble = output_capture.wait_for_rumble(true);
+ ASSERT_TRUE(rumble.has_value()) << "No normalized rumble callback followed XInputSetState";
+ EXPECT_NEAR(rumble->low_frequency_rumble, 0x5656U, 0x0101U);
+ EXPECT_NEAR(rumble->high_frequency_rumble, 0x1212U, 0x0101U);
+
+ ASSERT_TRUE(created.adapter->close().ok());
+}
+
TEST_F(WindowsConsumerTest, NativePlayStationFeatureAndOutputReportsReachOwningRuntime) {
struct NativePlayStationCase {
lvh::DeviceProfile profile;
diff --git a/tests/unit/test_windows_protocol.cpp b/tests/unit/test_windows_protocol.cpp
index bacefbc..94bc7bf 100644
--- a/tests/unit/test_windows_protocol.cpp
+++ b/tests/unit/test_windows_protocol.cpp
@@ -60,7 +60,7 @@ TEST(WindowsProtocolTest, ExposesStableProtocolConstants) {
EXPECT_STREQ(lvh::detail::windows::default_control_device_path.data(), R"(\\.\LibVirtualHid)");
EXPECT_STREQ(lvh::detail::windows::global_control_device_path.data(), R"(\\.\Global\LibVirtualHid)");
- EXPECT_EQ(LVH_WINDOWS_CONTROL_PROTOCOL_VERSION, 4U);
+ EXPECT_EQ(LVH_WINDOWS_CONTROL_PROTOCOL_VERSION, 5U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_CREATE_DEVICE, 0x8000E000U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_DESTROY_DEVICE, 0x8000E004U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT, 0x8000E008U);
@@ -74,11 +74,18 @@ TEST(WindowsProtocolTest, ExposesStableProtocolConstants) {
EXPECT_EQ(sizeof(LvhWindowsDeviceReportSizes), 24U);
EXPECT_EQ(sizeof(LvhWindowsCreateDeviceRequest), 2502U);
EXPECT_EQ(sizeof(LvhWindowsSessionToken), 32U);
- EXPECT_EQ(sizeof(LvhWindowsCreateDeviceResponse), 316U);
+ EXPECT_EQ(sizeof(LvhWindowsCreateDeviceResponse), 324U);
EXPECT_EQ(sizeof(LvhWindowsDestroyDeviceRequest), 48U);
EXPECT_EQ(sizeof(LvhWindowsResetDevicesRequest), 8U);
EXPECT_EQ(sizeof(LvhWindowsSubmitInputReportRequest), 312U);
EXPECT_EQ(sizeof(LvhWindowsOutputReportEvent), 280U);
+ EXPECT_EQ(sizeof(LvhWindowsXbox360InitializeRequest), 2550U);
+ EXPECT_EQ(sizeof(LvhWindowsXbox360InputState), 12U);
+ EXPECT_EQ(sizeof(LvhWindowsXbox360SubmitInputRequest), 60U);
+ EXPECT_EQ(sizeof(LvhWindowsXbox360ReadOutputRequest), 48U);
+ EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_INITIALIZE, 0x8000E400U);
+ EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_SUBMIT_INPUT, 0x8000E404U);
+ EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_READ_OUTPUT, 0x8000E408U);
}
TEST(WindowsProtocolTest, MapsBusTypesAndGamepadKinds) {
@@ -691,6 +698,41 @@ TEST(WindowsProtocolTest, PacksSubmitAndDestroyRequests) {
EXPECT_EQ(reset.size, sizeof(reset));
}
+TEST(WindowsProtocolTest, PacksNativeXbox360InputState) {
+ using enum lvh::GamepadButton;
+
+ lvh::GamepadState state;
+ state.buttons.set(a);
+ state.buttons.set(y);
+ state.buttons.set(back);
+ state.buttons.set(guide);
+ state.buttons.set(dpad_up);
+ state.buttons.set(dpad_right);
+ state.left_stick = {-1.0F, 1.0F};
+ state.right_stick = {0.5F, -0.5F};
+ state.left_trigger = 0.25F;
+ state.right_trigger = 1.0F;
+
+ const auto token = test_session_token();
+ const auto request = lvh::detail::windows::make_xbox_360_submit_input_request(23U, token, state);
+
+ EXPECT_EQ(request.version, LVH_WINDOWS_XBOX360_PROTOCOL_VERSION);
+ EXPECT_EQ(request.size, sizeof(request));
+ EXPECT_EQ(request.driver_device_id, 23U);
+ EXPECT_EQ(request.session_token.bytes, token.bytes);
+ EXPECT_EQ(
+ request.state.buttons,
+ LVH_WINDOWS_XINPUT_A | LVH_WINDOWS_XINPUT_Y | LVH_WINDOWS_XINPUT_BACK |
+ LVH_WINDOWS_XINPUT_GUIDE | LVH_WINDOWS_XINPUT_DPAD_UP | LVH_WINDOWS_XINPUT_DPAD_RIGHT
+ );
+ EXPECT_EQ(request.state.left_trigger, 64U);
+ EXPECT_EQ(request.state.right_trigger, 255U);
+ EXPECT_EQ(request.state.left_thumb_x, -32768);
+ EXPECT_EQ(request.state.left_thumb_y, 32767);
+ EXPECT_EQ(request.state.right_thumb_x, 16384);
+ EXPECT_EQ(request.state.right_thumb_y, -16384);
+}
+
TEST(WindowsProtocolTest, CompatibilityRequestHelpersUseEmptySessionToken) {
const auto submit = lvh::detail::windows::make_submit_input_report_request(17, std::vector {1});
const auto destroy = lvh::detail::windows::make_destroy_device_request(17);
From a2015c02d90fbadebe9fffccf8ca5ffa4f721d0a Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 11 Sep 2026 18:37:00 -0400
Subject: [PATCH 2/6] fix(windows): bind Xbox software devices to UMDF
Use ROOT-style PnP hardware IDs for SwDevice-created Xbox 360 instances so the INF selects the companion driver and publishes XUSB. Include Configuration Manager status in interface startup failures.
---
docs/windows-driver.md | 4 ++-
src/platform/windows/broker/CMakeLists.txt | 1 +
.../broker/xbox_360_software_device.cpp | 26 +++++++++++++++++--
.../windows/driver/libvirtualhid.inf.in | 4 +--
.../windows/shared/xbox_360_protocol.hpp | 2 +-
tests/unit/test_windows_protocol.cpp | 2 ++
6 files changed, 33 insertions(+), 6 deletions(-)
diff --git a/docs/windows-driver.md b/docs/windows-driver.md
index 49f053d..c9ec25e 100644
--- a/docs/windows-driver.md
+++ b/docs/windows-driver.md
@@ -86,7 +86,9 @@ and HID output writes are normalized back to the C++ output callback path.
Xbox 360 creation takes a separate path because an authentic wired Xbox 360
controller is XUSB rather than a standard HID-only device. For every requested
Xbox 360 controller, the broker calls `SwDeviceCreate` with a unique instance
-ID and an explicit non-null container ID. Windows binds the package's
+ID, an explicit non-null container ID, and conventional `ROOT\...` hardware IDs
+that match the package INF. The resulting device instance remains under the
+`SWD\LibVirtualHid` enumerator. Windows binds the package's
`libvirtualhid_xbox360_umdf.dll` to that System-class software devnode. The
companion publishes exactly one XUSB interface using
`{EC87F1E3-C13B-4100-B5F7-8B84D54260CB}`, and creates a VHF child with the
diff --git a/src/platform/windows/broker/CMakeLists.txt b/src/platform/windows/broker/CMakeLists.txt
index 8d944b2..c654015 100644
--- a/src/platform/windows/broker/CMakeLists.txt
+++ b/src/platform/windows/broker/CMakeLists.txt
@@ -25,6 +25,7 @@ target_link_libraries(libvirtualhid_broker
nlohmann_json::nlohmann_json
advapi32
bcrypt
+ cfgmgr32
crypt32
ole32
setupapi
diff --git a/src/platform/windows/broker/xbox_360_software_device.cpp b/src/platform/windows/broker/xbox_360_software_device.cpp
index 61a8438..43287f3 100644
--- a/src/platform/windows/broker/xbox_360_software_device.cpp
+++ b/src/platform/windows/broker/xbox_360_software_device.cpp
@@ -178,12 +178,33 @@ namespace lvh::windows {
std::wstring hardware_id(const LvhWindowsCreateDeviceRequest &request) {
return std::format(
- L"USB\\VID_{:04X}&PID_{:04X}&XI_00",
+ L"ROOT\\VID_{:04X}&PID_{:04X}&XI_00",
request.hardware_ids.vendor_id,
request.hardware_ids.product_id
);
}
+ std::string device_node_status(std::wstring_view instance_id) {
+ auto mutable_instance_id = std::wstring {instance_id};
+ DEVINST device_instance {};
+ auto config_result = ::CM_Locate_DevNodeW(
+ &device_instance,
+ mutable_instance_id.data(),
+ CM_LOCATE_DEVNODE_NORMAL
+ );
+ if (config_result != CR_SUCCESS) {
+ return std::format("devnode lookup result {}", config_result);
+ }
+
+ ULONG node_status = 0U;
+ ULONG problem_code = 0U;
+ config_result = ::CM_Get_DevNode_Status(&node_status, &problem_code, device_instance, 0U);
+ if (config_result != CR_SUCCESS) {
+ return std::format("devnode status result {}", config_result);
+ }
+ return std::format("devnode status 0x{:08X}, problem code {}", node_status, problem_code);
+ }
+
UniqueDeviceInfo make_unique_device_info(HDEVINFO value) {
if (value == INVALID_HANDLE_VALUE) {
value = nullptr;
@@ -457,7 +478,8 @@ namespace lvh::windows {
const auto xusb_path = wait_for_interface_path(LVH_WINDOWS_XUSB_INTERFACE_GUID, full_instance_id);
if (xusb_path.empty()) {
- message = "The Xbox 360 UMDF driver started without publishing its XUSB interface.";
+ message = "The Xbox 360 software device did not publish its XUSB interface (" +
+ device_node_status(full_instance_id) + ").";
return nullptr;
}
diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in
index 7b86278..0e20e3a 100644
--- a/src/platform/windows/driver/libvirtualhid.inf.in
+++ b/src/platform/windows/driver/libvirtualhid.inf.in
@@ -23,11 +23,11 @@ PnpLockdown=1
[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...19041]
%DeviceName%=DeviceInstall,ROOT\LIBVIRTUALHID
-%Xbox360DeviceName%=Xbox360Install,SWD\LIBVIRTUALHID_XBOX360
+%Xbox360DeviceName%=Xbox360Install,ROOT\LIBVIRTUALHID_XBOX360
[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...22000]
%DeviceName%=DeviceInstall_Win11,ROOT\LIBVIRTUALHID
-%Xbox360DeviceName%=Xbox360Install_Win11,SWD\LIBVIRTUALHID_XBOX360
+%Xbox360DeviceName%=Xbox360Install_Win11,ROOT\LIBVIRTUALHID_XBOX360
[DestinationDirs]
UMDriverCopy=13
diff --git a/src/platform/windows/shared/xbox_360_protocol.hpp b/src/platform/windows/shared/xbox_360_protocol.hpp
index b7bc8f0..c66b965 100644
--- a/src/platform/windows/shared/xbox_360_protocol.hpp
+++ b/src/platform/windows/shared/xbox_360_protocol.hpp
@@ -19,7 +19,7 @@
inline constexpr std::uint32_t LVH_WINDOWS_XBOX360_PROTOCOL_VERSION = 1U;
inline constexpr wchar_t LVH_WINDOWS_XBOX360_ENUMERATOR[] = L"LibVirtualHid";
-inline constexpr wchar_t LVH_WINDOWS_XBOX360_HARDWARE_ID[] = L"SWD\\LIBVIRTUALHID_XBOX360";
+inline constexpr wchar_t LVH_WINDOWS_XBOX360_HARDWARE_ID[] = L"ROOT\\LIBVIRTUALHID_XBOX360";
#if defined(_WIN32)
inline constexpr GUID LVH_WINDOWS_XUSB_INTERFACE_GUID {
diff --git a/tests/unit/test_windows_protocol.cpp b/tests/unit/test_windows_protocol.cpp
index 94bc7bf..db2a968 100644
--- a/tests/unit/test_windows_protocol.cpp
+++ b/tests/unit/test_windows_protocol.cpp
@@ -86,6 +86,8 @@ TEST(WindowsProtocolTest, ExposesStableProtocolConstants) {
EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_INITIALIZE, 0x8000E400U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_SUBMIT_INPUT, 0x8000E404U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_READ_OUTPUT, 0x8000E408U);
+ EXPECT_STREQ(LVH_WINDOWS_XBOX360_ENUMERATOR, L"LibVirtualHid");
+ EXPECT_STREQ(LVH_WINDOWS_XBOX360_HARDWARE_ID, L"ROOT\\LIBVIRTUALHID_XBOX360");
}
TEST(WindowsProtocolTest, MapsBusTypesAndGamepadKinds) {
From df015a9c7c4173d199433d6a2acbd2f608c23c8b Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 11 Sep 2026 19:51:14 -0400
Subject: [PATCH 3/6] fix(windows): match Xbox software device INF
Use bare hardware IDs for SwDevice-created Xbox 360 companions and add matching INF model entries so PnP installs the UMDF driver instead of leaving a Code 1 devnode.
Preserve the ROOT provisioning alias and align installed-driver validation and documentation with the software-device identity.
---
docs/windows-driver.md | 12 +++++++-----
scripts/windows/test-installed-driver.ps1 | 2 +-
.../windows/broker/xbox_360_software_device.cpp | 4 ++--
src/platform/windows/driver/libvirtualhid.inf.in | 4 ++++
src/platform/windows/shared/xbox_360_protocol.hpp | 2 +-
tests/unit/test_windows_protocol.cpp | 2 +-
6 files changed, 16 insertions(+), 10 deletions(-)
diff --git a/docs/windows-driver.md b/docs/windows-driver.md
index c9ec25e..c1372bc 100644
--- a/docs/windows-driver.md
+++ b/docs/windows-driver.md
@@ -86,11 +86,13 @@ and HID output writes are normalized back to the C++ output callback path.
Xbox 360 creation takes a separate path because an authentic wired Xbox 360
controller is XUSB rather than a standard HID-only device. For every requested
Xbox 360 controller, the broker calls `SwDeviceCreate` with a unique instance
-ID, an explicit non-null container ID, and conventional `ROOT\...` hardware IDs
-that match the package INF. The resulting device instance remains under the
-`SWD\LibVirtualHid` enumerator. Windows binds the package's
-`libvirtualhid_xbox360_umdf.dll` to that System-class software devnode. The
-companion publishes exactly one XUSB interface using
+ID, an explicit non-null container ID, and bare `VID_...` and
+`LIBVIRTUALHID_XBOX360` hardware IDs. The package INF retains a
+`ROOT\LIBVIRTUALHID_XBOX360` provisioning alias, but its bare model entry is
+what matches the IDs reported by `SwDeviceCreate`. The resulting device
+instance remains under the `SWD\LibVirtualHid` enumerator. Windows binds the
+package's `libvirtualhid_xbox360_umdf.dll` to that System-class software
+devnode. The companion publishes exactly one XUSB interface using
`{EC87F1E3-C13B-4100-B5F7-8B84D54260CB}`, and creates a VHF child with the
profile's `VID_045E&PID_028E&IG_00` identity. The shared container and ancestor
metadata let Windows correlate the XUSB and HID views as one controller while
diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1
index 7ea4d74..9fcd63a 100644
--- a/scripts/windows/test-installed-driver.ps1
+++ b/scripts/windows/test-installed-driver.ps1
@@ -271,7 +271,7 @@ function Wait-ForStartedGamepadChild {
function Wait-ForStartedXbox360Companion {
param([int] $TimeoutSeconds)
- $hardwareId = "SWD\LIBVIRTUALHID_XBOX360"
+ $hardwareId = "LIBVIRTUALHID_XBOX360"
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
$latestRecords = @()
do {
diff --git a/src/platform/windows/broker/xbox_360_software_device.cpp b/src/platform/windows/broker/xbox_360_software_device.cpp
index 43287f3..e44ccb6 100644
--- a/src/platform/windows/broker/xbox_360_software_device.cpp
+++ b/src/platform/windows/broker/xbox_360_software_device.cpp
@@ -178,7 +178,7 @@ namespace lvh::windows {
std::wstring hardware_id(const LvhWindowsCreateDeviceRequest &request) {
return std::format(
- L"ROOT\\VID_{:04X}&PID_{:04X}&XI_00",
+ L"VID_{:04X}&PID_{:04X}&XI_00",
request.hardware_ids.vendor_id,
request.hardware_ids.product_id
);
@@ -399,8 +399,8 @@ namespace lvh::windows {
const auto vendor_hardware_id = hardware_id(request);
const std::array hardware_ids {
- LVH_WINDOWS_XBOX360_HARDWARE_ID,
vendor_hardware_id,
+ LVH_WINDOWS_XBOX360_HARDWARE_ID,
};
const std::array compatible_ids {
L"USB\\MS_COMP_XUSB10",
diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in
index 0e20e3a..b15119a 100644
--- a/src/platform/windows/driver/libvirtualhid.inf.in
+++ b/src/platform/windows/driver/libvirtualhid.inf.in
@@ -24,10 +24,14 @@ PnpLockdown=1
[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...19041]
%DeviceName%=DeviceInstall,ROOT\LIBVIRTUALHID
%Xbox360DeviceName%=Xbox360Install,ROOT\LIBVIRTUALHID_XBOX360
+; SwDeviceCreate reports the supplied hardware ID verbatim. Keep a bare
+; model entry in addition to the ROOT provisioning alias above.
+%Xbox360DeviceName%=Xbox360Install,LIBVIRTUALHID_XBOX360
[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...22000]
%DeviceName%=DeviceInstall_Win11,ROOT\LIBVIRTUALHID
%Xbox360DeviceName%=Xbox360Install_Win11,ROOT\LIBVIRTUALHID_XBOX360
+%Xbox360DeviceName%=Xbox360Install_Win11,LIBVIRTUALHID_XBOX360
[DestinationDirs]
UMDriverCopy=13
diff --git a/src/platform/windows/shared/xbox_360_protocol.hpp b/src/platform/windows/shared/xbox_360_protocol.hpp
index c66b965..a5d565b 100644
--- a/src/platform/windows/shared/xbox_360_protocol.hpp
+++ b/src/platform/windows/shared/xbox_360_protocol.hpp
@@ -19,7 +19,7 @@
inline constexpr std::uint32_t LVH_WINDOWS_XBOX360_PROTOCOL_VERSION = 1U;
inline constexpr wchar_t LVH_WINDOWS_XBOX360_ENUMERATOR[] = L"LibVirtualHid";
-inline constexpr wchar_t LVH_WINDOWS_XBOX360_HARDWARE_ID[] = L"ROOT\\LIBVIRTUALHID_XBOX360";
+inline constexpr wchar_t LVH_WINDOWS_XBOX360_HARDWARE_ID[] = L"LIBVIRTUALHID_XBOX360";
#if defined(_WIN32)
inline constexpr GUID LVH_WINDOWS_XUSB_INTERFACE_GUID {
diff --git a/tests/unit/test_windows_protocol.cpp b/tests/unit/test_windows_protocol.cpp
index db2a968..e765be0 100644
--- a/tests/unit/test_windows_protocol.cpp
+++ b/tests/unit/test_windows_protocol.cpp
@@ -87,7 +87,7 @@ TEST(WindowsProtocolTest, ExposesStableProtocolConstants) {
EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_SUBMIT_INPUT, 0x8000E404U);
EXPECT_EQ(LVH_WINDOWS_IOCTL_XBOX360_READ_OUTPUT, 0x8000E408U);
EXPECT_STREQ(LVH_WINDOWS_XBOX360_ENUMERATOR, L"LibVirtualHid");
- EXPECT_STREQ(LVH_WINDOWS_XBOX360_HARDWARE_ID, L"ROOT\\LIBVIRTUALHID_XBOX360");
+ EXPECT_STREQ(LVH_WINDOWS_XBOX360_HARDWARE_ID, L"LIBVIRTUALHID_XBOX360");
}
TEST(WindowsProtocolTest, MapsBusTypesAndGamepadKinds) {
From f5ab5ec3386857daad4e91ba35cb524c9ad6054a Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 11 Sep 2026 20:37:58 -0400
Subject: [PATCH 4/6] fix(windows): prioritize Xbox 360 driver match
Match the default first-reported Xbox 360 VID/PID identity in both Windows model sections so PnP selects the UMDF package at rank zero instead of relying on the lower-ranked generic fallback.
Exercise the x360 profile during installed-driver smoke validation and document the ranked default and custom-identity fallback behavior.
---
.github/workflows/ci-build.yml | 2 +-
docs/windows-driver.md | 14 ++++++++------
src/platform/windows/driver/libvirtualhid.inf.in | 7 +++++--
3 files changed, 14 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml
index 5ca5d2c..0f0da84 100644
--- a/.github/workflows/ci-build.yml
+++ b/.github/workflows/ci-build.yml
@@ -392,7 +392,7 @@ jobs:
"$env:GITHUB_WORKSPACE\cmake-build-ci\examples\$env:CMAKE_BUILD_CONFIG" `
"gamepad_adapter.exe"
}
- $profiles = @("generic", "xone", "xseries", "ds4", "ds5", "switch")
+ $profiles = @("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch")
foreach ($profile in $profiles) {
.\scripts\windows\test-installed-driver.ps1 `
-GamepadAdapterPath $gamepadAdapterPath `
diff --git a/docs/windows-driver.md b/docs/windows-driver.md
index c1372bc..f594b23 100644
--- a/docs/windows-driver.md
+++ b/docs/windows-driver.md
@@ -87,12 +87,14 @@ Xbox 360 creation takes a separate path because an authentic wired Xbox 360
controller is XUSB rather than a standard HID-only device. For every requested
Xbox 360 controller, the broker calls `SwDeviceCreate` with a unique instance
ID, an explicit non-null container ID, and bare `VID_...` and
-`LIBVIRTUALHID_XBOX360` hardware IDs. The package INF retains a
-`ROOT\LIBVIRTUALHID_XBOX360` provisioning alias, but its bare model entry is
-what matches the IDs reported by `SwDeviceCreate`. The resulting device
-instance remains under the `SWD\LibVirtualHid` enumerator. Windows binds the
-package's `libvirtualhid_xbox360_umdf.dll` to that System-class software
-devnode. The companion publishes exactly one XUSB interface using
+`LIBVIRTUALHID_XBOX360` hardware IDs. The package INF matches the default
+`VID_045E&PID_028E&XI_00` identity directly so Windows gives it the highest
+driver-selection rank, retains `LIBVIRTUALHID_XBOX360` as the fallback for
+custom VID/PID profiles, and keeps `ROOT\LIBVIRTUALHID_XBOX360` as a
+provisioning alias. The resulting device instance remains under the
+`SWD\LibVirtualHid` enumerator. Windows binds the package's
+`libvirtualhid_xbox360_umdf.dll` to that System-class software devnode. The
+companion publishes exactly one XUSB interface using
`{EC87F1E3-C13B-4100-B5F7-8B84D54260CB}`, and creates a VHF child with the
profile's `VID_045E&PID_028E&IG_00` identity. The shared container and ancestor
metadata let Windows correlate the XUSB and HID views as one controller while
diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in
index b15119a..aedd658 100644
--- a/src/platform/windows/driver/libvirtualhid.inf.in
+++ b/src/platform/windows/driver/libvirtualhid.inf.in
@@ -24,13 +24,16 @@ PnpLockdown=1
[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...19041]
%DeviceName%=DeviceInstall,ROOT\LIBVIRTUALHID
%Xbox360DeviceName%=Xbox360Install,ROOT\LIBVIRTUALHID_XBOX360
-; SwDeviceCreate reports the supplied hardware ID verbatim. Keep a bare
-; model entry in addition to the ROOT provisioning alias above.
+; Prefer the default Xbox 360 identity reported first by SwDeviceCreate so
+; Windows selects this package at rank zero. Keep the generic bare model for
+; custom VID/PID profiles and the ROOT provisioning alias above.
+%Xbox360DeviceName%=Xbox360Install,VID_045E&PID_028E&XI_00
%Xbox360DeviceName%=Xbox360Install,LIBVIRTUALHID_XBOX360
[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...22000]
%DeviceName%=DeviceInstall_Win11,ROOT\LIBVIRTUALHID
%Xbox360DeviceName%=Xbox360Install_Win11,ROOT\LIBVIRTUALHID_XBOX360
+%Xbox360DeviceName%=Xbox360Install_Win11,VID_045E&PID_028E&XI_00
%Xbox360DeviceName%=Xbox360Install_Win11,LIBVIRTUALHID_XBOX360
[DestinationDirs]
From b47f3ccc2e8c7819121df41c5a48b4942c736d32 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 11 Sep 2026 20:53:10 -0400
Subject: [PATCH 5/6] fix(ci): validate Xbox 360 XUSB companion
Skip the HID child assertion for the native Xbox 360 profile and verify its software-device XUSB companion directly. This allows installed-package smoke testing to exercise the intended Windows device path.
---
scripts/windows/test-installed-driver.ps1 | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1
index 9fcd63a..190421f 100644
--- a/scripts/windows/test-installed-driver.ps1
+++ b/scripts/windows/test-installed-driver.ps1
@@ -217,7 +217,6 @@ function Get-ExpectedGamepadHardwareId {
switch ($ProfileName) {
"generic" { return @("HID\VID_1209&PID_0001") }
- "x360" { return @("HID\VID_045E&PID_028E&IG_00") }
"xone" { return @("HID\VID_045E&PID_02EA&IG_00") }
"xseries" { return @("HID\VID_045E&PID_0B12&IG_00") }
"ds4" { return @("HID\VID_054C&PID_05C4") }
@@ -327,9 +326,10 @@ function Invoke-GamepadAdapterSmoke {
throw "gamepad_adapter exited with code $($process.ExitCode).`nstdout:`n$stdout`nstderr:`n$stderr"
}
- Wait-ForStartedGamepadChild -ProfileName $ProfileName -TimeoutSeconds $DeviceStartTimeoutSeconds
if ($ProfileName -eq "x360") {
Wait-ForStartedXbox360Companion -TimeoutSeconds $DeviceStartTimeoutSeconds
+ } else {
+ Wait-ForStartedGamepadChild -ProfileName $ProfileName -TimeoutSeconds $DeviceStartTimeoutSeconds
}
} finally {
if (-not $process.HasExited) {
From ca11977e61ba81221f16158c3d15f38fa79fc6b5 Mon Sep 17 00:00:00 2001
From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com>
Date: Fri, 11 Sep 2026 21:32:28 -0400
Subject: [PATCH 6/6] fix(windows): support Xbox companions on server
Select server-specific INF models that omit the client-only xinputhid classifier while preserving the XUSB interface and VHF child. Let WDF tear down device-owned queues so cleanup cannot terminate the UMDF host on a failed synchronous purge.
---
docs/windows-driver.md | 6 ++
.../windows/driver/libvirtualhid.inf.in | 88 +++++++++++++++++--
.../driver/libvirtualhid_xbox360_umdf.cpp | 14 +--
3 files changed, 92 insertions(+), 16 deletions(-)
diff --git a/docs/windows-driver.md b/docs/windows-driver.md
index f594b23..510dd7b 100644
--- a/docs/windows-driver.md
+++ b/docs/windows-driver.md
@@ -100,6 +100,12 @@ profile's `VID_045E&PID_028E&IG_00` identity. The shared container and ancestor
metadata let Windows correlate the XUSB and HID views as one controller while
retaining DirectInput/HID compatibility.
+On Windows Server SKUs, the package omits the `xinputhid` upper-filter marker
+from the Xbox companion. That classifier belongs to the client gaming stack and
+is not required for XInput discovery; registering it on a server where the
+service is unavailable prevents PnP from completing the software-device stack.
+The XUSB interface and correlated VHF child remain enabled on server systems.
+
The broker opens the XUSB interface, authenticates an initialization request by
its SCM-registered process ID, and duplicates that per-device handle into the
requesting client. Subsequent input and feedback operations require both the
diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in
index aedd658..a450af2 100644
--- a/src/platform/windows/driver/libvirtualhid.inf.in
+++ b/src/platform/windows/driver/libvirtualhid.inf.in
@@ -18,10 +18,11 @@ PnpLockdown=1
[Manufacturer]
; Windows 10 requires explicit WUDFRd service registration. Windows 11 can use
-; the inbox WUDFRD.NT sections instead.
-%ManufacturerName%=Standard,NT@LIBVIRTUALHID_INF_ARCH@.10.0...19041,NT@LIBVIRTUALHID_INF_ARCH@.10.0...22000
+; the inbox WUDFRD.NT sections instead. Server SKUs use product-specific model
+; sections because the client-only xinputhid classifier is not present there.
+%ManufacturerName%=Standard,NT@LIBVIRTUALHID_INF_ARCH@.10.0.1..19041,NT@LIBVIRTUALHID_INF_ARCH@.10.0.2..19041,NT@LIBVIRTUALHID_INF_ARCH@.10.0.3..19041,NT@LIBVIRTUALHID_INF_ARCH@.10.0.1..22000,NT@LIBVIRTUALHID_INF_ARCH@.10.0.2..22000,NT@LIBVIRTUALHID_INF_ARCH@.10.0.3..22000
-[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...19041]
+[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0.1..19041]
%DeviceName%=DeviceInstall,ROOT\LIBVIRTUALHID
%Xbox360DeviceName%=Xbox360Install,ROOT\LIBVIRTUALHID_XBOX360
; Prefer the default Xbox 360 identity reported first by SwDeviceCreate so
@@ -30,12 +31,36 @@ PnpLockdown=1
%Xbox360DeviceName%=Xbox360Install,VID_045E&PID_028E&XI_00
%Xbox360DeviceName%=Xbox360Install,LIBVIRTUALHID_XBOX360
-[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0...22000]
+[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0.1..22000]
%DeviceName%=DeviceInstall_Win11,ROOT\LIBVIRTUALHID
%Xbox360DeviceName%=Xbox360Install_Win11,ROOT\LIBVIRTUALHID_XBOX360
%Xbox360DeviceName%=Xbox360Install_Win11,VID_045E&PID_028E&XI_00
%Xbox360DeviceName%=Xbox360Install_Win11,LIBVIRTUALHID_XBOX360
+[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0.2..19041]
+%DeviceName%=DeviceInstall,ROOT\LIBVIRTUALHID
+%Xbox360DeviceName%=Xbox360Install_Server,ROOT\LIBVIRTUALHID_XBOX360
+%Xbox360DeviceName%=Xbox360Install_Server,VID_045E&PID_028E&XI_00
+%Xbox360DeviceName%=Xbox360Install_Server,LIBVIRTUALHID_XBOX360
+
+[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0.3..19041]
+%DeviceName%=DeviceInstall,ROOT\LIBVIRTUALHID
+%Xbox360DeviceName%=Xbox360Install_Server,ROOT\LIBVIRTUALHID_XBOX360
+%Xbox360DeviceName%=Xbox360Install_Server,VID_045E&PID_028E&XI_00
+%Xbox360DeviceName%=Xbox360Install_Server,LIBVIRTUALHID_XBOX360
+
+[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0.2..22000]
+%DeviceName%=DeviceInstall_Win11,ROOT\LIBVIRTUALHID
+%Xbox360DeviceName%=Xbox360Install_Server_Win11,ROOT\LIBVIRTUALHID_XBOX360
+%Xbox360DeviceName%=Xbox360Install_Server_Win11,VID_045E&PID_028E&XI_00
+%Xbox360DeviceName%=Xbox360Install_Server_Win11,LIBVIRTUALHID_XBOX360
+
+[Standard.NT@LIBVIRTUALHID_INF_ARCH@.10.0.3..22000]
+%DeviceName%=DeviceInstall_Win11,ROOT\LIBVIRTUALHID
+%Xbox360DeviceName%=Xbox360Install_Server_Win11,ROOT\LIBVIRTUALHID_XBOX360
+%Xbox360DeviceName%=Xbox360Install_Server_Win11,VID_045E&PID_028E&XI_00
+%Xbox360DeviceName%=Xbox360Install_Server_Win11,LIBVIRTUALHID_XBOX360
+
[DestinationDirs]
UMDriverCopy=13
@@ -60,12 +85,14 @@ libvirtualhid_xbox360_umdf.dll
CopyFiles=UMDriverCopy
[Xbox360Install.NT.HW]
-AddReg=Xbox360_Device_AddReg,Xbox360_Vhf_AddReg
+AddReg=Xbox360_Device_AddReg,Xbox360_Xinputhid_AddReg,Xbox360_Vhf_AddReg
[Xbox360_Device_AddReg]
; The XUSB endpoint must be openable by normal game processes. Private
; libvirtualhid IOCTLs on that interface require a broker-created session token.
HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;GRGWGX;;;BU)(A;;GRGWGX;;;AU)(A;;GRGWGX;;;WD)(A;;GR;;;RC)"
+
+[Xbox360_Xinputhid_AddReg]
; Windows.Gaming.Input uses this ancestor value as an XUSB classifier marker.
; xinputhid.sys does not attach to this System-class function device.
HKR,,"UpperFilters",0x00010008,"xinputhid"
@@ -96,6 +123,28 @@ UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects
UmdfMethodNeitherAction=Copy
UmdfFsContextUsePolicy=CanUseFsContext2
+[Xbox360Install_Server.NT]
+CopyFiles=UMDriverCopy
+
+[Xbox360Install_Server.NT.HW]
+AddReg=Xbox360_Device_AddReg,Xbox360_Vhf_AddReg
+
+[Xbox360Install_Server.NT.Interfaces]
+AddInterface={EC87F1E3-C13B-4100-B5F7-8B84D54260CB},,Xbox360_Xusb_Interface
+
+[Xbox360Install_Server.NT.Services]
+AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall
+
+[Xbox360Install_Server.NT.Wdf]
+UmdfService=libvirtualhid_xbox360_umdf,libvirtualhid_xbox360_umdf_Install
+UmdfServiceOrder=libvirtualhid_xbox360_umdf
+UmdfHostProcessSharing=ProcessSharingDisabled
+UmdfHostPriority=PriorityHigh
+UmdfKernelModeClientPolicy=AllowKernelModeClients
+UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects
+UmdfMethodNeitherAction=Copy
+UmdfFsContextUsePolicy=CanUseFsContext2
+
[DeviceInstall_Device_AddReg]
HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GRGWGX;;;BU)(A;;GRGWGX;;;AU)(A;;GRGW;;;WD)(A;;GR;;;RC)"
@@ -145,7 +194,7 @@ Include=wudfrd.inf
Needs=WUDFRD.NT
[Xbox360Install_Win11.NT.HW]
-AddReg=Xbox360_Device_AddReg,Xbox360_Vhf_AddReg
+AddReg=Xbox360_Device_AddReg,Xbox360_Xinputhid_AddReg,Xbox360_Vhf_AddReg
Include=wudfrd.inf
Needs=WUDFRD.NT.HW
@@ -166,6 +215,33 @@ UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects
UmdfMethodNeitherAction=Copy
UmdfFsContextUsePolicy=CanUseFsContext2
+[Xbox360Install_Server_Win11.NT]
+CopyFiles=UMDriverCopy
+Include=wudfrd.inf
+Needs=WUDFRD.NT
+
+[Xbox360Install_Server_Win11.NT.HW]
+AddReg=Xbox360_Device_AddReg,Xbox360_Vhf_AddReg
+Include=wudfrd.inf
+Needs=WUDFRD.NT.HW
+
+[Xbox360Install_Server_Win11.NT.Interfaces]
+AddInterface={EC87F1E3-C13B-4100-B5F7-8B84D54260CB},,Xbox360_Xusb_Interface
+
+[Xbox360Install_Server_Win11.NT.Services]
+Include=wudfrd.inf
+Needs=WUDFRD.NT.Services
+
+[Xbox360Install_Server_Win11.NT.Wdf]
+UmdfService=libvirtualhid_xbox360_umdf,libvirtualhid_xbox360_umdf_Install
+UmdfServiceOrder=libvirtualhid_xbox360_umdf
+UmdfHostProcessSharing=ProcessSharingDisabled
+UmdfHostPriority=PriorityHigh
+UmdfKernelModeClientPolicy=AllowKernelModeClients
+UmdfFileObjectPolicy=AllowNullAndUnknownFileObjects
+UmdfMethodNeitherAction=Copy
+UmdfFsContextUsePolicy=CanUseFsContext2
+
[libvirtualhid_umdf_Install]
UmdfLibraryVersion=@LIBVIRTUALHID_UMDF_LIBRARY_VERSION@
ServiceBinary=%13%\libvirtualhid_umdf.dll
diff --git a/src/platform/windows/driver/libvirtualhid_xbox360_umdf.cpp b/src/platform/windows/driver/libvirtualhid_xbox360_umdf.cpp
index ab785e7..9f4eee0 100644
--- a/src/platform/windows/driver/libvirtualhid_xbox360_umdf.cpp
+++ b/src/platform/windows/driver/libvirtualhid_xbox360_umdf.cpp
@@ -405,15 +405,11 @@ namespace {
void cleanup_driver_state() {
auto &state = driver_state();
- WDFQUEUE wait_queue = nullptr;
- WDFQUEUE output_queue = nullptr;
WDFIOTARGET target = nullptr;
VHFHANDLE vhf_handle = nullptr;
{
std::unique_lock lock {state.mutex};
state.shutting_down = true;
- wait_queue = state.wait_input_queue;
- output_queue = state.output_queue;
vhf_handle = state.virtual_hid.handle;
state.virtual_hid.handle = nullptr;
state.virtual_hid.ready = false;
@@ -425,12 +421,10 @@ namespace {
state.virtual_hid.target = nullptr;
}
- if (wait_queue != nullptr) {
- WdfIoQueuePurgeSynchronously(wait_queue);
- }
- if (output_queue != nullptr) {
- WdfIoQueuePurgeSynchronously(output_queue);
- }
+ // The queues are children of the WDF device and are already being stopped
+ // when this device cleanup callback runs. WDF completes their outstanding
+ // requests as part of object teardown; purging them here can fail the
+ // framework state check and terminate the UMDF host.
if (vhf_handle != nullptr) {
VhfDelete(vhf_handle, TRUE);
}