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",
"euclid",
"gleam",
"glow",
"image",
"ipc-channel",
"libc",
"log",

View file

@ -36,6 +36,7 @@ script_traits = { workspace = true }
servo_config = { path = "../config" }
servo_geometry = { path = "../geometry" }
style_traits = { workspace = true }
surfman = { workspace = true }
tracing = { workspace = true, optional = true }
webrender = { workspace = true }
webrender_api = { workspace = true }

View file

@ -2,7 +2,6 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::OnceCell;
use std::collections::hash_set::Iter;
use std::collections::HashMap;
use std::env;
@ -24,7 +23,7 @@ use embedder_traits::{
Cursor, InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent,
TouchEvent, TouchEventAction, TouchId,
};
use euclid::{Point2D, Rect, Scale, Transform3D, Vector2D};
use euclid::{Point2D, Rect, Scale, Size2D, Transform3D, Vector2D};
use fnv::{FnvHashMap, FnvHashSet};
use image::{DynamicImage, ImageFormat};
use ipc_channel::ipc::{self, IpcSharedMemory};
@ -37,7 +36,7 @@ use script_traits::{
AnimationState, AnimationTickType, ScriptThreadMessage, ScrollState, WindowSizeData,
WindowSizeType,
};
use servo_geometry::{DeviceIndependentPixel, FramebufferUintLength};
use servo_geometry::DeviceIndependentPixel;
use style_traits::{CSSPixel, PinchZoomFactor};
use webrender::{CaptureBits, RenderApi, Transaction};
use webrender_api::units::{
@ -57,11 +56,10 @@ use webrender_traits::{
CompositorHitTestResult, CrossProcessCompositorMessage, ImageUpdate, UntrustedNodeAddress,
};
use crate::gl::RenderTargetInfo;
use crate::touch::{TouchAction, TouchHandler};
use crate::webview::{UnknownWebView, WebView, WebViewAlreadyExists, WebViewManager};
use crate::windowing::{self, EmbedderCoordinates, WebRenderDebugOption, WindowMethods};
use crate::{gl, InitialCompositorState};
use crate::InitialCompositorState;
#[derive(Debug, PartialEq)]
enum UnableToComposite {
@ -191,19 +189,6 @@ pub struct IOCompositor {
/// Current cursor position.
cursor_pos: DevicePoint,
/// Offscreen framebuffer object to render our next frame to.
/// We use this and `prev_offscreen_framebuffer` for double buffering when compositing to
/// [`CompositeTarget::OffscreenFbo`].
next_offscreen_framebuffer: OnceCell<gl::RenderTargetInfo>,
/// Offscreen framebuffer object for our most-recently-completed frame.
/// We use this and `next_offscreen_framebuffer` for double buffering when compositing to
/// [`CompositeTarget::OffscreenFbo`].
prev_offscreen_framebuffer: Option<gl::RenderTargetInfo>,
/// Whether to invalidate `prev_offscreen_framebuffer` at the end of the next frame.
invalidate_prev_offscreen_framebuffer: bool,
/// True to exit after page load ('-x').
exit_after_load: bool,
@ -348,10 +333,6 @@ pub enum CompositeTarget {
/// to [`RenderingContext::framebuffer_object`]
ContextFbo,
/// Draw to an offscreen OpenGL framebuffer object, which can be retrieved once complete at
/// [`IOCompositor::offscreen_framebuffer_id`].
OffscreenFbo,
/// Draw to an uncompressed image in shared memory.
SharedMemory,
@ -399,9 +380,6 @@ impl IOCompositor {
pending_paint_metrics: HashMap::new(),
cursor: Cursor::None,
cursor_pos: DevicePoint::new(0.0, 0.0),
next_offscreen_framebuffer: OnceCell::new(),
prev_offscreen_framebuffer: None,
invalidate_prev_offscreen_framebuffer: false,
exit_after_load,
convert_mouse_to_touch,
pending_frames: 0,
@ -1302,13 +1280,6 @@ impl IOCompositor {
let old_coords = self.embedder_coordinates;
self.embedder_coordinates = self.window.get_coordinates();
// If the framebuffer size has changed, invalidate the current framebuffer object, and mark
// the last framebuffer object as needing to be invalidated at the end of the next frame.
if self.embedder_coordinates.framebuffer != old_coords.framebuffer {
self.next_offscreen_framebuffer = OnceCell::new();
self.invalidate_prev_offscreen_framebuffer = true;
}
if self.embedder_coordinates.viewport != old_coords.viewport {
let mut transaction = Transaction::new();
let size = self.embedder_coordinates.get_viewport();
@ -1957,13 +1928,6 @@ impl IOCompositor {
target: CompositeTarget,
page_rect: Option<Rect<f32, CSSPixel>>,
) -> Result<Option<Image>, UnableToComposite> {
if !self.webviews_waiting_on_present.is_empty() {
debug!("tried to composite while waiting on present");
return Err(UnableToComposite::NotReadyToPaintImage(
NotReadyToPaint::WaitingOnConstellation,
));
}
let size = self.embedder_coordinates.framebuffer.to_u32();
if let Err(err) = self.rendering_context.make_current() {
warn!("Failed to make the rendering context current: {:?}", err);
@ -1978,12 +1942,6 @@ impl IOCompositor {
target,
CompositeTarget::SharedMemory | CompositeTarget::PngFile(_)
) || self.exit_after_load;
let use_offscreen_framebuffer = matches!(
target,
CompositeTarget::SharedMemory |
CompositeTarget::PngFile(_) |
CompositeTarget::OffscreenFbo
);
if wait_for_stable_image {
// The current image may be ready to output. However, if there are animations active,
@ -2000,23 +1958,7 @@ impl IOCompositor {
}
}
if use_offscreen_framebuffer {
self.next_offscreen_framebuffer
.get_or_init(|| {
RenderTargetInfo::new(
self.webrender_gl.clone(),
FramebufferUintLength::new(size.width),
FramebufferUintLength::new(size.height),
)
})
.bind();
} else {
// Bind the webrender framebuffer
let framebuffer_object = self.rendering_context.framebuffer_object();
self.webrender_gl
.bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object);
self.assert_gl_framebuffer_complete();
}
self.rendering_context.prepare_for_rendering();
time_profile!(
ProfilerCategory::Compositing,
@ -2056,44 +1998,20 @@ impl IOCompositor {
let rv = match target {
CompositeTarget::ContextFbo => None,
CompositeTarget::OffscreenFbo => {
self.next_offscreen_framebuffer
.get()
.expect("Guaranteed by needs_fbo")
.unbind();
if self.invalidate_prev_offscreen_framebuffer {
// Do not reuse the last render target as the new current render target.
self.prev_offscreen_framebuffer = None;
self.invalidate_prev_offscreen_framebuffer = false;
}
let old_prev = self.prev_offscreen_framebuffer.take();
self.prev_offscreen_framebuffer = self.next_offscreen_framebuffer.take();
if let Some(old_prev) = old_prev {
let result = self.next_offscreen_framebuffer.set(old_prev);
debug_assert!(result.is_ok(), "Guaranteed by take");
}
None
},
CompositeTarget::SharedMemory => {
let render_target_info = self
.next_offscreen_framebuffer
.take()
.expect("Guaranteed by needs_fbo");
let img = render_target_info.read_back_from_gpu(
x,
y,
FramebufferUintLength::new(width),
FramebufferUintLength::new(height),
);
Some(Image {
width: img.width(),
height: img.height(),
CompositeTarget::SharedMemory => self
.rendering_context
.read_to_image(Rect::new(
Point2D::new(x as u32, y as u32),
Size2D::new(width, height),
))
.map(|image| Image {
width: image.width(),
height: image.height(),
format: PixelFormat::RGBA8,
bytes: ipc::IpcSharedMemory::from_bytes(&img),
bytes: ipc::IpcSharedMemory::from_bytes(&image),
id: None,
cors_status: CorsStatus::Safe,
})
},
}),
CompositeTarget::PngFile(path) => {
time_profile!(
ProfilerCategory::ImageSaving,
@ -2101,19 +2019,15 @@ impl IOCompositor {
self.time_profiler_chan.clone(),
|| match File::create(&*path) {
Ok(mut file) => {
let render_target_info = self
.next_offscreen_framebuffer
.take()
.expect("Guaranteed by needs_fbo");
let img = render_target_info.read_back_from_gpu(
x,
y,
FramebufferUintLength::new(width),
FramebufferUintLength::new(height),
);
let dynamic_image = DynamicImage::ImageRgba8(img);
if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png) {
error!("Failed to save {} ({}).", path, e);
if let Some(image) = self.rendering_context.read_to_image(Rect::new(
Point2D::new(x as u32, y as u32),
Size2D::new(width, height),
)) {
let dynamic_image = DynamicImage::ImageRgba8(image);
if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png)
{
error!("Failed to save {} ({}).", path, e);
}
}
},
Err(e) => error!("Failed to create {} ({}).", path, e),
@ -2205,14 +2119,6 @@ impl IOCompositor {
}
}
/// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to
/// [`CompositeTarget::OffscreenFbo`], or None otherwise.
pub fn offscreen_framebuffer_id(&self) -> Option<gleam::gl::GLuint> {
self.prev_offscreen_framebuffer
.as_ref()
.map(|info| info.framebuffer_id())
}
#[cfg_attr(
feature = "tracing",
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")

View file

@ -1,208 +0,0 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::rc::Rc;
use gleam::gl::{self, Gl};
use image::RgbaImage;
use log::{trace, warn};
use servo_geometry::FramebufferUintLength;
pub struct RenderTargetInfo {
gl: Rc<dyn Gl>,
framebuffer_ids: Vec<gl::GLuint>,
renderbuffer_ids: Vec<gl::GLuint>,
texture_ids: Vec<gl::GLuint>,
}
impl RenderTargetInfo {
pub fn new(
gl: Rc<dyn Gl>,
width: FramebufferUintLength,
height: FramebufferUintLength,
) -> Self {
let framebuffer_ids = gl.gen_framebuffers(1);
gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]);
trace!("Configuring fbo {}", framebuffer_ids[0]);
let texture_ids = gl.gen_textures(1);
gl.bind_texture(gl::TEXTURE_2D, texture_ids[0]);
gl.tex_image_2d(
gl::TEXTURE_2D,
0,
gl::RGBA as gl::GLint,
width.get() as gl::GLsizei,
height.get() as gl::GLsizei,
0,
gl::RGBA,
gl::UNSIGNED_BYTE,
None,
);
gl.tex_parameter_i(
gl::TEXTURE_2D,
gl::TEXTURE_MAG_FILTER,
gl::NEAREST as gl::GLint,
);
gl.tex_parameter_i(
gl::TEXTURE_2D,
gl::TEXTURE_MIN_FILTER,
gl::NEAREST as gl::GLint,
);
gl.framebuffer_texture_2d(
gl::FRAMEBUFFER,
gl::COLOR_ATTACHMENT0,
gl::TEXTURE_2D,
texture_ids[0],
0,
);
gl.bind_texture(gl::TEXTURE_2D, 0);
let renderbuffer_ids = gl.gen_renderbuffers(1);
let depth_rb = renderbuffer_ids[0];
gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
gl.renderbuffer_storage(
gl::RENDERBUFFER,
gl::DEPTH_COMPONENT24,
width.get() as gl::GLsizei,
height.get() as gl::GLsizei,
);
gl.framebuffer_renderbuffer(
gl::FRAMEBUFFER,
gl::DEPTH_ATTACHMENT,
gl::RENDERBUFFER,
depth_rb,
);
Self {
gl,
framebuffer_ids,
renderbuffer_ids,
texture_ids,
}
}
pub fn framebuffer_id(&self) -> gl::GLuint {
*self.framebuffer_ids.first().expect("Guaranteed by new")
}
pub fn bind(&self) {
trace!("Binding FBO {}", self.framebuffer_id());
self.gl
.bind_framebuffer(gl::FRAMEBUFFER, self.framebuffer_id());
}
pub fn unbind(&self) {
trace!("Unbinding FBO {}", self.framebuffer_id());
self.gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
}
pub fn read_back_from_gpu(
self,
x: i32,
y: i32,
width: FramebufferUintLength,
height: FramebufferUintLength,
) -> RgbaImage {
let width = width.get() as usize;
let height = height.get() as usize;
// For some reason, OSMesa fails to render on the 3rd
// attempt in headless mode, under some conditions.
// I think this can only be some kind of synchronization
// bug in OSMesa, but explicitly un-binding any vertex
// array here seems to work around that bug.
// See https://github.com/servo/servo/issues/18606.
self.gl.bind_vertex_array(0);
let mut pixels = self.gl.read_pixels(
x,
y,
width as gl::GLsizei,
height as gl::GLsizei,
gl::RGBA,
gl::UNSIGNED_BYTE,
);
let gl_error = self.gl.get_error();
if gl_error != gl::NO_ERROR {
warn!("GL error code 0x{gl_error:x} set after read_pixels");
}
// flip image vertically (texture is upside down)
let orig_pixels = pixels.clone();
let stride = width * 4;
for y in 0..height {
let dst_start = y * stride;
let src_start = (height - y - 1) * stride;
let src_slice = &orig_pixels[src_start..src_start + stride];
pixels[dst_start..dst_start + stride].clone_from_slice(&src_slice[..stride]);
}
RgbaImage::from_raw(width as u32, height as u32, pixels).expect("Flipping image failed!")
}
}
impl Drop for RenderTargetInfo {
fn drop(&mut self) {
trace!("Dropping FBO {}", self.framebuffer_id());
self.unbind();
self.gl.delete_textures(&self.texture_ids);
self.gl.delete_renderbuffers(&self.renderbuffer_ids);
self.gl.delete_framebuffers(&self.framebuffer_ids);
}
}
#[cfg(test)]
mod test {
use gleam::gl;
use image::Rgba;
use servo_geometry::FramebufferUintLength;
use surfman::{Connection, ContextAttributeFlags, ContextAttributes, Error, GLApi, GLVersion};
use super::RenderTargetInfo;
#[test]
#[allow(unsafe_code)]
fn test_read_pixels() -> Result<(), Error> {
let connection = Connection::new()?;
let adapter = connection.create_software_adapter()?;
let mut device = connection.create_device(&adapter)?;
let context_descriptor = device.create_context_descriptor(&ContextAttributes {
version: GLVersion::new(3, 0),
flags: ContextAttributeFlags::empty(),
})?;
let mut context = device.create_context(&context_descriptor, None)?;
let gl = match connection.gl_api() {
GLApi::GL => unsafe { gl::GlFns::load_with(|s| device.get_proc_address(&context, s)) },
GLApi::GLES => unsafe {
gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
},
};
device.make_context_current(&context)?;
{
const WIDTH: FramebufferUintLength = FramebufferUintLength::new(16);
const HEIGHT: FramebufferUintLength = FramebufferUintLength::new(16);
let render_target = RenderTargetInfo::new(gl, WIDTH, HEIGHT);
render_target.bind();
render_target
.gl
.clear_color(12.0 / 255.0, 34.0 / 255.0, 56.0 / 255.0, 78.0 / 255.0);
render_target.gl.clear(gl::COLOR_BUFFER_BIT);
let img = render_target.read_back_from_gpu(0, 0, WIDTH, HEIGHT);
assert_eq!(img.width(), WIDTH.get());
assert_eq!(img.height(), HEIGHT.get());
let expected_pixel: Rgba<u8> = Rgba([12, 34, 56, 78]);
assert!(img.pixels().all(|&p| p == expected_pixel));
}
device.destroy_context(&mut context)?;
Ok(())
}
}

View file

@ -19,7 +19,6 @@ pub use crate::compositor::{CompositeTarget, IOCompositor, ShutdownState};
mod tracing;
mod compositor;
mod gl;
mod touch;
pub mod webview;
pub mod windowing;

View file

@ -305,10 +305,6 @@ impl Servo {
}
debug_assert_eq!(webrender_gl.get_error(), gleam::gl::NO_ERROR,);
// Bind the webrender framebuffer
let framebuffer_object = rendering_context.framebuffer_object();
webrender_gl.bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object);
// Reserving a namespace to create TopLevelBrowsingContextId.
PipelineNamespace::install(PipelineNamespaceId(0));
@ -346,6 +342,7 @@ impl Servo {
opts.debug.webrender_stats,
);
rendering_context.prepare_for_rendering();
let render_notifier = Box::new(RenderNotifier::new(compositor_proxy.clone()));
let clear_color = servo_config::pref!(shell_background_color_rgba);
let clear_color = ColorF::new(
@ -354,6 +351,7 @@ impl Servo {
clear_color[2] as f32,
clear_color[3] as f32,
);
// Use same texture upload method as Gecko with ANGLE:
// https://searchfox.org/mozilla-central/source/gfx/webrender_bindings/src/bindings.rs#1215-1219
let upload_method = if webrender_gl.get_string(RENDERER).starts_with("ANGLE") {
@ -702,12 +700,6 @@ impl Servo {
self.compositor.borrow_mut().present();
}
/// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to
/// [`CompositeTarget::OffscreenFbo`], or None otherwise.
pub fn offscreen_framebuffer_id(&self) -> Option<u32> {
self.compositor.borrow().offscreen_framebuffer_id()
}
pub fn new_webview(&self, url: url::Url) -> WebView {
let webview = WebView::new(&self.constellation_proxy, self.compositor.clone());
self.webviews

View file

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

View file

@ -4,13 +4,17 @@
#![deny(unsafe_code)]
use std::cell::RefCell;
use std::cell::{Cell, RefCell};
use std::ffi::c_void;
use std::num::NonZeroU32;
use std::rc::Rc;
use euclid::default::Size2D;
use gleam::gl;
use log::{debug, warn};
use euclid::default::{Rect, Size2D};
use euclid::Point2D;
use gleam::gl::{self, Gl};
use glow::NativeFramebuffer;
use image::RgbaImage;
use log::{debug, trace, warn};
use servo_media::player::context::{GlContext, NativeDisplay};
use surfman::chains::{PreserveBuffer, SwapChain};
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
@ -36,16 +40,26 @@ pub enum GLVersion {
/// management, and destruction of the rendering context and its associated
/// resources.
pub trait RenderingContext {
/// Prepare this [`RenderingContext`] to be rendered upon by Servo. For instance,
/// by binding a framebuffer to the current OpenGL context.
fn prepare_for_rendering(&self) {}
/// Read the contents of this [`Renderingcontext`] into an in-memory image. If the
/// image cannot be read (for instance, if no rendering has taken place yet), then
/// `None` is returned.
///
/// In a double-buffered [`RenderingContext`] this is expected to read from the back
/// buffer. That means that once Servo renders to the context, this should return those
/// results, even before [`RenderingContext::present`] is called.
fn read_to_image(&self, source_rectangle: Rect<u32>) -> Option<RgbaImage>;
/// Resizes the rendering surface to the given size.
fn resize(&self, size: Size2D<i32>);
/// Presents the rendered frame to the screen.
/// Presents the rendered frame to the screen. In a double-buffered context, this would
/// swap buffers.
fn present(&self);
/// Makes the context the current OpenGL context for this thread.
/// After calling this function, it is valid to use OpenGL rendering
/// commands.
fn make_current(&self) -> Result<(), Error>;
/// Returns the OpenGL framebuffer object needed to render to the surface.
fn framebuffer_object(&self) -> u32;
/// Returns the OpenGL or GLES API.
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl>;
/// Describes the OpenGL version that is requested when a context is created.
@ -86,6 +100,7 @@ pub trait RenderingContext {
pub struct SurfmanRenderingContext(Rc<RenderingContextData>);
struct RenderingContextData {
gl: Rc<dyn Gl>,
device: RefCell<Device>,
context: RefCell<Context>,
// We either render to a swap buffer or to a native widget
@ -164,6 +179,23 @@ impl RenderingContext for SurfmanRenderingContext {
NativeDisplay::Unknown
}
}
fn prepare_for_rendering(&self) {
self.0.gl.bind_framebuffer(
gleam::gl::FRAMEBUFFER,
self.framebuffer()
.map_or(0, |framebuffer| framebuffer.0.into()),
);
}
fn read_to_image(&self, source_rectangle: Rect<u32>) -> Option<RgbaImage> {
let framebuffer_id = self
.framebuffer()
.map(|framebuffer| framebuffer.0.into())
.unwrap_or(0);
Framebuffer::read_framebuffer_to_image(self.gl_api(), framebuffer_id, source_rectangle)
}
fn resize(&self, size: Size2D<i32>) {
if let Err(err) = self.resize(size) {
warn!("Failed to resize surface: {:?}", err);
@ -177,23 +209,9 @@ impl RenderingContext for SurfmanRenderingContext {
fn make_current(&self) -> Result<(), Error> {
self.make_gl_context_current()
}
fn framebuffer_object(&self) -> u32 {
self.context_surface_info()
.unwrap_or(None)
.and_then(|info| info.framebuffer_object)
.map(|fbo| fbo.0.get())
.unwrap_or(0)
}
#[allow(unsafe_code)]
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
let context = self.0.context.borrow();
let device = self.0.device.borrow();
match self.connection().gl_api() {
GLApi::GL => unsafe { gl::GlFns::load_with(|s| device.get_proc_address(&context, s)) },
GLApi::GLES => unsafe {
gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
},
}
self.0.gl.clone()
}
fn gl_version(&self) -> GLVersion {
let device = self.0.device.borrow();
@ -270,9 +288,23 @@ impl SurfmanRenderingContext {
} else {
None
};
#[allow(unsafe_code)]
let gl = {
match connection.gl_api() {
GLApi::GL => unsafe {
gl::GlFns::load_with(|s| device.get_proc_address(&context, s))
},
GLApi::GLES => unsafe {
gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
},
}
};
let device = RefCell::new(device);
let context = RefCell::new(context);
let data = RenderingContextData {
gl,
device,
context,
swap_chain,
@ -464,4 +496,367 @@ impl SurfmanRenderingContext {
device.make_context_current(&context)?;
Ok(())
}
pub fn framebuffer(&self) -> Option<NativeFramebuffer> {
self.context_surface_info()
.unwrap_or(None)
.and_then(|info| info.framebuffer_object)
}
/// Create a new offscreen context that is compatible with this [`SurfmanRenderingContext`].
/// The contents of the resulting [`OffscreenRenderingContext`] are guaranteed to be blit
/// compatible with the this context.
pub fn offscreen_context(&self, size: Size2D<u32>) -> OffscreenRenderingContext {
OffscreenRenderingContext::new(SurfmanRenderingContext(self.0.clone()), size)
}
}
struct Framebuffer {
gl: Rc<dyn Gl>,
size: Size2D<u32>,
framebuffer_id: gl::GLuint,
renderbuffer_id: gl::GLuint,
texture_id: gl::GLuint,
}
impl Framebuffer {
fn bind(&self) {
trace!("Binding FBO {}", self.framebuffer_id);
self.gl
.bind_framebuffer(gl::FRAMEBUFFER, self.framebuffer_id)
}
}
impl Drop for Framebuffer {
fn drop(&mut self) {
self.gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
self.gl.delete_textures(&[self.texture_id]);
self.gl.delete_renderbuffers(&[self.renderbuffer_id]);
self.gl.delete_framebuffers(&[self.framebuffer_id]);
}
}
impl Framebuffer {
fn new(gl: Rc<dyn Gl>, size: Size2D<u32>) -> Self {
let framebuffer_ids = gl.gen_framebuffers(1);
gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]);
let texture_ids = gl.gen_textures(1);
gl.bind_texture(gl::TEXTURE_2D, texture_ids[0]);
gl.tex_image_2d(
gl::TEXTURE_2D,
0,
gl::RGBA as gl::GLint,
size.width as gl::GLsizei,
size.height as gl::GLsizei,
0,
gl::RGBA,
gl::UNSIGNED_BYTE,
None,
);
gl.tex_parameter_i(
gl::TEXTURE_2D,
gl::TEXTURE_MAG_FILTER,
gl::NEAREST as gl::GLint,
);
gl.tex_parameter_i(
gl::TEXTURE_2D,
gl::TEXTURE_MIN_FILTER,
gl::NEAREST as gl::GLint,
);
gl.framebuffer_texture_2d(
gl::FRAMEBUFFER,
gl::COLOR_ATTACHMENT0,
gl::TEXTURE_2D,
texture_ids[0],
0,
);
gl.bind_texture(gl::TEXTURE_2D, 0);
let renderbuffer_ids = gl.gen_renderbuffers(1);
let depth_rb = renderbuffer_ids[0];
gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
gl.renderbuffer_storage(
gl::RENDERBUFFER,
gl::DEPTH_COMPONENT24,
size.width as gl::GLsizei,
size.height as gl::GLsizei,
);
gl.framebuffer_renderbuffer(
gl::FRAMEBUFFER,
gl::DEPTH_ATTACHMENT,
gl::RENDERBUFFER,
depth_rb,
);
Self {
gl,
size,
framebuffer_id: *framebuffer_ids
.first()
.expect("Guaranteed by GL operations"),
renderbuffer_id: *renderbuffer_ids
.first()
.expect("Guaranteed by GL operations"),
texture_id: *texture_ids.first().expect("Guaranteed by GL operations"),
}
}
fn read_to_image(&self, source_rectangle: Rect<u32>) -> Option<RgbaImage> {
Self::read_framebuffer_to_image(self.gl.clone(), self.framebuffer_id, source_rectangle)
}
fn read_framebuffer_to_image(
gl: Rc<dyn Gl>,
framebuffer_id: u32,
source_rectangle: Rect<u32>,
) -> Option<RgbaImage> {
gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_id);
// For some reason, OSMesa fails to render on the 3rd
// attempt in headless mode, under some conditions.
// I think this can only be some kind of synchronization
// bug in OSMesa, but explicitly un-binding any vertex
// array here seems to work around that bug.
// See https://github.com/servo/servo/issues/18606.
gl.bind_vertex_array(0);
let mut pixels = gl.read_pixels(
source_rectangle.origin.x as i32,
source_rectangle.origin.y as i32,
source_rectangle.width() as gl::GLsizei,
source_rectangle.height() as gl::GLsizei,
gl::RGBA,
gl::UNSIGNED_BYTE,
);
let gl_error = gl.get_error();
if gl_error != gl::NO_ERROR {
warn!("GL error code 0x{gl_error:x} set after read_pixels");
}
// flip image vertically (texture is upside down)
let source_rectangle = source_rectangle.to_usize();
let orig_pixels = pixels.clone();
let stride = source_rectangle.width() * 4;
for y in 0..source_rectangle.height() {
let dst_start = y * stride;
let src_start = (source_rectangle.height() - y - 1) * stride;
let src_slice = &orig_pixels[src_start..src_start + stride];
pixels[dst_start..dst_start + stride].clone_from_slice(&src_slice[..stride]);
}
RgbaImage::from_raw(
source_rectangle.width() as u32,
source_rectangle.height() as u32,
pixels,
)
}
}
pub struct OffscreenRenderingContext {
parent_context: SurfmanRenderingContext,
size: Cell<Size2D<u32>>,
back_framebuffer: RefCell<Framebuffer>,
front_framebuffer: RefCell<Option<Framebuffer>>,
}
type RenderToParentCallback = Box<dyn Fn(&glow::Context, Rect<i32>) + Send + Sync>;
impl OffscreenRenderingContext {
fn new(parent_context: SurfmanRenderingContext, size: Size2D<u32>) -> Self {
let next_framebuffer = Framebuffer::new(parent_context.gl_api(), size);
Self {
parent_context,
size: Cell::new(size),
back_framebuffer: RefCell::new(next_framebuffer),
front_framebuffer: Default::default(),
}
}
pub fn parent_context(&self) -> &SurfmanRenderingContext {
&self.parent_context
}
pub fn front_framebuffer_id(&self) -> Option<gl::GLuint> {
self.front_framebuffer
.borrow()
.as_ref()
.map(|framebuffer| framebuffer.framebuffer_id)
}
pub fn render_to_parent_callback(&self) -> Option<RenderToParentCallback> {
// Don't accept a `None` context for the read framebuffer.
let front_framebuffer_id =
NonZeroU32::new(self.front_framebuffer_id()?).map(NativeFramebuffer)?;
let parent_context_framebuffer_id = self.parent_context.framebuffer();
let size = self.size.get();
Some(Box::new(move |gl, target_rect| {
Self::render_framebuffer_to_parent_context(
gl,
Rect::new(Point2D::origin(), size.to_i32()),
front_framebuffer_id,
target_rect,
parent_context_framebuffer_id,
);
}))
}
#[allow(unsafe_code)]
fn render_framebuffer_to_parent_context(
gl: &glow::Context,
source_rect: Rect<i32>,
source_framebuffer_id: NativeFramebuffer,
target_rect: Rect<i32>,
target_framebuffer_id: Option<NativeFramebuffer>,
) {
use glow::HasContext as _;
unsafe {
gl.clear_color(0.0, 0.0, 0.0, 0.0);
gl.scissor(
target_rect.origin.x,
target_rect.origin.y,
target_rect.width(),
target_rect.height(),
);
gl.enable(gl::SCISSOR_TEST);
gl.clear(gl::COLOR_BUFFER_BIT);
gl.disable(gl::SCISSOR_TEST);
gl.bind_framebuffer(gl::READ_FRAMEBUFFER, Some(source_framebuffer_id));
gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, target_framebuffer_id);
gl.blit_framebuffer(
source_rect.origin.x,
source_rect.origin.y,
source_rect.origin.x + source_rect.width(),
source_rect.origin.y + source_rect.height(),
target_rect.origin.x,
target_rect.origin.y,
target_rect.origin.x + target_rect.width(),
target_rect.origin.y + target_rect.height(),
gl::COLOR_BUFFER_BIT,
gl::NEAREST,
);
gl.bind_framebuffer(gl::FRAMEBUFFER, target_framebuffer_id);
}
}
}
impl RenderingContext for OffscreenRenderingContext {
fn resize(&self, size: Size2D<i32>) {
// We do not resize any buffers right now. The current buffers might be too big or too
// small, but we only want to ensure (later) that next buffer that we draw to is the
// correct size.
self.size.set(size.to_u32());
}
fn prepare_for_rendering(&self) {
self.back_framebuffer.borrow().bind();
}
fn present(&self) {
trace!(
"Unbinding FBO {}",
self.back_framebuffer.borrow().framebuffer_id
);
self.gl_api().bind_framebuffer(gl::FRAMEBUFFER, 0);
let new_back_framebuffer = match self.front_framebuffer.borrow_mut().take() {
Some(framebuffer) if framebuffer.size == self.size.get() => framebuffer,
_ => Framebuffer::new(self.gl_api(), self.size.get()),
};
let new_front_framebuffer = std::mem::replace(
&mut *self.back_framebuffer.borrow_mut(),
new_back_framebuffer,
);
*self.front_framebuffer.borrow_mut() = Some(new_front_framebuffer);
}
fn make_current(&self) -> Result<(), surfman::Error> {
self.parent_context.make_gl_context_current()
}
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
self.parent_context.gl_api()
}
fn gl_version(&self) -> GLVersion {
self.parent_context.gl_version()
}
fn create_texture(&self, surface: Surface) -> Option<(SurfaceTexture, u32, Size2D<i32>)> {
self.parent_context.create_texture(surface)
}
fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
self.parent_context.destroy_texture(surface_texture)
}
fn connection(&self) -> Option<Connection> {
Some(self.parent_context.connection())
}
fn read_to_image(&self, source_rectangle: Rect<u32>) -> Option<RgbaImage> {
self.back_framebuffer
.borrow()
.read_to_image(source_rectangle)
}
}
#[cfg(test)]
mod test {
use euclid::{Point2D, Rect, Size2D};
use gleam::gl;
use image::Rgba;
use surfman::{Connection, ContextAttributeFlags, ContextAttributes, Error, GLApi, GLVersion};
use super::Framebuffer;
#[test]
#[allow(unsafe_code)]
fn test_read_pixels() -> Result<(), Error> {
let connection = Connection::new()?;
let adapter = connection.create_software_adapter()?;
let mut device = connection.create_device(&adapter)?;
let context_descriptor = device.create_context_descriptor(&ContextAttributes {
version: GLVersion::new(3, 0),
flags: ContextAttributeFlags::empty(),
})?;
let mut context = device.create_context(&context_descriptor, None)?;
let gl = match connection.gl_api() {
GLApi::GL => unsafe { gl::GlFns::load_with(|s| device.get_proc_address(&context, s)) },
GLApi::GLES => unsafe {
gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
},
};
device.make_context_current(&context)?;
{
const SIZE: u32 = 16;
let framebuffer = Framebuffer::new(gl, Size2D::new(SIZE, SIZE));
framebuffer.bind();
framebuffer
.gl
.clear_color(12.0 / 255.0, 34.0 / 255.0, 56.0 / 255.0, 78.0 / 255.0);
framebuffer.gl.clear(gl::COLOR_BUFFER_BIT);
let img = framebuffer
.read_to_image(Rect::new(Point2D::zero(), Size2D::new(SIZE, SIZE)))
.expect("Should have been able to read back image.");
assert_eq!(img.width(), SIZE);
assert_eq!(img.height(), SIZE);
let expected_pixel: Rgba<u8> = Rgba([12, 34, 56, 78]);
assert!(img.pixels().all(|&p| p == expected_pixel));
}
device.destroy_context(&mut context)?;
Ok(())
}
}

View file

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

View file

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

View file

@ -12,7 +12,7 @@ use std::time::Duration;
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
use keyboard_types::{Modifiers, ShortcutMatcher};
use log::{debug, info};
use log::{debug, info, warn};
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
use servo::compositing::windowing::{
AnimationState, EmbedderCoordinates, WebRenderDebugOption, WindowMethods,
@ -22,13 +22,14 @@ use servo::servo_config::pref;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel};
use servo::webrender_api::ScrollLocation;
use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext};
use servo::webrender_traits::SurfmanRenderingContext;
use servo::{
Cursor, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
MouseButtonAction, MouseButtonEvent, MouseMoveEvent, Theme, TouchEvent, TouchEventAction,
TouchId, WebView, WheelDelta, WheelEvent, WheelMode,
};
use surfman::{Context, Device, SurfaceType};
use surfman::{Connection, Context, Device, SurfaceType};
use url::Url;
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
use winit::event::{
@ -52,9 +53,9 @@ pub struct Window {
inner_size: Cell<PhysicalSize<u32>>,
toolbar_height: Cell<Length<f32, DeviceIndependentPixel>>,
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,
mouse_pos: Cell<Point2D<i32, DevicePixel>>,
webview_relative_mouse_point: Cell<Point2D<f32, DevicePixel>>,
last_pressed: Cell<Option<(KeyboardEvent, Option<LogicalKey>)>>,
/// A map of winit's key codes to key values that are interpreted from
/// winit's ReceivedChar events.
@ -64,13 +65,21 @@ pub struct Window {
device_pixel_ratio_override: Option<f32>,
xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>,
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 {
pub fn new(
opts: &Opts,
servoshell_preferences: &ServoShellPreferences,
rendering_context: &SurfmanRenderingContext,
event_loop: &ActiveEventLoop,
) -> Window {
// 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> =
Scale::new(screen_scale);
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
.window_handle()
.expect("could not get window handle from window");
let inner_size = winit_window.inner_size();
let native_widget = rendering_context
.connection()
let native_widget = connection
.create_native_widget_from_window_handle(
window_handle,
winit_size_to_euclid_size(inner_size).to_i32().to_untyped(),
)
.expect("Failed to create native widget");
let surface_type = SurfaceType::Widget { native_widget };
let surface = rendering_context
.create_surface(surface_type)
let window_rendering_context = SurfmanRenderingContext::create(&connection, &adapter, None)
.expect("Failed to create window RenderingContext");
let surface = window_rendering_context
.create_surface(SurfaceType::Widget { native_widget })
.expect("Failed to create surface");
rendering_context
window_rendering_context
.bind_surface(surface)
.expect("Failed to bind surface");
// 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());
Window {
winit_window,
mouse_down_button: Cell::new(None),
mouse_down_point: Cell::new(Point2D::zero()),
mouse_pos: Cell::new(Point2D::zero()),
webview_relative_mouse_down_point: Cell::new(Point2D::zero()),
webview_relative_mouse_point: Cell::new(Point2D::zero()),
last_pressed: Cell::new(None),
keys_down: RefCell::new(HashMap::new()),
animation_state: Cell::new(AnimationState::Idle),
@ -153,6 +173,8 @@ impl Window {
xr_window_poses: RefCell::new(vec![]),
modifiers_state: Cell::new(ModifiersState::empty()),
toolbar_height: Cell::new(Default::default()),
window_rendering_context,
rendering_context,
}
}
@ -244,13 +266,7 @@ impl Window {
}
/// Helper function to handle a click
fn handle_mouse(
&self,
webview: &WebView,
button: MouseButton,
action: ElementState,
coords: Point2D<i32, DevicePixel>,
) {
fn handle_mouse(&self, webview: &WebView, button: MouseButton, action: ElementState) {
let max_pixel_dist = 10.0 * self.hidpi_factor().get();
let mouse_button = match &button {
MouseButton::Left => ServoMouseButton::Left,
@ -261,9 +277,10 @@ impl Window {
MouseButton::Other(value) => ServoMouseButton::Other(*value),
};
let point = self.webview_relative_mouse_point.get();
let action = match action {
ElementState::Pressed => {
self.mouse_down_point.set(coords);
self.webview_relative_mouse_down_point.set(point);
self.mouse_down_button.set(Some(button));
MouseButtonAction::Down
},
@ -273,7 +290,7 @@ impl Window {
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
action,
button: mouse_button,
point: coords.to_f32(),
point,
}));
// 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() {
let pixel_dist = self.mouse_down_point.get() - coords;
let pixel_dist =
((pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y) as f32).sqrt();
let pixel_dist = self.webview_relative_mouse_down_point.get() - point;
let pixel_dist = (pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y).sqrt();
if mouse_down_button == button && pixel_dist < max_pixel_dist {
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
action: MouseButtonAction::Click,
button: mouse_button,
point: coords.to_f32(),
point,
}));
}
}
@ -418,6 +434,10 @@ impl Window {
.otherwise(|| handled = false);
handled
}
pub(crate) fn offscreen_rendering_context(&self) -> Rc<OffscreenRenderingContext> {
self.rendering_context.clone()
}
}
impl WindowPortsMethods for Window {
@ -545,15 +565,15 @@ impl WindowPortsMethods for Window {
WindowEvent::ModifiersChanged(modifiers) => self.modifiers_state.set(modifiers.state()),
WindowEvent::MouseInput { state, button, .. } => {
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, .. } => {
let position = winit_position_to_euclid_point(position);
self.mouse_pos.set(position.to_i32());
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent {
point: position.to_f32(),
}));
let mut point = winit_position_to_euclid_point(position).to_f32();
point.y -= (self.toolbar_height() * self.hidpi_factor()).0;
self.webview_relative_mouse_point.set(point);
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent { point }));
},
WindowEvent::MouseWheel { delta, phase, .. } => {
let (mut dx, mut dy, mode) = match delta {
@ -574,8 +594,8 @@ impl WindowPortsMethods for Window {
z: 0.0,
mode,
};
let pos = self.mouse_pos.get();
let point = Point2D::new(pos.x as f32, pos.y as f32);
let pos = self.webview_relative_mouse_point.get();
let point = Point2D::new(pos.x, pos.y);
// Scroll events snap to the major axis of movement, with vertical
// preferred over horizontal.
@ -590,7 +610,11 @@ impl WindowPortsMethods for Window {
// Send events
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) => {
webview.notify_input_event(InputEvent::Touch(TouchEvent {
@ -607,6 +631,14 @@ impl WindowPortsMethods for Window {
},
WindowEvent::Resized(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);
webview.notify_rendering_context_resized();
}
@ -658,6 +690,10 @@ impl WindowPortsMethods for Window {
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>) {
self.toolbar_height.set(height);
}
fn rendering_context(&self) -> Rc<dyn RenderingContext> {
self.rendering_context.clone()
}
}
impl WindowMethods for Window {
@ -671,7 +707,9 @@ impl WindowMethods for Window {
let window_rect = (window_rect.to_f64() / window_scale).to_i32();
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 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::servo_geometry::DeviceIndependentPixel;
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 crate::desktop::window_trait::WindowPortsMethods;
@ -24,19 +27,31 @@ pub struct Window {
inner_size: Cell<DeviceIntSize>,
screen_size: Size2D<i32, DeviceIndependentPixel>,
window_rect: Box2D<i32, DeviceIndependentPixel>,
rendering_context: SurfmanRenderingContext,
}
impl Window {
#[allow(clippy::new_ret_no_self)]
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: Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> =
device_pixel_ratio_override.map(Scale::new);
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 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(
|| window_rect.size(),
@ -50,6 +65,7 @@ impl Window {
inner_size,
screen_size,
window_rect,
rendering_context,
};
Rc::new(window)
@ -132,6 +148,12 @@ impl WindowPortsMethods for Window {
fn set_toolbar_height(&self, _height: Length<f32, DeviceIndependentPixel>) {
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 {

View file

@ -3,27 +3,25 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::cell::{Cell, RefCell};
use std::num::NonZeroU32;
use std::rc::Rc;
use std::sync::Arc;
use std::time::Instant;
use egui::text::{CCursor, CCursorRange};
use egui::text_edit::TextEditState;
use egui::{
pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, Pos2, SelectableLabel,
pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, SelectableLabel,
TopBottomPanel, Vec2,
};
use egui_glow::CallbackFn;
use egui_winit::EventResponse;
use euclid::{Box2D, Length, Point2D, Scale, Size2D};
use gleam::gl;
use glow::NativeFramebuffer;
use euclid::{Box2D, Length, Point2D, Rect, Scale, Size2D};
use log::{trace, warn};
use servo::base::id::WebViewId;
use servo::servo_geometry::DeviceIndependentPixel;
use servo::servo_url::ServoUrl;
use servo::webrender_api::units::DevicePixel;
use servo::webrender_traits::SurfmanRenderingContext;
use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext};
use servo::{LoadStatus, WebView};
use winit::event::{ElementState, MouseButton, WindowEvent};
use winit::event_loop::ActiveEventLoop;
@ -34,14 +32,11 @@ use super::egui_glue::EguiGlow;
use super::geometry::winit_position_to_euclid_point;
pub struct Minibrowser {
rendering_context: Rc<OffscreenRenderingContext>,
pub context: EguiGlow,
pub event_queue: RefCell<Vec<MinibrowserEvent>>,
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_mouse_position: Option<Point2D<f32, DeviceIndependentPixel>>,
location: RefCell<String>,
@ -81,12 +76,14 @@ impl Drop for Minibrowser {
impl Minibrowser {
pub fn new(
rendering_context: &SurfmanRenderingContext,
rendering_context: Rc<OffscreenRenderingContext>,
event_loop: &ActiveEventLoop,
initial_url: ServoUrl,
) -> Self {
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
@ -99,17 +96,11 @@ impl Minibrowser {
.egui_ctx
.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 {
rendering_context,
context,
event_queue: RefCell::new(vec![]),
toolbar_height: Default::default(),
widget_surface_fbo,
last_update: Instant::now(),
last_mouse_position: None,
location: RefCell::new(initial_url.to_string()),
@ -268,17 +259,16 @@ impl Minibrowser {
reason
);
let Self {
rendering_context,
context,
event_queue,
toolbar_height,
widget_surface_fbo,
last_update,
location,
location_dirty,
..
} = self;
let widget_fbo = *widget_surface_fbo;
let servo_framebuffer_id = state.servo().offscreen_framebuffer_id();
let _duration = context.run(window, |ctx| {
// 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
@ -387,26 +377,23 @@ impl Minibrowser {
return;
};
CentralPanel::default().frame(Frame::NONE).show(ctx, |ui| {
let Pos2 { x, y } = ui.cursor().min;
let Vec2 {
x: width,
y: height,
} = ui.available_size();
let rect =
Box2D::from_origin_and_size(Point2D::new(x, y), Size2D::new(width, height)) *
scale;
// If the top parts of the GUI changed size, then update the size of the WebView and also
// the size of its RenderingContext.
let available_size = ui.available_size();
let rect = Box2D::from_origin_and_size(
Point2D::origin(),
Size2D::new(available_size.x, available_size.y),
) * scale;
if rect != webview.rect() {
webview.move_resize(rect);
rendering_context.resize(rect.size().to_i32().to_untyped());
}
let min = ui.cursor().min;
let size = ui.available_size();
let rect = egui::Rect::from_min_size(min, size);
ui.allocate_space(size);
let Some(servo_fbo) = servo_framebuffer_id else {
return;
};
if let Some(status_text) = &self.status_text {
egui::containers::popup::show_tooltip_at(
ctx,
@ -417,45 +404,19 @@ impl Minibrowser {
);
}
ui.painter().add(PaintCallback {
rect,
callback: Arc::new(CallbackFn::new(move |info, painter| {
use glow::HasContext as _;
let clip = info.viewport_in_pixels();
let x = clip.left_px as gl::GLint;
let y = clip.from_bottom_px as gl::GLint;
let width = clip.width_px as gl::GLsizei;
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,
if let Some(render_to_parent) = rendering_context.render_to_parent_callback() {
ui.painter().add(PaintCallback {
rect,
callback: Arc::new(CallbackFn::new(move |info, painter| {
let clip = info.viewport_in_pixels();
let rect_in_parent = Rect::new(
Point2D::new(clip.left_px, clip.from_bottom_px),
Size2D::new(clip.width_px, clip.height_px),
);
painter.gl().bind_framebuffer(gl::FRAMEBUFFER, widget_fbo);
}
})),
});
render_to_parent(painter.gl(), rect_in_parent)
})),
});
}
});
*last_update = now;
@ -464,14 +425,11 @@ impl Minibrowser {
/// Paint the minibrowser, as of the last update.
pub fn paint(&mut self, window: &Window) {
unsafe {
use glow::HasContext as _;
self.context
.painter
.gl()
.bind_framebuffer(gl::FRAMEBUFFER, self.widget_surface_fbo);
}
self.rendering_context
.parent_context()
.prepare_for_rendering();
self.context.paint(window);
let _ = self.rendering_context.parent_context().present();
}
/// 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::servo_geometry::DeviceIndependentPixel;
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
use servo::webrender_traits::rendering_context::RenderingContext;
use servo::{Cursor, WebView};
use super::app_state::RunningAppState;
@ -46,4 +47,5 @@ pub trait WindowPortsMethods: WindowMethods {
fn winit_window(&self) -> Option<&winit::window::Window>;
fn toolbar_height(&self) -> 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 = [
"background_hang_monitor",
"base",
"compositing",
"constellation",
"fonts",
"hyper_serde",
@ -175,6 +174,7 @@ class MachCommands(CommandBase):
"servo_config",
"servoshell",
"style_config",
"webrender_traits",
]
if not packages:
packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store'])