Skip to content

fix/incremental restyle run topology - #66

Open
thejustinwalsh wants to merge 1 commit into
mainfrom
fix/incremental-restyle-run-topology
Open

fix/incremental restyle run topology#66
thejustinwalsh wants to merge 1 commit into
mainfrom
fix/incremental-restyle-run-topology

Conversation

@thejustinwalsh

@thejustinwalsh thejustinwalsh commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

What

A metrics-only restyle could permanently poison an engine session. The presentation soak surfaced it: paragraph 5's animated emphasis fontSize passes through the root's value, every subsequent frame fails with invalidRequest, and the session never recovers.

Mechanism

fontSize invalidates only metrics, so prepare_shape retained the committed shape. But shaping-run merging compares layout scalars including font_size — when a span's animated size crosses value-equality with its neighbor, the rebuilt pending run table merges (or splits) runs the committed shape doesn't have. Glyphless run ownership then rejects the shape/table mismatch, and because the failure happens after the pending table is built but before commit, every retry hits the same wall.

frame N   span fontSize == root  → pending runs merge   (13 runs)  → shaped + committed
frame N+1 span fontSize != root  → pending runs split   (15 runs)
          metrics-only invalidation → shape retained     (13 runs)
          fill_glyphless_run_ownership: shaped run ∉ source run → invalidRequest, forever

Fix

prepare_shape's retention early-return now also requires the committed and pending run tables to agree on run boundaries; any topology change reshapes.

Evidence

  • Red-green regression in three-engine-runtime.test.mjs: animated span fontSize passing through root equality, with a paint difference keeping style segments split while runs merge. Fails with invalidRequest before the fix, passes after.
  • Byte-exact replay of the four captured production frames (including the originally failing frame 11): all apply cleanly, revisions advance.
  • Full presentation soak (bitmap/WebGPU, the originally failing cell): exit 0, zero engine errors, all 8 workloads settled.
  • 172 Rust lib tests pass; clippy adds no warnings for the touched file; size gate green after a ~50-byte raw wasm re-price for the comparator (compressed budgets unchanged).

Debugging notes

Two missteps recorded in the log for posterity: Option::ok_or evaluates its error argument eagerly (a poisoned-site tracker silently lied until switched to ok_or_else), and the first regression attempt passed because a fontSize-only span at value-equality collapses to a single style segment — full invalidation hides the bug unless paint keeps the segments split.

…ology

A fontSize-only restyle invalidates metrics without shaping, but run
merging compares layout scalars including font_size: a span crossing
value-equality with its neighbor changes the pending run-table topology
while prepare_shape retains the stale merged shape. Glyphless run
ownership then rejects the mismatch as invalidRequest and the session
never recovers.

Guard the shape-retention early return with a committed-vs-pending run
boundary comparison so any topology change reshapes. Proven by a
red-green integration regression (animated span fontSize passing through
root equality with a paint difference keeping style segments split),
byte-exact replay of the captured production frames including the
failing one, and a clean full presentation soak. Raw wasm budget rises
~50 bytes for the comparator; compressed budgets unchanged.
@thejustinwalsh
thejustinwalsh marked this pull request as ready for review August 12, 2026 03:14
@github-actions

Copy link
Copy Markdown

size-limit report 📦

Path Size
Core JS (gzip) 18.37 KB (0%)
Shaper Wasm (gzip) 418.79 KB (+0.01% 🔺)
Three.js adapter JS (gzip) 57.7 KB (0%)
Inter font · Bitmap (gzip) 545.26 KB (0%)
Inter font · MTSDF (gzip) 6.48 MB (0%)
Inter font · Slug (gzip) 604.06 KB (0%)
Font Awesome icons · Bitmap (gzip) 439.53 KB (0%)
Font Awesome icons · MTSDF (gzip) 6.89 MB (0%)
Font Awesome icons · Slug (gzip) 642.64 KB (0%)
Font validator JS (gzip) 134.24 KB (0%)
Runtime bake host JS (gzip) 5.38 KB (0%)
Runtime bake Worker JS (gzip) 142.45 KB (0%)
Font baker JS (gzip) 2.41 KB (0%)
Font baker Wasm (gzip) 379.63 KB (0%)
Bitmap baker JS (gzip) 4.66 KB (0%)
Bitmap baker Wasm (gzip) 228.3 KB (0%)
MTSDF baker JS (gzip) 5.39 KB (0%)
MTSDF baker Wasm (gzip) 209.22 KB (0%)
Slug baker JS (gzip) 4.01 KB (0%)
Slug baker Wasm (gzip) 181.24 KB (0%)

thejustinwalsh added a commit that referenced this pull request Aug 13, 2026
…rement

Community feedback root-caused: feeding a raw f32 contentWidth back as
the next exact constraint flips line counts at knife-edge widths (39 of
811 swept), producing high-frequency break/unbreak flapping and CPU
churn in reactive layout engines; rounding the fed-back width up to the
point scale flips zero. The rule lived only in the conformance
fixture's roundUpToPointScale; the integration document now states it
with the stable Yoga callback pattern and notes the pre-#66 session
storms and the 11.17 synchronous-measure outlook.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant