From 1fa16c35529e478ebfdb90fff8b1e003896f69c8 Mon Sep 17 00:00:00 2001 From: Colin Marc Date: Fri, 13 Sep 2024 12:01:31 +0200 Subject: [PATCH] feat(server): add support for wp_game_controller_v1 --- mm-server/src/compositor.rs | 28 ++- mm-server/src/compositor/control.rs | 22 ++- mm-server/src/compositor/dispatch.rs | 1 + .../compositor/dispatch/wp_game_controller.rs | 37 ++++ mm-server/src/compositor/protocols.rs | 1 + .../protocols/game-controller-v1.xml | 161 ++++++++++++++++++ .../protocols/wp_game_controller.rs | 20 +++ mm-server/src/compositor/seat.rs | 80 ++++++++- mm-server/src/server/handlers.rs | 111 +++++++++++- 9 files changed, 447 insertions(+), 14 deletions(-) create mode 100644 mm-server/src/compositor/dispatch/wp_game_controller.rs create mode 100644 mm-server/src/compositor/protocols/game-controller-v1.xml create mode 100644 mm-server/src/compositor/protocols/wp_game_controller.rs diff --git a/mm-server/src/compositor.rs b/mm-server/src/compositor.rs index e7b827f..85ec38f 100644 --- a/mm-server/src/compositor.rs +++ b/mm-server/src/compositor.rs @@ -192,6 +192,7 @@ impl Compositor { create_global::(&dh, 1); create_global::(&dh, 1); create_global::(&dh, 1); + create_global::(&dh, 1); create_global::(&dh, 1); create_global::(&dh, 5); @@ -817,7 +818,7 @@ impl Compositor { self.state.new_display_params = Some(params); } ControlMessage::KeyboardInput { - evdev_scancode, + key_code: evdev_scancode, char, state, } => { @@ -927,6 +928,26 @@ impl Compositor { ControlMessage::PointerLeft => { self.state.default_seat.lift_pointer(&self.state.serial); } + // TODO: for now, we always assume a controller is plugged in. + // ControlMessage::GamepadAvailable(_) => (), + // ControlMessage::GamepadUnavailable(_) => (), + ControlMessage::GamepadAxis { + axis_code, value, .. + } => { + self.state.default_seat.gamepad_axis(axis_code, value); + } + ControlMessage::GamepadTrigger { + trigger_code, + value, + .. + } => { + self.state.default_seat.gamepad_trigger(trigger_code, value); + } + ControlMessage::GamepadInput { + button_code, state, .. + } => { + self.state.default_seat.gamepad_input(button_code, state); + } // Handled above. ControlMessage::Stop | ControlMessage::Attach { .. } => unreachable!(), } @@ -938,10 +959,11 @@ impl Compositor { fn create_global( dh: &wayland_server::DisplayHandle, version: u32, -) where +) -> wayland_server::backend::GlobalId +where State: wayland_server::GlobalDispatch, { - let _ = dh.create_global::(version, ()); + dh.create_global::(version, ()) } fn gen_socket_name() -> OsString { diff --git a/mm-server/src/compositor/control.rs b/mm-server/src/compositor/control.rs index f7caef3..214ac98 100644 --- a/mm-server/src/compositor/control.rs +++ b/mm-server/src/compositor/control.rs @@ -7,6 +7,7 @@ use crossbeam_channel::Sender; use crate::{ codec::{AudioCodec, VideoCodec}, color::VideoProfile, + compositor::ButtonState, pixel_scale::PixelScale, }; @@ -47,7 +48,7 @@ pub enum ControlMessage { Detach(u64), UpdateDisplayParams(DisplayParams), KeyboardInput { - evdev_scancode: u32, + key_code: u32, state: super::KeyState, char: Option, }, @@ -59,10 +60,27 @@ pub enum ControlMessage { x: f64, y: f64, button_code: u32, - state: super::ButtonState, + state: ButtonState, }, PointerAxis(f64, f64), PointerAxisDiscrete(f64, f64), + // GamepadAvailable(u64), + // GamepadUnavailable(u64), + GamepadAxis { + _id: u64, + axis_code: u32, + value: f64, + }, + GamepadTrigger { + _id: u64, + trigger_code: u32, + value: f64, + }, + GamepadInput { + _id: u64, + button_code: u32, + state: ButtonState, + }, } #[derive(Debug, Clone)] diff --git a/mm-server/src/compositor/dispatch.rs b/mm-server/src/compositor/dispatch.rs index 1071921..de0770e 100644 --- a/mm-server/src/compositor/dispatch.rs +++ b/mm-server/src/compositor/dispatch.rs @@ -8,6 +8,7 @@ mod wl_drm; mod wl_output; mod wl_seat; mod wl_shm; +mod wp_game_controller; mod wp_linux_dmabuf; mod wp_pointer_constraints; mod wp_presentation; diff --git a/mm-server/src/compositor/dispatch/wp_game_controller.rs b/mm-server/src/compositor/dispatch/wp_game_controller.rs new file mode 100644 index 0000000..595092c --- /dev/null +++ b/mm-server/src/compositor/dispatch/wp_game_controller.rs @@ -0,0 +1,37 @@ +// Copyright 2024 Colin Marc +// +// SPDX-License-Identifier: BUSL-1.1 + +use crate::compositor::{wp_game_controller::wp_game_controller_v1, State}; + +impl wayland_server::GlobalDispatch for State { + fn bind( + state: &mut Self, + _handle: &wayland_server::DisplayHandle, + _client: &wayland_server::Client, + resource: wayland_server::New, + _global_data: &(), + data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + let global = data_init.init(resource, ()); + state.default_seat.add_gamepad(global); + } +} + +impl wayland_server::Dispatch for State { + fn request( + state: &mut Self, + _client: &wayland_server::Client, + resource: &wp_game_controller_v1::WpGameControllerV1, + request: wp_game_controller_v1::Request, + _data: &(), + _dhandle: &wayland_server::DisplayHandle, + _data_init: &mut wayland_server::DataInit<'_, Self>, + ) { + match request { + wp_game_controller_v1::Request::Destroy => { + state.default_seat.destroy_gamepad(resource); + } + } + } +} diff --git a/mm-server/src/compositor/protocols.rs b/mm-server/src/compositor/protocols.rs index 6917ae6..5727e65 100644 --- a/mm-server/src/compositor/protocols.rs +++ b/mm-server/src/compositor/protocols.rs @@ -3,3 +3,4 @@ // SPDX-License-Identifier: BUSL-1.1 pub mod wl_drm; +pub mod wp_game_controller; diff --git a/mm-server/src/compositor/protocols/game-controller-v1.xml b/mm-server/src/compositor/protocols/game-controller-v1.xml new file mode 100644 index 0000000..69f8844 --- /dev/null +++ b/mm-server/src/compositor/protocols/game-controller-v1.xml @@ -0,0 +1,161 @@ + + + + Copyright © 2023-2024 Collabora, Ltd. + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + TODO high level documentation about game controllers and the input source + + + + + This interface represents a physical game controller, also known as a + gamepad, held by the user in their hands. + + The wp_game_controller_v1 interface generates events for every input + sources present on the game controller. The client can also request + haptic vibration, or rumble, if the hardware supports it. + + + + + Destroy the game controller device. + + + + + + User friendly device identification string. The game controller name + is a UTF-8 string that should not be NULL. + + + + + + + Each vendor uses a different set of glyph on its controller's buttons. + + + + + + + + + + Theme the client should follow to render the adequate glyph for matching + the button on the physical controller. + + + + + + + This event is sent after all of the other properties event have been + sent. No more property events will be sent to the client after this. + + + + + + Notification that the gamepad is focused on a given surface. + + + + + + + + Notification that the gamepad is no longer focused on the given surface. + + + + + + + + Describes the physical state of a button on the game controller. + + + + + + + + game controller button click and release notification. + + The source is a button code as defined in the Linux kernel's + linux/input-event-codes.h header file. + + The time argument is a timestamp in microseconds of the moment of the + state changes + + + + + + + + + game controller trigger pressure notification, expressed with a value + varying between 0.0 and 1.0, with 0.0 being the neutral state. + + The source is a button code as defined in the Linux kernel's + linux/input-event-codes.h header file. + + The time argument is a timestamp in microseconds of the moment of the + state changes + + + + + + + + + game controller axis position notification, expressed by a 2D + position varying from -1.0 to 1.0, with 0.0 being the neutral state. + + The source is a button code as defined in the Linux kernel's + linux/input-event-codes.h header file. + + The time argument is a timestamp in microseconds of the moment of the + state changes + + + + + + + + + Indicates the end of a set of events that logically belong together. + A client is expected to accumulate the data in all events within the + frame before proceeding + + All wp_game_controller_v1 events before a wp_game_controller_v1.frame + event belong together. + + + + diff --git a/mm-server/src/compositor/protocols/wp_game_controller.rs b/mm-server/src/compositor/protocols/wp_game_controller.rs new file mode 100644 index 0000000..e85a5c5 --- /dev/null +++ b/mm-server/src/compositor/protocols/wp_game_controller.rs @@ -0,0 +1,20 @@ +// Copyright 2024 Colin Marc +// +// SPDX-License-Identifier: BUSL-1.1 + +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] + +use wayland_server; +use wayland_server::protocol::*; + +pub mod __interfaces { + use wayland_server::backend as wayland_backend; + use wayland_server::protocol::__interfaces::*; + wayland_scanner::generate_interfaces!("src/compositor/protocols/game-controller-v1.xml"); +} + +use self::__interfaces::*; +wayland_scanner::generate_server_code!("src/compositor/protocols/game-controller-v1.xml"); + +pub use wp_game_controller_v1::*; diff --git a/mm-server/src/compositor/seat.rs b/mm-server/src/compositor/seat.rs index a0baea1..2c4ca59 100644 --- a/mm-server/src/compositor/seat.rs +++ b/mm-server/src/compositor/seat.rs @@ -13,12 +13,13 @@ use wayland_protocols::wp::{ }; use wayland_server::{ protocol::{wl_keyboard, wl_pointer, wl_surface}, - Resource as _, + Resource, }; use crate::compositor::{ buffers::BufferBacking, oneshot_render::shm_to_png, + protocols::wp_game_controller, sealed::SealedFile, serial::Serial, surface::{surface_vector_to_buffer, SurfaceKey, SurfaceRole}, @@ -91,6 +92,8 @@ pub struct Seat { pointer_lock: Option<(wl_surface::WlSurface, PointerLock)>, cursor: Cursor, + + gamepads: HashSet, } impl Default for Seat { @@ -116,6 +119,8 @@ impl Default for Seat { pointer_lock: None, cursor: Cursor::default(), + + gamepads: HashSet::default(), } } } @@ -396,6 +401,14 @@ impl Seat { wl_keyboard.leave(serial.next(), &old_surf); } + for wp_game_controller in self + .gamepads + .iter() + .filter(|ti| ti.id().same_client_as(&old_surf.id())) + { + wp_game_controller.leave(serial.next(), &old_surf); + } + for wp_text_input in self .text_inputs .iter() @@ -417,6 +430,14 @@ impl Seat { wl_keyboard.modifiers(serial.next(), 0, 0, 0, 0); } + for wp_game_controller in self + .gamepads + .iter() + .filter(|ti| ti.id().same_client_as(&new_surf.id())) + { + wp_game_controller.enter(serial.next(), new_surf); + } + for wp_text_input in self .text_inputs .iter() @@ -556,6 +577,63 @@ impl Seat { _ => (), } } + + pub fn add_gamepad(&mut self, global: wp_game_controller::WpGameControllerV1) { + global.name("Generic Remote Controller".to_string()); // TODO: determine from layout + global.done(); + + self.gamepads.insert(global); + } + + pub fn destroy_gamepad(&mut self, global: &wp_game_controller::WpGameControllerV1) { + self.gamepads.remove(global); + } + + pub fn focused_gamepads( + &self, + ) -> impl Iterator { + let client_id = self + .keyboard_focus + .as_ref() + .and_then(|focus| focus.client()) + .map(|c| c.id()); + + self.gamepads + .iter() + .filter(move |k| k.is_alive() && k.client().map(|c| c.id()) == client_id) + } + + pub fn gamepad_axis(&self, scancode: u32, value: f64) { + let ts = EPOCH.elapsed().as_micros() as u32; + + for wp_game_controller in self.focused_gamepads() { + wp_game_controller.axis(scancode, value, ts); + wp_game_controller.frame(); + } + } + + pub fn gamepad_trigger(&self, scancode: u32, value: f64) { + let ts = EPOCH.elapsed().as_micros() as u32; + + for wp_game_controller in self.focused_gamepads() { + wp_game_controller.trigger(scancode, value, ts); + wp_game_controller.frame(); + } + } + + pub fn gamepad_input(&self, scancode: u32, state: ButtonState) { + let ts = EPOCH.elapsed().as_micros() as u32; + + let state = match state { + ButtonState::Pressed => wp_game_controller::ButtonState::Pressed, + ButtonState::Released => wp_game_controller::ButtonState::Released, + }; + + for wp_game_controller in self.focused_gamepads() { + wp_game_controller.button(scancode, state, ts); + wp_game_controller.frame(); + } + } } impl State { diff --git a/mm-server/src/server/handlers.rs b/mm-server/src/server/handlers.rs index 41c7bee..2f380e9 100644 --- a/mm-server/src/server/handlers.rs +++ b/mm-server/src/server/handlers.rs @@ -400,7 +400,7 @@ fn attach( Ok(KeyState::Repeat) => compositor::KeyState::Repeat, }; - let evdev_scancode = match protocol::keyboard_input::Key::try_from(ev.key).map(key_to_evdev) { + let key_code = match protocol::keyboard_input::Key::try_from(ev.key).map(key_to_evdev) { Ok(Some(scancode)) => scancode, _ => { send_err(outgoing, ErrorCode::ErrorProtocol, Some("invalid key".to_string())); @@ -419,10 +419,10 @@ fn attach( } }; - trace!(evdev_scancode, ?state, ?ch, "translated keyboard event"); + trace!(key_code, ?state, ?ch, "translated keyboard event"); handle.control.send(ControlMessage::KeyboardInput{ - evdev_scancode, + key_code, state, char: ch, }).ok(); @@ -491,11 +491,62 @@ fn attach( } } } - // Gamepads are not yet supported. - protocol::MessageType::GamepadAvailable(ev) => debug!("{:?}", ev), - protocol::MessageType::GamepadUnavailable(ev) => debug!("{:?}", ev), - protocol::MessageType::GamepadMotion(ev) => debug!("{:?}", ev), - protocol::MessageType::GamepadInput(ev) => debug!("{:?}", ev), + protocol::MessageType::GamepadAvailable(_) => { + // handle.control.send(ControlMessage::GamepadAvailable(ev.id)).ok(); + } + protocol::MessageType::GamepadUnavailable(_) => { + // handle.control.send(ControlMessage::GamepadUnavailable(ev.id)).ok(); + } + protocol::MessageType::GamepadMotion(ev) => { + let (scancode, is_trigger) = match protocol::gamepad_motion::GamepadAxis::try_from(ev.axis).ok().and_then(axis_to_evdev) { + Some(v) => v, + _ => { + send_err(outgoing, ErrorCode::ErrorProtocol, Some("invalid gamepad axis".to_string())); + return; + } + }; + + let cm = if is_trigger { + ControlMessage::GamepadTrigger { + _id: ev.gamepad_id, + trigger_code: scancode, + value: ev.value, + } + } else { + ControlMessage::GamepadAxis { + _id: ev.gamepad_id, + axis_code: scancode, + value: ev.value, + } + }; + + handle.control.send(cm).ok(); + }, + protocol::MessageType::GamepadInput(ev) => { + use protocol::gamepad_input::{GamepadButton, GamepadButtonState}; + let state = match ev.state.try_into() { + Ok(GamepadButtonState::Unknown) | Err(_) => { + send_err(outgoing, ErrorCode::ErrorProtocol, Some("invalid gamepad button state".to_string())); + return; + } + Ok(GamepadButtonState::Pressed) => compositor::ButtonState::Pressed, + Ok(GamepadButtonState::Released) => compositor::ButtonState::Released, + }; + + let scancode = match GamepadButton::try_from(ev.button).ok().and_then(gamepad_button_to_evdev) { + Some(v) => v, + _ => { + send_err(outgoing, ErrorCode::ErrorProtocol, Some("invalid gamepad button".to_string())); + return; + } + }; + + handle.control.send(ControlMessage::GamepadInput { + _id: ev.gamepad_id, + button_code: scancode, + state, + }).ok(); + } protocol::MessageType::Error(ev) => { error!("received error from client: {}: {}", ev.err_code().as_str_name(), ev.error_text); } @@ -826,6 +877,50 @@ fn key_to_evdev(key: protocol::keyboard_input::Key) -> Option { } } +fn axis_to_evdev(axis: protocol::gamepad_motion::GamepadAxis) -> Option<(u32, bool)> { + use protocol::gamepad_motion::GamepadAxis; + match axis { + GamepadAxis::LeftX => Some((0x00, false)), // ABS_X + GamepadAxis::LeftY => Some((0x01, false)), // ABS_Y + GamepadAxis::RightX => Some((0x03, false)), // ABS_RX + GamepadAxis::RightY => Some((0x04, false)), // ABS_RY, + GamepadAxis::LeftTrigger => Some((0x02, true)), // ABS_Z + GamepadAxis::RightTrigger => Some((0x05, true)), // ABS_RZ + GamepadAxis::Unknown => None, + } +} + +fn gamepad_button_to_evdev(button: protocol::gamepad_input::GamepadButton) -> Option { + use protocol::gamepad_input::GamepadButton; + + // TODO: My Dualsense actually reports Dpad events as an axis (ABS_HAT0X). + // Otherwise, this simulates a Sony controller. + + match button { + GamepadButton::DpadLeft => Some(0x222), // BTN_DPAD_LEFT + GamepadButton::DpadRight => Some(0x223), // BTN_DPAD_RIGHT + GamepadButton::DpadUp => Some(0x220), // BTN_DPAD_UP + GamepadButton::DpadDown => Some(0x221), // BTN_DPAD_DOWN + GamepadButton::South => Some(0x130), // BTN_SOUTH + GamepadButton::East => Some(0x131), // BTN_EAST + GamepadButton::North => Some(0x133), // BTN_NORTH + GamepadButton::West => Some(0x134), // BTN_WEST + GamepadButton::C => Some(0x132), // BTN_C + GamepadButton::Z => Some(0x135), // BTN_Z + GamepadButton::ShoulderLeft => Some(0x136), // BTN_TL + GamepadButton::ShoulderRight => Some(0x137), // BTN_TR + GamepadButton::JoystickLeft => Some(0x13d), // BTN_THUMBL + GamepadButton::JoystickRight => Some(0x13e), // BTN_THUMBR + GamepadButton::Start => Some(0x13b), // BTN_START + GamepadButton::Select => Some(0x13a), // BTN_SELECT + GamepadButton::Logo => Some(0x13c), // BTN_MODE + GamepadButton::Share => None, // TODO I'm not sure what code to use. + GamepadButton::TriggerLeft => Some(0x138), // BTN_TL2 + GamepadButton::TriggerRight => Some(0x139), // BTN_TL3 + GamepadButton::Unknown => None, + } +} + fn cursor_icon_to_proto(icon: cursor_icon::CursorIcon) -> protocol::update_cursor::CursorIcon { use protocol::update_cursor::CursorIcon;