@@ -51,8 +51,20 @@ 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+ };
67+
5668 /* *
5769 * @brief Execute cleanup code when a scope exits.
5870 */
@@ -229,6 +241,139 @@ namespace {
229241 return false ;
230242 }
231243
244+ bool wait_for_sdl_controller_input (SDL_GameController *controller) {
245+ const auto deadline = std::chrono::steady_clock::now () + std::chrono::seconds {3 };
246+
247+ while (std::chrono::steady_clock::now () < deadline) {
248+ SDL_GameControllerUpdate ();
249+ pump_sdl_events ();
250+
251+ const auto button_pressed = SDL_GameControllerGetButton (controller, SDL_CONTROLLER_BUTTON_A ) != 0 ;
252+ const auto left_x = std::abs (static_cast <int >(SDL_GameControllerGetAxis (controller, SDL_CONTROLLER_AXIS_LEFTX )));
253+ const auto left_y = std::abs (static_cast <int >(SDL_GameControllerGetAxis (controller, SDL_CONTROLLER_AXIS_LEFTY )));
254+
255+ if (button_pressed && (left_x > 8000 || left_y > 8000 )) {
256+ return true ;
257+ }
258+
259+ std::this_thread::sleep_for (std::chrono::milliseconds {50 });
260+ }
261+
262+ return false ;
263+ }
264+
265+ void configure_sdl_hidapi_hints () {
266+ SDL_SetHint (SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS , " 1" );
267+ SDL_SetHint (" SDL_JOYSTICK_HIDAPI" , " 1" );
268+ SDL_SetHint (" SDL_JOYSTICK_HIDAPI_PS5" , " 1" );
269+ }
270+
271+ lvh::GamepadCreationResult create_sdl_gamepad (lvh::Runtime &runtime, SdlGamepadConsumerCase test_case) {
272+ lvh::CreateGamepadOptions options;
273+ options.profile = std::move (test_case.profile );
274+ options.profile .name = unique_device_name (test_case.name_suffix );
275+ options.metadata .stable_id = std::string {test_case.stable_id };
276+
277+ return runtime.create_gamepad (options);
278+ }
279+
280+ void expect_sdl_joystick_profile (SDL_Joystick *joystick, const lvh::DeviceProfile &profile, int minimum_buttons, int minimum_axes) {
281+ EXPECT_EQ (SDL_JoystickGetVendor (joystick), profile.vendor_id );
282+ EXPECT_EQ (SDL_JoystickGetProduct (joystick), profile.product_id );
283+ EXPECT_GE (SDL_JoystickNumButtons (joystick), minimum_buttons);
284+ EXPECT_GE (SDL_JoystickNumAxes (joystick), minimum_axes);
285+ }
286+
287+ void run_sdl_uhid_joystick_test (SdlGamepadConsumerCase test_case) {
288+ configure_sdl_hidapi_hints ();
289+ ASSERT_EQ (SDL_Init (SDL_INIT_JOYSTICK | SDL_INIT_EVENTS ), 0 ) << SDL_GetError ();
290+ ScopeExit sdl_quit {[]() {
291+ SDL_Quit ();
292+ }};
293+
294+ lvh::RuntimeOptions runtime_options;
295+ runtime_options.backend = lvh::BackendKind::platform_default;
296+ auto runtime = lvh::Runtime::create (runtime_options);
297+ ASSERT_TRUE (runtime->capabilities ().supports_gamepad );
298+
299+ const auto expected_profile = [&test_case]() {
300+ auto profile = test_case.profile ;
301+ profile.name = unique_device_name (test_case.name_suffix );
302+ return profile;
303+ }();
304+
305+ auto created = create_sdl_gamepad (*runtime, test_case);
306+ ASSERT_TRUE (created) << created.status .message ();
307+
308+ const auto joystick_index = wait_for_sdl_joystick (expected_profile);
309+ ASSERT_GE (joystick_index, 0 );
310+
311+ SdlJoystick joystick {SDL_JoystickOpen (joystick_index), SDL_JoystickClose};
312+ ASSERT_NE (joystick.get (), nullptr ) << SDL_GetError ();
313+ expect_sdl_joystick_profile (
314+ joystick.get (),
315+ expected_profile,
316+ test_case.minimum_buttons ,
317+ test_case.minimum_axes
318+ );
319+
320+ lvh::GamepadState state;
321+ state.buttons .set (lvh::GamepadButton::a);
322+ state.left_stick = {0 .75F , -0 .5F };
323+ ASSERT_TRUE (created.gamepad ->submit (state).ok ());
324+
325+ EXPECT_TRUE (wait_for_sdl_gamepad_input (joystick.get ())) << describe_sdl_state (joystick.get ());
326+ }
327+
328+ void run_sdl_dualsense_controller_test (SdlGamepadConsumerCase test_case) {
329+ configure_sdl_hidapi_hints ();
330+ ASSERT_EQ (SDL_Init (SDL_INIT_GAMECONTROLLER | SDL_INIT_JOYSTICK | SDL_INIT_EVENTS ), 0 ) << SDL_GetError ();
331+ ScopeExit sdl_quit {[]() {
332+ SDL_Quit ();
333+ }};
334+
335+ lvh::RuntimeOptions runtime_options;
336+ runtime_options.backend = lvh::BackendKind::platform_default;
337+ auto runtime = lvh::Runtime::create (runtime_options);
338+ ASSERT_TRUE (runtime->capabilities ().supports_gamepad );
339+
340+ const auto expected_profile = [&test_case]() {
341+ auto profile = test_case.profile ;
342+ profile.name = unique_device_name (test_case.name_suffix );
343+ return profile;
344+ }();
345+
346+ auto created = create_sdl_gamepad (*runtime, test_case);
347+ ASSERT_TRUE (created) << created.status .message ();
348+
349+ const auto joystick_index = wait_for_sdl_joystick (expected_profile);
350+ ASSERT_GE (joystick_index, 0 );
351+ ASSERT_EQ (SDL_IsGameController (joystick_index), SDL_TRUE ) << SDL_GetError ();
352+
353+ SdlGameController controller {SDL_GameControllerOpen (joystick_index), SDL_GameControllerClose};
354+ ASSERT_NE (controller.get (), nullptr ) << SDL_GetError ();
355+
356+ auto *joystick = SDL_GameControllerGetJoystick (controller.get ());
357+ ASSERT_NE (joystick, nullptr ) << SDL_GetError ();
358+ expect_sdl_joystick_profile (
359+ joystick,
360+ expected_profile,
361+ test_case.minimum_buttons ,
362+ test_case.minimum_axes
363+ );
364+
365+ lvh::GamepadState state;
366+ state.buttons .set (lvh::GamepadButton::a);
367+ state.left_stick = {0 .75F , -0 .5F };
368+ state.acceleration = lvh::Vector3 {.x = 1 .0F , .y = 2 .0F , .z = 3 .0F };
369+ state.gyroscope = lvh::Vector3 {.x = 4 .0F , .y = 5 .0F , .z = 6 .0F };
370+ state.battery = lvh::GamepadBattery {.state = lvh::GamepadBatteryState::charging, .percentage = 80 };
371+ state.touchpad_contacts [0 ] = {.id = 1 , .active = true , .x = 0 .5F , .y = 0 .25F };
372+ ASSERT_TRUE (created.gamepad ->submit (state).ok ());
373+
374+ EXPECT_TRUE (wait_for_sdl_controller_input (controller.get ())) << describe_sdl_state (joystick);
375+ }
376+
232377 void destroy_libinput_event (libinput_event *event) {
233378 if (event != nullptr ) {
234379 libinput_event_destroy (event);
@@ -306,39 +451,35 @@ namespace {
306451TEST_F (LinuxConsumerTest, SdlSeesUhidGamepadButtonAndAxisInput) {
307452 ASSERT_TRUE (HasReadableWritableDeviceNode (" /dev/uhid" ));
308453
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 ();
454+ run_sdl_uhid_joystick_test ({
455+ .profile = lvh::profiles::generic_gamepad (),
456+ .name_suffix = " SDL Gamepad" ,
457+ .stable_id = " libvirtualhid-sdl-gamepad-test" ,
458+ });
459+ }
327460
328- const auto joystick_index = wait_for_sdl_joystick (options. profile );
329- ASSERT_GE (joystick_index, 0 );
461+ TEST_F (LinuxConsumerTest, SdlSeesDualSenseUsbControllerBehavior) {
462+ ASSERT_TRUE ( HasReadableWritableDeviceNode ( " /dev/uhid " ) );
330463
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 );
464+ run_sdl_dualsense_controller_test ({
465+ .profile = lvh::profiles::dualsense_usb (),
466+ .name_suffix = " SDL DualSense USB" ,
467+ .stable_id = " 02:00:00:00:00:01" ,
468+ .minimum_buttons = 10 ,
469+ .minimum_axes = 4 ,
470+ });
471+ }
335472
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 ());
473+ TEST_F (LinuxConsumerTest, SdlSeesDualSenseBluetoothControllerBehavior) {
474+ ASSERT_TRUE (HasReadableWritableDeviceNode (" /dev/uhid" ));
340475
341- EXPECT_TRUE (wait_for_sdl_gamepad_input (joystick.get ())) << describe_sdl_state (joystick.get ());
476+ run_sdl_dualsense_controller_test ({
477+ .profile = lvh::profiles::dualsense_bluetooth (),
478+ .name_suffix = " SDL DualSense Bluetooth" ,
479+ .stable_id = " 02:00:00:00:00:02" ,
480+ .minimum_buttons = 10 ,
481+ .minimum_axes = 4 ,
482+ });
342483}
343484
344485TEST_F (LinuxConsumerTest, LibinputSeesUinputKeyboardKeys) {
@@ -569,6 +710,10 @@ TEST_F(LinuxConsumerTest, LibinputSeesUinputPenTabletTool) {
569710#else
570711TEST_F (LinuxConsumerTest, SdlSeesUhidGamepadButtonAndAxisInput) {}
571712
713+ TEST_F (LinuxConsumerTest, SdlSeesDualSenseUsbControllerBehavior) {}
714+
715+ TEST_F (LinuxConsumerTest, SdlSeesDualSenseBluetoothControllerBehavior) {}
716+
572717TEST_F (LinuxConsumerTest, LibinputSeesUinputKeyboardKeys) {}
573718
574719TEST_F (LinuxConsumerTest, LibinputSeesUinputMouseMotionAndButtons) {}
0 commit comments