mirror of
https://github.com/servo/servo.git
synced 2025-07-22 23:03:42 +01:00
libservo: Expose an OffscreenRenderingContext
and use it for servoshell (#35465)
Create a new `RenderingContext` which is used to render to a `SurfmanRenderingContext`-related offscreen buffer. This allows having a temporary place to render Servo and then blitting the results to a subsection of the parent `RenderingContext`. The goal with this change is to remove the details of how servoshell renders from the `Compositor` and prepare for the compositor-per-WebView world. Co-authred-by: Ngo Iok Ui (Wu Yu Wei) <yuweiwu@pm.me> Signed-off-by: Martin Robinson <mrobinson@igalia.com> Co-authored-by: Mukilan Thiyagarajan <mukilan@igalia.com>
This commit is contained in:
parent
d466688526
commit
6dce329acc
15 changed files with 655 additions and 608 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -8594,6 +8594,8 @@ dependencies = [
|
||||||
"embedder_traits",
|
"embedder_traits",
|
||||||
"euclid",
|
"euclid",
|
||||||
"gleam",
|
"gleam",
|
||||||
|
"glow",
|
||||||
|
"image",
|
||||||
"ipc-channel",
|
"ipc-channel",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
|
|
|
@ -36,6 +36,7 @@ script_traits = { workspace = true }
|
||||||
servo_config = { path = "../config" }
|
servo_config = { path = "../config" }
|
||||||
servo_geometry = { path = "../geometry" }
|
servo_geometry = { path = "../geometry" }
|
||||||
style_traits = { workspace = true }
|
style_traits = { workspace = true }
|
||||||
|
surfman = { workspace = true }
|
||||||
tracing = { workspace = true, optional = true }
|
tracing = { workspace = true, optional = true }
|
||||||
webrender = { workspace = true }
|
webrender = { workspace = true }
|
||||||
webrender_api = { workspace = true }
|
webrender_api = { workspace = true }
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
use std::cell::OnceCell;
|
|
||||||
use std::collections::hash_set::Iter;
|
use std::collections::hash_set::Iter;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::env;
|
use std::env;
|
||||||
|
@ -24,7 +23,7 @@ use embedder_traits::{
|
||||||
Cursor, InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent,
|
Cursor, InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent,
|
||||||
TouchEvent, TouchEventAction, TouchId,
|
TouchEvent, TouchEventAction, TouchId,
|
||||||
};
|
};
|
||||||
use euclid::{Point2D, Rect, Scale, Transform3D, Vector2D};
|
use euclid::{Point2D, Rect, Scale, Size2D, Transform3D, Vector2D};
|
||||||
use fnv::{FnvHashMap, FnvHashSet};
|
use fnv::{FnvHashMap, FnvHashSet};
|
||||||
use image::{DynamicImage, ImageFormat};
|
use image::{DynamicImage, ImageFormat};
|
||||||
use ipc_channel::ipc::{self, IpcSharedMemory};
|
use ipc_channel::ipc::{self, IpcSharedMemory};
|
||||||
|
@ -37,7 +36,7 @@ use script_traits::{
|
||||||
AnimationState, AnimationTickType, ScriptThreadMessage, ScrollState, WindowSizeData,
|
AnimationState, AnimationTickType, ScriptThreadMessage, ScrollState, WindowSizeData,
|
||||||
WindowSizeType,
|
WindowSizeType,
|
||||||
};
|
};
|
||||||
use servo_geometry::{DeviceIndependentPixel, FramebufferUintLength};
|
use servo_geometry::DeviceIndependentPixel;
|
||||||
use style_traits::{CSSPixel, PinchZoomFactor};
|
use style_traits::{CSSPixel, PinchZoomFactor};
|
||||||
use webrender::{CaptureBits, RenderApi, Transaction};
|
use webrender::{CaptureBits, RenderApi, Transaction};
|
||||||
use webrender_api::units::{
|
use webrender_api::units::{
|
||||||
|
@ -57,11 +56,10 @@ use webrender_traits::{
|
||||||
CompositorHitTestResult, CrossProcessCompositorMessage, ImageUpdate, UntrustedNodeAddress,
|
CompositorHitTestResult, CrossProcessCompositorMessage, ImageUpdate, UntrustedNodeAddress,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::gl::RenderTargetInfo;
|
|
||||||
use crate::touch::{TouchAction, TouchHandler};
|
use crate::touch::{TouchAction, TouchHandler};
|
||||||
use crate::webview::{UnknownWebView, WebView, WebViewAlreadyExists, WebViewManager};
|
use crate::webview::{UnknownWebView, WebView, WebViewAlreadyExists, WebViewManager};
|
||||||
use crate::windowing::{self, EmbedderCoordinates, WebRenderDebugOption, WindowMethods};
|
use crate::windowing::{self, EmbedderCoordinates, WebRenderDebugOption, WindowMethods};
|
||||||
use crate::{gl, InitialCompositorState};
|
use crate::InitialCompositorState;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
enum UnableToComposite {
|
enum UnableToComposite {
|
||||||
|
@ -191,19 +189,6 @@ pub struct IOCompositor {
|
||||||
/// Current cursor position.
|
/// Current cursor position.
|
||||||
cursor_pos: DevicePoint,
|
cursor_pos: DevicePoint,
|
||||||
|
|
||||||
/// Offscreen framebuffer object to render our next frame to.
|
|
||||||
/// We use this and `prev_offscreen_framebuffer` for double buffering when compositing to
|
|
||||||
/// [`CompositeTarget::OffscreenFbo`].
|
|
||||||
next_offscreen_framebuffer: OnceCell<gl::RenderTargetInfo>,
|
|
||||||
|
|
||||||
/// Offscreen framebuffer object for our most-recently-completed frame.
|
|
||||||
/// We use this and `next_offscreen_framebuffer` for double buffering when compositing to
|
|
||||||
/// [`CompositeTarget::OffscreenFbo`].
|
|
||||||
prev_offscreen_framebuffer: Option<gl::RenderTargetInfo>,
|
|
||||||
|
|
||||||
/// Whether to invalidate `prev_offscreen_framebuffer` at the end of the next frame.
|
|
||||||
invalidate_prev_offscreen_framebuffer: bool,
|
|
||||||
|
|
||||||
/// True to exit after page load ('-x').
|
/// True to exit after page load ('-x').
|
||||||
exit_after_load: bool,
|
exit_after_load: bool,
|
||||||
|
|
||||||
|
@ -348,10 +333,6 @@ pub enum CompositeTarget {
|
||||||
/// to [`RenderingContext::framebuffer_object`]
|
/// to [`RenderingContext::framebuffer_object`]
|
||||||
ContextFbo,
|
ContextFbo,
|
||||||
|
|
||||||
/// Draw to an offscreen OpenGL framebuffer object, which can be retrieved once complete at
|
|
||||||
/// [`IOCompositor::offscreen_framebuffer_id`].
|
|
||||||
OffscreenFbo,
|
|
||||||
|
|
||||||
/// Draw to an uncompressed image in shared memory.
|
/// Draw to an uncompressed image in shared memory.
|
||||||
SharedMemory,
|
SharedMemory,
|
||||||
|
|
||||||
|
@ -399,9 +380,6 @@ impl IOCompositor {
|
||||||
pending_paint_metrics: HashMap::new(),
|
pending_paint_metrics: HashMap::new(),
|
||||||
cursor: Cursor::None,
|
cursor: Cursor::None,
|
||||||
cursor_pos: DevicePoint::new(0.0, 0.0),
|
cursor_pos: DevicePoint::new(0.0, 0.0),
|
||||||
next_offscreen_framebuffer: OnceCell::new(),
|
|
||||||
prev_offscreen_framebuffer: None,
|
|
||||||
invalidate_prev_offscreen_framebuffer: false,
|
|
||||||
exit_after_load,
|
exit_after_load,
|
||||||
convert_mouse_to_touch,
|
convert_mouse_to_touch,
|
||||||
pending_frames: 0,
|
pending_frames: 0,
|
||||||
|
@ -1302,13 +1280,6 @@ impl IOCompositor {
|
||||||
let old_coords = self.embedder_coordinates;
|
let old_coords = self.embedder_coordinates;
|
||||||
self.embedder_coordinates = self.window.get_coordinates();
|
self.embedder_coordinates = self.window.get_coordinates();
|
||||||
|
|
||||||
// If the framebuffer size has changed, invalidate the current framebuffer object, and mark
|
|
||||||
// the last framebuffer object as needing to be invalidated at the end of the next frame.
|
|
||||||
if self.embedder_coordinates.framebuffer != old_coords.framebuffer {
|
|
||||||
self.next_offscreen_framebuffer = OnceCell::new();
|
|
||||||
self.invalidate_prev_offscreen_framebuffer = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.embedder_coordinates.viewport != old_coords.viewport {
|
if self.embedder_coordinates.viewport != old_coords.viewport {
|
||||||
let mut transaction = Transaction::new();
|
let mut transaction = Transaction::new();
|
||||||
let size = self.embedder_coordinates.get_viewport();
|
let size = self.embedder_coordinates.get_viewport();
|
||||||
|
@ -1957,13 +1928,6 @@ impl IOCompositor {
|
||||||
target: CompositeTarget,
|
target: CompositeTarget,
|
||||||
page_rect: Option<Rect<f32, CSSPixel>>,
|
page_rect: Option<Rect<f32, CSSPixel>>,
|
||||||
) -> Result<Option<Image>, UnableToComposite> {
|
) -> Result<Option<Image>, UnableToComposite> {
|
||||||
if !self.webviews_waiting_on_present.is_empty() {
|
|
||||||
debug!("tried to composite while waiting on present");
|
|
||||||
return Err(UnableToComposite::NotReadyToPaintImage(
|
|
||||||
NotReadyToPaint::WaitingOnConstellation,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let size = self.embedder_coordinates.framebuffer.to_u32();
|
let size = self.embedder_coordinates.framebuffer.to_u32();
|
||||||
if let Err(err) = self.rendering_context.make_current() {
|
if let Err(err) = self.rendering_context.make_current() {
|
||||||
warn!("Failed to make the rendering context current: {:?}", err);
|
warn!("Failed to make the rendering context current: {:?}", err);
|
||||||
|
@ -1978,12 +1942,6 @@ impl IOCompositor {
|
||||||
target,
|
target,
|
||||||
CompositeTarget::SharedMemory | CompositeTarget::PngFile(_)
|
CompositeTarget::SharedMemory | CompositeTarget::PngFile(_)
|
||||||
) || self.exit_after_load;
|
) || self.exit_after_load;
|
||||||
let use_offscreen_framebuffer = matches!(
|
|
||||||
target,
|
|
||||||
CompositeTarget::SharedMemory |
|
|
||||||
CompositeTarget::PngFile(_) |
|
|
||||||
CompositeTarget::OffscreenFbo
|
|
||||||
);
|
|
||||||
|
|
||||||
if wait_for_stable_image {
|
if wait_for_stable_image {
|
||||||
// The current image may be ready to output. However, if there are animations active,
|
// The current image may be ready to output. However, if there are animations active,
|
||||||
|
@ -2000,23 +1958,7 @@ impl IOCompositor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if use_offscreen_framebuffer {
|
self.rendering_context.prepare_for_rendering();
|
||||||
self.next_offscreen_framebuffer
|
|
||||||
.get_or_init(|| {
|
|
||||||
RenderTargetInfo::new(
|
|
||||||
self.webrender_gl.clone(),
|
|
||||||
FramebufferUintLength::new(size.width),
|
|
||||||
FramebufferUintLength::new(size.height),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.bind();
|
|
||||||
} else {
|
|
||||||
// Bind the webrender framebuffer
|
|
||||||
let framebuffer_object = self.rendering_context.framebuffer_object();
|
|
||||||
self.webrender_gl
|
|
||||||
.bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object);
|
|
||||||
self.assert_gl_framebuffer_complete();
|
|
||||||
}
|
|
||||||
|
|
||||||
time_profile!(
|
time_profile!(
|
||||||
ProfilerCategory::Compositing,
|
ProfilerCategory::Compositing,
|
||||||
|
@ -2056,44 +1998,20 @@ impl IOCompositor {
|
||||||
|
|
||||||
let rv = match target {
|
let rv = match target {
|
||||||
CompositeTarget::ContextFbo => None,
|
CompositeTarget::ContextFbo => None,
|
||||||
CompositeTarget::OffscreenFbo => {
|
CompositeTarget::SharedMemory => self
|
||||||
self.next_offscreen_framebuffer
|
.rendering_context
|
||||||
.get()
|
.read_to_image(Rect::new(
|
||||||
.expect("Guaranteed by needs_fbo")
|
Point2D::new(x as u32, y as u32),
|
||||||
.unbind();
|
Size2D::new(width, height),
|
||||||
if self.invalidate_prev_offscreen_framebuffer {
|
))
|
||||||
// Do not reuse the last render target as the new current render target.
|
.map(|image| Image {
|
||||||
self.prev_offscreen_framebuffer = None;
|
width: image.width(),
|
||||||
self.invalidate_prev_offscreen_framebuffer = false;
|
height: image.height(),
|
||||||
}
|
|
||||||
let old_prev = self.prev_offscreen_framebuffer.take();
|
|
||||||
self.prev_offscreen_framebuffer = self.next_offscreen_framebuffer.take();
|
|
||||||
if let Some(old_prev) = old_prev {
|
|
||||||
let result = self.next_offscreen_framebuffer.set(old_prev);
|
|
||||||
debug_assert!(result.is_ok(), "Guaranteed by take");
|
|
||||||
}
|
|
||||||
None
|
|
||||||
},
|
|
||||||
CompositeTarget::SharedMemory => {
|
|
||||||
let render_target_info = self
|
|
||||||
.next_offscreen_framebuffer
|
|
||||||
.take()
|
|
||||||
.expect("Guaranteed by needs_fbo");
|
|
||||||
let img = render_target_info.read_back_from_gpu(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
FramebufferUintLength::new(width),
|
|
||||||
FramebufferUintLength::new(height),
|
|
||||||
);
|
|
||||||
Some(Image {
|
|
||||||
width: img.width(),
|
|
||||||
height: img.height(),
|
|
||||||
format: PixelFormat::RGBA8,
|
format: PixelFormat::RGBA8,
|
||||||
bytes: ipc::IpcSharedMemory::from_bytes(&img),
|
bytes: ipc::IpcSharedMemory::from_bytes(&image),
|
||||||
id: None,
|
id: None,
|
||||||
cors_status: CorsStatus::Safe,
|
cors_status: CorsStatus::Safe,
|
||||||
})
|
}),
|
||||||
},
|
|
||||||
CompositeTarget::PngFile(path) => {
|
CompositeTarget::PngFile(path) => {
|
||||||
time_profile!(
|
time_profile!(
|
||||||
ProfilerCategory::ImageSaving,
|
ProfilerCategory::ImageSaving,
|
||||||
|
@ -2101,19 +2019,15 @@ impl IOCompositor {
|
||||||
self.time_profiler_chan.clone(),
|
self.time_profiler_chan.clone(),
|
||||||
|| match File::create(&*path) {
|
|| match File::create(&*path) {
|
||||||
Ok(mut file) => {
|
Ok(mut file) => {
|
||||||
let render_target_info = self
|
if let Some(image) = self.rendering_context.read_to_image(Rect::new(
|
||||||
.next_offscreen_framebuffer
|
Point2D::new(x as u32, y as u32),
|
||||||
.take()
|
Size2D::new(width, height),
|
||||||
.expect("Guaranteed by needs_fbo");
|
)) {
|
||||||
let img = render_target_info.read_back_from_gpu(
|
let dynamic_image = DynamicImage::ImageRgba8(image);
|
||||||
x,
|
if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png)
|
||||||
y,
|
{
|
||||||
FramebufferUintLength::new(width),
|
error!("Failed to save {} ({}).", path, e);
|
||||||
FramebufferUintLength::new(height),
|
}
|
||||||
);
|
|
||||||
let dynamic_image = DynamicImage::ImageRgba8(img);
|
|
||||||
if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png) {
|
|
||||||
error!("Failed to save {} ({}).", path, e);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => error!("Failed to create {} ({}).", path, e),
|
Err(e) => error!("Failed to create {} ({}).", path, e),
|
||||||
|
@ -2205,14 +2119,6 @@ impl IOCompositor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to
|
|
||||||
/// [`CompositeTarget::OffscreenFbo`], or None otherwise.
|
|
||||||
pub fn offscreen_framebuffer_id(&self) -> Option<gleam::gl::GLuint> {
|
|
||||||
self.prev_offscreen_framebuffer
|
|
||||||
.as_ref()
|
|
||||||
.map(|info| info.framebuffer_id())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg_attr(
|
#[cfg_attr(
|
||||||
feature = "tracing",
|
feature = "tracing",
|
||||||
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace")
|
||||||
|
|
|
@ -1,208 +0,0 @@
|
||||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gleam::gl::{self, Gl};
|
|
||||||
use image::RgbaImage;
|
|
||||||
use log::{trace, warn};
|
|
||||||
use servo_geometry::FramebufferUintLength;
|
|
||||||
|
|
||||||
pub struct RenderTargetInfo {
|
|
||||||
gl: Rc<dyn Gl>,
|
|
||||||
framebuffer_ids: Vec<gl::GLuint>,
|
|
||||||
renderbuffer_ids: Vec<gl::GLuint>,
|
|
||||||
texture_ids: Vec<gl::GLuint>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderTargetInfo {
|
|
||||||
pub fn new(
|
|
||||||
gl: Rc<dyn Gl>,
|
|
||||||
width: FramebufferUintLength,
|
|
||||||
height: FramebufferUintLength,
|
|
||||||
) -> Self {
|
|
||||||
let framebuffer_ids = gl.gen_framebuffers(1);
|
|
||||||
gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]);
|
|
||||||
trace!("Configuring fbo {}", framebuffer_ids[0]);
|
|
||||||
|
|
||||||
let texture_ids = gl.gen_textures(1);
|
|
||||||
gl.bind_texture(gl::TEXTURE_2D, texture_ids[0]);
|
|
||||||
gl.tex_image_2d(
|
|
||||||
gl::TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
gl::RGBA as gl::GLint,
|
|
||||||
width.get() as gl::GLsizei,
|
|
||||||
height.get() as gl::GLsizei,
|
|
||||||
0,
|
|
||||||
gl::RGBA,
|
|
||||||
gl::UNSIGNED_BYTE,
|
|
||||||
None,
|
|
||||||
);
|
|
||||||
gl.tex_parameter_i(
|
|
||||||
gl::TEXTURE_2D,
|
|
||||||
gl::TEXTURE_MAG_FILTER,
|
|
||||||
gl::NEAREST as gl::GLint,
|
|
||||||
);
|
|
||||||
gl.tex_parameter_i(
|
|
||||||
gl::TEXTURE_2D,
|
|
||||||
gl::TEXTURE_MIN_FILTER,
|
|
||||||
gl::NEAREST as gl::GLint,
|
|
||||||
);
|
|
||||||
|
|
||||||
gl.framebuffer_texture_2d(
|
|
||||||
gl::FRAMEBUFFER,
|
|
||||||
gl::COLOR_ATTACHMENT0,
|
|
||||||
gl::TEXTURE_2D,
|
|
||||||
texture_ids[0],
|
|
||||||
0,
|
|
||||||
);
|
|
||||||
|
|
||||||
gl.bind_texture(gl::TEXTURE_2D, 0);
|
|
||||||
|
|
||||||
let renderbuffer_ids = gl.gen_renderbuffers(1);
|
|
||||||
let depth_rb = renderbuffer_ids[0];
|
|
||||||
gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
|
|
||||||
gl.renderbuffer_storage(
|
|
||||||
gl::RENDERBUFFER,
|
|
||||||
gl::DEPTH_COMPONENT24,
|
|
||||||
width.get() as gl::GLsizei,
|
|
||||||
height.get() as gl::GLsizei,
|
|
||||||
);
|
|
||||||
gl.framebuffer_renderbuffer(
|
|
||||||
gl::FRAMEBUFFER,
|
|
||||||
gl::DEPTH_ATTACHMENT,
|
|
||||||
gl::RENDERBUFFER,
|
|
||||||
depth_rb,
|
|
||||||
);
|
|
||||||
|
|
||||||
Self {
|
|
||||||
gl,
|
|
||||||
framebuffer_ids,
|
|
||||||
renderbuffer_ids,
|
|
||||||
texture_ids,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn framebuffer_id(&self) -> gl::GLuint {
|
|
||||||
*self.framebuffer_ids.first().expect("Guaranteed by new")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bind(&self) {
|
|
||||||
trace!("Binding FBO {}", self.framebuffer_id());
|
|
||||||
self.gl
|
|
||||||
.bind_framebuffer(gl::FRAMEBUFFER, self.framebuffer_id());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unbind(&self) {
|
|
||||||
trace!("Unbinding FBO {}", self.framebuffer_id());
|
|
||||||
self.gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn read_back_from_gpu(
|
|
||||||
self,
|
|
||||||
x: i32,
|
|
||||||
y: i32,
|
|
||||||
width: FramebufferUintLength,
|
|
||||||
height: FramebufferUintLength,
|
|
||||||
) -> RgbaImage {
|
|
||||||
let width = width.get() as usize;
|
|
||||||
let height = height.get() as usize;
|
|
||||||
// For some reason, OSMesa fails to render on the 3rd
|
|
||||||
// attempt in headless mode, under some conditions.
|
|
||||||
// I think this can only be some kind of synchronization
|
|
||||||
// bug in OSMesa, but explicitly un-binding any vertex
|
|
||||||
// array here seems to work around that bug.
|
|
||||||
// See https://github.com/servo/servo/issues/18606.
|
|
||||||
self.gl.bind_vertex_array(0);
|
|
||||||
|
|
||||||
let mut pixels = self.gl.read_pixels(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
width as gl::GLsizei,
|
|
||||||
height as gl::GLsizei,
|
|
||||||
gl::RGBA,
|
|
||||||
gl::UNSIGNED_BYTE,
|
|
||||||
);
|
|
||||||
let gl_error = self.gl.get_error();
|
|
||||||
if gl_error != gl::NO_ERROR {
|
|
||||||
warn!("GL error code 0x{gl_error:x} set after read_pixels");
|
|
||||||
}
|
|
||||||
|
|
||||||
// flip image vertically (texture is upside down)
|
|
||||||
let orig_pixels = pixels.clone();
|
|
||||||
let stride = width * 4;
|
|
||||||
for y in 0..height {
|
|
||||||
let dst_start = y * stride;
|
|
||||||
let src_start = (height - y - 1) * stride;
|
|
||||||
let src_slice = &orig_pixels[src_start..src_start + stride];
|
|
||||||
pixels[dst_start..dst_start + stride].clone_from_slice(&src_slice[..stride]);
|
|
||||||
}
|
|
||||||
|
|
||||||
RgbaImage::from_raw(width as u32, height as u32, pixels).expect("Flipping image failed!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for RenderTargetInfo {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
trace!("Dropping FBO {}", self.framebuffer_id());
|
|
||||||
self.unbind();
|
|
||||||
self.gl.delete_textures(&self.texture_ids);
|
|
||||||
self.gl.delete_renderbuffers(&self.renderbuffer_ids);
|
|
||||||
self.gl.delete_framebuffers(&self.framebuffer_ids);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use gleam::gl;
|
|
||||||
use image::Rgba;
|
|
||||||
use servo_geometry::FramebufferUintLength;
|
|
||||||
use surfman::{Connection, ContextAttributeFlags, ContextAttributes, Error, GLApi, GLVersion};
|
|
||||||
|
|
||||||
use super::RenderTargetInfo;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[allow(unsafe_code)]
|
|
||||||
fn test_read_pixels() -> Result<(), Error> {
|
|
||||||
let connection = Connection::new()?;
|
|
||||||
let adapter = connection.create_software_adapter()?;
|
|
||||||
let mut device = connection.create_device(&adapter)?;
|
|
||||||
let context_descriptor = device.create_context_descriptor(&ContextAttributes {
|
|
||||||
version: GLVersion::new(3, 0),
|
|
||||||
flags: ContextAttributeFlags::empty(),
|
|
||||||
})?;
|
|
||||||
let mut context = device.create_context(&context_descriptor, None)?;
|
|
||||||
|
|
||||||
let gl = match connection.gl_api() {
|
|
||||||
GLApi::GL => unsafe { gl::GlFns::load_with(|s| device.get_proc_address(&context, s)) },
|
|
||||||
GLApi::GLES => unsafe {
|
|
||||||
gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
device.make_context_current(&context)?;
|
|
||||||
|
|
||||||
{
|
|
||||||
const WIDTH: FramebufferUintLength = FramebufferUintLength::new(16);
|
|
||||||
const HEIGHT: FramebufferUintLength = FramebufferUintLength::new(16);
|
|
||||||
let render_target = RenderTargetInfo::new(gl, WIDTH, HEIGHT);
|
|
||||||
render_target.bind();
|
|
||||||
render_target
|
|
||||||
.gl
|
|
||||||
.clear_color(12.0 / 255.0, 34.0 / 255.0, 56.0 / 255.0, 78.0 / 255.0);
|
|
||||||
render_target.gl.clear(gl::COLOR_BUFFER_BIT);
|
|
||||||
|
|
||||||
let img = render_target.read_back_from_gpu(0, 0, WIDTH, HEIGHT);
|
|
||||||
assert_eq!(img.width(), WIDTH.get());
|
|
||||||
assert_eq!(img.height(), HEIGHT.get());
|
|
||||||
|
|
||||||
let expected_pixel: Rgba<u8> = Rgba([12, 34, 56, 78]);
|
|
||||||
assert!(img.pixels().all(|&p| p == expected_pixel));
|
|
||||||
}
|
|
||||||
|
|
||||||
device.destroy_context(&mut context)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -19,7 +19,6 @@ pub use crate::compositor::{CompositeTarget, IOCompositor, ShutdownState};
|
||||||
mod tracing;
|
mod tracing;
|
||||||
|
|
||||||
mod compositor;
|
mod compositor;
|
||||||
mod gl;
|
|
||||||
mod touch;
|
mod touch;
|
||||||
pub mod webview;
|
pub mod webview;
|
||||||
pub mod windowing;
|
pub mod windowing;
|
||||||
|
|
|
@ -305,10 +305,6 @@ impl Servo {
|
||||||
}
|
}
|
||||||
debug_assert_eq!(webrender_gl.get_error(), gleam::gl::NO_ERROR,);
|
debug_assert_eq!(webrender_gl.get_error(), gleam::gl::NO_ERROR,);
|
||||||
|
|
||||||
// Bind the webrender framebuffer
|
|
||||||
let framebuffer_object = rendering_context.framebuffer_object();
|
|
||||||
webrender_gl.bind_framebuffer(gleam::gl::FRAMEBUFFER, framebuffer_object);
|
|
||||||
|
|
||||||
// Reserving a namespace to create TopLevelBrowsingContextId.
|
// Reserving a namespace to create TopLevelBrowsingContextId.
|
||||||
PipelineNamespace::install(PipelineNamespaceId(0));
|
PipelineNamespace::install(PipelineNamespaceId(0));
|
||||||
|
|
||||||
|
@ -346,6 +342,7 @@ impl Servo {
|
||||||
opts.debug.webrender_stats,
|
opts.debug.webrender_stats,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
rendering_context.prepare_for_rendering();
|
||||||
let render_notifier = Box::new(RenderNotifier::new(compositor_proxy.clone()));
|
let render_notifier = Box::new(RenderNotifier::new(compositor_proxy.clone()));
|
||||||
let clear_color = servo_config::pref!(shell_background_color_rgba);
|
let clear_color = servo_config::pref!(shell_background_color_rgba);
|
||||||
let clear_color = ColorF::new(
|
let clear_color = ColorF::new(
|
||||||
|
@ -354,6 +351,7 @@ impl Servo {
|
||||||
clear_color[2] as f32,
|
clear_color[2] as f32,
|
||||||
clear_color[3] as f32,
|
clear_color[3] as f32,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Use same texture upload method as Gecko with ANGLE:
|
// Use same texture upload method as Gecko with ANGLE:
|
||||||
// https://searchfox.org/mozilla-central/source/gfx/webrender_bindings/src/bindings.rs#1215-1219
|
// https://searchfox.org/mozilla-central/source/gfx/webrender_bindings/src/bindings.rs#1215-1219
|
||||||
let upload_method = if webrender_gl.get_string(RENDERER).starts_with("ANGLE") {
|
let upload_method = if webrender_gl.get_string(RENDERER).starts_with("ANGLE") {
|
||||||
|
@ -702,12 +700,6 @@ impl Servo {
|
||||||
self.compositor.borrow_mut().present();
|
self.compositor.borrow_mut().present();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the OpenGL framebuffer name of the most-recently-completed frame when compositing to
|
|
||||||
/// [`CompositeTarget::OffscreenFbo`], or None otherwise.
|
|
||||||
pub fn offscreen_framebuffer_id(&self) -> Option<u32> {
|
|
||||||
self.compositor.borrow().offscreen_framebuffer_id()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_webview(&self, url: url::Url) -> WebView {
|
pub fn new_webview(&self, url: url::Url) -> WebView {
|
||||||
let webview = WebView::new(&self.constellation_proxy, self.compositor.clone());
|
let webview = WebView::new(&self.constellation_proxy, self.compositor.clone());
|
||||||
self.webviews
|
self.webviews
|
||||||
|
|
|
@ -10,6 +10,7 @@ rust-version.workspace = true
|
||||||
[lib]
|
[lib]
|
||||||
name = "webrender_traits"
|
name = "webrender_traits"
|
||||||
path = "lib.rs"
|
path = "lib.rs"
|
||||||
|
test = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
no-wgl = ["surfman/sm-angle-default"]
|
no-wgl = ["surfman/sm-angle-default"]
|
||||||
|
@ -18,10 +19,12 @@ no-wgl = ["surfman/sm-angle-default"]
|
||||||
base = { workspace = true }
|
base = { workspace = true }
|
||||||
embedder_traits = { workspace = true }
|
embedder_traits = { workspace = true }
|
||||||
euclid = { workspace = true }
|
euclid = { workspace = true }
|
||||||
|
image = { workspace = true }
|
||||||
ipc-channel = { workspace = true }
|
ipc-channel = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
libc = { workspace = true }
|
libc = { workspace = true }
|
||||||
gleam = { workspace = true }
|
gleam = { workspace = true }
|
||||||
|
glow = { workspace = true }
|
||||||
webrender_api = { workspace = true }
|
webrender_api = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
servo_geometry = { path = "../../geometry" }
|
servo_geometry = { path = "../../geometry" }
|
||||||
|
|
|
@ -4,13 +4,17 @@
|
||||||
|
|
||||||
#![deny(unsafe_code)]
|
#![deny(unsafe_code)]
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::{Cell, RefCell};
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
|
use std::num::NonZeroU32;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use euclid::default::Size2D;
|
use euclid::default::{Rect, Size2D};
|
||||||
use gleam::gl;
|
use euclid::Point2D;
|
||||||
use log::{debug, warn};
|
use gleam::gl::{self, Gl};
|
||||||
|
use glow::NativeFramebuffer;
|
||||||
|
use image::RgbaImage;
|
||||||
|
use log::{debug, trace, warn};
|
||||||
use servo_media::player::context::{GlContext, NativeDisplay};
|
use servo_media::player::context::{GlContext, NativeDisplay};
|
||||||
use surfman::chains::{PreserveBuffer, SwapChain};
|
use surfman::chains::{PreserveBuffer, SwapChain};
|
||||||
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
|
#[cfg(all(target_os = "linux", not(target_env = "ohos")))]
|
||||||
|
@ -36,16 +40,26 @@ pub enum GLVersion {
|
||||||
/// management, and destruction of the rendering context and its associated
|
/// management, and destruction of the rendering context and its associated
|
||||||
/// resources.
|
/// resources.
|
||||||
pub trait RenderingContext {
|
pub trait RenderingContext {
|
||||||
|
/// Prepare this [`RenderingContext`] to be rendered upon by Servo. For instance,
|
||||||
|
/// by binding a framebuffer to the current OpenGL context.
|
||||||
|
fn prepare_for_rendering(&self) {}
|
||||||
|
/// Read the contents of this [`Renderingcontext`] into an in-memory image. If the
|
||||||
|
/// image cannot be read (for instance, if no rendering has taken place yet), then
|
||||||
|
/// `None` is returned.
|
||||||
|
///
|
||||||
|
/// In a double-buffered [`RenderingContext`] this is expected to read from the back
|
||||||
|
/// buffer. That means that once Servo renders to the context, this should return those
|
||||||
|
/// results, even before [`RenderingContext::present`] is called.
|
||||||
|
fn read_to_image(&self, source_rectangle: Rect<u32>) -> Option<RgbaImage>;
|
||||||
/// Resizes the rendering surface to the given size.
|
/// Resizes the rendering surface to the given size.
|
||||||
fn resize(&self, size: Size2D<i32>);
|
fn resize(&self, size: Size2D<i32>);
|
||||||
/// Presents the rendered frame to the screen.
|
/// Presents the rendered frame to the screen. In a double-buffered context, this would
|
||||||
|
/// swap buffers.
|
||||||
fn present(&self);
|
fn present(&self);
|
||||||
/// Makes the context the current OpenGL context for this thread.
|
/// Makes the context the current OpenGL context for this thread.
|
||||||
/// After calling this function, it is valid to use OpenGL rendering
|
/// After calling this function, it is valid to use OpenGL rendering
|
||||||
/// commands.
|
/// commands.
|
||||||
fn make_current(&self) -> Result<(), Error>;
|
fn make_current(&self) -> Result<(), Error>;
|
||||||
/// Returns the OpenGL framebuffer object needed to render to the surface.
|
|
||||||
fn framebuffer_object(&self) -> u32;
|
|
||||||
/// Returns the OpenGL or GLES API.
|
/// Returns the OpenGL or GLES API.
|
||||||
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl>;
|
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl>;
|
||||||
/// Describes the OpenGL version that is requested when a context is created.
|
/// Describes the OpenGL version that is requested when a context is created.
|
||||||
|
@ -86,6 +100,7 @@ pub trait RenderingContext {
|
||||||
pub struct SurfmanRenderingContext(Rc<RenderingContextData>);
|
pub struct SurfmanRenderingContext(Rc<RenderingContextData>);
|
||||||
|
|
||||||
struct RenderingContextData {
|
struct RenderingContextData {
|
||||||
|
gl: Rc<dyn Gl>,
|
||||||
device: RefCell<Device>,
|
device: RefCell<Device>,
|
||||||
context: RefCell<Context>,
|
context: RefCell<Context>,
|
||||||
// We either render to a swap buffer or to a native widget
|
// We either render to a swap buffer or to a native widget
|
||||||
|
@ -164,6 +179,23 @@ impl RenderingContext for SurfmanRenderingContext {
|
||||||
NativeDisplay::Unknown
|
NativeDisplay::Unknown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn prepare_for_rendering(&self) {
|
||||||
|
self.0.gl.bind_framebuffer(
|
||||||
|
gleam::gl::FRAMEBUFFER,
|
||||||
|
self.framebuffer()
|
||||||
|
.map_or(0, |framebuffer| framebuffer.0.into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_to_image(&self, source_rectangle: Rect<u32>) -> Option<RgbaImage> {
|
||||||
|
let framebuffer_id = self
|
||||||
|
.framebuffer()
|
||||||
|
.map(|framebuffer| framebuffer.0.into())
|
||||||
|
.unwrap_or(0);
|
||||||
|
Framebuffer::read_framebuffer_to_image(self.gl_api(), framebuffer_id, source_rectangle)
|
||||||
|
}
|
||||||
|
|
||||||
fn resize(&self, size: Size2D<i32>) {
|
fn resize(&self, size: Size2D<i32>) {
|
||||||
if let Err(err) = self.resize(size) {
|
if let Err(err) = self.resize(size) {
|
||||||
warn!("Failed to resize surface: {:?}", err);
|
warn!("Failed to resize surface: {:?}", err);
|
||||||
|
@ -177,23 +209,9 @@ impl RenderingContext for SurfmanRenderingContext {
|
||||||
fn make_current(&self) -> Result<(), Error> {
|
fn make_current(&self) -> Result<(), Error> {
|
||||||
self.make_gl_context_current()
|
self.make_gl_context_current()
|
||||||
}
|
}
|
||||||
fn framebuffer_object(&self) -> u32 {
|
|
||||||
self.context_surface_info()
|
|
||||||
.unwrap_or(None)
|
|
||||||
.and_then(|info| info.framebuffer_object)
|
|
||||||
.map(|fbo| fbo.0.get())
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
|
||||||
#[allow(unsafe_code)]
|
#[allow(unsafe_code)]
|
||||||
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
|
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
|
||||||
let context = self.0.context.borrow();
|
self.0.gl.clone()
|
||||||
let device = self.0.device.borrow();
|
|
||||||
match self.connection().gl_api() {
|
|
||||||
GLApi::GL => unsafe { gl::GlFns::load_with(|s| device.get_proc_address(&context, s)) },
|
|
||||||
GLApi::GLES => unsafe {
|
|
||||||
gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fn gl_version(&self) -> GLVersion {
|
fn gl_version(&self) -> GLVersion {
|
||||||
let device = self.0.device.borrow();
|
let device = self.0.device.borrow();
|
||||||
|
@ -270,9 +288,23 @@ impl SurfmanRenderingContext {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
let gl = {
|
||||||
|
match connection.gl_api() {
|
||||||
|
GLApi::GL => unsafe {
|
||||||
|
gl::GlFns::load_with(|s| device.get_proc_address(&context, s))
|
||||||
|
},
|
||||||
|
GLApi::GLES => unsafe {
|
||||||
|
gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let device = RefCell::new(device);
|
let device = RefCell::new(device);
|
||||||
let context = RefCell::new(context);
|
let context = RefCell::new(context);
|
||||||
let data = RenderingContextData {
|
let data = RenderingContextData {
|
||||||
|
gl,
|
||||||
device,
|
device,
|
||||||
context,
|
context,
|
||||||
swap_chain,
|
swap_chain,
|
||||||
|
@ -464,4 +496,367 @@ impl SurfmanRenderingContext {
|
||||||
device.make_context_current(&context)?;
|
device.make_context_current(&context)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn framebuffer(&self) -> Option<NativeFramebuffer> {
|
||||||
|
self.context_surface_info()
|
||||||
|
.unwrap_or(None)
|
||||||
|
.and_then(|info| info.framebuffer_object)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new offscreen context that is compatible with this [`SurfmanRenderingContext`].
|
||||||
|
/// The contents of the resulting [`OffscreenRenderingContext`] are guaranteed to be blit
|
||||||
|
/// compatible with the this context.
|
||||||
|
pub fn offscreen_context(&self, size: Size2D<u32>) -> OffscreenRenderingContext {
|
||||||
|
OffscreenRenderingContext::new(SurfmanRenderingContext(self.0.clone()), size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Framebuffer {
|
||||||
|
gl: Rc<dyn Gl>,
|
||||||
|
size: Size2D<u32>,
|
||||||
|
framebuffer_id: gl::GLuint,
|
||||||
|
renderbuffer_id: gl::GLuint,
|
||||||
|
texture_id: gl::GLuint,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Framebuffer {
|
||||||
|
fn bind(&self) {
|
||||||
|
trace!("Binding FBO {}", self.framebuffer_id);
|
||||||
|
self.gl
|
||||||
|
.bind_framebuffer(gl::FRAMEBUFFER, self.framebuffer_id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Framebuffer {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.gl.bind_framebuffer(gl::FRAMEBUFFER, 0);
|
||||||
|
self.gl.delete_textures(&[self.texture_id]);
|
||||||
|
self.gl.delete_renderbuffers(&[self.renderbuffer_id]);
|
||||||
|
self.gl.delete_framebuffers(&[self.framebuffer_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Framebuffer {
|
||||||
|
fn new(gl: Rc<dyn Gl>, size: Size2D<u32>) -> Self {
|
||||||
|
let framebuffer_ids = gl.gen_framebuffers(1);
|
||||||
|
gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_ids[0]);
|
||||||
|
|
||||||
|
let texture_ids = gl.gen_textures(1);
|
||||||
|
gl.bind_texture(gl::TEXTURE_2D, texture_ids[0]);
|
||||||
|
gl.tex_image_2d(
|
||||||
|
gl::TEXTURE_2D,
|
||||||
|
0,
|
||||||
|
gl::RGBA as gl::GLint,
|
||||||
|
size.width as gl::GLsizei,
|
||||||
|
size.height as gl::GLsizei,
|
||||||
|
0,
|
||||||
|
gl::RGBA,
|
||||||
|
gl::UNSIGNED_BYTE,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
gl.tex_parameter_i(
|
||||||
|
gl::TEXTURE_2D,
|
||||||
|
gl::TEXTURE_MAG_FILTER,
|
||||||
|
gl::NEAREST as gl::GLint,
|
||||||
|
);
|
||||||
|
gl.tex_parameter_i(
|
||||||
|
gl::TEXTURE_2D,
|
||||||
|
gl::TEXTURE_MIN_FILTER,
|
||||||
|
gl::NEAREST as gl::GLint,
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.framebuffer_texture_2d(
|
||||||
|
gl::FRAMEBUFFER,
|
||||||
|
gl::COLOR_ATTACHMENT0,
|
||||||
|
gl::TEXTURE_2D,
|
||||||
|
texture_ids[0],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
gl.bind_texture(gl::TEXTURE_2D, 0);
|
||||||
|
|
||||||
|
let renderbuffer_ids = gl.gen_renderbuffers(1);
|
||||||
|
let depth_rb = renderbuffer_ids[0];
|
||||||
|
gl.bind_renderbuffer(gl::RENDERBUFFER, depth_rb);
|
||||||
|
gl.renderbuffer_storage(
|
||||||
|
gl::RENDERBUFFER,
|
||||||
|
gl::DEPTH_COMPONENT24,
|
||||||
|
size.width as gl::GLsizei,
|
||||||
|
size.height as gl::GLsizei,
|
||||||
|
);
|
||||||
|
gl.framebuffer_renderbuffer(
|
||||||
|
gl::FRAMEBUFFER,
|
||||||
|
gl::DEPTH_ATTACHMENT,
|
||||||
|
gl::RENDERBUFFER,
|
||||||
|
depth_rb,
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
gl,
|
||||||
|
size,
|
||||||
|
framebuffer_id: *framebuffer_ids
|
||||||
|
.first()
|
||||||
|
.expect("Guaranteed by GL operations"),
|
||||||
|
renderbuffer_id: *renderbuffer_ids
|
||||||
|
.first()
|
||||||
|
.expect("Guaranteed by GL operations"),
|
||||||
|
texture_id: *texture_ids.first().expect("Guaranteed by GL operations"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_to_image(&self, source_rectangle: Rect<u32>) -> Option<RgbaImage> {
|
||||||
|
Self::read_framebuffer_to_image(self.gl.clone(), self.framebuffer_id, source_rectangle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_framebuffer_to_image(
|
||||||
|
gl: Rc<dyn Gl>,
|
||||||
|
framebuffer_id: u32,
|
||||||
|
source_rectangle: Rect<u32>,
|
||||||
|
) -> Option<RgbaImage> {
|
||||||
|
gl.bind_framebuffer(gl::FRAMEBUFFER, framebuffer_id);
|
||||||
|
|
||||||
|
// For some reason, OSMesa fails to render on the 3rd
|
||||||
|
// attempt in headless mode, under some conditions.
|
||||||
|
// I think this can only be some kind of synchronization
|
||||||
|
// bug in OSMesa, but explicitly un-binding any vertex
|
||||||
|
// array here seems to work around that bug.
|
||||||
|
// See https://github.com/servo/servo/issues/18606.
|
||||||
|
gl.bind_vertex_array(0);
|
||||||
|
|
||||||
|
let mut pixels = gl.read_pixels(
|
||||||
|
source_rectangle.origin.x as i32,
|
||||||
|
source_rectangle.origin.y as i32,
|
||||||
|
source_rectangle.width() as gl::GLsizei,
|
||||||
|
source_rectangle.height() as gl::GLsizei,
|
||||||
|
gl::RGBA,
|
||||||
|
gl::UNSIGNED_BYTE,
|
||||||
|
);
|
||||||
|
let gl_error = gl.get_error();
|
||||||
|
if gl_error != gl::NO_ERROR {
|
||||||
|
warn!("GL error code 0x{gl_error:x} set after read_pixels");
|
||||||
|
}
|
||||||
|
|
||||||
|
// flip image vertically (texture is upside down)
|
||||||
|
let source_rectangle = source_rectangle.to_usize();
|
||||||
|
let orig_pixels = pixels.clone();
|
||||||
|
let stride = source_rectangle.width() * 4;
|
||||||
|
for y in 0..source_rectangle.height() {
|
||||||
|
let dst_start = y * stride;
|
||||||
|
let src_start = (source_rectangle.height() - y - 1) * stride;
|
||||||
|
let src_slice = &orig_pixels[src_start..src_start + stride];
|
||||||
|
pixels[dst_start..dst_start + stride].clone_from_slice(&src_slice[..stride]);
|
||||||
|
}
|
||||||
|
|
||||||
|
RgbaImage::from_raw(
|
||||||
|
source_rectangle.width() as u32,
|
||||||
|
source_rectangle.height() as u32,
|
||||||
|
pixels,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct OffscreenRenderingContext {
|
||||||
|
parent_context: SurfmanRenderingContext,
|
||||||
|
size: Cell<Size2D<u32>>,
|
||||||
|
back_framebuffer: RefCell<Framebuffer>,
|
||||||
|
front_framebuffer: RefCell<Option<Framebuffer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
type RenderToParentCallback = Box<dyn Fn(&glow::Context, Rect<i32>) + Send + Sync>;
|
||||||
|
|
||||||
|
impl OffscreenRenderingContext {
|
||||||
|
fn new(parent_context: SurfmanRenderingContext, size: Size2D<u32>) -> Self {
|
||||||
|
let next_framebuffer = Framebuffer::new(parent_context.gl_api(), size);
|
||||||
|
Self {
|
||||||
|
parent_context,
|
||||||
|
size: Cell::new(size),
|
||||||
|
back_framebuffer: RefCell::new(next_framebuffer),
|
||||||
|
front_framebuffer: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent_context(&self) -> &SurfmanRenderingContext {
|
||||||
|
&self.parent_context
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn front_framebuffer_id(&self) -> Option<gl::GLuint> {
|
||||||
|
self.front_framebuffer
|
||||||
|
.borrow()
|
||||||
|
.as_ref()
|
||||||
|
.map(|framebuffer| framebuffer.framebuffer_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render_to_parent_callback(&self) -> Option<RenderToParentCallback> {
|
||||||
|
// Don't accept a `None` context for the read framebuffer.
|
||||||
|
let front_framebuffer_id =
|
||||||
|
NonZeroU32::new(self.front_framebuffer_id()?).map(NativeFramebuffer)?;
|
||||||
|
let parent_context_framebuffer_id = self.parent_context.framebuffer();
|
||||||
|
let size = self.size.get();
|
||||||
|
Some(Box::new(move |gl, target_rect| {
|
||||||
|
Self::render_framebuffer_to_parent_context(
|
||||||
|
gl,
|
||||||
|
Rect::new(Point2D::origin(), size.to_i32()),
|
||||||
|
front_framebuffer_id,
|
||||||
|
target_rect,
|
||||||
|
parent_context_framebuffer_id,
|
||||||
|
);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
fn render_framebuffer_to_parent_context(
|
||||||
|
gl: &glow::Context,
|
||||||
|
source_rect: Rect<i32>,
|
||||||
|
source_framebuffer_id: NativeFramebuffer,
|
||||||
|
target_rect: Rect<i32>,
|
||||||
|
target_framebuffer_id: Option<NativeFramebuffer>,
|
||||||
|
) {
|
||||||
|
use glow::HasContext as _;
|
||||||
|
unsafe {
|
||||||
|
gl.clear_color(0.0, 0.0, 0.0, 0.0);
|
||||||
|
gl.scissor(
|
||||||
|
target_rect.origin.x,
|
||||||
|
target_rect.origin.y,
|
||||||
|
target_rect.width(),
|
||||||
|
target_rect.height(),
|
||||||
|
);
|
||||||
|
gl.enable(gl::SCISSOR_TEST);
|
||||||
|
gl.clear(gl::COLOR_BUFFER_BIT);
|
||||||
|
gl.disable(gl::SCISSOR_TEST);
|
||||||
|
|
||||||
|
gl.bind_framebuffer(gl::READ_FRAMEBUFFER, Some(source_framebuffer_id));
|
||||||
|
gl.bind_framebuffer(gl::DRAW_FRAMEBUFFER, target_framebuffer_id);
|
||||||
|
|
||||||
|
gl.blit_framebuffer(
|
||||||
|
source_rect.origin.x,
|
||||||
|
source_rect.origin.y,
|
||||||
|
source_rect.origin.x + source_rect.width(),
|
||||||
|
source_rect.origin.y + source_rect.height(),
|
||||||
|
target_rect.origin.x,
|
||||||
|
target_rect.origin.y,
|
||||||
|
target_rect.origin.x + target_rect.width(),
|
||||||
|
target_rect.origin.y + target_rect.height(),
|
||||||
|
gl::COLOR_BUFFER_BIT,
|
||||||
|
gl::NEAREST,
|
||||||
|
);
|
||||||
|
gl.bind_framebuffer(gl::FRAMEBUFFER, target_framebuffer_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderingContext for OffscreenRenderingContext {
|
||||||
|
fn resize(&self, size: Size2D<i32>) {
|
||||||
|
// We do not resize any buffers right now. The current buffers might be too big or too
|
||||||
|
// small, but we only want to ensure (later) that next buffer that we draw to is the
|
||||||
|
// correct size.
|
||||||
|
self.size.set(size.to_u32());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_for_rendering(&self) {
|
||||||
|
self.back_framebuffer.borrow().bind();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn present(&self) {
|
||||||
|
trace!(
|
||||||
|
"Unbinding FBO {}",
|
||||||
|
self.back_framebuffer.borrow().framebuffer_id
|
||||||
|
);
|
||||||
|
self.gl_api().bind_framebuffer(gl::FRAMEBUFFER, 0);
|
||||||
|
|
||||||
|
let new_back_framebuffer = match self.front_framebuffer.borrow_mut().take() {
|
||||||
|
Some(framebuffer) if framebuffer.size == self.size.get() => framebuffer,
|
||||||
|
_ => Framebuffer::new(self.gl_api(), self.size.get()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_front_framebuffer = std::mem::replace(
|
||||||
|
&mut *self.back_framebuffer.borrow_mut(),
|
||||||
|
new_back_framebuffer,
|
||||||
|
);
|
||||||
|
*self.front_framebuffer.borrow_mut() = Some(new_front_framebuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn make_current(&self) -> Result<(), surfman::Error> {
|
||||||
|
self.parent_context.make_gl_context_current()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gl_api(&self) -> Rc<dyn gleam::gl::Gl> {
|
||||||
|
self.parent_context.gl_api()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn gl_version(&self) -> GLVersion {
|
||||||
|
self.parent_context.gl_version()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_texture(&self, surface: Surface) -> Option<(SurfaceTexture, u32, Size2D<i32>)> {
|
||||||
|
self.parent_context.create_texture(surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn destroy_texture(&self, surface_texture: SurfaceTexture) -> Option<Surface> {
|
||||||
|
self.parent_context.destroy_texture(surface_texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn connection(&self) -> Option<Connection> {
|
||||||
|
Some(self.parent_context.connection())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_to_image(&self, source_rectangle: Rect<u32>) -> Option<RgbaImage> {
|
||||||
|
self.back_framebuffer
|
||||||
|
.borrow()
|
||||||
|
.read_to_image(source_rectangle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use euclid::{Point2D, Rect, Size2D};
|
||||||
|
use gleam::gl;
|
||||||
|
use image::Rgba;
|
||||||
|
use surfman::{Connection, ContextAttributeFlags, ContextAttributes, Error, GLApi, GLVersion};
|
||||||
|
|
||||||
|
use super::Framebuffer;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[allow(unsafe_code)]
|
||||||
|
fn test_read_pixels() -> Result<(), Error> {
|
||||||
|
let connection = Connection::new()?;
|
||||||
|
let adapter = connection.create_software_adapter()?;
|
||||||
|
let mut device = connection.create_device(&adapter)?;
|
||||||
|
let context_descriptor = device.create_context_descriptor(&ContextAttributes {
|
||||||
|
version: GLVersion::new(3, 0),
|
||||||
|
flags: ContextAttributeFlags::empty(),
|
||||||
|
})?;
|
||||||
|
let mut context = device.create_context(&context_descriptor, None)?;
|
||||||
|
|
||||||
|
let gl = match connection.gl_api() {
|
||||||
|
GLApi::GL => unsafe { gl::GlFns::load_with(|s| device.get_proc_address(&context, s)) },
|
||||||
|
GLApi::GLES => unsafe {
|
||||||
|
gl::GlesFns::load_with(|s| device.get_proc_address(&context, s))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
device.make_context_current(&context)?;
|
||||||
|
|
||||||
|
{
|
||||||
|
const SIZE: u32 = 16;
|
||||||
|
let framebuffer = Framebuffer::new(gl, Size2D::new(SIZE, SIZE));
|
||||||
|
framebuffer.bind();
|
||||||
|
framebuffer
|
||||||
|
.gl
|
||||||
|
.clear_color(12.0 / 255.0, 34.0 / 255.0, 56.0 / 255.0, 78.0 / 255.0);
|
||||||
|
framebuffer.gl.clear(gl::COLOR_BUFFER_BIT);
|
||||||
|
|
||||||
|
let img = framebuffer
|
||||||
|
.read_to_image(Rect::new(Point2D::zero(), Size2D::new(SIZE, SIZE)))
|
||||||
|
.expect("Should have been able to read back image.");
|
||||||
|
assert_eq!(img.width(), SIZE);
|
||||||
|
assert_eq!(img.height(), SIZE);
|
||||||
|
|
||||||
|
let expected_pixel: Rgba<u8> = Rgba([12, 34, 56, 78]);
|
||||||
|
assert!(img.pixels().all(|&p| p == expected_pixel));
|
||||||
|
}
|
||||||
|
|
||||||
|
device.destroy_context(&mut context)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,19 +11,16 @@ use std::time::Instant;
|
||||||
use std::{env, fs};
|
use std::{env, fs};
|
||||||
|
|
||||||
use log::{info, trace, warn};
|
use log::{info, trace, warn};
|
||||||
use raw_window_handle::HasDisplayHandle;
|
|
||||||
use servo::compositing::windowing::{AnimationState, WindowMethods};
|
use servo::compositing::windowing::{AnimationState, WindowMethods};
|
||||||
use servo::compositing::CompositeTarget;
|
use servo::compositing::CompositeTarget;
|
||||||
use servo::config::opts::Opts;
|
use servo::config::opts::Opts;
|
||||||
use servo::config::prefs::Preferences;
|
use servo::config::prefs::Preferences;
|
||||||
use servo::servo_config::pref;
|
use servo::servo_config::pref;
|
||||||
use servo::servo_url::ServoUrl;
|
use servo::servo_url::ServoUrl;
|
||||||
use servo::webrender_traits::SurfmanRenderingContext;
|
|
||||||
use servo::webxr::glwindow::GlWindowDiscovery;
|
use servo::webxr::glwindow::GlWindowDiscovery;
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
use servo::webxr::openxr::{AppInfo, OpenXrDiscovery};
|
use servo::webxr::openxr::{AppInfo, OpenXrDiscovery};
|
||||||
use servo::{EventLoopWaker, Servo};
|
use servo::{EventLoopWaker, Servo};
|
||||||
use surfman::Connection;
|
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use winit::application::ApplicationHandler;
|
use winit::application::ApplicationHandler;
|
||||||
use winit::event::WindowEvent;
|
use winit::event::WindowEvent;
|
||||||
|
@ -55,18 +52,14 @@ pub struct App {
|
||||||
state: AppState,
|
state: AppState,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum Present {
|
|
||||||
Deferred,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Action to be taken by the caller of [`App::handle_events`].
|
/// Action to be taken by the caller of [`App::handle_events`].
|
||||||
pub(crate) enum PumpResult {
|
pub(crate) enum PumpResult {
|
||||||
/// The caller should shut down Servo and its related context.
|
/// The caller should shut down Servo and its related context.
|
||||||
Shutdown,
|
Shutdown,
|
||||||
Continue {
|
Continue {
|
||||||
update: bool,
|
need_update: bool,
|
||||||
present: Present,
|
new_servo_frame: bool,
|
||||||
|
need_window_redraw: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,52 +95,25 @@ impl App {
|
||||||
|
|
||||||
/// Initialize Application once event loop start running.
|
/// Initialize Application once event loop start running.
|
||||||
pub fn init(&mut self, event_loop: Option<&ActiveEventLoop>) {
|
pub fn init(&mut self, event_loop: Option<&ActiveEventLoop>) {
|
||||||
// Create rendering context
|
|
||||||
let initial_window_size = self.servoshell_preferences.initial_window_size;
|
|
||||||
let rendering_context = if self.servoshell_preferences.headless {
|
|
||||||
let connection = Connection::new().expect("Failed to create connection");
|
|
||||||
let adapter = connection
|
|
||||||
.create_software_adapter()
|
|
||||||
.expect("Failed to create adapter");
|
|
||||||
SurfmanRenderingContext::create(
|
|
||||||
&connection,
|
|
||||||
&adapter,
|
|
||||||
Some(initial_window_size.to_untyped().to_i32()),
|
|
||||||
)
|
|
||||||
.expect("Failed to create WR surfman")
|
|
||||||
} else {
|
|
||||||
let display_handle = event_loop
|
|
||||||
.unwrap()
|
|
||||||
.display_handle()
|
|
||||||
.expect("could not get display handle from window");
|
|
||||||
let connection = Connection::from_display_handle(display_handle)
|
|
||||||
.expect("Failed to create connection");
|
|
||||||
let adapter = connection
|
|
||||||
.create_adapter()
|
|
||||||
.expect("Failed to create adapter");
|
|
||||||
SurfmanRenderingContext::create(&connection, &adapter, None)
|
|
||||||
.expect("Failed to create WR surfman")
|
|
||||||
};
|
|
||||||
|
|
||||||
let headless = self.servoshell_preferences.headless;
|
let headless = self.servoshell_preferences.headless;
|
||||||
let window = if headless {
|
|
||||||
headless_window::Window::new(&self.servoshell_preferences)
|
|
||||||
} else {
|
|
||||||
Rc::new(headed_window::Window::new(
|
|
||||||
&self.opts,
|
|
||||||
&self.servoshell_preferences,
|
|
||||||
&rendering_context,
|
|
||||||
event_loop.unwrap(),
|
|
||||||
))
|
|
||||||
};
|
|
||||||
|
|
||||||
if window.winit_window().is_some() {
|
assert_eq!(headless, event_loop.is_none());
|
||||||
self.minibrowser = Some(Minibrowser::new(
|
let window = match event_loop {
|
||||||
&rendering_context,
|
Some(event_loop) => {
|
||||||
event_loop.unwrap(),
|
let window = headed_window::Window::new(
|
||||||
self.initial_url.clone(),
|
&self.opts,
|
||||||
));
|
&self.servoshell_preferences,
|
||||||
}
|
event_loop,
|
||||||
|
);
|
||||||
|
self.minibrowser = Some(Minibrowser::new(
|
||||||
|
window.offscreen_rendering_context(),
|
||||||
|
event_loop,
|
||||||
|
self.initial_url.clone(),
|
||||||
|
));
|
||||||
|
Rc::new(window)
|
||||||
|
},
|
||||||
|
None => headless_window::Window::new(&self.servoshell_preferences),
|
||||||
|
};
|
||||||
|
|
||||||
self.windows.insert(window.id(), window);
|
self.windows.insert(window.id(), window);
|
||||||
|
|
||||||
|
@ -174,12 +140,6 @@ impl App {
|
||||||
// Implements embedder methods, used by libservo and constellation.
|
// Implements embedder methods, used by libservo and constellation.
|
||||||
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
|
let embedder = Box::new(EmbedderCallbacks::new(self.waker.clone(), xr_discovery));
|
||||||
|
|
||||||
let composite_target = if self.minibrowser.is_some() {
|
|
||||||
CompositeTarget::OffscreenFbo
|
|
||||||
} else {
|
|
||||||
CompositeTarget::ContextFbo
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: Remove this once dyn upcasting coercion stabilises
|
// TODO: Remove this once dyn upcasting coercion stabilises
|
||||||
// <https://github.com/rust-lang/rust/issues/65991>
|
// <https://github.com/rust-lang/rust/issues/65991>
|
||||||
struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
|
struct UpcastedWindow(Rc<dyn WindowPortsMethods>);
|
||||||
|
@ -195,11 +155,11 @@ impl App {
|
||||||
let servo = Servo::new(
|
let servo = Servo::new(
|
||||||
self.opts.clone(),
|
self.opts.clone(),
|
||||||
self.preferences.clone(),
|
self.preferences.clone(),
|
||||||
Rc::new(rendering_context),
|
window.rendering_context(),
|
||||||
embedder,
|
embedder,
|
||||||
Rc::new(UpcastedWindow(window.clone())),
|
Rc::new(UpcastedWindow(window.clone())),
|
||||||
self.servoshell_preferences.user_agent.clone(),
|
self.servoshell_preferences.user_agent.clone(),
|
||||||
composite_target,
|
CompositeTarget::ContextFbo,
|
||||||
);
|
);
|
||||||
servo.setup_logging();
|
servo.setup_logging();
|
||||||
|
|
||||||
|
@ -233,33 +193,28 @@ impl App {
|
||||||
state.shutdown();
|
state.shutdown();
|
||||||
self.state = AppState::ShuttingDown;
|
self.state = AppState::ShuttingDown;
|
||||||
},
|
},
|
||||||
PumpResult::Continue { update, present } => {
|
PumpResult::Continue {
|
||||||
if update {
|
need_update: update,
|
||||||
if let Some(ref mut minibrowser) = self.minibrowser {
|
new_servo_frame,
|
||||||
if minibrowser.update_webview_data(state) {
|
need_window_redraw,
|
||||||
// Update the minibrowser immediately. While we could update by requesting a
|
} => {
|
||||||
// redraw, doing so would delay the location update by two frames.
|
// A new Servo frame is ready, so swap the buffer on our `RenderingContext`. In headed mode
|
||||||
minibrowser.update(
|
// this won't immediately update the widget surface, because we render to an offscreen
|
||||||
window.winit_window().unwrap(),
|
// `RenderingContext`.
|
||||||
state,
|
if new_servo_frame {
|
||||||
"update_location_in_toolbar",
|
state.servo().present();
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
match present {
|
|
||||||
Present::Deferred => {
|
let updated = match (update, &mut self.minibrowser) {
|
||||||
// The compositor has painted to this frame.
|
(true, Some(minibrowser)) => minibrowser.update_webview_data(state),
|
||||||
trace!("PumpResult::Present::Deferred");
|
_ => false,
|
||||||
// Request a winit redraw event, so we can paint the minibrowser and present.
|
};
|
||||||
// Otherwise, it's in headless mode and we present directly.
|
|
||||||
if let Some(window) = window.winit_window() {
|
// If in headed mode, request a winit redraw event, so we can paint the minibrowser.
|
||||||
window.request_redraw();
|
if updated || need_window_redraw || new_servo_frame {
|
||||||
} else {
|
if let Some(window) = window.winit_window() {
|
||||||
state.servo().present();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
},
|
|
||||||
Present::None => {},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -292,15 +247,12 @@ impl App {
|
||||||
state.shutdown();
|
state.shutdown();
|
||||||
self.state = AppState::ShuttingDown;
|
self.state = AppState::ShuttingDown;
|
||||||
},
|
},
|
||||||
PumpResult::Continue { present, .. } => {
|
PumpResult::Continue {
|
||||||
match present {
|
new_servo_frame, ..
|
||||||
Present::Deferred => {
|
} => {
|
||||||
// The compositor has painted to this frame.
|
if new_servo_frame {
|
||||||
trace!("PumpResult::Present::Deferred");
|
// In headless mode, we present directly.
|
||||||
// In headless mode, we present directly.
|
state.servo().present();
|
||||||
state.servo().present();
|
|
||||||
},
|
|
||||||
Present::None => {},
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -401,8 +353,6 @@ impl ApplicationHandler<WakerEvent> for App {
|
||||||
minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested");
|
minibrowser.update(window.winit_window().unwrap(), state, "RedrawRequested");
|
||||||
minibrowser.paint(window.winit_window().unwrap());
|
minibrowser.paint(window.winit_window().unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
state.servo().present();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the event
|
// Handle the event
|
||||||
|
|
|
@ -25,7 +25,7 @@ use servo::{
|
||||||
use tinyfiledialogs::MessageBoxIcon;
|
use tinyfiledialogs::MessageBoxIcon;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use super::app::{Present, PumpResult};
|
use super::app::PumpResult;
|
||||||
use super::dialog::Dialog;
|
use super::dialog::Dialog;
|
||||||
use super::gamepad::GamepadSupport;
|
use super::gamepad::GamepadSupport;
|
||||||
use super::keyutils::CMD_OR_CONTROL;
|
use super::keyutils::CMD_OR_CONTROL;
|
||||||
|
@ -74,7 +74,7 @@ pub struct RunningAppStateInner {
|
||||||
need_update: bool,
|
need_update: bool,
|
||||||
|
|
||||||
/// Whether or not the application needs to be redrawn.
|
/// Whether or not the application needs to be redrawn.
|
||||||
need_present: bool,
|
new_servo_frame_ready: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for RunningAppState {
|
impl Drop for RunningAppState {
|
||||||
|
@ -101,7 +101,7 @@ impl RunningAppState {
|
||||||
window,
|
window,
|
||||||
gamepad_support: GamepadSupport::maybe_new(),
|
gamepad_support: GamepadSupport::maybe_new(),
|
||||||
need_update: false,
|
need_update: false,
|
||||||
need_present: false,
|
new_servo_frame_ready: false,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,28 +133,21 @@ impl RunningAppState {
|
||||||
self.handle_gamepad_events();
|
self.handle_gamepad_events();
|
||||||
}
|
}
|
||||||
|
|
||||||
let should_continue = self.servo().spin_event_loop();
|
if !self.servo().spin_event_loop() {
|
||||||
|
|
||||||
// Delegate handlers may have asked us to present or update compositor contents.
|
|
||||||
let need_present = std::mem::replace(&mut self.inner_mut().need_present, false);
|
|
||||||
let need_update = std::mem::replace(&mut self.inner_mut().need_update, false);
|
|
||||||
|
|
||||||
if !should_continue {
|
|
||||||
return PumpResult::Shutdown;
|
return PumpResult::Shutdown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Currently, egui-file-dialog dialogs need to be constantly presented or animations aren't fluid.
|
// Delegate handlers may have asked us to present or update compositor contents.
|
||||||
let need_present = need_present || self.has_active_dialog();
|
let new_servo_frame = std::mem::replace(&mut self.inner_mut().new_servo_frame_ready, false);
|
||||||
|
let need_update = std::mem::replace(&mut self.inner_mut().need_update, false);
|
||||||
|
|
||||||
let present = if need_present {
|
// Currently, egui-file-dialog dialogs need to be constantly redrawn or animations aren't fluid.
|
||||||
Present::Deferred
|
let need_window_redraw = new_servo_frame || self.has_active_dialog();
|
||||||
} else {
|
|
||||||
Present::None
|
|
||||||
};
|
|
||||||
|
|
||||||
PumpResult::Continue {
|
PumpResult::Continue {
|
||||||
update: need_update,
|
need_update,
|
||||||
present,
|
new_servo_frame,
|
||||||
|
need_window_redraw,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,7 +233,6 @@ impl RunningAppState {
|
||||||
.or_default()
|
.or_default()
|
||||||
.push(dialog);
|
.push(dialog);
|
||||||
inner_mut.need_update = true;
|
inner_mut.need_update = true;
|
||||||
inner_mut.need_present = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_active_dialog(&self) -> bool {
|
fn has_active_dialog(&self) -> bool {
|
||||||
|
@ -415,22 +407,17 @@ impl WebViewDelegate for RunningAppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_ready_to_show(&self, webview: servo::WebView) {
|
fn notify_ready_to_show(&self, webview: servo::WebView) {
|
||||||
let scale = self.inner().window.hidpi_factor().get();
|
let rect = self
|
||||||
let toolbar = self.inner().window.toolbar_height().get();
|
|
||||||
|
|
||||||
// Adjust for our toolbar height.
|
|
||||||
// TODO: Adjust for egui window decorations if we end up using those
|
|
||||||
let mut rect = self
|
|
||||||
.inner()
|
.inner()
|
||||||
.window
|
.window
|
||||||
.get_coordinates()
|
.get_coordinates()
|
||||||
.get_viewport()
|
.get_viewport()
|
||||||
.to_f32();
|
.to_f32();
|
||||||
rect.min.y += toolbar * scale;
|
|
||||||
|
|
||||||
webview.focus();
|
webview.focus();
|
||||||
webview.move_resize(rect);
|
webview.move_resize(rect);
|
||||||
webview.raise_to_top(true);
|
webview.raise_to_top(true);
|
||||||
|
webview.notify_rendering_context_resized();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_closed(&self, webview: servo::WebView) {
|
fn notify_closed(&self, webview: servo::WebView) {
|
||||||
|
@ -497,7 +484,7 @@ impl WebViewDelegate for RunningAppState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn notify_new_frame_ready(&self, _webview: servo::WebView) {
|
fn notify_new_frame_ready(&self, _webview: servo::WebView) {
|
||||||
self.inner_mut().need_present = true;
|
self.inner_mut().new_servo_frame_ready = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn play_gamepad_haptic_effect(
|
fn play_gamepad_haptic_effect(
|
||||||
|
|
|
@ -12,7 +12,7 @@ use std::time::Duration;
|
||||||
|
|
||||||
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
|
use euclid::{Angle, Length, Point2D, Rotation3D, Scale, Size2D, UnknownUnit, Vector2D, Vector3D};
|
||||||
use keyboard_types::{Modifiers, ShortcutMatcher};
|
use keyboard_types::{Modifiers, ShortcutMatcher};
|
||||||
use log::{debug, info};
|
use log::{debug, info, warn};
|
||||||
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
use raw_window_handle::{HasDisplayHandle, HasWindowHandle};
|
||||||
use servo::compositing::windowing::{
|
use servo::compositing::windowing::{
|
||||||
AnimationState, EmbedderCoordinates, WebRenderDebugOption, WindowMethods,
|
AnimationState, EmbedderCoordinates, WebRenderDebugOption, WindowMethods,
|
||||||
|
@ -22,13 +22,14 @@ use servo::servo_config::pref;
|
||||||
use servo::servo_geometry::DeviceIndependentPixel;
|
use servo::servo_geometry::DeviceIndependentPixel;
|
||||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel};
|
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel};
|
||||||
use servo::webrender_api::ScrollLocation;
|
use servo::webrender_api::ScrollLocation;
|
||||||
|
use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext};
|
||||||
use servo::webrender_traits::SurfmanRenderingContext;
|
use servo::webrender_traits::SurfmanRenderingContext;
|
||||||
use servo::{
|
use servo::{
|
||||||
Cursor, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
|
Cursor, InputEvent, Key, KeyState, KeyboardEvent, MouseButton as ServoMouseButton,
|
||||||
MouseButtonAction, MouseButtonEvent, MouseMoveEvent, Theme, TouchEvent, TouchEventAction,
|
MouseButtonAction, MouseButtonEvent, MouseMoveEvent, Theme, TouchEvent, TouchEventAction,
|
||||||
TouchId, WebView, WheelDelta, WheelEvent, WheelMode,
|
TouchId, WebView, WheelDelta, WheelEvent, WheelMode,
|
||||||
};
|
};
|
||||||
use surfman::{Context, Device, SurfaceType};
|
use surfman::{Connection, Context, Device, SurfaceType};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
|
use winit::dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
|
||||||
use winit::event::{
|
use winit::event::{
|
||||||
|
@ -52,9 +53,9 @@ pub struct Window {
|
||||||
inner_size: Cell<PhysicalSize<u32>>,
|
inner_size: Cell<PhysicalSize<u32>>,
|
||||||
toolbar_height: Cell<Length<f32, DeviceIndependentPixel>>,
|
toolbar_height: Cell<Length<f32, DeviceIndependentPixel>>,
|
||||||
mouse_down_button: Cell<Option<MouseButton>>,
|
mouse_down_button: Cell<Option<MouseButton>>,
|
||||||
mouse_down_point: Cell<Point2D<i32, DevicePixel>>,
|
webview_relative_mouse_down_point: Cell<Point2D<f32, DevicePixel>>,
|
||||||
monitor: winit::monitor::MonitorHandle,
|
monitor: winit::monitor::MonitorHandle,
|
||||||
mouse_pos: Cell<Point2D<i32, DevicePixel>>,
|
webview_relative_mouse_point: Cell<Point2D<f32, DevicePixel>>,
|
||||||
last_pressed: Cell<Option<(KeyboardEvent, Option<LogicalKey>)>>,
|
last_pressed: Cell<Option<(KeyboardEvent, Option<LogicalKey>)>>,
|
||||||
/// A map of winit's key codes to key values that are interpreted from
|
/// A map of winit's key codes to key values that are interpreted from
|
||||||
/// winit's ReceivedChar events.
|
/// winit's ReceivedChar events.
|
||||||
|
@ -64,13 +65,21 @@ pub struct Window {
|
||||||
device_pixel_ratio_override: Option<f32>,
|
device_pixel_ratio_override: Option<f32>,
|
||||||
xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>,
|
xr_window_poses: RefCell<Vec<Rc<XRWindowPose>>>,
|
||||||
modifiers_state: Cell<ModifiersState>,
|
modifiers_state: Cell<ModifiersState>,
|
||||||
|
|
||||||
|
/// The RenderingContext that renders directly onto the Window. This is used as
|
||||||
|
/// the target of egui rendering and also where Servo rendering results are finally
|
||||||
|
/// blitted.
|
||||||
|
window_rendering_context: SurfmanRenderingContext,
|
||||||
|
|
||||||
|
/// The `RenderingContext` of Servo itself. This is used to render Servo results
|
||||||
|
/// temporarily until they can be blitted into the egui scene.
|
||||||
|
rendering_context: Rc<OffscreenRenderingContext>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
opts: &Opts,
|
opts: &Opts,
|
||||||
servoshell_preferences: &ServoShellPreferences,
|
servoshell_preferences: &ServoShellPreferences,
|
||||||
rendering_context: &SurfmanRenderingContext,
|
|
||||||
event_loop: &ActiveEventLoop,
|
event_loop: &ActiveEventLoop,
|
||||||
) -> Window {
|
) -> Window {
|
||||||
// If there's no chrome, start off with the window invisible. It will be set to visible in
|
// If there's no chrome, start off with the window invisible. It will be set to visible in
|
||||||
|
@ -111,37 +120,48 @@ impl Window {
|
||||||
let screen_scale: Scale<f64, DeviceIndependentPixel, DevicePixel> =
|
let screen_scale: Scale<f64, DeviceIndependentPixel, DevicePixel> =
|
||||||
Scale::new(screen_scale);
|
Scale::new(screen_scale);
|
||||||
let screen_size = (winit_size_to_euclid_size(screen_size).to_f64() / screen_scale).to_u32();
|
let screen_size = (winit_size_to_euclid_size(screen_size).to_f64() / screen_scale).to_u32();
|
||||||
|
let inner_size = winit_window.inner_size();
|
||||||
|
|
||||||
// Initialize surfman
|
let display_handle = event_loop
|
||||||
|
.display_handle()
|
||||||
|
.expect("could not get display handle from window");
|
||||||
|
let connection =
|
||||||
|
Connection::from_display_handle(display_handle).expect("Failed to create connection");
|
||||||
|
let adapter = connection
|
||||||
|
.create_adapter()
|
||||||
|
.expect("Failed to create adapter");
|
||||||
let window_handle = winit_window
|
let window_handle = winit_window
|
||||||
.window_handle()
|
.window_handle()
|
||||||
.expect("could not get window handle from window");
|
.expect("could not get window handle from window");
|
||||||
|
let native_widget = connection
|
||||||
let inner_size = winit_window.inner_size();
|
|
||||||
let native_widget = rendering_context
|
|
||||||
.connection()
|
|
||||||
.create_native_widget_from_window_handle(
|
.create_native_widget_from_window_handle(
|
||||||
window_handle,
|
window_handle,
|
||||||
winit_size_to_euclid_size(inner_size).to_i32().to_untyped(),
|
winit_size_to_euclid_size(inner_size).to_i32().to_untyped(),
|
||||||
)
|
)
|
||||||
.expect("Failed to create native widget");
|
.expect("Failed to create native widget");
|
||||||
|
|
||||||
let surface_type = SurfaceType::Widget { native_widget };
|
let window_rendering_context = SurfmanRenderingContext::create(&connection, &adapter, None)
|
||||||
let surface = rendering_context
|
.expect("Failed to create window RenderingContext");
|
||||||
.create_surface(surface_type)
|
let surface = window_rendering_context
|
||||||
|
.create_surface(SurfaceType::Widget { native_widget })
|
||||||
.expect("Failed to create surface");
|
.expect("Failed to create surface");
|
||||||
rendering_context
|
window_rendering_context
|
||||||
.bind_surface(surface)
|
.bind_surface(surface)
|
||||||
.expect("Failed to bind surface");
|
.expect("Failed to bind surface");
|
||||||
|
|
||||||
// Make sure the gl context is made current.
|
// Make sure the gl context is made current.
|
||||||
rendering_context.make_gl_context_current().unwrap();
|
window_rendering_context.make_gl_context_current().unwrap();
|
||||||
|
|
||||||
|
let rendering_context_size = Size2D::new(inner_size.width, inner_size.height);
|
||||||
|
let rendering_context =
|
||||||
|
Rc::new(window_rendering_context.offscreen_context(rendering_context_size));
|
||||||
|
|
||||||
debug!("Created window {:?}", winit_window.id());
|
debug!("Created window {:?}", winit_window.id());
|
||||||
Window {
|
Window {
|
||||||
winit_window,
|
winit_window,
|
||||||
mouse_down_button: Cell::new(None),
|
mouse_down_button: Cell::new(None),
|
||||||
mouse_down_point: Cell::new(Point2D::zero()),
|
webview_relative_mouse_down_point: Cell::new(Point2D::zero()),
|
||||||
mouse_pos: Cell::new(Point2D::zero()),
|
webview_relative_mouse_point: Cell::new(Point2D::zero()),
|
||||||
last_pressed: Cell::new(None),
|
last_pressed: Cell::new(None),
|
||||||
keys_down: RefCell::new(HashMap::new()),
|
keys_down: RefCell::new(HashMap::new()),
|
||||||
animation_state: Cell::new(AnimationState::Idle),
|
animation_state: Cell::new(AnimationState::Idle),
|
||||||
|
@ -153,6 +173,8 @@ impl Window {
|
||||||
xr_window_poses: RefCell::new(vec![]),
|
xr_window_poses: RefCell::new(vec![]),
|
||||||
modifiers_state: Cell::new(ModifiersState::empty()),
|
modifiers_state: Cell::new(ModifiersState::empty()),
|
||||||
toolbar_height: Cell::new(Default::default()),
|
toolbar_height: Cell::new(Default::default()),
|
||||||
|
window_rendering_context,
|
||||||
|
rendering_context,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -244,13 +266,7 @@ impl Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper function to handle a click
|
/// Helper function to handle a click
|
||||||
fn handle_mouse(
|
fn handle_mouse(&self, webview: &WebView, button: MouseButton, action: ElementState) {
|
||||||
&self,
|
|
||||||
webview: &WebView,
|
|
||||||
button: MouseButton,
|
|
||||||
action: ElementState,
|
|
||||||
coords: Point2D<i32, DevicePixel>,
|
|
||||||
) {
|
|
||||||
let max_pixel_dist = 10.0 * self.hidpi_factor().get();
|
let max_pixel_dist = 10.0 * self.hidpi_factor().get();
|
||||||
let mouse_button = match &button {
|
let mouse_button = match &button {
|
||||||
MouseButton::Left => ServoMouseButton::Left,
|
MouseButton::Left => ServoMouseButton::Left,
|
||||||
|
@ -261,9 +277,10 @@ impl Window {
|
||||||
MouseButton::Other(value) => ServoMouseButton::Other(*value),
|
MouseButton::Other(value) => ServoMouseButton::Other(*value),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let point = self.webview_relative_mouse_point.get();
|
||||||
let action = match action {
|
let action = match action {
|
||||||
ElementState::Pressed => {
|
ElementState::Pressed => {
|
||||||
self.mouse_down_point.set(coords);
|
self.webview_relative_mouse_down_point.set(point);
|
||||||
self.mouse_down_button.set(Some(button));
|
self.mouse_down_button.set(Some(button));
|
||||||
MouseButtonAction::Down
|
MouseButtonAction::Down
|
||||||
},
|
},
|
||||||
|
@ -273,7 +290,7 @@ impl Window {
|
||||||
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
|
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
|
||||||
action,
|
action,
|
||||||
button: mouse_button,
|
button: mouse_button,
|
||||||
point: coords.to_f32(),
|
point,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Also send a 'click' event if this is release and the press was recorded
|
// Also send a 'click' event if this is release and the press was recorded
|
||||||
|
@ -285,14 +302,13 @@ impl Window {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mouse_down_button) = self.mouse_down_button.get() {
|
if let Some(mouse_down_button) = self.mouse_down_button.get() {
|
||||||
let pixel_dist = self.mouse_down_point.get() - coords;
|
let pixel_dist = self.webview_relative_mouse_down_point.get() - point;
|
||||||
let pixel_dist =
|
let pixel_dist = (pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y).sqrt();
|
||||||
((pixel_dist.x * pixel_dist.x + pixel_dist.y * pixel_dist.y) as f32).sqrt();
|
|
||||||
if mouse_down_button == button && pixel_dist < max_pixel_dist {
|
if mouse_down_button == button && pixel_dist < max_pixel_dist {
|
||||||
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
|
webview.notify_input_event(InputEvent::MouseButton(MouseButtonEvent {
|
||||||
action: MouseButtonAction::Click,
|
action: MouseButtonAction::Click,
|
||||||
button: mouse_button,
|
button: mouse_button,
|
||||||
point: coords.to_f32(),
|
point,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -418,6 +434,10 @@ impl Window {
|
||||||
.otherwise(|| handled = false);
|
.otherwise(|| handled = false);
|
||||||
handled
|
handled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn offscreen_rendering_context(&self) -> Rc<OffscreenRenderingContext> {
|
||||||
|
self.rendering_context.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowPortsMethods for Window {
|
impl WindowPortsMethods for Window {
|
||||||
|
@ -545,15 +565,15 @@ impl WindowPortsMethods for Window {
|
||||||
WindowEvent::ModifiersChanged(modifiers) => self.modifiers_state.set(modifiers.state()),
|
WindowEvent::ModifiersChanged(modifiers) => self.modifiers_state.set(modifiers.state()),
|
||||||
WindowEvent::MouseInput { state, button, .. } => {
|
WindowEvent::MouseInput { state, button, .. } => {
|
||||||
if button == MouseButton::Left || button == MouseButton::Right {
|
if button == MouseButton::Left || button == MouseButton::Right {
|
||||||
self.handle_mouse(&webview, button, state, self.mouse_pos.get());
|
self.handle_mouse(&webview, button, state);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
WindowEvent::CursorMoved { position, .. } => {
|
WindowEvent::CursorMoved { position, .. } => {
|
||||||
let position = winit_position_to_euclid_point(position);
|
let mut point = winit_position_to_euclid_point(position).to_f32();
|
||||||
self.mouse_pos.set(position.to_i32());
|
point.y -= (self.toolbar_height() * self.hidpi_factor()).0;
|
||||||
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent {
|
|
||||||
point: position.to_f32(),
|
self.webview_relative_mouse_point.set(point);
|
||||||
}));
|
webview.notify_input_event(InputEvent::MouseMove(MouseMoveEvent { point }));
|
||||||
},
|
},
|
||||||
WindowEvent::MouseWheel { delta, phase, .. } => {
|
WindowEvent::MouseWheel { delta, phase, .. } => {
|
||||||
let (mut dx, mut dy, mode) = match delta {
|
let (mut dx, mut dy, mode) = match delta {
|
||||||
|
@ -574,8 +594,8 @@ impl WindowPortsMethods for Window {
|
||||||
z: 0.0,
|
z: 0.0,
|
||||||
mode,
|
mode,
|
||||||
};
|
};
|
||||||
let pos = self.mouse_pos.get();
|
let pos = self.webview_relative_mouse_point.get();
|
||||||
let point = Point2D::new(pos.x as f32, pos.y as f32);
|
let point = Point2D::new(pos.x, pos.y);
|
||||||
|
|
||||||
// Scroll events snap to the major axis of movement, with vertical
|
// Scroll events snap to the major axis of movement, with vertical
|
||||||
// preferred over horizontal.
|
// preferred over horizontal.
|
||||||
|
@ -590,7 +610,11 @@ impl WindowPortsMethods for Window {
|
||||||
|
|
||||||
// Send events
|
// Send events
|
||||||
webview.notify_input_event(InputEvent::Wheel(WheelEvent { delta, point }));
|
webview.notify_input_event(InputEvent::Wheel(WheelEvent { delta, point }));
|
||||||
webview.notify_scroll_event(scroll_location, self.mouse_pos.get(), phase);
|
webview.notify_scroll_event(
|
||||||
|
scroll_location,
|
||||||
|
self.webview_relative_mouse_point.get().to_i32(),
|
||||||
|
phase,
|
||||||
|
);
|
||||||
},
|
},
|
||||||
WindowEvent::Touch(touch) => {
|
WindowEvent::Touch(touch) => {
|
||||||
webview.notify_input_event(InputEvent::Touch(TouchEvent {
|
webview.notify_input_event(InputEvent::Touch(TouchEvent {
|
||||||
|
@ -607,6 +631,14 @@ impl WindowPortsMethods for Window {
|
||||||
},
|
},
|
||||||
WindowEvent::Resized(new_size) => {
|
WindowEvent::Resized(new_size) => {
|
||||||
if self.inner_size.get() != new_size {
|
if self.inner_size.get() != new_size {
|
||||||
|
let rendering_context_size = Size2D::new(new_size.width, new_size.height);
|
||||||
|
if let Err(error) = self
|
||||||
|
.window_rendering_context
|
||||||
|
.resize(rendering_context_size.to_i32())
|
||||||
|
{
|
||||||
|
warn!("Could not resize window RenderingContext: {error:?}");
|
||||||
|
}
|
||||||
|
|
||||||
self.inner_size.set(new_size);
|
self.inner_size.set(new_size);
|
||||||
webview.notify_rendering_context_resized();
|
webview.notify_rendering_context_resized();
|
||||||
}
|
}
|
||||||
|
@ -658,6 +690,10 @@ impl WindowPortsMethods for Window {
|
||||||
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>) {
|
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>) {
|
||||||
self.toolbar_height.set(height);
|
self.toolbar_height.set(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rendering_context(&self) -> Rc<dyn RenderingContext> {
|
||||||
|
self.rendering_context.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowMethods for Window {
|
impl WindowMethods for Window {
|
||||||
|
@ -671,7 +707,9 @@ impl WindowMethods for Window {
|
||||||
let window_rect = (window_rect.to_f64() / window_scale).to_i32();
|
let window_rect = (window_rect.to_f64() / window_scale).to_i32();
|
||||||
|
|
||||||
let viewport_origin = DeviceIntPoint::zero(); // bottom left
|
let viewport_origin = DeviceIntPoint::zero(); // bottom left
|
||||||
let viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32();
|
let mut viewport_size = winit_size_to_euclid_size(self.winit_window.inner_size()).to_f32();
|
||||||
|
viewport_size.height -= (self.toolbar_height() * self.hidpi_factor()).0;
|
||||||
|
|
||||||
let viewport = DeviceIntRect::from_origin_and_size(viewport_origin, viewport_size.to_i32());
|
let viewport = DeviceIntRect::from_origin_and_size(viewport_origin, viewport_size.to_i32());
|
||||||
let screen_size = self.screen_size.to_i32();
|
let screen_size = self.screen_size.to_i32();
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,9 @@ use euclid::{Box2D, Length, Point2D, Scale, Size2D};
|
||||||
use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods};
|
use servo::compositing::windowing::{AnimationState, EmbedderCoordinates, WindowMethods};
|
||||||
use servo::servo_geometry::DeviceIndependentPixel;
|
use servo::servo_geometry::DeviceIndependentPixel;
|
||||||
use servo::webrender_api::units::{DeviceIntSize, DevicePixel};
|
use servo::webrender_api::units::{DeviceIntSize, DevicePixel};
|
||||||
|
use servo::webrender_traits::rendering_context::RenderingContext;
|
||||||
|
use servo::webrender_traits::SurfmanRenderingContext;
|
||||||
|
use surfman::Connection;
|
||||||
|
|
||||||
use super::app_state::RunningAppState;
|
use super::app_state::RunningAppState;
|
||||||
use crate::desktop::window_trait::WindowPortsMethods;
|
use crate::desktop::window_trait::WindowPortsMethods;
|
||||||
|
@ -24,19 +27,31 @@ pub struct Window {
|
||||||
inner_size: Cell<DeviceIntSize>,
|
inner_size: Cell<DeviceIntSize>,
|
||||||
screen_size: Size2D<i32, DeviceIndependentPixel>,
|
screen_size: Size2D<i32, DeviceIndependentPixel>,
|
||||||
window_rect: Box2D<i32, DeviceIndependentPixel>,
|
window_rect: Box2D<i32, DeviceIndependentPixel>,
|
||||||
|
rendering_context: SurfmanRenderingContext,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Window {
|
impl Window {
|
||||||
#[allow(clippy::new_ret_no_self)]
|
#[allow(clippy::new_ret_no_self)]
|
||||||
pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc<dyn WindowPortsMethods> {
|
pub fn new(servoshell_preferences: &ServoShellPreferences) -> Rc<dyn WindowPortsMethods> {
|
||||||
|
let size = servoshell_preferences.initial_window_size;
|
||||||
|
let connection = Connection::new().expect("Failed to create connection");
|
||||||
|
let adapter = connection
|
||||||
|
.create_software_adapter()
|
||||||
|
.expect("Failed to create adapter");
|
||||||
|
let rendering_context = SurfmanRenderingContext::create(
|
||||||
|
&connection,
|
||||||
|
&adapter,
|
||||||
|
Some(size.to_untyped().to_i32()),
|
||||||
|
)
|
||||||
|
.expect("Failed to create WR surfman");
|
||||||
|
|
||||||
let device_pixel_ratio_override = servoshell_preferences.device_pixel_ratio_override;
|
let device_pixel_ratio_override = servoshell_preferences.device_pixel_ratio_override;
|
||||||
let device_pixel_ratio_override: Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> =
|
let device_pixel_ratio_override: Option<Scale<f32, DeviceIndependentPixel, DevicePixel>> =
|
||||||
device_pixel_ratio_override.map(Scale::new);
|
device_pixel_ratio_override.map(Scale::new);
|
||||||
let hidpi_factor = device_pixel_ratio_override.unwrap_or_else(Scale::identity);
|
let hidpi_factor = device_pixel_ratio_override.unwrap_or_else(Scale::identity);
|
||||||
|
|
||||||
let size = servoshell_preferences.initial_window_size.to_i32();
|
|
||||||
let inner_size = Cell::new((size.to_f32() * hidpi_factor).to_i32());
|
let inner_size = Cell::new((size.to_f32() * hidpi_factor).to_i32());
|
||||||
let window_rect = Box2D::from_origin_and_size(Point2D::zero(), size);
|
let window_rect = Box2D::from_origin_and_size(Point2D::zero(), size.to_i32());
|
||||||
|
|
||||||
let screen_size = servoshell_preferences.screen_size_override.map_or_else(
|
let screen_size = servoshell_preferences.screen_size_override.map_or_else(
|
||||||
|| window_rect.size(),
|
|| window_rect.size(),
|
||||||
|
@ -50,6 +65,7 @@ impl Window {
|
||||||
inner_size,
|
inner_size,
|
||||||
screen_size,
|
screen_size,
|
||||||
window_rect,
|
window_rect,
|
||||||
|
rendering_context,
|
||||||
};
|
};
|
||||||
|
|
||||||
Rc::new(window)
|
Rc::new(window)
|
||||||
|
@ -132,6 +148,12 @@ impl WindowPortsMethods for Window {
|
||||||
fn set_toolbar_height(&self, _height: Length<f32, DeviceIndependentPixel>) {
|
fn set_toolbar_height(&self, _height: Length<f32, DeviceIndependentPixel>) {
|
||||||
unimplemented!("headless Window only")
|
unimplemented!("headless Window only")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn rendering_context(&self) -> Rc<dyn RenderingContext> {
|
||||||
|
// `SurfmanRenderingContext` uses shared ownership internally so cloning it here does
|
||||||
|
// not create a new one really.
|
||||||
|
Rc::new(self.rendering_context.clone())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowMethods for Window {
|
impl WindowMethods for Window {
|
||||||
|
|
|
@ -3,27 +3,25 @@
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
use std::cell::{Cell, RefCell};
|
use std::cell::{Cell, RefCell};
|
||||||
use std::num::NonZeroU32;
|
use std::rc::Rc;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use egui::text::{CCursor, CCursorRange};
|
use egui::text::{CCursor, CCursorRange};
|
||||||
use egui::text_edit::TextEditState;
|
use egui::text_edit::TextEditState;
|
||||||
use egui::{
|
use egui::{
|
||||||
pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, Pos2, SelectableLabel,
|
pos2, CentralPanel, Frame, Key, Label, Modifiers, PaintCallback, SelectableLabel,
|
||||||
TopBottomPanel, Vec2,
|
TopBottomPanel, Vec2,
|
||||||
};
|
};
|
||||||
use egui_glow::CallbackFn;
|
use egui_glow::CallbackFn;
|
||||||
use egui_winit::EventResponse;
|
use egui_winit::EventResponse;
|
||||||
use euclid::{Box2D, Length, Point2D, Scale, Size2D};
|
use euclid::{Box2D, Length, Point2D, Rect, Scale, Size2D};
|
||||||
use gleam::gl;
|
|
||||||
use glow::NativeFramebuffer;
|
|
||||||
use log::{trace, warn};
|
use log::{trace, warn};
|
||||||
use servo::base::id::WebViewId;
|
use servo::base::id::WebViewId;
|
||||||
use servo::servo_geometry::DeviceIndependentPixel;
|
use servo::servo_geometry::DeviceIndependentPixel;
|
||||||
use servo::servo_url::ServoUrl;
|
use servo::servo_url::ServoUrl;
|
||||||
use servo::webrender_api::units::DevicePixel;
|
use servo::webrender_api::units::DevicePixel;
|
||||||
use servo::webrender_traits::SurfmanRenderingContext;
|
use servo::webrender_traits::rendering_context::{OffscreenRenderingContext, RenderingContext};
|
||||||
use servo::{LoadStatus, WebView};
|
use servo::{LoadStatus, WebView};
|
||||||
use winit::event::{ElementState, MouseButton, WindowEvent};
|
use winit::event::{ElementState, MouseButton, WindowEvent};
|
||||||
use winit::event_loop::ActiveEventLoop;
|
use winit::event_loop::ActiveEventLoop;
|
||||||
|
@ -34,14 +32,11 @@ use super::egui_glue::EguiGlow;
|
||||||
use super::geometry::winit_position_to_euclid_point;
|
use super::geometry::winit_position_to_euclid_point;
|
||||||
|
|
||||||
pub struct Minibrowser {
|
pub struct Minibrowser {
|
||||||
|
rendering_context: Rc<OffscreenRenderingContext>,
|
||||||
pub context: EguiGlow,
|
pub context: EguiGlow,
|
||||||
pub event_queue: RefCell<Vec<MinibrowserEvent>>,
|
pub event_queue: RefCell<Vec<MinibrowserEvent>>,
|
||||||
pub toolbar_height: Length<f32, DeviceIndependentPixel>,
|
pub toolbar_height: Length<f32, DeviceIndependentPixel>,
|
||||||
|
|
||||||
/// The framebuffer object name for the widget surface we should draw to, or None if our widget
|
|
||||||
/// surface does not use a framebuffer object.
|
|
||||||
widget_surface_fbo: Option<NativeFramebuffer>,
|
|
||||||
|
|
||||||
last_update: Instant,
|
last_update: Instant,
|
||||||
last_mouse_position: Option<Point2D<f32, DeviceIndependentPixel>>,
|
last_mouse_position: Option<Point2D<f32, DeviceIndependentPixel>>,
|
||||||
location: RefCell<String>,
|
location: RefCell<String>,
|
||||||
|
@ -81,12 +76,14 @@ impl Drop for Minibrowser {
|
||||||
|
|
||||||
impl Minibrowser {
|
impl Minibrowser {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
rendering_context: &SurfmanRenderingContext,
|
rendering_context: Rc<OffscreenRenderingContext>,
|
||||||
event_loop: &ActiveEventLoop,
|
event_loop: &ActiveEventLoop,
|
||||||
initial_url: ServoUrl,
|
initial_url: ServoUrl,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let gl = unsafe {
|
let gl = unsafe {
|
||||||
glow::Context::from_loader_function(|s| rendering_context.get_proc_address(s))
|
glow::Context::from_loader_function(|s| {
|
||||||
|
rendering_context.parent_context().get_proc_address(s)
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
// Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs
|
// Adapted from https://github.com/emilk/egui/blob/9478e50d012c5138551c38cbee16b07bc1fcf283/crates/egui_glow/examples/pure_glow.rs
|
||||||
|
@ -99,17 +96,11 @@ impl Minibrowser {
|
||||||
.egui_ctx
|
.egui_ctx
|
||||||
.options_mut(|options| options.zoom_with_keyboard = false);
|
.options_mut(|options| options.zoom_with_keyboard = false);
|
||||||
|
|
||||||
let widget_surface_fbo = match rendering_context.context_surface_info() {
|
|
||||||
Ok(Some(info)) => info.framebuffer_object,
|
|
||||||
Ok(None) => panic!("Failed to get widget surface info from surfman!"),
|
|
||||||
Err(error) => panic!("Failed to get widget surface info from surfman! {error:?}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
rendering_context,
|
||||||
context,
|
context,
|
||||||
event_queue: RefCell::new(vec![]),
|
event_queue: RefCell::new(vec![]),
|
||||||
toolbar_height: Default::default(),
|
toolbar_height: Default::default(),
|
||||||
widget_surface_fbo,
|
|
||||||
last_update: Instant::now(),
|
last_update: Instant::now(),
|
||||||
last_mouse_position: None,
|
last_mouse_position: None,
|
||||||
location: RefCell::new(initial_url.to_string()),
|
location: RefCell::new(initial_url.to_string()),
|
||||||
|
@ -268,17 +259,16 @@ impl Minibrowser {
|
||||||
reason
|
reason
|
||||||
);
|
);
|
||||||
let Self {
|
let Self {
|
||||||
|
rendering_context,
|
||||||
context,
|
context,
|
||||||
event_queue,
|
event_queue,
|
||||||
toolbar_height,
|
toolbar_height,
|
||||||
widget_surface_fbo,
|
|
||||||
last_update,
|
last_update,
|
||||||
location,
|
location,
|
||||||
location_dirty,
|
location_dirty,
|
||||||
..
|
..
|
||||||
} = self;
|
} = self;
|
||||||
let widget_fbo = *widget_surface_fbo;
|
|
||||||
let servo_framebuffer_id = state.servo().offscreen_framebuffer_id();
|
|
||||||
let _duration = context.run(window, |ctx| {
|
let _duration = context.run(window, |ctx| {
|
||||||
// TODO: While in fullscreen add some way to mitigate the increased phishing risk
|
// TODO: While in fullscreen add some way to mitigate the increased phishing risk
|
||||||
// when not displaying the URL bar: https://github.com/servo/servo/issues/32443
|
// when not displaying the URL bar: https://github.com/servo/servo/issues/32443
|
||||||
|
@ -387,26 +377,23 @@ impl Minibrowser {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
CentralPanel::default().frame(Frame::NONE).show(ctx, |ui| {
|
CentralPanel::default().frame(Frame::NONE).show(ctx, |ui| {
|
||||||
let Pos2 { x, y } = ui.cursor().min;
|
// If the top parts of the GUI changed size, then update the size of the WebView and also
|
||||||
let Vec2 {
|
// the size of its RenderingContext.
|
||||||
x: width,
|
let available_size = ui.available_size();
|
||||||
y: height,
|
let rect = Box2D::from_origin_and_size(
|
||||||
} = ui.available_size();
|
Point2D::origin(),
|
||||||
let rect =
|
Size2D::new(available_size.x, available_size.y),
|
||||||
Box2D::from_origin_and_size(Point2D::new(x, y), Size2D::new(width, height)) *
|
) * scale;
|
||||||
scale;
|
|
||||||
if rect != webview.rect() {
|
if rect != webview.rect() {
|
||||||
webview.move_resize(rect);
|
webview.move_resize(rect);
|
||||||
|
rendering_context.resize(rect.size().to_i32().to_untyped());
|
||||||
}
|
}
|
||||||
|
|
||||||
let min = ui.cursor().min;
|
let min = ui.cursor().min;
|
||||||
let size = ui.available_size();
|
let size = ui.available_size();
|
||||||
let rect = egui::Rect::from_min_size(min, size);
|
let rect = egui::Rect::from_min_size(min, size);
|
||||||
ui.allocate_space(size);
|
ui.allocate_space(size);
|
||||||
|
|
||||||
let Some(servo_fbo) = servo_framebuffer_id else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(status_text) = &self.status_text {
|
if let Some(status_text) = &self.status_text {
|
||||||
egui::containers::popup::show_tooltip_at(
|
egui::containers::popup::show_tooltip_at(
|
||||||
ctx,
|
ctx,
|
||||||
|
@ -417,45 +404,19 @@ impl Minibrowser {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.painter().add(PaintCallback {
|
if let Some(render_to_parent) = rendering_context.render_to_parent_callback() {
|
||||||
rect,
|
ui.painter().add(PaintCallback {
|
||||||
callback: Arc::new(CallbackFn::new(move |info, painter| {
|
rect,
|
||||||
use glow::HasContext as _;
|
callback: Arc::new(CallbackFn::new(move |info, painter| {
|
||||||
let clip = info.viewport_in_pixels();
|
let clip = info.viewport_in_pixels();
|
||||||
let x = clip.left_px as gl::GLint;
|
let rect_in_parent = Rect::new(
|
||||||
let y = clip.from_bottom_px as gl::GLint;
|
Point2D::new(clip.left_px, clip.from_bottom_px),
|
||||||
let width = clip.width_px as gl::GLsizei;
|
Size2D::new(clip.width_px, clip.height_px),
|
||||||
let height = clip.height_px as gl::GLsizei;
|
|
||||||
unsafe {
|
|
||||||
painter.gl().clear_color(0.0, 0.0, 0.0, 0.0);
|
|
||||||
painter.gl().scissor(x, y, width, height);
|
|
||||||
painter.gl().enable(gl::SCISSOR_TEST);
|
|
||||||
painter.gl().clear(gl::COLOR_BUFFER_BIT);
|
|
||||||
painter.gl().disable(gl::SCISSOR_TEST);
|
|
||||||
|
|
||||||
let servo_fbo = NonZeroU32::new(servo_fbo).map(NativeFramebuffer);
|
|
||||||
painter
|
|
||||||
.gl()
|
|
||||||
.bind_framebuffer(gl::READ_FRAMEBUFFER, servo_fbo);
|
|
||||||
painter
|
|
||||||
.gl()
|
|
||||||
.bind_framebuffer(gl::DRAW_FRAMEBUFFER, widget_fbo);
|
|
||||||
painter.gl().blit_framebuffer(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
x + width,
|
|
||||||
y + height,
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
x + width,
|
|
||||||
y + height,
|
|
||||||
gl::COLOR_BUFFER_BIT,
|
|
||||||
gl::NEAREST,
|
|
||||||
);
|
);
|
||||||
painter.gl().bind_framebuffer(gl::FRAMEBUFFER, widget_fbo);
|
render_to_parent(painter.gl(), rect_in_parent)
|
||||||
}
|
})),
|
||||||
})),
|
});
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
*last_update = now;
|
*last_update = now;
|
||||||
|
@ -464,14 +425,11 @@ impl Minibrowser {
|
||||||
|
|
||||||
/// Paint the minibrowser, as of the last update.
|
/// Paint the minibrowser, as of the last update.
|
||||||
pub fn paint(&mut self, window: &Window) {
|
pub fn paint(&mut self, window: &Window) {
|
||||||
unsafe {
|
self.rendering_context
|
||||||
use glow::HasContext as _;
|
.parent_context()
|
||||||
self.context
|
.prepare_for_rendering();
|
||||||
.painter
|
|
||||||
.gl()
|
|
||||||
.bind_framebuffer(gl::FRAMEBUFFER, self.widget_surface_fbo);
|
|
||||||
}
|
|
||||||
self.context.paint(window);
|
self.context.paint(window);
|
||||||
|
let _ = self.rendering_context.parent_context().present();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates the location field from the given [WebViewManager], unless the user has started
|
/// Updates the location field from the given [WebViewManager], unless the user has started
|
||||||
|
|
|
@ -11,6 +11,7 @@ use euclid::{Length, Scale};
|
||||||
use servo::compositing::windowing::WindowMethods;
|
use servo::compositing::windowing::WindowMethods;
|
||||||
use servo::servo_geometry::DeviceIndependentPixel;
|
use servo::servo_geometry::DeviceIndependentPixel;
|
||||||
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
|
use servo::webrender_api::units::{DeviceIntPoint, DeviceIntSize, DevicePixel};
|
||||||
|
use servo::webrender_traits::rendering_context::RenderingContext;
|
||||||
use servo::{Cursor, WebView};
|
use servo::{Cursor, WebView};
|
||||||
|
|
||||||
use super::app_state::RunningAppState;
|
use super::app_state::RunningAppState;
|
||||||
|
@ -46,4 +47,5 @@ pub trait WindowPortsMethods: WindowMethods {
|
||||||
fn winit_window(&self) -> Option<&winit::window::Window>;
|
fn winit_window(&self) -> Option<&winit::window::Window>;
|
||||||
fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel>;
|
fn toolbar_height(&self) -> Length<f32, DeviceIndependentPixel>;
|
||||||
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>);
|
fn set_toolbar_height(&self, height: Length<f32, DeviceIndependentPixel>);
|
||||||
|
fn rendering_context(&self) -> Rc<dyn RenderingContext>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,6 @@ class MachCommands(CommandBase):
|
||||||
self_contained_tests = [
|
self_contained_tests = [
|
||||||
"background_hang_monitor",
|
"background_hang_monitor",
|
||||||
"base",
|
"base",
|
||||||
"compositing",
|
|
||||||
"constellation",
|
"constellation",
|
||||||
"fonts",
|
"fonts",
|
||||||
"hyper_serde",
|
"hyper_serde",
|
||||||
|
@ -175,6 +174,7 @@ class MachCommands(CommandBase):
|
||||||
"servo_config",
|
"servo_config",
|
||||||
"servoshell",
|
"servoshell",
|
||||||
"style_config",
|
"style_config",
|
||||||
|
"webrender_traits",
|
||||||
]
|
]
|
||||||
if not packages:
|
if not packages:
|
||||||
packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store'])
|
packages = set(os.listdir(path.join(self.context.topdir, "tests", "unit"))) - set(['.DS_Store'])
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue