Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions FIDELITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,62 @@ The committed PNGs under `testdata/renders/` back every claim here. Reproduce
them with the commands at the bottom. The measured-vs-Chrome numbers live in
[`bench/REPORT.md`](bench/REPORT.md).

## 2026-09-05 (round 44) — a plain inline element's own `margin-left`/`margin-right` had NO EFFECT at all — `InlineItem` carries no margin field, so a browser-rendered gap between two adjacent inline elements with no source whitespace between them silently vanished (engine#124)

news.ycombinator.com re-investigated fresh with a full top-to-bottom
comparison (last touched round 33/38, via the general BlockBreak mechanism
rather than a dedicated look at this page's own remaining diffs). Live at
1024px, the header read "**Hacker News**new | past | comments | ..." — the
site title and the first nav link ran together with no gap, where Chrome
shows a clear space.

Root cause, confirmed against the real source: `<b class="hnname"
style="margin-right:5px">Hacker News</b><a href="newest">new</a>` — genuinely
no whitespace at all between the two elements in the HTML. The margin-right
IS the only source of the gap Chrome renders. `InlineItem` (the atom of
inline layout) has no margin field of any kind, and nothing in inline
collection ever reads `Style.Margin.Left`/`.Right` for a plain (non-replaced,
non-atomic) inline element — its margin was simply never consulted.

Fixed with a `pendingMargin` accumulator on the layouter: entering a plain
inline element adds its `margin-left`, leaving one adds its `margin-right`,
and whichever `InlineItem` is created next (in `appendWords`, or the
img/svg and form-control atomic-item branches) takes the accumulated
value into its `SpaceBefore` field via a new `takeMargin()` method — the
same field collapsible whitespace already uses to represent "space before
this item," reusing the existing line-layout machinery rather than adding a
parallel code path. Adjacent inline elements' margins correctly ADD (not
collapse, unlike block margins) since accumulation only resets at
`takeMargin()`. Reset alongside the whitespace-collapsing state at a
genuine break (a promoted block or forced `<br>`): a stale margin has
nothing left on the same line to apply to once one interrupts.

**A real, narrower limitation surfaced by the session's own OWN regression
test, not shipped over**: an inline margin lands correctly whenever the
margined element is not the very first item on its line — but
`layoutInline`/`wrapOneLine`/`WrapItems` deliberately ignore `SpaceBefore`
for a line's first item (so collapsible leading whitespace never creates a
phantom indent), and a margin riding in that SAME field is ignored for the
identical reason. A dedicated non-collapsible field would be needed to
survive that case too, not attempted here absent any CONFIRMED live page
needing it (this engine's real, confirmed use of inline margin-right is
always between two things already sharing a line, as on this page) — the
regression test suite covers the working case, documents the gap plainly in
`pendingMargin`'s own doc comment, and does not claim more than what is
actually fixed.

Verified live: "Hacker News new | past | ..." now renders with the correct
gap. **Bench is flat (SSIM 0.559→0.556, pixdiff ~16% either way)** — a
single-word header spacing fix on a page whose overall diff is dominated by
ordinary font-rasteriser variance across dozens of story-list rows, the
same already-documented pattern as every other small, correctly-scoped
fix this session that didn't move a shared-region score.

Four new regression tests in a new `layout/inlinemargin_test.go`
(margin-right, margin-left, adjacent-margins-add, and the block-break
drops-pending-margin boundary case), all confirmed to fail via genuine
revert-and-rerun before the fix.

## 2026-09-05 (round 43) — `filter` had NO EFFECT on any `<img>` element, block or inline, because an image is never represented as a layout.Box (the only thing the filter/opacity group-buffer pipeline knows how to wrap) (engine#123)

pkg.go.dev re-investigated fresh (last touched round 35, 8 rounds stale).
Expand Down
39 changes: 28 additions & 11 deletions bench/REPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<!-- AUTO-GENERATED: this header, the results table and the montage list are regenerated by `go run ./cmd/compare`. The hand-written analysis below the BEGIN ANALYSIS marker is preserved across re-runs. -->

