mirror of
https://github.com/servo/servo.git
synced 2025-06-04 07:35:36 +00:00
compositor: Add an initial RefreshDriver (#37169)
This adds a *very* basic implementation of the `RefreshDriver` concept to the Servo renderer. The initial idea is that controls the frequency of display during animations. It eliminates the "slowdown" workaround for WPT tests and now Servo animations don't move faster than 120 FPS (observed to be slower in practice). This establishes a base change which will be used to implement non-display-list-producing layouts in a followup change. Fixes #3406. (though much more work remains) Testing: Covered by existing WPT tests. Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
801ac9e22a
commit
9dc1391bef
6 changed files with 292 additions and 69 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -1109,6 +1109,7 @@ dependencies = [
|
|||
"servo_geometry",
|
||||
"stylo_traits",
|
||||
"surfman",
|
||||
"timers",
|
||||
"tracing",
|
||||
"webrender",
|
||||
"webrender_api",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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 {
|
||||
|
@ -386,6 +386,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 +410,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 +453,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 +531,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 +586,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);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1271,39 +1286,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 +1392,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 +1406,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 +1476,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,
|
||||
));
|
||||
|
|
|
@ -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>,
|
||||
}
|
||||
|
|
234
components/compositing/refresh_driver.rs
Normal file
234
components/compositing/refresh_driver.rs
Normal file
|
@ -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,
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -417,7 +417,7 @@ impl Servo {
|
|||
// Create the WebXR main thread
|
||||
#[cfg(feature = "webxr")]
|
||||
let mut webxr_main_thread =
|
||||
webxr::MainThreadRegistry::new(event_loop_waker, webxr_layer_grand_manager)
|
||||
webxr::MainThreadRegistry::new(event_loop_waker.clone(), webxr_layer_grand_manager)
|
||||
.expect("Failed to create WebXR device registry");
|
||||
#[cfg(feature = "webxr")]
|
||||
if pref!(dom_webxr_enabled) {
|
||||
|
@ -489,6 +489,7 @@ impl Servo {
|
|||
#[cfg(feature = "webxr")]
|
||||
webxr_main_thread,
|
||||
shutdown_state: shutdown_state.clone(),
|
||||
event_loop_waker,
|
||||
},
|
||||
opts.debug.convert_mouse_to_touch,
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue