Skip to content

Commit 91365cf

Browse files
fix: DualShock4 and DualSense improvements
1 parent d98966e commit 91365cf

11 files changed

Lines changed: 163 additions & 45 deletions

File tree

docs/platform-support.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,17 @@ and control channels. Numbered control-channel output is normalized before
138138
parsing, whether the kernel includes the report number in the payload or
139139
provides it separately on the UHID event.
140140

141+
The default DualShock 4 and DualSense profiles use Bluetooth framing, avoiding
142+
the parent-USB checks that can make virtual USB devices appear late in Steam.
143+
Explicit USB and Bluetooth factories remain available for consumers that
144+
require a particular transport. DualSense motion packing preserves the public
145+
meters-per-second-squared and degrees-per-second units while applying the same
146+
raw sensor calibration used by Inputtino. Periodic PlayStation reports are
147+
repacked at 100 Hz so their sequence number and sensor timestamp continue to
148+
advance even when controller state is unchanged. Periodic and application
149+
submissions are serialized so a repeated report cannot restore stale motion
150+
state after a newer application report.
151+
141152
The backend opens `/dev/uhid` in nonblocking mode, matching the original
142153
asynchronous gamepad registration path. Its event reader is active before
143154
device registration begins, and creation does not report success until the

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: 27 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>
@@ -61,6 +63,10 @@ namespace lvh::reports {
6163

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

66+
constexpr auto dualsense_acceleration_scale = 9.80665F * 100.0F;
67+
68+
constexpr auto dualsense_gyroscope_scale = 1145.0F * std::numbers::pi_v<float> / 180.0F;
69+
6470
constexpr std::uint8_t switch_rumble_and_subcommand_output_report_id = 0x01;
6571

6672
constexpr std::uint8_t switch_rumble_only_output_report_id = 0x10;
@@ -613,6 +619,19 @@ namespace lvh::reports {
613619
return static_cast<std::uint16_t>((static_cast<std::uint64_t>(elapsed) * 3U) / 16U);
614620
}
615621

622+
std::uint8_t dualsense_sequence_number() {
623+
static std::atomic_uint32_t sequence_number = 0;
624+
return static_cast<std::uint8_t>((sequence_number.fetch_add(1U, std::memory_order_relaxed) + 1U) % 255U);
625+
}
626+
627+
std::uint32_t dualsense_sensor_timestamp() {
628+
const auto elapsed = std::chrono::duration_cast<std::chrono::nanoseconds>(
629+
std::chrono::steady_clock::now().time_since_epoch()
630+
)
631+
.count();
632+
return static_cast<std::uint32_t>(static_cast<std::uint64_t>(elapsed) / 333U);
633+
}
634+
616635
std::vector<std::uint8_t> pack_dualshock4_input_report(const DeviceProfile &profile, const GamepadState &state) {
617636
const auto is_bluetooth = profile.bus_type == BusType::bluetooth;
618637
const auto payload_offset = is_bluetooth ? 3U : 1U;
@@ -722,6 +741,7 @@ namespace lvh::reports {
722741
report[payload_offset + 3U] = to_byte(normalize_u8_axis(-normalized.right_stick.y));
723742
report[payload_offset + 4U] = to_byte(normalize_trigger(normalized.left_trigger));
724743
report[payload_offset + 5U] = to_byte(normalize_trigger(normalized.right_trigger));
744+
report[payload_offset + 6U] = to_byte(dualsense_sequence_number());
725745
report[payload_offset + 7U] = to_byte(hat_from_buttons(normalized.buttons));
726746

727747
if (normalized.buttons.test(GamepadButton::x)) {
@@ -773,15 +793,16 @@ namespace lvh::reports {
773793
}
774794

775795
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));
796+
write_i16(report, payload_offset + 15U, scale_i16(normalized.gyroscope->x, dualsense_gyroscope_scale));
797+
write_i16(report, payload_offset + 17U, scale_i16(normalized.gyroscope->y, dualsense_gyroscope_scale));
798+
write_i16(report, payload_offset + 19U, scale_i16(normalized.gyroscope->z, dualsense_gyroscope_scale));
779799
}
780800
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));
801+
write_i16(report, payload_offset + 21U, scale_i16(normalized.acceleration->x, dualsense_acceleration_scale));
802+
write_i16(report, payload_offset + 23U, scale_i16(normalized.acceleration->y, dualsense_acceleration_scale));
803+
write_i16(report, payload_offset + 25U, scale_i16(normalized.acceleration->z, dualsense_acceleration_scale));
784804
}
805+
write_u32(report, payload_offset + 27U, dualsense_sensor_timestamp());
785806

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

