diff --git a/Cargo.toml b/Cargo.toml index 35a675b62f244..67bdd2908a97f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1788,6 +1788,19 @@ description = "Test rendering of many UI elements" category = "Stress Tests" wasm = true + +[[example]] +name = "many_rects" +path = "examples/stress_tests/many_rects.rs" +doc-scrape-examples = true + +[package.metadata.example.many_rects] +name = "Many Rects" +description = "Benchmark to test UI rendering performance" +category = "Stress Tests" +wasm = true + + [[example]] name = "many_cubes" path = "examples/stress_tests/many_cubes.rs" diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 1752180b77d86..e5f44a0d18f38 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -117,6 +117,7 @@ impl Plugin for UiPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .add_systems( PreUpdate, diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 6bbd5eebdf20d..e63164f0d0df2 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -5,7 +5,7 @@ use crate::widget::TextFlags; use crate::{ widget::{Button, UiImageSize}, BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, - UiTextureAtlasImage, ZIndex, + UiStackIndex, UiTextureAtlasImage, ZIndex, }; use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; @@ -52,6 +52,11 @@ pub struct NodeBundle { pub view_visibility: ViewVisibility, /// Indicates the depth at which the node should appear in the UI pub z_index: ZIndex, + /// The node's position in the UiStack. Nodes with lower stack indices are drawn first. + /// Nodes with a higher stack index are drawn over nodes with a lower stack index. + /// + /// This component is automatically managed by `ui_stack_system`. + pub stack_index: UiStackIndex, } impl Default for NodeBundle { @@ -69,6 +74,7 @@ impl Default for NodeBundle { inherited_visibility: Default::default(), view_visibility: Default::default(), z_index: Default::default(), + stack_index: Default::default(), } } } @@ -112,6 +118,11 @@ pub struct ImageBundle { pub view_visibility: ViewVisibility, /// Indicates the depth at which the node should appear in the UI pub z_index: ZIndex, + /// The node's position in the UiStack. Nodes with lower stack indices are drawn first. + /// Nodes with a higher stack index are drawn over nodes with a lower stack index. + /// + /// This component is automatically managed by `ui_stack_system`. + pub stack_index: UiStackIndex, } /// A UI node that is a texture atlas sprite @@ -155,6 +166,11 @@ pub struct AtlasImageBundle { pub view_visibility: ViewVisibility, /// Indicates the depth at which the node should appear in the UI pub z_index: ZIndex, + /// The node's position in the UiStack. Nodes with lower stack indices are drawn first. + /// Nodes with a higher stack index are drawn over nodes with a lower stack index. + /// + /// This component is automatically managed by `ui_stack_system`. + pub stack_index: UiStackIndex, } #[cfg(feature = "bevy_text")] @@ -193,7 +209,12 @@ pub struct TextBundle { pub view_visibility: ViewVisibility, /// Indicates the depth at which the node should appear in the UI pub z_index: ZIndex, - /// The background color that will fill the containing node + /// The node's position in the UiStack. Nodes with lower stack indices are drawn first. + /// Nodes with a higher stack index are drawn over nodes with a lower stack index. + /// + /// This component is automatically managed by `ui_stack_system`. + pub stack_index: UiStackIndex, + /// The background color that will fill the containing node pub background_color: BackgroundColor, } @@ -214,6 +235,7 @@ impl Default for TextBundle { inherited_visibility: Default::default(), view_visibility: Default::default(), z_index: Default::default(), + stack_index: Default::default(), // Transparent background background_color: BackgroundColor(Color::NONE), } @@ -307,6 +329,11 @@ pub struct ButtonBundle { pub view_visibility: ViewVisibility, /// Indicates the depth at which the node should appear in the UI pub z_index: ZIndex, + /// The node's position in the UiStack. Nodes with lower stack indices are drawn first. + /// Nodes with a higher stack index are drawn over nodes with a lower stack index. + /// + /// This component is automatically managed by `ui_stack_system`. + pub stack_index: UiStackIndex, } impl Default for ButtonBundle { @@ -326,6 +353,7 @@ impl Default for ButtonBundle { inherited_visibility: Default::default(), view_visibility: Default::default(), z_index: Default::default(), + stack_index: Default::default(), } } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 2f4229b5b83f5..a96aa947bd7ed 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -2,7 +2,6 @@ mod pipeline; mod render_pass; use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; -use bevy_ecs::storage::SparseSet; use bevy_hierarchy::Parent; use bevy_render::view::ViewVisibility; use bevy_render::{ExtractSchedule, Render}; @@ -10,9 +9,10 @@ use bevy_window::{PrimaryWindow, Window}; pub use pipeline::*; pub use render_pass::*; +use crate::UiStackIndex; use crate::{ prelude::UiCameraConfig, BackgroundColor, BorderColor, CalculatedClip, ContentSize, Node, - Style, UiImage, UiScale, UiStack, UiTextureAtlasImage, Val, + Style, UiImage, UiScale, UiTextureAtlasImage, Val, }; use bevy_app::prelude::*; @@ -39,8 +39,8 @@ use bevy_sprite::TextureAtlas; use bevy_text::{PositionedGlyph, Text, TextLayoutInfo}; use bevy_transform::components::GlobalTransform; use bevy_utils::HashMap; -use bevy_utils::{FloatOrd, Uuid}; use bytemuck::{Pod, Zeroable}; +use std::mem::replace; use std::ops::Range; pub mod node { @@ -155,7 +155,6 @@ fn get_ui_graph(render_app: &mut App) -> RenderGraph { } pub struct ExtractedUiNode { - pub stack_index: usize, pub transform: Mat4, pub color: Color, pub rect: Rect, @@ -166,20 +165,81 @@ pub struct ExtractedUiNode { pub flip_y: bool, } +pub struct ExtractedRange { + sort_key: u32, + range: Range, +} + +impl ExtractedRange { + #[inline] + pub fn range(&self) -> Range { + self.range.start as usize..self.range.end as usize + } +} + #[derive(Resource, Default)] pub struct ExtractedUiNodes { - pub uinodes: SparseSet, + ranges: Vec, + uinodes: Vec, + discriminator: u8, +} + +pub struct UiExtractionBuffer<'a>(&'a mut ExtractedUiNodes); + +impl UiExtractionBuffer<'_> { + #[inline] + fn get_key(&self, stack_index: u32) -> u32 { + (stack_index << 8) | (self.0.discriminator as u32) + } + + /// Add a single `ExtractedUiNode` for rendering. + pub fn push(&mut self, stack_index: u32, item: ExtractedUiNode) { + let start = self.0.uinodes.len() as u32; + self.0.ranges.push(ExtractedRange { + sort_key: self.get_key(stack_index), + range: start..start + 1, + }); + self.0.uinodes.push(item); + } + + /// Add multiple `ExtractedUiNode`s for rendering. + pub fn extend(&mut self, stack_index: u32, items: impl Iterator) { + let start = self.0.uinodes.len() as u32; + self.0.uinodes.extend(items); + self.0.ranges.push(ExtractedRange { + sort_key: self.get_key(stack_index), + range: start..self.0.uinodes.len() as u32, + }); + } +} + +impl Drop for UiExtractionBuffer<'_> { + fn drop(&mut self) { + self.0.discriminator += 1; + } +} + +impl ExtractedUiNodes { + pub fn get_buffer(&mut self) -> UiExtractionBuffer<'_> { + UiExtractionBuffer(self) + } + + /// Clear the buffers + fn clear(&mut self) { + self.ranges.clear(); + self.uinodes.clear(); + self.discriminator = 0; + } } pub fn extract_atlas_uinodes( mut extracted_uinodes: ResMut, images: Extract>>, texture_atlases: Extract>>, - ui_stack: Extract>, uinode_query: Extract< Query< ( - Entity, + &UiStackIndex, &Node, &GlobalTransform, &BackgroundColor, @@ -192,70 +252,69 @@ pub fn extract_atlas_uinodes( >, >, ) { - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok(( - entity, - uinode, - transform, - color, - view_visibility, - clip, - texture_atlas_handle, - atlas_image, - )) = uinode_query.get(*entity) - { - // Skip invisible and completely transparent nodes - if !view_visibility.get() || color.0.a() == 0.0 { - continue; - } - - let (mut atlas_rect, mut atlas_size, image) = - if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { - let atlas_rect = *texture_atlas - .textures - .get(atlas_image.index) - .unwrap_or_else(|| { - panic!( - "Atlas index {:?} does not exist for texture atlas handle {:?}.", - atlas_image.index, - texture_atlas_handle.id(), - ) - }); - ( - atlas_rect, - texture_atlas.size, - texture_atlas.texture.clone(), - ) - } else { - // Atlas not present in assets resource (should this warn the user?) - continue; - }; + let mut extraction_buffer = extracted_uinodes.get_buffer(); + + for ( + stack_index, + uinode, + transform, + color, + view_visibility, + clip, + texture_atlas_handle, + atlas_image, + ) in uinode_query.iter() + { + // Skip invisible and completely transparent nodes + if !view_visibility.get() || color.0.a() == 0.0 { + continue; + } - // Skip loading images - if !images.contains(&image) { + let (mut atlas_rect, mut atlas_size, image) = + if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { + let atlas_rect = *texture_atlas + .textures + .get(atlas_image.index) + .unwrap_or_else(|| { + panic!( + "Atlas index {:?} does not exist for texture atlas handle {:?}.", + atlas_image.index, + texture_atlas_handle.id(), + ) + }); + ( + atlas_rect, + texture_atlas.size, + texture_atlas.texture.clone(), + ) + } else { + // Atlas not present in assets resource (should this warn the user?) continue; - } + }; - let scale = uinode.size() / atlas_rect.size(); - atlas_rect.min *= scale; - atlas_rect.max *= scale; - atlas_size *= scale; - - extracted_uinodes.uinodes.insert( - entity, - ExtractedUiNode { - stack_index, - transform: transform.compute_matrix(), - color: color.0, - rect: atlas_rect, - clip: clip.map(|clip| clip.clip), - image, - atlas_size: Some(atlas_size), - flip_x: atlas_image.flip_x, - flip_y: atlas_image.flip_y, - }, - ); + // Skip loading images + if !images.contains(&image) { + continue; } + + let scale = uinode.size() / atlas_rect.size(); + atlas_rect.min *= scale; + atlas_rect.max *= scale; + atlas_size *= scale; + + extraction_buffer.push( + stack_index.0, + ExtractedUiNode { + transform: transform.compute_matrix(), + color: color.0, + rect: atlas_rect, + clip: clip.map(|clip| clip.clip), + image, + atlas_size: Some(atlas_size), + flip_x: atlas_image.flip_x, + flip_y: atlas_image.flip_y, + }, + ); } } @@ -272,14 +331,13 @@ fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) } pub fn extract_uinode_borders( - mut commands: Commands, mut extracted_uinodes: ResMut, windows: Extract>>, ui_scale: Extract>, - ui_stack: Extract>, uinode_query: Extract< Query< ( + &UiStackIndex, &Node, &GlobalTransform, &Style, @@ -293,6 +351,7 @@ pub fn extract_uinode_borders( >, node_query: Extract>, ) { + let mut extraction_buffer = extracted_uinodes.get_buffer(); let image = bevy_render::texture::DEFAULT_IMAGE_HANDLE.typed(); let ui_logical_viewport_size = windows @@ -303,105 +362,95 @@ pub fn extract_uinode_borders( // so we have to divide by `UiScale` to get the size of the UI viewport. / ui_scale.0 as f32; - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((node, global_transform, style, border_color, parent, view_visibility, clip)) = - uinode_query.get(*entity) + for (stack_index, node, global_transform, style, border_color, parent, view_visibility, clip) in + uinode_query.iter() + { + // Skip invisible borders + if !view_visibility.get() + || border_color.0.a() == 0.0 + || node.size().x <= 0. + || node.size().y <= 0. { - // Skip invisible borders - if !view_visibility.get() - || border_color.0.a() == 0.0 - || node.size().x <= 0. - || node.size().y <= 0. - { - continue; - } - - // Both vertical and horizontal percentage border values are calculated based on the width of the parent node - // - let parent_width = parent - .and_then(|parent| node_query.get(parent.get()).ok()) - .map(|parent_node| parent_node.size().x) - .unwrap_or(ui_logical_viewport_size.x); - let left = - resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size); - let right = resolve_border_thickness( - style.border.right, - parent_width, - ui_logical_viewport_size, - ); - let top = - resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size); - let bottom = resolve_border_thickness( - style.border.bottom, - parent_width, - ui_logical_viewport_size, - ); + continue; + } - // Calculate the border rects, ensuring no overlap. - // The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value. - let max = 0.5 * node.size(); - let min = -max; - let inner_min = min + Vec2::new(left, top); - let inner_max = (max - Vec2::new(right, bottom)).max(inner_min); - let border_rects = [ - // Left border - Rect { - min, - max: Vec2::new(inner_min.x, max.y), - }, - // Right border - Rect { - min: Vec2::new(inner_max.x, min.y), - max, - }, - // Top border - Rect { - min: Vec2::new(inner_min.x, min.y), - max: Vec2::new(inner_max.x, inner_min.y), - }, - // Bottom border - Rect { - min: Vec2::new(inner_min.x, inner_max.y), - max: Vec2::new(inner_max.x, max.y), - }, - ]; - - let transform = global_transform.compute_matrix(); - - for edge in border_rects { - if edge.min.x < edge.max.x && edge.min.y < edge.max.y { - extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), - ExtractedUiNode { - stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform: transform * Mat4::from_translation(edge.center().extend(0.)), - color: border_color.0, - rect: Rect { - max: edge.size(), - ..Default::default() - }, - image: image.clone_weak(), - atlas_size: None, - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, + // Both vertical and horizontal percentage border values are calculated based on the width of the parent node + // + let parent_width = parent + .and_then(|parent| node_query.get(parent.get()).ok()) + .map(|parent_node| parent_node.size().x) + .unwrap_or(ui_logical_viewport_size.x); + let left = + resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size); + let right = + resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size); + let top = + resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size); + let bottom = + resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size); + + // Calculate the border rects, ensuring no overlap. + // The border occupies the space between the node's bounding rect and the node's bounding rect inset in each direction by the node's corresponding border value. + let max = 0.5 * node.size(); + let min = -max; + let inner_min = min + Vec2::new(left, top); + let inner_max = (max - Vec2::new(right, bottom)).max(inner_min); + let border_rects = [ + // Left border + Rect { + min, + max: Vec2::new(inner_min.x, max.y), + }, + // Right border + Rect { + min: Vec2::new(inner_max.x, min.y), + max, + }, + // Top border + Rect { + min: Vec2::new(inner_min.x, min.y), + max: Vec2::new(inner_max.x, inner_min.y), + }, + // Bottom border + Rect { + min: Vec2::new(inner_min.x, inner_max.y), + max: Vec2::new(inner_max.x, max.y), + }, + ]; + + let transform = global_transform.compute_matrix(); + extraction_buffer.extend( + stack_index.0, + border_rects + .into_iter() + .filter(|edge| edge.min.x < edge.max.x && edge.min.y < edge.max.y) + .map(|edge| { + ExtractedUiNode { + // This translates the uinode's transform to the center of the current border rectangle + transform: transform * Mat4::from_translation(edge.center().extend(0.)), + color: border_color.0, + rect: Rect { + max: edge.size(), + ..Default::default() }, - ); - } - } - } + image: image.clone_weak(), + atlas_size: None, + clip: clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, + } + }), + ); } } pub fn extract_uinodes( mut extracted_uinodes: ResMut, images: Extract>>, - ui_stack: Extract>, uinode_query: Extract< Query< ( - Entity, + &UiStackIndex, &Node, &GlobalTransform, &BackgroundColor, @@ -413,43 +462,41 @@ pub fn extract_uinodes( >, >, ) { - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((entity, uinode, transform, color, maybe_image, view_visibility, clip)) = - uinode_query.get(*entity) - { - // Skip invisible and completely transparent nodes - if !view_visibility.get() || color.0.a() == 0.0 { + let mut extration_buffer = extracted_uinodes.get_buffer(); + for (stack_index, uinode, transform, color, maybe_image, view_visibility, clip) in + uinode_query.iter() + { + // Skip invisible and completely transparent nodes + if !view_visibility.get() || color.0.a() == 0.0 { + continue; + } + + let (image, flip_x, flip_y) = if let Some(image) = maybe_image { + // Skip loading images + if !images.contains(&image.texture) { continue; } + (image.texture.clone_weak(), image.flip_x, image.flip_y) + } else { + (DEFAULT_IMAGE_HANDLE.typed(), false, false) + }; - let (image, flip_x, flip_y) = if let Some(image) = maybe_image { - // Skip loading images - if !images.contains(&image.texture) { - continue; - } - (image.texture.clone_weak(), image.flip_x, image.flip_y) - } else { - (DEFAULT_IMAGE_HANDLE.typed(), false, false) - }; - - extracted_uinodes.uinodes.insert( - entity, - ExtractedUiNode { - stack_index, - transform: transform.compute_matrix(), - color: color.0, - rect: Rect { - min: Vec2::ZERO, - max: uinode.calculated_size, - }, - clip: clip.map(|clip| clip.clip), - image, - atlas_size: None, - flip_x, - flip_y, + extration_buffer.push( + stack_index.0, + ExtractedUiNode { + transform: transform.compute_matrix(), + color: color.0, + rect: Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, }, - ); - }; + clip: clip.map(|clip| clip.clip), + image, + atlas_size: None, + flip_x, + flip_y, + }, + ); } } @@ -528,14 +575,13 @@ pub fn extract_default_ui_camera_view( #[cfg(feature = "bevy_text")] pub fn extract_text_uinodes( - mut commands: Commands, mut extracted_uinodes: ResMut, texture_atlases: Extract>>, windows: Extract>>, - ui_stack: Extract>, ui_scale: Extract>, uinode_query: Extract< Query<( + &UiStackIndex, &Node, &GlobalTransform, &Text, @@ -545,6 +591,7 @@ pub fn extract_text_uinodes( )>, >, ) { + let mut extraction_buffer = extracted_uinodes.get_buffer(); // TODO: Support window-independent UI scale: https://github.com/bevyengine/bevy/issues/5621 let scale_factor = windows .get_single() @@ -554,39 +601,37 @@ pub fn extract_text_uinodes( let inverse_scale_factor = (scale_factor as f32).recip(); - for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((uinode, global_transform, text, text_layout_info, view_visibility, clip)) = - uinode_query.get(*entity) - { - // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) - if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. { - continue; - } - let transform = global_transform.compute_matrix() - * Mat4::from_translation(-0.5 * uinode.size().extend(0.)); - - let mut color = Color::WHITE; - let mut current_section = usize::MAX; - for PositionedGlyph { - position, - atlas_info, - section_index, - .. - } in &text_layout_info.glyphs - { - if *section_index != current_section { - color = text.sections[*section_index].style.color.as_rgba_linear(); - current_section = *section_index; - } - let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); + for (stack_index, uinode, global_transform, text, text_layout_info, view_visibility, clip) in + uinode_query.iter() + { + // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) + if !view_visibility.get() || uinode.size().x == 0. || uinode.size().y == 0. { + continue; + } + let transform = global_transform.compute_matrix() + * Mat4::from_translation(-0.5 * uinode.size().extend(0.)); + + let mut color = Color::WHITE; + let mut current_section = usize::MAX; + extraction_buffer.extend( + stack_index.0, + text_layout_info.glyphs.iter().map( + |PositionedGlyph { + position, + atlas_info, + section_index, + .. + }| { + if *section_index != current_section { + color = text.sections[*section_index].style.color.as_rgba_linear(); + current_section = *section_index; + } + let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let mut rect = atlas.textures[atlas_info.glyph_index]; - rect.min *= inverse_scale_factor; - rect.max *= inverse_scale_factor; - extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + let mut rect = atlas.textures[atlas_info.glyph_index]; + rect.min *= inverse_scale_factor; + rect.max *= inverse_scale_factor; ExtractedUiNode { - stack_index, transform: transform * Mat4::from_translation(position.extend(0.) * inverse_scale_factor), color, @@ -596,10 +641,10 @@ pub fn extract_text_uinodes( clip: clip.map(|clip| clip.clip), flip_x: false, flip_y: false, - }, - ); - } - } + } + }, + ), + ); } } @@ -645,43 +690,17 @@ pub struct UiBatch { const TEXTURED_QUAD: u32 = 0; const UNTEXTURED_QUAD: u32 = 1; -#[allow(clippy::too_many_arguments)] -pub fn queue_uinodes( - extracted_uinodes: Res, - ui_pipeline: Res, - mut pipelines: ResMut>, - mut views: Query<(&ExtractedView, &mut RenderPhase)>, - pipeline_cache: Res, - draw_functions: Res>, -) { - let draw_function = draw_functions.read().id::(); - for (view, mut transparent_phase) in &mut views { - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_pipeline, - UiPipelineKey { hdr: view.hdr }, - ); - transparent_phase - .items - .reserve(extracted_uinodes.uinodes.len()); - for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() { - transparent_phase.add(TransparentUi { - draw_function, - pipeline, - entity: *entity, - sort_key: FloatOrd(extracted_uinode.stack_index as f32), - // batch_size will be calculated in prepare_uinodes - batch_size: 0, - }); - } - } -} - #[derive(Resource, Default)] pub struct UiImageBindGroups { pub values: HashMap, BindGroup>, } +pub fn queue_uinodes(mut extracted_uinodes: ResMut) { + extracted_uinodes + .ranges + .sort_unstable_by_key(|extracted_range| extracted_range.sort_key); +} + #[allow(clippy::too_many_arguments)] pub fn prepare_uinodes( mut commands: Commands, @@ -693,10 +712,36 @@ pub fn prepare_uinodes( ui_pipeline: Res, mut image_bind_groups: ResMut, gpu_images: Res>, - mut phases: Query<&mut RenderPhase>, events: Res, mut previous_len: Local, + pipeline_cache: Res, + draw_functions: Res>, + mut render_phases: ParamSet<( + Query<&mut RenderPhase>, + Query<(&ExtractedView, &mut RenderPhase)>, + )>, + mut pipelines: ResMut>, ) { + let draw_function = draw_functions.read().id::(); + + for (view, mut transparent_phase) in &mut render_phases.p1() { + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_pipeline, + UiPipelineKey { hdr: view.hdr }, + ); + + transparent_phase.items.extend( + std::iter::repeat_with(|| TransparentUi { + draw_function, + pipeline, + entity: commands.spawn_empty().id(), + batch_size: 0, + }) + .take(extracted_uinodes.ranges.len()), + ); + } + // If an image has changed, the GpuImage has (probably) changed for event in &events.images { match event { @@ -707,9 +752,26 @@ pub fn prepare_uinodes( }; } - #[inline] - fn is_textured(image: &Handle) -> bool { - image.id() != DEFAULT_IMAGE_HANDLE.id() + if let Some(gpu_image) = gpu_images.get(&DEFAULT_IMAGE_HANDLE.typed()) { + image_bind_groups + .values + .entry(Handle::weak(DEFAULT_IMAGE_HANDLE.id())) + .or_insert_with(|| { + render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView(&gpu_image.texture_view), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&gpu_image.sampler), + }, + ], + label: Some("ui_material_bind_group"), + layout: &ui_pipeline.image_layout, + }) + }); } if let Some(view_binding) = view_uniforms.uniforms.binding() { @@ -727,64 +789,64 @@ pub fn prepare_uinodes( // Vertex buffer index let mut index = 0; - - for mut ui_phase in &mut phases { + for mut ui_phase in &mut render_phases.p0() { let mut batch_item_index = 0; - let mut batch_image_handle = HandleId::Id(Uuid::nil(), u64::MAX); - - 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) { - let mut existing_batch = batches - .last_mut() - .filter(|_| batch_image_handle == extracted_uinode.image.id()); - - if existing_batch.is_none() { - if let Some(gpu_image) = gpu_images.get(&extracted_uinode.image) { - batch_item_index = item_index; - batch_image_handle = extracted_uinode.image.id(); - - let new_batch = UiBatch { - range: index..index, - image_handle_id: extracted_uinode.image.id(), - }; - - batches.push((item.entity, new_batch)); - - image_bind_groups - .values - .entry(Handle::weak(batch_image_handle)) - .or_insert_with(|| { - render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView( - &gpu_image.texture_view, - ), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler( - &gpu_image.sampler, - ), - }, - ], - label: Some("ui_material_bind_group"), - layout: &ui_pipeline.image_layout, - }) - }); - - existing_batch = batches.last_mut(); + let mut batch_image_handle = DEFAULT_IMAGE_HANDLE.id(); + + for (n, extracted_range) in extracted_uinodes.ranges.iter().enumerate() { + for extracted_uinode in &extracted_uinodes.uinodes[extracted_range.range()] { + let existing_batch = if extracted_uinode.image.id() == DEFAULT_IMAGE_HANDLE.id() + || extracted_uinode.image.id() == batch_image_handle + { + batches.last_mut() + } else if let Some(gpu_image) = gpu_images.get(&extracted_uinode.image) { + image_bind_groups + .values + .entry(Handle::weak(extracted_uinode.image.id())) + .or_insert_with(|| { + render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView( + &gpu_image.texture_view, + ), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&gpu_image.sampler), + }, + ], + label: Some("ui_material_bind_group"), + layout: &ui_pipeline.image_layout, + }) + }); + if replace(&mut batch_image_handle, extracted_uinode.image.id()) + == DEFAULT_IMAGE_HANDLE.id() + { + batches.last_mut().map(|existing_batch| { + existing_batch.1.image_handle_id = batch_image_handle; + existing_batch + }) } else { - continue; + None } + } else { + continue; + }; + if existing_batch.is_none() { + batch_item_index = n; + let new_batch = UiBatch { + range: index..index, + image_handle_id: extracted_uinode.image.id(), + }; + batches.push((ui_phase.items[n].entity, new_batch)); } - let mode = if is_textured(&extracted_uinode.image) { - TEXTURED_QUAD - } else { + let mode = if extracted_uinode.image.id() == DEFAULT_IMAGE_HANDLE.id() { UNTEXTURED_QUAD + } else { + TEXTURED_QUAD }; let mut uinode_rect = extracted_uinode.rect; @@ -894,16 +956,14 @@ pub fn prepare_uinodes( }); } index += QUAD_INDICES.len() as u32; - existing_batch.unwrap().1.range.end = index; - ui_phase.items[batch_item_index].batch_size += 1; - } else { - batch_image_handle = HandleId::Id(Uuid::nil(), u64::MAX); + batches.last_mut().unwrap().1.range.end = index; } + ui_phase.items[batch_item_index].batch_size += 1; } } ui_meta.vertices.write_buffer(&render_device, &render_queue); *previous_len = batches.len(); commands.insert_or_spawn_batch(batches); } - extracted_uinodes.uinodes.clear(); + extracted_uinodes.clear(); } diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 697aa11104c7e..3c56db1fac5f4 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -12,7 +12,6 @@ use bevy_render::{ renderer::*, view::*, }; -use bevy_utils::FloatOrd; pub struct UiPassNode { ui_view_query: QueryState< @@ -87,7 +86,6 @@ impl Node for UiPassNode { } pub struct TransparentUi { - pub sort_key: FloatOrd, pub entity: Entity, pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, @@ -95,7 +93,7 @@ pub struct TransparentUi { } impl PhaseItem for TransparentUi { - type SortKey = FloatOrd; + type SortKey = (); #[inline] fn entity(&self) -> Entity { @@ -103,9 +101,7 @@ impl PhaseItem for TransparentUi { } #[inline] - fn sort_key(&self) -> Self::SortKey { - self.sort_key - } + fn sort_key(&self) -> Self::SortKey {} #[inline] fn draw_function(&self) -> DrawFunctionId { @@ -113,9 +109,7 @@ impl PhaseItem for TransparentUi { } #[inline] - fn sort(items: &mut [Self]) { - items.sort_by_key(|item| item.sort_key()); - } + fn sort(_items: &mut [Self]) {} #[inline] fn batch_size(&self) -> usize { diff --git a/crates/bevy_ui/src/stack.rs b/crates/bevy_ui/src/stack.rs index c24a8aad66b26..293bf2ca79c5e 100644 --- a/crates/bevy_ui/src/stack.rs +++ b/crates/bevy_ui/src/stack.rs @@ -3,7 +3,7 @@ use bevy_ecs::prelude::*; use bevy_hierarchy::prelude::*; -use crate::{Node, ZIndex}; +use crate::{Node, UiStackIndex, ZIndex}; /// The current UI stack, which contains all UI nodes ordered by their depth (back-to-front). /// @@ -35,6 +35,7 @@ pub fn ui_stack_system( root_node_query: Query, Without)>, zindex_query: Query<&ZIndex, With>, children_query: Query<&Children>, + mut stack_index_query: Query<&mut UiStackIndex>, ) { // Generate `StackingContext` tree let mut global_context = StackingContext::default(); @@ -55,6 +56,11 @@ pub fn ui_stack_system( ui_stack.uinodes.clear(); ui_stack.uinodes.reserve(total_entry_count); fill_stack_recursively(&mut ui_stack.uinodes, &mut global_context); + + for (i, entity) in ui_stack.uinodes.iter().enumerate() { + let mut stack_index = stack_index_query.get_mut(*entity).unwrap(); + stack_index.0 = i as u32; + } } /// Generate z-index based UI node tree @@ -124,19 +130,27 @@ mod tests { }; use bevy_hierarchy::BuildChildren; - use crate::{Node, UiStack, ZIndex}; + use crate::{Node, UiStack, UiStackIndex, 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_zindex( + name: &'static str, + z_index: ZIndex, + ) -> (Label, Node, UiStackIndex, ZIndex) { + ( + Label(name), + Node::default(), + UiStackIndex::default(), + z_index, + ) } - fn node_without_zindex(name: &'static str) -> (Label, Node) { - (Label(name), Node::default()) + fn node_without_zindex(name: &'static str) -> (Label, Node, UiStackIndex) { + (Label(name), Node::default(), UiStackIndex::default()) } /// Tests the UI Stack system. @@ -199,14 +213,17 @@ mod tests { schedule.add_systems(ui_stack_system); schedule.run(&mut world); - let mut query = world.query::<&Label>(); + let mut query = world.query::<(&Label, &UiStackIndex)>(); let ui_stack = world.resource::(); let actual_result = ui_stack .uinodes .iter() - .map(|entity| query.get(&world, *entity).unwrap().clone()) + .map(|entity| { + let (label, ui_stack_index) = query.get(&world, *entity).unwrap(); + (label.clone(), ui_stack_index.0) + }) .collect::>(); - let expected_result = vec![ + let expected_result: Vec<_> = [ (Label("1-2-1")), // ZIndex::Global(-3) (Label("3")), // ZIndex::Global(-2) (Label("1-2")), // ZIndex::Global(-1) @@ -225,7 +242,11 @@ mod tests { (Label("1-1")), (Label("1-3")), (Label("0")), // ZIndex::Global(2) - ]; + ] + .into_iter() + .enumerate() + .map(|(i, label)| (label, i as u32)) + .collect(); assert_eq!(actual_result, expected_result); } } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 1dfbbcd0204a4..c320c9581c0f4 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -81,6 +81,21 @@ impl Default for Node { } } +/// The position of the Ui Node in the [`UiStack`](`crate::UiStack`). +/// UI nodes with a higher `UiStackIndex` are drawn in front of those with a lower `UiStackIndex`. +/// +/// This component is automatically managed by `ui_stack_system`. +#[derive(Component, Copy, Clone, Debug, Default, Reflect)] +#[reflect(Component, Default)] +pub struct UiStackIndex(pub(crate) u32); + +impl UiStackIndex { + // returns the stack index value + pub fn get(self) -> u32 { + self.0 + } +} + /// Represents the possible value types for layout properties. /// /// This enum allows specifying values for various [`Style`] properties in different units, diff --git a/examples/README.md b/examples/README.md index 6f53c34f7048c..ac525b6e65987 100644 --- a/examples/README.md +++ b/examples/README.md @@ -315,6 +315,7 @@ Example | Description [Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos [Many Glyphs](../examples/stress_tests/many_glyphs.rs) | Simple benchmark to test text rendering. [Many Lights](../examples/stress_tests/many_lights.rs) | Simple benchmark to test rendering many point lights. Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights +[Many Rects](../examples/stress_tests/many_rects.rs) | Benchmark to test UI rendering performance [Many Sprites](../examples/stress_tests/many_sprites.rs) | Displays many sprites in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites. [Text Pipeline](../examples/stress_tests/text_pipeline.rs) | Text Pipeline benchmark [Transform Hierarchy](../examples/stress_tests/transform_hierarchy.rs) | Various test cases for hierarchy and transform propagation performance diff --git a/examples/stress_tests/many_buttons.rs b/examples/stress_tests/many_buttons.rs index 9cd125ac0d0dc..a54576ca7807b 100644 --- a/examples/stress_tests/many_buttons.rs +++ b/examples/stress_tests/many_buttons.rs @@ -6,6 +6,9 @@ //! //! To start the demo without borders run //! `cargo run --example many_buttons --release no-borders` //! +//! To start the demo without images run +//! `cargo run --example many_buttons --release no-images` +//! //| To do a full layout update each frame run //! `cargo run --example many_buttons --release recompute-layout` //! @@ -73,8 +76,13 @@ fn button_system( } } -fn setup(mut commands: Commands) { +fn setup(mut commands: Commands, assets: Res) { warn!(include_str!("warning_string.txt")); + let image = if !std::env::args().any(|arg| arg == "no-images") { + Some(assets.load("branding/icon.png")) + } else { + None + }; let count = ROW_COLUMN_COUNT; let count_f = count as f32; @@ -108,6 +116,7 @@ fn setup(mut commands: Commands) { spawn_text, border, border_color, + image.as_ref(), ); } } @@ -124,6 +133,7 @@ fn spawn_button( spawn_text: bool, border: UiRect, border_color: BorderColor, + image: Option<&Handle>, ) { let width = 90.0 / total; let mut builder = commands.spawn(( @@ -145,6 +155,12 @@ fn spawn_button( IdleColor(background_color), )); + if let Some(image) = image { + if (i + j) % 4 == 0 { + builder.insert(UiImage::new(image.clone())); + } + } + if spawn_text { builder.with_children(|commands| { commands.spawn(TextBundle::from_section( diff --git a/examples/stress_tests/many_rects.rs b/examples/stress_tests/many_rects.rs new file mode 100644 index 0000000000000..24b608b358eab --- /dev/null +++ b/examples/stress_tests/many_rects.rs @@ -0,0 +1,260 @@ +//! Draws a mix of approximately `100_000` textured and untextured rectangles on screen using the UI's renderer. +//! +//! This example doesn't spawn UI node bundles and instead add its own custom extraction functions to the `ExtractSchedule`. +//! This bypasses the layout systems so that only the UI's rendering systems are put under stress. +//! +//! To run the demo with extraction iterating the UI stack use: +//! `cargo run --example many_rects --release iter-stack` +//! +use bevy::render::{texture::DEFAULT_IMAGE_HANDLE, Extract, RenderApp}; +use bevy::ui::{ExtractedUiNode, ExtractedUiNodes}; + +use bevy::{ + diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin}, + prelude::*, + window::{PresentMode, WindowPlugin}, +}; +use rand::{seq::SliceRandom, Rng, SeedableRng}; + +const SEED: u64 = 42; +const MIN_EDGE: f32 = 10.; +const MAX_EDGE: f32 = 150.; +const WIDTH: f32 = 1024.; +const HEIGHT: f32 = 768.; +const STACK_SIZE: usize = 33000; +const TEXTURED_RATIO: f32 = 0.2; + +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] +struct ExtractRect; + +fn main() { + let mut app = App::new(); + app.add_plugins(( + DefaultPlugins.set(WindowPlugin { + primary_window: Some(Window { + resolution: (WIDTH, HEIGHT).into(), + title: "many_rects".into(), + present_mode: PresentMode::AutoNoVsync, + ..default() + }), + ..default() + }), + FrameTimeDiagnosticsPlugin, + LogDiagnosticsPlugin::default(), + )) + .add_systems(Startup, setup); + + let render_app = match app.get_sub_app_mut(RenderApp) { + Ok(render_app) => render_app, + Err(_) => return, + }; + + if std::env::args().any(|arg| arg == "iter-stack") { + render_app.add_systems( + ExtractSchedule, + ( + extract_rect_iter_stack::<1>, + extract_rect_iter_stack::<2>, + extract_rect_iter_stack::<4>, + extract_rect_iter_stack::<8>, + extract_rect_iter_stack::<16>, + extract_rect_iter_stack::<32>, + ) + .chain(), + ); + } else { + render_app.add_systems( + ExtractSchedule, + ( + extract_rect::<1>, + extract_rect::<2>, + extract_rect::<4>, + extract_rect::<8>, + extract_rect::<16>, + extract_rect::<32>, + ) + .chain(), + ); + } + + app.run(); +} + +#[derive(Component)] +pub struct ExtractionMarker; + +#[derive(Resource, Deref, DerefMut)] +pub struct RectStack(Vec); + +fn extract_rect_iter_stack( + mut extracted_uinodes: ResMut, + images: Extract>>, + ui_stack: Extract>, + uinode_query: Extract< + Query< + ( + &Size, + &GlobalTransform, + &BackgroundColor, + Option<&UiImage>, + &ViewVisibility, + ), + With>, + >, + >, +) { + let mut extraction_buffer = extracted_uinodes.get_buffer(); + for (stack_index, entity) in ui_stack.iter().enumerate() { + if let Ok((size, transform, color, maybe_image, visibility)) = uinode_query.get(*entity) { + // Skip invisible and completely transparent nodes + if !visibility.get() || color.0.a() == 0.0 { + continue; + } + + let (image, flip_x, flip_y) = if let Some(image) = maybe_image { + // Skip loading images + if !images.contains(&image.texture) { + continue; + } + (image.texture.clone_weak(), image.flip_x, image.flip_y) + } else { + (DEFAULT_IMAGE_HANDLE.typed(), false, false) + }; + extraction_buffer.extend( + stack_index as u32, + (0..N).map(|_| ExtractedUiNode { + transform: transform.compute_matrix(), + color: color.0, + rect: Rect { + min: Vec2::ZERO, + max: size.0, + }, + clip: None, + image: image.clone_weak(), + atlas_size: None, + flip_x, + flip_y, + }), + ); + } + } +} + +fn extract_rect( + mut extracted_uinodes: ResMut, + images: Extract>>, + uinode_query: Extract< + Query< + ( + &StackIndex, + &Size, + &GlobalTransform, + &BackgroundColor, + Option<&UiImage>, + &ViewVisibility, + ), + With>, + >, + >, +) { + let mut extraction_buffer = extracted_uinodes.get_buffer(); + for (stack_index, size, transform, color, maybe_image, visibility) in uinode_query.iter() { + // Skip invisible and completely transparent nodes + if !visibility.get() || color.0.a() == 0.0 { + continue; + } + + let (image, flip_x, flip_y) = if let Some(image) = maybe_image { + // Skip loading images + if !images.contains(&image.texture) { + continue; + } + (image.texture.clone_weak(), image.flip_x, image.flip_y) + } else { + (DEFAULT_IMAGE_HANDLE.typed(), false, false) + }; + extraction_buffer.extend( + stack_index.0 as u32, + (0..N).map(|_| ExtractedUiNode { + transform: transform.compute_matrix(), + color: color.0, + rect: Rect { + min: Vec2::ZERO, + max: size.0, + }, + clip: None, + image: image.clone_weak(), + atlas_size: None, + flip_x, + flip_y, + }), + ); + } +} + +#[derive(Component)] +pub struct Size(Vec2); + +#[derive(Component)] +pub struct StackIndex(usize); + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2dBundle::default()); + + let image_handles = [ + "branding/bevy_logo_light.png", + "branding/bevy_logo_dark.png", + "branding/icon.png", + ]; + let colors = [ + Color::WHITE, + Color::RED, + Color::GREEN, + Color::BLUE, + Color::YELLOW, + ]; + let mut rng = rand::rngs::StdRng::seed_from_u64(SEED); + let mut rect_stack = RectStack(Vec::with_capacity(STACK_SIZE)); + for _ in 0..STACK_SIZE { + let n = rng.gen_range(0..63); + let mut builder = match n { + 0..=31 => commands.spawn(ExtractionMarker::<1>), + 32..=47 => commands.spawn(ExtractionMarker::<2>), + 48..=55 => commands.spawn(ExtractionMarker::<4>), + 56..=59 => commands.spawn(ExtractionMarker::<8>), + 60..=61 => commands.spawn(ExtractionMarker::<16>), + _ => commands.spawn(ExtractionMarker::<32>), + }; + if rng.gen::() <= TEXTURED_RATIO { + let image = image_handles.choose(&mut rng).unwrap(); + builder.insert(UiImage::new(asset_server.load(*image))); + } + rect_stack.push(builder.id()); + } + rect_stack.shuffle(&mut rng); + + let bundles: Vec<_> = rect_stack + .iter() + .enumerate() + .map(|(stack_index, entity)| { + (*entity, { + let w = rng.gen_range(MIN_EDGE..MAX_EDGE); + let h = rng.gen_range(MIN_EDGE..MAX_EDGE); + let x = rng.gen_range(0.0..WIDTH); + let y = rng.gen_range(0.0..HEIGHT); + let color = *colors.choose(&mut rng).unwrap(); + ( + Size(Vec2::new(w, h)), + Transform::from_translation(Vec3::new(x, y, 1.0)), + GlobalTransform::default(), + StackIndex(stack_index), + BackgroundColor(color), + VisibilityBundle::default(), + ) + }) + }) + .collect(); + + commands.insert_or_spawn_batch(bundles); + commands.insert_resource(rect_stack); +}