diff --git a/AGENTS.md b/AGENTS.md
index 9575465c..05468ca7 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,26 +1,86 @@
-On Windows we use msys2 and ucrt64 to compile.
-You need to prefix commands with `C:\msys64\msys2_shell.cmd -defterm -here -no-start -ucrt64 -c`.
+# Repository guidance
-Prefix build directories with `cmake-build-`.
+## Build and tests
-The test executable is named `test_libvirtualhid` and will be located inside the `tests` directory within
-the build directory.
+On Windows, compile the normal C++ library with MSYS2/UCRT64. Prefix Windows
+build and test commands with
+`C:\msys64\msys2_shell.cmd -defterm -here -no-start -ucrt64 -c`.
+Keep the library buildable with both MSVC and MinGW/UCRT64. The UMDF driver
+package is a separate WDK/MSVC build.
-The project uses gtest as a test framework. GoogleTest is vendored as a submodule under `third-party/googletest`.
+Prefix build directories with `cmake-build-`. The GoogleTest executable is
+`tests/test_libvirtualhid` inside the build directory (with `.exe` on
+Windows). GoogleTest is vendored under
+`third-party/lizardbyte-common/third-party/googletest`.
-Keep the public c++ API platform-neutral. Platform-specific virtual HID details belong behind backend
-implementations and should not leak into consumer code.
+Add or update meaningful tests for changed behavior. Aim for full coverage of
+changed code, and validate gamepad API and lifecycle changes against
+`examples/gamepad_adapter.cpp` and the lifecycle tests. Run installed-device
+or hardware checks when the change depends on real devices; describe any
+validation that cannot run locally.
-Gamepad support is the primary target. Remote streaming hosts are the first consumer class, so validate API
-and behavior changes against the adapter examples and lifecycle tests.
+## API and platform boundaries
-Windows support must remain user-mode. Do not add a custom kernel-mode driver. The normal c++ library should
-remain buildable with both MSVC and MinGW/UCRT64; any future UMDF driver package is a separate WDK/MSVC build
-artifact.
+Keep the public C++ API platform-neutral. Put virtual HID details behind
+backend implementations, not in consumer code. Gamepads are the primary
+target, with remote streaming hosts as the first consumer class.
-Linux gamepad support should prefer uhid for descriptor-driven controllers. Keyboard and mouse support should
-prefer uinput, with X11 XTest only as a fallback.
+Windows support must remain user-mode; do not add a custom kernel-mode driver.
+For Linux, prefer `uhid` for descriptor-driven gamepads and `uinput` for
+keyboard and mouse, using X11/XTest only as a fallback.
-Always update public documentation when changing headers, backends, or consumer-facing behavior.
+## Code style and documentation
-Always follow the style guidelines defined in .clang-format for c/c++ code when that file is present.
+Follow `.clang-format` for C and C++ code. Do not add decorative separator
+comments; use descriptive names and code structure to show grouping.
+
+When changing headers, backends, or consumer-facing behavior, add or update
+the affected Doxygen documentation blocks. Document relevant declarations and
+behavior in code, including parameters and return values where applicable.
+Use this style for primary documentation:
+
+```cpp
+/**
+ * @brief Describe the function or type.
+ *
+ * @param value Describe the parameter.
+ * @return Describe the result.
+ */
+```
+
+Use `///< ...` for inline Doxygen comments. Keep Markdown guides focused on
+installation, usage, limitations, and troubleshooting that readers need.
+Update a Markdown page when one of those user-facing instructions actually
+changes; routine source changes do not require Markdown edits.
+Pad Markdown table cells so the `|` separators align vertically in source,
+accounting for wide Unicode characters such as emoji.
+
+## Issues and pull requests
+
+When asked to create an issue or pull request, use the applicable templates
+from LizardByte/.github, or this repository if it has a more specific template.
+At the end of the body, add an attribution naming the AI agent, model, and
+thinking level used to generate it.
+
+When asked only to create an issue, investigate enough to verify and accurately
+describe the observed behavior, expected behavior, and reproduction or
+evidence. Keep the issue concise. Investigate or fix fully when that is the
+request.
+
+## Code reviews
+
+Report concrete issues introduced by a change. Explain the trigger, impact,
+and a practical fix; point to the relevant code or test. Keep workflow status
+separate from code findings.
+
+If GitHub Actions workflows await maintainer approval, or a dependent check
+times out before its job can run, do not leave an inline comment, request
+changes, or ask the contributor to run them. If missing results materially
+limit the review, mention that once in the summary. Review actual failed
+checks when their logs are available.
+
+For pull request reviews, verify the current PR head and mergeability against
+its target branch. If conflicts are confirmed, tell the author to rebase and
+resolve them. Do not infer conflicts from unknown or pending mergeability.
+When approving a pull request, the approval itself is sufficient; do not add
+a comment solely to accompany it.
diff --git a/README.md b/README.md
index 0705fe1d..ade92673 100644
--- a/README.md
+++ b/README.md
@@ -16,15 +16,14 @@
-
-
🎮 Virtual HID Gamepad License
-
- A license is required for Windows driver-backed devices and macOS virtual gamepads.
- Linux and FreeBSD backends do not require a license.
- Yearly and lifetime options are available.
-
-

