Files
servo/components/script/image_animation.rs
Martin Robinson cf5b8592bf script: Skip running layout when only updating images or canvas (#38991)
Add a new super-lightweight layout mode that avoids any layout when
canvas is updated or animated images progress to the next frame. In the
future this can also be used for video elements.

Testing: This is a performance optimization, so shouldn't change any
WPT test results.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
2025-10-03 13:46:03 +00:00

163 lines
5.9 KiB
Rust

/* 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::sync::Arc;
use std::time::Duration;
use compositing_traits::{ImageUpdate, SerializableImageData};
use embedder_traits::UntrustedNodeAddress;
use ipc_channel::ipc::IpcSharedMemory;
use layout_api::ImageAnimationState;
use libc::c_void;
use malloc_size_of::MallocSizeOf;
use parking_lot::RwLock;
use rustc_hash::FxHashMap;
use script_bindings::codegen::GenericBindings::WindowBinding::WindowMethods;
use script_bindings::root::Dom;
use style::dom::OpaqueNode;
use timers::{TimerEventRequest, TimerId};
use webrender_api::units::DeviceIntSize;
use webrender_api::{ImageDescriptor, ImageDescriptorFlags, ImageFormat};
use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::trace::NoTrace;
use crate::dom::node::{Node, from_untrusted_node_address};
use crate::dom::window::Window;
use crate::script_thread::with_script_thread;
#[derive(Clone, Default, JSTraceable)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub struct ImageAnimationManager {
#[no_trace]
node_to_image_map: Arc<RwLock<FxHashMap<OpaqueNode, ImageAnimationState>>>,
/// A list of nodes with in-progress image animations.
///
/// TODO(mrobinson): This does not properly handle animating images that are in pseudo-elements.
rooted_nodes: DomRefCell<FxHashMap<NoTrace<OpaqueNode>, Dom<Node>>>,
/// The [`TimerId`] of the currently scheduled animated image update callback.
#[no_trace]
callback_timer_id: Cell<Option<TimerId>>,
}
impl MallocSizeOf for ImageAnimationManager {
fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
(*self.node_to_image_map.read()).size_of(ops) + self.rooted_nodes.size_of(ops)
}
}
impl ImageAnimationManager {
pub(crate) fn node_to_image_map(
&self,
) -> Arc<RwLock<FxHashMap<OpaqueNode, ImageAnimationState>>> {
self.node_to_image_map.clone()
}
fn duration_to_next_frame(&self, now: f64) -> Option<Duration> {
self.node_to_image_map
.read()
.values()
.map(|state| state.duration_to_next_frame(now))
.min()
}
pub(crate) fn update_active_frames(&self, window: &Window, now: f64) {
if self.node_to_image_map.read().is_empty() {
return;
}
let updates = self
.node_to_image_map
.write()
.values_mut()
.filter_map(|state| {
if !state.update_frame_for_animation_timeline_value(now) {
return None;
}
let image = &state.image;
let frame = image
.frame(state.active_frame)
.expect("active_frame should within range of frames");
Some(ImageUpdate::UpdateImage(
image.id.unwrap(),
ImageDescriptor {
format: ImageFormat::BGRA8,
size: DeviceIntSize::new(
image.metadata.width as i32,
image.metadata.height as i32,
),
stride: None,
offset: 0,
flags: ImageDescriptorFlags::ALLOW_MIPMAPS,
},
SerializableImageData::Raw(IpcSharedMemory::from_bytes(frame.bytes)),
None,
))
})
.collect();
window.compositor_api().update_images(updates);
self.maybe_schedule_animated_image_update_callback(window, now);
}
/// Ensure that all nodes with animating images are rooted and unroots any nodes that
/// no longer have an animating image. This should be called immediately after a
/// restyle, to ensure that these addresses are still valid.
#[allow(unsafe_code)]
pub(crate) fn update_rooted_dom_nodes(&self, window: &Window, now: f64) {
let mut rooted_nodes = self.rooted_nodes.borrow_mut();
let node_to_image_map = self.node_to_image_map.read();
let mut added_node = false;
for opaque_node in node_to_image_map.keys() {
let opaque_node = *opaque_node;
if rooted_nodes.contains_key(&NoTrace(opaque_node)) {
continue;
}
added_node = true;
let address = UntrustedNodeAddress(opaque_node.0 as *const c_void);
unsafe {
rooted_nodes.insert(
NoTrace(opaque_node),
Dom::from_ref(&*from_untrusted_node_address(address)),
)
};
}
let length_before = rooted_nodes.len();
rooted_nodes.retain(|node, _| node_to_image_map.contains_key(&node.0));
if added_node || length_before != rooted_nodes.len() {
self.maybe_schedule_animated_image_update_callback(window, now);
}
}
fn maybe_schedule_animated_image_update_callback(&self, window: &Window, now: f64) {
with_script_thread(|script_thread| {
if let Some(current_timer_id) = self.callback_timer_id.take() {
self.callback_timer_id.set(None);
script_thread.cancel_timer(current_timer_id);
}
if let Some(duration) = self.duration_to_next_frame(now) {
let trusted_window = Trusted::new(window);
let timer_id = script_thread.schedule_timer(TimerEventRequest {
callback: Box::new(move || {
let window = trusted_window.root();
window.Document().set_has_pending_animated_image_update();
}),
duration,
});
self.callback_timer_id.set(Some(timer_id));
}
})
}
}