libservo: Expose an OffscreenRenderingContext and use it for servoshell (#35465)

Create a new `RenderingContext` which is used to render to a
`SurfmanRenderingContext`-related offscreen buffer. This allows having a
temporary place to render Servo and then blitting the results to a
subsection of the parent `RenderingContext`.

The goal with this change is to remove the details of how servoshell
renders from the `Compositor` and prepare for the compositor-per-WebView
world.


Co-authred-by: Ngo Iok Ui (Wu Yu Wei) <yuweiwu@pm.me>

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
This commit is contained in:
Martin Robinson 2025-02-17 09:35:05 +01:00 committed by GitHub
parent d466688526
commit 6dce329acc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 655 additions and 608 deletions

View file

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

View file

@ -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<gl::RenderTargetInfo>,
/// 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<gl::RenderTargetInfo>,
/// 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<Rect<f32, CSSPixel>>,
) -> Result<Option<Image>, 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<gleam::gl::GLuint> {
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")

View file

@ -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<dyn Gl>,
framebuffer_ids: Vec<gl::GLuint>,
renderbuffer_ids: Vec<gl::GLuint>,
texture_ids: Vec<gl::GLuint>,
}
impl RenderTargetInfo {
pub fn new(
gl: Rc<dyn Gl>,
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<u8> = Rgba([12, 34, 56, 78]);
assert!(img.pixels().all(|&p| p == expected_pixel));
}
device.destroy_context(&mut context)?;
Ok(())
}
}

View file

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

View file

@ -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<u32> {
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

View file

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

View file

@ -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<u32>) -> Option<RgbaImage>;
/// Resizes the rendering surface to the given size.
fn resize(&self, size: Size2D<i32>);
/// 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<dyn gleam::gl::Gl>;
/// Describes the OpenGL version that is requested when a context is created.
@ -86,6 +100,7 @@ pub trait RenderingContext {
pub struct SurfmanRenderingContext(Rc<RenderingContextData>);
struct RenderingContextData {
gl: Rc<dyn Gl>,
device: RefCell<Device>,
context: RefCell<Context>,
// 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<u32>) -> Option<RgbaImage> {
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<i32>) {
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<dyn gleam::gl::Gl> {
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<NativeFramebuffer> {
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<u32>) -> OffscreenRenderingContext {
OffscreenRenderingContext::new(SurfmanRenderingContext(self.0.clone()), size)
}
}
struct Framebuffer {
gl: Rc<dyn Gl>,
size: Size2D<u32>,
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: Size2D<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,
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<u32>) -> Option<RgbaImage> {
Self::read_framebuffer_to_image(self.gl.clone(), self.framebuffer_id, source_rectangle)
}
fn read_framebuffer_to_image(
gl: Rc<dyn Gl>,
framebuffer_id: u32,
source_rectangle: Rect<u32>,
) -> 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.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<Size2D<u32>>,
back_framebuffer: RefCell<Framebuffer>,
front_framebuffer: RefCell<Option<Framebuffer>>,
}
type RenderToParentCallback = Box<dyn Fn(&glow::Context, Rect<i32>) + Send + Sync>;
impl OffscreenRenderingContext {
fn new(parent_context: SurfmanRenderingContext, size: Size2D<u32>) -> 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<gl::GLuint> {
self.front_framebuffer
.borrow()
.as_ref()
.map(|framebuffer| framebuffer.framebuffer_id)
}
pub fn render_to_parent_callback(&self) -> Option<RenderToParentCallback> {
// 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<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 resize(&self, size: Size2D<i32>) {
// 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<dyn gleam::gl::Gl> {
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<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> {
Some(self.parent_context.connection())
}
fn read_to_image(&self, source_rectangle: Rect<u32>) -> Option<RgbaImage> {
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<u8> = Rgba([12, 34, 56, 78]);
assert!(img.pixels().all(|&p| p == expected_pixel));
}
device.destroy_context(&mut context)?;
Ok(())
}
}