This repository has no description
bez docs geometry-verification.md
6.9 kB

How layout output is checked against real browsers #

The engine's layout is judged by one question: for a given page, does every box come out the same size and in the same place as in real browsers? This page explains how that is answered, starting from nothing. The exact wire format is in docs/dump-format.md.

The idea in one picture #

flowchart LR
  page["test page<br/>(HTML + CSS)"] --> chr["Chromium"]
  page --> ff["Firefox"]
  page --> wk["WebKit"]
  page --> nev["this engine"]
  chr --> vote["vote per<br/>measurement"]
  ff --> vote
  wk --> vote
  vote --> cmp{"engine within<br/>1/60 px of the vote?"}
  nev --> cmp
  cmp --> result["pass / fail<br/>+ per-box error"]

The browser side runs once per page and is cached; only the engine side runs on every candidate.

Step by step #

1. Dump. Each browser loads the page through Playwright (oracle/oracle.mjs) at a fixed 800×600 viewport, device pixel ratio 1, and walks every element in document order. For each it records the element's box rectangles (getClientRects()), a subset of computed style, and whether its content overflows. That list is a dump. The engine produces the same dump from its own fragment tree (crates/harness/src/dump.rs).

2. Compare shape first. Both dumps must list the same elements in the same tree shape (tag, depth, parent). If they do not, the difference is in parsing, not layout, and the case stops there.

3. Compare sizes and relative offsets. For each box: its width and height, and its position relative to its parent. Relative offsets mean one misplaced box shows up as one wrong number instead of shifting every box below it, so the first wrong box names the rule that is wrong.

4. Vote. With three browsers, each measurement is classified (crates/harness/src/compare.rs vote()):

flowchart TD
  m["one measurement,<br/>three browser values"] --> q1{"all three agree<br/>within 1/60 px?"}
  q1 -- yes --> u["unanimous:<br/>that value is the reference"]
  q1 -- no --> q2{"two agree,<br/>one differs?"}
  q2 -- yes --> o["2–1: the pair's value is the<br/>reference, the third is named outlier"]
  q2 -- no --> s["split: no reference,<br/>measurement skipped"]

Two browsers could only report "they disagree" and throw the case away; three can keep it and name who is off.

5. Discard what cannot be trusted. A whole case is dropped rather than scored when the browsers disagree about whether some box has a scrollbar, because a scrollbar takes a platform-dependent width from everything inside it (compare::scrollbar_dispute, docs/dump-format.md → "Scrollbar probe"). Unanimous agreement that a scrollbar is there does not discard the case.

6. Score. A case passes when every trustworthy measurement of the engine is within 1/60 px of the reference. 1/60 px is the coarsest grid any of the browsers rounds to (Firefox's); Chromium and WebKit use 1/64 px. The raw difference is kept too, as a finer-grained signal than pass/fail.

A worked example #

Twelve nested boxes, each width: 83.333% of its parent, starting from 800 px (the nesting-depth-12 recipe). At the innermost box:

source width at depth 12
Chromium 89.671875 px
WebKit 89.671875 px
Firefox 89.66667175292969 px (its own 1/60 px grid)
vote unanimous — all three within 1/60 px; reference 89.671875 px
engine, exact arithmetic 89.72098558887357 px — off by 0.049 px, fail
engine, rounding to nearest 1/64 px at each level 89.71875 px — fail
engine, truncating to 1/64 px at each level 89.671875 px — exact, pass

The browsers' shared answer told the engine exactly how they round: truncate toward zero on a 1/64 px grid at every step (crates/dom/src/px64.rs, specs/device-pixel-rounding.md). On other boxes of the same shape Firefox's coarser grid drifts up to 0.0375 px from the other two, past the tolerance; there the vote is 2–1 and Firefox is named the outlier. That drift turned out to be a real web-compatibility bug (docs/firefox-app-units.md).

Where the test pages come from #

flowchart LR
  recipes["recipe corpus<br/>227 cases"] --> gate["development signal<br/>and landing floor"]
  seamc["per-function corpora<br/>104–109 cases each"] --> gate
  grammar["grammar corpus<br/>200 cases"] --> held["held-out exam"]
  wpt["WPT normal-flow pages<br/>11 usable"] --> held
  • Recipe corpus (crates/harness/src/cases.rs). About thirty recipes, each a small page generator for one branch of the spec's case analysis (auto margins on one side, a negative margin collapsing with a positive one, a percentage against an auto-height parent...). Each recipe is a category, so scores are reported per category. 27 fixed cases plus 200 seeded-random draws from the same recipes (seed 0xC0FFEE) make 227. Values are deliberately fractional (33.333%, 1/64 px steps), because round numbers hide rounding bugs.
  • Per-function corpora (absolute_cases.rs, float_cases.rs, relpos_cases.rs, stacking_cases.rs, block_height_cases.rs, overflow_cases.rs). The same recipe idea, one set per generated function, kept separate so adding cases does not reshuffle everyone else's random draw.
  • Grammar corpus (crates/harness/src/grammar.rs). Pages composed freely from the same properties, with no recipe steering them, so it contains shapes no recipe thought of. Never used to generate code — only to check it.
  • WPT pages. The css/CSS2/normal-flow pages that the engine can read (no text, no external stylesheet): 11 of 1056. Scored the same way, never trained against.
  • SMT-sampled cases (crates/harness/src/smt.rs). A handful of margin-collapse cases whose values a solver chose to hit rule boundaries.

Floors #

A floor is a recorded score that a change may not go below; landing on main runs them (bench/land.sh):

  • bench/fragment-floor.sh — the recipe corpus through layout() must pass 227 of 227.
  • bench/verify.sh (bench/generated-scores.sh) — every generated function must pass at least as many cases of its own corpus as its <name>.floor.json holds, and the corpus must not shrink.

Floors are raised when more cases pass (by bench/generated-floors-update.sh after a landing), never lowered to let a change through.

Current numbers #

  • Recipe corpus: 227/227, worst per-box error 0.0000 px.
  • Held-out WPT pages: 11/11. Grammar corpus: 61/164 scored, 36 discarded for scrollbar disagreement. The gap between 227/227 and 61/164 is the honest measure of how much the recipes still miss.
  • Browser agreement: 699 of 705 browser pairs agreed across 235 documents; all six disagreements were the twelve-deep percentage shape above.

History of these numbers: roadmap.md → "Proof of concept".