libservo: Add a WebView::take_screenshot() API and use it for reftests

Co-authored-by: Delan Azabani <dazabani@igalia.com>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-09-26 09:24:24 +02:00
parent 92dd54b1ec
commit ebb12cb298
25 changed files with 481 additions and 414 deletions

View file

@ -26,8 +26,9 @@ use bluetooth_traits::BluetoothRequest;
use canvas_traits::webgl::WebGLChan;
use compositing_traits::CrossProcessCompositorApi;
use constellation_traits::{
DocumentState, LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationChan,
ScriptToConstellationMessage, StructuredSerializedData, WindowSizeType,
LoadData, LoadOrigin, NavigationHistoryBehavior, ScreenshotReadinessResponse,
ScriptToConstellationChan, ScriptToConstellationMessage, StructuredSerializedData,
WindowSizeType,
};
use crossbeam_channel::{Sender, unbounded};
use cssparser::SourceLocation;
@ -42,7 +43,7 @@ use embedder_traits::{
use euclid::default::{Point2D as UntypedPoint2D, Rect as UntypedRect, Size2D as UntypedSize2D};
use euclid::{Point2D, Scale, Size2D, Vector2D};
use fonts::FontContext;
use ipc_channel::ipc::{self, IpcSender};
use ipc_channel::ipc::IpcSender;
use js::glue::DumpJSStack;
use js::jsapi::{
GCReason, Heap, JS_GC, JSAutoRealm, JSContext as RawJSContext, JSObject, JSPROP_ENUMERATE,
@ -80,9 +81,10 @@ use script_bindings::root::Root;
use script_traits::{ConstellationInputEvent, ScriptThreadMessage};
use selectors::attr::CaseSensitivity;
use servo_arc::Arc as ServoArc;
use servo_config::{opts, pref};
use servo_config::pref;
use servo_geometry::{DeviceIndependentIntRect, f32_rect_to_au_rect};
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
use style::Zero;
use style::error_reporting::{ContextualParseError, ParseErrorReporter};
use style::properties::PropertyId;
use style::properties::style_structs::Font;
@ -441,6 +443,10 @@ pub(crate) struct Window {
/// <https://w3c.github.io/reporting/#windoworworkerglobalscope-endpoints>
#[no_trace]
endpoints_list: DomRefCell<Vec<ReportingEndpoint>>,
/// The number of pending screenshot readiness requests that we have received. We need
/// to send a response for each of these.
pending_screenshot_readiness_requests: Cell<usize>,
}
impl Window {
@ -2328,17 +2334,19 @@ impl Window {
}
document.update_animations_post_reflow();
self.update_constellation_epoch();
reflow_result.reflow_phases_run
}
pub(crate) fn maybe_send_idle_document_state_to_constellation(&self) {
if !opts::get().wait_for_stable_image {
return;
}
pub(crate) fn request_screenshot_readiness(&self) {
self.pending_screenshot_readiness_requests
.set(self.pending_screenshot_readiness_requests.get() + 1);
self.maybe_resolve_pending_screenshot_readiness_requests();
}
if self.has_sent_idle_message.get() {
pub(crate) fn maybe_resolve_pending_screenshot_readiness_requests(&self) {
let pending_requests = self.pending_screenshot_readiness_requests.get();
if pending_requests.is_zero() {
return;
}
@ -2371,17 +2379,20 @@ impl Window {
return;
}
// 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).
debug!(
"{:?}: Sending DocumentState::Idle to Constellation",
self.pipeline_id()
);
self.send_to_constellation(ScriptToConstellationMessage::SetDocumentState(
DocumentState::Idle,
));
self.has_sent_idle_message.set(true);
// When all these conditions are met, notify the Constellation that we are ready to
// have our screenshot taken, when the given layout Epoch has been rendered.
let epoch = self.layout.borrow().current_epoch();
let pipeline_id = self.pipeline_id();
debug!("Ready to take screenshot of {pipeline_id:?} at epoch={epoch:?}");
for _ in 0..pending_requests {
self.send_to_constellation(
ScriptToConstellationMessage::RespondToScreenshotReadinessRequest(
ScreenshotReadinessResponse::Ready(epoch),
),
);
}
self.pending_screenshot_readiness_requests.set(0);
}
/// If parsing has taken a long time and reflows are still waiting for the `load` event,
@ -2445,24 +2456,6 @@ impl Window {
self.layout_blocker.get().layout_blocked()
}
/// If writing a screenshot, synchronously update the layout epoch that it set
/// in the constellation.
pub(crate) fn update_constellation_epoch(&self) {
if !opts::get().wait_for_stable_image {
return;
}
let epoch = self.layout.borrow().current_epoch();
debug!(
"{:?}: Updating constellation epoch: {epoch:?}",
self.pipeline_id()
);
let (sender, receiver) = ipc::channel().expect("Failed to create IPC channel!");
let event = ScriptToConstellationMessage::SetLayoutEpoch(epoch, sender);
self.send_to_constellation(event);
let _ = receiver.recv();
}
/// Trigger a reflow that is required by a certain queries.
pub(crate) fn layout_reflow(&self, query_msg: QueryMsg) {
self.reflow(ReflowGoal::LayoutQuery(query_msg));
@ -3192,9 +3185,7 @@ impl Window {
node.dirty(NodeDamage::Other);
}
}
}
impl Window {
#[allow(unsafe_code)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
@ -3325,6 +3316,7 @@ impl Window {
reporting_observer_list: Default::default(),
report_list: Default::default(),
endpoints_list: Default::default(),
pending_screenshot_readiness_requests: Default::default(),
});
WindowBinding::Wrap::<crate::DomTypeHolder>(GlobalScope::get_cx(), win)

View file

@ -102,6 +102,7 @@ impl MixedMessage {
ScriptThreadMessage::PreferencesUpdated(..) => None,
ScriptThreadMessage::NoLongerWaitingOnAsychronousImageUpdates(_) => None,
ScriptThreadMessage::ForwardKeyboardScroll(id, _) => Some(*id),
ScriptThreadMessage::RequestScreenshotReadiness(id) => Some(*id),
},
MixedMessage::FromScript(inner_msg) => match inner_msg {
MainThreadScriptMsg::Common(CommonScriptMsg::Task(_, _, pipeline_id, _)) => {

View file

@ -38,8 +38,9 @@ use canvas_traits::webgl::WebGLPipeline;
use chrono::{DateTime, Local};
use compositing_traits::{CrossProcessCompositorApi, PipelineExitSource};
use constellation_traits::{
JsEvalResult, LoadData, LoadOrigin, NavigationHistoryBehavior, ScriptToConstellationChan,
ScriptToConstellationMessage, StructuredSerializedData, WindowSizeType,
JsEvalResult, LoadData, LoadOrigin, NavigationHistoryBehavior, ScreenshotReadinessResponse,
ScriptToConstellationChan, ScriptToConstellationMessage, StructuredSerializedData,
WindowSizeType,
};
use crossbeam_channel::unbounded;
use data_url::mime::Mime;
@ -1302,17 +1303,14 @@ impl ScriptThread {
}
}
/// If waiting for an idle `Pipeline` state in order to dump a screenshot at
/// the right time, inform the `Constellation` this `Pipeline` has entered
/// the idle state when applicable.
fn maybe_send_idle_document_state_to_constellation(&self) {
if !opts::get().wait_for_stable_image {
return;
}
/// If any `Pipeline`s are waiting to become ready for the purpose of taking a
/// screenshot, check to see if the `Pipeline` is now ready and send a message to the
/// Constellation, if so.
fn maybe_resolve_pending_screenshot_readiness_requests(&self) {
for (_, document) in self.documents.borrow().iter() {
document
.window()
.maybe_send_idle_document_state_to_constellation();
.maybe_resolve_pending_screenshot_readiness_requests();
}
}
@ -1539,7 +1537,7 @@ impl ScriptThread {
self.update_the_rendering(can_gc);
self.maybe_fulfill_font_ready_promises(can_gc);
self.maybe_send_idle_document_state_to_constellation();
self.maybe_resolve_pending_screenshot_readiness_requests();
// This must happen last to detect if any change above makes a rendering update necessary.
self.maybe_schedule_rendering_opportunity_after_ipc_message(built_any_display_lists);
@ -1916,6 +1914,9 @@ impl ScriptThread {
document.event_handler().do_keyboard_scroll(scroll);
}
},
ScriptThreadMessage::RequestScreenshotReadiness(pipeline_id) => {
self.handle_request_screenshot_readiness(pipeline_id);
},
}
}
@ -4024,6 +4025,19 @@ impl ScriptThread {
pub(crate) fn is_servo_privileged(url: ServoUrl) -> bool {
with_script_thread(|script_thread| script_thread.privileged_urls.contains(&url))
}
fn handle_request_screenshot_readiness(&self, pipeline_id: PipelineId) {
let Some(window) = self.documents.borrow().find_window(pipeline_id) else {
let _ = self.senders.pipeline_to_constellation_sender.send((
pipeline_id,
ScriptToConstellationMessage::RespondToScreenshotReadinessRequest(
ScreenshotReadinessResponse::NoLongerActive,
),
));
return;
};
window.request_screenshot_readiness();
}
}
impl Drop for ScriptThread {