From f4c9a8d4503f8218cfc02de3192879bc60c18445 Mon Sep 17 00:00:00 2001 From: danielng Date: Mon, 17 Apr 2023 07:24:26 +0000 Subject: [PATCH] vm_tools: add mapping for dualsense gamepad Adding an input mapping for the dualsense gamepad to correct it's mapping. BUG=b:277829347 TEST=tested on dut Change-Id: I961b0b546272a83e43164b53f72a3d70566a18a7 Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/4432333 Reviewed-by: Kenneth Albanowski Reviewed-by: Daniel Ng Tested-by: Daniel Ng Reviewed-by: Chloe Pelling Commit-Queue: Daniel Ng --- BUILD.gn | 12 +- libevdev/libevdev-shim.cc | 59 ++++++ libevdev/libevdev-shim.h | 50 +++++ libevdev/mock-libevdev-shim.h | 72 +++++++ meson.build | 4 +- sommelier-ctx.cc | 1 + sommelier-ctx.h | 1 + sommelier-gaming-test.cc | 353 ++++++++++++++++++++++++++++++++++ sommelier-gaming.cc | 255 ++++++++++++++++++------ sommelier.cc | 7 + sommelier.h | 9 +- testing/sommelier-test-util.h | 3 + testing/wayland-test-base.h | 17 ++ 13 files changed, 780 insertions(+), 63 deletions(-) create mode 100644 libevdev/libevdev-shim.cc create mode 100644 libevdev/libevdev-shim.h create mode 100644 libevdev/mock-libevdev-shim.h create mode 100644 sommelier-gaming-test.cc diff --git a/BUILD.gn b/BUILD.gn index 62546e3..d5fd56b 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -82,7 +82,10 @@ sommelier_defines = [ ] + gaming_defines + tracing_defines static_library("libgaming") { - sources = [ "sommelier-gaming.cc" ] + sources = [ + "libevdev/libevdev-shim.cc", + "sommelier-gaming.cc", + ] defines = gaming_defines pkg_deps = [ "libevdev", @@ -128,6 +131,7 @@ static_library("libsommelier") { pkg_deps = [ "gbm", "libdrm", + "libevdev", "pixman-1", "wayland-client", "wayland-server", @@ -155,6 +159,7 @@ executable("sommelier") { if (use.test) { executable("sommelier_test") { sources = [ + "sommelier-gaming-test.cc", "sommelier-output-test.cc", "sommelier-test-main.cc", "sommelier-test.cc", @@ -167,7 +172,10 @@ if (use.test) { defines = sommelier_defines - pkg_deps = [ "pixman-1" ] + pkg_deps = [ + "libevdev", + "pixman-1", + ] + tracing_pkg_deps # gnlint: disable=GnLintCommonTesting libs = [ diff --git a/libevdev/libevdev-shim.cc b/libevdev/libevdev-shim.cc new file mode 100644 index 0000000..ff35509 --- /dev/null +++ b/libevdev/libevdev-shim.cc @@ -0,0 +1,59 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "libevdev-shim.h" // NOLINT(build/include_directory) + +struct libevdev* LibevdevShim::new_evdev(void) { + return libevdev_new(); +} +void LibevdevShim::free(struct libevdev* dev) { + libevdev_free(dev); +} +int LibevdevShim::enable_event_code(struct libevdev* dev, + unsigned int type, + unsigned int code, + const void* data) { + return libevdev_enable_event_code(dev, type, code, data); +} +void LibevdevShim::set_name(struct libevdev* dev, const char* name) { + libevdev_set_name(dev, name); +} +void LibevdevShim::set_id_product(struct libevdev* dev, int product_id) { + libevdev_set_id_product(dev, product_id); +} +void LibevdevShim::set_id_vendor(struct libevdev* dev, int vendor_id) { + libevdev_set_id_vendor(dev, vendor_id); +} +void LibevdevShim::set_id_bustype(struct libevdev* dev, int bustype) { + libevdev_set_id_bustype(dev, bustype); +} +void LibevdevShim::set_id_version(struct libevdev* dev, int version) { + libevdev_set_id_version(dev, version); +} + +int LibevdevShim::uinput_create_from_device( + const struct libevdev* dev, + int uinput_fd, + struct libevdev_uinput** uinput_dev) { + return libevdev_uinput_create_from_device(dev, uinput_fd, uinput_dev); +} +int LibevdevShim::uinput_write_event(const struct libevdev_uinput* uinput_dev, + unsigned int type, + unsigned int code, + int value) { + return libevdev_uinput_write_event(uinput_dev, type, code, value); +} +void LibevdevShim::uinput_destroy(struct libevdev_uinput* uinput_dev) { + libevdev_uinput_destroy(uinput_dev); +} + +LibevdevShim* Libevdev::singleton = nullptr; + +LibevdevShim* Libevdev::Get() { + return singleton; +} + +void Libevdev::Set(LibevdevShim* shim) { + singleton = shim; +} diff --git a/libevdev/libevdev-shim.h b/libevdev/libevdev-shim.h new file mode 100644 index 0000000..8ee2359 --- /dev/null +++ b/libevdev/libevdev-shim.h @@ -0,0 +1,50 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef VM_TOOLS_SOMMELIER_LIBEVDEV_LIBEVDEV_SHIM_H_ +#define VM_TOOLS_SOMMELIER_LIBEVDEV_LIBEVDEV_SHIM_H_ + +#include +#include + +class LibevdevShim { + public: + LibevdevShim() = default; + LibevdevShim(LibevdevShim&&) = delete; + LibevdevShim& operator=(LibevdevShim&&) = delete; + + virtual ~LibevdevShim() = default; + + virtual struct libevdev* new_evdev(void); + virtual void free(struct libevdev* dev); + virtual int enable_event_code(struct libevdev* dev, + unsigned int type, + unsigned int code, + const void* data); + virtual void set_name(struct libevdev* dev, const char* name); + virtual void set_id_product(struct libevdev* dev, int product_id); + virtual void set_id_vendor(struct libevdev* dev, int vendor_id); + virtual void set_id_bustype(struct libevdev* dev, int bustype); + virtual void set_id_version(struct libevdev* dev, int version); + + virtual int uinput_create_from_device(const struct libevdev* dev, + int uinput_fd, + struct libevdev_uinput** uinput_dev); + virtual int uinput_write_event(const struct libevdev_uinput* uinput_dev, + unsigned int type, + unsigned int code, + int value); + virtual void uinput_destroy(struct libevdev_uinput* uinput_dev); +}; + +class Libevdev { + public: + static LibevdevShim* Get(); + static void Set(LibevdevShim* shim); + + private: + static LibevdevShim* singleton; +}; + +#endif // VM_TOOLS_SOMMELIER_LIBEVDEV_LIBEVDEV_SHIM_H_ diff --git a/libevdev/mock-libevdev-shim.h b/libevdev/mock-libevdev-shim.h new file mode 100644 index 0000000..63886b0 --- /dev/null +++ b/libevdev/mock-libevdev-shim.h @@ -0,0 +1,72 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef VM_TOOLS_SOMMELIER_LIBEVDEV_MOCK_LIBEVDEV_SHIM_H_ +#define VM_TOOLS_SOMMELIER_LIBEVDEV_MOCK_LIBEVDEV_SHIM_H_ + +#include + +#include "libevdev-shim.h" // NOLINT(build/include_directory) + +class MockLibevdevShim : public LibevdevShim { + public: + MOCK_METHOD(struct libevdev*, new_evdev, (), (override)); + + MOCK_METHOD(void, free, (struct libevdev * dev), (override)); + + MOCK_METHOD(int, + enable_event_code, + (struct libevdev * dev, + unsigned int type, + unsigned int code, + const void* data), + (override)); + + MOCK_METHOD(void, + set_name, + (struct libevdev * dev, const char* name), + (override)); + + MOCK_METHOD(void, + set_id_product, + (struct libevdev * dev, int product_id), + (override)); + + MOCK_METHOD(void, + set_id_vendor, + (struct libevdev * dev, int vendor_id), + (override)); + + MOCK_METHOD(void, + set_id_bustype, + (struct libevdev * dev, int bustype_id), + (override)); + + MOCK_METHOD(void, + set_id_version, + (struct libevdev * dev, int version_id), + (override)); + + MOCK_METHOD(int, + uinput_create_from_device, + (const struct libevdev* dev, + int uinput_fd, + struct libevdev_uinput** uinput_dev), + (override)); + + MOCK_METHOD(int, + uinput_write_event, + (const struct libevdev_uinput* uinput_dev, + unsigned int type, + unsigned int code, + int value), + (override)); + + MOCK_METHOD(void, + uinput_destroy, + (struct libevdev_uinput * uinput_dev), + (override)); +}; + +#endif // VM_TOOLS_SOMMELIER_LIBEVDEV_MOCK_LIBEVDEV_SHIM_H_ diff --git a/meson.build b/meson.build index fbcfcb6..53e03a5 100644 --- a/meson.build +++ b/meson.build @@ -109,6 +109,7 @@ gamepad_dependencies = [] if get_option('gamepad') gamepad_sources = [ 'sommelier-gaming.cc', + 'libevdev/libevdev-shim.cc', ] gamepad_dependencies = [ dependency('libevdev'), @@ -202,6 +203,7 @@ if get_option('with_tests') sommelier_test = executable('sommelier_test', install: true, sources: [ + 'sommelier-gaming-test.cc', 'sommelier-test.cc', 'sommelier-test-main.cc', 'sommelier-output-test.cc', @@ -215,7 +217,7 @@ if get_option('with_tests') dependency('gtest'), dependency('gmock'), dependency('pixman-1') - ], + ] + gamepad_dependencies + tracing_dependencies, cpp_args: cpp_args + sommelier_defines, include_directories: includes + testing_includes, ) diff --git a/sommelier-ctx.cc b/sommelier-ctx.cc index acdde8a..afbad58 100644 --- a/sommelier-ctx.cc +++ b/sommelier-ctx.cc @@ -115,6 +115,7 @@ void sl_context_init_default(struct sl_context* ctx) { ctx->xdg_output_manager = NULL; #ifdef GAMEPAD_SUPPORT ctx->gaming_input_manager = NULL; + ctx->gaming_seat = NULL; #endif ctx->display_event_source = NULL; ctx->display_ready_event_source = NULL; diff --git a/sommelier-ctx.h b/sommelier-ctx.h index fa9f9cc..cd6db84 100644 --- a/sommelier-ctx.h +++ b/sommelier-ctx.h @@ -88,6 +88,7 @@ struct sl_context { struct sl_xdg_output_manager* xdg_output_manager; #ifdef GAMEPAD_SUPPORT struct sl_gaming_input_manager* gaming_input_manager; + struct zcr_gaming_seat_v2* gaming_seat; #endif struct sl_relative_pointer_manager* relative_pointer_manager; struct sl_pointer_constraints* pointer_constraints; diff --git a/sommelier-gaming-test.cc b/sommelier-gaming-test.cc new file mode 100644 index 0000000..09fa401 --- /dev/null +++ b/sommelier-gaming-test.cc @@ -0,0 +1,353 @@ +// Copyright 2023 The ChromiumOS Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "sommelier-gaming.cc" // NOLINT + +#include +#include +#include +#include +#include +#include +#include + +#include "libevdev/mock-libevdev-shim.h" +#include "testing/x11-test-base.h" + +namespace vm_tools { +namespace sommelier { + +class GamepadTest : public X11TestBase { + public: + void Connect() override { + X11TestBase::Connect(); + + Libevdev::Set(&libevdevshim); + } + + protected: + // Normally a zcr_gamepad_v2 is generated and sent by the server. We don't + // have an easy way to do this client side and need to create it ourselves via + // this hack. + struct zcr_gamepad_v2* CreateGamepadProxy( + struct zcr_gaming_seat_v2* gaming_seat) { + return reinterpret_cast(wl_proxy_create( + reinterpret_cast(gaming_seat), &zcr_gamepad_v2_interface)); + } + + std::vector GetHostGamepads(struct sl_context* ctx) { + std::vector host_gamepads; + struct sl_host_gamepad* it; + wl_list_for_each(it, &(ctx->gamepads), link) { + host_gamepads.push_back(it); + } + return host_gamepads; + } + + void BindWLSeat(struct sl_context* ctx) { + xwayland.get()->BindToWlSeats(ctx); + Pump(); + } + + // Helper function that sets up a gamepad_with_info successfully. + void SetupGamepad(struct sl_context* ctx, + struct zcr_gamepad_v2*& gamepad, + struct libevdev*& ev_dev, + const char* name, + uint32_t bus, + uint32_t vendor_id, + uint32_t product_id, + uint32_t version) { + BindWLSeat(ctx); + gamepad = CreateGamepadProxy(ctx->gaming_seat); + ev_dev = libevdev_new(); + // Make the standard expectations for a successful gamepad_added_with_info + // request. Note that the name, bus, vendor_id, product_id and + // version args are referring to the host controller. At this stage, we hard + // code what the emulated controller will appear in the VM as, which is why + // the expectations below differ from the args from the host. + EXPECT_CALL(libevdevshim, new_evdev()).WillOnce(Return(ev_dev)); + EXPECT_CALL(libevdevshim, set_name(ev_dev, kXboxName)); + EXPECT_CALL(libevdevshim, set_id_bustype(ev_dev, kUsbBus)); + EXPECT_CALL(libevdevshim, set_id_vendor(ev_dev, kXboxVendor)); + EXPECT_CALL(libevdevshim, set_id_product(ev_dev, kXboxProduct)); + EXPECT_CALL(libevdevshim, set_id_version(ev_dev, kXboxVersion)); + for (unsigned int i = 0; i < ARRAY_SIZE(kButtons); i++) { + EXPECT_CALL(libevdevshim, + enable_event_code(ev_dev, EV_KEY, kButtons[i], nullptr)); + } + + HostEventHandler(ctx->gaming_seat) + ->gamepad_added_with_device_info(ctx, ctx->gaming_seat, gamepad, name, + bus, vendor_id, product_id, version); + } + + testing::StrictMock libevdevshim; +}; + +TEST_F(GamepadTest, GamingSeatCreatedOnWLSeatBind) { + EXPECT_EQ(ctx.gaming_seat, nullptr); + + BindWLSeat(&ctx); + + EXPECT_NE(ctx.gaming_seat, nullptr); +} + +TEST_F(GamepadTest, AddedDoesNothing) { + BindWLSeat(&ctx); + struct zcr_gamepad_v2* gamepad = CreateGamepadProxy(ctx.gaming_seat); + + HostEventHandler(ctx.gaming_seat) + ->gamepad_added(&ctx, ctx.gaming_seat, gamepad); + + EXPECT_EQ(GetHostGamepads(&ctx).size(), 0); +} + +TEST_F(GamepadTest, AddedWithInfoSetsErrorStateOnLibevdevFail) { + BindWLSeat(&ctx); + struct zcr_gamepad_v2* gamepad = CreateGamepadProxy(ctx.gaming_seat); + + EXPECT_CALL(libevdevshim, new_evdev()).WillOnce(Return(nullptr)); + + HostEventHandler(ctx.gaming_seat) + ->gamepad_added_with_device_info(&ctx, ctx.gaming_seat, gamepad, "Xbox", + 1, 2, 3, 4); + + EXPECT_EQ(GetHostGamepads(&ctx)[0]->state, kStateError); +} + +TEST_F(GamepadTest, AddedWithInfoSuccess) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + + std::vector host_gamepads = GetHostGamepads(&ctx); + EXPECT_EQ(host_gamepads[0]->state, kStatePending); + EXPECT_EQ(host_gamepads[0]->ev_dev, ev_dev); +} + +TEST_F(GamepadTest, MultipleGamepadAddedWithInfoSuccess) { + struct zcr_gamepad_v2* gamepad1; + struct libevdev* ev_dev1; + struct zcr_gamepad_v2* gamepad2; + struct libevdev* ev_dev2; + SetupGamepad(&ctx, gamepad1, ev_dev1, "Xbox", 1, 2, 3, 4); + SetupGamepad(&ctx, gamepad2, ev_dev2, "Xbox", 1, 2, 3, 4); + + EXPECT_EQ(GetHostGamepads(&ctx).size(), 2); +} + +TEST_F(GamepadTest, ActivatedSetsErrorStateIfGamepadNotActive) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + host_gamepads[0]->state = kStateUnknown; + + HostEventHandler(gamepad)->activated(host_gamepads[0], gamepad); + + EXPECT_EQ(host_gamepads[0]->state, kStateError); +} + +TEST_F(GamepadTest, ActivatedSetsErrorStateIfLibevdevFails) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + + EXPECT_CALL(libevdevshim, + uinput_create_from_device(ev_dev, LIBEVDEV_UINPUT_OPEN_MANAGED, + &host_gamepads[0]->uinput_dev)) + .WillOnce(Return(1)); + + HostEventHandler(gamepad)->activated(host_gamepads[0], gamepad); + + EXPECT_EQ(host_gamepads[0]->state, kStateError); +} + +TEST_F(GamepadTest, ActivatedSuccess) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + + EXPECT_CALL(libevdevshim, + uinput_create_from_device(ev_dev, LIBEVDEV_UINPUT_OPEN_MANAGED, + &host_gamepads[0]->uinput_dev)) + .WillOnce(Return(0)); + + HostEventHandler(gamepad)->activated(host_gamepads[0], gamepad); + + EXPECT_EQ(host_gamepads[0]->state, kStateActivated); +} + +TEST_F(GamepadTest, VibratorAddedDoesNothing) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + + HostEventHandler(gamepad)->vibrator_added(GetHostGamepads(&ctx)[0], gamepad, + nullptr); +} + +TEST_F(GamepadTest, AxisAddedSetsErrorStateIfGamepadNotActive) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + host_gamepads[0]->state = kStateUnknown; + + HostEventHandler(gamepad)->axis_added(host_gamepads[0], gamepad, 1, 2, 3, 4, + 5, 6); + + EXPECT_EQ(host_gamepads[0]->state, kStateError); +} + +TEST_F(GamepadTest, AxisAddedSuccess) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + + EXPECT_CALL(libevdevshim, enable_event_code(ev_dev, EV_ABS, 1, ::testing::_)); + + HostEventHandler(gamepad)->axis_added(GetHostGamepads(&ctx)[0], gamepad, 1, 2, + 3, 4, 5, 6); +} + +TEST_F(GamepadTest, FrameDoesNothingIfGamepadNotActive) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + host_gamepads[0]->state = kStateUnknown; + + HostEventHandler(gamepad)->frame(host_gamepads[0], gamepad, 1); +} + +TEST_F(GamepadTest, FrameSuccess) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + host_gamepads[0]->state = kStateActivated; + + EXPECT_CALL(libevdevshim, uinput_write_event(host_gamepads[0]->uinput_dev, + EV_SYN, SYN_REPORT, 0)); + + HostEventHandler(gamepad)->frame(host_gamepads[0], gamepad, 1); +} + +TEST_F(GamepadTest, ButtonDoesNothingIfGamepadNotActive) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + host_gamepads[0]->state = kStateUnknown; + + HostEventHandler(gamepad)->button(host_gamepads[0], gamepad, 1, 2, + ZCR_GAMEPAD_V2_BUTTON_STATE_RELEASED, 0); +} + +TEST_F(GamepadTest, ButtonSuccess) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + host_gamepads[0]->state = kStateActivated; + + EXPECT_CALL(libevdevshim, + uinput_write_event(host_gamepads[0]->uinput_dev, EV_KEY, 2, 0)); + + HostEventHandler(gamepad)->button(host_gamepads[0], gamepad, 1, 2, + ZCR_GAMEPAD_V2_BUTTON_STATE_RELEASED, 0); +} + +TEST_F(GamepadTest, AxisDoesNothingIfGamepadNotActive) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + host_gamepads[0]->state = kStateUnknown; + + HostEventHandler(gamepad)->axis(host_gamepads[0], gamepad, 1, 2, 250); +} + +TEST_F(GamepadTest, AxisSuccess) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + host_gamepads[0]->state = kStateActivated; + + EXPECT_CALL(libevdevshim, + uinput_write_event(host_gamepads[0]->uinput_dev, EV_ABS, 2, + wl_fixed_to_double(250))); + + HostEventHandler(gamepad)->axis(host_gamepads[0], gamepad, 1, 2, 250); +} + +TEST_F(GamepadTest, RemovedSuccess) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", 1, 2, 3, 4); + std::vector host_gamepads = GetHostGamepads(&ctx); + host_gamepads[0]->state = kStateActivated; + + EXPECT_CALL(libevdevshim, free(host_gamepads[0]->ev_dev)); + + HostEventHandler(gamepad)->removed(host_gamepads[0], gamepad); + + EXPECT_EQ(GetHostGamepads(&ctx).size(), 0); +} + +TEST_F(GamepadTest, MappingsWorkCorrectly) { + // This object forces all EXPECT_CALLs to occur in the order they are + // declared. This insures expectations are paired to the correct + // mappings and events. + testing::InSequence sequence; + + BindWLSeat(&ctx); + for (auto& it : kDeviceMappings) { + struct zcr_gamepad_v2* gamepad; + struct libevdev* ev_dev; + SetupGamepad(&ctx, gamepad, ev_dev, "Xbox", + ZCR_GAMING_SEAT_V2_BUS_TYPE_BLUETOOTH, it.first.vendor, + it.first.product, it.first.version); + std::vector host_gamepads = GetHostGamepads(&ctx); + + EXPECT_EQ(host_gamepads.size(), 1); + + for (auto& input : *it.second) { + EXPECT_CALL(libevdevshim, enable_event_code(ev_dev, EV_ABS, input.second, + ::testing::_)); + + HostEventHandler(gamepad)->axis_added(host_gamepads[0], gamepad, + input.first, 2, 3, 4, 5, 6); + } + + host_gamepads[0]->state = kStateActivated; + + for (auto& input : *it.second) { + EXPECT_CALL(libevdevshim, + uinput_write_event(host_gamepads[0]->uinput_dev, EV_ABS, + input.second, wl_fixed_to_double(250))); + EXPECT_CALL(libevdevshim, uinput_write_event(host_gamepads[0]->uinput_dev, + EV_KEY, input.second, 0)); + + HostEventHandler(gamepad)->axis(host_gamepads[0], gamepad, 1, input.first, + 250); + HostEventHandler(gamepad)->button( + host_gamepads[0], gamepad, 1, input.first, + ZCR_GAMEPAD_V2_BUTTON_STATE_RELEASED, 0); + } + + EXPECT_EQ(host_gamepads[0]->mapping, it.second); + EXPECT_CALL(libevdevshim, free(host_gamepads[0]->ev_dev)); + + HostEventHandler(gamepad)->removed(host_gamepads[0], gamepad); + } +} + +} // namespace sommelier +} // namespace vm_tools diff --git a/sommelier-gaming.cc b/sommelier-gaming.cc index edb543f..98dd111 100644 --- a/sommelier-gaming.cc +++ b/sommelier-gaming.cc @@ -9,10 +9,12 @@ #include #include #include +#include "libevdev/libevdev-shim.h" #include #include #include #include +#include #include "gaming-input-unstable-v2-client-protocol.h" // NOLINT(build/include_directory) @@ -46,21 +48,158 @@ enum GamepadActivationState { kStateError = 3 // Error occurred during construction; ignore gracefully }; +struct DeviceID { + const uint32_t vendor; + const uint32_t product; + const uint32_t version; + + bool operator==(const DeviceID& other) const { + return vendor == other.vendor && product == other.product && + version == other.version; + } +}; + +// Simple Hash/equal operators for DeviceID struct. Doesn't need to be efficient +// as the size of our map will be small. +template <> +struct std::hash { + std::size_t operator()(const DeviceID& device) const { + return device.vendor ^ device.product ^ device.version; + } +}; + +// Buttons being emulated by libevdev uinput. +// Note: Do not enable BTN_TL2 or BTN_TR2, as they will significantly +// change the Linux joydev interpretation of the triggers on ABS_Z/ABS_RZ. +const int kButtons[] = {BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, + BTN_TL, BTN_TR, BTN_THUMBL, BTN_THUMBR, + BTN_SELECT, BTN_START, BTN_MODE}; + +// ID constants for identifying gamepads. Note that some gamepads +// may share the same vendor if they're from the same brand. Bluetooth (BT) +// and USB variants may also share the same product id - though this is +// not guaranteed. + +// IDs for emulated controllers. const char kXboxName[] = "Microsoft X-Box One S pad"; const uint32_t kUsbBus = 0x03; const uint32_t kXboxVendor = 0x45e; const uint32_t kXboxProduct = 0x2ea; const uint32_t kXboxVersion = 0x301; -// Note: the BT vendor ID for SteelSeries is due to a chipset bug +// Note: the Bluetooth (BT) vendor ID for SteelSeries is due to a chipset bug // and is not an actual claimed Vendor ID. -const uint32_t kSteelSeriesVendorBt = 0x111; -const uint32_t kSteelSeriesProductStratusDuoBt = 0x1431; -const uint32_t kSteelSeriesProductStratusPlusBt = 0x1434; +const uint32_t kSteelSeriesBTVendor = 0x111; const uint32_t kStadiaVendor = 0x18d1; const uint32_t kStadiaProduct = 0x9400; -const uint32_t kStadiaVersion = 0x111; + +const uint32_t kSonyVendor = 0x54C; +const uint32_t kDualSenseProduct = 0xCE6; + +const DeviceID kStadiaUSB = { + .vendor = kStadiaVendor, .product = kStadiaProduct, .version = 0x111}; + +const DeviceID kStadiaBT = { + .vendor = kStadiaVendor, .product = kStadiaProduct, .version = 0x100}; + +const DeviceID kStratusDuoBT = { + .vendor = kSteelSeriesBTVendor, .product = 0x1431, .version = 0x11B}; + +const DeviceID kStratusPlusBT = { + .vendor = kSteelSeriesBTVendor, .product = 0x1434, .version = 0x216}; + +// DualSense versions are the HID specification versions (bcdHID). We care about +// these versions as hid-playstation and hid-sony use bcdHID to signal that the +// broken hid-generic mapping is not used. +const DeviceID kDualSenseUSB = { + .vendor = kSonyVendor, .product = kDualSenseProduct, .version = 0x111}; + +const DeviceID kDualSenseBT = { + .vendor = kSonyVendor, .product = kDualSenseProduct, .version = 0x100}; + +// Mappings from the input event of a given gamepad (key) to the +// appropriate output event (value). These mappings are intended to maintain +// the locality of a gamepad; i.e the left face button should map to a +// left face button event. Input events not represented in a map will be +// discarded. + +// DualSense (PS5). +const std::unordered_map kDualSenseMapping = { + // Left Joystick + {ABS_X, ABS_X}, + {ABS_Y, ABS_Y}, + // Right Joystick + {ABS_Z, ABS_RX}, + {ABS_RZ, ABS_RY}, + // Joystick press + {BTN_SELECT, BTN_THUMBL}, + {BTN_START, BTN_THUMBR}, + // DPad + {ABS_HAT0X, ABS_HAT0X}, + {ABS_HAT0Y, ABS_HAT0Y}, + // Face Buttons + {BTN_B, BTN_A}, + {BTN_C, BTN_B}, + {BTN_A, BTN_X}, + {BTN_X, BTN_Y}, + // Left bumper and trigger + {BTN_Y, BTN_TL}, + {ABS_RX, ABS_Z}, + // Right bumper and trigger + {BTN_Z, BTN_TR}, + {ABS_RY, ABS_RZ}, + // Menu buttons + {BTN_TL2, BTN_SELECT}, + {BTN_TR2, BTN_START}, + {BTN_MODE, BTN_MODE}, + // Unused buttons: Touchpad_click: BTN_THUMBL, Microphone_button: BTN_THUMBR +}; + +// Represents how the input events of a certain controllers (key) should be +// interpreted (value). So far this pattern has been observed in the Stadia +// and several SteelSeries controllers in BT mode. +const std::unordered_map kAxisQuirkMapping = { + // Left Joystick + {ABS_X, ABS_X}, + {ABS_Y, ABS_Y}, + // Right Joystick + {ABS_Z, ABS_RX}, + {ABS_RZ, ABS_RY}, + // Joystick press + {BTN_THUMBL, BTN_THUMBL}, + {BTN_THUMBR, BTN_THUMBR}, + // DPad + {ABS_HAT0X, ABS_HAT0X}, + {ABS_HAT0Y, ABS_HAT0Y}, + // Face Buttons + {BTN_A, BTN_A}, + {BTN_B, BTN_B}, + {BTN_X, BTN_X}, + {BTN_Y, BTN_Y}, + // Left bumper and trigger + {BTN_TL, BTN_TL}, + {ABS_BRAKE, ABS_Z}, + // Right bumper and trigger + {BTN_TR, BTN_TR}, + {ABS_GAS, ABS_RZ}, + // Menu buttons + {BTN_SELECT, BTN_SELECT}, + {BTN_START, BTN_START}, + {BTN_MODE, BTN_MODE}, +}; + +// Map of devices to their respctive input remappings. +const std::unordered_map*> + kDeviceMappings = { + {kStadiaUSB, &kAxisQuirkMapping}, + {kStadiaBT, &kAxisQuirkMapping}, + {kStratusDuoBT, &kAxisQuirkMapping}, + {kStratusPlusBT, &kAxisQuirkMapping}, + {kDualSenseUSB, &kDualSenseMapping}, + {kDualSenseBT, &kDualSenseMapping}, +}; // Note: the majority of protocol errors are treated as non-fatal, and // are intended to be handled gracefully, as is removal at any @@ -78,9 +217,9 @@ static void sl_internal_gamepad_removed(void* data, host_gamepad->state == kStateError); if (host_gamepad->uinput_dev != NULL) - libevdev_uinput_destroy(host_gamepad->uinput_dev); + Libevdev::Get()->uinput_destroy(host_gamepad->uinput_dev); if (host_gamepad->ev_dev != NULL) - libevdev_free(host_gamepad->ev_dev); + Libevdev::Get()->free(host_gamepad->ev_dev); zcr_gamepad_v2_destroy(gamepad); @@ -88,19 +227,23 @@ static void sl_internal_gamepad_removed(void* data, delete host_gamepad; } -static uint32_t remap_axis(struct sl_host_gamepad* host_gamepad, - uint32_t axis) { - if (host_gamepad->axes_quirk) { - if (axis == ABS_Z) - axis = ABS_RX; - else if (axis == ABS_RZ) - axis = ABS_RY; - else if (axis == ABS_BRAKE) - axis = ABS_Z; - else if (axis == ABS_GAS) - axis = ABS_RZ; +// Helper function to remap the input events from a host_gamepad into the +// correct output event to be emulated by the generated uinput device. +static bool remap_input(struct sl_host_gamepad* host_gamepad, uint32_t& input) { + if (host_gamepad->mapping == nullptr) { + return true; } - return axis; + auto it = host_gamepad->mapping->find(input); + if (it != host_gamepad->mapping->end()) { + input = it->second; + return true; + } + // If a mapping exists, and we get an input we don't expect + // or don't handle, we shouldn't emulate it. An example of this + // is that the DualSense controller's triggers activate an axis + // and a button at the same time, which would result in unexpected + // behaviour if we forwarded both inputs. + return false; } static void sl_internal_gamepad_axis(void* data, @@ -114,11 +257,12 @@ static void sl_internal_gamepad_axis(void* data, if (host_gamepad->state != kStateActivated) return; - axis = remap_axis(host_gamepad, axis); + if (!remap_input(host_gamepad, axis)) + return; // Note: incoming time is ignored, it will be regenerated from current time. - libevdev_uinput_write_event(host_gamepad->uinput_dev, EV_ABS, axis, - wl_fixed_to_double(value)); + Libevdev::Get()->uinput_write_event(host_gamepad->uinput_dev, EV_ABS, axis, + wl_fixed_to_double(value)); } static void sl_internal_gamepad_button(void* data, @@ -133,12 +277,16 @@ static void sl_internal_gamepad_button(void* data, if (host_gamepad->state != kStateActivated) return; + if (!remap_input(host_gamepad, button)) + return; + // Note: Exo wayland server always sends analog==0, only pay attention // to state. int value = (state == ZCR_GAMEPAD_V2_BUTTON_STATE_PRESSED) ? 1 : 0; // Note: incoming time is ignored, it will be regenerated from current time. - libevdev_uinput_write_event(host_gamepad->uinput_dev, EV_KEY, button, value); + Libevdev::Get()->uinput_write_event(host_gamepad->uinput_dev, EV_KEY, button, + value); } static void sl_internal_gamepad_frame(void* data, @@ -151,7 +299,8 @@ static void sl_internal_gamepad_frame(void* data, return; // Note: incoming time is ignored, it will be regenerated from current time. - libevdev_uinput_write_event(host_gamepad->uinput_dev, EV_SYN, SYN_REPORT, 0); + Libevdev::Get()->uinput_write_event(host_gamepad->uinput_dev, EV_SYN, + SYN_REPORT, 0); } static void sl_internal_gamepad_axis_added(void* data, @@ -178,9 +327,11 @@ static void sl_internal_gamepad_axis_added(void* data, return; } - index = remap_axis(host_gamepad, index); + if (!remap_input(host_gamepad, index)) + return; - libevdev_enable_event_code(host_gamepad->ev_dev, EV_ABS, index, &info); + Libevdev::Get()->enable_event_code(host_gamepad->ev_dev, EV_ABS, index, + &info); } static void sl_internal_gamepad_activated(void* data, @@ -195,9 +346,9 @@ static void sl_internal_gamepad_activated(void* data, return; } - int err = libevdev_uinput_create_from_device(host_gamepad->ev_dev, - LIBEVDEV_UINPUT_OPEN_MANAGED, - &host_gamepad->uinput_dev); + int err = Libevdev::Get()->uinput_create_from_device( + host_gamepad->ev_dev, LIBEVDEV_UINPUT_OPEN_MANAGED, + &host_gamepad->uinput_dev); if (err == 0) { // TODO(kenalba): can we destroy and clean up the ev_dev now? host_gamepad->state = kStateActivated; @@ -242,9 +393,9 @@ static void sl_internal_gaming_seat_gamepad_added_with_device_info( host_gamepad->ctx = ctx; host_gamepad->state = kStatePending; - host_gamepad->ev_dev = libevdev_new(); + host_gamepad->ev_dev = Libevdev::Get()->new_evdev(); host_gamepad->uinput_dev = NULL; - host_gamepad->axes_quirk = false; + host_gamepad->mapping = nullptr; if (host_gamepad->ev_dev == NULL) { fprintf(stderr, "error: libevdev_new failed\n"); @@ -254,34 +405,22 @@ static void sl_internal_gaming_seat_gamepad_added_with_device_info( // We provide limited remapping at this time. Only moderately XBox360 // HID compatible controllers are likely to work well. - - if (product_id == kStadiaProduct && vendor_id == kStadiaVendor && - version == kStadiaVersion) { - host_gamepad->axes_quirk = true; - } else if (bus == ZCR_GAMING_SEAT_V2_BUS_TYPE_BLUETOOTH && - vendor_id == kSteelSeriesVendorBt && - (product_id == kSteelSeriesProductStratusDuoBt || - product_id == kSteelSeriesProductStratusPlusBt)) { - host_gamepad->axes_quirk = true; + auto it = kDeviceMappings.find(DeviceID{vendor_id, product_id, version}); + if (it != kDeviceMappings.end()) { + host_gamepad->mapping = it->second; } // Describe a common controller - libevdev_set_name(host_gamepad->ev_dev, kXboxName); - libevdev_set_id_bustype(host_gamepad->ev_dev, kUsbBus); - libevdev_set_id_vendor(host_gamepad->ev_dev, kXboxVendor); - libevdev_set_id_product(host_gamepad->ev_dev, kXboxProduct); - libevdev_set_id_version(host_gamepad->ev_dev, kXboxVersion); + Libevdev::Get()->set_name(host_gamepad->ev_dev, kXboxName); + Libevdev::Get()->set_id_bustype(host_gamepad->ev_dev, kUsbBus); + Libevdev::Get()->set_id_vendor(host_gamepad->ev_dev, kXboxVendor); + Libevdev::Get()->set_id_product(host_gamepad->ev_dev, kXboxProduct); + Libevdev::Get()->set_id_version(host_gamepad->ev_dev, kXboxVersion); // Enable common set of buttons - - // Note: Do not enable BTN_TL2 or BTN_TR2, as they will significantly - // change the Linux joydev interpretation of the triggers on ABS_Z/ABS_RZ. - int buttons[] = {BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, - BTN_TL, BTN_TR, BTN_THUMBL, BTN_THUMBR, - BTN_SELECT, BTN_START, BTN_MODE}; - - for (unsigned int i = 0; i < ARRAY_SIZE(buttons); i++) - libevdev_enable_event_code(host_gamepad->ev_dev, EV_KEY, buttons[i], NULL); + for (unsigned int i = 0; i < ARRAY_SIZE(kButtons); i++) + Libevdev::Get()->enable_event_code(host_gamepad->ev_dev, EV_KEY, + kButtons[i], NULL); } // NOLINT(whitespace/indent), lint bug b/173143790 // Note: not currently implemented by Exo. @@ -302,11 +441,9 @@ static const struct zcr_gaming_seat_v2_listener void sl_gaming_seat_add_listener(struct sl_context* ctx) { if (ctx->gaming_input_manager && ctx->gaming_input_manager->internal) { TRACE_EVENT("gaming", "sl_gaming_seat_add_listener"); - // TODO(kenalba): does gaming_seat need to persist in ctx? - struct zcr_gaming_seat_v2* gaming_seat = - zcr_gaming_input_v2_get_gaming_seat(ctx->gaming_input_manager->internal, - ctx->default_seat->proxy); - zcr_gaming_seat_v2_add_listener(gaming_seat, + ctx->gaming_seat = zcr_gaming_input_v2_get_gaming_seat( + ctx->gaming_input_manager->internal, ctx->default_seat->proxy); + zcr_gaming_seat_v2_add_listener(ctx->gaming_seat, &sl_internal_gaming_seat_listener, ctx); } } diff --git a/sommelier.cc b/sommelier.cc index 3f885db..6eb0b15 100644 --- a/sommelier.cc +++ b/sommelier.cc @@ -6,6 +6,9 @@ #include "sommelier-tracing.h" // NOLINT(build/include_directory) #include "sommelier-transform.h" // NOLINT(build/include_directory) #include "sommelier-xshape.h" // NOLINT(build/include_directory) +#ifdef GAMEPAD_SUPPORT +#include "libevdev/libevdev-shim.h" +#endif #include "xcb/xcb-shim.h" #include @@ -3712,6 +3715,10 @@ void create_shims() { set_xdg_toplevel_shim(new XdgToplevelShim()); set_xdg_surface_shim(new XdgSurfaceShim()); set_xdg_wm_base_shim(new XdgWmBaseShim()); + +#ifdef GAMEPAD_SUPPORT + Libevdev::Set(new LibevdevShim()); +#endif } int real_main(int argc, char** argv) { diff --git a/sommelier.h b/sommelier.h index 8bc34e3..2e8b4ad 100644 --- a/sommelier.h +++ b/sommelier.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -414,13 +415,19 @@ struct sl_sync_point { }; #ifdef GAMEPAD_SUPPORT +struct sl_host_gaming_seat { + struct sl_host_gaming_seat* gaming_seat; + struct wl_resource* resource; + struct wl_seat* proxy; +}; + struct sl_host_gamepad { struct sl_context* ctx; int state; struct libevdev* ev_dev; struct libevdev_uinput* uinput_dev; - bool axes_quirk; struct wl_list link; + const std::unordered_map* mapping; }; #endif diff --git a/testing/sommelier-test-util.h b/testing/sommelier-test-util.h index 7eec52c..58e500c 100644 --- a/testing/sommelier-test-util.h +++ b/testing/sommelier-test-util.h @@ -10,6 +10,7 @@ #include "../sommelier.h" // NOLINT(build/include_directory) #include "aura-shell-client-protocol.h" // NOLINT(build/include_directory) +#include "gaming-input-unstable-v2-client-protocol.h" // NOLINT(build/include_directory) #include "viewporter-client-protocol.h" // NOLINT(build/include_directory) #include "xdg-output-unstable-v1-client-protocol.h" // NOLINT(build/include_directory) #include "xdg-shell-client-protocol.h" // NOLINT(build/include_directory) @@ -33,6 +34,8 @@ MAP_STRUCT_TO_LISTENER(wl_surface*, wl_surface_listener); MAP_STRUCT_TO_LISTENER(zaura_output*, zaura_output_listener); MAP_STRUCT_TO_LISTENER(zaura_toplevel*, zaura_toplevel_listener); MAP_STRUCT_TO_LISTENER(zxdg_output_v1*, zxdg_output_v1_listener); +MAP_STRUCT_TO_LISTENER(zcr_gaming_seat_v2*, zcr_gaming_seat_v2_listener); +MAP_STRUCT_TO_LISTENER(zcr_gamepad_v2*, zcr_gamepad_v2_listener); namespace vm_tools { namespace sommelier { diff --git a/testing/wayland-test-base.h b/testing/wayland-test-base.h index 1ef4b95..d21c65b 100644 --- a/testing/wayland-test-base.h +++ b/testing/wayland-test-base.h @@ -11,6 +11,7 @@ #include "../sommelier.h" // NOLINT(build/include_directory) #include "aura-shell-client-protocol.h" // NOLINT(build/include_directory) +#include "gaming-input-unstable-v2-client-protocol.h" // NOLINT(build/include_directory) #include "mock-wayland-channel.h" // NOLINT(build/include_directory) #include "sommelier-test-util.h" // NOLINT(build/include_directory) #include "viewporter-client-protocol.h" // NOLINT(build/include_directory) @@ -84,6 +85,18 @@ class FakeWaylandClient { Flush(); } + // Bind to all wl_seats. + void BindToWlSeats(struct sl_context* ctx) { + struct sl_global* global; + wl_list_for_each(global, &ctx->globals, link) { + if (global->interface == &wl_seat_interface) { + wl_registry_bind(client_registry, global->name, global->interface, + WL_OUTPUT_DONE_SINCE_VERSION); + } + } + Flush(); + } + // Create a surface. struct wl_surface* CreateSurface() { struct wl_surface* surface = wl_compositor_create_surface(compositor); @@ -220,6 +233,10 @@ class WaylandTestBase : public ::testing::Test { ZAURA_TOPLEVEL_SET_WINDOW_BOUNDS_SINCE_VERSION); sl_registry_handler(&ctx, registry, next_server_id++, "wp_viewporter", WP_VIEWPORTER_DESTROY_SINCE_VERSION); + sl_registry_handler(&ctx, registry, next_server_id++, "zcr_gaming_input_v2", + ZCR_GAMING_INPUT_V2_GET_GAMING_SEAT_SINCE_VERSION); + sl_registry_handler(&ctx, registry, next_server_id++, "wl_seat", + WL_SEAT_RELEASE_SINCE_VERSION); } // Set up one or more fake outputs for the test.