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

2
Cargo.lock generated
View file

@ -8594,6 +8594,8 @@ dependencies = [
"embedder_traits", "embedder_traits",
"euclid", "euclid",
"gleam", "gleam",
"glow",
"image",
"ipc-channel", "ipc-channel",
"libc", "libc",
"log", "log",

View file

@ -36,6 +36,7 @@ script_traits = { workspace = true }
servo_config = { path = "../config" } servo_config = { path = "../config" }
servo_geometry = { path = "../geometry" } servo_geometry = { path = "../geometry" }
style_traits = { workspace = true } style_traits = { workspace = true }
surfman = { workspace = true }
tracing = { workspace = true, optional = true } tracing = { workspace = true, optional = true }
webrender = { workspace = true } webrender = { workspace = true }
webrender_api = { 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 * 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/. */ * 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::hash_set::Iter;
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::env;
@ -24,7 +23,7 @@ use embedder_traits::{
Cursor, InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, Cursor, InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent,
TouchEvent, TouchEventAction, TouchId, TouchEvent, TouchEventAction, TouchId,
}; };
use euclid::{Point2D, Rect, Scale, Transform3D, Vector2D}; use euclid::{Point2D, Rect, Scale, Size2D, Transform3D, Vector2D};
use fnv::{FnvHashMap, FnvHashSet}; use fnv::{FnvHashMap, FnvHashSet};
use image::{DynamicImage, ImageFormat}; use image::{DynamicImage, ImageFormat};
use ipc_channel::ipc::{self, IpcSharedMemory}; use ipc_channel::ipc::{self, IpcSharedMemory};
@ -37,7 +36,7 @@ use script_traits::{
AnimationState, AnimationTickType, ScriptThreadMessage, ScrollState, WindowSizeData, AnimationState, AnimationTickType, ScriptThreadMessage, ScrollState, WindowSizeData,
WindowSizeType, WindowSizeType,
}; };
use servo_geometry::{DeviceIndependentPixel, FramebufferUintLength}; use servo_geometry::DeviceIndependentPixel;
use style_traits::{CSSPixel, PinchZoomFactor}; use style_traits::{CSSPixel, PinchZoomFactor};
use webrender::{CaptureBits, RenderApi, Transaction}; use webrender::{CaptureBits, RenderApi, Transaction};
use webrender_api::units::{ use webrender_api::units::{
@ -57,11 +56,10 @@ use webrender_traits::{
CompositorHitTestResult, CrossProcessCompositorMessage, ImageUpdate, UntrustedNodeAddress, CompositorHitTestResult, CrossProcessCompositorMessage, ImageUpdate, UntrustedNodeAddress,
}; };
use crate::gl::RenderTargetInfo;
use crate::touch::{TouchAction, TouchHandler}; use crate::touch::{TouchAction, TouchHandler};
use crate::webview::{UnknownWebView, WebView, WebViewAlreadyExists, WebViewManager}; use crate::webview::{UnknownWebView, WebView, WebViewAlreadyExists, WebViewManager};
use crate::windowing::{self, EmbedderCoordinates, WebRenderDebugOption, WindowMethods}; use crate::windowing::{self, EmbedderCoordinates, WebRenderDebugOption, WindowMethods};
use crate::{gl, InitialCompositorState}; use crate::InitialCompositorState;
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
enum UnableToComposite { enum UnableToComposite {
@ -191,19 +189,6 @@ pub struct IOCompositor {
/// Current cursor position. /// Current cursor position.
cursor_pos: DevicePoint, 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'). /// True to exit after page load ('-x').
exit_after_load: bool, exit_after_load: bool,
@ -348,10 +333,6 @@ pub enum CompositeTarget {
/// to [`RenderingContext::framebuffer_object`] /// to [`RenderingContext::framebuffer_object`]
ContextFbo, 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. /// Draw to an uncompressed image in shared memory.
SharedMemory, SharedMemory,
@ -399,9 +380,6 @@ impl IOCompositor {
pending_paint_metrics: HashMap::new(), pending_paint_metrics: HashMap::new(),
cursor: Cursor::None, cursor: Cursor::None,
cursor_pos: DevicePoint::new(0.0, 0.0), 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, exit_after_load,
convert_mouse_to_touch, convert_mouse_to_touch,
pending_frames: 0, pending_frames: 0,
@ -1302,13 +1280,6 @@ impl IOCompositor {
let old_coords = self.embedder_coordinates; let old_coords = self.embedder_coordinates;
self.embedder_coordinates = self.window.get_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 { if self.embedder_coordinates.viewport != old_coords.viewport {
let mut transaction = Transaction::new(); let mut transaction = Transaction::new();
let size = self.embedder_coordinates.get_viewport(); let size = self.embedder_coordinates.get_viewport();
@ -1957,13 +1928,6 @@ impl IOCompositor {
target: CompositeTarget, target: CompositeTarget,
page_rect: Option<Rect<f32, CSSPixel>>, page_rect: Option<Rect<f32, CSSPixel>>,
) -> Result<Option<Image>, UnableToComposite> { ) -> 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(); let size = self.embedder_coordinates.framebuffer.to_u32();
if let Err(err) = self.rendering_context.make_current() { if let Err(err) = self.rendering_context.make_current() {
warn!("Failed to make the rendering context current: {:?}", err); warn!("Failed to make the rendering context current: {:?}", err);
@ -1978,12 +1942,6 @@ impl IOCompositor {
target, target,
CompositeTarget::SharedMemory | CompositeTarget::PngFile(_) CompositeTarget::SharedMemory | CompositeTarget::PngFile(_)
) || self.exit_after_load; ) || self.exit_after_load;
let use_offscreen_framebuffer = matches!(
target,
CompositeTarget::SharedMemory |
CompositeTarget::PngFile(_) |
CompositeTarget::OffscreenFbo
);
if wait_for_stable_image { if wait_for_stable_image {
// The current image may be ready to output. However, if there are animations active, // 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.rendering_context.prepare_for_rendering();
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();
}
time_profile!( time_profile!(
ProfilerCategory::Compositing, ProfilerCategory::Compositing,
@ -2056,44 +1998,20 @@ impl IOCompositor {
let rv = match target { let rv = match target {
CompositeTarget::ContextFbo => None, CompositeTarget::ContextFbo => None,
CompositeTarget::OffscreenFbo => { CompositeTarget::SharedMemory => self
self.next_offscreen_framebuffer .rendering_context
.get() .read_to_image(Rect::new(
.expect("Guaranteed by needs_fbo") Point2D::new(x as u32, y as u32),
.unbind(); Size2D::new(width, height),
if self.invalidate_prev_offscreen_framebuffer { ))
// Do not reuse the last render target as the new current render target. .map(|image| Image {
self.prev_offscreen_framebuffer = None; width: image.width(),
self.invalidate_prev_offscreen_framebuffer = false; height: image.height(),
}
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(),
format: PixelFormat::RGBA8, format: PixelFormat::RGBA8,
bytes: ipc::IpcSharedMemory::from_bytes(&img), bytes: ipc::IpcSharedMemory::from_bytes(&image),
id: None, id: None,
cors_status: CorsStatus::Safe, cors_status: CorsStatus::Safe,
}) }),
},
CompositeTarget::PngFile(path) => { CompositeTarget::PngFile(path) => {
time_profile!( time_profile!(
ProfilerCategory::ImageSaving, ProfilerCategory::ImageSaving,
@ -2101,19 +2019,15 @@ impl IOCompositor {
self.time_profiler_chan.clone(), self.time_profiler_chan.clone(),
|| match File::create(&*path) { || match File::create(&*path) {
Ok(mut file) => { Ok(mut file) => {
let render_target_info = self if let Some(image) = self.rendering_context.read_to_image(Rect::new(
.next_offscreen_framebuffer Point2D::new(x as u32, y as u32),
.take() Size2D::new(width, height),
.expect("Guaranteed by needs_fbo"); )) {
let img = render_target_info.read_back_from_gpu( let dynamic_image = DynamicImage::ImageRgba8(image);
x, if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png)
y, {
FramebufferUintLength::new(width), error!("Failed to save {} ({}).", path, e);
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);
} }
}, },
Err(e) => error!("Failed to create {} ({}).", 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( #[cfg_attr(
feature = "tracing", feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") 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 tracing;
mod compositor; mod compositor;
mod gl;
mod touch; mod touch;
pub mod webview; pub mod webview;
pub mod windowing; pub mod windowing;

View file

@ -305,10 +305,6 @@ impl Servo {
} }
debug_assert_eq!(webrender_gl.get_error(), gleam::gl::NO_ERROR,); 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. // Reserving a namespace to create TopLevelBrowsingContextId.
PipelineNamespace::install(PipelineNamespaceId(0)); PipelineNamespace::install(PipelineNamespaceId(0));
@ -346,6 +342,7 @@ impl Servo {
opts.debug.webrender_stats, opts.debug.webrender_stats,
); );
rendering_context.prepare_for_rendering();
let render_notifier = Box::new(RenderNotifier::new(compositor_proxy.clone())); let render_notifier = Box::new(RenderNotifier::new(compositor_proxy.clone()));
let clear_color = servo_config::pref!(shell_background_color_rgba); let clear_color = servo_config::pref!(shell_background_color_rgba);
let clear_color = ColorF::new( let clear_color = ColorF::new(
@ -354,6 +351,7 @@ impl Servo {
clear_color[2] as f32, clear_color[2] as f32,
clear_color[3] as f32, clear_color[3] as f32,
); );
// Use same texture upload method as Gecko with ANGLE: // Use same texture upload method as Gecko with ANGLE:
// https://searchfox.org/mozilla-central/source/gfx/webrender_bindings/src/bindings.rs#1215-1219 // 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") { let upload_method = if webrender_gl.get_string(RENDERER).starts_with("ANGLE") {
@ -702,12 +700,6 @@ impl Servo {
self.compositor.borrow_mut().present(); 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 { pub fn new_webview(&self, url: url::Url) -> WebView {
let webview = WebView::new(&self.constellation_proxy, self.compositor.clone()); let webview = WebView::new(&self.constellation_proxy, self.compositor.clone());
self.webviews self.webviews

View file

@ -10,6 +10,7 @@ rust-version.workspace = true
[lib] [lib]
name = "webrender_traits" name = "webrender_traits"
path = "lib.rs" path = "lib.rs"
test = true
[features] [features]
no-wgl = ["surfman/sm-angle-default"] no-wgl = ["surfman/sm-angle-default"]
@ -18,10 +19,12 @@ no-wgl = ["surfman/sm-angle-default"]
base = { workspace = true } base = { workspace = true }
embedder_traits = { workspace = true } embedder_traits = { workspace = true }
euclid = { workspace = true } euclid = { workspace = true }
image = { workspace = true }
ipc-channel = { workspace = true } ipc-channel = { workspace = true }
log = { workspace = true } log = { workspace = true }
libc = { workspace = true } libc = { workspace = true }
gleam = { workspace = true } gleam = { workspace = true }
glow = { workspace = true }
webrender_api = { workspace = true } webrender_api = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
servo_geometry = { path = "../../geometry" } servo_geometry = { path = "../../geometry" }

View file

@ -4,13 +4,17 @@
#![deny(unsafe_code)] #![deny(unsafe_code)]
use std::cell::RefCell; use std::cell::{Cell, RefCell};
use std::ffi::c_void; use std::ffi::c_void;
use std::num::NonZeroU32;
use std::rc::Rc; use std::rc::Rc;
use euclid::default::Size2D; use euclid::default::{Rect, Size2D};
use gleam::gl; use euclid::Point2D;
use log::{debug, warn}; use gleam::gl::{self, Gl};
use glow::NativeFramebuffer;
use image::RgbaImage;
use log::{debug, trace, warn};
use servo_media::player::context::{GlContext, NativeDisplay}; use servo_media::player::context::{GlContext, NativeDisplay};
use surfman::chains::{PreserveBuffer, SwapChain}; use surfman::chains::{PreserveBuffer, SwapChain};
#[cfg(all(target_os = "linux", not(target_env = "ohos")))] #[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 /// management, and destruction of the rendering context and its associated
/// resources. /// resources.
pub trait RenderingContext { 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. /// Resizes the rendering surface to the given size.
fn resize(&self, size: Size2D<i32>); 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); fn present(&self);
/// Makes the context the current OpenGL context for this thread. /// Makes the context the current OpenGL context for this thread.
/// After calling this function, it is valid to use OpenGL rendering /// After calling this function, it is valid to use OpenGL rendering
/// commands. /// commands.
fn make_current(&self) -> Result<(), Error>; 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. /// Returns the OpenGL or GLES API.
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl>; fn gl_api(&self) -> Rc<dyn gleam::gl::Gl>;
/// Describes the OpenGL version that is requested when a context is created. /// Describes the OpenGL version that is requested when a context is created.
@ -86,6 +100,7 @@ pub trait RenderingContext {
pub struct SurfmanRenderingContext(Rc<RenderingContextData>); pub struct SurfmanRenderingContext(Rc<RenderingContextData>);
struct RenderingContextData { struct RenderingContextData {
gl: Rc<dyn Gl>,
device: RefCell<Device>, device: RefCell<Device>,
context: RefCell<Context>, context: RefCell<Context>,
// We either render to a swap buffer or to a native widget // We either render to a swap buffer or to a native widget
@ -164,6 +179,23 @@ impl RenderingContext for SurfmanRenderingContext {
NativeDisplay::Unknown 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>) { fn resize(&self, size: Size2D<i32>) {
if let Err(err) = self.resize(size) { if let Err(err) = self.resize(size) {
warn!("Failed to resize surface: {:?}", err); warn!("Failed to resize surface: {:?}", err);
@ -177,23 +209,9 @@ impl RenderingContext for SurfmanRenderingContext {
fn make_current(&self) -> Result<(), Error> { fn make_current(&self) -> Result<(), Error> {
self.make_gl_context_current() 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)] #[allow(unsafe_code)]
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl> { fn gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
let context = self.0.context.borrow(); self.0.gl.clone()
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))
},
}
} }
fn gl_version(&self) -> GLVersion { fn gl_version(&self) -> GLVersion {
let device = self.0.device.borrow(); let device = self.0.device.borrow();
@ -270,9 +288,23 @@ impl SurfmanRenderingContext {
} else { } else {
None 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 device = RefCell::new(device);
let context = RefCell::new(context); let context = RefCell::new(context);
let data = RenderingContextData { let data = RenderingContextData {
gl,
device, device,
context, context,
swap_chain, swap_chain,
@ -464,4 +496,367 @@ impl SurfmanRenderingContext {
device.make_context_current(&context)?; device.make_context_current(&context)?;
Ok(()) 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(())
}
} }

View file

@ -11,19 +11,16 @@ use std::time::Instant;
use std::{env, fs}; use std::{env, fs};
use log::{info, trace, warn}; use log::{info, trace, warn};
use raw_window_handle::HasDisplayHandle;
use servo::compositing::windowing::{AnimationState, WindowMethods}; use servo::compositing::windowing::{AnimationState, WindowMethods};
use servo::compositing::CompositeTarget; use servo::compositing::CompositeTarget;
use servo::config::opts::Opts; use servo::config::opts::Opts;
use servo::config::prefs::Preferences; use servo::config::prefs::Preferences;
use servo::servo_config::pref; use servo::servo_config::pref;
use servo::servo_url::ServoUrl; use servo::servo_url::ServoUrl;
use servo::webrender_traits::SurfmanRenderingContext;
use servo::webxr::glwindow::GlWindowDiscovery; use servo::webxr::glwindow::GlWindowDiscovery;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
use servo::webxr::openxr::{AppInfo, OpenXrDiscovery}; use servo::webxr::openxr::{AppInfo, OpenXrDiscovery};
use servo::{EventLoopWaker, Servo}; use servo::{EventLoopWaker, Servo};
use surfman::Connection;
use url::Url; use url::Url;
use winit::application::ApplicationHandler; use winit::application::ApplicationHandler;
use winit::event::WindowEvent; use winit::event::WindowEvent;
@ -55,18 +52,14 @@ pub struct App {
state: AppState, state: AppState,
} }
pub(crate) enum Present {
Deferred,
None,
}
/// Action to be taken by the caller of [`App::handle_events`]. /// Action to be taken by the caller of [`App::handle_events`].
pub(crate) enum PumpResult { pub(crate) enum PumpResult {
/// The caller should shut down Servo and its related context. /// The caller should shut down Servo and its related context.
Shutdown, Shutdown,
Continue { Continue {
update: bool, need_update: bool,
present: Present, new_servo_frame: bool,
need_window_redraw: bool,
}, },
} }
@ -102,52 +95,25 @@ impl App {
/// Initialize Application once event loop start running. /// Initialize Application once event loop start running.
pub fn init(&mut self, event_loop: Option<&ActiveEventLoop>) { pub fn init(&mut self, event_loop: Option<&ActiveEventLoop>) {
// Create rendering context
let initial_window_size = self.servoshell_preferences.initial_window_size;
let rendering_context = if self.servoshell_preferences.headless {
let connection = Connection::new().expect("Failed to create connection");
let adapter = connection
.create_software_adapter()
.expect("Failed to create adapter");
SurfmanRenderingContext::create(
&connection,
&adapter,
Some(initial_window_size.to_untyped().to_i32()),
)
.expect("Failed to create WR surfman")
} else {
let display_handle = event_loop
.unwrap()
.display_handle()
.expect("could not get display handle from window");
let connection = Connection::from_display_handle(display_handle)
.expect("Failed to create connection");
let adapter = connection
.create_adapter()
.expect("Failed to create adapter");
SurfmanRenderingContext::create(&connection, &adapter, None)
.expect("Failed to create WR surfman")
};
let headless = self.servoshell_preferences.headless; let headless = self.servoshell_preferences.headless;
let window = if headless {
headless_window::Window::new(&self.servoshell_preferences)
} else {
Rc::new(headed_window::Window::new(
&self.opts,
&self.servoshell_preferences,
&rendering_context,
event_loop.unwrap(),
))
};
if window.winit_window().is_some() { assert_eq!(headless, event_loop.is_none());
self.minibrowser = Some(Minibrowser::new( let window = match event_loop {
&rendering_context, Some(event_loop) => {
event_loop.unwrap(), let window = headed_window::Window::new(
self.initial_url.clone(), &self.opts,
)); &self.servoshell_preferences,
} event_loop,
);
self.minibrowser = Some(Minibrowser::new(
window.offscreen_rendering_context(),
event_loop,
self.initial_url.clone(),
));
Rc::new(window)
},
None => headless_window::Window::new(&self.servoshell_preferences),
};
self.windows.insert(window.id(), window); self.windows.insert(window.id(), window);
@ -174,12 +140,6 @@ impl App {
// Implements embedder methods, used by libservo and constellation. // Implements embedder methods, used by libservo and constellation.
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery)); let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
let composite_target = if self.minibrowser.is_some() {
CompositeTarget::OffscreenFbo
} else {
CompositeTarget::ContextFbo
};
// TODO: Remove this once dyn upcasting coercion stabilises // TODO: Remove this once dyn upcasting coercion stabilises
// <https://github.com/rust-lang/rust/issues/65991> // <https://github.com/rust-lang/rust/issues/65991>
struct UpcastedWindow(Rc<dyn WindowPortsMethods>); struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
@ -195,11 +155,11 @@ impl App {
let servo = Servo::new( let servo = Servo::new(
self.opts.clone(), self.opts.clone(),
self.preferences.clone(), self.preferences.clone(),
Rc::new(rendering_context), window.rendering_context(),
embedder, embedder,
Rc::new(UpcastedWindow(window.clone())), Rc::new(UpcastedWindow(window.clone())),
self.servoshell_preferences.user_agent.clone(), self.servoshell_preferences.user_agent.clone(),
composite_target, CompositeTarget::ContextFbo,
); );
servo.setup_logging(); servo.setup_logging();
@ -233,33 +193,28 @@ impl App {
state.shutdown(); state.shutdown();
self.state = AppState::ShuttingDown; self.state = AppState::ShuttingDown;
}, },
PumpResult::Continue { update, present } => { PumpResult::Continue {
if update { need_update: update,
if let Some(ref mut minibrowser) = self.minibrowser { new_servo_frame,
if minibrowser.update_webview_data(state) { need_window_redraw,
// Update the minibrowser immediately. While we could update by requesting a } => {
// redraw, doing so would delay the location update by two frames. // A new Servo frame is ready, so swap the buffer on our `RenderingContext`. In headed mode
minibrowser.update( // this won't immediately update the widget surface, because we render to an offscreen
window.winit_window().unwrap(), // `RenderingContext`.
state, if new_servo_frame {
"update_location_in_toolbar", state.servo().present();
);
}
}
} }
match present {
Present::Deferred => { let updated = match (update, &mut self.minibrowser) {
// The compositor has painted to this frame. (true, Some(minibrowser)) => minibrowser.update_webview_data(state),
trace!("PumpResult::Present::Deferred"); _ => false,
// Request a winit redraw event, so we can paint the minibrowser and present. };
// Otherwise, it's in headless mode and we present directly.
if let Some(window) = window.winit_window() { // If in headed mode, request a winit redraw event, so we can paint the minibrowser.
window.request_redraw(); if updated || need_window_redraw || new_servo_frame {
} else { if let Some(window) = window.winit_window() {
state.servo().present(); window.request_redraw();
} }
},
Present::None => {},
} }
}, },
} }
@ -292,15 +247,12 @@ impl App {
state.shutdown(); state.shutdown();
self.state = AppState::ShuttingDown; self.state = AppState::ShuttingDown;
}, },
PumpResult::Continue { present, .. } => { PumpResult::Continue {
match present { new_servo_frame, ..
Present::Deferred => { } => {
// The compositor has painted to this frame. if new_servo_frame {
trace!("PumpResult::Present::Deferred"); // In headless mode, we present directly.
// In headless mode, we present directly. state.servo().present();
state.servo().present();
},
Present::None => {},
} }
}, },
} }
@ -401,8 +353,6 @@ impl ApplicationHandler<WakerEvent> for App {
minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested"); minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested");
minibrowser.paint(window.winit_window().unwrap()); minibrowser.paint(window.winit_window().unwrap());
} }
state.servo().present();
} }
// Handle the event // Handle the event

View file

@ -25,7 +25,7 @@ use servo::{
use tinyfiledialogs::MessageBoxIcon; use tinyfiledialogs::MessageBoxIcon;
use url::Url; use url::Url;
use super::app::{Present, PumpResult}; use super::app::PumpResult;
use super::dialog::Dialog; use super::dialog::Dialog;
use super::gamepad::GamepadSupport; use super::gamepad::GamepadSupport;
use super::keyutils::CMD_OR_CONTROL; use super::keyutils::CMD_OR_CONTROL;
@ -74,7 +74,7 @@ pub struct RunningAppStateInner {
need_update: bool, need_update: bool,
/// Whether or not the application needs to be redrawn. /// Whether or not the application needs to be redrawn.
need_present: bool, new_servo_frame_ready: bool,
} }
impl Drop for RunningAppState { impl Drop for RunningAppState {
@ -101,7 +101,7 @@ impl RunningAppState {
window, window,
gamepad_support: GamepadSupport::maybe_new(), gamepad_support: GamepadSupport::maybe_new(),
need_update: false, need_update: false,
need_present: false, new_servo_frame_ready: false,
}), }),
} }
} }
@ -133,28 +133,21 @@ impl RunningAppState {
self.handle_gamepad_events(); self.handle_gamepad_events();
} }
let should_continue = self.servo().spin_event_loop(); if !self.servo().spin_event_loop() {
// Delegate handlers may have asked us to present or update compositor contents.
let need_present = std::mem::replace(&mut self.inner_mut().need_present, false);
let need_update = std::mem::replace(&mut self.inner_mut().need_update, false);
if !should_continue {
return PumpResult::Shutdown; return PumpResult::Shutdown;
} }
// Currently, egui-file-dialog dialogs need to be constantly presented or animations aren't fluid. // Delegate handlers may have asked us to present or update compositor contents.
let need_present = need_present || self.has_active_dialog(); let new_servo_frame = std::mem::replace(&mut self.inner_mut().new_servo_frame_ready, false);
let need_update = std::mem::replace(&mut self.inner_mut().need_update, false);
let present = if need_present { // Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid.
Present::Deferred let need_window_redraw = new_servo_frame || self.has_active_dialog();
} else {
Present::None
};
PumpResult::Continue { PumpResult::Continue {
update: need_update, need_update,
present, new_servo_frame,
need_window_redraw,
} }
} }
@ -240,7 +233,6 @@ impl RunningAppState {
.or_default() .or_default()
.push(dialog); .push(dialog);
inner_mut.need_update = true; inner_mut.need_update = true;
inner_mut.need_present = true;
} }
fn has_active_dialog(&self) -> bool { fn has_active_dialog(&self) -> bool {
@ -415,22 +407,17 @@ impl WebViewDelegate for RunningAppState {
} }
fn notify_ready_to_show(&self, webview: servo::WebView) { fn notify_ready_to_show(&self, webview: servo::WebView) {
let scale = self.inner().window.hidpi_factor().get(); let rect = self
let toolbar = self.inner().window.toolbar_height().get();
// Adjust for our toolbar height.
// TODO: Adjust for egui window decorations if we end up using those
let mut rect = self
.inner() .inner()
.window .window
.get_coordinates() .get_coordinates()
.get_viewport() .get_viewport()
.to_f32(); .to_f32();
rect.min.y += toolbar * scale;
webview.focus(); webview.focus();
webview.move_resize(rect); webview.move_resize(rect);
webview.raise_to_top(true); webview.raise_to_top(true);
webview.notify_rendering_context_resized();
} }
fn notify_closed(&self, webview: servo::WebView) { fn notify_closed(&self, webview: servo::WebView) {
@ -497,7 +484,7 @@ impl WebViewDelegate for RunningAppState {
} }
fn notify_new_frame_ready(&self, _webview: servo::WebView) { fn notify_new_frame_ready(&self, _webview: servo::WebView) {
self.inner_mut().need_present = true; self.inner_mut().new_servo_frame_ready = true;
} }
fn play_gamepad_haptic_effect( fn play_gamepad_haptic_effect(

View file

@ -12,7 +12,7 @@ use std::time::Duration;
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D}; use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
use keyboard_types::{Modifiers, ShortcutMatcher}; use keyboard_types::{Modifiers, ShortcutMatcher};
use log::{debug, info}; use log::{debug, info, warn};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use servo::compositing::windowing::{ use servo::compositing::windowing::{
AnimationState, EmbedderCoordinates, WebRenderDebugOption, WindowMethods, AnimationState, EmbedderCoordinates, WebRenderDebugOption, WindowMethods,
@ -22,13 +22,14 @@ use servo::servo_config::pref;
use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel}; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel};
use servo::webrender_api::ScrollLocation; use servo::webrender_api::ScrollLocation;
use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext};
use servo::webrender_traits::SurfmanRenderingContext; use servo::webrender_traits::SurfmanRenderingContext;
use servo::{ use servo::{
Cursor, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton, Cursor, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
MouseButtonAction, MouseButtonEvent, MouseMoveEvent, Theme, TouchEvent, TouchEventAction, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, Theme, TouchEvent, TouchEventAction,
TouchId, WebView, WheelDelta, WheelEvent, WheelMode, TouchId, WebView, WheelDelta, WheelEvent, WheelMode,
}; };
use surfman::{Context, Device, SurfaceType}; use surfman::{Connection, Context, Device, SurfaceType};
use url::Url; use url::Url;
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize}; use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
use winit::event::{ use winit::event::{
@ -52,9 +53,9 @@ pub struct Window {
inner_size: Cell<PhysicalSize<u32>>, inner_size: Cell<PhysicalSize<u32>>,
toolbar_height: Cell<Length<f32, DeviceIndependentPixel>>, toolbar_height: Cell<Length<f32, DeviceIndependentPixel>>,
mouse_down_button: Cell<Option<MouseButton>>, mouse_down_button: Cell<Option<MouseButton>>,
mouse_down_point: Cell<Point2D<i32, DevicePixel>>, webview_relative_mouse_down_point: Cell<Point2D<f32, DevicePixel>>,
monitor: winit::monitor::MonitorHandle, monitor: winit::monitor::MonitorHandle,
mouse_pos: Cell<Point2D<i32, DevicePixel>>, webview_relative_mouse_point: Cell<Point2D<f32, DevicePixel>>,
last_pressed: Cell<Option<(KeyboardEvent, Option<LogicalKey>)>>, last_pressed: Cell<Option<(KeyboardEvent, Option<LogicalKey>)>>,
/// A map of winit's key codes to key values that are interpreted from /// A map of winit's key codes to key values that are interpreted from
/// winit's ReceivedChar events. /// winit's ReceivedChar events.
@ -64,13 +65,21 @@ pub struct Window {
device_pixel_ratio_override: Option<f32>, device_pixel_ratio_override: Option<f32>,
xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>, xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>,
modifiers_state: Cell<ModifiersState>, modifiers_state: Cell<ModifiersState>,
/// The RenderingContext that renders directly onto the Window. This is used as
/// the target of egui rendering and also where Servo rendering results are finally
/// blitted.
window_rendering_context: SurfmanRenderingContext,
/// The `RenderingContext` of Servo itself. This is used to render Servo results
/// temporarily until they can be blitted into the egui scene.
rendering_context: Rc<OffscreenRenderingContext>,
} }
impl Window { impl Window {
pub fn new( pub fn new(
opts: &Opts, opts: &Opts,
servoshell_preferences: &ServoShellPreferences, servoshell_preferences: &ServoShellPreferences,
rendering_context: &SurfmanRenderingContext,
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
) -> Window { ) -> Window {
// If there's no chrome, start off with the window invisible. It will be set to visible in // If there's no chrome, start off with the window invisible. It will be set to visible in
@ -111,37 +120,48 @@ impl Window {
let screen_scale: Scale<f64, DeviceIndependentPixel, DevicePixel> = let screen_scale: Scale<f64, DeviceIndependentPixel, DevicePixel> =
Scale::new(screen_scale); Scale::new(screen_scale);
let screen_size = (winit_size_to_euclid_size(screen_size).to_f64() / screen_scale).to_u32(); let screen_size = (winit_size_to_euclid_size(screen_size).to_f64() / screen_scale).to_u32();
let inner_size = winit_window.inner_size();
// Initialize surfman let display_handle = event_loop
.display_handle()
.expect("could not get display handle from window");
let connection =
Connection::from_display_handle(display_handle).expect("Failed to create connection");
let adapter = connection
.create_adapter()
.expect("Failed to create adapter");
let window_handle = winit_window let window_handle = winit_window
.window_handle() .window_handle()
.expect("could not get window handle from window"); .expect("could not get window handle from window");
let native_widget = connection
let inner_size = winit_window.inner_size();
let native_widget = rendering_context
.connection()
.create_native_widget_from_window_handle( .create_native_widget_from_window_handle(
window_handle, window_handle,
winit_size_to_euclid_size(inner_size).to_i32().to_untyped(), winit_size_to_euclid_size(inner_size).to_i32().to_untyped(),
) )
.expect("Failed to create native widget"); .expect("Failed to create native widget");
let surface_type = SurfaceType::Widget { native_widget }; let window_rendering_context = SurfmanRenderingContext::create(&connection, &adapter, None)
let surface = rendering_context .expect("Failed to create window RenderingContext");
.create_surface(surface_type) let surface = window_rendering_context
.create_surface(SurfaceType::Widget { native_widget })
.expect("Failed to create surface"); .expect("Failed to create surface");
rendering_context window_rendering_context
.bind_surface(surface) .bind_surface(surface)
.expect("Failed to bind surface"); .expect("Failed to bind surface");
// Make sure the gl context is made current. // Make sure the gl context is made current.
rendering_context.make_gl_context_current().unwrap(); window_rendering_context.make_gl_context_current().unwrap();
let rendering_context_size = Size2D::new(inner_size.width, inner_size.height);
let rendering_context =
Rc::new(window_rendering_context.offscreen_context(rendering_context_size));
debug!("Created window {:?}", winit_window.id()); debug!("Created window {:?}", winit_window.id());
Window { Window {
winit_window, winit_window,
mouse_down_button: Cell::new(None), mouse_down_button: Cell::new(None),
mouse_down_point: Cell::new(Point2D::zero()), webview_relative_mouse_down_point: Cell::new(Point2D::zero()),
mouse_pos: Cell::new(Point2D::zero()), webview_relative_mouse_point: Cell::new(Point2D::zero()),
last_pressed: Cell::new(None), last_pressed: Cell::new(None),
keys_down: RefCell::new(HashMap::new()), keys_down: RefCell::new(HashMap::new()),
animation_state: Cell::new(AnimationState::Idle), animation_state: Cell::new(AnimationState::Idle),
@ -153,6 +173,8 @@ impl Window {
xr_window_poses: RefCell::new(vec![]), xr_window_poses: RefCell::new(vec![]),
modifiers_state: Cell::new(ModifiersState::empty()), modifiers_state: Cell::new(ModifiersState::empty()),
toolbar_height: Cell::new(Default::default()), toolbar_height: Cell::new(Default::default()),
window_rendering_context,
rendering_context,
} }
} }
@ -244,13 +266,7 @@ impl Window {
} }
/// Helper function to handle a click /// Helper function to handle a click
fn handle_mouse( fn handle_mouse(&self, webview: &WebView, button: MouseButton, action: ElementState) {
&self,
webview: &WebView,
button: MouseButton,
action: ElementState,
coords: Point2D<i32, DevicePixel>,
) {
let max_pixel_dist = 10.0 * self.hidpi_factor().get(); let max_pixel_dist = 10.0 * self.hidpi_factor().get();
let mouse_button = match &button { let mouse_button = match &button {
MouseButton::Left => ServoMouseButton::Left, MouseButton::Left => ServoMouseButton::Left,
@ -261,9 +277,10 @@ impl Window {
MouseButton::Other(value) => ServoMouseButton::Other(*value), MouseButton::Other(value) => ServoMouseButton::Other(*value),
}; };
let point = self.webview_relative_mouse_point.get();
let action = match action { let action = match action {
ElementState::Pressed => { ElementState::Pressed => {
self.mouse_down_point.set(coords); self.webview_relative_mouse_down_point.set(point);
self.mouse_down_button.set(Some(button)); self.mouse_down_button.set(Some(button));
MouseButtonAction::Down MouseButtonAction::Down
}, },
@ -273,7 +290,7 @@ impl Window {
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent { webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
action, action,
button: mouse_button, button: mouse_button,
point: coords.to_f32(), point,
})); }));
// Also send a 'click' event if this is release and the press was recorded // Also send a 'click' event if this is release and the press was recorded
@ -285,14 +302,13 @@ impl Window {
} }
if let Some(mouse_down_button) = self.mouse_down_button.get() { if let Some(mouse_down_button) = self.mouse_down_button.get() {
let pixel_dist = self.mouse_down_point.get() - coords; let pixel_dist = self.webview_relative_mouse_down_point.get() - point;
let pixel_dist = let pixel_dist = (pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y).sqrt();
((pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y) as f32).sqrt();
if mouse_down_button == button && pixel_dist < max_pixel_dist { if mouse_down_button == button && pixel_dist < max_pixel_dist {
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent { webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
action: MouseButtonAction::Click, action: MouseButtonAction::Click,
button: mouse_button, button: mouse_button,
point: coords.to_f32(), point,
})); }));
} }
} }
@ -418,6 +434,10 @@ impl Window {
.otherwise(|| handled = false); .otherwise(|| handled = false);
handled handled
} }
pub(crate) fn offscreen_rendering_context(&self) -> Rc<OffscreenRenderingContext> {
self.rendering_context.clone()
}
} }
impl WindowPortsMethods for Window { impl WindowPortsMethods for Window {
@ -545,15 +565,15 @@ impl WindowPortsMethods for Window {
WindowEvent::ModifiersChanged(modifiers) => self.modifiers_state.set(modifiers.state()), WindowEvent::ModifiersChanged(modifiers) => self.modifiers_state.set(modifiers.state()),
WindowEvent::MouseInput { state, button, .. } => { WindowEvent::MouseInput { state, button, .. } => {
if button == MouseButton::Left || button == MouseButton::Right { if button == MouseButton::Left || button == MouseButton::Right {
self.handle_mouse(&webview, button, state, self.mouse_pos.get()); self.handle_mouse(&webview, button, state);
} }
}, },
WindowEvent::CursorMoved { position, .. } => { WindowEvent::CursorMoved { position, .. } => {
let position = winit_position_to_euclid_point(position); let mut point = winit_position_to_euclid_point(position).to_f32();
self.mouse_pos.set(position.to_i32()); point.y -= (self.toolbar_height() * self.hidpi_factor()).0;
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent {
point: position.to_f32(), self.webview_relative_mouse_point.set(point);
})); webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent { point }));
}, },
WindowEvent::MouseWheel { delta, phase, .. } => { WindowEvent::MouseWheel { delta, phase, .. } => {
let (mut dx, mut dy, mode) = match delta { let (mut dx, mut dy, mode) = match delta {
@ -574,8 +594,8 @@ impl WindowPortsMethods for Window {
z: 0.0, z: 0.0,
mode, mode,
}; };
let pos = self.mouse_pos.get(); let pos = self.webview_relative_mouse_point.get();
let point = Point2D::new(pos.x as f32, pos.y as f32); let point = Point2D::new(pos.x, pos.y);
// Scroll events snap to the major axis of movement, with vertical // Scroll events snap to the major axis of movement, with vertical
// preferred over horizontal. // preferred over horizontal.
@ -590,7 +610,11 @@ impl WindowPortsMethods for Window {
// Send events // Send events
webview.notify_input_event(InputEvent::Wheel(WheelEvent { delta, point })); webview.notify_input_event(InputEvent::Wheel(WheelEvent { delta, point }));
webview.notify_scroll_event(scroll_location, self.mouse_pos.get(), phase); webview.notify_scroll_event(
scroll_location,
self.webview_relative_mouse_point.get().to_i32(),
phase,
);
}, },
WindowEvent::Touch(touch) => { WindowEvent::Touch(touch) => {
webview.notify_input_event(InputEvent::Touch(TouchEvent { webview.notify_input_event(InputEvent::Touch(TouchEvent {
@ -607,6 +631,14 @@ impl WindowPortsMethods for Window {
}, },
WindowEvent::Resized(new_size) => { WindowEvent::Resized(new_size) => {
if self.inner_size.get() != new_size { if self.inner_size.get() != new_size {
let rendering_context_size = Size2D::new(new_size.width, new_size.height);
if let Err(error) = self
.window_rendering_context
.resize(rendering_context_size.to_i32())
{
warn!("Could not resize window RenderingContext: {error:?}");
}
self.inner_size.set(new_size); self.inner_size.set(new_size);
webview.notify_rendering_context_resized(); webview.notify_rendering_context_resized();
} }
@ -658,6 +690,10 @@ impl WindowPortsMethods for Window {
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>) { fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>) {
self.toolbar_height.set(height); self.toolbar_height.set(height);
} }
fn rendering_context(&self) -> Rc<dyn RenderingContext> {
self.rendering_context.clone()
}
} }
impl WindowMethods for Window { impl WindowMethods for Window {
@ -671,7 +707,9 @@ impl WindowMethods for Window {
let window_rect = (window_rect.to_f64() / window_scale).to_i32(); let window_rect = (window_rect.to_f64() / window_scale).to_i32();
let viewport_origin = DeviceIntPoint::zero(); // bottom left let viewport_origin = DeviceIntPoint::zero(); // bottom left
let viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32(); let mut viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32();
viewport_size.height -= (self.toolbar_height() * self.hidpi_factor()).0;
let viewport = DeviceIntRect::from_origin_and_size(viewport_origin, viewport_size.to_i32()); let viewport = DeviceIntRect::from_origin_and_size(viewport_origin, viewport_size.to_i32());
let screen_size = self.screen_size.to_i32(); let screen_size = self.screen_size.to_i32();

View file

@ -12,6 +12,9 @@ use euclid::{Box2D, Length, Point2D, Scale, Size2D};
use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods}; use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods};
use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntSize, DevicePixel}; use servo::webrender_api::units::{DeviceIntSize, DevicePixel};
use servo::webrender_traits::rendering_context::RenderingContext;
use servo::webrender_traits::SurfmanRenderingContext;
use surfman::Connection;
use super::app_state::RunningAppState; use super::app_state::RunningAppState;
use crate::desktop::window_trait::WindowPortsMethods; use crate::desktop::window_trait::WindowPortsMethods;
@ -24,19 +27,31 @@ pub struct Window {
inner_size: Cell<DeviceIntSize>, inner_size: Cell<DeviceIntSize>,
screen_size: Size2D<i32, DeviceIndependentPixel>, screen_size: Size2D<i32, DeviceIndependentPixel>,
window_rect: Box2D<i32, DeviceIndependentPixel>, window_rect: Box2D<i32, DeviceIndependentPixel>,
rendering_context: SurfmanRenderingContext,
} }
impl Window { impl Window {
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc<dyn WindowPortsMethods> { pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc<dyn WindowPortsMethods> {
let size = servoshell_preferences.initial_window_size;
let connection = Connection::new().expect("Failed to create connection");
let adapter = connection
.create_software_adapter()
.expect("Failed to create adapter");
let rendering_context = SurfmanRenderingContext::create(
&connection,
&adapter,
Some(size.to_untyped().to_i32()),
)
.expect("Failed to create WR surfman");
let device_pixel_ratio_override = servoshell_preferences.device_pixel_ratio_override; let device_pixel_ratio_override = servoshell_preferences.device_pixel_ratio_override;
let device_pixel_ratio_override: Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> = let device_pixel_ratio_override: Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> =
device_pixel_ratio_override.map(Scale::new); device_pixel_ratio_override.map(Scale::new);
let hidpi_factor = device_pixel_ratio_override.unwrap_or_else(Scale::identity); let hidpi_factor = device_pixel_ratio_override.unwrap_or_else(Scale::identity);
let size = servoshell_preferences.initial_window_size.to_i32();
let inner_size = Cell::new((size.to_f32() * hidpi_factor).to_i32()); let inner_size = Cell::new((size.to_f32() * hidpi_factor).to_i32());
let window_rect = Box2D::from_origin_and_size(Point2D::zero(), size); let window_rect = Box2D::from_origin_and_size(Point2D::zero(), size.to_i32());
let screen_size = servoshell_preferences.screen_size_override.map_or_else( let screen_size = servoshell_preferences.screen_size_override.map_or_else(
|| window_rect.size(), || window_rect.size(),
@ -50,6 +65,7 @@ impl Window {
inner_size, inner_size,
screen_size, screen_size,
window_rect, window_rect,
rendering_context,
}; };
Rc::new(window) Rc::new(window)
@ -132,6 +148,12 @@ impl WindowPortsMethods for Window {
fn set_toolbar_height(&self, _height: Length<f32, DeviceIndependentPixel>) { fn set_toolbar_height(&self, _height: Length<f32, DeviceIndependentPixel>) {
unimplemented!("headless Window only") unimplemented!("headless Window only")
} }
fn rendering_context(&self) -> Rc<dyn RenderingContext> {
// `SurfmanRenderingContext` uses shared ownership internally so cloning it here does
// not create a new one really.
Rc::new(self.rendering_context.clone())
}
} }
impl WindowMethods for Window { impl WindowMethods for Window {

View file

@ -3,27 +3,25 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::{Cell, RefCell}; use std::cell::{Cell, RefCell};
use std::num::NonZeroU32; use std::rc::Rc;
use std::sync::Arc; use std::sync::Arc;
use std::time::Instant; use std::time::Instant;
use egui::text::{CCursor, CCursorRange}; use egui::text::{CCursor, CCursorRange};
use egui::text_edit::TextEditState; use egui::text_edit::TextEditState;
use egui::{ use egui::{
pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, Pos2, SelectableLabel, pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, SelectableLabel,
TopBottomPanel, Vec2, TopBottomPanel, Vec2,
}; };
use egui_glow::CallbackFn; use egui_glow::CallbackFn;
use egui_winit::EventResponse; use egui_winit::EventResponse;
use euclid::{Box2D, Length, Point2D, Scale, Size2D}; use euclid::{Box2D, Length, Point2D, Rect, Scale, Size2D};
use gleam::gl;
use glow::NativeFramebuffer;
use log::{trace, warn}; use log::{trace, warn};
use servo::base::id::WebViewId; use servo::base::id::WebViewId;
use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_geometry::DeviceIndependentPixel;
use servo::servo_url::ServoUrl; use servo::servo_url::ServoUrl;
use servo::webrender_api::units::DevicePixel; use servo::webrender_api::units::DevicePixel;
use servo::webrender_traits::SurfmanRenderingContext; use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext};
use servo::{LoadStatus, WebView}; use servo::{LoadStatus, WebView};
use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::event_loop::ActiveEventLoop; use winit::event_loop::ActiveEventLoop;
@ -34,14 +32,11 @@ use super::egui_glue::EguiGlow;
use super::geometry::winit_position_to_euclid_point; use super::geometry::winit_position_to_euclid_point;
pub struct Minibrowser { pub struct Minibrowser {
rendering_context: Rc<OffscreenRenderingContext>,
pub context: EguiGlow, pub context: EguiGlow,
pub event_queue: RefCell<Vec<MinibrowserEvent>>, pub event_queue: RefCell<Vec<MinibrowserEvent>>,
pub toolbar_height: Length<f32, DeviceIndependentPixel>, pub toolbar_height: Length<f32, DeviceIndependentPixel>,
/// The framebuffer object name for the widget surface we should draw to, or None if our widget
/// surface does not use a framebuffer object.
widget_surface_fbo: Option<NativeFramebuffer>,
last_update: Instant, last_update: Instant,
last_mouse_position: Option<Point2D<f32, DeviceIndependentPixel>>, last_mouse_position: Option<Point2D<f32, DeviceIndependentPixel>>,
location: RefCell<String>, location: RefCell<String>,
@ -81,12 +76,14 @@ impl Drop for Minibrowser {
impl Minibrowser { impl Minibrowser {
pub fn new( pub fn new(
rendering_context: &SurfmanRenderingContext, rendering_context: Rc<OffscreenRenderingContext>,
event_loop: &ActiveEventLoop, event_loop: &ActiveEventLoop,
initial_url: ServoUrl, initial_url: ServoUrl,
) -> Self { ) -> Self {
let gl = unsafe { let gl = unsafe {
glow::Context::from_loader_function(|s| rendering_context.get_proc_address(s)) glow::Context::from_loader_function(|s| {
rendering_context.parent_context().get_proc_address(s)
})
}; };
// Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs // Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs
@ -99,17 +96,11 @@ impl Minibrowser {
.egui_ctx .egui_ctx
.options_mut(|options| options.zoom_with_keyboard = false); .options_mut(|options| options.zoom_with_keyboard = false);
let widget_surface_fbo = match rendering_context.context_surface_info() {
Ok(Some(info)) => info.framebuffer_object,
Ok(None) => panic!("Failed to get widget surface info from surfman!"),
Err(error) => panic!("Failed to get widget surface info from surfman! {error:?}"),
};
Self { Self {
rendering_context,
context, context,
event_queue: RefCell::new(vec![]), event_queue: RefCell::new(vec![]),
toolbar_height: Default::default(), toolbar_height: Default::default(),
widget_surface_fbo,
last_update: Instant::now(), last_update: Instant::now(),
last_mouse_position: None, last_mouse_position: None,
location: RefCell::new(initial_url.to_string()), location: RefCell::new(initial_url.to_string()),
@ -268,17 +259,16 @@ impl Minibrowser {
reason reason
); );
let Self { let Self {
rendering_context,
context, context,
event_queue, event_queue,
toolbar_height, toolbar_height,
widget_surface_fbo,
last_update, last_update,
location, location,
location_dirty, location_dirty,
.. ..
} = self; } = self;
let widget_fbo = *widget_surface_fbo;
let servo_framebuffer_id = state.servo().offscreen_framebuffer_id();
let _duration = context.run(window, |ctx| { let _duration = context.run(window, |ctx| {
// TODO: While in fullscreen add some way to mitigate the increased phishing risk // TODO: While in fullscreen add some way to mitigate the increased phishing risk
// when not displaying the URL bar: https://github.com/servo/servo/issues/32443 // when not displaying the URL bar: https://github.com/servo/servo/issues/32443
@ -387,26 +377,23 @@ impl Minibrowser {
return; return;
}; };
CentralPanel::default().frame(Frame::NONE).show(ctx, |ui| { CentralPanel::default().frame(Frame::NONE).show(ctx, |ui| {
let Pos2 { x, y } = ui.cursor().min; // If the top parts of the GUI changed size, then update the size of the WebView and also
let Vec2 { // the size of its RenderingContext.
x: width, let available_size = ui.available_size();
y: height, let rect = Box2D::from_origin_and_size(
} = ui.available_size(); Point2D::origin(),
let rect = Size2D::new(available_size.x, available_size.y),
Box2D::from_origin_and_size(Point2D::new(x, y), Size2D::new(width, height)) * ) * scale;
scale;
if rect != webview.rect() { if rect != webview.rect() {
webview.move_resize(rect); webview.move_resize(rect);
rendering_context.resize(rect.size().to_i32().to_untyped());
} }
let min = ui.cursor().min; let min = ui.cursor().min;
let size = ui.available_size(); let size = ui.available_size();
let rect = egui::Rect::from_min_size(min, size); let rect = egui::Rect::from_min_size(min, size);
ui.allocate_space(size); ui.allocate_space(size);
let Some(servo_fbo) = servo_framebuffer_id else {
return;
};
if let Some(status_text) = &self.status_text { if let Some(status_text) = &self.status_text {
egui::containers::popup::show_tooltip_at( egui::containers::popup::show_tooltip_at(
ctx, ctx,
@ -417,45 +404,19 @@ impl Minibrowser {
); );
} }
ui.painter().add(PaintCallback { if let Some(render_to_parent) = rendering_context.render_to_parent_callback() {
rect, ui.painter().add(PaintCallback {
callback: Arc::new(CallbackFn::new(move |info, painter| { rect,
use glow::HasContext as _; callback: Arc::new(CallbackFn::new(move |info, painter| {
let clip = info.viewport_in_pixels(); let clip = info.viewport_in_pixels();
let x = clip.left_px as gl::GLint; let rect_in_parent = Rect::new(
let y = clip.from_bottom_px as gl::GLint; Point2D::new(clip.left_px, clip.from_bottom_px),
let width = clip.width_px as gl::GLsizei; Size2D::new(clip.width_px, clip.height_px),
let height = clip.height_px as gl::GLsizei;
unsafe {
painter.gl().clear_color(0.0, 0.0, 0.0, 0.0);
painter.gl().scissor(x, y, width, height);
painter.gl().enable(gl::SCISSOR_TEST);
painter.gl().clear(gl::COLOR_BUFFER_BIT);
painter.gl().disable(gl::SCISSOR_TEST);
let servo_fbo = NonZeroU32::new(servo_fbo).map(NativeFramebuffer);
painter
.gl()
.bind_framebuffer(gl::READ_FRAMEBUFFER, servo_fbo);
painter
.gl()
.bind_framebuffer(gl::DRAW_FRAMEBUFFER, widget_fbo);
painter.gl().blit_framebuffer(
x,
y,
x + width,
y + height,
x,
y,
x + width,
y + height,
gl::COLOR_BUFFER_BIT,
gl::NEAREST,
); );
painter.gl().bind_framebuffer(gl::FRAMEBUFFER, widget_fbo); render_to_parent(painter.gl(), rect_in_parent)
} })),
})), });
}); }
}); });
*last_update = now; *last_update = now;
@ -464,14 +425,11 @@ impl Minibrowser {
/// Paint the minibrowser, as of the last update. /// Paint the minibrowser, as of the last update.
pub fn paint(&mut self, window: &Window) { pub fn paint(&mut self, window: &Window) {
unsafe { self.rendering_context
use glow::HasContext as _; .parent_context()
self.context .prepare_for_rendering();
.painter
.gl()
.bind_framebuffer(gl::FRAMEBUFFER, self.widget_surface_fbo);
}
self.context.paint(window); self.context.paint(window);
let _ = self.rendering_context.parent_context().present();
} }
/// Updates the location field from the given [WebViewManager], unless the user has started /// Updates the location field from the given [WebViewManager], unless the user has started

View file

@ -11,6 +11,7 @@ use euclid::{Length, Scale};
use servo::compositing::windowing::WindowMethods; use servo::compositing::windowing::WindowMethods;
use servo::servo_geometry::DeviceIndependentPixel; use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel}; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
use servo::webrender_traits::rendering_context::RenderingContext;
use servo::{Cursor, WebView}; use servo::{Cursor, WebView};
use super::app_state::RunningAppState; use super::app_state::RunningAppState;
@ -46,4 +47,5 @@ pub trait WindowPortsMethods: WindowMethods {
fn winit_window(&self) -> Option<&winit::window::Window>; fn winit_window(&self) -> Option<&winit::window::Window>;
fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel>; fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel>;
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>); fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>);
fn rendering_context(&self) -> Rc<dyn RenderingContext>;
} }

View file

@ -161,7 +161,6 @@ class MachCommands(CommandBase):
self_contained_tests = [ self_contained_tests = [
"background_hang_monitor", "background_hang_monitor",
"base", "base",
"compositing",
"constellation", "constellation",
"fonts", "fonts",
"hyper_serde", "hyper_serde",
@ -175,6 +174,7 @@ class MachCommands(CommandBase):
"servo_config", "servo_config",
"servoshell", "servoshell",
"style_config", "style_config",
"webrender_traits",
] ]
if not packages: if not packages:
packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store']) packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store'])