From b0e3b3494a55b0e585994badef2e02ea260ff4a8 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 20 Jun 2026 19:06:04 -0400 Subject: [PATCH 1/5] feat: windows implementation --- .github/workflows/ci.yml | 128 +++- CMakeLists.txt | 13 + README.md | 52 +- cmake/build_version.cmake | 39 ++ cmake/packaging/common.cmake | 20 + cmake/packaging/windows.cmake | 30 + cmake/packaging/windows_wix.cmake | 64 ++ .../libvirtualhid-driver-installer.wxs | 37 + cmake/packaging/wix_resources/patch.xml | 5 + scripts/windows/install-driver.ps1 | 92 +++ scripts/windows/sign-driver-package.ps1 | 101 +++ scripts/windows/uninstall-driver.ps1 | 129 ++++ src/CMakeLists.txt | 18 + src/platform/windows/control_protocol.hpp | 158 +++++ src/platform/windows/driver/CMakeLists.txt | 168 +++++ .../windows/driver/libvirtualhid.inf.in | 50 ++ .../windows/driver/libvirtualhid_umdf.cpp | 231 +++++++ .../windows/shared/lvh_windows_protocol.h | 154 +++++ src/platform/windows/windows_backend.cpp | 636 ++++++++++++++++++ tests/CMakeLists.txt | 4 +- tests/unit/test_runtime.cpp | 14 + tests/unit/test_windows_protocol.cpp | 61 ++ 22 files changed, 2196 insertions(+), 8 deletions(-) create mode 100644 cmake/build_version.cmake create mode 100644 cmake/packaging/common.cmake create mode 100644 cmake/packaging/windows.cmake create mode 100644 cmake/packaging/windows_wix.cmake create mode 100644 cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs create mode 100644 cmake/packaging/wix_resources/patch.xml create mode 100644 scripts/windows/install-driver.ps1 create mode 100644 scripts/windows/sign-driver-package.ps1 create mode 100644 scripts/windows/uninstall-driver.ps1 create mode 100644 src/platform/windows/control_protocol.hpp create mode 100644 src/platform/windows/driver/CMakeLists.txt create mode 100644 src/platform/windows/driver/libvirtualhid.inf.in create mode 100644 src/platform/windows/driver/libvirtualhid_umdf.cpp create mode 100644 src/platform/windows/shared/lvh_windows_protocol.h create mode 100644 src/platform/windows/windows_backend.cpp create mode 100644 tests/unit/test_windows_protocol.cpp diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 74a6c27..110e352 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -196,7 +196,7 @@ jobs: if: matrix.kind != 'msvc' env: BRANCH: ${{ github.head_ref || github.ref_name }} - BUILD_VERSION: ${{ needs.setup_release.outputs.release_tag }} + BUILD_VERSION: ${{ needs.setup_release.outputs.release_version }} CC: ${{ matrix.cc }} COMMIT: ${{ needs.setup_release.outputs.release_commit }} CXX: ${{ matrix.cxx }} @@ -214,7 +214,7 @@ jobs: if: matrix.kind == 'msvc' env: BRANCH: ${{ github.head_ref || github.ref_name }} - BUILD_VERSION: ${{ needs.setup_release.outputs.release_tag }} + BUILD_VERSION: ${{ needs.setup_release.outputs.release_version }} COMMIT: ${{ needs.setup_release.outputs.release_commit }} run: | cmake ` @@ -358,6 +358,121 @@ jobs: path: cmake-build-ci/reports if-no-files-found: error + windows_driver: + name: Windows Driver Installer + needs: setup_release + permissions: + contents: read + runs-on: windows-2022 + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + submodules: recursive + + - name: Setup dotnet + uses: actions/setup-dotnet@9a946fdbd5fb07b82b2f5a4466058b876ab72bb2 # v5.3.0 + with: + dotnet-version: '10.x' + + - name: Configure Windows driver package + shell: pwsh + 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\certificates\libvirtualhid-ci-test.cer" + cmake ` + -DBUILD_DOCS=OFF ` + -DBUILD_EXAMPLES=OFF ` + -DBUILD_TESTS=OFF ` + -DLIBVIRTUALHID_BUILD_WINDOWS_DRIVER=ON ` + -DLIBVIRTUALHID_ENABLE_PACKAGING=ON ` + "-DLIBVIRTUALHID_DRIVER_TEST_CERTIFICATE=$certificatePath" ` + -A x64 ` + -B cmake-build-driver ` + -G "Visual Studio 17 2022" ` + -S . + + - name: Build Windows driver package + shell: pwsh + run: cmake --build cmake-build-driver --config Release --target libvirtualhid_windows_catalog --parallel 2 + + - name: Validate Azure signing configuration + if: >- + github.event_name == 'push' && + vars.AZURE_SIGNING_ACCOUNT == '' + shell: pwsh + run: throw "Push builds must use Azure Trusted Signing for the Windows driver package." + + - name: Sign Windows driver package with local test certificate + if: github.event_name == 'pull_request' + shell: pwsh + run: | + $packagePath = Join-Path ` + $env:GITHUB_WORKSPACE ` + "cmake-build-driver\src\platform\windows\driver\package\Release" + $certificatePath = Join-Path ` + $env:GITHUB_WORKSPACE ` + "cmake-build-driver\certificates\libvirtualhid-ci-test.cer" + .\scripts\windows\sign-driver-package.ps1 ` + -PackagePath $packagePath ` + -CertificatePath $certificatePath + + - name: Sign Windows driver package with Azure Trusted Signing + if: >- + github.event_name == 'push' && + vars.AZURE_SIGNING_ACCOUNT != '' + uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0 + with: + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + certificate-profile-name: ${{ vars.AZURE_SIGNING_CERT_PROFILE }} + endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }} + files: cmake-build-driver/src/platform/windows/driver/package/Release/libvirtualhid.cat + signing-account-name: ${{ vars.AZURE_SIGNING_ACCOUNT }} + + - name: Package Windows driver installer + shell: pwsh + run: | + cpack -G WIX --config .\cmake-build-driver\CPackConfig.cmake + New-Item -ItemType Directory -Force -Path artifacts | Out-Null + Copy-Item ` + -LiteralPath .\cmake-build-driver\cpack_artifacts\libvirtualhid.msi ` + -Destination .\artifacts\libvirtualhid-Windows-Driver-installer.msi + + - name: Sign Windows driver installer with Azure Trusted Signing + if: >- + github.event_name == 'push' && + vars.AZURE_SIGNING_ACCOUNT != '' + uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0 + with: + azure-client-id: ${{ secrets.AZURE_CLIENT_ID }} + azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }} + azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }} + certificate-profile-name: ${{ vars.AZURE_SIGNING_CERT_PROFILE }} + endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }} + files-folder: artifacts + files-folder-filter: msi + files-folder-recurse: false + signing-account-name: ${{ vars.AZURE_SIGNING_ACCOUNT }} + + - name: Debug wix + if: always() + shell: pwsh + run: | + Get-Content .\cmake-build-driver\cpack_artifacts\_CPack_Packages\win64\WIX\wix.log ` + -ErrorAction SilentlyContinue + + - name: Upload Windows driver installer artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: windows-driver-installer + path: artifacts + if-no-files-found: error + codecov: name: Codecov-${{ matrix.flag }} if: >- @@ -430,9 +545,11 @@ jobs: always() && needs.setup_release.outputs.publish_release == 'true' && needs.build.result == 'success' && + needs.windows_driver.result == 'success' && startsWith(github.repository, 'LizardByte/') needs: - build + - windows_driver - setup_release permissions: contents: read @@ -468,6 +585,12 @@ jobs: name: install-Windows-MSVC path: install-Windows-MSVC + - name: Download Windows driver installer artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: windows-driver-installer + path: windows-driver-installer + - name: Package install artifacts run: | mkdir -p artifacts @@ -476,6 +599,7 @@ jobs: "artifacts/libvirtualhid-${{ needs.setup_release.outputs.release_tag }}-${name}.zip" \ "install-${name}" done + cp windows-driver-installer/* artifacts/ - name: Create/Update GitHub Release if: needs.setup_release.outputs.publish_release == 'true' diff --git a/CMakeLists.txt b/CMakeLists.txt index 0e0fc9b..6115e65 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,6 +25,8 @@ endif() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/build_version.cmake") + # # Project optional configuration # @@ -32,6 +34,9 @@ option(BUILD_DOCS "Build documentation" ${LIBVIRTUALHID_IS_TOP_LEVEL}) option(BUILD_TESTS "Build tests" ${LIBVIRTUALHID_IS_TOP_LEVEL}) option(BUILD_EXAMPLES "Build examples" ${LIBVIRTUALHID_IS_TOP_LEVEL}) option(LIBVIRTUALHID_ENABLE_XTEST "Enable X11/XTest keyboard and mouse fallback on Linux" ON) +option(LIBVIRTUALHID_BUILD_WINDOWS_DRIVER "Build the Windows UMDF2 driver package with the WDK/MSVC toolchain" OFF) +option(LIBVIRTUALHID_ENABLE_PACKAGING "Enable CPack package metadata" ${LIBVIRTUALHID_IS_TOP_LEVEL}) +option(LIBVIRTUALHID_WARNINGS_AS_ERRORS "Treat libvirtualhid warnings as errors" ${LIBVIRTUALHID_IS_TOP_LEVEL}) set(CMAKE_COLOR_MAKEFILE ON) set(CMAKE_EXPORT_COMPILE_COMMANDS ON) @@ -85,6 +90,10 @@ add_subdirectory(src) # Examples, tests, and docs are top-level only # if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + if(WIN32 AND LIBVIRTUALHID_BUILD_WINDOWS_DRIVER) + add_subdirectory(src/platform/windows/driver) + endif() + if(BUILD_DOCS) add_subdirectory(third-party/doxyconfig docs) endif() @@ -118,3 +127,7 @@ install(FILES "${CMAKE_CURRENT_BINARY_DIR}/libvirtualhid-config.cmake" "${CMAKE_CURRENT_BINARY_DIR}/libvirtualhid-config-version.cmake" DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/libvirtualhid") + +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME AND LIBVIRTUALHID_ENABLE_PACKAGING) + include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/packaging/common.cmake") +endif() diff --git a/README.md b/README.md index fefdedd..186f66a 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,47 @@ MSBuild, or EWDK. The boundary between the library and driver should therefore be compiler-neutral: prefer a stable C ABI, named pipe, device interface IOCTL, 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 probes `\\.\LibVirtualHid`, 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. The client library stays buildable with MSVC and MinGW/UCRT64 because +the backend 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`. + +Build the UMDF package separately with the Microsoft driver toolchain: + +```powershell +cmake -S . -B cmake-build-windows-driver -G "Visual Studio 17 2022" -A x64 ` + -DLIBVIRTUALHID_BUILD_WINDOWS_DRIVER=ON -DLIBVIRTUALHID_ENABLE_PACKAGING=ON ` + -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 +``` + +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 +powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1 ` + -Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" +``` + +The helper stages the INF with `pnputil` and uses `devcon.exe` when available +to create the `ROOT\LIBVIRTUALHID` development device. + +Windows driver packages require a signed catalog for normal installation. Pull +request builds generate a short-lived self-signed test certificate, sign +`libvirtualhid.cat`, bundle the public `.cer` into the WiX installer, and import +that certificate into the local machine root and trusted-publisher stores during +install. The uninstall helper removes certificates matching +`CN=libvirtualhid CI Test Driver Signing`. Push/release builds must use Azure +Trusted Signing for the catalog and generated MSI, matching Sunshine's Windows +signing model, and must not ship the local PR test certificate. + ### Linux Linux should compile directly into the consuming project and use standard kernel @@ -309,9 +350,9 @@ The intended project layout is: src/include/libvirtualhid/ Public C++ headers src/core/ Shared profile, descriptor, and report logic src/platform/windows/ Windows client backend and UMDF control channel +src/platform/windows/driver/ Windows UMDF2 driver package sources src/platform/linux/ Linux uhid/uinput backend src/platform/macos/ Future macOS backend -drivers/windows/ UMDF2 driver package sources profiles/ Built-in gamepad profiles examples/ Minimal consumers and platform smoke tests tests/ Unit and integration tests @@ -389,13 +430,14 @@ third-party/googletest/ GoogleTest submodule ### Phase 3: Windows MVP -- [ ] Build a UMDF2 HID minidriver package with CMake/WDK integration. -- [ ] Implement the Windows backend and control channel between the C++ library and +- [x] Add CMake/WDK integration for the UMDF2 driver package. +- [x] Implement the Windows backend and control channel between the C++ library and the UMDF driver. - [x] Keep the client library buildable with MSVC and MinGW/UCRT64. Keep the driver package on the Microsoft WDK toolchain. -- [ ] Add install/uninstall tooling for developer workflows. -- [ ] Support hot-plug, multi-controller instances, and output report callbacks. +- [x] Add install/uninstall tooling for developer workflows. +- [x] Support backend hot-plug, multi-controller instances, and output report callbacks + through the Windows control protocol. - [ ] Validate visibility through DirectInput, XInput where applicable, SDL/HIDAPI, Windows.Gaming.Input/GameInput, and browser Gamepad API. diff --git a/cmake/build_version.cmake b/cmake/build_version.cmake new file mode 100644 index 0000000..d77f29c --- /dev/null +++ b/cmake/build_version.cmake @@ -0,0 +1,39 @@ +# Set build variables if env variables are defined. +if(DEFINED ENV{BRANCH}) + set(GITHUB_BRANCH "$ENV{BRANCH}") +endif() +if(DEFINED ENV{BUILD_VERSION}) # cmake-lint: disable=W0106 + set(BUILD_VERSION "$ENV{BUILD_VERSION}") +endif() +if(DEFINED ENV{CLONE_URL}) + set(GITHUB_CLONE_URL "$ENV{CLONE_URL}") +endif() +if(DEFINED ENV{COMMIT}) + set(GITHUB_COMMIT "$ENV{COMMIT}") +endif() +if(DEFINED ENV{TAG}) + set(GITHUB_TAG "$ENV{TAG}") +endif() + +if(DEFINED ENV{BUILD_VERSION} AND NOT "$ENV{BUILD_VERSION}" STREQUAL "") # cmake-lint: disable=W0106 + message(STATUS "Using CI build version '$ENV{BUILD_VERSION}'") + set(PROJECT_VERSION "$ENV{BUILD_VERSION}") + string(REGEX REPLACE "^v" "" PROJECT_VERSION "${PROJECT_VERSION}") + set(CMAKE_PROJECT_VERSION "${PROJECT_VERSION}") +endif() + +if(PROJECT_VERSION MATCHES "^([0-9]+)\\.([0-9]+)\\.([0-9]+)") + set(PROJECT_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(CMAKE_PROJECT_VERSION_MAJOR "${CMAKE_MATCH_1}") + set(PROJECT_VERSION_MINOR "${CMAKE_MATCH_2}") + set(CMAKE_PROJECT_VERSION_MINOR "${CMAKE_MATCH_2}") + set(PROJECT_VERSION_PATCH "${CMAKE_MATCH_3}") + set(CMAKE_PROJECT_VERSION_PATCH "${CMAKE_MATCH_3}") +endif() + +message(STATUS "PROJECT_VERSION: ${PROJECT_VERSION}") +message(STATUS "PROJECT_VERSION_MAJOR: ${PROJECT_VERSION_MAJOR}") +message(STATUS "PROJECT_VERSION_MINOR: ${PROJECT_VERSION_MINOR}") +message(STATUS "PROJECT_VERSION_PATCH: ${PROJECT_VERSION_PATCH}") +message(STATUS "GITHUB_BRANCH: ${GITHUB_BRANCH}") +message(STATUS "GITHUB_COMMIT: ${GITHUB_COMMIT}") diff --git a/cmake/packaging/common.cmake b/cmake/packaging/common.cmake new file mode 100644 index 0000000..8b31e36 --- /dev/null +++ b/cmake/packaging/common.cmake @@ -0,0 +1,20 @@ +# common cpack options +set(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME}) +set(CPACK_PACKAGE_VENDOR "LizardByte") +set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) +set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) +set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) +set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) +set(CPACK_PACKAGE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/cpack_artifacts) +set(CPACK_PACKAGE_CONTACT "https://app.lizardbyte.dev") +set(CPACK_PACKAGE_DESCRIPTION ${CMAKE_PROJECT_DESCRIPTION}) +set(CPACK_PACKAGE_HOMEPAGE_URL ${CMAKE_PROJECT_HOMEPAGE_URL}) +set(CPACK_RESOURCE_FILE_LICENSE ${PROJECT_SOURCE_DIR}/LICENSE) +set(CPACK_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}") +set(CPACK_STRIP_FILES YES) + +if(WIN32) + include("${CMAKE_CURRENT_LIST_DIR}/windows.cmake") +endif() + +include(CPack) diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake new file mode 100644 index 0000000..60a6e7d --- /dev/null +++ b/cmake/packaging/windows.cmake @@ -0,0 +1,30 @@ +# windows specific packaging +set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}") +set(CPACK_MONOLITHIC_INSTALL ON) + +if(NOT LIBVIRTUALHID_BUILD_WINDOWS_DRIVER) + return() +endif() + +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/install-driver.ps1" + "${PROJECT_SOURCE_DIR}/scripts/windows/uninstall-driver.ps1" + DESTINATION "scripts/windows" + COMPONENT driver) + +if(LIBVIRTUALHID_DRIVER_TEST_CERTIFICATE) + install(FILES "${LIBVIRTUALHID_DRIVER_TEST_CERTIFICATE}" + DESTINATION "certificates" + RENAME "libvirtualhid-ci-test.cer" + COMPONENT driver + OPTIONAL) +endif() + +set(CPACK_COMPONENT_DRIVER_DISPLAY_NAME "Windows UMDF Driver") +set(CPACK_COMPONENT_DRIVER_DESCRIPTION "libvirtualhid Windows UMDF virtual HID driver package.") +set(CPACK_COMPONENT_DRIVER_REQUIRED true) + +include("${CMAKE_CURRENT_LIST_DIR}/windows_wix.cmake") diff --git a/cmake/packaging/windows_wix.cmake b/cmake/packaging/windows_wix.cmake new file mode 100644 index 0000000..67667ce --- /dev/null +++ b/cmake/packaging/windows_wix.cmake @@ -0,0 +1,64 @@ +# WIX Packaging +# see options at: https://cmake.org/cmake/help/latest/cpack_gen/wix.html + +find_program(DOTNET_EXECUTABLE dotnet HINTS "C:/Program Files/dotnet") + +if(NOT DOTNET_EXECUTABLE) + message(WARNING "Dotnet executable not found, skipping WiX packaging.") + return() +endif() + +set(CPACK_WIX_VERSION 4) +set(WIX_VERSION 4.0.4) +set(WIX_UI_VERSION 4.0.4) +set(WIX_BUILD_PARENT_DIRECTORY "${CMAKE_BINARY_DIR}/wix_packaging") + +set(WIX_TOOL_PATH "${CMAKE_BINARY_DIR}/.wix") +file(MAKE_DIRECTORY ${WIX_TOOL_PATH}) + +if(NOT EXISTS "${WIX_TOOL_PATH}/wix.exe") + execute_process( + COMMAND ${DOTNET_EXECUTABLE} tool install --tool-path ${WIX_TOOL_PATH} wix --version ${WIX_VERSION} + ERROR_VARIABLE WIX_INSTALL_OUTPUT + RESULT_VARIABLE WIX_INSTALL_RESULT) + + if(NOT WIX_INSTALL_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to install WiX tools locally: ${WIX_INSTALL_OUTPUT}") + endif() +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() + +set(CPACK_WIX_ROOT "${WIX_TOOL_PATH}") +set(CPACK_WIX_UPGRADE_GUID "71D7B738-9D83-4E57-82E3-C3106D9F8053") +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") + +file(COPY "${CMAKE_CURRENT_LIST_DIR}/wix_resources/" + DESTINATION "${WIX_BUILD_PARENT_DIRECTORY}/") + +set(CPACK_WIX_EXTRA_SOURCES + "${WIX_BUILD_PARENT_DIRECTORY}/libvirtualhid-driver-installer.wxs") +set(CPACK_WIX_PATCH_FILE + "${WIX_BUILD_PARENT_DIRECTORY}/patch.xml") + +file(COPY "${CMAKE_SOURCE_DIR}/LICENSE" + DESTINATION "${CMAKE_BINARY_DIR}") +file(RENAME "${CMAKE_BINARY_DIR}/LICENSE" "${CMAKE_BINARY_DIR}/LICENSE.txt") +set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_BINARY_DIR}/LICENSE.txt") + +if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64") + set(CPACK_WIX_ARCHITECTURE "arm64") +else() + set(CPACK_WIX_ARCHITECTURE "x64") +endif() diff --git a/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs b/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs new file mode 100644 index 0000000..c9f95b4 --- /dev/null +++ b/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + diff --git a/cmake/packaging/wix_resources/patch.xml b/cmake/packaging/wix_resources/patch.xml new file mode 100644 index 0000000..65280ed --- /dev/null +++ b/cmake/packaging/wix_resources/patch.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/scripts/windows/install-driver.ps1 b/scripts/windows/install-driver.ps1 new file mode 100644 index 0000000..5b4d429 --- /dev/null +++ b/scripts/windows/install-driver.ps1 @@ -0,0 +1,92 @@ +<# +.SYNOPSIS +Stages and installs the libvirtualhid Windows UMDF development driver. +#> +[CmdletBinding(SupportsShouldProcess)] +param( + [Parameter(Mandatory = $true)] + [string] $InfPath, + + [string] $CertificatePath, + + [string] $HardwareId = "ROOT\LIBVIRTUALHID", + + [switch] $StageOnly +) + +$ErrorActionPreference = "Stop" + +function Invoke-CheckedCommand { + param( + [Parameter(Mandatory = $true)] + [string] $FilePath, + + [Parameter(Mandatory = $true)] + [string[]] $Arguments + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + 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) + + if (-not $Path -or -not (Test-Path -LiteralPath $Path)) { + return + } + + $resolvedCertificate = (Resolve-Path -LiteralPath $Path).Path + foreach ($store in @("Cert:\LocalMachine\Root", "Cert:\LocalMachine\TrustedPublisher")) { + if ($PSCmdlet.ShouldProcess($store, "Trust libvirtualhid driver certificate $resolvedCertificate")) { + Import-Certificate -FilePath $resolvedCertificate -CertStoreLocation $store | Out-Null + } + } +} + +$resolvedInf = (Resolve-Path -LiteralPath $InfPath).Path +Import-DriverCertificate -Path $CertificatePath + +if ($PSCmdlet.ShouldProcess($resolvedInf, "Stage libvirtualhid driver package")) { + Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments @("/add-driver", $resolvedInf, "/install") +} + +if ($StageOnly) { + return +} + +$devcon = Find-Devcon +if (-not $devcon) { + Write-Warning "devcon.exe was not found. The package was staged, but the ROOT\LIBVIRTUALHID development device was not created." + return +} + +if ($PSCmdlet.ShouldProcess($HardwareId, "Create libvirtualhid development device")) { + Invoke-CheckedCommand -FilePath $devcon -Arguments @("install", $resolvedInf, $HardwareId) +} diff --git a/scripts/windows/sign-driver-package.ps1 b/scripts/windows/sign-driver-package.ps1 new file mode 100644 index 0000000..570f45c --- /dev/null +++ b/scripts/windows/sign-driver-package.ps1 @@ -0,0 +1,101 @@ +<# +.SYNOPSIS +Creates a local test certificate and signs the libvirtualhid driver catalog. +#> +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string] $PackagePath, + + [string] $CatalogName = "libvirtualhid.cat", + + [string] $CertificatePath, + + [string] $CertificateSubject = "CN=libvirtualhid CI Test Driver Signing", + + [int] $ValidDays = 7, + + [switch] $KeepPrivateCertificate +) + +$ErrorActionPreference = "Stop" + +function Find-SignTool { + $candidate = Get-Command signtool.exe -ErrorAction SilentlyContinue + if ($candidate) { + return $candidate.Source + } + + $roots = @( + $env:WindowsSdkDir, + $env:WDKContentRoot, + "${env:ProgramFiles(x86)}\Windows Kits\10" + ) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } + + foreach ($root in $roots) { + $candidate = Get-ChildItem -LiteralPath $root -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -match "\\x64\\signtool\.exe$" } | + Sort-Object FullName -Descending | + Select-Object -First 1 + if ($candidate) { + return $candidate.FullName + } + } + + throw "signtool.exe was not found. Install the Windows SDK or WDK." +} + +function Invoke-CheckedCommand { + param( + [Parameter(Mandatory = $true)] + [string] $FilePath, + + [Parameter(Mandatory = $true)] + [string[]] $Arguments + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) { + throw "$FilePath exited with code $LASTEXITCODE" + } +} + +$resolvedPackagePath = (Resolve-Path -LiteralPath $PackagePath).Path +$catalogPath = Join-Path $resolvedPackagePath $CatalogName +if (-not (Test-Path -LiteralPath $catalogPath)) { + throw "Driver catalog was not found: $catalogPath" +} + +if (-not $CertificatePath) { + $CertificatePath = Join-Path $resolvedPackagePath "libvirtualhid-ci-test.cer" +} +$certificateDirectory = Split-Path -Path $CertificatePath -Parent +New-Item -ItemType Directory -Force -Path $certificateDirectory | Out-Null + +$certificate = New-SelfSignedCertificate ` + -Subject $CertificateSubject ` + -Type CodeSigningCert ` + -CertStoreLocation "Cert:\CurrentUser\My" ` + -KeyAlgorithm RSA ` + -KeyLength 3072 ` + -KeyUsage DigitalSignature ` + -HashAlgorithm SHA256 ` + -NotAfter (Get-Date).AddDays($ValidDays) + +try { + Export-Certificate -Cert $certificate -FilePath $CertificatePath -Force | Out-Null + + $signTool = Find-SignTool + Invoke-CheckedCommand -FilePath $signTool -Arguments @( + "sign", + "/fd", "SHA256", + "/sha1", $certificate.Thumbprint, + "/s", "My", + $catalogPath + ) +} +finally { + if (-not $KeepPrivateCertificate) { + Remove-Item -LiteralPath "Cert:\CurrentUser\My\$($certificate.Thumbprint)" -Force -ErrorAction SilentlyContinue + } +} diff --git a/scripts/windows/uninstall-driver.ps1 b/scripts/windows/uninstall-driver.ps1 new file mode 100644 index 0000000..0726fd6 --- /dev/null +++ b/scripts/windows/uninstall-driver.ps1 @@ -0,0 +1,129 @@ +<# +.SYNOPSIS +Removes the libvirtualhid Windows UMDF development driver. +#> +[CmdletBinding(SupportsShouldProcess)] +param( + [string] $PublishedName, + + [string] $OriginalName = "libvirtualhid.inf", + + [string] $HardwareId = "ROOT\LIBVIRTUALHID", + + [string] $RemoveCertificateSubject, + + [switch] $Force +) + +$ErrorActionPreference = "Stop" + +function Invoke-CheckedCommand { + param( + [Parameter(Mandatory = $true)] + [string] $FilePath, + + [Parameter(Mandatory = $true)] + [string[]] $Arguments, + + [switch] $IgnoreFailure + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0 -and -not $IgnoreFailure) { + 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 Find-PublishedName { + param([string] $TargetOriginalName) + + $drivers = & pnputil.exe /enum-drivers + $currentPublished = $null + $currentOriginal = $null + + foreach ($line in $drivers) { + if ($line -match "^\s*Published Name\s*:\s*(.+)$") { + $currentPublished = $Matches[1].Trim() + $currentOriginal = $null + continue + } + + if ($line -match "^\s*Original Name\s*:\s*(.+)$") { + $currentOriginal = $Matches[1].Trim() + if ($currentPublished -and $currentOriginal -ieq $TargetOriginalName) { + return $currentPublished + } + } + } + + return $null +} + +function Remove-DriverCertificate { + [CmdletBinding(SupportsShouldProcess)] + param([string] $Subject) + + if (-not $Subject) { + return + } + + foreach ($store in @("Cert:\LocalMachine\TrustedPublisher", "Cert:\LocalMachine\Root")) { + $certificates = Get-ChildItem -LiteralPath $store -ErrorAction SilentlyContinue | + Where-Object { $_.Subject -eq $Subject -and $_.Issuer -eq $Subject } + + foreach ($certificate in $certificates) { + if ($PSCmdlet.ShouldProcess("$store\$($certificate.Thumbprint)", "Remove libvirtualhid test driver certificate")) { + Remove-Item -LiteralPath "$store\$($certificate.Thumbprint)" -Force + } + } + } +} + +$devcon = Find-Devcon +if ($devcon -and $PSCmdlet.ShouldProcess($HardwareId, "Remove libvirtualhid development device")) { + Invoke-CheckedCommand -FilePath $devcon -Arguments @("remove", $HardwareId) -IgnoreFailure +} + +if (-not $PublishedName) { + $PublishedName = Find-PublishedName -TargetOriginalName $OriginalName +} + +if (-not $PublishedName) { + 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" +} + +if ($PSCmdlet.ShouldProcess($PublishedName, "Delete libvirtualhid driver package")) { + Invoke-CheckedCommand -FilePath "pnputil.exe" -Arguments $deleteArgs +} + +Remove-DriverCertificate -Subject $RemoveCertificateSubject diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 53d9c0a..0d6fc2b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -38,6 +38,18 @@ if(LIBVIRTUALHID_USES_THREADS) ${X11_LIBRARIES} ${X11_XTest_LIB}) endif() +elseif(WIN32) + target_sources(${PROJECT_NAME} + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/platform/windows/windows_backend.cpp") + target_include_directories(${PROJECT_NAME} + PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/platform/windows/shared") + target_compile_definitions(${PROJECT_NAME} + PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + _WIN32_WINNT=0x0600) else() target_sources(${PROJECT_NAME} PRIVATE @@ -58,8 +70,14 @@ set_target_properties(${PROJECT_NAME} PROPERTIES if(MSVC) target_compile_options(${PROJECT_NAME} PRIVATE /W4) + if(LIBVIRTUALHID_WARNINGS_AS_ERRORS) + target_compile_options(${PROJECT_NAME} PRIVATE /WX) + endif() else() target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic) + if(LIBVIRTUALHID_WARNINGS_AS_ERRORS) + target_compile_options(${PROJECT_NAME} PRIVATE -Werror) + endif() endif() install(TARGETS ${PROJECT_NAME} diff --git a/src/platform/windows/control_protocol.hpp b/src/platform/windows/control_protocol.hpp new file mode 100644 index 0000000..1c613d2 --- /dev/null +++ b/src/platform/windows/control_protocol.hpp @@ -0,0 +1,158 @@ +/** + * @file src/platform/windows/control_protocol.hpp + * @brief C++ helpers for the Windows UMDF control protocol. + */ +#pragma once + +// standard includes +#include +#include +#include +#include +#include +#include +#include +#include + +// driver includes +#include "lvh_windows_protocol.h" + +// local includes +#include + +namespace lvh::detail::windows { + + inline constexpr std::string_view default_control_device_path {LVH_WINDOWS_CONTROL_DEVICE_PATH}; + + inline std::uint32_t gamepad_flags(const GamepadProfileCapabilities &capabilities) { + std::uint32_t flags = 0; + if (capabilities.supports_rumble) { + flags |= LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE; + } + if (capabilities.supports_motion) { + flags |= LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION; + } + if (capabilities.supports_touchpad) { + flags |= LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD; + } + if (capabilities.supports_rgb_led) { + flags |= LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RGB_LED; + } + if (capabilities.supports_battery) { + flags |= LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY; + } + if (capabilities.supports_adaptive_triggers) { + flags |= LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_ADAPTIVE_TRIGGERS; + } + + return flags; + } + + inline std::uint32_t protocol_bus_type(BusType bus_type) { + switch (bus_type) { + case BusType::usb: + return LVH_WINDOWS_BUS_USB; + case BusType::bluetooth: + return LVH_WINDOWS_BUS_BLUETOOTH; + case BusType::unknown: + return LVH_WINDOWS_BUS_UNKNOWN; + } + + return LVH_WINDOWS_BUS_UNKNOWN; + } + + inline std::uint32_t protocol_gamepad_kind(GamepadProfileKind kind) { + switch (kind) { + case GamepadProfileKind::generic: + return LVH_WINDOWS_GAMEPAD_GENERIC; + case GamepadProfileKind::xbox_360: + return LVH_WINDOWS_GAMEPAD_XBOX_360; + case GamepadProfileKind::xbox_one: + return LVH_WINDOWS_GAMEPAD_XBOX_ONE; + case GamepadProfileKind::xbox_series: + return LVH_WINDOWS_GAMEPAD_XBOX_SERIES; + case GamepadProfileKind::dualsense: + return LVH_WINDOWS_GAMEPAD_DUALSENSE; + case GamepadProfileKind::switch_pro: + return LVH_WINDOWS_GAMEPAD_SWITCH_PRO; + } + + return LVH_WINDOWS_GAMEPAD_GENERIC; + } + + template + std::uint32_t copy_string(char (&target)[Size], std::string_view source) { + std::fill(std::begin(target), std::end(target), '\0'); + + const auto copied = std::min(source.size(), Size - 1U); + if (copied > 0U) { + std::memcpy(target, source.data(), copied); + } + + return static_cast(copied); + } + + template + std::uint32_t copy_bytes(std::uint8_t (&target)[Size], const std::vector &source) { + std::fill(std::begin(target), std::end(target), std::uint8_t {}); + + const auto copied = std::min(source.size(), Size); + if (copied > 0U) { + std::memcpy(target, source.data(), copied); + } + + return static_cast(copied); + } + + inline LvhWindowsCreateGamepadRequest make_create_gamepad_request( + DeviceId device_id, + const CreateGamepadOptions &options + ) { + LvhWindowsCreateGamepadRequest request {}; + request.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; + request.size = sizeof(request); + request.client_device_id = device_id; + request.bus_type = protocol_bus_type(options.profile.bus_type); + request.gamepad_kind = protocol_gamepad_kind(options.profile.gamepad_kind); + request.flags = gamepad_flags(options.profile.capabilities); + request.vendor_id = options.profile.vendor_id; + request.product_id = options.profile.product_id; + request.device_version = options.profile.version; + request.report_id = options.profile.report_id; + request.input_report_size = static_cast( + std::min(options.profile.input_report_size, static_cast(LVH_WINDOWS_MAX_INPUT_REPORT_SIZE)) + ); + request.output_report_size = static_cast( + std::min(options.profile.output_report_size, static_cast(LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE)) + ); + request.report_descriptor_size = copy_bytes(request.report_descriptor, options.profile.report_descriptor); + request.name_size = copy_string(request.name, options.profile.name); + request.manufacturer_size = copy_string(request.manufacturer, options.profile.manufacturer); + request.stable_id_size = copy_string(request.stable_id, options.metadata.stable_id); + + return request; + } + + inline LvhWindowsDestroyDeviceRequest make_destroy_device_request(std::uint64_t driver_device_id) { + LvhWindowsDestroyDeviceRequest request {}; + request.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; + request.size = sizeof(request); + request.driver_device_id = driver_device_id; + + return request; + } + + inline LvhWindowsSubmitInputReportRequest make_submit_input_report_request( + std::uint64_t driver_device_id, + const std::vector &report + ) { + LvhWindowsSubmitInputReportRequest request {}; + request.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; + request.size = sizeof(request); + request.driver_device_id = driver_device_id; + request.report_size = copy_bytes(request.report, report); + + return request; + } + +} // namespace lvh::detail::windows diff --git a/src/platform/windows/driver/CMakeLists.txt b/src/platform/windows/driver/CMakeLists.txt new file mode 100644 index 0000000..5b6beb4 --- /dev/null +++ b/src/platform/windows/driver/CMakeLists.txt @@ -0,0 +1,168 @@ +if(NOT WIN32) + message(FATAL_ERROR "The libvirtualhid Windows driver package can only be built on Windows.") +endif() + +if(NOT MSVC) + message(FATAL_ERROR "The libvirtualhid Windows driver package requires the Microsoft WDK/MSVC toolchain.") +endif() + +set(LIBVIRTUALHID_WDK_ARCH "${CMAKE_VS_PLATFORM_NAME}") +if(NOT LIBVIRTUALHID_WDK_ARCH) + if(CMAKE_SIZEOF_VOID_P EQUAL 8) + set(LIBVIRTUALHID_WDK_ARCH x64) + else() + set(LIBVIRTUALHID_WDK_ARCH x86) + endif() +endif() +string(TOLOWER "${LIBVIRTUALHID_WDK_ARCH}" LIBVIRTUALHID_WDK_ARCH) +if(LIBVIRTUALHID_WDK_ARCH STREQUAL "x64" OR LIBVIRTUALHID_WDK_ARCH STREQUAL "amd64") + set(LIBVIRTUALHID_WDK_LIBRARY_ARCH x64) + set(LIBVIRTUALHID_INF_ARCH amd64) + set(LIBVIRTUALHID_INF2CAT_OS 10_X64) +elseif(LIBVIRTUALHID_WDK_ARCH STREQUAL "arm64") + set(LIBVIRTUALHID_WDK_LIBRARY_ARCH arm64) + set(LIBVIRTUALHID_INF_ARCH arm64) + set(LIBVIRTUALHID_INF2CAT_OS 10_ARM64) +elseif(LIBVIRTUALHID_WDK_ARCH STREQUAL "win32" OR LIBVIRTUALHID_WDK_ARCH STREQUAL "x86") + set(LIBVIRTUALHID_WDK_LIBRARY_ARCH x86) + set(LIBVIRTUALHID_INF_ARCH x86) + set(LIBVIRTUALHID_INF2CAT_OS 10_X86) +else() + message(FATAL_ERROR "Unsupported WDK architecture: ${LIBVIRTUALHID_WDK_ARCH}") +endif() +set(LIBVIRTUALHID_DRIVER_VERSION "${PROJECT_VERSION}.0") + +set(_lvh_wdk_roots + "$ENV{WDKContentRoot}" + "$ENV{WindowsSdkDir}" + "C:/Program Files (x86)/Windows Kits/10") + +set(_lvh_wdf_include_candidates) +set(_lvh_wdf_library_candidates) +set(_lvh_wdk_tool_candidates) +foreach(lvh_wdk_root IN LISTS _lvh_wdk_roots) + if(NOT lvh_wdk_root) + continue() + endif() + + file(TO_CMAKE_PATH "${lvh_wdk_root}" lvh_wdk_root_cmake) + if(EXISTS "${lvh_wdk_root_cmake}") + file(GLOB _lvh_wdf_include_glob + LIST_DIRECTORIES true + "${lvh_wdk_root_cmake}/Include/wdf/umdf/2.*") + file(GLOB _lvh_wdf_library_glob + LIST_DIRECTORIES true + "${lvh_wdk_root_cmake}/Lib/wdf/umdf/${LIBVIRTUALHID_WDK_LIBRARY_ARCH}/2.*") + file(GLOB _lvh_wdk_tool_glob + LIST_DIRECTORIES true + "${lvh_wdk_root_cmake}/bin/*/x64" + "${lvh_wdk_root_cmake}/bin/x64" + "${lvh_wdk_root_cmake}/bin/*/x86" + "${lvh_wdk_root_cmake}/bin/x86") + list(APPEND _lvh_wdf_include_candidates ${_lvh_wdf_include_glob}) + list(APPEND _lvh_wdf_library_candidates ${_lvh_wdf_library_glob}) + list(APPEND _lvh_wdk_tool_candidates ${_lvh_wdk_tool_glob}) + endif() +endforeach() + +if(_lvh_wdf_include_candidates) + list(SORT _lvh_wdf_include_candidates COMPARE NATURAL ORDER DESCENDING) +endif() +if(_lvh_wdf_library_candidates) + list(SORT _lvh_wdf_library_candidates COMPARE NATURAL ORDER DESCENDING) +endif() +if(_lvh_wdk_tool_candidates) + list(SORT _lvh_wdk_tool_candidates COMPARE NATURAL ORDER DESCENDING) + list(REMOVE_DUPLICATES _lvh_wdk_tool_candidates) +endif() + +find_path(LIBVIRTUALHID_WDF_INCLUDE_DIR + NAMES wdf.h + PATHS ${_lvh_wdf_include_candidates} + NO_DEFAULT_PATH) + +find_library(LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY + NAMES WdfDriverStubUm + PATHS ${_lvh_wdf_library_candidates} + NO_DEFAULT_PATH) + +if(NOT LIBVIRTUALHID_WDF_INCLUDE_DIR OR NOT LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY) + message(FATAL_ERROR + "Could not find UMDF2 WDK headers/libraries. Install the Windows Driver Kit or set " + "LIBVIRTUALHID_WDF_INCLUDE_DIR and LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY.") +endif() + +message(STATUS "WDF include directory: ${LIBVIRTUALHID_WDF_INCLUDE_DIR}") +message(STATUS "WDF UMDF stub library: ${LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY}") + +find_program(LIBVIRTUALHID_STAMPINF + NAMES stampinf stampinf.exe + PATHS ${_lvh_wdk_tool_candidates}) +find_program(LIBVIRTUALHID_INF2CAT + NAMES inf2cat inf2cat.exe + PATHS ${_lvh_wdk_tool_candidates}) +if(NOT LIBVIRTUALHID_STAMPINF OR NOT LIBVIRTUALHID_INF2CAT) + message(FATAL_ERROR + "Could not find stampinf and inf2cat. Install the Windows Driver Kit before building the driver package.") +endif() +message(STATUS "stampinf executable: ${LIBVIRTUALHID_STAMPINF}") +message(STATUS "inf2cat executable: ${LIBVIRTUALHID_INF2CAT}") + +configure_file( + "${CMAKE_CURRENT_SOURCE_DIR}/libvirtualhid.inf.in" + "${CMAKE_CURRENT_BINARY_DIR}/libvirtualhid.inf" + @ONLY) + +add_library(libvirtualhid_umdf SHARED + "${CMAKE_CURRENT_SOURCE_DIR}/libvirtualhid_umdf.cpp") + +target_include_directories(libvirtualhid_umdf + PRIVATE + "${LIBVIRTUALHID_WDF_INCLUDE_DIR}" + "${CMAKE_CURRENT_SOURCE_DIR}/../shared") + +target_compile_definitions(libvirtualhid_umdf + PRIVATE + NOMINMAX + WIN32_LEAN_AND_MEAN + UMDF_USING_NTSTATUS + _WIN32_WINNT=0x0A00) + +target_compile_options(libvirtualhid_umdf PRIVATE /W4) +if(LIBVIRTUALHID_WARNINGS_AS_ERRORS) + target_compile_options(libvirtualhid_umdf PRIVATE /WX) +endif() +target_link_libraries(libvirtualhid_umdf PRIVATE "${LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY}") +set_target_properties(libvirtualhid_umdf PROPERTIES + OUTPUT_NAME libvirtualhid_umdf + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/package") + +add_custom_command(TARGET libvirtualhid_umdf POST_BUILD + COMMAND "${CMAKE_COMMAND}" -E make_directory "$" + COMMAND "${CMAKE_COMMAND}" -E copy_if_different + "${CMAKE_CURRENT_BINARY_DIR}/libvirtualhid.inf" + "$/libvirtualhid.inf" + COMMENT "Copying libvirtualhid driver INF") + +add_custom_command(TARGET libvirtualhid_umdf POST_BUILD + COMMAND "${LIBVIRTUALHID_STAMPINF}" + -f "$/libvirtualhid.inf" + -d "*" + -v "${LIBVIRTUALHID_DRIVER_VERSION}" + COMMENT "Stamping libvirtualhid.inf") + +add_custom_target(libvirtualhid_windows_catalog + COMMAND "${LIBVIRTUALHID_INF2CAT}" + /driver:"$" + /os:${LIBVIRTUALHID_INF2CAT_OS} + DEPENDS libvirtualhid_umdf + COMMENT "Generating libvirtualhid driver catalog") + +install(TARGETS libvirtualhid_umdf + RUNTIME DESTINATION "drivers/windows" + COMPONENT driver) +install(FILES + "$/libvirtualhid.inf" + "$/libvirtualhid.cat" + DESTINATION "drivers/windows" + COMPONENT driver) diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in new file mode 100644 index 0000000..4d2155b --- /dev/null +++ b/src/platform/windows/driver/libvirtualhid.inf.in @@ -0,0 +1,50 @@ +; +; libvirtualhid UMDF2 control driver package. +; + +[Version] +Signature="$WINDOWS NT$" +Class=HIDClass +ClassGuid={745A17A0-74D3-11D0-B6FE-00A0C90F57DA} +Provider=%ManufacturerName% +DriverVer=*,@LIBVIRTUALHID_DRIVER_VERSION@ +CatalogFile=libvirtualhid.cat +PnpLockdown=1 + +[Manufacturer] +%ManufacturerName%=Standard,NT@LIBVIRTUALHID_INF_ARCH@ + +[Standard.NT@LIBVIRTUALHID_INF_ARCH@] +%DeviceName%=DeviceInstall,ROOT\LIBVIRTUALHID + +[DestinationDirs] +UMDriverCopy=13 + +[DeviceInstall.NT] +CopyFiles=UMDriverCopy + +[UMDriverCopy] +libvirtualhid_umdf.dll + +[DeviceInstall.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 +UmdfServiceOrder=libvirtualhid_umdf + +[libvirtualhid_umdf_Install] +UmdfLibraryVersion=2.0 +ServiceBinary=%13%\libvirtualhid_umdf.dll + +[Strings] +ManufacturerName="LizardByte" +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 new file mode 100644 index 0000000..2abe726 --- /dev/null +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -0,0 +1,231 @@ +/** + * @file src/platform/windows/driver/libvirtualhid_umdf.cpp + * @brief UMDF2 control driver entry points for the Windows libvirtualhid backend. + */ + +#ifndef NOMINMAX + #define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN +#endif + +// platform includes +#define WIN32_NO_STATUS +#include +#undef WIN32_NO_STATUS + +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable : 4324 4471) +#endif +#include +#if defined(_MSC_VER) + #pragma warning(pop) +#endif + +// standard includes +#include +#include +#include +#include +#include +#include +#include +#include + +// local includes +#include "lvh_windows_protocol.h" + +namespace { + + constexpr auto symbolic_link_name = L"\\DosDevices\\LibVirtualHid"; + + struct DeviceRecord { + LvhWindowsCreateGamepadRequest request {}; + }; + + std::atomic next_driver_device_id {1}; + std::mutex devices_mutex; + std::map devices; + + std::mutex output_requests_mutex; + std::vector pending_output_requests; + + 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; + } + + void complete_request(WDFREQUEST request, NTSTATUS status, ULONG_PTR information = 0) { + WdfRequestCompleteWithInformation(request, status, information); + } + + bool remove_pending_output_request(WDFREQUEST request) { + std::lock_guard lock {output_requests_mutex}; + const auto iter = std::find(pending_output_requests.begin(), pending_output_requests.end(), request); + if (iter == pending_output_requests.end()) { + return false; + } + + pending_output_requests.erase(iter); + return true; + } + +} // namespace + +extern "C" DRIVER_INITIALIZE DriverEntry; +EVT_WDF_DRIVER_DEVICE_ADD LvhEvtDeviceAdd; +EVT_WDF_IO_QUEUE_IO_DEVICE_CONTROL LvhEvtIoDeviceControl; +EVT_WDF_REQUEST_CANCEL LvhEvtOutputReadCanceled; + +extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT driver_object, PUNICODE_STRING registry_path) { + WDF_DRIVER_CONFIG config; + WDF_DRIVER_CONFIG_INIT(&config, LvhEvtDeviceAdd); + + return WdfDriverCreate(driver_object, registry_path, WDF_NO_OBJECT_ATTRIBUTES, &config, WDF_NO_HANDLE); +} + +NTSTATUS LvhEvtDeviceAdd(WDFDRIVER driver, PWDFDEVICE_INIT device_init) { + UNREFERENCED_PARAMETER(driver); + + WDFDEVICE device = nullptr; + WDF_OBJECT_ATTRIBUTES device_attributes; + WDF_OBJECT_ATTRIBUTES_INIT(&device_attributes); + auto status = WdfDeviceCreate(&device_init, &device_attributes, &device); + if (!NT_SUCCESS(status)) { + return status; + } + + UNICODE_STRING symbolic_link; + RtlInitUnicodeString(&symbolic_link, symbolic_link_name); + status = WdfDeviceCreateSymbolicLink(device, &symbolic_link); + if (!NT_SUCCESS(status)) { + return 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); +} + +void LvhEvtOutputReadCanceled(WDFREQUEST request) { + if (remove_pending_output_request(request)) { + complete_request(request, STATUS_CANCELLED); + } +} + +void LvhEvtIoDeviceControl( + WDFQUEUE queue, + WDFREQUEST request, + size_t output_buffer_length, + 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: + { + auto *create_request = static_cast(nullptr); + auto status = WdfRequestRetrieveInputBuffer(request, sizeof(LvhWindowsCreateGamepadRequest), reinterpret_cast(&create_request), nullptr); + if (!NT_SUCCESS(status)) { + complete_request(request, status); + return; + } + + auto *create_response = static_cast(nullptr); + status = WdfRequestRetrieveOutputBuffer(request, sizeof(LvhWindowsCreateGamepadResponse), reinterpret_cast(&create_response), nullptr); + if (!NT_SUCCESS(status)) { + complete_request(request, status); + return; + } + + std::memset(create_response, 0, sizeof(*create_response)); + create_response->version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; + create_response->size = sizeof(*create_response); + + if (!valid_header(create_request->version, create_request->size, sizeof(*create_request)) || create_request->report_descriptor_size == 0U || create_request->report_descriptor_size > LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE || create_request->input_report_size == 0U || create_request->input_report_size > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE || create_request->output_report_size > LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE) { + create_response->status = LVH_WINDOWS_STATUS_INVALID_ARGUMENT; + complete_request(request, STATUS_SUCCESS, sizeof(*create_response)); + return; + } + + const auto driver_device_id = next_driver_device_id.fetch_add(1); + { + std::lock_guard lock {devices_mutex}; + devices.emplace(driver_device_id, DeviceRecord {.request = *create_request}); + } + + create_response->status = LVH_WINDOWS_STATUS_SUCCESS; + create_response->driver_device_id = driver_device_id; + std::snprintf(create_response->device_path, sizeof(create_response->device_path), "%s#%llu", LVH_WINDOWS_CONTROL_DEVICE_PATH, static_cast(driver_device_id)); + complete_request(request, STATUS_SUCCESS, sizeof(*create_response)); + return; + } + + case LVH_WINDOWS_IOCTL_DESTROY_DEVICE: + { + auto *destroy_request = static_cast(nullptr); + const auto status = WdfRequestRetrieveInputBuffer(request, sizeof(LvhWindowsDestroyDeviceRequest), reinterpret_cast(&destroy_request), nullptr); + if (!NT_SUCCESS(status)) { + complete_request(request, status); + return; + } + + if (!valid_header(destroy_request->version, destroy_request->size, sizeof(*destroy_request))) { + complete_request(request, STATUS_INVALID_PARAMETER); + return; + } + + std::lock_guard lock {devices_mutex}; + devices.erase(destroy_request->driver_device_id); + complete_request(request, STATUS_SUCCESS); + return; + } + + case LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT: + { + auto *submit_request = static_cast(nullptr); + const auto status = WdfRequestRetrieveInputBuffer(request, sizeof(LvhWindowsSubmitInputReportRequest), reinterpret_cast(&submit_request), nullptr); + if (!NT_SUCCESS(status)) { + complete_request(request, status); + return; + } + + if (!valid_header(submit_request->version, submit_request->size, sizeof(*submit_request)) || submit_request->report_size == 0U || submit_request->report_size > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE) { + complete_request(request, STATUS_INVALID_PARAMETER); + return; + } + + std::lock_guard lock {devices_mutex}; + if (devices.find(submit_request->driver_device_id) == devices.end()) { + complete_request(request, STATUS_OBJECT_NAME_NOT_FOUND); + return; + } + + complete_request(request, STATUS_SUCCESS); + return; + } + + case LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT: + { + std::lock_guard lock {output_requests_mutex}; + const auto status = WdfRequestMarkCancelableEx(request, LvhEvtOutputReadCanceled); + if (!NT_SUCCESS(status)) { + complete_request(request, status); + return; + } + + pending_output_requests.push_back(request); + return; + } + + default: + complete_request(request, STATUS_INVALID_DEVICE_REQUEST); + return; + } +} diff --git a/src/platform/windows/shared/lvh_windows_protocol.h b/src/platform/windows/shared/lvh_windows_protocol.h new file mode 100644 index 0000000..6043f42 --- /dev/null +++ b/src/platform/windows/shared/lvh_windows_protocol.h @@ -0,0 +1,154 @@ +/** + * @file src/platform/windows/shared/lvh_windows_protocol.h + * @brief Stable control protocol shared by the Windows client backend and UMDF driver. + */ +#pragma once + +#include + +#define LVH_WINDOWS_CONTROL_PROTOCOL_VERSION 1u +#define LVH_WINDOWS_CONTROL_DEVICE_PATH "\\\\.\\LibVirtualHid" + +#define LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE 1024u +#define LVH_WINDOWS_MAX_INPUT_REPORT_SIZE 256u +#define LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE 256u +#define LVH_WINDOWS_MAX_DEVICE_PATH_SIZE 260u +#define LVH_WINDOWS_MAX_DEVICE_NAME_SIZE 128u +#define LVH_WINDOWS_MAX_MANUFACTURER_SIZE 128u +#define LVH_WINDOWS_MAX_STABLE_ID_SIZE 128u + +#define LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID 0x8000u +#define LVH_WINDOWS_METHOD_BUFFERED 0u +#define LVH_WINDOWS_FILE_ANY_ACCESS 0u +#define LVH_WINDOWS_FILE_READ_ACCESS 1u +#define LVH_WINDOWS_FILE_WRITE_ACCESS 2u +#define LVH_WINDOWS_CTL_CODE(device_type, function_code, method, access) \ + (((device_type) << 16u) | ((access) << 14u) | ((function_code) << 2u) | (method)) + +#define LVH_WINDOWS_IOCTL_CREATE_GAMEPAD \ + LVH_WINDOWS_CTL_CODE( \ + LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, \ + 0x800u, \ + LVH_WINDOWS_METHOD_BUFFERED, \ + LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS \ + ) +#define LVH_WINDOWS_IOCTL_DESTROY_DEVICE \ + LVH_WINDOWS_CTL_CODE( \ + LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, \ + 0x801u, \ + LVH_WINDOWS_METHOD_BUFFERED, \ + LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS \ + ) +#define LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT \ + LVH_WINDOWS_CTL_CODE( \ + LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, \ + 0x802u, \ + LVH_WINDOWS_METHOD_BUFFERED, \ + LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS \ + ) +#define LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT \ + LVH_WINDOWS_CTL_CODE( \ + LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, \ + 0x803u, \ + LVH_WINDOWS_METHOD_BUFFERED, \ + LVH_WINDOWS_FILE_READ_ACCESS \ + ) + +#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE 0x00000001u +#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION 0x00000002u +#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD 0x00000004u +#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RGB_LED 0x00000008u +#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY 0x00000010u +#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_ADAPTIVE_TRIGGERS 0x00000020u + +#ifdef __cplusplus +extern "C" { +#endif + + enum LvhWindowsProtocolStatus { + LVH_WINDOWS_STATUS_SUCCESS = 0, + LVH_WINDOWS_STATUS_INVALID_ARGUMENT = 1, + LVH_WINDOWS_STATUS_UNSUPPORTED_PROFILE = 2, + LVH_WINDOWS_STATUS_DEVICE_NOT_FOUND = 3, + LVH_WINDOWS_STATUS_BACKEND_FAILURE = 4, + }; + + enum LvhWindowsBusType { + LVH_WINDOWS_BUS_UNKNOWN = 0, + LVH_WINDOWS_BUS_USB = 1, + LVH_WINDOWS_BUS_BLUETOOTH = 2, + }; + + enum LvhWindowsGamepadProfileKind { + LVH_WINDOWS_GAMEPAD_GENERIC = 0, + LVH_WINDOWS_GAMEPAD_XBOX_360 = 1, + LVH_WINDOWS_GAMEPAD_XBOX_ONE = 2, + LVH_WINDOWS_GAMEPAD_XBOX_SERIES = 3, + LVH_WINDOWS_GAMEPAD_DUALSENSE = 4, + LVH_WINDOWS_GAMEPAD_SWITCH_PRO = 5, + }; + +#pragma pack(push, 1) + + struct LvhWindowsCreateGamepadRequest { + uint32_t version; + uint32_t size; + uint64_t client_device_id; + uint32_t bus_type; + uint32_t gamepad_kind; + uint32_t flags; + uint16_t vendor_id; + uint16_t product_id; + uint16_t device_version; + uint8_t report_id; + uint8_t reserved0[7]; + uint32_t input_report_size; + uint32_t output_report_size; + uint32_t report_descriptor_size; + uint32_t name_size; + uint32_t manufacturer_size; + uint32_t stable_id_size; + uint8_t report_descriptor[LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE]; + char name[LVH_WINDOWS_MAX_DEVICE_NAME_SIZE]; + char manufacturer[LVH_WINDOWS_MAX_MANUFACTURER_SIZE]; + char stable_id[LVH_WINDOWS_MAX_STABLE_ID_SIZE]; + }; + + struct LvhWindowsCreateGamepadResponse { + uint32_t version; + uint32_t size; + uint32_t status; + uint32_t reserved0; + uint64_t driver_device_id; + char device_path[LVH_WINDOWS_MAX_DEVICE_PATH_SIZE]; + }; + + struct LvhWindowsDestroyDeviceRequest { + uint32_t version; + uint32_t size; + uint64_t driver_device_id; + }; + + struct LvhWindowsSubmitInputReportRequest { + uint32_t version; + uint32_t size; + uint64_t driver_device_id; + uint32_t report_size; + uint32_t reserved0; + uint8_t report[LVH_WINDOWS_MAX_INPUT_REPORT_SIZE]; + }; + + struct LvhWindowsOutputReportEvent { + uint32_t version; + uint32_t size; + uint64_t driver_device_id; + uint32_t report_size; + uint32_t reserved0; + uint8_t report[LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE]; + }; + +#pragma pack(pop) + +#ifdef __cplusplus +} +#endif diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp new file mode 100644 index 0000000..94561f9 --- /dev/null +++ b/src/platform/windows/windows_backend.cpp @@ -0,0 +1,636 @@ +/** + * @file src/platform/windows/windows_backend.cpp + * @brief Windows UMDF control-channel backend definitions. + */ + +// local includes +#include "core/backend.hpp" +#include "platform/windows/control_protocol.hpp" + +#include + +// standard includes +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef NOMINMAX + #define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN + #define WIN32_LEAN_AND_MEAN +#endif + +// platform includes +#include + +namespace lvh::detail { + namespace { + + class WindowsBackendContext; + + class UniqueHandle { + public: + UniqueHandle() = default; + + explicit UniqueHandle(HANDLE handle): + handle_ {handle} {} + + UniqueHandle(const UniqueHandle &) = delete; + UniqueHandle &operator=(const UniqueHandle &) = delete; + + UniqueHandle(UniqueHandle &&other) noexcept: + handle_ {std::exchange(other.handle_, nullptr)} {} + + UniqueHandle &operator=(UniqueHandle &&other) noexcept { + if (this != &other) { + reset(std::exchange(other.handle_, nullptr)); + } + + return *this; + } + + ~UniqueHandle() { + reset(); + } + + void reset(HANDLE handle = nullptr) { + if (handle_ != nullptr && handle_ != INVALID_HANDLE_VALUE) { + static_cast(::CloseHandle(handle_)); + } + + handle_ = handle; + } + + HANDLE get() const { + return handle_; + } + + explicit operator bool() const { + return handle_ != nullptr && handle_ != INVALID_HANDLE_VALUE; + } + + private: + HANDLE handle_ {nullptr}; + }; + + std::string windows_error_message(DWORD error_code) { + LPSTR message_buffer = nullptr; + const auto message_size = ::FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, + error_code, + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&message_buffer), + 0, + nullptr + ); + + std::string message; + if (message_size > 0U && message_buffer != nullptr) { + message.assign(message_buffer, message_size); + while (!message.empty() && (message.back() == '\r' || message.back() == '\n')) { + message.pop_back(); + } + } else { + message = "Windows error " + std::to_string(error_code); + } + + if (message_buffer != nullptr) { + ::LocalFree(message_buffer); + } + + return message; + } + + OperationStatus windows_failure(ErrorCode code, std::string_view operation, DWORD error_code) { + std::string message {operation}; + message += ": "; + message += windows_error_message(error_code); + return OperationStatus::failure(code, std::move(message)); + } + + std::string resolve_control_device_path() { + if (const auto *path = std::getenv("LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE"); path != nullptr && path[0] != '\0') { + return path; + } + + return std::string {windows::default_control_device_path}; + } + + OperationStatus protocol_status(std::uint32_t status, std::string_view operation) { + switch (status) { + case LVH_WINDOWS_STATUS_SUCCESS: + return OperationStatus::success(); + case LVH_WINDOWS_STATUS_INVALID_ARGUMENT: + return OperationStatus::failure(ErrorCode::invalid_argument, std::string {operation}); + case LVH_WINDOWS_STATUS_UNSUPPORTED_PROFILE: + return OperationStatus::failure(ErrorCode::unsupported_profile, std::string {operation}); + case LVH_WINDOWS_STATUS_DEVICE_NOT_FOUND: + return OperationStatus::failure(ErrorCode::device_closed, std::string {operation}); + case LVH_WINDOWS_STATUS_BACKEND_FAILURE: + default: + return OperationStatus::failure(ErrorCode::backend_failure, std::string {operation}); + } + } + + OperationStatus validate_windows_gamepad_profile(const DeviceProfile &profile) { + if (profile.report_descriptor.size() > LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE) { + return OperationStatus::failure(ErrorCode::invalid_argument, "Windows gamepad HID descriptor exceeds control protocol limit"); + } + if (profile.input_report_size > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE) { + return OperationStatus::failure(ErrorCode::invalid_argument, "Windows gamepad input report exceeds control protocol limit"); + } + if (profile.output_report_size > LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE) { + return OperationStatus::failure(ErrorCode::invalid_argument, "Windows gamepad output report exceeds control protocol limit"); + } + + return OperationStatus::success(); + } + + class WindowsControlChannel { + public: + static std::unique_ptr open(const std::string &path) { + UniqueHandle handle { + ::CreateFileA( + path.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, + nullptr + ) + }; + + if (!handle) { + return nullptr; + } + + return std::unique_ptr {new WindowsControlChannel(path, std::move(handle))}; + } + + const std::string &path() const { + return path_; + } + + OperationStatus create_gamepad( + const LvhWindowsCreateGamepadRequest &request, + LvhWindowsCreateGamepadResponse &response + ) const { + DWORD bytes_returned = 0; + const auto status = device_io_control( + LVH_WINDOWS_IOCTL_CREATE_GAMEPAD, + &request, + sizeof(request), + &response, + sizeof(response), + &bytes_returned, + "create Windows gamepad" + ); + if (!status.ok()) { + return status; + } + + if (bytes_returned < sizeof(response)) { + return OperationStatus::failure(ErrorCode::backend_failure, "Windows driver returned a truncated gamepad response"); + } + + return protocol_status(response.status, "Windows driver rejected gamepad creation"); + } + + OperationStatus destroy_device(std::uint64_t driver_device_id) const { + const auto request = windows::make_destroy_device_request(driver_device_id); + DWORD bytes_returned = 0; + return device_io_control( + LVH_WINDOWS_IOCTL_DESTROY_DEVICE, + &request, + sizeof(request), + nullptr, + 0, + &bytes_returned, + "destroy Windows virtual HID device" + ); + } + + OperationStatus submit_input_report(std::uint64_t driver_device_id, const std::vector &report) const { + if (report.size() > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE) { + return OperationStatus::failure(ErrorCode::invalid_argument, "input report exceeds Windows control protocol limit"); + } + + const auto request = windows::make_submit_input_report_request(driver_device_id, report); + DWORD bytes_returned = 0; + return device_io_control( + LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT, + &request, + sizeof(request), + nullptr, + 0, + &bytes_returned, + "submit Windows input report" + ); + } + + std::optional read_output_report(HANDLE stop_event) const { + LvhWindowsOutputReportEvent event {}; + event.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; + event.size = sizeof(event); + + UniqueHandle operation_event {::CreateEventA(nullptr, TRUE, FALSE, nullptr)}; + if (!operation_event) { + return std::nullopt; + } + + OVERLAPPED overlapped {}; + overlapped.hEvent = operation_event.get(); + DWORD bytes_returned = 0; + + const auto started = ::DeviceIoControl( + handle_.get(), + LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT, + nullptr, + 0, + &event, + sizeof(event), + &bytes_returned, + &overlapped + ); + + if (started == FALSE) { + const auto error_code = ::GetLastError(); + if (error_code != ERROR_IO_PENDING) { + return std::nullopt; + } + + HANDLE wait_handles[] { + operation_event.get(), + stop_event, + }; + const auto wait_result = ::WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE); + if (wait_result == WAIT_OBJECT_0 + 1U) { + static_cast(::CancelIoEx(handle_.get(), &overlapped)); + return std::nullopt; + } + if (wait_result != WAIT_OBJECT_0) { + static_cast(::CancelIoEx(handle_.get(), &overlapped)); + return std::nullopt; + } + } + + if (::GetOverlappedResult(handle_.get(), &overlapped, &bytes_returned, FALSE) == FALSE) { + return std::nullopt; + } + + if (bytes_returned < sizeof(event.version) + sizeof(event.size) + sizeof(event.driver_device_id) + sizeof(event.report_size)) { + return std::nullopt; + } + + event.report_size = std::min(event.report_size, static_cast(LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE)); + return event; + } + + private: + WindowsControlChannel(std::string path, UniqueHandle handle): + path_ {std::move(path)}, + handle_ {std::move(handle)} {} + + OperationStatus device_io_control( + DWORD control_code, + const void *input, + DWORD input_size, + void *output, + DWORD output_size, + DWORD *bytes_returned, + std::string_view operation + ) const { + if (::DeviceIoControl(handle_.get(), control_code, const_cast(input), input_size, output, output_size, bytes_returned, nullptr) == FALSE) { + return windows_failure(ErrorCode::backend_failure, operation, ::GetLastError()); + } + + return OperationStatus::success(); + } + + std::string path_; + UniqueHandle handle_; + }; + + struct WindowsGamepadState { + WindowsGamepadState( + DeviceId client_device_id, + std::uint64_t driver_device_id, + DeviceProfile device_profile, + std::string device_path + ): + client_id {client_device_id}, + driver_id {driver_device_id}, + profile {std::move(device_profile)}, + path {std::move(device_path)} {} + + mutable std::mutex mutex; + DeviceId client_id; + std::uint64_t driver_id; + DeviceProfile profile; + std::string path; + bool open = true; + OutputCallback output_callback; + }; + + class WindowsGamepad final: public BackendGamepad { + public: + WindowsGamepad(std::shared_ptr context, std::shared_ptr state): + context_ {std::move(context)}, + state_ {std::move(state)} {} + + OperationStatus submit(const std::vector &report) override; + void set_output_callback(OutputCallback callback) override; + std::vector device_nodes() const override; + OperationStatus close() override; + + private: + std::shared_ptr context_; + std::shared_ptr state_; + }; + + class WindowsBackendContext: public std::enable_shared_from_this { + public: + WindowsBackendContext( + std::unique_ptr command_channel, + std::unique_ptr event_channel + ): + command_channel_ {std::move(command_channel)}, + event_channel_ {std::move(event_channel)}, + stop_event_ {::CreateEventA(nullptr, TRUE, FALSE, nullptr)} {} + + WindowsBackendContext(const WindowsBackendContext &) = delete; + WindowsBackendContext &operator=(const WindowsBackendContext &) = delete; + + ~WindowsBackendContext() { + stop(); + } + + bool valid() const { + return command_channel_ != nullptr && event_channel_ != nullptr && static_cast(stop_event_); + } + + void start() { + if (!valid() || output_thread_.joinable()) { + return; + } + + output_thread_ = std::jthread {[this](std::stop_token stop_token) { + output_loop(stop_token); + }}; + } + + void stop() { + if (stop_event_) { + static_cast(::SetEvent(stop_event_.get())); + } + + if (output_thread_.joinable()) { + output_thread_.request_stop(); + output_thread_.join(); + } + } + + BackendGamepadCreationResult create_gamepad(DeviceId id, const CreateGamepadOptions &options) { + auto request = windows::make_create_gamepad_request(id, options); + LvhWindowsCreateGamepadResponse response {}; + response.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; + response.size = sizeof(response); + + const auto status = command_channel_->create_gamepad(request, response); + if (!status.ok()) { + return {status, nullptr}; + } + + auto state = std::make_shared( + id, + response.driver_device_id, + options.profile, + response.device_path[0] == '\0' ? command_channel_->path() : std::string {response.device_path} + ); + + { + std::lock_guard lock {devices_mutex_}; + gamepads_[state->driver_id] = state; + } + + auto gamepad = std::make_unique(shared_from_this(), std::move(state)); + return {OperationStatus::success(), std::move(gamepad)}; + } + + OperationStatus submit_gamepad_report(const std::shared_ptr &state, const std::vector &report) { + const auto driver_id = state->driver_id; + return command_channel_->submit_input_report(driver_id, report); + } + + OperationStatus close_gamepad(const std::shared_ptr &state) { + std::uint64_t driver_id = 0; + { + std::lock_guard lock {state->mutex}; + if (!state->open) { + return OperationStatus::success(); + } + + state->open = false; + driver_id = state->driver_id; + } + + { + std::lock_guard lock {devices_mutex_}; + gamepads_.erase(driver_id); + } + + return command_channel_->destroy_device(driver_id); + } + + private: + void output_loop(std::stop_token stop_token) { + while (!stop_token.stop_requested()) { + auto event = event_channel_->read_output_report(stop_event_.get()); + if (!event.has_value()) { + continue; + } + + dispatch_output_report(*event); + } + } + + void dispatch_output_report(const LvhWindowsOutputReportEvent &event) { + std::shared_ptr state; + { + std::lock_guard lock {devices_mutex_}; + if (const auto iter = gamepads_.find(event.driver_device_id); iter != gamepads_.end()) { + state = iter->second.lock(); + } + } + + if (!state) { + return; + } + + DeviceProfile profile; + OutputCallback callback; + { + std::lock_guard lock {state->mutex}; + if (!state->open || !state->output_callback) { + return; + } + + profile = state->profile; + callback = state->output_callback; + } + + std::vector report( + event.report, + event.report + std::min(event.report_size, static_cast(LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE)) + ); + for (const auto &output : reports::parse_output_reports(profile, report)) { + callback(output); + } + } + + std::unique_ptr command_channel_; + std::unique_ptr event_channel_; + UniqueHandle stop_event_; + std::jthread output_thread_; + std::mutex devices_mutex_; + std::map> gamepads_; + }; + + OperationStatus WindowsGamepad::submit(const std::vector &report) { + { + std::lock_guard lock {state_->mutex}; + if (!state_->open) { + return OperationStatus::failure(ErrorCode::device_closed, "Windows gamepad is closed"); + } + } + + return context_->submit_gamepad_report(state_, report); + } + + void WindowsGamepad::set_output_callback(OutputCallback callback) { + std::lock_guard lock {state_->mutex}; + state_->output_callback = std::move(callback); + } + + std::vector WindowsGamepad::device_nodes() const { + std::lock_guard lock {state_->mutex}; + if (state_->path.empty()) { + return {}; + } + + return {{.kind = DeviceNodeKind::other, .path = state_->path}}; + } + + OperationStatus WindowsGamepad::close() { + return context_->close_gamepad(state_); + } + + class WindowsBackend final: public Backend { + public: + WindowsBackend() { + capabilities_.backend_name = "windows-umdf"; + capabilities_.requires_installed_driver = true; + + auto command_channel = WindowsControlChannel::open(resolve_control_device_path()); + auto event_channel = WindowsControlChannel::open(resolve_control_device_path()); + if (!command_channel || !event_channel) { + return; + } + + context_ = std::make_shared(std::move(command_channel), std::move(event_channel)); + if (!context_->valid()) { + context_.reset(); + return; + } + + context_->start(); + capabilities_.supports_virtual_hid = true; + capabilities_.supports_gamepad = true; + capabilities_.supports_output_reports = true; + } + + ~WindowsBackend() override { + if (context_) { + context_->stop(); + } + } + + const BackendCapabilities &capabilities() const override { + return capabilities_; + } + + BackendGamepadCreationResult create_gamepad(DeviceId id, const CreateGamepadOptions &options) override { + if (const auto validation = validate_windows_gamepad_profile(options.profile); !validation.ok()) { + return {validation, nullptr}; + } + + if (!context_) { + return { + OperationStatus::failure( + ErrorCode::backend_unavailable, + "Windows UMDF control device is unavailable; install the libvirtualhid driver package" + ), + nullptr, + }; + } + + return context_->create_gamepad(id, options); + } + + BackendKeyboardCreationResult create_keyboard(DeviceId /*id*/, const CreateKeyboardOptions & /*options*/) override { + return {unsupported_device_status(), nullptr}; + } + + BackendMouseCreationResult create_mouse(DeviceId /*id*/, const CreateMouseOptions & /*options*/) override { + return {unsupported_device_status(), nullptr}; + } + + BackendTouchscreenCreationResult create_touchscreen( + DeviceId /*id*/, + const CreateTouchscreenOptions & /*options*/ + ) override { + return {unsupported_device_status(), nullptr}; + } + + BackendTrackpadCreationResult create_trackpad(DeviceId /*id*/, const CreateTrackpadOptions & /*options*/) override { + return {unsupported_device_status(), nullptr}; + } + + BackendPenTabletCreationResult create_pen_tablet( + DeviceId /*id*/, + const CreatePenTabletOptions & /*options*/ + ) override { + return {unsupported_device_status(), nullptr}; + } + + private: + static OperationStatus unsupported_device_status() { + return OperationStatus::failure( + ErrorCode::unsupported_profile, + "Windows MVP backend currently supports gamepad devices only" + ); + } + + BackendCapabilities capabilities_; + std::shared_ptr context_; + }; + + } // namespace + + std::unique_ptr create_platform_backend() { + return std::make_unique(); + } + +} // namespace lvh::detail diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 19c7295..26fbddf 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -21,7 +21,8 @@ set(LIBVIRTUALHID_TEST_SOURCES "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_linux_consumers.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_profiles.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_report.cpp" - "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_runtime.cpp") + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_runtime.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_protocol.cpp") if(CMAKE_SYSTEM_NAME STREQUAL "Linux") find_package(PkgConfig REQUIRED) @@ -43,6 +44,7 @@ add_executable(${TEST_BINARY} target_include_directories(${TEST_BINARY} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/fixtures/include" + "${PROJECT_SOURCE_DIR}/src/platform/windows/shared" "${PROJECT_SOURCE_DIR}/src") target_link_libraries(${TEST_BINARY} diff --git a/tests/unit/test_runtime.cpp b/tests/unit/test_runtime.cpp index bb012de..ec77d92 100644 --- a/tests/unit/test_runtime.cpp +++ b/tests/unit/test_runtime.cpp @@ -39,6 +39,20 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) { #if defined(__linux__) EXPECT_EQ(runtime->capabilities().backend_name, "linux-uhid-uinput"); EXPECT_FALSE(runtime->capabilities().requires_installed_driver); +#elif defined(_WIN32) + EXPECT_EQ(runtime->capabilities().backend_name, "windows-umdf"); + EXPECT_TRUE(runtime->capabilities().requires_installed_driver); + EXPECT_FALSE(runtime->capabilities().supports_keyboard); + EXPECT_FALSE(runtime->capabilities().supports_mouse); + EXPECT_FALSE(runtime->capabilities().supports_touchscreen); + EXPECT_FALSE(runtime->capabilities().supports_trackpad); + EXPECT_FALSE(runtime->capabilities().supports_pen_tablet); + + if (!runtime->capabilities().supports_gamepad) { + auto created = runtime->create_gamepad(lvh::profiles::xbox_360()); + EXPECT_FALSE(created); + EXPECT_EQ(created.status.code(), lvh::ErrorCode::backend_unavailable); + } #else EXPECT_EQ(runtime->capabilities().backend_name, "platform-default-unimplemented"); EXPECT_FALSE(runtime->capabilities().supports_gamepad); diff --git a/tests/unit/test_windows_protocol.cpp b/tests/unit/test_windows_protocol.cpp new file mode 100644 index 0000000..dd97bac --- /dev/null +++ b/tests/unit/test_windows_protocol.cpp @@ -0,0 +1,61 @@ +/** + * @file tests/unit/test_windows_protocol.cpp + * @brief Unit tests for the Windows control protocol helpers. + */ + +// local includes +#include "fixtures/fixtures.hpp" +#include "platform/windows/control_protocol.hpp" + +// standard includes +#include +#include + +// lib includes +#include + +TEST(WindowsProtocolTest, PacksGamepadCreateRequest) { + lvh::CreateGamepadOptions options; + options.profile = lvh::profiles::dualsense_bluetooth(); + options.metadata.stable_id = "client-0-controller-1"; + + const auto request = lvh::detail::windows::make_create_gamepad_request(42, options); + + EXPECT_EQ(request.version, LVH_WINDOWS_CONTROL_PROTOCOL_VERSION); + EXPECT_EQ(request.size, sizeof(request)); + EXPECT_EQ(request.client_device_id, 42U); + EXPECT_EQ(request.bus_type, LVH_WINDOWS_BUS_BLUETOOTH); + EXPECT_EQ(request.gamepad_kind, LVH_WINDOWS_GAMEPAD_DUALSENSE); + EXPECT_EQ(request.vendor_id, options.profile.vendor_id); + EXPECT_EQ(request.product_id, options.profile.product_id); + EXPECT_EQ(request.device_version, options.profile.version); + EXPECT_EQ(request.report_id, options.profile.report_id); + EXPECT_EQ(request.input_report_size, options.profile.input_report_size); + EXPECT_EQ(request.output_report_size, options.profile.output_report_size); + EXPECT_EQ(request.report_descriptor_size, options.profile.report_descriptor.size()); + EXPECT_EQ(request.report_descriptor[0], options.profile.report_descriptor[0]); + EXPECT_STREQ(request.name, options.profile.name.c_str()); + EXPECT_STREQ(request.manufacturer, options.profile.manufacturer.c_str()); + EXPECT_STREQ(request.stable_id, options.metadata.stable_id.c_str()); + EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE, 0U); + EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION, 0U); + EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD, 0U); + EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY, 0U); +} + +TEST(WindowsProtocolTest, PacksSubmitAndDestroyRequests) { + const std::vector report {1, 2, 3, 4, 5}; + + const auto submit = lvh::detail::windows::make_submit_input_report_request(17, report); + EXPECT_EQ(submit.version, LVH_WINDOWS_CONTROL_PROTOCOL_VERSION); + EXPECT_EQ(submit.size, sizeof(submit)); + EXPECT_EQ(submit.driver_device_id, 17U); + EXPECT_EQ(submit.report_size, report.size()); + EXPECT_EQ(submit.report[0], report[0]); + EXPECT_EQ(submit.report[4], report[4]); + + const auto destroy = lvh::detail::windows::make_destroy_device_request(17); + EXPECT_EQ(destroy.version, LVH_WINDOWS_CONTROL_PROTOCOL_VERSION); + EXPECT_EQ(destroy.size, sizeof(destroy)); + EXPECT_EQ(destroy.driver_device_id, 17U); +} From 16c6940f8b39718a4ae4cf3071289d3881d93ae2 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 20 Jun 2026 20:23:14 -0400 Subject: [PATCH 2/5] Windows: packaging & driver build fixes Refine Windows packaging and driver build integration: - cmake/packaging/windows.cmake: configure CPack for driver component when building driver and set CPACK_INSTALL_CMAKE_PROJECTS for driver installs. - cmake/packaging/windows_wix.cmake: point CPACK_WIX_EXTENSIONS to the full WixToolset.UI extension DLL path and remove use of a separate patch file. - Remove wix_resources/patch.xml and the FeatureRef from libvirtualhid-driver-installer.wxs. - src/platform/windows/driver/CMakeLists.txt: normalize and deduplicate WDK root paths, search for UM import libraries (ntdll) and expose LIBVIRTUALHID_NTDLL_LIBRARY; update error messages; link ntdll; restructure custom targets to prepare/stamp the INF and then generate the driver catalog with proper dependencies and working directories. - src/platform/windows/driver/libvirtualhid.inf.in: add SourceDisksNames/Files and DiskName entry so the INF lists source disk information. - src/platform/windows/windows_backend.cpp: replace getenv usage with GetEnvironmentVariableA for robust environment variable reading and remove unused include. These changes improve robustness when locating WDK components, produce correct installer inputs, and make INF/catalog generation deterministic in the build. --- cmake/packaging/windows.cmake | 6 +- cmake/packaging/windows_wix.cmake | 5 +- .../libvirtualhid-driver-installer.wxs | 2 - cmake/packaging/wix_resources/patch.xml | 5 -- src/platform/windows/driver/CMakeLists.txt | 80 +++++++++++++------ .../windows/driver/libvirtualhid.inf.in | 7 ++ src/platform/windows/windows_backend.cpp | 11 ++- 7 files changed, 77 insertions(+), 39 deletions(-) delete mode 100644 cmake/packaging/wix_resources/patch.xml diff --git a/cmake/packaging/windows.cmake b/cmake/packaging/windows.cmake index 60a6e7d..0f671c4 100644 --- a/cmake/packaging/windows.cmake +++ b/cmake/packaging/windows.cmake @@ -1,11 +1,15 @@ # windows specific packaging set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}") -set(CPACK_MONOLITHIC_INSTALL ON) if(NOT LIBVIRTUALHID_BUILD_WINDOWS_DRIVER) + set(CPACK_MONOLITHIC_INSTALL ON) return() endif() +set(CPACK_COMPONENTS_ALL driver) +set(CPACK_COMPONENTS_GROUPING ALL_COMPONENTS_IN_ONE) +set(CPACK_INSTALL_CMAKE_PROJECTS "${CMAKE_BINARY_DIR};${CMAKE_PROJECT_NAME};driver;/") + set(LIBVIRTUALHID_DRIVER_TEST_CERTIFICATE "" CACHE FILEPATH "Optional public test certificate to include in the Windows driver installer.") diff --git a/cmake/packaging/windows_wix.cmake b/cmake/packaging/windows_wix.cmake index 67667ce..576b316 100644 --- a/cmake/packaging/windows_wix.cmake +++ b/cmake/packaging/windows_wix.cmake @@ -42,15 +42,14 @@ set(CPACK_WIX_UPGRADE_GUID "71D7B738-9D83-4E57-82E3-C3106D9F8053") 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") +set(CPACK_WIX_EXTENSIONS + "${WIX_TOOL_PATH}/extensions/WixToolset.UI.wixext/${WIX_UI_VERSION}/wixext4/WixToolset.UI.wixext.dll") file(COPY "${CMAKE_CURRENT_LIST_DIR}/wix_resources/" DESTINATION "${WIX_BUILD_PARENT_DIRECTORY}/") set(CPACK_WIX_EXTRA_SOURCES "${WIX_BUILD_PARENT_DIRECTORY}/libvirtualhid-driver-installer.wxs") -set(CPACK_WIX_PATCH_FILE - "${WIX_BUILD_PARENT_DIRECTORY}/patch.xml") file(COPY "${CMAKE_SOURCE_DIR}/LICENSE" DESTINATION "${CMAKE_BINARY_DIR}") diff --git a/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs b/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs index c9f95b4..3c58c1d 100644 --- a/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs +++ b/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs @@ -31,7 +31,5 @@ - - diff --git a/cmake/packaging/wix_resources/patch.xml b/cmake/packaging/wix_resources/patch.xml deleted file mode 100644 index 65280ed..0000000 --- a/cmake/packaging/wix_resources/patch.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/platform/windows/driver/CMakeLists.txt b/src/platform/windows/driver/CMakeLists.txt index 5b6beb4..acbbc62 100644 --- a/src/platform/windows/driver/CMakeLists.txt +++ b/src/platform/windows/driver/CMakeLists.txt @@ -32,20 +32,26 @@ else() endif() set(LIBVIRTUALHID_DRIVER_VERSION "${PROJECT_VERSION}.0") -set(_lvh_wdk_roots - "$ENV{WDKContentRoot}" - "$ENV{WindowsSdkDir}" - "C:/Program Files (x86)/Windows Kits/10") - -set(_lvh_wdf_include_candidates) -set(_lvh_wdf_library_candidates) -set(_lvh_wdk_tool_candidates) -foreach(lvh_wdk_root IN LISTS _lvh_wdk_roots) +set(_lvh_wdk_roots) +foreach(lvh_wdk_root IN ITEMS + "$ENV{WDKContentRoot}" + "$ENV{WindowsSdkDir}" + "C:/Program Files (x86)/Windows Kits/10") if(NOT lvh_wdk_root) continue() endif() file(TO_CMAKE_PATH "${lvh_wdk_root}" lvh_wdk_root_cmake) + string(REGEX REPLACE "/+$" "" lvh_wdk_root_cmake "${lvh_wdk_root_cmake}") + list(APPEND _lvh_wdk_roots "${lvh_wdk_root_cmake}") +endforeach() +list(REMOVE_DUPLICATES _lvh_wdk_roots) + +set(_lvh_wdf_include_candidates) +set(_lvh_wdf_library_candidates) +set(_lvh_wdk_um_library_candidates) +set(_lvh_wdk_tool_candidates) +foreach(lvh_wdk_root_cmake IN LISTS _lvh_wdk_roots) if(EXISTS "${lvh_wdk_root_cmake}") file(GLOB _lvh_wdf_include_glob LIST_DIRECTORIES true @@ -53,6 +59,9 @@ foreach(lvh_wdk_root IN LISTS _lvh_wdk_roots) file(GLOB _lvh_wdf_library_glob LIST_DIRECTORIES true "${lvh_wdk_root_cmake}/Lib/wdf/umdf/${LIBVIRTUALHID_WDK_LIBRARY_ARCH}/2.*") + file(GLOB _lvh_wdk_um_library_glob + LIST_DIRECTORIES true + "${lvh_wdk_root_cmake}/Lib/*/um/${LIBVIRTUALHID_WDK_LIBRARY_ARCH}") file(GLOB _lvh_wdk_tool_glob LIST_DIRECTORIES true "${lvh_wdk_root_cmake}/bin/*/x64" @@ -61,6 +70,7 @@ foreach(lvh_wdk_root IN LISTS _lvh_wdk_roots) "${lvh_wdk_root_cmake}/bin/x86") list(APPEND _lvh_wdf_include_candidates ${_lvh_wdf_include_glob}) list(APPEND _lvh_wdf_library_candidates ${_lvh_wdf_library_glob}) + list(APPEND _lvh_wdk_um_library_candidates ${_lvh_wdk_um_library_glob}) list(APPEND _lvh_wdk_tool_candidates ${_lvh_wdk_tool_glob}) endif() endforeach() @@ -71,6 +81,9 @@ endif() if(_lvh_wdf_library_candidates) list(SORT _lvh_wdf_library_candidates COMPARE NATURAL ORDER DESCENDING) endif() +if(_lvh_wdk_um_library_candidates) + list(SORT _lvh_wdk_um_library_candidates COMPARE NATURAL ORDER DESCENDING) +endif() if(_lvh_wdk_tool_candidates) list(SORT _lvh_wdk_tool_candidates COMPARE NATURAL ORDER DESCENDING) list(REMOVE_DUPLICATES _lvh_wdk_tool_candidates) @@ -86,14 +99,23 @@ find_library(LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY PATHS ${_lvh_wdf_library_candidates} NO_DEFAULT_PATH) -if(NOT LIBVIRTUALHID_WDF_INCLUDE_DIR OR NOT LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY) +find_library(LIBVIRTUALHID_NTDLL_LIBRARY + NAMES ntdll + PATHS ${_lvh_wdk_um_library_candidates} + NO_DEFAULT_PATH) + +if(NOT LIBVIRTUALHID_WDF_INCLUDE_DIR + OR NOT LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY + OR NOT LIBVIRTUALHID_NTDLL_LIBRARY) message(FATAL_ERROR - "Could not find UMDF2 WDK headers/libraries. Install the Windows Driver Kit or set " - "LIBVIRTUALHID_WDF_INCLUDE_DIR and LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY.") + "Could not find UMDF2 WDK headers/libraries. Install the Windows Driver Kit or set the " + "LIBVIRTUALHID_WDF_INCLUDE_DIR, LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY, and " + "LIBVIRTUALHID_NTDLL_LIBRARY cache variables.") endif() message(STATUS "WDF include directory: ${LIBVIRTUALHID_WDF_INCLUDE_DIR}") message(STATUS "WDF UMDF stub library: ${LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY}") +message(STATUS "NTDLL import library: ${LIBVIRTUALHID_NTDLL_LIBRARY}") find_program(LIBVIRTUALHID_STAMPINF NAMES stampinf stampinf.exe @@ -132,31 +154,39 @@ target_compile_options(libvirtualhid_umdf PRIVATE /W4) if(LIBVIRTUALHID_WARNINGS_AS_ERRORS) target_compile_options(libvirtualhid_umdf PRIVATE /WX) endif() -target_link_libraries(libvirtualhid_umdf PRIVATE "${LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY}") +target_link_libraries(libvirtualhid_umdf + PRIVATE + "${LIBVIRTUALHID_WDF_DRIVER_STUB_UM_LIBRARY}" + "${LIBVIRTUALHID_NTDLL_LIBRARY}") set_target_properties(libvirtualhid_umdf PROPERTIES OUTPUT_NAME libvirtualhid_umdf RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/package") -add_custom_command(TARGET libvirtualhid_umdf POST_BUILD - COMMAND "${CMAKE_COMMAND}" -E make_directory "$" +add_custom_target(libvirtualhid_windows_inf + COMMAND "${CMAKE_COMMAND}" -E make_directory + "$" COMMAND "${CMAKE_COMMAND}" -E copy_if_different "${CMAKE_CURRENT_BINARY_DIR}/libvirtualhid.inf" "$/libvirtualhid.inf" - COMMENT "Copying libvirtualhid driver INF") - -add_custom_command(TARGET libvirtualhid_umdf POST_BUILD - COMMAND "${LIBVIRTUALHID_STAMPINF}" - -f "$/libvirtualhid.inf" + COMMAND "${CMAKE_COMMAND}" -E chdir "$" + "${LIBVIRTUALHID_STAMPINF}" + -f libvirtualhid.inf -d "*" -v "${LIBVIRTUALHID_DRIVER_VERSION}" - COMMENT "Stamping libvirtualhid.inf") + DEPENDS + libvirtualhid_umdf + "${CMAKE_CURRENT_BINARY_DIR}/libvirtualhid.inf" + COMMENT "Preparing libvirtualhid driver INF" + VERBATIM) add_custom_target(libvirtualhid_windows_catalog - COMMAND "${LIBVIRTUALHID_INF2CAT}" - /driver:"$" + COMMAND "${CMAKE_COMMAND}" -E chdir "$" + "${LIBVIRTUALHID_INF2CAT}" + /driver:. /os:${LIBVIRTUALHID_INF2CAT_OS} - DEPENDS libvirtualhid_umdf - COMMENT "Generating libvirtualhid driver catalog") + DEPENDS libvirtualhid_windows_inf + COMMENT "Generating libvirtualhid driver catalog" + VERBATIM) install(TARGETS libvirtualhid_umdf RUNTIME DESTINATION "drivers/windows" diff --git a/src/platform/windows/driver/libvirtualhid.inf.in b/src/platform/windows/driver/libvirtualhid.inf.in index 4d2155b..21bf4af 100644 --- a/src/platform/windows/driver/libvirtualhid.inf.in +++ b/src/platform/windows/driver/libvirtualhid.inf.in @@ -20,6 +20,12 @@ PnpLockdown=1 [DestinationDirs] UMDriverCopy=13 +[SourceDisksNames] +1=%DiskName%,,, + +[SourceDisksFiles] +libvirtualhid_umdf.dll=1 + [DeviceInstall.NT] CopyFiles=UMDriverCopy @@ -46,5 +52,6 @@ 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/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index 94561f9..9a4ee3b 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -12,7 +12,6 @@ // standard includes #include #include -#include #include #include #include @@ -120,8 +119,14 @@ namespace lvh::detail { } std::string resolve_control_device_path() { - if (const auto *path = std::getenv("LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE"); path != nullptr && path[0] != '\0') { - return path; + constexpr auto environment_name = "LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE"; + const auto required_size = ::GetEnvironmentVariableA(environment_name, nullptr, 0); + if (required_size > 1U) { + std::string path(required_size - 1U, '\0'); + const auto copied_size = ::GetEnvironmentVariableA(environment_name, path.data(), required_size); + if (copied_size > 0U && copied_size < required_size) { + return path; + } } return std::string {windows::default_control_device_path}; From fbb947c17e4fae395a48cae1ce045a1019d348bb Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 20 Jun 2026 21:07:47 -0400 Subject: [PATCH 3/5] Update WiX packaging and extension installation Refactor WiX packaging CMake: add WIX_BUILD_DIRECTORY and set a comment about UI extension versioning, always run dotnet tool install for WiX and fail on install errors, and add installation of the WixToolset.Util extension with error handling. Replace a hardcoded extension DLL path with logical extension names in CPACK_WIX_EXTENSIONS and emit the cpack package directory for debugging. These changes make extension installation more robust and simplify extension referencing for packaging. --- .github/workflows/ci.yml | 8 +++++++- cmake/packaging/windows_wix.cmake | 32 +++++++++++++++++++++---------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 110e352..e9edbf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -437,7 +437,13 @@ jobs: - name: Package Windows driver installer shell: pwsh run: | - cpack -G WIX --config .\cmake-build-driver\CPackConfig.cmake + Push-Location .\cmake-build-driver + cpack -G WIX + $packageExitCode = $LASTEXITCODE + Pop-Location + if ($packageExitCode -ne 0) { + exit $packageExitCode + } New-Item -ItemType Directory -Force -Path artifacts | Out-Null Copy-Item ` -LiteralPath .\cmake-build-driver\cpack_artifacts\libvirtualhid.msi ` diff --git a/cmake/packaging/windows_wix.cmake b/cmake/packaging/windows_wix.cmake index 576b316..8779038 100644 --- a/cmake/packaging/windows_wix.cmake +++ b/cmake/packaging/windows_wix.cmake @@ -10,21 +10,20 @@ endif() set(CPACK_WIX_VERSION 4) set(WIX_VERSION 4.0.4) -set(WIX_UI_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") +set(WIX_BUILD_DIRECTORY "${CPACK_PACKAGE_DIRECTORY}/_CPack_Packages/win64/WIX") set(WIX_TOOL_PATH "${CMAKE_BINARY_DIR}/.wix") file(MAKE_DIRECTORY ${WIX_TOOL_PATH}) -if(NOT EXISTS "${WIX_TOOL_PATH}/wix.exe") - execute_process( - COMMAND ${DOTNET_EXECUTABLE} tool install --tool-path ${WIX_TOOL_PATH} wix --version ${WIX_VERSION} - ERROR_VARIABLE WIX_INSTALL_OUTPUT - RESULT_VARIABLE WIX_INSTALL_RESULT) +execute_process( + COMMAND ${DOTNET_EXECUTABLE} tool install --tool-path ${WIX_TOOL_PATH} wix --version ${WIX_VERSION} + ERROR_VARIABLE WIX_INSTALL_OUTPUT + RESULT_VARIABLE WIX_INSTALL_RESULT) - if(NOT WIX_INSTALL_RESULT EQUAL 0) - message(FATAL_ERROR "Failed to install WiX tools locally: ${WIX_INSTALL_OUTPUT}") - endif() +if(NOT WIX_INSTALL_RESULT EQUAL 0) + message(FATAL_ERROR "Failed to install WiX tools locally: ${WIX_INSTALL_OUTPUT}") endif() execute_process( @@ -37,13 +36,26 @@ 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() + set(CPACK_WIX_ROOT "${WIX_TOOL_PATH}") set(CPACK_WIX_UPGRADE_GUID "71D7B738-9D83-4E57-82E3-C3106D9F8053") 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 - "${WIX_TOOL_PATH}/extensions/WixToolset.UI.wixext/${WIX_UI_VERSION}/wixext4/WixToolset.UI.wixext.dll") + "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}/") From db8e222a1b76f356e24077409a0691fff913de29 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:01:48 -0400 Subject: [PATCH 4/5] refactor: sonar fixes --- src/platform/windows/control_protocol.hpp | 47 +-- .../windows/driver/libvirtualhid_umdf.cpp | 108 ++++-- .../windows/shared/lvh_windows_protocol.h | 280 ++++++++++----- src/platform/windows/windows_backend.cpp | 324 ++++++++++-------- tests/unit/test_windows_protocol.cpp | 14 +- 5 files changed, 486 insertions(+), 287 deletions(-) diff --git a/src/platform/windows/control_protocol.hpp b/src/platform/windows/control_protocol.hpp index 1c613d2..d4b9eb0 100644 --- a/src/platform/windows/control_protocol.hpp +++ b/src/platform/windows/control_protocol.hpp @@ -50,11 +50,13 @@ namespace lvh::detail::windows { inline std::uint32_t protocol_bus_type(BusType bus_type) { switch (bus_type) { - case BusType::usb: + using enum BusType; + + case usb: return LVH_WINDOWS_BUS_USB; - case BusType::bluetooth: + case bluetooth: return LVH_WINDOWS_BUS_BLUETOOTH; - case BusType::unknown: + case unknown: return LVH_WINDOWS_BUS_UNKNOWN; } @@ -63,17 +65,19 @@ namespace lvh::detail::windows { inline std::uint32_t protocol_gamepad_kind(GamepadProfileKind kind) { switch (kind) { - case GamepadProfileKind::generic: + using enum GamepadProfileKind; + + case generic: return LVH_WINDOWS_GAMEPAD_GENERIC; - case GamepadProfileKind::xbox_360: + case xbox_360: return LVH_WINDOWS_GAMEPAD_XBOX_360; - case GamepadProfileKind::xbox_one: + case xbox_one: return LVH_WINDOWS_GAMEPAD_XBOX_ONE; - case GamepadProfileKind::xbox_series: + case xbox_series: return LVH_WINDOWS_GAMEPAD_XBOX_SERIES; - case GamepadProfileKind::dualsense: + case dualsense: return LVH_WINDOWS_GAMEPAD_DUALSENSE; - case GamepadProfileKind::switch_pro: + case switch_pro: return LVH_WINDOWS_GAMEPAD_SWITCH_PRO; } @@ -82,7 +86,7 @@ namespace lvh::detail::windows { template std::uint32_t copy_string(char (&target)[Size], std::string_view source) { - std::fill(std::begin(target), std::end(target), '\0'); + std::ranges::fill(target, '\0'); const auto copied = std::min(source.size(), Size - 1U); if (copied > 0U) { @@ -94,7 +98,7 @@ namespace lvh::detail::windows { template std::uint32_t copy_bytes(std::uint8_t (&target)[Size], const std::vector &source) { - std::fill(std::begin(target), std::end(target), std::uint8_t {}); + std::ranges::fill(target, std::uint8_t {}); const auto copied = std::min(source.size(), Size); if (copied > 0U) { @@ -115,20 +119,21 @@ namespace lvh::detail::windows { request.bus_type = protocol_bus_type(options.profile.bus_type); request.gamepad_kind = protocol_gamepad_kind(options.profile.gamepad_kind); request.flags = gamepad_flags(options.profile.capabilities); - request.vendor_id = options.profile.vendor_id; - request.product_id = options.profile.product_id; - request.device_version = options.profile.version; - request.report_id = options.profile.report_id; - request.input_report_size = static_cast( + request.hardware_ids.vendor_id = options.profile.vendor_id; + request.hardware_ids.product_id = options.profile.product_id; + request.hardware_ids.device_version = options.profile.version; + request.hardware_ids.report_id = options.profile.report_id; + request.report_sizes.input_report_size = static_cast( std::min(options.profile.input_report_size, static_cast(LVH_WINDOWS_MAX_INPUT_REPORT_SIZE)) ); - request.output_report_size = static_cast( + request.report_sizes.output_report_size = static_cast( std::min(options.profile.output_report_size, static_cast(LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE)) ); - request.report_descriptor_size = copy_bytes(request.report_descriptor, options.profile.report_descriptor); - request.name_size = copy_string(request.name, options.profile.name); - request.manufacturer_size = copy_string(request.manufacturer, options.profile.manufacturer); - request.stable_id_size = copy_string(request.stable_id, options.metadata.stable_id); + request.report_sizes.report_descriptor_size = + copy_bytes(request.report_descriptor, options.profile.report_descriptor); + request.report_sizes.name_size = copy_string(request.name, options.profile.name); + request.report_sizes.manufacturer_size = copy_string(request.manufacturer, options.profile.manufacturer); + request.report_sizes.stable_id_size = copy_string(request.stable_id, options.metadata.stable_id); return request; } diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp index 2abe726..b8fe3b3 100644 --- a/src/platform/windows/driver/libvirtualhid_umdf.cpp +++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp @@ -12,7 +12,7 @@ // platform includes #define WIN32_NO_STATUS -#include +#include #undef WIN32_NO_STATUS #if defined(_MSC_VER) @@ -28,10 +28,11 @@ #include #include #include -#include #include #include #include +#include +#include #include // local includes @@ -45,12 +46,18 @@ namespace { LvhWindowsCreateGamepadRequest request {}; }; - std::atomic next_driver_device_id {1}; - std::mutex devices_mutex; - std::map devices; + struct DriverState { + std::atomic next_driver_device_id {1}; + std::mutex devices_mutex; + std::map devices; + std::mutex output_requests_mutex; + std::vector pending_output_requests; + }; - std::mutex output_requests_mutex; - std::vector pending_output_requests; + DriverState &driver_state() { + static DriverState state; + return state; + } 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; @@ -61,16 +68,59 @@ namespace { } bool remove_pending_output_request(WDFREQUEST request) { - std::lock_guard lock {output_requests_mutex}; - const auto iter = std::find(pending_output_requests.begin(), pending_output_requests.end(), request); - if (iter == pending_output_requests.end()) { + auto &state = driver_state(); + std::lock_guard lock {state.output_requests_mutex}; + const auto iter = std::ranges::find(state.pending_output_requests, request); + if (iter == state.pending_output_requests.end()) { return false; } - pending_output_requests.erase(iter); + state.pending_output_requests.erase(iter); return true; } + template + NTSTATUS retrieve_input_buffer(WDFREQUEST request, ProtocolBuffer *&buffer) { + PVOID raw_buffer = nullptr; + const auto status = WdfRequestRetrieveInputBuffer(request, sizeof(ProtocolBuffer), &raw_buffer, nullptr); + buffer = static_cast(raw_buffer); + return status; + } + + template + NTSTATUS retrieve_output_buffer(WDFREQUEST request, ProtocolBuffer *&buffer) { + PVOID raw_buffer = nullptr; + const auto status = WdfRequestRetrieveOutputBuffer(request, sizeof(ProtocolBuffer), &raw_buffer, nullptr); + buffer = static_cast(raw_buffer); + return status; + } + + bool valid_create_request(const LvhWindowsCreateGamepadRequest &request) { + const auto descriptor_size = request.report_sizes.report_descriptor_size; + const auto input_report_size = request.report_sizes.input_report_size; + const auto output_report_size = request.report_sizes.output_report_size; + + return valid_header(request.version, request.size, sizeof(request)) && descriptor_size > 0U && + descriptor_size <= LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE && input_report_size > 0U && + input_report_size <= LVH_WINDOWS_MAX_INPUT_REPORT_SIZE && + output_report_size <= LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE; + } + + bool valid_submit_input_report_request(const LvhWindowsSubmitInputReportRequest &request) { + return valid_header(request.version, request.size, sizeof(request)) && request.report_size > 0U && + request.report_size <= LVH_WINDOWS_MAX_INPUT_REPORT_SIZE; + } + + void set_device_path(std::uint64_t driver_device_id, char (&device_path)[LVH_WINDOWS_MAX_DEVICE_PATH_SIZE]) { + std::ostringstream stream; + stream << LVH_WINDOWS_CONTROL_DEVICE_PATH << '#' << driver_device_id; + + const auto path = stream.str(); + const auto copied_size = std::min(path.size(), sizeof(device_path) - 1U); + std::memcpy(device_path, path.data(), copied_size); + device_path[copied_size] = '\0'; + } + } // namespace extern "C" DRIVER_INITIALIZE DriverEntry; @@ -131,14 +181,14 @@ void LvhEvtIoDeviceControl( case LVH_WINDOWS_IOCTL_CREATE_GAMEPAD: { auto *create_request = static_cast(nullptr); - auto status = WdfRequestRetrieveInputBuffer(request, sizeof(LvhWindowsCreateGamepadRequest), reinterpret_cast(&create_request), nullptr); + auto status = retrieve_input_buffer(request, create_request); if (!NT_SUCCESS(status)) { complete_request(request, status); return; } auto *create_response = static_cast(nullptr); - status = WdfRequestRetrieveOutputBuffer(request, sizeof(LvhWindowsCreateGamepadResponse), reinterpret_cast(&create_response), nullptr); + status = retrieve_output_buffer(request, create_response); if (!NT_SUCCESS(status)) { complete_request(request, status); return; @@ -148,21 +198,22 @@ void LvhEvtIoDeviceControl( create_response->version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; create_response->size = sizeof(*create_response); - if (!valid_header(create_request->version, create_request->size, sizeof(*create_request)) || create_request->report_descriptor_size == 0U || create_request->report_descriptor_size > LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE || create_request->input_report_size == 0U || create_request->input_report_size > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE || create_request->output_report_size > LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE) { + if (!valid_create_request(*create_request)) { create_response->status = LVH_WINDOWS_STATUS_INVALID_ARGUMENT; complete_request(request, STATUS_SUCCESS, sizeof(*create_response)); return; } - const auto driver_device_id = next_driver_device_id.fetch_add(1); + auto &state = driver_state(); + const auto driver_device_id = state.next_driver_device_id.fetch_add(1); { - std::lock_guard lock {devices_mutex}; - devices.emplace(driver_device_id, DeviceRecord {.request = *create_request}); + std::lock_guard lock {state.devices_mutex}; + state.devices.try_emplace(driver_device_id, DeviceRecord {.request = *create_request}); } create_response->status = LVH_WINDOWS_STATUS_SUCCESS; create_response->driver_device_id = driver_device_id; - std::snprintf(create_response->device_path, sizeof(create_response->device_path), "%s#%llu", LVH_WINDOWS_CONTROL_DEVICE_PATH, static_cast(driver_device_id)); + set_device_path(driver_device_id, create_response->device_path); complete_request(request, STATUS_SUCCESS, sizeof(*create_response)); return; } @@ -170,7 +221,7 @@ void LvhEvtIoDeviceControl( case LVH_WINDOWS_IOCTL_DESTROY_DEVICE: { auto *destroy_request = static_cast(nullptr); - const auto status = WdfRequestRetrieveInputBuffer(request, sizeof(LvhWindowsDestroyDeviceRequest), reinterpret_cast(&destroy_request), nullptr); + const auto status = retrieve_input_buffer(request, destroy_request); if (!NT_SUCCESS(status)) { complete_request(request, status); return; @@ -181,8 +232,9 @@ void LvhEvtIoDeviceControl( return; } - std::lock_guard lock {devices_mutex}; - devices.erase(destroy_request->driver_device_id); + auto &state = driver_state(); + std::lock_guard lock {state.devices_mutex}; + state.devices.erase(destroy_request->driver_device_id); complete_request(request, STATUS_SUCCESS); return; } @@ -190,19 +242,20 @@ void LvhEvtIoDeviceControl( case LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT: { auto *submit_request = static_cast(nullptr); - const auto status = WdfRequestRetrieveInputBuffer(request, sizeof(LvhWindowsSubmitInputReportRequest), reinterpret_cast(&submit_request), nullptr); + const auto status = retrieve_input_buffer(request, submit_request); if (!NT_SUCCESS(status)) { complete_request(request, status); return; } - if (!valid_header(submit_request->version, submit_request->size, sizeof(*submit_request)) || submit_request->report_size == 0U || submit_request->report_size > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE) { + if (!valid_submit_input_report_request(*submit_request)) { complete_request(request, STATUS_INVALID_PARAMETER); return; } - std::lock_guard lock {devices_mutex}; - if (devices.find(submit_request->driver_device_id) == devices.end()) { + auto &state = driver_state(); + std::lock_guard lock {state.devices_mutex}; + if (!state.devices.contains(submit_request->driver_device_id)) { complete_request(request, STATUS_OBJECT_NAME_NOT_FOUND); return; } @@ -213,14 +266,15 @@ void LvhEvtIoDeviceControl( case LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT: { - std::lock_guard lock {output_requests_mutex}; + auto &state = driver_state(); + std::lock_guard lock {state.output_requests_mutex}; const auto status = WdfRequestMarkCancelableEx(request, LvhEvtOutputReadCanceled); if (!NT_SUCCESS(status)) { complete_request(request, status); return; } - pending_output_requests.push_back(request); + state.pending_output_requests.push_back(request); return; } diff --git a/src/platform/windows/shared/lvh_windows_protocol.h b/src/platform/windows/shared/lvh_windows_protocol.h index 6043f42..74f4f55 100644 --- a/src/platform/windows/shared/lvh_windows_protocol.h +++ b/src/platform/windows/shared/lvh_windows_protocol.h @@ -6,108 +6,220 @@ #include -#define LVH_WINDOWS_CONTROL_PROTOCOL_VERSION 1u -#define LVH_WINDOWS_CONTROL_DEVICE_PATH "\\\\.\\LibVirtualHid" - -#define LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE 1024u -#define LVH_WINDOWS_MAX_INPUT_REPORT_SIZE 256u -#define LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE 256u -#define LVH_WINDOWS_MAX_DEVICE_PATH_SIZE 260u -#define LVH_WINDOWS_MAX_DEVICE_NAME_SIZE 128u -#define LVH_WINDOWS_MAX_MANUFACTURER_SIZE 128u -#define LVH_WINDOWS_MAX_STABLE_ID_SIZE 128u - -#define LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID 0x8000u -#define LVH_WINDOWS_METHOD_BUFFERED 0u -#define LVH_WINDOWS_FILE_ANY_ACCESS 0u -#define LVH_WINDOWS_FILE_READ_ACCESS 1u -#define LVH_WINDOWS_FILE_WRITE_ACCESS 2u -#define LVH_WINDOWS_CTL_CODE(device_type, function_code, method, access) \ - (((device_type) << 16u) | ((access) << 14u) | ((function_code) << 2u) | (method)) - -#define LVH_WINDOWS_IOCTL_CREATE_GAMEPAD \ - LVH_WINDOWS_CTL_CODE( \ - LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, \ - 0x800u, \ - LVH_WINDOWS_METHOD_BUFFERED, \ - LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS \ - ) -#define LVH_WINDOWS_IOCTL_DESTROY_DEVICE \ - LVH_WINDOWS_CTL_CODE( \ - LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, \ - 0x801u, \ - LVH_WINDOWS_METHOD_BUFFERED, \ - LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS \ - ) -#define LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT \ - LVH_WINDOWS_CTL_CODE( \ - LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, \ - 0x802u, \ - LVH_WINDOWS_METHOD_BUFFERED, \ - LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS \ - ) -#define LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT \ - LVH_WINDOWS_CTL_CODE( \ - LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, \ - 0x803u, \ - LVH_WINDOWS_METHOD_BUFFERED, \ - LVH_WINDOWS_FILE_READ_ACCESS \ - ) - -#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE 0x00000001u -#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION 0x00000002u -#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD 0x00000004u -#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RGB_LED 0x00000008u -#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY 0x00000010u -#define LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_ADAPTIVE_TRIGGERS 0x00000020u - #ifdef __cplusplus -extern "C" { -#endif - enum LvhWindowsProtocolStatus { - LVH_WINDOWS_STATUS_SUCCESS = 0, - LVH_WINDOWS_STATUS_INVALID_ARGUMENT = 1, - LVH_WINDOWS_STATUS_UNSUPPORTED_PROFILE = 2, - LVH_WINDOWS_STATUS_DEVICE_NOT_FOUND = 3, - LVH_WINDOWS_STATUS_BACKEND_FAILURE = 4, - }; +inline constexpr uint32_t LVH_WINDOWS_CONTROL_PROTOCOL_VERSION = 1u; +inline constexpr char LVH_WINDOWS_CONTROL_DEVICE_PATH[] = R"(\\.\LibVirtualHid)"; - enum LvhWindowsBusType { - LVH_WINDOWS_BUS_UNKNOWN = 0, - LVH_WINDOWS_BUS_USB = 1, - LVH_WINDOWS_BUS_BLUETOOTH = 2, - }; +inline constexpr uint32_t LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE = 1024u; +inline constexpr uint32_t LVH_WINDOWS_MAX_INPUT_REPORT_SIZE = 256u; +inline constexpr uint32_t LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE = 256u; +inline constexpr uint32_t LVH_WINDOWS_MAX_DEVICE_PATH_SIZE = 260u; +inline constexpr uint32_t LVH_WINDOWS_MAX_DEVICE_NAME_SIZE = 128u; +inline constexpr uint32_t LVH_WINDOWS_MAX_MANUFACTURER_SIZE = 128u; +inline constexpr uint32_t LVH_WINDOWS_MAX_STABLE_ID_SIZE = 128u; - enum LvhWindowsGamepadProfileKind { - LVH_WINDOWS_GAMEPAD_GENERIC = 0, - LVH_WINDOWS_GAMEPAD_XBOX_360 = 1, - LVH_WINDOWS_GAMEPAD_XBOX_ONE = 2, - LVH_WINDOWS_GAMEPAD_XBOX_SERIES = 3, - LVH_WINDOWS_GAMEPAD_DUALSENSE = 4, - LVH_WINDOWS_GAMEPAD_SWITCH_PRO = 5, - }; +inline constexpr uint32_t LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID = 0x8000u; +inline constexpr uint32_t LVH_WINDOWS_METHOD_BUFFERED = 0u; +inline constexpr uint32_t LVH_WINDOWS_FILE_READ_ACCESS = 1u; +inline constexpr uint32_t LVH_WINDOWS_FILE_WRITE_ACCESS = 2u; + +constexpr uint32_t lvh_windows_ctl_code( + uint32_t device_type, + uint32_t function_code, + uint32_t method, + uint32_t access +) noexcept { + return (device_type << 16u) | (access << 14u) | (function_code << 2u) | method; +} + +inline constexpr uint32_t LVH_WINDOWS_IOCTL_CREATE_GAMEPAD = lvh_windows_ctl_code( + LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, + 0x800u, + LVH_WINDOWS_METHOD_BUFFERED, + LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS +); +inline constexpr uint32_t LVH_WINDOWS_IOCTL_DESTROY_DEVICE = lvh_windows_ctl_code( + LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, + 0x801u, + LVH_WINDOWS_METHOD_BUFFERED, + LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS +); +inline constexpr uint32_t LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT = lvh_windows_ctl_code( + LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, + 0x802u, + LVH_WINDOWS_METHOD_BUFFERED, + LVH_WINDOWS_FILE_READ_ACCESS | LVH_WINDOWS_FILE_WRITE_ACCESS +); +inline constexpr uint32_t LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT = lvh_windows_ctl_code( + LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID, + 0x803u, + LVH_WINDOWS_METHOD_BUFFERED, + LVH_WINDOWS_FILE_READ_ACCESS +); + +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE = 0x00000001u; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION = 0x00000002u; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD = 0x00000004u; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RGB_LED = 0x00000008u; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY = 0x00000010u; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_ADAPTIVE_TRIGGERS = 0x00000020u; + +enum class LvhWindowsProtocolStatus : uint32_t { + success = 0, + invalid_argument = 1, + unsupported_profile = 2, + device_not_found = 3, + backend_failure = 4, +}; + +enum class LvhWindowsBusType : uint32_t { + unknown = 0, + usb = 1, + bluetooth = 2, +}; + +enum class LvhWindowsGamepadProfileKind : uint32_t { + generic = 0, + xbox_360 = 1, + xbox_one = 2, + xbox_series = 3, + dualsense = 4, + switch_pro = 5, +}; + +namespace lvh_windows_protocol_detail { + constexpr uint32_t to_uint32(auto value) noexcept { + return static_cast(value); + } + + using enum LvhWindowsProtocolStatus; + inline constexpr uint32_t status_success = to_uint32(success); + inline constexpr uint32_t status_invalid_argument = to_uint32(invalid_argument); + inline constexpr uint32_t status_unsupported_profile = to_uint32(unsupported_profile); + inline constexpr uint32_t status_device_not_found = to_uint32(device_not_found); + inline constexpr uint32_t status_backend_failure = to_uint32(backend_failure); + + using enum LvhWindowsBusType; + inline constexpr uint32_t bus_unknown = to_uint32(unknown); + inline constexpr uint32_t bus_usb = to_uint32(usb); + inline constexpr uint32_t bus_bluetooth = to_uint32(bluetooth); + + using enum LvhWindowsGamepadProfileKind; + inline constexpr uint32_t gamepad_generic = to_uint32(generic); + inline constexpr uint32_t gamepad_xbox_360 = to_uint32(xbox_360); + inline constexpr uint32_t gamepad_xbox_one = to_uint32(xbox_one); + inline constexpr uint32_t gamepad_xbox_series = to_uint32(xbox_series); + inline constexpr uint32_t gamepad_dualsense = to_uint32(dualsense); + inline constexpr uint32_t gamepad_switch_pro = to_uint32(switch_pro); +} // namespace lvh_windows_protocol_detail + +inline constexpr uint32_t LVH_WINDOWS_STATUS_SUCCESS = lvh_windows_protocol_detail::status_success; +inline constexpr uint32_t LVH_WINDOWS_STATUS_INVALID_ARGUMENT = + lvh_windows_protocol_detail::status_invalid_argument; +inline constexpr uint32_t LVH_WINDOWS_STATUS_UNSUPPORTED_PROFILE = + lvh_windows_protocol_detail::status_unsupported_profile; +inline constexpr uint32_t LVH_WINDOWS_STATUS_DEVICE_NOT_FOUND = lvh_windows_protocol_detail::status_device_not_found; +inline constexpr uint32_t LVH_WINDOWS_STATUS_BACKEND_FAILURE = lvh_windows_protocol_detail::status_backend_failure; + +inline constexpr uint32_t LVH_WINDOWS_BUS_UNKNOWN = lvh_windows_protocol_detail::bus_unknown; +inline constexpr uint32_t LVH_WINDOWS_BUS_USB = lvh_windows_protocol_detail::bus_usb; +inline constexpr uint32_t LVH_WINDOWS_BUS_BLUETOOTH = lvh_windows_protocol_detail::bus_bluetooth; + +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_GENERIC = lvh_windows_protocol_detail::gamepad_generic; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_XBOX_360 = lvh_windows_protocol_detail::gamepad_xbox_360; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_XBOX_ONE = lvh_windows_protocol_detail::gamepad_xbox_one; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_XBOX_SERIES = lvh_windows_protocol_detail::gamepad_xbox_series; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_DUALSENSE = lvh_windows_protocol_detail::gamepad_dualsense; +inline constexpr uint32_t LVH_WINDOWS_GAMEPAD_SWITCH_PRO = lvh_windows_protocol_detail::gamepad_switch_pro; + +#else + +enum { + LVH_WINDOWS_CONTROL_PROTOCOL_VERSION = 1u, + LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE = 1024u, + LVH_WINDOWS_MAX_INPUT_REPORT_SIZE = 256u, + LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE = 256u, + LVH_WINDOWS_MAX_DEVICE_PATH_SIZE = 260u, + LVH_WINDOWS_MAX_DEVICE_NAME_SIZE = 128u, + LVH_WINDOWS_MAX_MANUFACTURER_SIZE = 128u, + LVH_WINDOWS_MAX_STABLE_ID_SIZE = 128u, + LVH_WINDOWS_FILE_DEVICE_LIBVIRTUALHID = 0x8000u, + LVH_WINDOWS_METHOD_BUFFERED = 0u, + LVH_WINDOWS_FILE_READ_ACCESS = 1u, + LVH_WINDOWS_FILE_WRITE_ACCESS = 2u, + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE = 0x00000001u, + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION = 0x00000002u, + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD = 0x00000004u, + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RGB_LED = 0x00000008u, + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY = 0x00000010u, + LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_ADAPTIVE_TRIGGERS = 0x00000020u, +}; + +static const char LVH_WINDOWS_CONTROL_DEVICE_PATH[] = "\\\\.\\LibVirtualHid"; + +static const uint32_t LVH_WINDOWS_IOCTL_CREATE_GAMEPAD = 0x8000E000u; +static const uint32_t LVH_WINDOWS_IOCTL_DESTROY_DEVICE = 0x8000E004u; +static const uint32_t LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT = 0x8000E008u; +static const uint32_t LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT = 0x8000600Cu; + +enum LvhWindowsProtocolStatus { + LVH_WINDOWS_STATUS_SUCCESS = 0, + LVH_WINDOWS_STATUS_INVALID_ARGUMENT = 1, + LVH_WINDOWS_STATUS_UNSUPPORTED_PROFILE = 2, + LVH_WINDOWS_STATUS_DEVICE_NOT_FOUND = 3, + LVH_WINDOWS_STATUS_BACKEND_FAILURE = 4, +}; + +enum LvhWindowsBusType { + LVH_WINDOWS_BUS_UNKNOWN = 0, + LVH_WINDOWS_BUS_USB = 1, + LVH_WINDOWS_BUS_BLUETOOTH = 2, +}; + +enum LvhWindowsGamepadProfileKind { + LVH_WINDOWS_GAMEPAD_GENERIC = 0, + LVH_WINDOWS_GAMEPAD_XBOX_360 = 1, + LVH_WINDOWS_GAMEPAD_XBOX_ONE = 2, + LVH_WINDOWS_GAMEPAD_XBOX_SERIES = 3, + LVH_WINDOWS_GAMEPAD_DUALSENSE = 4, + LVH_WINDOWS_GAMEPAD_SWITCH_PRO = 5, +}; + +#endif + +#ifdef __cplusplus +extern "C" { +#endif #pragma pack(push, 1) - struct LvhWindowsCreateGamepadRequest { - uint32_t version; - uint32_t size; - uint64_t client_device_id; - uint32_t bus_type; - uint32_t gamepad_kind; - uint32_t flags; + struct LvhWindowsGamepadHardwareIds { uint16_t vendor_id; uint16_t product_id; uint16_t device_version; uint8_t report_id; uint8_t reserved0[7]; + }; + + struct LvhWindowsGamepadReportSizes { uint32_t input_report_size; uint32_t output_report_size; uint32_t report_descriptor_size; uint32_t name_size; uint32_t manufacturer_size; uint32_t stable_id_size; + }; + + struct LvhWindowsCreateGamepadRequest { + uint32_t version; + uint32_t size; + uint64_t client_device_id; + uint32_t bus_type; + uint32_t gamepad_kind; + uint32_t flags; + LvhWindowsGamepadHardwareIds hardware_ids; + LvhWindowsGamepadReportSizes report_sizes; uint8_t report_descriptor[LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE]; char name[LVH_WINDOWS_MAX_DEVICE_NAME_SIZE]; char manufacturer[LVH_WINDOWS_MAX_MANUFACTURER_SIZE]; diff --git a/src/platform/windows/windows_backend.cpp b/src/platform/windows/windows_backend.cpp index 9a4ee3b..9211a42 100644 --- a/src/platform/windows/windows_backend.cpp +++ b/src/platform/windows/windows_backend.cpp @@ -11,11 +11,14 @@ // standard includes #include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -30,98 +33,55 @@ #endif // platform includes -#include +#include namespace lvh::detail { namespace { class WindowsBackendContext; - class UniqueHandle { - public: - UniqueHandle() = default; - - explicit UniqueHandle(HANDLE handle): - handle_ {handle} {} - - UniqueHandle(const UniqueHandle &) = delete; - UniqueHandle &operator=(const UniqueHandle &) = delete; - - UniqueHandle(UniqueHandle &&other) noexcept: - handle_ {std::exchange(other.handle_, nullptr)} {} - - UniqueHandle &operator=(UniqueHandle &&other) noexcept { - if (this != &other) { - reset(std::exchange(other.handle_, nullptr)); - } - - return *this; - } - - ~UniqueHandle() { - reset(); - } - - void reset(HANDLE handle = nullptr) { - if (handle_ != nullptr && handle_ != INVALID_HANDLE_VALUE) { - static_cast(::CloseHandle(handle_)); - } - - handle_ = handle; - } - - HANDLE get() const { - return handle_; - } - - explicit operator bool() const { - return handle_ != nullptr && handle_ != INVALID_HANDLE_VALUE; - } + using UniqueHandle = std::unique_ptr; - private: - HANDLE handle_ {nullptr}; - }; + UniqueHandle make_unique_handle(HANDLE handle) { + return {handle, &::CloseHandle}; + } std::string windows_error_message(DWORD error_code) { - LPSTR message_buffer = nullptr; + std::array message_buffer {}; const auto message_size = ::FormatMessageA( - FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, + FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, nullptr, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), - reinterpret_cast(&message_buffer), - 0, + message_buffer.data(), + static_cast(message_buffer.size()), nullptr ); std::string message; - if (message_size > 0U && message_buffer != nullptr) { - message.assign(message_buffer, message_size); + if (message_size > 0U) { + message.assign(message_buffer.data(), message_size); while (!message.empty() && (message.back() == '\r' || message.back() == '\n')) { message.pop_back(); } } else { - message = "Windows error " + std::to_string(error_code); - } - - if (message_buffer != nullptr) { - ::LocalFree(message_buffer); + std::ostringstream fallback; + fallback << "Windows error " << error_code; + message = fallback.str(); } return message; } OperationStatus windows_failure(ErrorCode code, std::string_view operation, DWORD error_code) { - std::string message {operation}; - message += ": "; - message += windows_error_message(error_code); - return OperationStatus::failure(code, std::move(message)); + std::ostringstream message; + message << operation << ": " << windows_error_message(error_code); + return OperationStatus::failure(code, message.str()); } std::string resolve_control_device_path() { constexpr auto environment_name = "LIBVIRTUALHID_WINDOWS_CONTROL_DEVICE"; - const auto required_size = ::GetEnvironmentVariableA(environment_name, nullptr, 0); - if (required_size > 1U) { + if (const auto required_size = ::GetEnvironmentVariableA(environment_name, nullptr, 0); required_size > 1U) { std::string path(required_size - 1U, '\0'); const auto copied_size = ::GetEnvironmentVariableA(environment_name, path.data(), required_size); if (copied_size > 0U && copied_size < required_size) { @@ -133,30 +93,43 @@ namespace lvh::detail { } OperationStatus protocol_status(std::uint32_t status, std::string_view operation) { + using enum ErrorCode; + switch (status) { case LVH_WINDOWS_STATUS_SUCCESS: return OperationStatus::success(); case LVH_WINDOWS_STATUS_INVALID_ARGUMENT: - return OperationStatus::failure(ErrorCode::invalid_argument, std::string {operation}); + return OperationStatus::failure(invalid_argument, std::string {operation}); case LVH_WINDOWS_STATUS_UNSUPPORTED_PROFILE: - return OperationStatus::failure(ErrorCode::unsupported_profile, std::string {operation}); + return OperationStatus::failure(unsupported_profile, std::string {operation}); case LVH_WINDOWS_STATUS_DEVICE_NOT_FOUND: - return OperationStatus::failure(ErrorCode::device_closed, std::string {operation}); + return OperationStatus::failure(device_closed, std::string {operation}); case LVH_WINDOWS_STATUS_BACKEND_FAILURE: default: - return OperationStatus::failure(ErrorCode::backend_failure, std::string {operation}); + return OperationStatus::failure(backend_failure, std::string {operation}); } } OperationStatus validate_windows_gamepad_profile(const DeviceProfile &profile) { + using enum ErrorCode; + if (profile.report_descriptor.size() > LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE) { - return OperationStatus::failure(ErrorCode::invalid_argument, "Windows gamepad HID descriptor exceeds control protocol limit"); + return OperationStatus::failure( + invalid_argument, + "Windows gamepad HID descriptor exceeds control protocol limit" + ); } if (profile.input_report_size > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE) { - return OperationStatus::failure(ErrorCode::invalid_argument, "Windows gamepad input report exceeds control protocol limit"); + return OperationStatus::failure( + invalid_argument, + "Windows gamepad input report exceeds control protocol limit" + ); } if (profile.output_report_size > LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE) { - return OperationStatus::failure(ErrorCode::invalid_argument, "Windows gamepad output report exceeds control protocol limit"); + return OperationStatus::failure( + invalid_argument, + "Windows gamepad output report exceeds control protocol limit" + ); } return OperationStatus::success(); @@ -165,23 +138,21 @@ namespace lvh::detail { class WindowsControlChannel { public: static std::unique_ptr open(const std::string &path) { - UniqueHandle handle { - ::CreateFileA( - path.c_str(), - GENERIC_READ | GENERIC_WRITE, - FILE_SHARE_READ | FILE_SHARE_WRITE, - nullptr, - OPEN_EXISTING, - FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, - nullptr - ) - }; + const auto handle = ::CreateFileA( + path.c_str(), + GENERIC_READ | GENERIC_WRITE, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, + nullptr + ); - if (!handle) { + if (handle == INVALID_HANDLE_VALUE) { return nullptr; } - return std::unique_ptr {new WindowsControlChannel(path, std::move(handle))}; + return std::make_unique(path, make_unique_handle(handle)); } const std::string &path() const { @@ -189,57 +160,56 @@ namespace lvh::detail { } OperationStatus create_gamepad( - const LvhWindowsCreateGamepadRequest &request, + LvhWindowsCreateGamepadRequest request, LvhWindowsCreateGamepadResponse &response ) const { + using enum ErrorCode; + DWORD bytes_returned = 0; - const auto status = device_io_control( - LVH_WINDOWS_IOCTL_CREATE_GAMEPAD, - &request, - sizeof(request), - &response, - sizeof(response), - &bytes_returned, - "create Windows gamepad" - ); - if (!status.ok()) { + if (const auto status = device_io_control( + LVH_WINDOWS_IOCTL_CREATE_GAMEPAD, + request, + response, + &bytes_returned, + "create Windows gamepad" + ); + !status.ok()) { return status; } if (bytes_returned < sizeof(response)) { - return OperationStatus::failure(ErrorCode::backend_failure, "Windows driver returned a truncated gamepad response"); + return OperationStatus::failure(backend_failure, "Windows driver returned a truncated gamepad response"); } return protocol_status(response.status, "Windows driver rejected gamepad creation"); } OperationStatus destroy_device(std::uint64_t driver_device_id) const { - const auto request = windows::make_destroy_device_request(driver_device_id); + auto request = windows::make_destroy_device_request(driver_device_id); DWORD bytes_returned = 0; return device_io_control( LVH_WINDOWS_IOCTL_DESTROY_DEVICE, - &request, - sizeof(request), - nullptr, - 0, + request, &bytes_returned, "destroy Windows virtual HID device" ); } - OperationStatus submit_input_report(std::uint64_t driver_device_id, const std::vector &report) const { + OperationStatus submit_input_report( + std::uint64_t driver_device_id, + const std::vector &report + ) const { + using enum ErrorCode; + if (report.size() > LVH_WINDOWS_MAX_INPUT_REPORT_SIZE) { - return OperationStatus::failure(ErrorCode::invalid_argument, "input report exceeds Windows control protocol limit"); + return OperationStatus::failure(invalid_argument, "input report exceeds Windows control protocol limit"); } - const auto request = windows::make_submit_input_report_request(driver_device_id, report); + auto request = windows::make_submit_input_report_request(driver_device_id, report); DWORD bytes_returned = 0; return device_io_control( LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT, - &request, - sizeof(request), - nullptr, - 0, + request, &bytes_returned, "submit Windows input report" ); @@ -250,7 +220,7 @@ namespace lvh::detail { event.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; event.size = sizeof(event); - UniqueHandle operation_event {::CreateEventA(nullptr, TRUE, FALSE, nullptr)}; + auto operation_event = make_unique_handle(::CreateEventA(nullptr, TRUE, FALSE, nullptr)); if (!operation_event) { return std::nullopt; } @@ -259,28 +229,31 @@ namespace lvh::detail { overlapped.hEvent = operation_event.get(); DWORD bytes_returned = 0; - const auto started = ::DeviceIoControl( - handle_.get(), - LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT, - nullptr, - 0, - &event, - sizeof(event), - &bytes_returned, - &overlapped - ); - - if (started == FALSE) { - const auto error_code = ::GetLastError(); - if (error_code != ERROR_IO_PENDING) { + 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; } - HANDLE wait_handles[] { + std::array wait_handles { operation_event.get(), stop_event, }; - const auto wait_result = ::WaitForMultipleObjects(2, wait_handles, FALSE, INFINITE); + const auto wait_result = ::WaitForMultipleObjects( + static_cast(wait_handles.size()), + wait_handles.data(), + FALSE, + INFINITE + ); if (wait_result == WAIT_OBJECT_0 + 1U) { static_cast(::CancelIoEx(handle_.get(), &overlapped)); return std::nullopt; @@ -295,7 +268,9 @@ namespace lvh::detail { return std::nullopt; } - if (bytes_returned < sizeof(event.version) + sizeof(event.size) + sizeof(event.driver_device_id) + sizeof(event.report_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; } @@ -303,22 +278,57 @@ namespace lvh::detail { return event; } - private: WindowsControlChannel(std::string path, UniqueHandle handle): path_ {std::move(path)}, handle_ {std::move(handle)} {} + private: + template + OperationStatus device_io_control( + DWORD control_code, + Input &input, + Output &output, + DWORD *bytes_returned, + std::string_view operation + ) const { + using enum ErrorCode; + + if (::DeviceIoControl( + handle_.get(), + control_code, + &input, + sizeof(input), + &output, + sizeof(output), + bytes_returned, + nullptr + ) == FALSE) { + return windows_failure(backend_failure, operation, ::GetLastError()); + } + + return OperationStatus::success(); + } + + template OperationStatus device_io_control( DWORD control_code, - const void *input, - DWORD input_size, - void *output, - DWORD output_size, + Input &input, DWORD *bytes_returned, std::string_view operation ) const { - if (::DeviceIoControl(handle_.get(), control_code, const_cast(input), input_size, output, output_size, bytes_returned, nullptr) == FALSE) { - return windows_failure(ErrorCode::backend_failure, operation, ::GetLastError()); + using enum ErrorCode; + + if (::DeviceIoControl( + handle_.get(), + control_code, + &input, + sizeof(input), + nullptr, + 0, + bytes_returned, + nullptr + ) == FALSE) { + return windows_failure(backend_failure, operation, ::GetLastError()); } return OperationStatus::success(); @@ -328,7 +338,8 @@ namespace lvh::detail { UniqueHandle handle_; }; - struct WindowsGamepadState { + class WindowsGamepadState { + public: WindowsGamepadState( DeviceId client_device_id, std::uint64_t driver_device_id, @@ -340,7 +351,11 @@ namespace lvh::detail { profile {std::move(device_profile)}, path {std::move(device_path)} {} - mutable std::mutex mutex; + private: + friend class WindowsBackendContext; + friend class WindowsGamepad; + + mutable std::mutex mutex_; DeviceId client_id; std::uint64_t driver_id; DeviceProfile profile; @@ -372,8 +387,7 @@ namespace lvh::detail { std::unique_ptr event_channel ): command_channel_ {std::move(command_channel)}, - event_channel_ {std::move(event_channel)}, - stop_event_ {::CreateEventA(nullptr, TRUE, FALSE, nullptr)} {} + event_channel_ {std::move(event_channel)} {} WindowsBackendContext(const WindowsBackendContext &) = delete; WindowsBackendContext &operator=(const WindowsBackendContext &) = delete; @@ -413,8 +427,7 @@ namespace lvh::detail { response.version = LVH_WINDOWS_CONTROL_PROTOCOL_VERSION; response.size = sizeof(response); - const auto status = command_channel_->create_gamepad(request, response); - if (!status.ok()) { + if (const auto status = command_channel_->create_gamepad(request, response); !status.ok()) { return {status, nullptr}; } @@ -434,7 +447,10 @@ namespace lvh::detail { return {OperationStatus::success(), std::move(gamepad)}; } - OperationStatus submit_gamepad_report(const std::shared_ptr &state, const std::vector &report) { + OperationStatus submit_gamepad_report( + const std::shared_ptr &state, + const std::vector &report + ) const { const auto driver_id = state->driver_id; return command_channel_->submit_input_report(driver_id, report); } @@ -442,7 +458,7 @@ namespace lvh::detail { OperationStatus close_gamepad(const std::shared_ptr &state) { std::uint64_t driver_id = 0; { - std::lock_guard lock {state->mutex}; + std::lock_guard lock {state->mutex_}; if (!state->open) { return OperationStatus::success(); } @@ -487,7 +503,7 @@ namespace lvh::detail { DeviceProfile profile; OutputCallback callback; { - std::lock_guard lock {state->mutex}; + std::lock_guard lock {state->mutex_}; if (!state->open || !state->output_callback) { return; } @@ -507,17 +523,19 @@ namespace lvh::detail { std::unique_ptr command_channel_; std::unique_ptr event_channel_; - UniqueHandle stop_event_; + UniqueHandle stop_event_ {make_unique_handle(::CreateEventA(nullptr, TRUE, FALSE, nullptr))}; std::jthread output_thread_; std::mutex devices_mutex_; std::map> gamepads_; }; OperationStatus WindowsGamepad::submit(const std::vector &report) { + using enum ErrorCode; + { - std::lock_guard lock {state_->mutex}; + std::lock_guard lock {state_->mutex_}; if (!state_->open) { - return OperationStatus::failure(ErrorCode::device_closed, "Windows gamepad is closed"); + return OperationStatus::failure(device_closed, "Windows gamepad is closed"); } } @@ -525,12 +543,12 @@ namespace lvh::detail { } void WindowsGamepad::set_output_callback(OutputCallback callback) { - std::lock_guard lock {state_->mutex}; + std::lock_guard lock {state_->mutex_}; state_->output_callback = std::move(callback); } std::vector WindowsGamepad::device_nodes() const { - std::lock_guard lock {state_->mutex}; + std::lock_guard lock {state_->mutex_}; if (state_->path.empty()) { return {}; } @@ -577,6 +595,8 @@ namespace lvh::detail { } BackendGamepadCreationResult create_gamepad(DeviceId id, const CreateGamepadOptions &options) override { + using enum ErrorCode; + if (const auto validation = validate_windows_gamepad_profile(options.profile); !validation.ok()) { return {validation, nullptr}; } @@ -584,7 +604,7 @@ namespace lvh::detail { if (!context_) { return { OperationStatus::failure( - ErrorCode::backend_unavailable, + backend_unavailable, "Windows UMDF control device is unavailable; install the libvirtualhid driver package" ), nullptr, @@ -594,7 +614,10 @@ namespace lvh::detail { return context_->create_gamepad(id, options); } - BackendKeyboardCreationResult create_keyboard(DeviceId /*id*/, const CreateKeyboardOptions & /*options*/) override { + BackendKeyboardCreationResult create_keyboard( + DeviceId /*id*/, + const CreateKeyboardOptions & /*options*/ + ) override { return {unsupported_device_status(), nullptr}; } @@ -609,7 +632,10 @@ namespace lvh::detail { return {unsupported_device_status(), nullptr}; } - BackendTrackpadCreationResult create_trackpad(DeviceId /*id*/, const CreateTrackpadOptions & /*options*/) override { + BackendTrackpadCreationResult create_trackpad( + DeviceId /*id*/, + const CreateTrackpadOptions & /*options*/ + ) override { return {unsupported_device_status(), nullptr}; } @@ -622,8 +648,10 @@ namespace lvh::detail { private: static OperationStatus unsupported_device_status() { + using enum ErrorCode; + return OperationStatus::failure( - ErrorCode::unsupported_profile, + unsupported_profile, "Windows MVP backend currently supports gamepad devices only" ); } diff --git a/tests/unit/test_windows_protocol.cpp b/tests/unit/test_windows_protocol.cpp index dd97bac..e300a50 100644 --- a/tests/unit/test_windows_protocol.cpp +++ b/tests/unit/test_windows_protocol.cpp @@ -26,13 +26,13 @@ TEST(WindowsProtocolTest, PacksGamepadCreateRequest) { EXPECT_EQ(request.client_device_id, 42U); EXPECT_EQ(request.bus_type, LVH_WINDOWS_BUS_BLUETOOTH); EXPECT_EQ(request.gamepad_kind, LVH_WINDOWS_GAMEPAD_DUALSENSE); - EXPECT_EQ(request.vendor_id, options.profile.vendor_id); - EXPECT_EQ(request.product_id, options.profile.product_id); - EXPECT_EQ(request.device_version, options.profile.version); - EXPECT_EQ(request.report_id, options.profile.report_id); - EXPECT_EQ(request.input_report_size, options.profile.input_report_size); - EXPECT_EQ(request.output_report_size, options.profile.output_report_size); - EXPECT_EQ(request.report_descriptor_size, options.profile.report_descriptor.size()); + EXPECT_EQ(request.hardware_ids.vendor_id, options.profile.vendor_id); + EXPECT_EQ(request.hardware_ids.product_id, options.profile.product_id); + EXPECT_EQ(request.hardware_ids.device_version, options.profile.version); + EXPECT_EQ(request.hardware_ids.report_id, options.profile.report_id); + EXPECT_EQ(request.report_sizes.input_report_size, options.profile.input_report_size); + EXPECT_EQ(request.report_sizes.output_report_size, options.profile.output_report_size); + EXPECT_EQ(request.report_sizes.report_descriptor_size, options.profile.report_descriptor.size()); EXPECT_EQ(request.report_descriptor[0], options.profile.report_descriptor[0]); EXPECT_STREQ(request.name, options.profile.name.c_str()); EXPECT_STREQ(request.manufacturer, options.profile.manufacturer.c_str()); From 8d360c58ba186825ac8cc6432b2812c79d4007e0 Mon Sep 17 00:00:00 2001 From: ReenigneArcher <42013603+ReenigneArcher@users.noreply.github.com> Date: Sat, 20 Jun 2026 22:45:30 -0400 Subject: [PATCH 5/5] test: improve coverage --- tests/unit/test_runtime.cpp | 21 ++++ tests/unit/test_windows_protocol.cpp | 169 +++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/tests/unit/test_runtime.cpp b/tests/unit/test_runtime.cpp index ec77d92..b2e0778 100644 --- a/tests/unit/test_runtime.cpp +++ b/tests/unit/test_runtime.cpp @@ -8,6 +8,9 @@ // local includes #include "fixtures/fixtures.hpp" +#if defined(_WIN32) + #include "platform/windows/shared/lvh_windows_protocol.h" +#endif /** * @brief Test fixture for Linux runtime integration tests. @@ -48,11 +51,29 @@ TEST(RuntimeTest, PlatformDefaultReportsCurrentPlatformCapabilities) { EXPECT_FALSE(runtime->capabilities().supports_trackpad); EXPECT_FALSE(runtime->capabilities().supports_pen_tablet); + EXPECT_EQ(runtime->create_keyboard().status.code(), lvh::ErrorCode::unsupported_profile); + EXPECT_EQ(runtime->create_mouse().status.code(), lvh::ErrorCode::unsupported_profile); + EXPECT_EQ(runtime->create_touchscreen().status.code(), lvh::ErrorCode::unsupported_profile); + EXPECT_EQ(runtime->create_trackpad().status.code(), lvh::ErrorCode::unsupported_profile); + EXPECT_EQ(runtime->create_pen_tablet().status.code(), lvh::ErrorCode::unsupported_profile); + if (!runtime->capabilities().supports_gamepad) { auto created = runtime->create_gamepad(lvh::profiles::xbox_360()); EXPECT_FALSE(created); EXPECT_EQ(created.status.code(), lvh::ErrorCode::backend_unavailable); } + + auto invalid_profile = lvh::profiles::xbox_360(); + 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.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.output_report_size = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE + 1U; + EXPECT_EQ(runtime->create_gamepad(invalid_profile).status.code(), lvh::ErrorCode::invalid_argument); #else EXPECT_EQ(runtime->capabilities().backend_name, "platform-default-unimplemented"); EXPECT_FALSE(runtime->capabilities().supports_gamepad); diff --git a/tests/unit/test_windows_protocol.cpp b/tests/unit/test_windows_protocol.cpp index e300a50..adfbc0f 100644 --- a/tests/unit/test_windows_protocol.cpp +++ b/tests/unit/test_windows_protocol.cpp @@ -9,11 +9,118 @@ // standard includes #include +#include +#include #include // lib includes #include +namespace { + + lvh::DeviceProfile minimal_gamepad_profile() { + lvh::DeviceProfile profile; + profile.device_type = lvh::DeviceType::gamepad; + profile.gamepad_kind = lvh::GamepadProfileKind::generic; + profile.bus_type = lvh::BusType::usb; + profile.vendor_id = 0x1209; + profile.product_id = 0x0001; + profile.version = 0x0001; + profile.report_id = 1; + profile.input_report_size = 4; + profile.output_report_size = 2; + profile.name = "test gamepad"; + profile.manufacturer = "test manufacturer"; + profile.report_descriptor = {0x05, 0x01, 0x09, 0x05}; + return profile; + } + +} // namespace + +TEST(WindowsProtocolTest, ExposesStableProtocolConstants) { + EXPECT_STREQ(lvh::detail::windows::default_control_device_path.data(), R"(\\.\LibVirtualHid)"); + + EXPECT_EQ(LVH_WINDOWS_IOCTL_CREATE_GAMEPAD, 0x8000E000U); + EXPECT_EQ(LVH_WINDOWS_IOCTL_DESTROY_DEVICE, 0x8000E004U); + EXPECT_EQ(LVH_WINDOWS_IOCTL_SUBMIT_INPUT_REPORT, 0x8000E008U); + EXPECT_EQ(LVH_WINDOWS_IOCTL_READ_OUTPUT_REPORT, 0x8000600CU); + + EXPECT_EQ(sizeof(LvhWindowsGamepadHardwareIds), 14U); + EXPECT_EQ(sizeof(LvhWindowsGamepadReportSizes), 24U); + EXPECT_EQ(sizeof(LvhWindowsCreateGamepadRequest), 1474U); + EXPECT_EQ(sizeof(LvhWindowsCreateGamepadResponse), 284U); + EXPECT_EQ(sizeof(LvhWindowsDestroyDeviceRequest), 16U); + EXPECT_EQ(sizeof(LvhWindowsSubmitInputReportRequest), 280U); + EXPECT_EQ(sizeof(LvhWindowsOutputReportEvent), 280U); +} + +TEST(WindowsProtocolTest, MapsBusTypesAndGamepadKinds) { + EXPECT_EQ(lvh::detail::windows::protocol_bus_type(lvh::BusType::unknown), LVH_WINDOWS_BUS_UNKNOWN); + EXPECT_EQ(lvh::detail::windows::protocol_bus_type(lvh::BusType::usb), LVH_WINDOWS_BUS_USB); + EXPECT_EQ(lvh::detail::windows::protocol_bus_type(lvh::BusType::bluetooth), LVH_WINDOWS_BUS_BLUETOOTH); + + EXPECT_EQ( + lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::generic), + LVH_WINDOWS_GAMEPAD_GENERIC + ); + EXPECT_EQ( + lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::xbox_360), + LVH_WINDOWS_GAMEPAD_XBOX_360 + ); + EXPECT_EQ( + lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::xbox_one), + LVH_WINDOWS_GAMEPAD_XBOX_ONE + ); + EXPECT_EQ( + lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::xbox_series), + LVH_WINDOWS_GAMEPAD_XBOX_SERIES + ); + EXPECT_EQ( + lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::dualsense), + LVH_WINDOWS_GAMEPAD_DUALSENSE + ); + EXPECT_EQ( + lvh::detail::windows::protocol_gamepad_kind(lvh::GamepadProfileKind::switch_pro), + LVH_WINDOWS_GAMEPAD_SWITCH_PRO + ); +} + +TEST(WindowsProtocolTest, BuildsCapabilityFlags) { + lvh::GamepadProfileCapabilities capabilities; + EXPECT_EQ(lvh::detail::windows::gamepad_flags(capabilities), 0U); + + capabilities.supports_rumble = true; + capabilities.supports_motion = true; + capabilities.supports_touchpad = true; + capabilities.supports_rgb_led = true; + capabilities.supports_battery = true; + capabilities.supports_adaptive_triggers = true; + + const auto flags = lvh::detail::windows::gamepad_flags(capabilities); + EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RUMBLE, 0U); + EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_MOTION, 0U); + EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_TOUCHPAD, 0U); + EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_RGB_LED, 0U); + EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY, 0U); + EXPECT_NE(flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_ADAPTIVE_TRIGGERS, 0U); +} + +TEST(WindowsProtocolTest, CopyHelpersTruncateAndZeroFill) { + char text[5] {'x', 'x', 'x', 'x', 'x'}; + EXPECT_EQ(lvh::detail::windows::copy_string(text, "abcdef"), 4U); + EXPECT_EQ(std::memcmp(text, "abcd", 4), 0); + EXPECT_EQ(text[4], '\0'); + + std::uint8_t bytes[5] {0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; + const std::vector source {1, 2}; + EXPECT_EQ(lvh::detail::windows::copy_bytes(bytes, source), 2U); + EXPECT_EQ(bytes[0], 1U); + EXPECT_EQ(bytes[1], 2U); + EXPECT_EQ(bytes[2], 0U); + EXPECT_EQ(bytes[3], 0U); + EXPECT_EQ(bytes[4], 0U); +} + TEST(WindowsProtocolTest, PacksGamepadCreateRequest) { lvh::CreateGamepadOptions options; options.profile = lvh::profiles::dualsense_bluetooth(); @@ -43,6 +150,54 @@ TEST(WindowsProtocolTest, PacksGamepadCreateRequest) { EXPECT_NE(request.flags & LVH_WINDOWS_GAMEPAD_FLAG_SUPPORTS_BATTERY, 0U); } +TEST(WindowsProtocolTest, PacksGenericUnknownBusGamepadCreateRequestWithoutOptionalFlags) { + lvh::CreateGamepadOptions options; + options.profile = minimal_gamepad_profile(); + options.profile.bus_type = lvh::BusType::unknown; + options.profile.output_report_size = 0; + + const auto request = lvh::detail::windows::make_create_gamepad_request(7, options); + + EXPECT_EQ(request.bus_type, LVH_WINDOWS_BUS_UNKNOWN); + EXPECT_EQ(request.gamepad_kind, LVH_WINDOWS_GAMEPAD_GENERIC); + EXPECT_EQ(request.flags, 0U); + EXPECT_EQ(request.report_sizes.output_report_size, 0U); + EXPECT_EQ(request.report_sizes.name_size, options.profile.name.size()); + EXPECT_EQ(request.report_sizes.manufacturer_size, options.profile.manufacturer.size()); + EXPECT_EQ(request.report_sizes.stable_id_size, 0U); +} + +TEST(WindowsProtocolTest, TruncatesOversizedGamepadCreateRequestFields) { + lvh::CreateGamepadOptions options; + options.profile = minimal_gamepad_profile(); + options.profile.input_report_size = LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 1U; + options.profile.output_report_size = LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE + 1U; + options.profile.report_descriptor.assign(LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE + 5U, 0xAB); + options.profile.name.assign(LVH_WINDOWS_MAX_DEVICE_NAME_SIZE + 5U, 'n'); + options.profile.manufacturer.assign(LVH_WINDOWS_MAX_MANUFACTURER_SIZE + 5U, 'm'); + options.metadata.stable_id.assign(LVH_WINDOWS_MAX_STABLE_ID_SIZE + 5U, 's'); + + const auto request = lvh::detail::windows::make_create_gamepad_request(9, options); + + EXPECT_EQ(request.report_sizes.input_report_size, LVH_WINDOWS_MAX_INPUT_REPORT_SIZE); + EXPECT_EQ(request.report_sizes.output_report_size, LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE); + EXPECT_EQ(request.report_sizes.report_descriptor_size, LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE); + EXPECT_EQ(request.report_descriptor[0], 0xABU); + EXPECT_EQ(request.report_descriptor[LVH_WINDOWS_MAX_REPORT_DESCRIPTOR_SIZE - 1U], 0xABU); + + EXPECT_EQ(request.report_sizes.name_size, LVH_WINDOWS_MAX_DEVICE_NAME_SIZE - 1U); + EXPECT_EQ(request.name[LVH_WINDOWS_MAX_DEVICE_NAME_SIZE - 2U], 'n'); + EXPECT_EQ(request.name[LVH_WINDOWS_MAX_DEVICE_NAME_SIZE - 1U], '\0'); + + EXPECT_EQ(request.report_sizes.manufacturer_size, LVH_WINDOWS_MAX_MANUFACTURER_SIZE - 1U); + EXPECT_EQ(request.manufacturer[LVH_WINDOWS_MAX_MANUFACTURER_SIZE - 2U], 'm'); + EXPECT_EQ(request.manufacturer[LVH_WINDOWS_MAX_MANUFACTURER_SIZE - 1U], '\0'); + + EXPECT_EQ(request.report_sizes.stable_id_size, LVH_WINDOWS_MAX_STABLE_ID_SIZE - 1U); + EXPECT_EQ(request.stable_id[LVH_WINDOWS_MAX_STABLE_ID_SIZE - 2U], 's'); + EXPECT_EQ(request.stable_id[LVH_WINDOWS_MAX_STABLE_ID_SIZE - 1U], '\0'); +} + TEST(WindowsProtocolTest, PacksSubmitAndDestroyRequests) { const std::vector report {1, 2, 3, 4, 5}; @@ -59,3 +214,17 @@ TEST(WindowsProtocolTest, PacksSubmitAndDestroyRequests) { EXPECT_EQ(destroy.size, sizeof(destroy)); EXPECT_EQ(destroy.driver_device_id, 17U); } + +TEST(WindowsProtocolTest, SubmitInputReportTruncatesAndZeroFills) { + std::vector oversized_report(LVH_WINDOWS_MAX_INPUT_REPORT_SIZE + 3U, 0x7F); + const auto oversized = lvh::detail::windows::make_submit_input_report_request(19, oversized_report); + + EXPECT_EQ(oversized.report_size, LVH_WINDOWS_MAX_INPUT_REPORT_SIZE); + EXPECT_EQ(oversized.report[0], 0x7FU); + EXPECT_EQ(oversized.report[LVH_WINDOWS_MAX_INPUT_REPORT_SIZE - 1U], 0x7FU); + + const auto empty = lvh::detail::windows::make_submit_input_report_request(19, {}); + EXPECT_EQ(empty.report_size, 0U); + EXPECT_EQ(empty.report[0], 0U); + EXPECT_EQ(empty.report[LVH_WINDOWS_MAX_INPUT_REPORT_SIZE - 1U], 0U); +}