Skip to content

Commit 780c758

Browse files
fix: DualSense improvements
1 parent d98966e commit 780c758

11 files changed

Lines changed: 116 additions & 24 deletions

File tree

docs/platform-support.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ 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 DualSense profile uses Bluetooth framing, matching Inputtino's
142+
proven native-controller path and avoiding the parent-USB checks that can make
143+
virtual USB devices appear late in Steam. Explicit USB and Bluetooth factories
144+
remain available for consumers that require a particular transport. DualSense
145+
motion packing preserves the public meters-per-second-squared and
146+
degrees-per-second units while applying the same raw sensor calibration used by
147+
Inputtino. Periodic PlayStation reports are repacked at 100 Hz so their sequence
148+
number and sensor timestamp continue to advance even when controller state is
149+
unchanged.
150+
141151
The backend opens `/dev/uhid` in nonblocking mode, matching the original
142152
asynchronous gamepad registration path. Its event reader is active before
143153
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::dualsense()` selects Bluetooth framing for reliable native-controller
221+
discovery. Consumers can select `profiles::dualsense_usb()` or
222+
`profiles::dualsense_bluetooth()` 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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,11 @@ namespace lvh::profiles {
6565
/**
6666
* @brief Create the PlayStation DualSense-compatible gamepad profile.
6767
*
68-
* @return Default DualSense-compatible device profile.
68+
* The default uses Bluetooth framing because Linux native-controller
69+
* consumers discover virtual DualSense devices more reliably through that
70+
* transport. Use `dualsense_usb()` when USB framing is explicitly required.
71+
*
72+
* @return Bluetooth DualSense-compatible device profile.
6973
*/
7074
DeviceProfile dualsense();
7175

src/platform/linux/uhid_backend.cpp

Lines changed: 12 additions & 11 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,7 +2848,7 @@ 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 {
28542854
if (!open_) {
@@ -2865,8 +2865,8 @@ namespace lvh::detail {
28652865
std::memcpy(event.u.input2.data, report.data(), report.size());
28662866
auto status = write_event(event);
28672867
if (status.ok()) {
2868-
std::lock_guard lock {report_mutex_};
2869-
last_report_ = report;
2868+
std::lock_guard lock {state_mutex_};
2869+
last_state_ = state;
28702870
}
28712871
return status;
28722872
}
@@ -3042,13 +3042,14 @@ namespace lvh::detail {
30423042
break;
30433043
}
30443044

3045-
std::vector<std::uint8_t> report;
3045+
GamepadState state;
30463046
{
3047-
std::lock_guard lock {report_mutex_};
3048-
report = last_report_;
3047+
std::lock_guard lock {state_mutex_};
3048+
state = last_state_;
30493049
}
3050+
const auto report = reports::pack_input_report(profile_, state);
30503051
if (!report.empty()) {
3051-
static_cast<void>(submit({}, report));
3052+
static_cast<void>(submit(state, 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 valid periodic Bluetooth DualSense reports advanced their 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: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1698,13 +1698,30 @@ 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+
bool first_input_report_valid = false;
1702+
std::uint8_t first_sequence = 0;
1703+
std::uint32_t first_sensor_timestamp = 0;
17011704
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
17021705
const auto report_size = static_cast<std::size_t>(event.u.input2.size);
17031706
if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) {
17041707
const auto crc_offset = report_size - 4U;
17051708
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
17061709
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
1707-
result.saw_dualsense_bluetooth_input = expected_crc == actual_crc;
1710+
first_input_report_valid = expected_crc == actual_crc;
1711+
first_sequence = event.u.input2.data[8];
1712+
first_sensor_timestamp = read_u32_le(event.u.input2.data + 29U);
1713+
}
1714+
}
1715+
1716+
if (read_uhid_event_type(descriptors[1], UHID_INPUT2, event)) {
1717+
const auto report_size = static_cast<std::size_t>(event.u.input2.size);
1718+
if (report_size == options.profile.input_report_size && event.u.input2.data[0] == 0x31) {
1719+
const auto crc_offset = report_size - 4U;
1720+
const auto expected_crc = crc32(std::span<const std::uint8_t> {event.u.input2.data, crc_offset}, playstation_crc_seed(0xA1));
1721+
const auto actual_crc = read_u32_le(event.u.input2.data + crc_offset);
1722+
result.saw_dualsense_bluetooth_input_with_live_sensor_metadata =
1723+
first_input_report_valid && expected_crc == actual_crc && event.u.input2.data[8] != first_sequence &&
1724+
read_u32_le(event.u.input2.data + 29U) != first_sensor_timestamp;
17081725
}
17091726
}
17101727

tests/unit/test_linux_backend.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,7 @@ TEST_F(LinuxBackendTest, SocketpairBackedDualSenseBluetoothFramesReports) {
826826
EXPECT_TRUE(result.creation.saw_create);
827827
EXPECT_TRUE(result.creation.waited_for_start);
828828
EXPECT_EQ(result.creation.name, "Wireless Controller");
829-
EXPECT_TRUE(result.saw_dualsense_bluetooth_input);
829+
EXPECT_TRUE(result.saw_dualsense_bluetooth_input_with_live_sensor_metadata);
830830
EXPECT_TRUE(result.saw_dualsense_pairing);
831831
EXPECT_TRUE(result.saw_dualsense_feature_crc);
832832
}

tests/unit/test_profiles.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,10 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) {
196196
EXPECT_NE(dualshock4_bluetooth.report_descriptor, dualshock4.report_descriptor);
197197

198198
EXPECT_EQ(dualsense.vendor_id, 0x054C);
199+
EXPECT_EQ(dualsense.bus_type, lvh::BusType::bluetooth);
200+
EXPECT_EQ(dualsense.report_id, 0x31);
201+
EXPECT_EQ(dualsense.input_report_size, 78U);
202+
EXPECT_EQ(dualsense.output_report_size, 78U);
199203
EXPECT_TRUE(dualsense.capabilities.supports_motion);
200204
EXPECT_TRUE(dualsense.capabilities.supports_touchpad);
201205
EXPECT_TRUE(dualsense.capabilities.supports_rgb_led);
@@ -210,7 +214,14 @@ TEST(ProfileTest, StreamingControllerProfilesArePresent) {
210214
EXPECT_EQ(dualsense_bluetooth.report_id, 0x31);
211215
EXPECT_EQ(dualsense_bluetooth.input_report_size, 78U);
212216
EXPECT_EQ(dualsense_bluetooth.output_report_size, 78U);
213-
EXPECT_NE(dualsense_bluetooth.report_descriptor, dualsense.report_descriptor);
217+
EXPECT_EQ(dualsense_bluetooth.report_descriptor, dualsense.report_descriptor);
218+
219+
const auto dualsense_usb = lvh::profiles::dualsense_usb();
220+
EXPECT_EQ(dualsense_usb.bus_type, lvh::BusType::usb);
221+
EXPECT_EQ(dualsense_usb.report_id, 0x01);
222+
EXPECT_EQ(dualsense_usb.input_report_size, 64U);
223+
EXPECT_EQ(dualsense_usb.output_report_size, 48U);
224+
EXPECT_NE(dualsense_usb.report_descriptor, dualsense.report_descriptor);
214225

215226
EXPECT_EQ(switch_pro.vendor_id, 0x057E);
216227
EXPECT_EQ(switch_pro.product_id, 0x2009);

0 commit comments

Comments
 (0)