mirror of
https://github.com/servo/servo.git
synced 2025-08-07 22:45:34 +01:00
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:
parent
d466688526
commit
6dce329acc
15 changed files with 655 additions and 608 deletions
|
@ -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 }
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue