Gamepad: Implement GamepadHapticActuator (#32046)

* Implement Servo side of GamepadHapticActuator

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Get build working

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Create effect handling on embedder side

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update tracing for GamepadHapticEffect

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update gilrs to point to commit with effect complete event

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Implement playing and preempting haptic effects

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update IDL to add trigger rumble

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update WPT expectations

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Handle stopping haptic effects from reset()

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* ./mach fmt, fix test-tidy issues

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Add extra validity checks for trigger rumble

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Retrieve supported haptic effects from embedder

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Fix test expectations

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Add missing spec link, pin gilrs commit

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* servoshell cargo formatting

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Fix Cargo.toml

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Additional comments, realm proof, naming

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* ./mach fmt

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Update gilrs rev to gilrs-core 0.5.12 release

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Implement sequence ids for gamepad haptic promises

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Take playing effect promise instead of cloning

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Implement listener for reset function

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Fix Cargo.lock

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Restructure IPC listeners, add comments, handle visibility change

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Check that haptic effect still exists before handling ff completion event

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Visibility steps, add InRealm bindings for promises

Signed-off-by: Daniel Adams <msub2official@gmail.com>

* Add Gamepad EmbedderMsg arms to egl servo_glue

Signed-off-by: Daniel Adams <msub2official@gmail.com>

---------

Signed-off-by: Daniel Adams <msub2official@gmail.com>
This commit is contained in:
Daniel Adams 2024-07-19 20:29:27 -10:00 committed by GitHub
parent 9212ed203a
commit 2c17de7fa7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 652 additions and 68 deletions

View file

@ -98,7 +98,7 @@ egui = { version = "0.28.1" }
egui_glow = { version = "0.28.1", features = ["winit"] }
egui-winit = { version = "0.28.1", default-features = false, features = ["clipboard", "wayland"] }
euclid = { workspace = true }
gilrs = "0.10.8"
gilrs = { git = "https://gitlab.com/gilrs-project/gilrs", rev = "eafb7f2ef488874188c5d75adce9aef486be9d4e" }
gleam = { workspace = true }
glow = "0.13.1"
keyboard-types = { workspace = true }

View file

@ -176,6 +176,8 @@ mod from_servo {
Self::OnDevtoolsStarted(..) => target!("OnDevtoolsStarted"),
Self::ReadyToPresent(..) => target!("ReadyToPresent"),
Self::EventDelivered(..) => target!("EventDelivered"),
Self::PlayGamepadHapticEffect(..) => target!("PlayGamepadHapticEffect"),
Self::StopGamepadHapticEffect(..) => target!("StopGamepadHapticEffect"),
}
}
}

View file

