diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 74a6c27..e9edbf5 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,127 @@ 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: |
+ 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 `
+ -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 +551,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 +591,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 +605,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..0f671c4
--- /dev/null
+++ b/cmake/packaging/windows.cmake
@@ -0,0 +1,34 @@
+# windows specific packaging
+set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_NAME}")
+
+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.")
+
+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..8779038
--- /dev/null
+++ b/cmake/packaging/windows_wix.cmake
@@ -0,0 +1,75 @@
+# 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) # 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})
+
+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()
+
+execute_process(
+ COMMAND "${WIX_TOOL_PATH}/wix" extension add WixToolset.UI.wixext/${WIX_UI_VERSION}
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+ ERROR_VARIABLE WIX_UI_INSTALL_OUTPUT
+ RESULT_VARIABLE WIX_UI_INSTALL_RESULT)
+
+if(NOT WIX_UI_INSTALL_RESULT EQUAL 0)
+ message(FATAL_ERROR "Failed to install WiX UI extension: ${WIX_UI_INSTALL_OUTPUT}")
+endif()
+
+execute_process(
+ COMMAND "${WIX_TOOL_PATH}/wix" extension add WixToolset.Util.wixext/${WIX_UI_VERSION}
+ WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+ ERROR_VARIABLE WIX_UTIL_INSTALL_OUTPUT
+ RESULT_VARIABLE WIX_UTIL_INSTALL_RESULT)
+
+if(NOT WIX_UTIL_INSTALL_RESULT EQUAL 0)
+ message(FATAL_ERROR "Failed to install WiX Util extension: ${WIX_UTIL_INSTALL_OUTPUT}")
+endif()
+
+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"
+ "WixToolset.Util.wixext")
+
+message(STATUS "cpack package directory: ${CPACK_PACKAGE_DIRECTORY}")
+
+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")
+
+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..3c58c1d
--- /dev/null
+++ b/cmake/packaging/wix_resources/libvirtualhid-driver-installer.wxs
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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..d4b9eb0
--- /dev/null
+++ b/src/platform/windows/control_protocol.hpp
@@ -0,0 +1,163 @@
+/**
+ * @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) {
+ using enum BusType;
+
+ case usb:
+ return LVH_WINDOWS_BUS_USB;
+ case bluetooth:
+ return LVH_WINDOWS_BUS_BLUETOOTH;
+ case unknown:
+ return LVH_WINDOWS_BUS_UNKNOWN;
+ }
+
+ return LVH_WINDOWS_BUS_UNKNOWN;
+ }
+
+ inline std::uint32_t protocol_gamepad_kind(GamepadProfileKind kind) {
+ switch (kind) {
+ using enum GamepadProfileKind;
+
+ case generic:
+ return LVH_WINDOWS_GAMEPAD_GENERIC;
+ case xbox_360:
+ return LVH_WINDOWS_GAMEPAD_XBOX_360;
+ case xbox_one:
+ return LVH_WINDOWS_GAMEPAD_XBOX_ONE;
+ case xbox_series:
+ return LVH_WINDOWS_GAMEPAD_XBOX_SERIES;
+ case dualsense:
+ return LVH_WINDOWS_GAMEPAD_DUALSENSE;
+ case 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::ranges::fill(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::ranges::fill(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.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.report_sizes.output_report_size = static_cast(
+ std::min(options.profile.output_report_size, static_cast(LVH_WINDOWS_MAX_OUTPUT_REPORT_SIZE))
+ );
+ 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;
+ }
+
+ 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..acbbc62
--- /dev/null
+++ b/src/platform/windows/driver/CMakeLists.txt
@@ -0,0 +1,198 @@
+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)
+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
+ "${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_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"
+ "${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_um_library_candidates ${_lvh_wdk_um_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_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)
+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)
+
+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 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
+ 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}"
+ "${LIBVIRTUALHID_NTDLL_LIBRARY}")
+set_target_properties(libvirtualhid_umdf PROPERTIES
+ OUTPUT_NAME libvirtualhid_umdf
+ RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/package")
+
+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"
+ COMMAND "${CMAKE_COMMAND}" -E chdir "$"
+ "${LIBVIRTUALHID_STAMPINF}"
+ -f libvirtualhid.inf
+ -d "*"
+ -v "${LIBVIRTUALHID_DRIVER_VERSION}"
+ DEPENDS
+ libvirtualhid_umdf
+ "${CMAKE_CURRENT_BINARY_DIR}/libvirtualhid.inf"
+ COMMENT "Preparing libvirtualhid driver INF"
+ VERBATIM)
+
+add_custom_target(libvirtualhid_windows_catalog
+ COMMAND "${CMAKE_COMMAND}" -E chdir "$"
+ "${LIBVIRTUALHID_INF2CAT}"
+ /driver:.
+ /os:${LIBVIRTUALHID_INF2CAT_OS}
+ DEPENDS libvirtualhid_windows_inf
+ COMMENT "Generating libvirtualhid driver catalog"
+ VERBATIM)
+
+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..21bf4af
--- /dev/null
+++ b/src/platform/windows/driver/libvirtualhid.inf.in
@@ -0,0 +1,57 @@
+;
+; 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
+
+[SourceDisksNames]
+1=%DiskName%,,,
+
+[SourceDisksFiles]
+libvirtualhid_umdf.dll=1
+
+[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"
+DiskName="libvirtualhid UMDF Driver Install Disk"
+DeviceName="libvirtualhid Virtual HID Control Device"
+WudfRdDisplayName="Windows Driver Foundation - User-mode Driver Framework Reflector"
diff --git a/src/platform/windows/driver/libvirtualhid_umdf.cpp b/src/platform/windows/driver/libvirtualhid_umdf.cpp
new file mode 100644
index 0000000..b8fe3b3
--- /dev/null
+++ b/src/platform/windows/driver/libvirtualhid_umdf.cpp
@@ -0,0 +1,285 @@
+/**
+ * @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