src/include/libvirtualhid/profiles.hpp

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ 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. Use `dualshock4_usb()` when USB framing is explicitly required.
50+
*
51+
* @return Bluetooth DualShock 4-compatible device profile.
4852
*/
4953
DeviceProfile dualshock4();
5054

@@ -65,7 +69,11 @@ namespace lvh::profiles {
6569
/**
6670
* @brief Create the PlayStation DualSense-compatible gamepad profile.
6771
*
68-
* @return Default DualSense-compatible device profile.
72+
* The default uses Bluetooth framing because Linux native-controller
73+
* consumers discover virtual DualSense devices more reliably through that
74+
* transport. Use `dualsense_usb()` when USB framing is explicitly required.
75+
*
76+
* @return Bluetooth DualSense-compatible device profile.
6977
*/
7078
DeviceProfile dualsense();
7179

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
};

tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,9 +212,9 @@ namespace lvh::detail::test {
212212
bool saw_dualshock4_feature_crc = false;
213213

214214
/**
215-
* @brief Whether the peer observed a Bluetooth-framed DualSense input report.
215+
* @brief Whether periodic Bluetooth DualSense reports preserved motion and advanced sensor metadata.
216216
*/
217-
bool saw_dualsense_bluetooth_input = false;
217+
bool saw_dualsense_bluetooth_input_with_live_sensor_metadata = false;
218218

219219
/**
220220
* @brief Whether the peer observed a Bluetooth-framed DualShock 4 input report.

tests/fixtures/linux_backend_test_hooks.cpp

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,13 +1698,42 @@ namespace lvh::detail::test {
16981698
UhidGamepad gamepad {descriptors[0]};
16991699
auto event = create_started_profile_uhid_gamepad(gamepad, 9, options, descriptors[1], BUS_BLUETOOTH, result);
17001700

1701+
GamepadState motion_state;
1702+
motion_state.acceleration = Vector3 {.x = 1.0F, .y = 2.0F, .z = 3.0F};
1703+
motion_state.gyroscope = Vector3 {.x = 4.0F, .y = 5.0F, .z = 6.0F};
1704+
const auto submitted_report = reports::pack_input_report(options.profile, motion_state);
1705+
result.submit_status = gamepad.submit(motion_state, submitted_report);
1706+
1707+
bool first_input_report_valid = false;
1708+
std::uint8_t first_sequence = 0;
1709+
std::uint32_t first_sensor_timestamp = 0;
1710+
std::array<std::uint8_t, 12> first_sensor_values {};
17011711
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
17021712
const auto report_size = static_cast<std::size_t>(event.u.input2.size);
17031713
if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) {
17041714
const auto crc_offset = report_size - 4U;
17051715
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
17061716
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
1707-
result.saw_dualsense_bluetooth_input = expected_crc == actual_crc;
1717+
std::copy_n(event.u.input2.data + 17U, first_sensor_values.size(), first_sensor_values.begin());
1718+
first_input_report_valid = expected_crc == actual_crc && std::equal(
1719+
first_sensor_values.begin(),
1720+
first_sensor_values.end(),
1721+
submitted_report.begin() + 17
1722+
);
1723+
first_sequence = event.u.input2.data[8];
1724+
first_sensor_timestamp = read_u32_le(event.u.input2.data + 29U);
1725+
}
1726+
}
1727+
1728+
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
1729+
const auto report_size = static_cast<std::size_t>(event.u.input2.size);
1730+
if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) {
1731+
const auto crc_offset = report_size - 4U;
1732+
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
1733+
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
1734+
result.saw_dualsense_bluetooth_input_with_live_sensor_metadata =
1735+
first_input_report_valid && expected_crc == actual_crc && event.u.input2.data[8] != first_sequence &&
1736+
read_u32_le(event.u.input2.data + 29U) != first_sensor_timestamp && std::equal(first_sensor_values.begin(), first_sensor_values.end(), event.u.input2.data + 17U);
17081737
}
17091738
}
17101739

@@ -1732,7 +1761,6 @@ namespace lvh::detail::test {
17321761

17331762
result.close_status = gamepad.close();
17341763
static_cast<void>(::close(descriptors[1]));
1735-
result.submit_status = OperationStatus::success();
17361764
return result;
17371765
}
17381766

tests/unit/test_linux_backend.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -822,11 +822,12 @@ TEST_F(LinuxBackendTest, SocketpairBackedDualSenseRepliesToFeatureReports) {
822822
TEST_F(LinuxBackendTest, SocketpairBackedDualSenseBluetoothFramesReports) {
823823
const auto result = lvh::detail::test::linux_dualsense_bluetooth_uhid_socketpair_reports();
824824
EXPECT_TRUE(result.create_status.ok()) << result.create_status.message();
825+
EXPECT_TRUE(result.submit_status.ok()) << result.submit_status.message();
825826
EXPECT_TRUE(result.close_status.ok()) << result.close_status.message();
826827
EXPECT_TRUE(result.creation.saw_create);
827828
EXPECT_TRUE(result.creation.waited_for_start);
828829
EXPECT_EQ(result.creation.name, "Wireless Controller");
829-
EXPECT_TRUE(result.saw_dualsense_bluetooth_input);
830+
EXPECT_TRUE(result.saw_dualsense_bluetooth_input_with_live_sensor_metadata);
830831
EXPECT_TRUE(result.saw_dualsense_pairing);
831832
EXPECT_TRUE(result.saw_dualsense_feature_crc);
832833
}

tests/unit/test_profiles.cpp

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,8 +176,10 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) {
176176
EXPECT_EQ(dualshock4.vendor_id, 0x054C);
177177
EXPECT_EQ(dualshock4.product_id, 0x05C4);
178178
EXPECT_EQ(dualshock4.version, 0x0100);
179-
EXPECT_EQ(dualshock4.input_report_size, 64U);
180-
EXPECT_EQ(dualshock4.output_report_size, 32U);
179+
EXPECT_EQ(dualshock4.bus_type, lvh::BusType::bluetooth);
180+
EXPECT_EQ(dualshock4.report_id, 0x11);
181+
EXPECT_EQ(dualshock4.input_report_size, 78U);
182+
EXPECT_EQ(dualshock4.output_report_size, 78U);
181183
EXPECT_TRUE(dualshock4.capabilities.supports_motion);
182184
EXPECT_TRUE(dualshock4.capabilities.supports_touchpad);
183185
EXPECT_TRUE(dualshock4.capabilities.supports_rgb_led);
@@ -193,9 +195,20 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) {
193195
EXPECT_EQ(dualshock4_bluetooth.report_id, 0x11);
194196
EXPECT_EQ(dualshock4_bluetooth.input_report_size, 78U);
195197
EXPECT_EQ(dualshock4_bluetooth.output_report_size, 78U);
196-
EXPECT_NE(dualshock4_bluetooth.report_descriptor, dualshock4.report_descriptor);
198+
EXPECT_EQ(dualshock4_bluetooth.report_descriptor, dualshock4.report_descriptor);
199+
200+
const auto dualshock4_usb = lvh::profiles::dualshock4_usb();
201+
EXPECT_EQ(dualshock4_usb.bus_type, lvh::BusType::usb);
202+
EXPECT_EQ(dualshock4_usb.report_id, 0x01);
203+
EXPECT_EQ(dualshock4_usb.input_report_size, 64U);
204+
EXPECT_EQ(dualshock4_usb.output_report_size, 32U);
205+
EXPECT_NE(dualshock4_usb.report_descriptor, dualshock4.report_descriptor);
197206

198207
EXPECT_EQ(dualsense.vendor_id, 0x054C);
208+
EXPECT_EQ(dualsense.bus_type, lvh::BusType::bluetooth);
209+
EXPECT_EQ(dualsense.report_id, 0x31);
210+
EXPECT_EQ(dualsense.input_report_size, 78U);
211+
EXPECT_EQ(dualsense.output_report_size, 78U);
199212
EXPECT_TRUE(dualsense.capabilities.supports_motion);
200213
EXPECT_TRUE(dualsense.capabilities.supports_touchpad);
201214
EXPECT_TRUE(dualsense.capabilities.supports_rgb_led);
@@ -210,7 +223,14 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) {
210223
EXPECT_EQ(dualsense_bluetooth.report_id, 0x31);
211224
EXPECT_EQ(dualsense_bluetooth.input_report_size, 78U);
212225
EXPECT_EQ(dualsense_bluetooth.output_report_size, 78U);
213-
EXPECT_NE(dualsense_bluetooth.report_descriptor, dualsense.report_descriptor);
226+
EXPECT_EQ(dualsense_bluetooth.report_descriptor, dualsense.report_descriptor);
227+
228+
const auto dualsense_usb = lvh::profiles::dualsense_usb();
229+
EXPECT_EQ(dualsense_usb.bus_type, lvh::BusType::usb);
230+
EXPECT_EQ(dualsense_usb.report_id, 0x01);
231+
EXPECT_EQ(dualsense_usb.input_report_size, 64U);
232+
EXPECT_EQ(dualsense_usb.output_report_size, 48U);
233+
EXPECT_NE(dualsense_usb.report_descriptor, dualsense.report_descriptor);
214234

215235
EXPECT_EQ(switch_pro.vendor_id, 0x057E);
216236
EXPECT_EQ(switch_pro.product_id, 0x2009);

0 commit comments

Comments
 (0)