-
+> [!IMPORTANT]
+> **🎮 Virtual HID Gamepad License**
+>
+> A license is required for Windows driver-backed devices and macOS virtual
+> gamepads. Linux and FreeBSD backends do not require a license. Yearly and
+> lifetime options are available.
+>
+> [](https://buy.polar.sh/polar_cl_zj6Io5NVukXfZSl97ULtFvImfI5L1jbL2cSnc0Y72Pt)
# Overview
@@ -96,14 +95,12 @@ More complete examples live in `examples/`, including the streaming-host-oriente
- [End-user gamepad guide](docs/end-user-gamepad-guide.md): Sunshine and Moonlight setup, feature caveats,
troubleshooting, and controller support references.
-- [Usage and API](docs/usage.md): CMake consumption, build options, public API overview, profiles, and examples.
-- [Platform support](docs/platform-support.md): backend capability model, Windows, Linux, macOS,
- and Linux permission setup.
-- [Windows driver package](docs/windows-driver.md): UMDF/VHF package build, installation, validation, diagnostics,
- and signing notes.
-- [Streaming-host integration](docs/streaming-host-integration.md): integration contract.
-- [Development](docs/development.md): local build/test commands, repository layout, docs generation, and roadmap.
-- [TODO](docs/todo.md): known larger compatibility gaps and proposed solution paths.
+- [Usage and API](docs/usage.md): CMake consumption, profiles, and API examples.
+- [Platform support](docs/platform-support.md): device availability, platform limits, and Linux permissions.
+- [Windows driver package](docs/windows-driver.md): installation, licensing, diagnostics, and contributor builds.
+- [macOS gamepad setup](docs/macos-gamepad.md): installation, licensing, and signed builds.
+- [Streaming-host integration](docs/streaming-host-integration.md): integration pattern.
+- [Development](docs/development.md): build, test, and documentation commands.
## 🎯 Scope
diff --git a/dockle.toml b/dockle.toml
index f971dd28..04e02764 100644
--- a/dockle.toml
+++ b/dockle.toml
@@ -27,7 +27,6 @@ inputs = [
"docs/platform-support.md",
"docs/windows-driver.md",
"docs/macos-gamepad.md",
- "docs/todo.md",
"docs/streaming-host-integration.md",
"docs/development.md",
"LICENSES/license-map.md",
diff --git a/docs/development.md b/docs/development.md
index 95d44d9a..38ca51f6 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -1,102 +1,47 @@
# Development
-This page covers project layout, local build commands, documentation generation,
-and the remaining roadmap.
+Build directories use the `cmake-build-` prefix. The shared GoogleTest binary
+is `tests/test_libvirtualhid` in the build directory. Initialize submodules
+before building.
-## Repository Layout
+## Windows library and tests
-```text
-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/ macOS CoreGraphics and broker client backends
-src/platform/macos/broker/ Licensed macOS virtual HID broker
-examples/ Minimal consumers and platform smoke tests
-tests/ Unit and integration tests
-cmake/ Package config and helper modules
-docs/ Project documentation
-third-party/dockle/ LizardByte documentation toolchain submodule
-third-party/googletest/ GoogleTest submodule
-```
-
-## Windows Library Build
-
-On Windows, the normal library and tests are built with MSYS2/UCRT64 for MinGW
-coverage. Build directories should use the `cmake-build-` prefix.
-
-```bash
-cmake -S . -B cmake-build-debug-mingw-ucrt64-ninja -G Ninja \
- -DCMAKE_BUILD_TYPE=Debug
-cmake --build cmake-build-debug-mingw-ucrt64-ninja
-cmake-build-debug-mingw-ucrt64-ninja/tests/test_libvirtualhid.exe
-```
-
-The Windows UMDF driver package is a separate MSVC/WDK build. See
-[Windows driver package](windows-driver.md).
-
-## Linux Build
+Run each command from the repository root through MSYS2/UCRT64:
-Linux builds use the same CMake target shape. Integration tests that create real
-devices require access to `/dev/uhid` and `/dev/uinput`; see
-[Platform support](platform-support.md).
-
-```bash
-cmake -S . -B cmake-build-debug -G Ninja -DCMAKE_BUILD_TYPE=Debug
-cmake --build cmake-build-debug
-cmake-build-debug/tests/test_libvirtualhid
+```powershell
+& 'C:\msys64\msys2_shell.cmd' -defterm -here -no-start -ucrt64 -c 'cmake -S . -B cmake-build-debug-mingw-ucrt64-ninja -G Ninja -DCMAKE_BUILD_TYPE=Debug'
+& 'C:\msys64\msys2_shell.cmd' -defterm -here -no-start -ucrt64 -c 'cmake --build cmake-build-debug-mingw-ucrt64-ninja'
+& 'C:\msys64\msys2_shell.cmd' -defterm -here -no-start -ucrt64 -c './cmake-build-debug-mingw-ucrt64-ninja/tests/test_libvirtualhid.exe'
```
-## macOS Build
+The normal library also builds with MSVC. The Windows driver package requires
+the WDK/MSVC toolchain and is built separately; see
+[Windows package](windows-driver.md#build-the-driver-package).
-macOS builds link the CoreGraphics keyboard/mouse backend and the broker client
-against system frameworks. Top-level builds also compile the broker app. The
-ordinary test suite checks translation, protocol capacity, and lifecycle paths
-without creating a live virtual HID device. Live testing requires Apple's
-approved virtual HID entitlement and a signed installation.
+## Linux and macOS
-```bash
+```sh
cmake -S . -B cmake-build-debug -G Ninja -DCMAKE_BUILD_TYPE=Debug
cmake --build cmake-build-debug
-cmake-build-debug/tests/test_libvirtualhid
+./cmake-build-debug/tests/test_libvirtualhid
```
-For the universal Apple silicon and Intel release build, profile setup, and
-signing workflow, see [macOS gamepad setup](macos-gamepad.md).
+Linux installed-device tests need access to `/dev/uhid` or `/dev/uinput`.
+A macOS test build does not prove virtual gamepad creation; that requires a
+signed broker with Apple's virtual HID entitlement. See
+[macOS setup](macos-gamepad.md).
-## Documentation
+## Documentation and validation
-Documentation is generated by Dockle. The authored inputs and project-specific
-Doxygen settings are declared in `dockle.toml`; Dockle owns the generated
-Doxyfile used by the CMake `docs` target.
+Public declarations and changed behavior should have accurate Doxygen
+comments. Markdown pages cover tasks readers perform and limits they need to
+know. The documentation target uses Dockle and the sources listed in
+`dockle.toml`:
-```bash
-cmake --build cmake-build-debug-mingw-ucrt64-ninja --target docs
+```sh
+cmake --build cmake-build-debug --target docs
```
-Keep Markdown pages focused on decisions or workflows that consumers and
-maintainers need. Avoid preserving old implementation-plan checklists once the
-code and tests provide a better source of truth.
-
-## Validation Targets
-
-- `test_libvirtualhid`: shared GoogleTest binary under the build directory's
- `tests` directory.
-- `gamepad_adapter`: example executable for profile and lifecycle diagnostics.
-- `run_gamepad_adapter_example`: CMake target that runs `gamepad_adapter` from
- its generator-specific output path when a platform backend is available.
-- Windows driver helper scripts under `scripts/windows`.
-- Linux consumer tests through SDL2, libinput, `uhid`, and `uinput` where the
- host environment supports real virtual devices.
-- Doxygen docs target for README, Markdown pages, and public header rendering.
-
-## Roadmap
-
-- Validate each macOS HID gamepad profile against SDL, Steam, browsers, and
- Game Controller framework consumers after Apple grants the entitlement.
-- Add bindings for other languages, such as Python, Rust, and C#. Bindings will
- be considered for any requested language.
-- Evaluate an optional FreeBSD CUSE-backed `uhid(4)`-compatible device for
- direct HID consumers. This would supplement uinput; it is not equivalent to
- registering a virtual device with FreeBSD's kernel HID bus.
+Validate gamepad changes against the GoogleTest lifecycle suite and
+[adapter example](../examples/gamepad_adapter.cpp). Installed driver, signing,
+and physical-consumer behavior require platform-specific checks.
diff --git a/docs/macos-gamepad.md b/docs/macos-gamepad.md
index 8930553f..dc603f40 100644
--- a/docs/macos-gamepad.md
+++ b/docs/macos-gamepad.md
@@ -1,203 +1,84 @@
# macOS virtual gamepads
-The macOS backend creates descriptor-driven virtual gamepads through a separate
-root-owned broker. The broker alone calls Apple's `IOHIDUserDevice` API and holds
-the virtual HID entitlement. The ordinary C++ library has no Apple entitlement
-and uses CoreGraphics for keyboard and mouse input.
-
-The broker's gamepad report thread and `IOHIDUserDevice` callback queue request
-user-interactive QoS to reduce scheduling delay in the input path. License
-validation and other broker work keep their normal scheduling. This is a
-best-effort priority hint, not a real-time latency guarantee. The virtual HID
-device is provided by macOS, so there is no separate libvirtualhid driver
-process to prioritize.
-
-The client checks root ownership of the broker directory, socket, and
-connected peer before exchanging versioned messages. Socket transfers handle
-partial reads and writes so truncated messages are not treated as complete.
-The root-owned broker directory permits local clients to reach its socket, and
-the broker checks the machine license before gamepad creation.
-
-The built-in generic, Xbox 360, Xbox One, Xbox Series, DualShock 4, DualSense,
-and Switch Pro profiles, including the explicit USB and Bluetooth PlayStation
-variants, are accepted. The macOS broker receives the selected transport's HID
-descriptor and reports. Xbox 360 uses a USB HID identity (`045e:028e`, version
-`0114`) with numbered D-pad buttons that match Steam's macOS mapping. Xbox One
-and Xbox Series use distinct HID identities (`045e:0b20` and `045e:0b13`).
-macOS exposes broker-created devices as Virtual transport, so the backend
-frames their input as wired Xbox GIP packets for Steam's Xbox HID decoder. The
-backend accepts both wired GIP and report-ID-3 rumble output for these profiles.
-The macOS Xbox reports reverse the vertical stick bytes so positive public Y
-moves the sticks up in Steam, as with the PlayStation and Switch profiles.
-Xbox 360, Xbox One, and Xbox Series input, plus Xbox One and Series rumble,
-were validated with Steam's controller tester on an installed, notarized build.
-Windows uses a separate XUSB/XInput personality. Individual games may use
-Apple's Game Controller framework or their own HID mappings, so each profile
-still needs testing in the intended consumer.
-
-When metadata omits a stable ID, the client derives a locally administered
-`02:00:xx:xx:xx:xx` identifier from the device ID.
-
-## Signing prerequisites
-
-The broker needs a Developer ID provisioning profile for
-`dev.lizardbyte.app.libvirtualhid` containing
-`com.apple.developer.hid.virtual.device`. An existing Developer ID Application
-certificate and notarization credentials can be reused for this Apple team. A
-profile for another bundle ID cannot authorize the broker. Apple explains the
-[restricted entitlement bundle and embedded profile](https://developer.apple.com/documentation/xcode/signing-a-daemon-with-a-restricted-entitlement).
-
-## Build and distribute
-
-On macOS with Xcode and CMake installed:
+macOS gamepads use an installed broker with Apple's virtual HID entitlement.
+The C++ library uses the broker for virtual gamepads and CoreGraphics for
+keyboard and mouse input. Creating a gamepad requires a machine license.
+All built-in gamepad profiles are accepted, though individual games may map
+them differently.
-```sh
-export MACOSX_DEPLOYMENT_TARGET=14.2
-cmake -S . -B cmake-build-macos-universal \
- -DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' \
- -DCMAKE_BUILD_TYPE=Release -DBUILD_DOCS=OFF -DBUILD_TESTS=ON
-cmake --build cmake-build-macos-universal --parallel "$(sysctl -n hw.ncpu)"
-xcrun lipo -info cmake-build-macos-universal/src/platform/macos/broker/VirtualHIDBroker.app/Contents/MacOS/VirtualHIDBroker
-xcrun lipo -info cmake-build-macos-universal/tools/VirtualHIDControl.app/Contents/MacOS/VirtualHIDControl
-```
+## Install and activate
+
+Install the signed, notarized DMG from the
+[libvirtualhid releases](https://github.com/LizardByte/libvirtualhid/releases).
+Mount it and open **Install libvirtualhid.command**. The installer requests
+administrator authorization, installs **Virtual HID Broker** and
+**Virtual HID Control** in Applications, and starts the broker service.
-The broker and control app executables each contain Apple silicon and Intel slices.
-CI sets `MACOSX_DEPLOYMENT_TARGET` at the workflow level. The Apple builds use
-`-fexperimental-library` for libc++'s `std::jthread` support.
-The CI job checks the broker, control app, license CLI, and `libvirtualhid.a` with `lipo`.
-It runs the shared license-policy and macOS wire-protocol tests, starts the
-broker as root, and checks license IPC. These checks do not prove virtual HID
-creation: Apple's restricted entitlement needs a matching profile embedded in
-the signed app bundle, independent of the runner's System Integrity Protection
-setting.
-PR workflows receive no Apple signing or notarization secrets, so PR CI does
-not sign a package. Debug mode does not change the entitlement requirement.
-Use a Mac with the approved profile for a full device test before merging.
-
-For a release, set `APPLE_CODESIGN_IDENTITY` to the Developer ID Application
-identity, set `APPLE_MACOS_VIRTUAL_HID_PROVISIONING_PROFILE` to the broker
-profile path, and set the notarization variables (`APPLE_ID`, `APPLE_TEAM_ID`,
-`APPLE_NOTARYTOOL_PASSWORD`). Then run:
+In **System Settings > Privacy & Security > Device Control and Data Access**
+(**Accessibility** on older macOS versions), add
+`/Applications/VirtualHIDBroker.app`. The broker runs as a system service and
+cannot display the permission prompt while creating a gamepad. Review the
+signed application before granting this broad device-control permission.
+
+Activate the purchased license from Terminal:
```sh
-bash scripts/macos/package-dmg.sh cmake-build-macos-universal
+/usr/local/bin/libvirtualhid-license activate
+/usr/local/bin/libvirtualhid-license status
```
-The script embeds the profile, signs the broker and control apps with Hardened
-Runtime and a secure timestamp, verifies their signatures, makes one universal
-DMG, submits it using `notarytool`, and staples the ticket. The control app uses
-its own bundle ID and does not need the broker's restricted entitlement. Release
-CI performs these steps with the corresponding certificate, profile, and
-notarization secrets.
+Activation prompts for the key without echoing it. Use `validate` to refresh
+status or `deactivate` to release this machine's activation. Open **Virtual
+HID Control** to create and inspect a test controller. The host application
+runs in the normal user session; the broker runs as a LaunchDaemon.
-### Test locally
+## License validation during outages
-Check out the branch of interest on a Mac, install Xcode, and copy
-`.env.example` to `.env` in the repository root. Fill in the Apple ID,
-notarization app-specific password, Developer ID Application `.p12` file path
-and export password, and the provisioning profile path. Paths must be absolute.
-The script can also decode the base64 certificate and profile values used by
-CI, if you have those originals. The optional team ID and signing identity are
-detected from the profile and certificate. `.env` is ignored by Git and must
-stay local. GitHub's secrets API cannot return stored secret values.
+The broker validates the machine license when it starts and every 24 hours,
+retrying temporary failures about once a minute. While Polar validation is
+unavailable, new creation is limited to one active virtual gamepad. Existing
+gamepads remain for up to one hour; then the broker closes all but one. A yearly
+license must validate within 25 hours of its last successful validation or the
+remaining gamepad closes. After the broker restarts, including after a macOS
+reboot, a yearly license needs online validation before gamepad creation. A
+previously activated lifetime license can keep or create one gamepad while
+Polar is unreachable, with no offline time limit. The broker keeps retrying
+validation about once a minute and returns to the normal 24-hour schedule
+after success. Confirmed revocation or deactivation closes existing gamepads.
-```sh
-cp -n .env.example .env
-open -e .env
-bash scripts/macos/build-and-install.sh
-```
+## Troubleshoot
-The script uses the installed Xcode, finds CMake in the project `.venv` or
-installs it through Homebrew if needed, imports the certificate into a temporary
-Keychain, builds and tests universal binaries, signs and notarizes the DMG, and
-installs it. It removes the temporary Keychain afterward. The approved profile
-must be for `dev.lizardbyte.app.libvirtualhid`; a profile for another bundle
-ID fails before the build. The certificate and profile are necessary even when
-System Integrity Protection is disabled. A locally built unsigned broker can
-test IPC and licensing, but cannot establish that virtual gamepad creation works.
-
-Run `bash scripts/macos/build-and-install.sh --package-only` to create the
-signed DMG without installing it.
-
-After installation, activate a license if needed, then test each gamepad
-profile in a macOS consumer.
-
-For manual installation, mount the DMG and double-click
-**Install libvirtualhid.command**. It asks for administrator authorization,
-installs **Virtual HID Broker** and **Virtual HID Control** in `/Applications`,
-installs the static library and headers under `/usr/local`, and starts the
-`dev.lizardbyte.app.libvirtualhid` LaunchDaemon. Open **Virtual HID Control**
-from Applications to create and inspect test devices. The broker app runs as a
-system service and does not have a user interface. Run a host process in the
-normal user session. The broker socket is `/var/run/libvirtualhid/broker.sock`;
-only the root-owned installed broker can answer the library's requests.
-
-macOS also requires permission for the broker to create virtual HID devices.
-Open **System Settings**, then **Privacy & Security**, then **Device Control and
-Data Access** (**Accessibility** on older macOS versions). Click **Add**,
-authorize the settings change, and select **Virtual HID Broker** from
-`/Applications/VirtualHIDBroker.app`. The
-installed broker runs as a root LaunchDaemon, so macOS cannot show its prompt
-during gamepad creation. This permission gives the broker broad device control
-access; review the signed app before granting it.
-
-To activate a purchased license, open Terminal and run:
+If creation reports `license_required`, activate or validate the license.
+For `backend_unavailable`, check the installed broker service:
```sh
-/usr/local/bin/libvirtualhid-license activate
-/usr/local/bin/libvirtualhid-license status
+sudo launchctl print system/dev.lizardbyte.app.libvirtualhid
```
-The command prompts for the key without echoing it or placing it in the process
-arguments. `validate` refreshes from Polar and `deactivate` releases this
-machine activation. These commands do not require `sudo` after installation.
-
-With the broker installed and licensed, hold a test controller for a minute:
+For `backend_failure`, check the macOS permission above. If it persists,
+verify the broker signature and embedded provisioning profile. A local
+unsigned build can test library and broker communication but cannot prove
+virtual HID creation. Check the controller in the intended macOS game or
+streaming client; enumeration alone does not establish compatibility.
-```sh
-cmake-build-macos-universal/examples/gamepad_adapter generic --hold-seconds 60
-```
+## Build and package
-While it is running, use another Terminal window to inspect HID enumeration:
+A contributor build requires Xcode and CMake:
```sh
-hidutil list
+cmake -S . -B cmake-build-macos-universal \
+ -DCMAKE_OSX_ARCHITECTURES='arm64;x86_64' -DCMAKE_BUILD_TYPE=Release
+cmake --build cmake-build-macos-universal
+./cmake-build-macos-universal/tests/test_libvirtualhid
```
-Repeat with `x360`, `xone`, `xseries`, `ds4`, `ds5`, and `switch`. Check each
-device in the intended macOS game or streaming client; enumeration alone does
-not prove that a particular consumer recognizes its profile.
-
-The DMG contains both the MIT library and the LB-SAL broker. A release must
-retain both license texts.
-
-## License and validation
-
-The Windows and macOS brokers share the Polar organization, Yearly and Lifetime
-benefit IDs, purchase URL, customer portal, and license time limits. No
-unlicensed production gamepad is created. `lvh::get_license_status()`,
-`activate_license()`, `validate_license()`, and `deactivate_license()` talk to
-the installed broker;
-the license key never enters the virtual gamepad report stream. A five-minute
-GitHub Actions evaluation is available only when the broker itself starts in
-the GitHub Actions environment.
-
-The broker stores its machine activation in a root-only file under
-`/Library/Application Support/libvirtualhid`. It revalidates with Polar every
-24 hours. On a network outage, a previously validated license may create one
-gamepad while authorization remains current. Existing virtual gamepads close
-when their authorization expires or is revoked. A yearly activation needs
-online validation after the broker restarts; this deliberately fails closed
-when trusted elapsed time cannot be reconstructed.
-Successful licensed gamepad creations are saved as pending usage and reported
-through Polar's `increment_usage` field on the next license validation.
-Validation with no new gamepads leaves Polar usage unchanged.
-
-If creation returns `backend_unavailable`, inspect the launchd job with
-`sudo launchctl print system/dev.lizardbyte.app.libvirtualhid`. If it returns
-`backend_failure` during virtual HID creation, check the broker's macOS
-permission above, then inspect the embedded profile and signature with
-`codesign -d --entitlements :- /Applications/VirtualHIDBroker.app`
-and inspect the embedded profile with
-`security cms -D -i /Applications/VirtualHIDBroker.app/Contents/embedded.provisionprofile`.
-If it returns `license_required`, run `libvirtualhid-license activate`.
+For a signed local installation, copy `.env.example` to an untracked `.env`
+and provide the Apple Developer ID certificate, an approved provisioning
+profile for `dev.lizardbyte.app.libvirtualhid` with
+`com.apple.developer.hid.virtual.device`, and notarization credentials. Then
+run `bash scripts/macos/build-and-install.sh`. Use `--package-only` to produce
+a signed DMG without installing it. Keep signing credentials local. The
+broker's restricted entitlement is required even for debug builds.
+
+The DMG contains the MIT library and the LB-SAL broker; releases must retain
+both license texts.
diff --git a/docs/maintainer/store-review-validation.md b/docs/maintainer/store-review-validation.md
index a8eaa23c..2f10a43d 100644
--- a/docs/maintainer/store-review-validation.md
+++ b/docs/maintainer/store-review-validation.md
@@ -1,101 +1,47 @@
-# Microsoft Store Review Validation
+# Microsoft Store review validation
-These instructions are intended for Microsoft Store certification review of the
-libvirtualhid Windows driver installer. The reviewer does not need to build the
-project, install the Windows SDK/WDK, or write a consumer application.
+Use these steps for certification review of the Windows driver MSI. Reviewers
+do not need the Windows SDK/WDK or a consuming application. Supply an active
+review license key separately through Partner Center; do not embed it in the
+package or this document.
-The Windows package installs a user-mode UMDF/VHF virtual HID driver. The
-libvirtualhid-specific driver binary is a UMDF DLL installed through the Windows
-Driver Store. It is not a kernel-mode `.sys` driver.
-
-## Submission Notes
-
-Paste this into the Partner Center certification notes field:
+## Partner Center notes
```text
-This package installs the libvirtualhid Windows user-mode UMDF/VHF virtual HID driver and local broker service. Applications consume it through the libvirtualhid client API, and the MSI includes a native diagnostic UI for local validation.
-
-Every virtual gamepad, driver-backed keyboard, or driver-backed Raw Input mouse creation requires an active license. A currently granted review license key with an available device activation is supplied separately in the Partner Center certification credentials or notes. The key is not embedded in the package or this document.
-
-Launch the validation tool below.
-
-Default install root:
-C:\Program Files\libvirtualhid
+Install the production-signed libvirtualhid Windows AMD64 driver MSI. Reboot
+only if Windows requests it.
-Installed validation files:
-C:\Program Files\libvirtualhid\tools\windows\virtualhid_control.exe
-C:\Program Files\libvirtualhid\tools\windows\gamepad_adapter.exe
-C:\Program Files\libvirtualhid\services\windows\libvirtualhid_broker.exe
-
-Required validation:
+Open PowerShell and run:
$installRoot = Join-Path $env:ProgramFiles "libvirtualhid"
Start-Process "$installRoot\tools\windows\virtualhid_control.exe"
-In the libvirtualhid control window, paste the supplied review key into the License key field and click Activate license. Confirm the status changes to Licensed. Then leave the default Xbox Series profile selected and click Create. Use the button and axis controls in the UI to submit input to the virtual controller. Next, change Device type to Mouse and click Create. Use Tab or the arrow keys to highlight the mouse controls and Space or Enter to activate relative movement, momentary button, and wheel input without using the physical mouse.
-
-Expected result:
-- The backend status reports windows-umdf with gamepad, keyboard, and mouse support available
-- The libvirtualhid_broker service is running
-- License validation succeeds and the license status reports Licensed
-- A virtual HID gamepad is created and appears in the device list
-- A virtual HID gamepad child device starts with the Xbox Series HID ID
- HID\VID_045E&PID_0B12&IG_00
-- Button, axis, and Share values in the UI can be pressed or moved without
- errors
-- A driver-backed virtual HID mouse is created and appears in the device list
-- Keyboard activation of the mouse controls moves the pointer, changes button
- state, and scrolls without errors
-
-Optional browser validation:
-$installRoot = Join-Path $env:ProgramFiles "libvirtualhid"
-& "$installRoot\tools\windows\virtualhid_control.exe"
-
-Create the default Xbox Series gamepad, then open:
-https://hardwaretester.com/gamepad
-
-Use the libvirtualhid control window to press buttons or move axes while the browser page is open.
-
-Expected result:
-- The browser Gamepad API sees an Xbox-compatible controller
-- Button and axis values change while controls are used in the validation UI
+In Virtual HID Control, paste the supplied review key and click Activate
+license. Confirm the status is Licensed. Leave Xbox Series selected and click
+Create. Use the UI to press buttons and move axes. Then select Mouse, click
+Create, and use Tab or the arrow keys to highlight a mouse control. Press
+Space or Enter to test movement, buttons, and scrolling.
-For a browser mouse-event tester, create a mouse in the validation UI and enable Delayed browser test. Leave the pointer over the browser test target, activate a movement, button, or wheel action with the keyboard, then switch to the browser before the displayed countdown expires.
+Expected: the broker is running; gamepad and mouse creation succeed; the
+controller appears in the device list; button and axis values change; the
+mouse pointer, buttons, and wheel respond.
-Expected result:
-- The browser receives the queued mouse action while it owns focus
-- A queued button action produces one press followed by one release
+Optional browser test: with the Xbox Series gamepad created, open
+https://hardwaretester.com/gamepad and use the UI to press a button. The
+browser should detect the controller and show changing input values.
```
-## Manual Review Steps
-
-1. Install the released, production-signed
- `libvirtualhid-Windows-AMD64-driver-installer.msi`.
-2. Reboot only if Windows reports that a reboot is required.
-3. Open PowerShell.
-4. Run the required validation tool from the submission notes.
-5. Activate the review key supplied through Partner Center.
-6. Create the default gamepad and exercise its controls.
-7. Create a mouse and exercise its controls with keyboard navigation.
-8. Optionally, run the browser validation steps.
-
-If the default install location was changed during MSI installation, replace
-`$env:ProgramFiles\libvirtualhid` with the selected install directory.
-
-The MSI writes the driver-install transcript to:
-
-```text
-C:\ProgramData\libvirtualhid\install-driver.log
-```
+If the MSI was installed elsewhere, replace `$env:ProgramFiles\libvirtualhid`
+with the chosen installation directory. The installed UI can manage the
+machine license and exercise devices without administrator privileges.
-## Scope Notes
+## Release scope and diagnostics
-The default Store-review path still uses Xbox Series and does not exercise the
-`x360` profile. The package also installs the separate Xbox 360 XUSB companion;
-validate that path with `test-installed-driver.ps1 -GamepadProfile x360` and the
-installed-driver `Xbox360PublishesXInputStateAndRumble` integration test before
-claiming Xbox 360 compatibility for a release.
+The default review flow exercises Xbox Series. Before claiming Xbox 360
+compatibility for a release, also run
+`scripts/windows/test-installed-driver.ps1 -GamepadProfile x360` and the
+installed-driver `Xbox360PublishesXInputStateAndRumble` integration test.
-The reviewer-visible success signal is the installed `ROOT\LIBVIRTUALHID`
-control device, the `\\.\LibVirtualHid` control path, the running
-`libvirtualhid_broker` service, and started HID child devices while
-`virtualhid_control.exe` has a gamepad and mouse created.
+The MSI writes its install log to
+`C:\ProgramData\libvirtualhid\install-driver.log`. The driver log is
+`%WINDIR%\Temp\libvirtualhid-umdf-driver.log`. Include these logs when
+reporting installation or device-creation failures.
diff --git a/docs/platform-support.md b/docs/platform-support.md
index 436be6e9..2dd97cfd 100644
--- a/docs/platform-support.md
+++ b/docs/platform-support.md
@@ -1,385 +1,62 @@
-# Platform Support
+# Platform support
-`libvirtualhid` keeps the public C++ API platform-neutral. Consumers ask the
-runtime for capabilities, create devices from profiles, submit normalized state,
-and receive output callbacks. Backend-specific virtual HID details stay inside
-the platform implementation.
+The public C++ API is shared across platforms, but device availability and
+feedback depend on the installed backend and selected profile. Query runtime
+and effective profile capabilities before enabling optional features.
-## Capability Model
-
-Backends report what is available at runtime. A backend can be selectable while
-still reporting that a specific device type is unavailable because permissions,
-kernel modules, driver installation, or platform features are missing. Device
-creation then returns an operation status instead of forcing consumers onto
-platform-specific probing code.
-
-Use capability queries for behavior such as:
-
-- Whether the backend can create virtual HID devices.
-- Whether gamepad output reports are supported.
-- Whether keyboard, mouse, touchscreen, trackpad, or pen tablet creation is
- available.
-- Whether the X11/XTest keyboard and mouse fallback is active.
-- Whether a Windows driver package must be installed.
+| Platform | Gamepads | Other devices | Requirements |
+|----------|----------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
+| Windows | Generic, Xbox, PlayStation, Switch Pro | Driver-backed keyboard and relative mouse; Win32 keyboard, text, and mouse fallback | AMD64 UMDF package and machine license for driver-backed devices |
+| Linux | Generic and Xbox 360 through `uinput`; PlayStation, Switch Pro, Xbox One and Series through `uhid` | Keyboard, mouse, touchscreen, trackpad, pen tablet through `uinput`; XTest keyboard and mouse fallback | Writable device nodes and required kernel modules |
+| FreeBSD | Built-in profiles through `uinput` | Keyboard, mouse, touchscreen, trackpad, pen tablet through `uinput`; XTest fallback | Writable `uinput` device node |
+| macOS | Built-in profiles through the virtual HID broker | CoreGraphics keyboard and mouse | Signed broker, Apple entitlement, system permission, and machine license |
## Windows
-The Windows backend keeps the normal C++ library buildable with MSVC and
-MinGW/UCRT64. Gamepad creation, Raw Input-visible keyboard input, and Raw
-Input-visible relative mouse input use a user-mode UMDF2 package. Xbox 360 uses
-a per-controller XUSB software device plus a correlated VHF child; other
-profiles use the root control driver and Windows Virtual HID Framework.
-Keyboard text input, absolute mouse input, and the keyboard and mouse fallbacks
-use Win32 APIs.
-Set `KeyboardEvent::extended` when the input source positively identifies an
-extended key, such as keypad Enter. The Windows HID keyboard maps it to the
-corresponding HID usage, and the Win32 fallback sets the extended-key input
-flag. Leave it unset when the input source does not report this detail; existing
-key-code and scan-code classification still applies.
-
-The C++ library communicates with the driver through fixed-size protocol
-structures and `DeviceIoControl`, not C++ STL types. This keeps the public API
-compiler-neutral and preserves the boundary between the MinGW/MSVC client
-library and the WDK/MSVC driver package.
-
-When the driver is installed and licensed, the backend publishes gamepads,
-keyboards, and mice that standard Windows consumers can enumerate. Xbox 360 is
-a direct XInput/XUSB target while retaining a HID/DirectInput view; the other
-gamepads are descriptor-driven VHF devices. Consumers include XInput,
-SDL/HIDAPI, DirectInput, Windows.Gaming.Input/GameInput, and browser Gamepad API
-clients.
-
-Driver-backed keyboard key transitions use a standard keyboard-page HID report
-with modifier state and sixteen simultaneous non-modifier usages. Unicode text
-requests and keyboard-page keys that the descriptor cannot represent retain
-the Win32 injection path. If driver or license creation is unavailable,
-keyboard creation falls back to the existing Win32 implementation. Unexpected
-protocol and driver failures remain visible to the caller instead of silently
-changing the input path.
-
-The library and driver must use the same Windows control-protocol version. A
-descriptor-capacity change therefore increments the protocol version so a
-stale installed driver fails creation explicitly instead of misreading the
-request.
-
-The built-in Generic profile remains a platform-neutral Game Pad publicly. At
-the Windows transport boundary, VHF presents it as a DirectInput-compatible
-Joystick with the complete PID output-report contract required by DirectInput.
-Constant Force and Sine effects are normalized back into the same rumble
-callback used by the other backends; unsupported PID effect payloads are
-accepted without producing misleading feedback. Windows also applies
-DirectInput's idle-at-maximum Z/Rz trigger polarity. Start delay, duration, and
-loop count are honored; finite effects emit a zero-rumble callback when they
-expire, while explicit stop commands take effect immediately.
-
-Xbox One uses the native eight-byte PID payload exposed by the Windows Xbox HID
-stack. Xbox Series keeps the `0x045E:0x0B12` identity, release `0x0509`, and the
-`0x045E:0x0B12&IG_00` XInputHID match ID observed from physical Xbox Series USB
-and Xbox Wireless Adapter connections. The VHF device preserves the native
-17-byte GIP-shaped input report and native eight-byte four-motor rumble payload.
-The public Xbox Series profile remains `0x045E:0x0B12`; the Windows transport
-applies the captured release at device creation. Xbox One accepts native HID
-rumble writes. The Xbox Series report parser accepts the native eight-byte
-four-motor payload when a consumer delivers it, applies its actuator-enable
-mask and duration field, and reports the body motors as normalized
-low/high-frequency rumble and the independent trigger motors as trigger-rumble
-output.
-
-Xbox 360 uses a broker-owned System-class software devnode with an explicit
-container ID. Its dedicated UMDF2 companion publishes the XUSB interface used
-by `xinput1_4.dll`, while its VHF child preserves the public
-`0x045E:0x028E&IG_00` HID identity. Input state is delivered at native XInput
-precision and `XInputSetState` feedback is normalized into the public two-motor
-rumble callback. This is a private Windows implementation detail; the public C++
-profile and API remain platform-neutral. Because Microsoft does not document
-XUSB as a third-party virtual-driver API, this compatibility layer requires
-release-by-release installed-driver regression testing.
-
-The VHF driver answers the calibration, pairing, and firmware feature reports
-used to initialize DualShock 4 and DualSense HIDAPI output. It also answers the
-Switch Pro USB and subcommand initialization sequence and accepts the native
-`0x30` input layout, so descriptor-aware consumers can initialize those
-controllers before sending their native output reports. The Switch Pro profile
-uses the `0x0210` hardware revision reported by a physical Nintendo controller,
-and the Windows VHF device exposes that revision to HID consumers. Full-state
-and subcommand-reply reports use the same per-device packet counter on Windows,
-matching the counter that a native controller advances for every input report.
-The Windows client backend caches the newest complete Switch Pro state and
-streams native `0x30` reports every 15 milliseconds. This coalesces separate
-acceleration and gyroscope API updates into the three-sample report cadence used
-by a physical USB controller.
-
-For every gamepad report ID, the VHF driver caches the newest complete input
-report and answers synchronous `GetInputReport` requests from that cache. This
-lets Windows HID consumers retrieve the current battery state for Xbox One,
-Xbox Series, DualShock 4, DualSense, and Switch Pro instead of relying only on
-the asynchronous input stream.
-
-That HID report does not change the XInput battery classification of the Xbox
-One and Xbox Series VHF devices. On a Windows desktop where XInput enumerated
-one of those virtual Xbox controllers, `XInputGetBatteryInformation` returned
-`BATTERY_TYPE_DISCONNECTED` and `BATTERY_LEVEL_EMPTY` even while its input was available through
-`XInputGetState`. Headless Windows CI did not expose an XInput slot for the same
-device. Neither path exposes the remote battery through XInput. Consumers that
-prefer XInput, including SDL's correlated Windows Xbox path and Windows Game
-Bar, therefore do not receive the remote Xbox battery value. DualShock 4,
-DualSense, and Switch Pro battery state is independently covered through SDL's
-HID path. The Xbox 360 XUSB personality reports the fixed wired-controller
-battery state and does not accept remote battery input.
-
-The current Steam client displays its controller battery indicator only when it
-classifies the device as Bluetooth or wireless. Because VHF exposes a wired
-virtual transport, Steam can hide the battery indicator for every Windows
-profile even when another HID consumer can retrieve the submitted value.
+Install the [Windows driver package](windows-driver.md) to create gamepads
+and Raw Input-visible keyboard or relative mouse devices. Without the package
+or a valid license, supported keyboard and mouse operations use Win32
+fallbacks. Unicode text and absolute mouse movement use Win32 even when the
+driver is installed.
-Windows VHF devices do not expose a Bluetooth transport identity to HIDAPI.
-The Windows backend therefore reports DualShock 4 and DualSense requests as
-effective USB profiles through `Gamepad::profile()` and uses the matching USB
-descriptor, input reports, output reports, and feature-report framing. This
-keeps Steam and SDL's transport detection aligned with the reports accepted by
-the driver, including rumble and RGB LED output. The DualSense firmware feature
-report identifies the base controller's `0x0004` software series and current
-`0x0630` device software instead of reporting DualSense Edge series `0x0044`
-with the older `0x0154` revision. Linux keeps the Bluetooth defaults described
-below.
+Xbox 360 appears to XInput through the package's XUSB companion. The other
+profiles use virtual HID. Battery values submitted for Xbox One and Series
+can be read through HID, but XInput and applications relying on its battery
+API do not receive the remote value. Steam may hide battery indicators for
+wired virtual devices. Windows PlayStation devices use USB report framing,
+even when the public profile requested Bluetooth.
-See [Windows driver package](windows-driver.md) for build, install, validation,
-and signing details.
+The published installer targets Windows 11 version 21H2 or later on AMD64.
+Windows 10 version 2004 or later has a best-effort path. Windows ARM64 release
+packages are not available.
## Linux
-The Linux backend uses standard user-space kernel interfaces:
-
-- `uhid` for descriptor-driven PlayStation, Switch Pro, Xbox One, and Xbox
- Series gamepads.
-- `uinput` for Generic and Xbox 360 gamepads, for Xbox One and Xbox Series when
- `uhid` is unavailable, and for keyboard, mouse, touchscreen, trackpad, and pen
- tablet devices.
-- `libevdev` internally for uinput device construction.
-- X11/XTest only as a keyboard and mouse fallback when `uinput` cannot be used
- and an X11 session is available.
-
-One public mouse handle uses separate relative and absolute uinput devices. The
-relative device exposes `REL_X`, `REL_Y`, buttons, and scroll axes. The absolute
-device exposes `ABS_X`, `ABS_Y`, buttons, and `INPUT_PROP_DIRECT`, without
-relative axes, so libinput and the X11 libinput driver deliver absolute pointer
-motion instead of discarding it from a mouse-class relative device. Buttons are
-routed to the device that most recently received motion, while releases remain
-on the device that received the matching press. Scroll always uses the relative
-device.
-
-The relative uinput mouse advertises the legacy `REL_WHEEL` and `REL_HWHEEL`
-axes together with their high-resolution counterparts when the platform
-provides them. It accumulates high-resolution input independently for each axis
-and emits a legacy detent for every 120 accumulated units. This follows the
-Linux input protocol, lets libinput recognize the device as wheel-capable, and
-prevents libinput from reserving the physical middle button for button
-scrolling.
-
-Gamepad support normally prefers `uhid` because descriptors, raw HID identity,
-feature reports, and output reports matter for controller compatibility. Xbox
-One and Xbox Series use backend-only Bluetooth identities with a BLE descriptor,
-sparse input bitmap, four-motor output framing, and the native report-ID `0x04`
-battery notification. When `CreateGamepadOptions::metadata.has_battery` is true,
-the descriptor exposes the notification's four categorical wireless charge
-levels through a byte-aligned standard HID Battery Strength field and the
-backend emits it only when the submitted state contains battery data. Clients
-without battery support therefore do not create a phantom Linux power device
-or receive a fabricated charge level. The normal input report keeps
-the native byte layout used by HIDAPI while advertising `Rx`/`Ry` for the right
-stick and `Z`/`Rz` for the triggers, so Linux evdev exposes the canonical
-`ABS_RX`/`ABS_RY` and `ABS_Z`/`ABS_RZ` axes expected by Steam. This keeps the bus,
-vendor/product identity, descriptor, and reports consistent so Linux input,
-Steam, SDL2, and SDL3 select the canonical Xbox mapping and can expose ordinary
-and independent trigger rumble. A Bluetooth transport is necessary because
-Linux HIDAPI implementations require a physical USB parent for `BUS_USB`
-hidraw devices, which a user-space UHID device cannot provide. This transport
-override does not change the public Xbox profiles or the platform-neutral API.
-
-Generic and Xbox 360 profiles use `uinput` so SDL, Steam, browser Gamepad API
-implementations, and other evdev consumers receive canonical Linux gamepad
-events. Xbox One and Xbox Series use the same path only as a fallback when
-`/dev/uhid` cannot be opened or initialized. Face buttons, shoulders, menu
-buttons, stick clicks, and Guide use their native evdev codes; sticks use
-absolute axes. Every uinput gamepad exposes its directional pad through
-`ABS_HAT0X` and `ABS_HAT0Y`. Generic and Xbox triggers remain independent analog
-`ABS_Z` and `ABS_RZ` axes. Profiles with rumble support normalize rumble,
-constant, periodic, and ramp uinput force-feedback effects back into the public
-callback. Each requested playback repetition restarts the effect's ramp and
-envelope timing. A zero-length effect remains active until its explicit stop
-event, matching the infinite-effect contract used by SDL and Steam. The Linux
-backend lets a new uinput device settle before reading those effects, so an
-early poll error cannot disable feedback for the device lifetime.
-
-Generated UHID nodes are correlated by stable physical and unique identifiers
-when available, with device-name matching used only as a fallback. UHID
-identities include the virtual profile's vendor and product IDs so applications
-do not reuse metadata from another profile after the same virtual slot changes
-profiles. PlayStation rumble is read from native UHID interrupt-channel output
-reports.
-
-The Generic profile keeps its public `0x1209:0x0001` identity, USB bus, and
-Generic device name at the Linux transport boundary. Its uinput device exposes
-D-pad directions once through the standard `ABS_HAT0X` and `ABS_HAT0Y` axes,
-which avoids changing the raw button capability surface. It uses a compact
-Generic button layout rather than the sparse Xbox button slots.
-
-Xbox 360 retains its `0x045E:0x028E` identity, while its Linux uinput device uses
-the Bluetooth bus, so consumers select the sparse button mapping. The Xbox One
-and Xbox Series UHID transports use the native Bluetooth product identities
-`0x045E:0x0B20` and `0x045E:0x0B13`, respectively. Their Bluetooth HID reports
-carry canonical gamepad input, coarse battery levels, and four-motor output,
-which the backend decodes into ordinary and independent trigger-rumble
-callbacks. The backend maps the continuous percentage to the nearest native
-Xbox level exposed by SDL: 10, 40, 70, or 100 percent.
-
-If UHID is unavailable, the Xbox One and Xbox Series uinput fallbacks use the
-corresponding Bluetooth product identities (`0x0B20` and `0x0B13`, respectively),
-whose standard consumer mappings match the events that uinput exposes. The Xbox
-uinput profiles preserve the 15-slot Linux gamepad button sequence: unused
-`BTN_C`, `BTN_Z`, `BTN_TL2`, and `BTN_TR2` slots are advertised but never
-pressed, keeping face buttons, shoulders, menu buttons, Guide, L3, and R3 at
-their expected indices. D-pad directions are reported through the hat axes and
-exposed as logical buttons by standard gamepad consumers. The fallback retains
-all of those controls, analog trigger input, and ordinary force feedback, but
-Linux uinput cannot expose independent trigger motors or native Xbox battery
-notifications, so its effective profile clears trigger-rumble and battery
-support.
-
-DualShock 4 and DualSense remain on `uhid` so their descriptors, motion,
-touchpad, battery, feature reports, and profile-specific output reports stay
-available. The backend accepts PlayStation output through both UHID interrupt
-and control channels. Numbered control-channel output is normalized before
-parsing, whether the kernel includes the report number in the payload or
-provides it separately on the UHID event.
-
-The default DualShock 4 and DualSense profiles use Bluetooth framing, avoiding
-the parent-USB checks that can make virtual USB devices appear late in Steam.
-Explicit USB and Bluetooth factories remain available for consumers that
-require a particular transport. DualShock 4 Bluetooth input reports set the
-HID-present header flag required by HIDAPI consumers and include the transport
-CRC, so a running consumer can accept live input after hotplug. DualSense motion
-packing preserves the public meters-per-second-squared and degrees-per-second
-units while applying the same
-raw sensor calibration used by Inputtino. Periodic PlayStation reports are
-repacked at 100 Hz so their sequence number and sensor timestamp continue to
-advance even when controller state is unchanged. Periodic and application
-submissions are serialized so a repeated report cannot restore stale motion
-state after a newer application report.
-
-The backend opens `/dev/uhid` in nonblocking mode, matching the original
-asynchronous gamepad registration path. Its event reader is active before
-device registration begins, and creation does not report success until the
-kernel returns `UHID_START`. This keeps control-channel initialization
-available throughout registration and prevents streaming hosts from publishing
-a controller before its kernel HID device has started.
-
-On Linux, DualShock 4 and DualSense emit Sony's native `Wireless Controller`
-product name for Steam HID discovery. The requested USB or Bluetooth bus,
-descriptor, and report framing remain unchanged. This transport-only name is
-confined to the Linux backend; public profile names, Windows names, and VHF
-behavior are unchanged.
-
-Switch Pro uses Linux `uhid` with its native Nintendo descriptor and identity.
-Its backend-only UHID identity advertises Bluetooth transport because SDL2's
-Linux HIDAPI rejects virtual `BUS_USB` HIDRAW devices without a physical USB
-parent in sysfs. The public profile remains USB and its report framing is
-unchanged. The backend answers Nintendo subcommand initialization reports, and
-native `0x30` input reports carry buttons, sticks, battery state, and three live
-IMU samples.
-The public acceleration and gyroscope units remain meters per second squared
-and degrees per second; the packer converts them to Nintendo's coordinate
-system and sensor scales.
+Linux needs `uhid` for descriptor-driven profiles and `uinput` for the other
+device paths. Xbox One and Series can fall back to `uinput` when `uhid` is
+unavailable; that fallback retains ordinary input and rumble but loses native
+trigger rumble and battery notifications. FreeBSD uses `uinput` for all
+profiles and does not expose descriptor-driven PlayStation, Switch Pro, or
+Xbox features.
-Linux touchscreen and trackpad contacts use the lowest available multitouch
-slot while they are active. A newly placed contact receives a new tracking ID,
-including when it reuses a slot released by another contact, so replacing one
-finger cannot overwrite another active finger in standard evdev consumers.
-
-On descriptor-driven backends, native Switch Pro output reports `0x01` and
-`0x10` are decoded into the normalized low- and high-frequency rumble callback.
-Set Player Lights subcommand `0x30` additionally produces a `player_leds`
-callback with separate solid and flashing states for the four indicators. The
-Set HOME Light subcommand `0x38` produces a grayscale `rgb_led` callback whose
-equal channels preserve the requested monochrome intensity. The original native
-report remains available in `GamepadOutput::raw_report`.
-
-The optional `virtualhid_control` diagnostic UI uses SDL3 and Dear ImGui through
-the repository CPM lockfile. It is intended to stay on the same UI framework for
-Windows, Linux, and future macOS support. Static Linux linking is possible only
-when the target distribution provides static archives for all selected backend
-and UI dependencies, including SDL3, `libevdev`, and any enabled X11/XTest
-libraries. Many distro toolchains intentionally omit some static archives, so
-release packaging should keep full static linking as a packaging-mode choice
-rather than an unconditional default.
-
-The UI can create and exercise both gamepads and mice. Its mouse movement,
-button, and wheel controls participate in Dear ImGui keyboard navigation; use
-Tab or the arrow keys to highlight them and Space or Enter to activate them.
-Mouse buttons are momentary. A delayed browser-test mode queues an action long
-enough to switch focus to an external event tester, sending button actions as a
-single press-and-release click.
-
-### Permissions
-
-Linux deployment requires both device-node permissions and the kernel modules
-for the selected virtual-controller path. Install udev rules such as
-`/etc/udev/rules.d/60-libvirtualhid.rules`:
+Install persistent rules such as `/etc/udev/rules.d/60-libvirtualhid.rules`
+for the account running the host application:
```udev
-# Allows libvirtualhid consumers to access /dev/uinput
KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", GROUP="input", MODE="0660", TAG+="uaccess"
-
-# Allows libvirtualhid consumers to access /dev/uhid
KERNEL=="uhid", GROUP="input", MODE="0660", TAG+="uaccess"
-```
-
-UHID gamepads use a stable `libvirtualhid/uhid/*` physical path even when the
-library is compiled directly into a consuming application. Match that path for
-generated `hidraw` and input event nodes because native profiles such as
-DualShock 4 and DualSense intentionally do not retain the application's product
-name. For `hidraw`, Linux exposes `HID_PHYS` and `HID_NAME` as uevent properties
-on the HID parent rather than as sysfs attributes. Import those parent
-properties before matching them:
-
-```udev
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", IMPORT{parent}="HID_*"
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ENV{HID_PHYS}=="libvirtualhid/uhid/*", GROUP="input", MODE="0660", TAG+="uaccess"
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", GROUP="input", MODE="0660", TAG+="uaccess"
```
-Do not replace the `hidraw` import and `ENV{HID_PHYS}` match with
-`ATTRS{phys}`. `udevadm verify` validates rule syntax, but it does not prove
-that the matched sysfs attribute exists on the device's parent chain.
-
-Consuming applications may additionally install name-matched rules for stable
-virtual device names, including uinput-backed gamepads. The `hidraw` rule below
-uses `HID_NAME` imported by the preceding `IMPORT{parent}` rule:
-
-```udev
-SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ENV{HID_NAME}=="Your App Controller*", GROUP="input", MODE="0660", TAG+="uaccess"
-SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="Your App Controller*", GROUP="input", MODE="0660", TAG+="uaccess"
-```
+The `hidraw` rule imports `HID_PHYS` from the HID parent before matching it.
+A one-time `chmod` on a generated device node will not survive device
+recreation. Load `uhid` and `uinput`, and load `hid_playstation` when using
+the kernel feedback path for PlayStation controllers:
-For gamepad support, install a modules-load entry such as
-`/etc/modules-load.d/60-libvirtualhid.conf`. `hid_playstation` enables the
-kernel force-feedback path used by virtual DualShock 4 and DualSense
-controllers; descriptor-aware HIDAPI clients can also write their native
-output reports through `hidraw`:
-
-```text
-uhid
-uinput
-hid_playstation
-```
-
-After installing the rules, load the modules, reload udev, and trigger the
-device nodes:
-
-```bash
-sudo modprobe uhid
-sudo modprobe uinput
-sudo modprobe hid_playstation
+```sh
+sudo modprobe -a uhid uinput hid_playstation
sudo udevadm control --reload-rules
sudo udevadm trigger --property-match=DEVNAME=/dev/uinput
sudo udevadm trigger --property-match=DEVNAME=/dev/uhid
@@ -387,68 +64,23 @@ sudo udevadm trigger --subsystem-match=hidraw
sudo udevadm trigger --subsystem-match=input
```
-UHID gamepad nodes are recreated whenever a consumer destroys and recreates a
-virtual controller. Manual `chmod` or `setfacl` changes apply only to the
-current node and disappear after recreation; install a matching udev rule for
-persistent access.
-
-If input still does not work, add the user running the consuming application to
-the `input` group, then log out and back in:
-
-```bash
-sudo usermod -aG input $USER
-```
+The consuming user may need membership in the `input` group and a new login
+session. Desktop logins may instead receive access through `uaccess`.
## FreeBSD
-The FreeBSD backend uses the native evdev compatibility stack through
-`libevdev` and uinput. It accepts both `/dev/input/uinput`, which is the native
-FreeBSD path, and `/dev/uinput` for environments that provide the Linux-style
-alias. It supports the same uinput device categories as the Linux backend:
-
-- Generic, Xbox 360, Xbox One, Xbox Series, DualShock 4, DualSense, and Switch
- Pro gamepads.
-- Keyboard and mouse devices, with X11/XTest available as a fallback.
-- Touchscreen, trackpad, and pen tablet devices.
-
-FreeBSD's [uhid(4)](https://man.freebsd.org/cgi/man.cgi?query=uhid&sektion=4)
-is not the Linux UHID transport. It exposes an existing physical USB HID
-interface through `/dev/uhid?`; it does not let a process register a new device
-with the kernel HID bus. FreeBSD CUSE applications such as
-[uhidd(8)](https://man.freebsd.org/cgi/man.cgi?query=uhidd&sektion=8) can emulate
-a `uhid(4)`-compatible character device for direct consumers, but that is a
-different integration surface and is not used by the current backend.
-
-Generic, Xbox-family, Switch Pro, DualShock 4, and DualSense behavior therefore
-uses uinput. Ordinary buttons, sticks, analog triggers, and rumble are available,
-but raw HID reports and descriptor-driven features are not.
-
-For each created gamepad, `Gamepad::profile()` reports the effective FreeBSD
-uinput capability subset. Motion, touchpad contacts and click, battery state,
-RGB LED output, adaptive-trigger output, and raw HID output reports are disabled.
-This includes Switch Pro motion and battery state as well as the
-PlayStation-specific features. Streaming-host adapters can reject those
-operations instead of silently accepting state that uinput cannot expose.
-
-The `uinput` kernel module and a writable uinput device node are required.
+The `uinput` kernel module and a writable `/dev/input/uinput` or
+`/dev/uinput` device are required. Generic and Xbox-family profiles provide
+ordinary controls and rumble. PlayStation and Switch Pro profiles do not expose
+Linux `uhid` features such as motion, battery, or native output reports.
## macOS
-Gamepads use a licensed, signed user-space `IOHIDUserDevice` broker installed
-as a LaunchDaemon. The public C++ API and packed reports stay platform-neutral;
-only the broker owns the Apple virtual HID entitlement. All built-in gamepad
-profile descriptors are accepted, including Xbox-family, DualShock 4,
-DualSense, and Switch Pro. The broker handles input, output, PlayStation
-feature reports, and Switch Pro initialization replies. macOS presents Xbox
-360 as HID rather than Windows XInput/XUSB. Consumer recognition still depends
-on each game's macOS controller stack and needs installed validation.
-
-Keyboard and mouse input use CoreGraphics for UTF-8 text,
-portable key translation, modifier state, relative and absolute motion, and
-pixel-based scrolling. The host process needs macOS synthetic-input permission
-when the system requires it. Touchscreen, trackpad, and pen tablet creation
-return `unsupported_profile`.
+Gamepads require the installed, signed, licensed broker and macOS permission
+to create virtual HID devices. Keyboard and mouse use CoreGraphics and may
+require synthetic-input permission. Touchscreen, trackpad, and pen tablet
+creation are unavailable. See [macOS setup](macos-gamepad.md) for installation
+and diagnostics.
-Gamepad creation requires a machine license. The broker accepts the Yearly and
-Lifetime Polar benefits. See [macOS gamepad setup](macos-gamepad.md) for the
-universal build, signing, installation, and diagnostics.
+A game recognizing a profile still depends on that game's input stack. For
+streaming, see the [end-user compatibility matrix](end-user-gamepad-guide.md#compatibility-matrix).
diff --git a/docs/streaming-host-integration.md b/docs/streaming-host-integration.md
index 22deb5c1..d7767d31 100644
--- a/docs/streaming-host-integration.md
+++ b/docs/streaming-host-integration.md
@@ -1,70 +1,24 @@
-# Streaming-Host Integration
-
-Remote streaming hosts are the first consumer class for `libvirtualhid`.
-Integration should preserve a host application's existing network protocol,
-client input parsing, configuration, feedback queue, and device lifecycle while
-moving local virtual device creation behind the `libvirtualhid` API.
-
-## Integration Contract
-
-A streaming host should be able to:
-
-- Create stable per-client gamepad handles with both client-relative and global
- indexes.
-- Submit incremental button, axis, trigger, touchpad, motion, and battery
- updates without recreating a device.
-- Receive output callbacks for rumble, RGB and player LEDs, adaptive triggers,
- trigger rumble, and raw output reports where the selected profile supports
- them.
-- Query profile and backend capabilities before warning users about unsupported
- client features.
-- Read device nodes and platform paths when a downstream consumer or diagnostic
- needs to inspect SDL, HIDAPI, libinput, `hidraw`, or system device state.
-- Use keyboard and mouse APIs for relative mouse, absolute mouse, buttons,
- wheel, horizontal wheel, key events, and Unicode text input.
-
-On Linux and FreeBSD, one mouse handle may represent separate relative and
-absolute uinput nodes. Consumers should keep using the platform-neutral mouse
-API; the backend routes motion and matching button transitions to the correct
-node.
-
-`libvirtualhid` should not own the host application's network transport, packet
-schema, configuration model, controller assignment policy, or status API.
-
-## Adapter Pattern
-
-The `examples/gamepad_adapter.cpp` example demonstrates the
-intended shape:
-
-- Choose a built-in `DeviceProfile` from a host-facing profile name.
-- Fill `CreateGamepadOptions` with stable controller metadata.
-- Create a `GamepadStateAdapter` from a `Runtime`.
-- Cache state inside the adapter as separate input events arrive.
-- Submit an initial neutral report so operating-system consumers can enumerate
- the virtual controller before the first client input packet.
-- Forward output callbacks back to the physical client controller or feedback
- queue.
-
-This keeps one public code path for Linux, Windows, and future platforms while
-still letting each backend report real capability limits.
-
-## Current Readiness
-
-The core API and adapter shape cover the major streaming-host requirements:
-
-- Multiple controller lifecycles.
-- Built-in profiles for common controller classes.
-- Rich controller metadata.
-- Gamepad output callbacks.
-- Keyboard and mouse input paths.
-- Linux PlayStation, Switch Pro, Xbox One, and Xbox Series gamepads through
- descriptor-driven `uhid`, Generic and Xbox 360 gamepads through `uinput`,
- Xbox One and Xbox Series uinput fallbacks, and `uinput` keyboard/pointer
- devices.
-- Native Switch Pro motion, initialization replies, rumble, HOME-light, and
- player-light output handling on Linux and Windows descriptor-driven backends.
-- Linux DualSense and DualShock 4 USB/Bluetooth report handling.
-- Linux touchscreen, trackpad, and pen tablet device types.
-- FreeBSD uinput gamepads and pointer devices, with basic PlayStation input and
- rumble but without Linux UHID-only PlayStation features.
-- Windows UMDF/VHF gamepad creation through an installed driver package.
+# Streaming-host integration
+
+`libvirtualhid` creates local virtual devices for a streaming host. The host
+keeps its network protocol, client input mapping, controller assignment,
+configuration, and feedback transport.
+
+Use [`GamepadStateAdapter` in the example](../examples/gamepad_adapter.cpp)
+as the integration pattern:
+
+1. Choose a built-in profile and stable controller metadata.
+2. Create one gamepad per active client controller and submit an initial
+ neutral state.
+3. Apply button, axis, trigger, touch, motion, and battery updates as the
+ client sends them.
+4. Forward output callbacks, such as rumble and LEDs, to the client when the
+ selected profile and backend support them.
+5. Destroy the gamepad on disconnect; recreate it after a broker restart or
+ other loss of the local device.
+
+Check runtime and effective profile capabilities before advertising optional
+features. `DeviceNode` paths can help diagnose host-side enumeration; client
+feature support still depends on the physical controller, connection, client,
+and game. See the [end-user compatibility matrix](end-user-gamepad-guide.md#compatibility-matrix)
+for observed streaming behavior.
diff --git a/docs/todo.md b/docs/todo.md
deleted file mode 100644
index e8d1460c..00000000
--- a/docs/todo.md
+++ /dev/null
@@ -1,36 +0,0 @@
-# TODO
-
-This page tracks known compatibility work that is larger than a small report
-packing fix. Keep entries here until the repo has a validated implementation
-path and consumer tests or manual validation prove the behavior.
-
-## Controller Capture Tooling
-
-Status: proposed.
-
-Manual capture files are useful while debugging controller compatibility, but
-hand-curated JSON is not a durable source-control artifact. Future captures
-should be generated by the same tool that users run, with a documented schema
-and predictable redaction behavior.
-
-Proposed solution:
-
-1. Add a capture/export mode to `virtualhid_control` that can enumerate
- connected HID/gamepad devices, let the user select one, and save a JSON
- capture file.
-2. Make the capture schema useful for any controller type, not only the
- profiles currently implemented by libvirtualhid.
-3. Record stable evidence by default: VID/PID, release number, manufacturer,
- product, transport, HID descriptor/report metadata, supported feature/output
- report IDs, public PnP compatible IDs, and consumer observations when the
- tool can collect them.
-4. Redact machine-unique values by default, including instance IDs, serial-like
- values, Bluetooth addresses, container IDs, local file paths, and driver INF
- names. Add an explicit unsafe/debug option only if raw local evidence is
- needed.
-5. Keep JSON support scoped to the tool target. If this is implemented in C++,
- prefer `nlohmann/json`, but do not expose that dependency through the public
- libvirtualhid API.
-6. Treat generated capture files as local debugging artifacts by default. Only
- commit a generated capture later if it is reproducible, sanitized, and used
- by tests or documentation in a way that justifies keeping it.
diff --git a/docs/usage.md b/docs/usage.md
index 9d1e8a30..c92e3d94 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -1,194 +1,30 @@
# Usage and API
-This page covers how consumers bring `libvirtualhid` into a CMake project and
-which public API concepts they should build around.
+This page is for developers embedding `libvirtualhid`. If you use Sunshine,
+start with the [end-user guide](end-user-gamepad-guide.md).
-If you use Sunshine or another application that already embeds the library,
-see the [end-user gamepad guide](end-user-gamepad-guide.md) instead.
+## Add the CMake target
-## CMake Consumption
-
-The library exports `libvirtualhid::libvirtualhid`.
-
-For an installed package, install this project into a prefix and point the
-consumer configure at that prefix:
-
-```bash
-cmake --install cmake-build-release --prefix /opt/libvirtualhid
-cmake -S your-app -B cmake-build-your-app -DCMAKE_PREFIX_PATH=/opt/libvirtualhid
-```
-
-Then link the exported package from the consuming project:
+An installed package exports `libvirtualhid::libvirtualhid`:
```cmake
find_package(libvirtualhid CONFIG REQUIRED)
target_link_libraries(your_app PRIVATE libvirtualhid::libvirtualhid)
```
-For a vendored checkout, add the project directly and link the same target:
+Point `CMAKE_PREFIX_PATH` at the installation prefix if CMake cannot find it.
+For a vendored checkout, use:
```cmake
add_subdirectory(third-party/libvirtualhid)
target_link_libraries(your_app PRIVATE libvirtualhid::libvirtualhid)
```
-For `FetchContent`, pin a tag or commit and make the project available:
-
-```cmake
-include(FetchContent)
-
-FetchContent_Declare(
- libvirtualhid
- GIT_REPOSITORY https://github.com/LizardByte/libvirtualhid.git
- GIT_TAG
-)
-FetchContent_MakeAvailable(libvirtualhid)
-
-target_link_libraries(your_app PRIVATE libvirtualhid::libvirtualhid)
-```
-
-Examples, tests, docs, and the Windows driver and macOS broker packages are top-level or opt-in
-builds. Normal vendored and `FetchContent` consumers only get the library target
-unless they explicitly enable additional options.
-
-## Build Options
-
-- `BUILD_EXAMPLES`: build example executables when this repository is the top
- level project.
-- `BUILD_TESTS`: build the GoogleTest suite when this repository is the top
- level project.
-- `BUILD_DOCS`: build Doxygen documentation when this repository is the top
- level project.
-- `LIBVIRTUALHID_BUILD_MACOS_BROKER`: build the entitlement-bearing macOS
- broker app. Enabled for top-level macOS builds; see
- [macOS gamepad setup](macos-gamepad.md) for provisioning and installation.
-- `LIBVIRTUALHID_BUILD_TOOLS`: build diagnostic tool binaries, including
- `virtualhid_control`, when this repository is the top level project.
-- `LIBVIRTUALHID_TOOLS_STATIC_RUNTIME`: link diagnostic tools against static
- compiler runtimes where supported. This is enabled by default so the Windows
- MinGW/UCRT64 `virtualhid_control.exe` does not need adjacent MinGW runtime
- DLLs. MSVC uses the static runtime for tools in the Windows driver package
- build, where the packaged library, examples, and tools are all built with the
- same runtime setting.
-- `LIBVIRTUALHID_TOOLS_FULLY_STATIC`: pass full static link flags for
- diagnostic tools. On Linux this also requires static archives for backend
- dependencies such as `libevdev`, and may not be supported by every distro.
-- `LIBVIRTUALHID_TOOLS_STATIC_SDL3`: prefer SDL3's static library target for
- the diagnostic UI when it is available. This defaults to on.
-- `LIBVIRTUALHID_INSTALL`: install targets, headers, and CMake package files.
- This defaults to on for direct builds and off when consumed by another CMake
- project.
-- `LIBVIRTUALHID_ENABLE_XTEST`: enable the Linux X11/XTest keyboard and mouse
- fallback.
-- `LIBVIRTUALHID_BUILD_WINDOWS_DRIVER`: build the Windows UMDF2 driver package
- with the Microsoft WDK/MSVC toolchain.
-- `LIBVIRTUALHID_BUILD_WINDOWS_BROKER`: build the Windows broker service used by
- the driver package for licensed virtual HID device creation, active-device
- limits, and license state.
-- `LIBVIRTUALHID_ENABLE_PACKAGING`: enable CPack package metadata.
-- `LIBVIRTUALHID_WARNINGS_AS_ERRORS`: treat project warnings as errors.
-
-Linux consumers need the backend development packages used by the build, such as
-`libevdev` and `pkg-config`. Windows consumers can build the normal C++ library
-with MSVC or MinGW/UCRT64; the UMDF driver package is a separate WDK/MSVC build
-artifact.
-
-## Diagnostic UI
-
-`virtualhid_control` is an optional SDL3 and Dear ImGui diagnostic UI binary:
-
-```bash
-virtualhid_control
-```
-
-The UI is built from the repository CPM lockfile so Windows, Linux, and
-macOS builds share the same frontend stack. Builds prefer static SDL3 by
-default when a static target is available.
-
-The UI can create and remove gamepads from the built-in profiles, submit
-buttons, sticks, triggers, and battery state, show backend and profile
-capabilities, list device nodes reported for UI-created devices, and display
-normalized gamepad output such as rumble, RGB LED, player LED, adaptive trigger,
-trigger rumble, and raw report events delivered through the normal callback path. Button
-controls are momentary by default, so they behave like physical gamepad buttons;
-on Windows and macOS, the UI also displays broker license status and can activate,
-refresh, or deactivate a machine license without elevation. Windows UMDF
-virtual HID device creation requires a current machine authorization, but does
-not perform an online request per device. The broker validates in the
-background at startup and once per day. If Polar cannot be reached, it retries
-every 60 seconds. Existing devices are retained for one hour, but a new device
-can be created only when no licensed device is active. When the outage reaches one
-hour, the broker removes excess licensed devices and retains at most one. A
-yearly subscription must validate successfully within 25 hours of its previous
-validation, so the remaining device is removed when that deadline passes. A
-lifetime license can retain the one-device fallback until validation succeeds.
-If the broker service restarts, it removes devices left by the previous broker
-instance before accepting new creation requests. Failed removals are retried.
-Both supported plans rely on Polar's entitlement status rather than a locally
-enforced calendar expiration. A granted yearly license follows its subscription
-benefit, which Polar revokes when the entitlement ends. Polar's public license
-response does not include the subscription renewal date; the linked Polar account
-portal remains authoritative instead of the broker estimating a date. Polar
-server time, Windows uptime, and a per-boot marker track subscription validation
-age without relying on the user-adjustable Windows date. After Windows restarts,
-a yearly subscription must reconnect to Polar before device creation; a lifetime
-license can use the one-device outage fallback. A confirmed missing, revoked,
-disabled, or mismatched entitlement invalidates the license and removes all
-licensed virtual HID devices.
-Each successful licensed gamepad creation adds one pending Polar usage unit.
-The broker saves the pending count locally and sends it as `increment_usage`
-with its next successful license validation (normally within 24 hours, or on
-manual refresh). Routine validations do not increment usage when no gamepad
-was created. Keyboard and mouse creation and GitHub Actions evaluation do not
-count. Reporting is best effort: a lost response after Polar accepts an
-increment can cause a later retry to count it twice, while a local state-write
-failure or license deactivation before a pending count is sent can lose counts.
-Do not use Polar's usage limit as an exact device-creation quota.
-Purchase and account-management buttons use the compiled URLs in
-`src/platform/windows/shared/lvh_windows_broker_config.hpp`.
-Enable `Lock buttons` to click-to-toggle behavior for held inputs.
-The resizable window supports a compact width. Its device and control panels
-stack, and the button grid reflows to keep controls usable when it is narrowed.
-The UI intentionally does not use gamepad navigation, so virtual devices created
-by the tool cannot drive the tool's own controls.
+`FetchContent` consumers can use the same target after pinning a release tag or
+commit. Tests, examples, documentation, and platform packages are top-level or
+opt-in builds; ordinary subdirectory consumers get the library.
-External devices created by another process, such as Sunshine, are not
-enumerated yet. That requires backend protocol support, so the Windows driver or
-Linux backend can expose cross-process device snapshots without letting two
-processes race to control the same virtual device.
-
-## Public API Shape
-
-The API centers on portable device concepts:
-
-- `Runtime`: owns backend discovery, initialization, device creation, and
- shutdown.
-- `get_license_status`, `activate_license`, `validate_license`, and
- `deactivate_license`: provider-neutral machine license operations for host
- applications. On Windows and macOS these call the installed local broker;
- license keys are not retained by the client library or returned to the
- application. The Windows client verifies that the named-pipe server is the
- SCM-registered running broker. The macOS client verifies that its Unix socket
- and peer are owned by root before sending any request.
- `LicenseStatus::activation_limit` is the license-wide machine limit.
- `LicenseStatus::activation_usage` reports whether this machine has an
- activation (0 or 1); Polar does not return the license-wide activation count
- to the broker. Use the customer portal to view activations across machines.
-- `VirtualDevice`: common lifecycle for created devices.
-- `Gamepad`: submits normalized gamepad state and receives output callbacks.
-- `Keyboard`: submits key press/release and UTF-8 text input.
-- `Mouse`: submits relative motion, absolute motion, buttons, vertical scroll,
- and horizontal scroll.
-- `Touchscreen`, `Trackpad`, and `PenTablet`: expose touch and tablet device
- primitives where the backend supports them.
-- `DeviceProfile`: describes device identity, HID descriptors, report layout,
- and profile capabilities.
-- `DeviceNode`: reports backend-visible device nodes and paths for diagnostics
- or handoff to SDL, libinput, HIDAPI, and similar consumers.
-- `BackendCapabilities`: reports runtime/backend limits such as virtual HID,
- output report, keyboard, mouse, XTest fallback, and installed-driver support.
-
-## Gamepad Example
+## Create and update a gamepad
```cpp
#include
@@ -202,7 +38,7 @@ if (!created) {
auto &gamepad = *created.gamepad;
gamepad.set_output_callback([](const lvh::GamepadOutput &output) {
if (output.kind == lvh::GamepadOutputKind::rumble) {
- // Route rumble back to the physical client controller.
+ // Forward feedback to the client controller.
}
});
@@ -210,58 +46,35 @@ lvh::GamepadState state;
state.buttons.set(lvh::GamepadButton::a, true);
state.left_stick = {0.25F, -0.5F};
state.right_trigger = 1.0F;
-
gamepad.submit(state);
```
-The `examples/gamepad_adapter.cpp` example shows the
-streaming-host-oriented adapter path. It maps incremental button, axis, trigger,
-touch, motion, battery, feedback, and lifecycle updates onto the platform-neutral
-`Runtime` and `Gamepad` APIs.
-
-## Built-In Profiles
-
-Built-in gamepad profiles and their platform-neutral default device names are:
+Keep the `Gamepad` alive for the session and destroy it when the client
+disconnects. Query runtime and effective profile capabilities before exposing
+optional inputs or feedback. The
+[gamepad adapter example](../examples/gamepad_adapter.cpp) shows incremental
+updates, metadata, output callbacks, and lifecycle handling.
-| Profile | Default device name |
-|-------------------------------|-------------------------------------------|
-| Generic HID gamepad | `(libvirtualhid) Generic Controller` |
-| Xbox 360 | `(libvirtualhid) X-Box 360 Controller` |
-| Xbox One | `(libvirtualhid) X-Box One Controller` |
-| Xbox Series | `(libvirtualhid) X-Box Series Controller` |
-| DualShock 4 USB and Bluetooth | `(libvirtualhid) PS4 Controller` |
-| DualSense USB and Bluetooth | `(libvirtualhid) PS5 Controller` |
-| Nintendo Switch Pro | `(libvirtualhid) Nintendo Pro Controller` |
+Built-in profiles cover Generic HID, Xbox 360, Xbox One, Xbox Series,
+DualShock 4, DualSense, and Switch Pro. PlayStation profiles offer explicit USB
+and Bluetooth variants. Device names can be customized through
+`DeviceProfile::name`.
-Consumers may replace `DeviceProfile::name` before creating a gamepad, for
-example, to prepend an application name while preserving the default controller
-identity across platform backends.
+The API also exposes `Keyboard`, `Mouse`, `Touchscreen`, `Trackpad`, and
+`PenTablet` where supported. `DeviceNode` provides paths for diagnostics or
+handoff to another local input consumer.
-`profiles::dualshock4()` and `profiles::dualsense()` select Bluetooth framing
-for reliable native-controller discovery. Consumers can use the corresponding
-`_usb()` or `_bluetooth()` factory when the transport must be explicit.
+## License and diagnostic tool
-The platform-neutral Generic HID descriptor reports the D-pad as buttons 13
-through 16 in the input report. Linux may still route that profile through
-`uinput`, where the backend exposes those same logical directions through the
-standard `ABS_HAT0X` and `ABS_HAT0Y` axes.
+On Windows and macOS, `get_license_status`, `activate_license`,
+`validate_license`, and `deactivate_license` manage the machine license through
+the installed broker. Treat activation keys as transient secrets; do not log or
+persist them. The broker, not the application, maintains the activation.
-Profiles advertise support for features such as rumble, trigger rumble, RGB and
-player LEDs, adaptive triggers, motion sensors, touchpads, battery state,
-profile-specific buttons, and raw output reports. Consumers should query
-profile and backend capabilities before warning users about unsupported client
-features. Xbox One and Xbox Series advertise `supports_trigger_rumble` and
-`supports_battery`; the Linux UHID Bluetooth transport preserves both
-capabilities, while the uinput fallback clears them and retains ordinary
-rumble. The Linux Xbox transport includes its battery descriptor only when
-`CreateGamepadOptions::metadata.has_battery` is true, and it emits battery
-reports only for submitted states that contain battery data. On Windows, the
-Xbox HID report carries battery strength, but consumers
-that prefer XInput do not receive the submitted remote value. The current Steam
-client also hides its controller battery indicator for devices it does not
-classify as Bluetooth or wireless; Windows VHF exposes a wired virtual transport
-for every profile.
+The optional `virtualhid_control` tool creates test devices, displays
+capabilities and feedback, and manages the broker license on Windows and
+macOS. It lists devices created by that tool, not devices owned by other
+processes.
-The `misc1` button represents Share/Capture/Mic Mute-style controls and is
-available on the generic, Xbox Series, DualSense, and Switch Pro profiles; Xbox
-360 and Xbox One do not advertise that extra button.
+See [platform support](platform-support.md) for availability and
+[development](development.md) for build commands and optional targets.
diff --git a/docs/windows-driver.md b/docs/windows-driver.md
index da8edfc6..9a9d6b12 100644
--- a/docs/windows-driver.md
+++ b/docs/windows-driver.md
@@ -1,232 +1,75 @@
-# Windows Driver Package
-
-Windows virtual gamepad, keyboard, and Raw Input mouse support uses a user-mode
-UMDF2 package. Most devices are backed by Virtual HID Framework. The Xbox 360
-profile uses a second, per-controller UMDF2 XUSB personality and also publishes
-a VHF HID child for HID/DirectInput compatibility. The driver package is
-separate from the normal C++ library build: the library remains consumable from
-MSVC and MinGW/UCRT64, while the driver package is built with the Microsoft
-SDK/WDK toolchain.
-
-Windows 11 version 21H2 and later is the supported driver target. The INF also
-provides a best-effort compatibility path for Windows 10 version 2004 and later
-by using explicit `WUDFRd` service registration, but Windows 10 is not an
-officially supported driver target.
-
-## Microsoft Store Listing Text
-
-The Windows driver package is not the same product surface as the C++ library,
-so Store listing copy should describe the installed driver component.
-
-### Short Description
-
-```text
-User-mode virtual HID driver package that enables compatible apps to create virtual gamepads, keyboards, and Raw Input mice on Windows.
-```
-
-### Description
-
-```text
-Virtual HID Driver installs the user-mode driver component used by compatible
-applications to create virtual HID gamepads, keyboards, and mice on Windows.
-
-The package includes a local diagnostic UI for creating and testing virtual
-gamepads and mice. Compatible applications can also request virtual HID gamepads
-keyboards, or mice, and Windows applications that understand standard HID
-devices can discover them.
-```
-
-## Architecture
-
-Windows driver-device creation is brokered by `libvirtualhid_broker`. The normal
-C++ backend asks the broker service to create and destroy virtual HID devices
-through a local named pipe, while input reports stay on the direct driver path
-after creation. This keeps license and active-device checks outside the input hot
+# Windows driver package
+
+The Windows package lets compatible applications create virtual gamepads,
+Raw Input-visible keyboards, and relative mice. It uses a user-mode UMDF2
+driver and a local broker service. Xbox 360 uses an XUSB companion for XInput;
+other profiles use virtual HID. No libvirtualhid kernel-mode driver is
+installed.
+
+The released MSI targets Windows 11 version 21H2 or later on AMD64. Windows 10
+version 2004 or later has a best-effort compatibility path. Windows ARM64
+release packages are not yet available.
+
+## Install and use
+
+Install the production-signed MSI from the
+[libvirtualhid releases](https://github.com/LizardByte/libvirtualhid/releases).
+Restart Windows if the installer requests it. The package installs the
+`libvirtualhid_broker` service and a diagnostic tool at
+`C:\Program Files\libvirtualhid\tools\windows\virtualhid_control.exe` by
+default. Applications can then use the normal C++ API without administrator
+privileges.
+
+A machine license is required to create a driver-backed device. Open
+`virtualhid_control.exe` to activate or refresh a license, select a gamepad
+profile, and create a test controller. The tool can submit buttons, axes,
+triggers, and mouse actions and show supported feedback. It lists devices
+created by the tool; controllers owned by another application are not listed.
+
+Applications may use the public `get_license_status`, `activate_license`,
+`validate_license`, and `deactivate_license` APIs to offer the same workflow.
+Do not log or store activation keys. A machine activation covers licensed
+gamepads, keyboards, and mice on that machine. Successful licensed gamepad
+creations are reported as usage on a later validation; routine license checks
+do not count as gamepad creation.
+
+The broker validates the license at startup and every 24 hours, retrying
+temporary failures about once a minute. While Polar validation is unavailable,
+new driver-backed creation is limited to one active licensed device in total,
+whether gamepad, keyboard, or mouse. Existing licensed devices remain for up
+to one hour; then the broker removes all but one. A yearly license must
+validate within 25 hours of its last successful validation or the remaining
+device is removed. After Windows restarts, a yearly license needs online
+validation before device creation. A previously activated lifetime license
+can keep or create one licensed device while Polar is unreachable, with no
+offline time limit. The broker keeps retrying validation about once a minute
+and returns to the normal 24-hour schedule after success. Confirmed revocation
+or deactivation removes existing licensed devices.
+
+Keyboard and mouse have Win32 fallbacks when the driver or license is
+unavailable. Those fallback inputs are not Raw Input-visible virtual HID
+devices. Unicode text and absolute mouse positioning always use the Win32
path.
-Each UMDF service runs in a dedicated high-priority host process. This isolates
-its input work from normal-priority UMDF device pools while keeping all
-libvirtualhid driver code in user mode. The package relies only on Microsoft's
-inbox UMDF reflector and VHF lower filter in kernel mode; it does not install a
-libvirtualhid `.sys` driver.
-
-The broker pipe explicitly grants local authenticated users generic read access
-plus the individual data-write and attribute-write rights needed to exchange
-request and response messages in message mode. It does not grant clients the
-right to create pipe instances, and it rejects remote clients. This allows a
-normal desktop application to use the broker without running as administrator
-while keeping broker ownership and privileged device operations in the Windows
-service.
-
-A client that finds the pipe missing waits up to five seconds for it, in case
-the broker is still starting. It first asks the service manager whether the
-service can still answer: if the `libvirtualhid_broker` service is not
-installed, or is stopped or stopping, the request fails at once with
-`ERROR_SERVICE_DOES_NOT_EXIST` or `ERROR_SERVICE_NOT_ACTIVE` instead of
-spending the wait. The client never starts the service itself, and the service
-has no trigger start, so nothing would have appeared. Any other service state,
-or an unreadable service manager, keeps the wait.
-
-Status, current-license validation, activation, replacement, deactivation,
-virtual HID device creation, and owned-device destruction are available to
-authenticated local users without elevation. Before sending any request, clients compare the
-named-pipe server PID to the SCM-registered, currently running
-`libvirtualhid_broker` service. This prevents another local process from
-impersonating an unavailable broker and collecting a license key. The service
-also requests first ownership of the pipe name and rejects remote clients.
-
-All broker messages are fixed-size and fully validated before use, including
-protocol versions, exact byte counts, request types, reserved fields, enums,
-array bounds, string terminators, and unused payload bytes. Connection, request,
-and response operations use cancellable overlapped I/O with explicit completion
-and byte-count checks, so a stopped service or disconnected client cannot leave
-an operation using expired stack state.
-
-The backend sends fixed-size C protocol structures to the broker. A create
-request identifies the backend's existing control handle; the broker duplicates
-that handle from the named-pipe client process and issues `DeviceIoControl` on
-the same file object. This starts a VHF child device from the requested
-descriptor, VID/PID, version, and report layout while preserving handle-scoped
-output delivery. The driver returns a per-device session token, and
-submit/destroy requests include that token so stale or unrelated clients cannot
-control devices they did not create. Input reports are submitted through VHF,
-and HID output writes are normalized back to the C++ output callback path.
-
-Xbox 360 creation takes a separate path because an authentic wired Xbox 360
-controller is XUSB rather than a standard HID-only device. For every requested
-Xbox 360 controller, the broker calls `SwDeviceCreate` with a unique instance
-ID, an explicit non-null container ID, and bare `VID_...` and
-`LIBVIRTUALHID_XBOX360` hardware IDs. The package INF matches the default
-`VID_045E&PID_028E&XI_00` identity directly so Windows gives it the highest
-driver-selection rank, retains `LIBVIRTUALHID_XBOX360` as the fallback for
-custom VID/PID profiles, and keeps `ROOT\LIBVIRTUALHID_XBOX360` as a
-provisioning alias. The resulting device instance remains under the
-`SWD\LibVirtualHid` enumerator. Windows binds the package's
-`libvirtualhid_xbox360_umdf.dll` to that System-class software devnode. The
-companion publishes exactly one XUSB interface using
-`{EC87F1E3-C13B-4100-B5F7-8B84D54260CB}`, and creates a VHF child with the
-profile's `VID_045E&PID_028E&IG_00` identity. The shared container and ancestor
-metadata let Windows correlate the XUSB and HID views as one controller while
-retaining DirectInput/HID compatibility.
-
-On Windows Server SKUs, the package omits the `xinputhid` upper-filter marker
-from the Xbox companion. That classifier belongs to the client gaming stack and
-is not required for XInput discovery; registering it on a server where the
-service is unavailable prevents PnP from completing the software-device stack.
-The XUSB interface and correlated VHF child remain enabled on server systems.
-
-The broker opens the XUSB interface, authenticates an initialization request by
-its SCM-registered process ID, and duplicates that per-device handle into the
-requesting client. Subsequent input and feedback operations require both the
-broker-assigned device ID and a random 256-bit session token. The client submits
-native 16-bit sticks, independent 8-bit triggers, and XInput button bits through
-that handle. The companion answers XInput state/capability requests, completes
-the asynchronous XUSB input wait at an 8-millisecond cadence, and returns
-XInput rumble as the normal platform-neutral output callback. Only the broker
-retains the `HSWDEVICE`; destroying the API gamepad, losing the owning client,
-or stopping the broker closes it and removes both device views.
-
-`SwDeviceCreate`, UMDF2, and VHF are documented Windows facilities. The XUSB
-interface GUID, IOCTL numbers, and byte layouts are not a supported public
-Microsoft driver contract. They are an explicitly isolated compatibility layer
-based on observed inbox XInput behavior and HIDMaestro's independently
-documented companion design. Keep that wire protocol private to the Windows
-backend and regression-test it on every supported Windows release.
+## Check an installation
-The driver owns the VHF input buffering policy instead of allowing VHF to build
-the default HID report backlog. VHF readiness notifications permit one report at
-a time; while a gamepad consumer is not ready, the driver replaces superseded
-axis, trigger, motion, battery, and touch-position states with the newest report.
-Button, D-pad, trigger-threshold, report-ID, and touch-contact lifecycle changes
-remain ordered in a bounded transition queue. This keeps continuously moving
-controls close to the latest submitted state while preserving ordinary button
-press and release transitions. Keyboard reports also remain ordered so short
-key transitions are not coalesced away. Relative mouse motion and wheel values
-are accumulated by button state and emitted in descriptor-sized chunks, so VHF
-backpressure does not turn relative movement into a replaceable absolute state.
-Profile initialization replies are prioritized over pending controller states
-so the Switch Pro handshake remains responsive.
+The broker service should be running, and a gamepad created by
+`virtualhid_control.exe` should appear in Windows device tools. Use
+`joy.cpl` to check buttons and axes. Steam, browsers, and games may use
+different input APIs, so test with the intended consumer too. If the
+application cannot create a controller, check its log and the license status
+in `virtualhid_control.exe`.
-The driver also caches the newest complete input report for each report ID and
-answers VHF `GetInputReport` requests from that cache. Synchronous HID consumers
-can therefore query the current controller and battery state even when they do
-not consume the streaming read queue. Unnumbered reports are returned with the
-leading zero report-ID byte expected by Windows HID APIs.
+Installation and uninstall logs are under `C:\ProgramData\libvirtualhid`.
+The UMDF driver log is at
+`%WINDIR%\Temp\libvirtualhid-umdf-driver.log`; include it when reporting
+driver or device-creation failures. A broker restart removes its existing
+virtual devices, so reconnect the application afterward.
-For the Xbox One and Xbox Series VHF profiles, this HID input value is separate
-from the battery result returned by XInput. On a Windows desktop where XInput
-enumerated one of those VHF Xbox devices, `XInputGetBatteryInformation` returned
-`BATTERY_TYPE_DISCONNECTED` and `BATTERY_LEVEL_EMPTY` even while `XInputGetState` received its input and
-`GetInputReport` contained the submitted value. Headless Windows CI did not
-expose an XInput slot for the same device. Neither path exposes the remote
-battery through XInput. SDL's Windows Xbox path and Windows Game Bar therefore
-have no XInput battery value to display. VHF does not expose a
-wireless-transport or XInput battery-type setting in `VHF_CONFIG`.
-The Xbox 360 XUSB personality reports the fixed wired-controller battery state;
-its public profile does not advertise remote battery input.
+## Build the driver package
-The current Steam client also renders its controller battery indicator only for
-devices it classifies as Bluetooth or wireless. All Windows VHF profiles use a
-wired virtual transport, so this UI policy can hide battery values that remain
-available to HID consumers. SDL's HID path independently receives battery state
-for the Windows DualShock 4, DualSense, and Switch Pro profiles.
-
-The driver rejects virtual HID create, destroy, and broker-instance reset IOCTLs
-unless the requestor token contains the `NT SERVICE\libvirtualhid_broker`
-service SID. On the first boot after installation, before Windows applies a
-newly configured service SID to the process token, the driver instead requires
-the requestor PID to match the SCM-registered, currently running broker service.
-Administrators still control installation, repair, replacement, and service
-diagnostics through the normal Windows service and driver-management tools, but
-they are not a separate runtime bypass for creating or destroying virtual
-devices.
-
-The library and installed driver must use the same control-protocol version.
-Control protocol version 5 adds an optional broker-duplicated per-device
-transport handle to the create response; it is required for Xbox 360 and zero
-for ordinary VHF devices. It retains the canonical keyboard device type from
-version 4 and the explicit device type and 2048-byte report-descriptor capacity
-from version 3. A version mismatch is rejected rather than interpreting a
-request with different semantics.
-
-Each backend runtime uses one root control-file handle for ordinary VHF commands
-and its pending output read. Broker protocol version 5 additionally carries the
-Xbox 360 transport handle in the create response. The root driver associates
-ordinary VHF output events with its control file object, so feedback
-from a virtual gamepad is delivered only to the runtime that created it instead
-of being consumed by another libvirtualhid client. Because the shared handle is
-opened for overlapped I/O, command IOCTLs also supply a valid `OVERLAPPED` event
-and explicitly wait for pending completion instead of mixing synchronous calls
-with an asynchronous handle. Each caller thread reuses its event to avoid
-creating a kernel handle for every input report.
-
-The pending output read also participates in the UMDF power-managed queue
-lifecycle. When the system sleeps, the driver acknowledges the queue stop while
-retaining the cancelable request, then resumes that same request after the
-control device returns to D0. This allows sleep to complete without waiting for
-controller feedback and keeps the runtime's control handle and virtual devices
-valid across resume.
-
-The root driver opens a separate VHF source target for each ordinary virtual HID device and
-parents that target to the control-file handle that created it. If the creating
-process exits or crashes, Windows cleans up devices that were not explicitly
-destroyed. In brokered driver packages, the broker owns that control-file handle.
-The broker tracks the requesting client process for each created device and
-destroys broker-owned devices when that client process exits unexpectedly. A new
-broker process first asks the driver to remove every device left by the previous
-broker instance and refuses new creation until that reset succeeds. Clients must
-recreate their devices after the broker service restarts. Xbox 360 devices use
-the same ownership rule through their broker-held software-device handles.
-
-The backend reports `requires_installed_driver = true` and only advertises
-gamepad/output-report support when the broker is reachable and the control
-device can be opened. The keyboard and mouse `SendInput` fallbacks do not
-require the driver package; a Raw Input-visible keyboard or mouse requires the
-driver and the same broker license as a gamepad.
-
-## Build
-
-Build the UMDF package with a Visual Studio generator and the WDK installed:
+Contributors need Visual Studio 2022 and the Windows SDK/WDK. This is a
+separate MSVC build from the normal library's MSYS2/UCRT64 build:
```powershell
cmake -S . -B cmake-build-windows-driver -G "Visual Studio 17 2022" -A x64 `
@@ -237,350 +80,6 @@ cmake --build cmake-build-windows-driver --config Release `
cpack -G WIX -C Release --config .\cmake-build-windows-driver\CPackConfig.cmake
```
-The package defaults to UMDF 2.15, matching the inbox VHF UMDF source driver
-while still exposing the framework APIs used by libvirtualhid. The driver links
-the MSVC runtime statically, so the UMDF host process does not need VC runtime
-DLLs beside the driver.
-
-## Developer Install and Validation
-
-Developer 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 `
- -SetupPath .\cmake-build-windows-driver\src\platform\windows\driver\Release\libvirtualhid_driver_setup.exe `
- -BrokerPath .\cmake-build-windows-driver\src\platform\windows\broker\Release\libvirtualhid_broker.exe `
- -LogPath .\cmake-build-windows-driver\install-driver.log
-powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-installed-driver.ps1 `
- -GamepadAdapterPath .\cmake-build-windows-driver\examples\Release\gamepad_adapter.exe `
- -GamepadProfile xseries
-powershell -ExecutionPolicy Bypass -File .\scripts\windows\test-browser-gamepad.ps1 `
- -GamepadAdapterPath .\cmake-build-windows-driver\examples\Release\gamepad_adapter.exe `
- -GamepadProfile xseries
-powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1 `
- -Force -RemoveCertificateSubject "CN=libvirtualhid CI Test Driver Signing" `
- -LogPath .\cmake-build-windows-driver\uninstall-driver.log
-```
-
-The WiX installer places validation files under the selected install root, which
-defaults to `C:\Program Files\libvirtualhid`:
-
-- `tools\windows\gamepad_adapter.exe`
-- `tools\windows\virtualhid_control.exe`
-- `tools\windows\libvirtualhid_driver_setup.exe`
-- `services\windows\libvirtualhid_broker.exe`
-
-The source-tree validation scripts remain developer and CI helpers. They are not
-packaged as reviewer-facing MSI validation scripts because the native
-`virtualhid_control.exe` tool can create, exercise, and inspect virtual
-gamepads and mice interactively.
-
-The install script stages the INF with `pnputil`, updates an existing
-`ROOT\LIBVIRTUALHID` device when present, and creates that root-enumerated
-device when it is missing. A packaged, architecture-matched native helper makes
-the SetupAPI/NewDev calls, so MSI installs do not depend on PowerShell runtime
-C# compilation or require WDK tools on the target machine. When a broker executable is present,
-the install script also installs and starts the `libvirtualhid_broker` Windows service
-with a service SID. The service `ImagePath` is stored as a literal quoted path,
-and installation fails if the registry value is not safely quoted. This avoids
-CWE-428 unquoted-service-path escalation when the install root contains spaces.
-The install helper also clears any legacy broker service `Environment` value so
-licensing configuration cannot be overridden on the user's machine. The
-uninstall helper stops and deletes that service before removing the driver
-package. It discovers staged OEM INF names through language-neutral DISM and
-CIM objects instead of parsing localized `pnputil` labels. If an application
-has an outstanding device handle, the helper records the initial device-removal
-failure and continues with the forced driver-package uninstall, which can finish
-or schedule the removal. Uninstall still fails if package removal fails or if
-the broker service, root device, or staged driver package remains after cleanup,
-so the MSI cannot silently report a complete removal while driver state remains.
-MSI uninstall diagnostics are appended to
-`C:\ProgramData\libvirtualhid\uninstall-driver.log`.
-
-The installed-driver test fails if the root device is not started, if
-`\\.\LibVirtualHid` cannot be opened, or if a held `gamepad_adapter` instance
-does not produce a started HID child device. The browser helper launches a
-desktop browser at `https://hardwaretester.com/gamepad` and validates that the
-browser Gamepad API observes the held virtual controller.
-
-For manual browser validation, run the browser helper with `-KeepBrowserOpen`,
-run the interactive UI, or run:
-
-```powershell
-tools\windows\gamepad_adapter.exe xseries --hold-seconds 60
-```
-
-Then open `https://hardwaretester.com/gamepad` in a normal desktop browser and
-press one of the held virtual buttons if the browser requires a gamepad
-activation event.
-
-For interactive local validation, run:
-
-```powershell
-tools\windows\virtualhid_control.exe
-```
-
-The native UI can create, remove, control, and monitor gamepads and mice that it
-owns. Gamepad buttons are momentary by default, with an explicit lock mode for
-held inputs. Mouse controls provide relative movement, five momentary buttons,
-vertical scrolling, and horizontal panning. Use Tab or the arrow keys to
-highlight a mouse control and Space or Enter to activate it, avoiding use of the
-physical mouse while testing the virtual device. A mouse button remains pressed
-only while its activation key is held.
-
-For an external mouse-event tester, enable **Delayed browser test**, choose a
-delay, and activate the desired action. Switch to the browser before the
-countdown expires while leaving the pointer over its test target. Movement and
-wheel actions are submitted once the browser owns focus; a delayed button action
-submits one press-and-release click. The scheduler continues while the control
-window is unfocused or minimized.
-
-The UI identifies a driver-backed HID mouse separately from the `SendInput`
-fallback and also shows supported profile features, battery input state, device
-nodes, and normalized gamepad feedback reports such as rumble, RGB LED,
-adaptive trigger, and raw output events. Devices created by another process are
-not listed yet; that requires a future Windows control-protocol extension for
-cross-process diagnostics.
-
-On Windows, the UI also shows broker license status. It can activate a license
-key, refresh validation, deactivate the current machine, and open
-compiled purchase or account-management URLs. The Create button is enabled for
-both gamepads and mice only while the broker reports a current machine license.
-License management and normal virtual HID device use do not require elevation.
-
-## Installation Notes
-
-The driver binary is a user-mode UMDF DLL installed through the Windows Driver
-Store, not a libvirtualhid `.sys` copied into `C:\Windows\System32\drivers`.
-Windows still uses its built-in `WUDFRd.sys` and VHF components under
-`System32\drivers`.
-
-The libvirtualhid-specific sign that installation completed is the
-`ROOT\LIBVIRTUALHID` root device, the `\\.\LibVirtualHid` control device, and
-the running `libvirtualhid_broker` service.
-
-Host applications can present the same license workflow through the installed
-public C++ API. Include `libvirtualhid/license.hpp` (or the aggregate
-`libvirtualhid/libvirtualhid.hpp`) and call `get_license_status`,
-`activate_license`, `validate_license`, or `deactivate_license`. The API uses
-provider-neutral types, sends activation keys directly to the local broker,
-and returns purchase and account-management URLs with the status. Applications
-must treat activation keys as transient secrets and must not persist or log
-them.
-
-### Driver Diagnostic Logs
-
-The UMDF driver writes lifecycle events and operational failures to the
-following path (normally `C:\Windows\Temp`):
-
-```text
-%WINDIR%\Temp\libvirtualhid-umdf-driver.log
-```
-
-Successful input reports are deliberately excluded because they are the
-latency-sensitive hot path. When the active log would exceed 5 MiB, the driver
-rotates it before writing the next entry. Five previous logs are retained as
-`libvirtualhid-umdf-driver.log.1` through
-`libvirtualhid-umdf-driver.log.5`; `.1` is the newest backup. The active log
-and all numbered backups use at most approximately 30 MiB in total. Include
-the active log and any numbered backups when reporting a driver installation,
-device-lifecycle, authorization, or input-submission problem.
-
-During rapid development reinstalls, the fixed global control symbolic link can
-briefly outlive the previous root device. The driver treats that collision as
-non-fatal, and normal clients discover the PnP control device interface first.
-
-The broker stores machine-scoped license state in:
-
-```text
-C:\ProgramData\libvirtualhid\license.dat
-```
-
-The file is protected with Windows DPAPI local-machine scope. The state
-directory and both state files are owned by LocalSystem and use protected DACLs
-that grant full access only to `NT SERVICE\libvirtualhid_broker`, LocalSystem,
-and built-in administrators; reparse-point state paths are rejected. GitHub
-Actions evaluation timing is
-stored separately with the same DPAPI and ACL protection in
-`C:\ProgramData\libvirtualhid\github-actions-evaluation.dat`. Broker entitlement
-configuration is compiled into the Windows broker and diagnostic UI. Update
-`src/platform/windows/shared/lvh_windows_broker_config.hpp` when the Polar
-organization ID, allowed license-key benefit IDs, Checkout Links, customer
-portal URL changes, then rebuild the Windows package. No Polar access token or
-webhook secret is compiled into the client:
-activation, validation, and deactivation use Polar's
-[public customer license-key API](https://polar.sh/docs/features/benefits/license-keys).
-Successful licensed gamepad creations are counted in the protected broker state
-and reported through `increment_usage` on the next license validation. The
-create request still completes without an online Polar request.
-Those requests pin Polar's date-based API contract to `2026-04` with the
-`Polar-Version` header. Before Polar removes that version, update
-`polar_request_headers` in
-`src/platform/windows/broker/libvirtualhid_broker.cpp`, review Polar's
-[API versioning guidance](https://polar.sh/docs/api-reference/versioning), and
-validate the license response contract before rebuilding the Windows package.
-
-The production configuration accepts organization
-`3db9f05a-44d7-42f1-ba7c-a0f198235fb7` with yearly license-key benefit
-`eb316dac-bf6a-4359-95a2-86c299d48ecc` or lifetime license-key benefit
-`157374cb-f526-4154-81ba-9f2c92a053ca`. Polar's public response identifies the
-benefit rather than the purchased product, so the broker fails closed unless the
-returned organization and benefit are both allow-listed. The purchase button
-opens the shared persistent Polar Checkout Link. Account management opens the
-[LizardByte LLC Polar customer portal](https://polar.sh/lizardbyte-llc/portal),
-where customers can manage their five allowed machine activations.
-
-Normal Windows UMDF virtual HID device creation requires a current machine
-authorization, but device creation itself does not contact Polar. A keyboard or
-mouse does not consume another Polar machine activation; each is another active
-device under the existing machine license. The broker validates the
-saved activation immediately after service startup and then once per day in the
-background. If validation cannot complete because of a temporary network or
-provider failure, the broker retries every 60 seconds. Devices that already
-exist are retained for one hour unless the broker service restarts, but no
-additional device can be created while at least one licensed device
-remains active. When the outage reaches one hour, the broker removes excess
-licensed devices and retains at most one. A yearly subscription authorization
-is current for at most the daily validation interval plus that one-hour outage
-allowance; after 25 hours without successful validation, the remaining licensed
-device is also removed. A lifetime license can retain the one-device
-fallback until online validation succeeds. Failed driver destruction requests
-remain tracked and are retried instead of being treated as successful revocations.
-
-Polar's HTTPS `Date` response header supplies trusted time when a new
-authorization is issued. Both supported plans rely on Polar's entitlement status
-rather than a locally enforced calendar expiration. Subscription keys remain
-granted while their subscription is billable, and Polar revokes the benefit when
-the subscription entitlement ends. Polar's public license validation response
-does not include the subscription renewal date, so the broker does not fabricate
-one; customers can see the authoritative date in the linked Polar account portal.
-The one-hour outage retention does not extend the yearly subscription's 25-hour
-validation deadline.
-
-The broker advances Polar's trusted timestamp using Windows uptime and stores a
-random marker in a volatile registry key for the current boot session. This works
-across broker service restarts and includes sleep or hibernation, but never
-consults the user-adjustable Windows date. After Windows restarts, the marker
-changes, so a yearly subscription must reconnect to Polar before virtual HID
-device creation; a lifetime license can use the one-device outage fallback. Explicit validation
-requests always contact the provider. The sole exception to normal licensing is
-for CI runners where the broker service itself has the `GITHUB_ACTIONS`
-environment marker. That environment receives one machine-scoped five-minute
-evaluation window beginning with its first unlicensed creation attempt. The
-start survives broker restarts, clock rollback expires the window, and the
-broker destroys evaluation-created devices when the deadline is reached.
-Setting `GITHUB_ACTIONS` only in a consuming application does not affect the
-separately running service.
-
-Polar's `limit_activations` value is the machine limit and is configured as `5`
-on both license-key benefits. The broker gives yearly and lifetime licenses the
-same full local access when the provider reports the key status as `granted`.
-Polar revokes a subscription benefit when its entitlement ends. Licensed access
-has no local active-device cap after successful validation. A definitive missing
-activation, revoked or disabled key, activation mismatch, disallowed benefit, or
-explicit deactivation prevents new virtual HID devices and causes the broker to
-destroy existing licensed devices. A timeout or other transient provider failure
-starts the one-hour retention period and one-device
-creation limit instead of immediately revoking existing controllers. A yearly
-subscription that cannot validate for 25 hours is also denied until it reconnects.
-WinHTTP resolve, connect, send, and receive operations have explicit timeouts of
-5, 5, 5, and 10 seconds respectively.
-
-## Profile Compatibility
-
-For a keyboard, the Windows backend preserves the requested bus type, VID, PID,
-version, name, manufacturer, and stable ID. The Windows transport owns the HID
-framing: it uses a report-ID-free standard keyboard descriptor with eight
-modifier bits, sixteen simultaneous keyboard-page usages, and a one-byte LED
-output report. Normal key transitions use the licensed VHF device so Raw Input
-clients enumerate a physical-style HID keyboard instead of receiving only
-`SendInput` injection. Unicode text input and keys outside the descriptor's
-keyboard-page range continue through `SendInput`. If the driver, broker, or
-license is unavailable, keyboard creation retains the existing `SendInput`
-fallback; malformed requests and unexpected driver failures are returned to the
-caller.
-
-For a mouse, the Windows backend preserves the requested bus type, VID, PID,
-version, name, manufacturer, and stable ID. The Windows transport owns the HID
-framing: it uses a report-ID-free seven-byte descriptor with five buttons,
-16-bit relative X/Y, an 8-bit wheel, and an 8-bit AC Pan axis. Relative motion,
-buttons, and scrolling use the licensed VHF device so Raw Input clients can see
-them. Absolute positioning cannot be represented by that relative descriptor
-and continues through `SendInput`. If the driver, broker, or license is
-unavailable, mouse creation retains the existing `SendInput` fallback; malformed
-requests and unexpected driver failures are returned to the caller.
-
-The Windows backend publishes most gamepads through VHF. DirectInput,
-SDL/HIDAPI, Windows.Gaming.Input/GameInput, and browser Gamepad API clients
-should see standard HID devices after the driver is installed. Xbox 360 instead
-publishes a native XUSB-facing interface for XInput and a correlated VHF child
-for HID/DirectInput consumers.
-
-The built-in Xbox One profile uses its XboxGIP-shaped HID descriptor. The public
-Xbox Series profile remains `VID_045E&PID_0B12`; the Windows transport presents
-it with release `0x0509` and the `VID_045E&PID_0B12&IG_00` XInputHID match ID
-observed from physical Xbox Series USB and Xbox Wireless Adapter connections.
-The VHF child preserves the native 17-byte GIP-shaped input report, and the
-last byte carries battery strength for both Xbox One and Xbox Series. The report
-parser accepts the native eight-byte four-motor Xbox payload when a consumer
-delivers it. The Windows backend submits Xbox One and Xbox Series input only
-when the packed state changes. State transitions still reach VHF, while raw HID
-consumers are not asked to reinterpret the same unchanged Xbox state as fresh
-input. The Xbox 360 companion exposes the classic `0x045E:0x028E` wired
-identity, preserves native XInput button/axis precision, and normalizes the two
-XInput motors into the ordinary rumble callback.
-
-DualShock 4 and DualSense answer the calibration, pairing, and firmware feature
-requests used by their Windows HIDAPI initialization paths. Switch Pro answers
-the native USB and subcommand handshake and submits native `0x30` input reports
-with three live IMU samples. The client backend caches the newest complete
-Switch state and submits it every 15 milliseconds, matching a physical USB
-controller's report cadence while coalescing separate acceleration and
-gyroscope updates. Its Set Player Lights subcommand is normalized into solid
-and flashing player-indicator output states for the creating runtime, and its
-monochrome HOME light is normalized as equal RGB channels so existing streaming
-LED feedback paths can preserve its intensity.
-The built-in Generic profile is presented to Windows as a DirectInput PID
-Joystick with the complete output-report set required for DirectInput
-enumeration. Constant Force and Sine output is normalized to the portable
-gamepad rumble callback; other declared effect payloads are ignored safely. The
-backend honors PID start delay, duration, and loop count, and automatically
-stops finite effects. These changes remain private to the Windows transport and
-do not alter the public platform-neutral profile API.
-
-Consumers that display raw HID strings may still show the Windows VHF product
-label because VHF does not provide a product/manufacturer string callback.
-
-### Current Release Limits
-
-- The published Windows driver installer is AMD64-only. Windows ARM64 release
- packages require a Microsoft dashboard signing path that is not part of the
- current Azure Trusted Signing workflow.
-- Xbox 360 support depends on an undocumented XUSB compatibility contract and
- therefore requires installed-driver XInput, browser/WGI, and rumble validation
- on each supported Windows release before shipping.
-- A temporary Polar outage limits a previously activated machine to one active
- licensed virtual HID device. Yearly subscriptions must reconnect within 25
- hours of their last successful validation; lifetime licenses can retain one
- device until validation succeeds. Definitive invalidation prevents new devices
- and removes active licensed devices.
-
-## Signing
-
-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 certificate into the WiX installer, and
-import it into local machine trust stores during install.
-
-Release builds must use Azure Trusted Signing for the catalog and generated MSI
-and must not ship the local pull-request test certificate.
-
-## License
-
-The Windows UMDF driver, broker, proprietary entitlement/evaluation sources,
-and generated Windows driver package artifacts, including the driver MSI, are
-licensed under the LizardByte Source-Available License 1.0 (LB-SAL 1.0). See
-the [license map](../LICENSES/license-map.md) for the full repository license split.
-The MSI may also include MIT-licensed helper components from this repository,
-so packaged installs include both license texts.
+Developer install and installed-driver validation helpers are in
+`scripts/windows/`. A signed installer and installed consumer tests are needed
+to verify real Windows behavior; a library-only build does not do so.
diff --git a/src/platform/macos/broker/libvirtualhid_macos_broker.cpp b/src/platform/macos/broker/libvirtualhid_macos_broker.cpp
index 804665f7..9c9f8d0c 100644
--- a/src/platform/macos/broker/libvirtualhid_macos_broker.cpp
+++ b/src/platform/macos/broker/libvirtualhid_macos_broker.cpp
@@ -276,14 +276,14 @@ namespace lvh::detail::macos_broker {
return request.type == MessageType::create && request.descriptor_size != 0 && request.descriptor_size <= max_descriptor_size && request.input_report_size != 0 && request.input_report_size <= max_report_size && request.output_report_size <= max_report_size && terminated(request.name) && terminated(request.manufacturer) && terminated(request.stable_id) && request.kind <= static_cast(std::to_underlying(lvh::GamepadProfileKind::dualshock4)) && request.bus <= static_cast(std::to_underlying(lvh::BusType::bluetooth));
}
- void receive_reports(DeviceSession &session, LicenseManager &licenses, bool evaluation, std::uint32_t expected_input_size) {
+ void receive_reports(DeviceSession &session, LicenseManager &licenses, bool evaluation, std::uint64_t device_id, std::uint32_t expected_input_size) {
// Only the HID input path needs interactive scheduling; licensing stays at its normal QoS.
static_cast(::pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0));
Message request;
for (;;) {
pollfd descriptor {.fd = session.fd, .events = POLLIN, .revents = 0};
const int polled = ::poll(&descriptor, 1, 1000);
- if (!licenses.device_is_authorized(evaluation) || (polled < 0 && errno != EINTR)) {
+ if (!licenses.device_is_authorized(evaluation, device_id) || (polled < 0 && errno != EINTR)) {
return;
}
if (polled == 0 || polled < 0) {
@@ -347,16 +347,16 @@ namespace lvh::detail::macos_broker {
::close(fd);
return;
}
- licenses.add_device(evaluation, authorized_key);
+ const auto device_id = licenses.add_device(evaluation, authorized_key);
response.type = MessageType::response;
response.status = 0;
static_cast(session.send(response));
- receive_reports(session, licenses, evaluation, request.input_report_size);
+ receive_reports(session, licenses, evaluation, device_id, request.input_report_size);
session.open = false;
IOHIDUserDeviceCancel(device);
static_cast(dispatch_semaphore_wait(cancelled, DISPATCH_TIME_FOREVER));
CFRelease(device);
- licenses.remove_device(evaluation);
+ licenses.remove_device(evaluation, device_id);
::close(fd);
}
diff --git a/src/platform/macos/broker/license_manager.hpp b/src/platform/macos/broker/license_manager.hpp
index b0ea1e78..b03f81bf 100644
--- a/src/platform/macos/broker/license_manager.hpp
+++ b/src/platform/macos/broker/license_manager.hpp
@@ -8,6 +8,7 @@
#pragma once
+#include "platform/shared/lvh_broker_license_policy.hpp"
#include "protocol.hpp"
#include
@@ -29,9 +30,28 @@ namespace lvh::detail::macos_broker {
Message handle(const Message &request);
bool authorize_create(Message &response, bool &evaluation, std::string &authorized_key);
- bool device_is_authorized(bool evaluation);
- void add_device(bool evaluation, std::string_view authorized_key);
- void remove_device(bool evaluation);
+ /**
+ * @brief Check whether an active device may remain during license validation.
+ * @param evaluation Whether the device belongs to the CI evaluation.
+ * @param device_id Unique ID returned by add_device.
+ * @return Whether the device remains authorized.
+ */
+ bool device_is_authorized(bool evaluation, std::uint64_t device_id);
+
+ /**
+ * @brief Register a created device and return its unique ID.
+ * @param evaluation Whether the device belongs to the CI evaluation.
+ * @param authorized_key License key used for a licensed gamepad.
+ * @return Unique ID for later authorization checks and removal.
+ */
+ std::uint64_t add_device(bool evaluation, std::string_view authorized_key);
+
+ /**
+ * @brief Unregister a device when its session ends.
+ * @param evaluation Whether the device belongs to the CI evaluation.
+ * @param device_id Unique ID returned by add_device.
+ */
+ void remove_device(bool evaluation, std::uint64_t device_id);
private:
struct State {
@@ -62,6 +82,8 @@ namespace lvh::detail::macos_broker {
std::optional evaluation_started_at_;
std::uint32_t active_devices_ = 0;
std::uint32_t active_licensed_devices_ = 0;
+ std::uint64_t next_device_id_ = 0;
+ broker_license::OutageDeviceSelector outage_device_selector_;
bool github_actions_ = false;
bool online_confirmed_ = false;
std::jthread validator_;
diff --git a/src/platform/macos/broker/license_manager.mm b/src/platform/macos/broker/license_manager.mm
index b54622be..ea327ea4 100644
--- a/src/platform/macos/broker/license_manager.mm
+++ b/src/platform/macos/broker/license_manager.mm
@@ -324,6 +324,7 @@ bool valid_c_string(const std::array &value) {
validated_at_ = std::chrono::steady_clock::now();
unavailable_since_.reset();
online_confirmed_ = true;
+ outage_device_selector_.reset();
fill_status_locked(response);
}
set_text(response.message, "License activated on this machine.");
@@ -414,6 +415,7 @@ bool valid_c_string(const std::array &value) {
validated_at_ = std::chrono::steady_clock::now();
unavailable_since_.reset();
online_confirmed_ = true;
+ outage_device_selector_.reset();
}
auto response = status();
set_text(response.message, "License validated.");
@@ -519,7 +521,7 @@ bool valid_c_string(const std::array &value) {
return false;
}
- bool LicenseManager::device_is_authorized(bool evaluation) {
+ bool LicenseManager::device_is_authorized(bool evaluation, std::uint64_t device_id) {
std::lock_guard lock {mutex_};
if (evaluation) {
const auto now = std::chrono::system_clock::now();
@@ -528,14 +530,19 @@ bool valid_c_string(const std::array &value) {
if (!licensed_locked()) {
return false;
}
- return !unavailable_since_ || !broker_license::outage_retention_elapsed(std::chrono::steady_clock::now() - *unavailable_since_);
+ if (!unavailable_since_ || !broker_license::outage_retention_elapsed(std::chrono::steady_clock::now() - *unavailable_since_)) {
+ return true;
+ }
+ return outage_device_selector_.keep(device_id);
}
- void LicenseManager::add_device(bool evaluation, std::string_view authorized_key) {
+ std::uint64_t LicenseManager::add_device(bool evaluation, std::string_view authorized_key) {
std::lock_guard operation_lock {operation_mutex_};
std::optional state;
+ std::uint64_t device_id = 0;
{
std::lock_guard lock {mutex_};
+ device_id = ++next_device_id_;
++active_devices_;
if (!evaluation) {
++active_licensed_devices_;
@@ -548,10 +555,12 @@ bool valid_c_string(const std::array &value) {
if (state) {
static_cast(save_state(*state));
}
+ return device_id;
}
- void LicenseManager::remove_device(bool evaluation) {
+ void LicenseManager::remove_device(bool evaluation, std::uint64_t device_id) {
std::lock_guard lock {mutex_};
+ outage_device_selector_.remove(device_id);
--active_devices_;
if (!evaluation) {
--active_licensed_devices_;
diff --git a/src/platform/shared/lvh_broker_license_policy.hpp b/src/platform/shared/lvh_broker_license_policy.hpp
index 57735976..84f3e632 100644
--- a/src/platform/shared/lvh_broker_license_policy.hpp
+++ b/src/platform/shared/lvh_broker_license_policy.hpp
@@ -11,6 +11,7 @@
#include
#include
#include
+#include
#include
namespace lvh::broker_license {
@@ -61,6 +62,44 @@ namespace lvh::broker_license {
return elapsed >= outage_retention;
}
+ /**
+ * @brief Select one active device to retain after an outage grace period.
+ */
+ class OutageDeviceSelector {
+ public:
+ /**
+ * @brief Retain the first device checked and reject other devices.
+ * @param device_id Unique ID of an active device.
+ * @return Whether this device is the retained device.
+ */
+ bool keep(std::uint64_t device_id) noexcept {
+ if (!retained_device_id_.has_value()) {
+ retained_device_id_ = device_id;
+ }
+ return retained_device_id_ == device_id;
+ }
+
+ /**
+ * @brief Release the selected device when it is removed.
+ * @param device_id ID of the removed device.
+ */
+ void remove(std::uint64_t device_id) noexcept {
+ if (retained_device_id_ == device_id) {
+ retained_device_id_.reset();
+ }
+ }
+
+ /**
+ * @brief Clear the selection after successful online validation.
+ */
+ void reset() noexcept {
+ retained_device_id_.reset();
+ }
+
+ private:
+ std::optional retained_device_id_;
+ };
+
namespace github_actions_evaluation {
using Clock = std::chrono::system_clock;
diff --git a/tests/unit/test_license.cpp b/tests/unit/test_license.cpp
index f2fd8b16..69677b96 100644
--- a/tests/unit/test_license.cpp
+++ b/tests/unit/test_license.cpp
@@ -56,6 +56,22 @@ TEST(BrokerLicensePolicyTest, EnforcesSubscriptionAndOutageBoundaries) {
EXPECT_TRUE(lvh::broker_license::outage_retention_elapsed(1h));
}
+TEST(BrokerLicensePolicyTest, RetainsOneDeviceAfterAnOutage) {
+ lvh::broker_license::OutageDeviceSelector selector;
+
+ EXPECT_TRUE(selector.keep(1));
+ EXPECT_TRUE(selector.keep(1));
+ EXPECT_FALSE(selector.keep(2));
+ selector.remove(2);
+ EXPECT_FALSE(selector.keep(2));
+
+ selector.remove(1);
+ EXPECT_TRUE(selector.keep(2));
+ selector.reset();
+ EXPECT_TRUE(selector.keep(3));
+ EXPECT_FALSE(selector.keep(2));
+}
+
TEST(GitHubActionsEvaluationTest, IsActiveOnlyInsideFiveMinuteWindow) {
using namespace std::chrono_literals;
using lvh::broker_license::github_actions_evaluation::active;