From f32ce127dc803f6b8d90e8c4c5484475c013f46b Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 20 Jul 2026 16:27:07 -0700 Subject: [PATCH 1/6] Retain UI phase items from frame to frame, and consolidate the UI queuing systems into one. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Although PR #24893 added retention for UI render world instances themselves, in order to avoid re-extracting them from the main world ECS every frame, we still recreate the `TransparentUi` phase items every frame via `add_transient()`, which additionally removes them from the phase at the end of every frame. This is a significant CPU time sink and isn't the preferred pattern in Bevy nowadays. This commit makes the UI-related phase items retained just as 3D meshes are. All `queue_` methods in `bevy_ui_render` have been updated to walk the list of changed and removed meshes and update elements in the `SortedRenderPhase` only as necessary. The calls to `add_transient()` have been removed in favor of the more modern `add_retained()`. Additionally, all the custom queuing systems have been consolidated into a single generic system, `queue_ui_items`. The resources that hold extracted UI items have likewise been consolidated into a generic `UiRenderObjects` resource. The behavior specific to each individual item type (normal UI nodes, box shadows, gradients, etc.) has been factored into a trait named `UiRenderObject`. This has resulted in dramatic simplifications throughout UI rendering. See the documentation for more information. On `many_buttons`, this PR reduces the median frame time from 36.95 ms to 22.78 ms, or 27 FPS to 44 FPS. The `queue_uinodes` system has gone from 6.21 ms/frame to 15.2 μs/frame, a 409× speedup. And, because the Rust standard library's sorting algorithm is good at sorting data that's close to already sorted, the `sort_phase_system` time decreases from 5.17 ms/frame to 1.29 ms/frame, a 4.01× speedup. --- crates/bevy_ui_render/Cargo.toml | 1 + crates/bevy_ui_render/src/box_shadow.rs | 168 ++++---- crates/bevy_ui_render/src/debug_overlay.rs | 4 +- crates/bevy_ui_render/src/gradient.rs | 166 ++++---- crates/bevy_ui_render/src/lib.rs | 386 ++++++++++++++---- crates/bevy_ui_render/src/text.rs | 10 +- .../src/ui_material_pipeline.rs | 171 +++----- .../src/ui_texture_slice_pipeline.rs | 131 +++--- 8 files changed, 581 insertions(+), 456 deletions(-) diff --git a/crates/bevy_ui_render/Cargo.toml b/crates/bevy_ui_render/Cargo.toml index 1143fa0a4c134..1b77001a0a92d 100644 --- a/crates/bevy_ui_render/Cargo.toml +++ b/crates/bevy_ui_render/Cargo.toml @@ -42,6 +42,7 @@ bytemuck = { version = "1.5", features = ["derive"] } derive_more = { version = "2", default-features = false, features = ["from"] } tracing = { version = "0.1", default-features = false, features = ["std"] } indexmap = { version = "2" } +smallvec = { version = "1" } [features] default = ["bevy_ui_debug"] diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 7f3c2bf0d8683..2e3b4d1ef5741 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -6,7 +6,6 @@ use bevy_app::prelude::*; use bevy_asset::*; use bevy_camera::visibility::InheritedVisibility; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; -use bevy_ecs::entity::EntityIndexMap; use bevy_ecs::prelude::*; use bevy_ecs::{ prelude::Component, @@ -17,7 +16,7 @@ use bevy_ecs::{ }; use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2}; use bevy_mesh::VertexBufferLayout; -use bevy_render::sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}; +use bevy_render::sync_world::{MainEntity, MainEntityHashSet}; use bevy_render::{ render_phase::*, render_resource::{binding_types::uniform_buffer, *}, @@ -34,9 +33,12 @@ use bevy_ui::{ use bevy_utils::default; use bytemuck::{Pod, Zeroable}; -use crate::{BoxShadowSamples, RenderUiSystems, TransparentUi, UiCameraMap}; +use crate::{ + queue_ui_items, BoxShadowSamples, CachedCameraView, ChangedUiObject, RenderUiSystems, + TransparentUi, UiCameraMap, UiRenderObject, UiRenderObjects, +}; -use super::{stack_z_offsets, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; +use super::{stack_z_offsets, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; /// A plugin that enables the rendering of box shadows. pub struct BoxShadowPlugin; @@ -59,7 +61,7 @@ impl Plugin for BoxShadowPlugin { .add_systems( Render, ( - queue_shadows.in_set(RenderSystems::Queue), + queue_ui_items::.in_set(RenderSystems::Queue), prepare_shadows.in_set(RenderSystems::PrepareBindGroups), ), ); @@ -194,17 +196,44 @@ pub struct ExtractedBoxShadow { pub size: Vec2, } -/// List of extracted shadows to be sorted and queued for rendering -#[derive(Resource, Default)] -pub struct ExtractedBoxShadows { - /// The list of box shadows grouped by their main-world entity, along with - /// each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all box - /// shadows associated with a main-world entity when it changes. - pub box_shadows: MainEntityHashMap<(Entity, EntityIndexMap)>, +impl UiRenderObject for ExtractedBoxShadow { + type DrawFunctions = DrawBoxShadows; + type ViewPipelineKeyBuilder = UiBoxShadowViewPipelineKeyBuilder; + type ViewQueryData = Option<&'static BoxShadowSamples>; + type SpecializedRenderPipeline = BoxShadowPipeline; + type PipelineKeySystemParam = (); + + fn get_sort_key(&self) -> FloatOrd { + FloatOrd(self.stack_index as f32 + stack_z_offsets::BOX_SHADOW) + } + + fn create_view_pipeline_key_builder<'w, 's>( + box_shadow_samples: Option<&BoxShadowSamples>, + ) -> Self::ViewPipelineKeyBuilder { + UiBoxShadowViewPipelineKeyBuilder { + box_shadow_samples: box_shadow_samples.cloned(), + } + } + + fn create_pipeline_key( + &self, + cached_camera_view: &CachedCameraView, + _: &mut SystemParamItem, + ) -> Option { + Some(BoxShadowPipelineKey { + target_format: cached_camera_view.extracted_view.target_format, + samples: cached_camera_view + .pipeline_key_builder + .box_shadow_samples + .unwrap_or_default() + .0, + }) + } } +/// List of extracted shadows to be sorted and queued for rendering +pub type ExtractedBoxShadows = UiRenderObjects; + pub fn extract_shadows( mut commands: Commands, mut extracted_box_shadows: ResMut, @@ -269,6 +298,7 @@ pub fn extract_shadows( mut nodes_processed_this_frame: Local, ) { nodes_processed_this_frame.clear(); + extracted_box_shadows.changed.clear(); let mut mapping = camera_map.get_mapper(); @@ -281,14 +311,22 @@ pub fn extract_shadows( { let main_entity = MainEntity::from(entity); - // If there were any previous box shadows for this entity, despawn them. - for (render_entity, _) in extracted_box_shadows - .box_shadows - .get_mut(&main_entity) - .iter_mut() - .flat_map(|(_, shadows)| shadows.drain(..)) + // If there were any previous box shadows for this entity, despawn them + // and record them as changed so the render phase entry can be removed. + if let Some((prev_camera_entity, mut shadows)) = + extracted_box_shadows.objects.remove(&main_entity) { - commands.entity(render_entity).despawn(); + let changed = extracted_box_shadows + .changed + .entry(main_entity) + .or_default(); + for (render_entity, _) in shadows.drain(..) { + commands.entity(render_entity).despawn(); + changed.push(ChangedUiObject { + render_entity, + prev_camera_entity, + }); + } } // Skip if no visible shadows @@ -299,7 +337,7 @@ pub fn extract_shadows( let Some(extracted_camera_entity) = mapping.map(camera) else { continue; }; - if let Some((camera_entity, _)) = extracted_box_shadows.box_shadows.get_mut(&main_entity) { + if let Some((camera_entity, _)) = extracted_box_shadows.objects.get_mut(&main_entity) { *camera_entity = extracted_camera_entity; } @@ -347,7 +385,7 @@ pub fn extract_shadows( }; extracted_box_shadows - .box_shadows + .objects .entry(main_entity) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -383,81 +421,31 @@ pub fn extract_shadows( if nodes_processed_this_frame.contains(&main_entity) { continue; } - let Some((_, mut extracted_nodes)) = extracted_box_shadows.box_shadows.remove(&main_entity) + let Some((prev_camera_entity, mut extracted_nodes)) = + extracted_box_shadows.objects.remove(&main_entity) else { continue; }; + let changed = extracted_box_shadows + .changed + .entry(main_entity) + .or_default(); for (render_entity, _) in extracted_nodes.drain(..) { commands.entity(render_entity).despawn(); + changed.push(ChangedUiObject { + render_entity, + prev_camera_entity, + }); } } } -#[expect( - clippy::too_many_arguments, - reason = "it's a system that needs a lot of them" -)] -pub fn queue_shadows( - extracted_box_shadows: ResMut, - box_shadow_pipeline: Res, - mut pipelines: ResMut>, - mut transparent_render_phases: ResMut>, - render_views: Query<(&UiCameraView, Option<&BoxShadowSamples>), With>, - camera_views: Query<&ExtractedView>, - pipeline_cache: Res, - draw_functions: Res>, -) { - let draw_function = draw_functions.read().id::(); - let mut current_camera_entity = Entity::PLACEHOLDER; - let mut current_phase = None; - - for (main_entity, (extracted_camera_entity, extracted_sub_shadows)) in - extracted_box_shadows.box_shadows.iter() - { - if current_camera_entity != *extracted_camera_entity { - current_phase = render_views.get(*extracted_camera_entity).ok().and_then( - |(default_camera_view, shadow_samples)| { - camera_views - .get(default_camera_view.0) - .ok() - .and_then(|view| { - transparent_render_phases - .get_mut(&view.retained_view_entity) - .map(|transparent_phase| { - let pipeline = pipelines.specialize( - &pipeline_cache, - &box_shadow_pipeline, - BoxShadowPipelineKey { - target_format: view.target_format, - samples: shadow_samples.copied().unwrap_or_default().0, - }, - ); - (pipeline, transparent_phase) - }) - }) - }, - ); - current_camera_entity = *extracted_camera_entity; - } - - let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { - continue; - }; - for (entity, extracted_shadow) in extracted_sub_shadows.iter() { - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline: *pipeline, - entity: (*entity, *main_entity), - sort_key: FloatOrd( - extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW, - ), - - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - indexed: true, - }); - } - } +/// Information that the box shadow renderer needs from each view to construct +/// the pipeline key. +pub struct UiBoxShadowViewPipelineKeyBuilder { + /// The number of samples that this view requests to render box shadows + /// with. + box_shadow_samples: Option, } pub fn prepare_shadows( @@ -491,7 +479,7 @@ pub fn prepare_shadows( for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; let Some((extracted_camera_entity, box_shadow)) = extracted_shadows - .box_shadows + .objects .get(&item.main_entity()) .and_then(|(extracted_camera_entity, sub_shadows)| { sub_shadows diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 99aa2ed6ab106..567a84c2524da 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -194,7 +194,7 @@ pub fn extract_debug_overlay( for (entity, uinode, stack_index, transform, visibility, maybe_clip, computed_target, debug) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { let debug_options = debug.copied().unwrap_or((*debug_options.as_ref()).into()); @@ -222,7 +222,7 @@ pub fn extract_debug_overlay( } extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 887a6cebb2d8d..da67ee9fb8b58 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -61,7 +61,7 @@ impl Plugin for GradientPlugin { .add_systems( Render, ( - queue_gradient.in_set(RenderSystems::Queue), + queue_ui_items::.in_set(RenderSystems::Queue), prepare_gradient.in_set(RenderSystems::PrepareBindGroups), ), ); @@ -243,16 +243,50 @@ pub struct ExtractedGradient { pub color_space: InterpolationColorSpace, } -/// A render-world resource that stores all gradients in the scene. -#[derive(Resource, Default)] -pub struct ExtractedGradients { - /// The list of gradients grouped by their main-world entity, along with each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all - /// gradients associated with a main-world entity when it changes. - pub items: MainEntityHashMap<(Entity, EntityIndexMap)>, +impl UiRenderObject for ExtractedGradient { + type DrawFunctions = DrawGradientFns; + type ViewQueryData = Option<&'static UiAntiAlias>; + type SpecializedRenderPipeline = GradientPipeline; + type ViewPipelineKeyBuilder = UiGradientViewPipelineKeyBuilder; + type PipelineKeySystemParam = (); + + fn get_sort_key(&self) -> FloatOrd { + FloatOrd( + self.stack_index as f32 + + match self.node_type { + NodeType::Rect | NodeType::Inverted => stack_z_offsets::GRADIENT, + NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, + }, + ) + } + + fn create_view_pipeline_key_builder<'w, 's>( + anti_alias: Option<&UiAntiAlias>, + ) -> Self::ViewPipelineKeyBuilder { + UiGradientViewPipelineKeyBuilder { + anti_alias: anti_alias.cloned(), + } + } + + fn create_pipeline_key( + &self, + cached_camera_view: &CachedCameraView, + _: &mut SystemParamItem, + ) -> Option { + Some(UiGradientPipelineKey { + anti_alias: matches!( + cached_camera_view.pipeline_key_builder.anti_alias, + None | Some(UiAntiAlias::On) + ), + color_space: self.color_space, + target_format: cached_camera_view.extracted_view.target_format, + }) + } } +/// A render-world resource that stores all gradients in the scene. +pub type ExtractedGradients = UiRenderObjects; + // Interpolate implicit stops (where position is `f32::NAN`) // If the first and last stops are implicit set them to the `min` and `max` values // so that we always have explicit start and end points to interpolate between. @@ -388,6 +422,7 @@ pub fn extract_gradients( mut nodes_processed_this_frame: Local, ) { nodes_processed_this_frame.clear(); + extracted_gradients.changed.clear(); let mut camera_mapper = camera_map.get_mapper(); let mut sorted_stops = vec![]; @@ -405,14 +440,19 @@ pub fn extract_gradients( { let main_entity = MainEntity::from(entity); - // If there were any previous gradients for this entity, despawn them. - for (render_entity, _) in extracted_gradients - .items - .get_mut(&main_entity) - .iter_mut() - .flat_map(|(_, gradients)| gradients.drain(..)) + // If there were any previous gradients for this entity, despawn them + // and record them as changed so the render phase entry can be removed. + if let Some((prev_camera_entity, mut gradients)) = + extracted_gradients.objects.remove(&main_entity) { - commands.entity(render_entity).despawn(); + let changed = extracted_gradients.changed.entry(main_entity).or_default(); + for (render_entity, _) in gradients.drain(..) { + commands.entity(render_entity).despawn(); + changed.push(ChangedUiObject { + render_entity, + prev_camera_entity, + }); + } } // Skip invisible images @@ -423,7 +463,7 @@ pub fn extract_gradients( let Some(extracted_camera_entity) = camera_mapper.map(camera) else { continue; }; - if let Some((camera_entity, _)) = extracted_gradients.items.get_mut(&main_entity) { + if let Some((camera_entity, _)) = extracted_gradients.objects.get_mut(&main_entity) { *camera_entity = extracted_camera_entity; } @@ -455,7 +495,7 @@ pub fn extract_gradients( &mut sorted_stops, ); extracted_gradients - .items + .objects .entry(main_entity) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -496,7 +536,7 @@ pub fn extract_gradients( ); extracted_gradients - .items + .objects .entry(main_entity) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -549,7 +589,7 @@ pub fn extract_gradients( ); extracted_gradients - .items + .objects .entry(main_entity) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -608,7 +648,7 @@ pub fn extract_gradients( interpolate_color_stops(&mut extracted_color_stops, 0., TAU); extracted_gradients - .items + .objects .entry(main_entity) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -657,84 +697,26 @@ pub fn extract_gradients( if nodes_processed_this_frame.contains(&main_entity) { continue; } - let Some((_, mut extracted_nodes)) = extracted_gradients.items.remove(&main_entity) else { + let Some((prev_camera_entity, mut extracted_nodes)) = + extracted_gradients.objects.remove(&main_entity) + else { continue; }; + let changed = extracted_gradients.changed.entry(main_entity).or_default(); for (render_entity, _) in extracted_nodes.drain(..) { commands.entity(render_entity).despawn(); + changed.push(ChangedUiObject { + render_entity, + prev_camera_entity, + }); } } } -#[expect( - clippy::too_many_arguments, - reason = "it's a system that needs a lot of them" -)] -pub fn queue_gradient( - extracted_gradients: ResMut, - gradients_pipeline: Res, - mut pipelines: ResMut>, - mut transparent_render_phases: ResMut>, - render_views: Query<(&UiCameraView, Option<&UiAntiAlias>), With>, - camera_views: Query<&ExtractedView>, - pipeline_cache: Res, - draw_functions: Res>, -) { - let draw_function = draw_functions.read().id::(); - let mut current_camera_entity = Entity::PLACEHOLDER; - let mut current_phase = None; - - for (main_entity, (extracted_camera_entity, sub_gradients)) in extracted_gradients.items.iter() - { - if current_camera_entity != *extracted_camera_entity { - current_phase = render_views.get(*extracted_camera_entity).ok().and_then( - |(default_camera_view, ui_anti_alias)| { - camera_views - .get(default_camera_view.0) - .ok() - .and_then(|view| { - transparent_render_phases - .get_mut(&view.retained_view_entity) - .map(|transparent_phase| { - (view.target_format, ui_anti_alias, transparent_phase) - }) - }) - }, - ); - current_camera_entity = *extracted_camera_entity; - } - - let Some((target_format, ui_anti_alias, transparent_phase)) = current_phase.as_mut() else { - continue; - }; - for (render_entity, gradient) in sub_gradients.iter() { - let pipeline = pipelines.specialize( - &pipeline_cache, - &gradients_pipeline, - UiGradientPipelineKey { - anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), - color_space: gradient.color_space, - target_format: *target_format, - }, - ); - - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline, - entity: (*render_entity, *main_entity), - sort_key: FloatOrd( - gradient.stack_index as f32 - + match gradient.node_type { - NodeType::Rect | NodeType::Inverted => stack_z_offsets::GRADIENT, - NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, - }, - ), - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - indexed: true, - }); - } - } +/// Information from a view needed to construct a pipeline key for gradients. +pub struct UiGradientViewPipelineKeyBuilder { + /// Whether anti-aliasing is requested for UI nodes in this view. + anti_alias: Option, } #[repr(C)] @@ -821,7 +803,7 @@ pub fn prepare_gradient( for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; if let Some(gradient) = extracted_gradients - .items + .objects .get(&item.main_entity()) .and_then(|(_, subgradients)| subgradients.get(&item.entity())) { diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index b8644af1a4314..086333d510cab 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -10,6 +10,9 @@ pub mod box_shadow; mod gradient; mod image; +use bevy_ecs::query::QueryData; +use bevy_render::render_resource::SpecializedRenderPipeline; +use bevy_utils::default; pub use image::ImageNodeAssetChangedSystems; mod pipeline; pub mod render_pass; @@ -45,7 +48,7 @@ use bevy_core_pipeline::schedule::{Core2d, Core2dSystems, Core3d, Core3dSystems} use bevy_core_pipeline::upscaling::upscaling; use bevy_ecs::prelude::*; use bevy_ecs::schedule::IntoScheduleConfigs; -use bevy_ecs::system::SystemParam; +use bevy_ecs::system::{StaticSystemParam, SystemParam, SystemParamItem}; use bevy_image::{prelude::*, TRANSPARENT_IMAGE_HANDLE}; use bevy_math::{proj, Affine2, FloatOrd, Rect, UVec4, Vec2}; use bevy_render::{ @@ -77,6 +80,8 @@ use bevy_transform::components::GlobalTransform; use box_shadow::BoxShadowPlugin; use bytemuck::{Pod, Zeroable}; use core::ops::Range; +use smallvec::SmallVec; +use std::marker::Send; use std::mem; pub use pipeline::*; @@ -279,7 +284,7 @@ impl Plugin for UiRenderPlugin { .add_systems( Render, ( - queue_uinodes.in_set(RenderSystems::Queue), + queue_ui_items::.in_set(RenderSystems::Queue), sort_phase_system::.in_set(RenderSystems::PhaseSort), prepare_uinodes.in_set(RenderSystems::PrepareBindGroups), clear_batches.in_set(RenderSystems::Cleanup), @@ -355,6 +360,40 @@ pub struct ExtractedUiNode { pub transform: Affine2, } +impl UiRenderObject for ExtractedUiNode { + type DrawFunctions = DrawUi; + type ViewPipelineKeyBuilder = UiNodePipelineKeyBuilder; + type ViewQueryData = Option<&'static UiAntiAlias>; + type SpecializedRenderPipeline = UiPipeline; + type PipelineKeySystemParam = (); + + fn get_sort_key(&self) -> FloatOrd { + FloatOrd(self.z_order) + } + + fn create_view_pipeline_key_builder<'w, 's>( + item: <::ReadOnly as QueryData>::Item<'w, 's>, + ) -> Self::ViewPipelineKeyBuilder { + UiNodePipelineKeyBuilder { + anti_alias: item.cloned(), + } + } + + fn create_pipeline_key( + &self, + cached_camera_view: &CachedCameraView, + _: &mut SystemParamItem, + ) -> Option<::Key> { + Some(UiPipelineKey { + target_format: cached_camera_view.extracted_view.target_format, + anti_alias: matches!( + cached_camera_view.pipeline_key_builder.anti_alias, + None | Some(UiAntiAlias::On) + ), + }) + } +} + /// The type of UI node. /// This is used to determine how to render the UI node. #[derive(Clone, Copy, Debug, PartialEq)] @@ -395,17 +434,12 @@ pub struct ExtractedGlyph { /// The list of UI nodes, as well as the set of nodes that changed. /// /// This is a two-level data structure so that we can quickly remove all -/// gradients associated with a main-world entity when it changes. -#[derive(Resource, Default)] -pub struct ExtractedUiNodes { - /// The list of UI nodes grouped by their main-world entity, along with - /// each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all UI - /// nodes associated with a main-world entity when it changes. - pub uinodes: MainEntityHashMap<(Entity, EntityIndexMap)>, - /// UI nodes that changed this frame. - pub changed: MainEntityHashSet, +/// nodes associated with a main-world entity when it changes. +pub type ExtractedUiNodes = UiRenderObjects; + +pub struct ChangedUiObject { + render_entity: Entity, + prev_camera_entity: Entity, } /// A query filter that matches all UI nodes. @@ -664,11 +698,17 @@ pub fn extract_uinode_changes( ) { // Mark the node as changed so that the other `extract_` systems will // know to process it. - extracted_uinodes.changed.insert(main_entity); + let changed_ui_nodes = extracted_uinodes.changed.entry(main_entity).or_default(); - if let Some((_, mut render_entities)) = extracted_uinodes.uinodes.remove(&main_entity) { + if let Some((prev_camera_entity, mut render_entities)) = + extracted_uinodes.objects.remove(&main_entity) + { for (render_entity, _) in render_entities.drain(..) { commands.entity(render_entity).despawn(); + changed_ui_nodes.push(ChangedUiObject { + render_entity, + prev_camera_entity, + }); } } @@ -728,7 +768,7 @@ pub fn extract_uinode_background_colors( maybe_outer_color, ) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip invisible backgrounds @@ -740,7 +780,7 @@ pub fn extract_uinode_background_colors( continue; } - let extracted_sub_uinodes = match extracted_uinodes.uinodes.entry(entity.into()) { + let extracted_sub_uinodes = match extracted_uinodes.objects.entry(entity.into()) { Entry::Occupied(entry) => &mut entry.into_mut().1, Entry::Vacant(entry) => { let Some(extracted_camera_entity) = camera_mapper.map(camera) else { @@ -840,7 +880,7 @@ pub fn extract_uinode_images( image_size, ) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { let visual_box = match image.visual_box { @@ -903,7 +943,7 @@ pub fn extract_uinode_images( }; extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -964,7 +1004,7 @@ pub fn extract_uinode_borders( (maybe_border_color, maybe_outline), ) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip invisible borders and removed nodes @@ -1034,7 +1074,7 @@ pub fn extract_uinode_borders( }; extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1050,7 +1090,7 @@ pub fn extract_uinode_borders( { let outline_size = computed_node.outlined_node_size(); extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1296,7 +1336,7 @@ pub fn extract_viewport_nodes( viewport_node, ) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip invisible images @@ -1320,7 +1360,7 @@ pub fn extract_viewport_nodes( }; extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1391,7 +1431,7 @@ pub fn extract_text_sections( cursor_style, ) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) @@ -1480,7 +1520,7 @@ pub fn extract_text_sections( .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) { extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1541,7 +1581,7 @@ pub fn extract_text_shadows( editable_text, ) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) @@ -1591,7 +1631,7 @@ pub fn extract_text_shadows( || info.atlas_info.texture != atlas_info.texture }) { extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1625,7 +1665,7 @@ pub fn extract_text_shadows( if has_strikethrough { extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1656,7 +1696,7 @@ pub fn extract_text_shadows( if has_underline { extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1731,7 +1771,7 @@ pub fn extract_text_decorations( editable_text, ) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) @@ -1780,7 +1820,7 @@ pub fn extract_text_decorations( if let Some(text_background_color) = text_background_color { extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1815,7 +1855,7 @@ pub fn extract_text_decorations( .to_linear(); extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1851,7 +1891,7 @@ pub fn extract_text_decorations( .to_linear(); extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -1956,61 +1996,107 @@ pub mod shader_flags { pub const INVERT: u32 = 4096; } -pub fn queue_uinodes( - extracted_uinodes: Res, - ui_pipeline: Res, - mut pipelines: ResMut>, +/// Processes changed render objects of a single type, inserting and removing +/// sorted phase items as necessary. +/// +/// This system runs once per frame for each type of render object (normal UI +/// nodes, box shadows, gradients, etc.) It examines the list of changed render +/// nodes and adds and removes phase items as necessary. +pub fn queue_ui_items( + extracted_nodes: Res>, + pipeline: Res, + mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, - render_views: Query<(&UiCameraView, Option<&UiAntiAlias>), With>, - camera_views: Query<&ExtractedView>, + render_views: Query>, + ui_camera_views: Query<&UiCameraView>, + extracted_views: Query<&ExtractedView>, pipeline_cache: Res, draw_functions: Res>, -) { - let draw_function = draw_functions.read().id::(); - let mut current_camera_entity = Entity::PLACEHOLDER; - let mut current_phase = None; - - for (main_entity, (extracted_camera_entity, extracted_sub_uinodes)) in - extracted_uinodes.uinodes.iter() - { - if current_camera_entity != *extracted_camera_entity { - current_phase = render_views.get(*extracted_camera_entity).ok().and_then( - |(default_camera_view, ui_anti_alias)| { - camera_views - .get(default_camera_view.0) - .ok() - .and_then(|view| { - transparent_render_phases - .get_mut(&view.retained_view_entity) - .map(|transparent_phase| { - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_pipeline, - UiPipelineKey { - target_format: view.target_format, - anti_alias: matches!( - ui_anti_alias, - None | Some(UiAntiAlias::On) - ), - }, - ); - (pipeline, transparent_phase) - }) - }) - }, + system_param: StaticSystemParam, +) where + E: UiRenderObject, + ::Key: Send + Sync, +{ + let mut system_param = system_param.into_inner(); + let draw_function = draw_functions.read().id::(); + + // To avoid having to look up information about the camera over and over + // again for each changed render object, we cache the most recent view we + // looked at here. + let mut maybe_cached_camera_view = None; + + // Loop over all changed nodes. + for (main_entity, extracted_sub_uinodes) in extracted_nodes.changed.iter() { + // Examine all changed nodes (which includes nodes that were removed), + // and remove all the corresponding phase items. + for changed_uinode in extracted_sub_uinodes.iter() { + // Refresh the cached camera view. + CachedCameraView::::update::( + &mut maybe_cached_camera_view, + changed_uinode.prev_camera_entity, + &render_views, + &ui_camera_views, + &extracted_views, ); - current_camera_entity = *extracted_camera_entity; + let Some(ref mut cached_camera_view) = maybe_cached_camera_view else { + continue; + }; + + // Fetch the transparent render phase, and remove the appropriate + // phase items from it. + let Some(transparent_render_phase) = transparent_render_phases + .get_mut(&cached_camera_view.extracted_view.retained_view_entity) + else { + continue; + }; + transparent_render_phase.remove(changed_uinode.render_entity, *main_entity); } - let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { + // If the UI node no longer exists, stop here. + let Some((extracted_camera_entity, extracted_sub_uinodes)) = + extracted_nodes.objects.get(main_entity) + else { continue; }; + + // Now look at all the changed UI nodes again. For each, add the + // appropriate render objects of this type. for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() { - transparent_phase.add_transient(TransparentUi { + // Refresh the cached camera view. + CachedCameraView::::update::( + &mut maybe_cached_camera_view, + *extracted_camera_entity, + &render_views, + &ui_camera_views, + &extracted_views, + ); + let Some(ref mut cached_camera_view) = maybe_cached_camera_view else { + continue; + }; + + // Fetch the transparent render phase. + let Some(transparent_render_phase) = transparent_render_phases + .get_mut(&cached_camera_view.extracted_view.retained_view_entity) + else { + continue; + }; + + // Get the pipeline key, and specialize the pipeline. We need a + // pipeline in order to construct a `TransparentUi` phase item. + let Some(pipeline_key) = + extracted_uinode.create_pipeline_key(cached_camera_view, &mut system_param) + else { + continue; + }; + let pipeline = pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key); + + // Add the phase item. Note that this phase item will be retained + // from frame to frame. + transparent_render_phase.add_retained(TransparentUi { draw_function, - pipeline: *pipeline, + pipeline, entity: (*render_entity, *main_entity), - sort_key: FloatOrd(extracted_uinode.z_order), + sort_key: extracted_uinode.get_sort_key(), // batch_range will be calculated in prepare_uinodes batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, @@ -2020,6 +2106,13 @@ pub fn queue_uinodes( } } +/// Information from the view necessary to construct the pipeline key for a +/// plain UI node. +pub struct UiNodePipelineKeyBuilder { + /// Whether anti-aliasing is requested for UI nodes in this view. + anti_alias: Option, +} + #[derive(Resource, Default)] pub struct ImageNodeBindGroups { pub values: HashMap, BindGroup>, @@ -2075,7 +2168,7 @@ pub fn prepare_uinodes( for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; let Some(extracted_uinode) = extracted_uinodes - .uinodes + .objects .get(&item.main_entity()) .and_then(|(_, sub_uinodes)| sub_uinodes.get(&item.entity())) else { @@ -2447,3 +2540,138 @@ pub fn clear_batches(mut commands: Commands, batches_query: Query(); } } + +/// A render-world resource that holds all UI render objects of a single type. +/// +/// Types of UI render objects include normal UI nodes, box shadows, gradients, +/// texture slices, and so forth. +/// +/// This resource is retained from frame to frame. The various `extract_` +/// systems keep it up to date. +#[derive(Resource)] +pub struct UiRenderObjects +where + E: UiRenderObject, +{ + /// The list of UI objects grouped by their main-world entity, along with + /// each group's target camera entity. + /// + /// This is a two-level data structure so that we can quickly remove all UI + /// render objects associated with a main-world entity when it changes. + pub objects: MainEntityHashMap<(Entity, EntityIndexMap)>, + + /// UI render objects that changed this frame. + pub changed: MainEntityHashMap>, +} + +impl Default for UiRenderObjects +where + RO: UiRenderObject, +{ + fn default() -> UiRenderObjects { + UiRenderObjects { + objects: default(), + changed: default(), + } + } +} + +/// A piece of a UI node that renders using a specific shader and can enqueue +/// the phase items necessary to render itself. +/// +/// Types of UI render objects include normal UI nodes, box shadows, gradients, +/// texture slices, and so forth. +pub trait UiRenderObject: Send + Sync + 'static { + /// The set of draw functions that draw render objects of this type. + type DrawFunctions: 'static; + + /// A type that stores all information taken from a view that will be needed + /// to create the pipeline key for objects of this type. + /// + /// If no such data is required, this can be `()`. + type ViewPipelineKeyBuilder: Sized; + + /// ECS data from the [`ExtractedView`] needed to construct the + /// [`Self::ViewPipelineKeyBuilder`]. + /// + /// If no such data is required, this can be `()`. + type ViewQueryData: QueryData; + + /// The shader pipeline that renders UI objects of this type. + type SpecializedRenderPipeline: SpecializedRenderPipeline + Resource; + + /// Any extra system data needed in order to create a pipeline key. + type PipelineKeySystemParam: SystemParam; + + /// Returns the sort order for this render object. + fn get_sort_key(&self) -> FloatOrd; + + /// Extracts whatever render-world data is necessary to construct the + /// pipeline key from a single view. + fn create_view_pipeline_key_builder<'w, 's>( + view: <::ReadOnly as QueryData>::Item<'w, 's>, + ) -> Self::ViewPipelineKeyBuilder; + + /// Creates the pipeline key needed to render this object from the + /// information fetched in [`Self::create_view_pipeline_key_builder`]. + fn create_pipeline_key( + &self, + cached_camera_view: &CachedCameraView, + system_param: &mut SystemParamItem, + ) -> Option<::Key>; +} + +/// Information about a single view that [`queue_ui_items`] caches. +/// +/// All information needed to construct the pipeline key must be in this structure. +pub struct CachedCameraView<'w, PKB> { + /// The render-world entity of the current camera. + current_camera_entity: Entity, + + /// The [`ExtractedView`] structure corresponding to that camera. + extracted_view: &'w ExtractedView, + + /// Any render-object-specific data needed to construct a pipeline key. + /// + /// The [`UiRenderObject::ViewPipelineKeyBuilder`] specifies this type. + pipeline_key_builder: PKB, +} + +impl<'w, PKB> CachedCameraView<'w, PKB> { + fn update( + maybe_self: &'_ mut Option>, + this_camera_entity: Entity, + render_views: &'w Query>, + ui_camera_views: &'w Query<&UiCameraView>, + extracted_views: &'w Query<&ExtractedView>, + ) where + E: UiRenderObject, + { + if maybe_self.as_ref().is_some_and(|cached_camera_view| { + cached_camera_view.current_camera_entity == this_camera_entity + }) { + return; + } + + let maybe_current_view = + ui_camera_views + .get(this_camera_entity) + .ok() + .and_then(|default_camera_view| { + let view = extracted_views.get(default_camera_view.0).ok()?; + let pipeline_key_builder = render_views.get(default_camera_view.0).ok()?; + Some((view, pipeline_key_builder)) + }); + + let Some((extracted_view, pipeline_key_builder_item)) = maybe_current_view else { + *maybe_self = None; + return; + }; + + *maybe_self = Some(CachedCameraView { + current_camera_entity: this_camera_entity, + extracted_view, + pipeline_key_builder: E::create_view_pipeline_key_builder(pipeline_key_builder_item), + }); + } +} diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index b5a41c6163edb..d39c8a598c081 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -54,7 +54,7 @@ pub fn extract_text_cursor( editable_text, ) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| text_node_query.get(main_entity.entity()).ok()) { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) @@ -148,7 +148,7 @@ pub fn extract_text_cursor( } extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -183,7 +183,7 @@ pub fn extract_text_cursor( && *rwmode != TextReadWriteMode::Static { extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -251,7 +251,7 @@ pub fn extract_preedit_underlines( editable_text, ) in extracted_uinodes .changed - .iter() + .keys() .flat_map(|main_entity| text_node_query.get(main_entity.entity()).ok()) { if !inherited_visibility.get() @@ -278,7 +278,7 @@ pub fn extract_preedit_underlines( for rect in text_layout_info.preedit_underline_rects.iter() { extracted_uinodes - .uinodes + .objects .entry(entity.into()) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index b04757cb866df..2cd7c452682ce 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -2,7 +2,7 @@ use crate::ui_material::{MaterialNode, UiMaterial, UiMaterialKey}; use crate::*; use bevy_asset::*; use bevy_ecs::{ - prelude::{Component, With}, + prelude::Component, query::ROQueryItem, system::{ lifetimeless::{Read, SRes}, @@ -66,7 +66,7 @@ where .add_systems( Render, ( - queue_ui_material_nodes::.in_set(RenderSystems::Queue), + queue_ui_items::>.in_set(RenderSystems::Queue), prepare_uimaterial_nodes::.in_set(RenderSystems::PrepareBindGroups), ), ); @@ -302,25 +302,40 @@ pub struct ExtractedUiMaterialNode { } /// A render-world resource that stores all material nodes in the scene. -#[derive(Resource)] -pub struct ExtractedUiMaterialNodes { - /// The list of material nodes grouped by their main-world entity, along with - /// each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all - /// material nodes associated with a main-world entity when it changes. - pub uinodes: MainEntityHashMap<(Entity, EntityIndexMap>)>, -} +pub type ExtractedUiMaterialNodes = UiRenderObjects>; -impl Default for ExtractedUiMaterialNodes { - fn default() -> Self { - Self { - uinodes: Default::default(), - } +impl UiRenderObject for ExtractedUiMaterialNode +where + M: UiMaterial, + M::Data: PartialEq + Eq + Hash + Clone, +{ + type DrawFunctions = DrawUiMaterial; + type ViewPipelineKeyBuilder = (); + type ViewQueryData = (); + type SpecializedRenderPipeline = UiMaterialPipeline; + type PipelineKeySystemParam = Res<'static, RenderAssets>>; + + fn get_sort_key(&self) -> FloatOrd { + FloatOrd(self.stack_index as f32 + M::stack_z_offset()) + } + + fn create_view_pipeline_key_builder<'w, 's>(_: ()) {} + + fn create_pipeline_key( + &self, + cached_camera_view: &CachedCameraView, + render_materials: &mut SystemParamItem, + ) -> Option<::Key> { + render_materials + .get(self.material) + .map(|material| UiMaterialKey { + target_format: cached_camera_view.extracted_view.target_format, + bind_group_data: material.key.clone(), + }) } } -pub fn extract_ui_material_nodes( +pub fn extract_ui_material_nodes( mut commands: Commands, mut extracted_uinodes: ResMut>, materials: Extract>>, @@ -367,8 +382,12 @@ pub fn extract_ui_material_nodes( ), mut nodes_to_reextract_next_frame: Local, mut nodes_processed_this_frame: Local, -) { +) where + M: UiMaterial, + M::Data: PartialEq + Eq + Hash + Clone, +{ nodes_processed_this_frame.clear(); + extracted_uinodes.changed.clear(); let mut camera_mapper = camera_map.get_mapper(); let nodes_to_reextract = mem::take(&mut *nodes_to_reextract_next_frame); @@ -394,14 +413,20 @@ pub fn extract_ui_material_nodes( if nodes_processed_this_frame.contains(&main_entity) { continue; } - // If there were any previous UI nodes for this entity, despawn them. - for (render_entity, _) in extracted_uinodes - .uinodes - .get_mut(&main_entity) - .iter_mut() - .flat_map(|(_, nodes)| nodes.drain(..)) + // If there were any previous UI nodes for this entity, despawn them + // and record them as changed so the render phase entry can be + // removed. + if let Some((prev_camera_entity, mut nodes)) = + extracted_uinodes.objects.remove(&main_entity) { - commands.entity(render_entity).despawn(); + let changed = extracted_uinodes.changed.entry(main_entity).or_default(); + for (render_entity, _) in nodes.drain(..) { + commands.entity(render_entity).despawn(); + changed.push(ChangedUiObject { + render_entity, + prev_camera_entity, + }); + } } // skip invisible nodes @@ -419,14 +444,14 @@ pub fn extract_ui_material_nodes( let Some(extracted_camera_entity) = camera_mapper.map(camera) else { continue; }; - if let Some((camera_entity, _)) = extracted_uinodes.uinodes.get_mut(&main_entity) { + if let Some((camera_entity, _)) = extracted_uinodes.objects.get_mut(&main_entity) { *camera_entity = extracted_camera_entity; } nodes_processed_this_frame.insert(main_entity); extracted_uinodes - .uinodes + .objects .entry(main_entity) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -463,16 +488,23 @@ pub fn extract_ui_material_nodes( if nodes_processed_this_frame.contains(&main_entity) { continue; } - let Some((_, mut extracted_nodes)) = extracted_uinodes.uinodes.remove(&main_entity) else { + let Some((prev_camera_entity, mut extracted_nodes)) = + extracted_uinodes.objects.remove(&main_entity) + else { continue; }; + let changed = extracted_uinodes.changed.entry(main_entity).or_default(); for (render_entity, _) in extracted_nodes.drain(..) { commands.entity(render_entity).despawn(); + changed.push(ChangedUiObject { + render_entity, + prev_camera_entity, + }); } } } -pub fn prepare_uimaterial_nodes( +pub fn prepare_uimaterial_nodes( mut commands: Commands, render_device: Res, render_queue: Res, @@ -484,7 +516,10 @@ pub fn prepare_uimaterial_nodes( ui_material_pipeline: Res>, mut phases: ResMut>, mut previous_len: Local, -) { +) where + M: UiMaterial, + M::Data: PartialEq + Eq + Hash + Clone, +{ if let (Some(view_binding), Some(globals_binding)) = ( view_uniforms.uniforms.binding(), globals_buffer.buffer.binding(), @@ -506,7 +541,7 @@ pub fn prepare_uimaterial_nodes( 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 + .objects .get(&item.main_entity()) .and_then(|(_, subnodes)| subnodes.get(&item.entity())) { @@ -684,77 +719,3 @@ impl RenderAsset for PreparedUiMaterial { } } } - -pub fn queue_ui_material_nodes( - extracted_uinodes: Res>, - draw_functions: Res>, - ui_material_pipeline: Res>, - mut pipelines: ResMut>>, - pipeline_cache: Res, - render_materials: Res>>, - mut transparent_render_phases: ResMut>, - render_views: Query<&UiCameraView, With>, - camera_views: Query<&ExtractedView>, -) where - M::Data: PartialEq + Eq + Hash + Clone, -{ - let draw_function = draw_functions.read().id::>(); - let mut current_camera_entity = Entity::PLACEHOLDER; - let mut current_phase = None; - - for (main_entity, (extracted_camera_entity, extracted_sub_uinodes)) in - extracted_uinodes.uinodes.iter() - { - if current_camera_entity != *extracted_camera_entity { - current_phase = - render_views - .get(*extracted_camera_entity) - .ok() - .and_then(|default_camera_view| { - camera_views - .get(default_camera_view.0) - .ok() - .and_then(|view| { - transparent_render_phases - .get_mut(&view.retained_view_entity) - .map(|transparent_phase| { - (view.target_format, transparent_phase) - }) - }) - }); - current_camera_entity = *extracted_camera_entity; - } - - let Some((target_format, transparent_phase)) = current_phase.as_mut() else { - continue; - }; - for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() { - let Some(material) = render_materials.get(extracted_uinode.material) else { - continue; - }; - - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_material_pipeline, - UiMaterialKey { - target_format: *target_format, - bind_group_data: material.key.clone(), - }, - ); - if transparent_phase.items.capacity() < extracted_uinodes.uinodes.len() { - transparent_phase.items.reserve_exact( - extracted_uinodes.uinodes.len() - transparent_phase.items.capacity(), - ); - } - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline, - entity: (*render_entity, *main_entity), - sort_key: FloatOrd(extracted_uinode.stack_index as f32 + M::stack_z_offset()), - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - indexed: false, - }); - } - } -} diff --git a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs index 4c5d5bfa3c52e..9e4cf1fb82877 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -54,7 +54,7 @@ impl Plugin for UiTextureSlicerPlugin { .add_systems( Render, ( - queue_ui_slices.in_set(RenderSystems::Queue), + queue_ui_items::.in_set(RenderSystems::Queue), prepare_ui_slices.in_set(RenderSystems::PrepareBindGroups), ), ); @@ -204,17 +204,33 @@ pub struct ExtractedUiTextureSlice { pub inverse_scale_factor: f32, } -/// A render-world resource that stores all texture slices in the scene. -#[derive(Resource, Default)] -pub struct ExtractedUiTextureSlices { - /// The list of texture slices grouped by their main-world entity, along with - /// each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all - /// texture slices associated with a main-world entity when it changes. - pub slices: MainEntityHashMap<(Entity, EntityIndexMap)>, +impl UiRenderObject for ExtractedUiTextureSlice { + type DrawFunctions = DrawUiTextureSlices; + type ViewQueryData = (); + type SpecializedRenderPipeline = UiTextureSlicePipeline; + type ViewPipelineKeyBuilder = (); + type PipelineKeySystemParam = (); + + fn get_sort_key(&self) -> FloatOrd { + FloatOrd(self.stack_index as f32 + stack_z_offsets::IMAGE) + } + + fn create_view_pipeline_key_builder<'w, 's>(_: ()) {} + + fn create_pipeline_key( + &self, + cached_camera_view: &CachedCameraView, + _: &mut SystemParamItem, + ) -> Option<::Key> { + Some(UiTextureSlicePipelineKey { + target_format: cached_camera_view.extracted_view.target_format, + }) + } } +/// A render-world resource that stores all texture slices in the scene. +pub type ExtractedUiTextureSlices = UiRenderObjects; + pub fn extract_ui_texture_slices( mut commands: Commands, mut extracted_ui_slicers: ResMut, @@ -280,6 +296,7 @@ pub fn extract_ui_texture_slices( mut nodes_processed_this_frame: Local, ) { nodes_processed_this_frame.clear(); + extracted_ui_slicers.changed.clear(); let mut camera_mapper = camera_map.get_mapper(); for (entity, uinode, stack_index, transform, inherited_visibility, clip, camera, image) in @@ -291,14 +308,19 @@ pub fn extract_ui_texture_slices( { let main_entity = MainEntity::from(entity); - // If there were any previous UI slices for this entity, despawn them. - for (render_entity, _) in extracted_ui_slicers - .slices - .get_mut(&main_entity) - .iter_mut() - .flat_map(|(_, slices)| slices.drain(..)) + // If there were any previous UI slices for this entity, despawn them + // and record them as changed so the render phase entry can be removed. + if let Some((prev_camera_entity, mut slices)) = + extracted_ui_slicers.objects.remove(&main_entity) { - commands.entity(render_entity).despawn(); + let changed = extracted_ui_slicers.changed.entry(main_entity).or_default(); + for (render_entity, _) in slices.drain(..) { + commands.entity(render_entity).despawn(); + changed.push(ChangedUiObject { + render_entity, + prev_camera_entity, + }); + } } let visual_box = match image.visual_box { @@ -333,7 +355,7 @@ pub fn extract_ui_texture_slices( let Some(extracted_camera_entity) = camera_mapper.map(camera) else { continue; }; - if let Some((camera_entity, _)) = extracted_ui_slicers.slices.get_mut(&main_entity) { + if let Some((camera_entity, _)) = extracted_ui_slicers.objects.get_mut(&main_entity) { *camera_entity = extracted_camera_entity; } @@ -357,7 +379,7 @@ pub fn extract_ui_texture_slices( }; extracted_ui_slicers - .slices + .objects .entry(main_entity) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 @@ -398,74 +420,17 @@ pub fn extract_ui_texture_slices( if nodes_processed_this_frame.contains(&main_entity) { continue; } - let Some((_, mut extracted_nodes)) = extracted_ui_slicers.slices.remove(&main_entity) + let Some((prev_camera_entity, mut extracted_nodes)) = + extracted_ui_slicers.objects.remove(&main_entity) else { continue; }; + let changed = extracted_ui_slicers.changed.entry(main_entity).or_default(); for (render_entity, _) in extracted_nodes.drain(..) { commands.entity(render_entity).despawn(); - } - } -} - -#[expect( - clippy::too_many_arguments, - reason = "it's a system that needs a lot of them" -)] -pub fn queue_ui_slices( - extracted_ui_slicers: ResMut, - ui_slicer_pipeline: Res, - mut pipelines: ResMut>, - mut transparent_render_phases: ResMut>, - render_views: Query<&UiCameraView, With>, - camera_views: Query<&ExtractedView>, - pipeline_cache: Res, - draw_functions: Res>, -) { - let draw_function = draw_functions.read().id::(); - let mut current_camera_entity = Entity::PLACEHOLDER; - let mut current_phase = None; - - for (main_entity, (extracted_camera_entity, subslices)) in extracted_ui_slicers.slices.iter() { - if current_camera_entity != *extracted_camera_entity { - current_phase = - render_views - .get(*extracted_camera_entity) - .ok() - .and_then(|default_camera_view| { - camera_views - .get(default_camera_view.0) - .ok() - .and_then(|view| { - transparent_render_phases - .get_mut(&view.retained_view_entity) - .map(|transparent_phase| { - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_slicer_pipeline, - UiTextureSlicePipelineKey { - target_format: view.target_format, - }, - ); - (pipeline, transparent_phase) - }) - }) - }); - current_camera_entity = *extracted_camera_entity; - } - - let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { - continue; - }; - for (render_entity, extracted_slicer) in subslices.iter() { - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline: *pipeline, - entity: (*render_entity, *main_entity), - sort_key: FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::IMAGE), - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - indexed: true, + changed.push(ChangedUiObject { + render_entity, + prev_camera_entity, }); } } @@ -522,7 +487,7 @@ 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 + .objects .get(&item.main_entity()) .and_then(|(_, subslices)| subslices.get(&item.entity())) { From 0227e88e71d93333e0f5bbafc9aa7b4eb0ba7aef Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sat, 8 Aug 2026 22:07:55 -0700 Subject: [PATCH 2/6] Add a `TEXT_SHADOW` layer. Otherwise the `button` example puts the shadow in the wrong place. --- crates/bevy_ui_render/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 05c1012a89f7f..b70943b79d9db 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -123,6 +123,7 @@ pub mod stack_z_offsets { pub const BORDER_GRADIENT: f32 = 0.03; pub const IMAGE: f32 = 0.04; pub const MATERIAL: f32 = 0.05; + pub const TEXT_SHADOW: f32 = 0.0525; pub const TEXT_SELECTION: f32 = 0.055; pub const TEXT: f32 = 0.06; pub const TEXT_STRIKETHROUGH: f32 = 0.07; @@ -1639,7 +1640,7 @@ pub fn extract_text_shadows( commands.spawn_empty().id(), ExtractedUiNode { transform: node_transform, - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SHADOW, image: atlas_info.texture, clip, item: ExtractedUiItem::Glyphs { @@ -1672,7 +1673,7 @@ pub fn extract_text_shadows( .insert( commands.spawn_empty().id(), ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SHADOW, clip, image: AssetId::default(), transform: node_transform From 7c036b5de1b9f6fac6d4000f05af496c205dad20 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sun, 9 Aug 2026 10:49:17 -0700 Subject: [PATCH 3/6] Add newly spawned items to `changed` --- crates/bevy_ui_render/src/box_shadow.rs | 4 +- crates/bevy_ui_render/src/debug_overlay.rs | 65 +++---- crates/bevy_ui_render/src/gradient.rs | 174 ++++++++---------- crates/bevy_ui_render/src/lib.rs | 137 ++++++++------ crates/bevy_ui_render/src/text.rs | 157 ++++++++-------- .../src/ui_material_pipeline.rs | 39 ++-- .../src/ui_texture_slice_pipeline.rs | 49 +++-- 7 files changed, 318 insertions(+), 307 deletions(-) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 4566d8bf3c616..dcf49592ca9f7 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -324,7 +324,7 @@ pub fn extract_shadows( commands.entity(render_entity).despawn(); changed.push(ChangedUiObject { render_entity, - prev_camera_entity, + camera_entity: prev_camera_entity, }); } } @@ -434,7 +434,7 @@ pub fn extract_shadows( commands.entity(render_entity).despawn(); changed.push(ChangedUiObject { render_entity, - prev_camera_entity, + camera_entity: prev_camera_entity, }); } } diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 567a84c2524da..5ddd08669da1f 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -15,6 +15,7 @@ use bevy_ecs::prelude::ReflectComponent; use bevy_ecs::prelude::ReflectResource; use bevy_ecs::resource::Resource; use bevy_ecs::system::Commands; +use bevy_ecs::system::Local; use bevy_ecs::system::Query; use bevy_ecs::system::Res; use bevy_ecs::system::ResMut; @@ -22,6 +23,7 @@ use bevy_math::Affine2; use bevy_math::Rect; use bevy_math::Vec2; use bevy_reflect::Reflect; +use bevy_render::sync_world::MainEntityHashSet; use bevy_render::Extract; use bevy_sprite::BorderRect; use bevy_ui::ui_transform::UiGlobalTransform; @@ -187,15 +189,17 @@ pub fn extract_debug_overlay( >, ui_stack: Extract>, camera_map: Extract, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + for (entity, uinode, stack_index, transform, visibility, maybe_clip, computed_target, debug) in - extracted_uinodes - .changed - .keys() - .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) + changed_entities + .drain() + .filter_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { let debug_options = debug.copied().unwrap_or((*debug_options.as_ref()).into()); if !debug_options.enabled { @@ -221,36 +225,33 @@ pub fn extract_debug_overlay( return; } - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - // Keep all overlays above UI, and nudge each type slightly in Z so ordering is stable. - z_order, - clip: maybe_clip - .filter(|_| !debug_options.show_clipped) - .map(|clip| clip.clip), - image: AssetId::default(), - transform: transform * Affine2::from_translation(rect.center()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: rect.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border, - border_radius: radius, - node_type: NodeType::Border(shader_flags::BORDER_ALL), + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + // Keep all overlays above UI, and nudge each type slightly in Z so ordering is stable. + z_order, + clip: maybe_clip + .filter(|_| !debug_options.show_clipped) + .map(|clip| clip.clip), + image: AssetId::default(), + transform: transform * Affine2::from_translation(rect.center()), + item: ExtractedUiItem::Node { + color, + rect: Rect { + min: Vec2::ZERO, + max: rect.size(), }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border, + border_radius: radius, + node_type: NodeType::Border(shader_flags::BORDER_ALL), }, - ); + }, + ); }; let border_box = Rect::from_center_size(Vec2::ZERO, uinode.size); diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 235ba5326c11b..766b9506bc6de 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -466,7 +466,7 @@ pub fn extract_gradients( commands.entity(render_entity).despawn(); changed.push(ChangedUiObject { render_entity, - prev_camera_entity, + camera_entity: prev_camera_entity, }); } } @@ -510,29 +510,26 @@ pub fn extract_gradients( target.physical_size().as_vec2(), &mut sorted_stops, ); - extracted_gradients - .objects - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedGradient { - stack_index: stack_index.0, - transform: transform.into(), - stops: extracted_stops, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Linear { angle: 0.0 }, - color_space: gradient.get_color_space(), + extracted_gradients.add( + &mut commands, + main_entity, + extracted_camera_entity, + ExtractedGradient { + stack_index: stack_index.0, + transform: transform.into(), + stops: extracted_stops, + rect: Rect { + min: Vec2::ZERO, + max: uinode.size, }, - ); + clip: clip.map(|clip| clip.clip), + node_type, + border_radius: uinode.border_radius, + border: uinode.border, + resolved_gradient: ResolvedGradient::Linear { angle: 0.0 }, + color_space: gradient.get_color_space(), + }, + ); continue; } match gradient { @@ -551,29 +548,26 @@ pub fn extract_gradients( &mut sorted_stops, ); - extracted_gradients - .objects - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedGradient { - stack_index: stack_index.0, - transform: transform.into(), - stops: extracted_stops, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Linear { angle: *angle }, - color_space: *color_space, + extracted_gradients.add( + &mut commands, + main_entity, + extracted_camera_entity, + ExtractedGradient { + stack_index: stack_index.0, + transform: transform.into(), + stops: extracted_stops, + rect: Rect { + min: Vec2::ZERO, + max: uinode.size, }, - ); + clip: clip.map(|clip| clip.clip), + node_type, + border_radius: uinode.border_radius, + border: uinode.border, + resolved_gradient: ResolvedGradient::Linear { angle: *angle }, + color_space: *color_space, + }, + ); } Gradient::Radial(RadialGradient { color_space, @@ -604,29 +598,26 @@ pub fn extract_gradients( &mut sorted_stops, ); - extracted_gradients - .objects - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedGradient { - stack_index: stack_index.0, - transform: transform.into(), - stops: computed_stops, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Radial { center: c, size }, - color_space: *color_space, + extracted_gradients.add( + &mut commands, + main_entity, + extracted_camera_entity, + ExtractedGradient { + stack_index: stack_index.0, + transform: transform.into(), + stops: computed_stops, + rect: Rect { + min: Vec2::ZERO, + max: uinode.size, }, - ); + clip: clip.map(|clip| clip.clip), + node_type, + border_radius: uinode.border_radius, + border: uinode.border, + resolved_gradient: ResolvedGradient::Radial { center: c, size }, + color_space: *color_space, + }, + ); } Gradient::Conic(ConicGradient { color_space, @@ -663,32 +654,29 @@ pub fn extract_gradients( interpolate_color_stops(&mut extracted_color_stops, 0., TAU); - extracted_gradients - .objects - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedGradient { - stack_index: stack_index.0, - transform: transform.into(), - stops: extracted_color_stops, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Conic { - start: *start, - center: g_start, - }, - color_space: *color_space, + extracted_gradients.add( + &mut commands, + main_entity, + extracted_camera_entity, + ExtractedGradient { + stack_index: stack_index.0, + transform: transform.into(), + stops: extracted_color_stops, + rect: Rect { + min: Vec2::ZERO, + max: uinode.size, }, - ); + clip: clip.map(|clip| clip.clip), + node_type, + border_radius: uinode.border_radius, + border: uinode.border, + resolved_gradient: ResolvedGradient::Conic { + start: *start, + center: g_start, + }, + color_space: *color_space, + }, + ); } } } @@ -722,7 +710,7 @@ pub fn extract_gradients( commands.entity(render_entity).despawn(); changed.push(ChangedUiObject { render_entity, - prev_camera_entity, + camera_entity: prev_camera_entity, }); } } diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index b70943b79d9db..773bcfe6d922f 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -70,7 +70,7 @@ pub use debug_overlay::{GlobalUiDebugOptions, UiDebugOptions}; use gradient::GradientPlugin; -use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet}; +use bevy_platform::collections::{HashMap, HashSet}; use bevy_text::{ ComputedTextBlock, EditableText, PositionedGlyph, Strikethrough, StrikethroughColor, TextBackgroundColor, TextColor, TextCursorStyle, TextLayoutInfo, TextSpan, Underline, @@ -438,9 +438,10 @@ pub struct ExtractedGlyph { /// nodes associated with a main-world entity when it changes. pub type ExtractedUiNodes = UiRenderObjects; +#[derive(Clone, Copy, PartialEq, Eq, Hash)] pub struct ChangedUiObject { render_entity: Entity, - prev_camera_entity: Entity, + camera_entity: Entity, } /// A query filter that matches all UI nodes. @@ -708,7 +709,7 @@ pub fn extract_uinode_changes( commands.entity(render_entity).despawn(); changed_ui_nodes.push(ChangedUiObject { render_entity, - prev_camera_entity, + camera_entity: prev_camera_entity, }); } } @@ -753,10 +754,13 @@ pub fn extract_uinode_background_colors( )>, >, camera_map: Extract, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + for ( entity, uinode, @@ -767,10 +771,9 @@ pub fn extract_uinode_background_colors( camera, background_color, maybe_outer_color, - ) in extracted_uinodes - .changed - .keys() - .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) + ) in changed_entities + .drain() + .filter_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip invisible backgrounds if !inherited_visibility.get() @@ -781,21 +784,15 @@ pub fn extract_uinode_background_colors( continue; } - let extracted_sub_uinodes = match extracted_uinodes.objects.entry(entity.into()) { - Entry::Occupied(entry) => &mut entry.into_mut().1, - Entry::Vacant(entry) => { - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; - }; - &mut entry - .insert((extracted_camera_entity, Default::default())) - .1 - } + let Some(extracted_camera_entity) = camera_mapper.map(camera) else { + continue; }; if !background_color.is_fully_transparent() { - extracted_sub_uinodes.insert( - commands.spawn_empty().id(), + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, ExtractedUiNode { z_order: stack_index.0 as f32 + stack_z_offsets::BACKGROUND_COLOR, clip: clip.map(|clip| clip.clip), @@ -821,8 +818,10 @@ pub fn extract_uinode_background_colors( if let Some(outer_color) = maybe_outer_color && !outer_color.0.is_fully_transparent() { - extracted_sub_uinodes.insert( - commands.spawn_empty().id(), + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, ExtractedUiNode { z_order: stack_index.0 as f32 + stack_z_offsets::BACKGROUND_COLOR, clip: clip.map(|clip| clip.clip), @@ -865,10 +864,13 @@ pub fn extract_uinode_images( )>, >, camera_map: Extract, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + for ( entity, uinode, @@ -879,9 +881,8 @@ pub fn extract_uinode_images( camera, image, image_size, - ) in extracted_uinodes - .changed - .keys() + ) in changed_entities + .drain() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { let visual_box = match image.visual_box { @@ -943,31 +944,28 @@ pub fn extract_uinode_images( None }; - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::IMAGE, - clip: clip.map(|clip| clip.clip), - image: image.image.id(), - transform: Affine2::from(*transform) - * Affine2::from_translation(visual_box.center()), - item: ExtractedUiItem::Node { - color: image.color.into(), - rect, - atlas_scaling, - flip_x: image.flip_x, - flip_y: image.flip_y, - border: BorderRect::ZERO, - border_radius: uinode.border_radius, - node_type: NodeType::Rect, - }, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::IMAGE, + clip: clip.map(|clip| clip.clip), + image: image.image.id(), + transform: Affine2::from(*transform) + * Affine2::from_translation(visual_box.center()), + item: ExtractedUiItem::Node { + color: image.color.into(), + rect, + atlas_scaling, + flip_x: image.flip_x, + flip_y: image.flip_y, + border: BorderRect::ZERO, + border_radius: uinode.border_radius, + node_type: NodeType::Rect, }, - ); + }, + ); } } @@ -2013,6 +2011,7 @@ pub fn queue_ui_items( extracted_views: Query<&ExtractedView>, pipeline_cache: Res, draw_functions: Res>, + mut processed_ui_objects: Local>, system_param: StaticSystemParam, ) where E: UiRenderObject, @@ -2027,14 +2026,19 @@ pub fn queue_ui_items( let mut maybe_cached_camera_view = None; // Loop over all changed nodes. - for (main_entity, extracted_sub_uinodes) in extracted_nodes.changed.iter() { + for (main_entity, extracted_sub_ui_objects) in extracted_nodes.changed.iter() { // Examine all changed nodes (which includes nodes that were removed), // and remove all the corresponding phase items. - for changed_uinode in extracted_sub_uinodes.iter() { + processed_ui_objects.clear(); + for changed_ui_object in extracted_sub_ui_objects.iter() { + if !processed_ui_objects.insert(*changed_ui_object) { + continue; + } + // Refresh the cached camera view. CachedCameraView::::update::( &mut maybe_cached_camera_view, - changed_uinode.prev_camera_entity, + changed_ui_object.camera_entity, &render_views, &ui_camera_views, &extracted_views, @@ -2050,7 +2054,7 @@ pub fn queue_ui_items( else { continue; }; - transparent_render_phase.remove(changed_uinode.render_entity, *main_entity); + transparent_render_phase.remove(changed_ui_object.render_entity, *main_entity); } // If the UI node no longer exists, stop here. @@ -2577,6 +2581,35 @@ where } } +impl UiRenderObjects +where + E: UiRenderObject, +{ + pub fn add( + &mut self, + commands: &mut Commands, + main_entity: MainEntity, + extracted_camera_entity: Entity, + object: E, + ) { + let render_entity = commands.spawn_empty().id(); + + self.objects + .entry(main_entity) + .or_insert_with(|| (extracted_camera_entity, Default::default())) + .1 + .insert(render_entity, object); + + self.changed + .entry(main_entity) + .or_default() + .push(ChangedUiObject { + render_entity, + camera_entity: extracted_camera_entity, + }); + } +} + /// A piece of a UI node that renders using a specific shader and can enqueue /// the phase items necessary to render itself. /// diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index d39c8a598c081..3e939a4cb508e 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -4,7 +4,7 @@ use bevy_color::Alpha; use bevy_ecs::prelude::*; use bevy_input_focus::InputFocus; use bevy_math::{Affine2, Rect, Vec2}; -use bevy_render::Extract; +use bevy_render::{sync_world::MainEntityHashSet, Extract}; use bevy_sprite::BorderRect; use bevy_text::{EditableText, TextColor, TextCursorStyle, TextLayoutInfo, TextReadWriteMode}; use bevy_ui::{ @@ -36,10 +36,13 @@ pub fn extract_text_cursor( >, camera_map: Extract, input_focus: Extract>>, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + for ( entity, uinode, @@ -52,10 +55,9 @@ pub fn extract_text_cursor( cursor_style, rwmode, editable_text, - ) in extracted_uinodes - .changed - .keys() - .flat_map(|main_entity| text_node_query.get(main_entity.entity()).ok()) + ) in changed_entities + .drain() + .filter_map(|main_entity| text_node_query.get(main_entity.entity()).ok()) { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -147,68 +149,62 @@ pub fn extract_text_cursor( } } - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SELECTION, - clip, - image: AssetId::default(), - transform: transform * Affine2::from_translation(selection.center()), - item: ExtractedUiItem::Node { - color: selection_color, - rect: Rect { - min: Vec2::ZERO, - max: selection.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius, - node_type: NodeType::Rect, - }, - }, - ); - } - } - - if let Some((true, cursor_rect)) = text_layout_info.cursor - && !cursor_rect.is_empty() - && !cursor_style.color.is_fully_transparent() - && *rwmode != TextReadWriteMode::Static - { - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_CURSOR, + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SELECTION, clip, image: AssetId::default(), - transform: transform * Affine2::from_translation(cursor_rect.center()), + transform: transform * Affine2::from_translation(selection.center()), item: ExtractedUiItem::Node { - color: cursor_style.color.to_linear(), + color: selection_color, rect: Rect { min: Vec2::ZERO, - max: cursor_rect.size(), + max: selection.size(), }, atlas_scaling: None, flip_x: false, flip_y: false, border: BorderRect::default(), - border_radius: ResolvedBorderRadius::default(), + border_radius, node_type: NodeType::Rect, }, }, ); + } + } + + if let Some((true, cursor_rect)) = text_layout_info.cursor + && !cursor_rect.is_empty() + && !cursor_style.color.is_fully_transparent() + && *rwmode != TextReadWriteMode::Static + { + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_CURSOR, + clip, + image: AssetId::default(), + transform: transform * Affine2::from_translation(cursor_rect.center()), + item: ExtractedUiItem::Node { + color: cursor_style.color.to_linear(), + rect: Rect { + min: Vec2::ZERO, + max: cursor_rect.size(), + }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::default(), + border_radius: ResolvedBorderRadius::default(), + node_type: NodeType::Rect, + }, + }, + ); } } } @@ -234,10 +230,13 @@ pub fn extract_preedit_underlines( >, >, camera_map: Extract, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + for ( entity, uinode, @@ -249,10 +248,9 @@ pub fn extract_preedit_underlines( target_camera, stack_index, editable_text, - ) in extracted_uinodes - .changed - .keys() - .flat_map(|main_entity| text_node_query.get(main_entity.entity()).ok()) + ) in changed_entities + .drain() + .filter_map(|main_entity| text_node_query.get(main_entity.entity()).ok()) { if !inherited_visibility.get() || uinode.is_empty() @@ -277,33 +275,30 @@ pub fn extract_preedit_underlines( let color = text_color.0.to_linear(); for rect in text_layout_info.preedit_underline_rects.iter() { - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, - clip, - image: AssetId::default(), - transform: transform * Affine2::from_translation(rect.center()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: rect.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius: ResolvedBorderRadius::default(), - node_type: NodeType::Rect, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, + clip, + image: AssetId::default(), + transform: transform * Affine2::from_translation(rect.center()), + item: ExtractedUiItem::Node { + color, + rect: Rect { + min: Vec2::ZERO, + max: rect.size(), }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::default(), + border_radius: ResolvedBorderRadius::default(), + node_type: NodeType::Rect, }, - ); + }, + ); } } } diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 05c2dc7afe051..65650f2ef1434 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -438,7 +438,7 @@ pub fn extract_ui_material_nodes( commands.entity(render_entity).despawn(); changed.push(ChangedUiObject { render_entity, - prev_camera_entity, + camera_entity: prev_camera_entity, }); } } @@ -464,26 +464,23 @@ pub fn extract_ui_material_nodes( nodes_processed_this_frame.insert(main_entity); - extracted_uinodes - .objects - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiMaterialNode { - stack_index: stack_index.0, - transform: transform.into(), - material: handle.id(), - rect: Rect { - min: Vec2::ZERO, - max: computed_node.size(), - }, - border: computed_node.border(), - border_radius: computed_node.border_radius().into(), - clip: clip.map(|clip| clip.clip), + extracted_uinodes.add( + &mut commands, + main_entity, + extracted_camera_entity, + ExtractedUiMaterialNode { + stack_index: stack_index.0, + transform: transform.into(), + material: handle.id(), + rect: Rect { + min: Vec2::ZERO, + max: computed_node.size(), }, - ); + border: computed_node.border(), + border_radius: computed_node.border_radius().into(), + clip: clip.map(|clip| clip.clip), + }, + ); } // Only remove the render-world data if we didn't handle the node above. @@ -511,7 +508,7 @@ pub fn extract_ui_material_nodes( commands.entity(render_entity).despawn(); changed.push(ChangedUiObject { render_entity, - prev_camera_entity, + camera_entity: prev_camera_entity, }); } } diff --git a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs index eb83c0de953ba..a692cd299e152 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -318,7 +318,7 @@ pub fn extract_ui_texture_slices( commands.entity(render_entity).despawn(); changed.push(ChangedUiObject { render_entity, - prev_camera_entity, + camera_entity: prev_camera_entity, }); } } @@ -378,31 +378,28 @@ pub fn extract_ui_texture_slices( } }; - extracted_ui_slicers - .objects - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiTextureSlice { - stack_index: stack_index.0, - transform: Affine2::from(*transform) - * Affine2::from_translation(visual_box.center()), - color: image.color.into(), - rect: Rect { - min: Vec2::ZERO, - max: visual_box.size(), - }, - clip: clip.map(|clip| clip.clip), - image: image.image.id(), - image_scale_mode, - atlas_rect, - flip_x: image.flip_x, - flip_y: image.flip_y, - inverse_scale_factor: uinode.inverse_scale_factor, + extracted_ui_slicers.add( + &mut commands, + main_entity, + extracted_camera_entity, + ExtractedUiTextureSlice { + stack_index: stack_index.0, + transform: Affine2::from(*transform) + * Affine2::from_translation(visual_box.center()), + color: image.color.into(), + rect: Rect { + min: Vec2::ZERO, + max: visual_box.size(), }, - ); + clip: clip.map(|clip| clip.clip), + image: image.image.id(), + image_scale_mode, + atlas_rect, + flip_x: image.flip_x, + flip_y: image.flip_y, + inverse_scale_factor: uinode.inverse_scale_factor, + }, + ); } // Only remove the render-world data if we didn't handle the node above. @@ -430,7 +427,7 @@ pub fn extract_ui_texture_slices( commands.entity(render_entity).despawn(); changed.push(ChangedUiObject { render_entity, - prev_camera_entity, + camera_entity: prev_camera_entity, }); } } From 88669a8b5485f146910234bb9f866581d76d47b1 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sun, 9 Aug 2026 15:22:21 -0700 Subject: [PATCH 4/6] Partially address review comments --- crates/bevy_ui_render/src/box_shadow.rs | 49 ++- crates/bevy_ui_render/src/gradient.rs | 15 +- crates/bevy_ui_render/src/lib.rs | 506 +++++++++++++----------- 3 files changed, 307 insertions(+), 263 deletions(-) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index dcf49592ca9f7..9667c3db56901 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -34,8 +34,9 @@ use bevy_utils::default; use bytemuck::{Pod, Zeroable}; use crate::{ - queue_ui_items, BoxShadowSamples, CachedCameraView, ChangedUiObject, RenderUiSystems, - TransparentUi, UiCameraMap, UiRenderObject, UiRenderObjects, + queue_ui_items, wipe_phase_items_if_camera_component_changed, BoxShadowSamples, + CachedCameraView, ChangedUiObject, RenderUiSystems, TransparentUi, UiCameraMap, UiRenderObject, + UiRenderObjects, }; use super::{stack_z_offsets, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; @@ -56,7 +57,16 @@ impl Plugin for BoxShadowPlugin { .add_systems(RenderStartup, init_box_shadow_pipeline) .add_systems( ExtractSchedule, - extract_shadows.in_set(RenderUiSystems::ExtractBoxShadows), + ( + extract_shadows.in_set(RenderUiSystems::ExtractBoxShadows), + wipe_phase_items_if_camera_component_changed::< + ExtractedBoxShadow, + BoxShadowSamples, + > + .in_set( + RenderUiSystems::ExtractWipePhaseItemsIfCameraComponentsChanged, + ), + ), ) .add_systems( Render, @@ -384,24 +394,21 @@ pub fn extract_shadows( bottom_right: uinode.border_radius.bottom_right * spread_ratio, }; - extracted_box_shadows - .objects - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedBoxShadow { - stack_index: stack_index.0, - transform: Affine2::from(transform) * Affine2::from_translation(offset), - color: drop_shadow.color.into(), - bounds: shadow_size + 6. * blur_radius, - clip: clip.map(|clip| clip.clip), - radius, - blur_radius, - size: shadow_size, - }, - ); + extracted_box_shadows.add( + &mut commands, + main_entity, + extracted_camera_entity, + ExtractedBoxShadow { + stack_index: stack_index.0, + transform: Affine2::from(transform) * Affine2::from_translation(offset), + color: drop_shadow.color.into(), + bounds: shadow_size + 6. * blur_radius, + clip: clip.map(|clip| clip.clip), + radius, + blur_radius, + size: shadow_size, + }, + ); } } diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 766b9506bc6de..936178f0ee47d 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -54,9 +54,18 @@ impl Plugin for GradientPlugin { .add_systems(RenderStartup, init_gradient_pipeline) .add_systems( ExtractSchedule, - extract_gradients - .in_set(RenderUiSystems::ExtractGradient) - .after(extract_uinode_background_colors), + ( + extract_gradients + .in_set(RenderUiSystems::ExtractGradient) + .after(extract_uinode_background_colors), + wipe_phase_items_if_camera_component_changed::< + ExtractedGradient, + UiAntiAlias, + > + .in_set( + RenderUiSystems::ExtractWipePhaseItemsIfCameraComponentsChanged, + ), + ), ) .add_systems( Render, diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 773bcfe6d922f..503c413b829f5 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -27,7 +27,7 @@ mod debug_overlay; use bevy_a11y::AccessibilitySystems; use bevy_camera::visibility::InheritedVisibility; use bevy_camera::{Camera, Camera2d, Camera3d, RenderTarget}; -use bevy_ecs::entity::EntityIndexMap; +use bevy_ecs::entity::{EntityHashSet, EntityIndexMap}; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::camera::{extract_cameras, CameraMainPassTextureFormats}; @@ -146,6 +146,7 @@ pub enum RenderUiSystems { ExtractCursor, ExtractDebug, ExtractGradient, + ExtractWipePhaseItemsIfCameraComponentsChanged, } /// Marker for controlling whether UI is rendered with or without anti-aliasing @@ -258,6 +259,8 @@ impl Plugin for UiRenderPlugin { RenderUiSystems::ExtractText, RenderUiSystems::ExtractCursor, RenderUiSystems::ExtractDebug, + RenderUiSystems::ExtractGradient, + RenderUiSystems::ExtractWipePhaseItemsIfCameraComponentsChanged, ) .chain_weak(), ) @@ -278,6 +281,8 @@ impl Plugin for UiRenderPlugin { extract_text_sections.in_set(RenderUiSystems::ExtractText), extract_text_cursor.in_set(RenderUiSystems::ExtractCursor), extract_preedit_underlines.in_set(RenderUiSystems::ExtractCursor), + wipe_phase_items_if_camera_component_changed:: + .in_set(RenderUiSystems::ExtractWipePhaseItemsIfCameraComponentsChanged), #[cfg(feature = "bevy_ui_debug")] debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), ), @@ -986,11 +991,14 @@ pub fn extract_uinode_borders( )>, >, camera_map: Extract, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let image = AssetId::::default(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + for ( entity, node, @@ -1001,10 +1009,9 @@ pub fn extract_uinode_borders( maybe_clip, camera, (maybe_border_color, maybe_outline), - ) in extracted_uinodes - .changed - .keys() - .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) + ) in changed_entities + .drain() + .filter_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip invisible borders and removed nodes if !inherited_visibility.get() || node.is_some_and(|node| node.display == Display::None) { @@ -1072,12 +1079,7 @@ pub fn extract_uinode_borders( }, }; - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert(commands.spawn_empty().id(), node); + extracted_uinodes.add(&mut commands, entity.into(), extracted_camera_entity, node); } } @@ -1088,33 +1090,30 @@ 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 - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::BORDER, - image, - clip: maybe_clip.map(|clip| clip.clip), - transform: transform.into(), - item: ExtractedUiItem::Node { - color: outline.color.into(), - rect: Rect { - max: outline_size, - ..Default::default() - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::all(computed_node.outline_width()), - border_radius: computed_node.outline_radius(), - node_type: NodeType::Border(shader_flags::BORDER_ALL), + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::BORDER, + image, + clip: maybe_clip.map(|clip| clip.clip), + transform: transform.into(), + item: ExtractedUiItem::Node { + color: outline.color.into(), + rect: Rect { + max: outline_size, + ..Default::default() }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::all(computed_node.outline_width()), + border_radius: computed_node.outline_radius(), + node_type: NodeType::Border(shader_flags::BORDER_ALL), }, - ); + }, + ); } } } @@ -1303,6 +1302,52 @@ pub fn extract_ui_camera_view( transparent_render_phases.retain(|entity, _| live_entities.contains(entity)); } +pub fn wipe_phase_items_if_camera_component_changed( + changed_cameras_query: Extract< + Query, Or<(With, With)>)>, + >, + all_cameras_query: Extract, With)>>>, + mut removed_components: Extract>, + render_objects: ResMut>, + mut cameras_to_invalidate: Local, +) where + E: UiRenderObject, + C: Component, +{ + for main_entity in changed_cameras_query + .iter() + .chain(removed_components.read()) + { + let main_entity = MainEntity::from(main_entity); + if let Ok(camera_render_entity) = all_cameras_query.get(main_entity.entity()) { + cameras_to_invalidate.insert(camera_render_entity); + } + } + + if cameras_to_invalidate.is_empty() { + return; + } + + let render_objects = render_objects.into_inner(); + for (main_entity, (camera_entity, render_entities)) in render_objects.objects.iter() { + if !cameras_to_invalidate.contains(camera_entity) { + continue; + } + for render_entity in render_entities.keys() { + render_objects + .changed + .entry(*main_entity) + .or_default() + .push(ChangedUiObject { + render_entity: *render_entity, + camera_entity: *camera_entity, + }); + } + } + + cameras_to_invalidate.clear(); +} + pub fn extract_viewport_nodes( mut commands: Commands, extracted_uinodes: ResMut, @@ -1320,10 +1365,13 @@ pub fn extract_viewport_nodes( )>, >, camera_map: Extract, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + for ( entity, uinode, @@ -1333,10 +1381,9 @@ pub fn extract_viewport_nodes( clip, camera, viewport_node, - ) in extracted_uinodes - .changed - .keys() - .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) + ) in changed_entities + .drain() + .filter_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip invisible images if !inherited_visibility.get() || uinode.is_empty() { @@ -1358,33 +1405,30 @@ pub fn extract_viewport_nodes( continue; }; - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::IMAGE, - clip: clip.map(|clip| clip.clip), - image: image.id(), - transform: transform.into(), - item: ExtractedUiItem::Node { - color: LinearRgba::WHITE, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: uinode.border(), - border_radius: uinode.border_radius(), - node_type: NodeType::Rect, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::IMAGE, + clip: clip.map(|clip| clip.clip), + image: image.id(), + transform: transform.into(), + item: ExtractedUiItem::Node { + color: LinearRgba::WHITE, + rect: Rect { + min: Vec2::ZERO, + max: uinode.size, }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: uinode.border(), + border_radius: uinode.border_radius(), + node_type: NodeType::Rect, }, - ); + }, + ); } } @@ -1409,10 +1453,13 @@ pub fn extract_text_sections( >, text_styles: Extract>, camera_map: Extract, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + let mut glyphs = vec![]; for ( @@ -1428,10 +1475,9 @@ pub fn extract_text_sections( text_layout_info, editable_text, cursor_style, - ) in extracted_uinodes - .changed - .keys() - .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) + ) in changed_entities + .drain() + .filter_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -1518,23 +1564,20 @@ pub fn extract_text_sections( .get(i + 1) .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) { - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - image: atlas_info.texture, - clip, - item: ExtractedUiItem::Glyphs { - glyphs: mem::take(&mut glyphs), - }, - transform, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, + image: atlas_info.texture, + clip, + item: ExtractedUiItem::Glyphs { + glyphs: mem::take(&mut glyphs), }, - ); + transform, + }, + ); } } } @@ -1560,10 +1603,13 @@ pub fn extract_text_shadows( >, text_decoration_query: Extract, Has)>>, camera_map: Extract, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + let mut glyphs = vec![]; for ( @@ -1578,10 +1624,9 @@ pub fn extract_text_shadows( shadow, computed_block, editable_text, - ) in extracted_uinodes - .changed - .keys() - .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) + ) in changed_entities + .drain() + .filter_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -1629,23 +1674,20 @@ pub fn extract_text_shadows( info.section_index != *section_index || info.atlas_info.texture != atlas_info.texture }) { - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - transform: node_transform, - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SHADOW, - image: atlas_info.texture, - clip, - item: ExtractedUiItem::Glyphs { - glyphs: mem::take(&mut glyphs), - }, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + transform: node_transform, + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SHADOW, + image: atlas_info.texture, + clip, + item: ExtractedUiItem::Glyphs { + glyphs: mem::take(&mut glyphs), }, - ); + }, + ); } } @@ -1663,65 +1705,59 @@ pub fn extract_text_shadows( }; if has_strikethrough { - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SHADOW, - clip, - image: AssetId::default(), - transform: node_transform - * Affine2::from_translation(run.strikethrough_position()), - item: ExtractedUiItem::Node { - color: shadow.color.into(), - rect: Rect { - min: Vec2::ZERO, - max: run.strikethrough_size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SHADOW, + clip, + image: AssetId::default(), + transform: node_transform + * Affine2::from_translation(run.strikethrough_position()), + item: ExtractedUiItem::Node { + color: shadow.color.into(), + rect: Rect { + min: Vec2::ZERO, + max: run.strikethrough_size(), }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::ZERO, + border_radius: ResolvedBorderRadius::ZERO, + node_type: NodeType::Rect, }, - ); + }, + ); } if has_underline { - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - clip, - image: AssetId::default(), - transform: node_transform - * Affine2::from_translation(run.underline_position()), - item: ExtractedUiItem::Node { - color: shadow.color.into(), - rect: Rect { - min: Vec2::ZERO, - max: run.underline_size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, + clip, + image: AssetId::default(), + transform: node_transform + * Affine2::from_translation(run.underline_position()), + item: ExtractedUiItem::Node { + color: shadow.color.into(), + rect: Rect { + min: Vec2::ZERO, + max: run.underline_size(), }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::ZERO, + border_radius: ResolvedBorderRadius::ZERO, + node_type: NodeType::Rect, }, - ); + }, + ); } } } @@ -1753,10 +1789,13 @@ pub fn extract_text_decorations( )>, >, camera_map: Extract, + mut changed_entities: Local, ) { let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + changed_entities.extend(extracted_uinodes.changed.keys().copied()); + for ( entity, uinode, @@ -1768,10 +1807,9 @@ pub fn extract_text_decorations( camera, text_layout_info, editable_text, - ) in extracted_uinodes - .changed - .keys() - .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) + ) in changed_entities + .drain() + .filter_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -1818,33 +1856,30 @@ pub fn extract_text_decorations( }; if let Some(text_background_color) = text_background_color { - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - clip, - image: AssetId::default(), - transform: transform * Affine2::from_translation(run.bounds.center()), - item: ExtractedUiItem::Node { - color: text_background_color.0.to_linear(), - rect: Rect { - min: Vec2::ZERO, - max: run.bounds.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, + clip, + image: AssetId::default(), + transform: transform * Affine2::from_translation(run.bounds.center()), + item: ExtractedUiItem::Node { + color: text_background_color.0.to_linear(), + rect: Rect { + min: Vec2::ZERO, + max: run.bounds.size(), }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::ZERO, + border_radius: ResolvedBorderRadius::ZERO, + node_type: NodeType::Rect, }, - ); + }, + ); } if maybe_strikethrough.is_some() { @@ -1853,34 +1888,31 @@ pub fn extract_text_decorations( .unwrap_or(text_color.0) .to_linear(); - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, - clip, - image: AssetId::default(), - transform: transform - * Affine2::from_translation(run.strikethrough_position()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: run.strikethrough_size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, + clip, + image: AssetId::default(), + transform: transform + * Affine2::from_translation(run.strikethrough_position()), + item: ExtractedUiItem::Node { + color, + rect: Rect { + min: Vec2::ZERO, + max: run.strikethrough_size(), }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::ZERO, + border_radius: ResolvedBorderRadius::ZERO, + node_type: NodeType::Rect, }, - ); + }, + ); } if maybe_underline.is_some() { @@ -1889,34 +1921,30 @@ pub fn extract_text_decorations( .unwrap_or(text_color.0) .to_linear(); - extracted_uinodes - .objects - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, - clip, - image: AssetId::default(), - transform: transform - * Affine2::from_translation(run.underline_position()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: run.underline_size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, + extracted_uinodes.add( + &mut commands, + entity.into(), + extracted_camera_entity, + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, + clip, + image: AssetId::default(), + transform: transform * Affine2::from_translation(run.underline_position()), + item: ExtractedUiItem::Node { + color, + rect: Rect { + min: Vec2::ZERO, + max: run.underline_size(), }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::ZERO, + border_radius: ResolvedBorderRadius::ZERO, + node_type: NodeType::Rect, }, - ); + }, + ); } } } @@ -2693,7 +2721,7 @@ impl<'w, PKB> CachedCameraView<'w, PKB> { .ok() .and_then(|default_camera_view| { let view = extracted_views.get(default_camera_view.0).ok()?; - let pipeline_key_builder = render_views.get(default_camera_view.0).ok()?; + let pipeline_key_builder = render_views.get(this_camera_entity).ok()?; Some((view, pipeline_key_builder)) }); From e3a761c8bfc9697e75288a0e59733bf5a70e1a68 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sun, 9 Aug 2026 18:49:12 -0700 Subject: [PATCH 5/6] Attempt to fix Pixel Eagle --- crates/bevy_ui_render/src/lib.rs | 160 ++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 36 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 503c413b829f5..21aa2e022cabe 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -11,6 +11,7 @@ pub mod box_shadow; mod gradient; mod image; use bevy_ecs::query::QueryData; +use bevy_render::render_phase::DrawFunctionId; use bevy_render::render_resource::SpecializedRenderPipeline; use bevy_utils::default; pub use image::ImageNodeAssetChangedSystems; @@ -2023,6 +2024,13 @@ pub mod shader_flags { pub const INVERT: u32 = 4096; } +#[derive(Default)] +pub struct QueueUiItemsLocalData { + processed_ui_objects: HashSet, + ui_objects_to_retry_this_frame: HashSet<(MainEntity, ChangedUiObject)>, + ui_objects_to_retry_next_frame: HashSet<(MainEntity, ChangedUiObject)>, +} + /// Processes changed render objects of a single type, inserting and removing /// sorted phase items as necessary. /// @@ -2039,7 +2047,7 @@ pub fn queue_ui_items( extracted_views: Query<&ExtractedView>, pipeline_cache: Res, draw_functions: Res>, - mut processed_ui_objects: Local>, + mut local_data: Local, system_param: StaticSystemParam, ) where E: UiRenderObject, @@ -2047,6 +2055,13 @@ pub fn queue_ui_items( { let mut system_param = system_param.into_inner(); let draw_function = draw_functions.read().id::(); + let local_data = &mut *local_data; + + mem::swap( + &mut local_data.ui_objects_to_retry_this_frame, + &mut local_data.ui_objects_to_retry_next_frame, + ); + local_data.ui_objects_to_retry_next_frame.clear(); // To avoid having to look up information about the camera over and over // again for each changed render object, we cache the most recent view we @@ -2057,9 +2072,9 @@ pub fn queue_ui_items( for (main_entity, extracted_sub_ui_objects) in extracted_nodes.changed.iter() { // Examine all changed nodes (which includes nodes that were removed), // and remove all the corresponding phase items. - processed_ui_objects.clear(); + local_data.processed_ui_objects.clear(); for changed_ui_object in extracted_sub_ui_objects.iter() { - if !processed_ui_objects.insert(*changed_ui_object) { + if !local_data.processed_ui_objects.insert(*changed_ui_object) { continue; } @@ -2095,47 +2110,120 @@ pub fn queue_ui_items( // Now look at all the changed UI nodes again. For each, add the // appropriate render objects of this type. for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() { - // Refresh the cached camera view. - CachedCameraView::::update::( - &mut maybe_cached_camera_view, + try_add_phase_item( + *main_entity, + *render_entity, + extracted_uinode, *extracted_camera_entity, + &mut maybe_cached_camera_view, + draw_function, + &pipeline, + &mut pipelines, + &mut transparent_render_phases, &render_views, &ui_camera_views, &extracted_views, + &pipeline_cache, + &mut local_data.ui_objects_to_retry_next_frame, + &mut system_param, ); - let Some(ref mut cached_camera_view) = maybe_cached_camera_view else { - continue; - }; + } + } - // Fetch the transparent render phase. - let Some(transparent_render_phase) = transparent_render_phases - .get_mut(&cached_camera_view.extracted_view.retained_view_entity) - else { - continue; - }; + for (main_entity, changed_object) in local_data.ui_objects_to_retry_this_frame.drain() { + let Some((extracted_camera_entity, extracted_sub_nodes)) = + extracted_nodes.objects.get(&main_entity) + else { + continue; + }; + let Some(extracted_uinode) = extracted_sub_nodes.get(&changed_object.render_entity) else { + continue; + }; + try_add_phase_item( + main_entity, + changed_object.render_entity, + extracted_uinode, + *extracted_camera_entity, + &mut maybe_cached_camera_view, + draw_function, + &pipeline, + &mut pipelines, + &mut transparent_render_phases, + &render_views, + &ui_camera_views, + &extracted_views, + &pipeline_cache, + &mut local_data.ui_objects_to_retry_next_frame, + &mut system_param, + ); + } - // Get the pipeline key, and specialize the pipeline. We need a - // pipeline in order to construct a `TransparentUi` phase item. - let Some(pipeline_key) = - extracted_uinode.create_pipeline_key(cached_camera_view, &mut system_param) - else { - continue; - }; - let pipeline = pipelines.specialize(&pipeline_cache, &pipeline, pipeline_key); + fn try_add_phase_item<'w, E>( + main_entity: MainEntity, + render_entity: Entity, + extracted_uinode: &E, + extracted_camera_entity: Entity, + maybe_cached_camera_view: &mut Option>, + draw_function: DrawFunctionId, + pipeline: &E::SpecializedRenderPipeline, + pipelines: &mut SpecializedRenderPipelines, + transparent_render_phases: &mut ViewSortedRenderPhases, + render_views: &'w Query>, + ui_camera_views: &'w Query<&UiCameraView>, + extracted_views: &'w Query<&ExtractedView>, + pipeline_cache: &PipelineCache, + ui_objects_to_retry_next_frame: &mut HashSet<(MainEntity, ChangedUiObject)>, + system_param: &mut ::Item<'_, '_>, + ) where + E: UiRenderObject, + { + // Refresh the cached camera view. + CachedCameraView::::update::( + maybe_cached_camera_view, + extracted_camera_entity, + render_views, + ui_camera_views, + extracted_views, + ); + let Some(ref mut cached_camera_view) = *maybe_cached_camera_view else { + return; + }; - // Add the phase item. Note that this phase item will be retained - // from frame to frame. - transparent_render_phase.add_retained(TransparentUi { - draw_function, - pipeline, - entity: (*render_entity, *main_entity), - sort_key: extracted_uinode.get_sort_key(), - // batch_range will be calculated in prepare_uinodes - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - indexed: true, - }); - } + // Fetch the transparent render phase. + let Some(transparent_render_phase) = transparent_render_phases + .get_mut(&cached_camera_view.extracted_view.retained_view_entity) + else { + return; + }; + + // Get the pipeline key, and specialize the pipeline. We need a + // pipeline in order to construct a `TransparentUi` phase item. + let Some(pipeline_key) = + extracted_uinode.create_pipeline_key(cached_camera_view, system_param) + else { + ui_objects_to_retry_next_frame.insert(( + main_entity, + ChangedUiObject { + render_entity, + camera_entity: extracted_camera_entity, + }, + )); + return; + }; + let pipeline = pipelines.specialize(pipeline_cache, pipeline, pipeline_key); + + // Add the phase item. Note that this phase item will be retained + // from frame to frame. + transparent_render_phase.add_retained(TransparentUi { + draw_function, + pipeline, + entity: (render_entity, main_entity), + sort_key: extracted_uinode.get_sort_key(), + // batch_range will be calculated in prepare_uinodes + batch_range: 0..0, + extra_index: PhaseItemExtraIndex::None, + indexed: true, + }); } } From ef835226c7ffa28f85234dab9a243d1e1c1ab89e Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Sun, 9 Aug 2026 20:39:06 -0700 Subject: [PATCH 6/6] Add some comments --- crates/bevy_ui_render/src/lib.rs | 39 ++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 21aa2e022cabe..6d5ddf3905ae8 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -2024,10 +2024,26 @@ pub mod shader_flags { pub const INVERT: u32 = 4096; } +/// Information that the [`queue_ui_items`] system keeps internally. #[derive(Default)] pub struct QueueUiItemsLocalData { + /// A list of all UI objects that were processed this frame. processed_ui_objects: HashSet, + + /// A list of UI objects that couldn't have pipeline keys generated for them + /// on the previous frame. + /// + /// [`queue_ui_items`] will attempt to re-queue them on subsequent frames + /// until they successfully enqueue. + /// + /// Typically, a pipeline key will fail to be generated because a dependent + /// asset (e.g. a material) hasn't loaded yet. ui_objects_to_retry_this_frame: HashSet<(MainEntity, ChangedUiObject)>, + + /// A list of UI objects that couldn't have pipeline keys generated for them + /// on this frame. + /// + /// [`queue_ui_items`] will attempt to re-queue them on subsequent frames. ui_objects_to_retry_next_frame: HashSet<(MainEntity, ChangedUiObject)>, } @@ -2057,6 +2073,9 @@ pub fn queue_ui_items( let draw_function = draw_functions.read().id::(); let local_data = &mut *local_data; + // Save the list of UI objects we need to attempt to re-queue this frame. + // After processing current changes, we need to retry those in case they + // succeed now. mem::swap( &mut local_data.ui_objects_to_retry_this_frame, &mut local_data.ui_objects_to_retry_next_frame, @@ -2107,8 +2126,8 @@ pub fn queue_ui_items( continue; }; - // Now look at all the changed UI nodes again. For each, add the - // appropriate render objects of this type. + // Now look at all the changed UI nodes again. For each, attempt to add + // the appropriate render objects of this type. for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() { try_add_phase_item( *main_entity, @@ -2130,6 +2149,8 @@ pub fn queue_ui_items( } } + // Finally, attempt to re-queue all UI objects that we couldn't re-queue + // last frame (usually because a dependent asset hadn't loaded yet). for (main_entity, changed_object) in local_data.ui_objects_to_retry_this_frame.drain() { let Some((extracted_camera_entity, extracted_sub_nodes)) = extracted_nodes.objects.get(&main_entity) @@ -2139,6 +2160,7 @@ pub fn queue_ui_items( let Some(extracted_uinode) = extracted_sub_nodes.get(&changed_object.render_entity) else { continue; }; + try_add_phase_item( main_entity, changed_object.render_entity, @@ -2158,6 +2180,9 @@ pub fn queue_ui_items( ); } + // Attempts to enqueue a single phase item. If enqueuing fails because the + // pipeline key couldn't be generated, then this function adds the item to + // `ui_objects_to_retry_next_frame` and bails out. fn try_add_phase_item<'w, E>( main_entity: MainEntity, render_entity: Entity, @@ -2201,6 +2226,8 @@ pub fn queue_ui_items( let Some(pipeline_key) = extracted_uinode.create_pipeline_key(cached_camera_view, system_param) else { + // If we couldn't create the pipeline key, then make a note of this + // item so that we will try to enqueue it later, and bail out. ui_objects_to_retry_next_frame.insert(( main_entity, ChangedUiObject { @@ -2701,6 +2728,11 @@ impl UiRenderObjects where E: UiRenderObject, { + /// Spawns a new render entity corresponding to a UI node that changed since + /// the previous frame, and records the information needed to render it. + /// + /// There can be multiple render entities corresponding to a single + /// main-world UI node. pub fn add( &mut self, commands: &mut Commands, @@ -2710,12 +2742,15 @@ where ) { let render_entity = commands.spawn_empty().id(); + // Associate the newly spawned render world entity with the main world + // entity and camera. self.objects .entry(main_entity) .or_insert_with(|| (extracted_camera_entity, Default::default())) .1 .insert(render_entity, object); + // Note that it's changed so that we queue it later. self.changed .entry(main_entity) .or_default()