Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplified ui_stack_system #9889

Merged
merged 43 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4957851
Query for nodes with zindex global separately then walk the tree fil…
ickshonpe Sep 21, 2023
d03630b
fixed irrefutable pattern,
ickshonpe Sep 21, 2023
35db93a
Moved queries around to see if performance difference.
ickshonpe Sep 21, 2023
5c2e189
Made `Zindex::Local` and `ZIndex::Global` into separate components, `…
ickshonpe Sep 21, 2023
229ad8a
Fixed tests
ickshonpe Sep 21, 2023
805cafe
Added doc comments for GlobalZIndex
ickshonpe Sep 21, 2023
c909250
Fixed ZIndex in examples
ickshonpe Sep 21, 2023
549511b
Update crates/bevy_ui/src/ui_node.rs
ickshonpe Sep 22, 2023
6af7a3b
Update crates/bevy_ui/src/ui_node.rs
ickshonpe Sep 22, 2023
b9dd7ec
Fixed the type names in the test comments
ickshonpe Sep 23, 2023
0111471
Moved the `update_stack_recursively` function outside of `ui_stack_sy…
ickshonpe Sep 23, 2023
6d71e50
Added a test to validate ordering by `ZIndex` when `GlobalZIndex` is …
ickshonpe Sep 23, 2023
a769641
Merge branch 'main' into ui-stack-system-walk
ickshonpe Sep 8, 2024
a13c2a5
update stack indices
ickshonpe Sep 9, 2024
e93b479
use radixsort to sort the rootnodes
ickshonpe Sep 9, 2024
9cebb03
use radsort to sort children
ickshonpe Sep 9, 2024
ed8412b
update nodes during recursive tree traversal
ickshonpe Sep 9, 2024
e750769
Revert "update nodes during recursive tree traversal"
ickshonpe Sep 9, 2024
faddd31
Fixed zindex for fps_overlay
ickshonpe Sep 9, 2024
e99b0bc
fixed wrong syntax
ickshonpe Sep 9, 2024
d049dcf
Fixed imports
ickshonpe Sep 9, 2024
99a4dac
Fixed `shadow_biases` example
ickshonpe Sep 19, 2024
7e754d9
Walk the tree using a loop and stack.
ickshonpe Sep 20, 2024
8804004
clean up
ickshonpe Sep 20, 2024
eae57fe
Merge branch 'main' into ui-stack-system-walk
ickshonpe Sep 20, 2024
339eca5
Merge branch 'main' into ui-stack-system-walk
ickshonpe Sep 20, 2024
f25bf4a
updated `fps_overlay` comments
ickshonpe Sep 20, 2024
c1fba57
Simplified root list construction.
ickshonpe Sep 20, 2024
e1b052b
Improved comment
ickshonpe Sep 20, 2024
da66207
fixed doc test
ickshonpe Sep 20, 2024
8c96e19
Merge branch 'main' into ui-stack-system-walk
ickshonpe Sep 30, 2024
1623770
cache root nodes
ickshonpe Sep 30, 2024
9e9839a
Remove radix sort, cache `z_children`
ickshonpe Sep 30, 2024
3df9c3a
`z_children` vec, sort the traversal stack instead
ickshonpe Sep 30, 2024
3b57640
Removed unused radsort dependency
ickshonpe Sep 30, 2024
92dfd81
walk the stack recursively
ickshonpe Sep 30, 2024
1290d0c
removed unused
ickshonpe Sep 30, 2024
b0c27ac
try smallvec
ickshonpe Sep 30, 2024
158c9c4
removed traversal stack
ickshonpe Sep 30, 2024
99d9a28
Don't need to use `Reverse` for recursive version.
ickshonpe Sep 30, 2024
6a4734e
cache vecs used to contain the sorted children
ickshonpe Sep 30, 2024
36504a9
allow `too_many_arguments` for `ui_stack_system`
ickshonpe Sep 30, 2024
a5e01e4
Documentation fixes
alice-i-cecile Sep 30, 2024
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
243 changes: 144 additions & 99 deletions crates/bevy_ui/src/stack.rs
ickshonpe marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use bevy_ecs::prelude::*;
use bevy_hierarchy::prelude::*;

use crate::{Node, ZIndex};
use crate::{GlobalZIndex, Node, ZIndex};

/// The current UI stack, which contains all UI nodes ordered by their depth (back-to-front).
///
Expand All @@ -15,102 +15,78 @@ pub struct UiStack {
pub uinodes: Vec<Entity>,
}

