diff options
author | Martin Robinson <mrobinson@igalia.com> | 2025-02-20 19:27:49 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-02-20 18:27:49 +0000 |
commit | 54b5c7b632ecb48f134b232ee1b9aa8bc8c286c0 (patch) | |
tree | 3e3a5a3e21a0a27e79eab56912dbda181d48ecf1 | |
parent | 7d33e72bfca2d2257ca512f1b28f305f7b3a01d5 (diff) | |
download | servo-54b5c7b632ecb48f134b232ee1b9aa8bc8c286c0.tar.gz servo-54b5c7b632ecb48f134b232ee1b9aa8bc8c286c0.zip |
compositing: Move image output and shutdown management out of the compositor (#35538)
This is a step toward the renderer-per-WebView goal. It moves various
details out of `IOCompositor`.
- Image output: This is moved to servoshell as now applications can
access the image contents of a `WebView` via
`RenderingContext::read_to_image`. Most options for this are moved to
`ServoShellPreferences` apart from `wait_for_stable_image` as this
requires a specific kind of coordination in the `ScriptThread` that is
also very expensive. Instead, paint is now simply delayed until a
stable image is reached and `WebView::paint()` returns a boolean.
Maybe this can be revisited in the future.
- Shutdown: Shutdown is now managed by libservo itself. Shutdown state
is shared between the compositor and `Servo` instance. In the future,
this sharing might be unecessary.
- `CompositeTarget` has been removed entirely. This no longer needs to
be passed when creating a Servo instance.
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Ngo Iok Ui (Wu Yu Wei) <yuweiwu@pm.me>
25 files changed, 233 insertions, 270 deletions
diff --git a/Cargo.lock b/Cargo.lock index bc88bdcca9e..9f6379d8742 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1042,7 +1042,6 @@ dependencies = [ "euclid", "fnv", "gleam", - "image", "ipc-channel", "libc", "log", diff --git a/components/compositing/Cargo.toml b/components/compositing/Cargo.toml index 955ceead971..3b1811b923d 100644 --- a/components/compositing/Cargo.toml +++ b/components/compositing/Cargo.toml @@ -26,7 +26,6 @@ embedder_traits = { workspace = true } euclid = { workspace = true } fnv = { workspace = true } gleam = { workspace = true } -image = { workspace = true } ipc-channel = { workspace = true } libc = { workspace = true } log = { workspace = true } diff --git a/components/compositing/compositor.rs b/components/compositing/compositor.rs index b35adcf1dbb..6d34e8b96b1 100644 --- a/components/compositing/compositor.rs +++ b/components/compositing/compositor.rs @@ -22,11 +22,10 @@ use compositing_traits::{ use crossbeam_channel::Sender; use embedder_traits::{ Cursor, InputEvent, MouseButton, MouseButtonAction, MouseButtonEvent, MouseMoveEvent, - TouchAction, TouchEvent, TouchEventType, TouchId, + ShutdownState, TouchAction, TouchEvent, TouchEventType, TouchId, }; use euclid::{Point2D, Rect, Scale, Size2D, Transform3D, Vector2D}; use fnv::{FnvHashMap, FnvHashSet}; -use image::{DynamicImage, ImageFormat}; use ipc_channel::ipc::{self, IpcSharedMemory}; use libc::c_void; use log::{debug, error, info, trace, warn}; @@ -37,6 +36,7 @@ use script_traits::{ AnimationState, AnimationTickType, EventResult, ScriptThreadMessage, ScrollState, WindowSizeData, WindowSizeType, }; +use servo_config::opts; use servo_geometry::DeviceIndependentPixel; use style_traits::{CSSPixel, PinchZoomFactor}; use webrender::{CaptureBits, RenderApi, Transaction}; @@ -102,8 +102,8 @@ pub struct ServoRenderer { webviews: WebViewManager<WebView>, /// Tracks whether we are in the process of shutting down, or have shut down and should close - /// the compositor. - shutdown_state: ShutdownState, + /// the compositor. This is shared with the `Servo` instance. + shutdown_state: Rc<Cell<ShutdownState>>, /// The port on which we receive messages. compositor_receiver: CompositorReceiver, @@ -120,9 +120,6 @@ pub struct ServoRenderer { /// The GL bindings for webrender webrender_gl: Rc<dyn gleam::gl::Gl>, - /// True to exit after page load ('-x'). - exit_after_load: bool, - /// The string representing the version of Servo that is running. This is used to tag /// WebRender capture output. version_string: String, @@ -153,9 +150,6 @@ pub struct IOCompositor { /// "Desktop-style" zoom that resizes the viewport to fit the window. page_zoom: Scale<f32, CSSPixel, DeviceIndependentPixel>, - /// The type of composition to perform - composite_target: CompositeTarget, - /// Tracks whether or not the view needs to be repainted. needs_repaint: Cell<RepaintReason>, @@ -250,13 +244,6 @@ bitflags! { } } -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum ShutdownState { - NotShuttingDown, - ShuttingDown, - FinishedShuttingDown, -} - struct PipelineDetails { /// The pipeline associated with this PipelineDetails object. pipeline: Option<CompositionPipeline>, @@ -324,38 +311,22 @@ impl PipelineDetails { } } -#[derive(Clone, Debug, PartialEq)] -pub enum CompositeTarget { - /// Draw to a OpenGL framebuffer object that will then be used by the compositor to composite - /// to [`RenderingContext::framebuffer_object`] - ContextFbo, - - /// Draw to an uncompressed image in shared memory. - SharedMemory, - - /// Draw to a PNG file on disk, then exit the browser (for reftests). - PngFile(Rc<String>), -} - impl IOCompositor { pub fn new( window: Rc<dyn WindowMethods>, state: InitialCompositorState, - composite_target: CompositeTarget, - exit_after_load: bool, convert_mouse_to_touch: bool, version_string: String, ) -> Self { let compositor = IOCompositor { global: ServoRenderer { - shutdown_state: ShutdownState::NotShuttingDown, + shutdown_state: state.shutdown_state, webviews: WebViewManager::default(), compositor_receiver: state.receiver, constellation_sender: state.constellation_chan, time_profiler_chan: state.time_profiler_chan, webrender_api: state.webrender_api, webrender_gl: state.webrender_gl, - exit_after_load, version_string, #[cfg(feature = "webxr")] webxr_main_thread: state.webxr_main_thread, @@ -366,7 +337,6 @@ impl IOCompositor { needs_repaint: Cell::default(), touch_handler: TouchHandler::new(), pending_scroll_zoom_events: Vec::new(), - composite_target, page_zoom: Scale::new(1.0), viewport_zoom: PinchZoomFactor::new(1.0), min_viewport_zoom: Some(PinchZoomFactor::new(1.0)), @@ -394,7 +364,7 @@ impl IOCompositor { } pub fn shutdown_state(&self) -> ShutdownState { - self.global.shutdown_state + self.global.shutdown_state.get() } pub fn deinit(&mut self) { @@ -441,27 +411,7 @@ impl IOCompositor { } } - pub fn start_shutting_down(&mut self) { - if self.global.shutdown_state != ShutdownState::NotShuttingDown { - warn!("Requested shutdown while already shutting down"); - return; - } - - debug!("Compositor sending Exit message to Constellation"); - if let Err(e) = self - .global - .constellation_sender - .send(ConstellationMsg::Exit) - { - warn!("Sending exit message to constellation failed ({:?}).", e); - } - - self.global.shutdown_state = ShutdownState::ShuttingDown; - } - - fn finish_shutting_down(&mut self) { - debug!("Compositor received message that constellation shutdown is complete"); - + pub fn finish_shutting_down(&mut self) { // Drain compositor port, sometimes messages contain channels that are blocking // another thread from finishing (i.e. SetFrameTree). while self @@ -478,14 +428,12 @@ impl IOCompositor { .send(profile_time::ProfilerMsg::Exit(sender)); let _ = receiver.recv(); } - - self.global.shutdown_state = ShutdownState::FinishedShuttingDown; } fn handle_browser_message(&mut self, msg: CompositorMsg) { trace_msg_from_constellation!(msg, "{msg:?}"); - match self.global.shutdown_state { + match self.shutdown_state() { ShutdownState::NotShuttingDown => {}, ShutdownState::ShuttingDown => { self.handle_browser_message_while_shutting_down(msg); @@ -498,11 +446,6 @@ impl IOCompositor { } match msg { - CompositorMsg::ShutdownComplete => { - error!("Received `ShutdownComplete` while not shutting down."); - self.finish_shutting_down(); - }, - CompositorMsg::ChangeRunningAnimationsState(pipeline_id, animation_state) => { self.change_running_animations_state(pipeline_id, animation_state); }, @@ -521,7 +464,7 @@ impl IOCompositor { }, CompositorMsg::CreatePng(page_rect, reply) => { - let res = self.composite_specific_target(CompositeTarget::SharedMemory, page_rect); + let res = self.render_to_shared_memory(page_rect); if let Err(ref e) = res { info!("Error retrieving PNG: {:?}", e); } @@ -570,10 +513,7 @@ impl IOCompositor { }, CompositorMsg::LoadComplete(_) => { - // If we're painting in headless mode, schedule a recomposite. - if matches!(self.composite_target, CompositeTarget::PngFile(_)) || - self.global.exit_after_load - { + if opts::get().wait_for_stable_image { self.set_needs_repaint(RepaintReason::ReadyForScreenshot); } }, @@ -850,9 +790,6 @@ impl IOCompositor { /// compositor no longer does any WebRender frame generation. fn handle_browser_message_while_shutting_down(&mut self, msg: CompositorMsg) { match msg { - CompositorMsg::ShutdownComplete => { - self.finish_shutting_down(); - }, CompositorMsg::PipelineExited(pipeline_id, sender) => { debug!("Compositor got pipeline exited: {:?}", pipeline_id); self.remove_pipeline_root_layer(pipeline_id); @@ -1299,7 +1236,7 @@ impl IOCompositor { } pub fn on_rendering_context_resized(&mut self) -> bool { - if self.global.shutdown_state != ShutdownState::NotShuttingDown { + if self.shutdown_state() != ShutdownState::NotShuttingDown { return false; } @@ -1352,7 +1289,7 @@ impl IOCompositor { } pub fn on_input_event(&mut self, event: InputEvent) { - if self.global.shutdown_state != ShutdownState::NotShuttingDown { + if self.shutdown_state() != ShutdownState::NotShuttingDown { return; } @@ -1465,7 +1402,7 @@ impl IOCompositor { } pub fn on_touch_event(&mut self, event: TouchEvent) { - if self.global.shutdown_state != ShutdownState::NotShuttingDown { + if self.shutdown_state() != ShutdownState::NotShuttingDown { return; } @@ -1605,7 +1542,7 @@ impl IOCompositor { cursor: DeviceIntPoint, event_type: TouchEventType, ) { - if self.global.shutdown_state != ShutdownState::NotShuttingDown { + if self.shutdown_state() != ShutdownState::NotShuttingDown { return; } @@ -1822,9 +1759,6 @@ impl IOCompositor { } fn hidpi_factor(&self) -> Scale<f32, DeviceIndependentPixel, DevicePixel> { - if matches!(self.composite_target, CompositeTarget::PngFile(_)) { - return Scale::new(1.0); - } self.embedder_coordinates.hidpi_factor } @@ -1839,7 +1773,7 @@ impl IOCompositor { } pub fn on_zoom_reset_window_event(&mut self) { - if self.global.shutdown_state != ShutdownState::NotShuttingDown { + if self.shutdown_state() != ShutdownState::NotShuttingDown { return; } @@ -1848,7 +1782,7 @@ impl IOCompositor { } pub fn on_zoom_window_event(&mut self, magnification: f32) { - if self.global.shutdown_state != ShutdownState::NotShuttingDown { + if self.shutdown_state() != ShutdownState::NotShuttingDown { return; } @@ -1871,7 +1805,7 @@ impl IOCompositor { /// Simulate a pinch zoom pub fn on_pinch_zoom_window_event(&mut self, magnification: f32) { - if self.global.shutdown_state != ShutdownState::NotShuttingDown { + if self.shutdown_state() != ShutdownState::NotShuttingDown { return; } @@ -1985,10 +1919,12 @@ impl IOCompositor { } } - pub fn composite(&mut self) { - if let Err(error) = self.composite_specific_target(self.composite_target.clone(), None) { - warn!("Unable to composite: {error:?}"); - return; + /// Render the WebRender scene to the active `RenderingContext`. If successful, trigger + /// the next round of animations. + pub fn render(&mut self) -> bool { + if let Err(error) = self.render_inner() { + warn!("Unable to render: {error:?}"); + return false; } // We've painted the default target, which means that from the embedder's perspective, @@ -1998,28 +1934,54 @@ impl IOCompositor { // Queue up any subsequent paints for animations. self.process_animations(true); - if matches!(self.composite_target, CompositeTarget::PngFile(_)) || - self.global.exit_after_load - { - println!("Shutting down the Constellation after generating an output file or exit flag specified"); - self.start_shutting_down(); - } + true } - /// Composite to the given target if any, or the current target otherwise. - /// Returns Ok if composition was performed or Err if it was not possible to composite for some - /// reason. When the target is [CompositeTarget::SharedMemory], the image is read back from the - /// GPU and returned as Ok(Some(png::Image)), otherwise we return Ok(None). - #[cfg_attr( - feature = "tracing", - tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") - )] - fn composite_specific_target( + /// Render the WebRender scene to the shared memory, without updating other state of this + /// [`IOCompositor`]. If succesful return the output image in shared memory. + fn render_to_shared_memory( &mut self, - target: CompositeTarget, page_rect: Option<Rect<f32, CSSPixel>>, ) -> Result<Option<Image>, UnableToComposite> { + self.render_inner()?; + let size = self.embedder_coordinates.framebuffer.to_u32(); + let (x, y, width, height) = if let Some(rect) = page_rect { + let rect = self.device_pixels_per_page_pixel().transform_rect(&rect); + + let x = rect.origin.x as i32; + // We need to convert to the bottom-left origin coordinate + // system used by OpenGL + let y = (size.height as f32 - rect.origin.y - rect.size.height) as i32; + let w = rect.size.width as u32; + let h = rect.size.height as u32; + + (x, y, w, h) + } else { + (0, 0, size.width, size.height) + }; + + Ok(self + .rendering_context + .read_to_image(Rect::new( + Point2D::new(x as u32, y as u32), + Size2D::new(width, height), + )) + .map(|image| Image { + width: image.width(), + height: image.height(), + format: PixelFormat::RGBA8, + bytes: ipc::IpcSharedMemory::from_bytes(&image), + id: None, + cors_status: CorsStatus::Safe, + })) + } + + #[cfg_attr( + feature = "tracing", + tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") + )] + fn render_inner(&mut self) -> Result<(), UnableToComposite> { if let Err(err) = self.rendering_context.make_current() { warn!("Failed to make the rendering context current: {:?}", err); } @@ -2029,12 +1991,7 @@ impl IOCompositor { webrender.update(); } - let wait_for_stable_image = matches!( - target, - CompositeTarget::SharedMemory | CompositeTarget::PngFile(_) - ) || self.global.exit_after_load; - - if wait_for_stable_image { + if opts::get().wait_for_stable_image { // The current image may be ready to output. However, if there are animations active, // tick those instead and continue waiting for the image output to be stable AND // all active animations to complete. @@ -2071,64 +2028,7 @@ impl IOCompositor { ); self.send_pending_paint_metrics_messages_after_composite(); - - let (x, y, width, height) = if let Some(rect) = page_rect { - let rect = self.device_pixels_per_page_pixel().transform_rect(&rect); - - let x = rect.origin.x as i32; - // We need to convert to the bottom-left origin coordinate - // system used by OpenGL - let y = (size.height as f32 - rect.origin.y - rect.size.height) as i32; - let w = rect.size.width as u32; - let h = rect.size.height as u32; - - (x, y, w, h) - } else { - (0, 0, size.width, size.height) - }; - - let rv = match target { - CompositeTarget::ContextFbo => None, - CompositeTarget::SharedMemory => self - .rendering_context - .read_to_image(Rect::new( - Point2D::new(x as u32, y as u32), - Size2D::new(width, height), - )) - .map(|image| Image { - width: image.width(), - height: image.height(), - format: PixelFormat::RGBA8, - bytes: ipc::IpcSharedMemory::from_bytes(&image), - id: None, - cors_status: CorsStatus::Safe, - }), - CompositeTarget::PngFile(path) => { - time_profile!( - ProfilerCategory::ImageSaving, - None, - self.global.time_profiler_chan.clone(), - || match File::create(&*path) { - Ok(mut file) => { - if let Some(image) = self.rendering_context.read_to_image(Rect::new( - Point2D::new(x as u32, y as u32), - Size2D::new(width, height), - )) { - let dynamic_image = DynamicImage::ImageRgba8(image); - if let Err(e) = dynamic_image.write_to(&mut file, ImageFormat::Png) - { - error!("Failed to save {} ({}).", path, e); - } - } - }, - Err(e) => error!("Failed to create {} ({}).", path, e), - }, - ); - None - }, - }; - - Ok(rv) + Ok(()) } /// Send all pending paint metrics messages after a composite operation, which may advance @@ -2257,7 +2157,7 @@ impl IOCompositor { for msg in compositor_messages { self.handle_browser_message(msg); - if self.global.shutdown_state == ShutdownState::FinishedShuttingDown { + if self.shutdown_state() == ShutdownState::FinishedShuttingDown { return; } } @@ -2268,7 +2168,7 @@ impl IOCompositor { tracing::instrument(skip_all, fields(servo_profiling = true), level = "trace") )] pub fn perform_updates(&mut self) -> bool { - if self.global.shutdown_state == ShutdownState::FinishedShuttingDown { + if self.shutdown_state() == ShutdownState::FinishedShuttingDown { return false; } @@ -2292,7 +2192,7 @@ impl IOCompositor { if !self.pending_scroll_zoom_events.is_empty() { self.process_pending_scroll_events() } - self.global.shutdown_state != ShutdownState::FinishedShuttingDown + self.shutdown_state() != ShutdownState::FinishedShuttingDown } pub fn pinch_zoom_level(&self) -> Scale<f32, DevicePixel, DevicePixel> { diff --git a/components/compositing/lib.rs b/components/compositing/lib.rs index 33700145b54..3510304a5e5 100644 --- a/components/compositing/lib.rs +++ b/components/compositing/lib.rs @@ -4,16 +4,18 @@ #![deny(unsafe_code)] +use std::cell::Cell; use std::rc::Rc; use compositing_traits::{CompositorProxy, CompositorReceiver, ConstellationMsg}; use crossbeam_channel::Sender; +use embedder_traits::ShutdownState; use profile_traits::{mem, time}; use webrender::RenderApi; use webrender_api::DocumentId; use webrender_traits::rendering_context::RenderingContext; -pub use crate::compositor::{CompositeTarget, IOCompositor, ShutdownState}; +pub use crate::compositor::IOCompositor; #[macro_use] mod tracing; @@ -35,6 +37,9 @@ pub struct InitialCompositorState { pub time_profiler_chan: time::ProfilerChan, /// A channel to the memory profiler thread. pub mem_profiler_chan: mem::ProfilerChan, + /// A shared state which tracks whether Servo has started or has finished + /// shutting down. + pub shutdown_state: Rc<Cell<ShutdownState>>, /// Instance of webrender API pub webrender: webrender::Renderer, pub webrender_document: DocumentId, diff --git a/components/compositing/tracing.rs b/components/compositing/tracing.rs index 7b0c696a62b..1ef0b41971c 100644 --- a/components/compositing/tracing.rs +++ b/components/compositing/tracing.rs @@ -30,7 +30,6 @@ mod from_constellation { impl LogTarget for compositing_traits::CompositorMsg { fn log_target(&self) -> &'static str { match self { - Self::ShutdownComplete => target!("ShutdownComplete"), Self::ChangeRunningAnimationsState(..) => target!("ChangeRunningAnimationsState"), Self::CreateOrUpdateWebView(..) => target!("CreateOrUpdateWebView"), Self::RemoveWebView(..) => target!("RemoveWebView"), diff --git a/components/config/opts.rs b/components/config/opts.rs index fe5eff15e9d..64609160449 100644 --- a/components/config/opts.rs +++ b/components/config/opts.rs @@ -15,6 +15,11 @@ use servo_url::ServoUrl; /// Global flags for Servo, currently set on the command line. #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Opts { + /// Whether or not Servo should wait for web content to go into an idle state, therefore + /// likely producing a stable output image. This is useful for taking screenshots of pages + /// after they have loaded. + pub wait_for_stable_image: bool, + /// Whether or not the legacy layout system is enabled. pub legacy_layout: bool, @@ -44,8 +49,6 @@ pub struct Opts { pub user_stylesheets: Vec<(Vec<u8>, ServoUrl)>, - pub output_file: Option<String>, - /// True to exit on thread failure instead of displaying about:failure. pub hard_fail: bool, @@ -74,9 +77,6 @@ pub struct Opts { /// used for testing the hardening of the constellation. pub random_pipeline_closure_seed: Option<usize>, - /// True to exit after the page load (`-x`). - pub exit_after_load: bool, - /// Load shaders from disk. pub shaders_dir: Option<PathBuf>, @@ -194,6 +194,7 @@ pub enum OutputOptions { impl Default for Opts { fn default() -> Self { Self { + wait_for_stable_image: false, legacy_layout: false, time_profiling: None, time_profiler_trace_path: None, @@ -201,7 +202,6 @@ impl Default for Opts { nonincremental_layout: false, userscripts: None, user_stylesheets: Vec::new(), - output_file: None, hard_fail: true, webdriver_port: None, multiprocess: false, @@ -210,7 +210,6 @@ impl Default for Opts { random_pipeline_closure_seed: None, sandbox: false, debug: Default::default(), - exit_after_load: false, config_dir: None, shaders_dir: None, certificate_path: None, diff --git a/components/constellation/constellation.rs b/components/constellation/constellation.rs index 06f872992a2..2a199892561 100644 --- a/components/constellation/constellation.rs +++ b/components/constellation/constellation.rs @@ -2698,8 +2698,8 @@ where } } - debug!("Asking compositor to complete shutdown."); - self.compositor_proxy.send(CompositorMsg::ShutdownComplete); + debug!("Asking embedding layer to complete shutdown."); + self.embedder_proxy.send(EmbedderMsg::ShutdownComplete); debug!("Shutting-down IPC router thread in constellation."); ROUTER.shutdown(); diff --git a/components/constellation/tracing.rs b/components/constellation/tracing.rs index 6009b07b984..ee65167fa14 100644 --- a/components/constellation/tracing.rs +++ b/components/constellation/tracing.rs @@ -247,6 +247,7 @@ mod from_script { Self::RequestDevtoolsConnection(..) => target_variant!("RequestDevtoolsConnection"), Self::PlayGamepadHapticEffect(..) => target_variant!("PlayGamepadHapticEffect"), Self::StopGamepadHapticEffect(..) => target_variant!("StopGamepadHapticEffect"), + Self::ShutdownComplete => target_variant!("ShutdownComplete"), } } } diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index ee28c006d32..d29e178aa63 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -65,7 +65,7 @@ use script_traits::{ use selectors::attr::CaseSensitivity; use servo_arc::Arc as ServoArc; use servo_atoms::Atom; -use servo_config::pref; +use servo_config::{opts, pref}; use servo_geometry::{f32_rect_to_au_rect, DeviceIndependentIntRect, MaxRect}; use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl}; use style::dom::OpaqueNode; @@ -368,9 +368,6 @@ pub(crate) struct Window { /// Emits notifications when there is a relayout. relayout_event: bool, - /// True if it is safe to write to the image. - prepare_for_screenshot: bool, - /// Unminify Css. unminify_css: bool, @@ -2071,7 +2068,7 @@ impl Window { // When all these conditions are met, notify the constellation // that this pipeline is ready to write the image (from the script thread // perspective at least). - if self.prepare_for_screenshot && updating_the_rendering { + if opts::get().wait_for_stable_image && updating_the_rendering { // Checks if the html element has reftest-wait attribute present. // See http://testthewebforward.org/docs/reftests.html // and https://web-platform-tests.org/writing-tests/crashtest.html @@ -2166,7 +2163,7 @@ impl Window { /// If writing a screenshot, synchronously update the layout epoch that it set /// in the constellation. pub(crate) fn update_constellation_epoch(&self) { - if !self.prepare_for_screenshot { + if !opts::get().wait_for_stable_image { return; } @@ -2772,7 +2769,6 @@ impl Window { webrender_document: DocumentId, compositor_api: CrossProcessCompositorApi, relayout_event: bool, - prepare_for_screenshot: bool, unminify_js: bool, unminify_css: bool, local_script_source: Option<String>, @@ -2862,7 +2858,6 @@ impl Window { compositor_api, has_sent_idle_message: Cell::new(false), relayout_event, - prepare_for_screenshot, unminify_css, userscripts_path, player_context, diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index fcb7d714f4f..b11c814b604 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -298,9 +298,6 @@ pub struct ScriptThread { /// Emits notifications when there is a relayout. relayout_event: bool, - /// True if it is safe to write to the image. - prepare_for_screenshot: bool, - /// Unminify Javascript. unminify_js: bool, @@ -835,10 +832,6 @@ impl ScriptThread { system_font_service: Arc<SystemFontServiceProxy>, user_agent: Cow<'static, str>, ) -> ScriptThread { - let opts = opts::get(); - let prepare_for_screenshot = - opts.output_file.is_some() || opts.exit_after_load || opts.webdriver_port.is_some(); - let (self_sender, self_receiver) = unbounded(); let runtime = Runtime::new(Some(SendableTaskSource { sender: ScriptEventLoopSender::MainThread(self_sender.clone()), @@ -898,6 +891,7 @@ impl ScriptThread { webgpu_receiver: RefCell::new(crossbeam_channel::never()), }; + let opts = opts::get(); let senders = ScriptThreadSenders { self_sender, #[cfg(feature = "bluetooth")] @@ -946,7 +940,6 @@ impl ScriptThread { profile_script_events: opts.debug.profile_script_events, print_pwm: opts.print_pwm, relayout_event: opts.debug.relayout_event, - prepare_for_screenshot, unminify_js: opts.unminify_js, local_script_source: opts.local_script_source.clone(), unminify_css: opts.unminify_css, @@ -3099,7 +3092,6 @@ impl ScriptThread { self.webrender_document, self.compositor_api.clone(), self.relayout_event, - self.prepare_for_screenshot, self.unminify_js, self.unminify_css, self.local_script_source.clone(), diff --git a/components/servo/examples/winit_minimal.rs b/components/servo/examples/winit_minimal.rs index 388b9a6b749..1676081ea09 100644 --- a/components/servo/examples/winit_minimal.rs +++ b/components/servo/examples/winit_minimal.rs @@ -111,7 +111,6 @@ impl ApplicationHandler<WakerEvent> for App { }), window_delegate.clone(), Default::default(), - compositing::CompositeTarget::ContextFbo, ); servo.setup_logging(); diff --git a/components/servo/lib.rs b/components/servo/lib.rs index c078b392fc4..cabc2c40295 100644 --- a/components/servo/lib.rs +++ b/components/servo/lib.rs @@ -24,7 +24,7 @@ mod webview; mod webview_delegate; use std::borrow::Cow; -use std::cell::RefCell; +use std::cell::{Cell, RefCell}; use std::cmp::max; use std::collections::HashMap; use std::path::PathBuf; @@ -43,7 +43,7 @@ use canvas::WebGLComm; use canvas_traits::webgl::{GlType, WebGLThreads}; use clipboard_delegate::StringRequest; use compositing::windowing::{EmbedderMethods, WindowMethods}; -use compositing::{CompositeTarget, IOCompositor, InitialCompositorState, ShutdownState}; +use compositing::{IOCompositor, InitialCompositorState}; use compositing_traits::{CompositorMsg, CompositorProxy, CompositorReceiver, ConstellationMsg}; #[cfg(all( not(target_os = "windows"), @@ -79,7 +79,7 @@ use ipc_channel::router::ROUTER; pub use keyboard_types::*; #[cfg(feature = "layout_2013")] pub use layout_thread_2013; -use log::{warn, Log, Metadata, Record}; +use log::{debug, warn, Log, Metadata, Record}; use media::{GlApi, NativeDisplay, WindowGLContext}; use net::protocols::ProtocolRegistry; use net::resource_thread::new_resource_threads; @@ -201,6 +201,9 @@ pub struct Servo { compositor: Rc<RefCell<IOCompositor>>, constellation_proxy: ConstellationProxy, embedder_receiver: Receiver<EmbedderMsg>, + /// Tracks whether we are in the process of shutting down, or have shut down. + /// This is shared with `WebView`s and the `ServoRenderer`. + shutdown_state: Rc<Cell<ShutdownState>>, /// A map [`WebView`]s that are managed by this [`Servo`] instance. These are stored /// as `Weak` references so that the embedding application can control their lifetime. /// When accessed, `Servo` will be reponsible for cleaning up the invalid `Weak` @@ -261,7 +264,6 @@ impl Servo { mut embedder: Box<dyn EmbedderMethods>, window: Rc<dyn WindowMethods>, user_agent: Option<String>, - composite_target: CompositeTarget, ) -> Self { // Global configuration options, parsed from the command line. opts::set_options(opts); @@ -506,14 +508,9 @@ impl Servo { } } - let composite_target = if let Some(path) = opts.output_file.clone() { - CompositeTarget::PngFile(path.into()) - } else { - composite_target - }; - // The compositor coordinates with the client window to create the final // rendered page and display it somewhere. + let shutdown_state = Rc::new(Cell::new(ShutdownState::NotShuttingDown)); let compositor = IOCompositor::new( window, InitialCompositorState { @@ -529,18 +526,18 @@ impl Servo { webrender_gl, #[cfg(feature = "webxr")] webxr_main_thread, + shutdown_state: shutdown_state.clone(), }, - composite_target, - opts.exit_after_load, opts.debug.convert_mouse_to_touch, embedder.get_version_string().unwrap_or_default(), ); - Servo { + Self { delegate: RefCell::new(Rc::new(DefaultServoDelegate)), compositor: Rc::new(RefCell::new(compositor)), constellation_proxy: ConstellationProxy::new(constellation_chan), embedder_receiver, + shutdown_state, webviews: Default::default(), _js_engine_setup: js_engine_setup, } @@ -569,16 +566,18 @@ impl Servo { /// The return value of this method indicates whether or not Servo, false indicates that Servo /// has finished shutting down and you should not spin the event loop any longer. pub fn spin_event_loop(&self) -> bool { - if self.compositor.borrow().shutdown_state() == ShutdownState::FinishedShuttingDown { + if self.shutdown_state.get() == ShutdownState::FinishedShuttingDown { return false; } self.compositor.borrow_mut().receive_messages(); // Only handle incoming embedder messages if the compositor hasn't already started shutting down. - if self.compositor.borrow().shutdown_state() == ShutdownState::NotShuttingDown { - while let Ok(message) = self.embedder_receiver.try_recv() { - self.handle_embedder_message(message) + while let Ok(message) = self.embedder_receiver.try_recv() { + self.handle_embedder_message(message); + + if self.shutdown_state.get() == ShutdownState::FinishedShuttingDown { + break; } } @@ -591,7 +590,7 @@ impl Servo { self.send_new_frame_ready_messages(); self.clean_up_destroyed_webview_handles(); - if self.compositor.borrow().shutdown_state() == ShutdownState::FinishedShuttingDown { + if self.shutdown_state.get() == ShutdownState::FinishedShuttingDown { return false; } @@ -641,7 +640,20 @@ impl Servo { } pub fn start_shutting_down(&self) { - self.compositor.borrow_mut().start_shutting_down(); + if self.shutdown_state.get() != ShutdownState::NotShuttingDown { + warn!("Requested shutdown while already shutting down"); + return; + } + + debug!("Sending Exit message to Constellation"); + self.constellation_proxy.send(ConstellationMsg::Exit); + self.shutdown_state.set(ShutdownState::ShuttingDown); + } + + fn finish_shutting_down(&self) { + debug!("Servo received message that Constellation shutdown is complete"); + self.shutdown_state.set(ShutdownState::FinishedShuttingDown); + self.compositor.borrow_mut().finish_shutting_down(); } pub fn deinit(&self) { @@ -675,6 +687,7 @@ impl Servo { fn handle_embedder_message(&self, message: EmbedderMsg) { match message { + EmbedderMsg::ShutdownComplete => self.finish_shutting_down(), EmbedderMsg::Status(webview_id, status_text) => { if let Some(webview) = self.get_webview_handle(webview_id) { webview.set_status_text(status_text); diff --git a/components/servo/webview.rs b/components/servo/webview.rs index 6261f8205b3..10fcf321afb 100644 --- a/components/servo/webview.rs +++ b/components/servo/webview.rs @@ -436,7 +436,10 @@ impl WebView { .send(ConstellationMsg::SendError(Some(self.id()), message)); } - pub fn paint(&self) { - self.inner().compositor.borrow_mut().composite(); + /// Paint the contents of this [`WebView`] into its `RenderingContext`. This will + /// always paint, unless the `Opts::wait_for_stable_image` option is enabled. In + /// that case, this might do nothing. Returns true if a paint was actually performed. + pub fn paint(&self) -> bool { + self.inner().compositor.borrow_mut().render() } } diff --git a/components/shared/compositing/lib.rs b/components/shared/compositing/lib.rs index c8dcf2e7ea7..66bc59160fa 100644 --- a/components/shared/compositing/lib.rs +++ b/components/shared/compositing/lib.rs @@ -58,10 +58,6 @@ impl CompositorReceiver { /// Messages from (or via) the constellation thread to the compositor. pub enum CompositorMsg { - /// Informs the compositor that the constellation has completed shutdown. - /// Required because the constellation can have pending calls to make - /// (e.g. SetFrameTree) at the time that we send it an ExitMsg. - ShutdownComplete, /// Alerts the compositor that the given pipeline has changed whether it is running animations. ChangeRunningAnimationsState(PipelineId, AnimationState), /// Create or update a webview, given its frame tree. @@ -118,7 +114,6 @@ pub struct CompositionPipeline { impl Debug for CompositorMsg { fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { match *self { - CompositorMsg::ShutdownComplete => write!(f, "ShutdownComplete"), CompositorMsg::ChangeRunningAnimationsState(_, state) => { write!(f, "ChangeRunningAnimationsState({:?})", state) }, diff --git a/components/shared/embedder/lib.rs b/components/shared/embedder/lib.rs index 5d4bbc020fc..533826408c1 100644 --- a/components/shared/embedder/lib.rs +++ b/components/shared/embedder/lib.rs @@ -22,6 +22,15 @@ use webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize}; pub use crate::input_events::*; +/// Tracks whether Servo isn't shutting down, is in the process of shutting down, +/// or has finished shutting down. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ShutdownState { + NotShuttingDown, + ShuttingDown, + FinishedShuttingDown, +} + /// A cursor for the window. This is different from a CSS cursor (see /// `CursorKind`) in that it has no `Auto` value. #[repr(u8)] @@ -257,6 +266,10 @@ pub enum EmbedderMsg { PlayGamepadHapticEffect(WebViewId, usize, GamepadHapticEffectType, IpcSender<bool>), /// Request to stop a haptic effect on a connected gamepad. StopGamepadHapticEffect(WebViewId, usize, IpcSender<bool>), + /// Informs the embedder that the constellation has completed shutdown. + /// Required because the constellation can have pending calls to make + /// (e.g. SetFrameTree) at the time that we send it an ExitMsg. + ShutdownComplete, } impl Debug for EmbedderMsg { @@ -302,6 +315,7 @@ impl Debug for EmbedderMsg { EmbedderMsg::ShowContextMenu(..) => write!(f, "ShowContextMenu"), EmbedderMsg::PlayGamepadHapticEffect(..) => write!(f, "PlayGamepadHapticEffect"), EmbedderMsg::StopGamepadHapticEffect(..) => write!(f, "StopGamepadHapticEffect"), + EmbedderMsg::ShutdownComplete => write!(f, "ShutdownComplete"), } } } diff --git a/ports/servoshell/Cargo.toml b/ports/servoshell/Cargo.toml index 152cbe1f854..8864270e9fc 100644 --- a/ports/servoshell/Cargo.toml +++ b/ports/servoshell/Cargo.toml @@ -66,6 +66,7 @@ keyboard-types = { workspace = true } log = { workspace = true } getopts = { workspace = true } hitrace = { workspace = true, optional = true } +image = { workspace = true } mime_guess = { workspace = true } url = { workspace = true } raw-window-handle = { workspace = true } @@ -125,9 +126,6 @@ tinyfiledialogs = "3.0" egui-file-dialog = "0.9.0" winit = "0.30.9" -[target.'cfg(any(all(target_os = "linux", not(target_env = "ohos")), target_os = "windows"))'.dependencies] -image = { workspace = true } - [target.'cfg(any(all(target_os = "linux", not(target_env = "ohos")), target_os = "macos"))'.dependencies] sig = "1.0" diff --git a/ports/servoshell/desktop/app.rs b/ports/servoshell/desktop/app.rs index 54a49caceb3..28ba89a630e 100644 --- a/ports/servoshell/desktop/app.rs +++ b/ports/servoshell/desktop/app.rs @@ -12,7 +12,6 @@ use std::{env, fs}; use log::{info, trace, warn}; use servo::compositing::windowing::{AnimationState, WindowMethods}; -use servo::compositing::CompositeTarget; use servo::config::opts::Opts; use servo::config::prefs::Preferences; use servo::servo_config::pref; @@ -99,11 +98,7 @@ impl App { assert_eq!(headless, event_loop.is_none()); let window = match event_loop { Some(event_loop) => { - let window = headed_window::Window::new( - &self.opts, - &self.servoshell_preferences, - event_loop, - ); + let window = headed_window::Window::new(&self.servoshell_preferences, event_loop); self.minibrowser = Some(Minibrowser::new( window.offscreen_rendering_context(), event_loop, @@ -158,11 +153,14 @@ impl App { embedder, Rc::new(UpcastedWindow(window.clone())), self.servoshell_preferences.user_agent.clone(), - CompositeTarget::ContextFbo, ); servo.setup_logging(); - let running_state = Rc::new(RunningAppState::new(servo, window.clone(), headless)); + let running_state = Rc::new(RunningAppState::new( + servo, + window.clone(), + self.servoshell_preferences.clone(), + )); running_state.new_toplevel_webview(self.initial_url.clone().into_url()); if let Some(ref mut minibrowser) = self.minibrowser { diff --git a/ports/servoshell/desktop/app_state.rs b/ports/servoshell/desktop/app_state.rs index 3496e67337d..e504e9b7658 100644 --- a/ports/servoshell/desktop/app_state.rs +++ b/ports/servoshell/desktop/app_state.rs @@ -9,6 +9,7 @@ use std::rc::Rc; use std::thread; use euclid::Vector2D; +use image::DynamicImage; use keyboard_types::{Key, KeyboardEvent, Modifiers, ShortcutMatcher}; use log::{error, info}; use servo::base::id::WebViewId; @@ -30,6 +31,7 @@ use super::dialog::Dialog; use super::gamepad::GamepadSupport; use super::keyutils::CMD_OR_CONTROL; use super::window_trait::{WindowPortsMethods, LINE_HEIGHT}; +use crate::prefs::ServoShellPreferences; pub(crate) enum AppState { Initializing, @@ -42,13 +44,13 @@ pub(crate) struct RunningAppState { /// `inner` so that we can keep a reference to Servo in order to spin the event loop, /// which will in turn call delegates doing a mutable borrow on `inner`. servo: Servo, + /// The preferences for this run of servoshell. This is not mutable, so doesn't need to + /// be stored inside the [`RunningAppStateInner`]. + servoshell_preferences: ServoShellPreferences, inner: RefCell<RunningAppStateInner>, } pub struct RunningAppStateInner { - /// Whether or not this is a headless servoshell window. - headless: bool, - /// List of top-level browsing contexts. /// Modified by EmbedderMsg::WebViewOpened and EmbedderMsg::WebViewClosed, /// and we exit if it ever becomes empty. @@ -88,13 +90,13 @@ impl RunningAppState { pub fn new( servo: Servo, window: Rc<dyn WindowPortsMethods>, - headless: bool, + servoshell_preferences: ServoShellPreferences, ) -> RunningAppState { servo.set_delegate(Rc::new(ServoShellServoDelegate)); RunningAppState { servo, + servoshell_preferences, inner: RefCell::new(RunningAppStateInner { - headless, webviews: HashMap::default(), creation_order: Default::default(), focused_webview_id: None, @@ -125,6 +127,36 @@ impl RunningAppState { &self.servo } + pub(crate) fn save_output_image_if_necessary(&self) { + let Some(output_path) = self.servoshell_preferences.output_image_path.as_ref() else { + return; + }; + + let inner = self.inner(); + let viewport_rect = inner + .window + .get_coordinates() + .viewport + .to_rect() + .to_untyped() + .to_u32(); + let Some(image) = inner + .window + .rendering_context() + .read_to_image(viewport_rect) + else { + error!("Failed to read output image."); + return; + }; + + if let Err(error) = DynamicImage::ImageRgba8(image).save(output_path) { + error!("Failed to save {output_path}: {error}."); + } + } + + /// Repaint the Servo view is necessary, returning true if anything was actually + /// painted or false otherwise. Something may not be painted if Servo is waiting + /// for a stable image to paint. pub(crate) fn repaint_servo_if_necessary(&self) { if !self.inner().need_repaint { return; @@ -132,10 +164,21 @@ impl RunningAppState { let Some(webview) = self.focused_webview() else { return; }; + if !webview.paint() { + return; + } + + // This needs to be done before presenting(), because `ReneringContext::read_to_image` reads + // from the back buffer. + self.save_output_image_if_necessary(); - webview.paint(); - self.inner().window.rendering_context().present(); - self.inner_mut().need_repaint = false; + let mut inner_mut = self.inner_mut(); + inner_mut.window.rendering_context().present(); + inner_mut.need_repaint = false; + + if self.servoshell_preferences.exit_after_stable_image { + self.servo().start_shutting_down(); + } } /// Spins the internal application event loop. @@ -370,7 +413,7 @@ impl WebViewDelegate for RunningAppState { definition: PromptDefinition, _origin: PromptOrigin, ) { - if self.inner().headless { + if self.servoshell_preferences.headless { let _ = match definition { PromptDefinition::Alert(_message, sender) => sender.send(()), PromptDefinition::OkCancel(_message, sender) => sender.send(PromptResult::Primary), @@ -401,7 +444,7 @@ impl WebViewDelegate for RunningAppState { webview: WebView, authentication_request: AuthenticationRequest, ) { - if self.inner().headless { + if self.servoshell_preferences.headless { return; } @@ -493,7 +536,7 @@ impl WebViewDelegate for RunningAppState { } fn request_permission(&self, _webview: servo::WebView, request: PermissionRequest) { - if !self.inner().headless { + if !self.servoshell_preferences.headless { prompt_user(request); } } diff --git a/ports/servoshell/desktop/cli.rs b/ports/servoshell/desktop/cli.rs index a05cafb5e59..a77e20a5009 100644 --- a/ports/servoshell/desktop/cli.rs +++ b/ports/servoshell/desktop/cli.rs @@ -29,7 +29,8 @@ pub fn main() { crate::init_tracing(servoshell_preferences.tracing_filter.as_deref()); let clean_shutdown = servoshell_preferences.clean_shutdown; - let event_loop = EventsLoop::new(servoshell_preferences.headless, opts.output_file.is_some()) + let has_output_file = servoshell_preferences.output_image_path.is_some(); + let event_loop = EventsLoop::new(servoshell_preferences.headless, has_output_file) .expect("Failed to create events loop"); { diff --git a/ports/servoshell/desktop/headed_window.rs b/ports/servoshell/desktop/headed_window.rs index 58316ad1aae..c0706570d09 100644 --- a/ports/servoshell/desktop/headed_window.rs +++ b/ports/servoshell/desktop/headed_window.rs @@ -17,7 +17,6 @@ use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use servo::compositing::windowing::{ AnimationState, EmbedderCoordinates, WebRenderDebugOption, WindowMethods, }; -use servo::config::opts::Opts; use servo::servo_config::pref; use servo::servo_geometry::DeviceIndependentPixel; use servo::webrender_api::units::{DeviceIntPoint, DeviceIntRect, DeviceIntSize, DevicePixel}; @@ -78,24 +77,17 @@ pub struct Window { impl Window { pub fn new( - opts: &Opts, servoshell_preferences: &ServoShellPreferences, event_loop: &ActiveEventLoop, ) -> Window { - // If there's no chrome, start off with the window invisible. It will be set to visible in - // `load_end()`. This avoids an ugly flash of unstyled content (especially important since - // unstyled content is white and chrome often has a transparent background). See issue - // #9996. let no_native_titlebar = servoshell_preferences.no_native_titlebar; - let visible = opts.output_file.is_none() && !servoshell_preferences.no_native_titlebar; - let window_size = servoshell_preferences.initial_window_size; let window_attr = winit::window::Window::default_attributes() .with_title("Servo".to_string()) .with_decorations(!no_native_titlebar) .with_transparent(no_native_titlebar) .with_inner_size(LogicalSize::new(window_size.width, window_size.height)) - .with_visible(visible); + .with_visible(true); #[allow(deprecated)] let winit_window = event_loop diff --git a/ports/servoshell/egl/android/simpleservo.rs b/ports/servoshell/egl/android/simpleservo.rs index 53afb05f80b..4b2897269fb 100644 --- a/ports/servoshell/egl/android/simpleservo.rs +++ b/ports/servoshell/egl/android/simpleservo.rs @@ -7,7 +7,6 @@ use std::mem; use std::rc::Rc; use raw_window_handle::{DisplayHandle, RawDisplayHandle, RawWindowHandle, WindowHandle}; -use servo::compositing::CompositeTarget; pub use servo::webrender_api::units::DeviceIntRect; /// The EventLoopWaker::wake function will be called from any thread. /// It will be called to notify embedder that some events are available, @@ -97,7 +96,6 @@ pub fn init( embedder_callbacks, window_callbacks.clone(), None, - CompositeTarget::ContextFbo, ); APP.with(|app| { diff --git a/ports/servoshell/egl/ohos/simpleservo.rs b/ports/servoshell/egl/ohos/simpleservo.rs index 2b69ce952b4..3b40959d4d9 100644 --- a/ports/servoshell/egl/ohos/simpleservo.rs +++ b/ports/servoshell/egl/ohos/simpleservo.rs @@ -12,7 +12,6 @@ use raw_window_handle::{ DisplayHandle, OhosDisplayHandle, OhosNdkWindowHandle, RawDisplayHandle, RawWindowHandle, WindowHandle, }; -use servo::compositing::CompositeTarget; /// The EventLoopWaker::wake function will be called from any thread. /// It will be called to notify embedder that some events are available, /// and that perform_updates need to be called @@ -113,7 +112,6 @@ pub fn init( embedder_callbacks, window_callbacks.clone(), None, /* user_agent */ - CompositeTarget::ContextFbo, ); let app_state = RunningAppState::new( diff --git a/ports/servoshell/prefs.rs b/ports/servoshell/prefs.rs index 1d02a343636..fc29a414c79 100644 --- a/ports/servoshell/prefs.rs +++ b/ports/servoshell/prefs.rs @@ -19,6 +19,7 @@ use servo::servo_url::ServoUrl; use url::Url; #[cfg_attr(any(target_os = "android", target_env = "ohos"), allow(dead_code))] +#[derive(Clone)] pub(crate) struct ServoShellPreferences { /// The user agent to use for servoshell. pub user_agent: Option<String>, @@ -48,6 +49,11 @@ pub(crate) struct ServoShellPreferences { /// An override for the screen resolution. This is useful for testing behavior on different screen sizes, /// such as the screen of a mobile device. pub screen_size_override: Option<Size2D<u32, DeviceIndependentPixel>>, + /// If not-None, the path to a file to output the default WebView's rendered output + /// after waiting for a stable image, this implies `Self::exit_after_load`. + pub output_image_path: Option<String>, + /// Whether or not to exit after Servo detects a stable output image in all WebViews. + pub exit_after_stable_image: bool, } impl Default for ServoShellPreferences { @@ -64,6 +70,8 @@ impl Default for ServoShellPreferences { tracing_filter: None, url: None, user_agent: None, + output_image_path: None, + exit_after_stable_image: false, } } } @@ -168,7 +176,13 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing let mut opts = Options::new(); opts.optflag("", "legacy-layout", "Use the legacy layout engine"); - opts.optopt("o", "output", "Output file", "output.png"); + opts.optopt( + "o", + "output", + "Path to an output image. The format of the image is determined by the extension. \ + Supports all formats that `rust-image` does.", + "output.png", + ); opts.optopt("s", "size", "Size of tiles", "512"); opts.optflagopt( "p", @@ -190,7 +204,11 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing "Memory profiler flag and output interval", "10", ); - opts.optflag("x", "exit", "Exit after load flag"); + opts.optflag( + "x", + "exit", + "Exit after Servo has loaded the page and detected a stable output image", + ); opts.optopt( "y", "layout-threads", @@ -546,8 +564,8 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing }); // If an output file is specified the device pixel ratio is always 1. - let output_file = opt_match.opt_str("o"); - if output_file.is_some() { + let output_image_path = opt_match.opt_str("o"); + if output_image_path.is_some() { device_pixel_ratio_override = Some(1.0); } @@ -564,6 +582,8 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing preferences.js_ion_enabled = false; } + let exit_after_load = opt_match.opt_present("x") || output_image_path.is_some(); + let wait_for_stable_image = exit_after_load || webdriver_port.is_some(); let servoshell_preferences = ServoShellPreferences { user_agent: opt_match.opt_str("u"), url, @@ -574,6 +594,8 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing tracing_filter, initial_window_size, screen_size_override, + output_image_path, + exit_after_stable_image: exit_after_load, ..Default::default() }; @@ -584,6 +606,7 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing let opts = Opts { debug: debug_options.clone(), + wait_for_stable_image, legacy_layout, time_profiling, time_profiler_trace_path: opt_match.opt_str("profiler-trace-path"), @@ -591,7 +614,6 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing nonincremental_layout, userscripts: opt_match.opt_default("userscripts", ""), user_stylesheets, - output_file, hard_fail: opt_match.opt_present("f") && !opt_match.opt_present("F"), webdriver_port, multiprocess: opt_match.opt_present("M"), @@ -599,7 +621,6 @@ pub(crate) fn parse_command_line_arguments(args: Vec<String>) -> ArgumentParsing sandbox: opt_match.opt_present("S"), random_pipeline_closure_probability, random_pipeline_closure_seed, - exit_after_load: opt_match.opt_present("x"), config_dir, shaders_dir: opt_match.opt_str("shaders").map(Into::into), certificate_path: opt_match.opt_str("certificate-path"), diff --git a/tests/wpt/meta/MANIFEST.json b/tests/wpt/meta/MANIFEST.json index de14b643290..f3fe1be8e38 100644 --- a/tests/wpt/meta/MANIFEST.json +++ b/tests/wpt/meta/MANIFEST.json @@ -506147,7 +506147,7 @@ [] ], "executorservo.py": [ - "e3369d24ebc6d6aac1b4632ef673a36602417745", + "2710b1b844c8e93c251c91c8809697d357bd4f06", [] ], "executorservodriver.py": [ diff --git a/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorservo.py b/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorservo.py index e3369d24ebc..2710b1b844c 100644 --- a/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorservo.py +++ b/tests/wpt/tests/tools/wptrunner/wptrunner/executors/executorservo.py @@ -226,6 +226,7 @@ class ServoRefTestExecutor(ServoExecutor): def screenshot(self, test, viewport_size, dpi, page_ranges): with TempFilename(self.tempdir) as output_path: + output_path = f"{output_path}.png" extra_args = ["--exit", "--output=%s" % output_path, "--window-size", viewport_size or "800x600"] |