diff options
Diffstat (limited to 'components/shared/compositing')
-rw-r--r-- | components/shared/compositing/Cargo.toml | 15 | ||||
-rw-r--r-- | components/shared/compositing/display_list.rs | 373 | ||||
-rw-r--r-- | components/shared/compositing/lib.rs | 482 | ||||
-rw-r--r-- | components/shared/compositing/rendering_context.rs | 887 | ||||
-rw-r--r-- | components/shared/compositing/tests/compositor.rs | 188 |
5 files changed, 1942 insertions, 3 deletions
diff --git a/components/shared/compositing/Cargo.toml b/components/shared/compositing/Cargo.toml index 02488755cff..d1df90106cb 100644 --- a/components/shared/compositing/Cargo.toml +++ b/components/shared/compositing/Cargo.toml @@ -11,16 +11,27 @@ rust-version.workspace = true name = "compositing_traits" path = "lib.rs" +[features] +no-wgl = ["surfman/sm-angle-default"] + [dependencies] base = { workspace = true } crossbeam-channel = { workspace = true } +dpi = { version = "0.1" } embedder_traits = { workspace = true } euclid = { workspace = true } +gleam = { workspace = true } +glow = { workspace = true } +image = { workspace = true } ipc-channel = { workspace = true } log = { workspace = true } pixels = { path = '../../pixels' } -script_traits = { workspace = true } +raw-window-handle = { version = "0.6" } +serde = { workspace = true } +servo_geometry = { path = "../../geometry" } strum_macros = { workspace = true } +stylo = { workspace = true } stylo_traits = { workspace = true } +surfman = { workspace = true, features = ["sm-x11"] } webrender_api = { workspace = true } -webrender_traits = { workspace = true } + diff --git a/components/shared/compositing/display_list.rs b/components/shared/compositing/display_list.rs new file mode 100644 index 00000000000..2b7883c3ce1 --- /dev/null +++ b/components/shared/compositing/display_list.rs @@ -0,0 +1,373 @@ +/* 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/. */ + +//! Defines data structures which are consumed by the Compositor. + +use base::id::ScrollTreeNodeId; +use embedder_traits::Cursor; +use serde::{Deserialize, Serialize}; +use style::values::specified::Overflow; +use webrender_api::units::{LayoutSize, LayoutVector2D}; +use webrender_api::{Epoch, ExternalScrollId, PipelineId, ScrollLocation, SpatialId}; + +/// The scroll sensitivity of a scroll node in a particular axis ie whether it can be scrolled due to +/// input events and script events or only script events. +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub enum ScrollSensitivity { + /// This node can be scrolled by input and script events. + ScriptAndInputEvents, + /// This node can only be scrolled by script events. + Script, + /// This node cannot be scrolled. + None, +} + +/// Convert [Overflow] to [ScrollSensitivity]. +impl From<Overflow> for ScrollSensitivity { + fn from(overflow: Overflow) -> Self { + match overflow { + Overflow::Hidden => ScrollSensitivity::Script, + Overflow::Scroll | Overflow::Auto => ScrollSensitivity::ScriptAndInputEvents, + Overflow::Visible | Overflow::Clip => ScrollSensitivity::None, + } + } +} + +/// The [ScrollSensitivity] of particular node in the vertical and horizontal axes. +#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)] +pub struct AxesScrollSensitivity { + pub x: ScrollSensitivity, + pub y: ScrollSensitivity, +} + +/// Information that Servo keeps alongside WebRender display items +/// in order to add more context to hit test results. +#[derive(Debug, Deserialize, PartialEq, Serialize)] +pub struct HitTestInfo { + /// The id of the node of this hit test item. + pub node: u64, + + /// The cursor of this node's hit test item. + pub cursor: Option<Cursor>, + + /// The id of the [ScrollTree] associated with this hit test item. + pub scroll_tree_node: ScrollTreeNodeId, +} + +/// Data stored for nodes in the [ScrollTree] that actually scroll, +/// as opposed to reference frames and sticky nodes which do not. +#[derive(Debug, Deserialize, Serialize)] +pub struct ScrollableNodeInfo { + /// The external scroll id of this node, used to track + /// it between successive re-layouts. + pub external_id: ExternalScrollId, + + /// Amount that this `ScrollableNode` can scroll in both directions. + pub scrollable_size: LayoutSize, + + /// Whether this `ScrollableNode` is sensitive to input events. + pub scroll_sensitivity: AxesScrollSensitivity, + + /// The current offset of this scroll node. + pub offset: LayoutVector2D, +} + +#[derive(Debug, Deserialize, Serialize)] +/// A node in a tree of scroll nodes. This may either be a scrollable +/// node which responds to scroll events or a non-scrollable one. +pub struct ScrollTreeNode { + /// The index of the parent of this node in the tree. If this is + /// None then this is the root node. + pub parent: Option<ScrollTreeNodeId>, + + /// Scrolling data which will not be None if this is a scrolling node. + pub scroll_info: Option<ScrollableNodeInfo>, +} + +impl ScrollTreeNode { + /// Get the external id of this node. + pub fn external_id(&self) -> Option<ExternalScrollId> { + self.scroll_info.as_ref().map(|info| info.external_id) + } + + /// Get the offset id of this node if it applies. + pub fn offset(&self) -> Option<LayoutVector2D> { + self.scroll_info.as_ref().map(|info| info.offset) + } + + /// Set the offset for this node, returns false if this was a + /// non-scrolling node for which you cannot set the offset. + pub fn set_offset(&mut self, new_offset: LayoutVector2D) -> bool { + match self.scroll_info { + Some(ref mut info) => { + let scrollable_width = info.scrollable_size.width; + let scrollable_height = info.scrollable_size.height; + + if scrollable_width > 0. { + info.offset.x = (new_offset.x).min(0.0).max(-scrollable_width); + } + + if scrollable_height > 0. { + info.offset.y = (new_offset.y).min(0.0).max(-scrollable_height); + } + true + }, + _ => false, + } + } + + /// Scroll this node given a WebRender ScrollLocation. Returns a tuple that can + /// be used to scroll an individual WebRender scroll frame if the operation + /// actually changed an offset. + pub fn scroll( + &mut self, + scroll_location: ScrollLocation, + ) -> Option<(ExternalScrollId, LayoutVector2D)> { + let info = match self.scroll_info { + Some(ref mut data) => data, + None => return None, + }; + + if info.scroll_sensitivity.x != ScrollSensitivity::ScriptAndInputEvents && + info.scroll_sensitivity.y != ScrollSensitivity::ScriptAndInputEvents + { + return None; + } + + let delta = match scroll_location { + ScrollLocation::Delta(delta) => delta, + ScrollLocation::Start => { + if info.offset.y.round() >= 0.0 { + // Nothing to do on this layer. + return None; + } + + info.offset.y = 0.0; + return Some((info.external_id, info.offset)); + }, + ScrollLocation::End => { + let end_pos = -info.scrollable_size.height; + if info.offset.y.round() <= end_pos { + // Nothing to do on this layer. + return None; + } + + info.offset.y = end_pos; + return Some((info.external_id, info.offset)); + }, + }; + + let scrollable_width = info.scrollable_size.width; + let scrollable_height = info.scrollable_size.height; + let original_layer_scroll_offset = info.offset; + + if scrollable_width > 0. && + info.scroll_sensitivity.x == ScrollSensitivity::ScriptAndInputEvents + { + info.offset.x = (info.offset.x + delta.x).min(0.0).max(-scrollable_width); + } + + if scrollable_height > 0. && + info.scroll_sensitivity.y == ScrollSensitivity::ScriptAndInputEvents + { + info.offset.y = (info.offset.y + delta.y).min(0.0).max(-scrollable_height); + } + + if info.offset != original_layer_scroll_offset { + Some((info.external_id, info.offset)) + } else { + None + } + } +} + +/// A tree of spatial nodes, which mirrors the spatial nodes in the WebRender +/// display list, except these are used to scrolling in the compositor so that +/// new offsets can be sent to WebRender. +#[derive(Debug, Default, Deserialize, Serialize)] +pub struct ScrollTree { + /// A list of compositor-side scroll nodes that describe the tree + /// of WebRender spatial nodes, used by the compositor to scroll the + /// contents of the display list. + pub nodes: Vec<ScrollTreeNode>, +} + +impl ScrollTree { + /// Add a scroll node to this ScrollTree returning the id of the new node. + pub fn add_scroll_tree_node( + &mut self, + parent: Option<&ScrollTreeNodeId>, + spatial_id: SpatialId, + scroll_info: Option<ScrollableNodeInfo>, + ) -> ScrollTreeNodeId { + self.nodes.push(ScrollTreeNode { + parent: parent.cloned(), + scroll_info, + }); + ScrollTreeNodeId { + index: self.nodes.len() - 1, + spatial_id, + } + } + + /// Get a mutable reference to the node with the given index. + pub fn get_node_mut(&mut self, id: &ScrollTreeNodeId) -> &mut ScrollTreeNode { + &mut self.nodes[id.index] + } + + /// Get an immutable reference to the node with the given index. + pub fn get_node(&mut self, id: &ScrollTreeNodeId) -> &ScrollTreeNode { + &self.nodes[id.index] + } + + /// Scroll the given scroll node on this scroll tree. If the node cannot be scrolled, + /// because it isn't a scrollable node or it's already scrolled to the maximum scroll + /// extent, try to scroll an ancestor of this node. Returns the node scrolled and the + /// new offset if a scroll was performed, otherwise returns None. + pub fn scroll_node_or_ancestor( + &mut self, + scroll_node_id: &ScrollTreeNodeId, + scroll_location: ScrollLocation, + ) -> Option<(ExternalScrollId, LayoutVector2D)> { + let parent = { + let node = &mut self.get_node_mut(scroll_node_id); + let result = node.scroll(scroll_location); + if result.is_some() { + return result; + } + node.parent + }; + + parent.and_then(|parent| self.scroll_node_or_ancestor(&parent, scroll_location)) + } + + /// Given an [`ExternalScrollId`] and an offset, update the scroll offset of the scroll node + /// with the given id. + pub fn set_scroll_offsets_for_node_with_external_scroll_id( + &mut self, + external_scroll_id: ExternalScrollId, + offset: LayoutVector2D, + ) -> bool { + for node in self.nodes.iter_mut() { + match node.scroll_info { + Some(ref mut scroll_info) if scroll_info.external_id == external_scroll_id => { + scroll_info.offset = offset; + return true; + }, + _ => {}, + } + } + false + } +} + +/// A data structure which stores compositor-side information about +/// display lists sent to the compositor. +#[derive(Debug, Deserialize, Serialize)] +pub struct CompositorDisplayListInfo { + /// The WebRender [PipelineId] of this display list. + pub pipeline_id: PipelineId, + + /// The size of the viewport that this display list renders into. + pub viewport_size: LayoutSize, + + /// The size of this display list's content. + pub content_size: LayoutSize, + + /// The epoch of the display list. + pub epoch: Epoch, + + /// An array of `HitTestInfo` which is used to store information + /// to assist the compositor to take various actions (set the cursor, + /// scroll without layout) using a WebRender hit test result. + pub hit_test_info: Vec<HitTestInfo>, + + /// A ScrollTree used by the compositor to scroll the contents of the + /// display list. + pub scroll_tree: ScrollTree, + + /// The `ScrollTreeNodeId` of the root reference frame of this info's scroll + /// tree. + pub root_reference_frame_id: ScrollTreeNodeId, + + /// The `ScrollTreeNodeId` of the topmost scrolling frame of this info's scroll + /// tree. + pub root_scroll_node_id: ScrollTreeNodeId, + + /// Contentful paint i.e. whether the display list contains items of type + /// text, image, non-white canvas or SVG). Used by metrics. + /// See <https://w3c.github.io/paint-timing/#first-contentful-paint>. + pub is_contentful: bool, + + /// Whether the first layout or a subsequent (incremental) layout triggered this + /// display list creation. + pub first_reflow: bool, +} + +impl CompositorDisplayListInfo { + /// Create a new CompositorDisplayListInfo with the root reference frame + /// and scroll frame already added to the scroll tree. + pub fn new( + viewport_size: LayoutSize, + content_size: LayoutSize, + pipeline_id: PipelineId, + epoch: Epoch, + viewport_scroll_sensitivity: AxesScrollSensitivity, + first_reflow: bool, + ) -> Self { + let mut scroll_tree = ScrollTree::default(); + let root_reference_frame_id = scroll_tree.add_scroll_tree_node( + None, + SpatialId::root_reference_frame(pipeline_id), + None, + ); + let root_scroll_node_id = scroll_tree.add_scroll_tree_node( + Some(&root_reference_frame_id), + SpatialId::root_scroll_node(pipeline_id), + Some(ScrollableNodeInfo { + external_id: ExternalScrollId(0, pipeline_id), + scrollable_size: content_size - viewport_size, + scroll_sensitivity: viewport_scroll_sensitivity, + offset: LayoutVector2D::zero(), + }), + ); + + CompositorDisplayListInfo { + pipeline_id, + viewport_size, + content_size, + epoch, + hit_test_info: Default::default(), + scroll_tree, + root_reference_frame_id, + root_scroll_node_id, + is_contentful: false, + first_reflow, + } + } + + /// Add or re-use a duplicate HitTestInfo entry in this `CompositorHitTestInfo` + /// and return the index. + pub fn add_hit_test_info( + &mut self, + node: u64, + cursor: Option<Cursor>, + scroll_tree_node: ScrollTreeNodeId, + ) -> usize { + let hit_test_info = HitTestInfo { + node, + cursor, + scroll_tree_node, + }; + + if let Some(last) = self.hit_test_info.last() { + if hit_test_info == *last { + return self.hit_test_info.len() - 1; + } + } + + self.hit_test_info.push(hit_test_info); + self.hit_test_info.len() - 1 + } +} diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs index 9c13afb8dc3..c231960ef50 100644 --- a/components/shared/compositing/lib.rs +++ b/components/shared/compositing/lib.rs @@ -18,7 +18,27 @@ use pixels::Image; use strum_macros::IntoStaticStr; use style_traits::CSSPixel; use webrender_api::DocumentId; -use webrender_traits::{CrossProcessCompositorApi, CrossProcessCompositorMessage}; + +pub mod display_list; +pub mod rendering_context; + +use core::fmt; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +use display_list::CompositorDisplayListInfo; +use embedder_traits::{CompositorHitTestResult, ScreenGeometry}; +use euclid::default::Size2D as UntypedSize2D; +use ipc_channel::ipc::{self, IpcSharedMemory}; +use serde::{Deserialize, Serialize}; +use servo_geometry::{DeviceIndependentIntRect, DeviceIndependentIntSize}; +use webrender_api::units::{DevicePoint, LayoutPoint, TexelRect}; +use webrender_api::{ + BuiltDisplayList, BuiltDisplayListDescriptor, ExternalImage, ExternalImageData, + ExternalImageHandler, ExternalImageId, ExternalImageSource, ExternalScrollId, + FontInstanceFlags, FontInstanceKey, FontKey, HitTestFlags, ImageData, ImageDescriptor, + ImageKey, NativeFontHandle, PipelineId as WebRenderPipelineId, +}; /// Sends messages to the compositor. #[derive(Clone)] @@ -111,3 +131,463 @@ impl Debug for CompositorMsg { write!(formatter, "{string}") } } + +#[derive(Deserialize, Serialize)] +pub enum CrossProcessCompositorMessage { + /// Inform WebRender of the existence of this pipeline. + SendInitialTransaction(WebRenderPipelineId), + /// Perform a scroll operation. + SendScrollNode( + WebViewId, + WebRenderPipelineId, + LayoutPoint, + ExternalScrollId, + ), + /// Inform WebRender of a new display list for the given pipeline. + SendDisplayList { + /// The [`WebViewId`] that this display list belongs to. + webview_id: WebViewId, + /// The [CompositorDisplayListInfo] that describes the display list being sent. + display_list_info: Box<CompositorDisplayListInfo>, + /// A descriptor of this display list used to construct this display list from raw data. + display_list_descriptor: BuiltDisplayListDescriptor, + /// An [ipc::IpcBytesReceiver] used to send the raw data of the display list. + display_list_receiver: ipc::IpcBytesReceiver, + }, + /// Perform a hit test operation. The result will be returned via + /// the provided channel sender. + HitTest( + Option<WebRenderPipelineId>, + DevicePoint, + HitTestFlags, + IpcSender<Vec<CompositorHitTestResult>>, + ), + /// Create a new image key. The result will be returned via the + /// provided channel sender. + GenerateImageKey(IpcSender<ImageKey>), + /// Add an image with the given data and `ImageKey`. + AddImage(ImageKey, ImageDescriptor, SerializableImageData), + /// Perform a resource update operation. + UpdateImages(Vec<ImageUpdate>), + + /// Generate a new batch of font keys which can be used to allocate + /// keys asynchronously. + GenerateFontKeys( + usize, + usize, + IpcSender<(Vec<FontKey>, Vec<FontInstanceKey>)>, + ), + /// Add a font with the given data and font key. + AddFont(FontKey, Arc<IpcSharedMemory>, u32), + /// Add a system font with the given font key and handle. + AddSystemFont(FontKey, NativeFontHandle), + /// Add an instance of a font with the given instance key. + AddFontInstance(FontInstanceKey, FontKey, f32, FontInstanceFlags), + /// Remove the given font resources from our WebRender instance. + RemoveFonts(Vec<FontKey>, Vec<FontInstanceKey>), + + /// Get the client window size and position. + GetClientWindowRect(WebViewId, IpcSender<DeviceIndependentIntRect>), + /// Get the size of the screen that the client window inhabits. + GetScreenSize(WebViewId, IpcSender<DeviceIndependentIntSize>), + /// Get the available screen size (without toolbars and docks) for the screen + /// the client window inhabits. + GetAvailableScreenSize(WebViewId, IpcSender<DeviceIndependentIntSize>), +} + +impl fmt::Debug for CrossProcessCompositorMessage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::AddImage(..) => f.write_str("AddImage"), + Self::GenerateFontKeys(..) => f.write_str("GenerateFontKeys"), + Self::AddSystemFont(..) => f.write_str("AddSystemFont"), + Self::SendInitialTransaction(..) => f.write_str("SendInitialTransaction"), + Self::SendScrollNode(..) => f.write_str("SendScrollNode"), + Self::SendDisplayList { .. } => f.write_str("SendDisplayList"), + Self::HitTest(..) => f.write_str("HitTest"), + Self::GenerateImageKey(..) => f.write_str("GenerateImageKey"), + Self::UpdateImages(..) => f.write_str("UpdateImages"), + Self::RemoveFonts(..) => f.write_str("RemoveFonts"), + Self::AddFontInstance(..) => f.write_str("AddFontInstance"), + Self::AddFont(..) => f.write_str("AddFont"), + Self::GetClientWindowRect(..) => f.write_str("GetClientWindowRect"), + Self::GetScreenSize(..) => f.write_str("GetScreenSize"), + Self::GetAvailableScreenSize(..) => f.write_str("GetAvailableScreenSize"), + } + } +} + +/// A mechanism to send messages from ScriptThread to the parent process' WebRender instance. +#[derive(Clone, Deserialize, Serialize)] +pub struct CrossProcessCompositorApi(pub IpcSender<CrossProcessCompositorMessage>); + +impl CrossProcessCompositorApi { + /// Create a new [`CrossProcessCompositorApi`] struct that does not have a listener on the other + /// end to use for unit testing. + pub fn dummy() -> Self { + let (sender, _) = ipc::channel().unwrap(); + Self(sender) + } + + /// Get the sender for this proxy. + pub fn sender(&self) -> &IpcSender<CrossProcessCompositorMessage> { + &self.0 + } + + /// Inform WebRender of the existence of this pipeline. + pub fn send_initial_transaction(&self, pipeline: WebRenderPipelineId) { + if let Err(e) = self + .0 + .send(CrossProcessCompositorMessage::SendInitialTransaction( + pipeline, + )) + { + warn!("Error sending initial transaction: {}", e); + } + } + + /// Perform a scroll operation. + pub fn send_scroll_node( + &self, + webview_id: WebViewId, + pipeline_id: WebRenderPipelineId, + point: LayoutPoint, + scroll_id: ExternalScrollId, + ) { + if let Err(e) = self.0.send(CrossProcessCompositorMessage::SendScrollNode( + webview_id, + pipeline_id, + point, + scroll_id, + )) { + warn!("Error sending scroll node: {}", e); + } + } + + /// Inform WebRender of a new display list for the given pipeline. + pub fn send_display_list( + &self, + webview_id: WebViewId, + display_list_info: CompositorDisplayListInfo, + list: BuiltDisplayList, + ) { + let (display_list_data, display_list_descriptor) = list.into_data(); + let (display_list_sender, display_list_receiver) = ipc::bytes_channel().unwrap(); + if let Err(e) = self.0.send(CrossProcessCompositorMessage::SendDisplayList { + webview_id, + display_list_info: Box::new(display_list_info), + display_list_descriptor, + display_list_receiver, + }) { + warn!("Error sending display list: {}", e); + } + + if let Err(error) = display_list_sender.send(&display_list_data.items_data) { + warn!("Error sending display list items: {}", error); + } + if let Err(error) = display_list_sender.send(&display_list_data.cache_data) { + warn!("Error sending display list cache data: {}", error); + } + if let Err(error) = display_list_sender.send(&display_list_data.spatial_tree) { + warn!("Error sending display spatial tree: {}", error); + } + } + + /// Perform a hit test operation. Blocks until the operation is complete and + /// and a result is available. + pub fn hit_test( + &self, + pipeline: Option<WebRenderPipelineId>, + point: DevicePoint, + flags: HitTestFlags, + ) -> Vec<CompositorHitTestResult> { + let (sender, receiver) = ipc::channel().unwrap(); + self.0 + .send(CrossProcessCompositorMessage::HitTest( + pipeline, point, flags, sender, + )) + .expect("error sending hit test"); + receiver.recv().expect("error receiving hit test result") + } + + /// Create a new image key. Blocks until the key is available. + pub fn generate_image_key(&self) -> Option<ImageKey> { + let (sender, receiver) = ipc::channel().unwrap(); + self.0 + .send(CrossProcessCompositorMessage::GenerateImageKey(sender)) + .ok()?; + receiver.recv().ok() + } + + pub fn add_image( + &self, + key: ImageKey, + descriptor: ImageDescriptor, + data: SerializableImageData, + ) { + if let Err(e) = self.0.send(CrossProcessCompositorMessage::AddImage( + key, descriptor, data, + )) { + warn!("Error sending image update: {}", e); + } + } + + /// Perform an image resource update operation. + pub fn update_images(&self, updates: Vec<ImageUpdate>) { + if let Err(e) = self + .0 + .send(CrossProcessCompositorMessage::UpdateImages(updates)) + { + warn!("error sending image updates: {}", e); + } + } + + pub fn remove_unused_font_resources( + &self, + keys: Vec<FontKey>, + instance_keys: Vec<FontInstanceKey>, + ) { + if keys.is_empty() && instance_keys.is_empty() { + return; + } + let _ = self.0.send(CrossProcessCompositorMessage::RemoveFonts( + keys, + instance_keys, + )); + } + + pub fn add_font_instance( + &self, + font_instance_key: FontInstanceKey, + font_key: FontKey, + size: f32, + flags: FontInstanceFlags, + ) { + let _x = self.0.send(CrossProcessCompositorMessage::AddFontInstance( + font_instance_key, + font_key, + size, + flags, + )); + } + + pub fn add_font(&self, font_key: FontKey, data: Arc<IpcSharedMemory>, index: u32) { + let _ = self.0.send(CrossProcessCompositorMessage::AddFont( + font_key, data, index, + )); + } + + pub fn add_system_font(&self, font_key: FontKey, handle: NativeFontHandle) { + let _ = self.0.send(CrossProcessCompositorMessage::AddSystemFont( + font_key, handle, + )); + } + + pub fn fetch_font_keys( + &self, + number_of_font_keys: usize, + number_of_font_instance_keys: usize, + ) -> (Vec<FontKey>, Vec<FontInstanceKey>) { + let (sender, receiver) = ipc_channel::ipc::channel().expect("Could not create IPC channel"); + let _ = self.0.send(CrossProcessCompositorMessage::GenerateFontKeys( + number_of_font_keys, + number_of_font_instance_keys, + sender, + )); + receiver.recv().unwrap() + } +} + +/// This trait is used as a bridge between the different GL clients +/// in Servo that handles WebRender ExternalImages and the WebRender +/// ExternalImageHandler API. +// +/// This trait is used to notify lock/unlock messages and get the +/// required info that WR needs. +pub trait WebrenderExternalImageApi { + fn lock(&mut self, id: u64) -> (WebrenderImageSource, UntypedSize2D<i32>); + fn unlock(&mut self, id: u64); +} + +pub enum WebrenderImageSource<'a> { + TextureHandle(u32), + Raw(&'a [u8]), +} + +/// Type of Webrender External Image Handler. +pub enum WebrenderImageHandlerType { + WebGL, + Media, + WebGPU, +} + +/// List of Webrender external images to be shared among all external image +/// consumers (WebGL, Media, WebGPU). +/// It ensures that external image identifiers are unique. +#[derive(Default)] +pub struct WebrenderExternalImageRegistry { + /// Map of all generated external images. + external_images: HashMap<ExternalImageId, WebrenderImageHandlerType>, + /// Id generator for the next external image identifier. + next_image_id: u64, +} + +impl WebrenderExternalImageRegistry { + pub fn next_id(&mut self, handler_type: WebrenderImageHandlerType) -> ExternalImageId { + self.next_image_id += 1; + let key = ExternalImageId(self.next_image_id); + self.external_images.insert(key, handler_type); + key + } + + pub fn remove(&mut self, key: &ExternalImageId) { + self.external_images.remove(key); + } + + pub fn get(&self, key: &ExternalImageId) -> Option<&WebrenderImageHandlerType> { + self.external_images.get(key) + } +} + +/// WebRender External Image Handler implementation. +pub struct WebrenderExternalImageHandlers { + /// WebGL handler. + webgl_handler: Option<Box<dyn WebrenderExternalImageApi>>, + /// Media player handler. + media_handler: Option<Box<dyn WebrenderExternalImageApi>>, + /// WebGPU handler. + webgpu_handler: Option<Box<dyn WebrenderExternalImageApi>>, + /// Webrender external images. + external_images: Arc<Mutex<WebrenderExternalImageRegistry>>, +} + +impl WebrenderExternalImageHandlers { + pub fn new() -> (Self, Arc<Mutex<WebrenderExternalImageRegistry>>) { + let external_images = Arc::new(Mutex::new(WebrenderExternalImageRegistry::default())); + ( + Self { + webgl_handler: None, + media_handler: None, + webgpu_handler: None, + external_images: external_images.clone(), + }, + external_images, + ) + } + + pub fn set_handler( + &mut self, + handler: Box<dyn WebrenderExternalImageApi>, + handler_type: WebrenderImageHandlerType, + ) { + match handler_type { + WebrenderImageHandlerType::WebGL => self.webgl_handler = Some(handler), + WebrenderImageHandlerType::Media => self.media_handler = Some(handler), + WebrenderImageHandlerType::WebGPU => self.webgpu_handler = Some(handler), + } + } +} + +impl ExternalImageHandler for WebrenderExternalImageHandlers { + /// Lock the external image. Then, WR could start to read the + /// image content. + /// The WR client should not change the image content until the + /// unlock() call. + fn lock(&mut self, key: ExternalImageId, _channel_index: u8) -> ExternalImage { + let external_images = self.external_images.lock().unwrap(); + let handler_type = external_images + .get(&key) + .expect("Tried to get unknown external image"); + match handler_type { + WebrenderImageHandlerType::WebGL => { + let (source, size) = self.webgl_handler.as_mut().unwrap().lock(key.0); + let texture_id = match source { + WebrenderImageSource::TextureHandle(b) => b, + _ => panic!("Wrong type"), + }; + ExternalImage { + uv: TexelRect::new(0.0, size.height as f32, size.width as f32, 0.0), + source: ExternalImageSource::NativeTexture(texture_id), + } + }, + WebrenderImageHandlerType::Media => { + let (source, size) = self.media_handler.as_mut().unwrap().lock(key.0); + let texture_id = match source { + WebrenderImageSource::TextureHandle(b) => b, + _ => panic!("Wrong type"), + }; + ExternalImage { + uv: TexelRect::new(0.0, size.height as f32, size.width as f32, 0.0), + source: ExternalImageSource::NativeTexture(texture_id), + } + }, + WebrenderImageHandlerType::WebGPU => { + let (source, size) = self.webgpu_handler.as_mut().unwrap().lock(key.0); + let buffer = match source { + WebrenderImageSource::Raw(b) => b, + _ => panic!("Wrong type"), + }; + ExternalImage { + uv: TexelRect::new(0.0, size.height as f32, size.width as f32, 0.0), + source: ExternalImageSource::RawData(buffer), + } + }, + } + } + + /// Unlock the external image. The WR should not read the image + /// content after this call. + fn unlock(&mut self, key: ExternalImageId, _channel_index: u8) { + let external_images = self.external_images.lock().unwrap(); + let handler_type = external_images + .get(&key) + .expect("Tried to get unknown external image"); + match handler_type { + WebrenderImageHandlerType::WebGL => self.webgl_handler.as_mut().unwrap().unlock(key.0), + WebrenderImageHandlerType::Media => self.media_handler.as_mut().unwrap().unlock(key.0), + WebrenderImageHandlerType::WebGPU => { + self.webgpu_handler.as_mut().unwrap().unlock(key.0) + }, + }; + } +} + +#[derive(Deserialize, Serialize)] +/// Serializable image updates that must be performed by WebRender. +pub enum ImageUpdate { + /// Register a new image. + AddImage(ImageKey, ImageDescriptor, SerializableImageData), + /// Delete a previously registered image registration. + DeleteImage(ImageKey), + /// Update an existing image registration. + UpdateImage(ImageKey, ImageDescriptor, SerializableImageData), +} + +#[derive(Debug, Deserialize, Serialize)] +/// Serialized `ImageData`. It contains IPC byte channel receiver to prevent from loading bytes too +/// slow. +pub enum SerializableImageData { + /// A simple series of bytes, provided by the embedding and owned by WebRender. + /// The format is stored out-of-band, currently in ImageDescriptor. + Raw(IpcSharedMemory), + /// An image owned by the embedding, and referenced by WebRender. This may + /// take the form of a texture or a heap-allocated buffer. + External(ExternalImageData), +} + +impl From<SerializableImageData> for ImageData { + fn from(value: SerializableImageData) -> Self { + match value { + SerializableImageData::Raw(shared_memory) => ImageData::new(shared_memory.to_vec()), + SerializableImageData::External(image) => ImageData::External(image), + } + } +} + +/// A trait that exposes the embedding layer's `WebView` to the Servo renderer. +/// This is to prevent a dependency cycle between the renderer and the embedding +/// layer. +pub trait RendererWebView { + fn id(&self) -> WebViewId; + fn screen_geometry(&self) -> Option<ScreenGeometry>; +} diff --git a/components/shared/compositing/rendering_context.rs b/components/shared/compositing/rendering_context.rs new file mode 100644 index 00000000000..46d72917510 --- /dev/null +++ b/components/shared/compositing/rendering_context.rs @@ -0,0 +1,887 @@ +/* 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/. */ + +#![deny(unsafe_code)] + +use std::cell::{Cell, RefCell, RefMut}; +use std::num::NonZeroU32; +use std::rc::Rc; +use std::sync::Arc; + +use dpi::PhysicalSize; +use euclid::default::{Rect, Size2D as UntypedSize2D}; +use euclid::{Point2D, Size2D}; +use gleam::gl::{self, Gl}; +use glow::NativeFramebuffer; +use image::RgbaImage; +use log::{debug, trace, warn}; +use raw_window_handle::{DisplayHandle, WindowHandle}; +pub use surfman::Error; +use surfman::chains::{PreserveBuffer, SwapChain}; +use surfman::{ + Adapter, Connection, Context, ContextAttributeFlags, ContextAttributes, Device, GLApi, + NativeContext, NativeWidget, Surface, SurfaceAccess, SurfaceInfo, SurfaceTexture, SurfaceType, +}; +use webrender_api::units::{DeviceIntRect, DevicePixel}; + +/// The `RenderingContext` trait defines a set of methods for managing +/// an OpenGL or GLES rendering context. +/// Implementors of this trait are responsible for handling the creation, +/// management, and destruction of the rendering context and its associated +/// resources. +pub trait RenderingContext { + /// Prepare this [`RenderingContext`] to be rendered upon by Servo. For instance, + /// by binding a framebuffer to the current OpenGL context. + fn prepare_for_rendering(&self) {} + /// Read the contents of this [`Renderingcontext`] into an in-memory image. If the + /// image cannot be read (for instance, if no rendering has taken place yet), then + /// `None` is returned. + /// + /// In a double-buffered [`RenderingContext`] this is expected to read from the back + /// buffer. That means that once Servo renders to the context, this should return those + /// results, even before [`RenderingContext::present`] is called. + fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage>; + /// Get the current size of this [`RenderingContext`]. + fn size(&self) -> PhysicalSize<u32>; + /// Get the current size of this [`RenderingContext`] as [`Size2D`]. + fn size2d(&self) -> Size2D<u32, DevicePixel> { + let size = self.size(); + Size2D::new(size.width, size.height) + } + /// Resizes the rendering surface to the given size. + fn resize(&self, size: PhysicalSize<u32>); + /// Presents the rendered frame to the screen. In a double-buffered context, this would + /// swap buffers. + fn present(&self); + /// Makes the context the current OpenGL context for this thread. + /// After calling this function, it is valid to use OpenGL rendering + /// commands. + fn make_current(&self) -> Result<(), Error>; + /// Returns the `gleam` version of the OpenGL or GLES API. + fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl>; + /// Returns the OpenGL or GLES API. + fn glow_gl_api(&self) -> Arc<glow::Context>; + /// Creates a texture from a given surface and returns the surface texture, + /// the OpenGL texture object, and the size of the surface. Default to `None`. + fn create_texture( + &self, + _surface: Surface, + ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> { + None + } + /// Destroys the texture and returns the surface. Default to `None`. + fn destroy_texture(&self, _surface_texture: SurfaceTexture) -> Option<Surface> { + None + } + /// The connection to the display server for WebGL. Default to `None`. + fn connection(&self) -> Option<Connection> { + None + } +} + +/// A rendering context that uses the Surfman library to create and manage +/// the OpenGL context and surface. This struct provides the default implementation +/// of the `RenderingContext` trait, handling the creation, management, and destruction +/// of the rendering context and its associated resources. +/// +/// The `SurfmanRenderingContext` struct encapsulates the necessary data and methods +/// to interact with the Surfman library, including creating surfaces, binding surfaces, +/// resizing surfaces, presenting rendered frames, and managing the OpenGL context state. +struct SurfmanRenderingContext { + gleam_gl: Rc<dyn Gl>, + glow_gl: Arc<glow::Context>, + device: RefCell<Device>, + context: RefCell<Context>, +} + +impl Drop for SurfmanRenderingContext { + fn drop(&mut self) { + let device = &mut self.device.borrow_mut(); + let context = &mut self.context.borrow_mut(); + let _ = device.destroy_context(context); + } +} + +impl SurfmanRenderingContext { + fn new(connection: &Connection, adapter: &Adapter) -> Result<Self, Error> { + let mut device = connection.create_device(adapter)?; + + let flags = ContextAttributeFlags::ALPHA | + ContextAttributeFlags::DEPTH | + ContextAttributeFlags::STENCIL; + let gl_api = connection.gl_api(); + let version = match &gl_api { + GLApi::GLES => surfman::GLVersion { major: 3, minor: 0 }, + GLApi::GL => surfman::GLVersion { major: 3, minor: 2 }, + }; + let context_descriptor = + device.create_context_descriptor(&ContextAttributes { flags, version })?; + let context = device.create_context(&context_descriptor, None)?; + + #[allow(unsafe_code)] + let gleam_gl = { + match gl_api { + GLApi::GL => unsafe { + gl::GlFns::load_with(|func_name| device.get_proc_address(&context, func_name)) + }, + GLApi::GLES => unsafe { + gl::GlesFns::load_with(|func_name| device.get_proc_address(&context, func_name)) + }, + } + }; + + #[allow(unsafe_code)] + let glow_gl = unsafe { + glow::Context::from_loader_function(|function_name| { + device.get_proc_address(&context, function_name) + }) + }; + + Ok(SurfmanRenderingContext { + gleam_gl, + glow_gl: Arc::new(glow_gl), + device: RefCell::new(device), + context: RefCell::new(context), + }) + } + + fn create_surface(&self, surface_type: SurfaceType<NativeWidget>) -> Result<Surface, Error> { + let device = &mut self.device.borrow_mut(); + let context = &self.context.borrow(); + device.create_surface(context, SurfaceAccess::GPUOnly, surface_type) + } + + fn bind_surface(&self, surface: Surface) -> Result<(), Error> { + let device = &self.device.borrow(); + let context = &mut self.context.borrow_mut(); + device + .bind_surface_to_context(context, surface) + .map_err(|(err, mut surface)| { + let _ = device.destroy_surface(context, &mut surface); + err + })?; + Ok(()) + } + + fn create_attached_swap_chain(&self) -> Result<SwapChain<Device>, Error> { + let device = &mut self.device.borrow_mut(); + let context = &mut self.context.borrow_mut(); + SwapChain::create_attached(device, context, SurfaceAccess::GPUOnly) + } + + fn resize_surface(&self, size: PhysicalSize<u32>) -> Result<(), Error> { + let size = Size2D::new(size.width as i32, size.height as i32); + let device = &mut self.device.borrow_mut(); + let context = &mut self.context.borrow_mut(); + + let mut surface = device.unbind_surface_from_context(context)?.unwrap(); + device.resize_surface(context, &mut surface, size)?; + device + .bind_surface_to_context(context, surface) + .map_err(|(err, mut surface)| { + let _ = device.destroy_surface(context, &mut surface); + err + }) + } + + fn present_bound_surface(&self) -> Result<(), Error> { + let device = &self.device.borrow(); + let context = &mut self.context.borrow_mut(); + + let mut surface = device.unbind_surface_from_context(context)?.unwrap(); + device.present_surface(context, &mut surface)?; + device + .bind_surface_to_context(context, surface) + .map_err(|(err, mut surface)| { + let _ = device.destroy_surface(context, &mut surface); + err + }) + } + + #[allow(dead_code)] + fn native_context(&self) -> NativeContext { + let device = &self.device.borrow(); + let context = &self.context.borrow(); + device.native_context(context) + } + + fn framebuffer(&self) -> Option<NativeFramebuffer> { + let device = &self.device.borrow(); + let context = &self.context.borrow(); + device + .context_surface_info(context) + .unwrap_or(None) + .and_then(|info| info.framebuffer_object) + } + + fn prepare_for_rendering(&self) { + let framebuffer_id = self + .framebuffer() + .map_or(0, |framebuffer| framebuffer.0.into()); + self.gleam_gl + .bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_id); + } + + fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> { + let framebuffer_id = self + .framebuffer() + .map_or(0, |framebuffer| framebuffer.0.into()); + Framebuffer::read_framebuffer_to_image(&self.gleam_gl, framebuffer_id, source_rectangle) + } + + fn make_current(&self) -> Result<(), Error> { + let device = &self.device.borrow(); + let context = &mut self.context.borrow(); + device.make_context_current(context) + } + + fn create_texture( + &self, + surface: Surface, + ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> { + let device = &self.device.borrow(); + let context = &mut self.context.borrow_mut(); + let SurfaceInfo { + id: front_buffer_id, + size, + .. + } = device.surface_info(&surface); + debug!("... getting texture for surface {:?}", front_buffer_id); + let surface_texture = device.create_surface_texture(context, surface).unwrap(); + let gl_texture = device + .surface_texture_object(&surface_texture) + .map(|tex| tex.0.get()) + .unwrap_or(0); + Some((surface_texture, gl_texture, size)) + } + + fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> { + let device = &self.device.borrow(); + let context = &mut self.context.borrow_mut(); + device + .destroy_surface_texture(context, surface_texture) + .map_err(|(error, _)| error) + .ok() + } + + fn connection(&self) -> Option<Connection> { + Some(self.device.borrow().connection()) + } +} + +/// A software rendering context that uses a software OpenGL implementation to render +/// Servo. This will generally have bad performance, but can be used in situations where +/// it is more convenient to have consistent, but slower display output. +/// +/// The results of the render can be accessed via [`RenderingContext::read_to_image`]. +pub struct SoftwareRenderingContext { + size: Cell<PhysicalSize<u32>>, + surfman_rendering_info: SurfmanRenderingContext, + swap_chain: SwapChain<Device>, +} + +impl SoftwareRenderingContext { + pub fn new(size: PhysicalSize<u32>) -> Result<Self, Error> { + let connection = Connection::new()?; + let adapter = connection.create_software_adapter()?; + let surfman_rendering_info = SurfmanRenderingContext::new(&connection, &adapter)?; + + let surfman_size = Size2D::new(size.width as i32, size.height as i32); + let surface = + surfman_rendering_info.create_surface(SurfaceType::Generic { size: surfman_size })?; + surfman_rendering_info.bind_surface(surface)?; + surfman_rendering_info.make_current()?; + + let swap_chain = surfman_rendering_info.create_attached_swap_chain()?; + Ok(SoftwareRenderingContext { + size: Cell::new(size), + surfman_rendering_info, + swap_chain, + }) + } +} + +impl Drop for SoftwareRenderingContext { + fn drop(&mut self) { + let device = &mut self.surfman_rendering_info.device.borrow_mut(); + let context = &mut self.surfman_rendering_info.context.borrow_mut(); + let _ = self.swap_chain.destroy(device, context); + } +} + +impl RenderingContext for SoftwareRenderingContext { + fn prepare_for_rendering(&self) { + self.surfman_rendering_info.prepare_for_rendering(); + } + + fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> { + self.surfman_rendering_info.read_to_image(source_rectangle) + } + + fn size(&self) -> PhysicalSize<u32> { + self.size.get() + } + + fn resize(&self, size: PhysicalSize<u32>) { + if self.size.get() == size { + return; + } + + self.size.set(size); + + let device = &mut self.surfman_rendering_info.device.borrow_mut(); + let context = &mut self.surfman_rendering_info.context.borrow_mut(); + let size = Size2D::new(size.width as i32, size.height as i32); + let _ = self.swap_chain.resize(device, context, size); + } + + fn present(&self) { + let device = &mut self.surfman_rendering_info.device.borrow_mut(); + let context = &mut self.surfman_rendering_info.context.borrow_mut(); + let _ = self + .swap_chain + .swap_buffers(device, context, PreserveBuffer::No); + } + + fn make_current(&self) -> Result<(), Error> { + self.surfman_rendering_info.make_current() + } + + fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> { + self.surfman_rendering_info.gleam_gl.clone() + } + + fn glow_gl_api(&self) -> Arc<glow::Context> { + self.surfman_rendering_info.glow_gl.clone() + } + + fn create_texture( + &self, + surface: Surface, + ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> { + self.surfman_rendering_info.create_texture(surface) + } + + fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> { + self.surfman_rendering_info.destroy_texture(surface_texture) + } + + fn connection(&self) -> Option<Connection> { + self.surfman_rendering_info.connection() + } +} + +/// A [`RenderingContext`] that uses the `surfman` library to render to a +/// `raw-window-handle` identified window. `surfman` will attempt to create an +/// OpenGL context and surface for this window. This is a simple implementation +/// of the [`RenderingContext`] crate, but by default it paints to the entire window +/// surface. +/// +/// If you would like to paint to only a portion of the window, consider using +/// [`OffscreenRenderingContext`] by calling [`WindowRenderingContext::offscreen_context`]. +pub struct WindowRenderingContext { + size: Cell<PhysicalSize<u32>>, + surfman_context: SurfmanRenderingContext, +} + +impl WindowRenderingContext { + pub fn new( + display_handle: DisplayHandle, + window_handle: WindowHandle, + size: PhysicalSize<u32>, + ) -> Result<Self, Error> { + let connection = Connection::from_display_handle(display_handle)?; + let adapter = connection.create_adapter()?; + let surfman_context = SurfmanRenderingContext::new(&connection, &adapter)?; + + let native_widget = connection + .create_native_widget_from_window_handle( + window_handle, + Size2D::new(size.width as i32, size.height as i32), + ) + .expect("Failed to create native widget"); + + let surface = surfman_context.create_surface(SurfaceType::Widget { native_widget })?; + surfman_context.bind_surface(surface)?; + surfman_context.make_current()?; + + Ok(Self { + size: Cell::new(size), + surfman_context, + }) + } + + pub fn offscreen_context( + self: &Rc<Self>, + size: PhysicalSize<u32>, + ) -> OffscreenRenderingContext { + OffscreenRenderingContext::new(self.clone(), size) + } + + /// Stop rendering to the window that was used to create this `WindowRenderingContext` + /// or last set with [`Self::set_window`]. + /// + /// TODO: This should be removed once `WebView`s can replace their `RenderingContext`s. + pub fn take_window(&self) -> Result<(), Error> { + let device = self.surfman_context.device.borrow_mut(); + let mut context = self.surfman_context.context.borrow_mut(); + let mut surface = device.unbind_surface_from_context(&mut context)?.unwrap(); + device.destroy_surface(&mut context, &mut surface)?; + Ok(()) + } + + /// Replace the window that this [`WindowRenderingContext`] renders to and give it a new + /// size. + /// + /// TODO: This should be removed once `WebView`s can replace their `RenderingContext`s. + pub fn set_window( + &self, + window_handle: WindowHandle, + size: PhysicalSize<u32>, + ) -> Result<(), Error> { + let mut device = self.surfman_context.device.borrow_mut(); + let mut context = self.surfman_context.context.borrow_mut(); + + let native_widget = device + .connection() + .create_native_widget_from_window_handle( + window_handle, + Size2D::new(size.width as i32, size.height as i32), + ) + .expect("Failed to create native widget"); + + let surface_access = SurfaceAccess::GPUOnly; + let surface_type = SurfaceType::Widget { native_widget }; + let surface = device.create_surface(&context, surface_access, surface_type)?; + + device + .bind_surface_to_context(&mut context, surface) + .map_err(|(err, mut surface)| { + let _ = device.destroy_surface(&mut context, &mut surface); + err + })?; + device.make_context_current(&context)?; + Ok(()) + } + + pub fn surfman_details(&self) -> (RefMut<Device>, RefMut<Context>) { + ( + self.surfman_context.device.borrow_mut(), + self.surfman_context.context.borrow_mut(), + ) + } +} + +impl RenderingContext for WindowRenderingContext { + fn prepare_for_rendering(&self) { + self.surfman_context.prepare_for_rendering(); + } + + fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> { + self.surfman_context.read_to_image(source_rectangle) + } + + fn size(&self) -> PhysicalSize<u32> { + self.size.get() + } + + fn resize(&self, size: PhysicalSize<u32>) { + match self.surfman_context.resize_surface(size) { + Ok(..) => self.size.set(size), + Err(error) => warn!("Error resizing surface: {error:?}"), + } + } + + fn present(&self) { + if let Err(error) = self.surfman_context.present_bound_surface() { + warn!("Error presenting surface: {error:?}"); + } + } + + fn make_current(&self) -> Result<(), Error> { + self.surfman_context.make_current() + } + + fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> { + self.surfman_context.gleam_gl.clone() + } + + fn glow_gl_api(&self) -> Arc<glow::Context> { + self.surfman_context.glow_gl.clone() + } + + fn create_texture( + &self, + surface: Surface, + ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> { + self.surfman_context.create_texture(surface) + } + + fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> { + self.surfman_context.destroy_texture(surface_texture) + } + + fn connection(&self) -> Option<Connection> { + self.surfman_context.connection() + } +} + +struct Framebuffer { + gl: Rc<dyn Gl>, + framebuffer_id: gl::GLuint, + renderbuffer_id: gl::GLuint, + texture_id: gl::GLuint, +} + +impl Framebuffer { + fn bind(&self) { + trace!("Binding FBO {}", self.framebuffer_id); + self.gl + .bind_framebuffer(gl::FRAMEBUFFER, self.framebuffer_id) + } +} + +impl Drop for Framebuffer { + fn drop(&mut self) { + self.gl.bind_framebuffer(gl::FRAMEBUFFER, 0); + self.gl.delete_textures(&[self.texture_id]); + self.gl.delete_renderbuffers(&[self.renderbuffer_id]); + self.gl.delete_framebuffers(&[self.framebuffer_id]); + } +} + +impl Framebuffer { + fn new(gl: Rc<dyn Gl>, size: PhysicalSize<u32>) -> Self { + let framebuffer_ids = gl.gen_framebuffers(1); + gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]); + + let texture_ids = gl.gen_textures(1); + gl.bind_texture(gl::TEXTURE_2D, texture_ids[0]); + gl.tex_image_2d( + gl::TEXTURE_2D, + 0, + gl::RGBA as gl::GLint, + size.width as gl::GLsizei, + size.height as gl::GLsizei, + 0, + gl::RGBA, + gl::UNSIGNED_BYTE, + None, + ); + gl.tex_parameter_i( + gl::TEXTURE_2D, + gl::TEXTURE_MAG_FILTER, + gl::NEAREST as gl::GLint, + ); + gl.tex_parameter_i( + gl::TEXTURE_2D, + gl::TEXTURE_MIN_FILTER, + gl::NEAREST as gl::GLint, + ); + + gl.framebuffer_texture_2d( + gl::FRAMEBUFFER, + gl::COLOR_ATTACHMENT0, + gl::TEXTURE_2D, + texture_ids[0], + 0, + ); + + gl.bind_texture(gl::TEXTURE_2D, 0); + + let renderbuffer_ids = gl.gen_renderbuffers(1); + let depth_rb = renderbuffer_ids[0]; + gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb); + gl.renderbuffer_storage( + gl::RENDERBUFFER, + gl::DEPTH_COMPONENT24, + size.width as gl::GLsizei, + size.height as gl::GLsizei, + ); + gl.framebuffer_renderbuffer( + gl::FRAMEBUFFER, + gl::DEPTH_ATTACHMENT, + gl::RENDERBUFFER, + depth_rb, + ); + + Self { + gl, + framebuffer_id: *framebuffer_ids + .first() + .expect("Guaranteed by GL operations"), + renderbuffer_id: *renderbuffer_ids + .first() + .expect("Guaranteed by GL operations"), + texture_id: *texture_ids.first().expect("Guaranteed by GL operations"), + } + } + + fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> { + Self::read_framebuffer_to_image(&self.gl, self.framebuffer_id, source_rectangle) + } + + fn read_framebuffer_to_image( + gl: &Rc<dyn Gl>, + framebuffer_id: u32, + source_rectangle: DeviceIntRect, + ) -> Option<RgbaImage> { + gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_id); + + // For some reason, OSMesa fails to render on the 3rd + // attempt in headless mode, under some conditions. + // I think this can only be some kind of synchronization + // bug in OSMesa, but explicitly un-binding any vertex + // array here seems to work around that bug. + // See https://github.com/servo/servo/issues/18606. + gl.bind_vertex_array(0); + + let mut pixels = gl.read_pixels( + source_rectangle.min.x, + source_rectangle.min.y, + source_rectangle.width(), + source_rectangle.height(), + gl::RGBA, + gl::UNSIGNED_BYTE, + ); + let gl_error = gl.get_error(); + if gl_error != gl::NO_ERROR { + warn!("GL error code 0x{gl_error:x} set after read_pixels"); + } + + // flip image vertically (texture is upside down) + let source_rectangle = source_rectangle.to_usize(); + let orig_pixels = pixels.clone(); + let stride = source_rectangle.width() * 4; + for y in 0..source_rectangle.height() { + let dst_start = y * stride; + let src_start = (source_rectangle.height() - y - 1) * stride; + let src_slice = &orig_pixels[src_start..src_start + stride]; + pixels[dst_start..dst_start + stride].clone_from_slice(&src_slice[..stride]); + } + + RgbaImage::from_raw( + source_rectangle.width() as u32, + source_rectangle.height() as u32, + pixels, + ) + } +} + +pub struct OffscreenRenderingContext { + parent_context: Rc<WindowRenderingContext>, + size: Cell<PhysicalSize<u32>>, + framebuffer: RefCell<Framebuffer>, +} + +type RenderToParentCallback = Box<dyn Fn(&glow::Context, Rect<i32>) + Send + Sync>; + +impl OffscreenRenderingContext { + fn new(parent_context: Rc<WindowRenderingContext>, size: PhysicalSize<u32>) -> Self { + let framebuffer = RefCell::new(Framebuffer::new(parent_context.gleam_gl_api(), size)); + Self { + parent_context, + size: Cell::new(size), + framebuffer, + } + } + + pub fn parent_context(&self) -> &WindowRenderingContext { + &self.parent_context + } + + pub fn render_to_parent_callback(&self) -> Option<RenderToParentCallback> { + // Don't accept a `None` context for the source framebuffer. + let front_framebuffer_id = + NonZeroU32::new(self.framebuffer.borrow().framebuffer_id).map(NativeFramebuffer)?; + let parent_context_framebuffer_id = self.parent_context.surfman_context.framebuffer(); + let size = self.size.get(); + let size = Size2D::new(size.width as i32, size.height as i32); + Some(Box::new(move |gl, target_rect| { + Self::blit_framebuffer( + gl, + Rect::new(Point2D::origin(), size.to_i32()), + front_framebuffer_id, + target_rect, + parent_context_framebuffer_id, + ); + })) + } + + #[allow(unsafe_code)] + fn blit_framebuffer( + gl: &glow::Context, + source_rect: Rect<i32>, + source_framebuffer_id: NativeFramebuffer, + target_rect: Rect<i32>, + target_framebuffer_id: Option<NativeFramebuffer>, + ) { + use glow::HasContext as _; + unsafe { + gl.clear_color(0.0, 0.0, 0.0, 0.0); + gl.scissor( + target_rect.origin.x, + target_rect.origin.y, + target_rect.width(), + target_rect.height(), + ); + gl.enable(gl::SCISSOR_TEST); + gl.clear(gl::COLOR_BUFFER_BIT); + gl.disable(gl::SCISSOR_TEST); + + gl.bind_framebuffer(gl::READ_FRAMEBUFFER, Some(source_framebuffer_id)); + gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, target_framebuffer_id); + + gl.blit_framebuffer( + source_rect.origin.x, + source_rect.origin.y, + source_rect.origin.x + source_rect.width(), + source_rect.origin.y + source_rect.height(), + target_rect.origin.x, + target_rect.origin.y, + target_rect.origin.x + target_rect.width(), + target_rect.origin.y + target_rect.height(), + gl::COLOR_BUFFER_BIT, + gl::NEAREST, + ); + gl.bind_framebuffer(gl::FRAMEBUFFER, target_framebuffer_id); + } + } +} + +impl RenderingContext for OffscreenRenderingContext { + fn size(&self) -> PhysicalSize<u32> { + self.size.get() + } + + fn resize(&self, new_size: PhysicalSize<u32>) { + let old_size = self.size.get(); + if old_size == new_size { + return; + } + + let gl = self.parent_context.gleam_gl_api(); + let new_framebuffer = Framebuffer::new(gl.clone(), new_size); + + let old_framebuffer = + std::mem::replace(&mut *self.framebuffer.borrow_mut(), new_framebuffer); + self.size.set(new_size); + + let blit_size = new_size.min(old_size); + let rect = Rect::new( + Point2D::origin(), + Size2D::new(blit_size.width, blit_size.height), + ) + .to_i32(); + + let Some(old_framebuffer_id) = + NonZeroU32::new(old_framebuffer.framebuffer_id).map(NativeFramebuffer) + else { + return; + }; + let new_framebuffer_id = + NonZeroU32::new(self.framebuffer.borrow().framebuffer_id).map(NativeFramebuffer); + Self::blit_framebuffer( + &self.glow_gl_api(), + rect, + old_framebuffer_id, + rect, + new_framebuffer_id, + ); + } + + fn prepare_for_rendering(&self) { + self.framebuffer.borrow().bind(); + } + + fn present(&self) {} + + fn make_current(&self) -> Result<(), surfman::Error> { + self.parent_context.make_current() + } + + fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> { + self.parent_context.gleam_gl_api() + } + + fn glow_gl_api(&self) -> Arc<glow::Context> { + self.parent_context.glow_gl_api() + } + + fn create_texture( + &self, + surface: Surface, + ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> { + self.parent_context.create_texture(surface) + } + + fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> { + self.parent_context.destroy_texture(surface_texture) + } + + fn connection(&self) -> Option<Connection> { + self.parent_context.connection() + } + + fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> { + self.framebuffer.borrow().read_to_image(source_rectangle) + } +} + +#[cfg(test)] +mod test { + use dpi::PhysicalSize; + use euclid::{Box2D, Point2D, Size2D}; + use gleam::gl; + use image::Rgba; + use surfman::{Connection, ContextAttributeFlags, ContextAttributes, Error, GLApi, GLVersion}; + + use super::Framebuffer; + + #[test] + #[allow(unsafe_code)] + fn test_read_pixels() -> Result<(), Error> { + let connection = Connection::new()?; + let adapter = connection.create_software_adapter()?; + let mut device = connection.create_device(&adapter)?; + let context_descriptor = device.create_context_descriptor(&ContextAttributes { + version: GLVersion::new(3, 0), + flags: ContextAttributeFlags::empty(), + })?; + let mut context = device.create_context(&context_descriptor, None)?; + + let gl = match connection.gl_api() { + GLApi::GL => unsafe { gl::GlFns::load_with(|s| device.get_proc_address(&context, s)) }, + GLApi::GLES => unsafe { + gl::GlesFns::load_with(|s| device.get_proc_address(&context, s)) + }, + }; + + device.make_context_current(&context)?; + + { + const SIZE: u32 = 16; + let framebuffer = Framebuffer::new(gl, PhysicalSize::new(SIZE, SIZE)); + framebuffer.bind(); + framebuffer + .gl + .clear_color(12.0 / 255.0, 34.0 / 255.0, 56.0 / 255.0, 78.0 / 255.0); + framebuffer.gl.clear(gl::COLOR_BUFFER_BIT); + + let rect = Box2D::from_origin_and_size(Point2D::zero(), Size2D::new(SIZE, SIZE)); + let img = framebuffer + .read_to_image(rect.to_i32()) + .expect("Should have been able to read back image."); + assert_eq!(img.width(), SIZE); + assert_eq!(img.height(), SIZE); + + let expected_pixel: Rgba<u8> = Rgba([12, 34, 56, 78]); + assert!(img.pixels().all(|&p| p == expected_pixel)); + } + + device.destroy_context(&mut context)?; + + Ok(()) + } +} diff --git a/components/shared/compositing/tests/compositor.rs b/components/shared/compositing/tests/compositor.rs new file mode 100644 index 00000000000..4d2ecfd99c9 --- /dev/null +++ b/components/shared/compositing/tests/compositor.rs @@ -0,0 +1,188 @@ +/* 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 base::id::ScrollTreeNodeId; +use compositing_traits::display_list::{ + AxesScrollSensitivity, ScrollSensitivity, ScrollTree, ScrollableNodeInfo, +}; +use euclid::Size2D; +use webrender_api::units::LayoutVector2D; +use webrender_api::{ExternalScrollId, PipelineId, ScrollLocation, SpatialId}; + +fn add_mock_scroll_node(tree: &mut ScrollTree) -> ScrollTreeNodeId { + let pipeline_id = PipelineId(0, 0); + let num_nodes = tree.nodes.len(); + let parent = if num_nodes > 0 { + Some(ScrollTreeNodeId { + index: num_nodes - 1, + spatial_id: SpatialId::new(num_nodes - 1, pipeline_id), + }) + } else { + None + }; + + tree.add_scroll_tree_node( + parent.as_ref(), + SpatialId::new(num_nodes, pipeline_id), + Some(ScrollableNodeInfo { + external_id: ExternalScrollId(num_nodes as u64, pipeline_id), + scrollable_size: Size2D::new(100.0, 100.0), + scroll_sensitivity: AxesScrollSensitivity { + x: ScrollSensitivity::ScriptAndInputEvents, + y: ScrollSensitivity::ScriptAndInputEvents, + }, + offset: LayoutVector2D::zero(), + }), + ) +} + +#[test] +fn test_scroll_tree_simple_scroll() { + let mut scroll_tree = ScrollTree::default(); + let pipeline_id = PipelineId(0, 0); + let id = add_mock_scroll_node(&mut scroll_tree); + + let (scrolled_id, offset) = scroll_tree + .scroll_node_or_ancestor( + &id, + ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)), + ) + .unwrap(); + let expected_offset = LayoutVector2D::new(-20.0, -40.0); + assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id)); + assert_eq!(offset, expected_offset); + assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset)); + + let (scrolled_id, offset) = scroll_tree + .scroll_node_or_ancestor(&id, ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0))) + .unwrap(); + let expected_offset = LayoutVector2D::new(0.0, 0.0); + assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id)); + assert_eq!(offset, expected_offset); + assert_eq!(scroll_tree.get_node(&id).offset(), Some(expected_offset)); + + // Scroll offsets must be negative. + let result = scroll_tree + .scroll_node_or_ancestor(&id, ScrollLocation::Delta(LayoutVector2D::new(20.0, 40.0))); + assert!(result.is_none()); + assert_eq!( + scroll_tree.get_node(&id).offset(), + Some(LayoutVector2D::new(0.0, 0.0)) + ); +} + +#[test] +fn test_scroll_tree_simple_scroll_chaining() { + let mut scroll_tree = ScrollTree::default(); + + let pipeline_id = PipelineId(0, 0); + let parent_id = add_mock_scroll_node(&mut scroll_tree); + let unscrollable_child_id = + scroll_tree.add_scroll_tree_node(Some(&parent_id), SpatialId::new(1, pipeline_id), None); + + let (scrolled_id, offset) = scroll_tree + .scroll_node_or_ancestor( + &unscrollable_child_id, + ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)), + ) + .unwrap(); + let expected_offset = LayoutVector2D::new(-20.0, -40.0); + assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id)); + assert_eq!(offset, expected_offset); + assert_eq!( + scroll_tree.get_node(&parent_id).offset(), + Some(expected_offset) + ); + + let (scrolled_id, offset) = scroll_tree + .scroll_node_or_ancestor( + &unscrollable_child_id, + ScrollLocation::Delta(LayoutVector2D::new(-10.0, -15.0)), + ) + .unwrap(); + let expected_offset = LayoutVector2D::new(-30.0, -55.0); + assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id)); + assert_eq!(offset, expected_offset); + assert_eq!( + scroll_tree.get_node(&parent_id).offset(), + Some(expected_offset) + ); + assert_eq!(scroll_tree.get_node(&unscrollable_child_id).offset(), None); +} + +#[test] +fn test_scroll_tree_chain_when_at_extent() { + let mut scroll_tree = ScrollTree::default(); + + let pipeline_id = PipelineId(0, 0); + let parent_id = add_mock_scroll_node(&mut scroll_tree); + let child_id = add_mock_scroll_node(&mut scroll_tree); + + let (scrolled_id, offset) = scroll_tree + .scroll_node_or_ancestor(&child_id, ScrollLocation::End) + .unwrap(); + + let expected_offset = LayoutVector2D::new(0.0, -100.0); + assert_eq!(scrolled_id, ExternalScrollId(1, pipeline_id)); + assert_eq!(offset, expected_offset); + assert_eq!( + scroll_tree.get_node(&child_id).offset(), + Some(expected_offset) + ); + + // The parent will have scrolled because the child is already at the extent + // of its scroll area in the y axis. + let (scrolled_id, offset) = scroll_tree + .scroll_node_or_ancestor( + &child_id, + ScrollLocation::Delta(LayoutVector2D::new(0.0, -10.0)), + ) + .unwrap(); + let expected_offset = LayoutVector2D::new(0.0, -10.0); + assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id)); + assert_eq!(offset, expected_offset); + assert_eq!( + scroll_tree.get_node(&parent_id).offset(), + Some(expected_offset) + ); +} + +#[test] +fn test_scroll_tree_chain_through_overflow_hidden() { + let mut scroll_tree = ScrollTree::default(); + + // Create a tree with a scrollable leaf, but make its `scroll_sensitivity` + // reflect `overflow: hidden` ie not responsive to non-script scroll events. + let pipeline_id = PipelineId(0, 0); + let parent_id = add_mock_scroll_node(&mut scroll_tree); + let overflow_hidden_id = add_mock_scroll_node(&mut scroll_tree); + scroll_tree + .get_node_mut(&overflow_hidden_id) + .scroll_info + .as_mut() + .map(|info| { + info.scroll_sensitivity = AxesScrollSensitivity { + x: ScrollSensitivity::Script, + y: ScrollSensitivity::Script, + }; + }); + + let (scrolled_id, offset) = scroll_tree + .scroll_node_or_ancestor( + &overflow_hidden_id, + ScrollLocation::Delta(LayoutVector2D::new(-20.0, -40.0)), + ) + .unwrap(); + let expected_offset = LayoutVector2D::new(-20.0, -40.0); + assert_eq!(scrolled_id, ExternalScrollId(0, pipeline_id)); + assert_eq!(offset, expected_offset); + assert_eq!( + scroll_tree.get_node(&parent_id).offset(), + Some(expected_offset) + ); + assert_eq!( + scroll_tree.get_node(&overflow_hidden_id).offset(), + Some(LayoutVector2D::new(0.0, 0.0)) + ); +} |