Repository navigation
Conversation
Updated layout to follow new ghost rules.
…follow match the schedule.
…eing implemented as a system.
…st non-ghost ancestor, not grandparent.
…well in its `clear` method.
This branch has not been deployed
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.
Objective
Perform UI layout incrementally for better performance.
Other sub-objectives:
bevy_uistores aTaffyTreein theUiSurfaceresource that has to be kept synchronised with the bevy ecs UI node hierarchy. This has been a long time source of bugs and isn't very efficient. Instead, an adapter implementing Taffy's tree traits (fromtaffy::tree::traits) can give Taffy direct access to the UI node component data.Taffy has had calc support for a while, but it's not usuable through
TaffyTree. The adapter will allow us to add aVal::Calcvariant.ui_layout_systemhas too many responsibilities:Nodecomponent data with Taffy'sStyledata.TaffyTree. This includesGhostNodes andFixedNodes which generate all sorts of edge cases that need to be carefully handled.TaffyTree.ComputedNode's from the Taffy layout.It should be split up into multiple simpler systems that can be understood, tested and profiled in isolation.
Interactions between certain components, in particular
GhostNode,FixedNode,OverrideClipandUiTransform, haven't always been well defined or documented. Incrementality forces us to come up with a precise answer for every case.Fixes #25150
Solution
Apologies for the massive size of this PR. The number of line changes is inflated by the large number of trait impls, tests, and other boilerplate, and also the deletion of the
ui_surfaceandexperimentalmodules. But even so, it's still huge.I chose to focus more on correctness over super aggressive optimisation for this PR. Any hierarchy changes to a
Nodeentity trigger a full walk over every UI node from each UI root. Geometric updates cascade through all descendants, which is overkill for changes that can only affect the node itself. The UI roots list has no change detection, and is rebuilt every frame byupdate_ui_roots. The queries forui_layout_systemandmark_dirty_ui_treesoverlap somewhat, they use the results differently, but there is some redundancy and room for consolidation there.There are four main dirty flags used to track changes:
UiTreeDirtymarker component that uses change detection to indicate that a layout input changed for aNodeentity or one of its descendants.ComputedLayoutcomponent:self_dirty: A change to a local input, either for Taffy (such asNodeorContentSize) or geometry only (such asUiTransformorOutLine).subtree_dirty: The entire subtree needs a geometry update.layout_dirty: The output layout returned from Taffy changed for this node.I'd recommend for any reviewers to concentrate on these four functions, in order:
bevy_ui::layout::mark_dirty_ui_treesbevy_ui::layout::ui_layout_systembevy_ui::layout::layout_tree::sync_runtime_layout_treebevy_ui::layout::update_uinode_geometry_recursiveOtherwise It'd be good to look at existing projects built on top of
bevy_ui, especially bevy_reactor and bevy_immediate, and make sure they still work correctly. Also look at the more complex UI examples,testbed_ui,feathers_gallery,minesandtestbed_full_uiare probably the most likely to reveal any regressions.Also be aware that some of the notes below might be out of date. This PR has gone through a lot of revisions, and I've started to lose track of everything that was changed myself.
UiSurfaceandui_surfacehave been removed.The
UiSurfaceresource and theui_surfacemodule have been removed.Instead of maintaining a separate
TaffyTreein theUiSurfaceresource,bevy_uinow has a newUiLayoutTreeadapter that allows Taffy to access the ECS component data directly.The Taffy
NodeId<->Entitybimaps are goneSince
NodeIds are justu64s, we can generate them from eachNode's entity id usingEntity::to_bitsas needed.Entity::from_bitsis used on theNodeIdto map back.entity.to_bits()can't be zero, so aNodeIdof zero is used to represent viewport nodes.UiRootsresourceAll the different types of root UI nodes are collected into a resource
UiRootsby theupdate_ui_rootsinUiSystems::Prepare. UI root discovery is quite complicated now, repeating the logic in each system is too fragile.SystemParamscould have been used instead, but that makes it harder to enforce a stable ordering.UiStackandpropagate_ui_target_camerasdon't useUiRootsas they don't need any special rules for dealing withGhostNodes andFixedNodes.New
layout_treemoduleThe
bevy_ui::layoutmodule has a new submodulelayout_treecontaining the majority of the new incremental layout implementation and taffy communication layer.Measure func changes
Previously,
NodeMeasures were moved out of theContentSizecomponents during layout. Now they are accessed directly by querying forRef<ContentSize>. As a result, the receiver for theMeasuretrait and its implementation forNodeMeasureno longer needs to be mutable, instead&selfis sufficient.GhostNodereimplementationGhostNodes now requireNodeand behave more like regular UI nodes. During layout aGhostNodeis replaced by its children recursively, so that its nearest non-ghost descendants become children of its nearest non-ghost ancestor.Each
GhostNodehas a corresponding taffy node now, but it's zero-sized and disconnected singleton node. When updated theirComputedNodeis set to zero size (I had planned to give aGhostNodebounds that encompasses its children, but left this out for now, it seems useful but it would require a new mechanism to collect the children's bounds).UiTransformis propagated throughGhostNodes normally except that for percentage translations, the closest non-ghost ancestor's base size is used otherwise, sinceGhostNodes always have zero-size, percentage translations would always resolve to zero.GhostNodes requiringNodemakes traversal much simpler, we only need to consider ghost nodes duringComputedNodeupdates and in layout when they are replaced. TheUiChildrenandUiRootNodessystem params are no longer needed. Thebevy_ui::layout::experimentalmodule and theghost_hierarchysubmodule have been removed.The "ghost_nodes" feature gate has been removed.
GhostNodes are always enabled now. Profiling indicated that even with the previous system params implementation, it would be cheaper to have theGhostNodefeature enabled all the time, even when they aren't used.FixedNodechangesA
FixedNodecreates a new layout context, its ancestors layout is not affected by theFixedNodeor its descendants. So inmark_dirty_ui_treesthe upwards walk to set the dirty subtree flags stops at anyFixedNode.A
GhostNodecannot also be aFixedNode. If a node has bothFixedNodeandGhostNodecomponents,FixedNodeis ignored.There were some
OverrideClipchanges which were split off into a separate PR and have already been merged: #25613Taffy
Taffy'sTaffyTreeis no longer used, instead implemented all thetaffy::tree::traitson a new structUiLayoutTreethat acts as an adaptor allowingTaffydirect access to the necessary component data.Each UI entity has new components
TaffyStyleandComputedLayout.TaffyStylecontains the input layout data, and is updated fromNodeon changes by thesync_taffy_styles_with_nodessystem.ComputedLayoutholds the cached calculations, resolved children, dirty flags and data and output layout geometry for each node.These changes also allow us to add a
Calcvariant toVal. I implemented a basic version already, just waiting for this to get merged.ui_layout_systemchangesCalls
compute_layoutfor each UI root to update itsTaffylayout data.No longer responsible for component change dectection or updating
ComputedNodes.After layout updated, it clears any UI nodes that became unreachable since the previous update.
New
update_computed_nodessystemNew system that takes over responsibility for updating
ComputedNodes fromui_layout_system.Previously the entire UI hierarchy was walked to update every
ComputedNode, now only those nodes marked dirty are updated.GhostNodes are also updated here now, their size is set to zero.New
update_border_radiussystemResolvedBorderRadiusis updated after layout in a separate system. Profiling lead change.mark_dirty_ui_treesandUiTreeDirtyUiTreeDirtyis a new marker component that is set change detection to indicate that some input for layout changed for a node or one of its descendants.mark_dirty_ui_treeswatches for all UI component changes, insertions and removals that will require a layout update. For each dirtyNodeentity and its direct ancestors it sets theUiTreeDirtycomponent changed.update_clippingThe clipping bounds are updated incrementally.
Since
GhostNodes areNodes, I had to add some special casing for them. Clipping is propagated downwards through ghosts, but theoverflowfield on the ghost's ownNodecomponent is ignored.accessibilityThe changes here shouldn't introduce any new problems, unless someone does something weird with
GhostNodes, maybe.UiStackUiStackupdates are still immediate. The only change is that it no longer uses theUiChildrentraversal params.GhostNodes are full UI nodes now and have aComputedStackIndexso they are visted during the UI stack walk and this does this does affect the visual ordering of nodes. Consider:On main, the
GhostNodeis skipped and its children hoisted, so the render order would blue, green, red.But with this PR the
GhostNodeis a node, so its children are z sorted only relative to each other. The render order ends up blue, red, green instead. This isn't ideal, the blue, green, red ordering is the correct one. I left it to be fixed in a follow up as it would need a rewrite of the system and possibly changes to theComputedStackIndexcomponent as well.Future work
UiTransformshould be updated separately.-There is still full tree walk on structual changes, this could be done incrementally.
ComputedLayout+ComputedNodecould be consolidated.ui_layout_systemwalks up through all the ancestors of eachFixedNodeevery frame checking that each is a valid UINode. Should only have to do this on changes.Taffy's tree traversal traits, we can addVal::Calcsupport once this is merged.accessibilitymodule.ComputedStackIndex. This affects the stack ordering of nodes, and needs to be fixed in a follow up.UiSystemsshould be split up further with a separateSyncset betweenContentandLayout, where the pre-layout synchronisation is handled and the dirty flags are set.AI use disclosure
A review was done by Claude, which picked up the
tree_changed_querymistake, and an inconsistancy with root ghost nodes and transforms.Testing
Includes a lot of new regression tests now.
layout/mod.rswas getting too large, so moved them into alayout/tests.rsfile.The more complex UI examples such as
testbed_ui,feathers_gallery,minesandtestbed_full_uiare most likely to show up any regressions.The examples are unchanged, so screenshot CI should be passing.
Benchmarks
There are UI layout benchmarks now in
benches, you can run them with:You can perform a comparison by first running the UI benchmarks on main and saving a baseline:
Then switch to this PR and run:
I saw a ~75% improvement with the static layouts and a ~10% regression on full updates.
Showcase
These were run about a hundred commits ago, but nothing should have changed substantially 🤞 :
FPS comparison doesn't show much because pipelined rendering:

But just
PostUpdate:Just

PostUpdateagain:It's even a little faster for complete rebuilds.