feat(atlas): implement recompute_dirty with TargetId-based invalidation#19
Merged
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds selective (incremental) layout recomputation to the Atlas by consuming the Evaluator’s broadcast dirty-target stream (Vec<TargetId>) and filtering it via a new TargetKind discriminant plus an Atlas view-generation counter.
Changes:
- Introduces
frame::TargetKind(#[repr(u16)]) and uses it withTargetIdto support subsystem-scoped dirty-target broadcasts. - Extends
LayoutAtlaswith TargetId-based invalidation (mark_dirty_all) and incremental recomputation (recompute_dirty), plus generation/index helpers. - Adds Atlas-focused benchmarks and wiring in
Cargo.toml.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/byard-core/src/frame.rs | Adds TargetKind enum and a round-trip test for TargetId kind packing. |
| crates/byard-core/src/evaluator/signal.rs | Updates module docs to reference ViewArena more directly. |
| crates/byard-core/src/atlas/mod.rs | Expands Atlas docs to describe the new dirty-target flow and lifecycle. |
| crates/byard-core/src/atlas/layout.rs | Implements generation-aware TargetId invalidation (mark_dirty_all) and incremental layout (recompute_dirty), plus helpers and tests. |
| crates/byard-core/Cargo.toml | Registers the new atlas benchmark target. |
| crates/byard-core/benches/atlas.rs | Adds benchmark program comparing full compute vs incremental recompute across tree shapes/sizes. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Implements selective layout recomputation in the Atlas, wired to the
Evaluator's
EvaluatorTick::collect_dirty()output viaTargetIds andthe
TargetKinddiscriminant. Closes the Evaluator → Atlas loopdescribed in RFC-0001 2.2 and 4.1.
New surface area:
frame::TargetKind—#[repr(u16)]enum used to filter the broadcastTargetIdstream by owning subsystem. Phase 1 defines onlyAtlasNode; future subsystems extend the enum.LayoutAtlas::mark_dirty_all(&[TargetId])— broadcast/event-bus API.Filters silently by
kindandgeneration; foreign or stale entriesare ignored.
LayoutAtlas::recompute_dirty(viewport)— incremental layout pass.Stays in
Computedstate.LayoutAtlas::next_target_index()— exposes the index that the nextadded node will receive, so callers can build
TargetIds before thenode exists.
LayoutAtlas::current_generation()— current view generation,incremented by
clear()(wraps viawrapping_add).ContainerStyle::new(Option<f32>, Option<f32>)— public constructor,required because the struct is
#[non_exhaustive].Linked issue
Closes #16
Refs #1
Tasks
TargetKind::AtlasNode = 1discriminant inframe.rs.LayoutAtlas::mark_dirty_all(&[TargetId])(batch broadcast,stronger than the per-node
mark_dirty(node: AtlasNodeId)originally proposed in the issue).
LayoutAtlas::recompute_dirty(viewport).trees — and across flat vs deep shapes, since speedup depends on
tree shape.
Acceptance criteria
TargetId→one
mark_dirtycall. Verified bysignal_mutation_propagates_to_atlas_via_target_id, theend-to-end test that exercises Signal → tick → mark_dirty_all →
recompute_dirty.
on a 100-node tree. Verified in the deep-tree benchmarks (see
Performance section). The flat-tree case does not hit 5× —
this is a property of Flexbox, not a regression, and is
documented below.
mark_dirty_allandrecompute_dirtypanic inBuilding; mutations remain forbiddenin
Computed.Design decisions
Recorded here for review, all agreed before implementation:
TargetKindis#[repr(u16)]. Guarantees the in-memory layoutmatches the
TargetIdbit packing, soTargetKind::Foo as u16isa zero-cost cast usable in
const fncontexts.TargetId.generationcarries the Atlas view generation. EachLayoutAtlaskeeps acurrent_generation: u16that increments onclear()viawrapping_add. After u16::MAX clears the valuewraps; the collision probability with a stale
TargetIdsurvivingthat long is statistically negligible. Documented on
clear().Broadcast/event-bus dispatch.
mark_dirty_allaccepts the fullVec<TargetId>produced byEvaluatorTick::collect_dirty().Foreign kinds and stale generations are silently filtered. The Logic
thread can fan-out the same slice to multiple subsystems without
coordination.
Vec<AtlasNodeId>for index → node mapping. Cache-friendly,O(1)lookup, indexed bynodes_by_index.len()at insertion time.recompute_dirtyreturnsResult<(), AtlasError>. No partialgeometry diff. Callers re-run
populate_frameafter, which isO(N)over contiguous memory and measured in microseconds.
Performance
Benchmarks on release build, all measuring layout cost after marking
one real leaf dirty (not the root — that would be cache-reuse, not
invalidation).
Acceptance criterion: ~100-node tree
Meets the issue's "at least 5×" target.
Real-world UI hierarchies
Speedup plateaus around 5–6× because the chain of ancestors above a
dirty leaf must re-evaluate its flex pass; siblings of the dirty leaf
(and of every ancestor) keep their caches and are skipped.
In absolute terms, the IDE-scale 3906-node tree recomputes in 568 µs
— 3.4% of the 16.7 ms budget at 60 FPS. Chromium spends 8–15% of the
frame on layout for SPA pages of comparable complexity.
Flat trees (worst case)
Flat trees produce modest speedup because any leaf change forces the
single root's Flexbox pass to re-evaluate the full child distribution.
This is fundamental to Flexbox, not a Taffy limitation. Real UIs are
not flat — these numbers represent a pathological case that does not
occur in production (VS Code averages depth 12–18, Figma depth 8–15).
Overflow risks documented
next_target_index()truncatesusizetou32iflen() > u32::MAX(≈ 4.3 billion nodes; ~400 GB of tree storage required). Caught in
debug builds by a
debug_assert!; documented on the method.current_generationwraps atu16::MAX; documented onclear().Affects benchmarks only at depths beyond ~8000; not hit by Phase 1
benchmarks.
Checklist
cargo fmt --allpassescargo clippy --workspace --all-targets -- -D warningspassescargo test --workspacepasses (71 unit tests, +11 in this PR)CHANGELOG.mdhas an entry under[Unreleased]Notes for reviewers
The
mark_dirty_allfilter is a single integer comparison per target(no hashing, no allocation). Targets are processed in slice order and
any kind/generation mismatch short-circuits the iteration body.
recompute_dirtydelegates to Taffy's internal cache. Taffy invalidatesupward from a dirty node until it hits a container whose dimensions did
not change; this is why deep trees see large speedups (the invalidation
stops mid-tree) while flat trees see modest ones (the invalidation
always reaches the root).
End-to-end coverage is provided by
signal_mutation_propagates_to_atlas_via_target_id, which exercises thefull Evaluator → Atlas chain in a single test.
CHANGELOG checkbox N/A — pre-alpha; changelog will be introduced at v0.1.0.