diff --git a/patches/components/script/dom/document/document.rs.patch b/patches/components/script/dom/document/document.rs.patch index 628baeb..8ebd8a6 100644 --- a/patches/components/script/dom/document/document.rs.patch +++ b/patches/components/script/dom/document/document.rs.patch @@ -95,7 +95,26 @@ /// All websockets created that are associated with this document. websockets: DOMTracker, -@@ -707,6 +730,11 @@ +@@ -667,6 +690,18 @@ + /// Reflect the value of that preferences to prevent paying the cost of a RwLock access. + layout_animations_test_enabled: bool, + ++ /// Whether the frame in progress has pinned the animation timeline. ++ /// ++ /// A frame must hold one timestamp so that every animation it samples agrees, ++ /// which is what the pin protects. But reflows also happen *outside* a frame, like ++ /// script reading `offsetWidth`, or a timer callback that touches style and a ++ /// `ReflowRequest` carries the timeline value that stylo stamps onto any ++ /// transition the restyle starts. Unpinned, those reflows must advance the ++ /// clock first: otherwise the stamp is however old the last frame was, and on ++ /// an idle document a transition born that stale is already past its end and is ++ /// retired on its first tick instead of running. ++ animation_timeline_pinned_for_frame: Cell, ++ + /// + #[no_trace] + state_override: DomRefCell>, +@@ -707,6 +742,11 @@ /// history: MutNullableDom, @@ -107,7 +126,7 @@ } impl Document { -@@ -891,6 +919,68 @@ +@@ -891,6 +931,68 @@ self.dirty_root.take() } @@ -176,7 +195,7 @@ #[inline] pub(crate) fn loader(&self) -> Ref<'_, DocumentLoader> { self.loader.borrow() -@@ -962,6 +1052,12 @@ +@@ -962,6 +1064,12 @@ // Set the document's activity level, reflow if necessary, and suspend or resume timers. self.activity.set(activity); @@ -189,7 +208,7 @@ let media = ServoMedia::get(); let pipeline_id = self.window().pipeline_id(); let client_context_id = -@@ -975,6 +1071,7 @@ +@@ -975,6 +1083,7 @@ self.title_changed(); self.notify_embedder_favicon(); @@ -197,7 +216,7 @@ self.dirty_all_nodes(cx.no_gc()); self.window().resume(cx); media.resume(&client_context_id); -@@ -1362,6 +1459,9 @@ +@@ -1362,6 +1471,9 @@ LoadStatus::Started, )); self.send_to_embedder(EmbedderMsg::Status(self.webview_id(), None)); @@ -207,7 +226,7 @@ } }, DocumentReadyState::Complete => { -@@ -1370,6 +1470,9 @@ +@@ -1370,6 +1482,9 @@ self.webview_id(), LoadStatus::Complete, )); @@ -217,7 +236,7 @@ } update_with_current_instant(&self.navigation_timing.dom_complete); }, -@@ -1450,7 +1553,13 @@ +@@ -1450,7 +1565,13 @@ let window = self.window(); if window.is_top_level() { let title = self.title().map(String::from); @@ -232,7 +251,7 @@ } } -@@ -1459,6 +1568,18 @@ +@@ -1459,6 +1580,18 @@ window.send_to_embedder(msg); } @@ -251,7 +270,7 @@ pub(crate) fn dirty_all_nodes(&self, no_gc: &NoGC) { let root = match self.GetDocumentElement() { Some(root) => root, -@@ -2907,6 +3028,17 @@ +@@ -2907,6 +3040,17 @@ self.iframes.borrow_mut() } @@ -269,7 +288,7 @@ pub(crate) fn set_navigation_start(&self, navigation_start: CrossProcessInstant) { self.interactive_time .borrow_mut() -@@ -3187,6 +3319,60 @@ +@@ -3187,6 +3331,60 @@ .push(Dom::from_ref(resize_observer)); } @@ -330,7 +349,7 @@ /// /// pub(crate) fn gather_active_resize_observations_at_depth( -@@ -3764,6 +3950,7 @@ +@@ -3764,6 +3962,7 @@ anchors: Default::default(), applets: Default::default(), iframes: RefCell::new(IFrameCollection::new()), @@ -338,7 +357,7 @@ shared_style_locks, stylesheets: DomRefCell::new(DocumentStylesheetSet::new()), stylesheet_list: MutNullableDom::new(None), -@@ -3823,6 +4010,8 @@ +@@ -3823,6 +4022,8 @@ dirty_root: Default::default(), declarative_refresh: Default::default(), resize_observers: Default::default(), @@ -347,7 +366,7 @@ fonts: Default::default(), visibility_state: Cell::new(DocumentVisibilityState::Hidden), status_code, -@@ -3844,6 +4033,7 @@ +@@ -3844,10 +4045,12 @@ active_sandboxing_flag_set: Cell::new(creation_sandboxing_flag_set), creation_sandboxing_flag_set: Cell::new(creation_sandboxing_flag_set), favicon: RefCell::new(None), @@ -355,7 +374,12 @@ websockets: DOMTracker::new(), details_name_groups: Default::default(), protocol_handler_automation_mode: Default::default(), -@@ -3864,6 +4054,7 @@ + layout_animations_test_enabled: pref!(layout_animations_test_enabled), ++ animation_timeline_pinned_for_frame: Cell::new(false), + state_override: Default::default(), + value_override: Default::default(), + default_single_line_container_name: Default::default(), +@@ -3864,6 +4067,7 @@ )), image_cache, history: Default::default(), @@ -363,7 +387,54 @@ } } -@@ -4892,6 +5083,36 @@ +@@ -4615,6 +4819,33 @@ + .mark_animating_nodes_as_dirty(current_timeline_value); + } + ++ /// Sample the clock into the animation timeline, unless a test owns it (see ++ /// `advance_animation_timeline_for_testing`). ++ fn advance_animation_timeline(&self) { ++ if !self.layout_animations_test_enabled { ++ self.timeline.update(self.window()); ++ } ++ } ++ ++ /// Advance the timeline unless the frame in progress has already pinned it. ++ pub(crate) fn ensure_animation_timeline_is_current(&self) { ++ if !self.animation_timeline_pinned_for_frame.get() { ++ self.advance_animation_timeline(); ++ } ++ } ++ ++ /// Pin the timeline for the frame now starting its animation steps, so every ++ /// animation it samples and its own reflow share one timestamp. ++ pub(crate) fn pin_animation_timeline_for_frame(&self) { ++ self.animation_timeline_pinned_for_frame.set(true); ++ } ++ ++ /// Release the pin once a rendering update has finished with this document, so ++ /// a reflow forced from a later task gets a fresh timestamp. ++ pub(crate) fn release_animation_timeline_for_frame(&self) { ++ self.animation_timeline_pinned_for_frame.set(false); ++ } ++ + pub(crate) fn current_animation_timeline_value(&self) -> f64 { + self.timeline + .upcast::() +@@ -4643,10 +4874,8 @@ + + /// An implementation of . + pub(crate) fn update_animations_and_send_events(&self, cx: &mut CurrentRealm) { +- // Only update the time if it isn't being managed by a test. +- if !self.layout_animations_test_enabled { +- self.timeline.update(self.window()); +- } ++ self.advance_animation_timeline(); ++ self.pin_animation_timeline_for_frame(); + + // > 1. Update the current time of all timelines associated with doc passing now + // > as the timestamp. +@@ -4892,6 +5121,36 @@ pub(crate) fn notify_embedder_favicon(&self) { if let Some(ref image) = *self.favicon.borrow() { @@ -400,7 +471,7 @@ self.send_to_embedder(EmbedderMsg::NewFavicon(self.webview_id(), image.clone())); } } -@@ -4970,6 +5191,30 @@ +@@ -4970,6 +5229,30 @@ pub(crate) fn set_iframe_load_in_progress(&self, value: bool) { self.iframe_load_in_progress.set(value) } diff --git a/patches/components/script/dom/window/window.rs.patch b/patches/components/script/dom/window/window.rs.patch index bec6f36..c7ca2a7 100644 --- a/patches/components/script/dom/window/window.rs.patch +++ b/patches/components/script/dom/window/window.rs.patch @@ -143,7 +143,18 @@ /// fn Screen(&self, cx: &mut JSContext) -> DomRoot { self.screen.or_init(|| Screen::new(cx, self)) -@@ -3235,9 +3289,33 @@ +@@ -2717,6 +2771,10 @@ + accessibility_damage = Some(accessibility_data.drain_pending_accessibility_damage()); + } + ++ // Any transition this reflow's restyle starts is stamped with the timeline ++ // value carried below, so make sure it isn't a previous frame's. ++ document.ensure_animation_timeline_is_current(); ++ + // Send new document and relevant styles to layout. + let reflow = ReflowRequest { + document: document.upcast::().to_trusted_node_address(), +@@ -3235,9 +3293,33 @@ &self, input_event: &ConstellationInputEvent, ) -> Option { @@ -180,7 +191,7 @@ } #[expect(unsafe_code)] -@@ -3256,8 +3334,39 @@ +@@ -3256,8 +3338,39 @@ // SAFETY: This is safe because `Window::query_elements_from_point` has ensured that // layout has run and any OpaqueNodes that no longer refer to real nodes are gone. let address = UntrustedNodeAddress(result.node.0 as *const c_void); @@ -221,7 +232,7 @@ cursor: result.cursor, point_in_node: result.point_in_target, point_in_frame, -@@ -3628,17 +3737,19 @@ +@@ -3628,17 +3741,19 @@ } pub(crate) fn send_to_embedder(&self, msg: EmbedderMsg) { @@ -247,7 +258,7 @@ } #[cfg(feature = "webxr")] -@@ -3858,6 +3969,8 @@ +@@ -3858,6 +3973,8 @@ player_context: WindowGLContext, #[cfg(feature = "webgpu")] gpu_id_hub: Arc, inherited_secure_context: Option, @@ -256,7 +267,7 @@ theme: Theme, weak_script_thread: Weak, ) -> DomRoot { -@@ -3882,6 +3995,8 @@ +@@ -3882,6 +3999,8 @@ gpu_id_hub, inherited_secure_context, unminify_js, @@ -265,7 +276,7 @@ Some(font_context), ), caches: Default::default(), -@@ -4152,3 +4267,114 @@ +@@ -4152,3 +4271,114 @@ Window::origin(self) } } diff --git a/patches/components/script/script_thread.rs.patch b/patches/components/script/script_thread.rs.patch index 80b9f86..1f5bf9a 100644 --- a/patches/components/script/script_thread.rs.patch +++ b/patches/components/script/script_thread.rs.patch @@ -57,7 +57,39 @@ use crate::dom::servoparser::{ParserContext, ServoParser}; use crate::dom::types::DebuggerGlobalScope; #[cfg(feature = "webgpu")] -@@ -1832,6 +1840,14 @@ +@@ -1293,18 +1301,22 @@ + // Unspecified, but necessary: Any of the previous callbacks may have put the + // document into a render-blocked state. If that's the case, then abort the + // rendering process now. +- if document.is_render_blocked() { +- continue; +- } ++ if !document.is_render_blocked() { ++ // > Step 22: For each doc of docs, update the rendering or user interface of ++ // > doc and its node navigable to reflect the current state. ++ if document.update_the_rendering(cx).0.needs_frame() { ++ painters_generating_frames.insert(document.webview_id().into()); ++ } + +- // > Step 22: For each doc of docs, update the rendering or user interface of +- // > doc and its node navigable to reflect the current state. +- if document.update_the_rendering(cx).0.needs_frame() { +- painters_generating_frames.insert(document.webview_id().into()); ++ // TODO: Process top layer removals according to ++ // https://drafts.csswg.org/css-position-4/#process-top-layer-removals. + } + +- // TODO: Process top layer removals according to +- // https://drafts.csswg.org/css-position-4/#process-top-layer-removals. ++ // Unconditional, and nested above rather than `continue`d past, so that ++ // every way out of this document's frame releases the pin. Leaving it ++ // set would make the next out-of-frame reflow reuse this frame's ++ // timestamp - see `animation_timeline_pinned_for_frame`. ++ document.release_animation_timeline_for_frame(); + } + + let should_generate_frame = !painters_generating_frames.is_empty(); +@@ -1832,6 +1844,14 @@ ScriptThreadMessage::GetDocumentOrigin(pipeline_id, result_sender) => { self.handle_get_document_origin(pipeline_id, result_sender); }, @@ -72,7 +104,7 @@ ScriptThreadMessage::GetTitle(pipeline_id) => self.handle_get_title_msg(pipeline_id), ScriptThreadMessage::SetDocumentActivity(pipeline_id, activity) => { self.handle_set_document_activity_msg(cx, pipeline_id, activity) -@@ -1997,12 +2013,47 @@ +@@ -1997,11 +2017,46 @@ self.handle_refresh_cursor(pipeline_id); }, ScriptThreadMessage::PreferencesUpdated(updates) => { @@ -96,7 +128,7 @@ + + // Dispatch preferencechanged events to all Embedder instances + self.dispatch_preference_changed_to_embedders(cx, &updates); - }, ++ }, + ScriptThreadMessage::ShowTaskChooser( + request_id, + task_name, @@ -120,11 +152,10 @@ + ScriptThreadMessage::TaskProvidersUpdate(request_id, providers_json) => { + // Dispatch an embedder event to update the chooser with remote providers. + self.dispatch_task_providers_update(cx, &request_id, &providers_json); -+ }, + }, ScriptThreadMessage::ForwardKeyboardScroll(pipeline_id, scroll) => { if let Some(document) = self.documents.borrow().find_document(pipeline_id) { - document.event_handler().do_keyboard_scroll(cx, scroll); -@@ -2034,6 +2085,52 @@ +@@ -2034,6 +2089,52 @@ ScriptThreadMessage::TriggerGarbageCollection => unsafe { JS_GC(cx, GCReason::API); }, @@ -177,7 +208,7 @@ } } -@@ -3097,16 +3194,32 @@ +@@ -3097,16 +3198,32 @@ reason: UpdatePipelineIdReason, cx: &mut js::context::JSContext, ) { @@ -190,15 +221,13 @@ .borrow() .find_iframe(parent_pipeline_id, browsing_context_id); - let Some(frame_element) = frame_element else { -- return; + let webview = if iframe.is_none() { + self.documents + .borrow() + .find_webview(parent_pipeline_id, browsing_context_id) + } else { + None - }; -- if !frame_element.update_pipeline_id(new_pipeline_id, reason, cx) { ++ }; + let is_embedded_webview = webview.is_some(); + + let updated = if let Some(frame_element) = &iframe { @@ -208,13 +237,15 @@ + } else { return; }; +- if !frame_element.update_pipeline_id(new_pipeline_id, reason, cx) { + if !updated { -+ return; + return; +- }; + } let Some(window) = self.documents.borrow().find_window(new_pipeline_id) else { return; -@@ -3125,6 +3238,7 @@ +@@ -3125,6 +3242,7 @@ // is no need to pass along existing opener information that // will be discarded. None, @@ -222,7 +253,7 @@ ); } -@@ -3416,6 +3530,212 @@ +@@ -3416,6 +3534,212 @@ } } @@ -435,7 +466,7 @@ fn ask_constellation_for_top_level_info( &self, sender_webview_id: WebViewId, -@@ -3528,7 +3848,13 @@ +@@ -3528,7 +3852,13 @@ self.senders.pipeline_to_embedder_sender.clone(), self.senders.constellation_sender.clone(), incomplete.pipeline_id, @@ -450,7 +481,7 @@ incomplete.viewport_details, origin.clone(), final_url.clone(), -@@ -3550,6 +3876,8 @@ +@@ -3550,6 +3880,8 @@ #[cfg(feature = "webgpu")] self.gpu_id_hub.clone(), incomplete.load_data.inherited_secure_context, @@ -459,7 +490,7 @@ incomplete.theme, self.this.clone(), ); -@@ -3655,6 +3983,8 @@ +@@ -3655,6 +3987,8 @@ ); } @@ -468,7 +499,7 @@ document.set_ready_state(cx, DocumentReadyState::Loading); self.documents -@@ -3661,8 +3991,6 @@ +@@ -3661,8 +3995,6 @@ .borrow_mut() .insert(incomplete.pipeline_id, &document); @@ -477,7 +508,7 @@ // Initialize the browsing context for the window. let window_proxy = self.window_proxies.local_window_proxy( cx, -@@ -3673,6 +4001,7 @@ +@@ -3673,6 +4005,7 @@ incomplete.webview_id, incomplete.parent_info, incomplete.opener, @@ -485,7 +516,7 @@ ); if window_proxy.parent().is_some() { // https://html.spec.whatwg.org/multipage/#navigating-across-documents:delaying-load-events-mode-2 -@@ -3845,11 +4174,11 @@ +@@ -3845,11 +4178,11 @@ target_snapshot_params: TargetSnapshotParams, cx: &mut js::context::JSContext, ) { @@ -500,7 +531,7 @@ iframe.navigate_or_reload_child_browsing_context( load_data, history_handling, -@@ -3857,7 +4186,17 @@ +@@ -3857,7 +4190,17 @@ target_snapshot_params, cx, ); @@ -518,7 +549,7 @@ } /// Turn javascript: URL into JS code to eval, according to the steps in -@@ -4406,10 +4745,88 @@ +@@ -4406,10 +4749,88 @@ document.event_handler().handle_refresh_cursor(); } @@ -607,7 +638,7 @@ fn handle_request_screenshot_readiness( &self, webview_id: WebViewId, -@@ -4450,7 +4867,7 @@ +@@ -4450,7 +4871,7 @@ pinch_zoom_infos: PinchZoomInfos, ) { let Some(window) = self.documents.borrow().find_window(pipeline_id) else {