diff --git a/Cargo.lock b/Cargo.lock index 8ab82b9f3dd..a54fd197530 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8594,6 +8594,8 @@ dependencies = [ "embedder_traits", "euclid", "gleam", + "glow", + "image", "ipc-channel", "libc", "log", diff --git a/components/compositing/Cargo.toml b/components/compositing/Cargo.toml index 4382036accc..6c7d2df2c1f 100644 --- a/components/compositing/Cargo.toml +++ b/components/compositing/Cargo.toml @@ -36,6 +36,7 @@ script_traits = { workspace = true } servo_config = { path = "../config" } servo_geometry = { path = "../geometry" } style_traits = { workspace = true } +surfman = { workspace = true } tracing = { workspace = true, optional = true } webrender = { workspace = true } webrender_api = { workspace = true } diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index dc4da1f56d7..ecf3a95495d 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -2,7 +2,6 @@ * 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::hash_set::Iter; use std::collections::HashMap; use std::env; @@ -24,7 +23,7 @@ use embedder_traits::{ Cursor, InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, TouchEvent, TouchEventAction, TouchId, }; -use euclid::{Point2D, Rect, Scale, Transform3D, Vector2D}; +use euclid::{Point2D, Rect, Scale, Size2D, Transform3D, Vector2D}; use fnv::{FnvHashMap, FnvHashSet}; use image::{DynamicImage, ImageFormat}; use ipc_channel::ipc::{self, IpcSharedMemory}; @@ -37,7 +36,7 @@ use script_traits::{ AnimationState, AnimationTickType, ScriptThreadMessage, ScrollState, WindowSizeData, WindowSizeType, }; -use servo_geometry::{DeviceIndependentPixel, FramebufferUintLength}; +use servo_geometry::DeviceIndependentPixel; use style_traits::{CSSPixel, PinchZoomFactor}; use webrender::{CaptureBits, RenderApi, Transaction}; use webrender_api::units::{ @@ -57,11 +56,10 @@ use webrender_traits::{ CompositorHitTestResult, CrossProcessCompositorMessage, ImageUpdate, UntrustedNodeAddress, }; -use crate::gl::RenderTargetInfo; use crate::touch::{TouchAction, TouchHandler}; use crate::webview::{UnknownWebView, WebView, WebViewAlreadyExists, WebViewManager}; use crate::windowing::{self, EmbedderCoordinates, WebRenderDebugOption, WindowMethods}; -use crate::{gl, InitialCompositorState}; +use crate::InitialCompositorState; #[derive(Debug, PartialEq)] enum UnableToComposite { @@ -191,19 +189,6 @@ pub struct IOCompositor { /// Current cursor position. cursor_pos: DevicePoint, - /// Offscreen framebuffer object to render our next frame to. - /// We use this and `prev_offscreen_framebuffer` for double buffering when compositing to - /// [`CompositeTarget::OffscreenFbo`]. - 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::OffscreenFbo`]. - prev_offscreen_framebuffer: Option, - - /// Whether to invalidate `prev_offscreen_framebuffer` at the end of the next frame. - invalidate_prev_offscreen_framebuffer: bool, - /// True to exit after page load ('-x'). exit_after_load: bool, @@ -348,10 +333,6 @@ pub enum CompositeTarget { /// to [`RenderingContext::framebuffer_object`] ContextFbo, - /// Draw to an offscreen OpenGL framebuffer object, which can be retrieved once complete at - /// [`IOCompositor::offscreen_framebuffer_id`]. - OffscreenFbo, - /// Draw to an uncompressed image in shared memory. SharedMemory, @@ -399,9 +380,6 @@ impl IOCompositor { pending_paint_metrics: HashMap::new(), cursor: Cursor::None, cursor_pos: DevicePoint::new(0.0, 0.0), - next_offscreen_framebuffer: OnceCell::new(), - prev_offscreen_framebuffer: None, - invalidate_prev_offscreen_framebuffer: false, exit_after_load, convert_mouse_to_touch, pending_frames: 0, @@ -1302,13 +1280,6 @@ impl IOCompositor { let old_coords = self.embedder_coordinates; self.embedder_coordinates = self.window.get_coordinates(); - // 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 { let mut transaction = Transaction::new(); let size = self.embedder_coordinates.get_viewport(); @@ -1957,13 +1928,6 @@ impl IOCompositor { target: CompositeTarget, page_rect: Option>, ) -> Result, UnableToComposite> { - if !self.webviews_waiting_on_present.is_empty() { - debug!("tried to composite while waiting on present"); - return Err(UnableToComposite::NotReadyToPaintImage( - NotReadyToPaint::WaitingOnConstellation, - )); - } - let size = self.embedder_coordinates.framebuffer.to_u32(); if let Err(err) = self.rendering_context.make_current() { warn!("Failed to make the rendering context current: {:?}", err); @@ -1978,12 +1942,6 @@ impl IOCompositor { target, CompositeTarget::SharedMemory | CompositeTarget::PngFile(_) ) || self.exit_after_load; - let use_offscreen_framebuffer = matches!( - target, - CompositeTarget::SharedMemory | - CompositeTarget::PngFile(_) | - CompositeTarget::OffscreenFbo - ); if wait_for_stable_image { // The current image may be ready to output. However, if there are animations active, @@ -2000,23 +1958,7 @@ impl IOCompositor { } } - 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.rendering_context.framebuffer_object(); - self.webrender_gl - .bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object); - self.assert_gl_framebuffer_complete(); - } + self.rendering_context.prepare_for_rendering(); time_profile!( ProfilerCategory::Compositing, @@ -2056,44 +1998,20 @@ impl IOCompositor { let rv = match target { CompositeTarget::ContextFbo => None, - CompositeTarget::OffscreenFbo => { - 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), - FramebufferUintLength::new(height), - ); - Some(Image { - width: img.width(), - height: img.height(), + CompositeTarget::SharedMemory => self + .rendering_context + .read_to_image(Rect::new( + Point2D::new(x as u32, y as u32), + Size2D::new(width, height), + )) + .map(|image| Image { + width: image.width(), + height: image.height(), format: PixelFormat::RGBA8, - bytes: ipc::IpcSharedMemory::from_bytes(&img), + bytes: ipc::IpcSharedMemory::from_bytes(&image), id: None, cors_status: CorsStatus::Safe, - }) - }, + }), CompositeTarget::PngFile(path) => { time_profile!( ProfilerCategory::ImageSaving, @@ -2101,19 +2019,15 @@ impl IOCompositor { self.time_profiler_chan.clone(), || 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::ImageRgba8(img); - if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png) { - error!("Failed to save {} ({}).", path, e); + if let Some(image) = self.rendering_context.read_to_image(Rect::new( + Point2D::new(x as u32, y as u32), + Size2D::new(width, height), + )) { + let dynamic_image = DynamicImage::ImageRgba8(image); + 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), @@ -2205,14 +2119,6 @@ impl IOCompositor { } } - /// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to - /// [`CompositeTarget::OffscreenFbo`], or None otherwise. - pub fn offscreen_framebuffer_id(&self) -> Option { - self.prev_offscreen_framebuffer - .as_ref() - .map(|info| info.framebuffer_id()) - } - #[cfg_attr( feature = "tracing", tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") diff --git a/components/compositing/gl.rs b/components/compositing/gl.rs deleted file mode 100644 index 8587c61d7a0..00000000000 --- a/components/compositing/gl.rs +++ /dev/null @@ -1,208 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ - -use std::rc::Rc; - -use gleam::gl::{self, Gl}; -use image::RgbaImage; -use log::{trace, warn}; -use servo_geometry::FramebufferUintLength; - -pub struct RenderTargetInfo { - gl: Rc, - framebuffer_ids: Vec, - renderbuffer_ids: Vec, - texture_ids: Vec, -} - -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]); - gl.tex_image_2d( - gl::TEXTURE_2D, - 0, - gl::RGBA as gl::GLint, - width.get() as gl::GLsizei, - height.get() 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, - 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, - } - } - - 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, - ) -> RgbaImage { - 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::RGBA, - gl::UNSIGNED_BYTE, - ); - let gl_error = self.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 orig_pixels = pixels.clone(); - let stride = width * 4; - 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]; - pixels[dst_start..dst_start + stride].clone_from_slice(&src_slice[..stride]); - } - - RgbaImage::from_raw(width as u32, height as u32, pixels).expect("Flipping image failed!") - } -} - -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); - } -} - -#[cfg(test)] -mod test { - use gleam::gl; - use image::Rgba; - use servo_geometry::FramebufferUintLength; - use surfman::{Connection, ContextAttributeFlags, ContextAttributes, Error, GLApi, GLVersion}; - - use super::RenderTargetInfo; - - #[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 WIDTH: FramebufferUintLength = FramebufferUintLength::new(16); - const HEIGHT: FramebufferUintLength = FramebufferUintLength::new(16); - let render_target = RenderTargetInfo::new(gl, WIDTH, HEIGHT); - render_target.bind(); - render_target - .gl - .clear_color(12.0 / 255.0, 34.0 / 255.0, 56.0 / 255.0, 78.0 / 255.0); - render_target.gl.clear(gl::COLOR_BUFFER_BIT); - - let img = render_target.read_back_from_gpu(0, 0, WIDTH, HEIGHT); - assert_eq!(img.width(), WIDTH.get()); - assert_eq!(img.height(), HEIGHT.get()); - - let expected_pixel: Rgba = Rgba([12, 34, 56, 78]); - assert!(img.pixels().all(|&p| p == expected_pixel)); - } - - device.destroy_context(&mut context)?; - - Ok(()) - } -} diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index f38a8f64359..33700145b54 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -19,7 +19,6 @@ pub use crate::compositor::{CompositeTarget, IOCompositor, ShutdownState}; mod tracing; mod compositor; -mod gl; mod touch; pub mod webview; pub mod windowing; diff --git a/components/servo/lib.rs b/components/servo/lib.rs index 1a514a6f8ad..b90ceb3a3d8 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -305,10 +305,6 @@ impl Servo { } debug_assert_eq!(webrender_gl.get_error(), gleam::gl::NO_ERROR,); - // Bind the webrender framebuffer - let framebuffer_object = rendering_context.framebuffer_object(); - webrender_gl.bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object); - // Reserving a namespace to create TopLevelBrowsingContextId. PipelineNamespace::install(PipelineNamespaceId(0)); @@ -346,6 +342,7 @@ impl Servo { opts.debug.webrender_stats, ); + rendering_context.prepare_for_rendering(); let render_notifier = Box::new(RenderNotifier::new(compositor_proxy.clone())); let clear_color = servo_config::pref!(shell_background_color_rgba); let clear_color = ColorF::new( @@ -354,6 +351,7 @@ impl Servo { clear_color[2] as f32, clear_color[3] as f32, ); + // Use same texture upload method as Gecko with ANGLE: // https://searchfox.org/mozilla-central/source/gfx/webrender_bindings/src/bindings.rs#1215-1219 let upload_method = if webrender_gl.get_string(RENDERER).starts_with("ANGLE") { @@ -702,12 +700,6 @@ impl Servo { self.compositor.borrow_mut().present(); } - /// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to - /// [`CompositeTarget::OffscreenFbo`], or None otherwise. - pub fn offscreen_framebuffer_id(&self) -> Option { - self.compositor.borrow().offscreen_framebuffer_id() - } - pub fn new_webview(&self, url: url::Url) -> WebView { let webview = WebView::new(&self.constellation_proxy, self.compositor.clone()); self.webviews diff --git a/components/shared/webrender/Cargo.toml b/components/shared/webrender/Cargo.toml index 40270bfc126..97eca163107 100644 --- a/components/shared/webrender/Cargo.toml +++ b/components/shared/webrender/Cargo.toml @@ -10,6 +10,7 @@ rust-version.workspace = true [lib] name = "webrender_traits" path = "lib.rs" +test = true [features] no-wgl = ["surfman/sm-angle-default"] @@ -18,10 +19,12 @@ no-wgl = ["surfman/sm-angle-default"] base = { workspace = true } embedder_traits = { workspace = true } euclid = { workspace = true } +image = { workspace = true } ipc-channel = { workspace = true } log = { workspace = true } libc = { workspace = true } gleam = { workspace = true } +glow = { workspace = true } webrender_api = { workspace = true } serde = { workspace = true } servo_geometry = { path = "../../geometry" } diff --git a/components/shared/webrender/rendering_context.rs b/components/shared/webrender/rendering_context.rs index 8b1e1165a3a..9e1ef5922f3 100644 --- a/components/shared/webrender/rendering_context.rs +++ b/components/shared/webrender/rendering_context.rs @@ -4,13 +4,17 @@ #![deny(unsafe_code)] -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::ffi::c_void; +use std::num::NonZeroU32; use std::rc::Rc; -use euclid::default::Size2D; -use gleam::gl; -use log::{debug, warn}; +use euclid::default::{Rect, Size2D}; +use euclid::Point2D; +use gleam::gl::{self, Gl}; +use glow::NativeFramebuffer; +use image::RgbaImage; +use log::{debug, trace, warn}; use servo_media::player::context::{GlContext, NativeDisplay}; use surfman::chains::{PreserveBuffer, SwapChain}; #[cfg(all(target_os = "linux", not(target_env = "ohos")))] @@ -36,16 +40,26 @@ pub enum GLVersion { /// 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: Rect) -> Option; /// Resizes the rendering surface to the given size. fn resize(&self, size: Size2D); - /// Presents the rendered frame to the screen. + /// 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 OpenGL framebuffer object needed to render to the surface. - fn framebuffer_object(&self) -> u32; /// Returns the OpenGL or GLES API. fn gl_api(&self) -> Rc; /// Describes the OpenGL version that is requested when a context is created. @@ -86,6 +100,7 @@ pub trait RenderingContext { pub struct SurfmanRenderingContext(Rc); struct RenderingContextData { + gl: Rc, device: RefCell, context: RefCell, // We either render to a swap buffer or to a native widget @@ -164,6 +179,23 @@ impl RenderingContext for SurfmanRenderingContext { NativeDisplay::Unknown } } + + fn prepare_for_rendering(&self) { + self.0.gl.bind_framebuffer( + gleam::gl::FRAMEBUFFER, + self.framebuffer() + .map_or(0, |framebuffer| framebuffer.0.into()), + ); + } + + fn read_to_image(&self, source_rectangle: Rect) -> Option { + let framebuffer_id = self + .framebuffer() + .map(|framebuffer| framebuffer.0.into()) + .unwrap_or(0); + Framebuffer::read_framebuffer_to_image(self.gl_api(), framebuffer_id, source_rectangle) + } + fn resize(&self, size: Size2D) { if let Err(err) = self.resize(size) { warn!("Failed to resize surface: {:?}", err); @@ -177,23 +209,9 @@ impl RenderingContext for SurfmanRenderingContext { fn make_current(&self) -> Result<(), Error> { self.make_gl_context_current() } - fn framebuffer_object(&self) -> u32 { - self.context_surface_info() - .unwrap_or(None) - .and_then(|info| info.framebuffer_object) - .map(|fbo| fbo.0.get()) - .unwrap_or(0) - } #[allow(unsafe_code)] fn gl_api(&self) -> Rc { - let context = self.0.context.borrow(); - let device = self.0.device.borrow(); - match self.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)) - }, - } + self.0.gl.clone() } fn gl_version(&self) -> GLVersion { let device = self.0.device.borrow(); @@ -270,9 +288,23 @@ impl SurfmanRenderingContext { } else { None }; + + #[allow(unsafe_code)] + 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)) + }, + } + }; + let device = RefCell::new(device); let context = RefCell::new(context); let data = RenderingContextData { + gl, device, context, swap_chain, @@ -464,4 +496,367 @@ impl SurfmanRenderingContext { device.make_context_current(&context)?; Ok(()) } + + pub fn framebuffer(&self) -> Option { + self.context_surface_info() + .unwrap_or(None) + .and_then(|info| info.framebuffer_object) + } + + /// Create a new offscreen context that is compatible with this [`SurfmanRenderingContext`]. + /// The contents of the resulting [`OffscreenRenderingContext`] are guaranteed to be blit + /// compatible with the this context. + pub fn offscreen_context(&self, size: Size2D) -> OffscreenRenderingContext { + OffscreenRenderingContext::new(SurfmanRenderingContext(self.0.clone()), size) + } +} + +struct Framebuffer { + gl: Rc, + size: Size2D, + 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, size: Size2D) -> 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, + size, + 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: Rect) -> Option { + Self::read_framebuffer_to_image(self.gl.clone(), self.framebuffer_id, source_rectangle) + } + + fn read_framebuffer_to_image( + gl: Rc, + framebuffer_id: u32, + source_rectangle: Rect, + ) -> Option { + 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.origin.x as i32, + source_rectangle.origin.y as i32, + source_rectangle.width() as gl::GLsizei, + source_rectangle.height() as gl::GLsizei, + 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: SurfmanRenderingContext, + size: Cell>, + back_framebuffer: RefCell, + front_framebuffer: RefCell>, +} + +type RenderToParentCallback = Box) + Send + Sync>; + +impl OffscreenRenderingContext { + fn new(parent_context: SurfmanRenderingContext, size: Size2D) -> Self { + let next_framebuffer = Framebuffer::new(parent_context.gl_api(), size); + Self { + parent_context, + size: Cell::new(size), + back_framebuffer: RefCell::new(next_framebuffer), + front_framebuffer: Default::default(), + } + } + + pub fn parent_context(&self) -> &SurfmanRenderingContext { + &self.parent_context + } + + pub fn front_framebuffer_id(&self) -> Option { + self.front_framebuffer + .borrow() + .as_ref() + .map(|framebuffer| framebuffer.framebuffer_id) + } + + pub fn render_to_parent_callback(&self) -> Option { + // Don't accept a `None` context for the read framebuffer. + let front_framebuffer_id = + NonZeroU32::new(self.front_framebuffer_id()?).map(NativeFramebuffer)?; + let parent_context_framebuffer_id = self.parent_context.framebuffer(); + let size = self.size.get(); + Some(Box::new(move |gl, target_rect| { + Self::render_framebuffer_to_parent_context( + gl, + Rect::new(Point2D::origin(), size.to_i32()), + front_framebuffer_id, + target_rect, + parent_context_framebuffer_id, + ); + })) + } + + #[allow(unsafe_code)] + fn render_framebuffer_to_parent_context( + gl: &glow::Context, + source_rect: Rect, + source_framebuffer_id: NativeFramebuffer, + target_rect: Rect, + target_framebuffer_id: Option, + ) { + 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 resize(&self, size: Size2D) { + // We do not resize any buffers right now. The current buffers might be too big or too + // small, but we only want to ensure (later) that next buffer that we draw to is the + // correct size. + self.size.set(size.to_u32()); + } + + fn prepare_for_rendering(&self) { + self.back_framebuffer.borrow().bind(); + } + + fn present(&self) { + trace!( + "Unbinding FBO {}", + self.back_framebuffer.borrow().framebuffer_id + ); + self.gl_api().bind_framebuffer(gl::FRAMEBUFFER, 0); + + let new_back_framebuffer = match self.front_framebuffer.borrow_mut().take() { + Some(framebuffer) if framebuffer.size == self.size.get() => framebuffer, + _ => Framebuffer::new(self.gl_api(), self.size.get()), + }; + + let new_front_framebuffer = std::mem::replace( + &mut *self.back_framebuffer.borrow_mut(), + new_back_framebuffer, + ); + *self.front_framebuffer.borrow_mut() = Some(new_front_framebuffer); + } + + fn make_current(&self) -> Result<(), surfman::Error> { + self.parent_context.make_gl_context_current() + } + + fn gl_api(&self) -> Rc { + self.parent_context.gl_api() + } + + fn gl_version(&self) -> GLVersion { + self.parent_context.gl_version() + } + + fn create_texture(&self, surface: Surface) -> Option<(SurfaceTexture, u32, Size2D)> { + self.parent_context.create_texture(surface) + } + + fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option { + self.parent_context.destroy_texture(surface_texture) + } + + fn connection(&self) -> Option { + Some(self.parent_context.connection()) + } + + fn read_to_image(&self, source_rectangle: Rect) -> Option { + self.back_framebuffer + .borrow() + .read_to_image(source_rectangle) + } +} + +#[cfg(test)] +mod test { + use euclid::{Point2D, Rect, 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, Size2D::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 img = framebuffer + .read_to_image(Rect::new(Point2D::zero(), Size2D::new(SIZE, SIZE))) + .expect("Should have been able to read back image."); + assert_eq!(img.width(), SIZE); + assert_eq!(img.height(), SIZE); + + let expected_pixel: Rgba = Rgba([12, 34, 56, 78]); + assert!(img.pixels().all(|&p| p == expected_pixel)); + } + + device.destroy_context(&mut context)?; + + Ok(()) + } } diff --git a/ports/servoshell/desktop/app.rs b/ports/servoshell/desktop/app.rs index 6ab91f1d9b9..aa54b0a3b7c 100644 --- a/ports/servoshell/desktop/app.rs +++ b/ports/servoshell/desktop/app.rs @@ -11,19 +11,16 @@ use std::time::Instant; use std::{env, fs}; use log::{info, trace, warn}; -use raw_window_handle::HasDisplayHandle; use servo::compositing::windowing::{AnimationState, WindowMethods}; use servo::compositing::CompositeTarget; use servo::config::opts::Opts; use servo::config::prefs::Preferences; use servo::servo_config::pref; use servo::servo_url::ServoUrl; -use servo::webrender_traits::SurfmanRenderingContext; use servo::webxr::glwindow::GlWindowDiscovery; #[cfg(target_os = "windows")] use servo::webxr::openxr::{AppInfo, OpenXrDiscovery}; use servo::{EventLoopWaker, Servo}; -use surfman::Connection; use url::Url; use winit::application::ApplicationHandler; use winit::event::WindowEvent; @@ -55,18 +52,14 @@ pub struct App { state: AppState, } -pub(crate) enum Present { - Deferred, - None, -} - /// Action to be taken by the caller of [`App::handle_events`]. pub(crate) enum PumpResult { /// The caller should shut down Servo and its related context. Shutdown, Continue { - update: bool, - present: Present, + need_update: bool, + new_servo_frame: bool, + need_window_redraw: bool, }, } @@ -102,52 +95,25 @@ impl App { /// Initialize Application once event loop start running. pub fn init(&mut self, event_loop: Option<&ActiveEventLoop>) { - // Create rendering context - let initial_window_size = self.servoshell_preferences.initial_window_size; - let rendering_context = if self.servoshell_preferences.headless { - let connection = Connection::new().expect("Failed to create connection"); - let adapter = connection - .create_software_adapter() - .expect("Failed to create adapter"); - SurfmanRenderingContext::create( - &connection, - &adapter, - Some(initial_window_size.to_untyped().to_i32()), - ) - .expect("Failed to create WR surfman") - } else { - let display_handle = event_loop - .unwrap() - .display_handle() - .expect("could not get display handle from window"); - let connection = Connection::from_display_handle(display_handle) - .expect("Failed to create connection"); - let adapter = connection - .create_adapter() - .expect("Failed to create adapter"); - SurfmanRenderingContext::create(&connection, &adapter, None) - .expect("Failed to create WR surfman") - }; - let headless = self.servoshell_preferences.headless; - let window = if headless { - headless_window::Window::new(&self.servoshell_preferences) - } else { - Rc::new(headed_window::Window::new( - &self.opts, - &self.servoshell_preferences, - &rendering_context, - event_loop.unwrap(), - )) - }; - if window.winit_window().is_some() { - self.minibrowser = Some(Minibrowser::new( - &rendering_context, - event_loop.unwrap(), - self.initial_url.clone(), - )); - } + assert_eq!(headless, event_loop.is_none()); + let window = match event_loop { + Some(event_loop) => { + let window = headed_window::Window::new( + &self.opts, + &self.servoshell_preferences, + event_loop, + ); + self.minibrowser = Some(Minibrowser::new( + window.offscreen_rendering_context(), + event_loop, + self.initial_url.clone(), + )); + Rc::new(window) + }, + None => headless_window::Window::new(&self.servoshell_preferences), + }; self.windows.insert(window.id(), window); @@ -174,12 +140,6 @@ impl App { // Implements embedder methods, used by libservo and constellation. let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery)); - let composite_target = if self.minibrowser.is_some() { - CompositeTarget::OffscreenFbo - } else { - CompositeTarget::ContextFbo - }; - // TODO: Remove this once dyn upcasting coercion stabilises // struct UpcastedWindow(Rc); @@ -195,11 +155,11 @@ impl App { let servo = Servo::new( self.opts.clone(), self.preferences.clone(), - Rc::new(rendering_context), + window.rendering_context(), embedder, Rc::new(UpcastedWindow(window.clone())), self.servoshell_preferences.user_agent.clone(), - composite_target, + CompositeTarget::ContextFbo, ); servo.setup_logging(); @@ -233,33 +193,28 @@ impl App { state.shutdown(); self.state = AppState::ShuttingDown; }, - PumpResult::Continue { update, present } => { - if update { - if let Some(ref mut minibrowser) = self.minibrowser { - if minibrowser.update_webview_data(state) { - // Update the minibrowser immediately. While we could update by requesting a - // redraw, doing so would delay the location update by two frames. - minibrowser.update( - window.winit_window().unwrap(), - state, - "update_location_in_toolbar", - ); - } - } + PumpResult::Continue { + need_update: update, + new_servo_frame, + need_window_redraw, + } => { + // A new Servo frame is ready, so swap the buffer on our `RenderingContext`. In headed mode + // this won't immediately update the widget surface, because we render to an offscreen + // `RenderingContext`. + if new_servo_frame { + state.servo().present(); } - match present { - Present::Deferred => { - // The compositor has painted to this frame. - trace!("PumpResult::Present::Deferred"); - // Request a winit redraw event, so we can paint the minibrowser and present. - // Otherwise, it's in headless mode and we present directly. - if let Some(window) = window.winit_window() { - window.request_redraw(); - } else { - state.servo().present(); - } - }, - Present::None => {}, + + let updated = match (update, &mut self.minibrowser) { + (true, Some(minibrowser)) => minibrowser.update_webview_data(state), + _ => false, + }; + + // If in headed mode, request a winit redraw event, so we can paint the minibrowser. + if updated || need_window_redraw || new_servo_frame { + if let Some(window) = window.winit_window() { + window.request_redraw(); + } } }, } @@ -292,15 +247,12 @@ impl App { state.shutdown(); self.state = AppState::ShuttingDown; }, - PumpResult::Continue { present, .. } => { - match present { - Present::Deferred => { - // The compositor has painted to this frame. - trace!("PumpResult::Present::Deferred"); - // In headless mode, we present directly. - state.servo().present(); - }, - Present::None => {}, + PumpResult::Continue { + new_servo_frame, .. + } => { + if new_servo_frame { + // In headless mode, we present directly. + state.servo().present(); } }, } @@ -401,8 +353,6 @@ impl ApplicationHandler for App { minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested"); minibrowser.paint(window.winit_window().unwrap()); } - - state.servo().present(); } // Handle the event diff --git a/ports/servoshell/desktop/app_state.rs b/ports/servoshell/desktop/app_state.rs index 154daea0712..cd920e90683 100644 --- a/ports/servoshell/desktop/app_state.rs +++ b/ports/servoshell/desktop/app_state.rs @@ -25,7 +25,7 @@ use servo::{ use tinyfiledialogs::MessageBoxIcon; use url::Url; -use super::app::{Present, PumpResult}; +use super::app::PumpResult; use super::dialog::Dialog; use super::gamepad::GamepadSupport; use super::keyutils::CMD_OR_CONTROL; @@ -74,7 +74,7 @@ pub struct RunningAppStateInner { need_update: bool, /// Whether or not the application needs to be redrawn. - need_present: bool, + new_servo_frame_ready: bool, } impl Drop for RunningAppState { @@ -101,7 +101,7 @@ impl RunningAppState { window, gamepad_support: GamepadSupport::maybe_new(), need_update: false, - need_present: false, + new_servo_frame_ready: false, }), } } @@ -133,28 +133,21 @@ impl RunningAppState { self.handle_gamepad_events(); } - let should_continue = self.servo().spin_event_loop(); - - // Delegate handlers may have asked us to present or update compositor contents. - let need_present = std::mem::replace(&mut self.inner_mut().need_present, false); - let need_update = std::mem::replace(&mut self.inner_mut().need_update, false); - - if !should_continue { + if !self.servo().spin_event_loop() { return PumpResult::Shutdown; } - // Currently, egui-file-dialog dialogs need to be constantly presented or animations aren't fluid. - let need_present = need_present || self.has_active_dialog(); + // Delegate handlers may have asked us to present or update compositor contents. + let new_servo_frame = std::mem::replace(&mut self.inner_mut().new_servo_frame_ready, false); + let need_update = std::mem::replace(&mut self.inner_mut().need_update, false); - let present = if need_present { - Present::Deferred - } else { - Present::None - }; + // Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid. + let need_window_redraw = new_servo_frame || self.has_active_dialog(); PumpResult::Continue { - update: need_update, - present, + need_update, + new_servo_frame, + need_window_redraw, } } @@ -240,7 +233,6 @@ impl RunningAppState { .or_default() .push(dialog); inner_mut.need_update = true; - inner_mut.need_present = true; } fn has_active_dialog(&self) -> bool { @@ -415,22 +407,17 @@ impl WebViewDelegate for RunningAppState { } fn notify_ready_to_show(&self, webview: servo::WebView) { - let scale = self.inner().window.hidpi_factor().get(); - let toolbar = self.inner().window.toolbar_height().get(); - - // Adjust for our toolbar height. - // TODO: Adjust for egui window decorations if we end up using those - let mut rect = self + let rect = self .inner() .window .get_coordinates() .get_viewport() .to_f32(); - rect.min.y += toolbar * scale; webview.focus(); webview.move_resize(rect); webview.raise_to_top(true); + webview.notify_rendering_context_resized(); } fn notify_closed(&self, webview: servo::WebView) { @@ -497,7 +484,7 @@ impl WebViewDelegate for RunningAppState { } fn notify_new_frame_ready(&self, _webview: servo::WebView) { - self.inner_mut().need_present = true; + self.inner_mut().new_servo_frame_ready = true; } fn play_gamepad_haptic_effect( diff --git a/ports/servoshell/desktop/headed_window.rs b/ports/servoshell/desktop/headed_window.rs index a0348c0a8d7..b68f50ae284 100644 --- a/ports/servoshell/desktop/headed_window.rs +++ b/ports/servoshell/desktop/headed_window.rs @@ -12,7 +12,7 @@ use std::time::Duration; use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D}; use keyboard_types::{Modifiers, ShortcutMatcher}; -use log::{debug, info}; +use log::{debug, info, warn}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use servo::compositing::windowing::{ AnimationState, EmbedderCoordinates, WebRenderDebugOption, WindowMethods, @@ -22,13 +22,14 @@ use servo::servo_config::pref; use servo::servo_geometry::DeviceIndependentPixel; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel}; use servo::webrender_api::ScrollLocation; +use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext}; use servo::webrender_traits::SurfmanRenderingContext; use servo::{ Cursor, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, Theme, TouchEvent, TouchEventAction, TouchId, WebView, WheelDelta, WheelEvent, WheelMode, }; -use surfman::{Context, Device, SurfaceType}; +use surfman::{Connection, Context, Device, SurfaceType}; use url::Url; use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize}; use winit::event::{ @@ -52,9 +53,9 @@ pub struct Window { inner_size: Cell>, toolbar_height: Cell>, mouse_down_button: Cell>, - mouse_down_point: Cell>, + webview_relative_mouse_down_point: Cell>, monitor: winit::monitor::MonitorHandle, - mouse_pos: Cell>, + webview_relative_mouse_point: Cell>, last_pressed: Cell)>>, /// A map of winit's key codes to key values that are interpreted from /// winit's ReceivedChar events. @@ -64,13 +65,21 @@ pub struct Window { device_pixel_ratio_override: Option, xr_window_poses: RefCell>>, modifiers_state: Cell, + + /// The RenderingContext that renders directly onto the Window. This is used as + /// the target of egui rendering and also where Servo rendering results are finally + /// blitted. + window_rendering_context: SurfmanRenderingContext, + + /// The `RenderingContext` of Servo itself. This is used to render Servo results + /// temporarily until they can be blitted into the egui scene. + rendering_context: Rc, } impl Window { pub fn new( opts: &Opts, servoshell_preferences: &ServoShellPreferences, - rendering_context: &SurfmanRenderingContext, event_loop: &ActiveEventLoop, ) -> Window { // If there's no chrome, start off with the window invisible. It will be set to visible in @@ -111,37 +120,48 @@ impl Window { let screen_scale: Scale = Scale::new(screen_scale); let screen_size = (winit_size_to_euclid_size(screen_size).to_f64() / screen_scale).to_u32(); + let inner_size = winit_window.inner_size(); - // Initialize surfman + let display_handle = event_loop + .display_handle() + .expect("could not get display handle from window"); + let connection = + Connection::from_display_handle(display_handle).expect("Failed to create connection"); + let adapter = connection + .create_adapter() + .expect("Failed to create adapter"); let window_handle = winit_window .window_handle() .expect("could not get window handle from window"); - - let inner_size = winit_window.inner_size(); - let native_widget = rendering_context - .connection() + let native_widget = connection .create_native_widget_from_window_handle( window_handle, winit_size_to_euclid_size(inner_size).to_i32().to_untyped(), ) .expect("Failed to create native widget"); - let surface_type = SurfaceType::Widget { native_widget }; - let surface = rendering_context - .create_surface(surface_type) + let window_rendering_context = SurfmanRenderingContext::create(&connection, &adapter, None) + .expect("Failed to create window RenderingContext"); + let surface = window_rendering_context + .create_surface(SurfaceType::Widget { native_widget }) .expect("Failed to create surface"); - rendering_context + window_rendering_context .bind_surface(surface) .expect("Failed to bind surface"); + // Make sure the gl context is made current. - rendering_context.make_gl_context_current().unwrap(); + window_rendering_context.make_gl_context_current().unwrap(); + + let rendering_context_size = Size2D::new(inner_size.width, inner_size.height); + let rendering_context = + Rc::new(window_rendering_context.offscreen_context(rendering_context_size)); debug!("Created window {:?}", winit_window.id()); Window { winit_window, mouse_down_button: Cell::new(None), - mouse_down_point: Cell::new(Point2D::zero()), - mouse_pos: Cell::new(Point2D::zero()), + webview_relative_mouse_down_point: Cell::new(Point2D::zero()), + webview_relative_mouse_point: Cell::new(Point2D::zero()), last_pressed: Cell::new(None), keys_down: RefCell::new(HashMap::new()), animation_state: Cell::new(AnimationState::Idle), @@ -153,6 +173,8 @@ impl Window { xr_window_poses: RefCell::new(vec![]), modifiers_state: Cell::new(ModifiersState::empty()), toolbar_height: Cell::new(Default::default()), + window_rendering_context, + rendering_context, } } @@ -244,13 +266,7 @@ impl Window { } /// Helper function to handle a click - fn handle_mouse( - &self, - webview: &WebView, - button: MouseButton, - action: ElementState, - coords: Point2D, - ) { + fn handle_mouse(&self, webview: &WebView, button: MouseButton, action: ElementState) { let max_pixel_dist = 10.0 * self.hidpi_factor().get(); let mouse_button = match &button { MouseButton::Left => ServoMouseButton::Left, @@ -261,9 +277,10 @@ impl Window { MouseButton::Other(value) => ServoMouseButton::Other(*value), }; + let point = self.webview_relative_mouse_point.get(); let action = match action { ElementState::Pressed => { - self.mouse_down_point.set(coords); + self.webview_relative_mouse_down_point.set(point); self.mouse_down_button.set(Some(button)); MouseButtonAction::Down }, @@ -273,7 +290,7 @@ impl Window { webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent { action, button: mouse_button, - point: coords.to_f32(), + point, })); // Also send a 'click' event if this is release and the press was recorded @@ -285,14 +302,13 @@ impl Window { } if let Some(mouse_down_button) = self.mouse_down_button.get() { - let pixel_dist = self.mouse_down_point.get() - coords; - let pixel_dist = - ((pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y) as f32).sqrt(); + let pixel_dist = self.webview_relative_mouse_down_point.get() - point; + let pixel_dist = (pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y).sqrt(); if mouse_down_button == button && pixel_dist < max_pixel_dist { webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent { action: MouseButtonAction::Click, button: mouse_button, - point: coords.to_f32(), + point, })); } } @@ -418,6 +434,10 @@ impl Window { .otherwise(|| handled = false); handled } + + pub(crate) fn offscreen_rendering_context(&self) -> Rc { + self.rendering_context.clone() + } } impl WindowPortsMethods for Window { @@ -545,15 +565,15 @@ impl WindowPortsMethods for Window { WindowEvent::ModifiersChanged(modifiers) => self.modifiers_state.set(modifiers.state()), WindowEvent::MouseInput { state, button, .. } => { if button == MouseButton::Left || button == MouseButton::Right { - self.handle_mouse(&webview, button, state, self.mouse_pos.get()); + self.handle_mouse(&webview, button, state); } }, WindowEvent::CursorMoved { position, .. } => { - let position = winit_position_to_euclid_point(position); - self.mouse_pos.set(position.to_i32()); - webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent { - point: position.to_f32(), - })); + let mut point = winit_position_to_euclid_point(position).to_f32(); + point.y -= (self.toolbar_height() * self.hidpi_factor()).0; + + self.webview_relative_mouse_point.set(point); + webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent { point })); }, WindowEvent::MouseWheel { delta, phase, .. } => { let (mut dx, mut dy, mode) = match delta { @@ -574,8 +594,8 @@ impl WindowPortsMethods for Window { z: 0.0, mode, }; - let pos = self.mouse_pos.get(); - let point = Point2D::new(pos.x as f32, pos.y as f32); + let pos = self.webview_relative_mouse_point.get(); + let point = Point2D::new(pos.x, pos.y); // Scroll events snap to the major axis of movement, with vertical // preferred over horizontal. @@ -590,7 +610,11 @@ impl WindowPortsMethods for Window { // Send events webview.notify_input_event(InputEvent::Wheel(WheelEvent { delta, point })); - webview.notify_scroll_event(scroll_location, self.mouse_pos.get(), phase); + webview.notify_scroll_event( + scroll_location, + self.webview_relative_mouse_point.get().to_i32(), + phase, + ); }, WindowEvent::Touch(touch) => { webview.notify_input_event(InputEvent::Touch(TouchEvent { @@ -607,6 +631,14 @@ impl WindowPortsMethods for Window { }, WindowEvent::Resized(new_size) => { if self.inner_size.get() != new_size { + let rendering_context_size = Size2D::new(new_size.width, new_size.height); + if let Err(error) = self + .window_rendering_context + .resize(rendering_context_size.to_i32()) + { + warn!("Could not resize window RenderingContext: {error:?}"); + } + self.inner_size.set(new_size); webview.notify_rendering_context_resized(); } @@ -658,6 +690,10 @@ impl WindowPortsMethods for Window { fn set_toolbar_height(&self, height: Length) { self.toolbar_height.set(height); } + + fn rendering_context(&self) -> Rc { + self.rendering_context.clone() + } } impl WindowMethods for Window { @@ -671,7 +707,9 @@ impl WindowMethods for Window { let window_rect = (window_rect.to_f64() / window_scale).to_i32(); let viewport_origin = DeviceIntPoint::zero(); // bottom left - let viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32(); + let mut viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32(); + viewport_size.height -= (self.toolbar_height() * self.hidpi_factor()).0; + let viewport = DeviceIntRect::from_origin_and_size(viewport_origin, viewport_size.to_i32()); let screen_size = self.screen_size.to_i32(); diff --git a/ports/servoshell/desktop/headless_window.rs b/ports/servoshell/desktop/headless_window.rs index 1409b351ae3..5fa341b326a 100644 --- a/ports/servoshell/desktop/headless_window.rs +++ b/ports/servoshell/desktop/headless_window.rs @@ -12,6 +12,9 @@ use euclid::{Box2D, Length, Point2D, Scale, Size2D}; use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods}; use servo::servo_geometry::DeviceIndependentPixel; use servo::webrender_api::units::{DeviceIntSize, DevicePixel}; +use servo::webrender_traits::rendering_context::RenderingContext; +use servo::webrender_traits::SurfmanRenderingContext; +use surfman::Connection; use super::app_state::RunningAppState; use crate::desktop::window_trait::WindowPortsMethods; @@ -24,19 +27,31 @@ pub struct Window { inner_size: Cell, screen_size: Size2D, window_rect: Box2D, + rendering_context: SurfmanRenderingContext, } impl Window { #[allow(clippy::new_ret_no_self)] pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc { + let size = servoshell_preferences.initial_window_size; + let connection = Connection::new().expect("Failed to create connection"); + let adapter = connection + .create_software_adapter() + .expect("Failed to create adapter"); + let rendering_context = SurfmanRenderingContext::create( + &connection, + &adapter, + Some(size.to_untyped().to_i32()), + ) + .expect("Failed to create WR surfman"); + let device_pixel_ratio_override = servoshell_preferences.device_pixel_ratio_override; let device_pixel_ratio_override: Option> = device_pixel_ratio_override.map(Scale::new); let hidpi_factor = device_pixel_ratio_override.unwrap_or_else(Scale::identity); - let size = servoshell_preferences.initial_window_size.to_i32(); let inner_size = Cell::new((size.to_f32() * hidpi_factor).to_i32()); - let window_rect = Box2D::from_origin_and_size(Point2D::zero(), size); + let window_rect = Box2D::from_origin_and_size(Point2D::zero(), size.to_i32()); let screen_size = servoshell_preferences.screen_size_override.map_or_else( || window_rect.size(), @@ -50,6 +65,7 @@ impl Window { inner_size, screen_size, window_rect, + rendering_context, }; Rc::new(window) @@ -132,6 +148,12 @@ impl WindowPortsMethods for Window { fn set_toolbar_height(&self, _height: Length) { unimplemented!("headless Window only") } + + fn rendering_context(&self) -> Rc { + // `SurfmanRenderingContext` uses shared ownership internally so cloning it here does + // not create a new one really. + Rc::new(self.rendering_context.clone()) + } } impl WindowMethods for Window { diff --git a/ports/servoshell/desktop/minibrowser.rs b/ports/servoshell/desktop/minibrowser.rs index 31c4f5c6688..b9ae9966a5a 100644 --- a/ports/servoshell/desktop/minibrowser.rs +++ b/ports/servoshell/desktop/minibrowser.rs @@ -3,27 +3,25 @@ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ use std::cell::{Cell, RefCell}; -use std::num::NonZeroU32; +use std::rc::Rc; use std::sync::Arc; use std::time::Instant; use egui::text::{CCursor, CCursorRange}; use egui::text_edit::TextEditState; use egui::{ - pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, Pos2, SelectableLabel, + pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, SelectableLabel, TopBottomPanel, Vec2, }; use egui_glow::CallbackFn; use egui_winit::EventResponse; -use euclid::{Box2D, Length, Point2D, Scale, Size2D}; -use gleam::gl; -use glow::NativeFramebuffer; +use euclid::{Box2D, Length, Point2D, Rect, Scale, Size2D}; use log::{trace, warn}; use servo::base::id::WebViewId; use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_url::ServoUrl; use servo::webrender_api::units::DevicePixel; -use servo::webrender_traits::SurfmanRenderingContext; +use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext}; use servo::{LoadStatus, WebView}; use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::event_loop::ActiveEventLoop; @@ -34,14 +32,11 @@ use super::egui_glue::EguiGlow; use super::geometry::winit_position_to_euclid_point; pub struct Minibrowser { + rendering_context: Rc, pub context: EguiGlow, pub event_queue: RefCell>, 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, @@ -81,12 +76,14 @@ impl Drop for Minibrowser { impl Minibrowser { pub fn new( - rendering_context: &SurfmanRenderingContext, + rendering_context: Rc, event_loop: &ActiveEventLoop, initial_url: ServoUrl, ) -> Self { let gl = unsafe { - glow::Context::from_loader_function(|s| rendering_context.get_proc_address(s)) + glow::Context::from_loader_function(|s| { + rendering_context.parent_context().get_proc_address(s) + }) }; // Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs @@ -99,17 +96,11 @@ impl Minibrowser { .egui_ctx .options_mut(|options| options.zoom_with_keyboard = false); - let widget_surface_fbo = match rendering_context.context_surface_info() { - Ok(Some(info)) => info.framebuffer_object, - Ok(None) => panic!("Failed to get widget surface info from surfman!"), - Err(error) => panic!("Failed to get widget surface info from surfman! {error:?}"), - }; - Self { + rendering_context, 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()), @@ -268,17 +259,16 @@ impl Minibrowser { reason ); let Self { + rendering_context, context, event_queue, toolbar_height, - widget_surface_fbo, last_update, location, location_dirty, .. } = self; - let widget_fbo = *widget_surface_fbo; - let servo_framebuffer_id = state.servo().offscreen_framebuffer_id(); + let _duration = context.run(window, |ctx| { // TODO: While in fullscreen add some way to mitigate the increased phishing risk // when not displaying the URL bar: https://github.com/servo/servo/issues/32443 @@ -387,26 +377,23 @@ impl Minibrowser { return; }; CentralPanel::default().frame(Frame::NONE).show(ctx, |ui| { - let Pos2 { x, y } = ui.cursor().min; - let Vec2 { - x: width, - y: height, - } = ui.available_size(); - let rect = - Box2D::from_origin_and_size(Point2D::new(x, y), Size2D::new(width, height)) * - scale; + // If the top parts of the GUI changed size, then update the size of the WebView and also + // the size of its RenderingContext. + let available_size = ui.available_size(); + let rect = Box2D::from_origin_and_size( + Point2D::origin(), + Size2D::new(available_size.x, available_size.y), + ) * scale; if rect != webview.rect() { webview.move_resize(rect); + rendering_context.resize(rect.size().to_i32().to_untyped()); } + 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; - }; - if let Some(status_text) = &self.status_text { egui::containers::popup::show_tooltip_at( ctx, @@ -417,45 +404,19 @@ impl Minibrowser { ); } - 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, + if let Some(render_to_parent) = rendering_context.render_to_parent_callback() { + ui.painter().add(PaintCallback { + rect, + callback: Arc::new(CallbackFn::new(move |info, painter| { + let clip = info.viewport_in_pixels(); + let rect_in_parent = Rect::new( + Point2D::new(clip.left_px, clip.from_bottom_px), + Size2D::new(clip.width_px, clip.height_px), ); - painter.gl().bind_framebuffer(gl::FRAMEBUFFER, widget_fbo); - } - })), - }); + render_to_parent(painter.gl(), rect_in_parent) + })), + }); + } }); *last_update = now; @@ -464,14 +425,11 @@ impl Minibrowser { /// Paint the minibrowser, as of the last update. pub fn paint(&mut self, window: &Window) { - unsafe { - use glow::HasContext as _; - self.context - .painter - .gl() - .bind_framebuffer(gl::FRAMEBUFFER, self.widget_surface_fbo); - } + self.rendering_context + .parent_context() + .prepare_for_rendering(); self.context.paint(window); + let _ = self.rendering_context.parent_context().present(); } /// Updates the location field from the given [WebViewManager], unless the user has started diff --git a/ports/servoshell/desktop/window_trait.rs b/ports/servoshell/desktop/window_trait.rs index da3f2e4c82e..934a5d32990 100644 --- a/ports/servoshell/desktop/window_trait.rs +++ b/ports/servoshell/desktop/window_trait.rs @@ -11,6 +11,7 @@ use euclid::{Length, Scale}; use servo::compositing::windowing::WindowMethods; use servo::servo_geometry::DeviceIndependentPixel; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel}; +use servo::webrender_traits::rendering_context::RenderingContext; use servo::{Cursor, WebView}; use super::app_state::RunningAppState; @@ -46,4 +47,5 @@ pub trait WindowPortsMethods: WindowMethods { fn winit_window(&self) -> Option<&winit::window::Window>; fn toolbar_height(&self) -> Length; fn set_toolbar_height(&self, height: Length); + fn rendering_context(&self) -> Rc; } diff --git a/python/servo/testing_commands.py b/python/servo/testing_commands.py index 9411ee5387d..a592c50451e 100644 --- a/python/servo/testing_commands.py +++ b/python/servo/testing_commands.py @@ -161,7 +161,6 @@ class MachCommands(CommandBase): self_contained_tests = [ "background_hang_monitor", "base", - "compositing", "constellation", "fonts", "hyper_serde", @@ -175,6 +174,7 @@ class MachCommands(CommandBase): "servo_config", "servoshell", "style_config", + "webrender_traits", ] if not packages: packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store'])