Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .github/workflows/ci-build.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -181,9 +181,8 @@ jobs:
echo "::warning::${kernel_modules_package} is unavailable; relying on the runner image kernel modules."
fi
sudo tee /etc/udev/rules.d/99-libvirtualhid-ci.rules >/dev/null <<'EOF'
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", MODE="0666", TAG+="uaccess"
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess"
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess"
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="libvirtualhid*", MODE="0666", TAG+="uaccess"
EOF
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,14 +45,14 @@ behind backend implementations.

- Gamepad profiles for generic HID, Xbox 360, Xbox One, Xbox Series,
DualShock 4, DualSense, and Nintendo Switch Pro-style controllers.
- Descriptor-driven PlayStation gamepads through Linux `uhid`; Generic, Xbox,
and Switch Pro gamepads plus keyboard, mouse, touchscreen, trackpad, and pen
tablet devices through `uinput`.
- Descriptor-driven PlayStation and Switch Pro gamepads through Linux `uhid`;
Generic and Xbox gamepads plus keyboard, mouse, touchscreen, trackpad, and
pen tablet devices through `uinput`.
- Windows gamepads, keyboards, and Raw Input-visible mice through a user-mode
UMDF2 control driver backed by Virtual HID Framework, with Win32 keyboard and
mouse fallbacks when the licensed driver path is unavailable.
- Output callbacks for profile-specific feedback such as rumble, LEDs,
adaptive triggers, and raw HID output reports when available.
- Output callbacks for profile-specific feedback such as rumble, RGB and player
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 and mice through the public C++ API.
- CMake consumption through installed packages, vendored source,
Expand Down
49 changes: 29 additions & 20 deletions docs/end-user-gamepad-guide.md

Large diffs are not rendered by default.

45 changes: 30 additions & 15 deletions docs/platform-support.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -81,7 +81,15 @@ 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.
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.

Windows VHF devices do not expose a Bluetooth transport identity to HIDAPI.
The Windows backend therefore reports DualShock 4 and DualSense requests as
Expand All@@ -101,26 +109,23 @@ and signing details.

The Linux backend uses standard user-space kernel interfaces:

- `uhid` for descriptor-driven HID gamepads.
- `uinput` for Generic, Xbox 360, Xbox One, Xbox Series, and Switch Pro
gamepads, plus keyboard, mouse, touchscreen, trackpad, and pen tablet
devices.
- `uhid` for descriptor-driven PlayStation and Switch Pro gamepads.
- `uinput` for Generic, Xbox 360, Xbox One, and Xbox Series gamepads, plus
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.

Gamepad support normally prefers `uhid` because descriptors, raw HID identity,
feature reports, and output reports matter for controller compatibility.
Generic, Xbox-family, and Switch Pro profiles instead use `uinput` so SDL,
Generic and Xbox-family profiles instead use `uinput` so SDL,
Steam, browser Gamepad API implementations, and other evdev consumers receive
canonical Linux gamepad events. 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.
Switch Pro uses the Nintendo face-button
positions, button events for ZL/ZR, and `BTN_Z` for Capture. Profiles with rumble
support normalize rumble, constant, periodic, and ramp uinput force-feedback
effects back into the public callback. Each requested playback repetition
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
Expand DownExpand Up@@ -182,10 +187,16 @@ 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 keeps its Nintendo identity on the Linux uinput path. This follows
the evdev layout used by Linux-native virtual-controller implementations and
allows standard `FF_RUMBLE` effects without emulating the physical controller's
proprietary initialization handshake.
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 touchscreen and trackpad contacts use the lowest available multitouch
slot while they are active. A newly placed contact receives a new tracking ID,
Expand All@@ -194,7 +205,11 @@ 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.
The original native report remains available in `GamepadOutput::raw_report`.
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
Expand Down
11 changes: 7 additions & 4 deletions docs/streaming-host-integration.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,8 +13,9 @@ A streaming host should be able to:
indexes.
- Submit incremental button, axis, trigger, touchpad, motion, and battery
updates without recreating a device.
- Receive output callbacks for rumble, LEDs, adaptive triggers, trigger rumble,
and raw output reports where the selected profile supports them.
- 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
Expand DownExpand Up@@ -51,8 +52,10 @@ The core API and adapter shape cover the major streaming-host requirements:
- Rich controller metadata.
- Gamepad output callbacks.
- Keyboard and mouse input paths.
- Linux PlayStation gamepads through `uhid`, Generic/Xbox/Switch Pro gamepads
through `uinput`, and `uinput` keyboard/pointer devices.
- Linux PlayStation and Switch Pro gamepads through `uhid`, Generic/Xbox
gamepads through `uinput`, 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
Expand Down
13 changes: 7 additions & 6 deletions docs/usage.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -105,8 +105,8 @@ 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, adaptive trigger, trigger
rumble, and raw report events delivered through the normal callback path. Button
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, the UI also displays broker license status and can activate,
refresh, or deactivate a machine license without elevation. Windows UMDF
Expand DownExpand Up@@ -229,10 +229,11 @@ 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.

