aboutsummaryrefslogtreecommitdiffstats
path: root/components/compositing
diff options
context:
space:
mode:
Diffstat (limited to 'components/compositing')
-rw-r--r--components/compositing/Cargo.toml1
-rw-r--r--components/compositing/compositor.rs185
-rw-r--r--components/compositing/lib.rs10
-rw-r--r--components/compositing/refresh_driver.rs234
-rw-r--r--components/compositing/tracing.rs1
-rw-r--r--components/compositing/webview_renderer.rs168
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)]