diff options
Diffstat (limited to 'components/compositing')
-rw-r--r-- | components/compositing/Cargo.toml | 1 | ||||
-rw-r--r-- | components/compositing/compositor.rs | 185 | ||||
-rw-r--r-- | components/compositing/lib.rs | 10 | ||||
-rw-r--r-- | components/compositing/refresh_driver.rs | 234 | ||||
-rw-r--r-- | components/compositing/tracing.rs | 1 | ||||
-rw-r--r-- | components/compositing/webview_renderer.rs | 168 |
6 files changed, 461 insertions, 138 deletions
diff --git a/components/compositing/Cargo.toml b/components/compositing/Cargo.toml index 9b4ff54d29e..084bb54a5fc 100644 --- a/components/compositing/Cargo.toml +++ b/components/compositing/Cargo.toml @@ -38,6 +38,7 @@ servo_allocator = { path = "../allocator" } servo_config = { path = "../config" } servo_geometry = { path = "../geometry" } stylo_traits = { workspace = true } +timers = { path = "../timers" } tracing = { workspace = true, optional = true } webrender = { workspace = true } webrender_api = { workspace = true } diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 27d1e8f93e1..a4afeb01b3c 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -9,7 +9,7 @@ use std::fs::create_dir_all; use std::iter::once; use std::rc::Rc; use std::sync::Arc; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::time::{SystemTime, UNIX_EPOCH}; use base::cross_process_instant::CrossProcessInstant; use base::id::{PipelineId, WebViewId}; @@ -53,6 +53,7 @@ use webrender_api::{ }; use crate::InitialCompositorState; +use crate::refresh_driver::RefreshDriver; use crate::webview_manager::WebViewManager; use crate::webview_renderer::{PinchZoomResult, UnknownWebView, WebViewRenderer}; @@ -86,6 +87,9 @@ pub enum WebRenderDebugOption { } /// Data that is shared by all WebView renderers. pub struct ServoRenderer { + /// The [`RefreshDriver`] which manages the rythym of painting. + refresh_driver: RefreshDriver, + /// This is a temporary map between [`PipelineId`]s and their associated [`WebViewId`]. Once /// all renderer operations become per-`WebView` this map can be removed, but we still sometimes /// need to work backwards to figure out what `WebView` is associated with a `Pipeline`. @@ -151,18 +155,14 @@ pub struct IOCompositor { /// The number of frames pending to receive from WebRender. pending_frames: usize, - /// The [`Instant`] of the last animation tick, used to avoid flooding the Constellation and - /// ScriptThread with a deluge of animation ticks. - last_animation_tick: Instant, - /// A handle to the memory profiler which will automatically unregister /// when it's dropped. _mem_profiler_registration: ProfilerRegistration, } /// Why we need to be repainted. This is used for debugging. -#[derive(Clone, Copy, Default)] -struct RepaintReason(u8); +#[derive(Clone, Copy, Default, PartialEq)] +pub(crate) struct RepaintReason(u8); bitflags! { impl RepaintReason: u8 { @@ -281,6 +281,11 @@ impl PipelineDetails { } } +pub enum HitTestError { + EpochMismatch, + Others, +} + impl ServoRenderer { pub fn shutdown_state(&self) -> ShutdownState { self.shutdown_state.get() @@ -290,15 +295,19 @@ impl ServoRenderer { &self, point: DevicePoint, details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>, - ) -> Option<CompositorHitTestResult> { - self.hit_test_at_point_with_flags_and_pipeline( + ) -> Result<CompositorHitTestResult, HitTestError> { + match self.hit_test_at_point_with_flags_and_pipeline( point, HitTestFlags::empty(), None, details_for_pipeline, - ) - .first() - .cloned() + ) { + Ok(hit_test_results) => hit_test_results + .first() + .cloned() + .ok_or(HitTestError::Others), + Err(error) => Err(error), + } } // TODO: split this into first half (global) and second half (one for whole compositor, one for webview) @@ -308,14 +317,15 @@ impl ServoRenderer { flags: HitTestFlags, pipeline_id: Option<WebRenderPipelineId>, details_for_pipeline: impl Fn(PipelineId) -> Option<&'a PipelineDetails>, - ) -> Vec<CompositorHitTestResult> { + ) -> Result<Vec<CompositorHitTestResult>, HitTestError> { // DevicePoint and WorldPoint are the same for us. let world_point = WorldPoint::from_untyped(point.to_untyped()); let results = self.webrender_api .hit_test(self.webrender_document, pipeline_id, world_point, flags); - results + let mut epoch_mismatch = false; + let results = results .items .iter() .filter_map(|item| { @@ -323,10 +333,16 @@ impl ServoRenderer { let details = details_for_pipeline(pipeline_id)?; // If the epoch in the tag does not match the current epoch of the pipeline, - // then the hit test is against an old version of the display list and we - // should ignore this hit test for now. + // then the hit test is against an old version of the display list. match details.most_recent_display_list_epoch { - Some(epoch) if epoch.as_u16() == item.tag.1 => {}, + Some(epoch) => { + if epoch.as_u16() != item.tag.1 { + // It's too early to hit test for now. + // New scene building is in progress. + epoch_mismatch = true; + return None; + } + }, _ => return None, } @@ -340,7 +356,13 @@ impl ServoRenderer { scroll_tree_node: info.scroll_tree_node, }) }) - .collect() + .collect(); + + if epoch_mismatch { + return Err(HitTestError::EpochMismatch); + } + + Ok(results) } pub(crate) fn send_transaction(&mut self, transaction: Transaction) { @@ -386,6 +408,10 @@ impl IOCompositor { ); let compositor = IOCompositor { global: Rc::new(RefCell::new(ServoRenderer { + refresh_driver: RefreshDriver::new( + state.constellation_chan.clone(), + state.event_loop_waker, + ), shutdown_state: state.shutdown_state, pipeline_to_webview_map: Default::default(), compositor_receiver: state.receiver, @@ -406,7 +432,6 @@ impl IOCompositor { webrender: Some(state.webrender), rendering_context: state.rendering_context, pending_frames: 0, - last_animation_tick: Instant::now(), _mem_profiler_registration: registration, }; @@ -450,7 +475,16 @@ impl IOCompositor { } pub fn needs_repaint(&self) -> bool { - !self.needs_repaint.get().is_empty() + let repaint_reason = self.needs_repaint.get(); + if repaint_reason.is_empty() { + return false; + } + + !self + .global + .borrow() + .refresh_driver + .wait_to_paint(repaint_reason) } pub fn finish_shutting_down(&mut self) { @@ -519,15 +553,17 @@ impl IOCompositor { pipeline_id, animation_state, ) => { - if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) { - if webview_renderer - .change_pipeline_running_animations_state(pipeline_id, animation_state) && - webview_renderer.animating() - { - // These operations should eventually happen per-WebView, but they are - // global now as rendering is still global to all WebViews. - self.process_animations(true); - } + let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { + return; + }; + + if webview_renderer + .change_pipeline_running_animations_state(pipeline_id, animation_state) + { + self.global + .borrow() + .refresh_driver + .notify_animation_state_changed(webview_renderer); } }, @@ -572,14 +608,15 @@ impl IOCompositor { }, CompositorMsg::SetThrottled(webview_id, pipeline_id, throttled) => { - if let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) { - if webview_renderer.set_throttled(pipeline_id, throttled) && - webview_renderer.animating() - { - // These operations should eventually happen per-WebView, but they are - // global now as rendering is still global to all WebViews. - self.process_animations(true); - } + let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { + return; + }; + + if webview_renderer.set_throttled(pipeline_id, throttled) { + self.global + .borrow() + .refresh_driver + .notify_animation_state_changed(webview_renderer); } }, @@ -604,7 +641,7 @@ impl IOCompositor { .global .borrow() .hit_test_at_point(point, details_for_pipeline); - if let Some(result) = result { + if let Ok(result) = result { self.global.borrow_mut().update_cursor(point, &result); } } @@ -634,7 +671,7 @@ impl IOCompositor { }; let dppx = webview_renderer.device_pixels_per_page_pixel(); let point = dppx.transform_point(Point2D::new(x, y)); - webview_renderer.dispatch_input_event( + webview_renderer.dispatch_point_input_event( InputEvent::MouseButton(MouseButtonEvent::new(action, button, point)) .with_webdriver_message_id(Some(message_id)), ); @@ -647,7 +684,7 @@ impl IOCompositor { }; let dppx = webview_renderer.device_pixels_per_page_pixel(); let point = dppx.transform_point(Point2D::new(x, y)); - webview_renderer.dispatch_input_event( + webview_renderer.dispatch_point_input_event( InputEvent::MouseMove(MouseMoveEvent::new(point)) .with_webdriver_message_id(Some(message_id)), ); @@ -669,7 +706,7 @@ impl IOCompositor { let scroll_delta = dppx.transform_vector(Vector2D::new(delta_x as f32, delta_y as f32)); webview_renderer - .dispatch_input_event(InputEvent::Wheel(WheelEvent { delta, point })); + .dispatch_point_input_event(InputEvent::Wheel(WheelEvent { delta, point })); webview_renderer.on_webdriver_wheel_action(scroll_delta, point); }, @@ -777,6 +814,8 @@ impl IOCompositor { let Some(webview_renderer) = self.webview_renderers.get_mut(webview_id) else { return warn!("Could not find WebView for incoming display list"); }; + // WebRender is not ready until we receive "NewWebRenderFrameReady" + webview_renderer.webrender_frame_ready.set(false); let pipeline_id = display_list_info.pipeline_id; let details = webview_renderer.ensure_pipeline_details(pipeline_id.into()); @@ -826,7 +865,8 @@ impl IOCompositor { flags, pipeline, details_for_pipeline, - ); + ) + .unwrap_or_default(); let _ = sender.send(result); }, @@ -933,6 +973,11 @@ impl IOCompositor { warn!("Sending response to get screen size failed ({error:?})."); } }, + CompositorMsg::Viewport(webview_id, viewport_description) => { + if let Some(webview) = self.webview_renderers.get_mut(webview_id) { + webview.set_viewport_description(viewport_description); + } + }, } } @@ -1271,39 +1316,6 @@ impl IOCompositor { self.set_needs_repaint(RepaintReason::Resize); } - /// If there are any animations running, dispatches appropriate messages to the constellation. - fn process_animations(&mut self, force: bool) { - // When running animations in order to dump a screenshot (not after a full composite), don't send - // animation ticks faster than about 60Hz. - // - // TODO: This should be based on the refresh rate of the screen and also apply to all - // animation ticks, not just ones sent while waiting to dump screenshots. This requires - // something like a refresh driver concept though. - if !force && (Instant::now() - self.last_animation_tick) < Duration::from_millis(16) { - return; - } - self.last_animation_tick = Instant::now(); - - let animating_webviews: Vec<_> = self - .webview_renderers - .iter() - .filter_map(|webview_renderer| { - if webview_renderer.animating() { - Some(webview_renderer.id) - } else { - None - } - }) - .collect(); - if !animating_webviews.is_empty() { - if let Err(error) = self.global.borrow().constellation_sender.send( - EmbedderToConstellationMessage::TickAnimation(animating_webviews), - ) { - warn!("Sending tick to constellation failed ({error:?})."); - } - } - } - pub fn on_zoom_reset_window_event(&mut self, webview_id: WebViewId) { if self.global.borrow().shutdown_state() != ShutdownState::NotShuttingDown { return; @@ -1410,6 +1422,11 @@ impl IOCompositor { /// Render the WebRender scene to the active `RenderingContext`. If successful, trigger /// the next round of animations. pub fn render(&mut self) -> bool { + self.global + .borrow() + .refresh_driver + .notify_will_paint(self.webview_renderers.iter()); + if let Err(error) = self.render_inner() { warn!("Unable to render: {error:?}"); return false; @@ -1419,9 +1436,6 @@ impl IOCompositor { // the scene no longer needs to be repainted. self.needs_repaint.set(RepaintReason::empty()); - // Queue up any subsequent paints for animations. - self.process_animations(true); - true } @@ -1492,10 +1506,8 @@ impl IOCompositor { if opts::get().wait_for_stable_image { // The current image may be ready to output. However, if there are animations active, - // tick those instead and continue waiting for the image output to be stable AND - // all active animations to complete. + // continue waiting for the image output to be stable AND all active animations to complete. if self.animations_or_animation_callbacks_running() { - self.process_animations(false); return Err(UnableToComposite::NotReadyToPaintImage( NotReadyToPaint::AnimationsActive, )); @@ -1647,11 +1659,20 @@ impl IOCompositor { }, CompositorMsg::NewWebRenderFrameReady(..) => { found_recomposite_msg = true; - compositor_messages.push(msg) + compositor_messages.push(msg); }, _ => compositor_messages.push(msg), } } + + if found_recomposite_msg { + // Process all pending events + self.webview_renderers.iter().for_each(|webview| { + webview.dispatch_pending_point_input_events(); + webview.webrender_frame_ready.set(true); + }); + } + for msg in compositor_messages { self.handle_browser_message(msg); diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index a66c61499e5..4faeadf5ba6 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -11,7 +11,7 @@ use compositing_traits::rendering_context::RenderingContext; use compositing_traits::{CompositorMsg, CompositorProxy}; use constellation_traits::EmbedderToConstellationMessage; use crossbeam_channel::{Receiver, Sender}; -use embedder_traits::ShutdownState; +use embedder_traits::{EventLoopWaker, ShutdownState}; use profile_traits::{mem, time}; use webrender::RenderApi; use webrender_api::DocumentId; @@ -22,9 +22,10 @@ pub use crate::compositor::{IOCompositor, WebRenderDebugOption}; mod tracing; mod compositor; +mod refresh_driver; mod touch; -pub mod webview_manager; -pub mod webview_renderer; +mod webview_manager; +mod webview_renderer; /// Data used to construct a compositor. pub struct InitialCompositorState { @@ -49,4 +50,7 @@ pub struct InitialCompositorState { pub webrender_gl: Rc<dyn gleam::gl::Gl>, #[cfg(feature = "webxr")] pub webxr_main_thread: webxr::MainThreadRegistry, + /// An [`EventLoopWaker`] used in order to wake up the embedder when it is + /// time to paint. + pub event_loop_waker: Box<dyn EventLoopWaker>, } diff --git a/components/compositing/refresh_driver.rs b/components/compositing/refresh_driver.rs new file mode 100644 index 00000000000..5531a7257c8 --- /dev/null +++ b/components/compositing/refresh_driver.rs @@ -0,0 +1,234 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +use std::cell::Cell; +use std::collections::hash_map::Values; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +use base::id::WebViewId; +use constellation_traits::EmbedderToConstellationMessage; +use crossbeam_channel::{Sender, select}; +use embedder_traits::EventLoopWaker; +use log::warn; +use timers::{BoxedTimerCallback, TimerEventId, TimerEventRequest, TimerScheduler, TimerSource}; + +use crate::compositor::RepaintReason; +use crate::webview_renderer::WebViewRenderer; + +const FRAME_DURATION: Duration = Duration::from_millis(1000 / 120); + +/// The [`RefreshDriver`] is responsible for controlling updates to aall `WebView`s +/// onscreen presentation. Currently, it only manages controlling animation update +/// requests. +/// +/// The implementation is very basic at the moment, only requesting new animation +/// frames at a constant time after a repaint. +pub(crate) struct RefreshDriver { + /// The channel on which messages can be sent to the Constellation. + pub(crate) constellation_sender: Sender<EmbedderToConstellationMessage>, + + /// Whether or not we are currently animating via a timer. + pub(crate) animating: Cell<bool>, + + /// Whether or not we are waiting for our frame timeout to trigger + pub(crate) waiting_for_frame_timeout: Arc<AtomicBool>, + + /// A [`TimerThread`] which is used to schedule frame timeouts in the future. + timer_thread: TimerThread, + + /// An [`EventLoopWaker`] to be used to wake up the embedder when it is + /// time to paint a frame. + event_loop_waker: Box<dyn EventLoopWaker>, +} + +impl RefreshDriver { + pub(crate) fn new( + constellation_sender: Sender<EmbedderToConstellationMessage>, + event_loop_waker: Box<dyn EventLoopWaker>, + ) -> Self { + Self { + constellation_sender, + animating: Default::default(), + waiting_for_frame_timeout: Default::default(), + timer_thread: Default::default(), + event_loop_waker, + } + } + + fn timer_callback(&self) -> BoxedTimerCallback { + let waiting_for_frame_timeout = self.waiting_for_frame_timeout.clone(); + let event_loop_waker = self.event_loop_waker.clone_box(); + Box::new(move |_| { + waiting_for_frame_timeout.store(false, Ordering::Relaxed); + event_loop_waker.wake(); + }) + } + + /// Notify the [`RefreshDriver`] that a paint is about to happen. This will trigger + /// new animation frames for all active `WebView`s and schedule a new frame deadline. + pub(crate) fn notify_will_paint( + &self, + webview_renderers: Values<'_, WebViewId, WebViewRenderer>, + ) { + // If we are still waiting for the frame to timeout this paint was caused for some + // non-animation related reason and we should wait until the frame timeout to trigger + // the next one. + if self.waiting_for_frame_timeout.load(Ordering::Relaxed) { + return; + } + + // If any WebViews are animating ask them to paint again for another animation tick. + let animating_webviews: Vec<_> = webview_renderers + .filter_map(|webview_renderer| { + if webview_renderer.animating() { + Some(webview_renderer.id) + } else { + None + } + }) + .collect(); + + // If nothing is animating any longer, update our state and exit early without requesting + // any noew frames nor triggering a new animation deadline. + if animating_webviews.is_empty() { + self.animating.set(false); + return; + } + + if let Err(error) = + self.constellation_sender + .send(EmbedderToConstellationMessage::TickAnimation( + animating_webviews, + )) + { + warn!("Sending tick to constellation failed ({error:?})."); + } + + // Queue the next frame deadline. + self.animating.set(true); + self.waiting_for_frame_timeout + .store(true, Ordering::Relaxed); + self.timer_thread + .queue_timer(FRAME_DURATION, self.timer_callback()); + } + + /// Notify the [`RefreshDriver`] that the animation state of a particular `WebView` + /// via its associated [`WebViewRenderer`] has changed. In the case that a `WebView` + /// has started animating, the [`RefreshDriver`] will request a new frame from it + /// immediately, but only render that frame at the next frame deadline. + pub(crate) fn notify_animation_state_changed(&self, webview_renderer: &WebViewRenderer) { + if !webview_renderer.animating() { + // If no other WebView is animating we will officially stop animated once the + // next frame has been painted. + return; + } + + if let Err(error) = + self.constellation_sender + .send(EmbedderToConstellationMessage::TickAnimation(vec![ + webview_renderer.id, + ])) + { + warn!("Sending tick to constellation failed ({error:?})."); + } + + if self.animating.get() { + return; + } + + self.animating.set(true); + self.waiting_for_frame_timeout + .store(true, Ordering::Relaxed); + self.timer_thread + .queue_timer(FRAME_DURATION, self.timer_callback()); + } + + /// Whether or not the renderer should trigger a message to the embedder to request a + /// repaint. This might be false if: we are animating and the repaint reason is just + /// for a new frame. In that case, the renderer should wait until the frame timeout to + /// ask the embedder to repaint. + pub(crate) fn wait_to_paint(&self, repaint_reason: RepaintReason) -> bool { + if !self.animating.get() || repaint_reason != RepaintReason::NewWebRenderFrame { + return false; + } + + self.waiting_for_frame_timeout.load(Ordering::Relaxed) + } +} + +enum TimerThreadMessage { + Request(TimerEventRequest), + Quit, +} + +/// A thread that manages a [`TimerScheduler`] running in the background of the +/// [`RefreshDriver`]. This is necessary because we need a reliable way of waking up the +/// embedder's main thread, which may just be sleeping until the `EventLoopWaker` asks it +/// to wake up. +/// +/// It would be nice to integrate this somehow into the embedder thread, but it would +/// require both some communication with the embedder and for all embedders to be well +/// behave respecting wakeup timeouts -- a bit too much to ask at the moment. +struct TimerThread { + sender: Sender<TimerThreadMessage>, + join_handle: Option<JoinHandle<()>>, +} + +impl Drop for TimerThread { + fn drop(&mut self) { + let _ = self.sender.send(TimerThreadMessage::Quit); + if let Some(join_handle) = self.join_handle.take() { + let _ = join_handle.join(); + } + } +} + +impl Default for TimerThread { + fn default() -> Self { + let (sender, receiver) = crossbeam_channel::unbounded::<TimerThreadMessage>(); + let join_handle = thread::Builder::new() + .name(String::from("CompositorTimerThread")) + .spawn(move || { + let mut scheduler = TimerScheduler::default(); + + loop { + select! { + recv(receiver) -> message => { + match message { + Ok(TimerThreadMessage::Request(request)) => { + scheduler.schedule_timer(request); + }, + _ => return, + } + }, + recv(scheduler.wait_channel()) -> _message => { + scheduler.dispatch_completed_timers(); + }, + }; + } + }) + .expect("Could not create RefreshDriver timer thread."); + + Self { + sender, + join_handle: Some(join_handle), + } + } +} + +impl TimerThread { + fn queue_timer(&self, duration: Duration, callback: BoxedTimerCallback) { + let _ = self + .sender + .send(TimerThreadMessage::Request(TimerEventRequest { + callback, + source: TimerSource::FromWorker, + id: TimerEventId(0), + duration, + })); + } +} diff --git a/components/compositing/tracing.rs b/components/compositing/tracing.rs index a8bb8b42bb8..65f9bd76c08 100644 --- a/components/compositing/tracing.rs +++ b/components/compositing/tracing.rs @@ -59,6 +59,7 @@ mod from_constellation { Self::GetScreenSize(..) => target!("GetScreenSize"), Self::GetAvailableScreenSize(..) => target!("GetAvailableScreenSize"), Self::CollectMemoryReport(..) => target!("CollectMemoryReport"), + Self::Viewport(..) => target!("Viewport"), } } } diff --git a/components/compositing/webview_renderer.rs b/components/compositing/webview_renderer.rs index b0e91ccb02e..056ffc16b89 100644 --- a/components/compositing/webview_renderer.rs +++ b/components/compositing/webview_renderer.rs @@ -2,12 +2,15 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use std::cell::RefCell; -use std::collections::HashMap; +use std::cell::{Cell, RefCell}; use std::collections::hash_map::Keys; +use std::collections::{HashMap, VecDeque}; use std::rc::Rc; use base::id::{PipelineId, WebViewId}; +use compositing_traits::viewport_description::{ + DEFAULT_ZOOM, MAX_ZOOM, MIN_ZOOM, ViewportDescription, +}; use compositing_traits::{SendableFrameTree, WebViewTrait}; use constellation_traits::{EmbedderToConstellationMessage, ScrollState, WindowSizeType}; use embedder_traits::{ @@ -25,13 +28,9 @@ use webrender_api::units::{ }; use webrender_api::{ExternalScrollId, HitTestFlags, ScrollLocation}; -use crate::compositor::{PipelineDetails, ServoRenderer}; +use crate::compositor::{HitTestError, PipelineDetails, ServoRenderer}; use crate::touch::{TouchHandler, TouchMoveAction, TouchMoveAllowed, TouchSequenceState}; -// Default viewport constraints -const MAX_ZOOM: f32 = 8.0; -const MIN_ZOOM: f32 = 0.1; - #[derive(Clone, Copy)] struct ScrollEvent { /// Scroll by this offset, or to Start or End @@ -44,8 +43,10 @@ struct ScrollEvent { #[derive(Clone, Copy)] enum ScrollZoomEvent { - /// An pinch zoom event that magnifies the view by the given factor. + /// A pinch zoom event that magnifies the view by the given factor. PinchZoom(f32), + /// A zoom event that magnifies the view by the factor parsed from meta tag. + ViewportZoom(f32), /// A scroll event that scrolls the scroll node at the given location by the /// given amount. Scroll(ScrollEvent), @@ -58,7 +59,7 @@ pub(crate) struct ScrollResult { pub offset: LayoutVector2D, } -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub(crate) enum PinchZoomResult { DidPinchZoom, DidNotPinchZoom, @@ -89,15 +90,18 @@ pub(crate) struct WebViewRenderer { pub page_zoom: Scale<f32, CSSPixel, DeviceIndependentPixel>, /// "Mobile-style" zoom that does not reflow the page. viewport_zoom: PinchZoomFactor, - /// Viewport zoom constraints provided by @viewport. - min_viewport_zoom: Option<PinchZoomFactor>, - max_viewport_zoom: Option<PinchZoomFactor>, /// The HiDPI scale factor for the `WebView` associated with this renderer. This is controlled /// by the embedding layer. hidpi_scale_factor: Scale<f32, DeviceIndependentPixel, DevicePixel>, /// Whether or not this [`WebViewRenderer`] isn't throttled and has a pipeline with /// active animations or animation frame callbacks. animating: bool, + /// Pending input events queue. Priavte and only this thread pushes events to it. + pending_point_input_events: RefCell<VecDeque<InputEvent>>, + /// WebRender is not ready between `SendDisplayList` and `WebRenderFrameReady` messages. + pub webrender_frame_ready: Cell<bool>, + /// Viewport Description + viewport_description: Option<ViewportDescription>, } impl Drop for WebViewRenderer { @@ -127,11 +131,12 @@ impl WebViewRenderer { global, pending_scroll_zoom_events: Default::default(), page_zoom: Scale::new(1.0), - viewport_zoom: PinchZoomFactor::new(1.0), - min_viewport_zoom: Some(PinchZoomFactor::new(1.0)), - max_viewport_zoom: None, + viewport_zoom: PinchZoomFactor::new(DEFAULT_ZOOM), hidpi_scale_factor: Scale::new(hidpi_scale_factor.0), animating: false, + pending_point_input_events: Default::default(), + webrender_frame_ready: Cell::default(), + viewport_description: None, } } @@ -309,29 +314,89 @@ impl WebViewRenderer { } } - pub(crate) fn dispatch_input_event(&mut self, event: InputEvent) { + pub(crate) fn dispatch_point_input_event(&mut self, mut event: InputEvent) -> bool { // Events that do not need to do hit testing are sent directly to the // constellation to filter down. let Some(point) = event.point() else { - return; + return false; }; + // Delay the event if the epoch is not synchronized yet (new frame is not ready), + // or hit test result would fail and the event is rejected anyway. + if !self.webrender_frame_ready.get() || !self.pending_point_input_events.borrow().is_empty() + { + self.pending_point_input_events + .borrow_mut() + .push_back(event); + return false; + } + // If we can't find a pipeline to send this event to, we cannot continue. let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); - let Some(result) = self + let result = match self .global .borrow() .hit_test_at_point(point, get_pipeline_details) - else { - return; + { + Ok(hit_test_results) => hit_test_results, + Err(HitTestError::EpochMismatch) => { + self.pending_point_input_events + .borrow_mut() + .push_back(event); + return false; + }, + _ => { + return false; + }, }; - self.global.borrow_mut().update_cursor(point, &result); + match event { + InputEvent::Touch(ref mut touch_event) => { + touch_event.init_sequence_id(self.touch_handler.current_sequence_id); + }, + InputEvent::MouseButton(_) | InputEvent::MouseMove(_) | InputEvent::Wheel(_) => { + self.global.borrow_mut().update_cursor(point, &result); + }, + _ => unreachable!("Unexpected input event type: {event:?}"), + } if let Err(error) = self.global.borrow().constellation_sender.send( EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)), ) { warn!("Sending event to constellation failed ({error:?})."); + false + } else { + true + } + } + + pub(crate) fn dispatch_pending_point_input_events(&self) { + while let Some(event) = self.pending_point_input_events.borrow_mut().pop_front() { + // Events that do not need to do hit testing are sent directly to the + // constellation to filter down. + let Some(point) = event.point() else { + continue; + }; + + // If we can't find a pipeline to send this event to, we cannot continue. + let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); + let Ok(result) = self + .global + .borrow() + .hit_test_at_point(point, get_pipeline_details) + else { + // Don't need to process pending input events in this frame any more. + // TODO: Add multiple retry later if needed. + return; + }; + + self.global.borrow_mut().update_cursor(point, &result); + + if let Err(error) = self.global.borrow().constellation_sender.send( + EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)), + ) { + warn!("Sending event to constellation failed ({error:?})."); + } } } @@ -401,29 +466,11 @@ impl WebViewRenderer { } } - self.dispatch_input_event(event); + self.dispatch_point_input_event(event); } - fn send_touch_event(&self, mut event: TouchEvent) -> bool { - let get_pipeline_details = |pipeline_id| self.pipelines.get(&pipeline_id); - let Some(result) = self - .global - .borrow() - .hit_test_at_point(event.point, get_pipeline_details) - else { - return false; - }; - - event.init_sequence_id(self.touch_handler.current_sequence_id); - let event = InputEvent::Touch(event); - if let Err(e) = self.global.borrow().constellation_sender.send( - EmbedderToConstellationMessage::ForwardInputEvent(self.id, event, Some(result)), - ) { - warn!("Sending event to constellation failed ({:?}).", e); - false - } else { - true - } + fn send_touch_event(&mut self, event: TouchEvent) -> bool { + self.dispatch_point_input_event(InputEvent::Touch(event)) } pub(crate) fn on_touch_event(&mut self, event: TouchEvent) { @@ -687,13 +734,13 @@ impl WebViewRenderer { /// <http://w3c.github.io/touch-events/#mouse-events> fn simulate_mouse_click(&mut self, point: DevicePoint) { let button = MouseButton::Left; - self.dispatch_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point))); - self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new( + self.dispatch_point_input_event(InputEvent::MouseMove(MouseMoveEvent::new(point))); + self.dispatch_point_input_event(InputEvent::MouseButton(MouseButtonEvent::new( MouseButtonAction::Down, button, point, ))); - self.dispatch_input_event(InputEvent::MouseButton(MouseButtonEvent::new( + self.dispatch_point_input_event(InputEvent::MouseButton(MouseButtonEvent::new( MouseButtonAction::Up, button, point, @@ -764,7 +811,8 @@ impl WebViewRenderer { let mut combined_magnification = 1.0; for scroll_event in self.pending_scroll_zoom_events.drain(..) { match scroll_event { - ScrollZoomEvent::PinchZoom(magnification) => { + ScrollZoomEvent::PinchZoom(magnification) | + ScrollZoomEvent::ViewportZoom(magnification) => { combined_magnification *= magnification }, ScrollZoomEvent::Scroll(scroll_event_info) => { @@ -858,7 +906,8 @@ impl WebViewRenderer { HitTestFlags::FIND_ALL, None, get_pipeline_details, - ); + ) + .unwrap_or_default(); // Iterate through all hit test results, processing only the first node of each pipeline. // This is needed to propagate the scroll events from a pipeline representing an iframe to @@ -892,11 +941,8 @@ impl WebViewRenderer { } fn set_pinch_zoom_level(&mut self, mut zoom: f32) -> bool { - if let Some(min) = self.min_viewport_zoom { - zoom = f32::max(min.get(), zoom); - } - if let Some(max) = self.max_viewport_zoom { - zoom = f32::min(max.get(), zoom); + if let Some(viewport) = self.viewport_description.as_ref() { + zoom = viewport.clamp_zoom(zoom); } let old_zoom = std::mem::replace(&mut self.viewport_zoom, PinchZoomFactor::new(zoom)); @@ -926,7 +972,12 @@ impl WebViewRenderer { // TODO: Scroll to keep the center in view? self.pending_scroll_zoom_events - .push(ScrollZoomEvent::PinchZoom(magnification)); + .push(ScrollZoomEvent::PinchZoom( + self.viewport_description + .clone() + .unwrap_or_default() + .clamp_zoom(magnification), + )); } fn send_window_size_message(&self) { @@ -993,6 +1044,17 @@ impl WebViewRenderer { let screen_geometry = self.webview.screen_geometry().unwrap_or_default(); (screen_geometry.available_size.to_f32() / self.hidpi_scale_factor).to_i32() } + + pub fn set_viewport_description(&mut self, viewport_description: ViewportDescription) { + self.pending_scroll_zoom_events + .push(ScrollZoomEvent::ViewportZoom( + self.viewport_description + .clone() + .unwrap_or_default() + .clamp_zoom(viewport_description.initial_scale.get()), + )); + self.viewport_description = Some(viewport_description); + } } #[derive(Clone, Copy, Debug, PartialEq)] |