Extract UI nodes into a Vec (#17618)

# Objective

Extract UI nodes into a `Vec` instead of an `EntityHashMap`.

## Solution

Extract UI nodes into a `Vec` instead of an `EntityHashMap`.
Store an index into the `Vec` in each transparent UI item.
Compare both the index and render entity in prepare so there aren't any
collisions.

## Showcase

Yellow this PR, Red main

```
cargo run --example many_buttons --release --features trace_tracy
```

`extract_uinode_background_colors`
<img width="448" alt="extract_uinode_background_colors"
src="https://github.com/user-attachments/assets/09c0f434-ab4f-4c0f-956a-cf31e9060061"
/>

`extract_uinode_images`
<img width="587" alt="extract_uinode_images"
src="https://github.com/user-attachments/assets/43246d7f-d22c-46d0-9a07-7e13d5379f56"
/>

`prepare_uinodes`
<img width="441" alt="prepare_uinodes_vec"
src="https://github.com/user-attachments/assets/cc9a7eac-60e9-42fa-8093-bce833a1c153"
/>
This commit is contained in:
ickshonpe 2025-01-30 23:25:07 +00:00 committed by GitHub
parent 59697f9ccc
commit ba1b0092e5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 207 additions and 210 deletions

View File

@ -12,7 +12,6 @@ use bevy_color::{Alpha, ColorToComponents, LinearRgba};
use bevy_ecs::prelude::*;
use bevy_ecs::{
prelude::Component,
storage::SparseSet,
system::{
lifetimeless::{Read, SRes},
*,
@ -225,12 +224,13 @@ pub struct ExtractedBoxShadow {
pub blur_radius: f32,
pub size: Vec2,
pub main_entity: MainEntity,
pub render_entity: Entity,
}
/// List of extracted shadows to be sorted and queued for rendering
#[derive(Resource, Default)]
pub struct ExtractedBoxShadows {
pub box_shadows: SparseSet<Entity, ExtractedBoxShadow>,
pub box_shadows: Vec<ExtractedBoxShadow>,
}
pub fn extract_shadows(
@ -311,12 +311,10 @@ pub fn extract_shadows(
bottom_right: uinode.border_radius.bottom_right * spread_ratio,
};
extracted_box_shadows.box_shadows.insert(
commands.spawn(TemporaryRenderEntity).id(),
ExtractedBoxShadow {
extracted_box_shadows.box_shadows.push(ExtractedBoxShadow {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix()
* Mat4::from_translation(offset.extend(0.)),
transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)),
color: drop_shadow.color.into(),
bounds: shadow_size + 6. * blur_radius,
clip: clip.map(|clip| clip.clip),
@ -325,8 +323,7 @@ pub fn extract_shadows(
blur_radius,
size: shadow_size,
main_entity: entity.into(),
},
);
});
}
}
}
@ -346,7 +343,8 @@ pub fn queue_shadows(
draw_functions: Res<DrawFunctions<TransparentUi>>,
) {
let draw_function = draw_functions.read().id::<DrawBoxShadows>();
for (entity, extracted_shadow) in extracted_box_shadows.box_shadows.iter() {
for (index, extracted_shadow) in extracted_box_shadows.box_shadows.iter().enumerate() {
let entity = extracted_shadow.render_entity;
let Ok((default_camera_view, shadow_samples)) =
render_views.get_mut(extracted_shadow.extracted_camera_entity)
else {
@ -374,13 +372,14 @@ pub fn queue_shadows(
transparent_phase.add(TransparentUi {
draw_function,
pipeline,
entity: (*entity, extracted_shadow.main_entity),
entity: (entity, extracted_shadow.main_entity),
sort_key: (
FloatOrd(extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW),
entity.index(),
),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
index,
indexed: true,
});
}
@ -417,11 +416,13 @@ pub fn prepare_shadows(
let mut indices_index = 0;
for ui_phase in phases.values_mut() {
let mut item_index = 0;
while item_index < ui_phase.items.len() {
for item_index in 0..ui_phase.items.len() {
let item = &mut ui_phase.items[item_index];
if let Some(box_shadow) = extracted_shadows.box_shadows.get(item.entity()) {
if let Some(box_shadow) = extracted_shadows
.box_shadows
.get(item.index)
.filter(|n| item.entity() == n.render_entity)
{
let rect_size = box_shadow.bounds.extend(1.0);
// Specify the corners of the node
@ -532,7 +533,6 @@ pub fn prepare_shadows(
*ui_phase.items[item_index].batch_range_mut() =
item_index as u32..item_index as u32 + 1;
}
item_index += 1;
}
}
ui_meta.vertices.write_buffer(&render_device, &render_queue);

View File

@ -85,9 +85,8 @@ pub fn extract_debug_overlay(
};
// Extract a border box to display an outline for every UI Node in the layout
extracted_uinodes.uinodes.insert(
commands.spawn(TemporaryRenderEntity).id(),
ExtractedUiNode {
extracted_uinodes.uinodes.push(ExtractedUiNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
// Add a large number to the UI node's stack index so that the overlay is always drawn on top
stack_index: uinode.stack_index + u32::MAX / 2,
color: Hsla::sequential_dispersed(entity.index()).into(),
@ -105,14 +104,11 @@ pub fn extract_debug_overlay(
transform: transform.compute_matrix(),
flip_x: false,
flip_y: false,
border: BorderRect::all(
debug_options.line_width / uinode.inverse_scale_factor(),
),
border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()),
border_radius: uinode.border_radius(),
node_type: NodeType::Border,
},
main_entity: entity.into(),
},
);
});
}
}

View File

@ -18,7 +18,6 @@ use bevy_color::{Alpha, ColorToComponents, LinearRgba};
use bevy_core_pipeline::core_2d::graph::{Core2d, Node2d};
use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d};
use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
use bevy_ecs::entity::hash_map::EntityHashMap;
use bevy_ecs::prelude::*;
use bevy_ecs::system::SystemParam;
use bevy_image::prelude::*;
@ -203,6 +202,7 @@ pub struct ExtractedUiNode {
pub extracted_camera_entity: Entity,
pub item: ExtractedUiItem,
pub main_entity: MainEntity,
pub render_entity: Entity,
}
/// The type of UI node.
@ -241,7 +241,7 @@ pub struct ExtractedGlyph {
#[derive(Resource, Default)]
pub struct ExtractedUiNodes {
pub uinodes: EntityHashMap<ExtractedUiNode>,
pub uinodes: Vec<ExtractedUiNode>,
pub glyphs: Vec<ExtractedGlyph>,
}
@ -358,9 +358,8 @@ pub fn extract_uinode_background_colors(
continue;
};
extracted_uinodes.uinodes.insert(
commands.spawn(TemporaryRenderEntity).id(),
ExtractedUiNode {
extracted_uinodes.uinodes.push(ExtractedUiNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
color: background_color.0.into(),
rect: Rect {
@ -380,8 +379,7 @@ pub fn extract_uinode_background_colors(
node_type: NodeType::Rect,
},
main_entity: entity.into(),
},
);
});
}
}
@ -447,9 +445,8 @@ pub fn extract_uinode_images(
None
};
extracted_uinodes.uinodes.insert(
commands.spawn(TemporaryRenderEntity).id(),
ExtractedUiNode {
extracted_uinodes.uinodes.push(ExtractedUiNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
color: image.color.into(),
rect,
@ -466,8 +463,7 @@ pub fn extract_uinode_images(
node_type: NodeType::Rect,
},
main_entity: entity.into(),
},
);
});
}
}
@ -515,9 +511,7 @@ pub fn extract_uinode_borders(
if computed_node.border() != BorderRect::ZERO {
if let Some(border_color) = maybe_border_color.filter(|bc| !bc.0.is_fully_transparent())
{
extracted_uinodes.uinodes.insert(
commands.spawn(TemporaryRenderEntity).id(),
ExtractedUiNode {
extracted_uinodes.uinodes.push(ExtractedUiNode {
stack_index: computed_node.stack_index,
color: border_color.0.into(),
rect: Rect {
@ -537,8 +531,8 @@ pub fn extract_uinode_borders(
node_type: NodeType::Border,
},
main_entity: entity.into(),
},
);
render_entity: commands.spawn(TemporaryRenderEntity).id(),
});
}
}
@ -549,9 +543,8 @@ pub fn extract_uinode_borders(
if let Some(outline) = maybe_outline.filter(|outline| !outline.color.is_fully_transparent())
{
let outline_size = computed_node.outlined_node_size();
extracted_uinodes.uinodes.insert(
commands.spawn(TemporaryRenderEntity).id(),
ExtractedUiNode {
extracted_uinodes.uinodes.push(ExtractedUiNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: computed_node.stack_index,
color: outline.color.into(),
rect: Rect {
@ -571,8 +564,7 @@ pub fn extract_uinode_borders(
node_type: NodeType::Border,
},
main_entity: entity.into(),
},
);
});
}
}
}
@ -788,11 +780,8 @@ pub fn extract_text_sections(
if text_layout_info.glyphs.get(i + 1).is_none_or(|info| {
info.span_index != current_span || info.atlas_info.texture != atlas_info.texture
}) {
let id = commands.spawn(TemporaryRenderEntity).id();
extracted_uinodes.uinodes.insert(
id,
ExtractedUiNode {
extracted_uinodes.uinodes.push(ExtractedUiNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
color,
image: atlas_info.texture.id(),
@ -801,8 +790,7 @@ pub fn extract_text_sections(
rect,
item: ExtractedUiItem::Glyphs { range: start..end },
main_entity: entity.into(),
},
);
});
start = end;
}
@ -885,7 +873,8 @@ pub fn queue_uinodes(
draw_functions: Res<DrawFunctions<TransparentUi>>,
) {
let draw_function = draw_functions.read().id::<DrawUi>();
for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() {
for (index, extracted_uinode) in extracted_uinodes.uinodes.iter().enumerate() {
let entity = extracted_uinode.render_entity;
let Ok((default_camera_view, ui_anti_alias)) =
render_views.get_mut(extracted_uinode.extracted_camera_entity)
else {
@ -912,11 +901,12 @@ pub fn queue_uinodes(
transparent_phase.add(TransparentUi {
draw_function,
pipeline,
entity: (*entity, extracted_uinode.main_entity),
entity: (entity, extracted_uinode.main_entity),
sort_key: (
FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::NODE),
entity.index(),
),
index,
// batch_range will be calculated in prepare_uinodes
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
@ -978,7 +968,11 @@ pub fn prepare_uinodes(
for item_index in 0..ui_phase.items.len() {
let item = &mut ui_phase.items[item_index];
if let Some(extracted_uinode) = extracted_uinodes.uinodes.get(&item.entity()) {
if let Some(extracted_uinode) = extracted_uinodes
.uinodes
.get(item.index)
.filter(|n| item.entity() == n.render_entity)
{
let mut existing_batch = batches.last_mut();
if batch_image_handle == AssetId::invalid()

View File

@ -112,6 +112,7 @@ pub struct TransparentUi {
pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex,
pub index: usize,
pub indexed: bool,
}

View File

@ -5,7 +5,6 @@ use bevy_asset::*;
use bevy_ecs::{
prelude::Component,
query::ROQueryItem,
storage::SparseSet,
system::{
lifetimeless::{Read, SRes},
*,
@ -347,11 +346,12 @@ pub struct ExtractedUiMaterialNode<M: UiMaterial> {
// Nodes with ambiguous camera will be ignored.
pub extracted_camera_entity: Entity,
pub main_entity: MainEntity,
render_entity: Entity,
}
#[derive(Resource)]
pub struct ExtractedUiMaterialNodes<M: UiMaterial> {
pub uinodes: SparseSet<Entity, ExtractedUiMaterialNode<M>>,
pub uinodes: Vec<ExtractedUiMaterialNode<M>>,
}
impl<M: UiMaterial> Default for ExtractedUiMaterialNodes<M> {
@ -398,9 +398,8 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
continue;
};
extracted_uinodes.uinodes.insert(
commands.spawn(TemporaryRenderEntity).id(),
ExtractedUiMaterialNode {
extracted_uinodes.uinodes.push(ExtractedUiMaterialNode {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: computed_node.stack_index,
transform: transform.compute_matrix(),
material: handle.id(),
@ -413,8 +412,7 @@ pub fn extract_ui_material_nodes<M: UiMaterial>(
clip: clip.map(|clip| clip.clip),
extracted_camera_entity,
main_entity: entity.into(),
},
);
});
}
}
@ -450,7 +448,11 @@ pub fn prepare_uimaterial_nodes<M: UiMaterial>(
for item_index in 0..ui_phase.items.len() {
let item = &mut ui_phase.items[item_index];
if let Some(extracted_uinode) = extracted_uinodes.uinodes.get(item.entity()) {
if let Some(extracted_uinode) = extracted_uinodes
.uinodes
.get(item.index)
.filter(|n| item.entity() == n.render_entity)
{
let mut existing_batch = batches
.last_mut()
.filter(|_| batch_shader_handle == extracted_uinode.material);
@ -624,7 +626,7 @@ pub fn queue_ui_material_nodes<M: UiMaterial>(
{
let draw_function = draw_functions.read().id::<DrawUiMaterial<M>>();
for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() {
for (index, extracted_uinode) in extracted_uinodes.uinodes.iter().enumerate() {
let Some(material) = render_materials.get(extracted_uinode.material) else {
continue;
};
@ -660,13 +662,14 @@ pub fn queue_ui_material_nodes<M: UiMaterial>(
transparent_phase.add(TransparentUi {
draw_function,
pipeline,
entity: (*entity, extracted_uinode.main_entity),
entity: (extracted_uinode.render_entity, extracted_uinode.main_entity),
sort_key: (
FloatOrd(extracted_uinode.stack_index as f32 + stack_z_offsets::MATERIAL),
entity.index(),
extracted_uinode.render_entity.index(),
),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
index,
indexed: false,
});
}

View File

@ -5,7 +5,6 @@ use bevy_asset::*;
use bevy_color::{Alpha, ColorToComponents, LinearRgba};
use bevy_ecs::{
prelude::Component,
storage::SparseSet,
system::{
lifetimeless::{Read, SRes},
*,
@ -237,11 +236,12 @@ pub struct ExtractedUiTextureSlice {
pub flip_y: bool,
pub inverse_scale_factor: f32,
pub main_entity: MainEntity,
pub render_entity: Entity,
}
#[derive(Resource, Default)]
pub struct ExtractedUiTextureSlices {
pub slices: SparseSet<Entity, ExtractedUiTextureSlice>,
pub slices: Vec<ExtractedUiTextureSlice>,
}
pub fn extract_ui_texture_slices(
@ -309,9 +309,8 @@ pub fn extract_ui_texture_slices(
}
};
extracted_ui_slicers.slices.insert(
commands.spawn(TemporaryRenderEntity).id(),
ExtractedUiTextureSlice {
extracted_ui_slicers.slices.push(ExtractedUiTextureSlice {
render_entity: commands.spawn(TemporaryRenderEntity).id(),
stack_index: uinode.stack_index,
transform: transform.compute_matrix(),
color: image.color.into(),
@ -328,8 +327,7 @@ pub fn extract_ui_texture_slices(
flip_y: image.flip_y,
inverse_scale_factor: uinode.inverse_scale_factor,
main_entity: entity.into(),
},
);
});
}
}
@ -348,7 +346,7 @@ pub fn queue_ui_slices(
draw_functions: Res<DrawFunctions<TransparentUi>>,
) {
let draw_function = draw_functions.read().id::<DrawUiTextureSlices>();
for (entity, extracted_slicer) in extracted_ui_slicers.slices.iter() {
for (index, extracted_slicer) in extracted_ui_slicers.slices.iter().enumerate() {
let Ok(default_camera_view) =
render_views.get_mut(extracted_slicer.extracted_camera_entity)
else {
@ -373,13 +371,14 @@ pub fn queue_ui_slices(
transparent_phase.add(TransparentUi {
draw_function,
pipeline,
entity: (*entity, extracted_slicer.main_entity),
entity: (extracted_slicer.render_entity, extracted_slicer.main_entity),
sort_key: (
FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::TEXTURE_SLICE),
entity.index(),
extracted_slicer.render_entity.index(),
),
batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None,
index,
indexed: true,
});
}
@ -434,7 +433,11 @@ pub fn prepare_ui_slices(
for item_index in 0..ui_phase.items.len() {
let item = &mut ui_phase.items[item_index];
if let Some(texture_slices) = extracted_slices.slices.get(item.entity()) {
if let Some(texture_slices) = extracted_slices
.slices
.get(item.index)
.filter(|n| item.entity() == n.render_entity)
{
let mut existing_batch = batches.last_mut();
if batch_image_handle == AssetId::invalid()