From f3a4e64995f5bada99c7c65c0d5f71e2098f8520 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sat, 17 Oct 2026 22:41:18 +0800 Subject: [PATCH] fix(print): isolate PDF style and font environments from screen state --- moli-renderer-v8/src/layout_renderer/mod.rs | 17 ++- .../src/layout_renderer/style_resolver.rs | 29 +++-- .../src/native_bridge/context_host/layout.rs | 24 ++++- .../element/styles/declaration/observation.rs | 17 ++- .../element/styles/declaration/style_world.rs | 23 ++-- moli-renderer-v8/src/runtime/page_print.rs | 34 +++--- .../src/runtime/page_screenshot.rs | 8 +- .../runtime/page_vm/tests/rendering_update.rs | 1 + .../tests/rendering_update/print_isolation.rs | 102 ++++++++++++++++++ moli-renderer-v8/src/script_vm/layout.rs | 35 +++++- moli-renderer-v8/src/style_engine/cleanup.rs | 4 +- .../src/style_engine/document_world.rs | 23 +++- moli-renderer-v8/src/style_engine/mod.rs | 10 +- .../src/style_engine/registered_properties.rs | 2 +- .../src/style_engine/runtime_invalidation.rs | 8 +- moli-renderer-v8/src/style_engine/schedule.rs | 2 +- moli-renderer-v8/src/style_engine/state.rs | 21 +++- 17 files changed, 294 insertions(+), 66 deletions(-) create mode 100644 moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/print_isolation.rs diff --git a/moli-renderer-v8/src/layout_renderer/mod.rs b/moli-renderer-v8/src/layout_renderer/mod.rs index 6d6dddf618..e86daa494b 100644 --- a/moli-renderer-v8/src/layout_renderer/mod.rs +++ b/moli-renderer-v8/src/layout_renderer/mod.rs @@ -25,6 +25,7 @@ pub(crate) fn build_native_layout_pass( services: &mut DocumentLayoutServices, embedded_document_services: &mut HashMap, request: LayoutPassRequest, + environment_override: Option, ) -> Result, moli_layout::LayoutError> { let mut document_stack = Vec::new(); build_native_layout_pass_recursive( @@ -33,6 +34,7 @@ pub(crate) fn build_native_layout_pass( services, embedded_document_services, request, + environment_override, &mut document_stack, ) } @@ -43,6 +45,7 @@ fn build_native_layout_pass_recursive( services: &mut DocumentLayoutServices, embedded_document_services: &mut HashMap, request: LayoutPassRequest, + environment_override: Option, document_stack: &mut Vec, ) -> Result, moli_layout::LayoutError> { let profile_started = moli_trace::cpu_profile_enabled().then(std::time::Instant::now); @@ -60,8 +63,13 @@ fn build_native_layout_pass_recursive( request.requests_paint(), ); let source_ready = profile_started.map(|_| std::time::Instant::now()); - let mut styles = - style_resolver::NativeLayoutStyleResolver::new(runtime, root, document, request.viewport); + let mut styles = style_resolver::NativeLayoutStyleResolver::new( + runtime, + root, + document, + request.viewport, + environment_override, + ); let styles_ready = profile_started.map(|_| std::time::Instant::now()); let result = { let mut frames = NativeEmbeddedFrameRenderer { @@ -69,6 +77,7 @@ fn build_native_layout_pass_recursive( reason: request.reason, capture_paint: request.requests_paint(), include_backgrounds: request.includes_backgrounds(), + environment_override, document_stack, embedded_document_services, }; @@ -135,6 +144,7 @@ struct NativeEmbeddedFrameRenderer<'a> { reason: moli_layout::LayoutFlushReason, capture_paint: bool, include_backgrounds: bool, + environment_override: Option, document_stack: &'a mut Vec, embedded_document_services: &'a mut HashMap, } @@ -182,6 +192,7 @@ impl EmbeddedFrameRenderer for NativeEmbeddedFrameRenderer<'_> { &mut services, self.embedded_document_services, request, + self.environment_override, self.document_stack, ); self.embedded_document_services.insert(document, services); @@ -207,6 +218,6 @@ pub(crate) fn build_normalized_native_box_tree_for_test( let viewport = runtime.layout_viewport_for_document(document); let source = source_view::NativeLayoutSourceView::new(runtime, root); let mut styles = - style_resolver::NativeLayoutStyleResolver::new(runtime, root, document, viewport); + style_resolver::NativeLayoutStyleResolver::new(runtime, root, document, viewport, None); moli_layout::build_layout_world(&source, &mut styles).map(|world| world.normalized_tree()) } diff --git a/moli-renderer-v8/src/layout_renderer/style_resolver.rs b/moli-renderer-v8/src/layout_renderer/style_resolver.rs index 75c764e8f3..638dd97733 100644 --- a/moli-renderer-v8/src/layout_renderer/style_resolver.rs +++ b/moli-renderer-v8/src/layout_renderer/style_resolver.rs @@ -10,7 +10,7 @@ use crate::{ JsContextHost, element::{PreparedAnonymousStyleContext, StyleObservation}, }, - style_engine::{StyleViewport, StyloAnonymousBoxKind}, + style_engine::{StyleViewport, StyloAnonymousBoxKind, StyloStyleEnvironment}, }; pub(super) struct NativeLayoutStyleResolver<'a> { @@ -43,8 +43,9 @@ impl<'a> NativeLayoutStyleResolver<'a> { root: DomHandle, document: DomHandle, viewport: LayoutViewport, + environment_override: Option, ) -> Self { - let mut reads = layout_style_read_scope(runtime, document, viewport); + let mut reads = layout_style_read_scope(runtime, document, viewport, environment_override); // Drain pending invalidations and update the retained world before // freezing the anonymous-style context for this synchronous pass. let anonymous_context = reads.read(root).prepare_anonymous_style_context(); @@ -93,15 +94,24 @@ fn layout_style_read_scope<'a>( runtime: &'a JsContextHost, document: DomHandle, viewport: LayoutViewport, + environment_override: Option, ) -> StyleObservation<'a> { let viewport = layout_style_viewport(runtime, viewport); - if document == runtime.document_handle() && viewport == runtime.style_viewport() { + if environment_override.is_none() + && document == runtime.document_handle() + && viewport == runtime.style_viewport() + { // Keep the normal main-document observation environment so layout and // rendered-text collection read the same persistent style world. Only - // an override viewport or embedded Document needs an explicit context. + // an override viewport/media or embedded Document needs an explicit context. StyleObservation::new(runtime) } else { - StyleObservation::new_for_document_viewport(runtime, document, viewport) + StyleObservation::new_for_document_viewport( + runtime, + document, + viewport, + environment_override, + ) } } @@ -368,7 +378,7 @@ mod tests { let root = host.dom_host().document_element_handle().unwrap(); let owner = host.dom_host().element_handle_by_id("owner").unwrap(); let viewport = host.layout_viewport_for_document(document); - let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport); + let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport, None); let world = layout_world(&mut resolver, root); let parent = parent_context(&world, owner); let reads = resolver.reads.read_count_for_test(); @@ -406,7 +416,7 @@ mod tests { let root = host.dom_host().document_element_handle().unwrap(); let owner = host.dom_host().element_handle_by_id("owner").unwrap(); let viewport = host.layout_viewport_for_document(document); - let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport); + let mut resolver = NativeLayoutStyleResolver::new(&host, root, document, viewport, None); let world = layout_world(&mut resolver, root); let parent = parent_context(&world, owner); @@ -418,7 +428,7 @@ mod tests { .document_element_handle_for_document(foreign_document) .unwrap(); let mut foreign = - NativeLayoutStyleResolver::new(&host, foreign_root, foreign_document, viewport); + NativeLayoutStyleResolver::new(&host, foreign_root, foreign_document, viewport, None); let foreign_world = layout_world(&mut foreign, foreign_root); let foreign_parent = parent_context(&foreign_world, foreign_root); assert!( @@ -462,6 +472,7 @@ mod tests { root, document, host.layout_viewport_for_document(document), + None, ); let world = layout_world(&mut resolver, root); let reads = resolver.reads.read_count_for_test(); @@ -511,6 +522,7 @@ mod tests { root, document, host.layout_viewport_for_document(document), + None, ); let world = layout_world(&mut resolver, root); let parent = parent_context(&world, owner); @@ -545,6 +557,7 @@ mod tests { root, document, host.layout_viewport_for_document(document), + None, ); let world = layout_world(&mut resolver, root); let parent = parent_context(&world, owner); diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout.rs b/moli-renderer-v8/src/native_bridge/context_host/layout.rs index 648e26e1fd..5824064f3b 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout.rs @@ -123,7 +123,7 @@ impl JsContextHost { document, self.dom_host().dom_version(), self.style_engine - .computed_style_observation_generations(self.active_layout_document_handles()), + .visual_style_input_generations(self.active_layout_document_handles()), self.visual_state_generation(), self.visual_resource_generation(), viewport, @@ -213,7 +213,12 @@ impl JsContextHost { /// after the recursive pass succeeds. Advancing this input generation /// first also distinguishes an explicit main-Document capture viewport /// from ordinary DOM APIs selecting conflicting implicit viewports. - pub(crate) fn note_layout_style_viewport(&self, document: DomHandle, viewport: StyleViewport) { + pub(crate) fn note_layout_style_environment( + &self, + document: DomHandle, + viewport: StyleViewport, + environment_override: Option, + ) { let matches_cached = if document == self.document_handle() { viewport == self.style_viewport() } else { @@ -224,7 +229,10 @@ impl JsContextHost { && viewport.height == Some(f64::from(cached.css_height)) }) }; - if !matches_cached { + let different_media = environment_override.is_some_and(|environment| { + environment != StyloStyleEnvironment::from_emulated_media(self.emulated_media()) + }); + if !matches_cached || different_media { self.style_viewport_generation .set(self.style_viewport_generation.get().saturating_add(1)); } @@ -299,6 +307,15 @@ impl JsContextHost { &self, document: DomHandle, request: LayoutPassRequest, + ) -> Result>, LayoutError> { + self.build_layout_pass_for_document_in_environment(document, request, None) + } + + pub(crate) fn build_layout_pass_for_document_in_environment( + &self, + document: DomHandle, + request: LayoutPassRequest, + environment_override: Option, ) -> Result>, LayoutError> { let Some(root) = self .dom_host() @@ -324,6 +341,7 @@ impl JsContextHost { services, embedded_document_services, request, + environment_override, ) }, )? diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration/observation.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration/observation.rs index 490c6fd20f..17900de567 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration/observation.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration/observation.rs @@ -74,6 +74,9 @@ pub(crate) struct StyleObservation<'a> { /// detached CSSOM snapshots use `Some` to pin an exact caller-owned /// viewport instead. fixed_context: Option, + /// A capture may observe a temporary media environment without changing + /// the Window's live environment or scheduling environment events. + environment_override: Option, /// Explicit layout observations may intentionally use a caller-owned /// viewport that differs from the Window surface. They advance the style /// viewport input epoch, but do not become the baseline for ordinary DOM @@ -110,8 +113,9 @@ impl<'a> StyleObservationInputs<'a> { context: StyleComputationContext, _tracks_persistent_world: bool, sizing_target: Option, + environment_override: Option, ) -> Self { - let environment = stylo_style_environment(runtime, source_document); + let environment = stylo_style_environment(runtime, source_document, environment_override); Self { runtime, key: StyleObservationKey::for_document(runtime, source_document), @@ -246,14 +250,17 @@ impl<'a> StyleObservation<'a> { runtime: &'a JsContextHost, document: DomHandle, viewport: StyleViewport, + environment_override: Option, ) -> Self { - runtime.note_layout_style_viewport(document, viewport); - Self::new_with_fixed_context( + runtime.note_layout_style_environment(document, viewport, environment_override); + let mut observation = Self::new_with_fixed_context( runtime, Some(StyleComputationContext::new(viewport).with_read_document(Some(document))), false, None, - ) + ); + observation.environment_override = environment_override; + observation } pub(super) fn new_for_element( @@ -275,6 +282,7 @@ impl<'a> StyleObservation<'a> { #[cfg(test)] read_count: 0, fixed_context, + environment_override: None, tracks_persistent_world, sizing_target, drained_document: None, @@ -409,6 +417,7 @@ impl<'a> StyleObservation<'a> { context, self.tracks_persistent_world, self.sizing_target, + self.environment_override, )); if self.primary_document.is_none() { self.primary_document = Some(Rc::clone(&observation)); diff --git a/moli-renderer-v8/src/native_bridge/element/styles/declaration/style_world.rs b/moli-renderer-v8/src/native_bridge/element/styles/declaration/style_world.rs index 0311354a3e..4e6aa62e77 100644 --- a/moli-renderer-v8/src/native_bridge/element/styles/declaration/style_world.rs +++ b/moli-renderer-v8/src/native_bridge/element/styles/declaration/style_world.rs @@ -159,19 +159,22 @@ pub(super) fn stylesheet_query_fallback( pub(super) fn stylo_style_environment( runtime: &JsContextHost, document: Option, + environment_override: Option, ) -> StyloStyleEnvironment { #[cfg(test)] runtime.note_style_observation_environment_resolution_for_test(); - StyloStyleEnvironment::from_emulated_media(runtime.emulated_media()).with_page_color_schemes( - document - .map(|document| { - crate::document_color_scheme::document_page_color_schemes( - runtime.dom_host(), - document, - ) - }) - .unwrap_or_default(), - ) + environment_override + .unwrap_or_else(|| StyloStyleEnvironment::from_emulated_media(runtime.emulated_media())) + .with_page_color_schemes( + document + .map(|document| { + crate::document_color_scheme::document_page_color_schemes( + runtime.dom_host(), + document, + ) + }) + .unwrap_or_default(), + ) } fn complete_style_world_snapshot( diff --git a/moli-renderer-v8/src/runtime/page_print.rs b/moli-renderer-v8/src/runtime/page_print.rs index 44acccc45d..5b8b85eef6 100644 --- a/moli-renderer-v8/src/runtime/page_print.rs +++ b/moli-renderer-v8/src/runtime/page_print.rs @@ -33,30 +33,30 @@ impl PageVm { if self.layout_configuration.policy == LayoutPolicy::Mock { return Ok(Reply::LayoutDisabled); } - let previous = self.document_settings.emulated_media.clone(); - let restore = previous.media.is_none(); - if restore { - let mut print = previous.clone(); + let environment_override = if self.document_settings.emulated_media.media.is_none() { + let mut print = self.document_settings.emulated_media.clone(); print.media = Some("print".to_owned()); - self.set_emulated_media(&print)?; - } + Some(crate::style_engine::StyloStyleEnvironment::from_emulated_media(&print)) + } else { + None + }; let surface = self.document_settings.viewport_surface.unwrap_or_default(); let viewport = PaintViewport::new( surface.inner_width, surface.inner_height, surface.device_pixel_ratio as f32, ); - let result = self.vm_mut().paint_layout_snapshot_with_capture( - viewport, - moli_layout::LayoutFlushReason::Print, - PaintCaptureRequest { - include_backgrounds: request.print_background, - ..PaintCaptureRequest::print_document() - }, - ); - if restore { - self.set_emulated_media(&previous)?; - } + let result = self + .vm_mut() + .paint_layout_snapshot_with_capture_in_environment( + viewport, + moli_layout::LayoutFlushReason::Print, + PaintCaptureRequest { + include_backgrounds: request.print_background, + ..PaintCaptureRequest::print_document() + }, + environment_override, + ); Ok(match result? { Some(snapshot) => match moli_paint::pdf::pdf_snapshot(&snapshot, &request.options) { Ok(bytes) => Reply::Printed(bytes), diff --git a/moli-renderer-v8/src/runtime/page_screenshot.rs b/moli-renderer-v8/src/runtime/page_screenshot.rs index 02835a4ef1..2fdb8720a8 100644 --- a/moli-renderer-v8/src/runtime/page_screenshot.rs +++ b/moli-renderer-v8/src/runtime/page_screenshot.rs @@ -67,7 +67,7 @@ pub struct RendererVisualStateToken(Arc); struct RendererVisualState { document: RendererDocumentLifecycleIdentity, dom_generation: u64, - style_generations: Vec<(u32, u64, u64, u64)>, + style_generations: Vec<(u32, u64, u64)>, interaction_generation: u64, resource_generation: u64, viewport_width: u32, @@ -80,7 +80,7 @@ impl RendererVisualStateToken { pub(crate) fn new( document: RendererDocumentLifecycleIdentity, dom_generation: u64, - style_generations: Vec<(crate::document_runtime::DomHandle, u64, u64, u64)>, + style_generations: Vec<(crate::document_runtime::DomHandle, u64, u64)>, interaction_generation: u64, resource_generation: u64, viewport: PaintViewport, @@ -91,9 +91,7 @@ impl RendererVisualStateToken { dom_generation, style_generations: style_generations .into_iter() - .map(|(document, source, computed, context)| { - (document.index_u32(), source, computed, context) - }) + .map(|(document, source, input)| (document.index_u32(), source, input)) .collect(), interaction_generation, resource_generation, diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs index fe9fbc2680..ea05a99f52 100644 --- a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update.rs @@ -3,6 +3,7 @@ use super::*; mod image_repeat; mod planar_transforms; mod print; +mod print_isolation; mod transform_precision; use base64::Engine as _; diff --git a/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/print_isolation.rs b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/print_isolation.rs new file mode 100644 index 0000000000..aa0e84b436 --- /dev/null +++ b/moli-renderer-v8/src/runtime/page_vm/tests/rendering_update/print_isolation.rs @@ -0,0 +1,102 @@ +use super::*; +use crate::runtime::{ + RendererCaptureScreencastFrameReply, RendererCaptureScreenshotRequest, RendererPrintToPdfReply, + RendererPrintToPdfRequest, +}; + +fn screen_token(page: &PageVm) -> crate::runtime::RendererVisualStateToken { + page.vm().visual_state_token( + page.document_lifecycle.identity(), + moli_layout::PaintViewport::new(320, 100, 1.0), + [255; 4], + ) +} + +#[tokio::test(flavor = "current_thread")] +async fn native_pdf_preserves_live_fonts_and_screencast_input_identity() { + run_page_vm_async_test(async move { + use base64::Engine; + + let loader = crate::network::ResourceRequestClient::new(&FetchConfig::default())?; + loader.set_optional_resource_fetch_mask(crate::protocol_types::OptionalResourceFetchMask::FONT); + let mut page = test_page_vm_with_loader_and_document_url( + &loader, + Vec::new(), + Url::parse("https://print-isolation.test/")?, + ); + page.set_viewport_surface(Some(crate::protocol_types::ViewportSurface { + inner_width: 320, + inner_height: 100, + ..Default::default() + }))?; + let font = base64::engine::general_purpose::STANDARD.encode(include_bytes!( + "../../../../../../moli-layout/tests/fixtures/moli-ahem.woff2" + )); + page.vm_mut().eval(&format!(r#" + document.head.innerHTML=``; + document.body.innerHTML='
SCREENPRINT
'; + "#))?; + page.capture_screenshot(RendererCaptureScreenshotRequest::viewport_png())?; + page.vm_mut().eval(r#" + window.liveFonts=document.fonts; + window.liveReady=liveFonts.ready; + window.screenFace=[...liveFonts].find(face=>face.family==='ScreenOnly'); + "#)?; + assert_eq!(page.vm_mut().eval("String(!!screenFace)")?, "true"); + let RendererCaptureScreencastFrameReply::Captured(first) = page + .capture_screencast_frame(viewport_screencast_request(None))? else { + anyhow::bail!("expected initial screen capture"); + }; + let original = screen_token(&page); + assert_eq!(original, first.visual_state); + let fonts = page.vm_mut().document_web_font_counts_for_test(); + let publishes = page.vm().layout_snapshot_cache_observability_for_test().2; + let media = page.document_settings.emulated_media.clone(); + + let RendererPrintToPdfReply::Printed(bytes) = page.print_to_pdf(Default::default())? else { + anyhow::bail!("expected PDF"); + }; + let document = lopdf::Document::load_mem(&bytes)?; + assert_eq!( + document.extract_text(&document.get_pages().keys().copied().collect::>())? + .split_whitespace().collect::>().join(" "), + "PRINT" + ); + assert_eq!(page.document_settings.emulated_media, media); + assert_eq!(screen_token(&page), original); + assert_eq!(page.vm_mut().document_web_font_counts_for_test(), fonts); + assert_eq!(page.vm().layout_snapshot_cache_observability_for_test().2, publishes); + assert_eq!(page.vm_mut().eval("JSON.stringify([document.fonts===liveFonts,liveFonts.has(screenFace),liveFonts.ready===liveReady,target.offsetWidth,matchMedia('print').matches])")?, "[true,true,true,40,false]"); + assert_eq!(screen_token(&page), original); + assert!(matches!( + page.capture_screencast_frame(viewport_screencast_request(Some(first.visual_state)))?, + RendererCaptureScreencastFrameReply::Unchanged + )); + + let mut invalid = RendererPrintToPdfRequest::default(); + invalid.options.width = 0.0; + assert!(matches!(page.print_to_pdf(invalid)?, RendererPrintToPdfReply::Failed(_))); + assert_eq!(screen_token(&page), original); + assert_eq!(page.vm_mut().document_web_font_counts_for_test(), fonts); + assert_eq!(page.document_settings.emulated_media, media); + + page.vm_mut().eval("target.style.width='60px'")?; + let RendererCaptureScreencastFrameReply::Captured(changed) = page + .capture_screencast_frame(viewport_screencast_request(Some(original.clone())))? else { + anyhow::bail!("a real source mutation must invalidate screencast"); + }; + assert_ne!(changed.visual_state, original); + assert_eq!(page.vm_mut().eval("String(target.offsetWidth)")?, "60"); + Ok::<_, anyhow::Error>(()) + }) + .await + .expect("temporary print styles must preserve live screen inputs"); +} diff --git a/moli-renderer-v8/src/script_vm/layout.rs b/moli-renderer-v8/src/script_vm/layout.rs index 0d94f92172..5fb3efd869 100644 --- a/moli-renderer-v8/src/script_vm/layout.rs +++ b/moli-renderer-v8/src/script_vm/layout.rs @@ -154,8 +154,19 @@ impl ScriptVm { reason: moli_layout::LayoutFlushReason, capture: moli_layout::PaintCaptureRequest, ) -> anyhow::Result> { - self.with_fresh_layout_pass( + self.paint_layout_snapshot_with_capture_in_environment(viewport, reason, capture, None) + } + + pub(crate) fn paint_layout_snapshot_with_capture_in_environment( + &mut self, + viewport: moli_layout::PaintViewport, + reason: moli_layout::LayoutFlushReason, + capture: moli_layout::PaintCaptureRequest, + environment_override: Option, + ) -> anyhow::Result> { + self.with_fresh_layout_pass_in_environment( moli_layout::LayoutPassRequest::with_capture(viewport, reason, capture), + environment_override, moli_layout::LayoutPassResult::take_paint_snapshot, ) .map_err(anyhow::Error::new) @@ -187,13 +198,26 @@ impl ScriptVm { consume: impl FnOnce( &mut moli_layout::LayoutPassResult, ) -> Result, + ) -> Result, moli_layout::LayoutError> { + self.with_fresh_layout_pass_in_environment(request, None, consume) + } + + fn with_fresh_layout_pass_in_environment( + &mut self, + request: moli_layout::LayoutPassRequest, + environment_override: Option, + consume: impl FnOnce( + &mut moli_layout::LayoutPassResult, + ) -> Result, ) -> Result, moli_layout::LayoutError> { // Font-source reconciliation is a pre-pass lifecycle step. CSS image // URLs come back from the actual box-construction traversal below. // Once the guard is entered, layout performs no JS, event-loop, // observer, or resource completion work and owns no state beyond this // call. - self.reconcile_document_web_fonts_for_layout(); + if environment_override.is_none() { + self.reconcile_document_web_fonts_for_layout(); + } let requests_paint = request.requests_paint(); // Printing consumes a temporary projection. Screenshots, screencast // frames and explicit refreshes publish geometry for subsequent reads. @@ -202,7 +226,11 @@ impl ScriptVm { let context_host = self._context_host.borrow(); let document = context_host.document_handle(); let result = context_host - .build_layout_pass_for_document(document, request) + .build_layout_pass_for_document_in_environment( + document, + request, + environment_override, + ) .and_then(|pass| { let Some(mut pass) = pass else { return Ok(None); @@ -499,6 +527,7 @@ impl ScriptVm { &mut services, &mut embedded_document_services, request, + None, ) } diff --git a/moli-renderer-v8/src/style_engine/cleanup.rs b/moli-renderer-v8/src/style_engine/cleanup.rs index d5d921c399..eb3b2a9e23 100644 --- a/moli-renderer-v8/src/style_engine/cleanup.rs +++ b/moli-renderer-v8/src/style_engine/cleanup.rs @@ -183,7 +183,7 @@ impl<'a> StyleInvalidationCleanup<'a> { self.pseudo_style_cache.clear(); self.document_state.clear_selector_caches(); self.document_state.lazy_invalidation_roots.clear(); - self.document_state.bump_target_context_epoch(); + self.document_state.bump_computed_context_epoch(); let generations_after = self.document_state.generation_snapshot(); context.trace_clear_all_fallback(self.document, generations_before, generations_after); } @@ -278,7 +278,7 @@ impl<'a> StyleInvalidationCleanup<'a> { self.pseudo_style_cache .invalidate_handles(roots.iter().copied()); self.document_state.clear_selector_caches(); - self.document_state.bump_target_context_epoch(); + self.document_state.bump_computed_context_epoch(); Some(immediately_invalidated) } } diff --git a/moli-renderer-v8/src/style_engine/document_world.rs b/moli-renderer-v8/src/style_engine/document_world.rs index e6f5c539c3..b59974464a 100644 --- a/moli-renderer-v8/src/style_engine/document_world.rs +++ b/moli-renderer-v8/src/style_engine/document_world.rs @@ -64,7 +64,7 @@ impl DocumentStyleWorld { self.document_state.clear_retained_style_system(); self.document_state.bump_source_set_generation(); self.document_state.bump_computed_cache_generation(); - self.document_state.bump_target_context_epoch(); + self.document_state.note_style_input_change(); self.owner_style_sheet_sources.borrow_mut().clear_all(); *self.linked_stylesheet_sources.borrow_mut() = LinkedStylesheetSources::default(); self.adopted_style_sheet_sources.borrow_mut().clear_all(); @@ -169,6 +169,27 @@ impl DocumentStyleWorlds { .collect() } + pub(super) fn visual_input_generations( + &self, + documents: impl IntoIterator, + ) -> Vec<(DomHandle, u64, u64)> { + let mut documents = documents.into_iter().collect::>(); + documents.sort_by_key(|document| document.index()); + documents.dedup(); + documents + .into_iter() + .map(|document| { + let world = self.for_document(document); + ( + document, + world.document_state.source_set_generation(), + world.document_state.visual_input_generation(), + ) + }) + .collect() + } + + #[cfg(debug_assertions)] pub(super) fn observation_generations( &self, documents: impl IntoIterator, diff --git a/moli-renderer-v8/src/style_engine/mod.rs b/moli-renderer-v8/src/style_engine/mod.rs index 36ac463d0a..03791fce94 100644 --- a/moli-renderer-v8/src/style_engine/mod.rs +++ b/moli-renderer-v8/src/style_engine/mod.rs @@ -262,11 +262,13 @@ impl MoliStyleEngine { .computed_cache_generation } - pub(crate) fn computed_style_observation_generations( + /// Visual output changes with source inputs, not with materialization of + /// the computed cache (for example while observing temporary print media). + pub(crate) fn visual_style_input_generations( &self, documents: impl IntoIterator, - ) -> Vec<(DomHandle, u64, u64, u64)> { - self.document_worlds.observation_generations(documents) + ) -> Vec<(DomHandle, u64, u64)> { + self.document_worlds.visual_input_generations(documents) } pub(crate) fn stylesheet_resource_snapshot_for_document( @@ -414,7 +416,7 @@ impl MoliStyleEngine { pub(crate) fn bump_target_context_epoch_for_document(&self, document: DomHandle) { self.world_for_document(document) .document_state - .bump_target_context_epoch(); + .note_style_input_change(); } #[cfg(test)] diff --git a/moli-renderer-v8/src/style_engine/registered_properties.rs b/moli-renderer-v8/src/style_engine/registered_properties.rs index 69555e64d4..1d33eed1d7 100644 --- a/moli-renderer-v8/src/style_engine/registered_properties.rs +++ b/moli-renderer-v8/src/style_engine/registered_properties.rs @@ -96,7 +96,7 @@ impl MoliStyleEngine { world .registered_custom_properties .register(registration, base_url)?; - world.document_state.bump_target_context_epoch(); + world.document_state.note_style_input_change(); world.document_state.record_source_dirty_scope( StyleScopeId::Document(document), StyleSourceDirtyReason::CustomPropertyRegistration, diff --git a/moli-renderer-v8/src/style_engine/runtime_invalidation.rs b/moli-renderer-v8/src/style_engine/runtime_invalidation.rs index b7fc0d0954..59c126852b 100644 --- a/moli-renderer-v8/src/style_engine/runtime_invalidation.rs +++ b/moli-renderer-v8/src/style_engine/runtime_invalidation.rs @@ -91,7 +91,7 @@ impl MoliStyleEngine { .unwrap_or_default(); if should_defer_structural_mutations(document_effects) { let deferred_started = profile_enabled.then(std::time::Instant::now); - world.document_state.bump_target_context_epoch(); + world.document_state.note_style_input_change(); world .pending_structural_mutations .push(document_effects, emulated_media, viewport); @@ -146,7 +146,7 @@ impl MoliStyleEngine { return false; } if mutation_effects_have_source_scope(effects) { - world.document_state.bump_target_context_epoch(); + world.document_state.note_style_input_change(); } true } @@ -525,6 +525,7 @@ impl MoliStyleEngine { let Some(world) = self.owner_document_world(host, root) else { return; }; + world.document_state.note_visual_input_change(); self.invalidation_cleanup_for_world(&world) .invalidate_inline_style_subtree(host, root); } @@ -533,6 +534,7 @@ impl MoliStyleEngine { let Some(world) = self.owner_document_world(host, root) else { return; }; + world.document_state.note_visual_input_change(); self.invalidation_cleanup_for_world(&world) .invalidate_subtrees_and_shadow_cascade_data(host, [root]); } @@ -552,7 +554,7 @@ impl MoliStyleEngine { }; let source_stores = world.borrow_source_stores(); if !world_has_style_state(self, world) { - world.document_state.bump_target_context_epoch(); + world.document_state.note_style_input_change(); return; } queue_style_invalidation_for_scope( diff --git a/moli-renderer-v8/src/style_engine/schedule.rs b/moli-renderer-v8/src/style_engine/schedule.rs index 364040ba78..4788b5b7eb 100644 --- a/moli-renderer-v8/src/style_engine/schedule.rs +++ b/moli-renderer-v8/src/style_engine/schedule.rs @@ -30,7 +30,7 @@ pub(super) fn queue_style_invalidation_for_scope( let Some(source_scope) = source_scope else { return; }; - document_state.bump_target_context_epoch(); + document_state.note_style_input_change(); let plan_started = profile_enabled.then(std::time::Instant::now); let work = pending_work_for_pending_cause( host, diff --git a/moli-renderer-v8/src/style_engine/state.rs b/moli-renderer-v8/src/style_engine/state.rs index 6bb6016f8d..9c657afffd 100644 --- a/moli-renderer-v8/src/style_engine/state.rs +++ b/moli-renderer-v8/src/style_engine/state.rs @@ -87,6 +87,9 @@ pub(super) struct StyleDocumentState { computed_cache_generation: Cell, retained_style_system_generation: Cell, target_context_epoch: Cell, + /// Source-side invalidation only. The target context also advances when + /// derived styles are cleared, including temporary capture environments. + visual_input_generation: Cell, pub(super) lazy_invalidation_roots: LazyStyleInvalidationRoots, #[cfg(debug_assertions)] completed_style_observation: RefCell>, @@ -112,6 +115,7 @@ impl StyleDocumentState { computed_cache_generation: Cell::new(0), retained_style_system_generation: Cell::new(0), target_context_epoch: Cell::new(0), + visual_input_generation: Cell::new(0), lazy_invalidation_roots: LazyStyleInvalidationRoots::default(), #[cfg(debug_assertions)] completed_style_observation: RefCell::new(None), @@ -164,7 +168,22 @@ impl StyleDocumentState { } } - pub(super) fn bump_target_context_epoch(&self) { + pub(super) fn note_style_input_change(&self) { + self.note_visual_input_change(); + self.bump_computed_context_epoch(); + } + + pub(super) fn visual_input_generation(&self) -> u64 { + self.visual_input_generation.get() + } + + pub(super) fn note_visual_input_change(&self) { + self.visual_input_generation + .set(self.visual_input_generation.get().saturating_add(1)); + } + + /// Cache invalidation during style computation is not a new source input. + pub(super) fn bump_computed_context_epoch(&self) { self.target_context_epoch .set(self.target_context_epoch().saturating_add(1)); }