diff options
author | Martin Robinson <mrobinson@igalia.com> | 2025-04-06 19:34:18 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-04-06 17:34:18 +0000 |
commit | 0caa271176d4670eb06bedd05cdffb24df08fc4f (patch) | |
tree | 9a0c3431dcf7ac31cfbdc1f801c9f4ef3dda6ae7 /components/shared/compositing/rendering_context.rs | |
parent | e74a042efdf01ab2ff32e82e203bd1d954b599bd (diff) | |
download | servo-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.rs | 887 |
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(()) + } +} |