mirror of
https://github.com/LizardByte/libvirtualhid.git
synced 2026-10-23 03:27:34 +09:00
feat(windows): add user-mode Xbox 360 XInput support (#139)
This commit is contained in:
1 parent
a54cc91774
commit
f0e2a30687
29 files changed
+2515
-115
No files matched your search
@@ -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 `
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+27
-13
@@ -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
|
||||
|
||||
+86
-30
@@ -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,47 @@ 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, an explicit non-null container ID, and bare `VID_...` and
|
||||
`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
|
||||
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
|
||||
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 +144,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 +173,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.
|
||||
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 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
|
||||
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 +198,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 +206,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 +499,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 +512,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 +545,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
|
||||
|
||||
@@ -108,12 +108,12 @@ cover-img:
|
||||
support does not require this package.
|
||||
</li>
|
||||
<li>
|
||||
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.
|
||||
</li>
|
||||
<li>
|
||||
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.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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") }
|
||||
@@ -268,6 +267,32 @@ function Wait-ForStartedGamepadChild {
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-ForStartedXbox360Companion {
|
||||
param([int] $TimeoutSeconds)
|
||||
|
||||
$hardwareId = "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 +305,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"
|
||||
@@ -305,7 +326,11 @@ 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) {
|
||||
Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue
|
||||
|
||||
@@ -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
|
||||
@@ -24,7 +25,10 @@ target_link_libraries(libvirtualhid_broker
|
||||
nlohmann_json::nlohmann_json
|
||||
advapi32
|
||||
bcrypt
|
||||
cfgmgr32
|
||||
crypt32
|
||||
ole32
|
||||
setupapi
|
||||
winhttp)
|
||||
|
||||
if(MSVC AND LIBVIRTUALHID_BUILD_WINDOWS_DRIVER)
|
||||
|
||||
@@ -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 <lizardbyte/common/env.h>
|
||||
@@ -36,6 +37,7 @@
|
||||
// standard includes
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <bit>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
@@ -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<ULONG>(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<lvh::windows::Xbox360SoftwareDevice> 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<lvh::windows::Xbox360SoftwareDevice> 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<std::uint64_t, DeviceRecord> devices_;
|
||||
DriverChannel driver_;
|
||||
std::atomic<std::uint64_t> next_xbox360_driver_device_id_ {UINT64_C(0x8000000000000000)};
|
||||
std::optional<PolarLicenseState> license_state_ {load_license_state()};
|
||||
std::optional<LicenseValidationClock::time_point> next_license_validation_attempt_;
|
||||
std::optional<LicenseValidationClock::time_point> license_validation_unavailable_since_;
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
// 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 <algorithm>
|
||||
#include <array>
|
||||
#include <bcrypt.h>
|
||||
#include <bit>
|
||||
#include <cfgmgr32.h>
|
||||
#include <combaseapi.h>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <format>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <SetupAPI.h>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace lvh::windows {
|
||||
namespace {
|
||||
|
||||
using UniqueHandle = std::unique_ptr<void, decltype(&::CloseHandle)>;
|
||||
using UniqueDeviceInfo = std::unique_ptr<
|
||||
std::remove_pointer_t<HDEVINFO>,
|
||||
decltype(&::SetupDiDestroyDeviceInfoList)>;
|
||||
using UniqueModule = std::unique_ptr<std::remove_pointer_t<HMODULE>, 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<SoftwareDeviceCreateFunction>(::GetProcAddress(library.get(), "SwDeviceCreate"));
|
||||
close = std::bit_cast<SoftwareDeviceCloseFunction>(::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<CreationCallbackContext> 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<void>(::SetEvent(context->event.get()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string windows_error_message(DWORD error_code) {
|
||||
std::array<char, 1024> 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<DWORD>(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<std::byte, sizeof(GUID)> random_bytes {};
|
||||
if (const auto status = ::BCryptGenRandom(nullptr, std::bit_cast<PUCHAR>(random_bytes.data()), static_cast<ULONG>(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<GUID>(random_bytes);
|
||||
guid.Data3 = static_cast<unsigned short>((guid.Data3 & 0x0FFFU) | 0x4000U);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::wstring guid_string(const GUID &guid) {
|
||||
std::array<wchar_t, 40> buffer {};
|
||||
if (::StringFromGUID2(guid, buffer.data(), static_cast<int>(buffer.size())) == 0) {
|
||||
return {};
|
||||
}
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
std::wstring make_multi_string(std::span<const std::wstring_view> 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"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;
|
||||
}
|
||||
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<void>(::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<SP_DEVICE_INTERFACE_DETAIL_DATA_W> 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<wchar_t, MAX_DEVICE_ID_LEN> candidate_instance {};
|
||||
if (::SetupDiGetDeviceInstanceIdW(device_info.get(), &device_data, candidate_instance.data(), static_cast<DWORD>(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<int>(value.size()),
|
||||
nullptr,
|
||||
0,
|
||||
nullptr,
|
||||
nullptr
|
||||
);
|
||||
if (size <= 0) {
|
||||
return {};
|
||||
}
|
||||
std::string result(static_cast<std::size_t>(size), '\0');
|
||||
static_cast<void>(::WideCharToMultiByte(
|
||||
CP_UTF8,
|
||||
WC_ERR_INVALID_CHARS,
|
||||
value.data(),
|
||||
static_cast<int>(value.size()),
|
||||
result.data(),
|
||||
size,
|
||||
nullptr,
|
||||
nullptr
|
||||
));
|
||||
return result;
|
||||
}
|
||||
|
||||
template<std::size_t Size>
|
||||
void copy_string(std::array<char, Size> &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<SoftwareDeviceApi> api;
|
||||
SoftwareDeviceHandle handle {};
|
||||
|
||||
~Implementation() {
|
||||
if (handle != nullptr && api && api->close != nullptr) {
|
||||
api->close(handle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Xbox360SoftwareDevice::Xbox360SoftwareDevice(std::unique_ptr<Implementation> implementation):
|
||||
implementation_ {std::move(implementation)} {}
|
||||
|
||||
Xbox360SoftwareDevice::Xbox360SoftwareDevice(Xbox360SoftwareDevice &&) noexcept = default;
|
||||
Xbox360SoftwareDevice &Xbox360SoftwareDevice::operator=(Xbox360SoftwareDevice &&) noexcept = default;
|
||||
|
||||
Xbox360SoftwareDevice::~Xbox360SoftwareDevice() {
|
||||
implementation_.reset();
|
||||
}
|
||||
|
||||
std::unique_ptr<Xbox360SoftwareDevice> 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>();
|
||||
implementation->api = std::make_unique<SoftwareDeviceApi>();
|
||||
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<std::wstring_view, 2> hardware_ids {
|
||||
vendor_hardware_id,
|
||||
LVH_WINDOWS_XBOX360_HARDWARE_ID,
|
||||
};
|
||||
const std::array<std::wstring_view, 4> 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<CreationCallbackContext>();
|
||||
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<std::uint32_t>(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<void>(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<std::uint32_t>(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 software device did not publish its XUSB interface (" +
|
||||
device_node_status(full_instance_id) + ").";
|
||||
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<std::uintptr_t>(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<Xbox360SoftwareDevice>(std::move(implementation));
|
||||
}
|
||||
|
||||
} // namespace lvh::windows
|
||||
@@ -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 <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
|
||||
namespace lvh::windows {
|
||||
|
||||
class Xbox360SoftwareDevice {
|
||||
struct Implementation;
|
||||
|
||||
public:
|
||||
explicit Xbox360SoftwareDevice(std::unique_ptr<Implementation> implementation);
|
||||
Xbox360SoftwareDevice(const Xbox360SoftwareDevice &) = delete;
|
||||
Xbox360SoftwareDevice &operator=(const Xbox360SoftwareDevice &) = delete;
|
||||
Xbox360SoftwareDevice(Xbox360SoftwareDevice &&) noexcept;
|
||||
Xbox360SoftwareDevice &operator=(Xbox360SoftwareDevice &&) noexcept;
|
||||
~Xbox360SoftwareDevice();
|
||||
|
||||
static 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
|
||||
);
|
||||
|
||||
private:
|
||||
std::unique_ptr<Implementation> implementation_;
|
||||
};
|
||||
|
||||
} // namespace lvh::windows
|
||||
@@ -19,9 +19,11 @@
|
||||
// driver includes
|
||||
#include "generic_pid_protocol.hpp"
|
||||
#include "lvh_windows_protocol.h"
|
||||
#include "xbox_360_protocol.hpp"
|
||||
|
||||
// local includes
|
||||
#include <libvirtualhid/profiles.hpp>
|
||||
#include <libvirtualhid/report.hpp>
|
||||
#include <libvirtualhid/types.hpp>
|
||||
|
||||
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<std::uint16_t>(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
|
||||
@@ -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$<$<CONFIG:Debug>: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
|
||||
"$<TARGET_FILE_DIR:libvirtualhid_umdf>"
|
||||
@@ -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 \"$<TARGET_FILE:libvirtualhid_umdf>\")
|
||||
set(lvh_xbox360_driver_dll \"$<TARGET_FILE:libvirtualhid_xbox360_umdf>\")
|
||||
set(lvh_driver_inf \"$<TARGET_FILE_DIR:libvirtualhid_umdf>/libvirtualhid.inf\")
|
||||
set(lvh_driver_cat \"$<TARGET_FILE_DIR:libvirtualhid_umdf>/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 \"
|
||||
|
||||
@@ -18,14 +18,48 @@ 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
|
||||
; 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]
|
||||
[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
|
||||
@@ -35,6 +69,7 @@ UMDriverCopy=13
|
||||
|
||||
[SourceDisksFiles]
|
||||
libvirtualhid_umdf.dll=1
|
||||
libvirtualhid_xbox360_umdf.dll=1
|
||||
|
||||
[DeviceInstall.NT]
|
||||
CopyFiles=UMDriverCopy
|
||||
@@ -44,6 +79,71 @@ 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_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"
|
||||
|
||||
[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
|
||||
|
||||
[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)"
|
||||
@@ -88,12 +188,71 @@ 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_Xinputhid_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
|
||||
|
||||
[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
|
||||
|
||||
[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"
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,967 @@
|
||||
// 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 <Windows.h>
|
||||
#undef WIN32_NO_STATUS
|
||||
|
||||
#if defined(_MSC_VER)
|
||||
#pragma warning(push)
|
||||
#pragma warning(disable : 4324 4471)
|
||||
#endif
|
||||
#include <wdf.h>
|
||||
// VHF uses UMDF WDM types declared by wdf.h.
|
||||
#include <vhf.h>
|
||||
#if defined(_MSC_VER)
|
||||
#pragma warning(pop)
|
||||
#endif
|
||||
|
||||
#include "windows_device_identity.hpp"
|
||||
#include "xbox_360_protocol.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <bit>
|
||||
#include <condition_variable>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
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<std::vector<std::uint8_t>> report;
|
||||
std::uint8_t report_id {};
|
||||
};
|
||||
|
||||
struct VirtualHidState {
|
||||
WDFIOTARGET target {};
|
||||
VHFHANDLE handle {};
|
||||
std::vector<std::uint8_t> report_descriptor;
|
||||
std::wstring hardware_ids;
|
||||
std::vector<std::uint8_t> latest_report;
|
||||
std::optional<std::vector<std::uint8_t>> pending_report;
|
||||
std::shared_ptr<std::vector<std::uint8_t>> 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<LvhWindowsOutputReportEvent> 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<typename Value>
|
||||
void write_little_endian(std::span<std::uint8_t> output, std::size_t offset, Value value) {
|
||||
static_assert(std::is_integral_v<Value>);
|
||||
using Unsigned = std::make_unsigned_t<Value>;
|
||||
const auto raw = static_cast<Unsigned>(value);
|
||||
for (std::size_t byte = 0; byte < sizeof(Value); ++byte) {
|
||||
output[offset + byte] = static_cast<std::uint8_t>(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<typename Value>
|
||||
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<Value *>(buffer);
|
||||
return STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
NTSTATUS copy_to_request(WDFREQUEST request, std::span<const std::uint8_t> 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<std::uint8_t, 29> make_xusb_state(bool wait_for_input) {
|
||||
auto &state = driver_state();
|
||||
std::array<std::uint8_t, 29> 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<std::uint16_t>(output, 0U, xbox_version);
|
||||
output[2] = wait_for_input ? 0x03U : 0x01U;
|
||||
write_little_endian<std::uint32_t>(output, 5U, packet_number);
|
||||
if (wait_for_input) {
|
||||
output[9] = 0x00U;
|
||||
output[10] = 0x14U;
|
||||
}
|
||||
write_little_endian<std::uint16_t>(output, 11U, input.buttons);
|
||||
output[13] = input.left_trigger;
|
||||
output[14] = input.right_trigger;
|
||||
write_little_endian<std::int16_t>(output, 15U, input.left_thumb_x);
|
||||
write_little_endian<std::int16_t>(output, 17U, input.left_thumb_y);
|
||||
write_little_endian<std::int16_t>(output, 19U, input.right_thumb_x);
|
||||
write_little_endian<std::int16_t>(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<void>(copy_to_request(request, output));
|
||||
}
|
||||
|
||||
std::uint8_t signed_axis_to_hid(std::int16_t value, bool invert) {
|
||||
const auto adjusted = invert ? -static_cast<std::int32_t>(value) : static_cast<std::int32_t>(value);
|
||||
const auto clamped = std::clamp(adjusted, -32768, 32767);
|
||||
return static_cast<std::uint8_t>(
|
||||
(static_cast<std::uint32_t>(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<std::uint16_t>(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<std::uint8_t> make_hid_report(
|
||||
const LvhWindowsCreateDeviceRequest &create_request,
|
||||
const LvhWindowsXbox360InputState &input
|
||||
) {
|
||||
std::vector<std::uint8_t> 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<std::uint8_t>(buttons & 0xFFU);
|
||||
report[2] = static_cast<std::uint8_t>(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<VhfSubmission> 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::vector<std::uint8_t>>(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<ULONG>(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<std::uint8_t> 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<USHORT>(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<USHORT>(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();
|
||||
WDFIOTARGET target = nullptr;
|
||||
VHFHANDLE vhf_handle = nullptr;
|
||||
{
|
||||
std::unique_lock lock {state.mutex};
|
||||
state.shutting_down = true;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
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<SC_HANDLE>,
|
||||
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<LPBYTE>(service_status_bytes.data()),
|
||||
static_cast<DWORD>(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<LvhWindowsXbox360InitializeRequest *>(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<std::uint8_t> {};
|
||||
{
|
||||
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<void>(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<LvhWindowsXbox360SubmitInputRequest *>(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<LvhWindowsXbox360ReadOutputRequest *>(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<LvhWindowsOutputReportEvent> 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<void>(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<std::uint16_t>(low_frequency) * 257U;
|
||||
const auto high = static_cast<std::uint16_t>(high_frequency) * 257U;
|
||||
event.report[1] = static_cast<std::uint8_t>(low & 0xFFU);
|
||||
event.report[2] = static_cast<std::uint8_t>(low >> 8U);
|
||||
event.report[3] = static_cast<std::uint8_t>(high & 0xFFU);
|
||||
event.report[4] = static_cast<std::uint8_t>(high >> 8U);
|
||||
queue = state.output_queue;
|
||||
}
|
||||
|
||||
WDFREQUEST request = nullptr;
|
||||
if (queue != nullptr && NT_SUCCESS(WdfIoQueueRetrieveNextRequest(queue, &request))) {
|
||||
static_cast<void>(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<std::uint8_t, 12> information {};
|
||||
write_little_endian<std::uint16_t>(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<std::uint16_t>(information, 8U, vendor_id);
|
||||
write_little_endian<std::uint16_t>(information, 10U, product_id);
|
||||
static_cast<void>(copy_to_request(request, information));
|
||||
}
|
||||
|
||||
void handle_xusb_capabilities(WDFREQUEST request, size_t output_buffer_length) {
|
||||
constexpr std::array<std::uint8_t, 24> 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<void>(copy_to_request(request, capabilities_v1));
|
||||
return;
|
||||
}
|
||||
|
||||
std::array<std::uint8_t, 36> 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<std::uint8_t>(state.create_request.hardware_ids.vendor_id & 0xFFU);
|
||||
capabilities_v2[7] = static_cast<std::uint8_t>(state.create_request.hardware_ids.vendor_id >> 8U);
|
||||
capabilities_v2[8] = static_cast<std::uint8_t>(state.create_request.hardware_ids.product_id & 0xFFU);
|
||||
capabilities_v2[9] = static_cast<std::uint8_t>(state.create_request.hardware_ids.product_id >> 8U);
|
||||
}
|
||||
}
|
||||
static_cast<void>(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<const std::uint8_t *>(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<Xbox360DriverState *>(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<void>(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<Xbox360DriverState *>(vhf_client_context);
|
||||
if (state == nullptr || packet == nullptr || packet->reportBuffer == nullptr) {
|
||||
static_cast<void>(VhfAsyncOperationComplete(operation_handle, STATUS_INVALID_PARAMETER));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<std::uint8_t> 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<void>(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<void>(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<void>(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<std::uint8_t, 3> led {0U, 0U, 0x06U};
|
||||
static_cast<void>(copy_to_request(request, led));
|
||||
return;
|
||||
}
|
||||
case LVH_WINDOWS_IOCTL_XUSB_GET_BATTERY_INFO:
|
||||
{
|
||||
constexpr std::array<std::uint8_t, 4> battery {0U, 0x01U, 0x03U, 0U};
|
||||
static_cast<void>(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<std::uint8_t, 64> information {};
|
||||
write_little_endian<std::uint16_t>(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<std::uint16_t>(information, 8U, vendor_id);
|
||||
write_little_endian<std::uint16_t>(information, 10U, product_id);
|
||||
const auto size = std::min(output_buffer_length, information.size());
|
||||
static_cast<void>(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;
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
#include <array>
|
||||
#include <stdint.h>
|
||||
|
||||
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;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <array>
|
||||
#include <stdint.h>
|
||||
|
||||
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<char, LVH_WINDOWS_MAX_DEVICE_PATH_SIZE> device_path;
|
||||
};
|
||||
|
||||
@@ -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 <array>
|
||||
#include <cstdint>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <guiddef.h>
|
||||
#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"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)
|
||||
@@ -193,6 +193,15 @@ namespace lvh::detail {
|
||||
return {handle, &::CloseHandle};
|
||||
}
|
||||
|
||||
UniqueHandle take_transport_handle(LvhWindowsCreateDeviceResponse &response) {
|
||||
const auto raw_value = static_cast<std::uintptr_t>(response.transport_handle);
|
||||
if (raw_value == 0U || raw_value == std::bit_cast<std::uintptr_t>(INVALID_HANDLE_VALUE) || static_cast<std::uint64_t>(raw_value) != response.transport_handle) {
|
||||
return make_unique_handle(nullptr);
|
||||
}
|
||||
response.transport_handle = 0U;
|
||||
return make_unique_handle(std::bit_cast<HANDLE>(raw_value));
|
||||
}
|
||||
|
||||
std::string windows_error_message(DWORD error_code) {
|
||||
std::array<char, 1024> 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<LvhWindowsOutputReportEvent> 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<DWORD>(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<std::uint32_t>(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<WindowsBackendContext> {
|
||||
@@ -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<void>(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<WindowsVhfDeviceState>(
|
||||
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<std::uint8_t> report {
|
||||
event->report.begin(),
|
||||
event->report.begin() + std::min(
|
||||
event->report_size,
|
||||
static_cast<std::uint32_t>(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<void>(::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<std::uint8_t> &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);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
+6
-1
@@ -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<std::uintptr_t>(
|
||||
::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;
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ TEST(WindowsBrokerClientTest, BuildsVersionedRequestHeader) {
|
||||
EXPECT_EQ(header.size, sizeof(LvhWindowsBrokerLicenseRequest));
|
||||
EXPECT_EQ(header.type, static_cast<std::uint32_t>(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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<GetState>(GetProcAddress(module_, "XInputGetState"));
|
||||
set_state_ = std::bit_cast<SetState>(GetProcAddress(module_, "XInputSetState"));
|
||||
get_battery_information_ = std::bit_cast<GetBatteryInformation>(
|
||||
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;
|
||||
|
||||
@@ -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,20 @@ 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);
|
||||
EXPECT_STREQ(LVH_WINDOWS_XBOX360_ENUMERATOR, L"LibVirtualHid");
|
||||
EXPECT_STREQ(LVH_WINDOWS_XBOX360_HARDWARE_ID, L"LIBVIRTUALHID_XBOX360");
|
||||
}
|
||||
|
||||
TEST(WindowsProtocolTest, MapsBusTypesAndGamepadKinds) {
|
||||
@@ -691,6 +700,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<std::uint8_t> {1});
|
||||
const auto destroy = lvh::detail::windows::make_destroy_device_request(17);
|
||||
|
||||
Reference in new issue
Block a user