diff --git a/moli-renderer-v8/src/context_bootstrap.rs b/moli-renderer-v8/src/context_bootstrap.rs index a02f8f5d93..128423d866 100644 --- a/moli-renderer-v8/src/context_bootstrap.rs +++ b/moli-renderer-v8/src/context_bootstrap.rs @@ -146,7 +146,9 @@ pub(crate) use crypto::{ CryptoKeyAlgorithmClonePayload, CryptoKeyClonePayload, WebCryptoRejection, WebCryptoTaskResult, crypto_key_clone_payload_from_object, crypto_key_object_from_clone_payload, }; -pub(crate) use css_fontface_runtime::{new_font_face_set, rebuild_font_face_set_faces}; +pub(crate) use css_fontface_runtime::{ + new_font_face_set, rebuild_font_face_set_faces, settle_document_font_face_set_ready, +}; pub(crate) use location_navigation::{ LocationNavigationKind, dispatch_top_level_form_navigation_event, dispatch_top_level_navigation_event_with_source_element, is_native_location, diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs index 1e9f95a552..870dcdffe2 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime.rs @@ -56,3 +56,4 @@ pub(super) use font_face_set::{ pub(super) use storage::install_font_face_set_template_accessors; pub(crate) use storage::new_font_face_set; pub(crate) use storage::rebuild_font_face_set_faces; +pub(crate) use storage::settle_document_font_face_set_ready; diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/storage.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/storage.rs index 64c39734f6..bc0014fe0c 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/storage.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/storage.rs @@ -86,6 +86,12 @@ fn font_face_set_attribute_getter_callback<'s>( rv.set_undefined(); return; }; + if matches!( + attribute, + FontFaceSetAttribute::Status | FontFaceSetAttribute::Ready + ) { + reconcile_document_font_face_set_ready(scope, args.this()); + } match attribute { FontFaceSetAttribute::Status => rv.set( font_face_set_slot_value(scope, args.this(), FONT_FACE_SET_STATUS_SLOT) @@ -105,6 +111,86 @@ fn font_face_set_attribute_getter_callback<'s>( } } +const DOCUMENT_FONT_FACE_SET_OWNER: &str = "__moliFontFaceSetOwnerDocument"; +const DOCUMENT_FONT_FACE_SET_READY_RESOLVER: &str = "__moliDocumentFontFaceSetReadyResolver"; + +fn reconcile_document_font_face_set_ready<'s>( + scope: &mut v8::PinScope<'s, '_>, + fonts: v8::Local<'s, v8::Object>, +) { + let Some(document) = get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_OWNER) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Ok((host_ptr, handle)) = + crate::native_bridge::node_runtime_and_handle_from_object(scope, document) + else { + return; + }; + if unsafe { &*host_ptr }.document_handle() != handle { + return; + } + crate::document_runtime::DocumentRuntime::reconcile_document_web_fonts_in_current_scope( + scope, host_ptr, + ); + if unsafe { &*host_ptr }.document_web_fonts_have_pending_requests() { + if get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_READY_RESOLVER) + .is_some_and(|value| value.is_object()) + { + return; + } + if let Some(resolver) = v8::PromiseResolver::new(scope) { + let promise = resolver.get_promise(scope); + set_private_value( + scope, + fonts, + DOCUMENT_FONT_FACE_SET_READY_RESOLVER, + resolver.into(), + ); + set_font_face_set_slot_value(scope, fonts, FONT_FACE_SET_READY_SLOT, promise.into()); + set_font_face_set_status(scope, fonts, "loading"); + } + } else { + settle_document_font_face_set_ready(scope, host_ptr); + } +} + +pub(crate) fn settle_document_font_face_set_ready( + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut crate::native_bridge::JsContextHost, +) { + let host = unsafe { &*host_ptr }; + if host.document_web_fonts_have_pending_requests() { + return; + } + let Some(document) = crate::util::node_wrapper_from_handle(scope, host.document_handle()) + else { + return; + }; + let Some(fonts) = document + .get(scope, crate::util::v8str(scope, "__moliFonts").into()) + .and_then(|value| v8::Local::::try_from(value).ok()) + else { + return; + }; + let Some(resolver) = get_private_value(scope, fonts, DOCUMENT_FONT_FACE_SET_READY_RESOLVER) + .filter(|value| value.is_object()) + else { + return; + }; + // Only a native PromiseResolver can occupy this private slot. + let resolver = unsafe { v8::Local::::cast_unchecked(resolver) }; + set_font_face_set_status(scope, fonts, "loaded"); + set_private_value( + scope, + fonts, + DOCUMENT_FONT_FACE_SET_READY_RESOLVER, + v8::undefined(scope).into(), + ); + let _ = resolver.resolve(scope, fonts.into()); +} + pub(crate) fn new_font_face_set<'s>( scope: &mut v8::PinScope<'s, '_>, ) -> Option> { 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 09d40a9c46..a2772e3d70 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout.rs @@ -589,7 +589,21 @@ impl JsContextHost { &self, terminal: CompletedStylesheetWebFont, ) -> DocumentWebFontCompletion { - self.document_layout_state.borrow_mut().complete(terminal) + let completion = self.document_layout_state.borrow_mut().complete(terminal); + if matches!(&completion, DocumentWebFontCompletion::Registered(_)) { + // Geometry sampled while the request was pending contains fallback + // glyph advances. Retire that tree before fonts.ready observers run. + self.document_layout_state + .borrow_mut() + .clear_latest_layout(); + } + completion + } + + pub(crate) fn document_web_fonts_have_pending_requests(&self) -> bool { + self.document_layout_state + .borrow() + .web_fonts_have_pending_requests() } #[cfg(test)] diff --git a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs index dee9842b84..884a07e28e 100644 --- a/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs +++ b/moli-renderer-v8/src/native_bridge/context_host/layout_state.rs @@ -257,6 +257,10 @@ impl DocumentLayoutState { completion } + pub(super) fn web_fonts_have_pending_requests(&self) -> bool { + self.web_fonts.has_pending_requests() + } + #[cfg(test)] pub(super) fn web_font_counts(&self) -> (usize, usize, usize) { ( diff --git a/moli-renderer-v8/src/script_vm/layout.rs b/moli-renderer-v8/src/script_vm/layout.rs index d09f99f5fe..64feab510c 100644 --- a/moli-renderer-v8/src/script_vm/layout.rs +++ b/moli-renderer-v8/src/script_vm/layout.rs @@ -239,79 +239,12 @@ impl ScriptVm { } pub(super) fn reconcile_document_web_fonts_for_layout(&mut self) { - let font_fetch_enabled = self - .document_runtime - .current_document_resource_loader() - .is_some_and(|loader| { - loader - .request_client() - .optional_resource_fetch_enabled(crate::types::SubresourceResourceType::Font) - }); - if !font_fetch_enabled { - return; + if let Err(error) = self.with_default_context_scope(|scope, host_ptr| { + DocumentRuntime::reconcile_document_web_fonts_in_current_scope(scope, host_ptr); + Ok(()) + }) { + tracing::warn!(%error, "failed to reconcile current document web fonts"); } - let root = { - let host = self._context_host.borrow(); - host.dom_host().document_element_handle() - }; - let Some(root) = root else { - return; - }; - let skip_pristine_document = { - let host = self._context_host.borrow(); - let document = host.document_handle(); - host.document_web_font_sidecar_is_pristine() - && !host.document_has_style_state(document) - && !host.document_has_active_author_stylesheet_sources(document) - }; - if skip_pristine_document { - return; - } - let Some(resources) = crate::layout_renderer::current_native_stylesheet_resources( - &self._context_host.borrow(), - root, - ) else { - return; - }; - tracing::trace!( - stylesheet_import_dependency_count = resources.imports().len(), - stylesheet_web_font_dependency_count = resources.web_fonts().len(), - "observed typed stylesheet resource manifest" - ); - let generation = resources.generation(); - if self - ._context_host - .borrow() - .document_web_font_resources_are_current(generation) - { - return; - } - let resources = resources.web_fonts().to_vec(); - self._context_host - .borrow() - .retain_document_web_font_slots(resources.iter()); - if resources.is_empty() { - self._context_host - .borrow() - .publish_document_web_font_resource_generation(generation); - return; - } - let resource_count = resources.len(); - let bound = { - let mut host = self._context_host.borrow_mut(); - let bound = resources - .into_iter() - .filter_map(|resource| { - host.accept_current_main_stylesheet_subresource_load_delay() - .map(|binding| (binding, resource)) - }) - .collect::>(); - if bound.len() == resource_count { - host.publish_document_web_font_resource_generation(generation); - } - bound - }; - self.start_stylesheet_subresource_fetches(bound); } pub(super) fn start_css_images_discovered_by_layout( @@ -446,6 +379,12 @@ impl ScriptVm { tracing::debug!("discarded superseded document web font response") } } + if let Err(error) = self.with_default_context_scope(|scope, host_ptr| { + crate::context_bootstrap::settle_document_font_face_set_ready(scope, host_ptr); + Ok(()) + }) { + tracing::warn!(%error, "failed to settle current document font readiness"); + } } #[cfg(test)] diff --git a/moli-renderer-v8/src/script_vm/tests/extracted/resources_and_media.rs b/moli-renderer-v8/src/script_vm/tests/extracted/resources_and_media.rs index b8911e2fe7..002e2b5fb3 100644 --- a/moli-renderer-v8/src/script_vm/tests/extracted/resources_and_media.rs +++ b/moli-renderer-v8/src/script_vm/tests/extracted/resources_and_media.rs @@ -1,5 +1,63 @@ use super::*; +#[tokio::test] +async fn document_fonts_ready_waits_for_shadow_font_and_refreshes_cached_geometry() { + let (font_url, request_rx, release_tx, server) = spawn_gated_font_resource_server().await; + let document_url = font_url.replace("/print-only.woff2", "/page"); + let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); + loader.set_optional_resource_fetch_mask(crate::protocol_types::OptionalResourceFetchMask::FONT); + let (mut vm, mut completions) = new_parsed_test_vm_with_loader_and_resource_completion_queue( + &document_url, + "", + &loader, + ); + vm.eval(&format!(r#" + const host=document.createElement('span');host.id='font-host';document.body.append(host); + const shadow=host.attachShadow({{mode:'open'}}); + shadow.innerHTML='AAAA'; + globalThis.fontProbe=shadow.getElementById('font-probe'); + globalThis.fontReadyResolved=false; + globalThis.fontReady=document.fonts.ready; + fontReady.then(fonts=>fontReadyResolved=fonts===document.fonts); + 'installed' + "#)).expect("install shadow font without a layout request"); + tokio::time::timeout(std::time::Duration::from_secs(2), request_rx) + .await + .expect("fonts.ready starts the font request") + .expect("request signal"); + assert_eq!(vm.eval("JSON.stringify([fontReadyResolved,document.fonts.status,fontReady===document.fonts.ready])").unwrap(),r#"[false,"loading",true]"#); + let fallback_width = vm + .eval("fontProbe.getBoundingClientRect().width") + .expect("cache fallback font geometry") + .parse::() + .expect("numeric fallback width"); + // The project fixture has a 600-unit advance in a 1000-unit em: + // four glyphs at 20px must measure 48px after loading. + assert!((fallback_width - 48.0).abs() > 0.5); + release_tx + .send(()) + .expect("release the exact font response"); + server.await.expect("font server completes"); + assert!( + tokio::time::timeout( + std::time::Duration::from_secs(2), + completions.wait_for_arrival_without_timeout() + ) + .await + .expect("font completion") + ); + let completion = completions + .pop_next_async_subresource_event() + .expect("typed font completion"); + let _ = vm + .complete_async_subresource_fetch_event_body(completion) + .expect("register the current font"); + vm.perform_owner_lane_task_microtask_checkpoints() + .expect("font completion checkpoint"); + assert_eq!(vm.document_web_font_counts_for_test(), (1, 1, 1)); + assert_eq!(vm.eval("JSON.stringify([fontReadyResolved,document.fonts.status,fontProbe.getBoundingClientRect().width])").unwrap(),r#"[true,"loaded",48]"#); +} + #[tokio::test(flavor = "current_thread")] async fn linked_stylesheet_client_terminal_installs_source_before_its_load_event() { let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader"); diff --git a/moli-renderer-v8/src/script_vm/web_fonts.rs b/moli-renderer-v8/src/script_vm/web_fonts.rs index 6e979fadf3..ad9b8b2e89 100644 --- a/moli-renderer-v8/src/script_vm/web_fonts.rs +++ b/moli-renderer-v8/src/script_vm/web_fonts.rs @@ -48,6 +48,12 @@ impl DocumentWebFontState { self.slots.is_empty() } + pub(crate) fn has_pending_requests(&self) -> bool { + self.slots + .values() + .any(|slot| slot.status == WebFontRequestStatus::Pending) + } + /// Removes registrations whose declarations are no longer in the current /// Stylo source set. Pending network work may still finish, but its exact /// request identity will no longer be accepted by [`Self::complete`]. diff --git a/moli-renderer-v8/src/stylesheet_runtime/settlement.rs b/moli-renderer-v8/src/stylesheet_runtime/settlement.rs index d83e2474f4..f24f7294c4 100644 --- a/moli-renderer-v8/src/stylesheet_runtime/settlement.rs +++ b/moli-renderer-v8/src/stylesheet_runtime/settlement.rs @@ -7,6 +7,71 @@ use super::*; impl DocumentRuntime { + pub(crate) fn reconcile_document_web_fonts_in_current_scope( + scope: &mut v8::PinScope<'_, '_>, + host_ptr: *mut JsContextHost, + ) { + let host = unsafe { &mut *host_ptr }; + host.apply_pending_stylesheet_source_css_projections(scope, host_ptr); + let font_fetch_enabled = + host.current_main_document_resource_loader() + .is_some_and(|loader| { + loader.request_client().optional_resource_fetch_enabled( + crate::types::SubresourceResourceType::Font, + ) + }); + if !font_fetch_enabled { + return; + } + let root = host.dom_host().document_element_handle(); + let Some(root) = root else { + return; + }; + let skip_pristine_document = { + let document = host.document_handle(); + host.document_web_font_sidecar_is_pristine() + && !host.document_has_style_state(document) + && !host.document_has_active_author_stylesheet_sources(document) + }; + if skip_pristine_document { + return; + } + let Some(resources) = + crate::layout_renderer::current_native_stylesheet_resources(host, root) + else { + return; + }; + tracing::trace!( + stylesheet_import_dependency_count = resources.imports().len(), + stylesheet_web_font_dependency_count = resources.web_fonts().len(), + "observed typed stylesheet resource manifest" + ); + let generation = resources.generation(); + if host.document_web_font_resources_are_current(generation) { + return; + } + let resources = resources.web_fonts().to_vec(); + host.retain_document_web_font_slots(resources.iter()); + if resources.is_empty() { + host.publish_document_web_font_resource_generation(generation); + return; + } + let resource_count = resources.len(); + let bound = { + let bound = resources + .into_iter() + .filter_map(|resource| { + host.accept_current_main_stylesheet_subresource_load_delay() + .map(|binding| (binding, resource)) + }) + .collect::>(); + if bound.len() == resource_count { + host.publish_document_web_font_resource_generation(generation); + } + bound + }; + Self::start_stylesheet_subresource_fetches_in_current_scope(scope, host_ptr, bound); + } pub(crate) fn settle_stylesheet_link_clients_in_current_scope( &mut self, scope: &mut v8::PinScope<'_, '_>,