Retain only the frozen layout tree

This commit is contained in:
ldm0
2026-08-14 17:04:49 +08:00
parent 691bd02978
commit d8cc4548f8
24 changed files with 622 additions and 494 deletions
+4 -4
View File
@@ -136,8 +136,8 @@ Was die meisten Browserautomatisierungen wirklich benötigen, ist die Seitenstru
| Agentenanfrage | Verhalten von Moli |
| --- | --- |
| HTML/Markdown extrahieren, DOM abfragen, JS ausführen, Netzwerk/Speicher untersuchen | Liest den Zustand der Browser-Laufzeit direkt aus — löst weder Layout noch Zeichnen aus |
| Begrenzungsrahmen eines Elements lesen, Koordinaten testen, Koordinateneingaben senden | Führt eine Layoutberechnung aus und behält nur den neuesten Geometrie-Snapshot |
| Screenshot aufnehmen oder Screencast aktualisieren | Baut aus dem aktuellen DOM/Stil neu auf, rendert einen neuen Frame und verwirft ihn nach Gebrauch |
| Begrenzungsrahmen eines Elements lesen, Koordinaten testen, Koordinateneingaben senden | Führt eine Layoutberechnung aus und behält nur den neuesten eingefrorenen Layoutbaum |
| Screenshot aufnehmen oder Screencast aktualisieren | Baut aus dem aktuellen DOM/Stil neu auf, ersetzt den eingefrorenen Baum, rendert einen neuen Frame und verwirft den Frame nach Gebrauch |
<p align="center">
<a href="assets/moli_ondemand_rendering_flow.svg">
@@ -177,7 +177,7 @@ Aufwendige Browseroperationen müssen in Moli ausdrücklich aktiviert werden und
| `--image`, `--font`, `--audio`, `--video`, `--media`, `--text-track` | Eine bestimmte optionale Ressourcengruppe aktivieren |
| `--profile-dir`, `--http-cache-dir`, `--cookie-file` | Persistenz je nach Bedarf des Workloads gezielt aktivieren |
Das Layoutergebnis ist ein bei Bedarf erstellter Snapshot und kein dauerhaft gepflegter Zustand: Die erste Geometrieanfrage (Kaltstart) baut ein vollständiges Layout aus dem aktuellen DOM/Stil auf und behält nur die neueste `LayoutPassOutput`. Danach können normale Geometrieabfragen diesen Snapshot selbst dann wiederverwenden, wenn sich die Seite geändert hat. Screenshots und Screencasts werden dagegen jedes Mal neu aufgebaut und verwenden keine alten Ergebnisse.
Das Layoutergebnis ist ein bei Bedarf erstellter Snapshot und kein dauerhaft gepflegter Zustand: Die erste Geometrieanfrage (Kaltstart) baut aus dem aktuellen DOM/Stil einen temporären Arbeitsbaum auf, friert dessen kanonische Geometrie in einen unveränderlichen, DOM-unabhängigen `FrozenLayoutTree` ein und behält nur diesen neuesten Baum. Normale Geometrieabfragen können ihn auch nach Seitenänderungen wiederverwenden; Screenshots und Screencasts bauen stets neu auf, ersetzen den eingefrorenen Baum und verwenden keine alten Zeichenergebnisse.
## Architektur
@@ -190,7 +190,7 @@ Moli ist ein eigenständiger Browser-Kernel und kein Chromium-Wrapper. Er ist in
- Taffy + Parley — Box- und Textlayout
- AnyRender/Vello CPU, `usvg` und das Rust-Bildökosystem — Software-Rendering
Dokument und Stil haben genau eine maßgebliche Datenquelle: die Integration von nativem DOM und Stylo. Jede echte Aktualisierung baut das Layout daraus neu auf, überführt das Ergebnis in DOM-neutrale, unveränderliche Daten und verwirft anschließend den temporären Zustand aus diesem Layout- und Zeichendurchlauf. Das gesamte System besitzt keinen inkrementellen Layoutbaum, keinen Damage-Graph, keine beibehaltene Displayliste, keinen GPU-Compositor und kein persistentes Fenster.
Dokument und Stil haben genau eine maßgebliche Datenquelle: die Integration von nativem DOM und Stylo. Jede echte Aktualisierung erstellt daraus einen temporären Arbeitsbaum, erzeugt und verbraucht bei Bedarf einen frischen Paint-Snapshot, friert die endgültige Box- und Fragmentgeometrie in einen kompakten `FrozenLayoutTree` ein und verwirft Arbeitsbaum, Stilreferenzen, Layout-Caches, Diagnosen und Paint-Zustand. Quellzuordnung und Hit-Test-Kandidaten werden bei Abfragen aus dem eingefrorenen Baum abgeleitet. Das System besitzt keinen inkrementell gepflegten Layoutbaum, keinen Damage-Graph, keine beibehaltene Displayliste, keinen GPU-Compositor und kein persistentes Fenster.
## Testdaten
+4 -4
View File
@@ -136,8 +136,8 @@ Lo que la mayoría de las tareas de automatización de navegadores necesitan rea
| Solicitud del agente | Qué hace Moli |
| --- | --- |
| Extraer HTML/Markdown, consultar el DOM, ejecutar JS, inspeccionar la red o el almacenamiento | Lee directamente el estado del entorno de ejecución del navegador, sin activar la disposición ni el dibujo |
| Leer el rectángulo delimitador de un elemento, comprobar unas coordenadas, enviar entradas por coordenadas | Ejecuta un cálculo de disposición y conserva únicamente la instantánea de geometría más reciente |
| Capturar una pantalla o actualizar un screencast | Reconstruye desde el DOM y los estilos actuales, renderiza un fotograma nuevo y lo descarta después de usarlo |
| Leer el rectángulo delimitador de un elemento, comprobar unas coordenadas, enviar entradas por coordenadas | Ejecuta un cálculo de disposición y conserva únicamente el árbol de disposición congelado más reciente |
| Capturar una pantalla o actualizar un screencast | Reconstruye desde el DOM y los estilos actuales, sustituye el árbol congelado, renderiza un fotograma nuevo y descarta el fotograma después de usarlo |
<p align="center">
<a href="assets/moli_ondemand_rendering_flow.svg">
@@ -177,7 +177,7 @@ En Moli, las operaciones costosas del navegador deben activarse explícitamente
| `--image`, `--font`, `--audio`, `--video`, `--media`, `--text-track` | Activa por separado una familia concreta de recursos opcionales |
| `--profile-dir`, `--http-cache-dir`, `--cookie-file` | Activa selectivamente la persistencia que necesita la carga de trabajo |
El resultado de la disposición es una instantánea tomada bajo demanda, no un estado que se mantenga continuamente: la primera solicitud de geometría (inicio en frío) construye una disposición completa a partir del DOM y de los estilos actuales, y solo conserva el `LayoutPassOutput` más reciente. A partir de ese momento, las lecturas de geometría ordinarias pueden reutilizar esa instantánea aunque la página haya cambiado; las capturas de pantalla y los screencasts, en cambio, se reconstruyen siempre y nunca reutilizan resultados antiguos.
El resultado de la disposición es una instantánea tomada bajo demanda, no un estado que se mantenga continuamente: la primera solicitud de geometría (inicio en frío) construye un árbol de trabajo temporal a partir del DOM y de los estilos actuales, congela su geometría canónica en un `FrozenLayoutTree` inmutable e independiente del DOM y conserva únicamente ese árbol más reciente. Las lecturas de geometría ordinarias pueden reutilizarlo aunque la página haya cambiado; las capturas de pantalla y los screencasts siempre reconstruyen y sustituyen el árbol congelado, y nunca reutilizan resultados de dibujo antiguos.
## Arquitectura
@@ -190,7 +190,7 @@ Moli es un núcleo de navegador independiente, no una envoltura de Chromium. Est
- Taffy + Parley — disposición de cajas y texto
- AnyRender/Vello CPU, `usvg` y el ecosistema de imágenes de Rust — renderizado por software
El documento y los estilos tienen una única fuente de verdad: la integración del DOM nativo con Stylo. Cada actualización real reconstruye la disposición a partir de esa fuente, convierte el resultado en datos inmutables e independientes del DOM y después descarta el estado temporal producido durante esa pasada de disposición y dibujo. En todo el sistema no hay ningún árbol de disposición incremental, grafo de daños, lista de visualización retenida, compositor de GPU ni ventana persistente.
El documento y los estilos tienen una única fuente de verdad: la integración del DOM nativo con Stylo. Cada actualización real crea un árbol de trabajo temporal, produce y consume bajo demanda una instantánea de dibujo nueva, congela la geometría final de cajas y fragmentos en un `FrozenLayoutTree` compacto y después descarta el árbol de trabajo, las referencias de estilo, las cachés de disposición, los diagnósticos y el estado de dibujo. La asociación con las fuentes y los candidatos de hit-test se derivan del árbol congelado al consultarlos. No hay ningún árbol de disposición mantenido incrementalmente, grafo de daños, lista de visualización retenida, compositor de GPU ni ventana persistente.
## Datos de las pruebas
+4 -4
View File
@@ -136,8 +136,8 @@ Ce dont la plupart des tâches d'automatisation ont réellement besoin, c'est de
| Requête de l'agent | Traitement par Moli |
| --- | --- |
| Extraire du HTML/Markdown, interroger le DOM, exécuter du JS, inspecter le réseau ou le stockage | Lit directement l'état de l'environnement d'exécution du navigateur — ne déclenche ni mise en page ni dessin |
| Lire la boîte englobante d'un élément, tester des coordonnées, envoyer une entrée par coordonnées | Exécute un calcul de mise en page et ne conserve que le dernier instantané de géométrie |
| Prendre une capture d'écran ou actualiser un screencast | Reconstruit à partir du DOM et des styles actuels, rend une nouvelle image, puis la supprime après usage |
| Lire la boîte englobante d'un élément, tester des coordonnées, envoyer une entrée par coordonnées | Exécute un calcul de mise en page et ne conserve que le dernier arbre de mise en page figé |
| Prendre une capture d'écran ou actualiser un screencast | Reconstruit à partir du DOM et des styles actuels, remplace l'arbre figé, rend une nouvelle image, puis supprime l'image après usage |
<p align="center">
<a href="assets/moli_ondemand_rendering_flow.svg">
@@ -177,7 +177,7 @@ Dans Moli, les opérations coûteuses du navigateur doivent être activées expl
| `--image`, `--font`, `--audio`, `--video`, `--media`, `--text-track` | Active une famille précise de ressources facultatives |
| `--profile-dir`, `--http-cache-dir`, `--cookie-file` | Active sélectivement la persistance nécessaire à la charge de travail |
Le résultat de la mise en page est un instantané produit à la demande, et non un état maintenu en continu : la première demande de géométrie (à froid) construit une mise en page complète à partir du DOM et des styles actuels, puis ne conserve que le dernier `LayoutPassOutput`. Par la suite, les lectures de géométrie ordinaires peuvent réutiliser cet instantané même si la page a changé. Les captures d'écran et les screencasts, eux, sont reconstruits à chaque fois et ne réutilisent jamais d'anciens résultats.
Le résultat de la mise en page est un instantané produit à la demande, et non un état maintenu en continu : la première demande de géométrie (à froid) construit un arbre de travail temporaire à partir du DOM et des styles actuels, fige sa géométrie canonique dans un `FrozenLayoutTree` immuable et indépendant du DOM, puis ne conserve que ce dernier arbre. Les lectures de géométrie ordinaires peuvent le réutiliser même si la page a changé. Les captures d'écran et les screencasts reconstruisent et remplacent toujours l'arbre figé, sans réutiliser d'anciens résultats de dessin.
## Architecture
@@ -190,7 +190,7 @@ Moli est un noyau de navigateur autonome, et non une surcouche de Chromium. Dév
- Taffy + Parley — mise en page des boîtes et du texte
- AnyRender/Vello CPU, `usvg` et l'écosystème d'images Rust — rendu logiciel
Le document et les styles n'ont qu'une seule source de vérité : le DOM natif et son intégration à Stylo. Chaque véritable actualisation reconstruit la mise en page à partir de cette source, convertit le résultat en données immuables indépendantes du DOM, puis élimine l'état temporaire produit pendant cette passe de mise en page et de dessin. L'ensemble du système ne comporte ni arbre de mise en page incrémental, ni graphe de dommages, ni liste d'affichage persistante, ni compositeur GPU, ni fenêtre persistante.
Le document et les styles n'ont qu'une seule source de vérité : le DOM natif et son intégration à Stylo. Chaque véritable actualisation crée un arbre de travail temporaire, produit et consomme au besoin un nouvel instantané de dessin, fige la géométrie finale des boîtes et fragments dans un `FrozenLayoutTree` compact, puis élimine l'arbre de travail, les références de style, les caches de mise en page, les diagnostics et l'état de dessin. Les associations aux sources et les candidats de hit-test sont dérivés de l'arbre figé lors des requêtes. Le système ne comporte ni arbre de mise en page maintenu incrémentalement, ni graphe de dommages, ni liste d'affichage persistante, ni compositeur GPU, ni fenêtre persistante.
## Données de test
+4 -4
View File
@@ -135,8 +135,8 @@ AI エージェントの処理では、機能の充実度と速度に加え、
| AI エージェントが求める処理 | Moli の動作 |
| --- | --- |
| HTML や Markdown の抽出、DOM の照会、JavaScript の実行、通信や保存領域の確認 | ブラウザの実行状態を直接読み取ります。レイアウト計算も描画も行いません。 |
| 要素の位置・寸法の取得、座標判定、座標入力 | レイアウト計算を 1 回行い、最新の位置・寸法情報だけを保持します。 |
| 画面画像の取得、連続画面配信の更新 | 現在の DOM とスタイルから画面を再構築して描画し、使用後すぐに破棄します。 |
| 要素の位置・寸法の取得、座標判定、座標入力 | レイアウト計算を 1 回行い、最新の凍結レイアウト木だけを保持します。 |
| 画面画像の取得、連続画面配信の更新 | 現在の DOM とスタイルから再構築して凍結木を置き換え、新しいフレームを描画し、フレームは使用後すぐに破棄します。 |
<p align="center">
<a href="assets/moli_ondemand_rendering_flow.svg">
@@ -176,7 +176,7 @@ Moli では、処理負荷の高い機能は明示的に有効にした場合だ
| `--image`、`--font`、`--audio`、`--video`、`--media`、`--text-track` | 指定した種類のデータを取得します。 |
| `--profile-dir`、`--http-cache-dir`、`--cookie-file` | 必要な保存機能だけを選んで有効にします。 |
レイアウト結果は常に更新される状態ではなく、要求を受けた時点の状態です。最初に位置・寸法を要求されたときは、現在の DOM とスタイルから完全なレイアウトを 1 回構築し、最新の `LayoutPassOutput` だけを保持します。その後はページが変化していても、通常の位置・寸法の取得では、保持した結果を再利用する場合があります。一方、画面画像と連続画面配信は毎回構築し直し、古い結果を再利用しません。
レイアウト結果は常に更新される状態ではなく、要求を受けた時点の状態です。最初に位置・寸法を要求されたときは、現在の DOM とスタイルから一時的な作業用レイアウト木を構築し、その確定済みジオメトリを DOM から独立した不変の `FrozenLayoutTree` に凍結して、最新の木だけを保持します。その後はページが変化していても通常の位置・寸法取得で古い木を再利用する場合があります。一方、画面画像と連続画面配信は毎回再構築して凍結木を置き換え、古い描画結果を再利用しません。
## 仕組み
@@ -189,7 +189,7 @@ Moli は、Chromium を外から操作するだけの仕組みではなく、独
- Taffy + Parley — ボックス配置と文字組み
- AnyRender/Vello CPU、`usvg`、Rust の画像関連ライブラリ — ソフトウェア描画
文書とスタイルの唯一の正しい情報源は、ブラウザ本来の DOM と Stylo の組み合わせです。更新が必要になるたびに、この情報からレイアウトを再構築し、結果を DOM から独立した読み取り専用のデータへ変換します。その後、レイアウト計算と描画で生じた一時的な状態を破棄します。増分更新用のレイアウト木、描画差分の追跡情報、保持型の描画命令一覧、GPU による画面合成機構、常設のウィンドウは使用しません。
文書とスタイルの唯一の正しい情報源は、ブラウザ本来の DOM と Stylo の組み合わせです。更新のたびに一時的な作業木を構築し、必要なら新しい描画スナップショットを生成してその場で消費し、最終的なボックスと文字断片のジオメトリをコンパクトな `FrozenLayoutTree` に凍結します。その後、作業木、スタイル参照、レイアウトキャッシュ、診断情報、描画状態を破棄します。ソース対応表と hit-test 候補は問い合わせ時に凍結木から導出します。増分維持されるレイアウト木、描画差分の追跡情報、保持型の描画命令一覧、GPU による画面合成機構、常設のウィンドウは使用しません。
## 検証結果
+15 -11
View File
@@ -150,8 +150,8 @@ operations that genuinely require them.
| Agent request | What Moli does |
| --- | --- |
| Extract HTML/Markdown, query the DOM, run JS, inspect network/storage | Reads browser runtime state directly — does not trigger layout or paint |
| Read an element's box, hit-test coordinates, send coordinate input | Runs one layout calculation and keeps only the latest geometry snapshot |
| Capture a screenshot or refresh a screencast | Rebuilds from the current DOM/style, renders a fresh frame, and discards it after use |
| Read an element's box, hit-test coordinates, send coordinate input | Runs one layout calculation and retains only the latest frozen layout tree |
| Capture a screenshot or refresh a screencast | Rebuilds from the current DOM/style, replaces the frozen tree, renders a fresh frame, and discards the frame after use |
<p align="center">
<a href="assets/moli_ondemand_rendering_flow.svg">
@@ -211,10 +211,12 @@ enabled by default:
| `--profile-dir`, `--http-cache-dir`, `--cookie-file` | Selectively enable the persistence required by the workload |
Layout is an on-demand snapshot rather than continuously maintained state. The
first geometry request (a cold start) builds one complete layout from the
current DOM/style and retains only the latest `LayoutPassOutput`. After that,
ordinary geometry reads may reuse the snapshot even if the page has changed;
screenshots and screencasts always rebuild and never reuse stale results.
first geometry request (a cold start) builds a working layout tree from the
current DOM/style, freezes its canonical geometry into an immutable,
DOM-independent `FrozenLayoutTree`, and retains only that latest tree. Ordinary
geometry reads may reuse it even if the page has changed; screenshots and
screencasts always rebuild, replace the frozen tree, and never reuse stale
paint results.
## Architecture
@@ -229,11 +231,13 @@ Rust, has its own ownership and lifecycle rules, and relies on:
- AnyRender/Vello CPU, `usvg`, and the Rust image ecosystem — software rendering
Document and style have a single source of truth: the native DOM and its Stylo
integration. Every real refresh rebuilds layout from that source, converts the
result into immutable, DOM-independent data, and then discards the temporary
state created during that layout and paint pass. The system has no incremental
layout tree, damage graph, retained display list, GPU compositor, or persistent
window.
integration. Every real refresh builds a temporary working tree from that
source, optionally produces and consumes one fresh paint snapshot, freezes the
canonical box/fragment geometry into a compact `FrozenLayoutTree`, and discards
the working tree, style borrows, layout caches, diagnostics, and paint state.
Source lookup and hit-test candidates are derived from the frozen tree when
queried. The system has no incrementally maintained layout tree, damage graph,
retained display list, GPU compositor, or persistent window.
## Test data
+4 -4
View File
@@ -135,8 +135,8 @@ await browser.close();
| 智能体请求 | Moli 的处理方式 |
| --- | --- |
| 提取 HTML/Markdown、查询 DOM、运行 JS、检查网络/存储 | 直接读取浏览器运行时状态——不触发布局或绘制 |
| 读取元素边界框、对某个坐标做命中测试、发送坐标输入 | 执行一次布局计算,只保留最新的几何快照 |
| 截图或刷新屏幕串流 | 根据当前 DOM/样式重新构建、渲染新的一帧,用完即丢弃 |
| 读取元素边界框、对某个坐标做命中测试、发送坐标输入 | 执行一次布局计算,只保留最新的冻结布局树 |
| 截图或刷新屏幕串流 | 根据当前 DOM/样式重新构建并替换冻结树,渲染新的一帧,帧用完即丢弃 |
<p align="center">
<a href="assets/moli_ondemand_rendering_flow.svg">
@@ -176,7 +176,7 @@ Moli 是 Lexmount 旗下的开源无头浏览器;Lexmount Browser 则是围绕
| `--image`、`--font`、`--audio`、`--video`、`--media`、`--text-track` | 单独启用某一类可选资源 |
| `--profile-dir`、`--http-cache-dir`、`--cookie-file` | 按工作负载需要,选择性开启持久化能力 |
布局结果是按需采样的一份快照,而不是持续维护的状态:第一次几何请求(冷启动)会根据当前的 DOM/样式完整构建一次布局,并只保留最新的一份 `LayoutPassOutput`。在此之后,即便页面发生了变化,普通的几何读取也可能复用这份快照;但截图和屏幕串流每次都会重新构建,不会复用旧结果。
布局结果是按需采样的一份快照,而不是持续维护的状态:第一次几何请求(冷启动)会根据当前 DOM/样式构建临时工作布局树,再把工作树丢弃后无法重算的几何冻结成一棵与 DOM 无关、不可变的 `FrozenLayoutTree`,长期只保留最新的这一棵树。在此之后,即便页面发生了变化,普通几何读取也可能复用旧树;截图和屏幕串流则每次都会重新构建并替换冻结树,不会复用旧的绘制结果。
## 架构
@@ -189,7 +189,7 @@ Moli 是一个独立的浏览器内核,而不是对 Chromium 的封装。它
- Taffy + Parley——盒模型与文本布局
- AnyRender/Vello CPU、`usvg` 以及 Rust 图像生态——软件渲染
文档和样式只有一个事实来源:原生 DOM 与 Stylo 的集成。每次真正的刷新,都会基于这个事实来源重新构建布局,把结果转换成一份与 DOM 无关的不可变数据,然后丢弃这次布局和绘制过程中产生的临时状态。整个系统里没有增量布局树、没有损伤区域图、没有保留式显示列表、没有 GPU 合成器,也没有持久化的窗口。
文档和样式只有一个事实来源:原生 DOM 与 Stylo 的集成。每次真正的刷新,都会据此构建临时工作树,按需生成并消费一份新的绘制快照,把最终的盒与文字分片几何冻结成紧凑的 `FrozenLayoutTree`,随后丢弃工作树、样式借用、布局缓存、诊断和绘制状态。来源查询表和命中测试候选不再长期保存,而是在查询时从冻结树派生。整个系统没有增量维护的布局树、损伤区域图、保留式显示列表、GPU 合成器或持久化窗口。
## 测试数据
+5 -5
View File
@@ -1,6 +1,6 @@
<svg width="100%" viewBox="0 0 680 480" role="img" style="" xmlns="http://www.w3.org/2000/svg">
<title style="fill:rgb(0, 0, 0);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">How Moli handles a request: DOM-first by default, layout and paint built fresh only on demand</title>
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Native DOM and Stylo state is the single source of truth. Most requests take the DOM-first path directly, with no layout or paint. Requests that need geometry or pixels trigger an on-demand layout and paint pass, which produces a geometry snapshot or a screenshot, then that pass-local state is dropped.</desc>
<desc style="fill:rgb(0, 0, 0);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">Native DOM and Stylo state is the single source of truth. Most requests take the DOM-first path directly, with no layout or paint. Requests that need geometry or pixels trigger an on-demand pass. Its geometry is frozen into the single retained layout tree, while fresh paint and all working state are discarded after use.</desc>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
@@ -40,7 +40,7 @@
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="340" y="260" width="140" height="56" rx="8" stroke-width="0.5" style="fill:rgb(230, 241, 251);stroke:rgb(24, 95, 165);color:rgb(11, 11, 11);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="410" y="280" text-anchor="middle" dominant-baseline="central" style="fill:rgb(12, 68, 124);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Geometry</text>
<text x="410" y="298" text-anchor="middle" dominant-baseline="central" style="fill:rgb(24, 95, 165);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">hit-test, input</text>
<text x="410" y="298" text-anchor="middle" dominant-baseline="central" style="fill:rgb(24, 95, 165);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">latest frozen tree</text>
</g>
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
@@ -56,8 +56,8 @@
<g style="fill:rgb(0, 0, 0);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto">
<rect x="360" y="376" width="260" height="56" rx="8" stroke-width="0.5" style="fill:rgb(241, 239, 232);stroke:rgb(95, 94, 90);color:rgb(11, 11, 11);stroke-width:0.5px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:16px;font-weight:400;text-anchor:start;dominant-baseline:auto"/>
<text x="490" y="396" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Layout &amp; paint state dropped</text>
<text x="490" y="414" text-anchor="middle" dominant-baseline="central" style="fill:rgb(95, 94, 90);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Kept only until the next request</text>
<text x="490" y="396" text-anchor="middle" dominant-baseline="central" style="fill:rgb(68, 68, 65);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:14px;font-weight:500;text-anchor:middle;dominant-baseline:central">Working layout &amp; paint dropped</text>
<text x="490" y="414" text-anchor="middle" dominant-baseline="central" style="fill:rgb(95, 94, 90);stroke:none;color:rgb(11, 11, 11);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;opacity:1;font-family:anthropic-sans, -apple-system, &quot;system-ui&quot;, &quot;Segoe UI&quot;, sans-serif;font-size:12px;font-weight:400;text-anchor:middle;dominant-baseline:central">Only the frozen layout tree remains</text>
</g>
</svg>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+3 -3
View File
@@ -15,11 +15,11 @@ pub enum LayoutError {
/// A screenshot or screencast requested an invalid capture surface.
#[error("invalid paint capture: {detail}")]
InvalidPaintCapture { detail: String },
/// One owned geometry projection is too large to publish or retain.
/// One frozen layout tree is too large to publish or retain.
#[error(
"layout output contains {boxes} boxes, {fragments} fragments, and an estimated {estimated_bytes} bytes; limits are {max_boxes} boxes, {max_fragments} fragments, and {max_bytes} bytes"
"frozen layout tree contains {boxes} boxes, {fragments} fragments, and an estimated {estimated_bytes} bytes; limits are {max_boxes} boxes, {max_fragments} fragments, and {max_bytes} bytes"
)]
OutputRetentionBudgetExceeded {
TreeRetentionBudgetExceeded {
boxes: usize,
fragments: usize,
estimated_bytes: usize,
+12 -13
View File
@@ -3,7 +3,7 @@
//! The crate knows Stylo and Taffy but never knows Moli's live DOM or V8
//! runtime. Renderer-owned adapters lend a canonical source view and resolve
//! styles; all source/style borrows and per-pass caches are gone before the
//! returned [`LayoutPassOutput`] or [`PaintSnapshot`] crosses into a consumer.
//! returned [`FrozenLayoutTree`] or [`PaintSnapshot`] crosses into a consumer.
mod builder;
mod capture;
@@ -41,20 +41,19 @@ pub use normalize_source::{
NormalizedLayoutSourceNode, NormalizedLayoutSourceTree, normalize_layout_source,
};
pub use output::{
GeometryProvider, LayoutAnswers, LayoutBoxGeometry, LayoutBoxModel, LayoutCaretPosition,
LayoutClipChainId, LayoutClipNode, LayoutCoordinateSpace, LayoutCoordinateSpaceId,
LayoutDocumentMetrics, LayoutElementMetrics, LayoutFlushReason, LayoutFragment,
LayoutFragmentBoxModel, LayoutFragmentId, LayoutFragmentKind, LayoutHit, LayoutHitTestEntry,
LayoutHitTestIndex, LayoutIntersectionGeometry, LayoutNodeOutput, LayoutOutputBoxId,
LayoutOutputRetentionMetrics, LayoutPassMetrics, LayoutPassOutput, LayoutPoint, LayoutQuad,
LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect, LayoutScrollContainerMetrics,
LayoutScrollExtent, LayoutScrollExtentId, LayoutScrollIntoViewGeometry, LayoutSize,
LayoutTransform2D, LayoutViewport, MAX_RETAINED_LAYOUT_BOXES, MAX_RETAINED_LAYOUT_FRAGMENTS,
MAX_RETAINED_LAYOUT_OUTPUT_BYTES,
FrozenCoordinateSpace, FrozenLayoutBox, FrozenLayoutTree, GeometryProvider, LayoutAnswers,
LayoutBoxGeometry, LayoutBoxModel, LayoutCaretPosition, LayoutClipChainId, LayoutClipNode,
LayoutCoordinateSpaceId, LayoutDocumentMetrics, LayoutElementMetrics, LayoutFlushReason,
LayoutFragment, LayoutFragmentBoxModel, LayoutFragmentId, LayoutFragmentKind, LayoutHit,
LayoutIntersectionGeometry, LayoutNodeOutput, LayoutOutputBoxId, LayoutPassMetrics,
LayoutPassResult, LayoutPoint, LayoutQuad, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch,
LayoutRect, LayoutScrollContainerMetrics, LayoutScrollExtent, LayoutScrollIntoViewGeometry,
LayoutSize, LayoutTransform2D, LayoutTreeRetentionMetrics, LayoutViewport,
MAX_RETAINED_LAYOUT_BOXES, MAX_RETAINED_LAYOUT_FRAGMENTS, MAX_RETAINED_LAYOUT_TREE_BYTES,
};
pub use pass::{
EmbeddedFrameRenderer, LayoutPassRequest, ScreenshotLayoutRequest, build_layout_pass_output,
build_layout_pass_output_with_embedded_frames, build_screenshot_snapshot,
EmbeddedFrameRenderer, LayoutPassRequest, ScreenshotLayoutRequest, build_layout_pass,
build_layout_pass_with_embedded_frames, build_screenshot_snapshot,
};
pub use snapshot::{
PaintBlendMode, PaintBorderColors, PaintBorderStyle, PaintBorderStyles, PaintBoxShadow,
+310 -178
View File
@@ -1,17 +1,17 @@
use std::{
collections::{HashMap, HashSet},
collections::HashSet,
fmt::Debug,
hash::Hash,
ops::Range,
ops::{Deref, Range},
time::Duration,
};
use crate::{LayoutBoxKind, LayoutError, LayoutPosition, PaintDiagnostic, PaintSnapshot};
use crate::{LayoutError, LayoutPosition, PaintDiagnostic, PaintSnapshot};
/// Viewport inputs shared by layout, geometry queries, and paint projection.
///
/// Dimensions are CSS pixels. Device-pixel conversion belongs to the paint
/// backend and never changes the geometry stored in a layout output.
/// backend and never changes the geometry stored in a frozen layout tree.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutViewport {
pub css_width: u32,
@@ -59,7 +59,7 @@ impl LayoutSize {
}
}
/// An axis-aligned rectangle in one explicit [`LayoutCoordinateSpace`].
/// An axis-aligned rectangle in one explicit layout coordinate space.
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct LayoutRect {
pub x: f32,
@@ -230,7 +230,9 @@ macro_rules! dense_output_id {
impl $name {
pub(crate) fn from_index(index: usize) -> Self {
Self(u32::try_from(index).expect("one layout output exceeded the u32 id limit"))
Self(
u32::try_from(index).expect("one frozen layout tree exceeded the u32 id limit"),
)
}
pub const fn index(self) -> usize {
@@ -244,33 +246,47 @@ dense_output_id!(LayoutOutputBoxId);
dense_output_id!(LayoutFragmentId);
dense_output_id!(LayoutCoordinateSpaceId);
dense_output_id!(LayoutClipChainId);
dense_output_id!(LayoutScrollExtentId);
/// One explicit local coordinate system in a pass output.
/// One explicit local coordinate system in a frozen layout tree.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutCoordinateSpace {
pub id: LayoutCoordinateSpaceId,
pub owner: Option<LayoutOutputBoxId>,
pub parent: Option<LayoutCoordinateSpaceId>,
pub local_to_parent: LayoutTransform2D,
pub(crate) struct LayoutCoordinateSpace {
pub(crate) id: LayoutCoordinateSpaceId,
pub(crate) owner: Option<LayoutOutputBoxId>,
pub(crate) parent: Option<LayoutCoordinateSpaceId>,
pub(crate) local_to_parent: LayoutTransform2D,
/// Maps local coordinates to the visual document coordinate system. This
/// includes element scrolling but excludes the viewport scroll offset.
pub local_to_document: LayoutTransform2D,
pub(crate) local_to_document: LayoutTransform2D,
/// Maps local coordinates directly to viewport CSS pixels.
pub(crate) local_to_viewport: LayoutTransform2D,
}
/// Query-facing coordinate data retained for one frozen box-tree node.
#[derive(Clone, Debug, PartialEq)]
pub struct FrozenCoordinateSpace {
pub owner: Option<LayoutOutputBoxId>,
pub local_to_viewport: LayoutTransform2D,
}
impl From<LayoutCoordinateSpace> for FrozenCoordinateSpace {
fn from(space: LayoutCoordinateSpace) -> Self {
Self {
owner: space.owner,
local_to_viewport: space.local_to_viewport,
}
}
}
/// One rectangular clip linked to its ancestor clip.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutClipNode {
pub id: LayoutClipChainId,
pub parent: Option<LayoutClipChainId>,
pub owner: Option<LayoutOutputBoxId>,
pub coordinate_space: LayoutCoordinateSpaceId,
pub rect: LayoutRect,
}
/// Complete physical box model for one output-local CSS box.
/// Complete physical box model for one tree-local CSS box.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutBoxModel {
pub content: LayoutQuad,
@@ -282,12 +298,9 @@ pub struct LayoutBoxModel {
/// Per-box scroll geometry in the box's own coordinate space.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutScrollExtent {
pub id: LayoutScrollExtentId,
pub owner: LayoutOutputBoxId,
pub scrollport: LayoutRect,
pub scrollable_overflow: LayoutRect,
pub scroll_size: LayoutSize,
pub requested_offset: LayoutPoint,
pub applied_offset: LayoutPoint,
pub minimum_offset: LayoutPoint,
pub maximum_offset: LayoutPoint,
@@ -295,15 +308,12 @@ pub struct LayoutScrollExtent {
pub clips_overflow: bool,
}
/// Geometry and provenance retained for one output-local box.
/// Geometry retained for one tree-local box.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutBoxGeometry {
pub id: LayoutOutputBoxId,
pub parent: Option<LayoutOutputBoxId>,
pub layout_parent: Option<LayoutOutputBoxId>,
pub positioned_containing_block: Option<LayoutOutputBoxId>,
pub role: LayoutBoxKind,
pub source_label: String,
pub position: LayoutPosition,
pub coordinate_space: LayoutCoordinateSpaceId,
pub clip_chain: Option<LayoutClipChainId>,
@@ -311,7 +321,6 @@ pub struct LayoutBoxGeometry {
pub padding_box: LayoutRect,
pub border_box: LayoutRect,
pub margin_box: LayoutRect,
pub scroll_extent: LayoutScrollExtentId,
pub fragments: Vec<LayoutFragmentId>,
/// Untransformed border-box origin in document layout coordinates.
pub layout_origin_in_document: LayoutPoint,
@@ -323,6 +332,31 @@ pub struct LayoutBoxGeometry {
pub pointer_events: bool,
}
/// One node in the immutable layout tree retained after a full pass.
///
/// `geometry_source` associates ordinary CSSOM geometry with its source. A
/// split inline continuation can therefore share its originating source while
/// remaining a distinct box-tree node. `hit_source` is separate because a
/// generated pseudo box participates in hit testing as its originating DOM
/// element without manufacturing CSSOM rects for that element.
#[derive(Clone, Debug, PartialEq)]
pub struct FrozenLayoutBox<N> {
pub geometry: LayoutBoxGeometry,
pub scroll_extent: LayoutScrollExtent,
pub coordinate_space: FrozenCoordinateSpace,
pub geometry_source: Option<N>,
pub principal_source: Option<N>,
pub hit_source: Option<N>,
}
impl<N> Deref for FrozenLayoutBox<N> {
type Target = LayoutBoxGeometry;
fn deref(&self) -> &Self::Target {
&self.geometry
}
}
/// Physical boxes retained for one box fragment in that fragment's local
/// coordinate space. Inline elements can own several of these across lines.
#[derive(Clone, Copy, Debug, PartialEq)]
@@ -334,7 +368,7 @@ pub struct LayoutFragmentBoxModel {
}
/// A geometry fragment kind. IDs contained here are valid only in the same
/// [`LayoutPassOutput`].
/// [`FrozenLayoutTree`].
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LayoutFragmentKind {
Box {
@@ -353,7 +387,6 @@ pub enum LayoutFragmentKind {
Text {
box_id: LayoutOutputBoxId,
line_index: usize,
source_byte_range: Range<usize>,
source_utf16_range: Range<usize>,
rtl: bool,
},
@@ -366,13 +399,12 @@ pub struct LayoutFragment {
pub kind: LayoutFragmentKind,
pub rect: LayoutRect,
pub box_model: Option<LayoutFragmentBoxModel>,
pub baseline: Option<f32>,
pub coordinate_space: LayoutCoordinateSpaceId,
pub clip_chain: Option<LayoutClipChainId>,
pub paint_order: Option<u32>,
}
/// Output references associated with one source-tree node.
/// A short-lived source view derived from frozen box provenance.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct LayoutNodeOutput {
pub principal_box: Option<LayoutOutputBoxId>,
@@ -385,35 +417,28 @@ pub struct LayoutNodeOutput {
/// One front-to-back hit-test candidate.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutHitTestEntry<N> {
pub source: N,
pub fragment: LayoutFragmentId,
pub coordinate_space: LayoutCoordinateSpaceId,
pub clip_chain: Option<LayoutClipChainId>,
pub local_rect: LayoutRect,
pub paint_order: u32,
pub is_text: bool,
pub pointer_events: bool,
struct LayoutHitTestEntry<N> {
source: N,
fragment: LayoutFragmentId,
coordinate_space: LayoutCoordinateSpaceId,
clip_chain: Option<LayoutClipChainId>,
local_rect: LayoutRect,
paint_order: u32,
is_text: bool,
pointer_events: bool,
}
/// A pass-local spatial index. The initial implementation is a compact
/// paint-ordered vector; the schema does not expose or require a retained R-tree.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct LayoutHitTestIndex<N> {
pub entries: Vec<LayoutHitTestEntry<N>>,
}
/// Result of resolving a point against the pass-local hit-test index.
/// Result of resolving a point against hit candidates derived from the tree.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct LayoutHit<N> {
pub source: N,
/// The real provider identifies the exact fragment. Explicit mock
/// providers can return a source-only hit without manufacturing a
/// pass-local fragment identity.
/// tree-local fragment identity.
pub fragment: Option<LayoutFragmentId>,
pub local_point: LayoutPoint,
pub is_text: bool,
/// Box geometry copied from the same output when the hit source owns a
/// Box geometry copied from the same frozen tree when the hit source owns a
/// CSS box. Consumers use it for source-dependent follow-up work such as
/// descending through a transformed child-frame content box without
/// forcing a second parent-document pass.
@@ -596,7 +621,7 @@ pub enum LayoutQueryAnswer<N> {
EventOffset(Option<LayoutPoint>),
}
/// Minimal owned results copied from one transient pass output.
/// Minimal owned results derived from one frozen layout tree.
#[derive(Clone, Debug, PartialEq)]
pub struct LayoutAnswers<N> {
pub answers: Vec<LayoutQueryAnswer<N>>,
@@ -610,7 +635,7 @@ pub trait GeometryProvider {
/// Answers a batch from the provider's latest layout state.
///
/// The provider decides whether this requires a fresh pass or can reuse an
/// already-owned output; callers must not assume that one call equals one
/// already-owned tree; callers must not assume that one call equals one
/// layout computation.
fn answer(
&mut self,
@@ -620,14 +645,13 @@ pub trait GeometryProvider {
) -> Result<LayoutAnswers<Self::NodeId>, LayoutError>;
}
/// Retained geometry footprint for one owned pass output.
/// Retained footprint for one frozen layout tree.
///
/// The byte count is an allocation-capacity estimate for the output's
/// geometry, fragment, mapping, hit-test, and diagnostic storage. It excludes
/// allocator metadata and the optional paint snapshot, which consumers must
/// remove before retaining an output as a latest-layout snapshot.
/// The byte count is an allocation-capacity estimate for the tree's box,
/// fragment, source-provenance, scroll, transform, and clip storage. It
/// excludes allocator metadata and every pass-only diagnostic or paint value.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct LayoutOutputRetentionMetrics {
pub struct LayoutTreeRetentionMetrics {
pub box_count: usize,
pub fragment_count: usize,
pub estimated_geometry_bytes: usize,
@@ -637,40 +661,63 @@ pub struct LayoutOutputRetentionMetrics {
pub const MAX_RETAINED_LAYOUT_BOXES: usize = 1_000_000;
/// Maximum fragments retained in the single latest-layout snapshot.
pub const MAX_RETAINED_LAYOUT_FRAGMENTS: usize = 4_000_000;
/// Maximum estimated allocation capacity retained by one latest-layout output.
pub const MAX_RETAINED_LAYOUT_OUTPUT_BYTES: usize = 256 * 1024 * 1024;
/// Maximum estimated allocation capacity retained by one frozen layout tree.
pub const MAX_RETAINED_LAYOUT_TREE_BYTES: usize = 256 * 1024 * 1024;
/// Immutable geometry, fragment, hit-test, diagnostics, and paint projection
/// produced by exactly one full layout demand.
pub struct LayoutPassOutput<N>
/// Immutable, DOM-independent layout tree produced by one complete pass.
///
/// The box tree is stored densely through parent IDs. Text/inline fragments,
/// coordinate spaces, clips, and scroll extents are canonical layout data:
/// they preserve results that cannot be reconstructed after the working tree,
/// Taffy caches, Parley state, and computed styles are dropped. Source and
/// hit-test indexes are derived from the box provenance and fragments.
pub struct FrozenLayoutTree<N>
where
N: Copy + Debug + Eq + Hash,
{
pub schema_version: u32,
pub viewport: LayoutViewport,
pub viewport_scroll: LayoutPoint,
pub content_size: LayoutSize,
pub root_box: LayoutOutputBoxId,
pub boxes: Vec<LayoutBoxGeometry>,
pub box_sources: Vec<Option<N>>,
pub boxes: Vec<FrozenLayoutBox<N>>,
pub fragments: Vec<LayoutFragment>,
pub source_mapping: HashMap<N, LayoutNodeOutput>,
pub scroll_extents: Vec<LayoutScrollExtent>,
pub coordinate_spaces: Vec<LayoutCoordinateSpace>,
/// Source/box relationships for `display: contents` nodes, which own no
/// principal box but can still nominate rendered descendants for scroll.
pub scroll_proxy_links: Vec<(N, LayoutOutputBoxId)>,
viewport_coordinate_space: FrozenCoordinateSpace,
pub clip_chain: Vec<LayoutClipNode>,
pub paint_order: Vec<LayoutFragmentId>,
pub hit_test_index: LayoutHitTestIndex<N>,
}
/// Transient products of exactly one complete layout demand.
///
/// Consumers may inspect the tree and take an optional paint snapshot while
/// handling the demand. Only [`FrozenLayoutTree`] crosses the latest-layout
/// retention boundary; diagnostics, metrics, and paint remain pass-owned.
pub struct LayoutPassResult<N>
where
N: Copy + Debug + Eq + Hash,
{
pub tree: FrozenLayoutTree<N>,
pub diagnostics: Vec<PaintDiagnostic>,
pub metrics: LayoutPassMetrics,
paint_snapshot: Option<PaintSnapshot>,
}
impl<N> LayoutPassOutput<N>
impl<N> Deref for LayoutPassResult<N>
where
N: Copy + Debug + Eq + Hash,
{
pub const SCHEMA_VERSION: u32 = 2;
type Target = FrozenLayoutTree<N>;
fn deref(&self) -> &Self::Target {
&self.tree
}
}
impl<N> LayoutPassResult<N>
where
N: Copy + Debug + Eq + Hash,
{
pub fn paint_snapshot(&self) -> Option<&PaintSnapshot> {
self.paint_snapshot.as_ref()
}
@@ -686,81 +733,115 @@ where
.ok_or(LayoutError::PaintProjectionNotRequested)
}
pub fn retention_metrics(&self) -> LayoutOutputRetentionMetrics {
/// Consumes every pass-only product and returns the sole retainable tree.
pub fn into_tree(self) -> FrozenLayoutTree<N> {
self.tree
}
pub fn retention_metrics(&self) -> LayoutTreeRetentionMetrics {
self.tree.retention_metrics()
}
pub fn validate_retention_budget(&self) -> Result<(), LayoutError> {
self.tree.validate_retention_budget()
}
pub fn answer_queries(&self, batch: &LayoutQueryBatch<N>) -> LayoutAnswers<N> {
self.tree.answer_queries(batch, self.metrics)
}
}
impl<N> FrozenLayoutTree<N>
where
N: Copy + Debug + Eq + Hash,
{
pub fn retention_metrics(&self) -> LayoutTreeRetentionMetrics {
fn allocation<T>(capacity: usize) -> usize {
capacity.saturating_mul(std::mem::size_of::<T>())
}
let box_allocations = self.boxes.iter().fold(0usize, |bytes, geometry| {
bytes
.saturating_add(geometry.source_label.capacity())
.saturating_add(allocation::<LayoutFragmentId>(
geometry.fragments.capacity(),
))
});
let source_mapping_allocations = self.source_mapping.values().fold(
allocation::<(N, LayoutNodeOutput)>(self.source_mapping.capacity()),
|bytes, output| {
bytes
.saturating_add(allocation::<LayoutFragmentId>(output.fragments.capacity()))
.saturating_add(allocation::<LayoutOutputBoxId>(
output.scroll_proxy_boxes.capacity(),
))
},
);
let diagnostic_allocations = self.diagnostics.iter().fold(0usize, |bytes, diagnostic| {
bytes
.saturating_add(diagnostic.code.capacity())
.saturating_add(diagnostic.message.capacity())
let box_allocations = self.boxes.iter().fold(0usize, |bytes, layout_box| {
bytes.saturating_add(allocation::<LayoutFragmentId>(
layout_box.fragments.capacity(),
))
});
let estimated_geometry_bytes = std::mem::size_of::<Self>()
.saturating_add(allocation::<LayoutBoxGeometry>(self.boxes.capacity()))
.saturating_add(allocation::<Option<N>>(self.box_sources.capacity()))
.saturating_add(allocation::<FrozenLayoutBox<N>>(self.boxes.capacity()))
.saturating_add(allocation::<LayoutFragment>(self.fragments.capacity()))
.saturating_add(allocation::<LayoutScrollExtent>(
self.scroll_extents.capacity(),
))
.saturating_add(allocation::<LayoutCoordinateSpace>(
self.coordinate_spaces.capacity(),
.saturating_add(allocation::<(N, LayoutOutputBoxId)>(
self.scroll_proxy_links.capacity(),
))
.saturating_add(allocation::<LayoutClipNode>(self.clip_chain.capacity()))
.saturating_add(allocation::<LayoutFragmentId>(self.paint_order.capacity()))
.saturating_add(allocation::<LayoutHitTestEntry<N>>(
self.hit_test_index.entries.capacity(),
))
.saturating_add(allocation::<PaintDiagnostic>(self.diagnostics.capacity()))
.saturating_add(box_allocations)
.saturating_add(source_mapping_allocations)
.saturating_add(diagnostic_allocations);
LayoutOutputRetentionMetrics {
.saturating_add(box_allocations);
LayoutTreeRetentionMetrics {
box_count: self.boxes.len(),
fragment_count: self.fragments.len(),
estimated_geometry_bytes,
}
}
/// Rejects an output that would make the single latest-layout slot an
/// unbounded retained allocation. Paint resources are intentionally not
/// counted here because consumers remove them before publication and the
/// raster boundary applies separate command/glyph/pixel/surface budgets.
/// Rejects a tree that would make the single latest-layout slot an
/// unbounded retained allocation.
pub fn validate_retention_budget(&self) -> Result<(), LayoutError> {
validate_retention_metrics(self.retention_metrics())
}
pub fn box_geometry(&self, id: LayoutOutputBoxId) -> Option<&LayoutBoxGeometry> {
self.boxes.get(id.index())
self.boxes
.get(id.index())
.map(|layout_box| &layout_box.geometry)
}
pub fn fragment(&self, id: LayoutFragmentId) -> Option<&LayoutFragment> {
self.fragments.get(id.index())
}
pub fn coordinate_space(&self, id: LayoutCoordinateSpaceId) -> Option<&LayoutCoordinateSpace> {
self.coordinate_spaces.get(id.index())
pub fn coordinate_space(&self, id: LayoutCoordinateSpaceId) -> Option<&FrozenCoordinateSpace> {
match id.index() {
0 => Some(&self.viewport_coordinate_space),
index => self
.boxes
.get(index - 1)
.map(|layout_box| &layout_box.coordinate_space),
}
}
pub fn source_output(&self, source: N) -> Option<&LayoutNodeOutput> {
self.source_mapping.get(&source)
pub fn scroll_extent(&self, id: LayoutOutputBoxId) -> Option<&LayoutScrollExtent> {
self.boxes
.get(id.index())
.map(|layout_box| &layout_box.scroll_extent)
}
/// Derives the source view from canonical box provenance.
///
/// No source hash table survives the pass. The returned IDs are copied
/// into one short-lived query value.
pub fn source_output(&self, source: N) -> Option<LayoutNodeOutput> {
let mut found = false;
let mut output = LayoutNodeOutput::default();
for layout_box in &self.boxes {
if layout_box.principal_source == Some(source) {
output.principal_box = Some(layout_box.id);
found = true;
}
if layout_box.geometry_source == Some(source) {
output
.fragments
.extend(layout_box.fragments.iter().copied().filter(|id| {
self.fragment(*id).is_some_and(|fragment| {
!matches!(fragment.kind, LayoutFragmentKind::Line { .. })
})
}));
found = true;
}
}
for (proxy_source, box_id) in &self.scroll_proxy_links {
if *proxy_source == source {
output.scroll_proxy_boxes.push(*box_id);
found = true;
}
}
found.then_some(output)
}
pub fn element_metrics_for_source(&self, source: N) -> Option<LayoutElementMetrics<N>> {
@@ -772,7 +853,7 @@ where
/// ancestor tree scopes.
///
/// Shadow DOM tree-scope visibility is an HTML/DOM concern, not a CSS box
/// tree concern. The pass output therefore retains the complete box chain
/// tree concern. The frozen tree therefore retains the complete box chain
/// and lets its short-lived consumer supply that one predicate. Geometry
/// is still derived wholly from this pass; no live layout state is read or
/// retained here.
@@ -784,12 +865,15 @@ where
let output = self.source_output(source)?;
let box_id = output.principal_box?;
let geometry = self.box_geometry(box_id)?;
let extent = self.scroll_extents.get(geometry.scroll_extent.index())?;
let extent = self.scroll_extent(box_id)?;
let coordinate_space = self.coordinate_space(geometry.coordinate_space)?;
let is_root = box_id == self.root_box;
let offset_parent_id = self.offset_parent_box(box_id, &mut offset_parent_is_exposed);
let offset_parent =
offset_parent_id.and_then(|id| self.box_sources.get(id.index()).copied().flatten());
let offset_parent = offset_parent_id.and_then(|id| {
self.boxes
.get(id.index())
.and_then(|layout_box| layout_box.geometry_source)
});
let offset_parent_origin = offset_parent_id
.and_then(|id| self.box_geometry(id))
.map(|parent| {
@@ -978,10 +1062,12 @@ where
let mut scroll_containers = Vec::new();
while let Some(box_id) = candidate {
let geometry = self.box_geometry(box_id)?;
let extent = self.scroll_extents.get(geometry.scroll_extent.index())?;
let extent = self.scroll_extent(box_id)?;
if (extent.is_scroll_container || box_id == self.root_box)
&& let Some(container_source) =
self.box_sources.get(box_id.index()).copied().flatten()
&& let Some(container_source) = self
.boxes
.get(box_id.index())
.and_then(|layout_box| layout_box.geometry_source)
&& seen.insert(container_source)
&& let Some(metrics) = self.element_metrics_for_source(container_source)
{
@@ -1048,7 +1134,9 @@ where
root: Option<N>,
) -> Option<LayoutIntersectionGeometry> {
let target_output = self.source_output(target);
let target_box = target_output.and_then(|output| output.principal_box);
let target_box = target_output
.as_ref()
.and_then(|output| output.principal_box);
let root_box = root.and_then(|source| self.source_output(source)?.principal_box);
let root_is_layout_ancestor = match (target_box, root_box, root) {
(_, _, None) => true,
@@ -1058,8 +1146,7 @@ where
_ => false,
};
let root_clips_overflow = root_box
.and_then(|root_box| self.box_geometry(root_box))
.and_then(|geometry| self.scroll_extents.get(geometry.scroll_extent.index()))
.and_then(|root_box| self.scroll_extent(root_box))
.is_some_and(|extent| extent.clips_overflow);
let root_rect = if root.is_none() {
LayoutTransform2D::IDENTITY.map_rect(LayoutRect::new(
@@ -1072,7 +1159,7 @@ where
let geometry = self.box_geometry(root_box)?;
Some((
geometry,
self.scroll_extents.get(geometry.scroll_extent.index())?,
self.scroll_extent(root_box)?,
self.coordinate_space(geometry.coordinate_space)?,
))
}) {
@@ -1105,7 +1192,7 @@ where
clip = node.parent;
}
};
if let Some(target_output) = target_output {
if let Some(target_output) = target_output.as_ref() {
for fragment in target_output
.fragments
.iter()
@@ -1114,7 +1201,9 @@ where
add_clip_chain(fragment.clip_chain);
}
}
if target_output.is_none_or(|output| output.fragments.is_empty())
if target_output
.as_ref()
.is_none_or(|output| output.fragments.is_empty())
&& let Some(target_box) = target_box
&& let Some(geometry) = self.box_geometry(target_box)
{
@@ -1337,10 +1426,9 @@ where
viewport_point: LayoutPoint,
ignore_pointer_events_none: bool,
) -> Option<LayoutHit<N>> {
self.hit_test_index
.entries
.iter()
.find_map(|entry| self.hit_for_entry(entry, viewport_point, ignore_pointer_events_none))
self.hit_test_entries().into_iter().find_map(|entry| {
self.hit_for_entry(&entry, viewport_point, ignore_pointer_events_none)
})
}
pub fn hit_test_all(
@@ -1350,8 +1438,8 @@ where
) -> Vec<LayoutHit<N>> {
let mut seen = HashSet::new();
let mut hits = Vec::new();
for entry in &self.hit_test_index.entries {
let Some(hit) = self.hit_for_entry(entry, viewport_point, ignore_pointer_events_none)
for entry in self.hit_test_entries() {
let Some(hit) = self.hit_for_entry(&entry, viewport_point, ignore_pointer_events_none)
else {
continue;
};
@@ -1363,15 +1451,12 @@ where
}
pub fn caret_position(&self, viewport_point: LayoutPoint) -> Option<LayoutCaretPosition<N>> {
let top_entry = self
.hit_test_index
.entries
let entries = self.hit_test_entries();
let top_entry = entries
.iter()
.find(|entry| self.hit_for_entry(entry, viewport_point, true).is_some())?;
let top_box = self.fragment_box_id(top_entry.fragment)?;
let text_entry = self
.hit_test_index
.entries
let text_entry = entries
.iter()
.filter(|entry| entry.is_text)
.filter(|entry| {
@@ -1411,6 +1496,42 @@ where
})
}
/// Builds the front-to-back hit candidates for one query.
///
/// Paint order, source provenance, transforms, and clips are canonical
/// tree data. The duplicated candidate vector is deliberately temporary.
fn hit_test_entries(&self) -> Vec<LayoutHitTestEntry<N>> {
let mut entries = self
.fragments
.iter()
.filter_map(|fragment| {
let paint_order = fragment.paint_order?;
let (box_id, is_text) = match fragment.kind {
LayoutFragmentKind::Box { box_id }
| LayoutFragmentKind::InlineBox { box_id, .. } => (box_id, false),
LayoutFragmentKind::Text { box_id, .. } => (box_id, true),
LayoutFragmentKind::Line { .. } => return None,
};
let layout_box = self.boxes.get(box_id.index())?;
if !layout_box.visible {
return None;
}
Some(LayoutHitTestEntry {
source: layout_box.hit_source?,
fragment: fragment.id,
coordinate_space: fragment.coordinate_space,
clip_chain: fragment.clip_chain,
local_rect: fragment.rect,
paint_order,
is_text,
pointer_events: layout_box.pointer_events,
})
})
.collect::<Vec<_>>();
entries.sort_by_key(|entry| std::cmp::Reverse(entry.paint_order));
entries
}
fn caret_position_for_text_entry(
&self,
entry: &LayoutHitTestEntry<N>,
@@ -1505,7 +1626,10 @@ where
let mut seen = HashSet::new();
let mut ancestors = Vec::new();
loop {
if let Some(source) = self.box_sources.get(box_id.index()).copied().flatten()
if let Some(source) = self
.boxes
.get(box_id.index())
.and_then(|layout_box| layout_box.geometry_source)
&& seen.insert(source)
&& let Some(model) = self.box_model_for_source(source)
{
@@ -1545,7 +1669,11 @@ where
})
}
pub fn answer_queries(&self, batch: &LayoutQueryBatch<N>) -> LayoutAnswers<N> {
pub fn answer_queries(
&self,
batch: &LayoutQueryBatch<N>,
metrics: LayoutPassMetrics,
) -> LayoutAnswers<N> {
let answers = batch
.queries
.iter()
@@ -1603,10 +1731,7 @@ where
}
})
.collect();
LayoutAnswers {
answers,
metrics: self.metrics,
}
LayoutAnswers { answers, metrics }
}
fn point_passes_clip_chain(
@@ -1676,7 +1801,10 @@ where
let mut candidate = geometry.parent;
while let Some(id) = candidate {
let parent = self.box_geometry(id)?;
let source = self.box_sources.get(id.index()).copied().flatten();
let source = self
.boxes
.get(id.index())
.and_then(|layout_box| layout_box.geometry_source);
let Some(source) = source else {
candidate = parent.parent;
continue;
@@ -1727,34 +1855,38 @@ where
viewport_scroll: LayoutPoint,
content_size: LayoutSize,
root_box: LayoutOutputBoxId,
boxes: Vec<LayoutBoxGeometry>,
box_sources: Vec<Option<N>>,
boxes: Vec<FrozenLayoutBox<N>>,
fragments: Vec<LayoutFragment>,
source_mapping: HashMap<N, LayoutNodeOutput>,
scroll_extents: Vec<LayoutScrollExtent>,
coordinate_spaces: Vec<LayoutCoordinateSpace>,
scroll_proxy_links: Vec<(N, LayoutOutputBoxId)>,
viewport_coordinate_space: FrozenCoordinateSpace,
clip_chain: Vec<LayoutClipNode>,
paint_order: Vec<LayoutFragmentId>,
hit_test_index: LayoutHitTestIndex<N>,
diagnostics: Vec<PaintDiagnostic>,
metrics: LayoutPassMetrics,
paint_snapshot: Option<PaintSnapshot>,
) -> Self {
Self {
schema_version: Self::SCHEMA_VERSION,
viewport,
viewport_scroll,
content_size,
root_box,
boxes,
box_sources,
fragments,
source_mapping,
scroll_extents,
coordinate_spaces,
scroll_proxy_links,
viewport_coordinate_space,
clip_chain,
paint_order,
hit_test_index,
}
}
}
impl<N> LayoutPassResult<N>
where
N: Copy + Debug + Eq + Hash,
{
pub(crate) fn new(
tree: FrozenLayoutTree<N>,
diagnostics: Vec<PaintDiagnostic>,
metrics: LayoutPassMetrics,
paint_snapshot: Option<PaintSnapshot>,
) -> Self {
Self {
tree,
diagnostics,
metrics,
paint_snapshot,
@@ -1766,18 +1898,18 @@ fn axis_aligned_union(left: LayoutQuad, right: LayoutQuad) -> LayoutQuad {
LayoutTransform2D::IDENTITY.map_rect(left.bounding_rect().union(right.bounding_rect()))
}
fn validate_retention_metrics(metrics: LayoutOutputRetentionMetrics) -> Result<(), LayoutError> {
fn validate_retention_metrics(metrics: LayoutTreeRetentionMetrics) -> Result<(), LayoutError> {
if metrics.box_count > MAX_RETAINED_LAYOUT_BOXES
|| metrics.fragment_count > MAX_RETAINED_LAYOUT_FRAGMENTS
|| metrics.estimated_geometry_bytes > MAX_RETAINED_LAYOUT_OUTPUT_BYTES
|| metrics.estimated_geometry_bytes > MAX_RETAINED_LAYOUT_TREE_BYTES
{
return Err(LayoutError::OutputRetentionBudgetExceeded {
return Err(LayoutError::TreeRetentionBudgetExceeded {
boxes: metrics.box_count,
fragments: metrics.fragment_count,
estimated_bytes: metrics.estimated_geometry_bytes,
max_boxes: MAX_RETAINED_LAYOUT_BOXES,
max_fragments: MAX_RETAINED_LAYOUT_FRAGMENTS,
max_bytes: MAX_RETAINED_LAYOUT_OUTPUT_BYTES,
max_bytes: MAX_RETAINED_LAYOUT_TREE_BYTES,
});
}
Ok(())
@@ -1810,30 +1942,30 @@ mod tests {
}
#[test]
fn retained_output_budget_reports_each_bounded_dimension() {
fn retained_tree_budget_reports_each_bounded_dimension() {
for metrics in [
LayoutOutputRetentionMetrics {
LayoutTreeRetentionMetrics {
box_count: MAX_RETAINED_LAYOUT_BOXES + 1,
..Default::default()
},
LayoutOutputRetentionMetrics {
LayoutTreeRetentionMetrics {
fragment_count: MAX_RETAINED_LAYOUT_FRAGMENTS + 1,
..Default::default()
},
LayoutOutputRetentionMetrics {
estimated_geometry_bytes: MAX_RETAINED_LAYOUT_OUTPUT_BYTES + 1,
LayoutTreeRetentionMetrics {
estimated_geometry_bytes: MAX_RETAINED_LAYOUT_TREE_BYTES + 1,
..Default::default()
},
] {
assert!(matches!(
validate_retention_metrics(metrics),
Err(LayoutError::OutputRetentionBudgetExceeded { .. })
Err(LayoutError::TreeRetentionBudgetExceeded { .. })
));
}
validate_retention_metrics(LayoutOutputRetentionMetrics {
validate_retention_metrics(LayoutTreeRetentionMetrics {
box_count: MAX_RETAINED_LAYOUT_BOXES,
fragment_count: MAX_RETAINED_LAYOUT_FRAGMENTS,
estimated_geometry_bytes: MAX_RETAINED_LAYOUT_OUTPUT_BYTES,
estimated_geometry_bytes: MAX_RETAINED_LAYOUT_TREE_BYTES,
})
.expect("each exact retention limit should be accepted");
}
+19 -24
View File
@@ -1,9 +1,8 @@
use crate::{
DocumentLayoutServices, LayoutError, LayoutFlushReason, LayoutPassOutput, LayoutSource,
DocumentLayoutServices, LayoutError, LayoutFlushReason, LayoutPassResult, LayoutSource,
LayoutStyleResolver, LayoutViewport, PaintCaptureRequest, PaintSnapshot, PaintViewport,
build_layout_world, form::prepare_form_controls, inline::prepare_inline_contexts,
list::prepare_list_markers, projection::project_layout_output,
taffy_tree::compute_world_layout,
list::prepare_list_markers, projection::finish_layout_pass, taffy_tree::compute_world_layout,
};
use std::collections::HashMap;
use std::time::Instant;
@@ -101,10 +100,11 @@ impl ScreenshotLayoutRequest {
}
}
/// Builds, lays out, and projects one synchronous source view into an owned snapshot.
/// Builds, lays out, and paints one synchronous source view into an owned snapshot.
///
/// The pass-local box tree and Taffy caches are dropped before this function
/// returns. The snapshot contains no DOM handles or computed-style references.
/// The working box tree and Taffy caches are dropped before this function
/// returns. This convenience path consumes the frozen tree as well and returns
/// only DOM-neutral paint input.
pub fn build_screenshot_snapshot<S, R>(
source: &S,
styles: &mut R,
@@ -115,39 +115,34 @@ where
S: LayoutSource,
R: LayoutStyleResolver<S::NodeId>,
{
build_layout_pass_output(
build_layout_pass(
source,
styles,
services,
LayoutPassRequest::with_paint(request.viewport, LayoutFlushReason::Screenshot),
)
.and_then(LayoutPassOutput::into_paint_snapshot)
.and_then(LayoutPassResult::into_paint_snapshot)
}
/// Builds one complete layout result from a borrowed source view.
///
/// Box construction, inline shaping, Taffy caches, and all style borrows remain
/// local to this call. The returned value owns geometry, source handles,
/// diagnostics, and hit-test data. A DOM-neutral paint snapshot is included
/// only when requested. Consumers that need several geometry answers must batch
/// them against this one value instead of triggering a layout per query.
pub fn build_layout_pass_output<S, R>(
/// local to this call. The returned pass value owns one [`crate::FrozenLayoutTree`]
/// plus pass-only metrics, diagnostics, and an optional DOM-neutral paint
/// snapshot. Consumers may retain the tree, but not the surrounding pass
/// value. Several geometry answers should be batched against one tree instead
/// of triggering a layout per query.
pub fn build_layout_pass<S, R>(
source: &S,
styles: &mut R,
services: &mut DocumentLayoutServices,
request: LayoutPassRequest,
) -> Result<LayoutPassOutput<S::NodeId>, LayoutError>
) -> Result<LayoutPassResult<S::NodeId>, LayoutError>
where
S: LayoutSource,
R: LayoutStyleResolver<S::NodeId>,
{
build_layout_pass_output_with_embedded_frames(
source,
styles,
services,
request,
&mut NoEmbeddedFrames,
)
build_layout_pass_with_embedded_frames(source, styles, services, request, &mut NoEmbeddedFrames)
}
/// Builds one complete layout result and resolves embedded frame pixels after
@@ -156,13 +151,13 @@ where
/// This is a one-shot composition seam, not a retained subframe tree. Child
/// snapshots are consumed into the parent snapshot before all layout worlds
/// are dropped.
pub fn build_layout_pass_output_with_embedded_frames<S, R, F>(
pub fn build_layout_pass_with_embedded_frames<S, R, F>(
source: &S,
styles: &mut R,
services: &mut DocumentLayoutServices,
request: LayoutPassRequest,
frames: &mut F,
) -> Result<LayoutPassOutput<S::NodeId>, LayoutError>
) -> Result<LayoutPassResult<S::NodeId>, LayoutError>
where
S: LayoutSource,
R: LayoutStyleResolver<S::NodeId>,
@@ -212,7 +207,7 @@ where
}
}
}
project_layout_output(
finish_layout_pass(
&world,
request.viewport,
request.reason,
+93 -111
View File
@@ -2,25 +2,25 @@ use std::{collections::HashMap, fmt::Debug, hash::Hash, time::Instant};
use taffy::ResolveOrZero;
use crate::output::LayoutCoordinateSpace;
use crate::stacking::{PaintOrderEvent, build_paint_order};
use crate::{
LayoutAnonymousReason, LayoutBoxGeometry, LayoutBoxId, LayoutClipChainId, LayoutClipNode,
LayoutCoordinateSpace, LayoutCoordinateSpaceId, LayoutError, LayoutFlushReason, LayoutFragment,
LayoutFragmentBoxModel, LayoutFragmentId, LayoutFragmentKind, LayoutHitTestEntry,
LayoutHitTestIndex, LayoutNodeOutput, LayoutOutputBoxId, LayoutPassMetrics, LayoutPassOutput,
LayoutPoint, LayoutRect, LayoutScrollExtent, LayoutScrollExtentId, LayoutSize,
LayoutTransform2D, LayoutViewport, LayoutWorld, PaintCaptureRequest, PaintDiagnostic,
PaintDiagnosticSeverity,
FrozenCoordinateSpace, FrozenLayoutBox, FrozenLayoutTree, LayoutAnonymousReason,
LayoutBoxGeometry, LayoutBoxId, LayoutClipChainId, LayoutClipNode, LayoutCoordinateSpaceId,
LayoutError, LayoutFlushReason, LayoutFragment, LayoutFragmentBoxModel, LayoutFragmentId,
LayoutFragmentKind, LayoutOutputBoxId, LayoutPassMetrics, LayoutPassResult, LayoutPoint,
LayoutRect, LayoutScrollExtent, LayoutSize, LayoutTransform2D, LayoutViewport, LayoutWorld,
PaintCaptureRequest, PaintDiagnostic, PaintDiagnosticSeverity,
};
pub(crate) fn project_layout_output<N>(
pub(crate) fn finish_layout_pass<N>(
world: &LayoutWorld<N>,
viewport: LayoutViewport,
reason: LayoutFlushReason,
started: Instant,
paint_capture: Option<PaintCaptureRequest>,
embedded_frames: &mut HashMap<LayoutBoxId, crate::PaintSnapshot>,
) -> Result<LayoutPassOutput<N>, LayoutError>
) -> Result<LayoutPassResult<N>, LayoutError>
where
N: Copy + Debug + Eq + Hash,
{
@@ -28,7 +28,7 @@ where
projection.build_local_box_geometry();
projection.resolve_scrollable_overflow();
projection.build_coordinate_spaces()?;
projection.build_fragments_and_source_mapping();
projection.build_fragments();
projection.assign_clip_and_paint_order();
let content_size = projection.document_content_size();
@@ -72,22 +72,9 @@ where
.count(),
};
Ok(LayoutPassOutput::new(
viewport,
projection.viewport_scroll,
content_size,
LayoutOutputBoxId::from_index(world.root.index()),
projection.boxes,
projection.box_sources,
projection.fragments,
projection.source_mapping,
projection.scroll_extents,
projection.coordinate_spaces,
projection.clip_chain,
projection.paint_order,
LayoutHitTestIndex {
entries: projection.hit_entries,
},
let tree = projection.into_frozen_tree(content_size);
Ok(LayoutPassResult::new(
tree,
diagnostics,
metrics,
paint_snapshot,
@@ -103,13 +90,14 @@ where
viewport_scroll: LayoutPoint,
pub(crate) boxes: Vec<LayoutBoxGeometry>,
box_sources: Vec<Option<N>>,
principal_sources: Vec<Option<N>>,
hit_sources: Vec<Option<N>>,
fragments: Vec<LayoutFragment>,
source_mapping: HashMap<N, LayoutNodeOutput>,
scroll_proxy_links: Vec<(N, LayoutOutputBoxId)>,
pub(crate) scroll_extents: Vec<LayoutScrollExtent>,
pub(crate) coordinate_spaces: Vec<LayoutCoordinateSpace>,
pub(crate) clip_chain: Vec<LayoutClipNode>,
paint_order: Vec<LayoutFragmentId>,
hit_entries: Vec<LayoutHitTestEntry<N>>,
paint_order_count: usize,
pub(crate) diagnostics: Vec<PaintDiagnostic>,
resolved_transforms: Vec<LayoutTransform2D>,
overflow: Vec<LayoutRect>,
@@ -127,19 +115,33 @@ where
{
fn new(world: &'a LayoutWorld<N>, viewport: LayoutViewport) -> Self {
let count = world.boxes.len();
let mut principal_sources = vec![None; count];
for (source, box_id) in &world.source_mapping {
principal_sources[box_id.index()] = Some(*source);
}
let scroll_proxy_links = world
.display_contents_mapping
.iter()
.flat_map(|(source, boxes)| {
boxes
.iter()
.map(|box_id| (*source, LayoutOutputBoxId::from_index(box_id.index())))
})
.collect();
Self {
world,
viewport,
viewport_scroll: LayoutPoint::ZERO,
boxes: Vec::with_capacity(count),
box_sources: Vec::with_capacity(count),
principal_sources,
hit_sources: Vec::with_capacity(count),
fragments: Vec::new(),
source_mapping: HashMap::with_capacity(world.source_mapping.len()),
scroll_proxy_links,
scroll_extents: Vec::with_capacity(count),
coordinate_spaces: Vec::with_capacity(count + 1),
clip_chain: Vec::new(),
paint_order: Vec::new(),
hit_entries: Vec::new(),
paint_order_count: 0,
diagnostics: Vec::new(),
resolved_transforms: vec![LayoutTransform2D::IDENTITY; count],
overflow: vec![LayoutRect::ZERO; count],
@@ -239,6 +241,13 @@ where
.flatten()
});
self.box_sources.push(source);
self.hit_sources.push(layout_box.source.or_else(|| {
(layout_box.pseudo.is_some()
|| layout_box.anonymous_reason
== Some(LayoutAnonymousReason::InlineSplitContinuation))
.then_some(layout_box.owner)
.flatten()
}));
let semantics = layout_box.element_semantics();
let (layout_x, layout_y) = self
.world
@@ -251,11 +260,6 @@ where
layout_parent: layout_box
.layout_parent
.map(|parent| LayoutOutputBoxId::from_index(parent.index())),
positioned_containing_block: layout_box
.positioned_containing_block
.map(|parent| LayoutOutputBoxId::from_index(parent.index())),
role: layout_box.kind,
source_label: layout_box.source_label.clone(),
position: layout_box.style.position(),
coordinate_space: LayoutCoordinateSpaceId::from_index(index + 1),
clip_chain: None,
@@ -263,7 +267,6 @@ where
padding_box,
border_box,
margin_box,
scroll_extent: LayoutScrollExtentId::from_index(index),
fragments: Vec::new(),
layout_origin_in_document: LayoutPoint::new(layout_x, layout_y),
is_body_element: semantics.is_some_and(|element| element.is_html_element("body")),
@@ -368,12 +371,9 @@ where
LayoutPoint::ZERO
};
self.scroll_extents.push(LayoutScrollExtent {
id: LayoutScrollExtentId::from_index(index),
owner: LayoutOutputBoxId::from_index(index),
scrollport,
scrollable_overflow: overflow,
scroll_size,
requested_offset: requested,
applied_offset: applied,
minimum_offset,
maximum_offset,
@@ -419,23 +419,7 @@ where
Ok(())
}
fn build_fragments_and_source_mapping(&mut self) {
for (source, box_id) in &self.world.source_mapping {
self.source_mapping
.entry(*source)
.or_default()
.principal_box = Some(LayoutOutputBoxId::from_index(box_id.index()));
}
for (source, boxes) in &self.world.display_contents_mapping {
self.source_mapping
.entry(*source)
.or_default()
.scroll_proxy_boxes = boxes
.iter()
.map(|id| LayoutOutputBoxId::from_index(id.index()))
.collect();
}
fn build_fragments(&mut self) {
for index in 0..self.world.boxes.len() {
let layout_box = &self.world.boxes[index];
let output_box = LayoutOutputBoxId::from_index(index);
@@ -451,7 +435,6 @@ where
border: self.boxes[index].border_box,
margin: self.boxes[index].margin_box,
}),
baseline: None,
coordinate_space,
clip_chain: None,
paint_order: None,
@@ -477,7 +460,6 @@ where
},
rect: offset_rect(line.rect, content_origin),
box_model: None,
baseline: Some(content_origin.y + line.baseline),
coordinate_space,
clip_chain: None,
paint_order: None,
@@ -497,7 +479,6 @@ where
},
rect: box_model.border,
box_model: Some(box_model),
baseline: None,
coordinate_space,
clip_chain: None,
paint_order: None,
@@ -512,13 +493,11 @@ where
kind: LayoutFragmentKind::Text {
box_id: LayoutOutputBoxId::from_index(target),
line_index: text.line_index,
source_byte_range: text.source_byte_range.clone(),
source_utf16_range: text.source_utf16_range.clone(),
rtl: text.rtl,
},
rect: offset_rect(text.rect, content_origin),
box_model: None,
baseline: None,
coordinate_space,
clip_chain: None,
paint_order: None,
@@ -531,19 +510,6 @@ where
fn register_box_fragment(&mut self, box_index: usize, fragment: LayoutFragmentId) {
self.boxes[box_index].fragments.push(fragment);
let layout_box = &self.world.boxes[box_index];
let source = layout_box.source.or_else(|| {
(layout_box.anonymous_reason == Some(LayoutAnonymousReason::InlineSplitContinuation))
.then_some(layout_box.owner)
.flatten()
});
if let Some(source) = source {
self.source_mapping
.entry(source)
.or_default()
.fragments
.push(fragment);
}
}
fn push_fragment(&mut self, mut fragment: LayoutFragment) -> LayoutFragmentId {
@@ -589,8 +555,6 @@ where
| PaintOrderEvent::PopStackingContext(_) => {}
}
}
self.hit_entries
.sort_by_key(|entry| std::cmp::Reverse(entry.paint_order));
}
fn assign_box_clip_metadata(
@@ -644,42 +608,18 @@ where
fragment_id: LayoutFragmentId,
clip_chain: Option<LayoutClipChainId>,
) {
let order = u32::try_from(self.paint_order.len())
.expect("one layout output exceeded the u32 paint-order limit");
let (box_index, is_text) = match self.fragments[fragment_id.index()].kind {
LayoutFragmentKind::Box { box_id } => (box_id.index(), false),
LayoutFragmentKind::InlineBox { box_id, .. } => (box_id.index(), false),
LayoutFragmentKind::Text { box_id, .. } => (box_id.index(), true),
let order = u32::try_from(self.paint_order_count)
.expect("one frozen layout tree exceeded the u32 paint-order limit");
match self.fragments[fragment_id.index()].kind {
LayoutFragmentKind::Box { .. }
| LayoutFragmentKind::InlineBox { .. }
| LayoutFragmentKind::Text { .. } => {}
LayoutFragmentKind::Line { .. } => return,
};
}
let fragment = &mut self.fragments[fragment_id.index()];
fragment.clip_chain = clip_chain;
fragment.paint_order = Some(order);
self.paint_order.push(fragment_id);
let layout_box = &self.world.boxes[box_index];
if !layout_box.style.is_visible() {
return;
}
let source = layout_box.source.or_else(|| {
(layout_box.pseudo.is_some()
|| layout_box.anonymous_reason
== Some(LayoutAnonymousReason::InlineSplitContinuation))
.then_some(layout_box.owner)
.flatten()
});
if let Some(source) = source {
self.hit_entries.push(LayoutHitTestEntry {
source,
fragment: fragment_id,
coordinate_space: fragment.coordinate_space,
clip_chain,
local_rect: fragment.rect,
paint_order: order,
is_text,
pointer_events: layout_box.style.accepts_pointer_events(),
});
}
self.paint_order_count = self.paint_order_count.saturating_add(1);
}
fn push_clip(
@@ -691,7 +631,6 @@ where
) -> LayoutClipChainId {
let id = LayoutClipChainId::from_index(self.clip_chain.len());
self.clip_chain.push(LayoutClipNode {
id,
parent,
owner,
coordinate_space,
@@ -718,6 +657,49 @@ where
(self.viewport.css_height as f32).max(overflow.bottom().max(0.0)),
)
}
fn into_frozen_tree(self, content_size: LayoutSize) -> FrozenLayoutTree<N> {
let root_box = LayoutOutputBoxId::from_index(self.world.root.index());
let mut coordinate_spaces = self.coordinate_spaces.into_iter();
let viewport_coordinate_space = FrozenCoordinateSpace::from(
coordinate_spaces
.next()
.expect("a frozen layout tree always owns the viewport coordinate space"),
);
let boxes = self
.boxes
.into_iter()
.zip(self.box_sources)
.zip(self.principal_sources)
.zip(self.hit_sources)
.zip(self.scroll_extents)
.zip(coordinate_spaces.map(FrozenCoordinateSpace::from))
.map(
|(
((((geometry, geometry_source), principal_source), hit_source), scroll_extent),
coordinate_space,
)| FrozenLayoutBox {
geometry,
scroll_extent,
coordinate_space,
geometry_source,
principal_source,
hit_source,
},
)
.collect();
FrozenLayoutTree::new(
self.viewport,
self.viewport_scroll,
content_size,
root_box,
boxes,
self.fragments,
self.scroll_proxy_links,
viewport_coordinate_space,
self.clip_chain,
)
}
}
#[allow(clippy::too_many_arguments)]
+55 -24
View File
@@ -2,10 +2,10 @@ use std::collections::HashMap;
use moli_layout::{
DocumentLayoutServices, LayoutDisplay, LayoutElementCategory, LayoutElementSemantics,
LayoutError, LayoutFlushReason, LayoutFragmentKind, LayoutNamespace, LayoutPassOutput,
LayoutPassRequest, LayoutPoint, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect,
LayoutError, LayoutFlushReason, LayoutFragmentKind, LayoutNamespace, LayoutPassRequest,
LayoutPassResult, LayoutPoint, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutRect,
LayoutSource, LayoutSourceKind, LayoutStyleResolver, LayoutTransform2D, LayoutViewport,
PaintColor, ResolvedLayoutStyle, build_layout_pass_output,
PaintColor, ResolvedLayoutStyle, build_layout_pass,
};
use style::Atom;
use taffy::{
@@ -126,7 +126,7 @@ fn fixed_size(display: LayoutDisplay, width: f32, height: f32) -> ResolvedLayout
)
}
fn build(source: &Source, styles: &mut Styles) -> LayoutPassOutput<usize> {
fn build(source: &Source, styles: &mut Styles) -> LayoutPassResult<usize> {
build_with_request(
source,
styles,
@@ -138,8 +138,8 @@ fn build_with_request(
source: &Source,
styles: &mut Styles,
request: LayoutPassRequest,
) -> LayoutPassOutput<usize> {
build_layout_pass_output(source, styles, &mut DocumentLayoutServices::new(), request).unwrap()
) -> LayoutPassResult<usize> {
build_layout_pass(source, styles, &mut DocumentLayoutServices::new(), request).unwrap()
}
fn assert_close(actual: f32, expected: f32) {
@@ -178,7 +178,7 @@ fn display_none_root_uses_an_unmapped_internal_carrier() {
}
#[test]
fn output_owns_complete_box_models_and_answers_a_batch_from_one_pass() {
fn pass_result_owns_complete_box_models_and_answers_a_batch_from_one_pass() {
let source = Source(vec![Node::element("root", Vec::new())]);
let mut styles = Styles::default();
styles.0.insert(
@@ -257,6 +257,46 @@ fn output_owns_complete_box_models_and_answers_a_batch_from_one_pass() {
));
}
#[test]
fn pass_output_freezes_into_the_sole_queryable_retained_tree() {
let source = Source(vec![
Node::element("root", vec![1]),
Node::element("target", Vec::new()),
]);
let mut styles = Styles::default();
styles
.0
.insert(0, fixed_size(LayoutDisplay::Block, 200.0, 100.0));
styles
.0
.insert(1, fixed_size(LayoutDisplay::Block, 80.0, 30.0));
let output = build_with_request(
&source,
&mut styles,
LayoutPassRequest::with_paint(LayoutViewport::new(200, 100, 1.0), LayoutFlushReason::Test),
);
assert!(output.paint_snapshot().is_some());
let metrics = output.metrics;
let tree = output.into_tree();
assert!(tree.source_output(1).is_some());
assert_eq!(
tree.hit_test(LayoutPoint::new(10.0, 10.0), false)
.expect("the frozen tree derives a hit-test view")
.source,
1
);
let answers = tree.answer_queries(
&LayoutQueryBatch::new(vec![LayoutQuery::BoxModel { source: 1 }]),
metrics,
);
assert!(matches!(
answers.answers.as_slice(),
[LayoutQueryAnswer::BoxModel(Some(_))]
));
}
#[test]
fn document_content_size_includes_visible_descendant_end_margin() {
let source = Source(vec![
@@ -377,18 +417,14 @@ fn inline_output_preserves_line_text_and_utf16_source_fragments() {
.filter_map(|id| output.fragment(*id))
.filter_map(|fragment| match &fragment.kind {
LayoutFragmentKind::Text {
source_byte_range,
source_utf16_range,
..
} => Some((source_byte_range.clone(), source_utf16_range.clone())),
source_utf16_range, ..
} => Some(source_utf16_range.clone()),
_ => None,
})
.collect::<Vec<_>>();
assert!(!text_fragments.is_empty());
assert_eq!(text_fragments.first().unwrap().0.start, 0);
assert_eq!(text_fragments.last().unwrap().0.end, "ab😀cd".len());
assert_eq!(text_fragments.first().unwrap().1.start, 0);
assert_eq!(text_fragments.last().unwrap().1.end, 6);
assert_eq!(text_fragments.first().unwrap().start, 0);
assert_eq!(text_fragments.last().unwrap().end, 6);
let inline_model = output.box_model_for_source(1).unwrap();
assert!(inline_model.border.bounding_rect().width > 0.0);
@@ -553,8 +589,8 @@ fn caret_query_uses_parley_cluster_sides_and_inline_direction() {
let fragment = output
.source_output(1)
.into_iter()
.flat_map(|source| &source.fragments)
.filter_map(|id| output.fragment(*id))
.flat_map(|source| source.fragments)
.filter_map(|id| output.fragment(id))
.find(|fragment| {
matches!(
fragment.kind,
@@ -681,13 +717,8 @@ fn scroll_is_sampled_per_pass_and_updates_geometry_clip_and_hit_testing() {
.insert(2, fixed_size(LayoutDisplay::Block, 300.0, 200.0));
let first = build(&source, &mut styles);
let scroll_extent = &first.scroll_extents[first
.source_output(1)
.unwrap()
.principal_box
.unwrap()
.index()];
assert_eq!(scroll_extent.requested_offset, LayoutPoint::new(40.0, 30.0));
let scroll_box = first.source_output(1).unwrap().principal_box.unwrap();
let scroll_extent = first.scroll_extent(scroll_box).unwrap();
assert_eq!(scroll_extent.applied_offset, LayoutPoint::new(40.0, 30.0));
assert_eq!(scroll_extent.minimum_offset, LayoutPoint::ZERO);
assert_eq!(scroll_extent.maximum_offset, LayoutPoint::new(200.0, 120.0));
+9 -15
View File
@@ -5,8 +5,8 @@ mod style_resolver;
use std::collections::HashMap;
use moli_layout::{
DocumentLayoutServices, EmbeddedFrameRenderer, LayoutPassOutput, LayoutPassRequest,
LayoutSource, LayoutViewport, PaintSnapshot, build_layout_pass_output_with_embedded_frames,
DocumentLayoutServices, EmbeddedFrameRenderer, LayoutPassRequest, LayoutPassResult,
LayoutSource, LayoutViewport, PaintSnapshot, build_layout_pass_with_embedded_frames,
};
use style::values::generics::image::GenericImage;
@@ -105,15 +105,15 @@ fn collect_computed_css_image_urls(
}
}
pub(crate) fn build_native_layout_pass_output(
pub(crate) fn build_native_layout_pass(
runtime: &JsContextHost,
root: DomHandle,
services: &mut DocumentLayoutServices,
embedded_document_services: &mut HashMap<DomHandle, DocumentLayoutServices>,
request: LayoutPassRequest,
) -> Result<LayoutPassOutput<DomHandle>, moli_layout::LayoutError> {
) -> Result<LayoutPassResult<DomHandle>, moli_layout::LayoutError> {
let mut document_stack = Vec::new();
build_native_layout_pass_output_recursive(
build_native_layout_pass_recursive(
runtime,
root,
services,
@@ -123,14 +123,14 @@ pub(crate) fn build_native_layout_pass_output(
)
}
fn build_native_layout_pass_output_recursive(
fn build_native_layout_pass_recursive(
runtime: &JsContextHost,
root: DomHandle,
services: &mut DocumentLayoutServices,
embedded_document_services: &mut HashMap<DomHandle, DocumentLayoutServices>,
request: LayoutPassRequest,
document_stack: &mut Vec<DomHandle>,
) -> Result<LayoutPassOutput<DomHandle>, moli_layout::LayoutError> {
) -> Result<LayoutPassResult<DomHandle>, moli_layout::LayoutError> {
let document = runtime
.dom_host()
.owner_document_handle(root)
@@ -152,13 +152,7 @@ fn build_native_layout_pass_output_recursive(
document_stack,
embedded_document_services,
};
build_layout_pass_output_with_embedded_frames(
&source,
&mut styles,
services,
request,
&mut frames,
)
build_layout_pass_with_embedded_frames(&source, &mut styles, services, request, &mut frames)
};
document_stack.pop();
result
@@ -202,7 +196,7 @@ impl EmbeddedFrameRenderer<DomHandle> for NativeEmbeddedFrameRenderer<'_> {
.unwrap_or_default();
let mut capture = moli_layout::PaintCaptureRequest::viewport();
capture.include_backgrounds = self.include_backgrounds;
let result = build_native_layout_pass_output_recursive(
let result = build_native_layout_pass_recursive(
self.runtime,
root,
&mut services,
@@ -1,7 +1,8 @@
use moli_browser_profile::DEFAULT_WINDOW_SURFACE_PROFILE;
use moli_layout::{
GeometryProvider, LayoutAnswers, LayoutError, LayoutFlushReason, LayoutPassOutput,
LayoutPassRequest, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch, LayoutViewport,
FrozenLayoutTree, GeometryProvider, LayoutAnswers, LayoutError, LayoutFlushReason,
LayoutPassRequest, LayoutPassResult, LayoutQuery, LayoutQueryAnswer, LayoutQueryBatch,
LayoutViewport,
};
use super::JsContextHost;
@@ -40,7 +41,7 @@ pub(crate) struct LayoutSnapshotCacheObservability {
pub(crate) hits: u64,
pub(crate) misses: u64,
pub(crate) publishes: u64,
pub(crate) cached: Option<(DomHandle, moli_layout::LayoutOutputRetentionMetrics, bool)>,
pub(crate) cached: Option<(DomHandle, moli_layout::LayoutTreeRetentionMetrics)>,
}
impl JsContextHost {
@@ -92,11 +93,11 @@ impl JsContextHost {
)
}
pub(crate) fn with_fresh_layout_output_for_document<T>(
pub(crate) fn with_fresh_layout_pass_for_document<T>(
&self,
document: DomHandle,
request: LayoutPassRequest,
consume: impl FnOnce(&mut LayoutPassOutput<DomHandle>) -> Result<T, LayoutError>,
consume: impl FnOnce(&mut LayoutPassResult<DomHandle>) -> Result<T, LayoutError>,
) -> Result<Option<T>, LayoutError> {
let Some(root) = self
.dom_host()
@@ -106,7 +107,7 @@ impl JsContextHost {
return Ok(None);
};
let _active = ActiveLayoutPass::enter(&self.layout_pass_active)?;
let mut output = {
let mut pass = {
let mut state = self.document_layout_state.borrow_mut();
state.retain_live_embedded_document_services(|candidate| {
self.child_browsing_context_host_for_document_handle(candidate)
@@ -116,7 +117,7 @@ impl JsContextHost {
document,
self.document_handle(),
|services, embedded_document_services| {
crate::layout_renderer::build_native_layout_pass_output(
crate::layout_renderer::build_native_layout_pass(
self,
root,
services,
@@ -131,14 +132,16 @@ impl JsContextHost {
self.completed_layout_pass_time.set(
self.completed_layout_pass_time
.get()
.saturating_add(output.metrics.elapsed),
.saturating_add(pass.metrics.elapsed),
);
self.last_layout_pass_metrics.set(Some(output.metrics));
output.validate_retention_budget()?;
let consumed = consume(&mut output)?;
pass.validate_retention_budget()?;
let consumed = consume(&mut pass)?;
let metrics = pass.metrics;
let tree = pass.into_tree();
self.document_layout_state
.borrow_mut()
.publish_latest_layout(document, output);
.publish_latest_layout(document, tree);
self.last_layout_pass_metrics.set(Some(metrics));
self.layout_snapshot_cache_publishes
.set(self.layout_snapshot_cache_publishes.get().saturating_add(1));
Ok(Some(consumed))
@@ -165,16 +168,16 @@ impl JsContextHost {
.is_some()
}
/// Inspects the latest owned geometry snapshot for one exact Document.
/// Inspects the latest frozen layout tree for one exact Document.
///
/// The callback cannot retain the output or force a refresh. Consumers
/// The callback cannot retain the tree or force a refresh. Consumers
/// such as lazy-image admission may combine this sampled geometry with
/// cheap live browser state, but must tolerate the snapshot being absent
/// or stale after DOM/style mutation.
pub(crate) fn with_latest_layout_output_for_document<T>(
pub(crate) fn with_latest_layout_tree_for_document<T>(
&self,
document: DomHandle,
inspect: impl FnOnce(&LayoutPassOutput<DomHandle>) -> T,
inspect: impl FnOnce(&FrozenLayoutTree<DomHandle>) -> T,
) -> Option<T> {
let state = self.document_layout_state.borrow();
state.latest_layout(document).map(inspect)
@@ -193,9 +196,11 @@ impl JsContextHost {
let reuse_latest = true;
let cached = if reuse_latest {
let state = self.document_layout_state.borrow();
state
.latest_layout(document)
.map(|output| self.answer_layout_queries(output, viewport, queries))
state.latest_layout(document).and_then(|tree| {
self.last_layout_pass_metrics
.get()
.map(|metrics| self.answer_layout_queries(tree, metrics, viewport, queries))
})
} else {
None
};
@@ -207,21 +212,22 @@ impl JsContextHost {
self.layout_snapshot_cache_misses
.set(self.layout_snapshot_cache_misses.get().saturating_add(1));
self.with_fresh_layout_output_for_document(
self.with_fresh_layout_pass_for_document(
document,
LayoutPassRequest::new(viewport, reason),
|output| Ok(self.answer_layout_queries(output, viewport, queries)),
|pass| Ok(self.answer_layout_queries(&pass.tree, pass.metrics, viewport, queries)),
)?
.ok_or(LayoutError::NoLayoutRoot)
}
fn answer_layout_queries(
&self,
output: &LayoutPassOutput<DomHandle>,
tree: &FrozenLayoutTree<DomHandle>,
metrics: moli_layout::LayoutPassMetrics,
viewport: LayoutViewport,
queries: &LayoutQueryBatch<DomHandle>,
) -> LayoutAnswers<DomHandle> {
let mut answers = output.answer_queries(queries);
let mut answers = tree.answer_queries(queries, metrics);
for (query, answer) in queries.queries.iter().zip(&mut answers.answers) {
match (query, answer) {
(LayoutQuery::DocumentMetrics, LayoutQueryAnswer::DocumentMetrics(metrics)) => {
@@ -235,7 +241,7 @@ impl JsContextHost {
LayoutQuery::ElementMetrics { source },
LayoutQueryAnswer::ElementMetrics(metrics),
) => {
*metrics = output.element_metrics_for_source_with_offset_parent_filter(
*metrics = tree.element_metrics_for_source_with_offset_parent_filter(
*source,
|candidate| self.offset_parent_candidate_is_exposed(*source, candidate),
);
@@ -1,41 +1,34 @@
use moli_layout::LayoutPassOutput;
use moli_layout::FrozenLayoutTree;
use crate::document_runtime::DomHandle;
struct LatestLayoutSnapshot {
struct LatestFrozenLayout {
document: DomHandle,
output: Box<LayoutPassOutput<DomHandle>>,
tree: Box<FrozenLayoutTree<DomHandle>>,
}
/// Single-slot storage for the latest successful owned layout projection.
/// Single-slot storage for the latest successful frozen layout tree.
///
/// This deliberately owns no layout world, Taffy cache, style borrow, paint
/// snapshot, timer, freshness stamp, or invalidation policy. The context-host
/// orchestration decides when to read, publish, or clear the slot.
/// This owns exactly one frozen layout tree. It has no working layout world,
/// source index, hit-test index, Taffy cache, style borrow, pass diagnostics,
/// paint snapshot, timer, freshness stamp, or invalidation policy.
#[derive(Default)]
pub(super) struct LatestLayoutSnapshotCache {
latest: Option<LatestLayoutSnapshot>,
pub(super) struct LatestLayoutTreeCache {
latest: Option<LatestFrozenLayout>,
}
impl LatestLayoutSnapshotCache {
pub(super) fn get(&self, document: DomHandle) -> Option<&LayoutPassOutput<DomHandle>> {
impl LatestLayoutTreeCache {
pub(super) fn get(&self, document: DomHandle) -> Option<&FrozenLayoutTree<DomHandle>> {
self.latest
.as_ref()
.filter(|snapshot| snapshot.document == document)
.map(|snapshot| snapshot.output.as_ref())
.map(|snapshot| snapshot.tree.as_ref())
}
pub(super) fn publish(&mut self, document: DomHandle, mut output: LayoutPassOutput<DomHandle>) {
debug_assert!(
output.paint_snapshot().is_none(),
"a retained layout output must not own paint resources"
);
if output.paint_snapshot().is_some() {
drop(output.take_paint_snapshot());
}
self.latest = Some(LatestLayoutSnapshot {
pub(super) fn publish(&mut self, document: DomHandle, tree: FrozenLayoutTree<DomHandle>) {
self.latest = Some(LatestFrozenLayout {
document,
output: Box::new(output),
tree: Box::new(tree),
});
}
@@ -46,13 +39,9 @@ impl LatestLayoutSnapshotCache {
#[cfg(test)]
pub(super) fn observability(
&self,
) -> Option<(DomHandle, moli_layout::LayoutOutputRetentionMetrics, bool)> {
self.latest.as_ref().map(|snapshot| {
(
snapshot.document,
snapshot.output.retention_metrics(),
snapshot.output.paint_snapshot().is_some(),
)
})
) -> Option<(DomHandle, moli_layout::LayoutTreeRetentionMetrics)> {
self.latest
.as_ref()
.map(|snapshot| (snapshot.document, snapshot.tree.retention_metrics()))
}
}
@@ -1,8 +1,8 @@
use std::collections::HashMap;
use moli_layout::{DocumentLayoutServices, LayoutPassOutput};
use moli_layout::{DocumentLayoutServices, FrozenLayoutTree};
use super::layout_snapshot::LatestLayoutSnapshotCache;
use super::layout_snapshot::LatestLayoutTreeCache;
use crate::{
css_resource_urls::{CompletedStylesheetWebFont, StylesheetLoadBlockingResource},
document_runtime::DomHandle,
@@ -13,18 +13,18 @@ use crate::{
///
/// `ScriptVm` outlives `document.open()`, so the main-document owner
/// transition replaces this value explicitly. Full layout passes borrow the
/// services and discard every pass-local tree/cache. Only the latest owned
/// geometry projection and document-owned font/text sidecars survive a pass.
/// services and discard every working tree/cache. Only the latest frozen
/// layout tree and document-owned font/text sidecars survive a pass.
/// Embedded documents receive separate Parley services so a main-document
/// `@font-face` registration cannot leak across the browsing-context boundary.
/// The single snapshot slot may describe any exact Document in the current
/// document tree; its identity is stored alongside the output.
/// document tree; its identity is stored alongside the tree.
pub(super) struct DocumentLayoutState {
services: DocumentLayoutServices,
embedded_document_services: HashMap<DomHandle, DocumentLayoutServices>,
web_fonts: DocumentWebFontState,
web_font_sources_dirty: bool,
latest_layout: LatestLayoutSnapshotCache,
latest_layout: LatestLayoutTreeCache,
}
impl Default for DocumentLayoutState {
@@ -34,7 +34,7 @@ impl Default for DocumentLayoutState {
embedded_document_services: HashMap::new(),
web_fonts: DocumentWebFontState::default(),
web_font_sources_dirty: true,
latest_layout: LatestLayoutSnapshotCache::default(),
latest_layout: LatestLayoutTreeCache::default(),
}
}
}
@@ -84,16 +84,16 @@ impl DocumentLayoutState {
pub(super) fn latest_layout(
&self,
document: DomHandle,
) -> Option<&LayoutPassOutput<DomHandle>> {
) -> Option<&FrozenLayoutTree<DomHandle>> {
self.latest_layout.get(document)
}
pub(super) fn publish_latest_layout(
&mut self,
document: DomHandle,
output: LayoutPassOutput<DomHandle>,
tree: FrozenLayoutTree<DomHandle>,
) {
self.latest_layout.publish(document, output);
self.latest_layout.publish(document, tree);
}
pub(super) fn clear_latest_layout(&mut self) {
@@ -103,7 +103,7 @@ impl DocumentLayoutState {
#[cfg(test)]
pub(super) fn latest_layout_observability(
&self,
) -> Option<(DomHandle, moli_layout::LayoutOutputRetentionMetrics, bool)> {
) -> Option<(DomHandle, moli_layout::LayoutTreeRetentionMetrics)> {
self.latest_layout.observability()
}
@@ -80,7 +80,7 @@ fn default_child_frame_client_rect() -> ClientRect {
// Legacy geometry policy for explicit LayoutPolicy::Mock:
//
// OnDemand consumers must enter the unified GeometryProvider, which answers
// from the latest owned output or performs a cold refresh, and must never call
// from the latest frozen tree or performs a cold refresh, and must never call
// these helpers. Mock remains deterministic, cheap, and side-effect free for
// the default CLI policy without `--layout`:
//
@@ -2,11 +2,11 @@
//!
//! Lazy loading is a resource decision, not observable geometry. It must not
//! force layout or retain a layout world. The renderer instead inspects its
//! one latest owned [`LayoutPassOutput`] and combines that sampled geometry
//! one latest owned [`FrozenLayoutTree`] and combines that sampled geometry
//! with live scroll offsets. A later fresh screenshot or screencast frame
//! naturally replaces the sample.
use moli_layout::{LayoutPassOutput, LayoutPosition, LayoutRect};
use moli_layout::{FrozenLayoutTree, LayoutPosition, LayoutRect};
use crate::{document_runtime::DomHandle, dom::native::Node, native_bridge::JsContextHost};
@@ -54,7 +54,7 @@ pub(super) fn image_load_is_deferred(runtime: &JsContextHost, handle: DomHandle)
pub(super) fn revealed_lazy_image_handles(
runtime: &JsContextHost,
document: DomHandle,
output: &LayoutPassOutput<DomHandle>,
output: &FrozenLayoutTree<DomHandle>,
) -> Vec<DomHandle> {
(0..runtime.dom_host().dom().nodes().len())
.map(DomHandle::new)
@@ -85,7 +85,7 @@ fn is_unadmitted_lazy_image(
fn image_is_near_live_viewport(
runtime: &JsContextHost,
document: DomHandle,
output: &LayoutPassOutput<DomHandle>,
output: &FrozenLayoutTree<DomHandle>,
handle: DomHandle,
) -> bool {
let Some(geometry) = output.intersection_geometry(handle, None) else {
@@ -122,7 +122,7 @@ fn image_is_near_live_viewport(
fn live_scroll_translation(
runtime: &JsContextHost,
output: &LayoutPassOutput<DomHandle>,
output: &FrozenLayoutTree<DomHandle>,
handle: DomHandle,
) -> (f32, f32) {
let Some(geometry) = output.scroll_into_view_geometry_for_source(handle) else {
@@ -74,7 +74,7 @@ pub(crate) fn queue_revealed_lazy_image_loads(
let handles = {
let runtime = unsafe { &*runtime_ptr };
runtime
.with_latest_layout_output_for_document(document, |output| {
.with_latest_layout_tree_for_document(document, |output| {
revealed_lazy_image_handles(runtime, document, output)
})
.unwrap_or_default()
@@ -100,7 +100,7 @@ getComputedStyle(document.getElementById('fallback')).display
}
#[tokio::test(flavor = "current_thread")]
async fn geometry_batch_reuses_latest_output_until_fresh_paint_replaces_it() {
async fn geometry_batch_reuses_latest_tree_until_fresh_paint_replaces_it() {
run_page_vm_async_test(async move {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
@@ -257,16 +257,12 @@ document.body.innerHTML = '<div id="target"></div><div id="pass-through"></div><
assert_eq!(cache_after_screenshot.0, cache_before.0 + 1);
assert_eq!(cache_after_screenshot.1, cache_before.1 + 1);
assert_eq!(cache_after_screenshot.2, cache_before.2 + 2);
let (_, retention, retains_paint) = cache_after_screenshot
let (_, retention) = cache_after_screenshot
.3
.expect("fresh paint layout should publish its geometry output");
.expect("fresh paint layout should publish its frozen tree");
assert!(retention.box_count > 0);
assert!(retention.fragment_count > 0);
assert!(retention.estimated_geometry_bytes > 0);
assert!(
!retains_paint,
"the cached output must not retain paint resources"
);
let screenshot_metrics = after_screenshot.3.expect("screenshot layout metrics");
assert_eq!(
screenshot_metrics.reason,
@@ -354,7 +350,7 @@ document.body.innerHTML = '<div id=target></div>';
.vm()
.layout_snapshot_cache_observability_for_test();
assert_eq!(cache_after_first.2, cache_before.2 + 1);
assert!(cache_after_first.3.is_some_and(|(_, _, paint)| !paint));
assert!(cache_after_first.3.is_some());
page_vm
.vm_mut()
@@ -394,7 +390,7 @@ document.body.innerHTML = '<div id=target></div>';
assert_eq!(cache_after_second.0, cache_before.0 + 1);
assert_eq!(cache_after_second.1, cache_before.1);
assert_eq!(cache_after_second.2, cache_before.2 + 2);
assert!(cache_after_second.3.is_some_and(|(_, _, paint)| !paint));
assert!(cache_after_second.3.is_some());
let refreshed = moli_layout::GeometryProvider::answer(
page_vm.vm_mut(),
+7 -7
View File
@@ -2379,7 +2379,7 @@ impl ScriptVm {
&mut self,
viewport: moli_layout::LayoutViewport,
) -> Result<bool, moli_layout::LayoutError> {
self.with_fresh_layout_output(
self.with_fresh_layout_pass(
moli_layout::LayoutPassRequest::new(viewport, moli_layout::LayoutFlushReason::Test),
|_| Ok(()),
)
@@ -2405,18 +2405,18 @@ impl ScriptVm {
reason: moli_layout::LayoutFlushReason,
capture: moli_layout::PaintCaptureRequest,
) -> anyhow::Result<Option<moli_layout::PaintSnapshot>> {
self.with_fresh_layout_output(
self.with_fresh_layout_pass(
moli_layout::LayoutPassRequest::with_capture(viewport, reason, capture),
moli_layout::LayoutPassOutput::take_paint_snapshot,
moli_layout::LayoutPassResult::take_paint_snapshot,
)
.map_err(anyhow::Error::new)
}
fn with_fresh_layout_output<T>(
fn with_fresh_layout_pass<T>(
&mut self,
request: moli_layout::LayoutPassRequest,
consume: impl FnOnce(
&mut moli_layout::LayoutPassOutput<DomHandle>,
&mut moli_layout::LayoutPassResult<DomHandle>,
) -> Result<T, moli_layout::LayoutError>,
) -> Result<Option<T>, moli_layout::LayoutError> {
// Font/CSS source reconciliation is a pre-pass lifecycle step. Once
@@ -2430,7 +2430,7 @@ impl ScriptVm {
let context_host = self._context_host.borrow();
let document = context_host.document_handle();
let result =
context_host.with_fresh_layout_output_for_document(document, request, consume);
context_host.with_fresh_layout_pass_for_document(document, request, consume);
(document, result)
};
if matches!(&result, Ok(Some(_)))
@@ -2498,7 +2498,7 @@ impl ScriptVm {
u64,
u64,
u64,
Option<(DomHandle, moli_layout::LayoutOutputRetentionMetrics, bool)>,
Option<(DomHandle, moli_layout::LayoutTreeRetentionMetrics)>,
) {
let observability = self
._context_host
@@ -8023,7 +8023,7 @@ fn inner_text_new_sources_wait_for_a_fresh_paint_layout() {
)
.expect("the warm innerText read should evaluate"),
"a",
"a text source absent from the latest layout output remains unrendered until refresh"
"a text source absent from the latest frozen layout tree remains unrendered until refresh"
);
assert_eq!(vm.layout_pass_observability_for_test().1, passes_before + 1);
@@ -1482,7 +1482,7 @@ fn switching_to_mock_geometry_drops_the_latest_real_layout_snapshot() {
vm.layout_snapshot_cache_observability_for_test()
.3
.is_none(),
"Mock policy must not retain a real layout output"
"Mock policy must not retain a real frozen layout tree"
);
let passes_before_mock = vm.layout_pass_observability_for_test().1;
vm.eval("document.body.offsetTop")