mirror of
https://github.com/bevyengine/bevy
synced 2024-12-19 17:43:07 +00:00
535250c088
# Objective - Reduce allocations on the UI hot path. ## Solution - Cache buffers used by the `ui_stack_system`. ## Follow-Up - `sort_by_key` is potentially-allocating. It might be worthwhile to include the child index as part of the sort-key and use unstable sort.
267 lines
8.8 KiB
Rust
267 lines
8.8 KiB
Rust
//! This module contains the systems that update the stored UI nodes stack
|
|
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_hierarchy::prelude::*;
|
|
|
|
use crate::{Node, ZIndex};
|
|
|
|
/// The current UI stack, which contains all UI nodes ordered by their depth (back-to-front).
|
|
///
|
|
/// The first entry is the furthest node from the camera and is the first one to get rendered
|
|
/// while the last entry is the first node to receive interactions.
|
|
#[derive(Debug, Resource, Default)]
|
|
pub struct UiStack {
|
|
/// List of UI nodes ordered from back-to-front
|
|
pub uinodes: Vec<Entity>,
|
|
}
|
|
|
|
/// Caches stacking context buffers for use in [`ui_stack_system`].
|
|
#[derive(Default)]
|
|
pub(crate) struct StackingContextCache {
|
|
inner: Vec<StackingContext>,
|
|
}
|
|
|
|
impl StackingContextCache {
|
|
fn pop(&mut self) -> StackingContext {
|
|
self.inner.pop().unwrap_or_default()
|
|
}
|
|
|
|
fn push(&mut self, mut context: StackingContext) {
|
|
for entry in context.entries.drain(..) {
|
|
self.push(entry.stack);
|
|
}
|
|
self.inner.push(context);
|
|
}
|
|
}
|
|
|
|
#[derive(Default)]
|
|
struct StackingContext {
|
|
entries: Vec<StackingContextEntry>,
|
|
}
|
|
|
|
struct StackingContextEntry {
|
|
z_index: i32,
|
|
entity: Entity,
|
|
stack: StackingContext,
|
|
}
|
|
|
|
/// 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(crate) fn ui_stack_system(
|
|
mut cache: Local<StackingContextCache>,
|
|
mut ui_stack: ResMut<UiStack>,
|
|
root_node_query: Query<Entity, (With<Node>, Without<Parent>)>,
|
|
zindex_query: Query<&ZIndex, With<Node>>,
|
|
children_query: Query<&Children>,
|
|
mut update_query: Query<&mut Node>,
|
|
) {
|
|
// Generate `StackingContext` tree
|
|
let mut global_context = cache.pop();
|
|
let mut total_entry_count: usize = 0;
|
|
|
|
for entity in &root_node_query {
|
|
insert_context_hierarchy(
|
|
&mut cache,
|
|
&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 cache, &mut ui_stack.uinodes, &mut global_context);
|
|
cache.push(global_context);
|
|
|
|
for (i, entity) in ui_stack.uinodes.iter().enumerate() {
|
|
if let Ok(mut node) = update_query.get_mut(*entity) {
|
|
node.bypass_change_detection().stack_index = i as u32;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate z-index based UI node tree
|
|
fn insert_context_hierarchy(
|
|
cache: &mut StackingContextCache,
|
|
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 = cache.pop();
|
|
|
|
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(
|
|
cache,
|
|
zindex_query,
|
|
children_query,
|
|
*entity,
|
|
global_context,
|
|
Some(&mut new_context),
|
|
total_entry_count,
|
|
);
|
|
}
|
|
}
|
|
|
|
// 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),
|
|
};
|
|
|
|
*total_entry_count += 1;
|
|
entity_context.entries.push(StackingContextEntry {
|
|
z_index,
|
|
entity,
|
|
stack: new_context,
|
|
});
|
|
}
|
|
|
|
/// Flatten `StackingContext` (z-index based UI node tree) into back-to-front entities list
|
|
fn fill_stack_recursively(
|
|
cache: &mut StackingContextCache,
|
|
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 mut entry in stack.entries.drain(..) {
|
|
// Parent node renders before/behind child nodes
|
|
result.push(entry.entity);
|
|
fill_stack_recursively(cache, result, &mut entry.stack);
|
|
cache.push(entry.stack);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use bevy_ecs::{
|
|
component::Component,
|
|
schedule::Schedule,
|
|
system::Commands,
|
|
world::{CommandQueue, World},
|
|
};
|
|
use bevy_hierarchy::BuildChildren;
|
|
|
|
use crate::{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_without_zindex(name: &'static str) -> (Label, Node) {
|
|
(Label(name), Node::default())
|
|
}
|
|
|
|
/// Tests the UI Stack system.
|
|
///
|
|
/// This tests for siblings default ordering according to their insertion order, but it
|
|
/// can't test the same thing for UI roots. UI roots having no parents, they do not have
|
|
/// a stable ordering that we can test against. If we test it, it may pass now and start
|
|
/// failing randomly in the future because of some unrelated `bevy_ecs` change.
|
|
#[test]
|
|
fn test_ui_stack_system() {
|
|
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_zindex("0", ZIndex::Global(2)));
|
|
|
|
commands
|
|
.spawn(node_with_zindex("1", ZIndex::Local(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_without_zindex("1-1"));
|
|
parent
|
|
.spawn(node_with_zindex("1-2", ZIndex::Global(-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_without_zindex("1-2-2"))
|
|
.with_children(|_| ());
|
|
parent.spawn(node_without_zindex("1-2-3"));
|
|
});
|
|
parent.spawn(node_without_zindex("1-3"));
|
|
});
|
|
|
|
commands
|
|
.spawn(node_without_zindex("2"))
|
|
.with_children(|parent| {
|
|
parent
|
|
.spawn(node_without_zindex("2-0"))
|
|
.with_children(|_parent| ());
|
|
parent
|
|
.spawn(node_without_zindex("2-1"))
|
|
.with_children(|parent| {
|
|
parent.spawn(node_without_zindex("2-1-0"));
|
|
});
|
|
});
|
|
|
|
commands.spawn(node_with_zindex("3", ZIndex::Global(-2)));
|
|
|
|
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("1-2-1"), // ZIndex::Global(-3)
|
|
Label("3"), // ZIndex::Global(-2)
|
|
Label("1-2"), // ZIndex::Global(-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-0"),
|
|
Label("1-0-2"), // ZIndex::Local(-1)
|
|
Label("1-0-0"),
|
|
Label("1-0-1"),
|
|
Label("1-1"),
|
|
Label("1-3"),
|
|
Label("0"), // ZIndex::Global(2)
|
|
];
|
|
assert_eq!(actual_result, expected_result);
|
|
}
|
|
}
|