Skip to content
Merged
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
30 changes: 27 additions & 3 deletions docs/platform-support.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ 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.

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.

See [Windows driver package](windows-driver.md) for build, install, validation,
and signing details.

Expand Down Expand Up @@ -138,6 +149,20 @@ 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
Expand All @@ -147,9 +172,8 @@ 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; in particular, the default
DualShock 4 profile stays on its USB report contract. This transport-only name
is confined to the Linux backend; public profile names, Windows names, and VHF
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
Expand Down
4 changes: 4 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,10 @@ 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.

`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.

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
Expand Down
4 changes: 2 additions & 2 deletions src/core/profiles.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2095,7 +2095,7 @@ namespace lvh::profiles {
}

DeviceProfile dualshock4() {
return dualshock4_usb();
return dualshock4_bluetooth();
}

DeviceProfile dualshock4_usb() {
Expand All @@ -2107,7 +2107,7 @@ namespace lvh::profiles {
}

DeviceProfile dualsense() {
return dualsense_usb();
return dualsense_bluetooth();
}

DeviceProfile dualsense_usb() {
Expand Down
38 changes: 32 additions & 6 deletions src/core/report.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
// standard includes
#include <algorithm>
#include <array>
#include <atomic>
#include <chrono>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <numbers>
#include <optional>
#include <span>
#include <utility>
Expand All @@ -31,6 +33,8 @@ namespace lvh::reports {

constexpr auto dualshock4_bt_input_report_id = std::byte {0x11};

constexpr auto dualshock4_bt_input_hid_present = std::byte {0x80};

constexpr auto dualshock4_bt_output_report_id = std::byte {0x11};

constexpr auto dualshock4_output_hwctl_crc32 = std::byte {0x40};
Expand Down Expand Up @@ -61,6 +65,10 @@ namespace lvh::reports {

constexpr auto dualsense_flag2_compatible_vibration = std::byte {0x04};

constexpr auto dualsense_acceleration_scale = 9.80665F * 100.0F;

constexpr auto dualsense_gyroscope_scale = 1145.0F * std::numbers::pi_v<float> / 180.0F;

constexpr std::uint8_t switch_rumble_and_subcommand_output_report_id = 0x01;

constexpr std::uint8_t switch_rumble_only_output_report_id = 0x10;
Expand Down Expand Up @@ -613,6 +621,19 @@ namespace lvh::reports {
return static_cast<std::uint16_t>((static_cast<std::uint64_t>(elapsed) * 3U) / 16U);
}

std::uint8_t dualsense_sequence_number() {
static std::atomic_uint32_t sequence_number = 0;
return static_cast<std::uint8_t>((sequence_number.fetch_add(1U, std::memory_order_relaxed) + 1U) % 255U);
}

std::uint32_t dualsense_sensor_timestamp() {
const auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch()
)
.count();
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(elapsed) / 333U);
}

std::vector<std::uint8_t> pack_dualshock4_input_report(const DeviceProfile &profile, const GamepadState &state) {
const auto is_bluetooth = profile.bus_type == BusType::bluetooth;
const auto payload_offset = is_bluetooth ? 3U : 1U;
Expand All @@ -627,6 +648,9 @@ namespace lvh::reports {

ByteReport report(profile.input_report_size, zero_byte);
report[0] = is_bluetooth ? dualshock4_bt_input_report_id : to_byte(profile.report_id);
if (is_bluetooth) {
report[1] = dualshock4_bt_input_hid_present;
}

report[payload_offset + 0U] = to_byte(normalize_u8_axis(normalized.left_stick.x));
report[payload_offset + 1U] = to_byte(normalize_u8_axis(-normalized.left_stick.y));
Expand Down Expand Up @@ -722,6 +746,7 @@ namespace lvh::reports {
report[payload_offset + 3U] = to_byte(normalize_u8_axis(-normalized.right_stick.y));
report[payload_offset + 4U] = to_byte(normalize_trigger(normalized.left_trigger));
report[payload_offset + 5U] = to_byte(normalize_trigger(normalized.right_trigger));
report[payload_offset + 6U] = to_byte(dualsense_sequence_number());
report[payload_offset + 7U] = to_byte(hat_from_buttons(normalized.buttons));

if (normalized.buttons.test(GamepadButton::x)) {
Expand Down Expand Up @@ -773,15 +798,16 @@ namespace lvh::reports {
}

if (normalized.gyroscope) {
write_i16(report, payload_offset + 15U, scale_i16(normalized.gyroscope->x, 1145.0F));
write_i16(report, payload_offset + 17U, scale_i16(normalized.gyroscope->y, 1145.0F));
write_i16(report, payload_offset + 19U, scale_i16(normalized.gyroscope->z, 1145.0F));
write_i16(report, payload_offset + 15U, scale_i16(normalized.gyroscope->x, dualsense_gyroscope_scale));
write_i16(report, payload_offset + 17U, scale_i16(normalized.gyroscope->y, dualsense_gyroscope_scale));
write_i16(report, payload_offset + 19U, scale_i16(normalized.gyroscope->z, dualsense_gyroscope_scale));
}
if (normalized.acceleration) {
write_i16(report, payload_offset + 21U, scale_i16(normalized.acceleration->x, 100.0F));
write_i16(report, payload_offset + 23U, scale_i16(normalized.acceleration->y, 100.0F));
write_i16(report, payload_offset + 25U, scale_i16(normalized.acceleration->z, 100.0F));
write_i16(report, payload_offset + 21U, scale_i16(normalized.acceleration->x, dualsense_acceleration_scale));
write_i16(report, payload_offset + 23U, scale_i16(normalized.acceleration->y, dualsense_acceleration_scale));
write_i16(report, payload_offset + 25U, scale_i16(normalized.acceleration->z, dualsense_acceleration_scale));
}
write_u32(report, payload_offset + 27U, dualsense_sensor_timestamp());

write_dualsense_touch_contact(report, payload_offset + 32U, normalized.touchpad_contacts[0]);
write_dualsense_touch_contact(report, payload_offset + 36U, normalized.touchpad_contacts[1]);
Expand Down
18 changes: 16 additions & 2 deletions src/include/libvirtualhid/profiles.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,14 @@ namespace lvh::profiles {
/**
* @brief Create the PlayStation DualShock 4-compatible gamepad profile.
*
* @return Default DualShock 4-compatible device profile.
* The default uses Bluetooth framing because Linux native-controller
* consumers discover virtual DualShock 4 devices more reliably through that
* transport. Backends may select a different effective transport when their
* native virtual HID stack cannot expose Bluetooth identity; query
* `Gamepad::profile()` after creation. Use `dualshock4_usb()` when USB
* framing is explicitly required.
*
* @return Bluetooth DualShock 4-compatible device profile.
*/
DeviceProfile dualshock4();

Expand All @@ -65,7 +72,14 @@ namespace lvh::profiles {
/**
* @brief Create the PlayStation DualSense-compatible gamepad profile.
*
* @return Default DualSense-compatible device profile.
* The default uses Bluetooth framing because Linux native-controller
* consumers discover virtual DualSense devices more reliably through that
* transport. Backends may select a different effective transport when their
* native virtual HID stack cannot expose Bluetooth identity; query
* `Gamepad::profile()` after creation. Use `dualsense_usb()` when USB framing
* is explicitly required.
*
* @return Bluetooth DualSense-compatible device profile.
*/
DeviceProfile dualsense();

Expand Down
7 changes: 5 additions & 2 deletions src/include/libvirtualhid/runtime.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,12 @@ namespace lvh {
virtual DeviceId device_id() const = 0;

/**
* @brief Get the profile used to create this device.
* @brief Get the effective profile exposed by the backend for this device.
*
* @return Device profile.
* A backend may adjust transport-specific profile fields when its native
* device stack cannot represent the requested transport directly.
*
* @return Effective device profile.
*/
virtual const DeviceProfile &profile() const = 0;

Expand Down
53 changes: 27 additions & 26 deletions src/platform/linux/uhid_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2815,8 +2815,8 @@ namespace lvh::detail {
std::memcpy(request.rd_data, options.profile.report_descriptor.data(), options.profile.report_descriptor.size());
profile_ = options.profile;
{
std::lock_guard lock {report_mutex_};
last_report_ = reports::pack_input_report(profile_, {});
std::lock_guard lock {state_mutex_};
last_state_ = {};
}

{
Expand Down Expand Up @@ -2848,25 +2848,13 @@ namespace lvh::detail {
}

OperationStatus submit(
const GamepadState & /*state*/,
const GamepadState &state,
const std::vector<std::uint8_t> &report
) override {
if (!open_) {
return OperationStatus::failure(ErrorCode::device_closed, "UHID gamepad is closed");
}

uhid_event event {};
if (report.size() > sizeof(event.u.input2.data)) {
return OperationStatus::failure(ErrorCode::invalid_argument, "HID input report is too large for UHID");
}

event.type = UHID_INPUT2;
event.u.input2.size = static_cast<std::uint16_t>(report.size());
std::memcpy(event.u.input2.data, report.data(), report.size());
auto status = write_event(event);
std::lock_guard lock {state_mutex_};
auto status = write_input_report(report);
if (status.ok()) {
std::lock_guard lock {report_mutex_};
last_report_ = report;
last_state_ = state;
}
return status;
}
Expand Down Expand Up @@ -2922,6 +2910,22 @@ namespace lvh::detail {
}

private:
OperationStatus write_input_report(const std::vector<std::uint8_t> &report) {
if (!open_) {
return OperationStatus::failure(ErrorCode::device_closed, "UHID gamepad is closed");
}

uhid_event event {};
if (report.size() > sizeof(event.u.input2.data)) {
return OperationStatus::failure(ErrorCode::invalid_argument, "HID input report is too large for UHID");
}

event.type = UHID_INPUT2;
event.u.input2.size = static_cast<std::uint16_t>(report.size());
std::memcpy(event.u.input2.data, report.data(), report.size());
return write_event(event);
}

OperationStatus write_event(const uhid_event &event) {
using enum ErrorCode;

Expand Down Expand Up @@ -3042,13 +3046,10 @@ namespace lvh::detail {
break;
}

std::vector<std::uint8_t> report;
{
std::lock_guard lock {report_mutex_};
report = last_report_;
}
std::lock_guard lock {state_mutex_};
const auto report = reports::pack_input_report(profile_, last_state_);
if (!report.empty()) {
static_cast<void>(submit({}, report));
static_cast<void>(write_input_report(report));
}
}
}
Expand Down Expand Up @@ -3173,7 +3174,7 @@ namespace lvh::detail {
std::string physical_id_;
std::string unique_id_;
std::array<std::uint8_t, 6> playstation_mac_address_ {};
std::vector<std::uint8_t> last_report_;
GamepadState last_state_;
std::atomic_bool open_ = true;
std::atomic_bool running_ = false;
std::jthread reader_;
Expand All @@ -3183,7 +3184,7 @@ namespace lvh::detail {
bool started_ = false;
bool reader_exited_ = false;
std::mutex write_mutex_;
std::mutex report_mutex_;
std::mutex state_mutex_;
std::mutex callback_mutex_;
OutputCallback output_callback_;
};
Expand Down
28 changes: 28 additions & 0 deletions src/platform/windows/control_protocol.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
#include <iterator>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

// driver includes
#include "generic_pid_protocol.hpp"
#include "lvh_windows_protocol.h"

// local includes
#include <libvirtualhid/profiles.hpp>
#include <libvirtualhid/types.hpp>

namespace lvh::detail::windows {
Expand Down Expand Up @@ -94,6 +96,32 @@ namespace lvh::detail::windows {
return LVH_WINDOWS_GAMEPAD_GENERIC;
}

inline DeviceProfile effective_vhf_gamepad_profile(const DeviceProfile &requested_profile) {
if (requested_profile.bus_type != BusType::bluetooth) {
return requested_profile;
}

auto usb_profile = DeviceProfile {};
switch (requested_profile.gamepad_kind) {
case GamepadProfileKind::dualshock4:
usb_profile = profiles::dualshock4_usb();
break;
case GamepadProfileKind::dualsense:
usb_profile = profiles::dualsense_usb();
break;
default:
return requested_profile;
}

auto effective_profile = requested_profile;
effective_profile.bus_type = usb_profile.bus_type;
effective_profile.report_id = usb_profile.report_id;
effective_profile.input_report_size = usb_profile.input_report_size;
effective_profile.output_report_size = usb_profile.output_report_size;
effective_profile.report_descriptor = std::move(usb_profile.report_descriptor);
return effective_profile;
}

template<std::size_t Size>
std::uint32_t copy_string(std::array<char, Size> &target, std::string_view source) {
std::ranges::fill(target, '\0');
Expand Down
8 changes: 7 additions & 1 deletion src/platform/windows/windows_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2013,7 +2013,13 @@ namespace lvh::detail {
};
}

return context_->create_gamepad(id, options);
auto effective_options = options;
effective_options.profile = windows::effective_vhf_gamepad_profile(options.profile);
auto result = context_->create_gamepad(id, effective_options);
if (result) {
result.effective_profile = std::move(effective_options.profile);
}
return result;
}

BackendKeyboardCreationResult create_keyboard(
Expand Down
Loading
Loading