Profiles advertise support for features such as rumble, trigger rumble, RGB
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.
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.
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.
9 changes: 8 additions & 1 deletion docs/windows-driver.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -427,7 +427,14 @@ gamepad.

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.
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
Expand Down
3 changes: 3 additions & 0 deletions src/core/gamepad_adapter.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -83,6 +83,7 @@ namespace lvh {
support.supports_rumble = profile.capabilities.supports_rumble;
support.supports_rgb_led = profile.capabilities.supports_rgb_led;
support.supports_adaptive_triggers = profile.capabilities.supports_adaptive_triggers;
support.supports_player_leds = profile.capabilities.supports_player_leds;
support.supports_motion = profile.capabilities.supports_motion;
support.supports_touchpad = profile.capabilities.supports_touchpad;
support.supports_battery = profile.capabilities.supports_battery;
Expand DownExpand Up@@ -142,6 +143,8 @@ namespace lvh {
return support.supports_rgb_led;
case adaptive_triggers:
return support.supports_adaptive_triggers;
case player_leds:
return support.supports_player_leds;
case raw_report:
return profile.output_report_size > 0U;
}
Expand Down
10 changes: 8 additions & 2 deletions src/core/profiles.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -2025,13 +2025,19 @@ namespace lvh::profiles {
profile.bus_type = BusType::usb;
profile.vendor_id = 0x057E;
profile.product_id = 0x2009;
profile.version = 0x8111;
profile.version = 0x0210;
profile.report_id = switch_pro_report_id;
profile.input_report_size = switch_pro_input_report_size;
profile.output_report_size = switch_pro_output_report_size;
profile.name = "(libvirtualhid) Nintendo Pro Controller";
profile.manufacturer = "Nintendo Co., Ltd.";
profile.capabilities = {.supports_rumble = true, .supports_motion = true, .supports_battery = true};
profile.capabilities = {
.supports_rumble = true,
.supports_motion = true,
.supports_rgb_led = true,
.supports_battery = true,
.supports_player_leds = true,
};
profile.report_descriptor = make_switch_pro_report_descriptor();
return profile;
}
Expand Down
98 changes: 91 additions & 7 deletions src/core/report.cpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,6 +18,8 @@
#include <utility>

// local includes
#include "shared/switch_pro_protocol.hpp"

#include <libvirtualhid/report.hpp>

namespace lvh::reports {
Expand DownExpand Up@@ -75,6 +77,30 @@ namespace lvh::reports {

constexpr std::size_t switch_rumble_output_report_size = 10;

constexpr std::uint8_t switch_set_player_lights_subcommand = 0x30;

constexpr std::uint8_t switch_set_home_light_subcommand = 0x38;

constexpr float switch_acceleration_scale = 4096.0F / 9.80665F;

constexpr float switch_gyroscope_scale = 14.2842F;

std::uint8_t decode_switch_home_light_intensity(std::byte encoded_intensity) {
const auto intensity = std::to_integer<std::uint8_t>(encoded_intensity >> 4U);
if (intensity == 0U) {
return 0U;
}
if (intensity <= 6U) {
return static_cast<std::uint8_t>(std::lround(static_cast<float>(intensity) * 25.5F));
}
if (intensity == 15U) {
return 255U;
}

const auto normalized = (static_cast<float>(intensity) - 0.5F) / 15.0F;
return static_cast<std::uint8_t>(std::lround(std::pow(normalized, 1.0F / 2.13F) * 255.0F));
}

// SDL maps 16-bit rumble strengths to Nintendo's shared 101-step amplitude
// scale. This is the inverse table for the packed high- and low-band values:
// https://github.com/libsdl-org/SDL/blob/main/src/joystick/hidapi/SDL_hidapi_switch.c
Expand DownExpand Up@@ -950,6 +976,47 @@ namespace lvh::reports {
}
}

void append_switch_pro_outputs(
const std::vector<std::uint8_t> &report,
std::vector<GamepadOutput> &outputs
) {
if (const auto rumble = decode_switch_rumble_report(report); rumble.has_value()) {
GamepadOutput output;
output.kind = GamepadOutputKind::rumble;
output.low_frequency_rumble = rumble->low_frequency;
output.high_frequency_rumble = rumble->high_frequency;
output.raw_report = report;
outputs.push_back(std::move(output));
}
if (
report.size() >= 12U && report[0] == switch_rumble_and_subcommand_output_report_id &&
report[10] == switch_set_player_lights_subcommand
) {
GamepadOutput output;
output.kind = GamepadOutputKind::player_leds;
const auto player_lights = std::byte {report[11]};
for (std::size_t index = 0; index < output.player_leds.size(); ++index) {
output.player_leds[index] = (player_lights & (std::byte {1} << index)) != zero_byte;
output.flashing_player_leds[index] =
(player_lights & (std::byte {1} << (index + 4U))) != zero_byte;
}
output.raw_report = report;
outputs.push_back(std::move(output));
}
if (
report.size() >= 15U && report[0] == switch_rumble_and_subcommand_output_report_id &&
report[10] == switch_set_home_light_subcommand
) {
GamepadOutput output;
output.kind = GamepadOutputKind::rgb_led;
output.red = decode_switch_home_light_intensity(std::byte {report[12]});
output.green = output.red;
output.blue = output.red;
output.raw_report = report;
outputs.push_back(std::move(output));
}
}

} // namespace

float clamp_axis(float value) {
Expand DownExpand Up@@ -1061,6 +1128,22 @@ namespace lvh::reports {
report[offset + 2U] = to_byte((y >> 4U) & 0xFFU);
}

void write_switch_imu_sample(
ByteReport &report,
std::size_t offset,
const Vector3 &acceleration,
const Vector3 &gyroscope
) {
// Nintendo's native coordinate system differs from the portable
// PlayStation-style coordinate system exposed by GamepadState.
write_i16(report, offset, scale_i16(-acceleration.z, switch_acceleration_scale));
write_i16(report, offset + 2U, scale_i16(-acceleration.x, switch_acceleration_scale));
write_i16(report, offset + 4U, scale_i16(acceleration.y, switch_acceleration_scale));
write_i16(report, offset + 6U, scale_i16(-gyroscope.z, switch_gyroscope_scale));
write_i16(report, offset + 8U, scale_i16(-gyroscope.x, switch_gyroscope_scale));
write_i16(report, offset + 10U, scale_i16(gyroscope.y, switch_gyroscope_scale));
}

std::byte switch_battery_and_connection(const std::optional<GamepadBattery> &battery) {
constexpr auto usb_connection = std::byte {0x01};
if (!battery.has_value()) {
Expand DownExpand Up@@ -1151,6 +1234,7 @@ namespace lvh::reports {

ByteReport report(profile.input_report_size, zero_byte);
report[0] = to_byte(profile.report_id);
report[1] = to_byte(detail::switch_pro_protocol::next_switch_pro_packet_timer());
report[2] = switch_battery_and_connection(normalized.battery);

if (normalized.buttons.test(x)) {
Expand DownExpand Up@@ -1222,6 +1306,11 @@ namespace lvh::reports {
normalize_switch_stick_axis(normalized.right_stick.x),
normalize_switch_stick_axis(normalized.right_stick.y)
);
const auto acceleration = normalized.acceleration.value_or(Vector3 {.y = 9.80665F});
const auto gyroscope = normalized.gyroscope.value_or(Vector3 {});
for (const auto offset : {13U, 25U, 37U}) {
write_switch_imu_sample(report, offset, acceleration, gyroscope);
}
return to_uint8_report(report);
}

Expand DownExpand Up@@ -1302,13 +1391,8 @@ namespace lvh::reports {
}

if (profile.gamepad_kind == GamepadProfileKind::switch_pro) {
if (const auto rumble = decode_switch_rumble_report(report); rumble.has_value()) {
GamepadOutput output;
output.kind = GamepadOutputKind::rumble;
output.low_frequency_rumble = rumble->low_frequency;
output.high_frequency_rumble = rumble->high_frequency;
output.raw_report = report;
outputs.push_back(std::move(output));
append_switch_pro_outputs(report, outputs);
if (!outputs.empty()) {
return outputs;
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/include/libvirtualhid/gamepad_adapter.hpp
Original file line numberDiff line numberDiff line change
Expand Up@@ -69,6 +69,11 @@ namespace lvh {
* @brief Number of rear paddle buttons exposed by the profile.
*/
std::uint8_t supported_rear_paddle_count = 0;

/**
* @brief Whether the profile supports player indicator LED output.
*/
bool supports_player_leds = false;
};

/**
Expand Down
Loading
Loading