From b9172cdd7b47e3c911815695e21ada3f0c1d66b6 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:03:45 -0400 Subject: [PATCH 01/27] Refactor Windows driver installation and VHF setup Major refactor of Windows driver packaging and installation: - Switch WiX packaging from source files (.wxs) to patch files (.xml) with CMake-based extension management - Remove devcon dependency; use SetupAPI and pnputil for device creation and removal - Add UMDF library version configuration (defaults to 2.15) with validation during build - Implement lazy VHF target initialization in driver; gamepad creation opens VHF on demand - Add detailed trace logging to driver and install scripts for debugging - Improve device detection to handle both PnP enumeration and registry lookups - Set VhfMode=1 on root devices before driver start - Change driver device class from HIDClass to System; include WUDFRD.inf for proper service setup - Link MSVC runtime statically to avoid VC runtime dependencies in UMDF host - Add -LogPath parameter to install script for WiX integration - Update README with comprehensive driver architecture details --- CMakeLists.txt | 3 + README.md | 29 +- cmake/packaging/windows_wix.cmake | 58 ++-- .../libvirtualhid-driver-installer-patch.xml | 35 +++ .../libvirtualhid-driver-installer.wxs | 35 --- scripts/windows/install-driver.ps1 | 252 ++++++++++++++---- scripts/windows/uninstall-driver.ps1 | 80 +++++- src/platform/windows/driver/CMakeLists.txt | 34 +++ .../windows/driver/libvirtualhid.inf.in | 32 +-- .../windows/driver/libvirtualhid_umdf.cpp | 109 +++++++- 10 files changed, 505 insertions(+), 162 deletions(-) create mode 100644 cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml delete mode 100644 cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs diff --git a/CMakeLists.txt b/CMakeLists.txt index 6115e65..519120c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,6 +2,9 @@ # Project configuration # cmake_minimum_required(VERSION 3.24) +if(POLICY CMP0091) + cmake_policy(SET CMP0091 NEW) +endif() project(libvirtualhid VERSION 0.0.0 DESCRIPTION "Cross-platform virtual HID device library." HOMEPAGE_URL "https://app.lizardbyte.dev" diff --git a/README.md b/README.md index d88c248..7b552bc 100644 --- a/README.md +++ b/README.md @@ -134,22 +134,41 @@ Developer install/uninstall helpers live under `scripts/windows`: ```powershell powershell -ExecutionPolicy Bypass -File .\scripts\windows\install-driver.ps1 ` - -InfPath .\cmake-build-windows-driver\src\platform\windows\driver\package\Release\libvirtualhid.inf + -InfPath .\cmake-build-windows-driver\src\platform\windows\driver\package\Release\libvirtualhid.inf ` + -LogPath .\cmake-build-windows-driver\install-driver.log powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1 ` -Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" ``` The helper stages the INF with `pnputil`, updates an existing `ROOT\LIBVIRTUALHID` device when present, and creates that root-enumerated -device when it is missing. It uses `devcon.exe` when available, otherwise it -uses SetupAPI/NewDev directly so MSI installs do not require the WDK tools on -the target machine. +device when it is missing. It uses SetupAPI/NewDev directly so MSI installs do +not require the WDK tools on the target machine. Existing devices are detected +by matching the `ROOT\LIBVIRTUALHID` hardware ID. The SetupAPI path creates a +root-enumerated instance such as `ROOT\LIBVIRTUALHID\####`. +The install and uninstall helpers also clean up malformed development devices +left by earlier installer revisions. The WiX installer writes the helper +transcript to `C:\ProgramData\libvirtualhid\install-driver.log`. The driver binary is a UMDF DLL installed through the Windows Driver Store, not a libvirtualhid `.sys` copied into `C:\Windows\System32\drivers`. Windows still uses its built-in `WUDFRd.sys` and VHF components under `System32\drivers`; the libvirtualhid-specific sign that installation completed is the -`ROOT\LIBVIRTUALHID` device and the `\\.\LibVirtualHid` control device. +`ROOT\LIBVIRTUALHID` device and the `\\.\LibVirtualHid` control device. The INF +includes the built-in `WUDFRd` install sections for the root `System` control +device, appends the VHF lower filter, sets `VhfMode=1` for the UMDF VHF source +stack, and leaves UMDF dispatcher policy at the framework default to match the +inbox VHF source-driver shape. The installer also writes `VhfMode=1` onto the +root device before starting the driver so root-enumerated development installs +get the same VHF source mode as the INF hardware section. The UMDF control +device starts without opening VHF; gamepad creation opens VHF lazily so +target-open failures are reported through the create-device response instead of +making `\\.\LibVirtualHid` unavailable. The generated INF uses the same UMDF +library version as the WDF headers and stub library selected by CMake. The +package defaults to UMDF 2.15, matching the inbox VHF UMDF source driver while +still exposing the framework APIs used by libvirtualhid. The driver target links +the MSVC runtime statically to avoid requiring VC runtime DLLs in the UMDF host +process. Windows driver packages require a signed catalog for normal installation. Pull request builds generate a short-lived self-signed test certificate, sign diff --git a/cmake/packaging/windows_wix.cmake b/cmake/packaging/windows_wix.cmake index 8779038..4a9a59b 100644 --- a/cmake/packaging/windows_wix.cmake +++ b/cmake/packaging/windows_wix.cmake @@ -26,25 +26,33 @@ if(NOT WIX_INSTALL_RESULT EQUAL 0) message(FATAL_ERROR "Failed to install WiX tools locally: ${WIX_INSTALL_OUTPUT}") endif() -execute_process( - COMMAND "${WIX_TOOL_PATH}/wix" extension add WixToolset.UI.wixext/${WIX_UI_VERSION} - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - ERROR_VARIABLE WIX_UI_INSTALL_OUTPUT - RESULT_VARIABLE WIX_UI_INSTALL_RESULT) - -if(NOT WIX_UI_INSTALL_RESULT EQUAL 0) - message(FATAL_ERROR "Failed to install WiX UI extension: ${WIX_UI_INSTALL_OUTPUT}") -endif() - -execute_process( - COMMAND "${WIX_TOOL_PATH}/wix" extension add WixToolset.Util.wixext/${WIX_UI_VERSION} - WORKING_DIRECTORY ${CMAKE_BINARY_DIR} - ERROR_VARIABLE WIX_UTIL_INSTALL_OUTPUT - RESULT_VARIABLE WIX_UTIL_INSTALL_RESULT) - -if(NOT WIX_UTIL_INSTALL_RESULT EQUAL 0) - message(FATAL_ERROR "Failed to install WiX Util extension: ${WIX_UTIL_INSTALL_OUTPUT}") -endif() +function(libvirtualhid_wix_ensure_extension extension_name extension_version) + execute_process( + COMMAND "${WIX_TOOL_PATH}/wix" extension list --global + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + OUTPUT_VARIABLE WIX_EXTENSION_LIST_OUTPUT + ERROR_VARIABLE WIX_EXTENSION_LIST_ERROR + RESULT_VARIABLE WIX_EXTENSION_LIST_RESULT) + + if(WIX_EXTENSION_LIST_RESULT EQUAL 0 + AND WIX_EXTENSION_LIST_OUTPUT MATCHES "${extension_name}[ \t]+${extension_version}") + return() + endif() + + execute_process( + COMMAND "${WIX_TOOL_PATH}/wix" extension add --global "${extension_name}/${extension_version}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ERROR_VARIABLE WIX_EXTENSION_INSTALL_OUTPUT + RESULT_VARIABLE WIX_EXTENSION_INSTALL_RESULT) + + if(NOT WIX_EXTENSION_INSTALL_RESULT EQUAL 0) + message(FATAL_ERROR + "Failed to install WiX extension ${extension_name}/${extension_version}: " + "${WIX_EXTENSION_INSTALL_OUTPUT}${WIX_EXTENSION_LIST_ERROR}") + endif() +endfunction() + +libvirtualhid_wix_ensure_extension(WixToolset.UI.wixext ${WIX_UI_VERSION}) set(CPACK_WIX_ROOT "${WIX_TOOL_PATH}") set(CPACK_WIX_UPGRADE_GUID "71D7B738-9D83-4E57-82E3-C3106D9F8053") @@ -52,16 +60,10 @@ set(CPACK_WIX_HELP_LINK "https://app.lizardbyte.dev/support") set(CPACK_WIX_PRODUCT_URL "${CMAKE_PROJECT_HOMEPAGE_URL}") set(CPACK_WIX_PROGRAM_MENU_FOLDER "LizardByte") set(CPACK_WIX_EXTENSIONS - "WixToolset.UI.wixext" - "WixToolset.Util.wixext") - -message(STATUS "cpack package directory: ${CPACK_PACKAGE_DIRECTORY}") - -file(COPY "${CMAKE_CURRENT_LIST_DIR}/wix_resources/" - DESTINATION "${WIX_BUILD_PARENT_DIRECTORY}/") + "WixToolset.UI.wixext") -set(CPACK_WIX_EXTRA_SOURCES - "${WIX_BUILD_PARENT_DIRECTORY}/libvirtualhid-driver-installer.wxs") +set(CPACK_WIX_PATCH_FILE + "${CMAKE_CURRENT_LIST_DIR}/wix_resources/libvirtualhid-driver-installer-patch.xml") file(COPY "${CMAKE_SOURCE_DIR}/LICENSE" DESTINATION "${CMAKE_BINARY_DIR}") diff --git a/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml b/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml new file mode 100644 index 0000000..8387faf --- /dev/null +++ b/cmake/packaging/wix_resources/libvirtualhid-driver-installer-patch.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + diff --git a/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs b/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs deleted file mode 100644 index 984113f..0000000 --- a/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index 31ee9f6..ce44d7d 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -11,10 +11,44 @@ param( [string] $HardwareId = "ROOT\LIBVIRTUALHID", + [string] $LogPath, + [switch] $StageOnly ) $ErrorActionPreference = "Stop" +$script:LibVirtualHidTranscriptStarted = $false + +function Start-LibVirtualHidTranscript { + param([string] $Path) + + if (-not $Path) { + return + } + + try { + $logDirectory = Split-Path -Parent $Path + if ($logDirectory) { + New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null + } + Start-Transcript -Path $Path -Append | Out-Null + $script:LibVirtualHidTranscriptStarted = $true + } catch { + Write-Warning "Unable to start libvirtualhid install transcript: $($_.Exception.Message)" + } +} + +function Stop-LibVirtualHidTranscript { + if (-not $script:LibVirtualHidTranscriptStarted) { + return + } + + try { + Stop-Transcript | Out-Null + } catch { + Write-Warning "Unable to stop libvirtualhid install transcript: $($_.Exception.Message)" + } +} function Invoke-CheckedCommand { param( @@ -22,38 +56,17 @@ function Invoke-CheckedCommand { [string] $FilePath, [Parameter(Mandatory = $true)] - [string[]] $Arguments + [string[]] $Arguments, + + [int[]] $SuccessExitCodes = @(0) ) & $FilePath @Arguments - if ($LASTEXITCODE -ne 0) { + if ($LASTEXITCODE -notin $SuccessExitCodes) { throw "$FilePath exited with code $LASTEXITCODE" } } -function Find-Devcon { - if ($env:DEVCON_EXE -and (Test-Path -LiteralPath $env:DEVCON_EXE)) { - return $env:DEVCON_EXE - } - - $roots = @( - $env:WDKContentRoot, - $env:WindowsSdkDir, - "${env:ProgramFiles(x86)}\Windows Kits\10" - ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } - - foreach ($root in $roots) { - $candidate = Get-ChildItem -LiteralPath $root -Recurse -Filter devcon.exe -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -match "\\x64\\devcon\.exe$" } | - Select-Object -First 1 - if ($candidate) { - return $candidate.FullName - } - } - - return $null -} - function Import-DriverCertificate { [CmdletBinding(SupportsShouldProcess)] param([string] $Path) @@ -121,7 +134,7 @@ namespace LibVirtualHid.SetupApi { uint creationFlags, ref SpDevinfoData deviceInfoData); - [DllImport("setupapi.dll", SetLastError = true)] + [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool SetupDiSetDeviceRegistryProperty( IntPtr deviceInfoSet, ref SpDevinfoData deviceInfoData, @@ -146,9 +159,24 @@ namespace LibVirtualHid.SetupApi { uint installFlags, out bool rebootRequired); + public static void Update(string infPath, string hardwareId, out bool rebootRequired) { + rebootRequired = false; + + if (!UpdateDriverForPlugAndPlayDevices( + IntPtr.Zero, + hardwareId, + infPath, + InstallFlagForce | InstallFlagNonInteractive, + out rebootRequired)) { + ThrowLastWin32Error("UpdateDriverForPlugAndPlayDevices"); + } + } + public static void Install(string infPath, string hardwareId, out bool rebootRequired) { rebootRequired = false; + string rootDeviceName = GetRootDeviceName(hardwareId); + Guid classGuid; uint requiredSize; var className = new StringBuilder(256); @@ -167,7 +195,7 @@ namespace LibVirtualHid.SetupApi { if (!SetupDiCreateDeviceInfo( deviceInfoSet, - className.ToString(), + rootDeviceName, ref classGuid, null, IntPtr.Zero, @@ -192,15 +220,22 @@ namespace LibVirtualHid.SetupApi { } finally { SetupDiDestroyDeviceInfoList(deviceInfoSet); } + } - if (!UpdateDriverForPlugAndPlayDevices( - IntPtr.Zero, - hardwareId, - infPath, - InstallFlagForce | InstallFlagNonInteractive, - out rebootRequired)) { - ThrowLastWin32Error("UpdateDriverForPlugAndPlayDevices"); + private static string GetRootDeviceName(string hardwareId) { + const string rootPrefix = "ROOT\\"; + if (!hardwareId.StartsWith(rootPrefix, StringComparison.OrdinalIgnoreCase)) { + throw new ArgumentException("Hardware ID must use the ROOT\\ enumerator.", "hardwareId"); } + + string rootDeviceName = hardwareId.Substring(rootPrefix.Length); + if (rootDeviceName.Length == 0 || rootDeviceName.Contains("\\")) { + throw new ArgumentException( + "Hardware ID must be a root-enumerated device ID without an instance suffix.", + "hardwareId"); + } + + return rootDeviceName; } private static void ThrowLastWin32Error(string action) { @@ -216,10 +251,24 @@ namespace LibVirtualHid.SetupApi { function Get-RootDeviceInstanceId { param([string] $TargetHardwareId) + try { + $devices = & pnputil.exe /enum-devices /deviceid $TargetHardwareId /deviceids + if ($LASTEXITCODE -eq 0) { + $instanceIds = @($devices | + Where-Object { $_ -match "^\s*Instance ID\s*:\s*(.+)$" } | + ForEach-Object { $Matches[1].Trim() }) + if ($instanceIds.Count -gt 0) { + return $instanceIds + } + } + } catch { + Write-Verbose "Unable to enumerate PnP devices with pnputil: $($_.Exception.Message)" + } + try { $prefix = "$TargetHardwareId\" @(Get-CimInstance -ClassName Win32_PnPEntity -ErrorAction Stop | - Where-Object { $_.PNPDeviceID -like "$prefix*" } | + Where-Object { $_.PNPDeviceID -like "$prefix*" -or $_.HardwareID -contains $TargetHardwareId } | ForEach-Object { $_.PNPDeviceID }) } catch { Write-Verbose "Unable to enumerate PnP devices: $($_.Exception.Message)" @@ -227,6 +276,85 @@ function Get-RootDeviceInstanceId { } } +function Get-RegistryRootDevice { + param([string] $TargetHardwareId) + + $rootKey = "HKLM:\SYSTEM\CurrentControlSet\Enum\ROOT" + Get-ChildItem -LiteralPath $rootKey -ErrorAction SilentlyContinue | ForEach-Object { + $rootDeviceId = $_.PSChildName + Get-ChildItem -LiteralPath $_.PSPath -ErrorAction SilentlyContinue | ForEach-Object { + $instanceId = "ROOT\$rootDeviceId\$($_.PSChildName)" + $hardwareIds = @() + try { + $hardwareIds = @((Get-ItemProperty -LiteralPath $_.PSPath -Name HardwareID -ErrorAction Stop).HardwareID) + } catch { + $hardwareIds = @() + } + + $hasExactHardwareId = $hardwareIds -contains $TargetHardwareId + $hasCorruptHardwareId = ( + $hardwareIds.Count -gt 1 -and + -not $hasExactHardwareId -and + (($hardwareIds -join "") -ieq $TargetHardwareId) + ) + $hasTargetInstanceId = $instanceId -like "$TargetHardwareId\*" + + if ($hasExactHardwareId -or $hasCorruptHardwareId -or $hasTargetInstanceId) { + [pscustomobject]@{ + InstanceId = $instanceId + HasExactHardwareId = $hasExactHardwareId + HasCorruptHardwareId = $hasCorruptHardwareId + } + } + } + } +} + +function Remove-DeviceInstance { + [CmdletBinding(SupportsShouldProcess)] + param([string] $InstanceId) + + if ($PSCmdlet.ShouldProcess($InstanceId, "Remove stale libvirtualhid root device")) { + Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments @("/remove-device", $InstanceId) + } +} + +function Set-RootDeviceVhfMode { + [CmdletBinding(SupportsShouldProcess)] + param([string] $InstanceId) + + $deviceRegistryPath = "HKLM:\SYSTEM\CurrentControlSet\Enum\$InstanceId" + if (-not (Test-Path -LiteralPath $deviceRegistryPath)) { + Write-Verbose "Unable to set VhfMode because $deviceRegistryPath does not exist." + return + } + + if ($PSCmdlet.ShouldProcess($InstanceId, "Set VhfMode=1 for UMDF VHF source device")) { + New-ItemProperty -LiteralPath $deviceRegistryPath -Name "VhfMode" -Value 1 -PropertyType DWord -Force | Out-Null + Write-Information "Set VhfMode=1 on $InstanceId." -InformationAction Continue + } +} + +function Update-RootDeviceDriverWithSetupApi { + [CmdletBinding(SupportsShouldProcess)] + param( + [Parameter(Mandatory = $true)] + [string] $Path, + + [Parameter(Mandatory = $true)] + [string] $TargetHardwareId + ) + + Add-SetupApiRootDeviceInstaller + $rebootRequired = $false + if ($PSCmdlet.ShouldProcess($TargetHardwareId, "Update libvirtualhid development device driver")) { + [LibVirtualHid.SetupApi.RootDeviceInstaller]::Update($Path, $TargetHardwareId, [ref] $rebootRequired) + } + if ($rebootRequired) { + Write-Warning "Windows reported that a reboot is required to finish installing the libvirtualhid driver." + } +} + function Install-RootDeviceWithSetupApi { param( [Parameter(Mandatory = $true)] @@ -244,30 +372,44 @@ function Install-RootDeviceWithSetupApi { } } -$resolvedInf = (Resolve-Path -LiteralPath $InfPath).Path -Import-DriverCertificate -Path $CertificatePath +Start-LibVirtualHidTranscript -Path $LogPath -if ($PSCmdlet.ShouldProcess($resolvedInf, "Stage libvirtualhid driver package")) { - Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments @("/add-driver", $resolvedInf, "/install") -} +try { + $resolvedInf = (Resolve-Path -LiteralPath $InfPath).Path + Import-DriverCertificate -Path $CertificatePath -if ($StageOnly) { - return -} + if ($PSCmdlet.ShouldProcess($resolvedInf, "Stage libvirtualhid driver package")) { + Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments @("/add-driver", $resolvedInf) -SuccessExitCodes @(0, 5) + } -if ((Get-RootDeviceInstanceId -TargetHardwareId $HardwareId).Count -gt 0) { - Write-Information "The $HardwareId device already exists." -InformationAction Continue - return -} + if ($StageOnly) { + return + } -$devcon = Find-Devcon -if ($devcon) { - if ($PSCmdlet.ShouldProcess($HardwareId, "Create libvirtualhid development device with devcon")) { - Invoke-CheckedCommand -FilePath $devcon -Arguments @("install", $resolvedInf, $HardwareId) + $registryRootDevices = @(Get-RegistryRootDevice -TargetHardwareId $HardwareId) + foreach ($device in ($registryRootDevices | Where-Object { $_.HasCorruptHardwareId })) { + Remove-DeviceInstance -InstanceId $device.InstanceId } - return -} -if ($PSCmdlet.ShouldProcess($HardwareId, "Create libvirtualhid development device with SetupAPI")) { - Install-RootDeviceWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId + $rootDevices = @(Get-RootDeviceInstanceId -TargetHardwareId $HardwareId) + if ($rootDevices.Count -gt 0) { + Write-Information "Updating the existing $HardwareId device driver." -InformationAction Continue + foreach ($rootDevice in $rootDevices) { + Set-RootDeviceVhfMode -InstanceId $rootDevice + } + Update-RootDeviceDriverWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId + return + } + + if ($PSCmdlet.ShouldProcess($HardwareId, "Create libvirtualhid development device with SetupAPI")) { + Install-RootDeviceWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId + } + + $rootDevices = @(Get-RootDeviceInstanceId -TargetHardwareId $HardwareId) + foreach ($rootDevice in $rootDevices) { + Set-RootDeviceVhfMode -InstanceId $rootDevice + } + Update-RootDeviceDriverWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId +} finally { + Stop-LibVirtualHidTranscript } diff --git a/scripts/windows/uninstall-driver.ps1 b/scripts/windows/uninstall-driver.ps1 index 3012415..b80e226 100644 --- a/scripts/windows/uninstall-driver.ps1 +++ b/scripts/windows/uninstall-driver.ps1 @@ -63,6 +63,7 @@ function Find-PublishedName { $drivers = & pnputil.exe /enum-drivers $currentPublished = $null $currentOriginal = $null + $publishedNames = @() foreach ($line in $drivers) { if ($line -match "^\s*Published Name\s*:\s*(.+)$") { @@ -74,21 +75,35 @@ function Find-PublishedName { if ($line -match "^\s*Original Name\s*:\s*(.+)$") { $currentOriginal = $Matches[1].Trim() if ($currentPublished -and $currentOriginal -ieq $TargetOriginalName) { - return $currentPublished + $publishedNames += $currentPublished } } } - return $null + return $publishedNames } function Get-RootDeviceInstanceId { param([string] $TargetHardwareId) + try { + $devices = & pnputil.exe /enum-devices /deviceid $TargetHardwareId /deviceids + if ($LASTEXITCODE -eq 0) { + $instanceIds = @($devices | + Where-Object { $_ -match "^\s*Instance ID\s*:\s*(.+)$" } | + ForEach-Object { $Matches[1].Trim() }) + if ($instanceIds.Count -gt 0) { + return $instanceIds + } + } + } catch { + Write-Verbose "Unable to enumerate PnP devices with pnputil: $($_.Exception.Message)" + } + try { $prefix = "$TargetHardwareId\" @(Get-CimInstance -ClassName Win32_PnPEntity -ErrorAction Stop | - Where-Object { $_.PNPDeviceID -like "$prefix*" } | + Where-Object { $_.PNPDeviceID -like "$prefix*" -or $_.HardwareID -contains $TargetHardwareId } | ForEach-Object { $_.PNPDeviceID }) } catch { Write-Verbose "Unable to enumerate PnP devices: $($_.Exception.Message)" @@ -96,6 +111,36 @@ function Get-RootDeviceInstanceId { } } +function Get-RegistryRootDeviceInstanceId { + param([string] $TargetHardwareId) + + $rootKey = "HKLM:\SYSTEM\CurrentControlSet\Enum\ROOT" + Get-ChildItem -LiteralPath $rootKey -ErrorAction SilentlyContinue | ForEach-Object { + $rootDeviceId = $_.PSChildName + Get-ChildItem -LiteralPath $_.PSPath -ErrorAction SilentlyContinue | ForEach-Object { + $instanceId = "ROOT\$rootDeviceId\$($_.PSChildName)" + $hardwareIds = @() + try { + $hardwareIds = @((Get-ItemProperty -LiteralPath $_.PSPath -Name HardwareID -ErrorAction Stop).HardwareID) + } catch { + $hardwareIds = @() + } + + $hasExactHardwareId = $hardwareIds -contains $TargetHardwareId + $hasCorruptHardwareId = ( + $hardwareIds.Count -gt 1 -and + -not $hasExactHardwareId -and + (($hardwareIds -join "") -ieq $TargetHardwareId) + ) + $hasTargetInstanceId = $instanceId -like "$TargetHardwareId\*" + + if ($hasExactHardwareId -or $hasCorruptHardwareId -or $hasTargetInstanceId) { + $instanceId + } + } + } +} + function Remove-DriverCertificate { [CmdletBinding(SupportsShouldProcess)] param([string] $Subject) @@ -127,23 +172,34 @@ foreach ($instanceId in (Get-RootDeviceInstanceId -TargetHardwareId $HardwareId) } } -if (-not $PublishedName) { - $PublishedName = Find-PublishedName -TargetOriginalName $OriginalName +foreach ($instanceId in (Get-RegistryRootDeviceInstanceId -TargetHardwareId $HardwareId | Select-Object -Unique)) { + if ($PSCmdlet.ShouldProcess($instanceId, "Remove libvirtualhid registry-discovered development device with pnputil")) { + Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments @("/remove-device", $instanceId) -IgnoreFailure + } +} + +$publishedNames = @() +if ($PublishedName) { + $publishedNames += $PublishedName +} else { + $publishedNames = @(Find-PublishedName -TargetOriginalName $OriginalName) } -if (-not $PublishedName) { +if ($publishedNames.Count -eq 0) { Write-Warning "No staged libvirtualhid driver package matching $OriginalName was found." Remove-DriverCertificate -Subject $RemoveCertificateSubject return } -$deleteArgs = @("/delete-driver", $PublishedName, "/uninstall") -if ($Force) { - $deleteArgs += "/force" -} +foreach ($driverPackage in $publishedNames) { + $deleteArgs = @("/delete-driver", $driverPackage, "/uninstall") + if ($Force) { + $deleteArgs += "/force" + } -if ($PSCmdlet.ShouldProcess($PublishedName, "Delete libvirtualhid driver package")) { - Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments $deleteArgs + if ($PSCmdlet.ShouldProcess($driverPackage, "Delete libvirtualhid driver package")) { + Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments $deleteArgs + } } Remove-DriverCertificate -Subject $RemoveCertificateSubject diff --git a/src/platform/windows/driver/CMakeLists.txt b/src/platform/windows/driver/CMakeLists.txt index 0e197c6..8a2daf1 100644 --- a/src/platform/windows/driver/CMakeLists.txt +++ b/src/platform/windows/driver/CMakeLists.txt @@ -53,6 +53,12 @@ set(_lvh_wdk_shared_include_candidates) set(_lvh_wdf_library_candidates) set(_lvh_wdk_um_library_candidates) set(_lvh_wdk_tool_candidates) +set(LIBVIRTUALHID_UMDF_LIBRARY_VERSION "2.15" CACHE STRING + "UMDF library version used for the Windows driver package") +option(LIBVIRTUALHID_WINDOWS_DRIVER_ENABLE_VHF + "Attach the Windows driver package to the inbox Virtual HID Framework lower filter" + ON) +string(REGEX REPLACE "\\." "\\\\." _lvh_umdf_library_version_regex "${LIBVIRTUALHID_UMDF_LIBRARY_VERSION}") foreach(lvh_wdk_root_cmake IN LISTS _lvh_wdk_roots) if(EXISTS "${lvh_wdk_root_cmake}") file(GLOB _lvh_wdf_include_glob @@ -83,12 +89,14 @@ endforeach() if(_lvh_wdf_include_candidates) list(SORT _lvh_wdf_include_candidates COMPARE NATURAL ORDER DESCENDING) + list(FILTER _lvh_wdf_include_candidates INCLUDE REGEX "/${_lvh_umdf_library_version_regex}$") endif() if(_lvh_wdk_shared_include_candidates) list(SORT _lvh_wdk_shared_include_candidates COMPARE NATURAL ORDER DESCENDING) endif() if(_lvh_wdf_library_candidates) list(SORT _lvh_wdf_library_candidates COMPARE NATURAL ORDER DESCENDING) + list(FILTER _lvh_wdf_library_candidates INCLUDE REGEX "/${_lvh_umdf_library_version_regex}$") endif() if(_lvh_wdk_um_library_candidates) list(SORT _lvh_wdk_um_library_candidates COMPARE NATURAL ORDER DESCENDING) @@ -141,6 +149,31 @@ message(STATUS "WDF UMDF stub library: ${LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRAR message(STATUS "VHF UMDF library: ${LIBVIRTUALHID_VHF_UM_LIBRARY}") message(STATUS "NTDLL import library: ${LIBVIRTUALHID_NTDLL_LIBRARY}") +get_filename_component(_lvh_wdf_include_version "${LIBVIRTUALHID_WDF_INCLUDE_DIR}" NAME) +get_filename_component(_lvh_wdf_stub_directory "${LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY}" DIRECTORY) +get_filename_component(_lvh_wdf_stub_version "${_lvh_wdf_stub_directory}" NAME) +if(NOT _lvh_wdf_include_version MATCHES "^2\\.[0-9]+$") + message(FATAL_ERROR + "Could not determine the UMDF library version from ${LIBVIRTUALHID_WDF_INCLUDE_DIR}.") +endif() +if(NOT _lvh_wdf_include_version STREQUAL LIBVIRTUALHID_UMDF_LIBRARY_VERSION) + message(FATAL_ERROR + "UMDF include version ${_lvh_wdf_include_version} does not match requested " + "version ${LIBVIRTUALHID_UMDF_LIBRARY_VERSION}.") +endif() +if(NOT _lvh_wdf_stub_version STREQUAL LIBVIRTUALHID_UMDF_LIBRARY_VERSION) + message(FATAL_ERROR + "UMDF stub library version ${_lvh_wdf_stub_version} does not match requested " + "version ${LIBVIRTUALHID_UMDF_LIBRARY_VERSION}.") +endif() +message(STATUS "UMDF library version: ${LIBVIRTUALHID_UMDF_LIBRARY_VERSION}") +if(LIBVIRTUALHID_WINDOWS_DRIVER_ENABLE_VHF) + set(LIBVIRTUALHID_DRIVER_VHF_ADDREG ",DeviceInstall_Vhf_AddReg") +else() + set(LIBVIRTUALHID_DRIVER_VHF_ADDREG "") +endif() +message(STATUS "Windows driver VHF lower filter: ${LIBVIRTUALHID_WINDOWS_DRIVER_ENABLE_VHF}") + find_program(LIBVIRTUALHID_STAMPINF NAMES stampinf stampinf.exe PATHS ${_lvh_wdk_tool_candidates}) @@ -186,6 +219,7 @@ target_link_libraries(libvirtualhid_umdf "${LIBVIRTUALHID_VHF_UM_LIBRARY}" "${LIBVIRTUALHID_NTDLL_LIBRARY}") set_target_properties(libvirtualhid_umdf PROPERTIES + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>" OUTPUT_NAME libvirtualhid_umdf RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/package") diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in index 11db515..c609865 100644 --- a/src/platform/windows/driver/libvirtualhid.inf.in +++ b/src/platform/windows/driver/libvirtualhid.inf.in @@ -1,11 +1,12 @@ ; ; libvirtualhid UMDF2 control driver package. +; This package includes UMDF startup file tracing for local driver bring-up. ; [Version] Signature="$WINDOWS NT$" -Class=HIDClass -ClassGuid={745A17A0-74D3-11D0-B6FE-00A0C90F57DA} +Class=System +ClassGuid={4D36E97D-E325-11CE-BFC1-08002BE10318} Provider=%ManufacturerName% DriverVer=*,@LIBVIRTUALHID_DRIVER_VERSION@ CatalogFile=libvirtualhid.cat @@ -28,36 +29,37 @@ libvirtualhid_umdf.dll=1 [DeviceInstall.NT] CopyFiles=UMDriverCopy +Include=wudfrd.inf +Needs=WUDFRD.NT [DeviceInstall.NT.HW] -AddReg=DeviceInstall_AddReg +AddReg=DeviceInstall_Device_AddReg@LIBVIRTUALHID_DRIVER_VHF_ADDREG@ +Include=wudfrd.inf +Needs=WUDFRD.NT.HW [UMDriverCopy] libvirtualhid_umdf.dll -[DeviceInstall_AddReg] -HKR,,"LowerFilters",0x00010000,"vhf" +[DeviceInstall_Device_AddReg] +HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GRGWGX;;;AU)" -[DeviceInstall.NT.Services] -AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall +[DeviceInstall_Vhf_AddReg] +HKR,,"LowerFilters",0x00010008,"vhf" +HKR,,VhfMode,0x00010001,0x1 -[WUDFRD_ServiceInstall] -DisplayName=%WudfRdDisplayName% -ServiceType=1 -StartType=3 -ErrorControl=1 -ServiceBinary=%12%\WUDFRd.sys +[DeviceInstall.NT.Services] +Include=wudfrd.inf +Needs=WUDFRD.NT.Services [DeviceInstall.NT.Wdf] UmdfService=libvirtualhid_umdf,libvirtualhid_umdf_Install UmdfServiceOrder=libvirtualhid_umdf [libvirtualhid_umdf_Install] -UmdfLibraryVersion=2.0 +UmdfLibraryVersion=@LIBVIRTUALHID_UMDF_LIBRARY_VERSION@ ServiceBinary=%13%\libvirtualhid_umdf.dll [Strings] ManufacturerName="LizardByte" DiskName="libvirtualhid UMDF Driver Install Disk" DeviceName="libvirtualhid Virtual HID Control Device" -WudfRdDisplayName="Windows Driver Foundation - User-mode Driver Framework Reflector" diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index 2381fe7..4143bce 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -31,7 +31,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -56,6 +58,7 @@ namespace { constexpr auto symbolic_link_name = L"\\DosDevices\\LibVirtualHid"; constexpr auto global_symbolic_link_name = L"\\DosDevices\\Global\\LibVirtualHid"; + constexpr auto trace_file_name = L"libvirtualhid-umdf-driver.log"; struct DeviceRecord { std::mutex mutex; @@ -67,6 +70,7 @@ namespace { struct DriverState { std::atomic next_driver_device_id {1}; + std::mutex vhf_target_mutex; WDFIOTARGET vhf_io_target {}; std::mutex devices_mutex; std::map> devices; @@ -80,6 +84,67 @@ namespace { return state; } + void trace_status(const char *step, NTSTATUS status = STATUS_SUCCESS) { + static std::atomic sequence {0}; + + wchar_t trace_file_path[MAX_PATH] {}; + constexpr auto trace_file_path_length = static_cast(MAX_PATH); + auto trace_path_size = GetTempPathW(trace_file_path_length, trace_file_path); + if (trace_path_size == 0U || trace_path_size >= trace_file_path_length) { + return; + } + + const auto file_name_size = wcslen(trace_file_name); + if (trace_path_size + file_name_size >= trace_file_path_length) { + return; + } + std::memcpy( + trace_file_path + trace_path_size, + trace_file_name, + (file_name_size + 1U) * sizeof(wchar_t) + ); + + const auto file = CreateFileW( + trace_file_path, + FILE_APPEND_DATA, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr + ); + if (file == INVALID_HANDLE_VALUE) { + return; + } + + SYSTEMTIME time {}; + GetSystemTime(&time); + + char line[320] {}; + const auto written_chars = std::snprintf( + line, + sizeof(line), + "%04hu-%02hu-%02huT%02hu:%02hu:%02hu.%03huZ [%lu] %s status=0x%08lX\r\n", + time.wYear, + time.wMonth, + time.wDay, + time.wHour, + time.wMinute, + time.wSecond, + time.wMilliseconds, + sequence.fetch_add(1U) + 1U, + step, + static_cast(status) + ); + if (written_chars > 0) { + DWORD bytes_written {}; + const auto bytes_to_write = static_cast(std::min(written_chars, sizeof(line) - 1U)); + static_cast(WriteFile(file, line, bytes_to_write, &bytes_written, nullptr)); + } + + static_cast(CloseHandle(file)); + } + bool valid_header(std::uint32_t version, std::uint32_t size, std::uint32_t expected_size) { return version == LVH_WINDOWS_CONTROL_PROTOCOL_VERSION && size == expected_size; } @@ -221,6 +286,7 @@ namespace { NTSTATUS initialize_vhf_target(WDFDEVICE device) { auto &state = driver_state(); + std::lock_guard lock {state.vhf_target_mutex}; if (state.vhf_io_target != nullptr) { return STATUS_SUCCESS; } @@ -259,10 +325,11 @@ namespace { } } - NTSTATUS create_vhf_device(const std::shared_ptr &record) { + NTSTATUS create_vhf_device(WDFDEVICE device, const std::shared_ptr &record) { auto &state = driver_state(); - if (state.vhf_io_target == nullptr) { - return STATUS_DEVICE_NOT_READY; + auto status = initialize_vhf_target(device); + if (!NT_SUCCESS(status)) { + return status; } const auto descriptor_size = record->request.report_sizes.report_descriptor_size; @@ -284,7 +351,7 @@ namespace { vhf_config.VersionNumber = record->request.hardware_ids.device_version; vhf_config.EvtVhfAsyncOperationWriteReport = LvhEvtVhfWriteReport; - auto status = VhfCreate(&vhf_config, &record->vhf_handle); + status = VhfCreate(&vhf_config, &record->vhf_handle); if (!NT_SUCCESS(status)) { record->vhf_handle = nullptr; return status; @@ -334,7 +401,7 @@ namespace { } } - void handle_create_gamepad_request(WDFREQUEST request) { + void handle_create_gamepad_request(WDFDEVICE device, WDFREQUEST request) { auto *create_request = static_cast(nullptr); auto status = retrieve_input_buffer(request, create_request); if (!NT_SUCCESS(status)) { @@ -365,7 +432,7 @@ namespace { record->driver_device_id = driver_device_id; record->request = *create_request; - status = create_vhf_device(record); + status = create_vhf_device(device, record); if (!NT_SUCCESS(status)) { create_response->status = LVH_WINDOWS_STATUS_BACKEND_FAILURE; complete_request(request, STATUS_SUCCESS, sizeof(*create_response)); @@ -473,15 +540,21 @@ namespace { } // namespace extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT driver_object, PUNICODE_STRING registry_path) { + trace_status("DriverEntry begin"); + WDF_DRIVER_CONFIG config; WDF_DRIVER_CONFIG_INIT(&config, LvhEvtDeviceAdd); - return WdfDriverCreate(driver_object, registry_path, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE); + const auto status = WdfDriverCreate(driver_object, registry_path, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE); + trace_status("DriverEntry WdfDriverCreate", status); + return status; } NTSTATUS LvhEvtDeviceAdd(WDFDRIVER driver, PWDFDEVICE_INIT device_init) { UNREFERENCED_PARAMETER(driver); + trace_status("EvtDeviceAdd begin"); + WDF_PNPPOWER_EVENT_CALLBACKS pnp_callbacks; WDF_PNPPOWER_EVENT_CALLBACKS_INIT(&pnp_callbacks); pnp_callbacks.EvtDevicePrepareHardware = LvhEvtDevicePrepareHardware; @@ -493,6 +566,7 @@ NTSTATUS LvhEvtDeviceAdd(WDFDRIVER driver, PWDFDEVICE_INIT device_init) { WDF_OBJECT_ATTRIBUTES_INIT(&device_attributes); device_attributes.EvtCleanupCallback = LvhEvtDeviceCleanup; auto status = WdfDeviceCreate(&device_init, &device_attributes, &device); + trace_status("EvtDeviceAdd WdfDeviceCreate", status); if (!NT_SUCCESS(status)) { return status; } @@ -500,18 +574,22 @@ NTSTATUS LvhEvtDeviceAdd(WDFDRIVER driver, PWDFDEVICE_INIT device_init) { UNICODE_STRING symbolic_link; RtlInitUnicodeString(&symbolic_link, global_symbolic_link_name); status = WdfDeviceCreateSymbolicLink(device, &symbolic_link); + trace_status("EvtDeviceAdd WdfDeviceCreateSymbolicLink global", status); if (!NT_SUCCESS(status)) { return status; } RtlInitUnicodeString(&symbolic_link, symbolic_link_name); - static_cast(WdfDeviceCreateSymbolicLink(device, &symbolic_link)); + status = WdfDeviceCreateSymbolicLink(device, &symbolic_link); + trace_status("EvtDeviceAdd WdfDeviceCreateSymbolicLink local", status); WDF_IO_QUEUE_CONFIG queue_config; WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queue_config, WdfIoQueueDispatchParallel); queue_config.EvtIoDeviceControl = LvhEvtIoDeviceControl; - return WdfIoQueueCreate(device, &queue_config, WDF_NO_OBJECT_ATTRIBUTES, WDF_NO_HANDLE); + status = WdfIoQueueCreate(device, &queue_config, WDF_NO_OBJECT_ATTRIBUTES, WDF_NO_HANDLE); + trace_status("EvtDeviceAdd WdfIoQueueCreate", status); + return status; } NTSTATUS LvhEvtDevicePrepareHardware( @@ -519,16 +597,23 @@ NTSTATUS LvhEvtDevicePrepareHardware( WDFCMRESLIST resources_raw, WDFCMRESLIST resources_translated ) { + UNREFERENCED_PARAMETER(device); UNREFERENCED_PARAMETER(resources_raw); UNREFERENCED_PARAMETER(resources_translated); - return initialize_vhf_target(device); + trace_status("EvtDevicePrepareHardware begin"); + + // The control device should still start if the local VHF target cannot be + // opened yet. Gamepad creation will initialize VHF lazily and report the + // backend failure through the IOCTL response if the target is unavailable. + return STATUS_SUCCESS; } NTSTATUS LvhEvtDeviceReleaseHardware(WDFDEVICE device, WDFCMRESLIST resources_translated) { UNREFERENCED_PARAMETER(device); UNREFERENCED_PARAMETER(resources_translated); + trace_status("EvtDeviceReleaseHardware begin"); complete_pending_output_requests(STATUS_CANCELLED); reset_vhf_target(true); return STATUS_SUCCESS; @@ -537,6 +622,7 @@ NTSTATUS LvhEvtDeviceReleaseHardware(WDFDEVICE device, WDFCMRESLIST resources_tr void LvhEvtDeviceCleanup(WDFOBJECT device_object) { UNREFERENCED_PARAMETER(device_object); + trace_status("EvtDeviceCleanup begin"); complete_pending_output_requests(STATUS_CANCELLED); reset_vhf_target(false); } @@ -586,13 +672,12 @@ void LvhEvtIoDeviceControl( size_t input_buffer_length, ULONG io_control_code ) { - UNREFERENCED_PARAMETER(queue); UNREFERENCED_PARAMETER(output_buffer_length); UNREFERENCED_PARAMETER(input_buffer_length); switch (io_control_code) { case LVH_WINDOWS_IOCTL_CREATE_GAMEPAD: - handle_create_gamepad_request(request); + handle_create_gamepad_request(WdfIoQueueGetDevice(queue), request); return; case LVH_WINDOWS_IOCTL_DESTROY_DEVICE: From 5bf98498d9b9fe96def0c5ad212d7b054af40707 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 18:34:27 -0400 Subject: [PATCH 02/27] Fix UMDF HID report ID handling Update the Windows UMDF backend to translate report IDs correctly between the client protocol and VHF: strip the leading report ID byte from input reports before submission, restore it on output events, and reject malformed input. Also treat stale global symbolic link collisions as non-fatal during rapid driver reinstalls, and document both behaviors in the README. --- README.md | 11 +++ .../windows/driver/libvirtualhid_umdf.cpp | 78 ++++++++++++++----- 2 files changed, 70 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 7b552bc..84af3f6 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,17 @@ callback path. DirectInput, SDL/HIDAPI, Windows.Gaming.Input/GameInput, and the browser Gamepad API should therefore see standard HID gamepads after the driver is installed. XInput is not a direct target for this HID-only backend because it does not emulate the Xbox proprietary bus/API. +The client protocol uses complete HID reports with the report ID at byte 0. The +UMDF driver strips that byte when submitting to VHF, where `HID_XFER_PACKET` +carries the report ID separately, and prepends it again before forwarding output +reports back to the C++ backend. VHF exposes VID/PID/version and the report +descriptor for the child HID device; browser tester labels can still fall back +to generic names when the browser does not assign a standard mapping for that +descriptor. +During rapid development reinstalls, the fixed global control symbolic link can +outlive the previous root device briefly; the driver treats that collision as +non-fatal so stale object-manager state does not leave the control device in +Code 31. Build the UMDF package separately with the Microsoft driver toolchain: diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index 4143bce..281d8d8 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -381,6 +381,52 @@ namespace { request.report_size <= LVH_WINDOWS_MAX_INPUT_REPORT_SIZE; } + bool symbolic_link_already_exists(NTSTATUS status) { + constexpr auto status_object_name_collision = static_cast(0xC0000035L); + constexpr auto hresult_object_already_exists = static_cast(0x800700B7UL); + constexpr auto ntstatus_hresult_object_already_exists = static_cast(0x900700B7UL); + return status == status_object_name_collision || status == hresult_object_already_exists || + status == ntstatus_hresult_object_already_exists; + } + + std::vector make_vhf_input_payload( + const DeviceRecord &record, + const LvhWindowsSubmitInputReportRequest &request + ) { + const auto report_id = record.request.hardware_ids.report_id; + const auto report_begin = request.report; + const auto report_end = request.report + request.report_size; + if (report_id == 0U) { + return {report_begin, report_end}; + } + + if (request.report_size <= 1U || request.report[0] != report_id) { + return {}; + } + + return {report_begin + 1U, report_end}; + } + + void copy_vhf_output_payload( + LvhWindowsOutputReportEvent &event, + const HID_XFER_PACKET &packet + ) { + const auto report_id = packet.reportId; + const auto report_id_size = report_id == 0U ? 0U : 1U; + const auto payload_capacity = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE - report_id_size; + const auto payload_size = std::min(packet.reportBufferLen, static_cast(payload_capacity)); + + if (report_id != 0U) { + event.report[0] = report_id; + } + + if (payload_size > 0U) { + std::memcpy(event.report + report_id_size, packet.reportBuffer, payload_size); + } + + event.report_size = static_cast(report_id_size + payload_size); + } + void set_device_path(std::uint64_t driver_device_id, char (&device_path)[LVH_WINDOWS_MAX_DEVICE_PATH_SIZE]) { constexpr auto path_prefix_size = sizeof(LVH_WINDOWS_CONTROL_DEVICE_PATH) - 1U; constexpr auto separator_size = 1U; @@ -495,21 +541,23 @@ namespace { return; } - std::vector report( - submit_request->report, - submit_request->report + submit_request->report_size - ); - HID_XFER_PACKET packet {}; - packet.reportBuffer = report.data(); - packet.reportBufferLen = static_cast(report.size()); - packet.reportId = report.empty() ? 0 : report.front(); - std::lock_guard lock {record->mutex}; if (record->vhf_handle == nullptr) { complete_request(request, STATUS_OBJECT_NAME_NOT_FOUND); return; } + auto report = make_vhf_input_payload(*record, *submit_request); + if (report.empty()) { + complete_request(request, STATUS_INVALID_PARAMETER); + return; + } + + HID_XFER_PACKET packet {}; + packet.reportBuffer = report.data(); + packet.reportBufferLen = static_cast(report.size()); + packet.reportId = record->request.hardware_ids.report_id; + complete_request(request, VhfReadReportSubmit(record->vhf_handle, &packet)); } @@ -575,7 +623,7 @@ NTSTATUS LvhEvtDeviceAdd(WDFDRIVER driver, PWDFDEVICE_INIT device_init) { RtlInitUnicodeString(&symbolic_link, global_symbolic_link_name); status = WdfDeviceCreateSymbolicLink(device, &symbolic_link); trace_status("EvtDeviceAdd WdfDeviceCreateSymbolicLink global", status); - if (!NT_SUCCESS(status)) { + if (!NT_SUCCESS(status) && !symbolic_link_already_exists(status)) { return status; } @@ -651,15 +699,7 @@ void LvhEvtVhfWriteReport( event.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; event.size = sizeof(event); event.driver_device_id = record->driver_device_id; - event.report_size = - std::min(hid_transfer_packet->reportBufferLen, static_cast(LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE)); - - if (event.report_size > 0U) { - std::memcpy(event.report, hid_transfer_packet->reportBuffer, event.report_size); - } else if (hid_transfer_packet->reportId != 0U) { - event.report_size = 1U; - event.report[0] = hid_transfer_packet->reportId; - } + copy_vhf_output_payload(event, *hid_transfer_packet); queue_output_event(event); static_cast(VhfAsyncOperationComplete(vhf_operation_handle, STATUS_SUCCESS)); From 6ff70690510051a5668270aea621ee2494e3be1a Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:03:18 -0400 Subject: [PATCH 03/27] Use 8-bit axes in HID gamepad profiles Convert gamepad stick axes from 16-bit signed to 8-bit unsigned in HID descriptors and reports. This reduces the common gamepad report size from 14 to 10 bytes and improves compatibility with browsers and DirectInput-style HID consumers that don't expect 16-bit stick fields to be split into byte-sized axes. Updates profiles, report packing logic, and corresponding unit tests. --- README.md | 3 +++ src/core/profiles.cpp | 11 +++++------ src/core/report.cpp | 14 +++++--------- tests/unit/test_profiles.cpp | 20 +++++++++++++++++++- tests/unit/test_report.cpp | 12 ++++++------ 5 files changed, 38 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 84af3f6..a4c8670 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,9 @@ reports back to the C++ backend. VHF exposes VID/PID/version and the report descriptor for the child HID device; browser tester labels can still fall back to generic names when the browser does not assign a standard mapping for that descriptor. +The built-in generic, Xbox-style, and Switch Pro-style HID profiles use 8-bit +stick axes in their common descriptor so browser and DirectInput-style generic +HID consumers do not split 16-bit stick fields into byte-sized axes. During rapid development reinstalls, the fixed global control symbolic link can outlive the previous root device briefly; the driver treats that collision as non-fatal so stale object-manager state does not leave the control device in diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index 25acd2b..20627a0 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -15,7 +15,7 @@ namespace lvh::profiles { namespace { - constexpr std::size_t common_report_size = 14; + constexpr std::size_t common_report_size = 10; constexpr std::size_t common_output_report_size = 5; @@ -94,14 +94,13 @@ namespace lvh::profiles { 0x01, // Report Count (1) 0x81, 0x03, // Input (Const,Var,Abs) - 0x16, - 0x00, - 0x80, // Logical Minimum (-32768) + 0x15, + 0x00, // Logical Minimum (0) 0x26, 0xFF, - 0x7F, // Logical Maximum (32767) + 0x00, // Logical Maximum (255) 0x75, - 0x10, // Report Size (16) + 0x08, // Report Size (8) 0x95, 0x04, // Report Count (4) 0x09, diff --git a/src/core/report.cpp b/src/core/report.cpp index d0406f6..70538c4 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -104,10 +104,6 @@ namespace lvh::reports { report.push_back(static_cast((value >> 8U) & 0xFFU)); } - void append_i16(std::vector &report, std::int16_t value) { - append_u16(report, static_cast(value)); - } - void write_u16(ByteReport &report, std::size_t offset, std::uint16_t value) { report[offset] = to_low_byte(value); report[offset + 1U] = to_low_byte(value >> 8U); @@ -706,7 +702,7 @@ namespace lvh::reports { } } - constexpr std::size_t common_report_size = 14; + constexpr std::size_t common_report_size = 10; if (profile.device_type != DeviceType::gamepad || profile.input_report_size < common_report_size) { return {}; } @@ -718,10 +714,10 @@ namespace lvh::reports { report.push_back(profile.report_id); append_u16(report, report_button_bits(normalized.buttons)); report.push_back(hat_from_buttons(normalized.buttons)); - append_i16(report, normalize_axis(normalized.left_stick.x)); - append_i16(report, normalize_axis(normalized.left_stick.y)); - append_i16(report, normalize_axis(normalized.right_stick.x)); - append_i16(report, normalize_axis(normalized.right_stick.y)); + report.push_back(normalize_u8_axis(normalized.left_stick.x)); + report.push_back(normalize_u8_axis(normalized.left_stick.y)); + report.push_back(normalize_u8_axis(normalized.right_stick.x)); + report.push_back(normalize_u8_axis(normalized.right_stick.y)); report.push_back(normalize_trigger(normalized.left_trigger)); report.push_back(normalize_trigger(normalized.right_trigger)); diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index 8a35d52..e73107d 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -3,6 +3,10 @@ * @brief Unit tests for built-in gamepad profiles. */ +// standard includes +#include +#include + // local includes #include "fixtures/fixtures.hpp" @@ -18,7 +22,7 @@ TEST(ProfileTest, BuiltInProfilesHaveDescriptors) { EXPECT_NE(profile.vendor_id, 0); EXPECT_NE(profile.product_id, 0); EXPECT_NE(profile.report_id, 0); - EXPECT_GE(profile.input_report_size, 14U); + EXPECT_GE(profile.input_report_size, 10U); EXPECT_FALSE(profile.report_descriptor.empty()); } } @@ -32,6 +36,20 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(xbox_one.vendor_id, 0x045E); EXPECT_EQ(xbox_one.product_id, 0x02EA); EXPECT_TRUE(xbox_one.capabilities.supports_rumble); + EXPECT_EQ(xbox_one.input_report_size, 10U); + + const std::array byte_axis_descriptor { + 0x15, + 0x00, + 0x26, + 0xFF, + 0x00, + 0x75, + 0x08, + 0x95, + 0x04, + }; + EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, byte_axis_descriptor).begin() != xbox_one.report_descriptor.end()); EXPECT_EQ(dualshock4.vendor_id, 0x054C); EXPECT_EQ(dualshock4.product_id, 0x05C4); diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index 671126d..f96e471 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -87,12 +87,12 @@ TEST(ReportTest, PacksCommonGamepadReport) { EXPECT_EQ(report[1], 0x21); // A + Start EXPECT_EQ(report[2], 0x00); EXPECT_EQ(report[3], 6); // D-pad left - EXPECT_EQ(report[4], 0xFF); - EXPECT_EQ(report[5], 0x7F); - EXPECT_EQ(report[6], 0x00); - EXPECT_EQ(report[7], 0x80); - EXPECT_EQ(report[12], 64); - EXPECT_EQ(report[13], 255); + EXPECT_EQ(report[4], 255); // Left stick X + EXPECT_EQ(report[5], 0); // Left stick Y + EXPECT_EQ(report[6], 191); // Right stick X + EXPECT_EQ(report[7], 64); // Right stick Y + EXPECT_EQ(report[8], 64); // Left trigger + EXPECT_EQ(report[9], 255); // Right trigger } TEST(ReportTest, PacksDualSenseUsbReport) { From 6b0b53aadd22e992a10df1f4a86220b80db6a50f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:15:19 -0400 Subject: [PATCH 04/27] Fix HID descriptor axes and negate Y-axis values Update gamepad HID descriptor to use standard stick axes (X/Y/Z/Rz) and Slider usage for triggers instead of Rx/Ry. Negate Y-axis values for both sticks to match HID conventions. Adds test validation for new descriptor layout and updates expected report values. --- README.md | 5 +++-- src/core/profiles.cpp | 8 ++++---- src/core/report.cpp | 4 ++-- tests/unit/test_profiles.cpp | 20 ++++++++++++++++++++ tests/unit/test_report.cpp | 4 ++-- 5 files changed, 31 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a4c8670..c6320d0 100644 --- a/README.md +++ b/README.md @@ -126,8 +126,9 @@ descriptor for the child HID device; browser tester labels can still fall back to generic names when the browser does not assign a standard mapping for that descriptor. The built-in generic, Xbox-style, and Switch Pro-style HID profiles use 8-bit -stick axes in their common descriptor so browser and DirectInput-style generic -HID consumers do not split 16-bit stick fields into byte-sized axes. +`X`/`Y` and `Z`/`Rz` stick axes followed by trigger sliders in their common +descriptor so browser and DirectInput-style generic HID consumers do not split +stick fields into byte-sized axes or map trigger axes into the right stick. During rapid development reinstalls, the fixed global control symbolic link can outlive the previous root device briefly; the driver treats that collision as non-fatal so stale object-manager state does not leave the control device in diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index 20627a0..6000a15 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -108,9 +108,9 @@ namespace lvh::profiles { 0x09, 0x31, // Usage (Y) 0x09, - 0x33, // Usage (Rx) + 0x32, // Usage (Z) 0x09, - 0x34, // Usage (Ry) + 0x35, // Usage (Rz) 0x81, 0x02, // Input (Data,Var,Abs) 0x15, @@ -123,9 +123,9 @@ namespace lvh::profiles { 0x95, 0x02, // Report Count (2) 0x09, - 0x32, // Usage (Z) + 0x36, // Usage (Slider) 0x09, - 0x35, // Usage (Rz) + 0x36, // Usage (Slider) 0x81, 0x02, // Input (Data,Var,Abs) }; diff --git a/src/core/report.cpp b/src/core/report.cpp index 70538c4..977f9f8 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -715,9 +715,9 @@ namespace lvh::reports { append_u16(report, report_button_bits(normalized.buttons)); report.push_back(hat_from_buttons(normalized.buttons)); report.push_back(normalize_u8_axis(normalized.left_stick.x)); - report.push_back(normalize_u8_axis(normalized.left_stick.y)); + report.push_back(normalize_u8_axis(-normalized.left_stick.y)); report.push_back(normalize_u8_axis(normalized.right_stick.x)); - report.push_back(normalize_u8_axis(normalized.right_stick.y)); + report.push_back(normalize_u8_axis(-normalized.right_stick.y)); report.push_back(normalize_trigger(normalized.left_trigger)); report.push_back(normalize_trigger(normalized.right_trigger)); diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index e73107d..e917edb 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -51,6 +51,26 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { }; EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, byte_axis_descriptor).begin() != xbox_one.report_descriptor.end()); + const std::array stick_usage_descriptor { + 0x09, + 0x30, + 0x09, + 0x31, + 0x09, + 0x32, + 0x09, + 0x35, + }; + EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, stick_usage_descriptor).begin() != xbox_one.report_descriptor.end()); + + const std::array trigger_slider_descriptor { + 0x09, + 0x36, + 0x09, + 0x36, + }; + EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, trigger_slider_descriptor).begin() != xbox_one.report_descriptor.end()); + EXPECT_EQ(dualshock4.vendor_id, 0x054C); EXPECT_EQ(dualshock4.product_id, 0x05C4); EXPECT_EQ(dualshock4.input_report_size, 64U); diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index f96e471..26f245b 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -88,9 +88,9 @@ TEST(ReportTest, PacksCommonGamepadReport) { EXPECT_EQ(report[2], 0x00); EXPECT_EQ(report[3], 6); // D-pad left EXPECT_EQ(report[4], 255); // Left stick X - EXPECT_EQ(report[5], 0); // Left stick Y + EXPECT_EQ(report[5], 255); // Left stick Y EXPECT_EQ(report[6], 191); // Right stick X - EXPECT_EQ(report[7], 64); // Right stick Y + EXPECT_EQ(report[7], 191); // Right stick Y EXPECT_EQ(report[8], 64); // Left trigger EXPECT_EQ(report[9], 255); // Right trigger } From 572a0a250ed1ad558b3a616f9cfa12092ad9aa59 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:43:30 -0400 Subject: [PATCH 05/27] Align gamepad HID identity and report shape Reworked the common gamepad descriptor/report format to a 23-byte input report (report ID + 18 ordered 8-bit button/trigger fields + 4 stick axes), replacing the prior bitfield/hat/slider layout so generic HID consumers map controls more reliably. Updated report packing and unit tests to match the new ordering and semantics. Also made built-in profile manufacturer strings explicit per device family (e.g., Microsoft, Nintendo) and updated the Windows UMDF backend to publish explicit HID hardware IDs (VID/PID/REV) through VHF so hosts can identify selected profiles instead of a generic VHF device. README coverage was updated accordingly. --- README.md | 17 ++-- src/core/profiles.cpp | 77 +++++-------------- src/core/report.cpp | 59 +++++++------- .../windows/driver/libvirtualhid_umdf.cpp | 37 +++++++++ tests/unit/test_profiles.cpp | 41 +++++++--- tests/unit/test_report.cpp | 25 +++--- 6 files changed, 139 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index c6320d0..cbfa5c9 100644 --- a/README.md +++ b/README.md @@ -121,14 +121,15 @@ does not emulate the Xbox proprietary bus/API. The client protocol uses complete HID reports with the report ID at byte 0. The UMDF driver strips that byte when submitting to VHF, where `HID_XFER_PACKET` carries the report ID separately, and prepends it again before forwarding output -reports back to the C++ backend. VHF exposes VID/PID/version and the report -descriptor for the child HID device; browser tester labels can still fall back -to generic names when the browser does not assign a standard mapping for that -descriptor. -The built-in generic, Xbox-style, and Switch Pro-style HID profiles use 8-bit -`X`/`Y` and `Z`/`Rz` stick axes followed by trigger sliders in their common -descriptor so browser and DirectInput-style generic HID consumers do not split -stick fields into byte-sized axes or map trigger axes into the right stick. +reports back to the C++ backend. VHF exposes VID/PID/version, explicit +`HID\VID_....&PID_....` hardware IDs, and the report descriptor for the child +HID device so Windows and browser consumers can identify the selected profile +instead of a generic VHF-only device. +The built-in generic, Xbox-style, and Switch Pro-style HID profiles use a +standard-gamepad-shaped common descriptor: ordered 8-bit button values first, +including analog trigger values and d-pad buttons, followed by four 8-bit stick +axes. This avoids browser and DirectInput-style generic HID consumers treating +trigger axes as the right stick or leaving the device unmapped. During rapid development reinstalls, the fixed global control symbolic link can outlive the previous root device briefly; the driver treats that collision as non-fatal so stale object-manager state does not leave the control device in diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index 6000a15..4a623d5 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -15,7 +15,11 @@ namespace lvh::profiles { namespace { - constexpr std::size_t common_report_size = 10; + constexpr std::uint8_t common_button_count = 18; + + constexpr std::uint8_t common_axis_count = 4; + + constexpr std::size_t common_report_size = 1 + common_button_count + common_axis_count; constexpr std::size_t common_output_report_size = 5; @@ -50,50 +54,20 @@ namespace lvh::profiles { 0x19, 0x01, // Usage Minimum (Button 1) 0x29, - 0x0C, // Usage Maximum (Button 12) + common_button_count, // Usage Maximum 0x15, 0x00, // Logical Minimum (0) - 0x25, - 0x01, // Logical Maximum (1) + 0x26, + 0xFF, + 0x00, // Logical Maximum (255) 0x75, - 0x01, // Report Size (1) + 0x08, // Report Size (8) 0x95, - 0x0C, // Report Count (12) + common_button_count, // Report Count 0x81, 0x02, // Input (Data,Var,Abs) - 0x75, - 0x01, // Report Size (1) - 0x95, - 0x04, // Report Count (4) - 0x81, - 0x03, // Input (Const,Var,Abs) 0x05, 0x01, // Usage Page (Generic Desktop) - 0x09, - 0x39, // Usage (Hat switch) - 0x15, - 0x00, // Logical Minimum (0) - 0x25, - 0x07, // Logical Maximum (7) - 0x35, - 0x00, // Physical Minimum (0) - 0x46, - 0x3B, - 0x01, // Physical Maximum (315) - 0x65, - 0x14, // Unit (Degrees) - 0x75, - 0x04, // Report Size (4) - 0x95, - 0x01, // Report Count (1) - 0x81, - 0x42, // Input (Data,Var,Abs,Null) - 0x75, - 0x04, // Report Size (4) - 0x95, - 0x01, // Report Count (1) - 0x81, - 0x03, // Input (Const,Var,Abs) 0x15, 0x00, // Logical Minimum (0) 0x26, @@ -102,30 +76,15 @@ namespace lvh::profiles { 0x75, 0x08, // Report Size (8) 0x95, - 0x04, // Report Count (4) + common_axis_count, // Report Count 0x09, 0x30, // Usage (X) 0x09, 0x31, // Usage (Y) 0x09, - 0x32, // Usage (Z) - 0x09, - 0x35, // Usage (Rz) - 0x81, - 0x02, // Input (Data,Var,Abs) - 0x15, - 0x00, // Logical Minimum (0) - 0x26, - 0xFF, - 0x00, // Logical Maximum (255) - 0x75, - 0x08, // Report Size (8) - 0x95, - 0x02, // Report Count (2) - 0x09, - 0x36, // Usage (Slider) + 0x33, // Usage (Rx) 0x09, - 0x36, // Usage (Slider) + 0x34, // Usage (Ry) 0x81, 0x02, // Input (Data,Var,Abs) }; @@ -1598,6 +1557,7 @@ namespace lvh::profiles { DeviceProfile make_gamepad_profile( GamepadProfileKind kind, std::string name, + std::string manufacturer, std::uint16_t vendor_id, std::uint16_t product_id, std::uint16_t version, @@ -1616,7 +1576,7 @@ namespace lvh::profiles { profile.output_report_size = common_output_report_size; } profile.name = std::move(name); - profile.manufacturer = "LizardByte"; + profile.manufacturer = std::move(manufacturer); profile.capabilities = capabilities; profile.report_descriptor = make_gamepad_report_descriptor(profile.report_id, profile.capabilities.supports_rumble); return profile; @@ -1695,6 +1655,7 @@ namespace lvh::profiles { return make_gamepad_profile( GamepadProfileKind::generic, "libvirtualhid Generic Gamepad", + "LizardByte", 0x1209, 0x0001, 0x0001, @@ -1706,6 +1667,7 @@ namespace lvh::profiles { return make_gamepad_profile( GamepadProfileKind::xbox_360, "Microsoft X-Box 360 pad", + "Microsoft", 0x045E, 0x028E, 0x0114, @@ -1717,6 +1679,7 @@ namespace lvh::profiles { return make_gamepad_profile( GamepadProfileKind::xbox_one, "Xbox One Controller", + "Microsoft", 0x045E, 0x02EA, 0x0408, @@ -1728,6 +1691,7 @@ namespace lvh::profiles { return make_gamepad_profile( GamepadProfileKind::xbox_series, "Xbox Wireless Controller", + "Microsoft", 0x045E, 0x0B12, 0x0500, @@ -1765,6 +1729,7 @@ namespace lvh::profiles { return make_gamepad_profile( GamepadProfileKind::switch_pro, "Nintendo Switch Pro Controller", + "Nintendo Co., Ltd.", 0x057E, 0x2009, 0x8111, diff --git a/src/core/report.cpp b/src/core/report.cpp index 977f9f8..179e485 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -99,11 +99,6 @@ namespace lvh::reports { return bytes; } - void append_u16(std::vector &report, std::uint16_t value) { - report.push_back(static_cast(value & 0xFFU)); - report.push_back(static_cast((value >> 8U) & 0xFFU)); - } - void write_u16(ByteReport &report, std::size_t offset, std::uint16_t value) { report[offset] = to_low_byte(value); report[offset + 1U] = to_low_byte(value >> 8U); @@ -172,29 +167,32 @@ namespace lvh::reports { return static_cast(std::lround((static_cast(value) / 255.0F) * 65535.0F)); } - std::uint16_t report_button_bits(const ButtonSet &buttons) { - std::uint16_t bits = 0; - - const auto add = [&bits, &buttons](GamepadButton button, std::uint16_t bit) { - if (buttons.test(button)) { - bits |= bit; - } - }; - - add(GamepadButton::a, 1U << 0U); - add(GamepadButton::b, 1U << 1U); - add(GamepadButton::x, 1U << 2U); - add(GamepadButton::y, 1U << 3U); - add(GamepadButton::back, 1U << 4U); - add(GamepadButton::start, 1U << 5U); - add(GamepadButton::guide, 1U << 6U); - add(GamepadButton::left_stick, 1U << 7U); - add(GamepadButton::right_stick, 1U << 8U); - add(GamepadButton::left_shoulder, 1U << 9U); - add(GamepadButton::right_shoulder, 1U << 10U); - add(GamepadButton::misc1, 1U << 11U); + std::uint8_t digital_button_value(const ButtonSet &buttons, GamepadButton button) { + return buttons.test(button) ? 255U : 0U; + } - return bits; + void append_common_button_values( + std::vector &report, + const GamepadState &state + ) { + report.push_back(digital_button_value(state.buttons, GamepadButton::a)); + report.push_back(digital_button_value(state.buttons, GamepadButton::b)); + report.push_back(digital_button_value(state.buttons, GamepadButton::x)); + report.push_back(digital_button_value(state.buttons, GamepadButton::y)); + report.push_back(digital_button_value(state.buttons, GamepadButton::left_shoulder)); + report.push_back(digital_button_value(state.buttons, GamepadButton::right_shoulder)); + report.push_back(normalize_trigger(state.left_trigger)); + report.push_back(normalize_trigger(state.right_trigger)); + report.push_back(digital_button_value(state.buttons, GamepadButton::back)); + report.push_back(digital_button_value(state.buttons, GamepadButton::start)); + report.push_back(digital_button_value(state.buttons, GamepadButton::left_stick)); + report.push_back(digital_button_value(state.buttons, GamepadButton::right_stick)); + report.push_back(digital_button_value(state.buttons, GamepadButton::dpad_up)); + report.push_back(digital_button_value(state.buttons, GamepadButton::dpad_down)); + report.push_back(digital_button_value(state.buttons, GamepadButton::dpad_left)); + report.push_back(digital_button_value(state.buttons, GamepadButton::dpad_right)); + report.push_back(digital_button_value(state.buttons, GamepadButton::guide)); + report.push_back(digital_button_value(state.buttons, GamepadButton::misc1)); } std::byte dualsense_battery_state(GamepadBatteryState state) { @@ -702,7 +700,7 @@ namespace lvh::reports { } } - constexpr std::size_t common_report_size = 10; + constexpr std::size_t common_report_size = 23; if (profile.device_type != DeviceType::gamepad || profile.input_report_size < common_report_size) { return {}; } @@ -712,14 +710,11 @@ namespace lvh::reports { std::vector report; report.reserve(common_report_size); report.push_back(profile.report_id); - append_u16(report, report_button_bits(normalized.buttons)); - report.push_back(hat_from_buttons(normalized.buttons)); + append_common_button_values(report, normalized); report.push_back(normalize_u8_axis(normalized.left_stick.x)); report.push_back(normalize_u8_axis(-normalized.left_stick.y)); report.push_back(normalize_u8_axis(normalized.right_stick.x)); report.push_back(normalize_u8_axis(-normalized.right_stick.y)); - report.push_back(normalize_trigger(normalized.left_trigger)); - report.push_back(normalize_trigger(normalized.right_trigger)); report.resize(profile.input_report_size, 0); return report; diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index 281d8d8..ede184e 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include // local includes @@ -66,6 +67,7 @@ namespace { LvhWindowsCreateGamepadRequest request {}; VHFHANDLE vhf_handle {}; std::vector report_descriptor; + std::wstring hardware_ids; }; struct DriverState { @@ -325,6 +327,38 @@ namespace { } } + wchar_t hex_digit(unsigned value) { + constexpr wchar_t digits[] = L"0123456789ABCDEF"; + return digits[value & 0x0FU]; + } + + void append_hex4(std::wstring &text, std::uint16_t value) { + text.push_back(hex_digit(value >> 12U)); + text.push_back(hex_digit(value >> 8U)); + text.push_back(hex_digit(value >> 4U)); + text.push_back(hex_digit(value)); + } + + void append_hid_vid_pid(std::wstring &hardware_ids, const LvhWindowsGamepadHardwareIds &ids) { + hardware_ids.append(L"HID\\VID_"); + append_hex4(hardware_ids, ids.vendor_id); + hardware_ids.append(L"&PID_"); + append_hex4(hardware_ids, ids.product_id); + } + + std::wstring make_hardware_ids(const LvhWindowsGamepadHardwareIds &ids) { + std::wstring hardware_ids; + append_hid_vid_pid(hardware_ids, ids); + hardware_ids.append(L"&REV_"); + append_hex4(hardware_ids, ids.device_version); + hardware_ids.push_back(L'\0'); + + append_hid_vid_pid(hardware_ids, ids); + hardware_ids.push_back(L'\0'); + hardware_ids.push_back(L'\0'); + return hardware_ids; + } + NTSTATUS create_vhf_device(WDFDEVICE device, const std::shared_ptr &record) { auto &state = driver_state(); auto status = initialize_vhf_target(device); @@ -337,6 +371,7 @@ namespace { record->request.report_descriptor, record->request.report_descriptor + descriptor_size ); + record->hardware_ids = make_hardware_ids(record->request.hardware_ids); VHF_CONFIG vhf_config; VHF_CONFIG_INIT( @@ -349,6 +384,8 @@ namespace { vhf_config.VendorID = record->request.hardware_ids.vendor_id; vhf_config.ProductID = record->request.hardware_ids.product_id; vhf_config.VersionNumber = record->request.hardware_ids.device_version; + vhf_config.HardwareIDsLength = static_cast(record->hardware_ids.size() * sizeof(wchar_t)); + vhf_config.HardwareIDs = record->hardware_ids.data(); vhf_config.EvtVhfAsyncOperationWriteReport = LvhEvtVhfWriteReport; status = VhfCreate(&vhf_config, &record->vhf_handle); diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index e917edb..7e44c53 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -22,7 +22,7 @@ TEST(ProfileTest, BuiltInProfilesHaveDescriptors) { EXPECT_NE(profile.vendor_id, 0); EXPECT_NE(profile.product_id, 0); EXPECT_NE(profile.report_id, 0); - EXPECT_GE(profile.input_report_size, 10U); + EXPECT_GE(profile.input_report_size, 23U); EXPECT_FALSE(profile.report_descriptor.empty()); } } @@ -35,8 +35,32 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(xbox_one.vendor_id, 0x045E); EXPECT_EQ(xbox_one.product_id, 0x02EA); + EXPECT_EQ(xbox_one.manufacturer, "Microsoft"); EXPECT_TRUE(xbox_one.capabilities.supports_rumble); - EXPECT_EQ(xbox_one.input_report_size, 10U); + EXPECT_EQ(xbox_one.input_report_size, 23U); + + const auto xbox_series = lvh::profiles::xbox_series(); + EXPECT_EQ(xbox_series.vendor_id, 0x045E); + EXPECT_EQ(xbox_series.product_id, 0x0B12); + EXPECT_EQ(xbox_series.name, "Xbox Wireless Controller"); + EXPECT_EQ(xbox_series.manufacturer, "Microsoft"); + + const std::array standard_button_descriptor { + 0x19, + 0x01, + 0x29, + 0x12, + 0x15, + 0x00, + 0x26, + 0xFF, + 0x00, + 0x75, + 0x08, + 0x95, + 0x12, + }; + EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, standard_button_descriptor).begin() != xbox_one.report_descriptor.end()); const std::array byte_axis_descriptor { 0x15, @@ -57,20 +81,12 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { 0x09, 0x31, 0x09, - 0x32, + 0x33, 0x09, - 0x35, + 0x34, }; EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, stick_usage_descriptor).begin() != xbox_one.report_descriptor.end()); - const std::array trigger_slider_descriptor { - 0x09, - 0x36, - 0x09, - 0x36, - }; - EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, trigger_slider_descriptor).begin() != xbox_one.report_descriptor.end()); - EXPECT_EQ(dualshock4.vendor_id, 0x054C); EXPECT_EQ(dualshock4.product_id, 0x05C4); EXPECT_EQ(dualshock4.input_report_size, 64U); @@ -107,6 +123,7 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(switch_pro.vendor_id, 0x057E); EXPECT_EQ(switch_pro.product_id, 0x2009); + EXPECT_EQ(switch_pro.manufacturer, "Nintendo Co., Ltd."); } TEST(ProfileTest, RumbleProfilesExposeOutputReports) { diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index 26f245b..fad59af 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -75,6 +75,8 @@ TEST(ReportTest, PacksCommonGamepadReport) { state.buttons.set(lvh::GamepadButton::a); state.buttons.set(lvh::GamepadButton::start); state.buttons.set(lvh::GamepadButton::dpad_left); + state.buttons.set(lvh::GamepadButton::guide); + state.buttons.set(lvh::GamepadButton::misc1); state.left_stick = {1.0F, -1.0F}; state.right_stick = {0.5F, -0.5F}; state.left_trigger = 0.25F; @@ -84,15 +86,20 @@ TEST(ReportTest, PacksCommonGamepadReport) { ASSERT_EQ(report.size(), profile.input_report_size); EXPECT_EQ(report[0], profile.report_id); - EXPECT_EQ(report[1], 0x21); // A + Start - EXPECT_EQ(report[2], 0x00); - EXPECT_EQ(report[3], 6); // D-pad left - EXPECT_EQ(report[4], 255); // Left stick X - EXPECT_EQ(report[5], 255); // Left stick Y - EXPECT_EQ(report[6], 191); // Right stick X - EXPECT_EQ(report[7], 191); // Right stick Y - EXPECT_EQ(report[8], 64); // Left trigger - EXPECT_EQ(report[9], 255); // Right trigger + EXPECT_EQ(report[1], 255); // A + EXPECT_EQ(report[2], 0); // B + EXPECT_EQ(report[6], 0); // Right shoulder + EXPECT_EQ(report[7], 64); // Left trigger + EXPECT_EQ(report[8], 255); // Right trigger + EXPECT_EQ(report[9], 0); // Back + EXPECT_EQ(report[10], 255); // Start + EXPECT_EQ(report[15], 255); // D-pad left + EXPECT_EQ(report[17], 255); // Guide + EXPECT_EQ(report[18], 255); // Misc/share + EXPECT_EQ(report[19], 255); // Left stick X + EXPECT_EQ(report[20], 255); // Left stick Y + EXPECT_EQ(report[21], 191); // Right stick X + EXPECT_EQ(report[22], 191); // Right stick Y } TEST(ReportTest, PacksDualSenseUsbReport) { From 4445dfb51e4675b6e0521fa6dc931f2030652f30 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:08:03 -0400 Subject: [PATCH 06/27] Redesign Windows driver to use HIDClass Switch the libvirtualhid Windows driver from System class to HIDClass for proper device classification. Explicitly register the WUDFRd reflector service in the INF instead of using Include/Needs directives. Remove dynamic VhfMode registry manipulation and instead clean up legacy devices with stale VhfMode settings or System class during installation. Add install-time validation to verify driver package files exist and the catalog is not stale. Update documentation to reflect the architectural changes and simplify the INF configuration. --- README.md | 15 ++++---- cmake/packaging/windows_wix.cmake | 1 + scripts/windows/install-driver.ps1 | 38 ++++++++----------- src/platform/windows/driver/CMakeLists.txt | 25 +++++++----- .../windows/driver/libvirtualhid.inf.in | 30 +++++++-------- 5 files changed, 52 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index cbfa5c9..2b3d328 100644 --- a/README.md +++ b/README.md @@ -163,20 +163,19 @@ not require the WDK tools on the target machine. Existing devices are detected by matching the `ROOT\LIBVIRTUALHID` hardware ID. The SetupAPI path creates a root-enumerated instance such as `ROOT\LIBVIRTUALHID\####`. The install and uninstall helpers also clean up malformed development devices -left by earlier installer revisions. The WiX installer writes the helper -transcript to `C:\ProgramData\libvirtualhid\install-driver.log`. +left by earlier installer revisions, including root instances left in the +failed legacy `System` class or carrying stale `VhfMode` registry state. The WiX +installer writes the helper transcript to +`C:\ProgramData\libvirtualhid\install-driver.log`. The driver binary is a UMDF DLL installed through the Windows Driver Store, not a libvirtualhid `.sys` copied into `C:\Windows\System32\drivers`. Windows still uses its built-in `WUDFRd.sys` and VHF components under `System32\drivers`; the libvirtualhid-specific sign that installation completed is the `ROOT\LIBVIRTUALHID` device and the `\\.\LibVirtualHid` control device. The INF -includes the built-in `WUDFRd` install sections for the root `System` control -device, appends the VHF lower filter, sets `VhfMode=1` for the UMDF VHF source -stack, and leaves UMDF dispatcher policy at the framework default to match the -inbox VHF source-driver shape. The installer also writes `VhfMode=1` onto the -root device before starting the driver so root-enumerated development installs -get the same VHF source mode as the INF hardware section. The UMDF control +installs the root control device in the HIDClass class, registers the built-in +`WUDFRd` reflector service explicitly, and attaches the inbox VHF lower filter +so the UMDF driver can create VHF child HID devices on demand. The UMDF control device starts without opening VHF; gamepad creation opens VHF lazily so target-open failures are reported through the create-device response instead of making `\\.\LibVirtualHid` unavailable. The generated INF uses the same UMDF diff --git a/cmake/packaging/windows_wix.cmake b/cmake/packaging/windows_wix.cmake index 4a9a59b..ef82546 100644 --- a/cmake/packaging/windows_wix.cmake +++ b/cmake/packaging/windows_wix.cmake @@ -9,6 +9,7 @@ if(NOT DOTNET_EXECUTABLE) endif() set(CPACK_WIX_VERSION 4) +set(CPACK_GENERATOR "WIX") set(WIX_VERSION 4.0.4) set(WIX_UI_VERSION 4.0.4) # extension versioning is independent of the WiX version set(WIX_BUILD_PARENT_DIRECTORY "${CMAKE_BINARY_DIR}/wix_packaging") diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index ce44d7d..5a1f668 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -300,10 +300,22 @@ function Get-RegistryRootDevice { $hasTargetInstanceId = $instanceId -like "$TargetHardwareId\*" if ($hasExactHardwareId -or $hasCorruptHardwareId -or $hasTargetInstanceId) { + $classGuid = $null + $hasStaleVhfMode = $false + try { + $deviceProperties = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction Stop + $classGuid = $deviceProperties.ClassGUID + $hasStaleVhfMode = $null -ne $deviceProperties.PSObject.Properties["VhfMode"] + } catch { + Write-Verbose "Unable to read registry properties for $instanceId`: $($_.Exception.Message)" + } + [pscustomobject]@{ InstanceId = $instanceId HasExactHardwareId = $hasExactHardwareId HasCorruptHardwareId = $hasCorruptHardwareId + HasLegacySystemClass = $classGuid -ieq "{4d36e97d-e325-11ce-bfc1-08002be10318}" + HasStaleVhfMode = $hasStaleVhfMode } } } @@ -319,22 +331,6 @@ function Remove-DeviceInstance { } } -function Set-RootDeviceVhfMode { - [CmdletBinding(SupportsShouldProcess)] - param([string] $InstanceId) - - $deviceRegistryPath = "HKLM:\SYSTEM\CurrentControlSet\Enum\$InstanceId" - if (-not (Test-Path -LiteralPath $deviceRegistryPath)) { - Write-Verbose "Unable to set VhfMode because $deviceRegistryPath does not exist." - return - } - - if ($PSCmdlet.ShouldProcess($InstanceId, "Set VhfMode=1 for UMDF VHF source device")) { - New-ItemProperty -LiteralPath $deviceRegistryPath -Name "VhfMode" -Value 1 -PropertyType DWord -Force | Out-Null - Write-Information "Set VhfMode=1 on $InstanceId." -InformationAction Continue - } -} - function Update-RootDeviceDriverWithSetupApi { [CmdletBinding(SupportsShouldProcess)] param( @@ -387,16 +383,15 @@ try { } $registryRootDevices = @(Get-RegistryRootDevice -TargetHardwareId $HardwareId) - foreach ($device in ($registryRootDevices | Where-Object { $_.HasCorruptHardwareId })) { + foreach ($device in ($registryRootDevices | Where-Object { + $_.HasCorruptHardwareId -or $_.HasLegacySystemClass -or $_.HasStaleVhfMode + })) { Remove-DeviceInstance -InstanceId $device.InstanceId } $rootDevices = @(Get-RootDeviceInstanceId -TargetHardwareId $HardwareId) if ($rootDevices.Count -gt 0) { Write-Information "Updating the existing $HardwareId device driver." -InformationAction Continue - foreach ($rootDevice in $rootDevices) { - Set-RootDeviceVhfMode -InstanceId $rootDevice - } Update-RootDeviceDriverWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId return } @@ -406,9 +401,6 @@ try { } $rootDevices = @(Get-RootDeviceInstanceId -TargetHardwareId $HardwareId) - foreach ($rootDevice in $rootDevices) { - Set-RootDeviceVhfMode -InstanceId $rootDevice - } Update-RootDeviceDriverWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId } finally { Stop-LibVirtualHidTranscript diff --git a/src/platform/windows/driver/CMakeLists.txt b/src/platform/windows/driver/CMakeLists.txt index 8a2daf1..dbe8cd0 100644 --- a/src/platform/windows/driver/CMakeLists.txt +++ b/src/platform/windows/driver/CMakeLists.txt @@ -55,9 +55,6 @@ set(_lvh_wdk_um_library_candidates) set(_lvh_wdk_tool_candidates) set(LIBVIRTUALHID_UMDF_LIBRARY_VERSION "2.15" CACHE STRING "UMDF library version used for the Windows driver package") -option(LIBVIRTUALHID_WINDOWS_DRIVER_ENABLE_VHF - "Attach the Windows driver package to the inbox Virtual HID Framework lower filter" - ON) string(REGEX REPLACE "\\." "\\\\." _lvh_umdf_library_version_regex "${LIBVIRTUALHID_UMDF_LIBRARY_VERSION}") foreach(lvh_wdk_root_cmake IN LISTS _lvh_wdk_roots) if(EXISTS "${lvh_wdk_root_cmake}") @@ -167,13 +164,6 @@ if(NOT _lvh_wdf_stub_version STREQUAL LIBVIRTUALHID_UMDF_LIBRARY_VERSION) "version ${LIBVIRTUALHID_UMDF_LIBRARY_VERSION}.") endif() message(STATUS "UMDF library version: ${LIBVIRTUALHID_UMDF_LIBRARY_VERSION}") -if(LIBVIRTUALHID_WINDOWS_DRIVER_ENABLE_VHF) - set(LIBVIRTUALHID_DRIVER_VHF_ADDREG ",DeviceInstall_Vhf_AddReg") -else() - set(LIBVIRTUALHID_DRIVER_VHF_ADDREG "") -endif() -message(STATUS "Windows driver VHF lower filter: ${LIBVIRTUALHID_WINDOWS_DRIVER_ENABLE_VHF}") - find_program(LIBVIRTUALHID_STAMPINF NAMES stampinf stampinf.exe PATHS ${_lvh_wdk_tool_candidates}) @@ -252,6 +242,21 @@ add_custom_target(libvirtualhid_windows_catalog install(TARGETS libvirtualhid_umdf RUNTIME DESTINATION "drivers/windows" COMPONENT driver) +install(CODE + " + set(lvh_driver_dll \"$\") + set(lvh_driver_inf \"$/libvirtualhid.inf\") + set(lvh_driver_cat \"$/libvirtualhid.cat\") + foreach(lvh_driver_file IN ITEMS \"\${lvh_driver_dll}\" \"\${lvh_driver_inf}\" \"\${lvh_driver_cat}\") + 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_driver_inf}\" IS_NEWER_THAN \"\${lvh_driver_cat}\") + message(FATAL_ERROR \"Windows driver catalog is stale; build libvirtualhid_windows_catalog and sign libvirtualhid.cat before packaging.\") + endif() + " + COMPONENT driver) install(FILES "$/libvirtualhid.inf" "$/libvirtualhid.cat" diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in index c609865..d0718fa 100644 --- a/src/platform/windows/driver/libvirtualhid.inf.in +++ b/src/platform/windows/driver/libvirtualhid.inf.in @@ -1,12 +1,11 @@ ; ; libvirtualhid UMDF2 control driver package. -; This package includes UMDF startup file tracing for local driver bring-up. ; [Version] Signature="$WINDOWS NT$" -Class=System -ClassGuid={4D36E97D-E325-11CE-BFC1-08002BE10318} +Class=HIDClass +ClassGuid={745A17A0-74D3-11D0-B6FE-00A0C90F57DA} Provider=%ManufacturerName% DriverVer=*,@LIBVIRTUALHID_DRIVER_VERSION@ CatalogFile=libvirtualhid.cat @@ -29,27 +28,25 @@ libvirtualhid_umdf.dll=1 [DeviceInstall.NT] CopyFiles=UMDriverCopy -Include=wudfrd.inf -Needs=WUDFRD.NT [DeviceInstall.NT.HW] -AddReg=DeviceInstall_Device_AddReg@LIBVIRTUALHID_DRIVER_VHF_ADDREG@ -Include=wudfrd.inf -Needs=WUDFRD.NT.HW +AddReg=DeviceInstall_AddReg [UMDriverCopy] libvirtualhid_umdf.dll -[DeviceInstall_Device_AddReg] -HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GRGWGX;;;AU)" - -[DeviceInstall_Vhf_AddReg] -HKR,,"LowerFilters",0x00010008,"vhf" -HKR,,VhfMode,0x00010001,0x1 +[DeviceInstall_AddReg] +HKR,,"LowerFilters",0x00010000,"vhf" [DeviceInstall.NT.Services] -Include=wudfrd.inf -Needs=WUDFRD.NT.Services +AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall + +[WUDFRD_ServiceInstall] +DisplayName=%WudfRdDisplayName% +ServiceType=1 +StartType=3 +ErrorControl=1 +ServiceBinary=%12%\WUDFRd.sys [DeviceInstall.NT.Wdf] UmdfService=libvirtualhid_umdf,libvirtualhid_umdf_Install @@ -63,3 +60,4 @@ ServiceBinary=%13%\libvirtualhid_umdf.dll ManufacturerName="LizardByte" DiskName="libvirtualhid UMDF Driver Install Disk" DeviceName="libvirtualhid Virtual HID Control Device" +WudfRdDisplayName="Windows Driver Foundation - User-mode Driver Framework Reflector" From c4b35006b33f5297b767c77cda53f73d710be64c Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:17:05 -0400 Subject: [PATCH 07/27] Configure UMDF driver for VHF source mode Enable VHF source mode by setting VhfMode=1 in both the INF file and PowerShell installer. Changed root device class from HIDClass to System, refactored INF to include standard WUDFRD sections instead of manual service installation, and added registry configuration for both new and existing device installations. Added build-time check to prevent Debug configuration packaging. --- README.md | 14 ++++---- scripts/windows/install-driver.ps1 | 32 +++++++++++++++---- src/platform/windows/driver/CMakeLists.txt | 8 +++++ .../windows/driver/libvirtualhid.inf.in | 30 +++++++++-------- 4 files changed, 57 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 2b3d328..7a20887 100644 --- a/README.md +++ b/README.md @@ -164,18 +164,20 @@ by matching the `ROOT\LIBVIRTUALHID` hardware ID. The SetupAPI path creates a root-enumerated instance such as `ROOT\LIBVIRTUALHID\####`. The install and uninstall helpers also clean up malformed development devices left by earlier installer revisions, including root instances left in the -failed legacy `System` class or carrying stale `VhfMode` registry state. The WiX -installer writes the helper transcript to -`C:\ProgramData\libvirtualhid\install-driver.log`. +failed `HIDClass` package shape. The WiX installer writes the helper transcript +to `C:\ProgramData\libvirtualhid\install-driver.log`. The driver binary is a UMDF DLL installed through the Windows Driver Store, not a libvirtualhid `.sys` copied into `C:\Windows\System32\drivers`. Windows still uses its built-in `WUDFRd.sys` and VHF components under `System32\drivers`; the libvirtualhid-specific sign that installation completed is the `ROOT\LIBVIRTUALHID` device and the `\\.\LibVirtualHid` control device. The INF -installs the root control device in the HIDClass class, registers the built-in -`WUDFRd` reflector service explicitly, and attaches the inbox VHF lower filter -so the UMDF driver can create VHF child HID devices on demand. The UMDF control +includes the built-in `WUDFRd` install sections for the root `System` control +device, appends the VHF lower filter, sets `VhfMode=1` for the UMDF VHF source +stack, and leaves UMDF dispatcher policy at the framework default to match the +inbox VHF source-driver shape. The installer also writes `VhfMode=1` onto the +root device before starting the driver so root-enumerated development installs +get the same VHF source mode as the INF hardware section. The UMDF control device starts without opening VHF; gamepad creation opens VHF lazily so target-open failures are reported through the create-device response instead of making `\\.\LibVirtualHid` unavailable. The generated INF uses the same UMDF diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index 5a1f668..b9993e8 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -301,11 +301,10 @@ function Get-RegistryRootDevice { if ($hasExactHardwareId -or $hasCorruptHardwareId -or $hasTargetInstanceId) { $classGuid = $null - $hasStaleVhfMode = $false + $classGuid = $null try { $deviceProperties = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction Stop $classGuid = $deviceProperties.ClassGUID - $hasStaleVhfMode = $null -ne $deviceProperties.PSObject.Properties["VhfMode"] } catch { Write-Verbose "Unable to read registry properties for $instanceId`: $($_.Exception.Message)" } @@ -314,8 +313,7 @@ function Get-RegistryRootDevice { InstanceId = $instanceId HasExactHardwareId = $hasExactHardwareId HasCorruptHardwareId = $hasCorruptHardwareId - HasLegacySystemClass = $classGuid -ieq "{4d36e97d-e325-11ce-bfc1-08002be10318}" - HasStaleVhfMode = $hasStaleVhfMode + HasLegacyHidClass = $classGuid -ieq "{745a17a0-74d3-11d0-b6fe-00a0c90f57da}" } } } @@ -331,6 +329,22 @@ function Remove-DeviceInstance { } } +function Set-RootDeviceVhfMode { + [CmdletBinding(SupportsShouldProcess)] + param([string] $InstanceId) + + $deviceRegistryPath = "HKLM:\SYSTEM\CurrentControlSet\Enum\$InstanceId" + if (-not (Test-Path -LiteralPath $deviceRegistryPath)) { + Write-Verbose "Unable to set VhfMode because $deviceRegistryPath does not exist." + return + } + + if ($PSCmdlet.ShouldProcess($InstanceId, "Set VhfMode=1 for UMDF VHF source device")) { + New-ItemProperty -LiteralPath $deviceRegistryPath -Name "VhfMode" -Value 1 -PropertyType DWord -Force | Out-Null + Write-Information "Set VhfMode=1 on $InstanceId." -InformationAction Continue + } +} + function Update-RootDeviceDriverWithSetupApi { [CmdletBinding(SupportsShouldProcess)] param( @@ -383,15 +397,16 @@ try { } $registryRootDevices = @(Get-RegistryRootDevice -TargetHardwareId $HardwareId) - foreach ($device in ($registryRootDevices | Where-Object { - $_.HasCorruptHardwareId -or $_.HasLegacySystemClass -or $_.HasStaleVhfMode - })) { + foreach ($device in ($registryRootDevices | Where-Object { $_.HasCorruptHardwareId -or $_.HasLegacyHidClass })) { Remove-DeviceInstance -InstanceId $device.InstanceId } $rootDevices = @(Get-RootDeviceInstanceId -TargetHardwareId $HardwareId) if ($rootDevices.Count -gt 0) { Write-Information "Updating the existing $HardwareId device driver." -InformationAction Continue + foreach ($rootDevice in $rootDevices) { + Set-RootDeviceVhfMode -InstanceId $rootDevice + } Update-RootDeviceDriverWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId return } @@ -401,6 +416,9 @@ try { } $rootDevices = @(Get-RootDeviceInstanceId -TargetHardwareId $HardwareId) + foreach ($rootDevice in $rootDevices) { + Set-RootDeviceVhfMode -InstanceId $rootDevice + } Update-RootDeviceDriverWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId } finally { Stop-LibVirtualHidTranscript diff --git a/src/platform/windows/driver/CMakeLists.txt b/src/platform/windows/driver/CMakeLists.txt index dbe8cd0..0a4abc5 100644 --- a/src/platform/windows/driver/CMakeLists.txt +++ b/src/platform/windows/driver/CMakeLists.txt @@ -6,6 +6,14 @@ if(NOT MSVC) message(FATAL_ERROR "The libvirtualhid Windows driver package requires the Microsoft WDK/MSVC toolchain.") endif() +if(LIBVIRTUALHID_ENABLE_PACKAGING + AND NOT CMAKE_CONFIGURATION_TYPES + AND CMAKE_BUILD_TYPE STREQUAL "Debug") + message(FATAL_ERROR + "The libvirtualhid Windows driver package must not be built from a Debug configuration. " + "Configure with -DCMAKE_BUILD_TYPE=Release before packaging the UMDF driver.") +endif() + set(LIBVIRTUALHID_WDK_ARCH "${CMAKE_VS_PLATFORM_NAME}") if(NOT LIBVIRTUALHID_WDK_ARCH) if(CMAKE_SIZEOF_VOID_P EQUAL 8) diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in index d0718fa..212e239 100644 --- a/src/platform/windows/driver/libvirtualhid.inf.in +++ b/src/platform/windows/driver/libvirtualhid.inf.in @@ -1,11 +1,12 @@ ; ; libvirtualhid UMDF2 control driver package. +; This package includes UMDF startup file tracing for local driver bring-up. ; [Version] Signature="$WINDOWS NT$" -Class=HIDClass -ClassGuid={745A17A0-74D3-11D0-B6FE-00A0C90F57DA} +Class=System +ClassGuid={4D36E97D-E325-11CE-BFC1-08002BE10318} Provider=%ManufacturerName% DriverVer=*,@LIBVIRTUALHID_DRIVER_VERSION@ CatalogFile=libvirtualhid.cat @@ -28,25 +29,27 @@ libvirtualhid_umdf.dll=1 [DeviceInstall.NT] CopyFiles=UMDriverCopy +Include=wudfrd.inf +Needs=WUDFRD.NT [DeviceInstall.NT.HW] -AddReg=DeviceInstall_AddReg +AddReg=DeviceInstall_Device_AddReg,DeviceInstall_Vhf_AddReg +Include=wudfrd.inf +Needs=WUDFRD.NT.HW [UMDriverCopy] libvirtualhid_umdf.dll -[DeviceInstall_AddReg] -HKR,,"LowerFilters",0x00010000,"vhf" +[DeviceInstall_Device_AddReg] +HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GRGWGX;;;AU)" -[DeviceInstall.NT.Services] -AddService=WUDFRd,0x000001fa,WUDFRD_ServiceInstall +[DeviceInstall_Vhf_AddReg] +HKR,,"LowerFilters",0x00010008,"vhf" +HKR,,VhfMode,0x00010001,0x1 -[WUDFRD_ServiceInstall] -DisplayName=%WudfRdDisplayName% -ServiceType=1 -StartType=3 -ErrorControl=1 -ServiceBinary=%12%\WUDFRd.sys +[DeviceInstall.NT.Services] +Include=wudfrd.inf +Needs=WUDFRD.NT.Services [DeviceInstall.NT.Wdf] UmdfService=libvirtualhid_umdf,libvirtualhid_umdf_Install @@ -60,4 +63,3 @@ ServiceBinary=%13%\libvirtualhid_umdf.dll ManufacturerName="LizardByte" DiskName="libvirtualhid UMDF Driver Install Disk" DeviceName="libvirtualhid Virtual HID Control Device" -WudfRdDisplayName="Windows Driver Foundation - User-mode Driver Framework Reflector" From f4b7ede290a39f725052a44402984bb2df74286a Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:54:25 -0400 Subject: [PATCH 08/27] Refactor gamepad HID descriptor and add PnP device discovery Optimize HID descriptor format by packing buttons as 16 one-bit fields followed by 6 eight-bit axes (X, Y, Rx, Ry, Z, Rz) for better DirectInput and browser compatibility. This reduces the common report size from 23 to 9 bytes. Add Windows PnP device interface discovery with fallback to legacy device paths for robust control device enumeration. Implement automatic VHF gamepad cleanup on process exit via file cleanup callbacks. Update the UMDF driver to track file object ownership and clean up virtual gamepads when their owning file handles close. Enhance the gamepad adapter example with command-line argument parsing and interactive demo mode for testing sustained input. Link setupapi library for device enumeration on Windows. --- README.md | 32 ++++--- examples/gamepad_adapter.cpp | 91 ++++++++++++++++++- src/CMakeLists.txt | 3 + src/core/profiles.cpp | 19 ++-- src/core/report.cpp | 62 +++++++------ .../windows/driver/libvirtualhid_umdf.cpp | 83 ++++++++++++++--- src/platform/windows/windows_backend.cpp | 80 +++++++++++++++- tests/unit/test_profiles.cpp | 27 +++--- tests/unit/test_report.cpp | 22 ++--- tests/unit/test_windows_backend.cpp | 6 +- 10 files changed, 328 insertions(+), 97 deletions(-) diff --git a/README.md b/README.md index 7a20887..425142e 100644 --- a/README.md +++ b/README.md @@ -100,14 +100,16 @@ or similar control channel over passing C++ STL types across that boundary. The current Windows backend selects a UMDF control-channel implementation for `BackendKind::platform_default`. It always exposes keyboard and mouse through -Win32 `SendInput`, then probes `\\.\LibVirtualHid` for descriptor-driven virtual -gamepads. It reports `requires_installed_driver = true`, and only advertises -gamepad/output-report support when the driver package is installed and the -control device can be opened. Touchscreen, trackpad, and pen tablet support are -not implemented in the Windows backend yet. The client library stays buildable -with MSVC and MinGW/UCRT64 because the gamepad path talks to the driver through -fixed-size C protocol structures and Win32 `DeviceIoControl` calls. The default -control device path can be overridden for diagnostics with +Win32 `SendInput`, then probes the libvirtualhid control device interface for +descriptor-driven virtual gamepads. It falls back to the legacy fixed +`\\.\LibVirtualHid` and `\\.\Global\LibVirtualHid` links for diagnostics and +older driver builds. It reports `requires_installed_driver = true`, and only +advertises gamepad/output-report support when the driver package is installed +and the control device can be opened. Touchscreen, trackpad, and pen tablet +support are not implemented in the Windows backend yet. The client library +stays buildable with MSVC and MinGW/UCRT64 because the gamepad path talks to the +driver through fixed-size C protocol structures and Win32 `DeviceIoControl` +calls. The default control device path can be overridden for diagnostics with `LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE`. The UMDF driver uses Windows Virtual HID Framework (VHF) for OS-visible gamepad @@ -126,14 +128,18 @@ reports back to the C++ backend. VHF exposes VID/PID/version, explicit HID device so Windows and browser consumers can identify the selected profile instead of a generic VHF-only device. The built-in generic, Xbox-style, and Switch Pro-style HID profiles use a -standard-gamepad-shaped common descriptor: ordered 8-bit button values first, -including analog trigger values and d-pad buttons, followed by four 8-bit stick -axes. This avoids browser and DirectInput-style generic HID consumers treating -trigger axes as the right stick or leaving the device unmapped. +standard-gamepad-shaped common descriptor: 16 one-bit digital buttons followed +by 8-bit `X`, `Y`, `Rx`, `Ry`, `Z`, and `Rz` values for sticks and analog +triggers. Keeping the buttons as real HID button caps is required for +DirectInput-style and browser consumers to enumerate the VHF child as a gamepad. +The UMDF driver also owns each VHF child by the control-file handle that created +it, so process exits or crashes clean up any virtual gamepads that were not +explicitly destroyed. During rapid development reinstalls, the fixed global control symbolic link can outlive the previous root device briefly; the driver treats that collision as non-fatal so stale object-manager state does not leave the control device in -Code 31. +Code 31. Normal clients discover the PnP control device interface first, so a +stale fixed link does not block the backend from reaching the current device. Build the UMDF package separately with the Microsoft driver toolchain: diff --git a/examples/gamepad_adapter.cpp b/examples/gamepad_adapter.cpp index 92a9d1a..7941afd 100644 --- a/examples/gamepad_adapter.cpp +++ b/examples/gamepad_adapter.cpp @@ -4,19 +4,93 @@ */ // standard includes +#include #include +#include +#include +#include +#include // local includes #include -int main() { +namespace { + using namespace std::chrono_literals; + + std::optional profile_for_name(std::string_view name) { + if (name == "generic") { + return lvh::profiles::generic_gamepad(); + } + if (name == "x360") { + return lvh::profiles::xbox_360(); + } + if (name == "xone") { + return lvh::profiles::xbox_one(); + } + if (name == "xseries") { + return lvh::profiles::xbox_series(); + } + if (name == "ds4") { + return lvh::profiles::dualshock4(); + } + if (name == "ds5") { + return lvh::profiles::dualsense(); + } + if (name == "switch") { + return lvh::profiles::switch_pro(); + } + + return std::nullopt; + } + + lvh::ClientControllerType client_type_for_profile(lvh::GamepadProfileKind kind) { + switch (kind) { + case lvh::GamepadProfileKind::xbox_360: + case lvh::GamepadProfileKind::xbox_one: + case lvh::GamepadProfileKind::xbox_series: + return lvh::ClientControllerType::xbox; + case lvh::GamepadProfileKind::dualshock4: + case lvh::GamepadProfileKind::dualsense: + return lvh::ClientControllerType::playstation; + case lvh::GamepadProfileKind::switch_pro: + return lvh::ClientControllerType::nintendo; + case lvh::GamepadProfileKind::generic: + return lvh::ClientControllerType::unknown; + } + + return lvh::ClientControllerType::unknown; + } +} // namespace + +int main(int argc, char *argv[]) { + auto profile_name = std::string_view {"ds5"}; + auto hold = false; + auto hold_seconds = 60; + for (auto index = 1; index < argc; ++index) { + const auto argument = std::string_view {argv[index]}; + if (argument == "--hold") { + hold = true; + } else if (argument == "--hold-seconds" && index + 1 < argc) { + hold = true; + hold_seconds = std::stoi(argv[++index]); + } else { + profile_name = argument; + } + } + + auto profile = profile_for_name(profile_name); + if (!profile) { + std::cerr << "Unknown profile: " << profile_name << '\n'; + return 1; + } + auto runtime = lvh::Runtime::create(); lvh::CreateGamepadOptions options; - options.profile = lvh::profiles::dualsense(); + options.profile = *profile; options.metadata.global_index = 0; options.metadata.client_relative_index = 0; - options.metadata.client_type = lvh::ClientControllerType::playstation; + options.metadata.client_type = client_type_for_profile(profile->gamepad_kind); options.metadata.has_motion_sensors = true; options.metadata.has_touchpad = true; options.metadata.has_rgb_led = true; @@ -44,6 +118,17 @@ int main() { adapter.set_left_stick({0.25F, -0.5F}); adapter.set_right_trigger(1.0F); + if (hold) { + std::cout << "Holding " << profile->name << " for " << hold_seconds << " seconds\n"; + for (auto step = 0; step < hold_seconds * 5; ++step) { + const auto direction = step % 40 < 20 ? 1.0F : -1.0F; + adapter.set_left_stick({direction, 0.0F}); + adapter.set_right_stick({0.0F, -direction}); + adapter.set_button(lvh::GamepadButton::a, step % 20 < 10); + std::this_thread::sleep_for(200ms); + } + } + lvh::GamepadOutput rumble; rumble.kind = lvh::GamepadOutputKind::rumble; rumble.low_frequency_rumble = 0x4000; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 242541d..acccf7f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -58,6 +58,9 @@ elseif(WIN32) NOMINMAX WIN32_LEAN_AND_MEAN _WIN32_WINNT=0x0600) + target_link_libraries(${PROJECT_NAME} + PRIVATE + setupapi) else() target_sources(${PROJECT_NAME} PRIVATE diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index 4a623d5..8747480 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -15,11 +15,13 @@ namespace lvh::profiles { namespace { - constexpr std::uint8_t common_button_count = 18; + constexpr std::uint8_t common_button_count = 16; - constexpr std::uint8_t common_axis_count = 4; + constexpr std::uint8_t common_axis_count = 6; - constexpr std::size_t common_report_size = 1 + common_button_count + common_axis_count; + constexpr std::size_t common_button_bytes = 2; + + constexpr std::size_t common_report_size = 1 + common_button_bytes + common_axis_count; constexpr std::size_t common_output_report_size = 5; @@ -57,11 +59,10 @@ namespace lvh::profiles { common_button_count, // Usage Maximum 0x15, 0x00, // Logical Minimum (0) - 0x26, - 0xFF, - 0x00, // Logical Maximum (255) + 0x25, + 0x01, // Logical Maximum (1) 0x75, - 0x08, // Report Size (8) + 0x01, // Report Size (1) 0x95, common_button_count, // Report Count 0x81, @@ -85,6 +86,10 @@ namespace lvh::profiles { 0x33, // Usage (Rx) 0x09, 0x34, // Usage (Ry) + 0x09, + 0x32, // Usage (Z) + 0x09, + 0x35, // Usage (Rz) 0x81, 0x02, // Input (Data,Var,Abs) }; diff --git a/src/core/report.cpp b/src/core/report.cpp index 179e485..909067a 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -121,6 +121,11 @@ namespace lvh::reports { return static_cast(low | static_cast(high << 8U)); } + void append_u16(std::vector &report, std::uint16_t value) { + report.push_back(static_cast(value & 0xFFU)); + report.push_back(static_cast((value >> 8U) & 0xFFU)); + } + std::uint32_t read_u32(const ByteReport &report, std::size_t offset) { return std::to_integer(report[offset]) | (std::to_integer(report[offset + 1U]) << 8U) | @@ -167,32 +172,31 @@ namespace lvh::reports { return static_cast(std::lround((static_cast(value) / 255.0F) * 65535.0F)); } - std::uint8_t digital_button_value(const ButtonSet &buttons, GamepadButton button) { - return buttons.test(button) ? 255U : 0U; - } - - void append_common_button_values( - std::vector &report, - const GamepadState &state - ) { - report.push_back(digital_button_value(state.buttons, GamepadButton::a)); - report.push_back(digital_button_value(state.buttons, GamepadButton::b)); - report.push_back(digital_button_value(state.buttons, GamepadButton::x)); - report.push_back(digital_button_value(state.buttons, GamepadButton::y)); - report.push_back(digital_button_value(state.buttons, GamepadButton::left_shoulder)); - report.push_back(digital_button_value(state.buttons, GamepadButton::right_shoulder)); - report.push_back(normalize_trigger(state.left_trigger)); - report.push_back(normalize_trigger(state.right_trigger)); - report.push_back(digital_button_value(state.buttons, GamepadButton::back)); - report.push_back(digital_button_value(state.buttons, GamepadButton::start)); - report.push_back(digital_button_value(state.buttons, GamepadButton::left_stick)); - report.push_back(digital_button_value(state.buttons, GamepadButton::right_stick)); - report.push_back(digital_button_value(state.buttons, GamepadButton::dpad_up)); - report.push_back(digital_button_value(state.buttons, GamepadButton::dpad_down)); - report.push_back(digital_button_value(state.buttons, GamepadButton::dpad_left)); - report.push_back(digital_button_value(state.buttons, GamepadButton::dpad_right)); - report.push_back(digital_button_value(state.buttons, GamepadButton::guide)); - report.push_back(digital_button_value(state.buttons, GamepadButton::misc1)); + std::uint16_t common_button_bits(const ButtonSet &buttons) { + auto bits = std::uint16_t {}; + const auto set_bit = [&buttons, &bits](std::uint16_t bit, GamepadButton button) { + if (buttons.test(button)) { + bits |= static_cast(1U << bit); + } + }; + + set_bit(0U, GamepadButton::a); + set_bit(1U, GamepadButton::b); + set_bit(2U, GamepadButton::x); + set_bit(3U, GamepadButton::y); + set_bit(4U, GamepadButton::left_shoulder); + set_bit(5U, GamepadButton::right_shoulder); + set_bit(6U, GamepadButton::back); + set_bit(7U, GamepadButton::start); + set_bit(8U, GamepadButton::left_stick); + set_bit(9U, GamepadButton::right_stick); + set_bit(10U, GamepadButton::guide); + set_bit(11U, GamepadButton::misc1); + set_bit(12U, GamepadButton::dpad_up); + set_bit(13U, GamepadButton::dpad_down); + set_bit(14U, GamepadButton::dpad_left); + set_bit(15U, GamepadButton::dpad_right); + return bits; } std::byte dualsense_battery_state(GamepadBatteryState state) { @@ -700,7 +704,7 @@ namespace lvh::reports { } } - constexpr std::size_t common_report_size = 23; + constexpr std::size_t common_report_size = 9; if (profile.device_type != DeviceType::gamepad || profile.input_report_size < common_report_size) { return {}; } @@ -710,11 +714,13 @@ namespace lvh::reports { std::vector report; report.reserve(common_report_size); report.push_back(profile.report_id); - append_common_button_values(report, normalized); + append_u16(report, common_button_bits(normalized.buttons)); report.push_back(normalize_u8_axis(normalized.left_stick.x)); report.push_back(normalize_u8_axis(-normalized.left_stick.y)); report.push_back(normalize_u8_axis(normalized.right_stick.x)); report.push_back(normalize_u8_axis(-normalized.right_stick.y)); + report.push_back(normalize_trigger(normalized.left_trigger)); + report.push_back(normalize_trigger(normalized.right_trigger)); report.resize(profile.input_report_size, 0); return report; diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index ede184e..d08045f 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -50,6 +50,7 @@ extern "C" DRIVER_INITIALIZE DriverEntry; EVT_WDF_DRIVER_DEVICE_ADD LvhEvtDeviceAdd; EVT_WDF_DEVICE_PREPARE_HARDWARE LvhEvtDevicePrepareHardware; EVT_WDF_DEVICE_RELEASE_HARDWARE LvhEvtDeviceReleaseHardware; +EVT_WDF_FILE_CLEANUP LvhEvtFileCleanup; EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL LvhEvtIoDeviceControl; EVT_WDF_OBJECT_CONTEXT_CLEANUP LvhEvtDeviceCleanup; EVT_WDF_REQUEST_CANCEL LvhEvtOutputReadCanceled; @@ -64,6 +65,7 @@ namespace { struct DeviceRecord { std::mutex mutex; std::uint64_t driver_device_id {}; + WDFFILEOBJECT owner_file {}; LvhWindowsCreateGamepadRequest request {}; VHFHANDLE vhf_handle {}; std::vector report_descriptor; @@ -286,6 +288,30 @@ namespace { } } + void delete_vhf_devices_for_file(WDFFILEOBJECT file_object) { + if (file_object == nullptr) { + return; + } + + std::vector> devices; + { + auto &state = driver_state(); + std::lock_guard lock {state.devices_mutex}; + for (auto iter = state.devices.begin(); iter != state.devices.end();) { + if (iter->second->owner_file == file_object) { + devices.push_back(iter->second); + iter = state.devices.erase(iter); + } else { + ++iter; + } + } + } + + for (const auto &record : devices) { + delete_vhf_device(record); + } + } + NTSTATUS initialize_vhf_target(WDFDEVICE device) { auto &state = driver_state(); std::lock_guard lock {state.vhf_target_mutex}; @@ -419,11 +445,28 @@ namespace { } bool symbolic_link_already_exists(NTSTATUS status) { - constexpr auto status_object_name_collision = static_cast(0xC0000035L); - constexpr auto hresult_object_already_exists = static_cast(0x800700B7UL); - constexpr auto ntstatus_hresult_object_already_exists = static_cast(0x900700B7UL); - return status == status_object_name_collision || status == hresult_object_already_exists || - status == ntstatus_hresult_object_already_exists; + const auto value = static_cast(status); + return value == 0xC0000035U || value == 0x800700B7U || value == 0x900700B7U; + } + + constexpr GUID control_device_interface_guid { + 0x3890af65, + 0x2da0, + 0x443c, + {0x84, 0xff, 0x6e, 0x70, 0xe8, 0x41, 0xba, 0x1e} + }; + + NTSTATUS create_control_symbolic_link(WDFDEVICE device, const wchar_t *link_name, const char *trace_step) { + UNICODE_STRING symbolic_link; + RtlInitUnicodeString(&symbolic_link, link_name); + + const auto status = WdfDeviceCreateSymbolicLink(device, &symbolic_link); + trace_status(trace_step, status); + if (NT_SUCCESS(status) || symbolic_link_already_exists(status)) { + return STATUS_SUCCESS; + } + + return status; } std::vector make_vhf_input_payload( @@ -513,6 +556,7 @@ namespace { const auto driver_device_id = state.next_driver_device_id.fetch_add(1); auto record = std::make_shared(); record->driver_device_id = driver_device_id; + record->owner_file = WdfRequestGetFileObject(request); record->request = *create_request; status = create_vhf_device(device, record); @@ -646,6 +690,10 @@ NTSTATUS LvhEvtDeviceAdd(WDFDRIVER driver, PWDFDEVICE_INIT device_init) { pnp_callbacks.EvtDeviceReleaseHardware = LvhEvtDeviceReleaseHardware; WdfDeviceInitSetPnpPowerEventCallbacks(device_init, &pnp_callbacks); + WDF_FILEOBJECT_CONFIG file_config; + WDF_FILEOBJECT_CONFIG_INIT(&file_config, WDF_NO_EVENT_CALLBACK, WDF_NO_EVENT_CALLBACK, LvhEvtFileCleanup); + WdfDeviceInitSetFileObjectConfig(device_init, &file_config, WDF_NO_OBJECT_ATTRIBUTES); + WDFDEVICE device = nullptr; WDF_OBJECT_ATTRIBUTES device_attributes; WDF_OBJECT_ATTRIBUTES_INIT(&device_attributes); @@ -656,17 +704,21 @@ NTSTATUS LvhEvtDeviceAdd(WDFDRIVER driver, PWDFDEVICE_INIT device_init) { return status; } - UNICODE_STRING symbolic_link; - RtlInitUnicodeString(&symbolic_link, global_symbolic_link_name); - status = WdfDeviceCreateSymbolicLink(device, &symbolic_link); - trace_status("EvtDeviceAdd WdfDeviceCreateSymbolicLink global", status); - if (!NT_SUCCESS(status) && !symbolic_link_already_exists(status)) { + status = WdfDeviceCreateDeviceInterface(device, &control_device_interface_guid, nullptr); + trace_status("EvtDeviceAdd WdfDeviceCreateDeviceInterface", status); + if (!NT_SUCCESS(status)) { + return status; + } + + status = + create_control_symbolic_link(device, global_symbolic_link_name, "EvtDeviceAdd WdfDeviceCreateSymbolicLink global"); + if (!NT_SUCCESS(status)) { return status; } - RtlInitUnicodeString(&symbolic_link, symbolic_link_name); - status = WdfDeviceCreateSymbolicLink(device, &symbolic_link); - trace_status("EvtDeviceAdd WdfDeviceCreateSymbolicLink local", status); + static_cast( + create_control_symbolic_link(device, symbolic_link_name, "EvtDeviceAdd WdfDeviceCreateSymbolicLink local") + ); WDF_IO_QUEUE_CONFIG queue_config; WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queue_config, WdfIoQueueDispatchParallel); @@ -712,6 +764,11 @@ void LvhEvtDeviceCleanup(WDFOBJECT device_object) { reset_vhf_target(false); } +void LvhEvtFileCleanup(WDFFILEOBJECT file_object) { + trace_status("EvtFileCleanup begin"); + delete_vhf_devices_for_file(file_object); +} + void LvhEvtOutputReadCanceled(WDFREQUEST request) { if (remove_pending_output_request(request)) { complete_request(request, STATUS_CANCELLED); diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index e4766e4..f46ab7c 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -39,7 +39,10 @@ #endif // platform includes +// clang-format off #include +#include +// clang-format on namespace lvh::detail { namespace { // NOSONAR(cpp:S1000): Windows backend internals need internal linkage; tests include this file with the platform factory renamed. @@ -62,6 +65,13 @@ namespace lvh::detail { return function; } + constexpr GUID control_device_interface_guid { + 0x3890af65, + 0x2da0, + 0x443c, + {0x84, 0xff, 0x6e, 0x70, 0xe8, 0x41, 0xba, 0x1e} + }; + UniqueHandle make_unique_handle(HANDLE handle) { return {handle, &::CloseHandle}; } @@ -114,6 +124,68 @@ namespace lvh::detail { return send_input(std::span {inputs}, operation); } + std::vector enumerate_control_device_interface_paths() { + std::vector paths; + + const auto device_info_set = ::SetupDiGetClassDevsA( + &control_device_interface_guid, + nullptr, + nullptr, + DIGCF_PRESENT | DIGCF_DEVICEINTERFACE + ); + if (device_info_set == INVALID_HANDLE_VALUE) { + return paths; + } + + const auto cleanup = std::unique_ptr { + device_info_set, + &::SetupDiDestroyDeviceInfoList + }; + + for (DWORD index = 0;; ++index) { + SP_DEVICE_INTERFACE_DATA interface_data {}; + interface_data.cbSize = sizeof(interface_data); + if (::SetupDiEnumDeviceInterfaces( + device_info_set, + nullptr, + &control_device_interface_guid, + index, + &interface_data + ) == FALSE) { + break; + } + + DWORD required_size = 0; + static_cast(::SetupDiGetDeviceInterfaceDetailA( + device_info_set, + &interface_data, + nullptr, + 0, + &required_size, + nullptr + )); + if (required_size == 0U || ::GetLastError() != ERROR_INSUFFICIENT_BUFFER) { + continue; + } + + std::vector buffer(required_size); + auto *detail_data = reinterpret_cast(buffer.data()); + detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); + if (::SetupDiGetDeviceInterfaceDetailA( + device_info_set, + &interface_data, + detail_data, + required_size, + nullptr, + nullptr + ) != FALSE) { + paths.emplace_back(detail_data->DevicePath); + } + } + + return paths; + } + DWORD mouse_button_flags(MouseButton button, bool pressed) { switch (button) { using enum MouseButton; @@ -169,10 +241,10 @@ namespace lvh::detail { } } - return { - std::string {windows::default_control_device_path}, - std::string {windows::global_control_device_path}, - }; + auto paths = enumerate_control_device_interface_paths(); + paths.emplace_back(windows::default_control_device_path); + paths.emplace_back(windows::global_control_device_path); + return paths; } OperationStatus protocol_status(std::uint32_t status, std::string_view operation) { diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index 7e44c53..f8d5714 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -22,7 +22,7 @@ TEST(ProfileTest, BuiltInProfilesHaveDescriptors) { EXPECT_NE(profile.vendor_id, 0); EXPECT_NE(profile.product_id, 0); EXPECT_NE(profile.report_id, 0); - EXPECT_GE(profile.input_report_size, 23U); + EXPECT_GE(profile.input_report_size, 9U); EXPECT_FALSE(profile.report_descriptor.empty()); } } @@ -37,7 +37,7 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(xbox_one.product_id, 0x02EA); EXPECT_EQ(xbox_one.manufacturer, "Microsoft"); EXPECT_TRUE(xbox_one.capabilities.supports_rumble); - EXPECT_EQ(xbox_one.input_report_size, 23U); + EXPECT_EQ(xbox_one.input_report_size, 9U); const auto xbox_series = lvh::profiles::xbox_series(); EXPECT_EQ(xbox_series.vendor_id, 0x045E); @@ -45,20 +45,19 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(xbox_series.name, "Xbox Wireless Controller"); EXPECT_EQ(xbox_series.manufacturer, "Microsoft"); - const std::array standard_button_descriptor { + const std::array standard_button_descriptor { 0x19, 0x01, 0x29, - 0x12, + 0x10, 0x15, 0x00, - 0x26, - 0xFF, - 0x00, + 0x25, + 0x01, 0x75, - 0x08, + 0x01, 0x95, - 0x12, + 0x10, }; EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, standard_button_descriptor).begin() != xbox_one.report_descriptor.end()); @@ -71,11 +70,11 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { 0x75, 0x08, 0x95, - 0x04, + 0x06, }; EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, byte_axis_descriptor).begin() != xbox_one.report_descriptor.end()); - const std::array stick_usage_descriptor { + const std::array axis_usage_descriptor { 0x09, 0x30, 0x09, @@ -84,8 +83,12 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { 0x33, 0x09, 0x34, + 0x09, + 0x32, + 0x09, + 0x35, }; - EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, stick_usage_descriptor).begin() != xbox_one.report_descriptor.end()); + EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, axis_usage_descriptor).begin() != xbox_one.report_descriptor.end()); EXPECT_EQ(dualshock4.vendor_id, 0x054C); EXPECT_EQ(dualshock4.product_id, 0x05C4); diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index fad59af..8c4769e 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -86,20 +86,14 @@ TEST(ReportTest, PacksCommonGamepadReport) { ASSERT_EQ(report.size(), profile.input_report_size); EXPECT_EQ(report[0], profile.report_id); - EXPECT_EQ(report[1], 255); // A - EXPECT_EQ(report[2], 0); // B - EXPECT_EQ(report[6], 0); // Right shoulder - EXPECT_EQ(report[7], 64); // Left trigger - EXPECT_EQ(report[8], 255); // Right trigger - EXPECT_EQ(report[9], 0); // Back - EXPECT_EQ(report[10], 255); // Start - EXPECT_EQ(report[15], 255); // D-pad left - EXPECT_EQ(report[17], 255); // Guide - EXPECT_EQ(report[18], 255); // Misc/share - EXPECT_EQ(report[19], 255); // Left stick X - EXPECT_EQ(report[20], 255); // Left stick Y - EXPECT_EQ(report[21], 191); // Right stick X - EXPECT_EQ(report[22], 191); // Right stick Y + EXPECT_EQ(report[1], 0x81); // A and Start. + EXPECT_EQ(report[2], 0x4C); // Guide, Misc/share, and D-pad left. + EXPECT_EQ(report[3], 255); // Left stick X. + EXPECT_EQ(report[4], 255); // Left stick Y. + EXPECT_EQ(report[5], 191); // Right stick X. + EXPECT_EQ(report[6], 191); // Right stick Y. + EXPECT_EQ(report[7], 64); // Left trigger. + EXPECT_EQ(report[8], 255); // Right trigger. } TEST(ReportTest, PacksDualSenseUsbReport) { diff --git a/tests/unit/test_windows_backend.cpp b/tests/unit/test_windows_backend.cpp index 8eb8a50..5387fc2 100644 --- a/tests/unit/test_windows_backend.cpp +++ b/tests/unit/test_windows_backend.cpp @@ -81,9 +81,9 @@ TEST_F(WindowsBackendTest, FakeChannelCoversCreateFailureBranches) { TEST_F(WindowsBackendTest, UtilityHookCoversEnvironmentErrorAndThreadBranches) { const auto result = lvh::detail::test::windows_backend_fake_channel_utilities(); - ASSERT_EQ(result.default_device_paths.size(), 2U); - EXPECT_EQ(result.default_device_paths[0], R"(\\.\LibVirtualHid)"); - EXPECT_EQ(result.default_device_paths[1], R"(\\.\Global\LibVirtualHid)"); + ASSERT_GE(result.default_device_paths.size(), 2U); + EXPECT_EQ(result.default_device_paths[result.default_device_paths.size() - 2U], R"(\\.\LibVirtualHid)"); + EXPECT_EQ(result.default_device_paths[result.default_device_paths.size() - 1U], R"(\\.\Global\LibVirtualHid)"); ASSERT_EQ(result.custom_device_paths.size(), 1U); EXPECT_EQ(result.custom_device_paths[0], R"(\\.\LibVirtualHid-Test)"); EXPECT_EQ(result.formatted_error_status.code(), lvh::ErrorCode::backend_failure); From 799b7efd289ba66c56339ba91c0709fdeaa1b2ac Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:14:57 -0400 Subject: [PATCH 09/27] Harden UMDF config and improve tracing Update the Windows UMDF driver package to better support development and non-admin usage by expanding device ACLs and disabling UMDF host process sharing. Add richer driver trace points and move the trace output to C:\Windows\Temp\libvirtualhid-umdf-driver.log for easier diagnostics around create/destroy/report flows. The gamepad adapter example now creates Runtime with explicit RuntimeOptions using the platform-default backend. --- README.md | 9 +++-- examples/gamepad_adapter.cpp | 4 ++- .../windows/driver/libvirtualhid.inf.in | 3 +- .../windows/driver/libvirtualhid_umdf.cpp | 35 +++++++++++++++++-- 4 files changed, 43 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 425142e..9f7b7f6 100644 --- a/README.md +++ b/README.md @@ -180,8 +180,10 @@ libvirtualhid-specific sign that installation completed is the `ROOT\LIBVIRTUALHID` device and the `\\.\LibVirtualHid` control device. The INF includes the built-in `WUDFRd` install sections for the root `System` control device, appends the VHF lower filter, sets `VhfMode=1` for the UMDF VHF source -stack, and leaves UMDF dispatcher policy at the framework default to match the -inbox VHF source-driver shape. The installer also writes `VhfMode=1` onto the +stack, grants non-admin user-mode clients read/write access to the control +device, and disables UMDF host-process sharing so driver updates do not keep +using an older in-process UMDF module during development. The installer also +writes `VhfMode=1` onto the root device before starting the driver so root-enumerated development installs get the same VHF source mode as the INF hardware section. The UMDF control device starts without opening VHF; gamepad creation opens VHF lazily so @@ -191,7 +193,8 @@ library version as the WDF headers and stub library selected by CMake. The package defaults to UMDF 2.15, matching the inbox VHF UMDF source driver while still exposing the framework APIs used by libvirtualhid. The driver target links the MSVC runtime statically to avoid requiring VC runtime DLLs in the UMDF host -process. +process. Development driver builds write a lightweight UMDF trace to +`C:\Windows\Temp\libvirtualhid-umdf-driver.log`. Windows driver packages require a signed catalog for normal installation. Pull request builds generate a short-lived self-signed test certificate, sign diff --git a/examples/gamepad_adapter.cpp b/examples/gamepad_adapter.cpp index 7941afd..ca02873 100644 --- a/examples/gamepad_adapter.cpp +++ b/examples/gamepad_adapter.cpp @@ -84,7 +84,9 @@ int main(int argc, char *argv[]) { return 1; } - auto runtime = lvh::Runtime::create(); + lvh::RuntimeOptions runtime_options; + runtime_options.backend = lvh::BackendKind::platform_default; + auto runtime = lvh::Runtime::create(runtime_options); lvh::CreateGamepadOptions options; options.profile = *profile; diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in index 212e239..e3bfe93 100644 --- a/src/platform/windows/driver/libvirtualhid.inf.in +++ b/src/platform/windows/driver/libvirtualhid.inf.in @@ -41,7 +41,7 @@ Needs=WUDFRD.NT.HW libvirtualhid_umdf.dll [DeviceInstall_Device_AddReg] -HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GRGWGX;;;AU)" +HKR,,Security,,"D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GRGWGX;;;BU)(A;;GRGWGX;;;AU)(A;;GRGW;;;WD)(A;;GR;;;RC)" [DeviceInstall_Vhf_AddReg] HKR,,"LowerFilters",0x00010008,"vhf" @@ -57,6 +57,7 @@ UmdfServiceOrder=libvirtualhid_umdf [libvirtualhid_umdf_Install] UmdfLibraryVersion=@LIBVIRTUALHID_UMDF_LIBRARY_VERSION@ +UmdfHostProcessSharing=ProcessSharingDisabled ServiceBinary=%13%\libvirtualhid_umdf.dll [Strings] diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index d08045f..38c3180 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -93,15 +93,23 @@ namespace { wchar_t trace_file_path[MAX_PATH] {}; constexpr auto trace_file_path_length = static_cast(MAX_PATH); - auto trace_path_size = GetTempPathW(trace_file_path_length, trace_file_path); + auto trace_path_size = GetWindowsDirectoryW(trace_file_path, trace_file_path_length); if (trace_path_size == 0U || trace_path_size >= trace_file_path_length) { return; } + constexpr auto trace_directory = L"\\Temp\\"; + const auto directory_size = wcslen(trace_directory); const auto file_name_size = wcslen(trace_file_name); - if (trace_path_size + file_name_size >= trace_file_path_length) { + if (trace_path_size + directory_size + file_name_size >= trace_file_path_length) { return; } + std::memcpy( + trace_file_path + trace_path_size, + trace_directory, + directory_size * sizeof(wchar_t) + ); + trace_path_size += static_cast(directory_size); std::memcpy( trace_file_path + trace_path_size, trace_file_name, @@ -259,6 +267,8 @@ namespace { return; } + trace_status("delete_vhf_device begin"); + VHFHANDLE vhf_handle = nullptr; { std::lock_guard lock {record->mutex}; @@ -267,6 +277,7 @@ namespace { } if (vhf_handle != nullptr) { + trace_status("delete_vhf_device VhfDelete"); VhfDelete(vhf_handle, TRUE); } } @@ -293,12 +304,15 @@ namespace { return; } + trace_status("delete_vhf_devices_for_file begin"); + std::vector> devices; { auto &state = driver_state(); std::lock_guard lock {state.devices_mutex}; for (auto iter = state.devices.begin(); iter != state.devices.end();) { if (iter->second->owner_file == file_object) { + trace_status("delete_vhf_devices_for_file matched"); devices.push_back(iter->second); iter = state.devices.erase(iter); } else { @@ -415,12 +429,14 @@ namespace { vhf_config.EvtVhfAsyncOperationWriteReport = LvhEvtVhfWriteReport; status = VhfCreate(&vhf_config, &record->vhf_handle); + trace_status("create_vhf_device VhfCreate", status); if (!NT_SUCCESS(status)) { record->vhf_handle = nullptr; return status; } status = VhfStart(record->vhf_handle); + trace_status("create_vhf_device VhfStart", status); if (!NT_SUCCESS(status)) { delete_vhf_device(record); } @@ -558,9 +574,11 @@ namespace { record->driver_device_id = driver_device_id; record->owner_file = WdfRequestGetFileObject(request); record->request = *create_request; + trace_status("create_gamepad begin"); status = create_vhf_device(device, record); if (!NT_SUCCESS(status)) { + trace_status("create_gamepad failed", status); create_response->status = LVH_WINDOWS_STATUS_BACKEND_FAILURE; complete_request(request, STATUS_SUCCESS, sizeof(*create_response)); return; @@ -573,6 +591,7 @@ namespace { create_response->status = LVH_WINDOWS_STATUS_SUCCESS; create_response->driver_device_id = driver_device_id; set_device_path(driver_device_id, create_response->device_path); + trace_status("create_gamepad success"); complete_request(request, STATUS_SUCCESS, sizeof(*create_response)); } @@ -597,6 +616,9 @@ namespace { if (iter != state.devices.end()) { record = iter->second; state.devices.erase(iter); + trace_status("destroy_device found"); + } else { + trace_status("destroy_device missing"); } } delete_vhf_device(record); @@ -616,20 +638,25 @@ namespace { return; } + trace_status("submit_input_report begin"); + auto record = find_device(submit_request->driver_device_id); if (!record) { + trace_status("submit_input_report missing device"); complete_request(request, STATUS_OBJECT_NAME_NOT_FOUND); return; } std::lock_guard lock {record->mutex}; if (record->vhf_handle == nullptr) { + trace_status("submit_input_report missing vhf"); complete_request(request, STATUS_OBJECT_NAME_NOT_FOUND); return; } auto report = make_vhf_input_payload(*record, *submit_request); if (report.empty()) { + trace_status("submit_input_report invalid payload"); complete_request(request, STATUS_INVALID_PARAMETER); return; } @@ -639,7 +666,9 @@ namespace { packet.reportBufferLen = static_cast(report.size()); packet.reportId = record->request.hardware_ids.report_id; - complete_request(request, VhfReadReportSubmit(record->vhf_handle, &packet)); + const auto submit_status = VhfReadReportSubmit(record->vhf_handle, &packet); + trace_status("submit_input_report VhfReadReportSubmit", submit_status); + complete_request(request, submit_status); } void handle_read_output_report_request(WDFREQUEST request) { From 60043c18000f546ad89231cf05e3e35dc7b4b71c Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Wed, 1 Jul 2026 22:25:39 -0400 Subject: [PATCH 10/27] Use per-gamepad VHF targets Move VHF IO target ownership onto each virtual gamepad record so creation, cleanup, and failure handling are isolated per device. Also normalize HID report handling to preserve complete report buffers consistently when sending input and output through VHF, and update the README to match the new lifecycle behavior. --- README.md | 20 ++++--- .../windows/driver/libvirtualhid_umdf.cpp | 57 +++++++++---------- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index 9f7b7f6..6ac4703 100644 --- a/README.md +++ b/README.md @@ -121,9 +121,10 @@ browser Gamepad API should therefore see standard HID gamepads after the driver is installed. XInput is not a direct target for this HID-only backend because it does not emulate the Xbox proprietary bus/API. The client protocol uses complete HID reports with the report ID at byte 0. The -UMDF driver strips that byte when submitting to VHF, where `HID_XFER_PACKET` -carries the report ID separately, and prepends it again before forwarding output -reports back to the C++ backend. VHF exposes VID/PID/version, explicit +UMDF driver passes that complete report buffer to VHF and also sets +`HID_XFER_PACKET.reportId` for numbered reports. Output reports forwarded by +VHF are normalized back to the same complete-report shape before delivery to +the C++ backend. VHF exposes VID/PID/version, explicit `HID\VID_....&PID_....` hardware IDs, and the report descriptor for the child HID device so Windows and browser consumers can identify the selected profile instead of a generic VHF-only device. @@ -132,9 +133,9 @@ standard-gamepad-shaped common descriptor: 16 one-bit digital buttons followed by 8-bit `X`, `Y`, `Rx`, `Ry`, `Z`, and `Rz` values for sticks and analog triggers. Keeping the buttons as real HID button caps is required for DirectInput-style and browser consumers to enumerate the VHF child as a gamepad. -The UMDF driver also owns each VHF child by the control-file handle that created -it, so process exits or crashes clean up any virtual gamepads that were not -explicitly destroyed. +The UMDF driver opens a separate VHF source target for each virtual gamepad and +parents that target to the control-file handle that created it, so process exits +or crashes clean up any virtual gamepads that were not explicitly destroyed. During rapid development reinstalls, the fixed global control symbolic link can outlive the previous root device briefly; the driver treats that collision as non-fatal so stale object-manager state does not leave the control device in @@ -186,9 +187,10 @@ using an older in-process UMDF module during development. The installer also writes `VhfMode=1` onto the root device before starting the driver so root-enumerated development installs get the same VHF source mode as the INF hardware section. The UMDF control -device starts without opening VHF; gamepad creation opens VHF lazily so -target-open failures are reported through the create-device response instead of -making `\\.\LibVirtualHid` unavailable. The generated INF uses the same UMDF +device starts without opening VHF; each gamepad creation opens its own VHF +target from the creating file handle so target-open failures are reported +through the create-device response instead of making `\\.\LibVirtualHid` +unavailable. The generated INF uses the same UMDF library version as the WDF headers and stub library selected by CMake. The package defaults to UMDF 2.15, matching the inbox VHF UMDF source driver while still exposing the framework APIs used by libvirtualhid. The driver target links diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index 38c3180..83cf8ed 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -66,6 +66,7 @@ namespace { std::mutex mutex; std::uint64_t driver_device_id {}; WDFFILEOBJECT owner_file {}; + WDFIOTARGET vhf_io_target {}; LvhWindowsCreateGamepadRequest request {}; VHFHANDLE vhf_handle {}; std::vector report_descriptor; @@ -74,8 +75,6 @@ namespace { struct DriverState { std::atomic next_driver_device_id {1}; - std::mutex vhf_target_mutex; - WDFIOTARGET vhf_io_target {}; std::mutex devices_mutex; std::map> devices; std::mutex output_requests_mutex; @@ -270,16 +269,24 @@ namespace { trace_status("delete_vhf_device begin"); VHFHANDLE vhf_handle = nullptr; + WDFIOTARGET vhf_io_target = nullptr; { std::lock_guard lock {record->mutex}; vhf_handle = record->vhf_handle; record->vhf_handle = nullptr; + vhf_io_target = record->vhf_io_target; + record->vhf_io_target = nullptr; } if (vhf_handle != nullptr) { trace_status("delete_vhf_device VhfDelete"); VhfDelete(vhf_handle, TRUE); } + + if (vhf_io_target != nullptr) { + trace_status("delete_vhf_device WdfObjectDelete target"); + WdfObjectDelete(vhf_io_target); + } } void delete_all_vhf_devices() { @@ -326,19 +333,14 @@ namespace { } } - NTSTATUS initialize_vhf_target(WDFDEVICE device) { - auto &state = driver_state(); - std::lock_guard lock {state.vhf_target_mutex}; - if (state.vhf_io_target != nullptr) { - return STATUS_SUCCESS; - } - - WDFIOTARGET vhf_io_target = nullptr; + NTSTATUS initialize_vhf_target(WDFDEVICE device, const std::shared_ptr &record) { WDF_OBJECT_ATTRIBUTES target_attributes; WDF_OBJECT_ATTRIBUTES_INIT(&target_attributes); - target_attributes.ParentObject = device; + target_attributes.ParentObject = record->owner_file != nullptr ? WDFOBJECT(record->owner_file) : WDFOBJECT(device); + WDFIOTARGET vhf_io_target = nullptr; auto status = WdfIoTargetCreate(device, &target_attributes, &vhf_io_target); + trace_status("initialize_vhf_target WdfIoTargetCreate", status); if (!NT_SUCCESS(status)) { return status; } @@ -346,25 +348,18 @@ namespace { WDF_IO_TARGET_OPEN_PARAMS open_params; WDF_IO_TARGET_OPEN_PARAMS_INIT_OPEN_BY_FILE(&open_params, nullptr); status = WdfIoTargetOpen(vhf_io_target, &open_params); + trace_status("initialize_vhf_target WdfIoTargetOpen", status); if (!NT_SUCCESS(status)) { WdfObjectDelete(vhf_io_target); return status; } - state.vhf_io_target = vhf_io_target; + record->vhf_io_target = vhf_io_target; return STATUS_SUCCESS; } - void reset_vhf_target(bool delete_target) { - auto &state = driver_state(); - const auto vhf_io_target = state.vhf_io_target; - state.vhf_io_target = nullptr; - + void reset_vhf_devices() { delete_all_vhf_devices(); - - if (delete_target && vhf_io_target != nullptr) { - WdfObjectDelete(vhf_io_target); - } } wchar_t hex_digit(unsigned value) { @@ -400,8 +395,7 @@ namespace { } NTSTATUS create_vhf_device(WDFDEVICE device, const std::shared_ptr &record) { - auto &state = driver_state(); - auto status = initialize_vhf_target(device); + auto status = initialize_vhf_target(device, record); if (!NT_SUCCESS(status)) { return status; } @@ -416,7 +410,7 @@ namespace { VHF_CONFIG vhf_config; VHF_CONFIG_INIT( &vhf_config, - WdfIoTargetWdmGetTargetFileHandle(state.vhf_io_target), + WdfIoTargetWdmGetTargetFileHandle(record->vhf_io_target), static_cast(record->report_descriptor.size()), record->report_descriptor.data() ); @@ -432,6 +426,7 @@ namespace { trace_status("create_vhf_device VhfCreate", status); if (!NT_SUCCESS(status)) { record->vhf_handle = nullptr; + delete_vhf_device(record); return status; } @@ -496,11 +491,11 @@ namespace { return {report_begin, report_end}; } - if (request.report_size <= 1U || request.report[0] != report_id) { + if (request.report[0] != report_id) { return {}; } - return {report_begin + 1U, report_end}; + return {report_begin, report_end}; } void copy_vhf_output_payload( @@ -508,11 +503,13 @@ namespace { const HID_XFER_PACKET &packet ) { const auto report_id = packet.reportId; - const auto report_id_size = report_id == 0U ? 0U : 1U; + const auto packet_includes_report_id = + report_id != 0U && packet.reportBufferLen > 0U && packet.reportBuffer[0] == report_id; + const auto report_id_size = report_id == 0U || packet_includes_report_id ? 0U : 1U; const auto payload_capacity = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE - report_id_size; const auto payload_size = std::min(packet.reportBufferLen, static_cast(payload_capacity)); - if (report_id != 0U) { + if (report_id_size != 0U) { event.report[0] = report_id; } @@ -781,7 +778,7 @@ NTSTATUS LvhEvtDeviceReleaseHardware(WDFDEVICE device, WDFCMRESLIST resources_tr trace_status("EvtDeviceReleaseHardware begin"); complete_pending_output_requests(STATUS_CANCELLED); - reset_vhf_target(true); + reset_vhf_devices(); return STATUS_SUCCESS; } @@ -790,7 +787,7 @@ void LvhEvtDeviceCleanup(WDFOBJECT device_object) { trace_status("EvtDeviceCleanup begin"); complete_pending_output_requests(STATUS_CANCELLED); - reset_vhf_target(false); + reset_vhf_devices(); } void LvhEvtFileCleanup(WDFFILEOBJECT file_object) { From 3bd8ac36f627d446a29f444d2c18acd00518aa78 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:00:06 -0400 Subject: [PATCH 11/27] Implement XboxGIP profiles with unnumbered reports Add XboxGIP-based profiles for Xbox One and Series controllers with unnumbered 17-byte input reports. Update common gamepad descriptor to use 12 buttons plus a hat switch instead of 16 buttons. Support unnumbered reports (report_id=0) throughout the API. Improve driver device lifecycle management by tracking gamepad ownership and cleaning up only devices associated with the device being released. Add device restart functionality to the installer and Xbox interface IDs to hardware enumeration. --- README.md | 43 ++++--- scripts/windows/install-driver.ps1 | 33 ++++++ src/core/profiles.cpp | 111 ++++++++++++++++-- src/core/report.cpp | 77 ++++++++++-- src/core/runtime.cpp | 3 - src/include/libvirtualhid/report.hpp | 2 +- src/include/libvirtualhid/types.hpp | 2 +- .../windows/driver/libvirtualhid_umdf.cpp | 48 ++++++-- tests/unit/test_profiles.cpp | 85 ++++++++++---- tests/unit/test_report.cpp | 46 +++++++- 10 files changed, 367 insertions(+), 83 deletions(-) diff --git a/README.md b/README.md index 6ac4703..8ed5d6c 100644 --- a/README.md +++ b/README.md @@ -120,19 +120,25 @@ callback path. DirectInput, SDL/HIDAPI, Windows.Gaming.Input/GameInput, and the browser Gamepad API should therefore see standard HID gamepads after the driver is installed. XInput is not a direct target for this HID-only backend because it does not emulate the Xbox proprietary bus/API. -The client protocol uses complete HID reports with the report ID at byte 0. The -UMDF driver passes that complete report buffer to VHF and also sets -`HID_XFER_PACKET.reportId` for numbered reports. Output reports forwarded by -VHF are normalized back to the same complete-report shape before delivery to -the C++ backend. VHF exposes VID/PID/version, explicit -`HID\VID_....&PID_....` hardware IDs, and the report descriptor for the child -HID device so Windows and browser consumers can identify the selected profile -instead of a generic VHF-only device. -The built-in generic, Xbox-style, and Switch Pro-style HID profiles use a -standard-gamepad-shaped common descriptor: 16 one-bit digital buttons followed -by 8-bit `X`, `Y`, `Rx`, `Ry`, `Z`, and `Rz` values for sticks and analog -triggers. Keeping the buttons as real HID button caps is required for -DirectInput-style and browser consumers to enumerate the VHF child as a gamepad. +The client protocol uses complete HID reports; numbered reports carry the report +ID at byte 0 and unnumbered reports omit it. The UMDF driver passes that +complete report buffer to VHF and also sets `HID_XFER_PACKET.reportId` for +numbered reports. Output reports forwarded by VHF are normalized back to the +same complete-report shape before delivery to the C++ backend. VHF exposes +VID/PID/version, explicit +`HID\VID_....&PID_....` hardware IDs, Xbox +`HID\VID_....&PID_....&IG_00` hardware IDs where applicable, and the report +descriptor for the child HID device so Windows and browser consumers can +identify the selected profile instead of a generic VHF-only device. +The built-in Xbox One and Xbox Series profiles use an XboxGIP-shaped descriptor +and unnumbered 17-byte input reports derived from HIDMaestro's Xbox profiles, +with inputtino's Xbox One VID/PID/version retained for that profile. The +built-in generic and Xbox 360 HID profiles use a standard-gamepad-shaped common +descriptor: 12 one-bit digital buttons, a hat switch for the d-pad, and 8-bit +`X`, `Y`, `Z`, `Rx`, `Ry`, and `Rz` values for sticks and analog triggers. +Keeping the buttons as real HID button caps and the d-pad as a hat switch is +required for DirectInput-style and browser consumers to enumerate the VHF child +as a gamepad. The UMDF driver opens a separate VHF source target for each virtual gamepad and parents that target to the control-file handle that created it, so process exits or crashes clean up any virtual gamepads that were not explicitly destroyed. @@ -187,10 +193,13 @@ using an older in-process UMDF module during development. The installer also writes `VhfMode=1` onto the root device before starting the driver so root-enumerated development installs get the same VHF source mode as the INF hardware section. The UMDF control -device starts without opening VHF; each gamepad creation opens its own VHF -target from the creating file handle so target-open failures are reported -through the create-device response instead of making `\\.\LibVirtualHid` -unavailable. The generated INF uses the same UMDF +device is restarted after install or update so same-version development builds +load the current UMDF module; if Windows cannot unload the old host, the +installer reports the reboot requirement. The UMDF control device starts +without opening VHF; each gamepad creation opens its own VHF target from the +creating file handle so target-open failures are reported through the +create-device response instead of making `\\.\LibVirtualHid` unavailable. The +generated INF uses the same UMDF library version as the WDF headers and stub library selected by CMake. The package defaults to UMDF 2.15, matching the inbox VHF UMDF source driver while still exposing the framework APIs used by libvirtualhid. The driver target links diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index b9993e8..8a038e6 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -345,6 +345,33 @@ function Set-RootDeviceVhfMode { } } +function Restart-RootDevice { + [CmdletBinding(SupportsShouldProcess)] + param([string] $InstanceId) + + if (-not $InstanceId) { + return + } + + if (-not $PSCmdlet.ShouldProcess($InstanceId, "Restart libvirtualhid development device")) { + return + } + + $output = @(pnputil.exe /restart-device $InstanceId 2>&1) + $exitCode = $LASTEXITCODE + foreach ($line in $output) { + Write-Information $line -InformationAction Continue + } + + if ($exitCode -ne 0) { + throw "pnputil.exe /restart-device $InstanceId exited with code $exitCode" + } + + if ($output -match "reboot is needed") { + Write-Warning "Windows reported that a reboot is required to reload the libvirtualhid UMDF driver." + } +} + function Update-RootDeviceDriverWithSetupApi { [CmdletBinding(SupportsShouldProcess)] param( @@ -408,6 +435,9 @@ try { Set-RootDeviceVhfMode -InstanceId $rootDevice } Update-RootDeviceDriverWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId + foreach ($rootDevice in $rootDevices) { + Restart-RootDevice -InstanceId $rootDevice + } return } @@ -420,6 +450,9 @@ try { Set-RootDeviceVhfMode -InstanceId $rootDevice } Update-RootDeviceDriverWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId + foreach ($rootDevice in $rootDevices) { + Restart-RootDevice -InstanceId $rootDevice + } } finally { Stop-LibVirtualHidTranscript } diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index 8747480..f0bdf1d 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include // local includes @@ -15,7 +16,7 @@ namespace lvh::profiles { namespace { - constexpr std::uint8_t common_button_count = 16; + constexpr std::uint8_t common_button_count = 12; constexpr std::uint8_t common_axis_count = 6; @@ -25,6 +26,8 @@ namespace lvh::profiles { constexpr std::size_t common_output_report_size = 5; + constexpr std::size_t xbox_gip_input_report_size = 17; + constexpr std::size_t dualshock4_usb_input_report_size = 64; constexpr std::size_t dualshock4_usb_output_report_size = 32; @@ -41,6 +44,47 @@ namespace lvh::profiles { constexpr std::size_t dualsense_bluetooth_output_report_size = 78; + std::uint8_t hex_nibble(char digit) { + if (digit >= '0' && digit <= '9') { + return static_cast(digit - '0'); + } + if (digit >= 'A' && digit <= 'F') { + return static_cast(digit - 'A' + 10); + } + if (digit >= 'a' && digit <= 'f') { + return static_cast(digit - 'a' + 10); + } + return 0; + } + + std::vector bytes_from_hex(std::string_view hex) { + std::vector bytes; + bytes.reserve(hex.size() / 2U); + for (std::size_t index = 0; index + 1U < hex.size(); index += 2U) { + bytes.push_back(static_cast((hex_nibble(hex[index]) << 4U) | hex_nibble(hex[index + 1U]))); + } + return bytes; + } + + std::vector make_xbox_gip_report_descriptor(bool include_share_button) { + constexpr std::string_view xbox_one_descriptor = + "05010905a101a10009300931150027ffff0000950275108102c0a10009330934150027ffff0000950275108102c0" + "05010932150026ff039501750a81021500250075069501810305010935150026ff039501750a8102150025007506" + "9501810305091901290a950a750181021500250075069501810305010939150125083500463b0166140075049501" + "814275049501150025003500450065008103a102050f099715002501750495019102150025009103097015002564" + "7508950491020950660110550e26ff009501910209a7910265005500097c9102c005010980a100098515002501" + "95017501810215002500750795018103c005060920150026ff00750895018102c0"; + constexpr std::string_view xbox_series_descriptor = + "05010905a101a10009300931150027ffff0000950275108102c0a10009330934150027ffff0000950275108102c0" + "05010932150026ff039501750a81021500250075069501810305010935150026ff039501750a8102150025007506" + "9501810305091901290c950c750181021500250075049501810305010939150125083500463b0166140075049501" + "814275049501150025003500450065008103a102050f099715002501750495019102150025009103097015002564" + "7508950491020950660110550e26ff009501910209a7910265005500097c9102c005010980a100098515002501" + "95017501810215002500750795018103c005060920150026ff00750895018102c0"; + + return bytes_from_hex(include_share_button ? xbox_series_descriptor : xbox_one_descriptor); + } + std::vector make_gamepad_report_descriptor(std::uint8_t report_id, bool supports_rumble) { std::vector descriptor { 0x05, @@ -69,6 +113,29 @@ namespace lvh::profiles { 0x02, // Input (Data,Var,Abs) 0x05, 0x01, // Usage Page (Generic Desktop) + 0x09, + 0x39, // Usage (Hat switch) + 0x15, + 0x00, // Logical Minimum (0) + 0x25, + 0x07, // Logical Maximum (7) + 0x35, + 0x00, // Physical Minimum (0) + 0x46, + 0x3B, + 0x01, // Physical Maximum (315) + 0x65, + 0x14, // Unit (Eng Rot:Angular Pos) + 0x75, + 0x04, // Report Size (4) + 0x95, + 0x01, // Report Count (1) + 0x81, + 0x42, // Input (Data,Var,Abs,Null) + 0x65, + 0x00, // Unit (None) + 0x05, + 0x01, // Usage Page (Generic Desktop) 0x15, 0x00, // Logical Minimum (0) 0x26, @@ -83,12 +150,12 @@ namespace lvh::profiles { 0x09, 0x31, // Usage (Y) 0x09, + 0x32, // Usage (Z) + 0x09, 0x33, // Usage (Rx) 0x09, 0x34, // Usage (Ry) 0x09, - 0x32, // Usage (Z) - 0x09, 0x35, // Usage (Rz) 0x81, 0x02, // Input (Data,Var,Abs) @@ -1587,6 +1654,30 @@ namespace lvh::profiles { return profile; } + DeviceProfile make_xbox_gip_profile( + GamepadProfileKind kind, + std::string name, + std::uint16_t product_id, + std::uint16_t version, + bool include_share_button + ) { + DeviceProfile profile; + profile.device_type = DeviceType::gamepad; + profile.gamepad_kind = kind; + profile.bus_type = include_share_button ? BusType::bluetooth : BusType::usb; + profile.vendor_id = 0x045E; + profile.product_id = product_id; + profile.version = version; + profile.report_id = 0; + profile.input_report_size = xbox_gip_input_report_size; + profile.output_report_size = common_output_report_size; + profile.name = std::move(name); + profile.manufacturer = "Microsoft"; + profile.capabilities = {.supports_rumble = true, .supports_battery = include_share_button}; + profile.report_descriptor = make_xbox_gip_report_descriptor(include_share_button); + return profile; + } + DeviceProfile make_dualshock4_profile(BusType bus_type) { DeviceProfile profile; profile.device_type = DeviceType::gamepad; @@ -1681,26 +1772,22 @@ namespace lvh::profiles { } DeviceProfile xbox_one() { - return make_gamepad_profile( + return make_xbox_gip_profile( GamepadProfileKind::xbox_one, "Xbox One Controller", - "Microsoft", - 0x045E, 0x02EA, 0x0408, - {.supports_rumble = true} + false ); } DeviceProfile xbox_series() { - return make_gamepad_profile( + return make_xbox_gip_profile( GamepadProfileKind::xbox_series, "Xbox Wireless Controller", - "Microsoft", - 0x045E, - 0x0B12, + 0x0B13, 0x0500, - {.supports_rumble = true, .supports_battery = true} + true ); } diff --git a/src/core/report.cpp b/src/core/report.cpp index 909067a..ed4f29b 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -192,10 +192,28 @@ namespace lvh::reports { set_bit(9U, GamepadButton::right_stick); set_bit(10U, GamepadButton::guide); set_bit(11U, GamepadButton::misc1); - set_bit(12U, GamepadButton::dpad_up); - set_bit(13U, GamepadButton::dpad_down); - set_bit(14U, GamepadButton::dpad_left); - set_bit(15U, GamepadButton::dpad_right); + return bits; + } + + std::uint16_t xbox_gip_button_bits(const ButtonSet &buttons) { + auto bits = std::uint16_t {}; + const auto set_bit = [&buttons, &bits](std::uint16_t bit, GamepadButton button) { + if (buttons.test(button)) { + bits |= static_cast(1U << bit); + } + }; + + set_bit(0U, GamepadButton::a); + set_bit(1U, GamepadButton::b); + set_bit(2U, GamepadButton::x); + set_bit(3U, GamepadButton::y); + set_bit(4U, GamepadButton::left_shoulder); + set_bit(5U, GamepadButton::right_shoulder); + set_bit(6U, GamepadButton::back); + set_bit(7U, GamepadButton::start); + set_bit(8U, GamepadButton::left_stick); + set_bit(9U, GamepadButton::right_stick); + set_bit(11U, GamepadButton::misc1); return bits; } @@ -558,8 +576,7 @@ namespace lvh::reports { const auto right_trigger_effect_type = raw_report[offset + 10U]; const auto left_trigger_effect_type = raw_report[offset + 21U]; - if (const auto valid_flag2 = report[offset + 38U]; - has_flag(valid_flag0, dualsense_flag0_rumble) || has_flag(valid_flag2, dualsense_flag2_compatible_vibration)) { + if (const auto valid_flag2 = report[offset + 38U]; has_flag(valid_flag0, dualsense_flag0_rumble) || has_flag(valid_flag2, dualsense_flag2_compatible_vibration)) { GamepadOutput output; output.kind = GamepadOutputKind::rumble; output.low_frequency_rumble = scale_output_byte(motor_left); @@ -694,8 +711,47 @@ namespace lvh::reports { return neutral_hat; } + std::uint16_t normalize_u10_trigger(float value) { + return static_cast(std::lround(clamp_trigger(value) * 1023.0F)); + } + + std::uint8_t battery_strength(const std::optional &battery) { + if (!battery) { + return 0; + } + + return static_cast(std::lround((static_cast(battery->percentage) / 100.0F) * 255.0F)); + } + + std::vector pack_xbox_gip_input_report(const DeviceProfile &profile, const GamepadState &state) { + constexpr std::size_t xbox_gip_input_report_size = 17; + if (profile.input_report_size < xbox_gip_input_report_size) { + return {}; + } + + const auto normalized = normalize_state(state); + + ByteReport report(profile.input_report_size, zero_byte); + write_i16(report, 0U, normalize_axis(normalized.left_stick.x)); + write_i16(report, 2U, normalize_axis(normalized.left_stick.y)); + write_i16(report, 4U, normalize_axis(normalized.right_stick.x)); + write_i16(report, 6U, normalize_axis(normalized.right_stick.y)); + write_u16(report, 8U, normalize_u10_trigger(normalized.left_trigger)); + write_u16(report, 10U, normalize_u10_trigger(normalized.right_trigger)); + write_u16(report, 12U, xbox_gip_button_bits(normalized.buttons)); + report[14] = to_byte(hat_from_buttons(normalized.buttons)); + if (normalized.buttons.test(GamepadButton::guide)) { + report[15] = std::byte {0x01}; + } + report[16] = to_byte(battery_strength(normalized.battery)); + return to_uint8_report(report); + } + std::vector pack_input_report(const DeviceProfile &profile, const GamepadState &state) { if (profile.device_type == DeviceType::gamepad) { + if (profile.gamepad_kind == GamepadProfileKind::xbox_one || profile.gamepad_kind == GamepadProfileKind::xbox_series) { + return pack_xbox_gip_input_report(profile, state); + } if (profile.gamepad_kind == GamepadProfileKind::dualshock4) { return pack_dualshock4_input_report(profile, state); } @@ -714,12 +770,17 @@ namespace lvh::reports { std::vector report; report.reserve(common_report_size); report.push_back(profile.report_id); - append_u16(report, common_button_bits(normalized.buttons)); + append_u16( + report, + static_cast( + common_button_bits(normalized.buttons) | static_cast(hat_from_buttons(normalized.buttons) << 12U) + ) + ); report.push_back(normalize_u8_axis(normalized.left_stick.x)); report.push_back(normalize_u8_axis(-normalized.left_stick.y)); + report.push_back(normalize_trigger(normalized.left_trigger)); report.push_back(normalize_u8_axis(normalized.right_stick.x)); report.push_back(normalize_u8_axis(-normalized.right_stick.y)); - report.push_back(normalize_trigger(normalized.left_trigger)); report.push_back(normalize_trigger(normalized.right_trigger)); report.resize(profile.input_report_size, 0); diff --git a/src/core/runtime.cpp b/src/core/runtime.cpp index fce2171..db9b1d5 100644 --- a/src/core/runtime.cpp +++ b/src/core/runtime.cpp @@ -173,9 +173,6 @@ namespace lvh { if (options.profile.report_descriptor.empty()) { return OperationStatus::failure(invalid_argument, "device profile report descriptor must not be empty"); } - if (options.profile.report_id == 0) { - return OperationStatus::failure(invalid_argument, "device profile report id must not be zero"); - } if (options.profile.input_report_size == 0) { return OperationStatus::failure(invalid_argument, "device profile input report size must not be zero"); } diff --git a/src/include/libvirtualhid/report.hpp b/src/include/libvirtualhid/report.hpp index f902509..76b103d 100644 --- a/src/include/libvirtualhid/report.hpp +++ b/src/include/libvirtualhid/report.hpp @@ -62,7 +62,7 @@ namespace lvh::reports { std::uint8_t hat_from_buttons(const ButtonSet &buttons); /** - * @brief Pack a gamepad state into the profile's common input report format. + * @brief Pack a gamepad state into the profile's input report format. * * @param profile Device profile used for report identity and size. * @param state Gamepad state to pack. diff --git a/src/include/libvirtualhid/types.hpp b/src/include/libvirtualhid/types.hpp index 36e2c91..a90c89e 100644 --- a/src/include/libvirtualhid/types.hpp +++ b/src/include/libvirtualhid/types.hpp @@ -303,7 +303,7 @@ namespace lvh { std::uint16_t version = 0; /** - * @brief Primary input report identifier. + * @brief Primary input report identifier, or `0` for unnumbered HID reports. */ std::uint8_t report_id = 1; diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index 83cf8ed..f6a5a87 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -65,6 +65,7 @@ namespace { struct DeviceRecord { std::mutex mutex; std::uint64_t driver_device_id {}; + WDFDEVICE owner_device {}; WDFFILEOBJECT owner_file {}; WDFIOTARGET vhf_io_target {}; LvhWindowsCreateGamepadRequest request {}; @@ -289,16 +290,26 @@ namespace { } } - void delete_all_vhf_devices() { + void delete_vhf_devices_for_device(WDFDEVICE device) { + if (device == nullptr) { + return; + } + + trace_status("delete_vhf_devices_for_device begin"); + std::vector> devices; { auto &state = driver_state(); std::lock_guard lock {state.devices_mutex}; - for (const auto &[driver_device_id, record] : state.devices) { - static_cast(driver_device_id); - devices.push_back(record); + for (auto iter = state.devices.begin(); iter != state.devices.end();) { + if (iter->second->owner_device == device) { + trace_status("delete_vhf_devices_for_device matched"); + devices.push_back(iter->second); + iter = state.devices.erase(iter); + } else { + ++iter; + } } - state.devices.clear(); } for (const auto &record : devices) { @@ -358,8 +369,8 @@ namespace { return STATUS_SUCCESS; } - void reset_vhf_devices() { - delete_all_vhf_devices(); + void reset_vhf_devices(WDFDEVICE device) { + delete_vhf_devices_for_device(device); } wchar_t hex_digit(unsigned value) { @@ -381,8 +392,20 @@ namespace { append_hex4(hardware_ids, ids.product_id); } - std::wstring make_hardware_ids(const LvhWindowsGamepadHardwareIds &ids) { + bool is_xbox_gamepad(std::uint32_t gamepad_kind) { + return gamepad_kind == LVH_WINDOWS_GAMEPAD_XBOX_360 || gamepad_kind == LVH_WINDOWS_GAMEPAD_XBOX_ONE || + gamepad_kind == LVH_WINDOWS_GAMEPAD_XBOX_SERIES; + } + + std::wstring make_hardware_ids(const LvhWindowsCreateGamepadRequest &request) { + const auto &ids = request.hardware_ids; std::wstring hardware_ids; + if (is_xbox_gamepad(request.gamepad_kind)) { + append_hid_vid_pid(hardware_ids, ids); + hardware_ids.append(L"&IG_00"); + hardware_ids.push_back(L'\0'); + } + append_hid_vid_pid(hardware_ids, ids); hardware_ids.append(L"&REV_"); append_hex4(hardware_ids, ids.device_version); @@ -405,7 +428,7 @@ namespace { record->request.report_descriptor, record->request.report_descriptor + descriptor_size ); - record->hardware_ids = make_hardware_ids(record->request.hardware_ids); + record->hardware_ids = make_hardware_ids(record->request); VHF_CONFIG vhf_config; VHF_CONFIG_INIT( @@ -569,6 +592,7 @@ namespace { const auto driver_device_id = state.next_driver_device_id.fetch_add(1); auto record = std::make_shared(); record->driver_device_id = driver_device_id; + record->owner_device = device; record->owner_file = WdfRequestGetFileObject(request); record->request = *create_request; trace_status("create_gamepad begin"); @@ -778,16 +802,14 @@ NTSTATUS LvhEvtDeviceReleaseHardware(WDFDEVICE device, WDFCMRESLIST resources_tr trace_status("EvtDeviceReleaseHardware begin"); complete_pending_output_requests(STATUS_CANCELLED); - reset_vhf_devices(); + reset_vhf_devices(device); return STATUS_SUCCESS; } void LvhEvtDeviceCleanup(WDFOBJECT device_object) { - UNREFERENCED_PARAMETER(device_object); - trace_status("EvtDeviceCleanup begin"); complete_pending_output_requests(STATUS_CANCELLED); - reset_vhf_devices(); + reset_vhf_devices(WDFDEVICE(device_object)); } void LvhEvtFileCleanup(WDFFILEOBJECT file_object) { diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index f8d5714..0593923 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -21,7 +21,6 @@ TEST(ProfileTest, BuiltInProfilesHaveDescriptors) { EXPECT_FALSE(profile.name.empty()); EXPECT_NE(profile.vendor_id, 0); EXPECT_NE(profile.product_id, 0); - EXPECT_NE(profile.report_id, 0); EXPECT_GE(profile.input_report_size, 9U); EXPECT_FALSE(profile.report_descriptor.empty()); } @@ -37,58 +36,96 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(xbox_one.product_id, 0x02EA); EXPECT_EQ(xbox_one.manufacturer, "Microsoft"); EXPECT_TRUE(xbox_one.capabilities.supports_rumble); - EXPECT_EQ(xbox_one.input_report_size, 9U); + EXPECT_EQ(xbox_one.report_id, 0); + EXPECT_EQ(xbox_one.input_report_size, 17U); const auto xbox_series = lvh::profiles::xbox_series(); EXPECT_EQ(xbox_series.vendor_id, 0x045E); - EXPECT_EQ(xbox_series.product_id, 0x0B12); + EXPECT_EQ(xbox_series.product_id, 0x0B13); EXPECT_EQ(xbox_series.name, "Xbox Wireless Controller"); EXPECT_EQ(xbox_series.manufacturer, "Microsoft"); + EXPECT_EQ(xbox_series.report_id, 0); + EXPECT_EQ(xbox_series.input_report_size, 17U); - const std::array standard_button_descriptor { + const std::array xbox_gip_button_descriptor { + 0x05, + 0x09, 0x19, 0x01, 0x29, - 0x10, - 0x15, - 0x00, - 0x25, - 0x01, + 0x0A, + 0x95, + 0x0A, 0x75, 0x01, - 0x95, - 0x10, + 0x81, + 0x02, }; - EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, standard_button_descriptor).begin() != xbox_one.report_descriptor.end()); + EXPECT_TRUE( + std::ranges::search(xbox_one.report_descriptor, xbox_gip_button_descriptor).begin() != xbox_one.report_descriptor.end() + ); - const std::array byte_axis_descriptor { + const std::array xbox_gip_stick_axis_descriptor { 0x15, 0x00, - 0x26, + 0x27, + 0xFF, 0xFF, 0x00, - 0x75, - 0x08, + 0x00, 0x95, - 0x06, + 0x02, + 0x75, + 0x10, }; - EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, byte_axis_descriptor).begin() != xbox_one.report_descriptor.end()); + EXPECT_TRUE( + std::ranges::search(xbox_one.report_descriptor, xbox_gip_stick_axis_descriptor).begin() != xbox_one.report_descriptor.end() + ); - const std::array axis_usage_descriptor { - 0x09, - 0x30, - 0x09, - 0x31, + const std::array right_stick_usage_descriptor { 0x09, 0x33, 0x09, 0x34, + 0x15, + 0x00, + 0x27, + 0xFF, + 0xFF, + }; + EXPECT_TRUE( + std::ranges::search(xbox_one.report_descriptor, right_stick_usage_descriptor).begin() != xbox_one.report_descriptor.end() + ); + + const std::array trigger_usage_descriptor { 0x09, 0x32, + 0x15, + 0x00, + 0x26, + 0xFF, + 0x03, + 0x95, + 0x01, + }; + EXPECT_TRUE( + std::ranges::search(xbox_one.report_descriptor, trigger_usage_descriptor).begin() != xbox_one.report_descriptor.end() + ); + + const std::array right_trigger_usage_descriptor { 0x09, 0x35, + 0x15, + 0x00, + 0x26, + 0xFF, + 0x03, + 0x95, + 0x01, }; - EXPECT_TRUE(std::ranges::search(xbox_one.report_descriptor, axis_usage_descriptor).begin() != xbox_one.report_descriptor.end()); + EXPECT_TRUE( + std::ranges::search(xbox_one.report_descriptor, right_trigger_usage_descriptor).begin() != xbox_one.report_descriptor.end() + ); EXPECT_EQ(dualshock4.vendor_id, 0x054C); EXPECT_EQ(dualshock4.product_id, 0x05C4); diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index 8c4769e..5cf2811 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -4,6 +4,7 @@ */ // standard includes +#include #include #include #include @@ -87,15 +88,52 @@ TEST(ReportTest, PacksCommonGamepadReport) { ASSERT_EQ(report.size(), profile.input_report_size); EXPECT_EQ(report[0], profile.report_id); EXPECT_EQ(report[1], 0x81); // A and Start. - EXPECT_EQ(report[2], 0x4C); // Guide, Misc/share, and D-pad left. + EXPECT_EQ(report[2], 0x6C); // Guide, Misc/share, and D-pad-left hat value. EXPECT_EQ(report[3], 255); // Left stick X. EXPECT_EQ(report[4], 255); // Left stick Y. - EXPECT_EQ(report[5], 191); // Right stick X. - EXPECT_EQ(report[6], 191); // Right stick Y. - EXPECT_EQ(report[7], 64); // Left trigger. + EXPECT_EQ(report[5], 64); // Left trigger. + EXPECT_EQ(report[6], 191); // Right stick X. + EXPECT_EQ(report[7], 191); // Right stick Y. EXPECT_EQ(report[8], 255); // Right trigger. } +TEST(ReportTest, PacksXboxGipReport) { + auto profile = lvh::profiles::xbox_series(); + + lvh::GamepadState state; + state.buttons.set(lvh::GamepadButton::a); + state.buttons.set(lvh::GamepadButton::start); + state.buttons.set(lvh::GamepadButton::dpad_left); + state.buttons.set(lvh::GamepadButton::guide); + state.buttons.set(lvh::GamepadButton::misc1); + state.left_stick = {1.0F, -1.0F}; + state.right_stick = {0.5F, -0.5F}; + state.left_trigger = 0.25F; + state.right_trigger = 1.0F; + state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::discharging, .percentage = 80}; + + const auto report = lvh::reports::pack_input_report(profile, state); + const auto read_u16 = [&report](std::size_t offset) { + return static_cast(report[offset] | static_cast(report[offset + 1U] << 8U)); + }; + const auto read_i16 = [&read_u16](std::size_t offset) { + return static_cast(read_u16(offset)); + }; + + ASSERT_EQ(report.size(), profile.input_report_size); + EXPECT_EQ(profile.report_id, 0); + EXPECT_EQ(read_i16(0U), 32767); // Left stick X. + EXPECT_EQ(read_i16(2U), -32768); // Left stick Y. + EXPECT_EQ(read_i16(4U), 16384); // Right stick X. + EXPECT_EQ(read_i16(6U), -16384); // Right stick Y. + EXPECT_EQ(read_u16(8U), 256); // Left trigger. + EXPECT_EQ(read_u16(10U), 1023); // Right trigger. + EXPECT_EQ(read_u16(12U), 0x0881); // A, Start, and Share. + EXPECT_EQ(report[14], 6); // D-pad left. + EXPECT_EQ(report[15], 1); // Guide/System Main Menu. + EXPECT_EQ(report[16], 204); // Battery strength. +} + TEST(ReportTest, PacksDualSenseUsbReport) { auto profile = lvh::profiles::dualsense_usb(); From e3148f43d5b68a1c38eeece065841f7d30d2cc5c Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 08:46:58 -0400 Subject: [PATCH 12/27] Add Windows driver smoke test Add a PowerShell helper that verifies the installed libvirtualhid test driver starts, opens the control device, and can expose a started gamepad child device for a held `gamepad_adapter` instance. Wire the check into the Windows MSVC pull request CI leg and document the manual validation flow in the README. --- .github/workflows/ci.yml | 13 ++ README.md | 13 ++ scripts/windows/test-installed-driver.ps1 | 228 ++++++++++++++++++++++ 3 files changed, 254 insertions(+) create mode 100644 scripts/windows/test-installed-driver.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf96bff..4eab0ff 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -295,6 +295,19 @@ jobs: -InfPath (Join-Path $packagePath "libvirtualhid.inf") ` -CertificatePath $certificatePath + - name: Verify Windows test driver package + if: >- + matrix.kind == 'msvc' && + github.event_name == 'pull_request' + run: | + $profiles = @("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch") + foreach ($profile in $profiles) { + .\scripts\windows\test-installed-driver.ps1 ` + -GamepadAdapterPath "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\Debug\gamepad_adapter.exe" ` + -Profile $profile ` + -Verbose + } + - name: Prepare report directory run: cmake -E make_directory cmake-build-ci/reports diff --git a/README.md b/README.md index 8ed5d6c..3cd6b5f 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,9 @@ Developer install/uninstall helpers live under `scripts/windows`: powershell -ExecutionPolicy Bypass -File .\scripts\windows\install-driver.ps1 ` -InfPath .\cmake-build-windows-driver\src\platform\windows\driver\package\Release\libvirtualhid.inf ` -LogPath .\cmake-build-windows-driver\install-driver.log +powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-installed-driver.ps1 ` + -GamepadAdapterPath .\cmake-build-ci\examples\Debug\gamepad_adapter.exe ` + -Profile x360 powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1 ` -Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" ``` @@ -179,6 +182,16 @@ The install and uninstall helpers also clean up malformed development devices left by earlier installer revisions, including root instances left in the failed `HIDClass` package shape. The WiX installer writes the helper transcript to `C:\ProgramData\libvirtualhid\install-driver.log`. +The test helper fails if the root device is not reported as `Status: Started`, +if `\\.\LibVirtualHid` cannot be opened, or if a held gamepad adapter instance +does not produce a started HID child device such as +`HID\VID_045E&PID_028E&IG_00`. That check is also run by the Windows MSVC pull +request CI leg for every `gamepad_adapter` profile after installing the test +driver package. For manual browser validation, run the same helper or +`examples/gamepad_adapter x360 --hold-seconds 60`, then open +`https://app.lizardbyte.dev/gamepad-tester/` in a normal desktop browser and +press one of the held virtual buttons if the browser needs a gamepad activation +event. The driver binary is a UMDF DLL installed through the Windows Driver Store, not a libvirtualhid `.sys` copied into `C:\Windows\System32\drivers`. Windows still diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1 new file mode 100644 index 0000000..45c34f0 --- /dev/null +++ b/scripts/windows/test-installed-driver.ps1 @@ -0,0 +1,228 @@ +<# +.SYNOPSIS +Validates that the installed libvirtualhid Windows driver starts and can expose a gamepad child device. +#> +[CmdletBinding()] +param( + [string] $HardwareId = "ROOT\LIBVIRTUALHID", + + [string] $ControlDevicePath = "\\.\LibVirtualHid", + + [string] $GamepadAdapterPath, + + [ValidateSet("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch")] + [string] $Profile = "x360", + + [int] $HoldSeconds = 12, + + [int] $DeviceStartTimeoutSeconds = 20 +) + +$ErrorActionPreference = "Stop" + +function Invoke-PnPUtil { + param([string[]] $Arguments) + + $output = @(pnputil.exe @Arguments 2>&1) + $exitCode = $LASTEXITCODE + foreach ($line in $output) { + Write-Verbose $line + } + + if ($exitCode -ne 0) { + throw "pnputil.exe $($Arguments -join ' ') exited with code $exitCode`n$($output -join "`n")" + } + + return $output +} + +function ConvertFrom-PnPUtilDeviceOutput { + param([string[]] $Output) + + $records = @() + $current = $null + foreach ($line in $Output) { + if ($line -match "^\s*Instance ID:\s*(.+)\s*$") { + if ($current) { + $records += [pscustomobject] $current + } + $current = @{ + InstanceId = $Matches[1].Trim() + DeviceDescription = $null + DriverName = $null + Status = $null + ProblemCode = $null + ProblemStatus = $null + } + continue + } + + if (-not $current) { + continue + } + + if ($line -match "^\s{0,2}Device Description:\s*(.+)\s*$") { + $current.DeviceDescription = $Matches[1].Trim() + } elseif ($line -match "^\s{0,2}Driver Name:\s*(.+)\s*$") { + $current.DriverName = $Matches[1].Trim() + } elseif ($line -match "^\s*Status:\s*(.+)\s*$") { + $current.Status = $Matches[1].Trim() + } elseif ($line -match "^\s*Problem Code:\s*(.+)\s*$") { + $current.ProblemCode = $Matches[1].Trim() + } elseif ($line -match "^\s*Problem Status:\s*(.+)\s*$") { + $current.ProblemStatus = $Matches[1].Trim() + } + } + + if ($current) { + $records += [pscustomobject] $current + } + + return $records +} + +function Get-PnPUtilDevicesByDeviceId { + param([string] $DeviceId) + + $output = Invoke-PnPUtil -Arguments @( + "/enum-devices", + "/deviceid", + $DeviceId, + "/deviceids", + "/services", + "/drivers" + ) + return ConvertFrom-PnPUtilDeviceOutput -Output $output +} + +function Assert-StartedPnPRecord { + param( + [object] $Record, + [string] $Description + ) + + if ($Record.Status -ne "Started") { + $details = @( + "$Description did not report Status: Started.", + "Instance ID: $($Record.InstanceId)", + "Status: $($Record.Status)" + ) + if ($Record.ProblemCode) { + $details += "Problem Code: $($Record.ProblemCode)" + } + if ($Record.ProblemStatus) { + $details += "Problem Status: $($Record.ProblemStatus)" + } + + throw ($details -join "`n") + } +} + +function Assert-RootDeviceStarted { + $rootDevices = @(Get-PnPUtilDevicesByDeviceId -DeviceId $HardwareId) + if (-not $rootDevices) { + throw "No installed libvirtualhid root device was found for $HardwareId." + } + + foreach ($device in $rootDevices) { + Assert-StartedPnPRecord -Record $device -Description "Root device $($device.InstanceId)" + } +} + +function Assert-ControlDeviceOpens { + try { + $stream = [System.IO.File]::Open( + $ControlDevicePath, + [System.IO.FileMode]::Open, + [System.IO.FileAccess]::ReadWrite, + [System.IO.FileShare]::ReadWrite + ) + $stream.Dispose() + } catch { + throw "Could not open ${ControlDevicePath}: $($_.Exception.Message)" + } +} + +function Get-ExpectedGamepadHardwareId { + switch ($Profile) { + "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_0B13&IG_00" } + "ds4" { return "HID\VID_054C&PID_05C4" } + "ds5" { return "HID\VID_054C&PID_0CE6" } + "switch" { return "HID\VID_057E&PID_2009" } + } + + throw "Unsupported profile: $Profile" +} + +function Wait-ForStartedGamepadChild { + $deviceId = Get-ExpectedGamepadHardwareId + $deadline = (Get-Date).AddSeconds($DeviceStartTimeoutSeconds) + $latestRecords = @() + + do { + $latestRecords = @(Get-PnPUtilDevicesByDeviceId -DeviceId $deviceId) + $started = $latestRecords | + Where-Object { + $_.Status -eq "Started" -and + $_.DriverName -ne "hidvhf.inf" -and + $_.DeviceDescription -ne "Virtual HID Framework (VHF) HID device" + } | + Select-Object -First 1 + if ($started) { + Write-Information "Gamepad child device started: $($started.InstanceId) ($($started.DriverName))" -InformationAction Continue + return + } + + Start-Sleep -Milliseconds 500 + } while ((Get-Date) -lt $deadline) + + if (-not $latestRecords) { + throw "No gamepad child device was found for $deviceId." + } + + foreach ($record in $latestRecords) { + Assert-StartedPnPRecord -Record $record -Description "Gamepad child device $deviceId" + } +} + +function Invoke-GamepadAdapterSmoke { + if (-not $GamepadAdapterPath) { + return + } + + $resolvedGamepadAdapterPath = (Resolve-Path -LiteralPath $GamepadAdapterPath).Path + $stdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-gamepad-adapter.out" + $stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-gamepad-adapter.err" + Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue + + $process = Start-Process ` + -FilePath $resolvedGamepadAdapterPath ` + -ArgumentList @($Profile, "--hold-seconds", "$HoldSeconds") ` + -PassThru ` + -RedirectStandardOutput $stdoutPath ` + -RedirectStandardError $stderrPath ` + -WindowStyle Hidden + + try { + Start-Sleep -Seconds 2 + if ($process.HasExited) { + $stdout = Get-Content -LiteralPath $stdoutPath -Raw -ErrorAction SilentlyContinue + $stderr = Get-Content -LiteralPath $stderrPath -Raw -ErrorAction SilentlyContinue + throw "gamepad_adapter exited with code $($process.ExitCode).`nstdout:`n$stdout`nstderr:`n$stderr" + } + + Wait-ForStartedGamepadChild + } finally { + if (-not $process.HasExited) { + Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue + Wait-Process -Id $process.Id -Timeout 5 -ErrorAction SilentlyContinue + } + } +} + +Assert-RootDeviceStarted +Assert-ControlDeviceOpens +Invoke-GamepadAdapterSmoke From a2a4c2dab94a662710fff9bd7f36c556b1e0ba91 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 09:21:21 -0400 Subject: [PATCH 13/27] Fix Xbox GIP report encoding for XInput Align Xbox One/Series input reports with expected XInput behavior by encoding stick axes as unsigned 16-bit centered values, shifting hat values so neutral maps to 0, and defaulting unknown battery strength to 0xFF. Added/updated unit coverage for both active and neutral Xbox GIP reports, and enhanced the Windows installed-driver smoke test to probe XInput state changes before passing. --- scripts/windows/test-installed-driver.ps1 | 92 +++++++++++++++++++++++ src/core/report.cpp | 21 ++++-- tests/unit/test_report.cpp | 34 +++++++-- 3 files changed, 133 insertions(+), 14 deletions(-) diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1 index 45c34f0..d9ca749 100644 --- a/scripts/windows/test-installed-driver.ps1 +++ b/scripts/windows/test-installed-driver.ps1 @@ -143,6 +143,97 @@ function Assert-ControlDeviceOpens { } } +function Add-XInputProbeType { + if ("LvhXInputProbe" -as [type]) { + return + } + + Add-Type -TypeDefinition @" +using System.Runtime.InteropServices; + +public static class LvhXInputProbe { + [StructLayout(LayoutKind.Sequential)] + public struct XInputState { + public uint PacketNumber; + public XInputGamepad Gamepad; + } + + [StructLayout(LayoutKind.Sequential)] + public struct XInputGamepad { + public ushort Buttons; + public byte LeftTrigger; + public byte RightTrigger; + public short LeftThumbX; + public short LeftThumbY; + public short RightThumbX; + public short RightThumbY; + } + + [DllImport("xinput1_4.dll", EntryPoint = "XInputGetState")] + public static extern uint XInputGetState(uint userIndex, out XInputState state); +} +"@ +} + +function Wait-ForXInputReportFlow { + if ($Profile -ne "xone" -and $Profile -ne "xseries") { + return + } + + Add-XInputProbeType + $deadline = (Get-Date).AddSeconds($DeviceStartTimeoutSeconds) + $observedStates = @{} + + do { + for ($index = 0; $index -lt 4; ++$index) { + $state = New-Object LvhXInputProbe+XInputState + $status = [LvhXInputProbe]::XInputGetState([uint32] $index, [ref] $state) + if ($status -ne 0 -or $state.Gamepad.RightTrigger -ne 255) { + continue + } + + if (-not $observedStates.ContainsKey($index)) { + $observedStates[$index] = [pscustomobject] @{ + InitialButtons = $state.Gamepad.Buttons + ButtonChanged = $false + LeftThumbXNegative = $false + LeftThumbXPositive = $false + RightThumbYNegative = $false + RightThumbYPositive = $false + } + } + + $observed = $observedStates[$index] + if ($state.Gamepad.Buttons -ne $observed.InitialButtons) { + $observed.ButtonChanged = $true + } + if ($state.Gamepad.LeftThumbX -lt -20000) { + $observed.LeftThumbXNegative = $true + } elseif ($state.Gamepad.LeftThumbX -gt 20000) { + $observed.LeftThumbXPositive = $true + } + if ($state.Gamepad.RightThumbY -lt -20000) { + $observed.RightThumbYNegative = $true + } elseif ($state.Gamepad.RightThumbY -gt 20000) { + $observed.RightThumbYPositive = $true + } + + if ($observed.ButtonChanged -and + $observed.LeftThumbXNegative -and + $observed.LeftThumbXPositive -and + $observed.RightThumbYNegative -and + $observed.RightThumbYPositive) { + Write-Information "XInput observed changing $Profile input on index ${index}." -InformationAction Continue + return + } + } + + Start-Sleep -Milliseconds 250 + } while ((Get-Date) -lt $deadline) + + throw "XInput did not observe changing $Profile button, left-stick X, and right-stick Y input from the virtual gamepad." +} + function Get-ExpectedGamepadHardwareId { switch ($Profile) { "generic" { return "HID\VID_1209&PID_0001" } @@ -215,6 +306,7 @@ function Invoke-GamepadAdapterSmoke { } Wait-ForStartedGamepadChild + Wait-ForXInputReportFlow } finally { if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue diff --git a/src/core/report.cpp b/src/core/report.cpp index ed4f29b..3e76dac 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -164,6 +164,10 @@ namespace lvh::reports { return static_cast(std::lround(scaled)); } + std::uint16_t normalize_unsigned_axis(float value) { + return static_cast(std::lround((clamp_axis(value) + 1.0F) * 32767.5F)); + } + std::uint8_t normalize_u8_axis(float value) { return static_cast(std::lround((clamp_axis(value) + 1.0F) * 127.5F)); } @@ -715,9 +719,14 @@ namespace lvh::reports { return static_cast(std::lround(clamp_trigger(value) * 1023.0F)); } + std::uint8_t xbox_gip_hat_from_buttons(const ButtonSet &buttons) { + const auto hat = hat_from_buttons(buttons); + return hat == neutral_hat ? 0 : static_cast(hat + 1U); + } + std::uint8_t battery_strength(const std::optional &battery) { if (!battery) { - return 0; + return 0xFF; } return static_cast(std::lround((static_cast(battery->percentage) / 100.0F) * 255.0F)); @@ -732,14 +741,14 @@ namespace lvh::reports { const auto normalized = normalize_state(state); ByteReport report(profile.input_report_size, zero_byte); - write_i16(report, 0U, normalize_axis(normalized.left_stick.x)); - write_i16(report, 2U, normalize_axis(normalized.left_stick.y)); - write_i16(report, 4U, normalize_axis(normalized.right_stick.x)); - write_i16(report, 6U, normalize_axis(normalized.right_stick.y)); + write_u16(report, 0U, normalize_unsigned_axis(normalized.left_stick.x)); + write_u16(report, 2U, normalize_unsigned_axis(normalized.left_stick.y)); + write_u16(report, 4U, normalize_unsigned_axis(normalized.right_stick.x)); + write_u16(report, 6U, normalize_unsigned_axis(normalized.right_stick.y)); write_u16(report, 8U, normalize_u10_trigger(normalized.left_trigger)); write_u16(report, 10U, normalize_u10_trigger(normalized.right_trigger)); write_u16(report, 12U, xbox_gip_button_bits(normalized.buttons)); - report[14] = to_byte(hat_from_buttons(normalized.buttons)); + report[14] = to_byte(xbox_gip_hat_from_buttons(normalized.buttons)); if (normalized.buttons.test(GamepadButton::guide)) { report[15] = std::byte {0x01}; } diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index 5cf2811..17d4b81 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -116,24 +116,42 @@ TEST(ReportTest, PacksXboxGipReport) { const auto read_u16 = [&report](std::size_t offset) { return static_cast(report[offset] | static_cast(report[offset + 1U] << 8U)); }; - const auto read_i16 = [&read_u16](std::size_t offset) { - return static_cast(read_u16(offset)); - }; ASSERT_EQ(report.size(), profile.input_report_size); EXPECT_EQ(profile.report_id, 0); - EXPECT_EQ(read_i16(0U), 32767); // Left stick X. - EXPECT_EQ(read_i16(2U), -32768); // Left stick Y. - EXPECT_EQ(read_i16(4U), 16384); // Right stick X. - EXPECT_EQ(read_i16(6U), -16384); // Right stick Y. + EXPECT_EQ(read_u16(0U), 0xFFFF); // Left stick X. + EXPECT_EQ(read_u16(2U), 0x0000); // Left stick Y. + EXPECT_EQ(read_u16(4U), 0xBFFF); // Right stick X. + EXPECT_EQ(read_u16(6U), 0x4000); // Right stick Y. EXPECT_EQ(read_u16(8U), 256); // Left trigger. EXPECT_EQ(read_u16(10U), 1023); // Right trigger. EXPECT_EQ(read_u16(12U), 0x0881); // A, Start, and Share. - EXPECT_EQ(report[14], 6); // D-pad left. + EXPECT_EQ(report[14], 7); // D-pad left. EXPECT_EQ(report[15], 1); // Guide/System Main Menu. EXPECT_EQ(report[16], 204); // Battery strength. } +TEST(ReportTest, PacksXboxGipNeutralReport) { + const auto profile = lvh::profiles::xbox_series(); + + const auto report = lvh::reports::pack_input_report(profile, {}); + const auto read_u16 = [&report](std::size_t offset) { + return static_cast(report[offset] | static_cast(report[offset + 1U] << 8U)); + }; + + ASSERT_EQ(report.size(), profile.input_report_size); + EXPECT_EQ(read_u16(0U), 0x8000); // Left stick X. + EXPECT_EQ(read_u16(2U), 0x8000); // Left stick Y. + EXPECT_EQ(read_u16(4U), 0x8000); // Right stick X. + EXPECT_EQ(read_u16(6U), 0x8000); // Right stick Y. + EXPECT_EQ(read_u16(8U), 0x0000); // Left trigger. + EXPECT_EQ(read_u16(10U), 0x0000); // Right trigger. + EXPECT_EQ(read_u16(12U), 0x0000); // Buttons. + EXPECT_EQ(report[14], 0); // Neutral D-pad. + EXPECT_EQ(report[15], 0); // Guide/System Main Menu. + EXPECT_EQ(report[16], 0xFF); // Unknown battery defaults to full. +} + TEST(ReportTest, PacksDualSenseUsbReport) { auto profile = lvh::profiles::dualsense_usb(); From 7c3131f8c2079137696b43c74de7bfe2132a5b9f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:43:22 -0400 Subject: [PATCH 14/27] Add browser Gamepad API CI validation Adds a new Windows PowerShell helper (`scripts/windows/test-browser-gamepad.ps1`) that launches Edge/Chrome with DevTools, runs `gamepad_adapter`, and verifies the browser Gamepad API detects the expected controller ID plus changing button/axis input. The MSVC pull request CI job now runs this browser check across all supported gamepad profiles, and README testing docs were updated to include the new command and browser-based validation flow. --- .github/workflows/ci.yml | 13 + README.md | 14 +- scripts/windows/test-browser-gamepad.ps1 | 422 +++++++++++++++++++++++ 3 files changed, 445 insertions(+), 4 deletions(-) create mode 100644 scripts/windows/test-browser-gamepad.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4eab0ff..da7b37e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -308,6 +308,19 @@ jobs: -Verbose } + - name: Verify Windows browser Gamepad API + if: >- + matrix.kind == 'msvc' && + github.event_name == 'pull_request' + run: | + $profiles = @("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch") + foreach ($profile in $profiles) { + .\scripts\windows\test-browser-gamepad.ps1 ` + -GamepadAdapterPath "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\Debug\gamepad_adapter.exe" ` + -Profile $profile ` + -Verbose + } + - name: Prepare report directory run: cmake -E make_directory cmake-build-ci/reports diff --git a/README.md b/README.md index 3cd6b5f..a1c933d 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,9 @@ powershell -ExecutionPolicy Bypass -File .\scripts\windows\install-driver.ps1 ` powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-installed-driver.ps1 ` -GamepadAdapterPath .\cmake-build-ci\examples\Debug\gamepad_adapter.exe ` -Profile x360 +powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-browser-gamepad.ps1 ` + -GamepadAdapterPath .\cmake-build-ci\examples\Debug\gamepad_adapter.exe ` + -Profile x360 powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1 ` -Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" ``` @@ -187,11 +190,14 @@ if `\\.\LibVirtualHid` cannot be opened, or if a held gamepad adapter instance does not produce a started HID child device such as `HID\VID_045E&PID_028E&IG_00`. That check is also run by the Windows MSVC pull request CI leg for every `gamepad_adapter` profile after installing the test -driver package. For manual browser validation, run the same helper or +driver package. The browser helper launches a normal desktop Edge or Chrome +instance at `https://hardwaretester.com/gamepad`, holds a virtual gamepad, and +fails if the browser Gamepad API does not report a controller matching the +selected profile or does not observe changing button and axis input. For manual +browser validation, run the browser helper with `-KeepBrowserOpen`, or run `examples/gamepad_adapter x360 --hold-seconds 60`, then open -`https://app.lizardbyte.dev/gamepad-tester/` in a normal desktop browser and -press one of the held virtual buttons if the browser needs a gamepad activation -event. +`https://hardwaretester.com/gamepad` in a normal desktop browser and press one +of the held virtual buttons if the browser needs a gamepad activation event. The driver binary is a UMDF DLL installed through the Windows Driver Store, not a libvirtualhid `.sys` copied into `C:\Windows\System32\drivers`. Windows still diff --git a/scripts/windows/test-browser-gamepad.ps1 b/scripts/windows/test-browser-gamepad.ps1 new file mode 100644 index 0000000..50b5770 --- /dev/null +++ b/scripts/windows/test-browser-gamepad.ps1 @@ -0,0 +1,422 @@ +<# +.SYNOPSIS +Validates an installed libvirtualhid gamepad through a real browser Gamepad API. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string] $GamepadAdapterPath, + + [ValidateSet("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch")] + [string] $Profile = "x360", + + [string] $BrowserPath, + + [string] $Url = "https://hardwaretester.com/gamepad", + + [int] $TimeoutSeconds = 20, + + [int] $HoldSeconds = 35, + + [string] $ExpectedIdPattern, + + [switch] $AllowAnyGamepad, + + [switch] $KeepBrowserOpen +) + +$ErrorActionPreference = "Stop" +$script:DevToolsCommandId = 0 + +function Get-ExpectedGamepadIdPattern { + switch ($Profile) { + "generic" { return "(1209.*0001|vid[_ -]?1209.*pid[_ -]?0001|generic)" } + "x360" { return "(045e.*028e|vid[_ -]?045e.*pid[_ -]?028e|x-?box.*360)" } + "xone" { return "(045e.*02ea|vid[_ -]?045e.*pid[_ -]?02ea|xbox one|x-box one)" } + "xseries" { return "(045e.*0b13|vid[_ -]?045e.*pid[_ -]?0b13|xbox wireless|xbox series)" } + "ds4" { return "(054c.*05c4|vid[_ -]?054c.*pid[_ -]?05c4|dualshock|wireless controller)" } + "ds5" { return "(054c.*0ce6|vid[_ -]?054c.*pid[_ -]?0ce6|dualsense|wireless controller)" } + "switch" { return "(057e.*2009|vid[_ -]?057e.*pid[_ -]?2009|switch|pro controller)" } + } + + throw "Unsupported profile: $Profile" +} + +function Resolve-BrowserPath { + if ($BrowserPath) { + return (Resolve-Path -LiteralPath $BrowserPath).Path + } + + $candidates = @( + (Join-Path ${env:ProgramFiles(x86)} "Microsoft\Edge\Application\msedge.exe"), + (Join-Path $env:ProgramFiles "Microsoft\Edge\Application\msedge.exe"), + (Join-Path ${env:ProgramFiles(x86)} "Google\Chrome\Application\chrome.exe"), + (Join-Path $env:ProgramFiles "Google\Chrome\Application\chrome.exe") + ) + + foreach ($candidate in $candidates) { + if (Test-Path -LiteralPath $candidate) { + return $candidate + } + } + + throw "No supported browser was found. Pass -BrowserPath with msedge.exe or chrome.exe." +} + +function Get-FreeTcpPort { + $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0) + try { + $listener.Start() + return ([System.Net.IPEndPoint] $listener.LocalEndpoint).Port + } finally { + $listener.Stop() + } +} + +function Wait-ForDevToolsJson { + param( + [int] $Port, + [string] $Path, + [int] $TimeoutSeconds + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + $uri = "http://127.0.0.1:${Port}${Path}" + do { + try { + return Invoke-RestMethod -Uri $uri -TimeoutSec 2 + } catch { + Start-Sleep -Milliseconds 250 + } + } while ((Get-Date) -lt $deadline) + + throw "Timed out waiting for browser DevTools endpoint $uri." +} + +function Wait-ForDevToolsPageTarget { + param( + [int] $Port, + [int] $TimeoutSeconds + ) + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + do { + try { + $targets = @(Wait-ForDevToolsJson -Port $Port -Path "/json" -TimeoutSeconds 2) + } catch { + $targets = @() + } + $page = $targets | + Where-Object { $_.type -eq "page" -and $_.webSocketDebuggerUrl } | + Select-Object -First 1 + if ($page) { + return $page + } + + Start-Sleep -Milliseconds 250 + } while ((Get-Date) -lt $deadline) + + throw "Timed out waiting for a browser page target." +} + +function Invoke-DevToolsCommand { + param( + [string] $WebSocketDebuggerUrl, + [string] $Method, + [hashtable] $Params = @{}, + [int] $TimeoutSeconds = 30 + ) + + $socket = [System.Net.WebSockets.ClientWebSocket]::new() + $cancellation = [System.Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds($TimeoutSeconds)) + try { + $socket.ConnectAsync([Uri] $WebSocketDebuggerUrl, $cancellation.Token).GetAwaiter().GetResult() + + $id = [System.Threading.Interlocked]::Increment([ref] $script:DevToolsCommandId) + $message = @{ + id = $id + method = $Method + params = $Params + } | ConvertTo-Json -Depth 20 -Compress + $bytes = [System.Text.Encoding]::UTF8.GetBytes($message) + $socket.SendAsync( + [ArraySegment[byte]]::new($bytes), + [System.Net.WebSockets.WebSocketMessageType]::Text, + $true, + $cancellation.Token + ).GetAwaiter().GetResult() + + do { + $buffer = New-Object byte[] 65536 + $builder = [System.Text.StringBuilder]::new() + do { + $segment = [ArraySegment[byte]]::new($buffer) + $result = $socket.ReceiveAsync($segment, $cancellation.Token).GetAwaiter().GetResult() + if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) { + throw "Browser DevTools websocket closed before $Method returned." + } + [void] $builder.Append([System.Text.Encoding]::UTF8.GetString($buffer, 0, $result.Count)) + } while (-not $result.EndOfMessage) + + $response = $builder.ToString() | ConvertFrom-Json + if ($response.id -eq $id) { + if ($response.error) { + throw "DevTools $Method failed: $($response.error.message)" + } + return $response.result + } + } while ($true) + } finally { + if ($socket.State -eq [System.Net.WebSockets.WebSocketState]::Open) { + $socket.CloseAsync( + [System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, + "done", + [System.Threading.CancellationToken]::None + ).GetAwaiter().GetResult() + } + $socket.Dispose() + $cancellation.Dispose() + } +} + +function New-GamepadApiProbeExpression { + param( + [string] $ExpectedIdPattern, + [bool] $AllowAnyGamepad, + [int] $TimeoutSeconds + ) + + $patternJson = ConvertTo-Json $ExpectedIdPattern -Compress + $allowAnyJson = if ($AllowAnyGamepad) { "true" } else { "false" } + return @" +(async () => { + const expectedPattern = new RegExp($patternJson, "i"); + const allowAnyGamepad = $allowAnyJson; + const deadline = Date.now() + ($TimeoutSeconds * 1000); + const summary = { + ok: false, + gamepadApi: typeof navigator.getGamepads === "function", + secureContext: window.isSecureContext, + userAgent: navigator.userAgent, + ids: [], + samples: [], + matched: null + }; + + if (!summary.gamepadApi) { + return summary; + } + + function round(value) { + return Math.round(value * 1000) / 1000; + } + + function snapshot() { + return Array.from(navigator.getGamepads()) + .filter((pad) => pad) + .map((pad) => ({ + id: pad.id, + index: pad.index, + mapping: pad.mapping, + connected: pad.connected, + buttonCount: pad.buttons.length, + axisCount: pad.axes.length, + buttons: pad.buttons.map((button) => ({ + pressed: button.pressed, + value: round(button.value) + })), + axes: pad.axes.map(round) + })); + } + + const seenById = new Map(); + function stateFor(pad) { + if (!seenById.has(pad.id)) { + seenById.set(pad.id, { + id: pad.id, + mapping: pad.mapping, + buttonCount: pad.buttonCount, + axisCount: pad.axisCount, + buttonPressed: false, + buttonChanged: false, + axisMoved: false, + buttons: [], + axes: [] + }); + } + return seenById.get(pad.id); + } + + function updateExtents(extents, index, value) { + if (!extents[index]) { + extents[index] = { min: value, max: value }; + } else { + extents[index].min = Math.min(extents[index].min, value); + extents[index].max = Math.max(extents[index].max, value); + } + return extents[index].max - extents[index].min; + } + + while (Date.now() < deadline) { + const pads = snapshot(); + if (summary.samples.length < 5 || Date.now() + 1000 >= deadline) { + summary.samples.push(pads.map((pad) => ({ + id: pad.id, + mapping: pad.mapping, + buttonCount: pad.buttonCount, + axisCount: pad.axisCount, + pressedButtons: pad.buttons + .map((button, index) => button.pressed || button.value > 0.5 ? index : null) + .filter((index) => index !== null), + axes: pad.axes + }))); + } + + for (const pad of pads) { + if (!summary.ids.includes(pad.id)) { + summary.ids.push(pad.id); + } + if (!allowAnyGamepad && !expectedPattern.test(pad.id)) { + continue; + } + + const seen = stateFor(pad); + seen.mapping = pad.mapping; + for (let index = 0; index < pad.buttons.length; index += 1) { + const button = pad.buttons[index]; + if (button.pressed || button.value > 0.5) { + seen.buttonPressed = true; + } + if (updateExtents(seen.buttons, index, button.value) > 0.5) { + seen.buttonChanged = true; + } + } + + for (let index = 0; index < pad.axes.length; index += 1) { + if (updateExtents(seen.axes, index, pad.axes[index]) > 0.5) { + seen.axisMoved = true; + } + } + + if (seen.buttonPressed && seen.buttonChanged && seen.axisMoved) { + summary.ok = true; + summary.matched = seen; + return summary; + } + } + + await new Promise((resolve) => setTimeout(resolve, 200)); + } + + for (const seen of seenById.values()) { + summary.matched = seen; + break; + } + return summary; +})() +"@ +} + +if ($HoldSeconds -le $TimeoutSeconds) { + throw "-HoldSeconds must be greater than -TimeoutSeconds so the adapter remains alive for browser polling." +} + +$resolvedGamepadAdapterPath = (Resolve-Path -LiteralPath $GamepadAdapterPath).Path +$resolvedBrowserPath = Resolve-BrowserPath +$expectedPattern = if ($ExpectedIdPattern) { $ExpectedIdPattern } else { Get-ExpectedGamepadIdPattern } +$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" +$adapterStderrPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-browser-gamepad-adapter.err" +Remove-Item -LiteralPath $adapterStdoutPath, $adapterStderrPath -Force -ErrorAction SilentlyContinue + +$browserProcess = $null +$adapterProcess = $null +try { + $browserArguments = @( + "--user-data-dir=$browserUserDataDir", + "--remote-debugging-port=$remoteDebuggingPort", + "--no-first-run", + "--no-default-browser-check", + "--disable-background-timer-throttling", + "--disable-renderer-backgrounding", + "--disable-backgrounding-occluded-windows", + "--new-window", + $Url + ) + + $browserProcess = Start-Process ` + -FilePath $resolvedBrowserPath ` + -ArgumentList $browserArguments ` + -PassThru + + [void] (Wait-ForDevToolsJson -Port $remoteDebuggingPort -Path "/json/version" -TimeoutSeconds $TimeoutSeconds) + $page = Wait-ForDevToolsPageTarget -Port $remoteDebuggingPort -TimeoutSeconds $TimeoutSeconds + [void] (Invoke-DevToolsCommand ` + -WebSocketDebuggerUrl $page.webSocketDebuggerUrl ` + -Method "Page.bringToFront" ` + -TimeoutSeconds 5) + + $adapterProcess = Start-Process ` + -FilePath $resolvedGamepadAdapterPath ` + -WorkingDirectory (Split-Path -Parent $resolvedGamepadAdapterPath) ` + -ArgumentList @($Profile, "--hold-seconds", "$HoldSeconds") ` + -PassThru ` + -RedirectStandardOutput $adapterStdoutPath ` + -RedirectStandardError $adapterStderrPath ` + -WindowStyle Hidden + + Start-Sleep -Seconds 2 + if ($adapterProcess.HasExited) { + $stdout = Get-Content -LiteralPath $adapterStdoutPath -Raw -ErrorAction SilentlyContinue + $stderr = Get-Content -LiteralPath $adapterStderrPath -Raw -ErrorAction SilentlyContinue + throw "gamepad_adapter exited with code $($adapterProcess.ExitCode).`nstdout:`n$stdout`nstderr:`n$stderr" + } + + $expression = New-GamepadApiProbeExpression ` + -ExpectedIdPattern $expectedPattern ` + -AllowAnyGamepad:$AllowAnyGamepad ` + -TimeoutSeconds $TimeoutSeconds + $result = Invoke-DevToolsCommand ` + -WebSocketDebuggerUrl $page.webSocketDebuggerUrl ` + -Method "Runtime.evaluate" ` + -Params @{ + expression = $expression + awaitPromise = $true + returnByValue = $true + } ` + -TimeoutSeconds ($TimeoutSeconds + 10) + + if ($result.exceptionDetails) { + throw "Browser Gamepad API probe threw: $($result.exceptionDetails.text)" + } + + $probe = $result.result.value + if (-not $probe.ok) { + $probeJson = $probe | ConvertTo-Json -Depth 20 + $expectedMessage = if ($AllowAnyGamepad) { + "any gamepad" + } else { + "a gamepad matching /$expectedPattern/i" + } + throw "Browser Gamepad API did not observe changing input from $expectedMessage.`n$probeJson" + } + + Write-Information ` + "Browser Gamepad API observed $Profile as '$($probe.matched.id)' with mapping '$($probe.matched.mapping)'." ` + -InformationAction Continue +} finally { + if ($adapterProcess -and -not $adapterProcess.HasExited) { + Stop-Process -Id $adapterProcess.Id -Force -ErrorAction SilentlyContinue + Wait-Process -Id $adapterProcess.Id -Timeout 5 -ErrorAction SilentlyContinue + } + + if ($browserProcess -and -not $KeepBrowserOpen -and -not $browserProcess.HasExited) { + Stop-Process -Id $browserProcess.Id -Force -ErrorAction SilentlyContinue + Wait-Process -Id $browserProcess.Id -Timeout 5 -ErrorAction SilentlyContinue + } + + if (-not $KeepBrowserOpen) { + Remove-Item -LiteralPath $browserUserDataDir -Recurse -Force -ErrorAction SilentlyContinue + } +} From 00e9cfa574a860d28cee1e53e3b1e7ba08c3877c Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 11:54:04 -0400 Subject: [PATCH 15/27] Submit neutral gamepad report on create Create a neutral input report immediately after adapter creation so OS consumers can enumerate the virtual controller before any client input arrives. Update the adapter tests and README to reflect the extra initial submit. --- README.md | 4 +++- src/core/gamepad_adapter.cpp | 8 +++++++- tests/unit/test_gamepad_adapter.cpp | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a1c933d..7795231 100644 --- a/README.md +++ b/README.md @@ -640,7 +640,9 @@ platform-specific calls. `Runtime` and `Gamepad` APIs. - [x] Preserve Sunshine's asynchronous event shape by caching per-controller `GamepadState` and resubmitting after separate button, axis, trigger, touch, - motion, and battery updates. + motion, and battery updates. Adapter creation submits one neutral input report + immediately so operating-system consumers can enumerate the virtual controller + before the first client input event arrives. - [x] Expand or formally map the public button model so Sunshine's full controller flag set is preserved, including guide/home, profile-specific misc/share, and rear paddles where the emulated profile can expose them. diff --git a/src/core/gamepad_adapter.cpp b/src/core/gamepad_adapter.cpp index 6e7d7a8..23a9487 100644 --- a/src/core/gamepad_adapter.cpp +++ b/src/core/gamepad_adapter.cpp @@ -171,7 +171,13 @@ namespace lvh { return {std::move(created.status), nullptr}; } - return {OperationStatus::success(), std::make_unique(std::move(created.gamepad))}; + auto adapter = std::make_unique(std::move(created.gamepad)); + if (const auto status = adapter->submit(); !status.ok()) { + static_cast(adapter->close()); + return {status, nullptr}; + } + + return {OperationStatus::success(), std::move(adapter)}; } Gamepad *GamepadStateAdapter::gamepad() { diff --git a/tests/unit/test_gamepad_adapter.cpp b/tests/unit/test_gamepad_adapter.cpp index c42d4d7..46cd334 100644 --- a/tests/unit/test_gamepad_adapter.cpp +++ b/tests/unit/test_gamepad_adapter.cpp @@ -130,7 +130,7 @@ TEST(GamepadAdapterTest, CachesAndSubmitsPartialUpdates) { const auto *gamepad = adapter.gamepad(); ASSERT_NE(gamepad, nullptr); - EXPECT_EQ(gamepad->submit_count(), 9U); + EXPECT_EQ(gamepad->submit_count(), 10U); const auto submitted = gamepad->last_submitted_state(); EXPECT_TRUE(submitted.buttons.test(lvh::GamepadButton::a)); @@ -190,7 +190,7 @@ TEST(GamepadAdapterTest, RejectsUnsupportedPartialUpdates) { EXPECT_EQ(adapter.clear_touchpad_contact(0).code(), lvh::ErrorCode::unsupported_profile); EXPECT_EQ(adapter.set_button(lvh::GamepadButton::touchpad, true).code(), lvh::ErrorCode::unsupported_profile); EXPECT_EQ(adapter.set_button(lvh::GamepadButton::paddle1, true).code(), lvh::ErrorCode::unsupported_profile); - EXPECT_EQ(adapter.gamepad()->submit_count(), 0U); + EXPECT_EQ(adapter.gamepad()->submit_count(), 1U); } TEST(GamepadAdapterTest, RejectsInvalidCreationAndClosedAdapterUpdates) { From e92b1a8340b3c3a960ca5dbcaa064b752d504796 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:04:38 -0400 Subject: [PATCH 16/27] Align Windows gamepad profiles and reports Updates gamepad profile behavior to match real-device expectations and Windows backend limits. Generic and Switch now use a standard 16-button browser-friendly descriptor/report layout, Xbox One switches to PID 0x02FF, and DualSense/Switch naming is adjusted for expected Gamepad API IDs. Input packing now inverts Y axes consistently across Xbox GIP, DualShock, and DualSense paths. Windows UMDF/VHF creation now rejects the x360 profile as unsupported (XUSB-only), and tests/docs/scripts were updated accordingly: defaults move to xseries, hardware-id expectations are corrected, browser automation targeting is made more reliable, and PR CI no longer runs the browser gamepad step. --- .github/workflows/ci.yml | 15 +-- README.md | 42 +++--- scripts/windows/test-browser-gamepad.ps1 | 75 +++++++++-- scripts/windows/test-installed-driver.ps1 | 8 +- src/core/profiles.cpp | 127 +++++++++++++++++- src/core/report.cpp | 56 +++++++- src/platform/windows/windows_backend.cpp | 73 +++------- .../fixtures/windows_backend_test_hooks.hpp | 1 + tests/fixtures/windows_backend_test_hooks.cpp | 21 ++- tests/unit/test_profiles.cpp | 48 ++++++- tests/unit/test_report.cpp | 38 +++++- tests/unit/test_runtime.cpp | 10 +- tests/unit/test_windows_backend.cpp | 3 +- 13 files changed, 391 insertions(+), 126 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da7b37e..ca79304 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -300,7 +300,7 @@ jobs: matrix.kind == 'msvc' && github.event_name == 'pull_request' run: | - $profiles = @("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch") + $profiles = @("generic", "xone", "xseries", "ds4", "ds5", "switch") foreach ($profile in $profiles) { .\scripts\windows\test-installed-driver.ps1 ` -GamepadAdapterPath "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\Debug\gamepad_adapter.exe" ` @@ -308,19 +308,6 @@ jobs: -Verbose } - - name: Verify Windows browser Gamepad API - if: >- - matrix.kind == 'msvc' && - github.event_name == 'pull_request' - run: | - $profiles = @("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch") - foreach ($profile in $profiles) { - .\scripts\windows\test-browser-gamepad.ps1 ` - -GamepadAdapterPath "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\Debug\gamepad_adapter.exe" ` - -Profile $profile ` - -Verbose - } - - name: Prepare report directory run: cmake -E make_directory cmake-build-ci/reports diff --git a/README.md b/README.md index 7795231..ac0107b 100644 --- a/README.md +++ b/README.md @@ -132,13 +132,20 @@ descriptor for the child HID device so Windows and browser consumers can identify the selected profile instead of a generic VHF-only device. The built-in Xbox One and Xbox Series profiles use an XboxGIP-shaped descriptor and unnumbered 17-byte input reports derived from HIDMaestro's Xbox profiles, -with inputtino's Xbox One VID/PID/version retained for that profile. The -built-in generic and Xbox 360 HID profiles use a standard-gamepad-shaped common -descriptor: 12 one-bit digital buttons, a hat switch for the d-pad, and 8-bit -`X`, `Y`, `Z`, `Rx`, `Ry`, and `Rz` values for sticks and analog triggers. -Keeping the buttons as real HID button caps and the d-pad as a hat switch is -required for DirectInput-style and browser consumers to enumerate the VHF child -as a gamepad. +with WinUHid's Xbox One PID and hardware-ID shape used for that profile. The +built-in generic and Switch HID profiles use a browser-standard generic +gamepad descriptor: 16 one-bit digital buttons including the d-pad, followed by +8-bit `X`, `Y`, `Rx`, `Ry`, `Z`, and `Rz` values so the sticks occupy the first +four axis slots and the analog triggers follow them. The Switch profile uses +the `Pro Controller` product name, and the DualSense profiles use the standard +`Wireless Controller` product name, so browser Gamepad API consumers see the +same ID strings they expect from physical devices. The Xbox 360 HID profile +keeps the legacy common descriptor with 12 one-bit digital buttons, a hat +switch for the d-pad, and 8-bit `X`, `Y`, `Z`, `Rx`, `Ry`, and `Rz` values. +On Windows, the UMDF/VHF backend rejects the Xbox 360 profile because a real +Xbox 360 controller is an XUSB device rather than a VHF HID gamepad; consumers +that still expose an Xbox 360 option should use their XUSB fallback for that +profile. The UMDF driver opens a separate VHF source target for each virtual gamepad and parents that target to the control-file handle that created it, so process exits or crashes clean up any virtual gamepads that were not explicitly destroyed. @@ -167,10 +174,10 @@ powershell -ExecutionPolicy Bypass -File .\scripts\windows\install-driver.ps1 ` -LogPath .\cmake-build-windows-driver\install-driver.log powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-installed-driver.ps1 ` -GamepadAdapterPath .\cmake-build-ci\examples\Debug\gamepad_adapter.exe ` - -Profile x360 + -Profile xseries powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-browser-gamepad.ps1 ` -GamepadAdapterPath .\cmake-build-ci\examples\Debug\gamepad_adapter.exe ` - -Profile x360 + -Profile xseries powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1 ` -Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" ``` @@ -188,14 +195,15 @@ to `C:\ProgramData\libvirtualhid\install-driver.log`. The test helper fails if the root device is not reported as `Status: Started`, if `\\.\LibVirtualHid` cannot be opened, or if a held gamepad adapter instance does not produce a started HID child device such as -`HID\VID_045E&PID_028E&IG_00`. That check is also run by the Windows MSVC pull -request CI leg for every `gamepad_adapter` profile after installing the test -driver package. The browser helper launches a normal desktop Edge or Chrome -instance at `https://hardwaretester.com/gamepad`, holds a virtual gamepad, and -fails if the browser Gamepad API does not report a controller matching the -selected profile or does not observe changing button and axis input. For manual -browser validation, run the browser helper with `-KeepBrowserOpen`, or run -`examples/gamepad_adapter x360 --hold-seconds 60`, then open +`HID\VID_045E&PID_0B13&IG_00`. That check is also run by the Windows MSVC pull +request CI leg for every Windows UMDF/VHF-supported `gamepad_adapter` profile +after installing the test driver package. The browser helper is for manual +diagnostics: it launches a normal desktop Edge or Chrome instance at +`https://hardwaretester.com/gamepad`, holds a virtual gamepad, and fails if the +browser Gamepad API does not report a controller matching the selected profile +or does not observe changing button and axis input. For manual browser +validation, run the browser helper with `-KeepBrowserOpen`, or run +`examples/gamepad_adapter xseries --hold-seconds 60`, then open `https://hardwaretester.com/gamepad` in a normal desktop browser and press one of the held virtual buttons if the browser needs a gamepad activation event. diff --git a/scripts/windows/test-browser-gamepad.ps1 b/scripts/windows/test-browser-gamepad.ps1 index 50b5770..410f555 100644 --- a/scripts/windows/test-browser-gamepad.ps1 +++ b/scripts/windows/test-browser-gamepad.ps1 @@ -8,7 +8,7 @@ param( [string] $GamepadAdapterPath, [ValidateSet("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch")] - [string] $Profile = "x360", + [string] $Profile = "xseries", [string] $BrowserPath, @@ -32,7 +32,7 @@ function Get-ExpectedGamepadIdPattern { switch ($Profile) { "generic" { return "(1209.*0001|vid[_ -]?1209.*pid[_ -]?0001|generic)" } "x360" { return "(045e.*028e|vid[_ -]?045e.*pid[_ -]?028e|x-?box.*360)" } - "xone" { return "(045e.*02ea|vid[_ -]?045e.*pid[_ -]?02ea|xbox one|x-box one)" } + "xone" { return "(045e.*02ff|vid[_ -]?045e.*pid[_ -]?02ff|xbox one|x-box one)" } "xseries" { return "(045e.*0b13|vid[_ -]?045e.*pid[_ -]?0b13|xbox wireless|xbox series)" } "ds4" { return "(054c.*05c4|vid[_ -]?054c.*pid[_ -]?05c4|dualshock|wireless controller)" } "ds5" { return "(054c.*0ce6|vid[_ -]?054c.*pid[_ -]?0ce6|dualsense|wireless controller)" } @@ -96,21 +96,46 @@ function Wait-ForDevToolsJson { function Wait-ForDevToolsPageTarget { param( [int] $Port, + [string] $ExpectedUrl, [int] $TimeoutSeconds ) $deadline = (Get-Date).AddSeconds($TimeoutSeconds) do { try { - $targets = @(Wait-ForDevToolsJson -Port $Port -Path "/json" -TimeoutSeconds 2) + $rawTargets = Wait-ForDevToolsJson -Port $Port -Path "/json" -TimeoutSeconds 2 + $targets = @($rawTargets) + if ($targets.Count -eq 1 -and $targets[0] -is [System.Array]) { + $targets = @($targets[0]) + } } catch { $targets = @() } $page = $targets | - Where-Object { $_.type -eq "page" -and $_.webSocketDebuggerUrl } | + Where-Object { + $_.type -eq "page" -and + $_.webSocketDebuggerUrl -and + $_.url -and + $_.url.StartsWith($ExpectedUrl, [System.StringComparison]::OrdinalIgnoreCase) + } | Select-Object -First 1 + if (-not $page) { + $page = $targets | + Where-Object { + $_.type -eq "page" -and + $_.webSocketDebuggerUrl -and + $_.url -and + -not $_.url.StartsWith("edge://", [System.StringComparison]::OrdinalIgnoreCase) -and + -not $_.url.StartsWith("chrome://", [System.StringComparison]::OrdinalIgnoreCase) + } | + Select-Object -First 1 + } if ($page) { - return $page + Write-Verbose "Using browser page target: $($page.url) ($($page.webSocketDebuggerUrl))" + return [pscustomobject] @{ + url = $page.url + webSocketDebuggerUrl = $page.webSocketDebuggerUrl + } } Start-Sleep -Milliseconds 250 @@ -130,7 +155,8 @@ function Invoke-DevToolsCommand { $socket = [System.Net.WebSockets.ClientWebSocket]::new() $cancellation = [System.Threading.CancellationTokenSource]::new([TimeSpan]::FromSeconds($TimeoutSeconds)) try { - $socket.ConnectAsync([Uri] $WebSocketDebuggerUrl, $cancellation.Token).GetAwaiter().GetResult() + $normalizedWebSocketDebuggerUrl = $WebSocketDebuggerUrl -replace "://localhost(:|/)", '://127.0.0.1$1' + $socket.ConnectAsync([Uri] $normalizedWebSocketDebuggerUrl, $cancellation.Token).GetAwaiter().GetResult() $id = [System.Threading.Interlocked]::Increment([ref] $script:DevToolsCommandId) $message = @{ @@ -324,6 +350,9 @@ if ($HoldSeconds -le $TimeoutSeconds) { $resolvedGamepadAdapterPath = (Resolve-Path -LiteralPath $GamepadAdapterPath).Path $resolvedBrowserPath = Resolve-BrowserPath $expectedPattern = if ($ExpectedIdPattern) { $ExpectedIdPattern } else { Get-ExpectedGamepadIdPattern } +if ($Profile -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" @@ -336,6 +365,7 @@ try { $browserArguments = @( "--user-data-dir=$browserUserDataDir", "--remote-debugging-port=$remoteDebuggingPort", + "--remote-allow-origins=*", "--no-first-run", "--no-default-browser-check", "--disable-background-timer-throttling", @@ -351,12 +381,11 @@ try { -PassThru [void] (Wait-ForDevToolsJson -Port $remoteDebuggingPort -Path "/json/version" -TimeoutSeconds $TimeoutSeconds) - $page = Wait-ForDevToolsPageTarget -Port $remoteDebuggingPort -TimeoutSeconds $TimeoutSeconds + $page = Wait-ForDevToolsPageTarget -Port $remoteDebuggingPort -ExpectedUrl $Url -TimeoutSeconds $TimeoutSeconds [void] (Invoke-DevToolsCommand ` -WebSocketDebuggerUrl $page.webSocketDebuggerUrl ` -Method "Page.bringToFront" ` -TimeoutSeconds 5) - $adapterProcess = Start-Process ` -FilePath $resolvedGamepadAdapterPath ` -WorkingDirectory (Split-Path -Parent $resolvedGamepadAdapterPath) ` @@ -373,6 +402,36 @@ try { throw "gamepad_adapter exited with code $($adapterProcess.ExitCode).`nstdout:`n$stdout`nstderr:`n$stderr" } + [void] (Invoke-DevToolsCommand ` + -WebSocketDebuggerUrl $page.webSocketDebuggerUrl ` + -Method "Runtime.evaluate" ` + -Params @{ + expression = "window.focus(); document.body && document.body.focus && document.body.focus();" + } ` + -TimeoutSeconds 5) + [void] (Invoke-DevToolsCommand ` + -WebSocketDebuggerUrl $page.webSocketDebuggerUrl ` + -Method "Input.dispatchMouseEvent" ` + -Params @{ + type = "mousePressed" + x = 32 + y = 32 + button = "left" + clickCount = 1 + } ` + -TimeoutSeconds 5) + [void] (Invoke-DevToolsCommand ` + -WebSocketDebuggerUrl $page.webSocketDebuggerUrl ` + -Method "Input.dispatchMouseEvent" ` + -Params @{ + type = "mouseReleased" + x = 32 + y = 32 + button = "left" + clickCount = 1 + } ` + -TimeoutSeconds 5) + $expression = New-GamepadApiProbeExpression ` -ExpectedIdPattern $expectedPattern ` -AllowAnyGamepad:$AllowAnyGamepad ` diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1 index d9ca749..41a9327 100644 --- a/scripts/windows/test-installed-driver.ps1 +++ b/scripts/windows/test-installed-driver.ps1 @@ -11,7 +11,7 @@ param( [string] $GamepadAdapterPath, [ValidateSet("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch")] - [string] $Profile = "x360", + [string] $Profile = "xseries", [int] $HoldSeconds = 12, @@ -238,7 +238,7 @@ function Get-ExpectedGamepadHardwareId { switch ($Profile) { "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" } + "xone" { return "HID\VID_045E&PID_02FF&IG_00" } "xseries" { return "HID\VID_045E&PID_0B13&IG_00" } "ds4" { return "HID\VID_054C&PID_05C4" } "ds5" { return "HID\VID_054C&PID_0CE6" } @@ -284,6 +284,10 @@ function Invoke-GamepadAdapterSmoke { return } + if ($Profile -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 $GamepadAdapterPath).Path $stdoutPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-gamepad-adapter.out" $stderrPath = Join-Path ([System.IO.Path]::GetTempPath()) "libvirtualhid-gamepad-adapter.err" diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index f0bdf1d..b82fe7e 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -18,6 +18,8 @@ namespace lvh::profiles { constexpr std::uint8_t common_button_count = 12; + constexpr std::uint8_t standard_button_count = 16; + constexpr std::uint8_t common_axis_count = 6; constexpr std::size_t common_button_bytes = 2; @@ -189,6 +191,90 @@ namespace lvh::profiles { return descriptor; } + std::vector make_standard_gamepad_report_descriptor( + std::uint8_t report_id, + bool supports_rumble + ) { + std::vector descriptor { + 0x05, + 0x01, // Usage Page (Generic Desktop) + 0x09, + 0x05, // Usage (Game Pad) + 0xA1, + 0x01, // Collection (Application) + 0x85, + report_id, // Report ID + 0x05, + 0x09, // Usage Page (Button) + 0x19, + 0x01, // Usage Minimum (Button 1) + 0x29, + standard_button_count, // Usage Maximum + 0x15, + 0x00, // Logical Minimum (0) + 0x25, + 0x01, // Logical Maximum (1) + 0x75, + 0x01, // Report Size (1) + 0x95, + standard_button_count, // Report Count + 0x81, + 0x02, // Input (Data,Var,Abs) + 0x05, + 0x01, // Usage Page (Generic Desktop) + 0x15, + 0x00, // Logical Minimum (0) + 0x26, + 0xFF, + 0x00, // Logical Maximum (255) + 0x75, + 0x08, // Report Size (8) + 0x95, + common_axis_count, // Report Count + 0x09, + 0x30, // Usage (X) + 0x09, + 0x31, // Usage (Y) + 0x09, + 0x33, // Usage (Rx) + 0x09, + 0x34, // Usage (Ry) + 0x09, + 0x32, // Usage (Z) + 0x09, + 0x35, // Usage (Rz) + 0x81, + 0x02, // Input (Data,Var,Abs) + }; + + if (supports_rumble) { + descriptor.insert( + descriptor.end(), + { + 0x06, + 0x00, + 0xFF, // Usage Page (Vendor Defined) + 0x09, + 0x01, // Usage (Vendor Usage 1) + 0x15, + 0x00, // Logical Minimum (0) + 0x26, + 0xFF, + 0x00, // Logical Maximum (255) + 0x75, + 0x08, // Report Size (8) + 0x95, + 0x04, // Report Count (4) + 0x91, + 0x02, // Output (Data,Var,Abs) + } + ); + } + + descriptor.push_back(0xC0); // End Collection + return descriptor; + } + std::vector make_playstation_common_gamepad_descriptor_prefix(std::uint8_t report_id) { return { // Usage Page (Generic Desktop) @@ -1654,6 +1740,35 @@ namespace lvh::profiles { return profile; } + DeviceProfile make_standard_gamepad_profile( + GamepadProfileKind kind, + std::string name, + std::string manufacturer, + std::uint16_t vendor_id, + std::uint16_t product_id, + std::uint16_t version, + GamepadProfileCapabilities capabilities + ) { + DeviceProfile profile; + profile.device_type = DeviceType::gamepad; + profile.gamepad_kind = kind; + profile.bus_type = BusType::usb; + profile.vendor_id = vendor_id; + profile.product_id = product_id; + profile.version = version; + profile.report_id = 1; + profile.input_report_size = common_report_size; + if (capabilities.supports_rumble) { + profile.output_report_size = common_output_report_size; + } + profile.name = std::move(name); + profile.manufacturer = std::move(manufacturer); + profile.capabilities = capabilities; + profile.report_descriptor = + make_standard_gamepad_report_descriptor(profile.report_id, profile.capabilities.supports_rumble); + return profile; + } + DeviceProfile make_xbox_gip_profile( GamepadProfileKind kind, std::string name, @@ -1718,7 +1833,7 @@ namespace lvh::profiles { bus_type == BusType::bluetooth ? dualsense_bluetooth_input_report_size : dualsense_usb_input_report_size; profile.output_report_size = bus_type == BusType::bluetooth ? dualsense_bluetooth_output_report_size : dualsense_usb_output_report_size; - profile.name = "DualSense Wireless Controller"; + profile.name = "Wireless Controller"; profile.manufacturer = "Sony Interactive Entertainment"; profile.capabilities = { .supports_rumble = true, @@ -1748,7 +1863,7 @@ namespace lvh::profiles { } // namespace DeviceProfile generic_gamepad() { - return make_gamepad_profile( + return make_standard_gamepad_profile( GamepadProfileKind::generic, "libvirtualhid Generic Gamepad", "LizardByte", @@ -1775,7 +1890,7 @@ namespace lvh::profiles { return make_xbox_gip_profile( GamepadProfileKind::xbox_one, "Xbox One Controller", - 0x02EA, + 0x02FF, 0x0408, false ); @@ -1813,14 +1928,14 @@ namespace lvh::profiles { DeviceProfile dualsense_bluetooth() { auto profile = make_dualsense_profile(BusType::bluetooth); - profile.name = "DualSense Wireless Controller"; + profile.name = "Wireless Controller"; return profile; } DeviceProfile switch_pro() { - return make_gamepad_profile( + return make_standard_gamepad_profile( GamepadProfileKind::switch_pro, - "Nintendo Switch Pro Controller", + "Pro Controller", "Nintendo Co., Ltd.", 0x057E, 0x2009, diff --git a/src/core/report.cpp b/src/core/report.cpp index 3e76dac..58246a4 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -199,6 +199,21 @@ namespace lvh::reports { return bits; } + std::uint16_t standard_gamepad_button_bits(const ButtonSet &buttons) { + auto bits = common_button_bits(buttons); + const auto set_bit = [&buttons, &bits](std::uint16_t bit, GamepadButton button) { + if (buttons.test(button)) { + bits |= static_cast(1U << bit); + } + }; + + set_bit(12U, GamepadButton::dpad_up); + set_bit(13U, GamepadButton::dpad_down); + set_bit(14U, GamepadButton::dpad_left); + set_bit(15U, GamepadButton::dpad_right); + return bits; + } + std::uint16_t xbox_gip_button_bits(const ButtonSet &buttons) { auto bits = std::uint16_t {}; const auto set_bit = [&buttons, &bits](std::uint16_t bit, GamepadButton button) { @@ -323,9 +338,9 @@ namespace lvh::reports { report[0] = is_bluetooth ? dualshock4_bt_input_report_id : to_byte(profile.report_id); report[payload_offset + 0U] = to_byte(normalize_u8_axis(normalized.left_stick.x)); - report[payload_offset + 1U] = to_byte(normalize_u8_axis(normalized.left_stick.y)); + report[payload_offset + 1U] = to_byte(normalize_u8_axis(-normalized.left_stick.y)); report[payload_offset + 2U] = to_byte(normalize_u8_axis(normalized.right_stick.x)); - report[payload_offset + 3U] = to_byte(normalize_u8_axis(normalized.right_stick.y)); + report[payload_offset + 3U] = to_byte(normalize_u8_axis(-normalized.right_stick.y)); report[payload_offset + 4U] = to_byte(hat_from_buttons(normalized.buttons)); if (normalized.buttons.test(GamepadButton::x)) { @@ -411,9 +426,9 @@ namespace lvh::reports { } report[payload_offset + 0U] = to_byte(normalize_u8_axis(normalized.left_stick.x)); - report[payload_offset + 1U] = to_byte(normalize_u8_axis(normalized.left_stick.y)); + report[payload_offset + 1U] = to_byte(normalize_u8_axis(-normalized.left_stick.y)); report[payload_offset + 2U] = to_byte(normalize_u8_axis(normalized.right_stick.x)); - report[payload_offset + 3U] = to_byte(normalize_u8_axis(normalized.right_stick.y)); + report[payload_offset + 3U] = to_byte(normalize_u8_axis(-normalized.right_stick.y)); report[payload_offset + 4U] = to_byte(normalize_trigger(normalized.left_trigger)); report[payload_offset + 5U] = to_byte(normalize_trigger(normalized.right_trigger)); report[payload_offset + 7U] = to_byte(hat_from_buttons(normalized.buttons)); @@ -742,9 +757,9 @@ namespace lvh::reports { ByteReport report(profile.input_report_size, zero_byte); write_u16(report, 0U, normalize_unsigned_axis(normalized.left_stick.x)); - write_u16(report, 2U, normalize_unsigned_axis(normalized.left_stick.y)); + write_u16(report, 2U, normalize_unsigned_axis(-normalized.left_stick.y)); write_u16(report, 4U, normalize_unsigned_axis(normalized.right_stick.x)); - write_u16(report, 6U, normalize_unsigned_axis(normalized.right_stick.y)); + write_u16(report, 6U, normalize_unsigned_axis(-normalized.right_stick.y)); write_u16(report, 8U, normalize_u10_trigger(normalized.left_trigger)); write_u16(report, 10U, normalize_u10_trigger(normalized.right_trigger)); write_u16(report, 12U, xbox_gip_button_bits(normalized.buttons)); @@ -756,6 +771,32 @@ namespace lvh::reports { return to_uint8_report(report); } + std::vector pack_standard_gamepad_input_report( + const DeviceProfile &profile, + const GamepadState &state + ) { + constexpr std::size_t standard_report_size = 9; + if (profile.input_report_size < standard_report_size) { + return {}; + } + + const auto normalized = normalize_state(state); + + std::vector report; + report.reserve(standard_report_size); + report.push_back(profile.report_id); + append_u16(report, standard_gamepad_button_bits(normalized.buttons)); + report.push_back(normalize_u8_axis(normalized.left_stick.x)); + report.push_back(normalize_u8_axis(-normalized.left_stick.y)); + report.push_back(normalize_u8_axis(normalized.right_stick.x)); + report.push_back(normalize_u8_axis(-normalized.right_stick.y)); + report.push_back(normalize_trigger(normalized.left_trigger)); + report.push_back(normalize_trigger(normalized.right_trigger)); + + report.resize(profile.input_report_size, 0); + return report; + } + std::vector pack_input_report(const DeviceProfile &profile, const GamepadState &state) { if (profile.device_type == DeviceType::gamepad) { if (profile.gamepad_kind == GamepadProfileKind::xbox_one || profile.gamepad_kind == GamepadProfileKind::xbox_series) { @@ -767,6 +808,9 @@ namespace lvh::reports { if (profile.gamepad_kind == GamepadProfileKind::dualsense) { return pack_dualsense_input_report(profile, state); } + if (profile.gamepad_kind == GamepadProfileKind::generic || profile.gamepad_kind == GamepadProfileKind::switch_pro) { + return pack_standard_gamepad_input_report(profile, state); + } } constexpr std::size_t common_report_size = 9; diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index f46ab7c..7e506ce 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -145,13 +145,7 @@ namespace lvh::detail { for (DWORD index = 0;; ++index) { SP_DEVICE_INTERFACE_DATA interface_data {}; interface_data.cbSize = sizeof(interface_data); - if (::SetupDiEnumDeviceInterfaces( - device_info_set, - nullptr, - &control_device_interface_guid, - index, - &interface_data - ) == FALSE) { + if (::SetupDiEnumDeviceInterfaces(device_info_set, nullptr, &control_device_interface_guid, index, &interface_data) == FALSE) { break; } @@ -171,14 +165,7 @@ namespace lvh::detail { std::vector buffer(required_size); auto *detail_data = reinterpret_cast(buffer.data()); detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); - if (::SetupDiGetDeviceInterfaceDetailA( - device_info_set, - &interface_data, - detail_data, - required_size, - nullptr, - nullptr - ) != FALSE) { + if (::SetupDiGetDeviceInterfaceDetailA(device_info_set, &interface_data, detail_data, required_size, nullptr, nullptr) != FALSE) { paths.emplace_back(detail_data->DevicePath); } } @@ -351,14 +338,7 @@ namespace lvh::detail { auto request_copy = request; DWORD bytes_returned = 0; - if (const auto status = device_io_control( - LVH_WINDOWS_IOCTL_CREATE_GAMEPAD, - request_copy, - response, - &bytes_returned, - "create Windows gamepad" - ); - !status.ok()) { + if (const auto status = device_io_control(LVH_WINDOWS_IOCTL_CREATE_GAMEPAD, request_copy, response, &bytes_returned, "create Windows gamepad"); !status.ok()) { return status; } @@ -414,17 +394,7 @@ namespace lvh::detail { overlapped.hEvent = operation_event.get(); DWORD bytes_returned = 0; - if (const auto started = ::DeviceIoControl( - handle_.get(), - LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT, - nullptr, - 0, - &event, - sizeof(event), - &bytes_returned, - &overlapped - ); - started == FALSE) { + if (const auto started = ::DeviceIoControl(handle_.get(), LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT, nullptr, 0, &event, sizeof(event), &bytes_returned, &overlapped); started == FALSE) { if (const auto error_code = ::GetLastError(); error_code != ERROR_IO_PENDING) { return std::nullopt; } @@ -453,9 +423,7 @@ namespace lvh::detail { return std::nullopt; } - if (constexpr auto event_header_size = - sizeof(event.version) + sizeof(event.size) + sizeof(event.driver_device_id) + sizeof(event.report_size); - bytes_returned < event_header_size) { + if (constexpr auto event_header_size = sizeof(event.version) + sizeof(event.size) + sizeof(event.driver_device_id) + sizeof(event.report_size); bytes_returned < event_header_size) { return std::nullopt; } @@ -478,16 +446,7 @@ namespace lvh::detail { ) const { using enum ErrorCode; - if (::DeviceIoControl( - handle_.get(), - control_code, - &input, - sizeof(input), - &output, - sizeof(output), - bytes_returned, - nullptr - ) == FALSE) { + if (::DeviceIoControl(handle_.get(), control_code, &input, sizeof(input), &output, sizeof(output), bytes_returned, nullptr) == FALSE) { return windows_failure(backend_failure, operation, ::GetLastError()); } @@ -503,16 +462,7 @@ namespace lvh::detail { ) const { using enum ErrorCode; - if (::DeviceIoControl( - handle_.get(), - control_code, - &input, - sizeof(input), - nullptr, - 0, - bytes_returned, - nullptr - ) == FALSE) { + if (::DeviceIoControl(handle_.get(), control_code, &input, sizeof(input), nullptr, 0, bytes_returned, nullptr) == FALSE) { return windows_failure(backend_failure, operation, ::GetLastError()); } @@ -950,6 +900,15 @@ namespace lvh::detail { }; } + if (options.profile.gamepad_kind == GamepadProfileKind::xbox_360) { + return { + unsupported_device_status( + "Windows UMDF/VHF backend cannot expose Xbox 360 XUSB gamepads; use an XUSB fallback for this profile" + ), + nullptr, + }; + } + return context_->create_gamepad(id, options); } diff --git a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp index 9ba6746..fe75b62 100644 --- a/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp +++ b/tests/fixtures/include/fixtures/windows_backend_test_hooks.hpp @@ -37,6 +37,7 @@ namespace lvh::detail::test { OperationStatus backend_failure_status; OperationStatus transport_failure_status; OperationStatus unavailable_status; + OperationStatus xbox_360_unsupported_status; OperationStatus oversized_descriptor_status; OperationStatus oversized_input_report_status; OperationStatus oversized_output_report_status; diff --git a/tests/fixtures/windows_backend_test_hooks.cpp b/tests/fixtures/windows_backend_test_hooks.cpp index 34a925f..7311d94 100644 --- a/tests/fixtures/windows_backend_test_hooks.cpp +++ b/tests/fixtures/windows_backend_test_hooks.cpp @@ -226,7 +226,7 @@ namespace lvh::detail { auto backend = make_fake_windows_backend(command_state, std::make_shared()); CreateGamepadOptions options; - options.profile = profiles::xbox_360(); + options.profile = profiles::xbox_series(); return backend->create_gamepad(1, options).status; } @@ -299,7 +299,7 @@ namespace lvh::detail { result.capabilities = backend->capabilities(); CreateGamepadOptions options; - options.profile = profiles::xbox_360(); + options.profile = profiles::xbox_series(); auto created = backend->create_gamepad(7, options); result.create_status = created.status; if (created) { @@ -351,7 +351,7 @@ namespace lvh::detail { auto backend = make_fake_windows_backend(command_state, std::make_shared()); CreateGamepadOptions options; - options.profile = profiles::xbox_360(); + options.profile = profiles::xbox_series(); result.transport_failure_status = backend->create_gamepad(2, options).status; } @@ -359,7 +359,7 @@ namespace lvh::detail { WindowsBackend backend {nullptr, nullptr}; CreateGamepadOptions options; - options.profile = profiles::xbox_360(); + options.profile = profiles::xbox_series(); result.unavailable_status = backend.create_gamepad(3, options).status; auto oversized_descriptor_options = options; @@ -375,12 +375,23 @@ namespace lvh::detail { result.oversized_output_report_status = backend.create_gamepad(19, oversized_output_options).status; } + { + auto backend = make_fake_windows_backend( + std::make_shared(), + std::make_shared() + ); + + CreateGamepadOptions options; + options.profile = profiles::xbox_360(); + result.xbox_360_unsupported_status = backend->create_gamepad(20, options).status; + } + { auto command_state = std::make_shared("", ""); auto backend = make_fake_windows_backend(command_state, std::make_shared()); CreateGamepadOptions options; - options.profile = profiles::xbox_360(); + options.profile = profiles::xbox_series(); auto created = backend->create_gamepad(4, options); result.empty_nodes_create_status = created.status; if (created) { diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index 0593923..7db7aab 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -33,7 +33,7 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { const auto switch_pro = lvh::profiles::switch_pro(); EXPECT_EQ(xbox_one.vendor_id, 0x045E); - EXPECT_EQ(xbox_one.product_id, 0x02EA); + EXPECT_EQ(xbox_one.product_id, 0x02FF); EXPECT_EQ(xbox_one.manufacturer, "Microsoft"); EXPECT_TRUE(xbox_one.capabilities.supports_rumble); EXPECT_EQ(xbox_one.report_id, 0); @@ -146,6 +146,7 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_NE(dualshock4_bluetooth.report_descriptor, dualshock4.report_descriptor); EXPECT_EQ(dualsense.vendor_id, 0x054C); + EXPECT_EQ(dualsense.name, "Wireless Controller"); EXPECT_TRUE(dualsense.capabilities.supports_motion); EXPECT_TRUE(dualsense.capabilities.supports_touchpad); EXPECT_TRUE(dualsense.capabilities.supports_rgb_led); @@ -156,6 +157,7 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { const auto dualsense_bluetooth = lvh::profiles::dualsense_bluetooth(); EXPECT_EQ(dualsense_bluetooth.bus_type, lvh::BusType::bluetooth); + EXPECT_EQ(dualsense_bluetooth.name, "Wireless Controller"); EXPECT_EQ(dualsense_bluetooth.report_id, 0x31); EXPECT_EQ(dualsense_bluetooth.input_report_size, 78U); EXPECT_EQ(dualsense_bluetooth.output_report_size, 78U); @@ -163,7 +165,51 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(switch_pro.vendor_id, 0x057E); EXPECT_EQ(switch_pro.product_id, 0x2009); + EXPECT_EQ(switch_pro.name, "Pro Controller"); EXPECT_EQ(switch_pro.manufacturer, "Nintendo Co., Ltd."); + + const auto generic = lvh::profiles::generic_gamepad(); + const std::array standard_button_descriptor { + 0x05, + 0x09, + 0x19, + 0x01, + 0x29, + 0x10, + 0x15, + 0x00, + 0x25, + 0x01, + 0x75, + 0x01, + }; + EXPECT_TRUE( + std::ranges::search(generic.report_descriptor, standard_button_descriptor).begin() != generic.report_descriptor.end() + ); + EXPECT_TRUE( + std::ranges::search(switch_pro.report_descriptor, standard_button_descriptor).begin() != switch_pro.report_descriptor.end() + ); + + const std::array standard_axis_order { + 0x09, + 0x30, + 0x09, + 0x31, + 0x09, + 0x33, + 0x09, + 0x34, + 0x09, + 0x32, + 0x09, + 0x35, + }; + EXPECT_TRUE( + std::ranges::search(generic.report_descriptor, standard_axis_order).begin() != generic.report_descriptor.end() + ); + EXPECT_TRUE( + std::ranges::search(switch_pro.report_descriptor, standard_axis_order).begin() != switch_pro.report_descriptor.end() + ); } TEST(ProfileTest, RumbleProfilesExposeOutputReports) { diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index 17d4b81..3b6d787 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -97,6 +97,34 @@ TEST(ReportTest, PacksCommonGamepadReport) { EXPECT_EQ(report[8], 255); // Right trigger. } +TEST(ReportTest, PacksStandardGamepadReport) { + auto profile = lvh::profiles::generic_gamepad(); + + lvh::GamepadState state; + state.buttons.set(lvh::GamepadButton::a); + state.buttons.set(lvh::GamepadButton::start); + state.buttons.set(lvh::GamepadButton::dpad_left); + state.buttons.set(lvh::GamepadButton::guide); + state.buttons.set(lvh::GamepadButton::misc1); + 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 report = lvh::reports::pack_input_report(profile, state); + + ASSERT_EQ(report.size(), profile.input_report_size); + EXPECT_EQ(report[0], profile.report_id); + EXPECT_EQ(report[1], 0x81); // A and Start. + EXPECT_EQ(report[2], 0x4C); // Guide, Misc/share, and D-pad-left button. + EXPECT_EQ(report[3], 255); // Left stick X. + EXPECT_EQ(report[4], 255); // Left stick Y. + EXPECT_EQ(report[5], 191); // Right stick X. + EXPECT_EQ(report[6], 191); // Right stick Y. + EXPECT_EQ(report[7], 64); // Left trigger. + EXPECT_EQ(report[8], 255); // Right trigger. +} + TEST(ReportTest, PacksXboxGipReport) { auto profile = lvh::profiles::xbox_series(); @@ -120,9 +148,9 @@ TEST(ReportTest, PacksXboxGipReport) { ASSERT_EQ(report.size(), profile.input_report_size); EXPECT_EQ(profile.report_id, 0); EXPECT_EQ(read_u16(0U), 0xFFFF); // Left stick X. - EXPECT_EQ(read_u16(2U), 0x0000); // Left stick Y. + EXPECT_EQ(read_u16(2U), 0xFFFF); // Left stick Y. EXPECT_EQ(read_u16(4U), 0xBFFF); // Right stick X. - EXPECT_EQ(read_u16(6U), 0x4000); // Right stick Y. + EXPECT_EQ(read_u16(6U), 0xBFFF); // Right stick Y. EXPECT_EQ(read_u16(8U), 256); // Left trigger. EXPECT_EQ(read_u16(10U), 1023); // Right trigger. EXPECT_EQ(read_u16(12U), 0x0881); // A, Start, and Share. @@ -171,7 +199,7 @@ TEST(ReportTest, PacksDualSenseUsbReport) { ASSERT_EQ(report.size(), profile.input_report_size); EXPECT_EQ(report[0], 1); EXPECT_EQ(report[1], 255); - EXPECT_EQ(report[2], 0); + EXPECT_EQ(report[2], 255); EXPECT_EQ(report[5], 255); EXPECT_EQ(report[8] & 0x20, 0x20); EXPECT_EQ(report[9] & 0x05, 0x05); @@ -196,7 +224,7 @@ TEST(ReportTest, PacksDualSenseBluetoothReportWithCrc) { EXPECT_EQ(report[0], 0x31); EXPECT_EQ(report[1], 0x00); EXPECT_EQ(report[2], 255); - EXPECT_EQ(report[3], 0); + EXPECT_EQ(report[3], 255); EXPECT_EQ(report[7], 255); EXPECT_EQ(report[9] & 0x20, 0x20); EXPECT_EQ(report[10] & 0x08, 0x08); @@ -240,7 +268,7 @@ TEST(ReportTest, PacksDualShock4UsbReport) { ASSERT_EQ(report.size(), profile.input_report_size); EXPECT_EQ(report[0], 0x01); EXPECT_EQ(report[1], 255); - EXPECT_EQ(report[2], 0); + EXPECT_EQ(report[2], 255); EXPECT_EQ(report[3], 128); EXPECT_EQ(report[4], 128); EXPECT_EQ(report[5], 0xF8); diff --git a/tests/unit/test_runtime.cpp b/tests/unit/test_runtime.cpp index 8c85e77..7e19466 100644 --- a/tests/unit/test_runtime.cpp +++ b/tests/unit/test_runtime.cpp @@ -66,7 +66,9 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) { EXPECT_FALSE(created); EXPECT_EQ(created.status.code(), lvh::ErrorCode::backend_unavailable); } else { - auto created = runtime->create_gamepad(lvh::profiles::xbox_360()); + EXPECT_EQ(runtime->create_gamepad(lvh::profiles::xbox_360()).status.code(), lvh::ErrorCode::unsupported_profile); + + auto created = runtime->create_gamepad(lvh::profiles::xbox_series()); ASSERT_TRUE(created) << created.status.message(); ASSERT_NE(created.gamepad, nullptr); EXPECT_FALSE(created.gamepad->device_nodes().empty()); @@ -82,15 +84,15 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) { EXPECT_EQ(created.gamepad->submit(state).code(), lvh::ErrorCode::device_closed); } - auto invalid_profile = lvh::profiles::xbox_360(); + auto invalid_profile = lvh::profiles::xbox_series(); invalid_profile.report_descriptor.resize(LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE + 1U); EXPECT_EQ(runtime->create_gamepad(invalid_profile).status.code(), lvh::ErrorCode::invalid_argument); - invalid_profile = lvh::profiles::xbox_360(); + invalid_profile = lvh::profiles::xbox_series(); invalid_profile.input_report_size = LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 1U; EXPECT_EQ(runtime->create_gamepad(invalid_profile).status.code(), lvh::ErrorCode::invalid_argument); - invalid_profile = lvh::profiles::xbox_360(); + invalid_profile = lvh::profiles::xbox_series(); invalid_profile.output_report_size = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE + 1U; EXPECT_EQ(runtime->create_gamepad(invalid_profile).status.code(), lvh::ErrorCode::invalid_argument); #else diff --git a/tests/unit/test_windows_backend.cpp b/tests/unit/test_windows_backend.cpp index 5387fc2..e52462d 100644 --- a/tests/unit/test_windows_backend.cpp +++ b/tests/unit/test_windows_backend.cpp @@ -57,7 +57,7 @@ TEST_F(WindowsBackendTest, FakeChannelExercisesLifecycleSubmitCloseAndOutput) { EXPECT_EQ(result.last_output.low_frequency_rumble, 0x5678U); EXPECT_EQ(result.last_output.high_frequency_rumble, 0x1234U); ASSERT_GE(result.last_output.raw_report.size(), 5U); - EXPECT_EQ(result.last_output.raw_report[0], 1U); + EXPECT_EQ(result.last_output.raw_report[0], 0U); } TEST_F(WindowsBackendTest, FakeChannelCoversCreateFailureBranches) { @@ -69,6 +69,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_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); From 2b35a10d3c091d26f7b6011c1beab0b624d93e88 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 18:42:30 -0400 Subject: [PATCH 17/27] Align Xbox IDs and add Switch Pro HID reports Update built-in profile identities so Xbox One uses VID/PID 045E:02EA and Xbox Series uses 045E:02FF, with USB bus metadata and matching Windows driver-test expectations. Replace the Switch Pro generic descriptor/profile with a dedicated HID descriptor and 64-byte report format, and add Switch-specific input report packing (buttons, hat, 16-bit stick axes, trigger-click bits). Refresh README details and unit tests to validate the new profile metadata, descriptor shape, output capabilities, and packed report bytes. --- README.md | 27 +++++++----- scripts/windows/test-installed-driver.ps1 | 4 +- src/core/profiles.cpp | 53 +++++++++++++++++------ src/core/report.cpp | 53 ++++++++++++++++++++++- tests/unit/test_gamepad_adapter.cpp | 10 +++++ tests/unit/test_profiles.cpp | 42 +++++++++++++++--- tests/unit/test_report.cpp | 30 +++++++++++++ 7 files changed, 186 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index ac0107b..163c0b7 100644 --- a/README.md +++ b/README.md @@ -131,17 +131,24 @@ VID/PID/version, explicit descriptor for the child HID device so Windows and browser consumers can identify the selected profile instead of a generic VHF-only device. The built-in Xbox One and Xbox Series profiles use an XboxGIP-shaped descriptor -and unnumbered 17-byte input reports derived from HIDMaestro's Xbox profiles, -with WinUHid's Xbox One PID and hardware-ID shape used for that profile. The -built-in generic and Switch HID profiles use a browser-standard generic -gamepad descriptor: 16 one-bit digital buttons including the d-pad, followed by -8-bit `X`, `Y`, `Rx`, `Ry`, `Z`, and `Rz` values so the sticks occupy the first -four axis slots and the analog triggers follow them. The Switch profile uses -the `Pro Controller` product name, and the DualSense profiles use the standard +and unnumbered 17-byte input reports derived from HIDMaestro's USB Xbox +profiles. The Xbox One profile uses `VID_045E&PID_02EA`, and the Xbox Series +profile uses HIDMaestro's GIP HID `driverPid` identity `VID_045E&PID_02FF`. +Bluetooth Xbox identities are intentionally not used for the built-in profiles. +The physical USB Xbox Series parent ID is `VID_045E&PID_0B12`, but Windows' +`xinputhid.inf` does not bind `HID\VID_045E&PID_0B12&IG_00` VHF children to +XInput. The built-in generic profile uses a browser-standard generic gamepad +descriptor: 16 one-bit digital buttons including the d-pad, followed by 8-bit +`X`, `Y`, `Rx`, `Ry`, `Z`, and `Rz` values so the sticks occupy the first four +axis slots and the analog triggers follow them. The Switch profile uses +HIDMaestro's Nintendo Switch Pro Controller identity (`VID_057E&PID_2009`, +product name `Pro Controller`) with Report ID `0x30`, a 64-byte input report, a +hat d-pad, four 16-bit stick axes, and digital ZL/ZR trigger-click bits rather +than analog trigger axes. The DualSense profiles use the standard `Wireless Controller` product name, so browser Gamepad API consumers see the same ID strings they expect from physical devices. The Xbox 360 HID profile -keeps the legacy common descriptor with 12 one-bit digital buttons, a hat -switch for the d-pad, and 8-bit `X`, `Y`, `Z`, `Rx`, `Ry`, and `Rz` values. +keeps the legacy common descriptor with 12 one-bit digital buttons, a hat switch +for the d-pad, and 8-bit `X`, `Y`, `Z`, `Rx`, `Ry`, and `Rz` values. On Windows, the UMDF/VHF backend rejects the Xbox 360 profile because a real Xbox 360 controller is an XUSB device rather than a VHF HID gamepad; consumers that still expose an Xbox 360 option should use their XUSB fallback for that @@ -195,7 +202,7 @@ to `C:\ProgramData\libvirtualhid\install-driver.log`. The test helper fails if the root device is not reported as `Status: Started`, if `\\.\LibVirtualHid` cannot be opened, or if a held gamepad adapter instance does not produce a started HID child device such as -`HID\VID_045E&PID_0B13&IG_00`. That check is also run by the Windows MSVC pull +`HID\VID_045E&PID_02FF&IG_00`. That check is also run by the Windows MSVC pull request CI leg for every Windows UMDF/VHF-supported `gamepad_adapter` profile after installing the test driver package. The browser helper is for manual diagnostics: it launches a normal desktop Edge or Chrome instance at diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1 index 41a9327..6e985a4 100644 --- a/scripts/windows/test-installed-driver.ps1 +++ b/scripts/windows/test-installed-driver.ps1 @@ -238,8 +238,8 @@ function Get-ExpectedGamepadHardwareId { switch ($Profile) { "generic" { return "HID\VID_1209&PID_0001" } "x360" { return "HID\VID_045E&PID_028E&IG_00" } - "xone" { return "HID\VID_045E&PID_02FF&IG_00" } - "xseries" { return "HID\VID_045E&PID_0B13&IG_00" } + "xone" { return "HID\VID_045E&PID_02EA&IG_00" } + "xseries" { return "HID\VID_045E&PID_02FF&IG_00" } "ds4" { return "HID\VID_054C&PID_05C4" } "ds5" { return "HID\VID_054C&PID_0CE6" } "switch" { return "HID\VID_057E&PID_2009" } diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index b82fe7e..f0cfdaa 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -30,6 +30,12 @@ namespace lvh::profiles { constexpr std::size_t xbox_gip_input_report_size = 17; + constexpr std::uint8_t switch_pro_report_id = 0x30; + + constexpr std::size_t switch_pro_input_report_size = 64; + + constexpr std::size_t switch_pro_output_report_size = 64; + constexpr std::size_t dualshock4_usb_input_report_size = 64; constexpr std::size_t dualshock4_usb_output_report_size = 32; @@ -87,6 +93,17 @@ namespace lvh::profiles { return bytes_from_hex(include_share_button ? xbox_series_descriptor : xbox_one_descriptor); } + std::vector make_switch_pro_report_descriptor() { + constexpr std::string_view descriptor = + "050115000904a1018530050105091901290a150025017501950a5500650081020509190b290e150025017501" + "950481027501950281030b01000100a1000b300001000b310001000b320001000b35000100150027ffff0000" + "751095048102c00b39000100150025073500463b0165147504950181020509190f291215002501750195048102" + "7508953481030600ff852109017508953f8103858109027508953f8103850109037508953f9183851009047508" + "953f9183858009057508953f9183858209067508953f9183c0"; + + return bytes_from_hex(descriptor); + } + std::vector make_gamepad_report_descriptor(std::uint8_t report_id, bool supports_rumble) { std::vector descriptor { 0x05, @@ -1779,7 +1796,7 @@ namespace lvh::profiles { DeviceProfile profile; profile.device_type = DeviceType::gamepad; profile.gamepad_kind = kind; - profile.bus_type = include_share_button ? BusType::bluetooth : BusType::usb; + profile.bus_type = BusType::usb; profile.vendor_id = 0x045E; profile.product_id = product_id; profile.version = version; @@ -1848,6 +1865,24 @@ namespace lvh::profiles { return profile; } + DeviceProfile make_switch_pro_profile() { + DeviceProfile profile; + profile.device_type = DeviceType::gamepad; + profile.gamepad_kind = GamepadProfileKind::switch_pro; + profile.bus_type = BusType::usb; + profile.vendor_id = 0x057E; + profile.product_id = 0x2009; + profile.version = 0x8111; + profile.report_id = switch_pro_report_id; + profile.input_report_size = switch_pro_input_report_size; + profile.output_report_size = switch_pro_output_report_size; + profile.name = "Pro Controller"; + profile.manufacturer = "Nintendo Co., Ltd."; + profile.capabilities = {.supports_motion = true, .supports_battery = true}; + profile.report_descriptor = make_switch_pro_report_descriptor(); + return profile; + } + DeviceProfile make_simple_profile(DeviceType device_type, std::string name, std::uint16_t product_id) { DeviceProfile profile; profile.device_type = device_type; @@ -1890,7 +1925,7 @@ namespace lvh::profiles { return make_xbox_gip_profile( GamepadProfileKind::xbox_one, "Xbox One Controller", - 0x02FF, + 0x02EA, 0x0408, false ); @@ -1899,8 +1934,8 @@ namespace lvh::profiles { DeviceProfile xbox_series() { return make_xbox_gip_profile( GamepadProfileKind::xbox_series, - "Xbox Wireless Controller", - 0x0B13, + "Xbox Controller", + 0x02FF, 0x0500, true ); @@ -1933,15 +1968,7 @@ namespace lvh::profiles { } DeviceProfile switch_pro() { - return make_standard_gamepad_profile( - GamepadProfileKind::switch_pro, - "Pro Controller", - "Nintendo Co., Ltd.", - 0x057E, - 0x2009, - 0x8111, - {.supports_rumble = true, .supports_motion = true, .supports_battery = true} - ); + return make_switch_pro_profile(); } DeviceProfile keyboard() { diff --git a/src/core/report.cpp b/src/core/report.cpp index 58246a4..8a2ac75 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -236,6 +236,35 @@ namespace lvh::reports { return bits; } + std::uint16_t switch_pro_button_bits(const GamepadState &state) { + auto bits = std::uint16_t {}; + const auto set_bit = [&state, &bits](std::uint16_t bit, GamepadButton button) { + if (state.buttons.test(button)) { + bits |= static_cast(1U << bit); + } + }; + + set_bit(0U, GamepadButton::a); + set_bit(1U, GamepadButton::b); + set_bit(2U, GamepadButton::x); + set_bit(3U, GamepadButton::y); + set_bit(4U, GamepadButton::left_shoulder); + set_bit(5U, GamepadButton::right_shoulder); + if (state.left_trigger > 0.0F) { + bits |= static_cast(1U << 6U); + } + if (state.right_trigger > 0.0F) { + bits |= static_cast(1U << 7U); + } + set_bit(8U, GamepadButton::back); + set_bit(9U, GamepadButton::start); + set_bit(10U, GamepadButton::left_stick); + set_bit(11U, GamepadButton::right_stick); + set_bit(12U, GamepadButton::guide); + set_bit(13U, GamepadButton::misc1); + return bits; + } + std::byte dualsense_battery_state(GamepadBatteryState state) { switch (state) { using enum GamepadBatteryState; @@ -797,6 +826,25 @@ namespace lvh::reports { return report; } + std::vector pack_switch_pro_input_report(const DeviceProfile &profile, const GamepadState &state) { + constexpr std::size_t switch_pro_input_report_size = 64; + if (profile.input_report_size < switch_pro_input_report_size) { + return {}; + } + + const auto normalized = normalize_state(state); + + ByteReport report(profile.input_report_size, zero_byte); + report[0] = to_byte(profile.report_id); + write_u16(report, 1U, switch_pro_button_bits(normalized)); + write_u16(report, 3U, normalize_unsigned_axis(normalized.left_stick.x)); + write_u16(report, 5U, normalize_unsigned_axis(-normalized.left_stick.y)); + write_u16(report, 7U, normalize_unsigned_axis(normalized.right_stick.x)); + write_u16(report, 9U, normalize_unsigned_axis(-normalized.right_stick.y)); + report[11] = to_byte(hat_from_buttons(normalized.buttons)); + return to_uint8_report(report); + } + std::vector pack_input_report(const DeviceProfile &profile, const GamepadState &state) { if (profile.device_type == DeviceType::gamepad) { if (profile.gamepad_kind == GamepadProfileKind::xbox_one || profile.gamepad_kind == GamepadProfileKind::xbox_series) { @@ -808,7 +856,10 @@ namespace lvh::reports { if (profile.gamepad_kind == GamepadProfileKind::dualsense) { return pack_dualsense_input_report(profile, state); } - if (profile.gamepad_kind == GamepadProfileKind::generic || profile.gamepad_kind == GamepadProfileKind::switch_pro) { + if (profile.gamepad_kind == GamepadProfileKind::switch_pro) { + return pack_switch_pro_input_report(profile, state); + } + if (profile.gamepad_kind == GamepadProfileKind::generic) { return pack_standard_gamepad_input_report(profile, state); } } diff --git a/tests/unit/test_gamepad_adapter.cpp b/tests/unit/test_gamepad_adapter.cpp index 46cd334..9f207ab 100644 --- a/tests/unit/test_gamepad_adapter.cpp +++ b/tests/unit/test_gamepad_adapter.cpp @@ -16,6 +16,7 @@ TEST(GamepadAdapterTest, ReportsProfileSupport) { const auto generic = lvh::profiles::generic_gamepad(); const auto dualshock4 = lvh::profiles::dualshock4(); const auto dualsense = lvh::profiles::dualsense(); + const auto switch_pro = lvh::profiles::switch_pro(); const auto keyboard = lvh::profiles::keyboard(); const auto generic_support = lvh::gamepad_profile_support(generic); @@ -44,6 +45,12 @@ TEST(GamepadAdapterTest, ReportsProfileSupport) { EXPECT_TRUE(dualsense_support.supports_misc1_button); EXPECT_EQ(dualsense_support.supported_rear_paddle_count, 0U); + const auto switch_pro_support = lvh::gamepad_profile_support(switch_pro); + EXPECT_FALSE(switch_pro_support.supports_rumble); + EXPECT_TRUE(switch_pro_support.supports_motion); + EXPECT_TRUE(switch_pro_support.supports_battery); + EXPECT_TRUE(switch_pro_support.supports_misc1_button); + const auto keyboard_support = lvh::gamepad_profile_support(keyboard); EXPECT_FALSE(keyboard_support.supports_rumble); EXPECT_FALSE(keyboard_support.supports_motion); @@ -61,6 +68,7 @@ TEST(GamepadAdapterTest, ChecksButtonsAndOutputsByProfile) { const auto generic = lvh::profiles::generic_gamepad(); const auto dualshock4 = lvh::profiles::dualshock4(); const auto dualsense = lvh::profiles::dualsense(); + const auto switch_pro = lvh::profiles::switch_pro(); const auto keyboard = lvh::profiles::keyboard(); EXPECT_TRUE(lvh::supports_gamepad_button(xbox, lvh::GamepadButton::guide)); @@ -83,6 +91,8 @@ TEST(GamepadAdapterTest, ChecksButtonsAndOutputsByProfile) { EXPECT_FALSE(lvh::supports_gamepad_output(dualshock4, lvh::GamepadOutputKind::trigger_rumble)); EXPECT_TRUE(lvh::supports_gamepad_output(dualshock4, lvh::GamepadOutputKind::raw_report)); EXPECT_TRUE(lvh::supports_gamepad_output(dualsense, lvh::GamepadOutputKind::adaptive_triggers)); + EXPECT_FALSE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::rumble)); + EXPECT_TRUE(lvh::supports_gamepad_output(switch_pro, lvh::GamepadOutputKind::raw_report)); EXPECT_FALSE(lvh::supports_gamepad_output(generic, lvh::GamepadOutputKind::raw_report)); EXPECT_FALSE(lvh::supports_gamepad_output(keyboard, lvh::GamepadOutputKind::rumble)); EXPECT_FALSE(lvh::supports_gamepad_output(generic, static_cast(255))); diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index 7db7aab..149fb1f 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -33,7 +33,8 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { const auto switch_pro = lvh::profiles::switch_pro(); EXPECT_EQ(xbox_one.vendor_id, 0x045E); - EXPECT_EQ(xbox_one.product_id, 0x02FF); + EXPECT_EQ(xbox_one.product_id, 0x02EA); + EXPECT_EQ(xbox_one.bus_type, lvh::BusType::usb); EXPECT_EQ(xbox_one.manufacturer, "Microsoft"); EXPECT_TRUE(xbox_one.capabilities.supports_rumble); EXPECT_EQ(xbox_one.report_id, 0); @@ -41,8 +42,9 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { const auto xbox_series = lvh::profiles::xbox_series(); EXPECT_EQ(xbox_series.vendor_id, 0x045E); - EXPECT_EQ(xbox_series.product_id, 0x0B13); - EXPECT_EQ(xbox_series.name, "Xbox Wireless Controller"); + EXPECT_EQ(xbox_series.product_id, 0x02FF); + EXPECT_EQ(xbox_series.bus_type, lvh::BusType::usb); + EXPECT_EQ(xbox_series.name, "Xbox Controller"); EXPECT_EQ(xbox_series.manufacturer, "Microsoft"); EXPECT_EQ(xbox_series.report_id, 0); EXPECT_EQ(xbox_series.input_report_size, 17U); @@ -167,6 +169,12 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(switch_pro.product_id, 0x2009); EXPECT_EQ(switch_pro.name, "Pro Controller"); EXPECT_EQ(switch_pro.manufacturer, "Nintendo Co., Ltd."); + EXPECT_EQ(switch_pro.report_id, 0x30); + EXPECT_EQ(switch_pro.input_report_size, 64U); + EXPECT_EQ(switch_pro.output_report_size, 64U); + EXPECT_FALSE(switch_pro.capabilities.supports_rumble); + EXPECT_TRUE(switch_pro.capabilities.supports_motion); + EXPECT_TRUE(switch_pro.capabilities.supports_battery); const auto generic = lvh::profiles::generic_gamepad(); const std::array standard_button_descriptor { @@ -186,9 +194,6 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_TRUE( std::ranges::search(generic.report_descriptor, standard_button_descriptor).begin() != generic.report_descriptor.end() ); - EXPECT_TRUE( - std::ranges::search(switch_pro.report_descriptor, standard_button_descriptor).begin() != switch_pro.report_descriptor.end() - ); const std::array standard_axis_order { 0x09, @@ -207,8 +212,31 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_TRUE( std::ranges::search(generic.report_descriptor, standard_axis_order).begin() != generic.report_descriptor.end() ); + EXPECT_NE(switch_pro.report_descriptor, generic.report_descriptor); + + const std::array switch_pro_report_id_descriptor {0x85, 0x30}; + EXPECT_TRUE( + std::ranges::search(switch_pro.report_descriptor, switch_pro_report_id_descriptor).begin() != + switch_pro.report_descriptor.end() + ); + + const std::array switch_pro_button_descriptor { + 0x19, + 0x01, + 0x29, + 0x0A, + 0x15, + 0x00, + 0x25, + 0x01, + 0x75, + 0x01, + 0x95, + 0x0A, + }; EXPECT_TRUE( - std::ranges::search(switch_pro.report_descriptor, standard_axis_order).begin() != switch_pro.report_descriptor.end() + std::ranges::search(switch_pro.report_descriptor, switch_pro_button_descriptor).begin() != + switch_pro.report_descriptor.end() ); } diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index 3b6d787..869a114 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -38,6 +38,10 @@ namespace { (static_cast(bytes[offset + 2U]) << 16U) | (static_cast(bytes[offset + 3U]) << 24U); } + + std::uint16_t read_u16_le(const std::vector &bytes, std::size_t offset) { + return static_cast(bytes[offset] | static_cast(bytes[offset + 1U] << 8U)); + } } // namespace TEST(ReportTest, NormalizesAxesAndTriggers) { @@ -159,6 +163,32 @@ TEST(ReportTest, PacksXboxGipReport) { EXPECT_EQ(report[16], 204); // Battery strength. } +TEST(ReportTest, PacksSwitchProReport) { + auto profile = lvh::profiles::switch_pro(); + + lvh::GamepadState state; + state.buttons.set(lvh::GamepadButton::a); + state.buttons.set(lvh::GamepadButton::b); + state.buttons.set(lvh::GamepadButton::start); + state.buttons.set(lvh::GamepadButton::guide); + state.buttons.set(lvh::GamepadButton::misc1); + state.buttons.set(lvh::GamepadButton::dpad_left); + state.left_stick = {1.0F, -1.0F}; + state.right_stick = {0.5F, -0.5F}; + state.left_trigger = 0.25F; + + const auto report = lvh::reports::pack_input_report(profile, state); + + ASSERT_EQ(report.size(), profile.input_report_size); + EXPECT_EQ(report[0], 0x30); + EXPECT_EQ(read_u16_le(report, 1U), 0x3243); // B, A, ZL, Plus, Home, and Capture. + EXPECT_EQ(read_u16_le(report, 3U), 0xFFFF); // Left stick X. + EXPECT_EQ(read_u16_le(report, 5U), 0xFFFF); // Left stick Y. + EXPECT_EQ(read_u16_le(report, 7U), 0xBFFF); // Right stick X. + EXPECT_EQ(read_u16_le(report, 9U), 0xBFFF); // Right stick Y. + EXPECT_EQ(report[11] & 0x0F, 6); // D-pad left. +} + TEST(ReportTest, PacksXboxGipNeutralReport) { const auto profile = lvh::profiles::xbox_series(); From 221ab80c6bc9ff1269abc64d809a2572adc59776 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 19:15:17 -0400 Subject: [PATCH 18/27] Switch Xbox Series PID to 0x0B12, add GIP match ID Change the Xbox Series public profile PID from HIDMaestro's GIP driverPid (0x02FF) to the physical USB identity (0x0B12). The UMDF driver now publishes 0x02FF as an additional hardware ID (HID\VID_045E&PID_02FF&IG_00) for xinputhid.inf matching while keeping the profile PID at 0x0B12. Update test scripts, browser gamepad ID patterns, and unit tests to match the new identity. --- README.md | 20 ++++++++----- examples/gamepad_adapter.cpp | 7 +++-- scripts/windows/test-browser-gamepad.ps1 | 4 +-- scripts/windows/test-installed-driver.ps1 | 28 +++++++++++-------- src/core/profiles.cpp | 2 +- .../windows/driver/libvirtualhid_umdf.cpp | 24 ++++++++++++---- tests/unit/test_profiles.cpp | 2 +- 7 files changed, 56 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index 163c0b7..952215a 100644 --- a/README.md +++ b/README.md @@ -129,15 +129,20 @@ VID/PID/version, explicit `HID\VID_....&PID_....` hardware IDs, Xbox `HID\VID_....&PID_....&IG_00` hardware IDs where applicable, and the report descriptor for the child HID device so Windows and browser consumers can -identify the selected profile instead of a generic VHF-only device. +match the selected profile by HID attributes and report shape. VHF does not +provide a product/manufacturer string callback, so consumers that display the +raw HID product string may still show the Windows VHF product label even when +the VID/PID and descriptor match the selected controller. The built-in Xbox One and Xbox Series profiles use an XboxGIP-shaped descriptor and unnumbered 17-byte input reports derived from HIDMaestro's USB Xbox profiles. The Xbox One profile uses `VID_045E&PID_02EA`, and the Xbox Series -profile uses HIDMaestro's GIP HID `driverPid` identity `VID_045E&PID_02FF`. -Bluetooth Xbox identities are intentionally not used for the built-in profiles. -The physical USB Xbox Series parent ID is `VID_045E&PID_0B12`, but Windows' +profile uses the physical USB identity `VID_045E&PID_0B12`. Bluetooth Xbox +identities are intentionally not used for the built-in profiles. Windows' `xinputhid.inf` does not bind `HID\VID_045E&PID_0B12&IG_00` VHF children to -XInput. The built-in generic profile uses a browser-standard generic gamepad +XInput, so the UMDF driver also publishes HIDMaestro's GIP HID `driverPid` +identity `HID\VID_045E&PID_02FF&IG_00` as a driver-matching hardware ID while +keeping the public profile PID at `0B12`. The built-in generic profile uses a +browser-standard generic gamepad descriptor: 16 one-bit digital buttons including the d-pad, followed by 8-bit `X`, `Y`, `Rx`, `Ry`, `Z`, and `Rz` values so the sticks occupy the first four axis slots and the analog triggers follow them. The Switch profile uses @@ -145,8 +150,8 @@ HIDMaestro's Nintendo Switch Pro Controller identity (`VID_057E&PID_2009`, product name `Pro Controller`) with Report ID `0x30`, a 64-byte input report, a hat d-pad, four 16-bit stick axes, and digital ZL/ZR trigger-click bits rather than analog trigger axes. The DualSense profiles use the standard -`Wireless Controller` product name, so browser Gamepad API consumers see the -same ID strings they expect from physical devices. The Xbox 360 HID profile +`Wireless Controller` product name in the public profile and control protocol. +The Xbox 360 HID profile keeps the legacy common descriptor with 12 one-bit digital buttons, a hat switch for the d-pad, and 8-bit `X`, `Y`, `Z`, `Rx`, `Ry`, and `Rz` values. On Windows, the UMDF/VHF backend rejects the Xbox 360 profile because a real @@ -202,6 +207,7 @@ to `C:\ProgramData\libvirtualhid\install-driver.log`. The test helper fails if the root device is not reported as `Status: Started`, if `\\.\LibVirtualHid` cannot be opened, or if a held gamepad adapter instance does not produce a started HID child device such as +`HID\VID_045E&PID_0B12&IG_00` or the Xbox Series xinputhid match identity `HID\VID_045E&PID_02FF&IG_00`. That check is also run by the Windows MSVC pull request CI leg for every Windows UMDF/VHF-supported `gamepad_adapter` profile after installing the test driver package. The browser helper is for manual diff --git a/examples/gamepad_adapter.cpp b/examples/gamepad_adapter.cpp index ca02873..80df02d 100644 --- a/examples/gamepad_adapter.cpp +++ b/examples/gamepad_adapter.cpp @@ -124,8 +124,11 @@ int main(int argc, char *argv[]) { std::cout << "Holding " << profile->name << " for " << hold_seconds << " seconds\n"; for (auto step = 0; step < hold_seconds * 5; ++step) { const auto direction = step % 40 < 20 ? 1.0F : -1.0F; - adapter.set_left_stick({direction, 0.0F}); - adapter.set_right_stick({0.0F, -direction}); + const auto sweep = static_cast(step % 20) / 19.0F; + adapter.set_left_stick({direction, direction * 0.5F}); + adapter.set_right_stick({-direction * 0.5F, -direction}); + adapter.set_left_trigger(sweep); + adapter.set_right_trigger(1.0F - sweep); adapter.set_button(lvh::GamepadButton::a, step % 20 < 10); std::this_thread::sleep_for(200ms); } diff --git a/scripts/windows/test-browser-gamepad.ps1 b/scripts/windows/test-browser-gamepad.ps1 index 410f555..3e0b525 100644 --- a/scripts/windows/test-browser-gamepad.ps1 +++ b/scripts/windows/test-browser-gamepad.ps1 @@ -32,8 +32,8 @@ function Get-ExpectedGamepadIdPattern { switch ($Profile) { "generic" { return "(1209.*0001|vid[_ -]?1209.*pid[_ -]?0001|generic)" } "x360" { return "(045e.*028e|vid[_ -]?045e.*pid[_ -]?028e|x-?box.*360)" } - "xone" { return "(045e.*02ff|vid[_ -]?045e.*pid[_ -]?02ff|xbox one|x-box one)" } - "xseries" { return "(045e.*0b13|vid[_ -]?045e.*pid[_ -]?0b13|xbox wireless|xbox series)" } + "xone" { return "(045e.*02ea|vid[_ -]?045e.*pid[_ -]?02ea|xbox one|x-box one)" } + "xseries" { return "(045e.*0b12|045e.*02ff|vid[_ -]?045e.*pid[_ -]?0b12|vid[_ -]?045e.*pid[_ -]?02ff|xbox wireless|xbox series)" } "ds4" { return "(054c.*05c4|vid[_ -]?054c.*pid[_ -]?05c4|dualshock|wireless controller)" } "ds5" { return "(054c.*0ce6|vid[_ -]?054c.*pid[_ -]?0ce6|dualsense|wireless controller)" } "switch" { return "(057e.*2009|vid[_ -]?057e.*pid[_ -]?2009|switch|pro controller)" } diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1 index 6e985a4..9ed5251 100644 --- a/scripts/windows/test-installed-driver.ps1 +++ b/scripts/windows/test-installed-driver.ps1 @@ -234,27 +234,31 @@ function Wait-ForXInputReportFlow { throw "XInput did not observe changing $Profile button, left-stick X, and right-stick Y input from the virtual gamepad." } -function Get-ExpectedGamepadHardwareId { +function Get-ExpectedGamepadHardwareIds { switch ($Profile) { - "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_02FF&IG_00" } - "ds4" { return "HID\VID_054C&PID_05C4" } - "ds5" { return "HID\VID_054C&PID_0CE6" } - "switch" { return "HID\VID_057E&PID_2009" } + "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", "HID\VID_045E&PID_02FF&IG_00") } + "ds4" { return @("HID\VID_054C&PID_05C4") } + "ds5" { return @("HID\VID_054C&PID_0CE6") } + "switch" { return @("HID\VID_057E&PID_2009") } } throw "Unsupported profile: $Profile" } function Wait-ForStartedGamepadChild { - $deviceId = Get-ExpectedGamepadHardwareId + $deviceIds = @(Get-ExpectedGamepadHardwareIds) $deadline = (Get-Date).AddSeconds($DeviceStartTimeoutSeconds) $latestRecords = @() do { - $latestRecords = @(Get-PnPUtilDevicesByDeviceId -DeviceId $deviceId) + $latestRecords = @() + foreach ($deviceId in $deviceIds) { + $latestRecords += @(Get-PnPUtilDevicesByDeviceId -DeviceId $deviceId) + } + $started = $latestRecords | Where-Object { $_.Status -eq "Started" -and @@ -271,11 +275,11 @@ function Wait-ForStartedGamepadChild { } while ((Get-Date) -lt $deadline) if (-not $latestRecords) { - throw "No gamepad child device was found for $deviceId." + throw "No gamepad child device was found for $($deviceIds -join ', ')." } foreach ($record in $latestRecords) { - Assert-StartedPnPRecord -Record $record -Description "Gamepad child device $deviceId" + Assert-StartedPnPRecord -Record $record -Description "Gamepad child device $($deviceIds -join ', ')" } } diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index f0cfdaa..f4413ec 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -1935,7 +1935,7 @@ namespace lvh::profiles { return make_xbox_gip_profile( GamepadProfileKind::xbox_series, "Xbox Controller", - 0x02FF, + 0x0B12, 0x0500, true ); diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index f6a5a87..4ea4a5b 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -385,11 +385,23 @@ namespace { text.push_back(hex_digit(value)); } - void append_hid_vid_pid(std::wstring &hardware_ids, const LvhWindowsGamepadHardwareIds &ids) { + void append_hid_vid_pid( + std::wstring &hardware_ids, + std::uint16_t vendor_id, + std::uint16_t product_id + ) { hardware_ids.append(L"HID\\VID_"); - append_hex4(hardware_ids, ids.vendor_id); + append_hex4(hardware_ids, vendor_id); hardware_ids.append(L"&PID_"); - append_hex4(hardware_ids, ids.product_id); + append_hex4(hardware_ids, product_id); + } + + std::uint16_t xinputhid_match_product_id(const LvhWindowsCreateGamepadRequest &request) { + if (request.gamepad_kind == LVH_WINDOWS_GAMEPAD_XBOX_SERIES) { + return 0x02FF; + } + + return request.hardware_ids.product_id; } bool is_xbox_gamepad(std::uint32_t gamepad_kind) { @@ -401,17 +413,17 @@ namespace { const auto &ids = request.hardware_ids; std::wstring hardware_ids; if (is_xbox_gamepad(request.gamepad_kind)) { - append_hid_vid_pid(hardware_ids, ids); + append_hid_vid_pid(hardware_ids, ids.vendor_id, xinputhid_match_product_id(request)); hardware_ids.append(L"&IG_00"); hardware_ids.push_back(L'\0'); } - append_hid_vid_pid(hardware_ids, ids); + append_hid_vid_pid(hardware_ids, ids.vendor_id, ids.product_id); hardware_ids.append(L"&REV_"); append_hex4(hardware_ids, ids.device_version); hardware_ids.push_back(L'\0'); - append_hid_vid_pid(hardware_ids, ids); + append_hid_vid_pid(hardware_ids, ids.vendor_id, ids.product_id); hardware_ids.push_back(L'\0'); hardware_ids.push_back(L'\0'); return hardware_ids; diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index 149fb1f..50c90f7 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -42,7 +42,7 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { const auto xbox_series = lvh::profiles::xbox_series(); EXPECT_EQ(xbox_series.vendor_id, 0x045E); - EXPECT_EQ(xbox_series.product_id, 0x02FF); + EXPECT_EQ(xbox_series.product_id, 0x0B12); EXPECT_EQ(xbox_series.bus_type, lvh::BusType::usb); EXPECT_EQ(xbox_series.name, "Xbox Controller"); EXPECT_EQ(xbox_series.manufacturer, "Microsoft"); From 28e09202dd0d4d41217b01f04e0f358931e3955d Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:26:13 -0400 Subject: [PATCH 19/27] Update DualShock 4 profile to v1 identity Set DualShock 4 profile version to 0x0100 and manufacturer name to 'Sony Computer Entertainment' to match first-generation controller specs used by ViGEmBus DS4 target and HIDMaestro's DS4 v1 reference. Update documentation to clarify default profiles vs. transport-specific variants. Add test assertions for version, name, and manufacturer. --- README.md | 8 ++++++-- src/core/profiles.cpp | 4 ++-- src/include/libvirtualhid/profiles.hpp | 7 +++++-- tests/unit/test_profiles.cpp | 7 ++++++- 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 952215a..48df974 100644 --- a/README.md +++ b/README.md @@ -149,8 +149,12 @@ axis slots and the analog triggers follow them. The Switch profile uses HIDMaestro's Nintendo Switch Pro Controller identity (`VID_057E&PID_2009`, product name `Pro Controller`) with Report ID `0x30`, a 64-byte input report, a hat d-pad, four 16-bit stick axes, and digital ZL/ZR trigger-click bits rather -than analog trigger axes. The DualSense profiles use the standard -`Wireless Controller` product name in the public profile and control protocol. +than analog trigger axes. The DualShock 4 profiles use the first-generation +controller identity (`VID_054C&PID_05C4`, version `0100`, product name +`Wireless Controller`, manufacturer `Sony Computer Entertainment`) to match the +ViGEmBus DS4 target and HIDMaestro's DS4 v1 reference. The DualSense profiles +use the standard `Wireless Controller` product name in the public profile and +control protocol. The Xbox 360 HID profile keeps the legacy common descriptor with 12 one-bit digital buttons, a hat switch for the d-pad, and 8-bit `X`, `Y`, `Z`, `Rx`, `Ry`, and `Rz` values. diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index f4413ec..f3fed0d 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -1817,14 +1817,14 @@ namespace lvh::profiles { profile.bus_type = bus_type; profile.vendor_id = 0x054C; profile.product_id = 0x05C4; - profile.version = 0x0000; + profile.version = 0x0100; profile.report_id = bus_type == BusType::bluetooth ? 0x11 : 1; profile.input_report_size = bus_type == BusType::bluetooth ? dualshock4_bluetooth_input_report_size : dualshock4_usb_input_report_size; profile.output_report_size = bus_type == BusType::bluetooth ? dualshock4_bluetooth_output_report_size : dualshock4_usb_output_report_size; profile.name = "Wireless Controller"; - profile.manufacturer = "Sony Interactive Entertainment"; + profile.manufacturer = "Sony Computer Entertainment"; profile.capabilities = { .supports_rumble = true, .supports_motion = true, diff --git a/src/include/libvirtualhid/profiles.hpp b/src/include/libvirtualhid/profiles.hpp index f379d9a..0ca6a4e 100644 --- a/src/include/libvirtualhid/profiles.hpp +++ b/src/include/libvirtualhid/profiles.hpp @@ -134,9 +134,12 @@ namespace lvh::profiles { std::optional gamepad_profile(GamepadProfileKind kind); /** - * @brief Get every built-in gamepad profile. + * @brief Get the default built-in gamepad profile set. * - * @return Built-in gamepad profiles. + * Transport-specific variants, such as explicit Bluetooth PlayStation profiles, + * are available through their named profile constructors. + * + * @return Default built-in gamepad profiles. */ std::vector built_in_gamepad_profiles(); diff --git a/tests/unit/test_profiles.cpp b/tests/unit/test_profiles.cpp index 50c90f7..8baa564 100644 --- a/tests/unit/test_profiles.cpp +++ b/tests/unit/test_profiles.cpp @@ -131,6 +131,8 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_EQ(dualshock4.vendor_id, 0x054C); EXPECT_EQ(dualshock4.product_id, 0x05C4); + EXPECT_EQ(dualshock4.version, 0x0100); + EXPECT_EQ(dualshock4.name, "Wireless Controller"); EXPECT_EQ(dualshock4.input_report_size, 64U); EXPECT_EQ(dualshock4.output_report_size, 32U); EXPECT_TRUE(dualshock4.capabilities.supports_motion); @@ -138,10 +140,13 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) { EXPECT_TRUE(dualshock4.capabilities.supports_rgb_led); EXPECT_TRUE(dualshock4.capabilities.supports_battery); EXPECT_FALSE(dualshock4.capabilities.supports_adaptive_triggers); - EXPECT_EQ(dualshock4.manufacturer, "Sony Interactive Entertainment"); + EXPECT_EQ(dualshock4.manufacturer, "Sony Computer Entertainment"); const auto dualshock4_bluetooth = lvh::profiles::dualshock4_bluetooth(); EXPECT_EQ(dualshock4_bluetooth.bus_type, lvh::BusType::bluetooth); + EXPECT_EQ(dualshock4_bluetooth.version, 0x0100); + EXPECT_EQ(dualshock4_bluetooth.name, "Wireless Controller"); + EXPECT_EQ(dualshock4_bluetooth.manufacturer, "Sony Computer Entertainment"); EXPECT_EQ(dualshock4_bluetooth.report_id, 0x11); EXPECT_EQ(dualshock4_bluetooth.input_report_size, 78U); EXPECT_EQ(dualshock4_bluetooth.output_report_size, 78U); From 6aedab423888b8905ddac3b44b99a961d9c0de4f Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 21:56:17 -0400 Subject: [PATCH 20/27] Refactor Windows CI driver install and test scripts Replace the per-build driver package build/sign/install steps with a reusable windows_driver job that produces a windows-driver-installer artifact consumed by all Windows build legs. Key changes: - Build job now depends on both setup_release and windows_driver - Download and install/uninstall driver via MSI artifact instead of raw INF+cert steps - Expand driver install/verify/uninstall and gamepad adapter steps to cover msys2 legs, not just msvc - Rename `-Profile` to `-GamepadProfile` (with backward-compat alias) in test-installed-driver.ps1 and test-browser-gamepad.ps1 to avoid collision with PowerShell's built-in $Profile variable - Refactor internal helper functions to accept explicit parameters instead of relying on script-level variables - Add HardwareIds multi-value parsing to ConvertFrom-PnPUtilDeviceOutput; fix pnputil /enum-devices invocation - Add SupportsShouldProcess to transcript helpers in install-driver.ps1 - Minor formatting fixes in windows_wix.cmake and CMakeLists.txt --- .github/workflows/ci.yml | 146 ++++++++++----------- README.md | 10 +- cmake/packaging/windows_wix.cmake | 1 + scripts/windows/install-driver.ps1 | 14 +- scripts/windows/test-browser-gamepad.ps1 | 33 +++-- scripts/windows/test-installed-driver.ps1 | 101 ++++++++++---- src/platform/windows/driver/CMakeLists.txt | 7 +- 7 files changed, 184 insertions(+), 128 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ca79304..eb2f2ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,9 @@ jobs: build: name: Build (${{ matrix.name }}) - needs: setup_release + needs: + - setup_release + - windows_driver permissions: contents: read runs-on: ${{ matrix.os }} @@ -235,75 +237,47 @@ jobs: if: matrix.kind == 'msvc' run: cmake --build cmake-build-ci --config Debug --parallel 2 - - name: Configure Windows test driver package - if: >- - matrix.kind == 'msvc' && - github.event_name == 'pull_request' - env: - BRANCH: ${{ github.head_ref || github.ref_name }} - BUILD_VERSION: ${{ needs.setup_release.outputs.release_version }} - COMMIT: ${{ needs.setup_release.outputs.release_commit }} - run: | - $certificatePath = Join-Path ` - $env:GITHUB_WORKSPACE ` - "cmake-build-driver-test\certificates\libvirtualhid-ci-test.cer" - cmake ` - -DBUILD_DOCS=OFF ` - -DBUILD_EXAMPLES=OFF ` - -DBUILD_TESTS=OFF ` - -DLIBVIRTUALHID_BUILD_WINDOWS_DRIVER=ON ` - -DLIBVIRTUALHID_ENABLE_PACKAGING=OFF ` - "-DLIBVIRTUALHID_DRIVER_TEST_CERTIFICATE=$certificatePath" ` - -A x64 ` - -B cmake-build-driver-test ` - -G "Visual Studio 17 2022" ` - -S . - - - name: Build Windows test driver package - if: >- - matrix.kind == 'msvc' && - github.event_name == 'pull_request' - run: cmake --build cmake-build-driver-test --config Release --target libvirtualhid_windows_catalog --parallel 2 - - - name: Sign Windows test driver package - if: >- - matrix.kind == 'msvc' && - github.event_name == 'pull_request' - run: | - $packagePath = Join-Path ` - $env:GITHUB_WORKSPACE ` - "cmake-build-driver-test\src\platform\windows\driver\package\Release" - $certificatePath = Join-Path ` - $env:GITHUB_WORKSPACE ` - "cmake-build-driver-test\certificates\libvirtualhid-ci-test.cer" - .\scripts\windows\sign-driver-package.ps1 ` - -PackagePath $packagePath ` - -CertificatePath $certificatePath + - name: Download Windows driver installer artifact + if: runner.os == 'Windows' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: windows-driver-installer + path: windows-driver-installer - - name: Install Windows test driver package - if: >- - matrix.kind == 'msvc' && - github.event_name == 'pull_request' + - name: Install Windows driver installer + if: runner.os == 'Windows' + shell: pwsh run: | - $packagePath = Join-Path ` - $env:GITHUB_WORKSPACE ` - "cmake-build-driver-test\src\platform\windows\driver\package\Release" - $certificatePath = Join-Path ` - $env:GITHUB_WORKSPACE ` - "cmake-build-driver-test\certificates\libvirtualhid-ci-test.cer" - .\scripts\windows\install-driver.ps1 ` - -InfPath (Join-Path $packagePath "libvirtualhid.inf") ` - -CertificatePath $certificatePath + $installer = Get-ChildItem -LiteralPath .\windows-driver-installer -Filter *.msi | Select-Object -First 1 + if (!$installer) { + throw "Windows driver installer artifact did not contain an MSI." + } + $logPath = Join-Path $env:RUNNER_TEMP "libvirtualhid-driver-install.log" + $process = Start-Process ` + -FilePath msiexec.exe ` + -ArgumentList @("/i", $installer.FullName, "/qn", "/norestart", "/L*v", $logPath) ` + -Wait ` + -PassThru ` + -NoNewWindow + if ($process.ExitCode -notin @(0, 3010)) { + Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue + throw "Windows driver installer exited with code $($process.ExitCode)." + } - name: Verify Windows test driver package - if: >- - matrix.kind == 'msvc' && - github.event_name == 'pull_request' + if: runner.os == 'Windows' + shell: pwsh run: | + if ("${{ matrix.kind }}" -eq "msys2") { + $env:PATH = "C:\msys64\${{ matrix.msystem }}\bin;C:\msys64\usr\bin;$env:PATH" + $gamepadAdapterPath = "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\gamepad_adapter.exe" + } else { + $gamepadAdapterPath = "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\Debug\gamepad_adapter.exe" + } $profiles = @("generic", "xone", "xseries", "ds4", "ds5", "switch") foreach ($profile in $profiles) { .\scripts\windows\test-installed-driver.ps1 ` - -GamepadAdapterPath "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\Debug\gamepad_adapter.exe" ` + -GamepadAdapterPath $gamepadAdapterPath ` -Profile $profile ` -Verbose } @@ -391,28 +365,44 @@ jobs: -o reports/coverage.xml - name: Run gamepad adapter example - if: matrix.kind != 'msvc' + if: runner.os == 'Linux' run: | - if [[ "${RUNNER_OS}" == "Windows" ]]; then - ./cmake-build-ci/examples/gamepad_adapter.exe - else - ./cmake-build-ci/examples/gamepad_adapter - fi + ./cmake-build-ci/examples/gamepad_adapter - - name: Run gamepad adapter example MSVC - if: matrix.kind == 'msvc' - run: .\cmake-build-ci\examples\Debug\gamepad_adapter.exe + - name: Run gamepad adapter example Windows + if: runner.os == 'Windows' + shell: pwsh + run: | + if ("${{ matrix.kind }}" -eq "msys2") { + $env:PATH = "C:\msys64\${{ matrix.msystem }}\bin;C:\msys64\usr\bin;$env:PATH" + & .\cmake-build-ci\examples\gamepad_adapter.exe + } else { + & .\cmake-build-ci\examples\Debug\gamepad_adapter.exe + } - - name: Uninstall Windows test driver package + - name: Uninstall Windows driver installer if: >- always() && - matrix.kind == 'msvc' && - github.event_name == 'pull_request' + runner.os == 'Windows' + shell: pwsh run: | - .\scripts\windows\uninstall-driver.ps1 ` - -OriginalName "libvirtualhid.inf" ` - -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" ` - -Force + $installer = Get-ChildItem -LiteralPath .\windows-driver-installer ` + -Filter *.msi ` + -ErrorAction SilentlyContinue | + Select-Object -First 1 + if ($installer) { + $logPath = Join-Path $env:RUNNER_TEMP "libvirtualhid-driver-uninstall.log" + $process = Start-Process ` + -FilePath msiexec.exe ` + -ArgumentList @("/x", $installer.FullName, "/qn", "/norestart", "/L*v", $logPath) ` + -Wait ` + -PassThru ` + -NoNewWindow + if ($process.ExitCode -notin @(0, 3010)) { + Get-Content -LiteralPath $logPath -ErrorAction SilentlyContinue + throw "Windows driver installer uninstall exited with code $($process.ExitCode)." + } + } - name: Install if: matrix.kind != 'msvc' diff --git a/README.md b/README.md index 48df974..ace20da 100644 --- a/README.md +++ b/README.md @@ -190,10 +190,10 @@ powershell -ExecutionPolicy Bypass -File .\scripts\windows\install-driver.ps1 ` -LogPath .\cmake-build-windows-driver\install-driver.log powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-installed-driver.ps1 ` -GamepadAdapterPath .\cmake-build-ci\examples\Debug\gamepad_adapter.exe ` - -Profile xseries + -GamepadProfile xseries powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-browser-gamepad.ps1 ` -GamepadAdapterPath .\cmake-build-ci\examples\Debug\gamepad_adapter.exe ` - -Profile xseries + -GamepadProfile xseries powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1 ` -Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" ``` @@ -212,9 +212,9 @@ The test helper fails if the root device is not reported as `Status: Started`, if `\\.\LibVirtualHid` cannot be opened, or if a held gamepad adapter instance does not produce a started HID child device such as `HID\VID_045E&PID_0B12&IG_00` or the Xbox Series xinputhid match identity -`HID\VID_045E&PID_02FF&IG_00`. That check is also run by the Windows MSVC pull -request CI leg for every Windows UMDF/VHF-supported `gamepad_adapter` profile -after installing the test driver package. The browser helper is for manual +`HID\VID_045E&PID_02FF&IG_00`. That check is also run by the Windows CI legs +for every Windows UMDF/VHF-supported `gamepad_adapter` profile +after installing the Windows Driver Installer artifact. The browser helper is for manual diagnostics: it launches a normal desktop Edge or Chrome instance at `https://hardwaretester.com/gamepad`, holds a virtual gamepad, and fails if the browser Gamepad API does not report a controller matching the selected profile diff --git a/cmake/packaging/windows_wix.cmake b/cmake/packaging/windows_wix.cmake index ef82546..c604ef5 100644 --- a/cmake/packaging/windows_wix.cmake +++ b/cmake/packaging/windows_wix.cmake @@ -27,6 +27,7 @@ if(NOT WIX_INSTALL_RESULT EQUAL 0) message(FATAL_ERROR "Failed to install WiX tools locally: ${WIX_INSTALL_OUTPUT}") endif() +# Ensure a WiX extension is installed in the local tool cache. function(libvirtualhid_wix_ensure_extension extension_name extension_version) execute_process( COMMAND "${WIX_TOOL_PATH}/wix" extension list --global diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index 8a038e6..e295328 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -20,6 +20,7 @@ $ErrorActionPreference = "Stop" $script:LibVirtualHidTranscriptStarted = $false function Start-LibVirtualHidTranscript { + [CmdletBinding(SupportsShouldProcess)] param([string] $Path) if (-not $Path) { @@ -31,20 +32,27 @@ function Start-LibVirtualHidTranscript { if ($logDirectory) { New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null } - Start-Transcript -Path $Path -Append | Out-Null - $script:LibVirtualHidTranscriptStarted = $true + if ($PSCmdlet.ShouldProcess($Path, "Start libvirtualhid install transcript")) { + Start-Transcript -Path $Path -Append | Out-Null + $script:LibVirtualHidTranscriptStarted = $true + } } catch { Write-Warning "Unable to start libvirtualhid install transcript: $($_.Exception.Message)" } } function Stop-LibVirtualHidTranscript { + [CmdletBinding(SupportsShouldProcess)] + param() + if (-not $script:LibVirtualHidTranscriptStarted) { return } try { - Stop-Transcript | Out-Null + if ($PSCmdlet.ShouldProcess("libvirtualhid install transcript", "Stop transcript")) { + Stop-Transcript | Out-Null + } } catch { Write-Warning "Unable to stop libvirtualhid install transcript: $($_.Exception.Message)" } diff --git a/scripts/windows/test-browser-gamepad.ps1 b/scripts/windows/test-browser-gamepad.ps1 index 3e0b525..de2c82f 100644 --- a/scripts/windows/test-browser-gamepad.ps1 +++ b/scripts/windows/test-browser-gamepad.ps1 @@ -8,7 +8,8 @@ param( [string] $GamepadAdapterPath, [ValidateSet("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch")] - [string] $Profile = "xseries", + [Alias("Profile")] + [string] $GamepadProfile = "xseries", [string] $BrowserPath, @@ -29,7 +30,9 @@ $ErrorActionPreference = "Stop" $script:DevToolsCommandId = 0 function Get-ExpectedGamepadIdPattern { - switch ($Profile) { + param([string] $ProfileName) + + switch ($ProfileName) { "generic" { return "(1209.*0001|vid[_ -]?1209.*pid[_ -]?0001|generic)" } "x360" { return "(045e.*028e|vid[_ -]?045e.*pid[_ -]?028e|x-?box.*360)" } "xone" { return "(045e.*02ea|vid[_ -]?045e.*pid[_ -]?02ea|xbox one|x-box one)" } @@ -39,12 +42,14 @@ function Get-ExpectedGamepadIdPattern { "switch" { return "(057e.*2009|vid[_ -]?057e.*pid[_ -]?2009|switch|pro controller)" } } - throw "Unsupported profile: $Profile" + throw "Unsupported profile: $ProfileName" } function Resolve-BrowserPath { - if ($BrowserPath) { - return (Resolve-Path -LiteralPath $BrowserPath).Path + param([string] $Path) + + if ($Path) { + return (Resolve-Path -LiteralPath $Path).Path } $candidates = @( @@ -205,7 +210,7 @@ function Invoke-DevToolsCommand { } } -function New-GamepadApiProbeExpression { +function Get-GamepadApiProbeExpression { param( [string] $ExpectedIdPattern, [bool] $AllowAnyGamepad, @@ -348,9 +353,13 @@ if ($HoldSeconds -le $TimeoutSeconds) { } $resolvedGamepadAdapterPath = (Resolve-Path -LiteralPath $GamepadAdapterPath).Path -$resolvedBrowserPath = Resolve-BrowserPath -$expectedPattern = if ($ExpectedIdPattern) { $ExpectedIdPattern } else { Get-ExpectedGamepadIdPattern } -if ($Profile -eq "x360") { +$resolvedBrowserPath = Resolve-BrowserPath -Path $BrowserPath +$expectedPattern = if ($ExpectedIdPattern) { + $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 @@ -389,7 +398,7 @@ try { $adapterProcess = Start-Process ` -FilePath $resolvedGamepadAdapterPath ` -WorkingDirectory (Split-Path -Parent $resolvedGamepadAdapterPath) ` - -ArgumentList @($Profile, "--hold-seconds", "$HoldSeconds") ` + -ArgumentList @($GamepadProfile, "--hold-seconds", "$HoldSeconds") ` -PassThru ` -RedirectStandardOutput $adapterStdoutPath ` -RedirectStandardError $adapterStderrPath ` @@ -432,7 +441,7 @@ try { } ` -TimeoutSeconds 5) - $expression = New-GamepadApiProbeExpression ` + $expression = Get-GamepadApiProbeExpression ` -ExpectedIdPattern $expectedPattern ` -AllowAnyGamepad:$AllowAnyGamepad ` -TimeoutSeconds $TimeoutSeconds @@ -462,7 +471,7 @@ try { } Write-Information ` - "Browser Gamepad API observed $Profile as '$($probe.matched.id)' with mapping '$($probe.matched.mapping)'." ` + "Browser Gamepad API observed $GamepadProfile as '$($probe.matched.id)' with mapping '$($probe.matched.mapping)'." ` -InformationAction Continue } finally { if ($adapterProcess -and -not $adapterProcess.HasExited) { diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1 index 9ed5251..acd356c 100644 --- a/scripts/windows/test-installed-driver.ps1 +++ b/scripts/windows/test-installed-driver.ps1 @@ -11,7 +11,8 @@ param( [string] $GamepadAdapterPath, [ValidateSet("generic", "x360", "xone", "xseries", "ds4", "ds5", "switch")] - [string] $Profile = "xseries", + [Alias("Profile")] + [string] $GamepadProfile = "xseries", [int] $HoldSeconds = 12, @@ -50,10 +51,12 @@ function ConvertFrom-PnPUtilDeviceOutput { InstanceId = $Matches[1].Trim() DeviceDescription = $null DriverName = $null + HardwareIds = @() Status = $null ProblemCode = $null ProblemStatus = $null } + $section = $null continue } @@ -62,15 +65,29 @@ function ConvertFrom-PnPUtilDeviceOutput { } if ($line -match "^\s{0,2}Device Description:\s*(.+)\s*$") { + $section = $null $current.DeviceDescription = $Matches[1].Trim() } elseif ($line -match "^\s{0,2}Driver Name:\s*(.+)\s*$") { + $section = $null $current.DriverName = $Matches[1].Trim() + } elseif ($line -match "^\s{0,2}Hardware IDs:\s*(.*)\s*$") { + $section = "HardwareIds" + if ($Matches[1].Trim()) { + $current.HardwareIds += $Matches[1].Trim() + } } elseif ($line -match "^\s*Status:\s*(.+)\s*$") { + $section = $null $current.Status = $Matches[1].Trim() } elseif ($line -match "^\s*Problem Code:\s*(.+)\s*$") { + $section = $null $current.ProblemCode = $Matches[1].Trim() } elseif ($line -match "^\s*Problem Status:\s*(.+)\s*$") { + $section = $null $current.ProblemStatus = $Matches[1].Trim() + } elseif ($line -match "^\s{0,2}[^:]+:\s*.*$") { + $section = $null + } elseif ($section -eq "HardwareIds" -and $line -match "^\s+(.+)\s*$") { + $current.HardwareIds += $Matches[1].Trim() } } @@ -86,13 +103,14 @@ function Get-PnPUtilDevicesByDeviceId { $output = Invoke-PnPUtil -Arguments @( "/enum-devices", - "/deviceid", - $DeviceId, "/deviceids", - "/services", "/drivers" ) - return ConvertFrom-PnPUtilDeviceOutput -Output $output + return ConvertFrom-PnPUtilDeviceOutput -Output $output | + Where-Object { + $_.InstanceId -like "$DeviceId\*" -or + $_.HardwareIds -contains $DeviceId + } } function Assert-StartedPnPRecord { @@ -119,9 +137,11 @@ function Assert-StartedPnPRecord { } function Assert-RootDeviceStarted { - $rootDevices = @(Get-PnPUtilDevicesByDeviceId -DeviceId $HardwareId) + param([string] $TargetHardwareId) + + $rootDevices = @(Get-PnPUtilDevicesByDeviceId -DeviceId $TargetHardwareId) if (-not $rootDevices) { - throw "No installed libvirtualhid root device was found for $HardwareId." + throw "No installed libvirtualhid root device was found for $TargetHardwareId." } foreach ($device in $rootDevices) { @@ -129,17 +149,19 @@ function Assert-RootDeviceStarted { } } -function Assert-ControlDeviceOpens { +function Assert-ControlDeviceOpen { + param([string] $Path) + try { $stream = [System.IO.File]::Open( - $ControlDevicePath, + $Path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::ReadWrite ) $stream.Dispose() } catch { - throw "Could not open ${ControlDevicePath}: $($_.Exception.Message)" + throw "Could not open ${Path}: $($_.Exception.Message)" } } @@ -176,12 +198,17 @@ public static class LvhXInputProbe { } function Wait-ForXInputReportFlow { - if ($Profile -ne "xone" -and $Profile -ne "xseries") { + param( + [string] $ProfileName, + [int] $TimeoutSeconds + ) + + if ($ProfileName -ne "xone" -and $ProfileName -ne "xseries") { return } Add-XInputProbeType - $deadline = (Get-Date).AddSeconds($DeviceStartTimeoutSeconds) + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) $observedStates = @{} do { @@ -223,7 +250,7 @@ function Wait-ForXInputReportFlow { $observed.LeftThumbXPositive -and $observed.RightThumbYNegative -and $observed.RightThumbYPositive) { - Write-Information "XInput observed changing $Profile input on index ${index}." -InformationAction Continue + Write-Information "XInput observed changing $ProfileName input on index ${index}." -InformationAction Continue return } } @@ -231,11 +258,13 @@ function Wait-ForXInputReportFlow { Start-Sleep -Milliseconds 250 } while ((Get-Date) -lt $deadline) - throw "XInput did not observe changing $Profile button, left-stick X, and right-stick Y input from the virtual gamepad." + throw "XInput did not observe changing $ProfileName button, left-stick X, and right-stick Y input from the virtual gamepad." } -function Get-ExpectedGamepadHardwareIds { - switch ($Profile) { +function Get-ExpectedGamepadHardwareId { + param([string] $ProfileName) + + 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") } @@ -245,12 +274,17 @@ function Get-ExpectedGamepadHardwareIds { "switch" { return @("HID\VID_057E&PID_2009") } } - throw "Unsupported profile: $Profile" + throw "Unsupported profile: $ProfileName" } function Wait-ForStartedGamepadChild { - $deviceIds = @(Get-ExpectedGamepadHardwareIds) - $deadline = (Get-Date).AddSeconds($DeviceStartTimeoutSeconds) + param( + [string] $ProfileName, + [int] $TimeoutSeconds + ) + + $deviceIds = @(Get-ExpectedGamepadHardwareId -ProfileName $ProfileName) + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) $latestRecords = @() do { @@ -284,22 +318,29 @@ function Wait-ForStartedGamepadChild { } function Invoke-GamepadAdapterSmoke { - if (-not $GamepadAdapterPath) { + param( + [string] $Path, + [string] $ProfileName, + [int] $HoldSeconds, + [int] $DeviceStartTimeoutSeconds + ) + + if (-not $Path) { return } - if ($Profile -eq "x360") { + 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 $GamepadAdapterPath).Path + $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" Remove-Item -LiteralPath $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue $process = Start-Process ` -FilePath $resolvedGamepadAdapterPath ` - -ArgumentList @($Profile, "--hold-seconds", "$HoldSeconds") ` + -ArgumentList @($ProfileName, "--hold-seconds", "$HoldSeconds") ` -PassThru ` -RedirectStandardOutput $stdoutPath ` -RedirectStandardError $stderrPath ` @@ -313,8 +354,8 @@ function Invoke-GamepadAdapterSmoke { throw "gamepad_adapter exited with code $($process.ExitCode).`nstdout:`n$stdout`nstderr:`n$stderr" } - Wait-ForStartedGamepadChild - Wait-ForXInputReportFlow + Wait-ForStartedGamepadChild -ProfileName $ProfileName -TimeoutSeconds $DeviceStartTimeoutSeconds + Wait-ForXInputReportFlow -ProfileName $ProfileName -TimeoutSeconds $DeviceStartTimeoutSeconds } finally { if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue @@ -323,6 +364,10 @@ function Invoke-GamepadAdapterSmoke { } } -Assert-RootDeviceStarted -Assert-ControlDeviceOpens -Invoke-GamepadAdapterSmoke +Assert-RootDeviceStarted -TargetHardwareId $HardwareId +Assert-ControlDeviceOpen -Path $ControlDevicePath +Invoke-GamepadAdapterSmoke ` + -Path $GamepadAdapterPath ` + -ProfileName $GamepadProfile ` + -HoldSeconds $HoldSeconds ` + -DeviceStartTimeoutSeconds $DeviceStartTimeoutSeconds diff --git a/src/platform/windows/driver/CMakeLists.txt b/src/platform/windows/driver/CMakeLists.txt index 0a4abc5..f1eff88 100644 --- a/src/platform/windows/driver/CMakeLists.txt +++ b/src/platform/windows/driver/CMakeLists.txt @@ -260,8 +260,11 @@ install(CODE 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_driver_inf}\" IS_NEWER_THAN \"\${lvh_driver_cat}\") - message(FATAL_ERROR \"Windows driver catalog is stale; build libvirtualhid_windows_catalog and sign libvirtualhid.cat before packaging.\") + if(\"\${lvh_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 \" + \"and sign libvirtualhid.cat before packaging.\") endif() " COMPONENT driver) From 62f2ccd8c667f3299aa600cac7942112e8418927 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:16:00 -0400 Subject: [PATCH 21/27] Simplify Windows driver smoke test checks Refactors `test-installed-driver.ps1` to focus on PnP/HID readiness and control-device checks by removing the XInput report-flow probe for xone/xseries profiles. It also improves verbose diagnostics by logging only matched PnP records (instance ID, status, driver, hardware IDs, and problem fields) instead of dumping all `pnputil` output. README wording was updated to clarify the expected Xbox Series-compatible HID child identity. --- README.md | 2 +- scripts/windows/test-installed-driver.ps1 | 132 +++++----------------- 2 files changed, 32 insertions(+), 102 deletions(-) diff --git a/README.md b/README.md index ace20da..4d48140 100644 --- a/README.md +++ b/README.md @@ -211,7 +211,7 @@ to `C:\ProgramData\libvirtualhid\install-driver.log`. The test helper fails if the root device is not reported as `Status: Started`, if `\\.\LibVirtualHid` cannot be opened, or if a held gamepad adapter instance does not produce a started HID child device such as -`HID\VID_045E&PID_0B12&IG_00` or the Xbox Series xinputhid match identity +`HID\VID_045E&PID_0B12&IG_00` or an Xbox Series-compatible HID child such as `HID\VID_045E&PID_02FF&IG_00`. That check is also run by the Windows CI legs for every Windows UMDF/VHF-supported `gamepad_adapter` profile after installing the Windows Driver Installer artifact. The browser helper is for manual diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1 index acd356c..1415b22 100644 --- a/scripts/windows/test-installed-driver.ps1 +++ b/scripts/windows/test-installed-driver.ps1 @@ -26,9 +26,6 @@ function Invoke-PnPUtil { $output = @(pnputil.exe @Arguments 2>&1) $exitCode = $LASTEXITCODE - foreach ($line in $output) { - Write-Verbose $line - } if ($exitCode -ne 0) { throw "pnputil.exe $($Arguments -join ' ') exited with code $exitCode`n$($output -join "`n")" @@ -98,6 +95,30 @@ function ConvertFrom-PnPUtilDeviceOutput { return $records } +function Write-PnPRecordVerbose { + param([object] $Record) + + Write-Verbose "Matched device: $($Record.InstanceId)" + if ($Record.DeviceDescription) { + Write-Verbose " Description: $($Record.DeviceDescription)" + } + if ($Record.Status) { + Write-Verbose " Status: $($Record.Status)" + } + if ($Record.DriverName) { + Write-Verbose " Driver Name: $($Record.DriverName)" + } + if ($Record.HardwareIds) { + Write-Verbose " Hardware IDs: $($Record.HardwareIds -join ', ')" + } + if ($Record.ProblemCode) { + Write-Verbose " Problem Code: $($Record.ProblemCode)" + } + if ($Record.ProblemStatus) { + Write-Verbose " Problem Status: $($Record.ProblemStatus)" + } +} + function Get-PnPUtilDevicesByDeviceId { param([string] $DeviceId) @@ -106,11 +127,17 @@ function Get-PnPUtilDevicesByDeviceId { "/deviceids", "/drivers" ) - return ConvertFrom-PnPUtilDeviceOutput -Output $output | + $matchingDevices = @(ConvertFrom-PnPUtilDeviceOutput -Output $output | Where-Object { $_.InstanceId -like "$DeviceId\*" -or $_.HardwareIds -contains $DeviceId } + ) + foreach ($device in $matchingDevices) { + Write-PnPRecordVerbose -Record $device + } + + return $matchingDevices } function Assert-StartedPnPRecord { @@ -165,102 +192,6 @@ function Assert-ControlDeviceOpen { } } -function Add-XInputProbeType { - if ("LvhXInputProbe" -as [type]) { - return - } - - Add-Type -TypeDefinition @" -using System.Runtime.InteropServices; - -public static class LvhXInputProbe { - [StructLayout(LayoutKind.Sequential)] - public struct XInputState { - public uint PacketNumber; - public XInputGamepad Gamepad; - } - - [StructLayout(LayoutKind.Sequential)] - public struct XInputGamepad { - public ushort Buttons; - public byte LeftTrigger; - public byte RightTrigger; - public short LeftThumbX; - public short LeftThumbY; - public short RightThumbX; - public short RightThumbY; - } - - [DllImport("xinput1_4.dll", EntryPoint = "XInputGetState")] - public static extern uint XInputGetState(uint userIndex, out XInputState state); -} -"@ -} - -function Wait-ForXInputReportFlow { - param( - [string] $ProfileName, - [int] $TimeoutSeconds - ) - - if ($ProfileName -ne "xone" -and $ProfileName -ne "xseries") { - return - } - - Add-XInputProbeType - $deadline = (Get-Date).AddSeconds($TimeoutSeconds) - $observedStates = @{} - - do { - for ($index = 0; $index -lt 4; ++$index) { - $state = New-Object LvhXInputProbe+XInputState - $status = [LvhXInputProbe]::XInputGetState([uint32] $index, [ref] $state) - if ($status -ne 0 -or $state.Gamepad.RightTrigger -ne 255) { - continue - } - - if (-not $observedStates.ContainsKey($index)) { - $observedStates[$index] = [pscustomobject] @{ - InitialButtons = $state.Gamepad.Buttons - ButtonChanged = $false - LeftThumbXNegative = $false - LeftThumbXPositive = $false - RightThumbYNegative = $false - RightThumbYPositive = $false - } - } - - $observed = $observedStates[$index] - if ($state.Gamepad.Buttons -ne $observed.InitialButtons) { - $observed.ButtonChanged = $true - } - if ($state.Gamepad.LeftThumbX -lt -20000) { - $observed.LeftThumbXNegative = $true - } elseif ($state.Gamepad.LeftThumbX -gt 20000) { - $observed.LeftThumbXPositive = $true - } - if ($state.Gamepad.RightThumbY -lt -20000) { - $observed.RightThumbYNegative = $true - } elseif ($state.Gamepad.RightThumbY -gt 20000) { - $observed.RightThumbYPositive = $true - } - - if ($observed.ButtonChanged -and - $observed.LeftThumbXNegative -and - $observed.LeftThumbXPositive -and - $observed.RightThumbYNegative -and - $observed.RightThumbYPositive) { - Write-Information "XInput observed changing $ProfileName input on index ${index}." -InformationAction Continue - return - } - } - - Start-Sleep -Milliseconds 250 - } while ((Get-Date) -lt $deadline) - - throw "XInput did not observe changing $ProfileName button, left-stick X, and right-stick Y input from the virtual gamepad." -} - function Get-ExpectedGamepadHardwareId { param([string] $ProfileName) @@ -355,7 +286,6 @@ function Invoke-GamepadAdapterSmoke { } Wait-ForStartedGamepadChild -ProfileName $ProfileName -TimeoutSeconds $DeviceStartTimeoutSeconds - Wait-ForXInputReportFlow -ProfileName $ProfileName -TimeoutSeconds $DeviceStartTimeoutSeconds } finally { if (-not $process.HasExited) { Stop-Process -Id $process.Id -Force -ErrorAction SilentlyContinue From 0a54fe282f4fa7dd103517b705cf2120b51c0569 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:33:49 -0400 Subject: [PATCH 22/27] Fix Sonar issues in Windows driver PR --- examples/gamepad_adapter.cpp | 34 ++-- scripts/windows/test-installed-driver.ps1 | 88 ++++++---- src/core/profiles.cpp | 65 +++++--- src/core/report.cpp | 156 ++++++++---------- .../windows/driver/libvirtualhid_umdf.cpp | 85 +++++----- src/platform/windows/windows_backend.cpp | 6 +- tests/unit/test_report.cpp | 107 ++++++------ 7 files changed, 278 insertions(+), 263 deletions(-) diff --git a/examples/gamepad_adapter.cpp b/examples/gamepad_adapter.cpp index 80df02d..3dedf70 100644 --- a/examples/gamepad_adapter.cpp +++ b/examples/gamepad_adapter.cpp @@ -45,17 +45,20 @@ namespace { lvh::ClientControllerType client_type_for_profile(lvh::GamepadProfileKind kind) { switch (kind) { - case lvh::GamepadProfileKind::xbox_360: - case lvh::GamepadProfileKind::xbox_one: - case lvh::GamepadProfileKind::xbox_series: - return lvh::ClientControllerType::xbox; - case lvh::GamepadProfileKind::dualshock4: - case lvh::GamepadProfileKind::dualsense: - return lvh::ClientControllerType::playstation; - case lvh::GamepadProfileKind::switch_pro: - return lvh::ClientControllerType::nintendo; - case lvh::GamepadProfileKind::generic: - return lvh::ClientControllerType::unknown; + using enum lvh::ClientControllerType; + using enum lvh::GamepadProfileKind; + + case xbox_360: + case xbox_one: + case xbox_series: + return xbox; + case dualshock4: + case dualsense: + return playstation; + case switch_pro: + return nintendo; + case generic: + return unknown; } return lvh::ClientControllerType::unknown; @@ -66,13 +69,16 @@ int main(int argc, char *argv[]) { auto profile_name = std::string_view {"ds5"}; auto hold = false; auto hold_seconds = 60; - for (auto index = 1; index < argc; ++index) { + auto index = 1; + while (index < argc) { const auto argument = std::string_view {argv[index]}; + ++index; if (argument == "--hold") { hold = true; - } else if (argument == "--hold-seconds" && index + 1 < argc) { + } else if (argument == "--hold-seconds" && index < argc) { hold = true; - hold_seconds = std::stoi(argv[++index]); + hold_seconds = std::stoi(argv[index]); + ++index; } else { profile_name = argument; } diff --git a/scripts/windows/test-installed-driver.ps1 b/scripts/windows/test-installed-driver.ps1 index 1415b22..15051b0 100644 --- a/scripts/windows/test-installed-driver.ps1 +++ b/scripts/windows/test-installed-driver.ps1 @@ -34,6 +34,58 @@ function Invoke-PnPUtil { return $output } +function ConvertTo-PnPUtilDeviceRecord { + param([string] $InstanceId) + + return @{ + InstanceId = $InstanceId.Trim() + DeviceDescription = $null + DriverName = $null + HardwareIds = @() + Status = $null + ProblemCode = $null + ProblemStatus = $null + } +} + +function ConvertFrom-PnPUtilDeviceLine { + param( + [hashtable] $Record, + [string] $Line, + [string] $Section + ) + + $singleValueLabels = @{ + "Device Description" = "DeviceDescription" + "Driver Name" = "DriverName" + "Status" = "Status" + "Problem Code" = "ProblemCode" + "Problem Status" = "ProblemStatus" + } + + if ($Line -match "^\s{0,2}([^:]+):\s*(.*)\s*$") { + $label = $Matches[1].Trim() + $value = $Matches[2].Trim() + if ($label -eq "Hardware IDs") { + if ($value) { + $Record.HardwareIds += $value + } + return "HardwareIds" + } + + if ($singleValueLabels.ContainsKey($label)) { + $Record[$singleValueLabels[$label]] = $value + } + return $null + } + + if ($Section -eq "HardwareIds" -and $Line -match "^\s+(.+)\s*$") { + $Record.HardwareIds += $Matches[1].Trim() + } + + return $Section +} + function ConvertFrom-PnPUtilDeviceOutput { param([string[]] $Output) @@ -44,15 +96,7 @@ function ConvertFrom-PnPUtilDeviceOutput { if ($current) { $records += [pscustomobject] $current } - $current = @{ - InstanceId = $Matches[1].Trim() - DeviceDescription = $null - DriverName = $null - HardwareIds = @() - Status = $null - ProblemCode = $null - ProblemStatus = $null - } + $current = ConvertTo-PnPUtilDeviceRecord -InstanceId $Matches[1] $section = $null continue } @@ -61,31 +105,7 @@ function ConvertFrom-PnPUtilDeviceOutput { continue } - if ($line -match "^\s{0,2}Device Description:\s*(.+)\s*$") { - $section = $null - $current.DeviceDescription = $Matches[1].Trim() - } elseif ($line -match "^\s{0,2}Driver Name:\s*(.+)\s*$") { - $section = $null - $current.DriverName = $Matches[1].Trim() - } elseif ($line -match "^\s{0,2}Hardware IDs:\s*(.*)\s*$") { - $section = "HardwareIds" - if ($Matches[1].Trim()) { - $current.HardwareIds += $Matches[1].Trim() - } - } elseif ($line -match "^\s*Status:\s*(.+)\s*$") { - $section = $null - $current.Status = $Matches[1].Trim() - } elseif ($line -match "^\s*Problem Code:\s*(.+)\s*$") { - $section = $null - $current.ProblemCode = $Matches[1].Trim() - } elseif ($line -match "^\s*Problem Status:\s*(.+)\s*$") { - $section = $null - $current.ProblemStatus = $Matches[1].Trim() - } elseif ($line -match "^\s{0,2}[^:]+:\s*.*$") { - $section = $null - } elseif ($section -eq "HardwareIds" -and $line -match "^\s+(.+)\s*$") { - $current.HardwareIds += $Matches[1].Trim() - } + $section = ConvertFrom-PnPUtilDeviceLine -Record $current -Line $line -Section $section } if ($current) { diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index f3fed0d..913d5cc 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -65,13 +65,23 @@ namespace lvh::profiles { return 0; } + std::byte byte_from_hex(char high, char low) { + return static_cast((hex_nibble(high) << 4U) | hex_nibble(low)); + } + std::vector bytes_from_hex(std::string_view hex) { - std::vector bytes; - bytes.reserve(hex.size() / 2U); + std::vector parsed_bytes; + parsed_bytes.reserve(hex.size() / 2U); for (std::size_t index = 0; index + 1U < hex.size(); index += 2U) { - bytes.push_back(static_cast((hex_nibble(hex[index]) << 4U) | hex_nibble(hex[index + 1U]))); + parsed_bytes.push_back(byte_from_hex(hex[index], hex[index + 1U])); } - return bytes; + + std::vector descriptor; + descriptor.reserve(parsed_bytes.size()); + for (const auto byte : parsed_bytes) { + descriptor.push_back(std::to_integer(byte)); + } + return descriptor; } std::vector make_xbox_gip_report_descriptor(bool include_share_button) { @@ -1729,7 +1739,7 @@ namespace lvh::profiles { return descriptor; } - DeviceProfile make_gamepad_profile( + DeviceProfile make_base_gamepad_profile( GamepadProfileKind kind, std::string name, std::string manufacturer, @@ -1753,6 +1763,27 @@ namespace lvh::profiles { profile.name = std::move(name); profile.manufacturer = std::move(manufacturer); profile.capabilities = capabilities; + return profile; + } + + DeviceProfile make_gamepad_profile( + GamepadProfileKind kind, + std::string name, + std::string manufacturer, + std::uint16_t vendor_id, + std::uint16_t product_id, + std::uint16_t version, + GamepadProfileCapabilities capabilities + ) { + auto profile = make_base_gamepad_profile( + kind, + std::move(name), + std::move(manufacturer), + vendor_id, + product_id, + version, + capabilities + ); profile.report_descriptor = make_gamepad_report_descriptor(profile.report_id, profile.capabilities.supports_rumble); return profile; } @@ -1766,21 +1797,15 @@ namespace lvh::profiles { std::uint16_t version, GamepadProfileCapabilities capabilities ) { - DeviceProfile profile; - profile.device_type = DeviceType::gamepad; - profile.gamepad_kind = kind; - profile.bus_type = BusType::usb; - profile.vendor_id = vendor_id; - profile.product_id = product_id; - profile.version = version; - profile.report_id = 1; - profile.input_report_size = common_report_size; - if (capabilities.supports_rumble) { - profile.output_report_size = common_output_report_size; - } - profile.name = std::move(name); - profile.manufacturer = std::move(manufacturer); - profile.capabilities = capabilities; + auto profile = make_base_gamepad_profile( + kind, + std::move(name), + std::move(manufacturer), + vendor_id, + product_id, + version, + capabilities + ); profile.report_descriptor = make_standard_gamepad_report_descriptor(profile.report_id, profile.capabilities.supports_rumble); return profile; diff --git a/src/core/report.cpp b/src/core/report.cpp index 8a2ac75..58335fd 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -116,8 +116,8 @@ namespace lvh::reports { } std::uint16_t read_u16(const std::vector &report, std::size_t offset) { - const auto low = static_cast(report[offset]); - const auto high = static_cast(report[offset + 1U]); + const auto low = std::to_integer(to_byte(report[offset])); + const auto high = std::to_integer(to_byte(report[offset + 1U])); return static_cast(low | static_cast(high << 8U)); } @@ -176,92 +176,74 @@ namespace lvh::reports { return static_cast(std::lround((static_cast(value) / 255.0F) * 65535.0F)); } + void set_button_bit(std::uint16_t &bits, const ButtonSet &buttons, std::uint16_t bit, GamepadButton button) { + if (buttons.test(button)) { + bits |= static_cast(1U << bit); + } + } + std::uint16_t common_button_bits(const ButtonSet &buttons) { auto bits = std::uint16_t {}; - const auto set_bit = [&buttons, &bits](std::uint16_t bit, GamepadButton button) { - if (buttons.test(button)) { - bits |= static_cast(1U << bit); - } - }; - - set_bit(0U, GamepadButton::a); - set_bit(1U, GamepadButton::b); - set_bit(2U, GamepadButton::x); - set_bit(3U, GamepadButton::y); - set_bit(4U, GamepadButton::left_shoulder); - set_bit(5U, GamepadButton::right_shoulder); - set_bit(6U, GamepadButton::back); - set_bit(7U, GamepadButton::start); - set_bit(8U, GamepadButton::left_stick); - set_bit(9U, GamepadButton::right_stick); - set_bit(10U, GamepadButton::guide); - set_bit(11U, GamepadButton::misc1); + set_button_bit(bits, buttons, 0U, GamepadButton::a); + set_button_bit(bits, buttons, 1U, GamepadButton::b); + set_button_bit(bits, buttons, 2U, GamepadButton::x); + set_button_bit(bits, buttons, 3U, GamepadButton::y); + set_button_bit(bits, buttons, 4U, GamepadButton::left_shoulder); + set_button_bit(bits, buttons, 5U, GamepadButton::right_shoulder); + set_button_bit(bits, buttons, 6U, GamepadButton::back); + set_button_bit(bits, buttons, 7U, GamepadButton::start); + set_button_bit(bits, buttons, 8U, GamepadButton::left_stick); + set_button_bit(bits, buttons, 9U, GamepadButton::right_stick); + set_button_bit(bits, buttons, 10U, GamepadButton::guide); + set_button_bit(bits, buttons, 11U, GamepadButton::misc1); return bits; } std::uint16_t standard_gamepad_button_bits(const ButtonSet &buttons) { auto bits = common_button_bits(buttons); - const auto set_bit = [&buttons, &bits](std::uint16_t bit, GamepadButton button) { - if (buttons.test(button)) { - bits |= static_cast(1U << bit); - } - }; - - set_bit(12U, GamepadButton::dpad_up); - set_bit(13U, GamepadButton::dpad_down); - set_bit(14U, GamepadButton::dpad_left); - set_bit(15U, GamepadButton::dpad_right); + set_button_bit(bits, buttons, 12U, GamepadButton::dpad_up); + set_button_bit(bits, buttons, 13U, GamepadButton::dpad_down); + set_button_bit(bits, buttons, 14U, GamepadButton::dpad_left); + set_button_bit(bits, buttons, 15U, GamepadButton::dpad_right); return bits; } std::uint16_t xbox_gip_button_bits(const ButtonSet &buttons) { auto bits = std::uint16_t {}; - const auto set_bit = [&buttons, &bits](std::uint16_t bit, GamepadButton button) { - if (buttons.test(button)) { - bits |= static_cast(1U << bit); - } - }; - - set_bit(0U, GamepadButton::a); - set_bit(1U, GamepadButton::b); - set_bit(2U, GamepadButton::x); - set_bit(3U, GamepadButton::y); - set_bit(4U, GamepadButton::left_shoulder); - set_bit(5U, GamepadButton::right_shoulder); - set_bit(6U, GamepadButton::back); - set_bit(7U, GamepadButton::start); - set_bit(8U, GamepadButton::left_stick); - set_bit(9U, GamepadButton::right_stick); - set_bit(11U, GamepadButton::misc1); + set_button_bit(bits, buttons, 0U, GamepadButton::a); + set_button_bit(bits, buttons, 1U, GamepadButton::b); + set_button_bit(bits, buttons, 2U, GamepadButton::x); + set_button_bit(bits, buttons, 3U, GamepadButton::y); + set_button_bit(bits, buttons, 4U, GamepadButton::left_shoulder); + set_button_bit(bits, buttons, 5U, GamepadButton::right_shoulder); + set_button_bit(bits, buttons, 6U, GamepadButton::back); + set_button_bit(bits, buttons, 7U, GamepadButton::start); + set_button_bit(bits, buttons, 8U, GamepadButton::left_stick); + set_button_bit(bits, buttons, 9U, GamepadButton::right_stick); + set_button_bit(bits, buttons, 11U, GamepadButton::misc1); return bits; } std::uint16_t switch_pro_button_bits(const GamepadState &state) { auto bits = std::uint16_t {}; - const auto set_bit = [&state, &bits](std::uint16_t bit, GamepadButton button) { - if (state.buttons.test(button)) { - bits |= static_cast(1U << bit); - } - }; - - set_bit(0U, GamepadButton::a); - set_bit(1U, GamepadButton::b); - set_bit(2U, GamepadButton::x); - set_bit(3U, GamepadButton::y); - set_bit(4U, GamepadButton::left_shoulder); - set_bit(5U, GamepadButton::right_shoulder); + set_button_bit(bits, state.buttons, 0U, GamepadButton::a); + set_button_bit(bits, state.buttons, 1U, GamepadButton::b); + set_button_bit(bits, state.buttons, 2U, GamepadButton::x); + set_button_bit(bits, state.buttons, 3U, GamepadButton::y); + set_button_bit(bits, state.buttons, 4U, GamepadButton::left_shoulder); + set_button_bit(bits, state.buttons, 5U, GamepadButton::right_shoulder); if (state.left_trigger > 0.0F) { bits |= static_cast(1U << 6U); } if (state.right_trigger > 0.0F) { bits |= static_cast(1U << 7U); } - set_bit(8U, GamepadButton::back); - set_bit(9U, GamepadButton::start); - set_bit(10U, GamepadButton::left_stick); - set_bit(11U, GamepadButton::right_stick); - set_bit(12U, GamepadButton::guide); - set_bit(13U, GamepadButton::misc1); + set_button_bit(bits, state.buttons, 8U, GamepadButton::back); + set_button_bit(bits, state.buttons, 9U, GamepadButton::start); + set_button_bit(bits, state.buttons, 10U, GamepadButton::left_stick); + set_button_bit(bits, state.buttons, 11U, GamepadButton::right_stick); + set_button_bit(bits, state.buttons, 12U, GamepadButton::guide); + set_button_bit(bits, state.buttons, 13U, GamepadButton::misc1); return bits; } @@ -777,8 +759,7 @@ namespace lvh::reports { } std::vector pack_xbox_gip_input_report(const DeviceProfile &profile, const GamepadState &state) { - constexpr std::size_t xbox_gip_input_report_size = 17; - if (profile.input_report_size < xbox_gip_input_report_size) { + if (constexpr std::size_t xbox_gip_input_report_size = 17; profile.input_report_size < xbox_gip_input_report_size) { return {}; } @@ -827,8 +808,7 @@ namespace lvh::reports { } std::vector pack_switch_pro_input_report(const DeviceProfile &profile, const GamepadState &state) { - constexpr std::size_t switch_pro_input_report_size = 64; - if (profile.input_report_size < switch_pro_input_report_size) { + if (constexpr std::size_t switch_pro_input_report_size = 64; profile.input_report_size < switch_pro_input_report_size) { return {}; } @@ -847,20 +827,22 @@ namespace lvh::reports { std::vector pack_input_report(const DeviceProfile &profile, const GamepadState &state) { if (profile.device_type == DeviceType::gamepad) { - if (profile.gamepad_kind == GamepadProfileKind::xbox_one || profile.gamepad_kind == GamepadProfileKind::xbox_series) { - return pack_xbox_gip_input_report(profile, state); - } - if (profile.gamepad_kind == GamepadProfileKind::dualshock4) { - return pack_dualshock4_input_report(profile, state); - } - if (profile.gamepad_kind == GamepadProfileKind::dualsense) { - return pack_dualsense_input_report(profile, state); - } - if (profile.gamepad_kind == GamepadProfileKind::switch_pro) { - return pack_switch_pro_input_report(profile, state); - } - if (profile.gamepad_kind == GamepadProfileKind::generic) { - return pack_standard_gamepad_input_report(profile, state); + switch (profile.gamepad_kind) { + using enum GamepadProfileKind; + + case xbox_one: + case xbox_series: + return pack_xbox_gip_input_report(profile, state); + case dualshock4: + return pack_dualshock4_input_report(profile, state); + case dualsense: + return pack_dualsense_input_report(profile, state); + case switch_pro: + return pack_switch_pro_input_report(profile, state); + case generic: + return pack_standard_gamepad_input_report(profile, state); + case xbox_360: + break; } } @@ -874,12 +856,8 @@ namespace lvh::reports { std::vector report; report.reserve(common_report_size); report.push_back(profile.report_id); - append_u16( - report, - static_cast( - common_button_bits(normalized.buttons) | static_cast(hat_from_buttons(normalized.buttons) << 12U) - ) - ); + const auto dpad_hat_bits = static_cast(hat_from_buttons(normalized.buttons) << 12U); + append_u16(report, static_cast(common_button_bits(normalized.buttons) | dpad_hat_bits)); report.push_back(normalize_u8_axis(normalized.left_stick.x)); report.push_back(normalize_u8_axis(-normalized.left_stick.y)); report.push_back(normalize_trigger(normalized.left_trigger)); diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index 4ea4a5b..949e546 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -31,14 +31,15 @@ #include #include #include -#include #include -#include +#include +#include #include #include #include #include #include +#include #include // local includes @@ -60,7 +61,7 @@ namespace { constexpr auto symbolic_link_name = L"\\DosDevices\\LibVirtualHid"; constexpr auto global_symbolic_link_name = L"\\DosDevices\\Global\\LibVirtualHid"; - constexpr auto trace_file_name = L"libvirtualhid-umdf-driver.log"; + constexpr auto trace_file_name = std::wstring_view {L"libvirtualhid-umdf-driver.log"}; struct DeviceRecord { std::mutex mutex; @@ -91,33 +92,25 @@ namespace { void trace_status(const char *step, NTSTATUS status = STATUS_SUCCESS) { static std::atomic sequence {0}; - wchar_t trace_file_path[MAX_PATH] {}; constexpr auto trace_file_path_length = static_cast(MAX_PATH); - auto trace_path_size = GetWindowsDirectoryW(trace_file_path, trace_file_path_length); + std::wstring trace_file_path(trace_file_path_length, L'\0'); + auto trace_path_size = GetWindowsDirectoryW(trace_file_path.data(), trace_file_path_length); if (trace_path_size == 0U || trace_path_size >= trace_file_path_length) { return; } - constexpr auto trace_directory = L"\\Temp\\"; - const auto directory_size = wcslen(trace_directory); - const auto file_name_size = wcslen(trace_file_name); - if (trace_path_size + directory_size + file_name_size >= trace_file_path_length) { + constexpr auto trace_directory = std::wstring_view {L"\\Temp\\"}; + const auto required_path_size = + static_cast(trace_path_size) + trace_directory.size() + trace_file_name.size(); + if (required_path_size >= trace_file_path_length) { return; } - std::memcpy( - trace_file_path + trace_path_size, - trace_directory, - directory_size * sizeof(wchar_t) - ); - trace_path_size += static_cast(directory_size); - std::memcpy( - trace_file_path + trace_path_size, - trace_file_name, - (file_name_size + 1U) * sizeof(wchar_t) - ); + trace_file_path.resize(trace_path_size); + trace_file_path.append(trace_directory); + trace_file_path.append(trace_file_name); const auto file = CreateFileW( - trace_file_path, + trace_file_path.c_str(), FILE_APPEND_DATA, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, @@ -132,11 +125,8 @@ namespace { SYSTEMTIME time {}; GetSystemTime(&time); - char line[320] {}; - const auto written_chars = std::snprintf( - line, - sizeof(line), - "%04hu-%02hu-%02huT%02hu:%02hu:%02hu.%03huZ [%lu] %s status=0x%08lX\r\n", + const auto line = std::format( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z [{}] {} status=0x{:08X}\r\n", time.wYear, time.wMonth, time.wDay, @@ -148,11 +138,10 @@ namespace { step, static_cast(status) ); - if (written_chars > 0) { - DWORD bytes_written {}; - const auto bytes_to_write = static_cast(std::min(written_chars, sizeof(line) - 1U)); - static_cast(WriteFile(file, line, bytes_to_write, &bytes_written, nullptr)); - } + DWORD bytes_written {}; + const auto bytes_to_write = + static_cast(std::min(line.size(), static_cast(std::numeric_limits::max()))); + static_cast(WriteFile(file, line.data(), bytes_to_write, &bytes_written, nullptr)); static_cast(CloseHandle(file)); } @@ -301,15 +290,18 @@ namespace { { auto &state = driver_state(); std::lock_guard lock {state.devices_mutex}; - for (auto iter = state.devices.begin(); iter != state.devices.end();) { - if (iter->second->owner_device == device) { + static_cast(std::erase_if( + state.devices, + [&](const auto &entry) { + if (entry.second->owner_device != device) { + return false; + } + trace_status("delete_vhf_devices_for_device matched"); - devices.push_back(iter->second); - iter = state.devices.erase(iter); - } else { - ++iter; + devices.push_back(entry.second); + return true; } - } + )); } for (const auto &record : devices) { @@ -328,15 +320,18 @@ namespace { { auto &state = driver_state(); std::lock_guard lock {state.devices_mutex}; - for (auto iter = state.devices.begin(); iter != state.devices.end();) { - if (iter->second->owner_file == file_object) { + static_cast(std::erase_if( + state.devices, + [&](const auto &entry) { + if (entry.second->owner_file != file_object) { + return false; + } + trace_status("delete_vhf_devices_for_file matched"); - devices.push_back(iter->second); - iter = state.devices.erase(iter); - } else { - ++iter; + devices.push_back(entry.second); + return true; } - } + )); } for (const auto &record : devices) { diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index 7e506ce..132e48d 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -41,7 +41,7 @@ // platform includes // clang-format off #include -#include +#include // clang-format on namespace lvh::detail { @@ -162,8 +162,8 @@ namespace lvh::detail { continue; } - std::vector buffer(required_size); - auto *detail_data = reinterpret_cast(buffer.data()); + auto buffer = std::make_unique_for_overwrite(required_size); + auto *detail_data = static_cast(static_cast(buffer.get())); detail_data->cbSize = sizeof(SP_DEVICE_INTERFACE_DETAIL_DATA_A); if (::SetupDiGetDeviceInterfaceDetailA(device_info_set, &interface_data, detail_data, required_size, nullptr, nullptr) != FALSE) { paths.emplace_back(detail_data->DevicePath); diff --git a/tests/unit/test_report.cpp b/tests/unit/test_report.cpp index 869a114..f4f7a06 100644 --- a/tests/unit/test_report.cpp +++ b/tests/unit/test_report.cpp @@ -16,6 +16,10 @@ #include namespace { + std::byte to_byte(std::uint8_t value) { + return static_cast(value); + } + std::uint32_t test_crc32(std::span buffer, std::uint32_t seed = 0) { auto crc = seed ^ 0xFFFFFFFFU; for (const auto byte : buffer) { @@ -39,8 +43,26 @@ namespace { (static_cast(bytes[offset + 3U]) << 24U); } - std::uint16_t read_u16_le(const std::vector &bytes, std::size_t offset) { - return static_cast(bytes[offset] | static_cast(bytes[offset + 1U] << 8U)); + std::uint16_t read_u16_le(std::span bytes, std::size_t offset) { + const auto low = std::to_integer(to_byte(bytes[offset])); + const auto high = std::to_integer(to_byte(bytes[offset + 1U])); + return static_cast(low | static_cast(high << 8U)); + } + + lvh::GamepadState make_active_gamepad_state() { + using enum lvh::GamepadButton; + + lvh::GamepadState state; + state.buttons.set(a); + state.buttons.set(start); + state.buttons.set(dpad_left); + state.buttons.set(guide); + state.buttons.set(misc1); + state.left_stick = {1.0F, -1.0F}; + state.right_stick = {0.5F, -0.5F}; + state.left_trigger = 0.25F; + state.right_trigger = 1.0F; + return state; } } // namespace @@ -76,16 +98,7 @@ TEST(ReportTest, EncodesHatSwitch) { TEST(ReportTest, PacksCommonGamepadReport) { auto profile = lvh::profiles::xbox_360(); - lvh::GamepadState state; - state.buttons.set(lvh::GamepadButton::a); - state.buttons.set(lvh::GamepadButton::start); - state.buttons.set(lvh::GamepadButton::dpad_left); - state.buttons.set(lvh::GamepadButton::guide); - state.buttons.set(lvh::GamepadButton::misc1); - state.left_stick = {1.0F, -1.0F}; - state.right_stick = {0.5F, -0.5F}; - state.left_trigger = 0.25F; - state.right_trigger = 1.0F; + auto state = make_active_gamepad_state(); const auto report = lvh::reports::pack_input_report(profile, state); @@ -104,16 +117,7 @@ TEST(ReportTest, PacksCommonGamepadReport) { TEST(ReportTest, PacksStandardGamepadReport) { auto profile = lvh::profiles::generic_gamepad(); - lvh::GamepadState state; - state.buttons.set(lvh::GamepadButton::a); - state.buttons.set(lvh::GamepadButton::start); - state.buttons.set(lvh::GamepadButton::dpad_left); - state.buttons.set(lvh::GamepadButton::guide); - state.buttons.set(lvh::GamepadButton::misc1); - state.left_stick = {1.0F, -1.0F}; - state.right_stick = {0.5F, -0.5F}; - state.left_trigger = 0.25F; - state.right_trigger = 1.0F; + auto state = make_active_gamepad_state(); const auto report = lvh::reports::pack_input_report(profile, state); @@ -132,47 +136,37 @@ TEST(ReportTest, PacksStandardGamepadReport) { TEST(ReportTest, PacksXboxGipReport) { auto profile = lvh::profiles::xbox_series(); - lvh::GamepadState state; - state.buttons.set(lvh::GamepadButton::a); - state.buttons.set(lvh::GamepadButton::start); - state.buttons.set(lvh::GamepadButton::dpad_left); - state.buttons.set(lvh::GamepadButton::guide); - state.buttons.set(lvh::GamepadButton::misc1); - state.left_stick = {1.0F, -1.0F}; - state.right_stick = {0.5F, -0.5F}; - state.left_trigger = 0.25F; - state.right_trigger = 1.0F; + auto state = make_active_gamepad_state(); state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::discharging, .percentage = 80}; const auto report = lvh::reports::pack_input_report(profile, state); - const auto read_u16 = [&report](std::size_t offset) { - return static_cast(report[offset] | static_cast(report[offset + 1U] << 8U)); - }; ASSERT_EQ(report.size(), profile.input_report_size); EXPECT_EQ(profile.report_id, 0); - EXPECT_EQ(read_u16(0U), 0xFFFF); // Left stick X. - EXPECT_EQ(read_u16(2U), 0xFFFF); // Left stick Y. - EXPECT_EQ(read_u16(4U), 0xBFFF); // Right stick X. - EXPECT_EQ(read_u16(6U), 0xBFFF); // Right stick Y. - EXPECT_EQ(read_u16(8U), 256); // Left trigger. - EXPECT_EQ(read_u16(10U), 1023); // Right trigger. - EXPECT_EQ(read_u16(12U), 0x0881); // A, Start, and Share. + EXPECT_EQ(read_u16_le(report, 0U), 0xFFFF); // Left stick X. + EXPECT_EQ(read_u16_le(report, 2U), 0xFFFF); // Left stick Y. + EXPECT_EQ(read_u16_le(report, 4U), 0xBFFF); // Right stick X. + EXPECT_EQ(read_u16_le(report, 6U), 0xBFFF); // Right stick Y. + EXPECT_EQ(read_u16_le(report, 8U), 256); // Left trigger. + EXPECT_EQ(read_u16_le(report, 10U), 1023); // Right trigger. + EXPECT_EQ(read_u16_le(report, 12U), 0x0881); // A, Start, and Share. EXPECT_EQ(report[14], 7); // D-pad left. EXPECT_EQ(report[15], 1); // Guide/System Main Menu. EXPECT_EQ(report[16], 204); // Battery strength. } TEST(ReportTest, PacksSwitchProReport) { + using enum lvh::GamepadButton; + auto profile = lvh::profiles::switch_pro(); lvh::GamepadState state; - state.buttons.set(lvh::GamepadButton::a); - state.buttons.set(lvh::GamepadButton::b); - state.buttons.set(lvh::GamepadButton::start); - state.buttons.set(lvh::GamepadButton::guide); - state.buttons.set(lvh::GamepadButton::misc1); - state.buttons.set(lvh::GamepadButton::dpad_left); + state.buttons.set(a); + state.buttons.set(b); + state.buttons.set(start); + state.buttons.set(guide); + state.buttons.set(misc1); + state.buttons.set(dpad_left); state.left_stick = {1.0F, -1.0F}; state.right_stick = {0.5F, -0.5F}; state.left_trigger = 0.25F; @@ -193,18 +187,15 @@ TEST(ReportTest, PacksXboxGipNeutralReport) { const auto profile = lvh::profiles::xbox_series(); const auto report = lvh::reports::pack_input_report(profile, {}); - const auto read_u16 = [&report](std::size_t offset) { - return static_cast(report[offset] | static_cast(report[offset + 1U] << 8U)); - }; ASSERT_EQ(report.size(), profile.input_report_size); - EXPECT_EQ(read_u16(0U), 0x8000); // Left stick X. - EXPECT_EQ(read_u16(2U), 0x8000); // Left stick Y. - EXPECT_EQ(read_u16(4U), 0x8000); // Right stick X. - EXPECT_EQ(read_u16(6U), 0x8000); // Right stick Y. - EXPECT_EQ(read_u16(8U), 0x0000); // Left trigger. - EXPECT_EQ(read_u16(10U), 0x0000); // Right trigger. - EXPECT_EQ(read_u16(12U), 0x0000); // Buttons. + EXPECT_EQ(read_u16_le(report, 0U), 0x8000); // Left stick X. + EXPECT_EQ(read_u16_le(report, 2U), 0x8000); // Left stick Y. + EXPECT_EQ(read_u16_le(report, 4U), 0x8000); // Right stick X. + EXPECT_EQ(read_u16_le(report, 6U), 0x8000); // Right stick Y. + EXPECT_EQ(read_u16_le(report, 8U), 0x0000); // Left trigger. + EXPECT_EQ(read_u16_le(report, 10U), 0x0000); // Right trigger. + EXPECT_EQ(read_u16_le(report, 12U), 0x0000); // Buttons. EXPECT_EQ(report[14], 0); // Neutral D-pad. EXPECT_EQ(report[15], 0); // Guide/System Main Menu. EXPECT_EQ(report[16], 0xFF); // Unknown battery defaults to full. From deb76c257b59e00d0f1e7588962bb9d99198b7b1 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:44:20 -0400 Subject: [PATCH 23/27] Resolve remaining Sonar report packing issues --- src/core/profiles.cpp | 12 +++--- src/core/report.cpp | 89 ++++++++++++++++++++++++------------------- 2 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index 913d5cc..bc0248a 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -52,21 +52,21 @@ namespace lvh::profiles { constexpr std::size_t dualsense_bluetooth_output_report_size = 78; - std::uint8_t hex_nibble(char digit) { + std::byte hex_nibble(char digit) { if (digit >= '0' && digit <= '9') { - return static_cast(digit - '0'); + return static_cast(digit - '0'); } if (digit >= 'A' && digit <= 'F') { - return static_cast(digit - 'A' + 10); + return static_cast(digit - 'A' + 10); } if (digit >= 'a' && digit <= 'f') { - return static_cast(digit - 'a' + 10); + return static_cast(digit - 'a' + 10); } - return 0; + return std::byte {0}; } std::byte byte_from_hex(char high, char low) { - return static_cast((hex_nibble(high) << 4U) | hex_nibble(low)); + return (hex_nibble(high) << 4U) | hex_nibble(low); } std::vector bytes_from_hex(std::string_view hex) { diff --git a/src/core/report.cpp b/src/core/report.cpp index 58335fd..727afb0 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -183,67 +183,75 @@ namespace lvh::reports { } std::uint16_t common_button_bits(const ButtonSet &buttons) { + using enum GamepadButton; + auto bits = std::uint16_t {}; - set_button_bit(bits, buttons, 0U, GamepadButton::a); - set_button_bit(bits, buttons, 1U, GamepadButton::b); - set_button_bit(bits, buttons, 2U, GamepadButton::x); - set_button_bit(bits, buttons, 3U, GamepadButton::y); - set_button_bit(bits, buttons, 4U, GamepadButton::left_shoulder); - set_button_bit(bits, buttons, 5U, GamepadButton::right_shoulder); - set_button_bit(bits, buttons, 6U, GamepadButton::back); - set_button_bit(bits, buttons, 7U, GamepadButton::start); - set_button_bit(bits, buttons, 8U, GamepadButton::left_stick); - set_button_bit(bits, buttons, 9U, GamepadButton::right_stick); - set_button_bit(bits, buttons, 10U, GamepadButton::guide); - set_button_bit(bits, buttons, 11U, GamepadButton::misc1); + set_button_bit(bits, buttons, 0U, a); + set_button_bit(bits, buttons, 1U, b); + set_button_bit(bits, buttons, 2U, x); + set_button_bit(bits, buttons, 3U, y); + set_button_bit(bits, buttons, 4U, left_shoulder); + set_button_bit(bits, buttons, 5U, right_shoulder); + set_button_bit(bits, buttons, 6U, back); + set_button_bit(bits, buttons, 7U, start); + set_button_bit(bits, buttons, 8U, left_stick); + set_button_bit(bits, buttons, 9U, right_stick); + set_button_bit(bits, buttons, 10U, guide); + set_button_bit(bits, buttons, 11U, misc1); return bits; } std::uint16_t standard_gamepad_button_bits(const ButtonSet &buttons) { + using enum GamepadButton; + auto bits = common_button_bits(buttons); - set_button_bit(bits, buttons, 12U, GamepadButton::dpad_up); - set_button_bit(bits, buttons, 13U, GamepadButton::dpad_down); - set_button_bit(bits, buttons, 14U, GamepadButton::dpad_left); - set_button_bit(bits, buttons, 15U, GamepadButton::dpad_right); + set_button_bit(bits, buttons, 12U, dpad_up); + set_button_bit(bits, buttons, 13U, dpad_down); + set_button_bit(bits, buttons, 14U, dpad_left); + set_button_bit(bits, buttons, 15U, dpad_right); return bits; } std::uint16_t xbox_gip_button_bits(const ButtonSet &buttons) { + using enum GamepadButton; + auto bits = std::uint16_t {}; - set_button_bit(bits, buttons, 0U, GamepadButton::a); - set_button_bit(bits, buttons, 1U, GamepadButton::b); - set_button_bit(bits, buttons, 2U, GamepadButton::x); - set_button_bit(bits, buttons, 3U, GamepadButton::y); - set_button_bit(bits, buttons, 4U, GamepadButton::left_shoulder); - set_button_bit(bits, buttons, 5U, GamepadButton::right_shoulder); - set_button_bit(bits, buttons, 6U, GamepadButton::back); - set_button_bit(bits, buttons, 7U, GamepadButton::start); - set_button_bit(bits, buttons, 8U, GamepadButton::left_stick); - set_button_bit(bits, buttons, 9U, GamepadButton::right_stick); - set_button_bit(bits, buttons, 11U, GamepadButton::misc1); + set_button_bit(bits, buttons, 0U, a); + set_button_bit(bits, buttons, 1U, b); + set_button_bit(bits, buttons, 2U, x); + set_button_bit(bits, buttons, 3U, y); + set_button_bit(bits, buttons, 4U, left_shoulder); + set_button_bit(bits, buttons, 5U, right_shoulder); + set_button_bit(bits, buttons, 6U, back); + set_button_bit(bits, buttons, 7U, start); + set_button_bit(bits, buttons, 8U, left_stick); + set_button_bit(bits, buttons, 9U, right_stick); + set_button_bit(bits, buttons, 11U, misc1); return bits; } std::uint16_t switch_pro_button_bits(const GamepadState &state) { + using enum GamepadButton; + auto bits = std::uint16_t {}; - set_button_bit(bits, state.buttons, 0U, GamepadButton::a); - set_button_bit(bits, state.buttons, 1U, GamepadButton::b); - set_button_bit(bits, state.buttons, 2U, GamepadButton::x); - set_button_bit(bits, state.buttons, 3U, GamepadButton::y); - set_button_bit(bits, state.buttons, 4U, GamepadButton::left_shoulder); - set_button_bit(bits, state.buttons, 5U, GamepadButton::right_shoulder); + set_button_bit(bits, state.buttons, 0U, a); + set_button_bit(bits, state.buttons, 1U, b); + set_button_bit(bits, state.buttons, 2U, x); + set_button_bit(bits, state.buttons, 3U, y); + set_button_bit(bits, state.buttons, 4U, left_shoulder); + set_button_bit(bits, state.buttons, 5U, right_shoulder); if (state.left_trigger > 0.0F) { bits |= static_cast(1U << 6U); } if (state.right_trigger > 0.0F) { bits |= static_cast(1U << 7U); } - set_button_bit(bits, state.buttons, 8U, GamepadButton::back); - set_button_bit(bits, state.buttons, 9U, GamepadButton::start); - set_button_bit(bits, state.buttons, 10U, GamepadButton::left_stick); - set_button_bit(bits, state.buttons, 11U, GamepadButton::right_stick); - set_button_bit(bits, state.buttons, 12U, GamepadButton::guide); - set_button_bit(bits, state.buttons, 13U, GamepadButton::misc1); + set_button_bit(bits, state.buttons, 8U, back); + set_button_bit(bits, state.buttons, 9U, start); + set_button_bit(bits, state.buttons, 10U, left_stick); + set_button_bit(bits, state.buttons, 11U, right_stick); + set_button_bit(bits, state.buttons, 12U, guide); + set_button_bit(bits, state.buttons, 13U, misc1); return bits; } @@ -856,7 +864,8 @@ namespace lvh::reports { std::vector report; report.reserve(common_report_size); report.push_back(profile.report_id); - const auto dpad_hat_bits = static_cast(hat_from_buttons(normalized.buttons) << 12U); + const auto dpad_hat = static_cast(hat_from_buttons(normalized.buttons)); + const auto dpad_hat_bits = static_cast(dpad_hat << 12U); append_u16(report, static_cast(common_button_bits(normalized.buttons) | dpad_hat_bits)); report.push_back(normalize_u8_axis(normalized.left_stick.x)); report.push_back(normalize_u8_axis(-normalized.left_stick.y)); From 4ff8a63fa071b793bbf53b0ee9d92422b5908da2 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 22:53:26 -0400 Subject: [PATCH 24/27] Refactor driver scripts and button bit packing Extract duplicated Windows driver device-enumeration logic into a new shared `libvirtualhid-driver-common.ps1` module, source it from install/uninstall scripts, and package it in CMake so distributed scripts stay in sync. The uninstall flow was updated to consume the shared registry helper output. In core report/profile code, improve readability and maintainability by splitting hex nibble assembly into named locals, replacing repetitive button-bit setters with constexpr button maps + a shared `button_bits` helper, and centralizing d-pad hat bit packing with `dpad_hat_bits()`. --- cmake/packaging/windows.cmake | 1 + scripts/windows/install-driver.ps1 | 146 ++---------------- .../windows/libvirtualhid-driver-common.ps1 | 70 +++++++++ scripts/windows/uninstall-driver.ps1 | 63 +------- src/core/profiles.cpp | 4 +- src/core/report.cpp | 139 ++++++++++------- 6 files changed, 171 insertions(+), 252 deletions(-) create mode 100644 scripts/windows/libvirtualhid-driver-common.ps1 diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake index 0f671c4..197889a 100644 --- a/cmake/packaging/windows.cmake +++ b/cmake/packaging/windows.cmake @@ -14,6 +14,7 @@ set(LIBVIRTUALHID_DRIVER_TEST_CERTIFICATE "" CACHE FILEPATH "Optional public test certificate to include in the Windows driver installer.") install(FILES + "${PROJECT_SOURCE_DIR}/scripts/windows/libvirtualhid-driver-common.ps1" "${PROJECT_SOURCE_DIR}/scripts/windows/install-driver.ps1" "${PROJECT_SOURCE_DIR}/scripts/windows/uninstall-driver.ps1" DESTINATION "scripts/windows" diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 index e295328..1444501 100644 --- a/scripts/windows/install-driver.ps1 +++ b/scripts/windows/install-driver.ps1 @@ -18,6 +18,7 @@ param( $ErrorActionPreference = "Stop" $script:LibVirtualHidTranscriptStarted = $false +. (Join-Path $PSScriptRoot "libvirtualhid-driver-common.ps1") function Start-LibVirtualHidTranscript { [CmdletBinding(SupportsShouldProcess)] @@ -120,62 +121,30 @@ namespace LibVirtualHid.SetupApi { } [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern bool SetupDiGetINFClass( - string infName, - out Guid classGuid, - StringBuilder className, - uint classNameSize, - out uint requiredSize); + private static extern bool SetupDiGetINFClass(string infName, out Guid classGuid, StringBuilder className, uint classNameSize, out uint requiredSize); [DllImport("setupapi.dll", SetLastError = true)] - private static extern IntPtr SetupDiCreateDeviceInfoList( - ref Guid classGuid, - IntPtr hwndParent); + private static extern IntPtr SetupDiCreateDeviceInfoList(ref Guid classGuid, IntPtr hwndParent); [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern bool SetupDiCreateDeviceInfo( - IntPtr deviceInfoSet, - string deviceName, - ref Guid classGuid, - string deviceDescription, - IntPtr hwndParent, - uint creationFlags, - ref SpDevinfoData deviceInfoData); + private static extern bool SetupDiCreateDeviceInfo(IntPtr deviceInfoSet, string deviceName, ref Guid classGuid, string deviceDescription, IntPtr hwndParent, uint creationFlags, ref SpDevinfoData deviceInfoData); [DllImport("setupapi.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern bool SetupDiSetDeviceRegistryProperty( - IntPtr deviceInfoSet, - ref SpDevinfoData deviceInfoData, - uint property, - byte[] propertyBuffer, - uint propertyBufferSize); + private static extern bool SetupDiSetDeviceRegistryProperty(IntPtr deviceInfoSet, ref SpDevinfoData deviceInfoData, uint property, byte[] propertyBuffer, uint propertyBufferSize); [DllImport("setupapi.dll", SetLastError = true)] - private static extern bool SetupDiCallClassInstaller( - uint installFunction, - IntPtr deviceInfoSet, - ref SpDevinfoData deviceInfoData); + private static extern bool SetupDiCallClassInstaller(uint installFunction, IntPtr deviceInfoSet, ref SpDevinfoData deviceInfoData); [DllImport("setupapi.dll", SetLastError = true)] private static extern bool SetupDiDestroyDeviceInfoList(IntPtr deviceInfoSet); [DllImport("newdev.dll", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern bool UpdateDriverForPlugAndPlayDevices( - IntPtr hwndParent, - string hardwareId, - string fullInfPath, - uint installFlags, - out bool rebootRequired); + private static extern bool UpdateDriverForPlugAndPlayDevices(IntPtr hwndParent, string hardwareId, string fullInfPath, uint installFlags, out bool rebootRequired); public static void Update(string infPath, string hardwareId, out bool rebootRequired) { rebootRequired = false; - if (!UpdateDriverForPlugAndPlayDevices( - IntPtr.Zero, - hardwareId, - infPath, - InstallFlagForce | InstallFlagNonInteractive, - out rebootRequired)) { + if (!UpdateDriverForPlugAndPlayDevices(IntPtr.Zero, hardwareId, infPath, InstallFlagForce | InstallFlagNonInteractive, out rebootRequired)) { ThrowLastWin32Error("UpdateDriverForPlugAndPlayDevices"); } } @@ -198,27 +167,14 @@ namespace LibVirtualHid.SetupApi { } try { - var deviceInfoData = new SpDevinfoData(); - deviceInfoData.cbSize = (uint) Marshal.SizeOf(typeof(SpDevinfoData)); - - if (!SetupDiCreateDeviceInfo( - deviceInfoSet, - rootDeviceName, - ref classGuid, - null, - IntPtr.Zero, - DicdGenerateId, - ref deviceInfoData)) { + var deviceInfoData = new SpDevinfoData { cbSize = (uint) Marshal.SizeOf(typeof(SpDevinfoData)) }; + + if (!SetupDiCreateDeviceInfo(deviceInfoSet, rootDeviceName, ref classGuid, null, IntPtr.Zero, DicdGenerateId, ref deviceInfoData)) { ThrowLastWin32Error("SetupDiCreateDeviceInfo"); } byte[] hardwareIds = Encoding.Unicode.GetBytes(hardwareId + "\0\0"); - if (!SetupDiSetDeviceRegistryProperty( - deviceInfoSet, - ref deviceInfoData, - SpdrpHardwareId, - hardwareIds, - (uint) hardwareIds.Length)) { + if (!SetupDiSetDeviceRegistryProperty(deviceInfoSet, ref deviceInfoData, SpdrpHardwareId, hardwareIds, (uint) hardwareIds.Length)) { ThrowLastWin32Error("SetupDiSetDeviceRegistryProperty"); } @@ -256,78 +212,6 @@ namespace LibVirtualHid.SetupApi { "@ } -function Get-RootDeviceInstanceId { - param([string] $TargetHardwareId) - - try { - $devices = & pnputil.exe /enum-devices /deviceid $TargetHardwareId /deviceids - if ($LASTEXITCODE -eq 0) { - $instanceIds = @($devices | - Where-Object { $_ -match "^\s*Instance ID\s*:\s*(.+)$" } | - ForEach-Object { $Matches[1].Trim() }) - if ($instanceIds.Count -gt 0) { - return $instanceIds - } - } - } catch { - Write-Verbose "Unable to enumerate PnP devices with pnputil: $($_.Exception.Message)" - } - - try { - $prefix = "$TargetHardwareId\" - @(Get-CimInstance -ClassName Win32_PnPEntity -ErrorAction Stop | - Where-Object { $_.PNPDeviceID -like "$prefix*" -or $_.HardwareID -contains $TargetHardwareId } | - ForEach-Object { $_.PNPDeviceID }) - } catch { - Write-Verbose "Unable to enumerate PnP devices: $($_.Exception.Message)" - @() - } -} - -function Get-RegistryRootDevice { - param([string] $TargetHardwareId) - - $rootKey = "HKLM:\SYSTEM\CurrentControlSet\Enum\ROOT" - Get-ChildItem -LiteralPath $rootKey -ErrorAction SilentlyContinue | ForEach-Object { - $rootDeviceId = $_.PSChildName - Get-ChildItem -LiteralPath $_.PSPath -ErrorAction SilentlyContinue | ForEach-Object { - $instanceId = "ROOT\$rootDeviceId\$($_.PSChildName)" - $hardwareIds = @() - try { - $hardwareIds = @((Get-ItemProperty -LiteralPath $_.PSPath -Name HardwareID -ErrorAction Stop).HardwareID) - } catch { - $hardwareIds = @() - } - - $hasExactHardwareId = $hardwareIds -contains $TargetHardwareId - $hasCorruptHardwareId = ( - $hardwareIds.Count -gt 1 -and - -not $hasExactHardwareId -and - (($hardwareIds -join "") -ieq $TargetHardwareId) - ) - $hasTargetInstanceId = $instanceId -like "$TargetHardwareId\*" - - if ($hasExactHardwareId -or $hasCorruptHardwareId -or $hasTargetInstanceId) { - $classGuid = $null - $classGuid = $null - try { - $deviceProperties = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction Stop - $classGuid = $deviceProperties.ClassGUID - } catch { - Write-Verbose "Unable to read registry properties for $instanceId`: $($_.Exception.Message)" - } - - [pscustomobject]@{ - InstanceId = $instanceId - HasExactHardwareId = $hasExactHardwareId - HasCorruptHardwareId = $hasCorruptHardwareId - HasLegacyHidClass = $classGuid -ieq "{745a17a0-74d3-11d0-b6fe-00a0c90f57da}" - } - } - } - } -} - function Remove-DeviceInstance { [CmdletBinding(SupportsShouldProcess)] param([string] $InstanceId) @@ -431,12 +315,12 @@ try { return } - $registryRootDevices = @(Get-RegistryRootDevice -TargetHardwareId $HardwareId) + $registryRootDevices = @(Get-LibVirtualHidRegistryRootDevice -TargetHardwareId $HardwareId) foreach ($device in ($registryRootDevices | Where-Object { $_.HasCorruptHardwareId -or $_.HasLegacyHidClass })) { Remove-DeviceInstance -InstanceId $device.InstanceId } - $rootDevices = @(Get-RootDeviceInstanceId -TargetHardwareId $HardwareId) + $rootDevices = @(Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId) if ($rootDevices.Count -gt 0) { Write-Information "Updating the existing $HardwareId device driver." -InformationAction Continue foreach ($rootDevice in $rootDevices) { @@ -453,7 +337,7 @@ try { Install-RootDeviceWithSetupApi -Path $resolvedInf -TargetHardwareId $HardwareId } - $rootDevices = @(Get-RootDeviceInstanceId -TargetHardwareId $HardwareId) + $rootDevices = @(Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId) foreach ($rootDevice in $rootDevices) { Set-RootDeviceVhfMode -InstanceId $rootDevice } diff --git a/scripts/windows/libvirtualhid-driver-common.ps1 b/scripts/windows/libvirtualhid-driver-common.ps1 new file mode 100644 index 0000000..ad5caa2 --- /dev/null +++ b/scripts/windows/libvirtualhid-driver-common.ps1 @@ -0,0 +1,70 @@ +function Get-LibVirtualHidRootDeviceInstanceId { + param([string] $TargetHardwareId) + + try { + $devices = & pnputil.exe /enum-devices /deviceid $TargetHardwareId /deviceids + if ($LASTEXITCODE -eq 0) { + $instanceIds = @($devices | + Where-Object { $_ -match "^\s*Instance ID\s*:\s*(.+)$" } | + ForEach-Object { $Matches[1].Trim() }) + if ($instanceIds.Count -gt 0) { + return $instanceIds + } + } + } catch { + Write-Verbose "Unable to enumerate PnP devices with pnputil: $($_.Exception.Message)" + } + + try { + $prefix = "$TargetHardwareId\" + @(Get-CimInstance -ClassName Win32_PnPEntity -ErrorAction Stop | + Where-Object { $_.PNPDeviceID -like "$prefix*" -or $_.HardwareID -contains $TargetHardwareId } | + ForEach-Object { $_.PNPDeviceID }) + } catch { + Write-Verbose "Unable to enumerate PnP devices: $($_.Exception.Message)" + @() + } +} + +function Get-LibVirtualHidRegistryRootDevice { + param([string] $TargetHardwareId) + + $rootKey = "HKLM:\SYSTEM\CurrentControlSet\Enum\ROOT" + Get-ChildItem -LiteralPath $rootKey -ErrorAction SilentlyContinue | ForEach-Object { + $rootDeviceId = $_.PSChildName + Get-ChildItem -LiteralPath $_.PSPath -ErrorAction SilentlyContinue | ForEach-Object { + $instanceId = "ROOT\$rootDeviceId\$($_.PSChildName)" + $hardwareIds = @() + try { + $hardwareIds = @((Get-ItemProperty -LiteralPath $_.PSPath -Name HardwareID -ErrorAction Stop).HardwareID) + } catch { + $hardwareIds = @() + } + + $hasExactHardwareId = $hardwareIds -contains $TargetHardwareId + $hasCorruptHardwareId = ( + $hardwareIds.Count -gt 1 -and + -not $hasExactHardwareId -and + (($hardwareIds -join "") -ieq $TargetHardwareId) + ) + $hasTargetInstanceId = $instanceId -like "$TargetHardwareId\*" + + if ($hasExactHardwareId -or $hasCorruptHardwareId -or $hasTargetInstanceId) { + $classGuid = $null + try { + $deviceProperties = Get-ItemProperty -LiteralPath $_.PSPath -ErrorAction Stop + $classGuid = $deviceProperties.ClassGUID + } catch { + Write-Verbose "Unable to read registry properties for $instanceId`: $($_.Exception.Message)" + } + + [pscustomobject]@{ + InstanceId = $instanceId + HasExactHardwareId = $hasExactHardwareId + HasCorruptHardwareId = $hasCorruptHardwareId + HasLegacyHidClass = $classGuid -ieq "{745a17a0-74d3-11d0-b6fe-00a0c90f57da}" + } + } + } + } +} diff --git a/scripts/windows/uninstall-driver.ps1 b/scripts/windows/uninstall-driver.ps1 index b80e226..b89cc3b 100644 --- a/scripts/windows/uninstall-driver.ps1 +++ b/scripts/windows/uninstall-driver.ps1 @@ -16,6 +16,7 @@ param( ) $ErrorActionPreference = "Stop" +. (Join-Path $PSScriptRoot "libvirtualhid-driver-common.ps1") function Invoke-CheckedCommand { param( @@ -83,64 +84,6 @@ function Find-PublishedName { return $publishedNames } -function Get-RootDeviceInstanceId { - param([string] $TargetHardwareId) - - try { - $devices = & pnputil.exe /enum-devices /deviceid $TargetHardwareId /deviceids - if ($LASTEXITCODE -eq 0) { - $instanceIds = @($devices | - Where-Object { $_ -match "^\s*Instance ID\s*:\s*(.+)$" } | - ForEach-Object { $Matches[1].Trim() }) - if ($instanceIds.Count -gt 0) { - return $instanceIds - } - } - } catch { - Write-Verbose "Unable to enumerate PnP devices with pnputil: $($_.Exception.Message)" - } - - try { - $prefix = "$TargetHardwareId\" - @(Get-CimInstance -ClassName Win32_PnPEntity -ErrorAction Stop | - Where-Object { $_.PNPDeviceID -like "$prefix*" -or $_.HardwareID -contains $TargetHardwareId } | - ForEach-Object { $_.PNPDeviceID }) - } catch { - Write-Verbose "Unable to enumerate PnP devices: $($_.Exception.Message)" - @() - } -} - -function Get-RegistryRootDeviceInstanceId { - param([string] $TargetHardwareId) - - $rootKey = "HKLM:\SYSTEM\CurrentControlSet\Enum\ROOT" - Get-ChildItem -LiteralPath $rootKey -ErrorAction SilentlyContinue | ForEach-Object { - $rootDeviceId = $_.PSChildName - Get-ChildItem -LiteralPath $_.PSPath -ErrorAction SilentlyContinue | ForEach-Object { - $instanceId = "ROOT\$rootDeviceId\$($_.PSChildName)" - $hardwareIds = @() - try { - $hardwareIds = @((Get-ItemProperty -LiteralPath $_.PSPath -Name HardwareID -ErrorAction Stop).HardwareID) - } catch { - $hardwareIds = @() - } - - $hasExactHardwareId = $hardwareIds -contains $TargetHardwareId - $hasCorruptHardwareId = ( - $hardwareIds.Count -gt 1 -and - -not $hasExactHardwareId -and - (($hardwareIds -join "") -ieq $TargetHardwareId) - ) - $hasTargetInstanceId = $instanceId -like "$TargetHardwareId\*" - - if ($hasExactHardwareId -or $hasCorruptHardwareId -or $hasTargetInstanceId) { - $instanceId - } - } - } -} - function Remove-DriverCertificate { [CmdletBinding(SupportsShouldProcess)] param([string] $Subject) @@ -166,13 +109,13 @@ if ($devcon -and $PSCmdlet.ShouldProcess($HardwareId, "Remove libvirtualhid deve Invoke-CheckedCommand -FilePath $devcon -Arguments @("remove", $HardwareId) -IgnoreFailure } -foreach ($instanceId in (Get-RootDeviceInstanceId -TargetHardwareId $HardwareId)) { +foreach ($instanceId in (Get-LibVirtualHidRootDeviceInstanceId -TargetHardwareId $HardwareId)) { if ($PSCmdlet.ShouldProcess($instanceId, "Remove libvirtualhid development device with pnputil")) { Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments @("/remove-device", $instanceId) -IgnoreFailure } } -foreach ($instanceId in (Get-RegistryRootDeviceInstanceId -TargetHardwareId $HardwareId | Select-Object -Unique)) { +foreach ($instanceId in (Get-LibVirtualHidRegistryRootDevice -TargetHardwareId $HardwareId | Select-Object -ExpandProperty InstanceId -Unique)) { if ($PSCmdlet.ShouldProcess($instanceId, "Remove libvirtualhid registry-discovered development device with pnputil")) { Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments @("/remove-device", $instanceId) -IgnoreFailure } diff --git a/src/core/profiles.cpp b/src/core/profiles.cpp index bc0248a..9305536 100644 --- a/src/core/profiles.cpp +++ b/src/core/profiles.cpp @@ -66,7 +66,9 @@ namespace lvh::profiles { } std::byte byte_from_hex(char high, char low) { - return (hex_nibble(high) << 4U) | hex_nibble(low); + const auto high_nibble = hex_nibble(high); + const auto low_nibble = hex_nibble(low); + return (high_nibble << 4U) | low_nibble; } std::vector bytes_from_hex(std::string_view hex) { diff --git a/src/core/report.cpp b/src/core/report.cpp index 727afb0..b98ad24 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -176,82 +176,98 @@ namespace lvh::reports { return static_cast(std::lround((static_cast(value) / 255.0F) * 65535.0F)); } - void set_button_bit(std::uint16_t &bits, const ButtonSet &buttons, std::uint16_t bit, GamepadButton button) { - if (buttons.test(button)) { - bits |= static_cast(1U << bit); + struct ButtonBit { + std::uint16_t bit; + GamepadButton button; + }; + + using enum GamepadButton; + + constexpr std::array face_shoulder_button_map { + ButtonBit {0U, a}, + ButtonBit {1U, b}, + ButtonBit {2U, x}, + ButtonBit {3U, y}, + ButtonBit {4U, left_shoulder}, + ButtonBit {5U, right_shoulder}, + }; + + constexpr std::array common_menu_button_map { + ButtonBit {6U, back}, + ButtonBit {7U, start}, + ButtonBit {8U, left_stick}, + ButtonBit {9U, right_stick}, + }; + + constexpr std::array common_extra_button_map { + ButtonBit {10U, guide}, + ButtonBit {11U, misc1}, + }; + + constexpr std::array standard_dpad_button_map { + ButtonBit {12U, dpad_up}, + ButtonBit {13U, dpad_down}, + ButtonBit {14U, dpad_left}, + ButtonBit {15U, dpad_right}, + }; + + constexpr std::array xbox_extra_button_map { + ButtonBit {11U, misc1}, + }; + + constexpr std::array switch_menu_button_map { + ButtonBit {8U, back}, + ButtonBit {9U, start}, + ButtonBit {10U, left_stick}, + ButtonBit {11U, right_stick}, + ButtonBit {12U, guide}, + ButtonBit {13U, misc1}, + }; + + std::uint16_t button_bits(std::span button_map, const ButtonSet &buttons) { + auto bits = std::uint16_t {}; + for (const auto [bit, button] : button_map) { + if (buttons.test(button)) { + bits |= static_cast(1U << bit); + } } + return bits; } std::uint16_t common_button_bits(const ButtonSet &buttons) { - using enum GamepadButton; - - auto bits = std::uint16_t {}; - set_button_bit(bits, buttons, 0U, a); - set_button_bit(bits, buttons, 1U, b); - set_button_bit(bits, buttons, 2U, x); - set_button_bit(bits, buttons, 3U, y); - set_button_bit(bits, buttons, 4U, left_shoulder); - set_button_bit(bits, buttons, 5U, right_shoulder); - set_button_bit(bits, buttons, 6U, back); - set_button_bit(bits, buttons, 7U, start); - set_button_bit(bits, buttons, 8U, left_stick); - set_button_bit(bits, buttons, 9U, right_stick); - set_button_bit(bits, buttons, 10U, guide); - set_button_bit(bits, buttons, 11U, misc1); - return bits; + return static_cast( + button_bits(face_shoulder_button_map, buttons) | + button_bits(common_menu_button_map, buttons) | + button_bits(common_extra_button_map, buttons) + ); } std::uint16_t standard_gamepad_button_bits(const ButtonSet &buttons) { - using enum GamepadButton; - - auto bits = common_button_bits(buttons); - set_button_bit(bits, buttons, 12U, dpad_up); - set_button_bit(bits, buttons, 13U, dpad_down); - set_button_bit(bits, buttons, 14U, dpad_left); - set_button_bit(bits, buttons, 15U, dpad_right); - return bits; + return static_cast( + common_button_bits(buttons) | + button_bits(standard_dpad_button_map, buttons) + ); } std::uint16_t xbox_gip_button_bits(const ButtonSet &buttons) { - using enum GamepadButton; - - auto bits = std::uint16_t {}; - set_button_bit(bits, buttons, 0U, a); - set_button_bit(bits, buttons, 1U, b); - set_button_bit(bits, buttons, 2U, x); - set_button_bit(bits, buttons, 3U, y); - set_button_bit(bits, buttons, 4U, left_shoulder); - set_button_bit(bits, buttons, 5U, right_shoulder); - set_button_bit(bits, buttons, 6U, back); - set_button_bit(bits, buttons, 7U, start); - set_button_bit(bits, buttons, 8U, left_stick); - set_button_bit(bits, buttons, 9U, right_stick); - set_button_bit(bits, buttons, 11U, misc1); - return bits; + return static_cast( + button_bits(face_shoulder_button_map, buttons) | + button_bits(common_menu_button_map, buttons) | + button_bits(xbox_extra_button_map, buttons) + ); } std::uint16_t switch_pro_button_bits(const GamepadState &state) { - using enum GamepadButton; - - auto bits = std::uint16_t {}; - set_button_bit(bits, state.buttons, 0U, a); - set_button_bit(bits, state.buttons, 1U, b); - set_button_bit(bits, state.buttons, 2U, x); - set_button_bit(bits, state.buttons, 3U, y); - set_button_bit(bits, state.buttons, 4U, left_shoulder); - set_button_bit(bits, state.buttons, 5U, right_shoulder); + auto bits = static_cast( + button_bits(face_shoulder_button_map, state.buttons) | + button_bits(switch_menu_button_map, state.buttons) + ); if (state.left_trigger > 0.0F) { bits |= static_cast(1U << 6U); } if (state.right_trigger > 0.0F) { bits |= static_cast(1U << 7U); } - set_button_bit(bits, state.buttons, 8U, back); - set_button_bit(bits, state.buttons, 9U, start); - set_button_bit(bits, state.buttons, 10U, left_stick); - set_button_bit(bits, state.buttons, 11U, right_stick); - set_button_bit(bits, state.buttons, 12U, guide); - set_button_bit(bits, state.buttons, 13U, misc1); return bits; } @@ -766,6 +782,11 @@ namespace lvh::reports { return static_cast(std::lround((static_cast(battery->percentage) / 100.0F) * 255.0F)); } + std::uint16_t dpad_hat_bits(const ButtonSet &buttons) { + const auto hat = to_byte(hat_from_buttons(buttons)); + return static_cast(std::to_integer(hat) << 12U); + } + std::vector pack_xbox_gip_input_report(const DeviceProfile &profile, const GamepadState &state) { if (constexpr std::size_t xbox_gip_input_report_size = 17; profile.input_report_size < xbox_gip_input_report_size) { return {}; @@ -864,9 +885,7 @@ namespace lvh::reports { std::vector report; report.reserve(common_report_size); report.push_back(profile.report_id); - const auto dpad_hat = static_cast(hat_from_buttons(normalized.buttons)); - const auto dpad_hat_bits = static_cast(dpad_hat << 12U); - append_u16(report, static_cast(common_button_bits(normalized.buttons) | dpad_hat_bits)); + append_u16(report, static_cast(common_button_bits(normalized.buttons) | dpad_hat_bits(normalized.buttons))); report.push_back(normalize_u8_axis(normalized.left_stick.x)); report.push_back(normalize_u8_axis(-normalized.left_stick.y)); report.push_back(normalize_trigger(normalized.left_trigger)); From 41afe8c62bcd9dc0228f68f2c08ac0fbea97fddf Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:00:47 -0400 Subject: [PATCH 25/27] Refactor button maps into constexpr helpers Convert the static gamepad button map arrays in `report.cpp` into `constexpr` helper functions and update all bitmask builders to call them. This keeps the mappings unchanged while localizing `using enum GamepadButton` usage and avoiding a file-scope enum import. --- src/core/report.cpp | 114 ++++++++++++++++++++++++++------------------ 1 file changed, 68 insertions(+), 46 deletions(-) diff --git a/src/core/report.cpp b/src/core/report.cpp index b98ad24..b4d546e 100644 --- a/src/core/report.cpp +++ b/src/core/report.cpp @@ -181,48 +181,70 @@ namespace lvh::reports { GamepadButton button; }; - using enum GamepadButton; - - constexpr std::array face_shoulder_button_map { - ButtonBit {0U, a}, - ButtonBit {1U, b}, - ButtonBit {2U, x}, - ButtonBit {3U, y}, - ButtonBit {4U, left_shoulder}, - ButtonBit {5U, right_shoulder}, - }; + constexpr auto face_shoulder_button_map() { + using enum GamepadButton; - constexpr std::array common_menu_button_map { - ButtonBit {6U, back}, - ButtonBit {7U, start}, - ButtonBit {8U, left_stick}, - ButtonBit {9U, right_stick}, - }; + return std::array { + ButtonBit {0U, a}, + ButtonBit {1U, b}, + ButtonBit {2U, x}, + ButtonBit {3U, y}, + ButtonBit {4U, left_shoulder}, + ButtonBit {5U, right_shoulder}, + }; + } - constexpr std::array common_extra_button_map { - ButtonBit {10U, guide}, - ButtonBit {11U, misc1}, - }; + constexpr auto common_menu_button_map() { + using enum GamepadButton; - constexpr std::array standard_dpad_button_map { - ButtonBit {12U, dpad_up}, - ButtonBit {13U, dpad_down}, - ButtonBit {14U, dpad_left}, - ButtonBit {15U, dpad_right}, - }; + return std::array { + ButtonBit {6U, back}, + ButtonBit {7U, start}, + ButtonBit {8U, left_stick}, + ButtonBit {9U, right_stick}, + }; + } - constexpr std::array xbox_extra_button_map { - ButtonBit {11U, misc1}, - }; + constexpr auto common_extra_button_map() { + using enum GamepadButton; - constexpr std::array switch_menu_button_map { - ButtonBit {8U, back}, - ButtonBit {9U, start}, - ButtonBit {10U, left_stick}, - ButtonBit {11U, right_stick}, - ButtonBit {12U, guide}, - ButtonBit {13U, misc1}, - }; + return std::array { + ButtonBit {10U, guide}, + ButtonBit {11U, misc1}, + }; + } + + constexpr auto standard_dpad_button_map() { + using enum GamepadButton; + + return std::array { + ButtonBit {12U, dpad_up}, + ButtonBit {13U, dpad_down}, + ButtonBit {14U, dpad_left}, + ButtonBit {15U, dpad_right}, + }; + } + + constexpr auto xbox_extra_button_map() { + using enum GamepadButton; + + return std::array { + ButtonBit {11U, misc1}, + }; + } + + constexpr auto switch_menu_button_map() { + using enum GamepadButton; + + return std::array { + ButtonBit {8U, back}, + ButtonBit {9U, start}, + ButtonBit {10U, left_stick}, + ButtonBit {11U, right_stick}, + ButtonBit {12U, guide}, + ButtonBit {13U, misc1}, + }; + } std::uint16_t button_bits(std::span button_map, const ButtonSet &buttons) { auto bits = std::uint16_t {}; @@ -236,31 +258,31 @@ namespace lvh::reports { std::uint16_t common_button_bits(const ButtonSet &buttons) { return static_cast( - button_bits(face_shoulder_button_map, buttons) | - button_bits(common_menu_button_map, buttons) | - button_bits(common_extra_button_map, buttons) + button_bits(face_shoulder_button_map(), buttons) | + button_bits(common_menu_button_map(), buttons) | + button_bits(common_extra_button_map(), buttons) ); } std::uint16_t standard_gamepad_button_bits(const ButtonSet &buttons) { return static_cast( common_button_bits(buttons) | - button_bits(standard_dpad_button_map, buttons) + button_bits(standard_dpad_button_map(), buttons) ); } std::uint16_t xbox_gip_button_bits(const ButtonSet &buttons) { return static_cast( - button_bits(face_shoulder_button_map, buttons) | - button_bits(common_menu_button_map, buttons) | - button_bits(xbox_extra_button_map, buttons) + button_bits(face_shoulder_button_map(), buttons) | + button_bits(common_menu_button_map(), buttons) | + button_bits(xbox_extra_button_map(), buttons) ); } std::uint16_t switch_pro_button_bits(const GamepadState &state) { auto bits = static_cast( - button_bits(face_shoulder_button_map, state.buttons) | - button_bits(switch_menu_button_map, state.buttons) + button_bits(face_shoulder_button_map(), state.buttons) | + button_bits(switch_menu_button_map(), state.buttons) ); if (state.left_trigger > 0.0F) { bits |= static_cast(1U << 6U); From ce8d83314c60dd92a660309b4a3d7628f3e63d95 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:25:27 -0400 Subject: [PATCH 26/27] Parameterize CI build configs Introduce shared `CMAKE_BUILD_CONFIG` and `DRIVER_BUILD_CONFIG` environment variables in CI, then use them consistently for build/install commands, artifact paths, test/example execution, driver packaging, and signing inputs. This removes hardcoded `Debug`/`Release` values and keeps MSVC and non-MSVC steps aligned. README Windows driver packaging docs were updated to include `cpack -C Release` so local instructions match CI behavior. --- .github/workflows/ci.yml | 32 ++++++++++++-------------------- README.md | 2 +- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb2f2ae..0e6e3e8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,6 +13,8 @@ concurrency: cancel-in-progress: true env: + CMAKE_BUILD_CONFIG: Debug + DRIVER_BUILD_CONFIG: Release OPENCPPCOVERAGE_VERSION: '0.9.9.0' PYTHON_VERSION: '3.14' @@ -208,7 +210,7 @@ jobs: -DBUILD_DOCS=OFF \ -DBUILD_EXAMPLES=ON \ -DBUILD_TESTS=ON \ - -DCMAKE_BUILD_TYPE:STRING=Debug \ + -DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_CONFIG} \ -B cmake-build-ci \ -G Ninja \ -S . @@ -230,12 +232,7 @@ jobs: -S . - name: Build - if: matrix.kind != 'msvc' - run: cmake --build cmake-build-ci -- -j2 - - - name: Build MSVC - if: matrix.kind == 'msvc' - run: cmake --build cmake-build-ci --config Debug --parallel 2 + run: cmake --build cmake-build-ci --config ${{ env.CMAKE_BUILD_CONFIG }} --parallel 2 - name: Download Windows driver installer artifact if: runner.os == 'Windows' @@ -272,7 +269,7 @@ jobs: $env:PATH = "C:\msys64\${{ matrix.msystem }}\bin;C:\msys64\usr\bin;$env:PATH" $gamepadAdapterPath = "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\gamepad_adapter.exe" } else { - $gamepadAdapterPath = "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\Debug\gamepad_adapter.exe" + $gamepadAdapterPath = "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\$env:CMAKE_BUILD_CONFIG\gamepad_adapter.exe" } $profiles = @("generic", "xone", "xseries", "ds4", "ds5", "switch") foreach ($profile in $profiles) { @@ -312,7 +309,7 @@ jobs: "--export_type=cobertura:$env:GITHUB_WORKSPACE\cmake-build-ci\reports\coverage.xml" ` --working_dir "$env:GITHUB_WORKSPACE\cmake-build-ci\tests" ` -- ` - "$env:GITHUB_WORKSPACE\cmake-build-ci\tests\Debug\test_libvirtualhid.exe" ` + "$env:GITHUB_WORKSPACE\cmake-build-ci\tests\$env:CMAKE_BUILD_CONFIG\test_libvirtualhid.exe" ` --gtest_color=yes ` "--gtest_output=xml:$env:GITHUB_WORKSPACE\cmake-build-ci\reports\junit.xml" @@ -377,7 +374,7 @@ jobs: $env:PATH = "C:\msys64\${{ matrix.msystem }}\bin;C:\msys64\usr\bin;$env:PATH" & .\cmake-build-ci\examples\gamepad_adapter.exe } else { - & .\cmake-build-ci\examples\Debug\gamepad_adapter.exe + & ".\cmake-build-ci\examples\$env:CMAKE_BUILD_CONFIG\gamepad_adapter.exe" } - name: Uninstall Windows driver installer @@ -405,12 +402,7 @@ jobs: } - name: Install - if: matrix.kind != 'msvc' - run: cmake --install cmake-build-ci --prefix cmake-build-ci/install - - - name: Install MSVC - if: matrix.kind == 'msvc' - run: cmake --install cmake-build-ci --config Debug --prefix cmake-build-ci/install + run: cmake --install cmake-build-ci --config ${{ env.CMAKE_BUILD_CONFIG }} --prefix cmake-build-ci/install - name: Upload install artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -472,7 +464,7 @@ jobs: - name: Build Windows driver package shell: pwsh - run: cmake --build cmake-build-driver --config Release --target libvirtualhid_windows_catalog --parallel 2 + run: cmake --build cmake-build-driver --config ${{ env.DRIVER_BUILD_CONFIG }} --target libvirtualhid_windows_catalog --parallel 2 - name: Validate Azure signing configuration if: >- @@ -487,7 +479,7 @@ jobs: run: | $packagePath = Join-Path ` $env:GITHUB_WORKSPACE ` - "cmake-build-driver\src\platform\windows\driver\package\Release" + "cmake-build-driver\src\platform\windows\driver\package\$env:DRIVER_BUILD_CONFIG" $certificatePath = Join-Path ` $env:GITHUB_WORKSPACE ` "cmake-build-driver\certificates\libvirtualhid-ci-test.cer" @@ -507,14 +499,14 @@ jobs: certificate-profile-name: ${{ vars.AZURE_SIGNING_CERT_PROFILE }} endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }} files: | - ${{ github.workspace }}/cmake-build-driver/src/platform/windows/driver/package/Release/libvirtualhid.cat + ${{ github.workspace }}/cmake-build-driver/src/platform/windows/driver/package/${{ env.DRIVER_BUILD_CONFIG }}/libvirtualhid.cat signing-account-name: ${{ vars.AZURE_SIGNING_ACCOUNT }} - name: Package Windows driver installer shell: pwsh run: | Push-Location .\cmake-build-driver - cpack -G WIX + cpack -G WIX -C $env:DRIVER_BUILD_CONFIG $packageExitCode = $LASTEXITCODE Pop-Location if ($packageExitCode -ne 0) { diff --git a/README.md b/README.md index 4d48140..ec98ee1 100644 --- a/README.md +++ b/README.md @@ -179,7 +179,7 @@ cmake -S . -B cmake-build-windows-driver -G "Visual Studio 17 2022" -A x64 ` -DBUILD_TESTS=OFF -DBUILD_EXAMPLES=OFF cmake --build cmake-build-windows-driver --config Release --target libvirtualhid_umdf cmake --build cmake-build-windows-driver --config Release --target libvirtualhid_windows_catalog -cpack -G WIX --config .\cmake-build-windows-driver\CPackConfig.cmake +cpack -G WIX -C Release --config .\cmake-build-windows-driver\CPackConfig.cmake ``` Developer install/uninstall helpers live under `scripts/windows`: From ea6d29c6ab83af668da05aa569e0635191e584fb Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Thu, 2 Jul 2026 23:38:53 -0400 Subject: [PATCH 27/27] Fix yamllint errors --- .github/workflows/ci.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e6e3e8..4a7185c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -269,7 +269,9 @@ jobs: $env:PATH = "C:\msys64\${{ matrix.msystem }}\bin;C:\msys64\usr\bin;$env:PATH" $gamepadAdapterPath = "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\gamepad_adapter.exe" } else { - $gamepadAdapterPath = "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\$env:CMAKE_BUILD_CONFIG\gamepad_adapter.exe" + $gamepadAdapterPath = Join-Path ` + "$env:GITHUB_WORKSPACE\cmake-build-ci\examples\$env:CMAKE_BUILD_CONFIG" ` + "gamepad_adapter.exe" } $profiles = @("generic", "xone", "xseries", "ds4", "ds5", "switch") foreach ($profile in $profiles) { @@ -464,7 +466,11 @@ jobs: - name: Build Windows driver package shell: pwsh - run: cmake --build cmake-build-driver --config ${{ env.DRIVER_BUILD_CONFIG }} --target libvirtualhid_windows_catalog --parallel 2 + run: >- + cmake --build cmake-build-driver + --config ${{ env.DRIVER_BUILD_CONFIG }} + --target libvirtualhid_windows_catalog + --parallel 2 - name: Validate Azure signing configuration if: >- @@ -487,6 +493,18 @@ jobs: -PackagePath $packagePath ` -CertificatePath $certificatePath + - name: Locate Windows driver catalog + id: driver_catalog + if: >- + github.event_name == 'push' && + vars.AZURE_SIGNING_ACCOUNT != '' + shell: pwsh + run: | + $catalogPath = Join-Path ` + $env:GITHUB_WORKSPACE ` + "cmake-build-driver\src\platform\windows\driver\package\$env:DRIVER_BUILD_CONFIG\libvirtualhid.cat" + "path=$catalogPath" >> $env:GITHUB_OUTPUT + - name: Sign Windows driver package with Azure Trusted Signing if: >- github.event_name == 'push' && @@ -499,7 +517,7 @@ jobs: certificate-profile-name: ${{ vars.AZURE_SIGNING_CERT_PROFILE }} endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }} files: | - ${{ github.workspace }}/cmake-build-driver/src/platform/windows/driver/package/${{ env.DRIVER_BUILD_CONFIG }}/libvirtualhid.cat + ${{ steps.driver_catalog.outputs.path }} signing-account-name: ${{ vars.AZURE_SIGNING_ACCOUNT }} - name: Package Windows driver installer