Skip to content

Commit 7902b76

Browse files
fix(linux): restore PlayStation gamepad hotplug in Steam (#85)
1 parent abfd0ee commit 7902b76

7 files changed

Lines changed: 335 additions & 110 deletions

File tree

.github/workflows/ci-build.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,8 @@ jobs:
181181
echo "::warning::${kernel_modules_package} is unavailable; relying on the runner image kernel modules."
182182
fi
183183
sudo tee /etc/udev/rules.d/99-libvirtualhid-ci.rules >/dev/null <<'EOF'
184-
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ENV{HID_PHYS}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
184+
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
185+
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", MODE="0666", TAG+="uaccess"
185186
SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess"
186187
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="(libvirtualhid)*", MODE="0666", TAG+="uaccess"
187188
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{name}=="libvirtualhid*", MODE="0666", TAG+="uaccess"

docs/platform-support.md

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,20 @@ 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 backend opens `/dev/uhid` in nonblocking mode, matching the original
142+
asynchronous gamepad registration path. Its event reader is active before
143+
device registration begins, and creation does not report success until the
144+
kernel returns `UHID_START`. This keeps control-channel initialization
145+
available throughout registration and prevents streaming hosts from publishing
146+
a controller before its kernel HID device has started.
147+
148+
On Linux, DualShock 4 and DualSense emit Sony's native `Wireless Controller`
149+
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
153+
behavior are unchanged.
154+
141155
Switch Pro keeps its Nintendo identity on the Linux uinput path. This follows
142156
the evdev layout used by Linux-native virtual-controller implementations and
143157
allows standard `FF_RUMBLE` effects without emulating the physical controller's
@@ -170,9 +184,19 @@ KERNEL=="uinput", SUBSYSTEM=="misc", OPTIONS+="static_node=uinput", GROUP="input
170184
KERNEL=="uhid", GROUP="input", MODE="0660", TAG+="uaccess"
171185
```
172186

173-
Consuming applications may also install name-matched rules for stable virtual
174-
device names when generated `hidraw` or `input` nodes must be accessible to the
175-
session user:
187+
UHID gamepads use a stable `libvirtualhid/uhid/*` physical path even when the
188+
library is compiled directly into a consuming application. Match that path for
189+
generated `hidraw` and input event nodes because native profiles such as
190+
DualShock 4 and DualSense intentionally do not retain the application's product
191+
name:
192+
193+
```udev
194+
KERNEL=="hidraw*", ATTRS{phys}=="libvirtualhid/uhid/*", GROUP="input", MODE="0660", TAG+="uaccess"
195+
SUBSYSTEM=="input", KERNEL=="event*", ATTRS{phys}=="libvirtualhid/uhid/*", GROUP="input", MODE="0660", TAG+="uaccess"
196+
```
197+
198+
Consuming applications may additionally install name-matched rules for stable
199+
virtual device names, including uinput-backed gamepads:
176200

177201
```udev
178202
KERNEL=="hidraw*", ATTRS{name}=="Your App Controller*", GROUP="input", MODE="0660", TAG+="uaccess"

src/platform/linux/uhid_backend.cpp

Lines changed: 101 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#include <cerrno>
1111
#include <chrono>
1212
#include <cmath>
13+
#include <condition_variable>
1314
#include <cstddef>
1415
#include <cstdint>
1516
#include <cstring>
@@ -99,6 +100,7 @@ namespace lvh::detail {
99100
#if defined(__linux__)
100101
namespace ps = playstation_feature_reports;
101102
constexpr auto playstation_periodic_report_ms = 10;
103+
constexpr auto uhid_start_timeout = std::chrono::seconds {5};
102104
#endif
103105

104106
int system_access(const char *path, int mode) {
@@ -303,6 +305,16 @@ namespace lvh::detail {
303305
}
304306
return to_uhid_bus(profile.bus_type);
305307
}
308+
309+
std::string_view uhid_gamepad_name(const DeviceProfile &profile) {
310+
// Steam's PlayStation HID path expects Sony's native product name. Keep
311+
// consumer branding out of the Linux transport identity while preserving
312+
// the requested descriptor, bus, and report framing.
313+
if (is_playstation_profile(profile.gamepad_kind)) {
314+
return "Wireless Controller";
315+
}
316+
return profile.name;
317+
}
306318
#endif
307319

308320
std::uint16_t to_uinput_bus(BusType bus_type) {
@@ -2787,7 +2799,8 @@ namespace lvh::detail {
27872799
}
27882800
physical_id_ = std::format("libvirtualhid/uhid/{}", id);
27892801

2790-
copy_string(request.name, options.profile.name);
2802+
device_name_ = uhid_gamepad_name(options.profile);
2803+
copy_string(request.name, device_name_);
27912804
copy_string(request.phys, physical_id_);
27922805
copy_string(request.uniq, unique_id_);
27932806
request.rd_size = static_cast<std::uint16_t>(options.profile.report_descriptor.size());
@@ -2797,20 +2810,31 @@ namespace lvh::detail {
27972810
request.version = options.profile.version;
27982811
std::memcpy(request.rd_data, options.profile.report_descriptor.data(), options.profile.report_descriptor.size());
27992812
profile_ = options.profile;
2800-
device_name_ = options.profile.name;
28012813
{
28022814
std::lock_guard lock {report_mutex_};
28032815
last_report_ = reports::pack_input_report(profile_, {});
28042816
}
28052817

2806-
if (const auto status = write_event(event); !status.ok()) {
2807-
return status;
2818+
{
2819+
std::lock_guard lock {lifecycle_mutex_};
2820+
started_ = false;
2821+
reader_exited_ = false;
28082822
}
2809-
28102823
running_ = true;
28112824
reader_ = std::jthread {[this](std::stop_token stop_token) {
28122825
read_loop(stop_token);
28132826
}};
2827+
2828+
if (const auto status = write_event(event); !status.ok()) {
2829+
stop_reader();
2830+
return status;
2831+
}
2832+
2833+
if (const auto status = wait_for_start(); !status.ok()) {
2834+
stop_reader();
2835+
return status;
2836+
}
2837+
28142838
if (is_playstation_profile(profile_.gamepad_kind)) {
28152839
periodic_reporter_ = std::jthread {[this](std::stop_token stop_token) {
28162840
periodic_report_loop(stop_token);
@@ -2914,47 +2938,65 @@ namespace lvh::detail {
29142938
return OperationStatus::success();
29152939
}
29162940

2917-
void read_loop(std::stop_token stop_token) {
2918-
while (!stop_token.stop_requested() && running_) {
2919-
pollfd descriptor {};
2920-
descriptor.fd = fd_;
2921-
descriptor.events = POLLIN;
2941+
enum class ReadEventResult {
2942+
event,
2943+
retry,
2944+
stop,
2945+
};
29222946

2923-
const auto result = system_poll(&descriptor, 1, poll_timeout_ms);
2924-
if (result < 0) {
2925-
if (errno == EINTR) {
2926-
continue;
2927-
}
2928-
return;
2929-
}
2930-
if (result == 0) {
2931-
continue;
2932-
}
2933-
if ((descriptor.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
2934-
return;
2935-
}
2936-
if ((descriptor.revents & POLLIN) == 0) {
2937-
continue;
2938-
}
2947+
ReadEventResult read_event(uhid_event &event) const {
2948+
pollfd descriptor {};
2949+
descriptor.fd = fd_;
2950+
descriptor.events = POLLIN;
29392951

2952+
const auto result = system_poll(&descriptor, 1, poll_timeout_ms);
2953+
if (result < 0) {
2954+
return errno == EINTR ? ReadEventResult::retry : ReadEventResult::stop;
2955+
}
2956+
if ((descriptor.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
2957+
return ReadEventResult::stop;
2958+
}
2959+
if (result == 0 || (descriptor.revents & POLLIN) == 0) {
2960+
return ReadEventResult::retry;
2961+
}
2962+
2963+
const auto result_read = system_read(fd_, std::as_writable_bytes(std::span {&event, 1U}));
2964+
if (result_read < 0) {
2965+
return errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR ?
2966+
ReadEventResult::retry :
2967+
ReadEventResult::stop;
2968+
}
2969+
return result_read == 0 ? ReadEventResult::stop : ReadEventResult::event;
2970+
}
2971+
2972+
void read_loop(std::stop_token stop_token) {
2973+
while (!stop_token.stop_requested() && running_) {
29402974
uhid_event event {};
2941-
const auto read_result = system_read(fd_, std::as_writable_bytes(std::span {&event, 1U}));
2942-
if (read_result < 0) {
2943-
if (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR) {
2944-
continue;
2945-
}
2946-
return;
2975+
const auto result = read_event(event);
2976+
if (result == ReadEventResult::stop) {
2977+
break;
29472978
}
2948-
if (read_result == 0) {
2949-
return;
2979+
if (result == ReadEventResult::event) {
2980+
handle_event(event);
29502981
}
2982+
}
29512983

2952-
handle_event(event);
2984+
{
2985+
std::lock_guard lock {lifecycle_mutex_};
2986+
reader_exited_ = true;
29532987
}
2988+
lifecycle_condition_.notify_all();
29542989
}
29552990

29562991
void handle_event(const uhid_event &event) {
29572992
switch (event.type) {
2993+
case UHID_START:
2994+
{
2995+
std::lock_guard lock {lifecycle_mutex_};
2996+
started_ = true;
2997+
}
2998+
lifecycle_condition_.notify_all();
2999+
break;
29583000
case UHID_OUTPUT:
29593001
dispatch_output_report(event.u.output.data, event.u.output.size);
29603002
break;
@@ -2970,6 +3012,25 @@ namespace lvh::detail {
29703012
}
29713013
}
29723014

3015+
void stop_reader() {
3016+
running_ = false;
3017+
if (reader_.joinable()) {
3018+
reader_.request_stop();
3019+
reader_.join();
3020+
}
3021+
}
3022+
3023+
OperationStatus wait_for_start() {
3024+
if (std::unique_lock lock {lifecycle_mutex_}; !lifecycle_condition_.wait_for(lock, uhid_start_timeout, [this]() {
3025+
return started_ || reader_exited_;
3026+
})) {
3027+
return OperationStatus::failure(ErrorCode::backend_failure, "timed out waiting for UHID_START");
3028+
} else if (!started_) {
3029+
return OperationStatus::failure(ErrorCode::backend_failure, "UHID reader stopped before UHID_START");
3030+
}
3031+
return OperationStatus::success();
3032+
}
3033+
29733034
void periodic_report_loop(std::stop_token stop_token) {
29743035
while (!stop_token.stop_requested() && running_) {
29753036
std::this_thread::sleep_for(std::chrono::milliseconds {playstation_periodic_report_ms});
@@ -3113,6 +3174,10 @@ namespace lvh::detail {
31133174
std::atomic_bool running_ = false;
31143175
std::jthread reader_;
31153176
std::jthread periodic_reporter_;
3177+
std::mutex lifecycle_mutex_;
3178+
std::condition_variable lifecycle_condition_;
3179+
bool started_ = false;
3180+
bool reader_exited_ = false;
31163181
std::mutex write_mutex_;
31173182
std::mutex report_mutex_;
31183183
std::mutex callback_mutex_;
@@ -3185,7 +3250,7 @@ namespace lvh::detail {
31853250
}
31863251

31873252
#if defined(__linux__)
3188-
const auto fd = system_open(uhid_path, O_RDWR | O_CLOEXEC);
3253+
const auto fd = system_open(uhid_path, O_RDWR | O_CLOEXEC | O_NONBLOCK);
31893254
if (fd < 0) {
31903255
return {system_error_status(ErrorCode::backend_unavailable, "failed to open /dev/uhid", errno), nullptr};
31913256
}

tests/fixtures/include/fixtures/linux_backend_test_hooks.hpp

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,41 @@ namespace lvh::detail::test {
102102
std::uint64_t remaining = 0;
103103
};
104104

105+
/**
106+
* @brief UHID device-creation observations captured by a socketpair peer.
107+
*/
108+
struct LinuxUhidCreationObservation {
109+
/**
110+
* @brief Whether the peer observed a create event.
111+
*/
112+
bool saw_create = false;
113+
114+
/**
115+
* @brief Whether create remained pending until the peer sent UHID_START.
116+
*/
117+
bool waited_for_start = false;
118+
119+
/**
120+
* @brief Product name carried by the observed create event.
121+
*/
122+
std::string name;
123+
};
124+
125+
/**
126+
* @brief UHID output callback observations captured during a round trip.
127+
*/
128+
struct LinuxUhidOutputObservation {
129+
/**
130+
* @brief Number of output callbacks received.
131+
*/
132+
std::size_t callback_count = 0;
133+
134+
/**
135+
* @brief Last output callback payload.
136+
*/
137+
GamepadOutput last;
138+
};
139+
105140
/**
106141
* @brief Result from a socketpair-backed UHID lifecycle test.
107142
*/
@@ -122,9 +157,9 @@ namespace lvh::detail::test {
122157
OperationStatus close_status;
123158

124159
/**
125-
* @brief Whether the peer observed a create event.
160+
* @brief Device-creation observations.
126161
*/
127-
bool saw_create = false;
162+
LinuxUhidCreationObservation creation;
128163

129164
/**
130165
* @brief Whether the peer observed an input report event.
@@ -186,6 +221,11 @@ namespace lvh::detail::test {
186221
*/
187222
bool saw_dualshock4_bluetooth_input = false;
188223

224+
/**
225+
* @brief Whether the peer observed a USB-framed DualShock 4 input report.
226+
*/
227+
bool saw_dualshock4_usb_input = false;
228+
189229
/**
190230
* @brief Whether the peer observed a set-report reply.
191231
*/
@@ -197,14 +237,9 @@ namespace lvh::detail::test {
197237
bool saw_destroy = false;
198238

199239
/**
200-
* @brief Number of output callbacks received.
201-
*/
202-
std::size_t output_callback_count = 0;
203-
204-
/**
205-
* @brief Last output callback payload.
240+
* @brief Output callback observations.
206241
*/
207-
GamepadOutput last_output;
242+
LinuxUhidOutputObservation output;
208243
};
209244

210245
/**
@@ -830,6 +865,13 @@ namespace lvh::detail::test {
830865
*/
831866
OperationStatus linux_backend_gamepad_fake_open_failure();
832867

868+
/**
869+
* @brief Capture the flags used to open UHID for a descriptor-driven gamepad.
870+
*
871+
* @return Flags passed to `open()` for `/dev/uhid`.
872+
*/
873+
int linux_backend_gamepad_open_flags();
874+
833875
/**
834876
* @brief Try creating a Linux backend gamepad while fake UHID creation fails.
835877
*

0 commit comments

Comments
 (0)