Skip to content

Commit 85cc60d

Browse files
fix(gamepad)!: improve PlayStation controller compatibility (#94)
1 parent e343226 commit 85cc60d

22 files changed

Lines changed: 422 additions & 111 deletions

docs/platform-support.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ Switch Pro USB and subcommand initialization sequence and accepts the native
7777
`0x30` input layout, so descriptor-aware consumers can initialize those
7878
controllers before sending their native output reports.
7979

80+
Windows VHF devices do not expose a Bluetooth transport identity to HIDAPI.
81+
The Windows backend therefore reports DualShock 4 and DualSense requests as
82+
effective USB profiles through `Gamepad::profile()` and uses the matching USB
83+
descriptor, input reports, output reports, and feature-report framing. This
84+
keeps Steam and SDL's transport detection aligned with the reports accepted by
85+
the driver, including rumble and RGB LED output. The DualSense firmware feature
86+
report identifies the base controller's `0x0004` software series and current
87+
`0x0630` device software instead of reporting DualSense Edge series `0x0044`
88+
with the older `0x0154` revision. Linux keeps the Bluetooth defaults described
89+
below.
90+
8091
See [Windows driver package](windows-driver.md) for build, install, validation,
8192
and signing details.
8293

@@ -138,6 +149,20 @@ and control channels. Numbered control-channel output is normalized before
138149
parsing, whether the kernel includes the report number in the payload or
139150
provides it separately on the UHID event.
140151

152+
The default DualShock 4 and DualSense profiles use Bluetooth framing, avoiding
153+
the parent-USB checks that can make virtual USB devices appear late in Steam.
154+
Explicit USB and Bluetooth factories remain available for consumers that
155+
require a particular transport. DualShock 4 Bluetooth input reports set the
156+
HID-present header flag required by HIDAPI consumers and include the transport
157+
CRC, so a running consumer can accept live input after hotplug. DualSense motion
158+
packing preserves the public meters-per-second-squared and degrees-per-second
159+
units while applying the same
160+
raw sensor calibration used by Inputtino. Periodic PlayStation reports are
161+
repacked at 100 Hz so their sequence number and sensor timestamp continue to
162+
advance even when controller state is unchanged. Periodic and application
163+
submissions are serialized so a repeated report cannot restore stale motion
164+
state after a newer application report.
165+
141166
The backend opens `/dev/uhid` in nonblocking mode, matching the original
142167
asynchronous gamepad registration path. Its event reader is active before
143168
device registration begins, and creation does not report success until the
@@ -147,9 +172,8 @@ a controller before its kernel HID device has started.
147172

148173
On Linux, DualShock 4 and DualSense emit Sony's native `Wireless Controller`
149174
product name for Steam HID discovery. The requested USB or Bluetooth bus,
150-
descriptor, and report framing remain unchanged; in particular, the default
151-
DualShock 4 profile stays on its USB report contract. This transport-only name
152-
is confined to the Linux backend; public profile names, Windows names, and VHF
175+
descriptor, and report framing remain unchanged. This transport-only name is
176+
confined to the Linux backend; public profile names, Windows names, and VHF
153177
behavior are unchanged.
154178

155179
Switch Pro keeps its Nintendo identity on the Linux uinput path. This follows

docs/usage.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,10 @@ Consumers may replace `DeviceProfile::name` before creating a gamepad, for
217217
example, to prepend an application name while preserving the default controller
218218
identity across platform backends.
219219
220+
`profiles::dualshock4()` and `profiles::dualsense()` select Bluetooth framing
221+
for reliable native-controller discovery. Consumers can use the corresponding
222+
`_usb()` or `_bluetooth()` factory when the transport must be explicit.
223+
220224
The platform-neutral Generic HID descriptor reports the D-pad as buttons 13
221225
through 16 in the input report. Linux may still route that profile through
222226
`uinput`, where the backend exposes those same logical directions through the

src/core/profiles.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2095,7 +2095,7 @@ namespace lvh::profiles {
20952095
}
20962096

20972097
DeviceProfile dualshock4() {
2098-
return dualshock4_usb();
2098+
return dualshock4_bluetooth();
20992099
}
21002100

21012101
DeviceProfile dualshock4_usb() {
@@ -2107,7 +2107,7 @@ namespace lvh::profiles {
21072107
}
21082108

21092109
DeviceProfile dualsense() {
2110-
return dualsense_usb();
2110+
return dualsense_bluetooth();
21112111
}
21122112

21132113
DeviceProfile dualsense_usb() {

src/core/report.cpp

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,13 @@
66
// standard includes
77
#include <algorithm>
88
#include <array>
9+
#include <atomic>
910
#include <chrono>
1011
#include <cmath>
1112
#include <cstddef>
1213
#include <cstdint>
1314
#include <cstring>
15+
#include <numbers>
1416
#include <optional>
1517
#include <span>
1618
#include <utility>
@@ -31,6 +33,8 @@ namespace lvh::reports {
3133

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

36+
constexpr auto dualshock4_bt_input_hid_present = std::byte {0x80};
37+
3438
constexpr auto dualshock4_bt_output_report_id = std::byte {0x11};
3539

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

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

68+
constexpr auto dualsense_acceleration_scale = 9.80665F * 100.0F;
69+
70+
constexpr auto dualsense_gyroscope_scale = 1145.0F * std::numbers::pi_v<float> / 180.0F;
71+
6472
constexpr std::uint8_t switch_rumble_and_subcommand_output_report_id = 0x01;
6573

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

624+
std::uint8_t dualsense_sequence_number() {
625+
static std::atomic_uint32_t sequence_number = 0;
626+
return static_cast<std::uint8_t>((sequence_number.fetch_add(1U, std::memory_order_relaxed) + 1U) % 255U);
627+
}
628+
629+
std::uint32_t dualsense_sensor_timestamp() {
630+
const auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(
631+
std::chrono::steady_clock::now().time_since_epoch()
632+
)
633+
.count();
634+
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(elapsed) / 333U);
635+
}
636+
616637
std::vector<std::uint8_t> pack_dualshock4_input_report(const DeviceProfile &profile, const GamepadState &state) {
617638
const auto is_bluetooth = profile.bus_type == BusType::bluetooth;
618639
const auto payload_offset = is_bluetooth ? 3U : 1U;
@@ -627,6 +648,9 @@ namespace lvh::reports {
627648

628649
ByteReport report(profile.input_report_size, zero_byte);
629650
report[0] = is_bluetooth ? dualshock4_bt_input_report_id : to_byte(profile.report_id);
651+
if (is_bluetooth) {
652+
report[1] = dualshock4_bt_input_hid_present;
653+
}
630654

631655
report[payload_offset + 0U] = to_byte(normalize_u8_axis(normalized.left_stick.x));
632656
report[payload_offset + 1U] = to_byte(normalize_u8_axis(-normalized.left_stick.y));
@@ -722,6 +746,7 @@ namespace lvh::reports {
722746
report[payload_offset + 3U] = to_byte(normalize_u8_axis(-normalized.right_stick.y));
723747
report[payload_offset + 4U] = to_byte(normalize_trigger(normalized.left_trigger));
724748
report[payload_offset + 5U] = to_byte(normalize_trigger(normalized.right_trigger));
749+
report[payload_offset + 6U] = to_byte(dualsense_sequence_number());
725750
report[payload_offset + 7U] = to_byte(hat_from_buttons(normalized.buttons));
726751

727752
if (normalized.buttons.test(GamepadButton::x)) {
@@ -773,15 +798,16 @@ namespace lvh::reports {
773798
}
774799

775800
if (normalized.gyroscope) {
776-
write_i16(report, payload_offset + 15U, scale_i16(normalized.gyroscope->x, 1145.0F));
777-
write_i16(report, payload_offset + 17U, scale_i16(normalized.gyroscope->y, 1145.0F));
778-
write_i16(report, payload_offset + 19U, scale_i16(normalized.gyroscope->z, 1145.0F));
801+
write_i16(report, payload_offset + 15U, scale_i16(normalized.gyroscope->x, dualsense_gyroscope_scale));
802+
write_i16(report, payload_offset + 17U, scale_i16(normalized.gyroscope->y, dualsense_gyroscope_scale));
803+
write_i16(report, payload_offset + 19U, scale_i16(normalized.gyroscope->z, dualsense_gyroscope_scale));
779804
}
780805
if (normalized.acceleration) {
781-
write_i16(report, payload_offset + 21U, scale_i16(normalized.acceleration->x, 100.0F));
782-
write_i16(report, payload_offset + 23U, scale_i16(normalized.acceleration->y, 100.0F));
783-
write_i16(report, payload_offset + 25U, scale_i16(normalized.acceleration->z, 100.0F));
806+
write_i16(report, payload_offset + 21U, scale_i16(normalized.acceleration->x, dualsense_acceleration_scale));
807+
write_i16(report, payload_offset + 23U, scale_i16(normalized.acceleration->y, dualsense_acceleration_scale));
808+
write_i16(report, payload_offset + 25U, scale_i16(normalized.acceleration->z, dualsense_acceleration_scale));
784809
}
810+
write_u32(report, payload_offset + 27U, dualsense_sensor_timestamp());
785811

786812
write_dualsense_touch_contact(report, payload_offset + 32U, normalized.touchpad_contacts[0]);
787813
write_dualsense_touch_contact(report, payload_offset + 36U, normalized.touchpad_contacts[1]);

src/include/libvirtualhid/profiles.hpp

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,14 @@ namespace lvh::profiles {
4444
/**
4545
* @brief Create the PlayStation DualShock 4-compatible gamepad profile.
4646
*
47-
* @return Default DualShock 4-compatible device profile.
47+
* The default uses Bluetooth framing because Linux native-controller
48+
* consumers discover virtual DualShock 4 devices more reliably through that
49+
* transport. Backends may select a different effective transport when their
50+
* native virtual HID stack cannot expose Bluetooth identity; query
51+
* `Gamepad::profile()` after creation. Use `dualshock4_usb()` when USB
52+
* framing is explicitly required.
53+
*
54+
* @return Bluetooth DualShock 4-compatible device profile.
4855
*/
4956
DeviceProfile dualshock4();
5057

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

src/include/libvirtualhid/runtime.hpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,12 @@ namespace lvh {
5454
virtual DeviceId device_id() const = 0;
5555

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

src/platform/linux/uhid_backend.cpp

Lines changed: 27 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2815,8 +2815,8 @@ namespace lvh::detail {
28152815
std::memcpy(request.rd_data, options.profile.report_descriptor.data(), options.profile.report_descriptor.size());
28162816
profile_ = options.profile;
28172817
{
2818-
std::lock_guard lock {report_mutex_};
2819-
last_report_ = reports::pack_input_report(profile_, {});
2818+
std::lock_guard lock {state_mutex_};
2819+
last_state_ = {};
28202820
}
28212821

28222822
{
@@ -2848,25 +2848,13 @@ namespace lvh::detail {
28482848
}
28492849

28502850
OperationStatus submit(
2851-
const GamepadState & /*state*/,
2851+
const GamepadState &state,
28522852
const std::vector<std::uint8_t> &report
28532853
) override {
2854-
if (!open_) {
2855-
return OperationStatus::failure(ErrorCode::device_closed, "UHID gamepad is closed");
2856-
}
2857-
2858-
uhid_event event {};
2859-
if (report.size() > sizeof(event.u.input2.data)) {
2860-
return OperationStatus::failure(ErrorCode::invalid_argument, "HID input report is too large for UHID");
2861-
}
2862-
2863-
event.type = UHID_INPUT2;
2864-
event.u.input2.size = static_cast<std::uint16_t>(report.size());
2865-
std::memcpy(event.u.input2.data, report.data(), report.size());
2866-
auto status = write_event(event);
2854+
std::lock_guard lock {state_mutex_};
2855+
auto status = write_input_report(report);
28672856
if (status.ok()) {
2868-
std::lock_guard lock {report_mutex_};
2869-
last_report_ = report;
2857+
last_state_ = state;
28702858
}
28712859
return status;
28722860
}
@@ -2922,6 +2910,22 @@ namespace lvh::detail {
29222910
}
29232911

29242912
private:
2913+
OperationStatus write_input_report(const std::vector<std::uint8_t> &report) {
2914+
if (!open_) {
2915+
return OperationStatus::failure(ErrorCode::device_closed, "UHID gamepad is closed");
2916+
}
2917+
2918+
uhid_event event {};
2919+
if (report.size() > sizeof(event.u.input2.data)) {
2920+
return OperationStatus::failure(ErrorCode::invalid_argument, "HID input report is too large for UHID");
2921+
}
2922+
2923+
event.type = UHID_INPUT2;
2924+
event.u.input2.size = static_cast<std::uint16_t>(report.size());
2925+
std::memcpy(event.u.input2.data, report.data(), report.size());
2926+
return write_event(event);
2927+
}
2928+
29252929
OperationStatus write_event(const uhid_event &event) {
29262930
using enum ErrorCode;
29272931

@@ -3042,13 +3046,10 @@ namespace lvh::detail {
30423046
break;
30433047
}
30443048

3045-
std::vector<std::uint8_t> report;
3046-
{
3047-
std::lock_guard lock {report_mutex_};
3048-
report = last_report_;
3049-
}
3049+
std::lock_guard lock {state_mutex_};
3050+
const auto report = reports::pack_input_report(profile_, last_state_);
30503051
if (!report.empty()) {
3051-
static_cast<void>(submit({}, report));
3052+
static_cast<void>(write_input_report(report));
30523053
}
30533054
}
30543055
}
@@ -3173,7 +3174,7 @@ namespace lvh::detail {
31733174
std::string physical_id_;
31743175
std::string unique_id_;
31753176
std::array<std::uint8_t, 6> playstation_mac_address_ {};
3176-
std::vector<std::uint8_t> last_report_;
3177+
GamepadState last_state_;
31773178
std::atomic_bool open_ = true;
31783179
std::atomic_bool running_ = false;
31793180
std::jthread reader_;
@@ -3183,7 +3184,7 @@ namespace lvh::detail {
31833184
bool started_ = false;
31843185
bool reader_exited_ = false;
31853186
std::mutex write_mutex_;
3186-
std::mutex report_mutex_;
3187+
std::mutex state_mutex_;
31873188
std::mutex callback_mutex_;
31883189
OutputCallback output_callback_;
31893190
};

src/platform/windows/control_protocol.hpp

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,15 @@
1313
#include <iterator>
1414
#include <string>
1515
#include <string_view>
16+
#include <utility>
1617
#include <vector>
1718

1819
// driver includes
1920
#include "generic_pid_protocol.hpp"
2021
#include "lvh_windows_protocol.h"
2122

2223
// local includes
24+
#include <libvirtualhid/profiles.hpp>
2325
#include <libvirtualhid/types.hpp>
2426

2527
namespace lvh::detail::windows {
@@ -94,6 +96,32 @@ namespace lvh::detail::windows {
9496
return LVH_WINDOWS_GAMEPAD_GENERIC;
9597
}
9698

99+
inline DeviceProfile effective_vhf_gamepad_profile(const DeviceProfile &requested_profile) {
100+
if (requested_profile.bus_type != BusType::bluetooth) {
101+
return requested_profile;
102+
}
103+
104+
auto usb_profile = DeviceProfile {};
105+
switch (requested_profile.gamepad_kind) {
106+
case GamepadProfileKind::dualshock4:
107+
usb_profile = profiles::dualshock4_usb();
108+
break;
109+
case GamepadProfileKind::dualsense:
110+
usb_profile = profiles::dualsense_usb();
111+
break;
112+
default:
113+
return requested_profile;
114+
}
115+
116+
auto effective_profile = requested_profile;
117+
effective_profile.bus_type = usb_profile.bus_type;
118+
effective_profile.report_id = usb_profile.report_id;
119+
effective_profile.input_report_size = usb_profile.input_report_size;
120+
effective_profile.output_report_size = usb_profile.output_report_size;
121+
effective_profile.report_descriptor = std::move(usb_profile.report_descriptor);
122+
return effective_profile;
123+
}
124+
97125
template<std::size_t Size>
98126
std::uint32_t copy_string(std::array<char, Size> &target, std::string_view source) {
99127
std::ranges::fill(target, '\0');

src/platform/windows/windows_backend.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2013,7 +2013,13 @@ namespace lvh::detail {
20132013
};
20142014
}
20152015

2016-
return context_->create_gamepad(id, options);
2016+
auto effective_options = options;
2017+
effective_options.profile = windows::effective_vhf_gamepad_profile(options.profile);
2018+
auto result = context_->create_gamepad(id, effective_options);
2019+
if (result) {
2020+
result.effective_profile = std::move(effective_options.profile);
2021+
}
2022+
return result;
20172023
}
20182024

20192025
BackendKeyboardCreationResult create_keyboard(

0 commit comments

Comments
 (0)