compositing: Combine webrender_traits and compositing_traits (#36372)

These two traits both exposed different parts of the compositing API,
but now that the compositor doesn't depend directly on `script` any
longer and the `script_traits` crate has been split into the
`constellation_traits` crate, this can be finally be cleaned up without
causing circular dependencies. In addition, some unit tests for the
`IOPCompositor`'s scroll node tree are also moved into
`compositing_traits` as well.

Testing: This just combines two crates, so no new tests are necessary.
Fixes: #35984.
Signed-off-by: Martin Robinson <mrobinson@igalia.com>

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-04-06 19:34:18 +02:00 committed by GitHub
parent e74a042efd
commit 0caa271176
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 582 additions and 637 deletions

View file

@ -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 }

View file

@ -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
}
}

View file

@ -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>;
}

View file

@ -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(())
}
}

View file

@ -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))
);
}