**Generated:** 2026-09-05 14:59 UTC
**Generated:** 2026-09-05 15:43 UTC
**Viewport:** 1024×768, device-scale 1
**Timing:** median of N=5 runs after 1 warmup, wall-clock incl. network fetch
**Chrome:** `/Applications/Google Chrome.app/Contents/MacOS/Google Chrome`
Expand All @@ -11,16 +11,16 @@
<!-- BEGIN RESULTS TABLE -->
| URL | SSIM | pixdiff % | webengine ms | chrome ms | speed× | region | status |
|-----|-----:|----------:|-------------:|----------:|-------:|:------:|:------:|
| example.com/ | 0.954 | 1.5 | 124.7 | 1859.6 | 14.91 | 1024×768 | ok |
| en.wikipedia.org/wiki/Go_(programming_language) | 0.429 | 22.3 | 1758.9 | 4404.9 | 2.50 | 1024×2500 | ok |
| pkg.go.dev/net/http | 0.616 | 49.6 | 4194.3 | 4600.1 | 1.10 | 1024×2500 | ok |
| go.dev/blog/ | 0.684 | 19.0 | 3610.8 | 2959.8 | 0.82 | 1024×1597 | ok |
| react.dev/ | 0.615 | 46.3 | 2189.0 | 2789.3 | 1.27 | 1024×2500 | ok |
| news.ycombinator.com/ | 0.558 | 15.9 | 1346.8 | 2595.7 | 1.93 | 1024×1217 | ok |
| developer.mozilla.org/en-US/docs/Web/CSS | 0.629 | 15.6 | 528.9 | 2068.5 | 3.91 | 1009×2500 | ok |
| github.com/golang/go | 0.550 | 30.2 | 2741.2 | 2394.9 | 0.87 | 1024×2500 | ok |
| tailwindcss.com/ | 0.695 | 14.2 | 3841.9 | 2553.2 | 0.66 | 1024×2500 | ok |
| caniuse.com/ | 0.645 | 22.0 | 4075.2 | 7588.1 | 1.86 | 1024×1522 | ok |
| example.com/ | 0.954 | 1.5 | 107.2 | 1906.4 | 17.78 | 1024×768 | ok |
| en.wikipedia.org/wiki/Go_(programming_language) | 0.431 | 21.9 | 1658.8 | 4391.8 | 2.65 | 1024×2500 | ok |
| pkg.go.dev/net/http | 0.616 | 49.6 | 4193.6 | 4503.4 | 1.07 | 1024×2500 | ok |
| go.dev/blog/ | 0.684 | 19.0 | 3682.5 | 3083.2 | 0.84 | 1024×1597 | ok |
| react.dev/ | 0.615 | 46.3 | 2289.2 | 2795.2 | 1.22 | 1024×2500 | ok |
| news.ycombinator.com/ | 0.556 | 16.0 | 1367.3 | 2627.4 | 1.92 | 1024×1217 | ok |
| developer.mozilla.org/en-US/docs/Web/CSS | 0.636 | 13.5 | 525.2 | 2080.6 | 3.96 | 1009×2500 | ok |
| github.com/golang/go | 0.550 | 30.2 | 2780.6 | 2405.0 | 0.86 | 1024×2500 | ok |
| tailwindcss.com/ | 0.702 | 13.7 | 3728.6 | 2657.7 | 0.71 | 1024×2500 | ok |
| caniuse.com/ | 0.644 | 22.0 | 4088.8 | 4116.3 | 1.01 | 1024×1522 | ok |
<!-- END RESULTS TABLE -->

Speed× is `chrome_ms / webengine_ms`: >1 means webengine is faster.
Expand Down Expand Up @@ -69,6 +69,23 @@ Speed× is `chrome_ms / webengine_ms`: >1 means webengine is faster.

<!-- BEGIN ANALYSIS (hand-written, preserved across re-runs) -->

## Honest analysis — 2026-09-05 (round 44): a plain inline element's own margin now applies; news.ycombinator.com flat (engine#124)

