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 a87c3ea6a9e5b..9667c3db56901 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,13 @@ use bevy_ui::{ use bevy_utils::default; use bytemuck::{Pod, Zeroable}; -use crate::{BoxShadowSamples, RenderUiSystems, TransparentUi, UiCameraMap}; +use crate::{ + queue_ui_items, wipe_phase_items_if_camera_component_changed, 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; @@ -54,12 +57,21 @@ 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, ( - queue_shadows.in_set(RenderSystems::Queue), + queue_ui_items::.in_set(RenderSystems::Queue), prepare_shadows.in_set(RenderSystems::PrepareBindGroups), ), ); @@ -194,17 +206,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 +308,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 +321,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, + camera_entity: prev_camera_entity, + }); + } } // Skip if no visible shadows @@ -299,7 +347,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; } @@ -346,24 +394,21 @@ pub fn extract_shadows( bottom_right: uinode.border_radius.bottom_right * spread_ratio, }; - extracted_box_shadows - .box_shadows - .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, + }, + ); } } @@ -383,81 +428,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, + camera_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 +486,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..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 - .iter() - .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 - .uinodes - .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 992a639ff26f0..936178f0ee47d 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -54,14 +54,23 @@ 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, ( - queue_gradient.in_set(RenderSystems::Queue), + queue_ui_items::.in_set(RenderSystems::Queue), prepare_gradient.in_set(RenderSystems::PrepareBindGroups), ), ); @@ -243,16 +252,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. @@ -401,6 +444,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![]; @@ -421,14 +465,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, + camera_entity: prev_camera_entity, + }); + } } // Skip invisible images @@ -439,7 +488,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; } @@ -470,29 +519,26 @@ pub fn extract_gradients( target.physical_size().as_vec2(), &mut sorted_stops, ); - extracted_gradients - .items - .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 { @@ -511,29 +557,26 @@ pub fn extract_gradients( &mut sorted_stops, ); - extracted_gradients - .items - .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, @@ -564,29 +607,26 @@ pub fn extract_gradients( &mut sorted_stops, ); - extracted_gradients - .items - .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, @@ -623,32 +663,29 @@ pub fn extract_gradients( interpolate_color_stops(&mut extracted_color_stops, 0., TAU); - extracted_gradients - .items - .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, + }, + ); } } } @@ -672,84 +709,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, + camera_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)] @@ -836,7 +815,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 89de018f3a0b3..6d5ddf3905ae8 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -10,6 +10,10 @@ 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; mod pipeline; pub mod render_pass; @@ -24,7 +28,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}; @@ -45,7 +49,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::{ @@ -67,7 +71,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, @@ -77,6 +81,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::*; @@ -118,6 +124,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; @@ -140,6 +147,7 @@ pub enum RenderUiSystems { ExtractCursor, ExtractDebug, ExtractGradient, + ExtractWipePhaseItemsIfCameraComponentsChanged, } /// Marker for controlling whether UI is rendered with or without anti-aliasing @@ -252,6 +260,8 @@ impl Plugin for UiRenderPlugin { RenderUiSystems::ExtractText, RenderUiSystems::ExtractCursor, RenderUiSystems::ExtractDebug, + RenderUiSystems::ExtractGradient, + RenderUiSystems::ExtractWipePhaseItemsIfCameraComponentsChanged, ) .chain_weak(), ) @@ -272,6 +282,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), ), @@ -279,7 +291,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 +367,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 +441,13 @@ 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; + +#[derive(Clone, Copy, PartialEq, Eq, Hash)] +pub struct ChangedUiObject { + render_entity: Entity, + camera_entity: Entity, } /// A query filter that matches all UI nodes. @@ -664,11 +706,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, + camera_entity: prev_camera_entity, + }); } } @@ -712,10 +760,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, @@ -726,10 +777,9 @@ pub fn extract_uinode_background_colors( camera, background_color, maybe_outer_color, - ) in extracted_uinodes - .changed - .iter() - .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() @@ -740,21 +790,15 @@ pub fn extract_uinode_background_colors( continue; } - let extracted_sub_uinodes = match extracted_uinodes.uinodes.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), @@ -780,8 +824,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), @@ -824,10 +870,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, @@ -838,9 +887,8 @@ pub fn extract_uinode_images( camera, image, image_size, - ) in extracted_uinodes - .changed - .iter() + ) in changed_entities + .drain() .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { let visual_box = match image.visual_box { @@ -902,31 +950,28 @@ pub fn extract_uinode_images( None }; - extracted_uinodes - .uinodes - .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, }, - ); + }, + ); } } @@ -947,11 +992,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, @@ -962,10 +1010,9 @@ pub fn extract_uinode_borders( maybe_clip, camera, (maybe_border_color, maybe_outline), - ) in extracted_uinodes - .changed - .iter() - .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) { @@ -1033,12 +1080,7 @@ pub fn extract_uinode_borders( }, }; - extracted_uinodes - .uinodes - .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); } } @@ -1049,33 +1091,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 - .uinodes - .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), }, - ); + }, + ); } } } @@ -1264,6 +1303,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, @@ -1281,10 +1366,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, @@ -1294,10 +1382,9 @@ pub fn extract_viewport_nodes( clip, camera, viewport_node, - ) in extracted_uinodes - .changed - .iter() - .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() { @@ -1319,33 +1406,30 @@ pub fn extract_viewport_nodes( continue; }; - extracted_uinodes - .uinodes - .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, }, - ); + }, + ); } } @@ -1370,10 +1454,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 ( @@ -1389,10 +1476,9 @@ pub fn extract_text_sections( text_layout_info, editable_text, cursor_style, - ) in extracted_uinodes - .changed - .iter() - .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() { @@ -1479,23 +1565,20 @@ pub fn extract_text_sections( .get(i + 1) .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) { - extracted_uinodes - .uinodes - .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, + }, + ); } } } @@ -1521,10 +1604,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 ( @@ -1539,10 +1625,9 @@ pub fn extract_text_shadows( shadow, computed_block, editable_text, - ) in extracted_uinodes - .changed - .iter() - .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() { @@ -1590,23 +1675,20 @@ pub fn extract_text_shadows( info.section_index != *section_index || info.atlas_info.texture != atlas_info.texture }) { - extracted_uinodes - .uinodes - .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, - 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), }, - ); + }, + ); } } @@ -1624,65 +1706,59 @@ pub fn extract_text_shadows( }; if has_strikethrough { - extracted_uinodes - .uinodes - .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.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 - .uinodes - .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, }, - ); + }, + ); } } } @@ -1714,10 +1790,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, @@ -1729,10 +1808,9 @@ pub fn extract_text_decorations( camera, text_layout_info, editable_text, - ) in extracted_uinodes - .changed - .iter() - .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() { @@ -1779,33 +1857,30 @@ pub fn extract_text_decorations( }; if let Some(text_background_color) = text_background_color { - extracted_uinodes - .uinodes - .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() { @@ -1814,34 +1889,31 @@ pub fn extract_text_decorations( .unwrap_or(text_color.0) .to_linear(); - extracted_uinodes - .uinodes - .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() { @@ -1850,34 +1922,30 @@ pub fn extract_text_decorations( .unwrap_or(text_color.0) .to_linear(); - extracted_uinodes - .uinodes - .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, }, - ); + }, + ); } } } @@ -1956,68 +2024,241 @@ pub mod shader_flags { pub const INVERT: u32 = 4096; } -pub fn queue_uinodes( - extracted_uinodes: Res, - ui_pipeline: Res, - mut pipelines: ResMut>, +/// 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)>, +} + +/// 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; + mut local_data: Local, + system_param: StaticSystemParam, +) where + E: UiRenderObject, + ::Key: Send + Sync, +{ + let mut system_param = system_param.into_inner(); + 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, + ); + 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 + // looked at here. + let mut maybe_cached_camera_view = None; + + // Loop over all changed nodes. + 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. + local_data.processed_ui_objects.clear(); + for changed_ui_object in extracted_sub_ui_objects.iter() { + if !local_data.processed_ui_objects.insert(*changed_ui_object) { + continue; + } - 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) - }) - }) - }, + // Refresh the cached camera view. + CachedCameraView::::update::( + &mut maybe_cached_camera_view, + changed_ui_object.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_ui_object.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, attempt to add + // the appropriate render objects of this type. for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() { - transparent_phase.add_transient(TransparentUi { + try_add_phase_item( + *main_entity, + *render_entity, + extracted_uinode, + *extracted_camera_entity, + &mut maybe_cached_camera_view, draw_function, - pipeline: *pipeline, - entity: (*render_entity, *main_entity), - sort_key: FloatOrd(extracted_uinode.z_order), - // batch_range will be calculated in prepare_uinodes - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - indexed: true, - }); + &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, + ); } } + + // 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) + 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, + ); + } + + // 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, + 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; + }; + + // 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 { + // 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 { + 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, + }); + } +} + +/// 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)] @@ -2075,7 +2316,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 +2688,175 @@ 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(), + } + } +} + +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, + main_entity: MainEntity, + extracted_camera_entity: Entity, + object: E, + ) { + 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() + .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. +/// +/// 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(this_camera_entity).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..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 - .iter() - .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 - .uinodes - .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 - .uinodes - .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 - .iter() - .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 - .uinodes - .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 e718c9f941c75..65650f2ef1434 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>>, @@ -379,8 +394,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); @@ -408,14 +427,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, + camera_entity: prev_camera_entity, + }); + } } // skip invisible nodes @@ -433,32 +458,29 @@ 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 - .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. @@ -476,16 +498,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, + camera_entity: prev_camera_entity, + }); } } } -pub fn prepare_uimaterial_nodes( +pub fn prepare_uimaterial_nodes( mut commands: Commands, render_device: Res, render_queue: Res, @@ -497,7 +526,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(), @@ -519,7 +551,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())) { @@ -697,77 +729,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 276655cbd10cb..a692cd299e152 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, + camera_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; } @@ -356,31 +378,28 @@ pub fn extract_ui_texture_slices( } }; - extracted_ui_slicers - .slices - .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. @@ -398,74 +417,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, + camera_entity: prev_camera_entity, }); } } @@ -522,7 +484,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())) {