From 17f3c45d4ff597dc1e179d89784bb5f57b4c03d7 Mon Sep 17 00:00:00 2001 From: Delan Azabani Date: Wed, 13 Dec 2023 10:49:25 +0800 Subject: [PATCH] Add initial support for offscreen rendering (#30767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Offscreen rendering * shared memory case never actually rendered to backbuffer * fix compile errors (in theory) when gl crate feature disabled * update doc comments * remove dark CentralPanel border covering edges of viewport * clear to transparent, to avoid pink artifacts * fix mouse input for browser being consumed by egui * avoid destroying OpenGL resources unless resizing window * clean up compositing::gl * fix flickering around edges after resizing window * unset invalidate_last_render_target after invalidating * fix incorrect DRAW_FRAMEBUFFER name when blitting * bind the widget surface fbo before painting egui * make composite_specific_target take CompositeTarget, not Option * compositing: remove cargo feature “gl” * capitalise FBO in bind log message Co-authored-by: Martin Robinson * capitalise FBO in drop log message Co-authored-by: Martin Robinson * rename RenderTargetInfo fields and use OnceCell for next field * rename RenderTargetInfo.read to read_back_from_gpu * document servo_framebuffer_id in Minibrowser::update * rename needs_fbo to use_offscreen_framebuffer * capitalise FBO in unbind log message * clarify the purpose of Minibrowser::on_event * fix unused_must_use warning * reduce nesting in Minibrowser::update * use implicit format argument in panic * store Minibrowser.widget_surface_fbo as glow type * explain why servo_framebuffer_id is None in first call site * rename output_framebuffer_id to offscreen_framebuffer_id --------- Co-authored-by: Martin Robinson --- Cargo.lock | 1 + components/compositing/Cargo.toml | 6 +- components/compositing/compositor.rs | 250 ++++++++++++++++----------- components/compositing/gl.rs | 228 +++++++++++++----------- components/compositing/lib.rs | 3 +- components/servo/Cargo.toml | 2 +- components/servo/lib.rs | 17 +- ports/servoshell/Cargo.toml | 2 +- ports/servoshell/app.rs | 28 ++- ports/servoshell/geometry.rs | 15 ++ ports/servoshell/headed_window.rs | 9 +- ports/servoshell/main.rs | 1 + ports/servoshell/minibrowser.rs | 209 +++++++++++++++++----- 13 files changed, 498 insertions(+), 273 deletions(-) create mode 100644 ports/servoshell/geometry.rs diff --git a/Cargo.lock b/Cargo.lock index 77d3da67f8d..49087886be7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -919,6 +919,7 @@ name = "compositing" version = "0.0.1" dependencies = [ "canvas", + "cfg-if 1.0.0", "compositing_traits", "crossbeam-channel", "embedder_traits", diff --git a/components/compositing/Cargo.toml b/components/compositing/Cargo.toml index 67fd9f3e9af..ec2d44baea0 100644 --- a/components/compositing/Cargo.toml +++ b/components/compositing/Cargo.toml @@ -13,18 +13,18 @@ path = "lib.rs" [features] default = [] -gl = ["gleam", "pixels"] multiview = [] [dependencies] canvas = { path = "../canvas" } +cfg-if = { workspace = true } compositing_traits = { workspace = true } crossbeam-channel = { workspace = true } embedder_traits = { workspace = true } euclid = { workspace = true } fnv = { workspace = true } gfx_traits = { workspace = true } -gleam = { workspace = true, optional = true } +gleam = { workspace = true } image = { workspace = true } ipc-channel = { workspace = true } keyboard-types = { workspace = true } @@ -33,7 +33,7 @@ log = { workspace = true } msg = { workspace = true } net_traits = { workspace = true } num-traits = { workspace = true } -pixels = { path = "../pixels", optional = true } +pixels = { path = "../pixels" } profile_traits = { workspace = true } script_traits = { workspace = true } servo_config = { path = "../config" } diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index 49f869601da..d0af82067fc 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -2,6 +2,7 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ +use std::cell::OnceCell; use std::collections::HashMap; use std::env; use std::fs::{create_dir_all, File}; @@ -21,17 +22,15 @@ use embedder_traits::Cursor; use euclid::{Point2D, Rect, Scale, Transform3D, Vector2D}; use fnv::{FnvHashMap, FnvHashSet}; use gfx_traits::{Epoch, FontData, WebRenderEpochToU16}; -#[cfg(feature = "gl")] use image::{DynamicImage, ImageFormat}; use ipc_channel::ipc; use libc::c_void; -use log::{debug, error, info, warn}; +use log::{debug, error, info, trace, warn}; use msg::constellation_msg::{ PipelineId, PipelineIndex, PipelineNamespaceId, TopLevelBrowsingContextId, }; use net_traits::image::base::Image; use net_traits::image_cache::CorsStatus; -#[cfg(feature = "gl")] use pixels::PixelFormat; use profile_traits::time::{self as profile_time, profile, ProfilerCategory}; use script_traits::compositor::{HitTestInfo, ScrollTree}; @@ -57,13 +56,12 @@ use webrender_api::{ }; use webrender_surfman::WebrenderSurfman; -#[cfg(feature = "gl")] -use crate::gl; +use crate::gl::RenderTargetInfo; use crate::touch::{TouchAction, TouchHandler}; use crate::windowing::{ self, EmbedderCoordinates, MouseWindowEvent, WebRenderDebugOption, WindowMethods, }; -use crate::InitialCompositorState; +use crate::{gl, InitialCompositorState}; #[derive(Debug, PartialEq)] enum UnableToComposite { @@ -225,7 +223,18 @@ pub struct IOCompositor { /// Current cursor position. cursor_pos: DevicePoint, - output_file: Option, + /// Offscreen framebuffer object to render our next frame to. + /// We use this and `prev_offscreen_framebuffer` for double buffering when compositing to + /// [`CompositeTarget::Fbo`]. + next_offscreen_framebuffer: OnceCell, + + /// Offscreen framebuffer object for our most-recently-completed frame. + /// We use this and `next_offscreen_framebuffer` for double buffering when compositing to + /// [`CompositeTarget::Fbo`]. + prev_offscreen_framebuffer: Option, + + /// Whether to invalidate `prev_offscreen_framebuffer` at the end of the next frame. + invalidate_prev_offscreen_framebuffer: bool, is_running_problem_test: bool, @@ -339,33 +348,32 @@ impl PipelineDetails { } } -#[derive(Clone, Copy, Debug, PartialEq)] -enum CompositeTarget { - /// Normal composition to a window +#[derive(Clone, Debug, PartialEq)] +pub enum CompositeTarget { + /// Draw directly to a window. Window, - /// Compose as normal, but also return a PNG of the composed output - WindowAndPng, + /// Draw to an offscreen OpenGL framebuffer object, which can be retrieved once complete at + /// [`IOCompositor::offscreen_framebuffer_id`]. + Fbo, - /// Compose to a PNG, write it to disk, and then exit the browser (used for reftests) - PngFile, + /// Draw to an uncompressed image in shared memory. + SharedMemory, + + /// Draw to a PNG file on disk, then exit the browser (for reftests). + PngFile(Rc), } impl IOCompositor { fn new( window: Rc, state: InitialCompositorState, - output_file: Option, + composite_target: CompositeTarget, is_running_problem_test: bool, exit_after_load: bool, convert_mouse_to_touch: bool, top_level_browsing_context_id: TopLevelBrowsingContextId, ) -> Self { - let composite_target = match output_file { - Some(_) => CompositeTarget::PngFile, - None => CompositeTarget::Window, - }; - IOCompositor { embedder_coordinates: window.get_coordinates(), window, @@ -401,7 +409,9 @@ impl IOCompositor { pending_paint_metrics: HashMap::new(), cursor: Cursor::None, cursor_pos: DevicePoint::new(0.0, 0.0), - output_file, + next_offscreen_framebuffer: OnceCell::new(), + prev_offscreen_framebuffer: None, + invalidate_prev_offscreen_framebuffer: false, is_running_problem_test, exit_after_load, convert_mouse_to_touch, @@ -413,7 +423,7 @@ impl IOCompositor { pub fn create( window: Rc, state: InitialCompositorState, - output_file: Option, + composite_target: CompositeTarget, is_running_problem_test: bool, exit_after_load: bool, convert_mouse_to_touch: bool, @@ -422,7 +432,7 @@ impl IOCompositor { let mut compositor = IOCompositor::new( window, state, - output_file, + composite_target, is_running_problem_test, exit_after_load, convert_mouse_to_touch, @@ -525,7 +535,7 @@ impl IOCompositor { }, (CompositorMsg::CreatePng(rect, reply), ShutdownState::NotShuttingDown) => { - let res = self.composite_specific_target(CompositeTarget::WindowAndPng, rect); + let res = self.composite_specific_target(CompositeTarget::SharedMemory, rect); if let Err(ref e) = res { info!("Error retrieving PNG: {:?}", e); } @@ -592,7 +602,9 @@ impl IOCompositor { (CompositorMsg::LoadComplete(_), ShutdownState::NotShuttingDown) => { // If we're painting in headless mode, schedule a recomposite. - if self.output_file.is_some() || self.exit_after_load { + if matches!(self.composite_target, CompositeTarget::PngFile(_)) || + self.exit_after_load + { self.composite_if_necessary(CompositingReason::Headless); } }, @@ -1050,7 +1062,7 @@ impl IOCompositor { } pub fn on_resize_window_event(&mut self) -> bool { - debug!("compositor resize requested"); + trace!("Compositor resize requested"); let old_coords = self.embedder_coordinates; self.embedder_coordinates = self.window.get_coordinates(); @@ -1060,6 +1072,13 @@ impl IOCompositor { self.update_zoom_transform(); } + // If the framebuffer size has changed, invalidate the current framebuffer object, and mark + // the last framebuffer object as needing to be invalidated at the end of the next frame. + if self.embedder_coordinates.framebuffer != old_coords.framebuffer { + self.next_offscreen_framebuffer = OnceCell::new(); + self.invalidate_prev_offscreen_framebuffer = true; + } + if self.embedder_coordinates.viewport == old_coords.viewport { return false; } @@ -1491,7 +1510,7 @@ impl IOCompositor { } fn hidpi_factor(&self) -> Scale { - if self.output_file.is_some() { + if matches!(self.composite_target, CompositeTarget::PngFile(_)) { return Scale::new(1.0); } self.embedder_coordinates.hidpi_factor @@ -1636,10 +1655,11 @@ impl IOCompositor { } pub fn composite(&mut self) { - let target = self.composite_target; - match self.composite_specific_target(target, None) { + match self.composite_specific_target(self.composite_target.clone(), None) { Ok(_) => { - if self.output_file.is_some() || self.exit_after_load { + if matches!(self.composite_target, CompositeTarget::PngFile(_)) || + self.exit_after_load + { println!("Shutting down the Constellation after generating an output file or exit flag specified"); self.start_shutting_down(); } @@ -1656,11 +1676,10 @@ impl IOCompositor { } } - /// Composite either to the screen or to a png image or both. - /// Returns Ok if composition was performed or Err if it was not possible to composite - /// for some reason. If CompositeTarget is Window or Png no image data is returned; - /// in the latter case the image is written directly to a file. If CompositeTarget - /// is WindowAndPng Ok(Some(png::Image)) is returned. + /// Composite to the given target if any, or the current target otherwise. + /// Returns Ok if composition was performed or Err if it was not possible to composite for some + /// reason. When the target is [CompositeTarget::SharedMemory], the image is read back from the + /// GPU and returned as Ok(Some(png::Image)), otherwise we return Ok(None). fn composite_specific_target( &mut self, target: CompositeTarget, @@ -1680,23 +1699,16 @@ impl IOCompositor { } self.assert_no_gl_error(); - // Bind the webrender framebuffer - let framebuffer_object = self - .webrender_surfman - .context_surface_info() - .unwrap_or(None) - .map(|info| info.framebuffer_object) - .unwrap_or(0); - self.webrender_gl - .bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object); - self.assert_gl_framebuffer_complete(); - self.webrender.update(); - let wait_for_stable_image = match target { - CompositeTarget::WindowAndPng | CompositeTarget::PngFile => true, - CompositeTarget::Window => self.exit_after_load, - }; + let wait_for_stable_image = matches!( + target, + CompositeTarget::SharedMemory | CompositeTarget::PngFile(_) + ) || self.exit_after_load; + let use_offscreen_framebuffer = matches!( + target, + CompositeTarget::SharedMemory | CompositeTarget::PngFile(_) | CompositeTarget::Fbo + ); if wait_for_stable_image { // The current image may be ready to output. However, if there are animations active, @@ -1713,25 +1725,35 @@ impl IOCompositor { } } - let rt_info = match target { - #[cfg(feature = "gl")] - CompositeTarget::Window => gl::RenderTargetInfo::default(), - #[cfg(feature = "gl")] - CompositeTarget::WindowAndPng | CompositeTarget::PngFile => gl::initialize_png( - &*self.webrender_gl, - FramebufferUintLength::new(size.width), - FramebufferUintLength::new(size.height), - ), - #[cfg(not(feature = "gl"))] - _ => (), - }; + if use_offscreen_framebuffer { + self.next_offscreen_framebuffer + .get_or_init(|| { + RenderTargetInfo::new( + self.webrender_gl.clone(), + FramebufferUintLength::new(size.width), + FramebufferUintLength::new(size.height), + ) + }) + .bind(); + } else { + // Bind the webrender framebuffer + let framebuffer_object = self + .webrender_surfman + .context_surface_info() + .unwrap_or(None) + .map(|info| info.framebuffer_object) + .unwrap_or(0); + self.webrender_gl + .bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object); + self.assert_gl_framebuffer_complete(); + } profile( ProfilerCategory::Compositing, None, self.time_profiler_chan.clone(), || { - debug!("compositor: compositing"); + trace!("Compositing"); let size = DeviceIntSize::from_untyped(self.embedder_coordinates.framebuffer.to_untyped()); @@ -1781,29 +1803,47 @@ impl IOCompositor { } } - let (x, y, width, height) = match rect { - Some(rect) => { - let rect = self.device_pixels_per_page_px().transform_rect(&rect); + let (x, y, width, height) = if let Some(rect) = rect { + let rect = self.device_pixels_per_page_px().transform_rect(&rect); - let x = rect.origin.x as i32; - // We need to convert to the bottom-left origin coordinate - // system used by OpenGL - let y = (size.height as f32 - rect.origin.y - rect.size.height) as i32; - let w = rect.size.width as u32; - let h = rect.size.height as u32; + let x = rect.origin.x as i32; + // We need to convert to the bottom-left origin coordinate + // system used by OpenGL + let y = (size.height as f32 - rect.origin.y - rect.size.height) as i32; + let w = rect.size.width as u32; + let h = rect.size.height as u32; - (x, y, w, h) - }, - None => (0, 0, size.width, size.height), + (x, y, w, h) + } else { + (0, 0, size.width, size.height) }; let rv = match target { CompositeTarget::Window => None, - #[cfg(feature = "gl")] - CompositeTarget::WindowAndPng => { - let img = gl::draw_img( - &*self.webrender_gl, - rt_info, + CompositeTarget::Fbo => { + self.next_offscreen_framebuffer + .get() + .expect("Guaranteed by needs_fbo") + .unbind(); + if self.invalidate_prev_offscreen_framebuffer { + // Do not reuse the last render target as the new current render target. + self.prev_offscreen_framebuffer = None; + self.invalidate_prev_offscreen_framebuffer = false; + } + let old_prev = self.prev_offscreen_framebuffer.take(); + self.prev_offscreen_framebuffer = self.next_offscreen_framebuffer.take(); + if let Some(old_prev) = old_prev { + let result = self.next_offscreen_framebuffer.set(old_prev); + debug_assert!(result.is_ok(), "Guaranteed by take"); + } + None + }, + CompositeTarget::SharedMemory => { + let render_target_info = self + .next_offscreen_framebuffer + .take() + .expect("Guaranteed by needs_fbo"); + let img = render_target_info.read_back_from_gpu( x, y, FramebufferUintLength::new(width), @@ -1818,39 +1858,33 @@ impl IOCompositor { cors_status: CorsStatus::Safe, }) }, - #[cfg(feature = "gl")] - CompositeTarget::PngFile => { - let gl = &*self.webrender_gl; + CompositeTarget::PngFile(path) => { profile( ProfilerCategory::ImageSaving, None, self.time_profiler_chan.clone(), - || match self.output_file.as_ref() { - Some(path) => match File::create(path) { - Ok(mut file) => { - let img = gl::draw_img( - gl, - rt_info, - x, - y, - FramebufferUintLength::new(width), - FramebufferUintLength::new(height), - ); - let dynamic_image = DynamicImage::ImageRgb8(img); - if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png) - { - error!("Failed to save {} ({}).", path, e); - } - }, - Err(e) => error!("Failed to create {} ({}).", path, e), + || match File::create(&*path) { + Ok(mut file) => { + let render_target_info = self + .next_offscreen_framebuffer + .take() + .expect("Guaranteed by needs_fbo"); + let img = render_target_info.read_back_from_gpu( + x, + y, + FramebufferUintLength::new(width), + FramebufferUintLength::new(height), + ); + let dynamic_image = DynamicImage::ImageRgb8(img); + if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png) { + error!("Failed to save {} ({}).", path, e); + } }, - None => error!("No file specified."), + Err(e) => error!("Failed to create {} ({}).", path, e), }, ); None }, - #[cfg(not(feature = "gl"))] - _ => None, }; // Nottify embedder that servo is ready to present. @@ -1871,6 +1905,14 @@ impl IOCompositor { Ok(rv) } + /// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to + /// [`CompositeTarget::Fbo`], or None otherwise. + pub fn offscreen_framebuffer_id(&self) -> Option { + self.prev_offscreen_framebuffer + .as_ref() + .map(|info| info.framebuffer_id()) + } + pub fn present(&mut self) { if let Err(err) = self.webrender_surfman.present() { warn!("Failed to present surface: {:?}", err); diff --git a/components/compositing/gl.rs b/components/compositing/gl.rs index 45d33f19a7d..407fb34546b 100644 --- a/components/compositing/gl.rs +++ b/components/compositing/gl.rs @@ -2,125 +2,149 @@ * 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 gleam::gl; +use std::rc::Rc; + +use gleam::gl::{self, Gl}; use image::RgbImage; +use log::trace; use servo_geometry::FramebufferUintLength; -#[derive(Default)] pub struct RenderTargetInfo { + gl: Rc, framebuffer_ids: Vec, renderbuffer_ids: Vec, texture_ids: Vec, } -pub fn initialize_png( - gl: &dyn gl::Gl, - width: FramebufferUintLength, - height: FramebufferUintLength, -) -> RenderTargetInfo { - let framebuffer_ids = gl.gen_framebuffers(1); - gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]); +impl RenderTargetInfo { + pub fn new( + gl: Rc, + width: FramebufferUintLength, + height: FramebufferUintLength, + ) -> Self { + let framebuffer_ids = gl.gen_framebuffers(1); + gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]); + trace!("Configuring fbo {}", framebuffer_ids[0]); - let texture_ids = gl.gen_textures(1); - gl.bind_texture(gl::TEXTURE_2D, texture_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::RGB as gl::GLint, + width.get() as gl::GLsizei, + height.get() as gl::GLsizei, + 0, + gl::RGB, + 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.tex_image_2d( - gl::TEXTURE_2D, - 0, - gl::RGB as gl::GLint, - width.get() as gl::GLsizei, - height.get() as gl::GLsizei, - 0, - gl::RGB, - 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.framebuffer_texture_2d( - gl::FRAMEBUFFER, - gl::COLOR_ATTACHMENT0, - gl::TEXTURE_2D, - texture_ids[0], - 0, - ); + gl.bind_texture(gl::TEXTURE_2D, 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, + width.get() as gl::GLsizei, + height.get() as gl::GLsizei, + ); + gl.framebuffer_renderbuffer( + gl::FRAMEBUFFER, + gl::DEPTH_ATTACHMENT, + gl::RENDERBUFFER, + depth_rb, + ); - 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, - width.get() as gl::GLsizei, - height.get() as gl::GLsizei, - ); - gl.framebuffer_renderbuffer( - gl::FRAMEBUFFER, - gl::DEPTH_ATTACHMENT, - gl::RENDERBUFFER, - depth_rb, - ); + Self { + gl, + framebuffer_ids, + renderbuffer_ids, + texture_ids, + } + } - RenderTargetInfo { - framebuffer_ids, - renderbuffer_ids, - texture_ids, + pub fn framebuffer_id(&self) -> gl::GLuint { + *self.framebuffer_ids.first().expect("Guaranteed by new") + } + + pub fn bind(&self) { + trace!("Binding FBO {}", self.framebuffer_id()); + self.gl + .bind_framebuffer(gl::FRAMEBUFFER, self.framebuffer_id()); + } + + pub fn unbind(&self) { + trace!("Unbinding FBO {}", self.framebuffer_id()); + self.gl.bind_framebuffer(gl::FRAMEBUFFER, 0); + } + + pub fn read_back_from_gpu( + self, + x: i32, + y: i32, + width: FramebufferUintLength, + height: FramebufferUintLength, + ) -> RgbImage { + let width = width.get() as usize; + let height = height.get() as usize; + // 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. + self.gl.bind_vertex_array(0); + + let mut pixels = self.gl.read_pixels( + x, + y, + width as gl::GLsizei, + height as gl::GLsizei, + gl::RGB, + gl::UNSIGNED_BYTE, + ); + + // flip image vertically (texture is upside down) + let orig_pixels = pixels.clone(); + let stride = width * 3; + for y in 0..height { + let dst_start = y * stride; + let src_start = (height - y - 1) * stride; + let src_slice = &orig_pixels[src_start..src_start + stride]; + (&mut pixels[dst_start..dst_start + stride]).clone_from_slice(&src_slice[..stride]); + } + + RgbImage::from_raw(width as u32, height as u32, pixels).expect("Flipping image failed!") } } -pub fn draw_img( - gl: &dyn gl::Gl, - render_target_info: RenderTargetInfo, - x: i32, - y: i32, - width: FramebufferUintLength, - height: FramebufferUintLength, -) -> RgbImage { - let width = width.get() as usize; - let height = height.get() as usize; - // 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( - x, - y, - width as gl::GLsizei, - height as gl::GLsizei, - gl::RGB, - gl::UNSIGNED_BYTE, - ); - - gl.bind_framebuffer(gl::FRAMEBUFFER, 0); - - gl.delete_textures(&render_target_info.texture_ids); - gl.delete_renderbuffers(&render_target_info.renderbuffer_ids); - gl.delete_framebuffers(&render_target_info.framebuffer_ids); - - // flip image vertically (texture is upside down) - let orig_pixels = pixels.clone(); - let stride = width * 3; - for y in 0..height { - let dst_start = y * stride; - let src_start = (height - y - 1) * stride; - let src_slice = &orig_pixels[src_start..src_start + stride]; - (&mut pixels[dst_start..dst_start + stride]).clone_from_slice(&src_slice[..stride]); +impl Drop for RenderTargetInfo { + fn drop(&mut self) { + trace!("Dropping FBO {}", self.framebuffer_id()); + self.unbind(); + self.gl.delete_textures(&self.texture_ids); + self.gl.delete_renderbuffers(&self.renderbuffer_ids); + self.gl.delete_framebuffers(&self.framebuffer_ids); } - - RgbImage::from_raw(width as u32, height as u32, pixels).expect("Flipping image failed!") } diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index 5eb6eeded48..58a9398556f 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -13,10 +13,9 @@ use webrender::RenderApi; use webrender_api::DocumentId; use webrender_surfman::WebrenderSurfman; -pub use crate::compositor::{IOCompositor, ShutdownState}; +pub use crate::compositor::{CompositeTarget, IOCompositor, ShutdownState}; mod compositor; -#[cfg(feature = "gl")] mod gl; mod touch; pub mod windowing; diff --git a/components/servo/Cargo.toml b/components/servo/Cargo.toml index e3bdc4921a4..e7a6e786534 100644 --- a/components/servo/Cargo.toml +++ b/components/servo/Cargo.toml @@ -37,7 +37,7 @@ bluetooth = { path = "../bluetooth" } bluetooth_traits = { workspace = true } canvas = { path = "../canvas", default-features = false } canvas_traits = { workspace = true } -compositing = { path = "../compositing", features = ["gl"] } +compositing = { path = "../compositing" } compositing_traits = { workspace = true } constellation = { path = "../constellation" } crossbeam-channel = { workspace = true } diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 122ff4c6d2d..424f3fc647e 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -30,7 +30,7 @@ use canvas::canvas_paint_thread::{self, CanvasPaintThread}; use canvas::WebGLComm; use canvas_traits::webgl::WebGLThreads; use compositing::windowing::{EmbedderEvent, EmbedderMethods, WindowMethods}; -use compositing::{IOCompositor, InitialCompositorState, ShutdownState}; +use compositing::{CompositeTarget, IOCompositor, InitialCompositorState, ShutdownState}; use compositing_traits::{ CanvasToCompositorMsg, CompositingReason, CompositorMsg, CompositorProxy, CompositorReceiver, ConstellationMsg, FontToCompositorMsg, ForwardedToCompositorMsg, @@ -224,6 +224,7 @@ where mut embedder: Box, window: Rc, user_agent: Option, + composite_target: CompositeTarget, ) -> InitializedServo { // Global configuration options, parsed from the command line. let opts = opts::get(); @@ -447,6 +448,12 @@ where } } + let composite_target = if let Some(path) = opts.output_file.clone() { + CompositeTarget::PngFile(path.into()) + } else { + composite_target + }; + // The compositor coordinates with the client window to create the final // rendered page and display it somewhere. let compositor = IOCompositor::create( @@ -464,7 +471,7 @@ where webrender_gl, webxr_main_thread, }, - opts.output_file.clone(), + composite_target, opts.is_running_problem_test, opts.exit_after_load, opts.debug.convert_mouse_to_touch, @@ -769,6 +776,12 @@ where pub fn recomposite(&mut self) { self.compositor.composite(); } + + /// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to + /// [`CompositeTarget::Fbo`], or None otherwise. + pub fn offscreen_framebuffer_id(&self) -> Option { + self.compositor.offscreen_framebuffer_id() + } } fn create_embedder_channel( diff --git a/ports/servoshell/Cargo.toml b/ports/servoshell/Cargo.toml index 505346cf5b5..be044f7fa79 100644 --- a/ports/servoshell/Cargo.toml +++ b/ports/servoshell/Cargo.toml @@ -4,7 +4,7 @@ name = "servoshell" version = "0.0.1" authors = ["The Servo Project Developers"] license = "MPL-2.0" -edition = "2018" +edition = "2021" build = "build.rs" publish = false diff --git a/ports/servoshell/app.rs b/ports/servoshell/app.rs index c5965751e6d..b7e8b86b5b2 100644 --- a/ports/servoshell/app.rs +++ b/ports/servoshell/app.rs @@ -13,6 +13,7 @@ use std::{env, fs}; use gleam::gl; use log::{info, trace, warn}; use servo::compositing::windowing::EmbedderEvent; +use servo::compositing::CompositeTarget; use servo::config::opts; use servo::servo_config::pref; use servo::Servo; @@ -122,8 +123,9 @@ impl App { } if let Some(mut minibrowser) = app.minibrowser() { - minibrowser.update(window.winit_window().unwrap(), "init"); - window.set_toolbar_height(minibrowser.toolbar_height.get()); + // Servo is not yet initialised, so there is no `servo_framebuffer_id`. + minibrowser.update(window.winit_window().unwrap(), None, "init"); + window.set_toolbar_height(minibrowser.toolbar_height); } // Whether or not to recomposite during the next RedrawRequested event. @@ -185,7 +187,17 @@ impl App { // Implements embedder methods, used by libservo and constellation. let embedder = Box::new(EmbedderCallbacks::new(ev_waker.clone(), xr_discovery)); - let servo_data = Servo::new(embedder, window.clone(), user_agent.clone()); + let composite_target = if app.minibrowser.is_some() { + CompositeTarget::Fbo + } else { + CompositeTarget::Window + }; + let servo_data = Servo::new( + embedder, + window.clone(), + user_agent.clone(), + composite_target, + ); let mut servo = servo_data.servo; servo.handle_events(vec![EmbedderEvent::NewBrowser( @@ -217,7 +229,11 @@ impl App { app.servo.as_mut().unwrap().recomposite(); } if let Some(mut minibrowser) = app.minibrowser() { - minibrowser.update(window.winit_window().unwrap(), "RedrawRequested"); + minibrowser.update( + window.winit_window().unwrap(), + app.servo.as_ref().unwrap().offscreen_framebuffer_id(), + "RedrawRequested", + ); minibrowser.paint(window.winit_window().unwrap()); } app.servo.as_mut().unwrap().present(); @@ -252,7 +268,7 @@ impl App { window.winit_window().unwrap().request_redraw(); }, winit::event::Event::WindowEvent { ref event, .. } => { - let response = minibrowser.context.on_event(&event); + let response = minibrowser.on_event(&event); if response.repaint { // Request a winit redraw event, so we can recomposite, update and paint // the minibrowser, and present the new frame. @@ -306,6 +322,7 @@ impl App { // redraw, doing so would delay the location update by two frames. minibrowser.update( window.winit_window().unwrap(), + app.servo.as_ref().unwrap().offscreen_framebuffer_id(), "update_location_in_toolbar", ); } @@ -323,6 +340,7 @@ impl App { if let Some(mut minibrowser) = app.minibrowser() { minibrowser.update( window.winit_window().unwrap(), + app.servo.as_ref().unwrap().offscreen_framebuffer_id(), "PumpResult::Present::Immediate", ); minibrowser.paint(window.winit_window().unwrap()); diff --git a/ports/servoshell/geometry.rs b/ports/servoshell/geometry.rs new file mode 100644 index 00000000000..bca28fcf839 --- /dev/null +++ b/ports/servoshell/geometry.rs @@ -0,0 +1,15 @@ +/* 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 euclid::{Point2D, Size2D}; +use servo::style_traits::DevicePixel; +use winit::dpi::{PhysicalPosition, PhysicalSize}; + +pub fn winit_size_to_euclid_size(size: PhysicalSize) -> Size2D { + Size2D::new(size.width, size.height) +} + +pub fn winit_position_to_euclid_point(position: PhysicalPosition) -> Point2D { + Point2D::new(position.x, position.y) +} diff --git a/ports/servoshell/headed_window.rs b/ports/servoshell/headed_window.rs index 0174a3b999d..9d9e4fd142e 100644 --- a/ports/servoshell/headed_window.rs +++ b/ports/servoshell/headed_window.rs @@ -41,6 +41,7 @@ use winit::event::{ use winit::window::Icon; use crate::events_loop::{EventsLoop, WakerEvent}; +use crate::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size}; use crate::keyutils::keyboard_event_from_winit; use crate::window_trait::{WindowPortsMethods, LINE_HEIGHT}; @@ -519,14 +520,6 @@ impl WindowPortsMethods for Window { } } -fn winit_size_to_euclid_size(size: PhysicalSize) -> Size2D { - Size2D::new(size.width, size.height) -} - -fn winit_position_to_euclid_point(position: PhysicalPosition) -> Point2D { - Point2D::new(position.x, position.y) -} - impl WindowMethods for Window { fn get_coordinates(&self) -> EmbedderCoordinates { let window_size = winit_size_to_euclid_size(self.winit_window.outer_size()).to_i32(); diff --git a/ports/servoshell/main.rs b/ports/servoshell/main.rs index f252cdb3ef2..fd56fcdc61b 100644 --- a/ports/servoshell/main.rs +++ b/ports/servoshell/main.rs @@ -36,6 +36,7 @@ cfg_if::cfg_if! { mod egui_glue; mod embedder; mod events_loop; + mod geometry; mod headed_window; mod headless_window; mod keyutils; diff --git a/ports/servoshell/minibrowser.rs b/ports/servoshell/minibrowser.rs index 3e16e4e3447..42659ec1964 100644 --- a/ports/servoshell/minibrowser.rs +++ b/ports/servoshell/minibrowser.rs @@ -3,11 +3,16 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::{Cell, RefCell}; +use std::num::NonZeroU32; use std::sync::Arc; use std::time::Instant; -use egui::{Key, Modifiers, TopBottomPanel}; -use euclid::Length; +use egui::{CentralPanel, Frame, InnerResponse, Key, Modifiers, PaintCallback, TopBottomPanel}; +use egui_glow::CallbackFn; +use egui_winit::EventResponse; +use euclid::{Length, Point2D, Scale}; +use gleam::gl; +use glow::NativeFramebuffer; use log::{trace, warn}; use servo::compositing::windowing::EmbedderEvent; use servo::msg::constellation_msg::TraversalDirection; @@ -18,14 +23,21 @@ use servo::webrender_surfman::WebrenderSurfman; use crate::browser::Browser; use crate::egui_glue::EguiGlow; use crate::events_loop::EventsLoop; +use crate::geometry::winit_position_to_euclid_point; use crate::parser::location_bar_input_to_url; use crate::window_trait::WindowPortsMethods; pub struct Minibrowser { pub context: EguiGlow, pub event_queue: RefCell>, - pub toolbar_height: Cell>, + pub toolbar_height: Length, + + /// The framebuffer object name for the widget surface we should draw to, or None if our widget + /// surface does not use a framebuffer object. + widget_surface_fbo: Option, + last_update: Instant, + last_mouse_position: Option>, location: RefCell, /// Whether the location has been edited by the user without clicking Go. @@ -56,18 +68,62 @@ impl Minibrowser { .egui_ctx .set_pixels_per_point(window.hidpi_factor().get()); + let widget_surface_fbo = match webrender_surfman.context_surface_info() { + Ok(Some(info)) => NonZeroU32::new(info.framebuffer_object).map(NativeFramebuffer), + Ok(None) => panic!("Failed to get widget surface info from surfman!"), + Err(error) => panic!("Failed to get widget surface info from surfman! {error:?}"), + }; + Self { context, event_queue: RefCell::new(vec![]), toolbar_height: Default::default(), + widget_surface_fbo, last_update: Instant::now(), + last_mouse_position: None, location: RefCell::new(initial_url.to_string()), location_dirty: false.into(), } } + /// Preprocess the given [winit::event::WindowEvent], returning unconsumed for mouse events in + /// the Servo browser rect. This is needed because the CentralPanel we create for our webview + /// would otherwise make egui report events in that area as consumed. + pub fn on_event(&mut self, event: &winit::event::WindowEvent<'_>) -> EventResponse { + let mut result = self.context.on_event(event); + result.consumed &= match event { + winit::event::WindowEvent::CursorMoved { position, .. } => { + let scale = Scale::<_, DeviceIndependentPixel, _>::new( + self.context.egui_ctx.pixels_per_point(), + ); + self.last_mouse_position = + Some(winit_position_to_euclid_point(*position).to_f32() / scale); + self.last_mouse_position + .map_or(false, |p| self.is_in_browser_rect(p)) + }, + winit::event::WindowEvent::MouseWheel { .. } | + winit::event::WindowEvent::MouseInput { .. } => self + .last_mouse_position + .map_or(false, |p| self.is_in_browser_rect(p)), + _ => true, + }; + result + } + + /// Return true iff the given position is in the Servo browser rect. + fn is_in_browser_rect(&self, position: Point2D) -> bool { + position.y < self.toolbar_height.get() + } + /// Update the minibrowser, but don’t paint. - pub fn update(&mut self, window: &winit::window::Window, reason: &'static str) { + /// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our + /// CentralPanel when [`Minibrowser::paint`] is called. + pub fn update( + &mut self, + window: &winit::window::Window, + servo_framebuffer_id: Option, + reason: &'static str, + ) { let now = Instant::now(); trace!( "{:?} since last update ({})", @@ -78,62 +134,125 @@ impl Minibrowser { context, event_queue, toolbar_height, + widget_surface_fbo, last_update, location, location_dirty, + .. } = self; + let widget_fbo = *widget_surface_fbo; let _duration = context.run(window, |ctx| { - TopBottomPanel::top("toolbar").show(ctx, |ui| { - ui.allocate_ui_with_layout( - ui.available_size(), - egui::Layout::left_to_right(egui::Align::Center), - |ui| { - if ui.button("back").clicked() { - event_queue.borrow_mut().push(MinibrowserEvent::Back); - } - if ui.button("forward").clicked() { - event_queue.borrow_mut().push(MinibrowserEvent::Forward); - } + let InnerResponse { inner: height, .. } = + TopBottomPanel::top("toolbar").show(ctx, |ui| { + ui.allocate_ui_with_layout( + ui.available_size(), + egui::Layout::left_to_right(egui::Align::Center), + |ui| { + if ui.button("back").clicked() { + event_queue.borrow_mut().push(MinibrowserEvent::Back); + } + if ui.button("forward").clicked() { + event_queue.borrow_mut().push(MinibrowserEvent::Forward); + } + ui.allocate_ui_with_layout( + ui.available_size(), + egui::Layout::right_to_left(egui::Align::Center), + |ui| { + if ui.button("go").clicked() { + event_queue.borrow_mut().push(MinibrowserEvent::Go); + location_dirty.set(false); + } - ui.allocate_ui_with_layout( - ui.available_size(), - egui::Layout::right_to_left(egui::Align::Center), - |ui| { - if ui.button("go").clicked() { - event_queue.borrow_mut().push(MinibrowserEvent::Go); - location_dirty.set(false); - } + let location_field = ui.add_sized( + ui.available_size(), + egui::TextEdit::singleline(&mut *location.borrow_mut()), + ); - let location_field = ui.add_sized( - ui.available_size(), - egui::TextEdit::singleline(&mut *location.borrow_mut()), + if location_field.changed() { + location_dirty.set(true); + } + if ui.input(|i| { + i.clone().consume_key(Modifiers::COMMAND, Key::L) + }) { + location_field.request_focus(); + } + if location_field.lost_focus() && + ui.input(|i| i.clone().key_pressed(Key::Enter)) + { + event_queue.borrow_mut().push(MinibrowserEvent::Go); + location_dirty.set(false); + } + }, + ); + }, + ); + ui.cursor().min.y + }); + *toolbar_height = Length::new(height); + + CentralPanel::default() + .frame(Frame::none()) + .show(ctx, |ui| { + let min = ui.cursor().min; + let size = ui.available_size(); + let rect = egui::Rect::from_min_size(min, size); + ui.allocate_space(size); + + let Some(servo_fbo) = servo_framebuffer_id else { return }; + ui.painter().add(PaintCallback { + rect, + callback: Arc::new(CallbackFn::new(move |info, painter| { + use glow::HasContext as _; + let clip = info.viewport_in_pixels(); + let x = clip.left_px as gl::GLint; + let y = clip.from_bottom_px as gl::GLint; + let width = clip.width_px as gl::GLsizei; + let height = clip.height_px as gl::GLsizei; + unsafe { + painter.gl().clear_color(0.0, 0.0, 0.0, 0.0); + painter.gl().scissor(x, y, width, height); + painter.gl().enable(gl::SCISSOR_TEST); + painter.gl().clear(gl::COLOR_BUFFER_BIT); + painter.gl().disable(gl::SCISSOR_TEST); + + let servo_fbo = NonZeroU32::new(servo_fbo).map(NativeFramebuffer); + painter + .gl() + .bind_framebuffer(gl::READ_FRAMEBUFFER, servo_fbo); + painter + .gl() + .bind_framebuffer(gl::DRAW_FRAMEBUFFER, widget_fbo); + painter.gl().blit_framebuffer( + x, + y, + x + width, + y + height, + x, + y, + x + width, + y + height, + gl::COLOR_BUFFER_BIT, + gl::NEAREST, ); + painter.gl().bind_framebuffer(gl::FRAMEBUFFER, widget_fbo); + } + })), + }); + }); - if location_field.changed() { - location_dirty.set(true); - } - if ui.input(|i| i.clone().consume_key(Modifiers::COMMAND, Key::L)) { - location_field.request_focus(); - } - if location_field.lost_focus() && - ui.input(|i| i.clone().key_pressed(Key::Enter)) - { - event_queue.borrow_mut().push(MinibrowserEvent::Go); - location_dirty.set(false); - } - }, - ); - }, - ); - }); - - toolbar_height.set(Length::new(ctx.used_rect().height())); *last_update = now; }); } /// Paint the minibrowser, as of the last update. pub fn paint(&mut self, window: &winit::window::Window) { + unsafe { + use glow::HasContext as _; + self.context + .painter + .gl() + .bind_framebuffer(gl::FRAMEBUFFER, self.widget_surface_fbo); + } self.context.paint(window); }