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>
This commit is contained in:
Martin Robinson 2025-02-20 19:27:49 +01:00 committed by GitHub
parent 7d33e72bfc
commit 54b5c7b632
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 233 additions and 270 deletions

View file

@ -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"

View file

@ -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 {

View file

@ -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;
}
webview.paint();
self.inner().window.rendering_context().present();
self.inner_mut().need_repaint = false;
// This needs to be done before presenting(), because `ReneringContext::read_to_image` reads
// from the back buffer.
self.save_output_image_if_necessary();
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);
}
}

View file

@ -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");
{

View file

@ -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

View file

@ -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| {

View file

@ -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(

View file

@ -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"),