Skip to content

Make paint-placement maintenance incremental (O(damage), not O(tree)) - #803

Open
nicoburns wants to merge 33 commits into
mainfrom
devin/1787709349-incremental-paint-attach
Open

nicoburns wants to merge 33 commits into
mainfrom
devin/1787709349-incremental-paint-attach

Conversation

@nicoburns

@nicoburns nicoburns commented Aug 26, 2026 •

Copy link
Copy Markdown
Member

Summary

Stacked on #805 (this PR's branch includes #805's commits; the diff reduces to the three commits below once #805 merges). Removes the avoidable O(tree) per-frame work from the paint-placement side of the OOF hoisting branch, with no behavior change. Three independent commits:

1. Hoisted paint-child positions are derived at use-time, not baked.

 pub struct HoistedPaintChild {
     pub node_id: NodeId,
     pub z_index: i32,
-    pub position: taffy::Point<f32>,
 }

Paint, hit-testing, and compute_content_size now compute the offset via Node::hoisted_child_position(child_id), which accumulates location - scroll_offset along the child's containing_block() chain (the OOF containing block for hoisted boxes, otherwise layout_parent) up to the owning node (with a negated walk for the effect-ancestor case where the owner is below the containing block). Since nothing baked can go stale, layout/scroll changes elsewhere no longer require re-baking every hoisted entry.

2. attach_hoisted_children no longer scans the whole node slab.

BaseDocument gains oof_containing_blocks: HashSet<NodeId>, maintained by Taffy's OOF callbacks (add_hoisted_children inserts, clear_hoisted_children removes). Attach iterates only these registered containing blocks (sorted for determinism, lazily pruning removed/emptied ones), so its cost is bounded by the number of OOF boxes, not document size.

3. flush_styles_to_layout skips clean subtrees (incremental mode).

Damage propagation already bubbles descendant damage into each ancestor's stored damage, so damage.is_empty() && !has_damaged_descendants() && !is_anonymous() proves the whole subtree is unchanged: its taffy styles, layout_children order-sort, paint_children, and stacking contexts are all still valid, and the flush returns early. The one piece a skipped subtree still owes its surroundings — the HoistedPaintChild entries it contributed to the nearest ancestor stacking context — is replayed from a per-node sc_contribution_cache captured on the last flush that visited it, so a damaged ancestor SC that rebuilds its list keeps entries from clean descendant subtrees. This makes the flush's per-frame cost O(damaged nodes).

Not addressed (intentionally): the tree-order splice and is_before_in_tree_order are unchanged (same ordering semantics as the base branch), Appendix E step-8 interleaving remains approximate (grid-items-relative-positioned-containing-block-003 still fails), and attach still visits every registered CB each frame — it can't be safely damage-gated because a rebuilt ancestor stacking context needs clean CBs' z-indexed entries re-pushed.

Testing

Link to Devin session: https://dioxus.staging.devinenterprise.com/sessions/2641a1705e2547a0b385ec07a70a3abb
Open in Devin Desktop: https://dioxus.staging.devinenterprise.com/desktop/session/2641a1705e2547a0b385ec07a70a3abb?variant=devin-insiders
Requested by: @nicoburns

@staging-devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787406503-taffy-oof-hoisting branch from 55127ef to cf47ec8 Compare August 26, 2026 11:01
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787709349-incremental-paint-attach branch from d2a2fd2 to 7555327 Compare August 26, 2026 11:03
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787406503-taffy-oof-hoisting branch from cf47ec8 to 8860581 Compare August 26, 2026 11:21
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787709349-incremental-paint-attach branch from 7555327 to c435a98 Compare August 26, 2026 11:23
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787709349-incremental-paint-attach branch from c435a98 to 84db66d Compare September 2, 2026 19:30
@staging-devin-ai-integration
staging-devin-ai-integration Bot changed the base branch from devin/1787406503-taffy-oof-hoisting to devin/1787748926-taffy-oof-hoisting-latebound September 2, 2026 19:30
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787748926-taffy-oof-hoisting-latebound branch from eca059a to 07c2ec6 Compare September 12, 2026 22:23
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787709349-incremental-paint-attach branch from 84db66d to bf3afe0 Compare September 17, 2026 14:30
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787748926-taffy-oof-hoisting-latebound branch from 06b66c6 to 7367a75 Compare September 17, 2026 14:51
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787709349-incremental-paint-attach branch 14 times, most recently from 7c63dd6 to 47dea7c Compare September 17, 2026 17:43
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787748926-taffy-oof-hoisting-latebound branch from 64991d8 to dc19e19 Compare September 17, 2026 18:38
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787748926-taffy-oof-hoisting-latebound branch from 57ae385 to 7e28382 Compare September 17, 2026 19:25
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787709349-incremental-paint-attach branch from e8f2315 to 402e100 Compare September 17, 2026 19:26
Taffy's add/clear_hoisted_children callbacks now record the containing
block in a dedicated Node::containing_block cell instead of repointing
layout_parent, which stays the layout-tree parent. Coordinate
accumulation (absolute_position, offsetParent, debug overlay, stacking
context offsets) follows position_parent() = containing_block or
layout_parent.
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787709349-incremental-paint-attach branch from 402e100 to 8ae93ea Compare September 17, 2026 19:36
getComputedStyle for top/right/bottom/left on absolute/fixed boxes must
measure against the containing block the box's Layout.location is
relative to, which is no longer layout_parent since hoisting stopped
mutating it.
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787709349-incremental-paint-attach branch from 8ae93ea to 4da2cb7 Compare September 17, 2026 19:43
HoistedPaintChild no longer stores a position offset. The offset of a
hoisted child relative to the node that paints it is derived from the
layout_parent chain when painting/hit-testing (Node::hoisted_child_position),
so it cannot go stale when layout or scroll offsets change elsewhere.
This removes the main reason attach_hoisted_children had to re-run over
the whole tree every frame.
Taffy's out-of-flow callbacks (set_hoisted_children/add_hoisted_children)
maintain a registry of containing blocks that own hoisted boxes, and
attach_hoisted_children iterates that registry (with lazy pruning of
removed/emptied entries) rather than every node in the document.
In incremental mode, subtrees with no damage (damage propagation bubbles
descendant damage into each ancestor's stored damage) are skipped by the
style flush: their taffy styles, layout/paint child lists and stacking
contexts are unchanged from the previous flush. The HoistedPaintChild
entries a skipped subtree contributed to an ancestor stacking context are
replayed from a per-node cache captured when the subtree was last visited,
so a rebuilt ancestor stacking context keeps entries from clean subtrees.
This makes the per-frame style flush O(damaged nodes) rather than O(tree).
@staging-devin-ai-integration
staging-devin-ai-integration Bot force-pushed the devin/1787709349-incremental-paint-attach branch from 4da2cb7 to 8f73948 Compare September 17, 2026 19:49
@staging-devin-ai-integration
staging-devin-ai-integration Bot changed the base branch from devin/1787748926-taffy-oof-hoisting-latebound to main September 17, 2026 20:04

This branch has not been deployed

No deployments
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