feat(Windows): Add virtualhid_control diagnostic UI tool (#44)

This commit is contained in:
Dave Lane authored and GitHub committed 2026-07-06 15:54:01 -04:00
1 parent bef70b8480
commit 47150206f0
19 files changed
+2114 -55

No files matched your search

+14 -18
View File
@@ -211,6 +211,7 @@ jobs:
-DBUILD_EXAMPLES=ON \
-DBUILD_TESTS=ON \
-DCMAKE_BUILD_TYPE:STRING=${CMAKE_BUILD_CONFIG} \
-DLIBVIRTUALHID_BUILD_TOOLS=ON \
-B cmake-build-ci \
-G Ninja \
-S .
@@ -226,6 +227,7 @@ jobs:
-DBUILD_DOCS=OFF `
-DBUILD_EXAMPLES=ON `
-DBUILD_TESTS=ON `
-DLIBVIRTUALHID_BUILD_TOOLS=ON `
-A x64 `
-B cmake-build-ci `
-G "Visual Studio 17 2022" `
@@ -339,7 +341,9 @@ jobs:
}
& $openCppCoverage `
--sources "$env:GITHUB_WORKSPACE\examples" `
--sources "$env:GITHUB_WORKSPACE\src" `
--sources "$env:GITHUB_WORKSPACE\tools" `
"--export_type=cobertura:$env:GITHUB_WORKSPACE\cmake-build-ci\reports\coverage.xml" `
--working_dir "$env:GITHUB_WORKSPACE\cmake-build-ci\tests" `
-- `
@@ -375,6 +379,9 @@ jobs:
$coverage.Save($coveragePath)
- name: Run gamepad adapter example
run: cmake --build cmake-build-ci --config ${{ env.CMAKE_BUILD_CONFIG }} --target run_gamepad_adapter_example
- name: Generate gcov report
id: test_report
if: >-
@@ -386,8 +393,13 @@ jobs:
GCOV_EXECUTABLE: ${{ matrix.gcov_executable }}
MSYS2_PATH_TYPE: inherit
run: |
uv run --project ../third-party/lizardbyte-common --locked --no-sync gcovr . -r ../src \
uv run --project ../third-party/lizardbyte-common --locked --no-sync gcovr . -r .. \
--filter ../examples/ \
--filter ../src/ \
--filter ../tools/ \
--gcov-executable "${GCOV_EXECUTABLE}" \
--exclude ../tests/ \
--exclude ../third-party/ \
--exclude-noncode-lines \
--exclude-throw-branches \
--exclude-unreachable-branches \
@@ -395,22 +407,6 @@ jobs:
--xml-pretty \
-o reports/coverage.xml
- name: Run gamepad adapter example
if: runner.os == 'Linux'
run: |
./cmake-build-ci/examples/gamepad_adapter
- name: Run gamepad adapter example Windows
if: runner.os == 'Windows'
shell: pwsh
run: |
if ("${{ matrix.kind }}" -eq "msys2") {
$env:PATH = "C:\msys64\${{ matrix.msystem }}\bin;C:\msys64\usr\bin;$env:PATH"
& .\cmake-build-ci\examples\gamepad_adapter.exe
} else {
& ".\cmake-build-ci\examples\$env:CMAKE_BUILD_CONFIG\gamepad_adapter.exe"
}
- name: Uninstall Windows driver installer
if: >-
always() &&
@@ -505,7 +501,7 @@ jobs:
run: >-
cmake --build cmake-build-driver
--config ${{ env.DRIVER_BUILD_CONFIG }}
--target libvirtualhid_windows_catalog gamepad_adapter
--target libvirtualhid_windows_catalog gamepad_adapter virtualhid_control
--parallel 2
- name: Validate Azure signing configuration
+1 -1
View File
@@ -1,5 +1,5 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="docs" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="file:///$PROJECT_DIR$/../tray/cmake-build-release/docs/build/html/index.html" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="libvirtualhid" TARGET_NAME="docs" CONFIG_NAME="Debug-MinGW (ucrt64-ninja)" RUN_PATH="$PROJECT_DIR$/../../../../../Windows/explorer.exe">
<configuration default="false" name="docs" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="file:///$PROJECT_DIR$/../tray/cmake-build-release/docs/build/html/index.html" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="libvirtualhid" TARGET_NAME="docs" CONFIG_NAME="Release-MinGW (ucrt64-ninja)" RUN_PATH="$PROJECT_DIR$/../../../../../Windows/explorer.exe">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
+7
View File
@@ -36,6 +36,9 @@ include("${CMAKE_CURRENT_SOURCE_DIR}/cmake/build_version.cmake")
option(BUILD_DOCS "Build documentation" ${LIBVIRTUALHID_IS_TOP_LEVEL})
option(BUILD_TESTS "Build tests" ${LIBVIRTUALHID_IS_TOP_LEVEL})
option(BUILD_EXAMPLES "Build examples" ${LIBVIRTUALHID_IS_TOP_LEVEL})
option(LIBVIRTUALHID_BUILD_TOOLS "Build libvirtualhid diagnostic tools" ${LIBVIRTUALHID_IS_TOP_LEVEL})
option(LIBVIRTUALHID_TOOLS_STATIC_RUNTIME "Link libvirtualhid tools to static compiler runtimes where supported" ON)
option(LIBVIRTUALHID_TOOLS_FULLY_STATIC "Attempt to link libvirtualhid tools as fully static binaries" OFF)
option(LIBVIRTUALHID_ENABLE_XTEST "Enable X11/XTest keyboard and mouse fallback on Linux" ON)
option(LIBVIRTUALHID_BUILD_WINDOWS_DRIVER "Build the Windows UMDF2 driver package with the WDK/MSVC toolchain" OFF)
option(LIBVIRTUALHID_INSTALL
@@ -107,6 +110,10 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
add_subdirectory(examples)
endif()
if(LIBVIRTUALHID_BUILD_TOOLS)
add_subdirectory(tools)
endif()
if(BUILD_TESTS)
enable_testing()
add_subdirectory(tests)
+2
View File
@@ -40,6 +40,8 @@ behind backend implementations.
HID Framework, with keyboard and mouse support through normal Win32 APIs.
- Output callbacks for profile-specific feedback such as rumble, LEDs,
adaptive triggers, and raw HID output reports when available.
- An optional `virtualhid_control` native UI tool for creating, removing,
controlling, and inspecting test gamepads through the public C++ API.
- CMake consumption through installed packages, vendored source,
`add_subdirectory`, or `FetchContent`.
+10 -2
View File
@@ -18,8 +18,6 @@ set(LIBVIRTUALHID_DRIVER_LICENSE_FILE
install(FILES
"${PROJECT_SOURCE_DIR}/scripts/windows/libvirtualhid-driver-common.ps1"
"${PROJECT_SOURCE_DIR}/scripts/windows/install-driver.ps1"
"${PROJECT_SOURCE_DIR}/scripts/windows/test-browser-gamepad.ps1"
"${PROJECT_SOURCE_DIR}/scripts/windows/test-installed-driver.ps1"
"${PROJECT_SOURCE_DIR}/scripts/windows/uninstall-driver.ps1"
DESTINATION "scripts/windows"
COMPONENT driver)
@@ -30,10 +28,20 @@ if(NOT TARGET gamepad_adapter)
"gamepad_adapter validation tool can be packaged.")
endif()
if(NOT TARGET virtualhid_control)
message(FATAL_ERROR
"The Windows driver installer requires LIBVIRTUALHID_BUILD_TOOLS=ON "
"so the virtualhid_control UI tool can be packaged.")
endif()
install(TARGETS gamepad_adapter
RUNTIME DESTINATION "tools/windows"
COMPONENT driver)
install(TARGETS virtualhid_control
RUNTIME DESTINATION "tools/windows"
COMPONENT driver)
if(LIBVIRTUALHID_DRIVER_TEST_CERTIFICATE)
install(FILES "${LIBVIRTUALHID_DRIVER_TEST_CERTIFICATE}"
DESTINATION "certificates"
+2
View File
@@ -66,6 +66,8 @@ code and tests provide a better source of truth.
- `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.
+9
View File
@@ -56,6 +56,15 @@ reports, and output reports matter for controller compatibility. Keyboard and
pointer devices prefer `uinput` because those devices map naturally to Linux
input devices.
The optional `virtualhid_control` diagnostic UI is currently implemented with
native Windows controls. A future Linux UI should use a native toolkit that can
be packaged as a release asset. Static Linux linking is possible only when the
target distribution provides static archives for all selected backend and UI
dependencies, including `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.
### Permissions
Linux deployment is usually a permissions problem, not a kernel-module problem.
+31 -26
View File
@@ -14,50 +14,55 @@ Paste this into the Partner Center certification notes field:
```text
This package installs the libvirtualhid Windows user-mode UMDF/VHF virtual HID
driver. It has no standalone end-user UI; applications consume it through the
libvirtualhid client API.
driver. Applications consume it through the libvirtualhid client API, and the
MSI includes a native diagnostic UI for local validation.
A prebuilt validation tool and PowerShell validation scripts are installed by
the MSI so no programming, SDK setup, or driver build environment is required.
Please install the released, production-signed MSI, then run the validation
commands below from PowerShell.
A prebuilt native validation tool is installed by the MSI so no programming,
SDK setup, or driver build environment is required. Please install the released,
production-signed MSI, then launch the validation tool below.
Default install root:
C:\Program Files\libvirtualhid
Installed validation files:
C:\Program Files\libvirtualhid\scripts\windows\test-installed-driver.ps1
C:\Program Files\libvirtualhid\scripts\windows\test-browser-gamepad.ps1
C:\Program Files\libvirtualhid\tools\windows\virtualhid_control.exe
C:\Program Files\libvirtualhid\tools\windows\gamepad_adapter.exe
Required validation:
$installRoot = Join-Path $env:ProgramFiles "libvirtualhid"
powershell -ExecutionPolicy Bypass -File "$installRoot\scripts\windows\test-installed-driver.ps1" `
-GamepadAdapterPath "$installRoot\tools\windows\gamepad_adapter.exe" `
-GamepadProfile xseries `
-Verbose
& "$installRoot\tools\windows\virtualhid_control.exe"
In the libvirtualhid control window, 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.
Expected result:
- The command exits successfully.
- The ROOT\LIBVIRTUALHID control device reports Status: Started.
- The \\.\LibVirtualHid control device opens successfully.
- The UI launches without requiring SDK or WDK tools.
- The backend status reports windows-umdf with gamepad support available.
- A virtual HID gamepad is created and appears in the device list.
- A virtual HID gamepad child device starts, matching either
HID\VID_045E&PID_0B12&IG_00 or HID\VID_045E&PID_02FF&IG_00.
- Button and axis values in the UI can be pressed or moved without errors.
Optional browser validation:
$installRoot = Join-Path $env:ProgramFiles "libvirtualhid"
powershell -ExecutionPolicy Bypass -File "$installRoot\scripts\windows\test-browser-gamepad.ps1" `
-GamepadAdapterPath "$installRoot\tools\windows\gamepad_adapter.exe" `
-GamepadProfile xseries `
-KeepBrowserOpen
& "$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:
- Microsoft Edge or Google Chrome opens a gamepad test page.
- Microsoft Edge, Google Chrome, or another desktop browser opens a gamepad
test page.
- The browser Gamepad API sees an Xbox-compatible controller.
- Button and axis values change while the validation adapter is running.
- Button and axis values change while controls are used in the validation UI.
The installed gamepad_adapter.exe is built with the static MSVC runtime, so it
does not require the Visual C++ Redistributable to be installed separately.
The installed virtualhid_control.exe and gamepad_adapter.exe binaries are built
with the static MSVC runtime, so they do not require the Visual C++
Redistributable to be installed separately.
```
## Manual Review Steps
@@ -66,8 +71,8 @@ does not require the Visual C++ Redistributable to be installed separately.
`libvirtualhid-Windows-Driver-installer.msi`.
2. Reboot only if Windows reports that a reboot is required.
3. Open PowerShell.
4. Run the required validation command from the submission notes.
5. Optionally run the browser validation command.
4. Run the required validation tool from the submission notes.
5. 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.
@@ -85,4 +90,4 @@ HID-only and intentionally does not emulate the Xbox 360 XUSB stack.
The reviewer-visible success signal is the installed `ROOT\LIBVIRTUALHID`
control device, the `\\.\LibVirtualHid` control path, and a started HID gamepad
child device while `gamepad_adapter.exe` is running.
child device while `virtualhid_control.exe` has a gamepad created.
+33
View File
@@ -56,6 +56,17 @@ unless they explicitly enable additional options.
level project.
- `BUILD_DOCS`: build Doxygen documentation when this repository is the top
level project.
- `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_INSTALL`: install targets, headers, and CMake package files.
This defaults to on for direct builds and off when consumed by another CMake
project.
@@ -71,6 +82,28 @@ Linux consumers need the backend development packages used by the build, such as
with MSVC or MinGW/UCRT64; the UMDF driver package is a separate WDK/MSVC build
artifact.
## Diagnostic UI
`virtualhid_control` is an optional native diagnostic UI binary:
```bash
virtualhid_control
```
The current Windows 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, 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; enable `Lock buttons` to keep the old click-to-toggle behavior
for held inputs.
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:
+27 -8
View File
@@ -22,9 +22,10 @@ User-mode virtual HID driver package that enables compatible apps to create virt
Virtual HID Driver installs the user-mode driver component used by compatible
applications to create virtual HID gamepads on Windows.
The package has no standalone user interface. After installation, compatible
applications can request virtual HID gamepads, and Windows applications that
understand standard HID gamepads can discover those devices.
The package includes a local diagnostic UI for creating and testing virtual
gamepads. Compatible applications can also request virtual HID gamepads, and
Windows applications that understand standard HID gamepads can discover those
devices.
```
## Architecture
@@ -51,9 +52,9 @@ Build the UMDF package with a Visual Studio generator and the WDK installed:
```powershell
cmake -S . -B cmake-build-windows-driver -G "Visual Studio 17 2022" -A x64 `
-DLIBVIRTUALHID_BUILD_WINDOWS_DRIVER=ON -DLIBVIRTUALHID_ENABLE_PACKAGING=ON `
-DBUILD_TESTS=OFF -DBUILD_EXAMPLES=ON
-DBUILD_TESTS=OFF -DBUILD_EXAMPLES=ON -DLIBVIRTUALHID_BUILD_TOOLS=ON
cmake --build cmake-build-windows-driver --config Release `
--target libvirtualhid_windows_catalog gamepad_adapter
--target libvirtualhid_windows_catalog gamepad_adapter virtualhid_control
cpack -G WIX -C Release --config .\cmake-build-windows-driver\CPackConfig.cmake
```
@@ -83,9 +84,13 @@ powershell -ExecutionPolicy Bypass -File .\scripts\windows\uninstall-driver.ps1
The WiX installer also places validation files under the default install root,
`C:\Program Files\libvirtualhid`:
- `scripts\windows\test-installed-driver.ps1`
- `scripts\windows\test-browser-gamepad.ps1`
- `tools\windows\gamepad_adapter.exe`
- `tools\windows\virtualhid_control.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 interactively.
The install helper stages the INF with `pnputil`, updates an existing
`ROOT\LIBVIRTUALHID` device when present, and creates that root-enumerated
@@ -99,7 +104,7 @@ 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`,
or run:
run the interactive UI, or run:
```powershell
tools\windows\gamepad_adapter.exe xseries --hold-seconds 60
@@ -109,6 +114,20 @@ 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 that it owns.
Buttons are momentary by default, with an explicit lock mode for held inputs.
The UI also shows supported profile features, battery input state, device nodes,
and normalized 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.
## Installation Notes
The driver binary is a user-mode UMDF DLL installed through the Windows Driver
+17
View File
@@ -19,3 +19,20 @@ endif()
libvirtualhid_copy_mingw_runtime(gamepad_adapter)
libvirtualhid_copy_mingw_runtime(keyboard_mouse_adapter)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux" OR WIN32)
set(RUN_GAMEPAD_EXAMPLE_COMMAND "$<TARGET_FILE:gamepad_adapter>")
set(RUN_GAMEPAD_EXAMPLE_COMMENT "Running gamepad_adapter example")
else()
set(RUN_GAMEPAD_EXAMPLE_COMMAND
"${CMAKE_COMMAND}" -E echo
"Skipping gamepad_adapter example: no ${CMAKE_SYSTEM_NAME} backend")
set(RUN_GAMEPAD_EXAMPLE_COMMENT "Skipping gamepad_adapter example")
endif()
add_custom_target(run_gamepad_adapter_example
COMMAND ${RUN_GAMEPAD_EXAMPLE_COMMAND}
COMMENT "${RUN_GAMEPAD_EXAMPLE_COMMENT}"
DEPENDS gamepad_adapter
VERBATIM
USES_TERMINAL)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

+11
View File
@@ -31,6 +31,11 @@ set(LIBVIRTUALHID_TEST_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/unit/test_runtime.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/unit/test_windows_protocol.cpp")
if(TARGET virtualhid_control_model)
list(APPEND LIBVIRTUALHID_TEST_SOURCES
"${CMAKE_CURRENT_SOURCE_DIR}/unit/test_virtualhid_control_model.cpp")
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(PkgConfig REQUIRED)
pkg_check_modules(LIBEVDEV REQUIRED IMPORTED_TARGET libevdev)
@@ -64,6 +69,12 @@ target_link_libraries(${TEST_BINARY}
libvirtualhid::libvirtualhid
lizardbyte::test_support)
if(TARGET virtualhid_control_model)
target_link_libraries(${TEST_BINARY}
PRIVATE
virtualhid_control_model)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_link_libraries(${TEST_BINARY}
PRIVATE
@@ -0,0 +1,245 @@
/**
* @file tests/unit/test_virtualhid_control_model.cpp
* @brief Unit tests for virtualhid_control model helpers.
*/
// standard includes
#include <array>
#include <cstdint>
#include <string>
#include <vector>
// third-party includes
#include <gtest/gtest.h>
// local includes
#include <virtualhid_control_model.hpp>
namespace {
namespace control = lvh::tools::virtualhid_control;
bool visible_button(
const std::array<bool, control::button_choices.size()> &visible_buttons,
lvh::GamepadButton button
) {
for (std::size_t index = 0; index < control::button_choices.size(); ++index) {
if (control::button_choices[index].button == button) {
return visible_buttons[index];
}
}
return false;
}
lvh::GamepadOutput output(lvh::GamepadOutputKind kind) {
lvh::GamepadOutput value;
value.kind = kind;
return value;
}
} // namespace
TEST(VirtualHidControlModelTest, NamesKnownAndFallbackEnumValues) {
EXPECT_EQ(control::device_type_name(lvh::DeviceType::gamepad), L"gamepad");
EXPECT_EQ(control::device_type_name(lvh::DeviceType::keyboard), L"keyboard");
EXPECT_EQ(control::device_type_name(lvh::DeviceType::mouse), L"mouse");
EXPECT_EQ(control::device_type_name(lvh::DeviceType::touchscreen), L"touchscreen");
EXPECT_EQ(control::device_type_name(lvh::DeviceType::trackpad), L"trackpad");
EXPECT_EQ(control::device_type_name(lvh::DeviceType::pen_tablet), L"pen tablet");
EXPECT_EQ(control::device_type_name(static_cast<lvh::DeviceType>(255)), L"unknown");
EXPECT_EQ(control::node_kind_name(lvh::DeviceNodeKind::input_event), L"input");
EXPECT_EQ(control::node_kind_name(lvh::DeviceNodeKind::joystick), L"joystick");
EXPECT_EQ(control::node_kind_name(lvh::DeviceNodeKind::hidraw), L"hidraw");
EXPECT_EQ(control::node_kind_name(lvh::DeviceNodeKind::sysfs), L"sysfs");
EXPECT_EQ(control::node_kind_name(lvh::DeviceNodeKind::other), L"other");
EXPECT_EQ(control::node_kind_name(static_cast<lvh::DeviceNodeKind>(255)), L"other");
EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::rumble), L"rumble");
EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::rgb_led), L"rgb led");
EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::adaptive_triggers), L"adaptive triggers");
EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::raw_report), L"raw report");
EXPECT_EQ(control::output_kind_name(lvh::GamepadOutputKind::trigger_rumble), L"trigger rumble");
EXPECT_EQ(control::output_kind_name(static_cast<lvh::GamepadOutputKind>(255)), L"raw report");
EXPECT_EQ(control::battery_state_name(lvh::GamepadBatteryState::unknown), L"unknown");
EXPECT_EQ(control::battery_state_name(lvh::GamepadBatteryState::discharging), L"discharging");
EXPECT_EQ(control::battery_state_name(lvh::GamepadBatteryState::charging), L"charging");
EXPECT_EQ(control::battery_state_name(lvh::GamepadBatteryState::full), L"full");
EXPECT_EQ(
control::battery_state_name(lvh::GamepadBatteryState::voltage_or_temperature_error),
L"voltage/temperature error"
);
EXPECT_EQ(control::battery_state_name(lvh::GamepadBatteryState::temperature_error), L"temperature error");
EXPECT_EQ(control::battery_state_name(lvh::GamepadBatteryState::charging_error), L"charging error");
EXPECT_EQ(control::battery_state_name(static_cast<lvh::GamepadBatteryState>(255)), L"unknown");
EXPECT_EQ(control::yes_no(true), L"yes");
EXPECT_EQ(control::yes_no(false), L"no");
}
TEST(VirtualHidControlModelTest, MapsProfileChoicesToProfiles) {
for (const auto &choice : control::profile_choices) {
const auto profile = control::profile_for_choice(choice);
ASSERT_TRUE(profile.has_value()) << std::string(choice.id.begin(), choice.id.end());
EXPECT_EQ(profile->device_type, lvh::DeviceType::gamepad);
EXPECT_EQ(profile->gamepad_kind, choice.kind);
EXPECT_FALSE(profile->name.empty());
}
const control::ProfileChoice invalid {
L"invalid",
L"Invalid",
static_cast<lvh::GamepadProfileKind>(255),
lvh::ClientControllerType::unknown,
};
EXPECT_FALSE(control::profile_for_choice(invalid).has_value());
}
TEST(VirtualHidControlModelTest, ConvertsSliderValues) {
EXPECT_EQ(control::axis_to_slider(-2.0F), -control::slider_scale);
EXPECT_EQ(control::axis_to_slider(-0.5F), -50);
EXPECT_EQ(control::axis_to_slider(0.0F), 0);
EXPECT_EQ(control::axis_to_slider(0.5F), 50);
EXPECT_EQ(control::axis_to_slider(2.0F), control::slider_scale);
EXPECT_EQ(control::trigger_to_slider(-1.0F), 0);
EXPECT_EQ(control::trigger_to_slider(0.25F), 25);
EXPECT_EQ(control::trigger_to_slider(1.0F), control::slider_scale);
EXPECT_EQ(control::trigger_to_slider(2.0F), control::slider_scale);
EXPECT_FLOAT_EQ(control::slider_to_float(-25), -0.25F);
EXPECT_FLOAT_EQ(control::slider_to_float(0), 0.0F);
EXPECT_FLOAT_EQ(control::slider_to_float(75), 0.75F);
}
TEST(VirtualHidControlModelTest, MapsBatteryComboChoices) {
for (std::size_t index = 0; index < control::battery_choices.size(); ++index) {
EXPECT_EQ(control::battery_choice_index(control::battery_choices[index].state), static_cast<int>(index));
}
EXPECT_EQ(control::battery_choice_index(static_cast<lvh::GamepadBatteryState>(255)), 0);
}
TEST(VirtualHidControlModelTest, FormatsRawHex) {
EXPECT_EQ(control::raw_hex({}), L"");
EXPECT_EQ(control::raw_hex({0x00, 0x0F, 0xA5, 0xFF}), L"000fa5ff");
}
TEST(VirtualHidControlModelTest, SummarizesProfileFeatures) {
const auto generic = lvh::profiles::generic_gamepad();
const auto dualsense = lvh::profiles::dualsense();
EXPECT_FALSE(control::supports_normalized_feedback(generic));
EXPECT_TRUE(control::supports_normalized_feedback(dualsense));
EXPECT_EQ(
control::profile_feature_summary(generic),
L"Features: battery no | rumble no | trigger rumble no | RGB LED no | adaptive triggers no | raw output no"
);
EXPECT_EQ(
control::profile_feature_summary(dualsense),
L"Features: battery yes | rumble yes | trigger rumble no | RGB LED yes | adaptive triggers yes | raw output yes"
);
}
TEST(VirtualHidControlModelTest, UpdatesVisibleControlsForProfiles) {
std::array<bool, control::button_choices.size()> visible_buttons {};
auto battery_visible = false;
const auto dualshock4 = lvh::profiles::dualshock4();
EXPECT_TRUE(control::update_visible_controls_for_profile(dualshock4, visible_buttons, battery_visible));
EXPECT_TRUE(visible_button(visible_buttons, lvh::GamepadButton::a));
EXPECT_TRUE(visible_button(visible_buttons, lvh::GamepadButton::touchpad));
EXPECT_FALSE(visible_button(visible_buttons, lvh::GamepadButton::misc1));
EXPECT_FALSE(visible_button(visible_buttons, lvh::GamepadButton::paddle1));
EXPECT_TRUE(battery_visible);
EXPECT_FALSE(control::update_visible_controls_for_profile(dualshock4, visible_buttons, battery_visible));
const auto generic = lvh::profiles::generic_gamepad();
EXPECT_TRUE(control::update_visible_controls_for_profile(generic, visible_buttons, battery_visible));
EXPECT_TRUE(visible_button(visible_buttons, lvh::GamepadButton::misc1));
EXPECT_FALSE(visible_button(visible_buttons, lvh::GamepadButton::touchpad));
EXPECT_FALSE(battery_visible);
}
TEST(VirtualHidControlModelTest, SummarizesOutputState) {
using enum lvh::GamepadOutputKind;
const auto generic = lvh::profiles::generic_gamepad();
const auto dualsense = lvh::profiles::dualsense();
control::OutputState state;
EXPECT_EQ(
control::output_summary(state, generic),
L"Output: no reports received | profile has no normalized feedback categories"
);
EXPECT_EQ(control::output_summary(state, dualsense), L"Output: no reports received");
state.outputs.push_back({.sequence = 1, .output = output(raw_report)});
EXPECT_EQ(control::output_summary(state, dualsense), L"Output: reports received");
state.latest_raw_report = output(raw_report);
EXPECT_EQ(control::output_summary(state, dualsense), L"Output: raw report");
state.latest_rumble = output(rumble);
state.latest_rumble->low_frequency_rumble = 10;
state.latest_rumble->high_frequency_rumble = 20;
state.latest_trigger_rumble = output(trigger_rumble);
state.latest_trigger_rumble->left_trigger_rumble = 30;
state.latest_trigger_rumble->right_trigger_rumble = 40;
state.latest_rgb_led = output(rgb_led);
state.latest_rgb_led->red = 1;
state.latest_rgb_led->green = 2;
state.latest_rgb_led->blue = 3;
state.latest_adaptive_triggers = output(adaptive_triggers);
state.latest_adaptive_triggers->adaptive_trigger_flags = 4;
EXPECT_EQ(
control::output_summary(state, dualsense),
L"Output: rumble low=10 high=20 | trigger rumble L=30 R=40 | RGB 1,2,3 | adaptive flags=4"
);
}
TEST(VirtualHidControlModelTest, RecordsOutputsAndMaintainsLatestSummaryFields) {
control::OutputState state;
auto next_sequence = std::uint64_t {7};
auto rumble = output(lvh::GamepadOutputKind::rumble);
rumble.low_frequency_rumble = 100;
rumble.high_frequency_rumble = 200;
control::record_output(state, rumble, next_sequence, 3);
auto trigger_rumble = output(lvh::GamepadOutputKind::trigger_rumble);
trigger_rumble.left_trigger_rumble = 300;
trigger_rumble.right_trigger_rumble = 400;
control::record_output(state, trigger_rumble, next_sequence, 3);
auto rgb = output(lvh::GamepadOutputKind::rgb_led);
rgb.red = 5;
rgb.green = 6;
rgb.blue = 7;
control::record_output(state, rgb, next_sequence, 3);
auto adaptive = output(lvh::GamepadOutputKind::adaptive_triggers);
adaptive.adaptive_trigger_flags = 8;
control::record_output(state, adaptive, next_sequence, 3);
auto raw = output(lvh::GamepadOutputKind::raw_report);
raw.raw_report = {0x12, 0x34};
control::record_output(state, raw, next_sequence, 3);
ASSERT_EQ(state.outputs.size(), 3U);
EXPECT_EQ(state.outputs.front().sequence, 9U);
EXPECT_EQ(state.outputs.back().sequence, 11U);
EXPECT_EQ(next_sequence, 12U);
ASSERT_TRUE(state.latest_rumble.has_value());
EXPECT_EQ(state.latest_rumble->low_frequency_rumble, 100);
ASSERT_TRUE(state.latest_trigger_rumble.has_value());
EXPECT_EQ(state.latest_trigger_rumble->right_trigger_rumble, 400);
ASSERT_TRUE(state.latest_rgb_led.has_value());
EXPECT_EQ(state.latest_rgb_led->blue, 7);
ASSERT_TRUE(state.latest_adaptive_triggers.has_value());
EXPECT_EQ(state.latest_adaptive_triggers->adaptive_trigger_flags, 8);
ASSERT_TRUE(state.latest_raw_report.has_value());
EXPECT_EQ(state.latest_raw_report->raw_report, (std::vector<std::uint8_t> {0x12, 0x34}));
}
+66
View File
@@ -0,0 +1,66 @@
add_library(virtualhid_control_model STATIC
"${CMAKE_CURRENT_SOURCE_DIR}/virtualhid_control_model.cpp")
target_include_directories(virtualhid_control_model
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(virtualhid_control_model
PUBLIC
libvirtualhid::libvirtualhid)
add_executable(virtualhid_control
"${CMAKE_CURRENT_SOURCE_DIR}/virtualhid_control.cpp")
target_link_libraries(virtualhid_control
PRIVATE
virtualhid_control_model)
if(WIN32)
set_target_properties(virtualhid_control PROPERTIES
WIN32_EXECUTABLE TRUE)
target_sources(virtualhid_control
PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/windows/virtualhid_control.rc")
target_include_directories(virtualhid_control
PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/windows"
"${PROJECT_SOURCE_DIR}")
target_compile_definitions(virtualhid_control
PRIVATE
NOMINMAX
WIN32_LEAN_AND_MEAN
_WIN32_WINNT=0x0600)
target_link_libraries(virtualhid_control
PRIVATE
comctl32
gdi32
user32)
endif()
if(MSVC AND LIBVIRTUALHID_TOOLS_STATIC_RUNTIME AND LIBVIRTUALHID_BUILD_WINDOWS_DRIVER)
set_property(TARGET virtualhid_control virtualhid_control_model PROPERTY
MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND LIBVIRTUALHID_TOOLS_STATIC_RUNTIME)
target_link_options(virtualhid_control
PRIVATE
-static-libgcc
-static-libstdc++)
if(WIN32)
target_link_options(virtualhid_control
PRIVATE
-static)
endif()
endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND LIBVIRTUALHID_TOOLS_FULLY_STATIC)
target_link_options(virtualhid_control
PRIVATE
-static)
endif()
install(TARGETS virtualhid_control
RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}")
File diff suppressed because it is too large. Load diff
+281
View File
@@ -0,0 +1,281 @@
/**
* @file tools/virtualhid_control_model.cpp
* @brief Testable model helper definitions for the native libvirtualhid control UI.
*/
// standard includes
#include <algorithm>
#include <cmath>
#include <iomanip>
#include <sstream>
// local includes
#include "virtualhid_control_model.hpp"
namespace lvh::tools::virtualhid_control {
namespace {
void append_summary_separator(std::wostringstream &stream, bool wrote) {
if (wrote) {
stream << L" | ";
}
}
} // namespace
std::wstring device_type_name(DeviceType type) {
switch (type) {
using enum DeviceType;
case gamepad:
return L"gamepad";
case keyboard:
return L"keyboard";
case mouse:
return L"mouse";
case touchscreen:
return L"touchscreen";
case trackpad:
return L"trackpad";
case pen_tablet:
return L"pen tablet";
}
return L"unknown";
}
std::wstring node_kind_name(DeviceNodeKind kind) {
switch (kind) {
using enum DeviceNodeKind;
case input_event:
return L"input";
case joystick:
return L"joystick";
case hidraw:
return L"hidraw";
case sysfs:
return L"sysfs";
case other:
return L"other";
}
return L"other";
}
std::wstring output_kind_name(GamepadOutputKind kind) {
switch (kind) {
using enum GamepadOutputKind;
case rumble:
return L"rumble";
case rgb_led:
return L"rgb led";
case adaptive_triggers:
return L"adaptive triggers";
case raw_report:
return L"raw report";
case trigger_rumble:
return L"trigger rumble";
}
return L"raw report";
}
std::wstring battery_state_name(GamepadBatteryState state) {
switch (state) {
using enum GamepadBatteryState;
case unknown:
return L"unknown";
case discharging:
return L"discharging";
case charging:
return L"charging";
case full:
return L"full";
case voltage_or_temperature_error:
return L"voltage/temperature error";
case temperature_error:
return L"temperature error";
case charging_error:
return L"charging error";
}
return L"unknown";
}
int battery_choice_index(GamepadBatteryState state) {
for (std::size_t index = 0; index < battery_choices.size(); ++index) {
if (battery_choices[index].state == state) {
return static_cast<int>(index);
}
}
return 0;
}
std::wstring raw_hex(const std::vector<std::uint8_t> &bytes) {
std::wostringstream stream;
stream << std::hex << std::setfill(L'0');
for (const auto value : bytes) {
stream << std::setw(2) << static_cast<unsigned>(value);
}
return stream.str();
}
std::optional<DeviceProfile> profile_for_choice(const ProfileChoice &choice) {
switch (choice.kind) {
using enum GamepadProfileKind;
case generic:
return profiles::generic_gamepad();
case xbox_360:
return profiles::xbox_360();
case xbox_one:
return profiles::xbox_one();
case xbox_series:
return profiles::xbox_series();
case dualshock4:
return profiles::dualshock4();
case dualsense:
return profiles::dualsense();
case switch_pro:
return profiles::switch_pro();
}
return std::nullopt;
}
int axis_to_slider(float value) {
return static_cast<int>(std::lround(std::clamp(value, -1.0F, 1.0F) * static_cast<float>(slider_scale)));
}
int trigger_to_slider(float value) {
return static_cast<int>(std::lround(std::clamp(value, 0.0F, 1.0F) * static_cast<float>(slider_scale)));
}
float slider_to_float(long value) {
return static_cast<float>(value) / static_cast<float>(slider_scale);
}
std::wstring yes_no(bool value) {
return value ? L"yes" : L"no";
}
bool supports_normalized_feedback(const DeviceProfile &profile) {
using enum GamepadOutputKind;
return supports_gamepad_output(profile, rumble) ||
supports_gamepad_output(profile, rgb_led) ||
supports_gamepad_output(profile, adaptive_triggers) ||
supports_gamepad_output(profile, trigger_rumble);
}
std::wstring profile_feature_summary(const DeviceProfile &profile) {
using enum GamepadOutputKind;
const auto support = gamepad_profile_support(profile);
std::wostringstream stream;
stream << L"Features: battery " << yes_no(support.supports_battery);
stream << L" | rumble " << yes_no(supports_gamepad_output(profile, rumble));
stream << L" | trigger rumble " << yes_no(supports_gamepad_output(profile, trigger_rumble));
stream << L" | RGB LED " << yes_no(supports_gamepad_output(profile, rgb_led));
stream << L" | adaptive triggers " << yes_no(supports_gamepad_output(profile, adaptive_triggers));
stream << L" | raw output " << yes_no(supports_gamepad_output(profile, raw_report));
return stream.str();
}
bool append_latest_output_summary(std::wostringstream &stream, const OutputState &state) {
auto wrote = false;
if (state.latest_rumble) {
stream << L"rumble low=" << state.latest_rumble->low_frequency_rumble
<< L" high=" << state.latest_rumble->high_frequency_rumble;
wrote = true;
}
if (state.latest_trigger_rumble) {
append_summary_separator(stream, wrote);
stream << L"trigger rumble L=" << state.latest_trigger_rumble->left_trigger_rumble
<< L" R=" << state.latest_trigger_rumble->right_trigger_rumble;
wrote = true;
}
if (state.latest_rgb_led) {
append_summary_separator(stream, wrote);
stream << L"RGB " << static_cast<unsigned>(state.latest_rgb_led->red) << L","
<< static_cast<unsigned>(state.latest_rgb_led->green) << L","
<< static_cast<unsigned>(state.latest_rgb_led->blue);
wrote = true;
}
if (state.latest_adaptive_triggers) {
append_summary_separator(stream, wrote);
stream << L"adaptive flags=" << static_cast<unsigned>(state.latest_adaptive_triggers->adaptive_trigger_flags);
wrote = true;
}
if (!wrote && state.latest_raw_report) {
stream << L"raw report";
wrote = true;
}
return wrote;
}
std::wstring output_summary(const OutputState &state, const DeviceProfile &profile) {
std::wostringstream stream;
stream << L"Output: ";
if (state.outputs.empty()) {
stream << L"no reports received";
} else if (!append_latest_output_summary(stream, state)) {
stream << L"reports received";
}
if (!supports_normalized_feedback(profile)) {
stream << L" | profile has no normalized feedback categories";
}
return stream.str();
}
bool update_visible_controls_for_profile(
const DeviceProfile &profile,
std::array<bool, button_choices.size()> &visible_buttons,
bool &battery_controls_visible
) {
auto changed = false;
for (std::size_t index = 0; index < button_choices.size(); ++index) {
const auto visible = supports_gamepad_button(profile, button_choices[index].button);
changed = changed || visible_buttons[index] != visible;
visible_buttons[index] = visible;
}
changed = changed || battery_controls_visible != profile.capabilities.supports_battery;
battery_controls_visible = profile.capabilities.supports_battery;
return changed;
}
void record_output(
OutputState &state,
const GamepadOutput &output,
std::uint64_t &next_sequence,
std::size_t max_output_events
) {
state.outputs.push_back({.sequence = next_sequence++, .output = output});
if (state.outputs.size() > max_output_events) {
state.outputs.erase(
state.outputs.begin(),
state.outputs.begin() + static_cast<std::ptrdiff_t>(state.outputs.size() - max_output_events)
);
}
switch (output.kind) {
using enum GamepadOutputKind;
case rumble:
state.latest_rumble = output;
break;
case trigger_rumble:
state.latest_trigger_rumble = output;
break;
case rgb_led:
state.latest_rgb_led = output;
break;
case adaptive_triggers:
state.latest_adaptive_triggers = output;
break;
case raw_report:
state.latest_raw_report = output;
break;
}
}
} // namespace lvh::tools::virtualhid_control
+142
View File
@@ -0,0 +1,142 @@
/**
* @file tools/virtualhid_control_model.hpp
* @brief Testable model helpers for the native libvirtualhid control UI.
*/
#pragma once
// standard includes
#include <array>
#include <cstddef>
#include <cstdint>
#include <iosfwd>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
// local includes
#include <libvirtualhid/libvirtualhid.hpp>
namespace lvh::tools::virtualhid_control {
inline constexpr auto slider_scale = 100;
struct ProfileChoice {
std::wstring_view id;
std::wstring_view label;
GamepadProfileKind kind;
ClientControllerType client_type;
};
struct ButtonChoice {
std::wstring_view label;
GamepadButton button;
};
struct AxisChoice {
std::wstring_view label;
int minimum;
int maximum;
};
struct BatteryChoice {
std::wstring_view label;
GamepadBatteryState state;
};
struct OutputLogEntry {
std::uint64_t sequence = 0;
GamepadOutput output;
};
struct OutputState {
std::vector<OutputLogEntry> outputs;
std::optional<GamepadOutput> latest_rumble;
std::optional<GamepadOutput> latest_trigger_rumble;
std::optional<GamepadOutput> latest_rgb_led;
std::optional<GamepadOutput> latest_adaptive_triggers;
std::optional<GamepadOutput> latest_raw_report;
};
inline constexpr std::array profile_choices {
ProfileChoice {L"generic", L"Generic HID", GamepadProfileKind::generic, ClientControllerType::unknown},
ProfileChoice {L"x360", L"Xbox 360", GamepadProfileKind::xbox_360, ClientControllerType::xbox},
ProfileChoice {L"xone", L"Xbox One", GamepadProfileKind::xbox_one, ClientControllerType::xbox},
ProfileChoice {L"xseries", L"Xbox Series", GamepadProfileKind::xbox_series, ClientControllerType::xbox},
ProfileChoice {L"ds4", L"DualShock 4", GamepadProfileKind::dualshock4, ClientControllerType::playstation},
ProfileChoice {L"ds5", L"DualSense", GamepadProfileKind::dualsense, ClientControllerType::playstation},
ProfileChoice {L"switch", L"Switch Pro", GamepadProfileKind::switch_pro, ClientControllerType::nintendo},
};
inline constexpr std::array button_choices {
ButtonChoice {L"A", GamepadButton::a},
ButtonChoice {L"B", GamepadButton::b},
ButtonChoice {L"X", GamepadButton::x},
ButtonChoice {L"Y", GamepadButton::y},
ButtonChoice {L"Back", GamepadButton::back},
ButtonChoice {L"Start", GamepadButton::start},
ButtonChoice {L"Guide", GamepadButton::guide},
ButtonChoice {L"L3", GamepadButton::left_stick},
ButtonChoice {L"R3", GamepadButton::right_stick},
ButtonChoice {L"LB", GamepadButton::left_shoulder},
ButtonChoice {L"RB", GamepadButton::right_shoulder},
ButtonChoice {L"D-pad Up", GamepadButton::dpad_up},
ButtonChoice {L"D-pad Down", GamepadButton::dpad_down},
ButtonChoice {L"D-pad Left", GamepadButton::dpad_left},
ButtonChoice {L"D-pad Right", GamepadButton::dpad_right},
ButtonChoice {L"Misc", GamepadButton::misc1},
ButtonChoice {L"Touchpad", GamepadButton::touchpad},
ButtonChoice {L"Paddle 1", GamepadButton::paddle1},
ButtonChoice {L"Paddle 2", GamepadButton::paddle2},
ButtonChoice {L"Paddle 3", GamepadButton::paddle3},
ButtonChoice {L"Paddle 4", GamepadButton::paddle4},
};
inline constexpr std::array axis_choices {
AxisChoice {L"Left X", -slider_scale, slider_scale},
AxisChoice {L"Left Y", -slider_scale, slider_scale},
AxisChoice {L"Right X", -slider_scale, slider_scale},
AxisChoice {L"Right Y", -slider_scale, slider_scale},
AxisChoice {L"Left Trigger", 0, slider_scale},
AxisChoice {L"Right Trigger", 0, slider_scale},
};
inline constexpr std::array battery_choices {
BatteryChoice {L"Unknown", GamepadBatteryState::unknown},
BatteryChoice {L"Discharging", GamepadBatteryState::discharging},
BatteryChoice {L"Charging", GamepadBatteryState::charging},
BatteryChoice {L"Full", GamepadBatteryState::full},
BatteryChoice {L"Voltage/temperature error", GamepadBatteryState::voltage_or_temperature_error},
BatteryChoice {L"Temperature error", GamepadBatteryState::temperature_error},
BatteryChoice {L"Charging error", GamepadBatteryState::charging_error},
};
std::wstring device_type_name(DeviceType type);
std::wstring node_kind_name(DeviceNodeKind kind);
std::wstring output_kind_name(GamepadOutputKind kind);
std::wstring battery_state_name(GamepadBatteryState state);
int battery_choice_index(GamepadBatteryState state);
std::wstring raw_hex(const std::vector<std::uint8_t> &bytes);
std::optional<DeviceProfile> profile_for_choice(const ProfileChoice &choice);
int axis_to_slider(float value);
int trigger_to_slider(float value);
float slider_to_float(long value);
std::wstring yes_no(bool value);
bool supports_normalized_feedback(const DeviceProfile &profile);
std::wstring profile_feature_summary(const DeviceProfile &profile);
bool append_latest_output_summary(std::wostringstream &stream, const OutputState &state);
std::wstring output_summary(const OutputState &state, const DeviceProfile &profile);
bool update_visible_controls_for_profile(
const DeviceProfile &profile,
std::array<bool, button_choices.size()> &visible_buttons,
bool &battery_controls_visible
);
void record_output(
OutputState &state,
const GamepadOutput &output,
std::uint64_t &next_sequence,
std::size_t max_output_events
);
} // namespace lvh::tools::virtualhid_control
+1
View File
@@ -0,0 +1 @@
101 ICON "libvirtualhid.ico"