This repository has no description
bez specs device-pixel-rounding.md
8.0 kB


enabled: true touches: [crates/dom/src/px64.rs, crates/dom/src/lib.rs, crates/logic/src/lib.rs, crates/layout/src/fragment.rs, crates/layout/src/bin/fragment_check.rs, roadmap.md] depends-on: [] #

Device-pixel rounding: bound the width chain's compounding drift #

roadmap.md → "Proof of concept" claim 1, measured on the assembled engine once keep-winners landed its winners: 216/227, every category at 100% except NestingDepth 9/20, "whose eleven losses are the device-pixel rounding gap below (worst node delta 0.078 px against the 1/60 px tolerance)." crates/layout/src/fragment.rs's own module doc already names the cause: it is "hand-directed integration scaffolding," and roadmap.md → "Substrate contract" says plainly that "Phase 0/1 scaffolding does not yet meet this contract itself: crates/layout/src/fragment.rs computes in physical f64 with no computed/used distinction." roadmap.md → "Geometry mapping" names the fix: "fixed-point arithmetic internally for determinism is the target for generated layout... the only 1/64 px fixed-point type in the workspace so far is crates/logic/src/px64.rs, scoped to the margin-collapse rules crate." docs/firefox-app-units.md → "The mechanism, now instrumented directly" reproduces the exact recipe shape (recipe_nesting_depth, twelve levels of width: 83.333%) directly: this engine's own unrounded chain reaches 89.72098558887357px against Chromium's and WebKit's shared, device-pixel-rounded 89.671875px — 5739/64 exactly.

That last number is the reason this spec rounds to 1/64 px, not 1/60 px, despite the queue line naming "the 1/60 px fixed-point rounding": 1/60 px is dump.rs's TOLERANCE_PX, the harness's own pass/fail comparison threshold, sized to Gecko's coarser app-unit grid because Gecko is the outlier the three-engine vote already excludes on this exact case shape (docs/firefox-app-units.md → "The mechanism"). It is not a target for this engine's own arithmetic. roadmap.md → "Geometry mapping" names 1/64 px — Blink's and WebKit's LayoutUnit grid, the same grid px64.rs already implements and the grid the majority ground truth for nesting-depth-12 lands on — as what generated layout should round to internally. This spec rounds to that grid; TOLERANCE_PX and the comparison logic in dump.rs/compare.rs are untouched.

Scope. In: rounding every used-value f64 layout_box (crates/layout/src/fragment.rs) computes to 1/64 px, through dom::px64::{to_px64_truncating, from_px64} (re-exported at logic::px64 so every existing call site keeps resolving), at the point each value is chosen — before it becomes a fragment's own size/offset or the basis (Available::inline_size/height_basis) a child's percentage resolves against — so the twelve-deep compounding recipe_nesting_depth (crates/harness/src/cases.rs) exercises stays bounded the way Blink's/WebKit's own per-operation quantization does. Reuses px64.rs's existing Px64/to_px64_truncating/from_px64 rather than inventing a second fixed-point type. Out: this is not a generated seam. No model batch, no candidate-admission loop, no new plain-data resolver type, no corpus of its own — hand-written engineering directly in the substrate (fragment.rs), unlike every other item currently in the queue (block-height-equation, overflow-scrollbars, inline-atomic, tables-fixed-layout are each either extracted to a plain-data seam or run model-first). None of crates/generation, corpus/*-ground-truth/, or a provenance record applies here — a future reader must not assume the batch-loop machinery this repo uses for every other queued item is in play. Also out: text metrics, device-pixel-ratio snapping (already pinned to 1 harness-wide, "Oracles" → confounds), and any change to TOLERANCE_PX or compare.rs's comparison logic.

Build:

  • crates/logic/src/px64.rs moves to crates/dom/src/px64.rs, with pub use dom::px64; added at crates/logic/src/lib.rs so every existing crate::px64:: and logic::px64:: call site still resolves unchanged. The move is necessary, not cosmetic: the workspace's crate edges run harness <- dom <- layout <- logic <- generation, and logic already depends on layout (logic::facts, rules, vote_check and two binaries read layout::boxtree::BoxTree), so a layout -> logic edge closes a cycle Cargo refuses. dom is the lowest crate both layout and logic can reach, and it already owns computed CSS values, which is where a CSS-pixel unit belongs. layout/Cargo.toml needs no change — layout already depends on dom. Update the module doc's claim "No such type exists elsewhere in the workspace yet... this is a new type scoped to this crate, not a reused one" once fragment.rs depends on it below — that sentence goes stale the moment it's reused.
  • Of to_px64/to_px64_truncating, use to_px64_truncating, not to_px64. crates/harness/src/cases.rs's twelve-level recipe_nesting_depth chain lands on 89.671875px (5739/64 exactly) only with truncation toward zero; round-to-nearest measures worse than not quantizing at all (delta moves from 0.049px to 0.047px instead of to 0.000px), because it hands each nesting level a basis larger than any browser ever computes, the same way a LayoutUnit is built from a float. Measure both if this is ever revisited — the numbers are the argument, not the name of the function.
  • crates/layout/src/fragment.rs, layout_box() — round content_width (both the float branch's shrink-to-fit result and the width_resolver branch's used_width) immediately after it's chosen, before it feeds child_available (Available { inline_size: content_width, .. }, a few lines below) — this is the value every one of the twelve nesting levels resolves its own percentage width against, and the one field that actually closes NestingDepth. Round border_box_width, the height basis definite_content_height produces, and the fragment's own offset_x/offset_y at assembly the same way, so every used value the substrate contract's "quantizes at every operation" language calls for is actually quantized, not only the one field this category's recipe happens to exercise.
  • crates/layout/src/bin/fragment_check.rs — CategoryTally tracks only passed/total today. Add a worst_abs_delta field, read from CaseComparison::Compared(r) .worst_abs_delta (harness::compare::CaseReport already carries it, unused by this binary), and print it beside each category's count. This is what turns "worst delta 0.078 px" into a number this track can re-measure directly rather than one inspected by hand, the way docs/firefox-app-units.md's mechanism section was.

Gates: cargo test --workspace passes. fragment_check --random (cargo run --release --bin fragment_check -p layout -- --random) still reports at least 216/227, with every category currently at 100% staying at 100% — rounding to 1/64 px must not introduce a new mismatch anywhere outside nesting. NestingDepth's own row in that same run must improve above 9/20, and the worst_abs_delta this step adds to the binary's output for NestingDepth must drop under the current 0.078 px — the recipe's twelve levels are exact percentages of exact pixel-pool literals (cases.rs's own src.pct/px pools), so an engine quantizing at every level the same grid Blink and WebKit do has no arithmetic reason to land anywhere but exactly on their shared value, i.e. as close to 0.0 as floating-point round-trip noise allows.

Afterwards, update roadmap.md → "Proof of concept" claim 1 with the new fragment_check --random count and NestingDepth row, the way specs/floats-bfc.md's own closing paragraph updates claim 2's held-out numbers — and record the 1/60-vs-1/64 correction above in that same update, so the next reader of the queue line does not repeat it.