aboutsummaryrefslogtreecommitdiffstats
path: root/components/compositing/refresh_driver.rs
diff options
context:
space:
mode:
Diffstat (limited to 'components/compositing/refresh_driver.rs')
-rw-r--r--components/compositing/refresh_driver.rs234
1 files changed, 234 insertions, 0 deletions
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,
+ }));
+ }
+}