#[derive(Default)]
struct StackingContext {
pub entries: Vec<StackingContextEntry>,
}
fn update_uistack_recursively(
entity: Entity,
uinodes: &mut Vec<Entity>,
children_query: &Query<&Children>,
zindex_query: &Query<Option<&ZIndex>, (With<Node>, Without<GlobalZIndex>)>,
) {
uinodes.push(entity);

struct StackingContextEntry {
pub z_index: i32,
pub entity: Entity,
pub stack: StackingContext,
if let Ok(children) = children_query.get(entity) {
let mut z_children: Vec<(Entity, i32)> = children
.iter()
.filter_map(|entity| {
zindex_query
.get(*entity)
.ok()
.map(|zindex| (*entity, zindex.map(|zindex| zindex.0).unwrap_or(0)))
})
.collect();
z_children.sort_by_key(|k| k.1);
for (child_id, _) in z_children {
update_uistack_recursively(child_id, uinodes, children_query, zindex_query);
}
}
}

/// Generates the render stack for UI nodes.
///
/// First generate a UI node tree (`StackingContext`) based on z-index.
/// Then flatten that tree into back-to-front ordered `UiStack`.
pub fn ui_stack_system(
mut ui_stack: ResMut<UiStack>,
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
zindex_query: Query<&ZIndex, With<Node>>,
root_node_query: Query<
(Entity, Option<&GlobalZIndex>, Option<&ZIndex>),
(With<Node>, Without<Parent>),
>,
zindex_global_node_query: Query<
(Entity, &GlobalZIndex, Option<&ZIndex>),
(With<Node>, With<Parent>),
>,
children_query: Query<&Children>,
zindex_query: Query<Option<&ZIndex>, (With<Node>, Without<GlobalZIndex>)>,
) {
// Generate `StackingContext` tree
let mut global_context = StackingContext::default();
let mut total_entry_count: usize = 0;

for entity in &root_node_query {
insert_context_hierarchy(
&zindex_query,
&children_query,
entity,
&mut global_context,
None,
&mut total_entry_count,
);
}

// Flatten `StackingContext` into `UiStack`
ui_stack.uinodes.clear();
ui_stack.uinodes.reserve(total_entry_count);
fill_stack_recursively(&mut ui_stack.uinodes, &mut global_context);
}

/// Generate z-index based UI node tree
fn insert_context_hierarchy(
zindex_query: &Query<&ZIndex, With<Node>>,
children_query: &Query<&Children>,
entity: Entity,
global_context: &mut StackingContext,
parent_context: Option<&mut StackingContext>,
total_entry_count: &mut usize,
) {
let mut new_context = StackingContext::default();
let uinodes = &mut ui_stack.uinodes;

if let Ok(children) = children_query.get(entity) {
// Reserve space for all children. In practice, some may not get pushed since
// nodes with `ZIndex::Global` are pushed to the global (root) context.
new_context.entries.reserve_exact(children.len());

for entity in children {
insert_context_hierarchy(
zindex_query,
children_query,
*entity,
global_context,
Some(&mut new_context),
total_entry_count,
);
}
}
let global_nodes = zindex_global_node_query
.iter()
.map(|(id, global_zindex, maybe_zindex)| {
(
id,
(
global_zindex.0,
maybe_zindex.map(|zindex| zindex.0).unwrap_or(0),
),
)
});

// The node will be added either to global/parent based on its z-index type: global/local.
let z_index = zindex_query.get(entity).unwrap_or(&ZIndex::Local(0));
let (entity_context, z_index) = match z_index {
ZIndex::Local(value) => (parent_context.unwrap_or(global_context), *value),
ZIndex::Global(value) => (global_context, *value),
};
let mut root_nodes: Vec<_> = root_node_query
.iter()
.map(|(root_id, maybe_global_zindex, maybe_zindex)| {
(
root_id,
(
maybe_global_zindex.map(|zindex| zindex.0).unwrap_or(0),
maybe_zindex.map(|zindex| zindex.0).unwrap_or(0),
),
)
})
.chain(global_nodes)
ickshonpe marked this conversation as resolved.
Show resolved Hide resolved
.collect();

*total_entry_count += 1;
entity_context.entries.push(StackingContextEntry {
z_index,
entity,
stack: new_context,
});
}
root_nodes.sort_by_key(|(_, z)| *z);
ickshonpe marked this conversation as resolved.
Show resolved Hide resolved
ickshonpe marked this conversation as resolved.
Show resolved Hide resolved

/// Flatten `StackingContext` (z-index based UI node tree) into back-to-front entities list
fn fill_stack_recursively(result: &mut Vec<Entity>, stack: &mut StackingContext) {
// Sort entries by ascending z_index, while ensuring that siblings
// with the same local z_index will keep their ordering. This results
// in `back-to-front` ordering, low z_index = back; high z_index = front.
stack.entries.sort_by_key(|e| e.z_index);

for entry in &mut stack.entries {
// Parent node renders before/behind child nodes
result.push(entry.entity);
fill_stack_recursively(result, &mut entry.stack);
for (entity, _) in root_nodes {
update_uistack_recursively(entity, uinodes, &children_query, &zindex_query);
}
}

Expand All @@ -124,15 +100,35 @@ mod tests {
};
use bevy_hierarchy::BuildChildren;

use crate::{Node, UiStack, ZIndex};
use crate::{GlobalZIndex, Node, UiStack, ZIndex};

use super::ui_stack_system;

#[derive(Component, PartialEq, Debug, Clone)]
struct Label(&'static str);

fn node_with_zindex(name: &'static str, z_index: ZIndex) -> (Label, Node, ZIndex) {
(Label(name), Node::default(), z_index)
fn node_with_global_and_local_zindex(
name: &'static str,
global_zindex: i32,
local_zindex: i32,
) -> (Label, Node, GlobalZIndex, ZIndex) {
(
Label(name),
Node::default(),
GlobalZIndex(global_zindex),
ZIndex(local_zindex),
)
}

fn node_with_global_zindex(
name: &'static str,
global_zindex: i32,
) -> (Label, Node, GlobalZIndex) {
(Label(name), Node::default(), GlobalZIndex(global_zindex))
}

fn node_with_zindex(name: &'static str, zindex: i32) -> (Label, Node, ZIndex) {
(Label(name), Node::default(), ZIndex(zindex))
}

fn node_without_zindex(name: &'static str) -> (Label, Node) {
Expand All @@ -152,24 +148,24 @@ mod tests {

let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, &world);
commands.spawn(node_with_zindex("0", ZIndex::Global(2)));
commands.spawn(node_with_global_zindex("0", 2));

commands
.spawn(node_with_zindex("1", ZIndex::Local(1)))
.spawn(node_with_zindex("1", 1))
.with_children(|parent| {
parent
.spawn(node_without_zindex("1-0"))
.with_children(|parent| {
parent.spawn(node_without_zindex("1-0-0"));
parent.spawn(node_without_zindex("1-0-1"));
parent.spawn(node_with_zindex("1-0-2", ZIndex::Local(-1)));
parent.spawn(node_with_zindex("1-0-2", -1));
});
parent.spawn(node_without_zindex("1-1"));
parent
.spawn(node_with_zindex("1-2", ZIndex::Global(-1)))
.spawn(node_with_global_zindex("1-2", -1))
.with_children(|parent| {
parent.spawn(node_without_zindex("1-2-0"));
parent.spawn(node_with_zindex("1-2-1", ZIndex::Global(-3)));
parent.spawn(node_with_global_zindex("1-2-1", -3));
parent
.spawn(node_without_zindex("1-2-2"))
.with_children(|_| ());
Expand All @@ -191,7 +187,7 @@ mod tests {
});
});

commands.spawn(node_with_zindex("3", ZIndex::Global(-2)));
commands.spawn(node_with_global_zindex("3", -2));

queue.apply(&mut world);

Expand All @@ -207,25 +203,74 @@ mod tests {
.map(|entity| query.get(&world, *entity).unwrap().clone())
.collect::<Vec<_>>();
let expected_result = vec![
(Label("1-2-1")), // ZIndex::Global(-3)
(Label("3")), // ZIndex::Global(-2)
(Label("1-2")), // ZIndex::Global(-1)
(Label("1-2-1")), // GlobalZIndex(-3)
(Label("3")), // GlobalZIndex(-2)
(Label("1-2")), // GlobalZIndex(-1)
(Label("1-2-0")),
(Label("1-2-2")),
(Label("1-2-3")),
(Label("2")),
(Label("2-0")),
(Label("2-1")),
(Label("2-1-0")),
(Label("1")), // ZIndex::Local(1)
(Label("1")), // ZIndex(1)
(Label("1-0")),
(Label("1-0-2")), // ZIndex::Local(-1)
(Label("1-0-2")), // ZIndex(-1)
(Label("1-0-0")),
(Label("1-0-1")),
(Label("1-1")),
(Label("1-3")),
(Label("0")), // ZIndex::Global(2)
(Label("0")), // GlobalZIndex(2)
];
assert_eq!(actual_result, expected_result);
}

#[test]
fn test_with_equal_global_zindex_zindex_decides_order() {
let mut world = World::default();
world.init_resource::<UiStack>();

let mut queue = CommandQueue::default();
let mut commands = Commands::new(&mut queue, &world);
commands.spawn(node_with_global_and_local_zindex("0", -1, 1));
commands.spawn(node_with_global_and_local_zindex("1", -1, 2));
commands.spawn(node_with_global_and_local_zindex("2", 1, 3));
commands.spawn(node_with_global_and_local_zindex("3", 1, -3));
commands
.spawn(node_without_zindex("4"))
.with_children(|builder| {
builder.spawn(node_with_global_and_local_zindex("5", 0, -1));
builder.spawn(node_with_global_and_local_zindex("6", 0, 1));
builder.spawn(node_with_global_and_local_zindex("7", -1, -1));
builder.spawn(node_with_global_zindex("8", 1));
});

queue.apply(&mut world);

let mut schedule = Schedule::default();
schedule.add_systems(ui_stack_system);
schedule.run(&mut world);

let mut query = world.query::<&Label>();
let ui_stack = world.resource::<UiStack>();
let actual_result = ui_stack
.uinodes
.iter()
.map(|entity| query.get(&world, *entity).unwrap().clone())
.collect::<Vec<_>>();

let expected_result = vec![
(Label("7")),
(Label("0")),
(Label("1")),
(Label("5")),
(Label("4")),
(Label("6")),
(Label("3")),
(Label("8")),
(Label("2")),
];

assert_eq!(actual_result, expected_result);
}
}
30 changes: 12 additions & 18 deletions crates/bevy_ui/src/ui_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1505,26 +1505,20 @@ pub struct CalculatedClip {
/// appear in the UI hierarchy. In such a case, the last node to be added to its parent
/// will appear in front of this parent's other children.
///
/// Internally, nodes with a global z-index share the stacking context of root UI nodes
/// (nodes that have no parent). Because of this, there is no difference between using
/// [`ZIndex::Local(n)`] and [`ZIndex::Global(n)`] for root nodes.
///
/// Nodes without this component will be treated as if they had a value of [`ZIndex::Local(0)`].
#[derive(Component, Copy, Clone, Debug, Reflect)]
/// Nodes without this component will be treated as if they had a value of [`ZIndex(0)`].
#[derive(Component, Copy, Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub enum ZIndex {
/// Indicates the order in which this node should be rendered relative to its siblings.
Local(i32),
/// Indicates the order in which this node should be rendered relative to root nodes and
/// all other nodes that have a global z-index.
Global(i32),
}
pub struct ZIndex(pub i32);

impl Default for ZIndex {
fn default() -> Self {
Self::Local(0)
}
}
/// `GlobalZIndex` allows a node anywhere in the UI hierarchy to escape the implicit draw ordering of the UI's layout tree and
/// be rendered above above or below other UI nodes.
/// Nodes with a `GlobalZIndex` of greater than 0 will be drawn on top of nodes without a `GlobalZIndex` or nodes with a lower `GlobalZIndex`.
/// Nodes with a `GlobalZIndex` of less than 0 will be drawn below nodes without a `GlobalZIndex` or nodes with a greater `GlobalZIndex`.
///
/// If two Nodes have the same `GlobalZIndex`, the node with the greater [`ZIndex`] will be drawn on top.
#[derive(Component, Copy, Clone, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct GlobalZIndex(pub i32);

#[cfg(test)]
mod tests {
Expand Down
18 changes: 10 additions & 8 deletions examples/stress_tests/bevymark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,16 +219,18 @@ fn setup(

commands.spawn(Camera2dBundle::default());
commands
.spawn(NodeBundle {
style: Style {
position_type: PositionType::Absolute,
padding: UiRect::all(Val::Px(5.0)),
.spawn((
NodeBundle {
style: Style {
position_type: PositionType::Absolute,
padding: UiRect::all(Val::Px(5.0)),
..default()
},
background_color: Color::BLACK.with_a(0.75).into(),
..default()
},
z_index: ZIndex::Global(i32::MAX),
background_color: Color::BLACK.with_a(0.75).into(),
..default()
})
GlobalZIndex(i32::MAX),
))
.with_children(|c| {
c.spawn((
TextBundle::from_sections([
Expand Down
Loading