**news.ycombinator.com: SSIM 0.559→0.556, pixdiff ~16% both before and
after — flat**, despite a real, visually-confirmed fix (the "Hacker
News"/"new" header text no longer runs together — see the before/after
crops referenced in FIDELITY.md's round 44 entry). A single 5px gap in
one line of an otherwise-correct header is a vanishingly small fraction
of ink on a page whose comparison region is dominated by 30 story-list
rows' worth of ordinary font-rasteriser variance — the same pattern this
session has now documented on nearly every small, correctly-scoped fix
that touches header/nav chrome rather than the page's dominant content
block (github.com rounds 39/41, pkg.go.dev round 43).

The other nine corpus pages move by sub-0.01 SSIM / ordinary timing
noise, unrelated to a fix scoped to inline-element margins that none of
them happens to depend on for a visible difference from Chrome.

## Honest analysis — 2026-09-05 (round 43): `filter` now applies to `<img>` elements; pkg.go.dev flat, MDN moves slightly the OTHER way despite both being real fixes (engine#123)

**pkg.go.dev/net/http: SSIM 0.617→0.616, pixdiff 49.6%→49.6% — flat**,
Expand Down
Binary file modified bench/out/caniuse.com.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/developer.mozilla.org_en-US_docs_Web_CSS.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/en.wikipedia.org_wiki_Go_programming_language.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/github.com_golang_go.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/news.ycombinator.com.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/pkg.go.dev_net_http.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified bench/out/tailwindcss.com.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 44 additions & 44 deletions bench/results.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[
{
"url": "https://example.com/",
"webengine_ms": 124.715,
"chrome_ms": 1859.569,
"speed_ratio": 14.91054804955298,
"webengine_ms": 107.223,
"chrome_ms": 1906.379,
"speed_ratio": 17.7795715471494,
"ssim": 0.9542315003467446,
"pixdiff_pct": 1.5338897705078125,
"region_w": 1024,
Expand All @@ -14,11 +14,11 @@
},
{
"url": "https://en.wikipedia.org/wiki/Go_(programming_language)",
"webengine_ms": 1758.868,
"chrome_ms": 4404.95,
"speed_ratio": 2.5044232995312896,
"ssim": 0.4290406279886116,
"pixdiff_pct": 22.289140625,
"webengine_ms": 1658.794,
"chrome_ms": 4391.798,
"speed_ratio": 2.647584932185672,
"ssim": 0.4314415240770195,
"pixdiff_pct": 21.912499999999998,
"region_w": 1024,
"region_h": 2500,
"montage": "out/en.wikipedia.org_wiki_Go_programming_language.png",
Expand All @@ -27,11 +27,11 @@
},
{
"url": "https://pkg.go.dev/net/http",
"webengine_ms": 4194.286,
"chrome_ms": 4600.147,
"speed_ratio": 1.0967652182040042,
"ssim": 0.6159638197337821,
"pixdiff_pct": 49.6362109375,
"webengine_ms": 4193.616,
"chrome_ms": 4503.437,
"speed_ratio": 1.0738792011476492,
"ssim": 0.6158435236491993,
"pixdiff_pct": 49.638984375,
"region_w": 1024,
"region_h": 2500,
"montage": "out/pkg.go.dev_net_http.png",
Expand All @@ -40,9 +40,9 @@
},
{
"url": "https://go.dev/blog/",
"webengine_ms": 3610.751,
"chrome_ms": 2959.778,
"speed_ratio": 0.8197125750294051,
"webengine_ms": 3682.489,
"chrome_ms": 3083.207,
"speed_ratio": 0.8372616998991714,
"ssim": 0.6838510700295284,
"pixdiff_pct": 19.031594884940514,
"region_w": 1024,
Expand All @@ -53,9 +53,9 @@
},
{
"url": "https://react.dev/",
"webengine_ms": 2189.023,
"chrome_ms": 2789.28,
"speed_ratio": 1.274212285572148,
"webengine_ms": 2289.198,
"chrome_ms": 2795.151,
"speed_ratio": 1.2210175790822813,
"ssim": 0.6145175634133307,
"pixdiff_pct": 46.2719140625,
"region_w": 1024,
Expand All @@ -66,11 +66,11 @@
},
{
"url": "https://news.ycombinator.com/",
"webengine_ms": 1346.808,
"chrome_ms": 2595.687,
"speed_ratio": 1.927288076696901,
"ssim": 0.558431492294827,
"pixdiff_pct": 15.947016870377977,
"webengine_ms": 1367.262,
"chrome_ms": 2627.422,
"speed_ratio": 1.921666805630523,
"ssim": 0.5558571737864567,
"pixdiff_pct": 16.045234824363188,
"region_w": 1024,
"region_h": 1217,
"montage": "out/news.ycombinator.com.png",
Expand All @@ -79,11 +79,11 @@
},
{
"url": "https://developer.mozilla.org/en-US/docs/Web/CSS",
"webengine_ms": 528.852,
"chrome_ms": 2068.479,
"speed_ratio": 3.9112625082253634,
"ssim": 0.6286406848741246,
"pixdiff_pct": 15.597026759167493,
"webengine_ms": 525.244,
"chrome_ms": 2080.631,
"speed_ratio": 3.9612656213112376,
"ssim": 0.6357537218685836,
"pixdiff_pct": 13.493002973240834,
"region_w": 1009,
"region_h": 2500,
"montage": "out/developer.mozilla.org_en-US_docs_Web_CSS.png",
Expand All @@ -92,11 +92,11 @@
},
{
"url": "https://github.com/golang/go",
"webengine_ms": 2741.196,
"chrome_ms": 2394.866,
"speed_ratio": 0.8736573378919275,
"ssim": 0.5503981297417342,
"pixdiff_pct": 30.208476562499996,
"webengine_ms": 2780.603,
"chrome_ms": 2405.04,
"speed_ratio": 0.8649346922232336,
"ssim": 0.5502544091087782,
"pixdiff_pct": 30.2062890625,
"region_w": 1024,
"region_h": 2500,
"montage": "out/github.com_golang_go.png",
Expand All @@ -105,11 +105,11 @@
},
{
"url": "https://tailwindcss.com/",
"webengine_ms": 3841.902,
"chrome_ms": 2553.221,
"speed_ratio": 0.6645721311995986,
"ssim": 0.6953557742919156,
"pixdiff_pct": 14.155507812500002,
"webengine_ms": 3728.558,
"chrome_ms": 2657.735,
"speed_ratio": 0.7128050576120849,
"ssim": 0.7022401926884426,
"pixdiff_pct": 13.698046875,
"region_w": 1024,
"region_h": 2500,
"montage": "out/tailwindcss.com.png",
Expand All @@ -118,11 +118,11 @@
},
{
"url": "https://caniuse.com/",
"webengine_ms": 4075.15,
"chrome_ms": 7588.091,
"speed_ratio": 1.8620396795209992,
"ssim": 0.6449877966594669,
"pixdiff_pct": 21.950776630256243,
"webengine_ms": 4088.794,
"chrome_ms": 4116.255,
"speed_ratio": 1.0067161612935258,
"ssim": 0.6440262660584839,
"pixdiff_pct": 22.03951420827858,
"region_w": 1024,
"region_h": 1522,
"montage": "out/caniuse.com.png",
Expand Down
90 changes: 90 additions & 0 deletions layout/inlinemargin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Copyright (c) the go-webengine/engine authors.
// SPDX-License-Identifier: BSD-3-Clause

package layout

import "testing"

// TestInlineElementMarginRightAddsGap covers news.ycombinator.com's own real
// markup: `<b class="hnname" style="margin-right:5px">Hacker News</b><a
// href="newest">new</a>` — no source whitespace between the two elements, so
// the only gap between "Hacker News" and "new" is the <b>'s own margin-right.
// InlineItem carries no margin field of its own; before this fix that margin
// was silently dropped, running the words together as "Hacker Newsnew".
func TestInlineElementMarginRightAddsGap(t *testing.T) {
src := `<html><body style="margin:0"><span>` +
`<b style="margin-right:5px">AB</b><a href="x">CD</a>` +
`</span></body></html>`
items := firstLineItems(findBox(layoutHTML(t, src, 300), "body"))
if len(items) != 2 {
t.Fatalf("items = %d, want 2: %v", len(items), items)
}
assertF(t, "AB.X", items[0].X, 0)
assertF(t, "CD.SpaceBefore", items[1].SpaceBefore, 5)
// "AB" is 2 runes * 10px (fakeMeasurer) = 20px wide; "CD" starts 5px after.
assertF(t, "CD.X", items[1].X, 25)
}

// TestInlineElementMarginLeftAddsGap covers margin-left applying as leading
// space before an inline element's own content, mirroring the margin-right
// case above. Deliberately NOT the very first item of its line: pendingMargin
// is carried in SpaceBefore, the same field collapsible whitespace uses, and
// layoutInline/wrapOneLine's line-breaking deliberately ignore SpaceBefore
// for a line's first item (so collapsible whitespace never creates a phantom
// indent) — a real margin on the line-INITIAL element would need a separate
// field to survive that, unconfirmed as a live bug on any of this project's
// corpus pages and NOT fixed here; this test covers the confirmed, common
// case (an inline margin between two things already on the same line).
func TestInlineElementMarginLeftAddsGap(t *testing.T) {
src := `<html><body style="margin:0"><span>` +
`<a href="x">AB</a><b style="margin-left:5px">CD</b>` +
`</span></body></html>`
items := firstLineItems(findBox(layoutHTML(t, src, 300), "body"))
if len(items) != 2 {
t.Fatalf("items = %d, want 2: %v", len(items), items)
}
assertF(t, "CD.SpaceBefore", items[1].SpaceBefore, 5)
assertF(t, "CD.X", items[1].X, 25)
}

// TestAdjacentInlineMarginsAdd covers two adjacent inline elements' margins
// stacking rather than collapsing — unlike BLOCK margins, adjacent inline
// margins are independent horizontal space that simply adds up.
func TestAdjacentInlineMarginsAdd(t *testing.T) {
src := `<html><body style="margin:0"><span>` +
`<b style="margin-right:5px">AB</b><i style="margin-left:3px">CD</i>` +
`</span></body></html>`
items := firstLineItems(findBox(layoutHTML(t, src, 300), "body"))
if len(items) != 2 {
t.Fatalf("items = %d, want 2: %v", len(items), items)
}
assertF(t, "CD.SpaceBefore", items[1].SpaceBefore, 8)
assertF(t, "CD.X", items[1].X, 28)
}

// TestBlockBreakDropsPendingMargin covers the boundary case explicitly: a
// pending margin-right with nothing left on the same line to apply to (a
// promoted block, see BlockBreak, immediately follows) is dropped rather than
// leaking into whatever comes after the block in the SAME parent scope.
func TestBlockBreakDropsPendingMargin(t *testing.T) {
src := `<html><body style="margin:0"><span>` +
`<b style="margin-right:5px">AB</b>` +
`<div style="display:block">block</div>CD` +
`</span></body></html>`
body := findBox(layoutHTML(t, src, 300), "body")
if body == nil || len(body.Children) < 3 {
n := -1
if body != nil {
n = len(body.Children)
}
t.Fatalf("body.Children = %d, want >= 3 (anon run, div, anon run)", n)
}
lastAnon := body.Children[len(body.Children)-1]
items := firstLineItems(lastAnon)
if len(items) != 1 || items[0].Text != "CD" {
t.Fatalf("items after the block break = %v, want just \"CD\"", items)
}
if items[0].SpaceBefore != 0 {
t.Errorf("CD.SpaceBefore = %v, want 0 (pending margin dropped at the block break)", items[0].SpaceBefore)
}
}
Loading