#9548·lynx

[WPT] border width rounding changes flex-line capacity at fractional device scale

Author: zhongyrCreated Sep 15, 2026Updated Sep 15, 2026
Labelsstatus:need triagetype:bug

System Info

  • Android 17, Pixel 10 Pro; observed device scale: 2.625 physical pixels per CSS pixel.
  • Test app: Lynx UIApp, package versionName 2.7.0 (shell version, not a verified Lynx engine version).
  • Inspected public engine source: d698de373c028a3edf9abf244d7393c2743d7507. The relevant rounding functions match the locally inspected vendor source. The installed APK's exact engine revision/configuration has not been established; this is not a claim that this SHA was built into the APK.

Details

Test and property roles

Three source-pinned WPT cases expose one border-sizing/flex-line-capacity problem:

The relevant default-row source is:

xml
<div class="flex-wrapper">
  <div>1</div><div>2</div><div>3</div>
  <div>4</div><div>5</div><div>6</div>
  <div>7</div><div>8</div><div>9</div>
</div>
css
* { box-sizing: content-box; }
.flex-wrapper {
  display: flex;
  flex-wrap: wrap;
  width: 120px;
  height: 120px;
  background: red;
  direction: ltr;
  writing-mode: horizontal-tb;
}
.flex-wrapper > div {
  width: 38px;
  height: 38px;
  border: 1px solid limegreen;
  background: green;
  color: white;
  line-height: 40px;
  text-align: center;
}
  • content-box keeps 38px as the content size; border widths contribute to each item's outer size.
  • 120px is the available main-axis length. flex-wrap: wrap exposes whether three outer items fit before flex shrinking.
  • Direction is intentionally omitted for the default-row test. The other cases set column or column-reverse, making the same capacity boundary vertical.
  • Nine numbered items expose line formation and ordering; limegreen borders allow used widths to be measured separately from green content.
  • Horizontal LTR is setup, not a test of nondefault writing modes.

Observed behavior

The WebView render has three items per line; Lynx has two. The default row consequently produces two columns/five rows rather than a 3×3 grid. The column variants produce five columns with at most two items each.

At scale 2.625:

Quantity WebView Lynx
Used border, physical pixels per side 2 3
Nominal content width, physical pixels 99.75 99.75
Three outer items, approximate physical pixels 311.25 317.25
Available container length, physical pixels 315 315

The measured borders explain why the third Lynx item does not fit. This is not evidence that the direction values or the generic wrapping comparison are absent/broken.

Full screenshot differences: default 2.8933%, column 2.8908%, column-reverse 2.8927%, versus a 0.5% limit. There is a narrow red remainder even in the actual WebView reference; the source's idealized “no red” wording is not permission to hard-code a full-width grid.

Adaptation integrity

The Lynx cards retain all nine items, content-box dimensions, border width/style/color, wrapping, direction declarations/default, and text order. Original divs become views with text children; selectors are scoped to the original items, and only body/paragraph setup is normalized. Default horizontal LTR is retained without claiming writing-mode support. Source vertical-align does not participate in flex-item placement. No density-specific sizing, explicit three-column layout, or border-box conversion was used.

Engine code and causal path

CSSStyleUtils::GetBorderWidthFromLengthToFloat applies the generic pixel-grid rounding helper while resolving a border width:

cpp
float raw_value = value.NumericLength().GetFixedPart();
return CSSStyleUtils::RoundValueToPixelGrid(
    raw_value, context.physical_pixels_per_layout_unit_);

RoundValueToPixelGrid uses nearest-integer rounding:

cpp
return std::roundf(value * physical_pixels_per_layout_unit) /
       physical_pixels_per_layout_unit;

Thus a nominal border of 2.625 physical pixels resolves to 3 rather than the observed WebView used width of 2. This occurs before layout, not just in painting. CalculateFromBorderWidthStringToFloat stores the resolved value; LayoutObject::GetPaddingAndBorderHorizontal/Vertical includes the final border widths in item geometry. FlexLayoutAlgorithm line collection then compares the sum of outer hypothetical main sizes against the available length and correctly breaks the line for those enlarged inputs.

The confirmed checkout behavior is nearest rounding of border widths before layout. Its numeric effect matches the device pixels. The defect is the Web-incompatible border used-width quantization feeding line formation, not a reason to change the line collector's fit test. Exact installed-binary provenance remains a limitation.

Expected correction and regressions

Preserve Web-compatible border used widths consistently across sizing and painting. Do not change the generic rounding helper globally without auditing unrelated callers; border-specific snapping is the relevant policy boundary.

  • All three source cases should retain three items per line at the affected fractional scale, with Web-equivalent content/border geometry and ordering.
  • Test densities on both sides of half-pixel boundaries and subpixel/thin border behavior.
  • Cover content-box and border-box, both main axes, and wrapping/nonwrapping layouts.
  • Do not “fix” the cards by setting 40px border-box items: after snapping, that produces different content and grid dimensions from the source.

Reproduce link

Source WPT links above; adapted cards and committed evidence are in lynx-wpt MR !192 (internal access required).

Reproduce Steps

  1. Use the original WPT source as the WebView test render at device scale 2.625; do not substitute its WPT match-reference target.
  2. Run tests/css-flexbox/flexbox-flex-direction-default in Lynx UIApp at the same scale. Repeat with the column and column-reverse cards.
  3. Compare outer item border widths and the number of items per flex line, not just text rasterization or global bitmap percentage.

Evidence: each named card has evidence/{expected,actual,diff}.png; corresponding summaries are docs/evidence/css-flexbox-next-ten-20260915-<case>-summary.tsv. Each completed isolated run exited 1 with an exact reference-backed result and audited nonblank PNGs; source/adaptation and cleanup checks passed. No match-reference content was used. Full diagnosis: docs/diagnostics/flexbox-next-ten-20260915.md in the linked MR.

Duplicate check

Searched open and closed issues by the three test names, border rounding/pixels/wrapping, and WPT. Existing #8980 concerns wrap-reverse baseline offsets, not border used-width quantization; #8979 concerns early stretch through aspect ratio. No equivalent issue was found at filing time.