@@ -51,8 +51,21 @@ namespace {
5151#if defined(__linux__)
5252 using LibinputContext = std::unique_ptr<libinput, void (*)(libinput *)>;
5353 using LibinputEvent = std::unique_ptr<libinput_event, void (*)(libinput_event *)>;
54+ using SdlGameController = std::unique_ptr<SDL_GameController, void (*)(SDL_GameController *)>;
5455 using SdlJoystick = std::unique_ptr<SDL_Joystick, void (*)(SDL_Joystick *)>;
5556
57+ /* *
58+ * @brief SDL-visible gamepad case.
59+ */
60+ struct SdlGamepadConsumerCase {
61+ lvh::DeviceProfile profile;
62+ std::string_view name_suffix;
63+ std::string_view stable_id;
64+ int minimum_buttons = 1 ;
65+ int minimum_axes = 2 ;
66+ bool expect_live_input = true ;
67+ };
68+
5669 /* *
5770 * @brief Execute cleanup code when a scope exits.
5871 */
@@ -204,22 +217,97 @@ namespace {
204217 return stream.str ();
205218 }
206219
220+ bool sdl_joystick_has_pressed_button (SDL_Joystick *joystick) {
221+ for (int button = 0 ; button < SDL_JoystickNumButtons (joystick); ++button) {
222+ if (SDL_JoystickGetButton (joystick, button) != 0 ) {
223+ return true ;
224+ }
225+ }
226+
227+ return false ;
228+ }
229+
230+ bool sdl_joystick_has_moved_axis (SDL_Joystick *joystick) {
231+ for (int axis = 0 ; axis < SDL_JoystickNumAxes (joystick); ++axis) {
232+ if (std::abs (static_cast <int >(SDL_JoystickGetAxis (joystick, axis))) > 8000 ) {
233+ return true ;
234+ }
235+ }
236+
237+ return false ;
238+ }
239+
207240 bool wait_for_sdl_gamepad_input (SDL_Joystick *joystick) {
208241 const auto deadline = std::chrono::steady_clock::now () + std::chrono::seconds {3 };
209242
210243 while (std::chrono::steady_clock::now () < deadline) {
211244 pump_sdl_events ();
212245
213- const auto button_pressed = SDL_JoystickNumButtons (joystick) > 0 && SDL_JoystickGetButton (joystick, 0 ) != 0 ;
214- bool axis_moved = false ;
215- for (int axis = 0 ; axis < SDL_JoystickNumAxes (joystick); ++axis) {
216- if (std::abs (static_cast <int >(SDL_JoystickGetAxis (joystick, axis))) > 8000 ) {
217- axis_moved = true ;
218- break ;
219- }
246+ if (sdl_joystick_has_pressed_button (joystick) && sdl_joystick_has_moved_axis (joystick)) {
247+ return true ;
220248 }
221249
222- if (button_pressed && axis_moved) {
250+ std::this_thread::sleep_for (std::chrono::milliseconds {50 });
251+ }
252+
253+ return false ;
254+ }
255+
256+ bool sdl_controller_has_pressed_button (SDL_GameController *controller) {
257+ for (int button = SDL_CONTROLLER_BUTTON_A ; button < SDL_CONTROLLER_BUTTON_MAX ; ++button) {
258+ if (SDL_GameControllerGetButton (controller, static_cast <SDL_GameControllerButton>(button)) != 0 ) {
259+ return true ;
260+ }
261+ }
262+
263+ return false ;
264+ }
265+
266+ bool sdl_controller_has_moved_axis (SDL_GameController *controller) {
267+ for (int axis = SDL_CONTROLLER_AXIS_LEFTX ; axis < SDL_CONTROLLER_AXIS_MAX ; ++axis) {
268+ if (std::abs (static_cast <int >(SDL_GameControllerGetAxis (controller, static_cast <SDL_GameControllerAxis>(axis)))) > 8000 ) {
269+ return true ;
270+ }
271+ }
272+
273+ return false ;
274+ }
275+
276+ std::string describe_sdl_controller_state (SDL_GameController *controller) {
277+ std::ostringstream stream;
278+ stream << " controller_type=" << SDL_GameControllerGetType (controller);
279+
280+ for (int button = SDL_CONTROLLER_BUTTON_A ; button < SDL_CONTROLLER_BUTTON_MAX ; ++button) {
281+ stream << " controller_button[" << button << " ]="
282+ << static_cast <int >(SDL_GameControllerGetButton (controller, static_cast <SDL_GameControllerButton>(button)));
283+ }
284+
285+ for (int axis = SDL_CONTROLLER_AXIS_LEFTX ; axis < SDL_CONTROLLER_AXIS_MAX ; ++axis) {
286+ stream << " controller_axis[" << axis << " ]="
287+ << SDL_GameControllerGetAxis (controller, static_cast <SDL_GameControllerAxis>(axis));
288+ }
289+
290+ if (auto *joystick = SDL_GameControllerGetJoystick (controller)) {
291+ stream << " " << describe_sdl_state (joystick);
292+ }
293+
294+ return stream.str ();
295+ }
296+
297+ bool wait_for_sdl_controller_input (SDL_GameController *controller) {
298+ const auto deadline = std::chrono::steady_clock::now () + std::chrono::seconds {3 };
299+ auto *joystick = SDL_GameControllerGetJoystick (controller);
300+
301+ while (std::chrono::steady_clock::now () < deadline) {
302+ SDL_GameControllerUpdate ();
303+ pump_sdl_events ();
304+
305+ const auto controller_button_pressed = sdl_controller_has_pressed_button (controller);
306+ const auto controller_axis_moved = sdl_controller_has_moved_axis (controller);
307+ const auto joystick_button_pressed = joystick != nullptr && sdl_joystick_has_pressed_button (joystick);
308+ const auto joystick_axis_moved = joystick != nullptr && sdl_joystick_has_moved_axis (joystick);
309+
310+ if ((controller_button_pressed || joystick_button_pressed) && (controller_axis_moved || joystick_axis_moved)) {
223311 return true ;
224312 }
225313
@@ -229,6 +317,135 @@ namespace {
229317 return false ;
230318 }
231319
320+ void configure_sdl_hidapi_hints () {
321+ SDL_SetHint (SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS , " 1" );
322+ SDL_SetHint (" SDL_JOYSTICK_HIDAPI" , " 1" );
323+ SDL_SetHint (" SDL_JOYSTICK_HIDAPI_PS5" , " 1" );
324+ }
325+
326+ lvh::GamepadCreationResult create_sdl_gamepad (lvh::Runtime &runtime, SdlGamepadConsumerCase test_case) {
327+ lvh::CreateGamepadOptions options;
328+ options.profile = std::move (test_case.profile );
329+ options.profile .name = unique_device_name (test_case.name_suffix );
330+ options.metadata .stable_id = std::string {test_case.stable_id };
331+
332+ return runtime.create_gamepad (options);
333+ }
334+
335+ void expect_sdl_joystick_profile (SDL_Joystick *joystick, const lvh::DeviceProfile &profile, int minimum_buttons, int minimum_axes) {
336+ EXPECT_EQ (SDL_JoystickGetVendor (joystick), profile.vendor_id );
337+ EXPECT_EQ (SDL_JoystickGetProduct (joystick), profile.product_id );
338+ EXPECT_GE (SDL_JoystickNumButtons (joystick), minimum_buttons);
339+ EXPECT_GE (SDL_JoystickNumAxes (joystick), minimum_axes);
340+ }
341+
342+ void expect_sdl_dualsense_controller_profile (SDL_GameController *controller) {
343+ auto *mapping = SDL_GameControllerMapping (controller);
344+ EXPECT_NE (mapping, nullptr ) << SDL_GetError ();
345+ if (mapping != nullptr ) {
346+ SDL_free (mapping);
347+ }
348+ }
349+
350+ void run_sdl_uhid_joystick_test (SdlGamepadConsumerCase test_case) {
351+ configure_sdl_hidapi_hints ();
352+ ASSERT_EQ (SDL_Init (SDL_INIT_JOYSTICK | SDL_INIT_EVENTS ), 0 ) << SDL_GetError ();
353+ ScopeExit sdl_quit {[]() {
354+ SDL_Quit ();
355+ }};
356+
357+ lvh::RuntimeOptions runtime_options;
358+ runtime_options.backend = lvh::BackendKind::platform_default;
359+ auto runtime = lvh::Runtime::create (runtime_options);
360+ ASSERT_TRUE (runtime->capabilities ().supports_gamepad );
361+
362+ const auto expected_profile = [&test_case]() {
363+ auto profile = test_case.profile ;
364+ profile.name = unique_device_name (test_case.name_suffix );
365+ return profile;
366+ }();
367+
368+ auto created = create_sdl_gamepad (*runtime, test_case);
369+ ASSERT_TRUE (created) << created.status .message ();
370+
371+ const auto joystick_index = wait_for_sdl_joystick (expected_profile);
372+ ASSERT_GE (joystick_index, 0 );
373+
374+ SdlJoystick joystick {SDL_JoystickOpen (joystick_index), SDL_JoystickClose};
375+ ASSERT_NE (joystick.get (), nullptr ) << SDL_GetError ();
376+ expect_sdl_joystick_profile (
377+ joystick.get (),
378+ expected_profile,
379+ test_case.minimum_buttons ,
380+ test_case.minimum_axes
381+ );
382+
383+ lvh::GamepadState state;
384+ state.buttons .set (lvh::GamepadButton::a);
385+ state.left_stick = {0 .75F , -0 .5F };
386+ ASSERT_TRUE (created.gamepad ->submit (state).ok ());
387+
388+ EXPECT_TRUE (wait_for_sdl_gamepad_input (joystick.get ())) << describe_sdl_state (joystick.get ());
389+ }
390+
391+ void run_sdl_dualsense_controller_test (SdlGamepadConsumerCase test_case) {
392+ configure_sdl_hidapi_hints ();
393+ ASSERT_EQ (SDL_Init (SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS ), 0 ) << SDL_GetError ();
394+ ScopeExit sdl_quit {[]() {
395+ SDL_Quit ();
396+ }};
397+
398+ lvh::RuntimeOptions runtime_options;
399+ runtime_options.backend = lvh::BackendKind::platform_default;
400+ auto runtime = lvh::Runtime::create (runtime_options);
401+ ASSERT_TRUE (runtime->capabilities ().supports_gamepad );
402+
403+ const auto expected_profile = [&test_case]() {
404+ auto profile = test_case.profile ;
405+ profile.name = unique_device_name (test_case.name_suffix );
406+ return profile;
407+ }();
408+
409+ auto created = create_sdl_gamepad (*runtime, test_case);
410+ ASSERT_TRUE (created) << created.status .message ();
411+
412+ const auto joystick_index = wait_for_sdl_joystick (expected_profile);
413+ ASSERT_GE (joystick_index, 0 );
414+ ASSERT_EQ (SDL_IsGameController (joystick_index), SDL_TRUE ) << SDL_GetError ();
415+
416+ SdlGameController controller {SDL_GameControllerOpen (joystick_index), SDL_GameControllerClose};
417+ ASSERT_NE (controller.get (), nullptr ) << SDL_GetError ();
418+
419+ auto *joystick = SDL_GameControllerGetJoystick (controller.get ());
420+ ASSERT_NE (joystick, nullptr ) << SDL_GetError ();
421+ expect_sdl_joystick_profile (
422+ joystick,
423+ expected_profile,
424+ test_case.minimum_buttons ,
425+ test_case.minimum_axes
426+ );
427+
428+ lvh::GamepadState state;
429+ state.buttons .set (lvh::GamepadButton::a);
430+ state.buttons .set (lvh::GamepadButton::b);
431+ state.buttons .set (lvh::GamepadButton::x);
432+ state.buttons .set (lvh::GamepadButton::y);
433+ state.left_stick = {0 .75F , -0 .5F };
434+ state.right_stick = {-0 .25F , 0 .5F };
435+ state.left_trigger = 0 .25F ;
436+ state.right_trigger = 0 .75F ;
437+ state.acceleration = lvh::Vector3 {.x = 1 .0F , .y = 2 .0F , .z = 3 .0F };
438+ state.gyroscope = lvh::Vector3 {.x = 4 .0F , .y = 5 .0F , .z = 6 .0F };
439+ state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::charging, .percentage = 80 };
440+ state.touchpad_contacts [0 ] = {.id = 1 , .active = true , .x = 0 .5F , .y = 0 .25F };
441+ ASSERT_TRUE (created.gamepad ->submit (state).ok ());
442+
443+ expect_sdl_dualsense_controller_profile (controller.get ());
444+ if (test_case.expect_live_input ) {
445+ EXPECT_TRUE (wait_for_sdl_controller_input (controller.get ())) << describe_sdl_controller_state (controller.get ());
446+ }
447+ }
448+
232449 void destroy_libinput_event (libinput_event *event) {
233450 if (event != nullptr ) {
234451 libinput_event_destroy (event);
@@ -306,39 +523,36 @@ namespace {
306523TEST_F (LinuxConsumerTest, SdlSeesUhidGamepadButtonAndAxisInput) {
307524 ASSERT_TRUE (HasReadableWritableDeviceNode (" /dev/uhid" ));
308525
309- SDL_SetHint (SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS , " 1" );
310- ASSERT_EQ (SDL_Init (SDL_INIT_JOYSTICK | SDL_INIT_EVENTS ), 0 ) << SDL_GetError ();
311- ScopeExit sdl_quit {[]() {
312- SDL_Quit ();
313- }};
314-
315- lvh::RuntimeOptions runtime_options;
316- runtime_options.backend = lvh::BackendKind::platform_default;
317- auto runtime = lvh::Runtime::create (runtime_options);
318- ASSERT_TRUE (runtime->capabilities ().supports_gamepad );
319-
320- lvh::CreateGamepadOptions options;
321- options.profile = lvh::profiles::generic_gamepad ();
322- options.profile .name = unique_device_name (" SDL Gamepad" );
323- options.metadata .stable_id = " libvirtualhid-sdl-gamepad-test" ;
324-
325- auto created = runtime->create_gamepad (options);
326- ASSERT_TRUE (created) << created.status .message ();
526+ run_sdl_uhid_joystick_test ({
527+ .profile = lvh::profiles::generic_gamepad (),
528+ .name_suffix = " SDL Gamepad" ,
529+ .stable_id = " libvirtualhid-sdl-gamepad-test" ,
530+ });
531+ }
327532
328- const auto joystick_index = wait_for_sdl_joystick (options. profile );
329- ASSERT_GE (joystick_index, 0 );
533+ TEST_F (LinuxConsumerTest, SdlSeesDualSenseUsbControllerBehavior) {
534+ ASSERT_TRUE ( HasReadableWritableDeviceNode ( " /dev/uhid " ) );
330535
331- SdlJoystick joystick {SDL_JoystickOpen (joystick_index), SDL_JoystickClose};
332- ASSERT_NE (joystick.get (), nullptr ) << SDL_GetError ();
333- EXPECT_GE (SDL_JoystickNumButtons (joystick.get ()), 1 );
334- EXPECT_GE (SDL_JoystickNumAxes (joystick.get ()), 2 );
536+ run_sdl_dualsense_controller_test ({
537+ .profile = lvh::profiles::dualsense_usb (),
538+ .name_suffix = " SDL DualSense USB" ,
539+ .stable_id = " 02:00:00:00:00:01" ,
540+ .minimum_buttons = 10 ,
541+ .minimum_axes = 4 ,
542+ });
543+ }
335544
336- lvh::GamepadState state;
337- state.buttons .set (lvh::GamepadButton::a);
338- state.left_stick = {0 .75F , -0 .5F };
339- ASSERT_TRUE (created.gamepad ->submit (state).ok ());
545+ TEST_F (LinuxConsumerTest, SdlSeesDualSenseBluetoothControllerDiscovery) {
546+ ASSERT_TRUE (HasReadableWritableDeviceNode (" /dev/uhid" ));
340547
341- EXPECT_TRUE (wait_for_sdl_gamepad_input (joystick.get ())) << describe_sdl_state (joystick.get ());
548+ run_sdl_dualsense_controller_test ({
549+ .profile = lvh::profiles::dualsense_bluetooth (),
550+ .name_suffix = " SDL DualSense Bluetooth" ,
551+ .stable_id = " 02:00:00:00:00:02" ,
552+ .minimum_buttons = 10 ,
553+ .minimum_axes = 4 ,
554+ .expect_live_input = false ,
555+ });
342556}
343557
344558TEST_F (LinuxConsumerTest, LibinputSeesUinputKeyboardKeys) {
@@ -569,6 +783,10 @@ TEST_F(LinuxConsumerTest, LibinputSeesUinputPenTabletTool) {
569783#else
570784TEST_F (LinuxConsumerTest, SdlSeesUhidGamepadButtonAndAxisInput) {}
571785
786+ TEST_F (LinuxConsumerTest, SdlSeesDualSenseUsbControllerBehavior) {}
787+
788+ TEST_F (LinuxConsumerTest, SdlSeesDualSenseBluetoothControllerDiscovery) {}
789+
572790TEST_F (LinuxConsumerTest, LibinputSeesUinputKeyboardKeys) {}
573791
574792TEST_F (LinuxConsumerTest, LibinputSeesUinputMouseMotionAndButtons) {}
0 commit comments