@ -12,18 +12,21 @@ use std::{env, thread};
use arboard::Clipboard;
use euclid::{Point2D, Vector2D};
use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Replay, Ticks};
use gilrs::{EventType, Gilrs};
use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher};
use log::{debug, error, info, trace, warn};
use servo::base::id::TopLevelBrowsingContextId as WebViewId;
use servo::compositing::windowing::{EmbedderEvent, WebRenderDebugOption};
use servo::embedder_traits::{
CompositorEventVariant, ContextMenuResult, EmbedderMsg, FilterPattern, PermissionPrompt,
PermissionRequest, PromptDefinition, PromptOrigin, PromptResult,
CompositorEventVariant, ContextMenuResult, DualRumbleEffectParams, EmbedderMsg, FilterPattern,
GamepadHapticEffectType, PermissionPrompt, PermissionRequest, PromptDefinition, PromptOrigin,
PromptResult,
};
use servo::ipc_channel::ipc::IpcSender;
use servo::script_traits::{
GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadUpdateType, TouchEventType,
TraversalDirection,
GamepadEvent, GamepadIndex, GamepadInputBounds, GamepadSupportedHapticEffects,
GamepadUpdateType, TouchEventType, TraversalDirection,
};
use servo::servo_config::opts;
use servo::servo_url::ServoUrl;
@ -59,6 +62,7 @@ pub struct WebViewManager<Window: WindowPortsMethods + ?Sized> {
event_queue: Vec<EmbedderEvent>,
clipboard: Option<Clipboard>,
gamepad: Option<Gilrs>,
haptic_effects: HashMap<usize, HapticEffect>,
shutdown_requested: bool,
load_status: LoadStatus,
}
@ -80,6 +84,11 @@ pub enum LoadStatus {
LoadComplete,
}
pub struct HapticEffect {
pub effect: Effect,
pub sender: IpcSender<bool>,
}
impl<Window> WebViewManager<Window>
where
Window: WindowPortsMethods + ?Sized,
@ -108,6 +117,8 @@ where
None
},
},
haptic_effects: HashMap::default(),
event_queue: Vec::new(),
shutdown_requested: false,
load_status: LoadStatus::LoadComplete,
@ -218,11 +229,32 @@ where
axis_bounds: (-1.0, 1.0),
button_bounds: (0.0, 1.0),
};
gamepad_event = Some(GamepadEvent::Connected(index, name, bounds));
// GilRs does not yet support trigger rumble
let supported_haptic_effects = GamepadSupportedHapticEffects {
supports_dual_rumble: true,
supports_trigger_rumble: false,
};
gamepad_event = Some(GamepadEvent::Connected(
index,
name,
bounds,
supported_haptic_effects,
));
},
EventType::Disconnected => {
gamepad_event = Some(GamepadEvent::Disconnected(index));
},
EventType::ForceFeedbackEffectCompleted => {
let Some(effect) = self.haptic_effects.get(&event.id.into()) else {
warn!("Failed to find haptic effect for id {}", event.id);
return;
};
effect
.sender
.send(true)
.expect("Failed to send haptic effect completion.");
self.haptic_effects.remove(&event.id.into());
},
_ => {},
}
@ -258,6 +290,79 @@ where
}
}
fn play_haptic_effect(
&mut self,
index: usize,
params: DualRumbleEffectParams,
effect_complete_sender: IpcSender<bool>,
) {
let Some(ref mut gilrs) = self.gamepad else {
debug!("Unable to get gilrs instance!");
return;
};
if let Some(connected_gamepad) = gilrs
.gamepads()
.find(|gamepad| usize::from(gamepad.0) == index)
{
let start_delay = Ticks::from_ms(params.start_delay as u32);
let duration = Ticks::from_ms(params.duration as u32);
let strong_magnitude = (params.strong_magnitude * u16::MAX as f64).round() as u16;
let weak_magnitude = (params.weak_magnitude * u16::MAX as f64).round() as u16;
let scheduling = Replay {
after: start_delay,
play_for: duration,
with_delay: Ticks::from_ms(0),
};
let effect = EffectBuilder::new()
.add_effect(BaseEffect {
kind: BaseEffectType::Strong { magnitude: strong_magnitude },
scheduling,
envelope: Default::default(),
})
.add_effect(BaseEffect {
kind: BaseEffectType::Weak { magnitude: weak_magnitude },
scheduling,
envelope: Default::default(),
})
.repeat(Repeat::For(start_delay + duration))
.add_gamepad(&connected_gamepad.1)
.finish(gilrs)
.expect("Failed to create haptic effect, ensure connected gamepad supports force feedback.");
self.haptic_effects.insert(
index,
HapticEffect {
effect,
sender: effect_complete_sender,
},
);
self.haptic_effects[&index]
.effect
.play()
.expect("Failed to play haptic effect.");
} else {
debug!("Couldn't find connected gamepad to play haptic effect on");
}
}
fn stop_haptic_effect(&mut self, index: usize) -> bool {
let Some(haptic_effect) = self.haptic_effects.get(&index) else {
return false;
};
let stopped_successfully = match haptic_effect.effect.stop() {
Ok(()) => true,
Err(e) => {
debug!("Failed to stop haptic effect: {:?}", e);
false
},
};
self.haptic_effects.remove(&index);
stopped_successfully
}
pub fn shutdown_requested(&self) -> bool {
self.shutdown_requested
}
@ -744,6 +849,19 @@ where
.push(EmbedderEvent::FocusWebView(webview_id));
}
},
EmbedderMsg::PlayGamepadHapticEffect(index, effect, effect_complete_sender) => {
match effect {
GamepadHapticEffectType::DualRumble(params) => {
self.play_haptic_effect(index, params, effect_complete_sender);
},
}
},
EmbedderMsg::StopGamepadHapticEffect(index, haptic_stop_sender) => {
let stopped_successfully = self.stop_haptic_effect(index);
haptic_stop_sender
.send(stopped_successfully)
.expect("Failed to send haptic stop result");
},
}
}

View file

@ -623,7 +623,9 @@ impl ServoGlue {
EmbedderMsg::HeadParsed |
EmbedderMsg::SetFullscreenState(..) |
EmbedderMsg::ReportProfile(..) |
EmbedderMsg::EventDelivered(..) => {},
EmbedderMsg::EventDelivered(..) |
EmbedderMsg::PlayGamepadHapticEffect(..) |
EmbedderMsg::StopGamepadHapticEffect(..) => {},
}
}