mirror of
https://github.com/servo/servo.git
synced 2025-07-24 15:50:21 +01:00
Add initial support for offscreen rendering (#30767)
* Offscreen rendering * shared memory case never actually rendered to backbuffer * fix compile errors (in theory) when gl crate feature disabled * update doc comments * remove dark CentralPanel border covering edges of viewport * clear to transparent, to avoid pink artifacts * fix mouse input for browser being consumed by egui * avoid destroying OpenGL resources unless resizing window * clean up compositing::gl * fix flickering around edges after resizing window * unset invalidate_last_render_target after invalidating * fix incorrect DRAW_FRAMEBUFFER name when blitting * bind the widget surface fbo before painting egui * make composite_specific_target take CompositeTarget, not Option * compositing: remove cargo feature “gl” * capitalise FBO in bind log message Co-authored-by: Martin Robinson <mrobinson@igalia.com> * capitalise FBO in drop log message Co-authored-by: Martin Robinson <mrobinson@igalia.com> * rename RenderTargetInfo fields and use OnceCell for next field * rename RenderTargetInfo.read to read_back_from_gpu * document servo_framebuffer_id in Minibrowser::update * rename needs_fbo to use_offscreen_framebuffer * capitalise FBO in unbind log message * clarify the purpose of Minibrowser::on_event * fix unused_must_use warning * reduce nesting in Minibrowser::update * use implicit format argument in panic * store Minibrowser.widget_surface_fbo as glow type * explain why servo_framebuffer_id is None in first call site * rename output_framebuffer_id to offscreen_framebuffer_id --------- Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
parent
97e6c72f57
commit
17f3c45d4f
13 changed files with 498 additions and 273 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -919,6 +919,7 @@ name = "compositing"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"canvas",
|
"canvas",
|
||||||
|
"cfg-if 1.0.0",
|
||||||
"compositing_traits",
|
"compositing_traits",
|
||||||
"crossbeam-channel",
|
"crossbeam-channel",
|
||||||
"embedder_traits",
|
"embedder_traits",
|
||||||
|
|
|
@ -13,18 +13,18 @@ path = "lib.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
gl = ["gleam", "pixels"]
|
|
||||||
multiview = []
|
multiview = []
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
canvas = { path = "../canvas" }
|
canvas = { path = "../canvas" }
|
||||||
|
cfg-if = { workspace = true }
|
||||||
compositing_traits = { workspace = true }
|
compositing_traits = { workspace = true }
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
embedder_traits = { workspace = true }
|
embedder_traits = { workspace = true }
|
||||||
euclid = { workspace = true }
|
euclid = { workspace = true }
|
||||||
fnv = { workspace = true }
|
fnv = { workspace = true }
|
||||||
gfx_traits = { workspace = true }
|
gfx_traits = { workspace = true }
|
||||||
gleam = { workspace = true, optional = true }
|
gleam = { workspace = true }
|
||||||
image = { workspace = true }
|
image = { workspace = true }
|
||||||
ipc-channel = { workspace = true }
|
ipc-channel = { workspace = true }
|
||||||
keyboard-types = { workspace = true }
|
keyboard-types = { workspace = true }
|
||||||
|
@ -33,7 +33,7 @@ log = { workspace = true }
|
||||||
msg = { workspace = true }
|
msg = { workspace = true }
|
||||||
net_traits = { workspace = true }
|
net_traits = { workspace = true }
|
||||||
num-traits = { workspace = true }
|
num-traits = { workspace = true }
|
||||||
pixels = { path = "../pixels", optional = true }
|
pixels = { path = "../pixels" }
|
||||||
profile_traits = { workspace = true }
|
profile_traits = { workspace = true }
|
||||||
script_traits = { workspace = true }
|
script_traits = { workspace = true }
|
||||||
servo_config = { path = "../config" }
|
servo_config = { path = "../config" }
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
* 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::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::fs::{create_dir_all, File};
|
use std::fs::{create_dir_all, File};
|
||||||
|
@ -21,17 +22,15 @@ use embedder_traits::Cursor;
|
||||||
use euclid::{Point2D, Rect, Scale, Transform3D, Vector2D};
|
use euclid::{Point2D, Rect, Scale, Transform3D, Vector2D};
|
||||||
use fnv::{FnvHashMap, FnvHashSet};
|
use fnv::{FnvHashMap, FnvHashSet};
|
||||||
use gfx_traits::{Epoch, FontData, WebRenderEpochToU16};
|
use gfx_traits::{Epoch, FontData, WebRenderEpochToU16};
|
||||||
#[cfg(feature = "gl")]
|
|
||||||
use image::{DynamicImage, ImageFormat};
|
use image::{DynamicImage, ImageFormat};
|
||||||
use ipc_channel::ipc;
|
use ipc_channel::ipc;
|
||||||
use libc::c_void;
|
use libc::c_void;
|
||||||
use log::{debug, error, info, warn};
|
use log::{debug, error, info, trace, warn};
|
||||||
use msg::constellation_msg::{
|
use msg::constellation_msg::{
|
||||||
PipelineId, PipelineIndex, PipelineNamespaceId, TopLevelBrowsingContextId,
|
PipelineId, PipelineIndex, PipelineNamespaceId, TopLevelBrowsingContextId,
|
||||||
};
|
};
|
||||||
use net_traits::image::base::Image;
|
use net_traits::image::base::Image;
|
||||||
use net_traits::image_cache::CorsStatus;
|
use net_traits::image_cache::CorsStatus;
|
||||||
#[cfg(feature = "gl")]
|
|
||||||
use pixels::PixelFormat;
|
use pixels::PixelFormat;
|
||||||
use profile_traits::time::{self as profile_time, profile, ProfilerCategory};
|
use profile_traits::time::{self as profile_time, profile, ProfilerCategory};
|
||||||
use script_traits::compositor::{HitTestInfo, ScrollTree};
|
use script_traits::compositor::{HitTestInfo, ScrollTree};
|
||||||
|
@ -57,13 +56,12 @@ use webrender_api::{
|
||||||
};
|
};
|
||||||
use webrender_surfman::WebrenderSurfman;
|
use webrender_surfman::WebrenderSurfman;
|
||||||
|
|
||||||
#[cfg(feature = "gl")]
|
use crate::gl::RenderTargetInfo;
|
||||||
use crate::gl;
|
|
||||||
use crate::touch::{TouchAction, TouchHandler};
|
use crate::touch::{TouchAction, TouchHandler};
|
||||||
use crate::windowing::{
|
use crate::windowing::{
|
||||||
self, EmbedderCoordinates, MouseWindowEvent, WebRenderDebugOption, WindowMethods,
|
self, EmbedderCoordinates, MouseWindowEvent, WebRenderDebugOption, WindowMethods,
|
||||||
};
|
};
|
||||||
use crate::InitialCompositorState;
|
use crate::{gl, InitialCompositorState};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum UnableToComposite {
|
enum UnableToComposite {
|
||||||
|
@ -225,7 +223,18 @@ pub struct IOCompositor<Window: WindowMethods + ?Sized> {
|
||||||
/// Current cursor position.
|
/// Current cursor position.
|
||||||
cursor_pos: DevicePoint,
|
cursor_pos: DevicePoint,
|
||||||
|
|
||||||
output_file: Option<String>,
|
/// Offscreen framebuffer object to render our next frame to.
|
||||||
|
/// We use this and `prev_offscreen_framebuffer` for double buffering when compositing to
|
||||||
|
/// [`CompositeTarget::Fbo`].
|
||||||
|
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::Fbo`].
|
||||||
|
prev_offscreen_framebuffer: Option<gl::RenderTargetInfo>,
|
||||||
|
|
||||||
|
/// Whether to invalidate `prev_offscreen_framebuffer` at the end of the next frame.
|
||||||
|
invalidate_prev_offscreen_framebuffer: bool,
|
||||||
|
|
||||||
is_running_problem_test: bool,
|
is_running_problem_test: bool,
|
||||||
|
|
||||||
|
@ -339,33 +348,32 @@ impl PipelineDetails {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
enum CompositeTarget {
|
pub enum CompositeTarget {
|
||||||
/// Normal composition to a window
|
/// Draw directly to a window.
|
||||||
Window,
|
Window,
|
||||||
|
|
||||||
/// Compose as normal, but also return a PNG of the composed output
|
/// Draw to an offscreen OpenGL framebuffer object, which can be retrieved once complete at
|
||||||
WindowAndPng,
|
/// [`IOCompositor::offscreen_framebuffer_id`].
|
||||||
|
Fbo,
|
||||||
|
|
||||||
/// Compose to a PNG, write it to disk, and then exit the browser (used for reftests)
|
/// Draw to an uncompressed image in shared memory.
|
||||||
PngFile,
|
SharedMemory,
|
||||||
|
|
||||||
|
/// Draw to a PNG file on disk, then exit the browser (for reftests).
|
||||||
|
PngFile(Rc<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
fn new(
|
fn new(
|
||||||
window: Rc<Window>,
|
window: Rc<Window>,
|
||||||
state: InitialCompositorState,
|
state: InitialCompositorState,
|
||||||
output_file: Option<String>,
|
composite_target: CompositeTarget,
|
||||||
is_running_problem_test: bool,
|
is_running_problem_test: bool,
|
||||||
exit_after_load: bool,
|
exit_after_load: bool,
|
||||||
convert_mouse_to_touch: bool,
|
convert_mouse_to_touch: bool,
|
||||||
top_level_browsing_context_id: TopLevelBrowsingContextId,
|
top_level_browsing_context_id: TopLevelBrowsingContextId,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let composite_target = match output_file {
|
|
||||||
Some(_) => CompositeTarget::PngFile,
|
|
||||||
None => CompositeTarget::Window,
|
|
||||||
};
|
|
||||||
|
|
||||||
IOCompositor {
|
IOCompositor {
|
||||||
embedder_coordinates: window.get_coordinates(),
|
embedder_coordinates: window.get_coordinates(),
|
||||||
window,
|
window,
|
||||||
|
@ -401,7 +409,9 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
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),
|
||||||
output_file,
|
next_offscreen_framebuffer: OnceCell::new(),
|
||||||
|
prev_offscreen_framebuffer: None,
|
||||||
|
invalidate_prev_offscreen_framebuffer: false,
|
||||||
is_running_problem_test,
|
is_running_problem_test,
|
||||||
exit_after_load,
|
exit_after_load,
|
||||||
convert_mouse_to_touch,
|
convert_mouse_to_touch,
|
||||||
|
@ -413,7 +423,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
pub fn create(
|
pub fn create(
|
||||||
window: Rc<Window>,
|
window: Rc<Window>,
|
||||||
state: InitialCompositorState,
|
state: InitialCompositorState,
|
||||||
output_file: Option<String>,
|
composite_target: CompositeTarget,
|
||||||
is_running_problem_test: bool,
|
is_running_problem_test: bool,
|
||||||
exit_after_load: bool,
|
exit_after_load: bool,
|
||||||
convert_mouse_to_touch: bool,
|
convert_mouse_to_touch: bool,
|
||||||
|
@ -422,7 +432,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
let mut compositor = IOCompositor::new(
|
let mut compositor = IOCompositor::new(
|
||||||
window,
|
window,
|
||||||
state,
|
state,
|
||||||
output_file,
|
composite_target,
|
||||||
is_running_problem_test,
|
is_running_problem_test,
|
||||||
exit_after_load,
|
exit_after_load,
|
||||||
convert_mouse_to_touch,
|
convert_mouse_to_touch,
|
||||||
|
@ -525,7 +535,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
},
|
},
|
||||||
|
|
||||||
(CompositorMsg::CreatePng(rect, reply), ShutdownState::NotShuttingDown) => {
|
(CompositorMsg::CreatePng(rect, reply), ShutdownState::NotShuttingDown) => {
|
||||||
let res = self.composite_specific_target(CompositeTarget::WindowAndPng, rect);
|
let res = self.composite_specific_target(CompositeTarget::SharedMemory, rect);
|
||||||
if let Err(ref e) = res {
|
if let Err(ref e) = res {
|
||||||
info!("Error retrieving PNG: {:?}", e);
|
info!("Error retrieving PNG: {:?}", e);
|
||||||
}
|
}
|
||||||
|
@ -592,7 +602,9 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
|
|
||||||
(CompositorMsg::LoadComplete(_), ShutdownState::NotShuttingDown) => {
|
(CompositorMsg::LoadComplete(_), ShutdownState::NotShuttingDown) => {
|
||||||
// If we're painting in headless mode, schedule a recomposite.
|
// If we're painting in headless mode, schedule a recomposite.
|
||||||
if self.output_file.is_some() || self.exit_after_load {
|
if matches!(self.composite_target, CompositeTarget::PngFile(_)) ||
|
||||||
|
self.exit_after_load
|
||||||
|
{
|
||||||
self.composite_if_necessary(CompositingReason::Headless);
|
self.composite_if_necessary(CompositingReason::Headless);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1050,7 +1062,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_resize_window_event(&mut self) -> bool {
|
pub fn on_resize_window_event(&mut self) -> bool {
|
||||||
debug!("compositor resize requested");
|
trace!("Compositor resize requested");
|
||||||
|
|
||||||
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();
|
||||||
|
@ -1060,6 +1072,13 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
self.update_zoom_transform();
|
self.update_zoom_transform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -1491,7 +1510,7 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> {
|
||||||
if self.output_file.is_some() {
|
if matches!(self.composite_target, CompositeTarget::PngFile(_)) {
|
||||||
return Scale::new(1.0);
|
return Scale::new(1.0);
|
||||||
}
|
}
|
||||||
self.embedder_coordinates.hidpi_factor
|
self.embedder_coordinates.hidpi_factor
|
||||||
|
@ -1636,10 +1655,11 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn composite(&mut self) {
|
pub fn composite(&mut self) {
|
||||||
let target = self.composite_target;
|
match self.composite_specific_target(self.composite_target.clone(), None) {
|
||||||
match self.composite_specific_target(target, None) {
|
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
if self.output_file.is_some() || self.exit_after_load {
|
if matches!(self.composite_target, CompositeTarget::PngFile(_)) ||
|
||||||
|
self.exit_after_load
|
||||||
|
{
|
||||||
println!("Shutting down the Constellation after generating an output file or exit flag specified");
|
println!("Shutting down the Constellation after generating an output file or exit flag specified");
|
||||||
self.start_shutting_down();
|
self.start_shutting_down();
|
||||||
}
|
}
|
||||||
|
@ -1656,11 +1676,10 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Composite either to the screen or to a png image or both.
|
/// Composite to the given target if any, or the current target otherwise.
|
||||||
/// Returns Ok if composition was performed or Err if it was not possible to composite
|
/// Returns Ok if composition was performed or Err if it was not possible to composite for some
|
||||||
/// for some reason. If CompositeTarget is Window or Png no image data is returned;
|
/// reason. When the target is [CompositeTarget::SharedMemory], the image is read back from the
|
||||||
/// in the latter case the image is written directly to a file. If CompositeTarget
|
/// GPU and returned as Ok(Some(png::Image)), otherwise we return Ok(None).
|
||||||
/// is WindowAndPng Ok(Some(png::Image)) is returned.
|
|
||||||
fn composite_specific_target(
|
fn composite_specific_target(
|
||||||
&mut self,
|
&mut self,
|
||||||
target: CompositeTarget,
|
target: CompositeTarget,
|
||||||
|
@ -1680,23 +1699,16 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
}
|
}
|
||||||
self.assert_no_gl_error();
|
self.assert_no_gl_error();
|
||||||
|
|
||||||
// Bind the webrender framebuffer
|
|
||||||
let framebuffer_object = self
|
|
||||||
.webrender_surfman
|
|
||||||
.context_surface_info()
|
|
||||||
.unwrap_or(None)
|
|
||||||
.map(|info| info.framebuffer_object)
|
|
||||||
.unwrap_or(0);
|
|
||||||
self.webrender_gl
|
|
||||||
.bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object);
|
|
||||||
self.assert_gl_framebuffer_complete();
|
|
||||||
|
|
||||||
self.webrender.update();
|
self.webrender.update();
|
||||||
|
|
||||||
let wait_for_stable_image = match target {
|
let wait_for_stable_image = matches!(
|
||||||
CompositeTarget::WindowAndPng | CompositeTarget::PngFile => true,
|
target,
|
||||||
CompositeTarget::Window => self.exit_after_load,
|
CompositeTarget::SharedMemory | CompositeTarget::PngFile(_)
|
||||||
};
|
) || self.exit_after_load;
|
||||||
|
let use_offscreen_framebuffer = matches!(
|
||||||
|
target,
|
||||||
|
CompositeTarget::SharedMemory | CompositeTarget::PngFile(_) | CompositeTarget::Fbo
|
||||||
|
);
|
||||||
|
|
||||||
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,
|
||||||
|
@ -1713,25 +1725,35 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let rt_info = match target {
|
if use_offscreen_framebuffer {
|
||||||
#[cfg(feature = "gl")]
|
self.next_offscreen_framebuffer
|
||||||
CompositeTarget::Window => gl::RenderTargetInfo::default(),
|
.get_or_init(|| {
|
||||||
#[cfg(feature = "gl")]
|
RenderTargetInfo::new(
|
||||||
CompositeTarget::WindowAndPng | CompositeTarget::PngFile => gl::initialize_png(
|
self.webrender_gl.clone(),
|
||||||
&*self.webrender_gl,
|
FramebufferUintLength::new(size.width),
|
||||||
FramebufferUintLength::new(size.width),
|
FramebufferUintLength::new(size.height),
|
||||||
FramebufferUintLength::new(size.height),
|
)
|
||||||
),
|
})
|
||||||
#[cfg(not(feature = "gl"))]
|
.bind();
|
||||||
_ => (),
|
} else {
|
||||||
};
|
// Bind the webrender framebuffer
|
||||||
|
let framebuffer_object = self
|
||||||
|
.webrender_surfman
|
||||||
|
.context_surface_info()
|
||||||
|
.unwrap_or(None)
|
||||||
|
.map(|info| info.framebuffer_object)
|
||||||
|
.unwrap_or(0);
|
||||||
|
self.webrender_gl
|
||||||
|
.bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object);
|
||||||
|
self.assert_gl_framebuffer_complete();
|
||||||
|
}
|
||||||
|
|
||||||
profile(
|
profile(
|
||||||
ProfilerCategory::Compositing,
|
ProfilerCategory::Compositing,
|
||||||
None,
|
None,
|
||||||
self.time_profiler_chan.clone(),
|
self.time_profiler_chan.clone(),
|
||||||
|| {
|
|| {
|
||||||
debug!("compositor: compositing");
|
trace!("Compositing");
|
||||||
|
|
||||||
let size =
|
let size =
|
||||||
DeviceIntSize::from_untyped(self.embedder_coordinates.framebuffer.to_untyped());
|
DeviceIntSize::from_untyped(self.embedder_coordinates.framebuffer.to_untyped());
|
||||||
|
@ -1781,29 +1803,47 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (x, y, width, height) = match rect {
|
let (x, y, width, height) = if let Some(rect) = rect {
|
||||||
Some(rect) => {
|
let rect = self.device_pixels_per_page_px().transform_rect(&rect);
|
||||||
let rect = self.device_pixels_per_page_px().transform_rect(&rect);
|
|
||||||
|
|
||||||
let x = rect.origin.x as i32;
|
let x = rect.origin.x as i32;
|
||||||
// We need to convert to the bottom-left origin coordinate
|
// We need to convert to the bottom-left origin coordinate
|
||||||
// system used by OpenGL
|
// system used by OpenGL
|
||||||
let y = (size.height as f32 - rect.origin.y - rect.size.height) as i32;
|
let y = (size.height as f32 - rect.origin.y - rect.size.height) as i32;
|
||||||
let w = rect.size.width as u32;
|
let w = rect.size.width as u32;
|
||||||
let h = rect.size.height as u32;
|
let h = rect.size.height as u32;
|
||||||
|
|
||||||
(x, y, w, h)
|
(x, y, w, h)
|
||||||
},
|
} else {
|
||||||
None => (0, 0, size.width, size.height),
|
(0, 0, size.width, size.height)
|
||||||
};
|
};
|
||||||
|
|
||||||
let rv = match target {
|
let rv = match target {
|
||||||
CompositeTarget::Window => None,
|
CompositeTarget::Window => None,
|
||||||
#[cfg(feature = "gl")]
|
CompositeTarget::Fbo => {
|
||||||
CompositeTarget::WindowAndPng => {
|
self.next_offscreen_framebuffer
|
||||||
let img = gl::draw_img(
|
.get()
|
||||||
&*self.webrender_gl,
|
.expect("Guaranteed by needs_fbo")
|
||||||
rt_info,
|
.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,
|
x,
|
||||||
y,
|
y,
|
||||||
FramebufferUintLength::new(width),
|
FramebufferUintLength::new(width),
|
||||||
|
@ -1818,39 +1858,33 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
cors_status: CorsStatus::Safe,
|
cors_status: CorsStatus::Safe,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
#[cfg(feature = "gl")]
|
CompositeTarget::PngFile(path) => {
|
||||||
CompositeTarget::PngFile => {
|
|
||||||
let gl = &*self.webrender_gl;
|
|
||||||
profile(
|
profile(
|
||||||
ProfilerCategory::ImageSaving,
|
ProfilerCategory::ImageSaving,
|
||||||
None,
|
None,
|
||||||
self.time_profiler_chan.clone(),
|
self.time_profiler_chan.clone(),
|
||||||
|| match self.output_file.as_ref() {
|
|| match File::create(&*path) {
|
||||||
Some(path) => match File::create(path) {
|
Ok(mut file) => {
|
||||||
Ok(mut file) => {
|
let render_target_info = self
|
||||||
let img = gl::draw_img(
|
.next_offscreen_framebuffer
|
||||||
gl,
|
.take()
|
||||||
rt_info,
|
.expect("Guaranteed by needs_fbo");
|
||||||
x,
|
let img = render_target_info.read_back_from_gpu(
|
||||||
y,
|
x,
|
||||||
FramebufferUintLength::new(width),
|
y,
|
||||||
FramebufferUintLength::new(height),
|
FramebufferUintLength::new(width),
|
||||||
);
|
FramebufferUintLength::new(height),
|
||||||
let dynamic_image = DynamicImage::ImageRgb8(img);
|
);
|
||||||
if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png)
|
let dynamic_image = DynamicImage::ImageRgb8(img);
|
||||||
{
|
if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png) {
|
||||||
error!("Failed to save {} ({}).", path, e);
|
error!("Failed to save {} ({}).", path, e);
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Err(e) => error!("Failed to create {} ({}).", path, e),
|
|
||||||
},
|
},
|
||||||
None => error!("No file specified."),
|
Err(e) => error!("Failed to create {} ({}).", path, e),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
#[cfg(not(feature = "gl"))]
|
|
||||||
_ => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Nottify embedder that servo is ready to present.
|
// Nottify embedder that servo is ready to present.
|
||||||
|
@ -1871,6 +1905,14 @@ impl<Window: WindowMethods + ?Sized> IOCompositor<Window> {
|
||||||
Ok(rv)
|
Ok(rv)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to
|
||||||
|
/// [`CompositeTarget::Fbo`], or None otherwise.
|
||||||
|
pub fn offscreen_framebuffer_id(&self) -> Option<gleam::gl::GLuint> {
|
||||||
|
self.prev_offscreen_framebuffer
|
||||||
|
.as_ref()
|
||||||
|
.map(|info| info.framebuffer_id())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn present(&mut self) {
|
pub fn present(&mut self) {
|
||||||
if let Err(err) = self.webrender_surfman.present() {
|
if let Err(err) = self.webrender_surfman.present() {
|
||||||
warn!("Failed to present surface: {:?}", err);
|
warn!("Failed to present surface: {:?}", err);
|
||||||
|
|
|
@ -2,125 +2,149 @@
|
||||||
* 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 gleam::gl;
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gleam::gl::{self, Gl};
|
||||||
use image::RgbImage;
|
use image::RgbImage;
|
||||||
|
use log::trace;
|
||||||
use servo_geometry::FramebufferUintLength;
|
use servo_geometry::FramebufferUintLength;
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct RenderTargetInfo {
|
pub struct RenderTargetInfo {
|
||||||
|
gl: Rc<dyn Gl>,
|
||||||
framebuffer_ids: Vec<gl::GLuint>,
|
framebuffer_ids: Vec<gl::GLuint>,
|
||||||
renderbuffer_ids: Vec<gl::GLuint>,
|
renderbuffer_ids: Vec<gl::GLuint>,
|
||||||
texture_ids: Vec<gl::GLuint>,
|
texture_ids: Vec<gl::GLuint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn initialize_png(
|
impl RenderTargetInfo {
|
||||||
gl: &dyn gl::Gl,
|
pub fn new(
|
||||||
width: FramebufferUintLength,
|
gl: Rc<dyn Gl>,
|
||||||
height: FramebufferUintLength,
|
width: FramebufferUintLength,
|
||||||
) -> RenderTargetInfo {
|
height: FramebufferUintLength,
|
||||||
let framebuffer_ids = gl.gen_framebuffers(1);
|
) -> Self {
|
||||||
gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]);
|
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);
|
let texture_ids = gl.gen_textures(1);
|
||||||
gl.bind_texture(gl::TEXTURE_2D, texture_ids[0]);
|
gl.bind_texture(gl::TEXTURE_2D, texture_ids[0]);
|
||||||
|
gl.tex_image_2d(
|
||||||
|
gl::TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
gl::RGB as gl::GLint,
|
||||||
|
width.get() as gl::GLsizei,
|
||||||
|
height.get() as gl::GLsizei,
|
||||||
|
0,
|
||||||
|
gl::RGB,
|
||||||
|
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.tex_image_2d(
|
gl.framebuffer_texture_2d(
|
||||||
gl::TEXTURE_2D,
|
gl::FRAMEBUFFER,
|
||||||
0,
|
gl::COLOR_ATTACHMENT0,
|
||||||
gl::RGB as gl::GLint,
|
gl::TEXTURE_2D,
|
||||||
width.get() as gl::GLsizei,
|
texture_ids[0],
|
||||||
height.get() as gl::GLsizei,
|
0,
|
||||||
0,
|
);
|
||||||
gl::RGB,
|
|
||||||
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.bind_texture(gl::TEXTURE_2D, 0);
|
||||||
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,
|
||||||
|
);
|
||||||
|
|
||||||
let renderbuffer_ids = gl.gen_renderbuffers(1);
|
Self {
|
||||||
let depth_rb = renderbuffer_ids[0];
|
gl,
|
||||||
gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
|
framebuffer_ids,
|
||||||
gl.renderbuffer_storage(
|
renderbuffer_ids,
|
||||||
gl::RENDERBUFFER,
|
texture_ids,
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
RenderTargetInfo {
|
pub fn framebuffer_id(&self) -> gl::GLuint {
|
||||||
framebuffer_ids,
|
*self.framebuffer_ids.first().expect("Guaranteed by new")
|
||||||
renderbuffer_ids,
|
}
|
||||||
texture_ids,
|
|
||||||
|
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,
|
||||||
|
) -> RgbImage {
|
||||||
|
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::RGB,
|
||||||
|
gl::UNSIGNED_BYTE,
|
||||||
|
);
|
||||||
|
|
||||||
|
// flip image vertically (texture is upside down)
|
||||||
|
let orig_pixels = pixels.clone();
|
||||||
|
let stride = width * 3;
|
||||||
|
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];
|
||||||
|
(&mut pixels[dst_start..dst_start + stride]).clone_from_slice(&src_slice[..stride]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RgbImage::from_raw(width as u32, height as u32, pixels).expect("Flipping image failed!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_img(
|
impl Drop for RenderTargetInfo {
|
||||||
gl: &dyn gl::Gl,
|
fn drop(&mut self) {
|
||||||
render_target_info: RenderTargetInfo,
|
trace!("Dropping FBO {}", self.framebuffer_id());
|
||||||
x: i32,
|
self.unbind();
|
||||||
y: i32,
|
self.gl.delete_textures(&self.texture_ids);
|
||||||
width: FramebufferUintLength,
|
self.gl.delete_renderbuffers(&self.renderbuffer_ids);
|
||||||
height: FramebufferUintLength,
|
self.gl.delete_framebuffers(&self.framebuffer_ids);
|
||||||
) -> RgbImage {
|
|
||||||
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.
|
|
||||||
gl.bind_vertex_array(0);
|
|
||||||
|
|
||||||
let mut pixels = gl.read_pixels(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width as gl::GLsizei,
|
|
||||||
height as gl::GLsizei,
|
|
||||||
gl::RGB,
|
|
||||||
gl::UNSIGNED_BYTE,
|
|
||||||
);
|
|
||||||
|
|
||||||
gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
|
|
||||||
|
|
||||||
gl.delete_textures(&render_target_info.texture_ids);
|
|
||||||
gl.delete_renderbuffers(&render_target_info.renderbuffer_ids);
|
|
||||||
gl.delete_framebuffers(&render_target_info.framebuffer_ids);
|
|
||||||
|
|
||||||
// flip image vertically (texture is upside down)
|
|
||||||
let orig_pixels = pixels.clone();
|
|
||||||
let stride = width * 3;
|
|
||||||
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];
|
|
||||||
(&mut pixels[dst_start..dst_start + stride]).clone_from_slice(&src_slice[..stride]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RgbImage::from_raw(width as u32, height as u32, pixels).expect("Flipping image failed!")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,9 @@ use webrender::RenderApi;
|
||||||
use webrender_api::DocumentId;
|
use webrender_api::DocumentId;
|
||||||
use webrender_surfman::WebrenderSurfman;
|
use webrender_surfman::WebrenderSurfman;
|
||||||
|
|
||||||
pub use crate::compositor::{IOCompositor, ShutdownState};
|
pub use crate::compositor::{CompositeTarget, IOCompositor, ShutdownState};
|
||||||
|
|
||||||
mod compositor;
|
mod compositor;
|
||||||
#[cfg(feature = "gl")]
|
|
||||||
mod gl;
|
mod gl;
|
||||||
mod touch;
|
mod touch;
|
||||||
pub mod windowing;
|
pub mod windowing;
|
||||||
|
|
|
@ -37,7 +37,7 @@ bluetooth = { path = "../bluetooth" }
|
||||||
bluetooth_traits = { workspace = true }
|
bluetooth_traits = { workspace = true }
|
||||||
canvas = { path = "../canvas", default-features = false }
|
canvas = { path = "../canvas", default-features = false }
|
||||||
canvas_traits = { workspace = true }
|
canvas_traits = { workspace = true }
|
||||||
compositing = { path = "../compositing", features = ["gl"] }
|
compositing = { path = "../compositing" }
|
||||||
compositing_traits = { workspace = true }
|
compositing_traits = { workspace = true }
|
||||||
constellation = { path = "../constellation" }
|
constellation = { path = "../constellation" }
|
||||||
crossbeam-channel = { workspace = true }
|
crossbeam-channel = { workspace = true }
|
||||||
|
|
|
@ -30,7 +30,7 @@ use canvas::canvas_paint_thread::{self, CanvasPaintThread};
|
||||||
use canvas::WebGLComm;
|
use canvas::WebGLComm;
|
||||||
use canvas_traits::webgl::WebGLThreads;
|
use canvas_traits::webgl::WebGLThreads;
|
||||||
use compositing::windowing::{EmbedderEvent, EmbedderMethods, WindowMethods};
|
use compositing::windowing::{EmbedderEvent, EmbedderMethods, WindowMethods};
|
||||||
use compositing::{IOCompositor, InitialCompositorState, ShutdownState};
|
use compositing::{CompositeTarget, IOCompositor, InitialCompositorState, ShutdownState};
|
||||||
use compositing_traits::{
|
use compositing_traits::{
|
||||||
CanvasToCompositorMsg, CompositingReason, CompositorMsg, CompositorProxy, CompositorReceiver,
|
CanvasToCompositorMsg, CompositingReason, CompositorMsg, CompositorProxy, CompositorReceiver,
|
||||||
ConstellationMsg, FontToCompositorMsg, ForwardedToCompositorMsg,
|
ConstellationMsg, FontToCompositorMsg, ForwardedToCompositorMsg,
|
||||||
|
@ -224,6 +224,7 @@ where
|
||||||
mut embedder: Box<dyn EmbedderMethods>,
|
mut embedder: Box<dyn EmbedderMethods>,
|
||||||
window: Rc<Window>,
|
window: Rc<Window>,
|
||||||
user_agent: Option<String>,
|
user_agent: Option<String>,
|
||||||
|
composite_target: CompositeTarget,
|
||||||
) -> InitializedServo<Window> {
|
) -> InitializedServo<Window> {
|
||||||
// Global configuration options, parsed from the command line.
|
// Global configuration options, parsed from the command line.
|
||||||
let opts = opts::get();
|
let opts = opts::get();
|
||||||
|
@ -447,6 +448,12 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let composite_target = if let Some(path) = opts.output_file.clone() {
|
||||||
|
CompositeTarget::PngFile(path.into())
|
||||||
|
} else {
|
||||||
|
composite_target
|
||||||
|
};
|
||||||
|
|
||||||
// The compositor coordinates with the client window to create the final
|
// The compositor coordinates with the client window to create the final
|
||||||
// rendered page and display it somewhere.
|
// rendered page and display it somewhere.
|
||||||
let compositor = IOCompositor::create(
|
let compositor = IOCompositor::create(
|
||||||
|
@ -464,7 +471,7 @@ where
|
||||||
webrender_gl,
|
webrender_gl,
|
||||||
webxr_main_thread,
|
webxr_main_thread,
|
||||||
},
|
},
|
||||||
opts.output_file.clone(),
|
composite_target,
|
||||||
opts.is_running_problem_test,
|
opts.is_running_problem_test,
|
||||||
opts.exit_after_load,
|
opts.exit_after_load,
|
||||||
opts.debug.convert_mouse_to_touch,
|
opts.debug.convert_mouse_to_touch,
|
||||||
|
@ -769,6 +776,12 @@ where
|
||||||
pub fn recomposite(&mut self) {
|
pub fn recomposite(&mut self) {
|
||||||
self.compositor.composite();
|
self.compositor.composite();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to
|
||||||
|
/// [`CompositeTarget::Fbo`], or None otherwise.
|
||||||
|
pub fn offscreen_framebuffer_id(&self) -> Option<u32> {
|
||||||
|
self.compositor.offscreen_framebuffer_id()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_embedder_channel(
|
fn create_embedder_channel(
|
||||||
|
|
|
@ -4,7 +4,7 @@ name = "servoshell"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
authors = ["The Servo Project Developers"]
|
authors = ["The Servo Project Developers"]
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ use std::{env, fs};
|
||||||
use gleam::gl;
|
use gleam::gl;
|
||||||
use log::{info, trace, warn};
|
use log::{info, trace, warn};
|
||||||
use servo::compositing::windowing::EmbedderEvent;
|
use servo::compositing::windowing::EmbedderEvent;
|
||||||
|
use servo::compositing::CompositeTarget;
|
||||||
use servo::config::opts;
|
use servo::config::opts;
|
||||||
use servo::servo_config::pref;
|
use servo::servo_config::pref;
|
||||||
use servo::Servo;
|
use servo::Servo;
|
||||||
|
@ -122,8 +123,9 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mut minibrowser) = app.minibrowser() {
|
if let Some(mut minibrowser) = app.minibrowser() {
|
||||||
minibrowser.update(window.winit_window().unwrap(), "init");
|
// Servo is not yet initialised, so there is no `servo_framebuffer_id`.
|
||||||
window.set_toolbar_height(minibrowser.toolbar_height.get());
|
minibrowser.update(window.winit_window().unwrap(), None, "init");
|
||||||
|
window.set_toolbar_height(minibrowser.toolbar_height);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Whether or not to recomposite during the next RedrawRequested event.
|
// Whether or not to recomposite during the next RedrawRequested event.
|
||||||
|
@ -185,7 +187,17 @@ impl App {
|
||||||
// Implements embedder methods, used by libservo and constellation.
|
// Implements embedder methods, used by libservo and constellation.
|
||||||
let embedder = Box::new(EmbedderCallbacks::new(ev_waker.clone(), xr_discovery));
|
let embedder = Box::new(EmbedderCallbacks::new(ev_waker.clone(), xr_discovery));
|
||||||
|
|
||||||
let servo_data = Servo::new(embedder, window.clone(), user_agent.clone());
|
let composite_target = if app.minibrowser.is_some() {
|
||||||
|
CompositeTarget::Fbo
|
||||||
|
} else {
|
||||||
|
CompositeTarget::Window
|
||||||
|
};
|
||||||
|
let servo_data = Servo::new(
|
||||||
|
embedder,
|
||||||
|
window.clone(),
|
||||||
|
user_agent.clone(),
|
||||||
|
composite_target,
|
||||||
|
);
|
||||||
let mut servo = servo_data.servo;
|
let mut servo = servo_data.servo;
|
||||||
|
|
||||||
servo.handle_events(vec![EmbedderEvent::NewBrowser(
|
servo.handle_events(vec![EmbedderEvent::NewBrowser(
|
||||||
|
@ -217,7 +229,11 @@ impl App {
|
||||||
app.servo.as_mut().unwrap().recomposite();
|
app.servo.as_mut().unwrap().recomposite();
|
||||||
}
|
}
|
||||||
if let Some(mut minibrowser) = app.minibrowser() {
|
if let Some(mut minibrowser) = app.minibrowser() {
|
||||||
minibrowser.update(window.winit_window().unwrap(), "RedrawRequested");
|
minibrowser.update(
|
||||||
|
window.winit_window().unwrap(),
|
||||||
|
app.servo.as_ref().unwrap().offscreen_framebuffer_id(),
|
||||||
|
"RedrawRequested",
|
||||||
|
);
|
||||||
minibrowser.paint(window.winit_window().unwrap());
|
minibrowser.paint(window.winit_window().unwrap());
|
||||||
}
|
}
|
||||||
app.servo.as_mut().unwrap().present();
|
app.servo.as_mut().unwrap().present();
|
||||||
|
@ -252,7 +268,7 @@ impl App {
|
||||||
window.winit_window().unwrap().request_redraw();
|
window.winit_window().unwrap().request_redraw();
|
||||||
},
|
},
|
||||||
winit::event::Event::WindowEvent { ref event, .. } => {
|
winit::event::Event::WindowEvent { ref event, .. } => {
|
||||||
let response = minibrowser.context.on_event(&event);
|
let response = minibrowser.on_event(&event);
|
||||||
if response.repaint {
|
if response.repaint {
|
||||||
// Request a winit redraw event, so we can recomposite, update and paint
|
// Request a winit redraw event, so we can recomposite, update and paint
|
||||||
// the minibrowser, and present the new frame.
|
// the minibrowser, and present the new frame.
|
||||||
|
@ -306,6 +322,7 @@ impl App {
|
||||||
// redraw, doing so would delay the location update by two frames.
|
// redraw, doing so would delay the location update by two frames.
|
||||||
minibrowser.update(
|
minibrowser.update(
|
||||||
window.winit_window().unwrap(),
|
window.winit_window().unwrap(),
|
||||||
|
app.servo.as_ref().unwrap().offscreen_framebuffer_id(),
|
||||||
"update_location_in_toolbar",
|
"update_location_in_toolbar",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -323,6 +340,7 @@ impl App {
|
||||||
if let Some(mut minibrowser) = app.minibrowser() {
|
if let Some(mut minibrowser) = app.minibrowser() {
|
||||||
minibrowser.update(
|
minibrowser.update(
|
||||||
window.winit_window().unwrap(),
|
window.winit_window().unwrap(),
|
||||||
|
app.servo.as_ref().unwrap().offscreen_framebuffer_id(),
|
||||||
"PumpResult::Present::Immediate",
|
"PumpResult::Present::Immediate",
|
||||||
);
|
);
|
||||||
minibrowser.paint(window.winit_window().unwrap());
|
minibrowser.paint(window.winit_window().unwrap());
|
||||||
|
|
15
ports/servoshell/geometry.rs
Normal file
15
ports/servoshell/geometry.rs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/* 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 euclid::{Point2D, Size2D};
|
||||||
|
use servo::style_traits::DevicePixel;
|
||||||
|
use winit::dpi::{PhysicalPosition, PhysicalSize};
|
||||||
|
|
||||||
|
pub fn winit_size_to_euclid_size<T>(size: PhysicalSize<T>) -> Size2D<T, DevicePixel> {
|
||||||
|
Size2D::new(size.width, size.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn winit_position_to_euclid_point<T>(position: PhysicalPosition<T>) -> Point2D<T, DevicePixel> {
|
||||||
|
Point2D::new(position.x, position.y)
|
||||||
|
}
|
|
@ -41,6 +41,7 @@ use winit::event::{
|
||||||
use winit::window::Icon;
|
use winit::window::Icon;
|
||||||
|
|
||||||
use crate::events_loop::{EventsLoop, WakerEvent};
|
use crate::events_loop::{EventsLoop, WakerEvent};
|
||||||
|
use crate::geometry::{winit_position_to_euclid_point, winit_size_to_euclid_size};
|
||||||
use crate::keyutils::keyboard_event_from_winit;
|
use crate::keyutils::keyboard_event_from_winit;
|
||||||
use crate::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
use crate::window_trait::{WindowPortsMethods, LINE_HEIGHT};
|
||||||
|
|
||||||
|
@ -519,14 +520,6 @@ impl WindowPortsMethods for Window {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn winit_size_to_euclid_size<T>(size: PhysicalSize<T>) -> Size2D<T, DevicePixel> {
|
|
||||||
Size2D::new(size.width, size.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn winit_position_to_euclid_point<T>(position: PhysicalPosition<T>) -> Point2D<T, DevicePixel> {
|
|
||||||
Point2D::new(position.x, position.y)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WindowMethods for Window {
|
impl WindowMethods for Window {
|
||||||
fn get_coordinates(&self) -> EmbedderCoordinates {
|
fn get_coordinates(&self) -> EmbedderCoordinates {
|
||||||
let window_size = winit_size_to_euclid_size(self.winit_window.outer_size()).to_i32();
|
let window_size = winit_size_to_euclid_size(self.winit_window.outer_size()).to_i32();
|
||||||
|
|
|
@ -36,6 +36,7 @@ cfg_if::cfg_if! {
|
||||||
mod egui_glue;
|
mod egui_glue;
|
||||||
mod embedder;
|
mod embedder;
|
||||||
mod events_loop;
|
mod events_loop;
|
||||||
|
mod geometry;
|
||||||
mod headed_window;
|
mod headed_window;
|
||||||
mod headless_window;
|
mod headless_window;
|
||||||
mod keyutils;
|
mod keyutils;
|
||||||
|
|
|
@ -3,11 +3,16 @@
|
||||||
* 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::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use egui::{Key, Modifiers, TopBottomPanel};
|
use egui::{CentralPanel, Frame, InnerResponse, Key, Modifiers, PaintCallback, TopBottomPanel};
|
||||||
use euclid::Length;
|
use egui_glow::CallbackFn;
|
||||||
|
use egui_winit::EventResponse;
|
||||||
|
use euclid::{Length, Point2D, Scale};
|
||||||
|
use gleam::gl;
|
||||||
|
use glow::NativeFramebuffer;
|
||||||
use log::{trace, warn};
|
use log::{trace, warn};
|
||||||
use servo::compositing::windowing::EmbedderEvent;
|
use servo::compositing::windowing::EmbedderEvent;
|
||||||
use servo::msg::constellation_msg::TraversalDirection;
|
use servo::msg::constellation_msg::TraversalDirection;
|
||||||
|
@ -18,14 +23,21 @@ use servo::webrender_surfman::WebrenderSurfman;
|
||||||
use crate::browser::Browser;
|
use crate::browser::Browser;
|
||||||
use crate::egui_glue::EguiGlow;
|
use crate::egui_glue::EguiGlow;
|
||||||
use crate::events_loop::EventsLoop;
|
use crate::events_loop::EventsLoop;
|
||||||
|
use crate::geometry::winit_position_to_euclid_point;
|
||||||
use crate::parser::location_bar_input_to_url;
|
use crate::parser::location_bar_input_to_url;
|
||||||
use crate::window_trait::WindowPortsMethods;
|
use crate::window_trait::WindowPortsMethods;
|
||||||
|
|
||||||
pub struct Minibrowser {
|
pub struct Minibrowser {
|
||||||
pub context: EguiGlow,
|
pub context: EguiGlow,
|
||||||
pub event_queue: RefCell<Vec<MinibrowserEvent>>,
|
pub event_queue: RefCell<Vec<MinibrowserEvent>>,
|
||||||
pub toolbar_height: Cell<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>>,
|
||||||
location: RefCell<String>,
|
location: RefCell<String>,
|
||||||
|
|
||||||
/// Whether the location has been edited by the user without clicking Go.
|
/// Whether the location has been edited by the user without clicking Go.
|
||||||
|
@ -56,18 +68,62 @@ impl Minibrowser {
|
||||||
.egui_ctx
|
.egui_ctx
|
||||||
.set_pixels_per_point(window.hidpi_factor().get());
|
.set_pixels_per_point(window.hidpi_factor().get());
|
||||||
|
|
||||||
|
let widget_surface_fbo = match webrender_surfman.context_surface_info() {
|
||||||
|
Ok(Some(info)) => NonZeroU32::new(info.framebuffer_object).map(NativeFramebuffer),
|
||||||
|
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 {
|
||||||
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,
|
||||||
location: RefCell::new(initial_url.to_string()),
|
location: RefCell::new(initial_url.to_string()),
|
||||||
location_dirty: false.into(),
|
location_dirty: false.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Preprocess the given [winit::event::WindowEvent], returning unconsumed for mouse events in
|
||||||
|
/// the Servo browser rect. This is needed because the CentralPanel we create for our webview
|
||||||
|
/// would otherwise make egui report events in that area as consumed.
|
||||||
|
pub fn on_event(&mut self, event: &winit::event::WindowEvent<'_>) -> EventResponse {
|
||||||
|
let mut result = self.context.on_event(event);
|
||||||
|
result.consumed &= match event {
|
||||||
|
winit::event::WindowEvent::CursorMoved { position, .. } => {
|
||||||
|
let scale = Scale::<_, DeviceIndependentPixel, _>::new(
|
||||||
|
self.context.egui_ctx.pixels_per_point(),
|
||||||
|
);
|
||||||
|
self.last_mouse_position =
|
||||||
|
Some(winit_position_to_euclid_point(*position).to_f32() / scale);
|
||||||
|
self.last_mouse_position
|
||||||
|
.map_or(false, |p| self.is_in_browser_rect(p))
|
||||||
|
},
|
||||||
|
winit::event::WindowEvent::MouseWheel { .. } |
|
||||||
|
winit::event::WindowEvent::MouseInput { .. } => self
|
||||||
|
.last_mouse_position
|
||||||
|
.map_or(false, |p| self.is_in_browser_rect(p)),
|
||||||
|
_ => true,
|
||||||
|
};
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return true iff the given position is in the Servo browser rect.
|
||||||
|
fn is_in_browser_rect(&self, position: Point2D<f32, DeviceIndependentPixel>) -> bool {
|
||||||
|
position.y < self.toolbar_height.get()
|
||||||
|
}
|
||||||
|
|
||||||
/// Update the minibrowser, but don’t paint.
|
/// Update the minibrowser, but don’t paint.
|
||||||
pub fn update(&mut self, window: &winit::window::Window, reason: &'static str) {
|
/// If `servo_framebuffer_id` is given, set up a paint callback to blit its contents to our
|
||||||
|
/// CentralPanel when [`Minibrowser::paint`] is called.
|
||||||
|
pub fn update(
|
||||||
|
&mut self,
|
||||||
|
window: &winit::window::Window,
|
||||||
|
servo_framebuffer_id: Option<gl::GLuint>,
|
||||||
|
reason: &'static str,
|
||||||
|
) {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
trace!(
|
trace!(
|
||||||
"{:?} since last update ({})",
|
"{:?} since last update ({})",
|
||||||
|
@ -78,62 +134,125 @@ impl Minibrowser {
|
||||||
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 _duration = context.run(window, |ctx| {
|
let _duration = context.run(window, |ctx| {
|
||||||
TopBottomPanel::top("toolbar").show(ctx, |ui| {
|
let InnerResponse { inner: height, .. } =
|
||||||
ui.allocate_ui_with_layout(
|
TopBottomPanel::top("toolbar").show(ctx, |ui| {
|
||||||
ui.available_size(),
|
ui.allocate_ui_with_layout(
|
||||||
egui::Layout::left_to_right(egui::Align::Center),
|
ui.available_size(),
|
||||||
|ui| {
|
egui::Layout::left_to_right(egui::Align::Center),
|
||||||
if ui.button("back").clicked() {
|
|ui| {
|
||||||
event_queue.borrow_mut().push(MinibrowserEvent::Back);
|
if ui.button("back").clicked() {
|
||||||
}
|
event_queue.borrow_mut().push(MinibrowserEvent::Back);
|
||||||
if ui.button("forward").clicked() {
|
}
|
||||||
event_queue.borrow_mut().push(MinibrowserEvent::Forward);
|
if ui.button("forward").clicked() {
|
||||||
}
|
event_queue.borrow_mut().push(MinibrowserEvent::Forward);
|
||||||
|
}
|
||||||
|
ui.allocate_ui_with_layout(
|
||||||
|
ui.available_size(),
|
||||||
|
egui::Layout::right_to_left(egui::Align::Center),
|
||||||
|
|ui| {
|
||||||
|
if ui.button("go").clicked() {
|
||||||
|
event_queue.borrow_mut().push(MinibrowserEvent::Go);
|
||||||
|
location_dirty.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
ui.allocate_ui_with_layout(
|
let location_field = ui.add_sized(
|
||||||
ui.available_size(),
|
ui.available_size(),
|
||||||
egui::Layout::right_to_left(egui::Align::Center),
|
egui::TextEdit::singleline(&mut *location.borrow_mut()),
|
||||||
|ui| {
|
);
|
||||||
if ui.button("go").clicked() {
|
|
||||||
event_queue.borrow_mut().push(MinibrowserEvent::Go);
|
|
||||||
location_dirty.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
let location_field = ui.add_sized(
|
if location_field.changed() {
|
||||||
ui.available_size(),
|
location_dirty.set(true);
|
||||||
egui::TextEdit::singleline(&mut *location.borrow_mut()),
|
}
|
||||||
|
if ui.input(|i| {
|
||||||
|
i.clone().consume_key(Modifiers::COMMAND, Key::L)
|
||||||
|
}) {
|
||||||
|
location_field.request_focus();
|
||||||
|
}
|
||||||
|
if location_field.lost_focus() &&
|
||||||
|
ui.input(|i| i.clone().key_pressed(Key::Enter))
|
||||||
|
{
|
||||||
|
event_queue.borrow_mut().push(MinibrowserEvent::Go);
|
||||||
|
location_dirty.set(false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
ui.cursor().min.y
|
||||||
|
});
|
||||||
|
*toolbar_height = Length::new(height);
|
||||||
|
|
||||||
|
CentralPanel::default()
|
||||||
|
.frame(Frame::none())
|
||||||
|
.show(ctx, |ui| {
|
||||||
|
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 };
|
||||||
|
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,
|
||||||
);
|
);
|
||||||
|
painter.gl().bind_framebuffer(gl::FRAMEBUFFER, widget_fbo);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if location_field.changed() {
|
|
||||||
location_dirty.set(true);
|
|
||||||
}
|
|
||||||
if ui.input(|i| i.clone().consume_key(Modifiers::COMMAND, Key::L)) {
|
|
||||||
location_field.request_focus();
|
|
||||||
}
|
|
||||||
if location_field.lost_focus() &&
|
|
||||||
ui.input(|i| i.clone().key_pressed(Key::Enter))
|
|
||||||
{
|
|
||||||
event_queue.borrow_mut().push(MinibrowserEvent::Go);
|
|
||||||
location_dirty.set(false);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
toolbar_height.set(Length::new(ctx.used_rect().height()));
|
|
||||||
*last_update = now;
|
*last_update = now;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Paint the minibrowser, as of the last update.
|
/// Paint the minibrowser, as of the last update.
|
||||||
pub fn paint(&mut self, window: &winit::window::Window) {
|
pub fn paint(&mut self, window: &winit::window::Window) {
|
||||||
|
unsafe {
|
||||||
|
use glow::HasContext as _;
|
||||||
|
self.context
|
||||||
|
.painter
|
||||||
|
.gl()
|
||||||
|
.bind_framebuffer(gl::FRAMEBUFFER, self.widget_surface_fbo);
|
||||||
|
}
|
||||||
self.context.paint(window);
|
self.context.paint(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue