aboutsummaryrefslogtreecommitdiffstats
path: root/components/shared/compositing/rendering_context.rs
diff options
context:
space:
mode:
authorMartin Robinson <mrobinson@igalia.com>2025-04-06 19:34:18 +0200
committerGitHub <noreply@github.com>2025-04-06 17:34:18 +0000
commit0caa271176d4670eb06bedd05cdffb24df08fc4f (patch)
tree9a0c3431dcf7ac31cfbdc1f801c9f4ef3dda6ae7 /components/shared/compositing/rendering_context.rs
parente74a042efdf01ab2ff32e82e203bd1d954b599bd (diff)
downloadservo-0caa271176d4670eb06bedd05cdffb24df08fc4f.tar.gz
servo-0caa271176d4670eb06bedd05cdffb24df08fc4f.zip
`compositing`: Combine `webrender_traits` and `compositing_traits` (#36372)
These two traits both exposed different parts of the compositing API, but now that the compositor doesn't depend directly on `script` any longer and the `script_traits` crate has been split into the `constellation_traits` crate, this can be finally be cleaned up without causing circular dependencies. In addition, some unit tests for the `IOPCompositor`'s scroll node tree are also moved into `compositing_traits` as well. Testing: This just combines two crates, so no new tests are necessary. Fixes: #35984. Signed-off-by: Martin Robinson <mrobinson@igalia.com> Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Diffstat (limited to 'components/shared/compositing/rendering_context.rs')
-rw-r--r--components/shared/compositing/rendering_context.rs887
1 files changed, 887 insertions, 0 deletions
diff --git a/components/shared/compositing/rendering_context.rs b/components/shared/compositing/rendering_context.rs
new file mode 100644
index 00000000000..46d72917510
--- /dev/null
+++ b/components/shared/compositing/rendering_context.rs
@@ -0,0 +1,887 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#![deny(unsafe_code)]
+
+use std::cell::{Cell, RefCell, RefMut};
+use std::num::NonZeroU32;
+use std::rc::Rc;
+use std::sync::Arc;
+
+use dpi::PhysicalSize;
+use euclid::default::{Rect, Size2D as UntypedSize2D};
+use euclid::{Point2D, Size2D};
+use gleam::gl::{self, Gl};
+use glow::NativeFramebuffer;
+use image::RgbaImage;
+use log::{debug, trace, warn};
+use raw_window_handle::{DisplayHandle, WindowHandle};
+pub use surfman::Error;
+use surfman::chains::{PreserveBuffer, SwapChain};
+use surfman::{
+ Adapter, Connection, Context, ContextAttributeFlags, ContextAttributes, Device, GLApi,
+ NativeContext, NativeWidget, Surface, SurfaceAccess, SurfaceInfo, SurfaceTexture, SurfaceType,
+};
+use webrender_api::units::{DeviceIntRect, DevicePixel};
+
+/// The `RenderingContext` trait defines a set of methods for managing
+/// an OpenGL or GLES rendering context.
+/// Implementors of this trait are responsible for handling the creation,
+/// management, and destruction of the rendering context and its associated
+/// resources.
+pub trait RenderingContext {
+ /// Prepare this [`RenderingContext`] to be rendered upon by Servo. For instance,
+ /// by binding a framebuffer to the current OpenGL context.
+ fn prepare_for_rendering(&self) {}
+ /// Read the contents of this [`Renderingcontext`] into an in-memory image. If the
+ /// image cannot be read (for instance, if no rendering has taken place yet), then
+ /// `None` is returned.
+ ///
+ /// In a double-buffered [`RenderingContext`] this is expected to read from the back
+ /// buffer. That means that once Servo renders to the context, this should return those
+ /// results, even before [`RenderingContext::present`] is called.
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage>;
+ /// Get the current size of this [`RenderingContext`].
+ fn size(&self) -> PhysicalSize<u32>;
+ /// Get the current size of this [`RenderingContext`] as [`Size2D`].
+ fn size2d(&self) -> Size2D<u32, DevicePixel> {
+ let size = self.size();
+ Size2D::new(size.width, size.height)
+ }
+ /// Resizes the rendering surface to the given size.
+ fn resize(&self, size: PhysicalSize<u32>);
+ /// Presents the rendered frame to the screen. In a double-buffered context, this would
+ /// swap buffers.
+ fn present(&self);
+ /// Makes the context the current OpenGL context for this thread.
+ /// After calling this function, it is valid to use OpenGL rendering
+ /// commands.
+ fn make_current(&self) -> Result<(), Error>;
+ /// Returns the `gleam` version of the OpenGL or GLES API.
+ fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl>;
+ /// Returns the OpenGL or GLES API.
+ fn glow_gl_api(&self) -> Arc<glow::Context>;
+ /// Creates a texture from a given surface and returns the surface texture,
+ /// the OpenGL texture object, and the size of the surface. Default to `None`.
+ fn create_texture(
+ &self,
+ _surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ None
+ }
+ /// Destroys the texture and returns the surface. Default to `None`.
+ fn destroy_texture(&self, _surface_texture: SurfaceTexture) -> Option<Surface> {
+ None
+ }
+ /// The connection to the display server for WebGL. Default to `None`.
+ fn connection(&self) -> Option<Connection> {
+ None
+ }
+}
+
+/// A rendering context that uses the Surfman library to create and manage
+/// the OpenGL context and surface. This struct provides the default implementation
+/// of the `RenderingContext` trait, handling the creation, management, and destruction
+/// of the rendering context and its associated resources.
+///
+/// The `SurfmanRenderingContext` struct encapsulates the necessary data and methods
+/// to interact with the Surfman library, including creating surfaces, binding surfaces,
+/// resizing surfaces, presenting rendered frames, and managing the OpenGL context state.
+struct SurfmanRenderingContext {
+ gleam_gl: Rc<dyn Gl>,
+ glow_gl: Arc<glow::Context>,
+ device: RefCell<Device>,
+ context: RefCell<Context>,
+}
+
+impl Drop for SurfmanRenderingContext {
+ fn drop(&mut self) {
+ let device = &mut self.device.borrow_mut();
+ let context = &mut self.context.borrow_mut();
+ let _ = device.destroy_context(context);
+ }
+}
+
+impl SurfmanRenderingContext {
+ fn new(connection: &Connection, adapter: &Adapter) -> Result<Self, Error> {
+ let mut device = connection.create_device(adapter)?;
+
+ let flags = ContextAttributeFlags::ALPHA |
+ ContextAttributeFlags::DEPTH |
+ ContextAttributeFlags::STENCIL;
+ let gl_api = connection.gl_api();
+ let version = match &gl_api {
+ GLApi::GLES => surfman::GLVersion { major: 3, minor: 0 },
+ GLApi::GL => surfman::GLVersion { major: 3, minor: 2 },
+ };
+ let context_descriptor =
+ device.create_context_descriptor(&ContextAttributes { flags, version })?;
+ let context = device.create_context(&context_descriptor, None)?;
+
+ #[allow(unsafe_code)]
+ let gleam_gl = {
+ match gl_api {
+ GLApi::GL => unsafe {
+ gl::GlFns::load_with(|func_name| device.get_proc_address(&context, func_name))
+ },
+ GLApi::GLES => unsafe {
+ gl::GlesFns::load_with(|func_name| device.get_proc_address(&context, func_name))
+ },
+ }
+ };
+
+ #[allow(unsafe_code)]
+ let glow_gl = unsafe {
+ glow::Context::from_loader_function(|function_name| {
+ device.get_proc_address(&context, function_name)
+ })
+ };
+
+ Ok(SurfmanRenderingContext {
+ gleam_gl,
+ glow_gl: Arc::new(glow_gl),
+ device: RefCell::new(device),
+ context: RefCell::new(context),
+ })
+ }
+
+ fn create_surface(&self, surface_type: SurfaceType<NativeWidget>) -> Result<Surface, Error> {
+ let device = &mut self.device.borrow_mut();
+ let context = &self.context.borrow();
+ device.create_surface(context, SurfaceAccess::GPUOnly, surface_type)
+ }
+
+ fn bind_surface(&self, surface: Surface) -> Result<(), Error> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow_mut();
+ device
+ .bind_surface_to_context(context, surface)
+ .map_err(|(err, mut surface)| {
+ let _ = device.destroy_surface(context, &mut surface);
+ err
+ })?;
+ Ok(())
+ }
+
+ fn create_attached_swap_chain(&self) -> Result<SwapChain<Device>, Error> {
+ let device = &mut self.device.borrow_mut();
+ let context = &mut self.context.borrow_mut();
+ SwapChain::create_attached(device, context, SurfaceAccess::GPUOnly)
+ }
+
+ fn resize_surface(&self, size: PhysicalSize<u32>) -> Result<(), Error> {
+ let size = Size2D::new(size.width as i32, size.height as i32);
+ let device = &mut self.device.borrow_mut();
+ let context = &mut self.context.borrow_mut();
+
+ let mut surface = device.unbind_surface_from_context(context)?.unwrap();
+ device.resize_surface(context, &mut surface, size)?;
+ device
+ .bind_surface_to_context(context, surface)
+ .map_err(|(err, mut surface)| {
+ let _ = device.destroy_surface(context, &mut surface);
+ err
+ })
+ }
+
+ fn present_bound_surface(&self) -> Result<(), Error> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow_mut();
+
+ let mut surface = device.unbind_surface_from_context(context)?.unwrap();
+ device.present_surface(context, &mut surface)?;
+ device
+ .bind_surface_to_context(context, surface)
+ .map_err(|(err, mut surface)| {
+ let _ = device.destroy_surface(context, &mut surface);
+ err
+ })
+ }
+
+ #[allow(dead_code)]
+ fn native_context(&self) -> NativeContext {
+ let device = &self.device.borrow();
+ let context = &self.context.borrow();
+ device.native_context(context)
+ }
+
+ fn framebuffer(&self) -> Option<NativeFramebuffer> {
+ let device = &self.device.borrow();
+ let context = &self.context.borrow();
+ device
+ .context_surface_info(context)
+ .unwrap_or(None)
+ .and_then(|info| info.framebuffer_object)
+ }
+
+ fn prepare_for_rendering(&self) {
+ let framebuffer_id = self
+ .framebuffer()
+ .map_or(0, |framebuffer| framebuffer.0.into());
+ self.gleam_gl
+ .bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_id);
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ let framebuffer_id = self
+ .framebuffer()
+ .map_or(0, |framebuffer| framebuffer.0.into());
+ Framebuffer::read_framebuffer_to_image(&self.gleam_gl, framebuffer_id, source_rectangle)
+ }
+
+ fn make_current(&self) -> Result<(), Error> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow();
+ device.make_context_current(context)
+ }
+
+ fn create_texture(
+ &self,
+ surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow_mut();
+ let SurfaceInfo {
+ id: front_buffer_id,
+ size,
+ ..
+ } = device.surface_info(&surface);
+ debug!("... getting texture for surface {:?}", front_buffer_id);
+ let surface_texture = device.create_surface_texture(context, surface).unwrap();
+ let gl_texture = device
+ .surface_texture_object(&surface_texture)
+ .map(|tex| tex.0.get())
+ .unwrap_or(0);
+ Some((surface_texture, gl_texture, size))
+ }
+
+ fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
+ let device = &self.device.borrow();
+ let context = &mut self.context.borrow_mut();
+ device
+ .destroy_surface_texture(context, surface_texture)
+ .map_err(|(error, _)| error)
+ .ok()
+ }
+
+ fn connection(&self) -> Option<Connection> {
+ Some(self.device.borrow().connection())
+ }
+}
+
+/// A software rendering context that uses a software OpenGL implementation to render
+/// Servo. This will generally have bad performance, but can be used in situations where
+/// it is more convenient to have consistent, but slower display output.
+///
+/// The results of the render can be accessed via [`RenderingContext::read_to_image`].
+pub struct SoftwareRenderingContext {
+ size: Cell<PhysicalSize<u32>>,
+ surfman_rendering_info: SurfmanRenderingContext,
+ swap_chain: SwapChain<Device>,
+}
+
+impl SoftwareRenderingContext {
+ pub fn new(size: PhysicalSize<u32>) -> Result<Self, Error> {
+ let connection = Connection::new()?;
+ let adapter = connection.create_software_adapter()?;
+ let surfman_rendering_info = SurfmanRenderingContext::new(&connection, &adapter)?;
+
+ let surfman_size = Size2D::new(size.width as i32, size.height as i32);
+ let surface =
+ surfman_rendering_info.create_surface(SurfaceType::Generic { size: surfman_size })?;
+ surfman_rendering_info.bind_surface(surface)?;
+ surfman_rendering_info.make_current()?;
+
+ let swap_chain = surfman_rendering_info.create_attached_swap_chain()?;
+ Ok(SoftwareRenderingContext {
+ size: Cell::new(size),
+ surfman_rendering_info,
+ swap_chain,
+ })
+ }
+}
+
+impl Drop for SoftwareRenderingContext {
+ fn drop(&mut self) {
+ let device = &mut self.surfman_rendering_info.device.borrow_mut();
+ let context = &mut self.surfman_rendering_info.context.borrow_mut();
+ let _ = self.swap_chain.destroy(device, context);
+ }
+}
+
+impl RenderingContext for SoftwareRenderingContext {
+ fn prepare_for_rendering(&self) {
+ self.surfman_rendering_info.prepare_for_rendering();
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ self.surfman_rendering_info.read_to_image(source_rectangle)
+ }
+
+ fn size(&self) -> PhysicalSize<u32> {
+ self.size.get()
+ }
+
+ fn resize(&self, size: PhysicalSize<u32>) {
+ if self.size.get() == size {
+ return;
+ }
+
+ self.size.set(size);
+
+ let device = &mut self.surfman_rendering_info.device.borrow_mut();
+ let context = &mut self.surfman_rendering_info.context.borrow_mut();
+ let size = Size2D::new(size.width as i32, size.height as i32);
+ let _ = self.swap_chain.resize(device, context, size);
+ }
+
+ fn present(&self) {
+ let device = &mut self.surfman_rendering_info.device.borrow_mut();
+ let context = &mut self.surfman_rendering_info.context.borrow_mut();
+ let _ = self
+ .swap_chain
+ .swap_buffers(device, context, PreserveBuffer::No);
+ }
+
+ fn make_current(&self) -> Result<(), Error> {
+ self.surfman_rendering_info.make_current()
+ }
+
+ fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
+ self.surfman_rendering_info.gleam_gl.clone()
+ }
+
+ fn glow_gl_api(&self) -> Arc<glow::Context> {
+ self.surfman_rendering_info.glow_gl.clone()
+ }
+
+ fn create_texture(
+ &self,
+ surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ self.surfman_rendering_info.create_texture(surface)
+ }
+
+ fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
+ self.surfman_rendering_info.destroy_texture(surface_texture)
+ }
+
+ fn connection(&self) -> Option<Connection> {
+ self.surfman_rendering_info.connection()
+ }
+}
+
+/// A [`RenderingContext`] that uses the `surfman` library to render to a
+/// `raw-window-handle` identified window. `surfman` will attempt to create an
+/// OpenGL context and surface for this window. This is a simple implementation
+/// of the [`RenderingContext`] crate, but by default it paints to the entire window
+/// surface.
+///
+/// If you would like to paint to only a portion of the window, consider using
+/// [`OffscreenRenderingContext`] by calling [`WindowRenderingContext::offscreen_context`].
+pub struct WindowRenderingContext {
+ size: Cell<PhysicalSize<u32>>,
+ surfman_context: SurfmanRenderingContext,
+}
+
+impl WindowRenderingContext {
+ pub fn new(
+ display_handle: DisplayHandle,
+ window_handle: WindowHandle,
+ size: PhysicalSize<u32>,
+ ) -> Result<Self, Error> {
+ let connection = Connection::from_display_handle(display_handle)?;
+ let adapter = connection.create_adapter()?;
+ let surfman_context = SurfmanRenderingContext::new(&connection, &adapter)?;
+
+ let native_widget = connection
+ .create_native_widget_from_window_handle(
+ window_handle,
+ Size2D::new(size.width as i32, size.height as i32),
+ )
+ .expect("Failed to create native widget");
+
+ let surface = surfman_context.create_surface(SurfaceType::Widget { native_widget })?;
+ surfman_context.bind_surface(surface)?;
+ surfman_context.make_current()?;
+
+ Ok(Self {
+ size: Cell::new(size),
+ surfman_context,
+ })
+ }
+
+ pub fn offscreen_context(
+ self: &Rc<Self>,
+ size: PhysicalSize<u32>,
+ ) -> OffscreenRenderingContext {
+ OffscreenRenderingContext::new(self.clone(), size)
+ }
+
+ /// Stop rendering to the window that was used to create this `WindowRenderingContext`
+ /// or last set with [`Self::set_window`].
+ ///
+ /// TODO: This should be removed once `WebView`s can replace their `RenderingContext`s.
+ pub fn take_window(&self) -> Result<(), Error> {
+ let device = self.surfman_context.device.borrow_mut();
+ let mut context = self.surfman_context.context.borrow_mut();
+ let mut surface = device.unbind_surface_from_context(&mut context)?.unwrap();
+ device.destroy_surface(&mut context, &mut surface)?;
+ Ok(())
+ }
+
+ /// Replace the window that this [`WindowRenderingContext`] renders to and give it a new
+ /// size.
+ ///
+ /// TODO: This should be removed once `WebView`s can replace their `RenderingContext`s.
+ pub fn set_window(
+ &self,
+ window_handle: WindowHandle,
+ size: PhysicalSize<u32>,
+ ) -> Result<(), Error> {
+ let mut device = self.surfman_context.device.borrow_mut();
+ let mut context = self.surfman_context.context.borrow_mut();
+
+ let native_widget = device
+ .connection()
+ .create_native_widget_from_window_handle(
+ window_handle,
+ Size2D::new(size.width as i32, size.height as i32),
+ )
+ .expect("Failed to create native widget");
+
+ let surface_access = SurfaceAccess::GPUOnly;
+ let surface_type = SurfaceType::Widget { native_widget };
+ let surface = device.create_surface(&context, surface_access, surface_type)?;
+
+ device
+ .bind_surface_to_context(&mut context, surface)
+ .map_err(|(err, mut surface)| {
+ let _ = device.destroy_surface(&mut context, &mut surface);
+ err
+ })?;
+ device.make_context_current(&context)?;
+ Ok(())
+ }
+
+ pub fn surfman_details(&self) -> (RefMut<Device>, RefMut<Context>) {
+ (
+ self.surfman_context.device.borrow_mut(),
+ self.surfman_context.context.borrow_mut(),
+ )
+ }
+}
+
+impl RenderingContext for WindowRenderingContext {
+ fn prepare_for_rendering(&self) {
+ self.surfman_context.prepare_for_rendering();
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ self.surfman_context.read_to_image(source_rectangle)
+ }
+
+ fn size(&self) -> PhysicalSize<u32> {
+ self.size.get()
+ }
+
+ fn resize(&self, size: PhysicalSize<u32>) {
+ match self.surfman_context.resize_surface(size) {
+ Ok(..) => self.size.set(size),
+ Err(error) => warn!("Error resizing surface: {error:?}"),
+ }
+ }
+
+ fn present(&self) {
+ if let Err(error) = self.surfman_context.present_bound_surface() {
+ warn!("Error presenting surface: {error:?}");
+ }
+ }
+
+ fn make_current(&self) -> Result<(), Error> {
+ self.surfman_context.make_current()
+ }
+
+ fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
+ self.surfman_context.gleam_gl.clone()
+ }
+
+ fn glow_gl_api(&self) -> Arc<glow::Context> {
+ self.surfman_context.glow_gl.clone()
+ }
+
+ fn create_texture(
+ &self,
+ surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ self.surfman_context.create_texture(surface)
+ }
+
+ fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
+ self.surfman_context.destroy_texture(surface_texture)
+ }
+
+ fn connection(&self) -> Option<Connection> {
+ self.surfman_context.connection()
+ }
+}
+
+struct Framebuffer {
+ gl: Rc<dyn Gl>,
+ framebuffer_id: gl::GLuint,
+ renderbuffer_id: gl::GLuint,
+ texture_id: gl::GLuint,
+}
+
+impl Framebuffer {
+ fn bind(&self) {
+ trace!("Binding FBO {}", self.framebuffer_id);
+ self.gl
+ .bind_framebuffer(gl::FRAMEBUFFER, self.framebuffer_id)
+ }
+}
+
+impl Drop for Framebuffer {
+ fn drop(&mut self) {
+ self.gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
+ self.gl.delete_textures(&[self.texture_id]);
+ self.gl.delete_renderbuffers(&[self.renderbuffer_id]);
+ self.gl.delete_framebuffers(&[self.framebuffer_id]);
+ }
+}
+
+impl Framebuffer {
+ fn new(gl: Rc<dyn Gl>, size: PhysicalSize<u32>) -> Self {
+ let framebuffer_ids = gl.gen_framebuffers(1);
+ gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]);
+
+ let texture_ids = gl.gen_textures(1);
+ gl.bind_texture(gl::TEXTURE_2D, texture_ids[0]);
+ gl.tex_image_2d(
+ gl::TEXTURE_2D,
+ 0,
+ gl::RGBA as gl::GLint,
+ size.width as gl::GLsizei,
+ size.height as gl::GLsizei,
+ 0,
+ gl::RGBA,
+ gl::UNSIGNED_BYTE,
+ None,
+ );
+ gl.tex_parameter_i(
+ gl::TEXTURE_2D,
+ gl::TEXTURE_MAG_FILTER,
+ gl::NEAREST as gl::GLint,
+ );
+ gl.tex_parameter_i(
+ gl::TEXTURE_2D,
+ gl::TEXTURE_MIN_FILTER,
+ gl::NEAREST as gl::GLint,
+ );
+
+ gl.framebuffer_texture_2d(
+ gl::FRAMEBUFFER,
+ gl::COLOR_ATTACHMENT0,
+ gl::TEXTURE_2D,
+ texture_ids[0],
+ 0,
+ );
+
+ gl.bind_texture(gl::TEXTURE_2D, 0);
+
+ let renderbuffer_ids = gl.gen_renderbuffers(1);
+ let depth_rb = renderbuffer_ids[0];
+ gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
+ gl.renderbuffer_storage(
+ gl::RENDERBUFFER,
+ gl::DEPTH_COMPONENT24,
+ size.width as gl::GLsizei,
+ size.height as gl::GLsizei,
+ );
+ gl.framebuffer_renderbuffer(
+ gl::FRAMEBUFFER,
+ gl::DEPTH_ATTACHMENT,
+ gl::RENDERBUFFER,
+ depth_rb,
+ );
+
+ Self {
+ gl,
+ framebuffer_id: *framebuffer_ids
+ .first()
+ .expect("Guaranteed by GL operations"),
+ renderbuffer_id: *renderbuffer_ids
+ .first()
+ .expect("Guaranteed by GL operations"),
+ texture_id: *texture_ids.first().expect("Guaranteed by GL operations"),
+ }
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ Self::read_framebuffer_to_image(&self.gl, self.framebuffer_id, source_rectangle)
+ }
+
+ fn read_framebuffer_to_image(
+ gl: &Rc<dyn Gl>,
+ framebuffer_id: u32,
+ source_rectangle: DeviceIntRect,
+ ) -> Option<RgbaImage> {
+ gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_id);
+
+ // For some reason, OSMesa fails to render on the 3rd
+ // attempt in headless mode, under some conditions.
+ // I think this can only be some kind of synchronization
+ // bug in OSMesa, but explicitly un-binding any vertex
+ // array here seems to work around that bug.
+ // See https://github.com/servo/servo/issues/18606.
+ gl.bind_vertex_array(0);
+
+ let mut pixels = gl.read_pixels(
+ source_rectangle.min.x,
+ source_rectangle.min.y,
+ source_rectangle.width(),
+ source_rectangle.height(),
+ gl::RGBA,
+ gl::UNSIGNED_BYTE,
+ );
+ let gl_error = gl.get_error();
+ if gl_error != gl::NO_ERROR {
+ warn!("GL error code 0x{gl_error:x} set after read_pixels");
+ }
+
+ // flip image vertically (texture is upside down)
+ let source_rectangle = source_rectangle.to_usize();
+ let orig_pixels = pixels.clone();
+ let stride = source_rectangle.width() * 4;
+ for y in 0..source_rectangle.height() {
+ let dst_start = y * stride;
+ let src_start = (source_rectangle.height() - y - 1) * stride;
+ let src_slice = &orig_pixels[src_start..src_start + stride];
+ pixels[dst_start..dst_start + stride].clone_from_slice(&src_slice[..stride]);
+ }
+
+ RgbaImage::from_raw(
+ source_rectangle.width() as u32,
+ source_rectangle.height() as u32,
+ pixels,
+ )
+ }
+}
+
+pub struct OffscreenRenderingContext {
+ parent_context: Rc<WindowRenderingContext>,
+ size: Cell<PhysicalSize<u32>>,
+ framebuffer: RefCell<Framebuffer>,
+}
+
+type RenderToParentCallback = Box<dyn Fn(&glow::Context, Rect<i32>) + Send + Sync>;
+
+impl OffscreenRenderingContext {
+ fn new(parent_context: Rc<WindowRenderingContext>, size: PhysicalSize<u32>) -> Self {
+ let framebuffer = RefCell::new(Framebuffer::new(parent_context.gleam_gl_api(), size));
+ Self {
+ parent_context,
+ size: Cell::new(size),
+ framebuffer,
+ }
+ }
+
+ pub fn parent_context(&self) -> &WindowRenderingContext {
+ &self.parent_context
+ }
+
+ pub fn render_to_parent_callback(&self) -> Option<RenderToParentCallback> {
+ // Don't accept a `None` context for the source framebuffer.
+ let front_framebuffer_id =
+ NonZeroU32::new(self.framebuffer.borrow().framebuffer_id).map(NativeFramebuffer)?;
+ let parent_context_framebuffer_id = self.parent_context.surfman_context.framebuffer();
+ let size = self.size.get();
+ let size = Size2D::new(size.width as i32, size.height as i32);
+ Some(Box::new(move |gl, target_rect| {
+ Self::blit_framebuffer(
+ gl,
+ Rect::new(Point2D::origin(), size.to_i32()),
+ front_framebuffer_id,
+ target_rect,
+ parent_context_framebuffer_id,
+ );
+ }))
+ }
+
+ #[allow(unsafe_code)]
+ fn blit_framebuffer(
+ gl: &glow::Context,
+ source_rect: Rect<i32>,
+ source_framebuffer_id: NativeFramebuffer,
+ target_rect: Rect<i32>,
+ target_framebuffer_id: Option<NativeFramebuffer>,
+ ) {
+ use glow::HasContext as _;
+ unsafe {
+ gl.clear_color(0.0, 0.0, 0.0, 0.0);
+ gl.scissor(
+ target_rect.origin.x,
+ target_rect.origin.y,
+ target_rect.width(),
+ target_rect.height(),
+ );
+ gl.enable(gl::SCISSOR_TEST);
+ gl.clear(gl::COLOR_BUFFER_BIT);
+ gl.disable(gl::SCISSOR_TEST);
+
+ gl.bind_framebuffer(gl::READ_FRAMEBUFFER, Some(source_framebuffer_id));
+ gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, target_framebuffer_id);
+
+ gl.blit_framebuffer(
+ source_rect.origin.x,
+ source_rect.origin.y,
+ source_rect.origin.x + source_rect.width(),
+ source_rect.origin.y + source_rect.height(),
+ target_rect.origin.x,
+ target_rect.origin.y,
+ target_rect.origin.x + target_rect.width(),
+ target_rect.origin.y + target_rect.height(),
+ gl::COLOR_BUFFER_BIT,
+ gl::NEAREST,
+ );
+ gl.bind_framebuffer(gl::FRAMEBUFFER, target_framebuffer_id);
+ }
+ }
+}
+
+impl RenderingContext for OffscreenRenderingContext {
+ fn size(&self) -> PhysicalSize<u32> {
+ self.size.get()
+ }
+
+ fn resize(&self, new_size: PhysicalSize<u32>) {
+ let old_size = self.size.get();
+ if old_size == new_size {
+ return;
+ }
+
+ let gl = self.parent_context.gleam_gl_api();
+ let new_framebuffer = Framebuffer::new(gl.clone(), new_size);
+
+ let old_framebuffer =
+ std::mem::replace(&mut *self.framebuffer.borrow_mut(), new_framebuffer);
+ self.size.set(new_size);
+
+ let blit_size = new_size.min(old_size);
+ let rect = Rect::new(
+ Point2D::origin(),
+ Size2D::new(blit_size.width, blit_size.height),
+ )
+ .to_i32();
+
+ let Some(old_framebuffer_id) =
+ NonZeroU32::new(old_framebuffer.framebuffer_id).map(NativeFramebuffer)
+ else {
+ return;
+ };
+ let new_framebuffer_id =
+ NonZeroU32::new(self.framebuffer.borrow().framebuffer_id).map(NativeFramebuffer);
+ Self::blit_framebuffer(
+ &self.glow_gl_api(),
+ rect,
+ old_framebuffer_id,
+ rect,
+ new_framebuffer_id,
+ );
+ }
+
+ fn prepare_for_rendering(&self) {
+ self.framebuffer.borrow().bind();
+ }
+
+ fn present(&self) {}
+
+ fn make_current(&self) -> Result<(), surfman::Error> {
+ self.parent_context.make_current()
+ }
+
+ fn gleam_gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
+ self.parent_context.gleam_gl_api()
+ }
+
+ fn glow_gl_api(&self) -> Arc<glow::Context> {
+ self.parent_context.glow_gl_api()
+ }
+
+ fn create_texture(
+ &self,
+ surface: Surface,
+ ) -> Option<(SurfaceTexture, u32, UntypedSize2D<i32>)> {
+ self.parent_context.create_texture(surface)
+ }
+
+ fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
+ self.parent_context.destroy_texture(surface_texture)
+ }
+
+ fn connection(&self) -> Option<Connection> {
+ self.parent_context.connection()
+ }
+
+ fn read_to_image(&self, source_rectangle: DeviceIntRect) -> Option<RgbaImage> {
+ self.framebuffer.borrow().read_to_image(source_rectangle)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use dpi::PhysicalSize;
+ use euclid::{Box2D, Point2D, Size2D};
+ use gleam::gl;
+ use image::Rgba;
+ use surfman::{Connection, ContextAttributeFlags, ContextAttributes, Error, GLApi, GLVersion};
+
+ use super::Framebuffer;
+
+ #[test]
+ #[allow(unsafe_code)]
+ fn test_read_pixels() -> Result<(), Error> {
+ let connection = Connection::new()?;
+ let adapter = connection.create_software_adapter()?;
+ let mut device = connection.create_device(&adapter)?;
+ let context_descriptor = device.create_context_descriptor(&ContextAttributes {
+ version: GLVersion::new(3, 0),
+ flags: ContextAttributeFlags::empty(),
+ })?;
+ let mut context = device.create_context(&context_descriptor, None)?;
+
+ let gl = match connection.gl_api() {
+ GLApi::GL => unsafe { gl::GlFns::load_with(|s| device.get_proc_address(&context, s)) },
+ GLApi::GLES => unsafe {
+ gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
+ },
+ };
+
+ device.make_context_current(&context)?;
+
+ {
+ const SIZE: u32 = 16;
+ let framebuffer = Framebuffer::new(gl, PhysicalSize::new(SIZE, SIZE));
+ framebuffer.bind();
+ framebuffer
+ .gl
+ .clear_color(12.0 / 255.0, 34.0 / 255.0, 56.0 / 255.0, 78.0 / 255.0);
+ framebuffer.gl.clear(gl::COLOR_BUFFER_BIT);
+
+ let rect = Box2D::from_origin_and_size(Point2D::zero(), Size2D::new(SIZE, SIZE));
+ let img = framebuffer
+ .read_to_image(rect.to_i32())
+ .expect("Should have been able to read back image.");
+ assert_eq!(img.width(), SIZE);
+ assert_eq!(img.height(), SIZE);
+
+ let expected_pixel: Rgba<u8> = Rgba([12, 34, 56, 78]);
+ assert!(img.pixels().all(|&p| p == expected_pixel));
+ }
+
+ device.destroy_context(&mut context)?;
+
+ Ok(())
+ }
+}