diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 68bd5e1bb6fe6..424e7b0bc269d 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -2032,7 +2032,7 @@ pub trait PhaseItem: Sized + Send + Sync + 'static { /// The corresponding entity that will be drawn. /// /// This is used to fetch the render data of the entity, required by the draw function, - /// from the render world . + /// from the render world. fn entity(&self) -> Entity; /// The main world entity represented by this `PhaseItem`. diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 26103cb967ce5..1389752537851 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -310,15 +310,23 @@ pub fn ui_layout_system( } let content_size = Vec2::new(layout.content_size.width, layout.content_size.height); - node.bypass_change_detection().content_size = content_size; + if node.content_size != content_size { + node.content_size = content_size; + } let taffy_rect_to_border_rect = |rect: taffy::Rect| BorderRect { min_inset: Vec2::new(rect.left, rect.top), max_inset: Vec2::new(rect.right, rect.bottom), }; - node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border); - node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding); + let new_border = taffy_rect_to_border_rect(layout.border); + if node.border != new_border { + node.border = new_border; + } + let new_padding = taffy_rect_to_border_rect(layout.padding); + if node.padding != new_padding { + node.padding = new_padding; + } // Compute the node's new global transform let mut local_transform = transform.compute_affine( @@ -334,16 +342,19 @@ pub fn ui_layout_system( } // We don't trigger change detection for changes to border radius - node.bypass_change_detection().border_radius = style.border_radius.resolve( + // unless the border radius actually changed + let new_border_radius = style.border_radius.resolve( inverse_target_scale_factor.recip(), node.size, target_size, ); + if node.border_radius != new_border_radius { + node.border_radius = new_border_radius; + } if let Some(outline) = maybe_outline { - // don't trigger change detection when only outlines are changed - let node = node.bypass_change_detection(); - node.outline_width = if style.display != Display::None { + // don't trigger change detection unless the outline actually changed + let new_outline_width = if style.display != Display::None { outline .width .resolve( @@ -357,7 +368,11 @@ pub fn ui_layout_system( 0. }; - node.outline_offset = outline + if node.outline_width != new_outline_width { + node.outline_width = new_outline_width; + } + + let new_outline_offset = outline .offset .resolve( inverse_target_scale_factor.recip(), @@ -368,10 +383,16 @@ pub fn ui_layout_system( // Clamp outline offsets to at least the length of the node's shorter side // Negative offset outlines can be useful to create thing like in-set focus indicators .max(-0.5 * node.size.min_element()); + if node.outline_offset != new_outline_offset { + node.outline_offset = new_outline_offset; + } } - node.bypass_change_detection().scrollbar_size = + let new_scrollbar_size = Vec2::new(layout.scrollbar_size.width, layout.scrollbar_size.height); + if node.scrollbar_size != new_scrollbar_size { + node.scrollbar_size = new_scrollbar_size; + } let scroll_position: Vec2 = maybe_scroll_position .map(|scroll_pos| { @@ -396,7 +417,9 @@ pub fn ui_layout_system( let physical_scroll_position = clamped_scroll_position.floor(); - node.bypass_change_detection().scroll_position = physical_scroll_position; + if node.scroll_position != physical_scroll_position { + node.scroll_position = physical_scroll_position; + } for child_uinode in ui_children.iter_ui_children(entity) { update_uinode_geometry_recursive( diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 966616a705506..1b685beaabfec 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -201,7 +201,12 @@ impl Plugin for UiPlugin { // They run independently since `widget::image_node_system` will only ever observe // its own ImageNode, and `widget::text_system` & `bevy_text::update_text2d_layout` // will never modify a pre-existing `Image` asset. - widget::update_image_content_size_system + ( + widget::update_image_content_size_system, + widget::update_texture_atlas_layout_components, + widget::mark_images_as_changed_if_their_assets_changed, + ) + .chain() .in_set(UiSystems::Content) .in_set(AmbiguousWithText) .in_set(AmbiguousWithUpdateText2dLayout), diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index fc8347dd68e11..fa8451f08e09d 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -12,7 +12,8 @@ use bevy_app::Propagate; use bevy_camera::Camera; use bevy_ecs::{ entity::Entity, - query::Has, + hierarchy::ChildOf, + query::{Has, With, Without}, system::{Commands, Query, Res}, }; use bevy_math::{Rect, UVec2}; @@ -118,11 +119,20 @@ pub fn propagate_ui_target_cameras( ui_scale: Res, camera_query: Query<&Camera>, target_camera_query: Query<&UiTargetCamera>, - ui_root_nodes: UiRootNodes, + ui_root_nodes: Query< + ( + Entity, + Option<&Propagate>, + Option<&Propagate>, + ), + (With, Without), + >, ) { let default_camera_entity = default_ui_camera.get(); - for root_entity in ui_root_nodes.iter() { + for (root_entity, maybe_computed_ui_target_camera, maybe_computed_ui_render_target_info) in + ui_root_nodes.iter() + { let camera = target_camera_query .get(root_entity) .ok() @@ -130,9 +140,15 @@ pub fn propagate_ui_target_cameras( .or(default_camera_entity) .unwrap_or(Entity::PLACEHOLDER); - commands - .entity(root_entity) - .try_insert(Propagate(ComputedUiTargetCamera { camera })); + // Only update `ComputedUiTargetCamera` if it actually changed. + match maybe_computed_ui_target_camera { + Some(computed_ui_target_camera) if computed_ui_target_camera.0.camera == camera => {} + _ => { + commands + .entity(root_entity) + .try_insert(Propagate(ComputedUiTargetCamera { camera })); + } + } let (scale_factor, physical_size) = camera_query .get(camera) @@ -145,12 +161,21 @@ pub fn propagate_ui_target_cameras( }) .unwrap_or((1., UVec2::ZERO)); - commands - .entity(root_entity) - .try_insert(Propagate(ComputedUiRenderTargetInfo { - scale_factor, - physical_size, - })); + let new_computed_ui_render_target_info = ComputedUiRenderTargetInfo { + scale_factor, + physical_size, + }; + + // Only update `ComputedUiRenderTargetInfo` if it actually changed. + match maybe_computed_ui_render_target_info { + Some(computed_ui_render_target_info) + if computed_ui_render_target_info.0 == new_computed_ui_render_target_info => {} + _ => { + commands + .entity(root_entity) + .try_insert(Propagate(new_computed_ui_render_target_info)); + } + } } } diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index 8b5a389510191..656494cd800d4 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -2,8 +2,9 @@ use crate::{ ComputedUiRenderTargetInfo, ContentSize, Measure, MeasureArgs, Node, NodeMeasure, ResolvedAxis, VisualBox, }; -use bevy_asset::{AsAssetId, AssetId, Assets, Handle}; +use bevy_asset::{asset_changed::AssetChanged, AsAssetId, AssetId, Assets, Handle}; use bevy_color::Color; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_image::{prelude::*, TRANSPARENT_IMAGE_HANDLE}; use bevy_math::{Rect, UVec2, Vec2}; @@ -256,6 +257,34 @@ impl ImageNodeSize { } } +// This component is more or less a workaround for the fact that `AsAssetId` +// only allows each component to expose one asset. `ImageNode` exposes two types +// of assets: an `Image` and a `TextureAtlasLayout`. We have to mark the image +// node as changed if either one of those assets changes. The only way to detect +// asset changes is to use the `AssetChanged` query filter. Unfortunately, the +// `AssetChanged` query filter relies on `AsAssetId`, which we can only +// implement once per component. Thus we need this second component, which +// essentially serves to provide a second implementation of `AsAssetId` on +// `ImageNode`. + +/// The texture atlas layout, if the image has one. +/// +/// The [`update_texture_atlas_layout_components`] system automatically keeps +/// this component up to date based on [`ImageNode::texture_atlas`]. Don't +/// update this component yourself; [`ImageNode::texture_atlas`] is the source +/// of truth. +#[derive(Component, Debug, Clone, Reflect, Deref, DerefMut)] +#[reflect(Component, Debug, Clone)] +pub struct ImageNodeTextureAtlasLayout(pub Handle); + +impl AsAssetId for ImageNodeTextureAtlasLayout { + type Asset = TextureAtlasLayout; + + fn as_asset_id(&self) -> AssetId { + self.id() + } +} + #[derive(Clone)] /// Used to calculate the size of UI image nodes pub struct ImageMeasure { @@ -415,3 +444,42 @@ pub fn update_image_content_size_system( } } } + +/// A system that marks [`ImageNode`]s as changed if either their [`Image`] or +/// [`TextureAtlasLayout`] changed. +pub fn mark_images_as_changed_if_their_assets_changed( + mut images_query: Query<&mut ImageNode, AssetChanged>, + mut texture_atlas_layouts_query: Query< + &mut ImageNodeTextureAtlasLayout, + AssetChanged, + >, +) { + for mut image in &mut images_query { + image.set_changed(); + } + for mut texture_atlas_layout in &mut texture_atlas_layouts_query { + texture_atlas_layout.set_changed(); + } +} + +/// A system that copies the [`TextureAtlasLayout`] stored within an +/// [`ImageNode`] to the [`TextureAtlasLayout`] component. +pub fn update_texture_atlas_layout_components( + mut commands: Commands, + images_query: Query<(Entity, &ImageNode), Changed>, +) { + for (entity, image_node) in &images_query { + match image_node.texture_atlas { + Some(ref texture_atlas) => { + commands + .entity(entity) + .insert(ImageNodeTextureAtlasLayout(texture_atlas.layout.clone())); + } + None => { + commands + .entity(entity) + .remove::(); + } + } + } +} diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 030ae69669a3b..8befb7d235bb9 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -6,6 +6,7 @@ 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, @@ -16,7 +17,7 @@ use bevy_ecs::{ }; use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2}; use bevy_mesh::VertexBufferLayout; -use bevy_render::sync_world::{MainEntity, TemporaryRenderEntity}; +use bevy_render::sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}; use bevy_render::{ render_phase::*, render_resource::{binding_types::uniform_buffer, *}, @@ -190,39 +191,77 @@ pub struct ExtractedBoxShadow { pub radius: ResolvedBorderRadius, pub blur_radius: f32, pub size: Vec2, - pub main_entity: MainEntity, - pub render_entity: Entity, } /// List of extracted shadows to be sorted and queued for rendering #[derive(Resource, Default)] pub struct ExtractedBoxShadows { - pub box_shadows: Vec, + /// The list of box shadows. + /// + /// 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>, } pub fn extract_shadows( mut commands: Commands, mut extracted_box_shadows: ResMut, box_shadow_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - &BoxShadow, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &ComputedUiRenderTargetInfo, - )>, + Query< + ( + Entity, + &ComputedNode, + &ComputedStackIndex, + &UiGlobalTransform, + &InheritedVisibility, + &BoxShadow, + Option<&CalculatedClip>, + &ComputedUiTargetCamera, + &ComputedUiRenderTargetInfo, + ), + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + >, >, camera_map: Extract, + ( + mut removed_computed_node_query, + mut removed_computed_stack_index_query, + mut removed_ui_global_transform_query, + mut removed_inherited_visibility_query, + mut removed_box_shadow_query, + mut removed_calculated_clip_query, + mut removed_computed_ui_target_camera_query, + mut removed_computed_ui_render_target_info_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), + mut nodes_processed_this_frame: Local, ) { + nodes_processed_this_frame.clear(); + let mut mapping = camera_map.get_mapper(); for (entity, uinode, stack_index, transform, visibility, box_shadow, clip, camera, target) in &box_shadow_query { + let main_entity = MainEntity::from(entity); + // Skip if no visible shadows if !visibility.get() || box_shadow.is_empty() || uinode.is_empty() { continue; @@ -266,6 +305,8 @@ pub fn extract_shadows( continue; } + nodes_processed_this_frame.insert(main_entity); + let radius = ResolvedBorderRadius { top_left: uinode.border_radius.top_left * spread_ratio, top_right: uinode.border_radius.top_right * spread_ratio, @@ -273,19 +314,50 @@ pub fn extract_shadows( bottom_right: uinode.border_radius.bottom_right * spread_ratio, }; - extracted_box_shadows.box_shadows.push(ExtractedBoxShadow { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - 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), - extracted_camera_entity, - radius, - blur_radius, - size: shadow_size, - main_entity: entity.into(), - }); + extracted_box_shadows + .box_shadows + .entry(main_entity) + .or_default() + .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), + extracted_camera_entity, + radius, + blur_radius, + size: shadow_size, + }, + ); + } + } + + // Only remove the render-world data if we didn't handle the node above. + // It's possible that a relevant component was removed and added in the same + // frame. + for main_entity in removed_computed_node_query + .read() + .chain(removed_computed_stack_index_query.read()) + .chain(removed_ui_global_transform_query.read()) + .chain(removed_inherited_visibility_query.read()) + .chain(removed_box_shadow_query.read()) + .chain(removed_calculated_clip_query.read()) + .chain(removed_computed_ui_target_camera_query.read()) + .chain(removed_computed_ui_render_target_info_query.read()) + { + let main_entity = MainEntity::from(main_entity); + if nodes_processed_this_frame.contains(&main_entity) { + continue; + } + let Some(mut extracted_nodes) = extracted_box_shadows.box_shadows.remove(&main_entity) + else { + continue; + }; + for (render_entity, _) in extracted_nodes.drain(..) { + commands.entity(render_entity).despawn(); } } } @@ -305,43 +377,46 @@ pub fn queue_shadows( draw_functions: Res>, ) { let draw_function = draw_functions.read().id::(); - for (index, extracted_shadow) in extracted_box_shadows.box_shadows.iter().enumerate() { - let entity = extracted_shadow.render_entity; - let Ok((default_camera_view, shadow_samples)) = - render_views.get_mut(extracted_shadow.extracted_camera_entity) - else { - continue; - }; + for (main_entity, extracted_sub_shadows) in extracted_box_shadows.box_shadows.iter() { + for (entity, extracted_shadow) in extracted_sub_shadows.iter() { + let Ok((default_camera_view, shadow_samples)) = + render_views.get_mut(extracted_shadow.extracted_camera_entity) + else { + continue; + }; - let Ok(view) = camera_views.get(default_camera_view.0) else { - continue; - }; + let Ok(view) = camera_views.get(default_camera_view.0) else { + continue; + }; - let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) - else { - continue; - }; + let Some(transparent_phase) = + transparent_render_phases.get_mut(&view.retained_view_entity) + else { + continue; + }; - let pipeline = pipelines.specialize( - &pipeline_cache, - &box_shadow_pipeline, - BoxShadowPipelineKey { - target_format: view.target_format, - samples: shadow_samples.copied().unwrap_or_default().0, - }, - ); + let pipeline = pipelines.specialize( + &pipeline_cache, + &box_shadow_pipeline, + BoxShadowPipelineKey { + target_format: view.target_format, + samples: shadow_samples.copied().unwrap_or_default().0, + }, + ); - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline, - entity: (entity, extracted_shadow.main_entity), - sort_key: FloatOrd(extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW), - - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - index, - indexed: true, - }); + transparent_phase.add_transient(TransparentUi { + draw_function, + 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, + }); + } } } @@ -351,7 +426,7 @@ pub fn prepare_shadows( render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut, - mut extracted_shadows: ResMut, + extracted_shadows: Res, view_uniforms: Res, box_shadow_pipeline: Res, mut phases: ResMut>, @@ -377,11 +452,15 @@ pub fn prepare_shadows( let item = &mut ui_phase.items[item_index]; let Some(box_shadow) = extracted_shadows .box_shadows - .get(item.index) - .filter(|n| item.entity() == n.render_entity) + .get(&item.main_entity()) + .and_then(|sub_shadows| sub_shadows.get(&item.entity())) else { continue; }; + + // Initialize the batch range to be zero-length initially. + // We'll extend it as we accumulate items into this batch. + item.batch_range = (item_index as u32)..(item_index as u32); let rect_size = box_shadow.bounds; // Specify the corners of the node @@ -495,7 +574,6 @@ pub fn prepare_shadows( *previous_len = batches.len(); commands.try_insert_batch(batches); } - extracted_shadows.box_shadows.clear(); } pub type DrawBoxShadows = (SetItemPipeline, SetBoxShadowViewBindGroup<0>, DrawBoxShadow); diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 73138c4f7ed3d..9c356d10ed2e7 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -8,6 +8,7 @@ use bevy_asset::AssetId; use bevy_camera::visibility::InheritedVisibility; use bevy_color::Hsla; use bevy_color::LinearRgba; +use bevy_ecs::entity::ContainsEntity as _; use bevy_ecs::entity::Entity; use bevy_ecs::prelude::Component; use bevy_ecs::prelude::ReflectComponent; @@ -21,7 +22,6 @@ use bevy_math::Affine2; use bevy_math::Rect; use bevy_math::Vec2; use bevy_reflect::Reflect; -use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::Extract; use bevy_sprite::BorderRect; use bevy_ui::ui_transform::UiGlobalTransform; @@ -172,7 +172,7 @@ impl From for GlobalUiDebugOptions { pub fn extract_debug_overlay( mut commands: Commands, debug_options: Extract>, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, uinode_query: Extract< Query<( Entity, @@ -188,10 +188,14 @@ pub fn extract_debug_overlay( ui_stack: Extract>, camera_map: Extract, ) { + let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); for (entity, uinode, stack_index, transform, visibility, maybe_clip, computed_target, debug) in - &uinode_query + extracted_uinodes + .changed + .iter() + .flat_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 { @@ -217,31 +221,36 @@ pub fn extract_debug_overlay( return; } - extracted_uinodes.uinodes.push(ExtractedUiNode { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - // 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(), - extracted_camera_entity, - transform: transform * Affine2::from_translation(rect.center()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: rect.size(), + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .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(), + extracted_camera_entity, + 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), + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border, - border_radius: radius, - node_type: NodeType::Border(shader_flags::BORDER_ALL), - }, - main_entity: entity.into(), - }); + ); }; 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 13fc7155e4b4d..4f0415852e58d 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -25,11 +25,10 @@ use bevy_render::{ render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, - sync_world::TemporaryRenderEntity, view::*, Extract, ExtractSchedule, Render, RenderSystems, }; -use bevy_render::{sync_world::MainEntity, GpuResourceAppExt, RenderStartup}; +use bevy_render::{GpuResourceAppExt, RenderStartup}; use bevy_shader::Shader; use bevy_sprite::BorderRect; use bevy_ui::{ @@ -50,7 +49,6 @@ impl Plugin for GradientPlugin { render_app .add_render_command::() .init_resource::() - .init_resource::() .init_gpu_resource::() .init_gpu_resource::>() .add_systems(RenderStartup, init_gradient_pipeline) @@ -232,11 +230,8 @@ pub struct ExtractedGradient { pub rect: Rect, pub clip: Option, pub extracted_camera_entity: Entity, - /// range into `ExtractedColorStops` - pub stops_range: Range, + pub stops: Vec<(LinearRgba, f32, f32)>, pub node_type: NodeType, - pub main_entity: MainEntity, - pub render_entity: Entity, /// Border radius of the UI node. /// Ordering: top left, top right, bottom right, bottom left. pub border_radius: ResolvedBorderRadius, @@ -247,14 +242,16 @@ pub struct ExtractedGradient { pub color_space: InterpolationColorSpace, } +/// A render-world resource that stores all gradients in the scene. #[derive(Resource, Default)] pub struct ExtractedGradients { - pub items: Vec, + /// The list of gradients. + /// + /// 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>, } -#[derive(Resource, Default)] -pub struct ExtractedColorStops(pub Vec<(LinearRgba, f32, f32)>); - // 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. @@ -295,8 +292,9 @@ fn compute_color_stops( length: f32, target_size: Vec2, scratch: &mut Vec<(LinearRgba, f32, f32)>, - extracted_color_stops: &mut Vec<(LinearRgba, f32, f32)>, -) { +) -> Vec<(LinearRgba, f32, f32)> { + let mut extracted_color_stops = vec![]; + // resolve the physical distances of explicit stops and sort them scratch.extend(stops.iter().filter_map(|stop| { stop.point @@ -321,8 +319,6 @@ fn compute_color_stops( let mut sorted_stops_drain = scratch.drain(..); - let range_start = extracted_color_stops.len(); - // Fill the extracted color stops buffer extracted_color_stops.extend(stops.iter().map(|stop| { if stop.point == Val::Auto { @@ -332,29 +328,66 @@ fn compute_color_stops( } })); - interpolate_color_stops(&mut extracted_color_stops[range_start..], min, max); + interpolate_color_stops(&mut extracted_color_stops, min, max); + + extracted_color_stops } pub fn extract_gradients( mut commands: Commands, mut extracted_gradients: ResMut, - mut extracted_color_stops: ResMut, mut extracted_uinodes: ResMut, gradients_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &ComputedUiTargetCamera, - &ComputedUiRenderTargetInfo, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - AnyOf<(&BackgroundGradient, &BorderGradient)>, - )>, + Query< + ( + Entity, + &ComputedNode, + &ComputedStackIndex, + &ComputedUiTargetCamera, + &ComputedUiRenderTargetInfo, + &UiGlobalTransform, + &InheritedVisibility, + Option<&CalculatedClip>, + AnyOf<(&BackgroundGradient, &BorderGradient)>, + ), + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + >, >, + ( + mut removed_computed_node_query, + mut removed_computed_stack_index_query, + mut removed_computed_ui_target_camera_query, + mut removed_computed_ui_render_target_info_query, + mut removed_ui_global_transform_query, + mut removed_inherited_visibility_query, + mut removed_calculated_clip_query, + mut removed_background_gradient_query, + mut removed_border_gradient_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), camera_map: Extract, + mut nodes_processed_this_frame: Local, ) { + nodes_processed_this_frame.clear(); let mut camera_mapper = camera_map.get_mapper(); let mut sorted_stops = vec![]; @@ -370,6 +403,8 @@ pub fn extract_gradients( (gradient, gradient_border), ) in &gradients_query { + let main_entity = MainEntity::from(entity); + // Skip invisible images if !inherited_visibility.get() { continue; @@ -390,34 +425,44 @@ pub fn extract_gradients( if gradient.is_empty() { continue; } + + nodes_processed_this_frame.insert(main_entity); + if let Some(color) = gradient.get_single() { // With a single color stop there's no gradient, fill the node with the color - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: stack_index.0 as f32 - + match node_type { - NodeType::Rect | NodeType::Inverted => stack_z_offsets::GRADIENT, - NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, + extracted_uinodes + .uinodes + .entry(main_entity) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + + match node_type { + NodeType::Rect | NodeType::Inverted => { + stack_z_offsets::GRADIENT + } + NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, + }, + image: AssetId::default(), + clip: clip.map(|clip| clip.clip), + extracted_camera_entity, + transform: transform.into(), + item: ExtractedUiItem::Node { + color: color.into(), + rect: Rect { + min: Vec2::ZERO, + max: uinode.size, + }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border_radius: uinode.border_radius, + border: uinode.border, + node_type, + }, }, - image: AssetId::default(), - clip: clip.map(|clip| clip.clip), - extracted_camera_entity, - transform: transform.into(), - item: ExtractedUiItem::Node { - color: color.into(), - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border_radius: uinode.border_radius, - border: uinode.border, - node_type, - }, - main_entity: entity.into(), - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - }); + ); continue; } match gradient { @@ -428,35 +473,37 @@ pub fn extract_gradients( }) => { let length = compute_gradient_line_length(*angle, uinode.size); - let range_start = extracted_color_stops.0.len(); - - compute_color_stops( + let extracted_stops = compute_color_stops( stops, target.scale_factor(), length, target.physical_size().as_vec2(), &mut sorted_stops, - &mut extracted_color_stops.0, ); - extracted_gradients.items.push(ExtractedGradient { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - stack_index: stack_index.0, - transform: transform.into(), - stops_range: range_start..extracted_color_stops.0.len(), - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - extracted_camera_entity, - main_entity: entity.into(), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Linear { angle: *angle }, - color_space: *color_space, - }); + extracted_gradients + .items + .entry(main_entity) + .or_default() + .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), + extracted_camera_entity, + 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, @@ -479,34 +526,37 @@ pub fn extract_gradients( let length = size.x; - let range_start = extracted_color_stops.0.len(); - compute_color_stops( + let computed_stops = compute_color_stops( stops, target.scale_factor(), length, target.physical_size().as_vec2(), &mut sorted_stops, - &mut extracted_color_stops.0, ); - extracted_gradients.items.push(ExtractedGradient { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - stack_index: stack_index.0, - transform: transform.into(), - stops_range: range_start..extracted_color_stops.0.len(), - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - extracted_camera_entity, - main_entity: entity.into(), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Radial { center: c, size }, - color_space: *color_space, - }); + extracted_gradients + .items + .entry(main_entity) + .or_default() + .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), + extracted_camera_entity, + 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, @@ -519,7 +569,6 @@ pub fn extract_gradients( uinode.size, target.physical_size().as_vec2(), ); - let range_start = extracted_color_stops.0.len(); // sort the explicit stops sorted_stops.extend(stops.iter().filter_map(|stop| { @@ -531,46 +580,76 @@ pub fn extract_gradients( let mut sorted_stops_drain = sorted_stops.drain(..); // fill the extracted stops buffer - extracted_color_stops.0.extend(stops.iter().map(|stop| { - if stop.angle.is_none() { - (stop.color.to_linear(), f32::NAN, stop.hint) - } else { - sorted_stops_drain.next().unwrap() - } - })); - - interpolate_color_stops( - &mut extracted_color_stops.0[range_start..], - 0., - TAU, - ); - - extracted_gradients.items.push(ExtractedGradient { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - stack_index: stack_index.0, - transform: transform.into(), - stops_range: range_start..extracted_color_stops.0.len(), - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - extracted_camera_entity, - main_entity: entity.into(), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Conic { - start: *start, - center: g_start, - }, - color_space: *color_space, - }); + let mut extracted_color_stops: Vec<_> = stops + .iter() + .map(|stop| { + if stop.angle.is_none() { + (stop.color.to_linear(), f32::NAN, stop.hint) + } else { + sorted_stops_drain.next().unwrap() + } + }) + .collect(); + + interpolate_color_stops(&mut extracted_color_stops, 0., TAU); + + extracted_gradients + .items + .entry(main_entity) + .or_default() + .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), + extracted_camera_entity, + node_type, + border_radius: uinode.border_radius, + border: uinode.border, + resolved_gradient: ResolvedGradient::Conic { + start: *start, + center: g_start, + }, + color_space: *color_space, + }, + ); } } } } } + + // Only remove the render-world data if we didn't handle the node above. + // It's possible that a relevant component was removed and added in the same + // frame. + for main_entity in removed_computed_node_query + .read() + .chain(removed_computed_stack_index_query.read()) + .chain(removed_computed_ui_target_camera_query.read()) + .chain(removed_computed_ui_render_target_info_query.read()) + .chain(removed_ui_global_transform_query.read()) + .chain(removed_inherited_visibility_query.read()) + .chain(removed_calculated_clip_query.read()) + .chain(removed_background_gradient_query.read()) + .chain(removed_border_gradient_query.read()) + { + let main_entity = MainEntity::from(main_entity); + if nodes_processed_this_frame.contains(&main_entity) { + continue; + } + let Some(mut extracted_nodes) = extracted_gradients.items.remove(&main_entity) else { + continue; + }; + for (render_entity, _) in extracted_nodes.drain(..) { + commands.entity(render_entity).despawn(); + } + } } #[expect( @@ -588,48 +667,50 @@ pub fn queue_gradient( draw_functions: Res>, ) { let draw_function = draw_functions.read().id::(); - for (index, gradient) in extracted_gradients.items.iter().enumerate() { - let Ok((default_camera_view, ui_anti_alias)) = - render_views.get_mut(gradient.extracted_camera_entity) - else { - continue; - }; - - let Ok(view) = camera_views.get(default_camera_view.0) else { - continue; - }; - - let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) - else { - continue; - }; - - 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: view.target_format, - }, - ); - - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline, - entity: (gradient.render_entity, gradient.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, - index, - indexed: true, - }); + for (main_entity, sub_gradients) in extracted_gradients.items.iter() { + for (render_entity, gradient) in sub_gradients.iter() { + let Ok((default_camera_view, ui_anti_alias)) = + render_views.get_mut(gradient.extracted_camera_entity) + else { + continue; + }; + + let Ok(view) = camera_views.get(default_camera_view.0) else { + continue; + }; + + let Some(transparent_phase) = + transparent_render_phases.get_mut(&view.retained_view_entity) + else { + continue; + }; + + 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: view.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, + }); + } } } @@ -692,8 +773,7 @@ pub fn prepare_gradient( render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut, - mut extracted_gradients: ResMut, - mut extracted_color_stops: ResMut, + extracted_gradients: Res, view_uniforms: Res, gradients_pipeline: Res, mut phases: ResMut>, @@ -719,8 +799,8 @@ pub fn prepare_gradient( let item = &mut ui_phase.items[item_index]; if let Some(gradient) = extracted_gradients .items - .get(item.index) - .filter(|n| item.entity() == n.render_entity) + .get(&item.main_entity()) + .and_then(|subgradients| subgradients.get(&item.entity())) { *item.batch_range_mut() = item_index as u32..item_index as u32 + 1; let uinode_rect = gradient.rect; @@ -823,14 +903,14 @@ pub fn prepare_gradient( flags |= g_flags; - let range = gradient.stops_range.start..gradient.stops_range.end - 1; + let range = 0..gradient.stops.len() - 1; let mut segment_count = 0; for stop_index in range { - let mut start_stop = extracted_color_stops.0[stop_index]; - let end_stop = extracted_color_stops.0[stop_index + 1]; + let mut start_stop = gradient.stops[stop_index]; + let end_stop = gradient.stops[stop_index + 1]; if start_stop.1 == end_stop.1 { - if stop_index == gradient.stops_range.end - 2 { + if stop_index == gradient.stops.len() - 2 { if 0 < segment_count { start_stop.0 = LinearRgba::NONE; } @@ -842,12 +922,10 @@ pub fn prepare_gradient( convert_color_to_space(start_stop.0, gradient.color_space); let end_color = convert_color_to_space(end_stop.0, gradient.color_space); let mut stop_flags = flags; - if 0. < start_stop.1 - && (stop_index == gradient.stops_range.start || segment_count == 0) - { + if 0. < start_stop.1 && (stop_index == 0 || segment_count == 0) { stop_flags |= shader_flags::FILL_START; } - if stop_index == gradient.stops_range.end - 2 { + if stop_index == gradient.stops.len() - 2 { stop_flags |= shader_flags::FILL_END; } @@ -907,8 +985,6 @@ pub fn prepare_gradient( *previous_len = batches.len(); commands.try_insert_batch(batches); } - extracted_gradients.items.clear(); - extracted_color_stops.0.clear(); } pub type DrawGradientFns = (SetItemPipeline, SetGradientViewBindGroup<0>, DrawGradient); diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index a1efacc959275..ff318621489a5 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -21,18 +21,20 @@ mod debug_overlay; use bevy_camera::visibility::InheritedVisibility; use bevy_camera::{Camera, Camera2d, Camera3d, RenderTarget}; +use bevy_ecs::entity::EntityIndexMap; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::camera::{extract_cameras, CameraMainPassTextureFormats}; +use bevy_render::sync_world::{MainEntityHashMap, MainEntityHashSet}; use bevy_shader::load_shader_library; use bevy_sprite_render::SpriteAssetEvents; use bevy_ui::widget::{ - ImageNode, ImageNodeSize, NodeImageMode, TextScroll, TextShadow, ViewportNode, + ImageNode, ImageNodeSize, NodeImageMode, Text, TextScroll, TextShadow, ViewportNode, }; use bevy_ui::{ - BackgroundColor, BorderColor, CalculatedClip, ComputedNode, ComputedStackIndex, - ComputedUiTargetCamera, Display, Node, OuterColor, Outline, ResolvedBorderRadius, - UiGlobalTransform, VisualBox, + BackgroundColor, BackgroundGradient, BorderColor, BorderGradient, BoxShadow, CalculatedClip, + ComputedNode, ComputedStackIndex, ComputedUiTargetCamera, Display, Node, OuterColor, Outline, + ResolvedBorderRadius, UiGlobalTransform, VisualBox, }; use bevy_app::prelude::*; @@ -53,7 +55,7 @@ use bevy_render::{ }, render_resource::*, renderer::{RenderDevice, RenderQueue}, - sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity}, + sync_world::{MainEntity, RenderEntity}, texture::GpuImage, view::{ExtractedView, RetainedViewEntity, ViewUniforms}, Extract, ExtractSchedule, GpuResourceAppExt, Render, RenderApp, RenderStartup, RenderSystems, @@ -66,13 +68,15 @@ use gradient::GradientPlugin; use bevy_platform::collections::{HashMap, HashSet}; use bevy_text::{ - ComputedTextBlock, PositionedGlyph, Strikethrough, StrikethroughColor, TextBackgroundColor, - TextColor, TextCursorStyle, TextLayoutInfo, Underline, UnderlineColor, + ComputedTextBlock, EditableText, PositionedGlyph, Strikethrough, StrikethroughColor, + TextBackgroundColor, TextColor, TextCursorStyle, TextLayoutInfo, TextSpan, Underline, + UnderlineColor, }; use bevy_transform::components::GlobalTransform; use box_shadow::BoxShadowPlugin; use bytemuck::{Pod, Zeroable}; use core::ops::Range; +use std::mem; pub use pipeline::*; pub use render_pass::*; @@ -121,6 +125,7 @@ pub mod stack_z_offsets { #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum RenderUiSystems { + ExtractChanges, ExtractCameraViews, ExtractBoxShadows, ExtractBackgrounds, @@ -220,6 +225,7 @@ impl Plugin for UiRenderPlugin { .configure_sets( ExtractSchedule, ( + RenderUiSystems::ExtractChanges, RenderUiSystems::ExtractCameraViews, RenderUiSystems::ExtractBoxShadows, RenderUiSystems::ExtractBackgrounds, @@ -238,6 +244,7 @@ impl Plugin for UiRenderPlugin { .add_systems( ExtractSchedule, ( + extract_uinode_changes.in_set(RenderUiSystems::ExtractChanges), extract_ui_camera_view .after(extract_cameras) .in_set(RenderUiSystems::ExtractCameraViews), @@ -260,6 +267,7 @@ impl Plugin for UiRenderPlugin { queue_uinodes.in_set(RenderSystems::Queue), sort_phase_system::.in_set(RenderSystems::PhaseSort), prepare_uinodes.in_set(RenderSystems::PrepareBindGroups), + clear_batches.in_set(RenderSystems::Cleanup), ), ) .add_systems( @@ -331,8 +339,6 @@ pub struct ExtractedUiNode { /// Render world entity of the extracted camera corresponding to this node's target camera. pub extracted_camera_entity: Entity, pub item: ExtractedUiItem, - pub main_entity: MainEntity, - pub render_entity: Entity, pub transform: Affine2, } @@ -362,8 +368,8 @@ pub enum ExtractedUiItem { }, /// A contiguous sequence of text glyphs from the same section Glyphs { - /// Indices into [`ExtractedUiNodes::glyphs`] - range: Range, + /// The color, position, and UV rect of each glyph. + glyphs: Vec, }, } @@ -373,22 +379,250 @@ pub struct ExtractedGlyph { pub rect: Rect, } +/// The list of gradients. +/// +/// 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 { - pub uinodes: Vec, - pub glyphs: Vec, + /// The list of UI nodes. + /// + /// 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>, + /// UI nodes that changed this frame. + pub changed: MainEntityHashSet, } -impl ExtractedUiNodes { - pub fn clear(&mut self) { - self.uinodes.clear(); - self.glyphs.clear(); +// Note: Whenever you add a new component that affects UI rendering, make sure +// to add a `Changed` query filter and a reference to the `RemovedComponents` +// resource to `extract_uinode_changes` below. +// +// Note: We don't have to match on `AssetChanged` for images or texture atlas +// layouts because the +// `bevy_ui::widget::mark_images_as_changed_as_their_assets_changed` image marks +// the `ImageNode` for us automatically as changed when those assets change. + +/// A render-world system that scans for any UI nodes that have changed and +/// removes the render world data associated with them. +pub fn extract_uinode_changes( + mut commands: Commands, + mut extracted_uinodes: ResMut, + uinode_query: Extract< + Query< + Entity, + ( + With, + With, + With, + With, + With, + Or<( + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + )>, + ), + >, + >, + text_span_query: Extract< + Query< + Entity, + ( + With, + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + ), + >, + >, + text_span_parent_query: Extract>>, + text_query: Extract>>, + ( + mut removed_computed_node_query, + mut removed_computed_stack_index_query, + mut removed_ui_global_transform_query, + mut removed_inherited_visibility_query, + mut removed_calculated_clip_query, + mut removed_computed_ui_target_camera_query, + mut removed_background_color_query, + mut removed_outer_color_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), + ( + mut removed_image_node_query, + mut removed_image_node_size_query, + mut removed_border_color_query, + mut removed_outline_query, + mut removed_viewport_node_query, + mut removed_computed_text_block_query, + mut removed_text_color_query, + mut removed_text_layout_info_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), + ( + mut removed_text_scroll_query, + mut removed_text_cursor_style_query, + mut removed_text_shadow_query, + mut removed_background_gradient_query, + mut removed_border_gradient_query, + mut removed_editable_text_query, + mut removed_ui_debug_options, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), + mut extra_nodes_to_invalidate: Local, +) { + extracted_uinodes.changed.clear(); + + // Go through all nodes that have changed and invalidate any render world + // data associated with them. + for main_entity in uinode_query + .iter() + .chain(text_span_query.iter()) + .chain(removed_computed_node_query.read()) + .chain(removed_computed_stack_index_query.read()) + .chain(removed_ui_global_transform_query.read()) + .chain(removed_inherited_visibility_query.read()) + .chain(removed_calculated_clip_query.read()) + .chain(removed_computed_ui_target_camera_query.read()) + .chain(removed_background_color_query.read()) + .chain(removed_outer_color_query.read()) + .chain(removed_image_node_query.read()) + .chain(removed_image_node_size_query.read()) + .chain(removed_border_color_query.read()) + .chain(removed_outline_query.read()) + .chain(removed_viewport_node_query.read()) + .chain(removed_computed_text_block_query.read()) + .chain(removed_text_color_query.read()) + .chain(removed_text_layout_info_query.read()) + .chain(removed_text_scroll_query.read()) + .chain(removed_text_cursor_style_query.read()) + .chain(removed_text_shadow_query.read()) + .chain(removed_background_gradient_query.read()) + .chain(removed_border_gradient_query.read()) + .chain(removed_editable_text_query.read()) + .chain(removed_ui_debug_options.read()) + { + process_changed_entity( + main_entity.into(), + &mut commands, + &text_span_parent_query, + &text_query, + &mut extracted_uinodes, + Some(&mut extra_nodes_to_invalidate), + ); + } + + for main_entity in extra_nodes_to_invalidate.drain() { + process_changed_entity( + main_entity, + &mut commands, + &text_span_parent_query, + &text_query, + &mut extracted_uinodes, + None, + ); + } + + fn process_changed_entity( + mut main_entity: MainEntity, + commands: &mut Commands, + text_span_parent_query: &Query<&ChildOf, With>, + text_query: &Query>, + extracted_uinodes: &mut ExtractedUiNodes, + maybe_extra_nodes_to_invalidate: Option<&mut MainEntityHashSet>, + ) { + // Mark the node as changed so that the other `extract_` systems will + // know to process it. + extracted_uinodes.changed.insert(main_entity); + + if let Some(mut render_entities) = extracted_uinodes.uinodes.remove(&main_entity) { + for (render_entity, _) in render_entities.drain(..) { + commands.entity(render_entity).despawn(); + } + } + + // If this node is a `TextSpan`, then we need to invalidate the ancestor + // `Text` node too. This is because `extract_text_decorations` only + // looks at the `text_background_colors_query` for the text spans if the + // `uinode_query` that it's iterating over matched the ancestor `Text` + // node. + if let Some(extra_nodes_to_invalidate) = maybe_extra_nodes_to_invalidate + && let Ok(parent) = text_span_parent_query.get(main_entity.entity()) + { + main_entity = parent.parent().into(); + loop { + if text_query.contains(main_entity.entity()) { + extra_nodes_to_invalidate.insert(main_entity); + break; + } + match text_span_parent_query.get(main_entity.entity()) { + Ok(parent) => main_entity = parent.parent().into(), + Err(_) => break, + } + } + } } } pub fn extract_uinode_background_colors( mut commands: Commands, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, uinode_query: Extract< Query<( Entity, @@ -404,6 +638,7 @@ pub fn extract_uinode_background_colors( >, camera_map: Extract, ) { + let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); for ( @@ -416,7 +651,10 @@ pub fn extract_uinode_background_colors( camera, background_color, maybe_outer_color, - ) in &uinode_query + ) in extracted_uinodes + .changed + .iter() + .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip invisible backgrounds if !inherited_visibility.get() @@ -432,62 +670,72 @@ pub fn extract_uinode_background_colors( }; if !background_color.is_fully_transparent() { - extracted_uinodes.uinodes.push(ExtractedUiNode { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - z_order: stack_index.0 as f32 + stack_z_offsets::BACKGROUND_COLOR, - clip: clip.map(|clip| clip.clip), - image: AssetId::default(), - extracted_camera_entity, - transform: transform.into(), - item: ExtractedUiItem::Node { - color: background_color.0.into(), - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::BACKGROUND_COLOR, + clip: clip.map(|clip| clip.clip), + image: AssetId::default(), + extracted_camera_entity, + transform: transform.into(), + item: ExtractedUiItem::Node { + color: background_color.0.into(), + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: uinode.border(), - border_radius: uinode.border_radius(), - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } if let Some(outer_color) = maybe_outer_color && !outer_color.0.is_fully_transparent() { - extracted_uinodes.uinodes.push(ExtractedUiNode { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - z_order: stack_index.0 as f32 + stack_z_offsets::BACKGROUND_COLOR, - clip: clip.map(|clip| clip.clip), - image: AssetId::default(), - extracted_camera_entity, - transform: transform.into(), - item: ExtractedUiItem::Node { - color: outer_color.0.into(), - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::BACKGROUND_COLOR, + clip: clip.map(|clip| clip.clip), + image: AssetId::default(), + extracted_camera_entity, + transform: transform.into(), + item: ExtractedUiItem::Node { + color: outer_color.0.into(), + rect: Rect { + min: Vec2::ZERO, + max: uinode.size, + }, + atlas_scaling: None, + flip_x: false, + flip_y: false, + border: BorderRect::ZERO, + border_radius: uinode.border_radius(), + node_type: NodeType::Inverted, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: uinode.border_radius(), - node_type: NodeType::Inverted, - }, - main_entity: entity.into(), - }); + ); } } } pub fn extract_uinode_images( mut commands: Commands, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, texture_atlases: Extract>>, uinode_query: Extract< Query<( @@ -504,7 +752,9 @@ pub fn extract_uinode_images( >, camera_map: Extract, ) { + let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + for ( entity, uinode, @@ -515,7 +765,10 @@ pub fn extract_uinode_images( camera, image, image_size, - ) in &uinode_query + ) in extracted_uinodes + .changed + .iter() + .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { let visual_box = match image.visual_box { VisualBox::ContentBox => uinode.content_box(), @@ -576,31 +829,37 @@ pub fn extract_uinode_images( None }; - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::IMAGE, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - clip: clip.map(|clip| clip.clip), - image: image.image.id(), - extracted_camera_entity, - 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, - }, - main_entity: entity.into(), - }); + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .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(), + extracted_camera_entity, + 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, + }, + }, + ); } } pub fn extract_uinode_borders( mut commands: Commands, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, uinode_query: Extract< Query<( Entity, @@ -616,6 +875,7 @@ pub fn extract_uinode_borders( >, camera_map: Extract, ) { + let extracted_uinodes = extracted_uinodes.into_inner(); let image = AssetId::::default(); let mut camera_mapper = camera_map.get_mapper(); @@ -629,7 +889,10 @@ pub fn extract_uinode_borders( maybe_clip, camera, (maybe_border_color, maybe_outline), - ) in &uinode_query + ) in extracted_uinodes + .changed + .iter() + .flat_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) { @@ -677,7 +940,7 @@ pub fn extract_uinode_borders( } completed_flags |= border_flags; - extracted_uinodes.uinodes.push(ExtractedUiNode { + let node = ExtractedUiNode { z_order: stack_index.0 as f32 + stack_z_offsets::BORDER, image, clip: maybe_clip.map(|clip| clip.clip), @@ -696,9 +959,13 @@ pub fn extract_uinode_borders( border_radius: computed_node.border_radius(), node_type: NodeType::Border(border_flags), }, - main_entity: entity.into(), - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - }); + }; + + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert(commands.spawn_empty().id(), node); } } @@ -709,28 +976,33 @@ 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.push(ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::BORDER, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - image, - clip: maybe_clip.map(|clip| clip.clip), - extracted_camera_entity, - transform: transform.into(), - item: ExtractedUiItem::Node { - color: outline.color.into(), - rect: Rect { - max: outline_size, - ..Default::default() + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .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), + extracted_camera_entity, + 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), + }, }, - 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), - }, - main_entity: entity.into(), - }); + ); } } } @@ -772,6 +1044,15 @@ pub struct UiCameraView(pub Entity); #[derive(Component)] pub struct UiViewTarget(pub Entity); +/// Information that [`extract_ui_camera_view`] maintains about each view that +/// it has seen. +pub struct CachedUiViewData { + /// The render-world [`ExtractedView`]. + extracted_view_entity: Entity, + /// The unique, stable identifier for the view across frames. + retained_view_entity: RetainedViewEntity, +} + /// Extracts all UI elements associated with a camera into the render world. pub fn extract_ui_camera_view( mut commands: Commands, @@ -781,42 +1062,69 @@ pub fn extract_ui_camera_view( ( Entity, RenderEntity, - &Camera, - Option<&UiAntiAlias>, - Option<&BoxShadowSamples>, + Ref, + Option>, + Option>, ), Or<(With, With)>, >, >, + changed_query: Extract< + Query< + Entity, + Or<( + Changed, + Changed, + Changed, + )>, + >, + >, main_pass_formats: Res, mut live_entities: Local>, + mut cached_ui_view_data: Local>, + ( + mut removed_cameras_query, + mut removed_ui_anti_alias_query, + mut removed_box_shadow_samples_query, + ): ( + Extract>, + Extract>, + Extract>, + ), + mut changed_cameras: Local, + mut cameras_updated_this_frame: Local, ) { - live_entities.clear(); + changed_cameras.clear(); + for main_entity in changed_query + .iter() + .chain(removed_ui_anti_alias_query.read()) + .chain(removed_box_shadow_samples_query.read()) + { + changed_cameras.insert(main_entity.into()); + } + cameras_updated_this_frame.clear(); for (main_entity, render_entity, camera, ui_anti_alias, shadow_samples) in &query { - // ignore inactive cameras - if !camera.is_active { - commands - .get_entity(render_entity) - .expect("Camera entity wasn't synced.") - .remove::<(UiCameraView, UiAntiAlias, BoxShadowSamples)>(); - continue; - } + let main_entity = MainEntity::from(main_entity); + let retained_view_entity = RetainedViewEntity::new(main_entity, None, UI_CAMERA_SUBVIEW); - if let (Some(physical_viewport_rect), Some(_viewport_size), Some(target_size)) = ( + // ignore inactive cameras + if let (Some(physical_viewport_rect), Some(target_size), Some(target_format)) = ( camera.physical_viewport_rect(), - camera.physical_viewport_size(), camera.physical_target_size(), + main_pass_formats.get(&render_entity).copied(), ) && target_size.x != 0 && target_size.y != 0 + && camera.physical_viewport_size().is_some() + && camera.is_active { - let Some(target_format) = main_pass_formats.get(&render_entity).copied() else { - commands - .get_entity(render_entity) - .expect("Camera entity wasn't synced.") - .remove::<(UiCameraView, UiAntiAlias, BoxShadowSamples)>(); + cameras_updated_this_frame.insert(main_entity); + transparent_render_phases.prepare_for_new_frame(retained_view_entity); + + // If the camera hasn't changed, we're done. + if !changed_cameras.contains(&main_entity) { continue; - }; + } // use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection let projection_matrix = Mat4::orthographic_rh( @@ -829,33 +1137,33 @@ pub fn extract_ui_camera_view( ); // We use `UI_CAMERA_SUBVIEW` here so as not to conflict with the // main 3D or 2D camera, which will have subview index 0. - let retained_view_entity = - RetainedViewEntity::new(main_entity.into(), None, UI_CAMERA_SUBVIEW); // Creates the UI view. - let ui_camera_view = commands - .spawn(( - ExtractedView { - retained_view_entity, - clip_from_view: projection_matrix, - world_from_view: GlobalTransform::from_xyz( - 0.0, - 0.0, - UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, - ), - clip_from_world: None, - target_format, - viewport: UVec4::from(( - physical_viewport_rect.min, - physical_viewport_rect.size(), - )), - color_grading: Default::default(), - invert_culling: false, - }, - // Link to the main camera view. - UiViewTarget(render_entity), - TemporaryRenderEntity::default(), - )) - .id(); + let extracted_view = ExtractedView { + retained_view_entity, + clip_from_view: projection_matrix, + world_from_view: GlobalTransform::from_xyz( + 0.0, + 0.0, + UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, + ), + clip_from_world: None, + target_format, + viewport: UVec4::from((physical_viewport_rect.min, physical_viewport_rect.size())), + color_grading: Default::default(), + invert_culling: false, + }; + // Link to the main camera view. + let ui_view_target_component = UiViewTarget(render_entity); + + let ui_camera_view = match cached_ui_view_data.get(&main_entity) { + Some(cached_ui_view_data) => commands + .entity(cached_ui_view_data.extracted_view_entity) + .insert((extracted_view, ui_view_target_component)) + .id(), + None => commands + .spawn((extracted_view, ui_view_target_component)) + .id(), + }; let mut entity_commands = commands .get_entity(render_entity) @@ -868,18 +1176,57 @@ pub fn extract_ui_camera_view( if let Some(shadow_samples) = shadow_samples { entity_commands.insert(*shadow_samples); } - transparent_render_phases.prepare_for_new_frame(retained_view_entity); live_entities.insert(retained_view_entity); + cached_ui_view_data.insert( + main_entity, + CachedUiViewData { + extracted_view_entity: ui_camera_view, + retained_view_entity, + }, + ); + continue; + } + + // If we got here, the camera no longer exists or is no longer + // renderable. Remove its associated render-world data. + commands + .get_entity(render_entity) + .expect("Camera entity wasn't synced.") + .remove::<(UiCameraView, UiAntiAlias, BoxShadowSamples)>(); + live_entities.remove(&retained_view_entity); + if let Some(cached_ui_view_data) = cached_ui_view_data.remove(&main_entity) { + commands + .entity(cached_ui_view_data.extracted_view_entity) + .despawn(); + } + } + + // Only remove the render-world data for a camera if we didn't handle the + // camera above. + // It's possible that the `Camera` component was removed and added in the + // same frame. + for main_entity in removed_cameras_query.read() { + let main_entity = MainEntity::from(main_entity); + if cameras_updated_this_frame.contains(&main_entity) { + continue; + } + + if let Some(cached_ui_view_data) = cached_ui_view_data.remove(&main_entity) { + commands + .entity(cached_ui_view_data.extracted_view_entity) + .despawn(); + live_entities.remove(&cached_ui_view_data.retained_view_entity); } } + // Clean up render phases belonging to cameras that no longer exist. transparent_render_phases.retain(|entity, _| live_entities.contains(entity)); } pub fn extract_viewport_nodes( mut commands: Commands, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, camera_query: Extract>, uinode_query: Extract< Query<( @@ -895,7 +1242,9 @@ pub fn extract_viewport_nodes( >, camera_map: Extract, ) { + let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + for ( entity, uinode, @@ -905,7 +1254,10 @@ pub fn extract_viewport_nodes( clip, camera, viewport_node, - ) in &uinode_query + ) in extracted_uinodes + .changed + .iter() + .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) { // Skip invisible images if !inherited_visibility.get() || uinode.is_empty() { @@ -927,34 +1279,39 @@ pub fn extract_viewport_nodes( continue; }; - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::IMAGE, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - clip: clip.map(|clip| clip.clip), - image: image.id(), - extracted_camera_entity, - transform: transform.into(), - item: ExtractedUiItem::Node { - color: LinearRgba::WHITE, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .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(), + extracted_camera_entity, + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: uinode.border(), - border_radius: uinode.border_radius(), - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } } pub fn extract_text_sections( mut commands: Commands, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, uinode_query: Extract< Query<( Entity, @@ -974,10 +1331,11 @@ pub fn extract_text_sections( text_styles: Extract>, camera_map: Extract, ) { - let mut start = extracted_uinodes.glyphs.len(); - let mut end = start + 1; - + let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + + let mut glyphs = vec![]; + for ( entity, uinode, @@ -991,7 +1349,10 @@ pub fn extract_text_sections( text_layout_info, text_scroll, cursor_style, - ) in &uinode_query + ) in extracted_uinodes + .changed + .iter() + .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`) if !inherited_visibility.get() || uinode.is_empty() { @@ -1066,7 +1427,7 @@ pub fn extract_text_sections( color }; - extracted_uinodes.glyphs.push(ExtractedGlyph { + glyphs.push(ExtractedGlyph { color, translation: *position, rect: atlas_info.rect, @@ -1077,27 +1438,31 @@ pub fn extract_text_sections( .get(i + 1) .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) { - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - image: atlas_info.texture, - clip, - extracted_camera_entity, - item: ExtractedUiItem::Glyphs { range: start..end }, - main_entity: entity.into(), - transform, - }); - start = end; + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, + image: atlas_info.texture, + clip, + extracted_camera_entity, + item: ExtractedUiItem::Glyphs { + glyphs: mem::take(&mut glyphs), + }, + transform, + }, + ); } - - end += 1; } } } pub fn extract_text_shadows( mut commands: Commands, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, uinode_query: Extract< Query<( Entity, @@ -1116,10 +1481,11 @@ pub fn extract_text_shadows( text_decoration_query: Extract, Has)>>, camera_map: Extract, ) { - let mut start = extracted_uinodes.glyphs.len(); - let mut end = start + 1; - + let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + + let mut glyphs = vec![]; + for ( entity, uinode, @@ -1132,7 +1498,10 @@ pub fn extract_text_shadows( shadow, computed_block, text_scroll, - ) in &uinode_query + ) in extracted_uinodes + .changed + .iter() + .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`) if !inherited_visibility.get() || uinode.is_empty() { @@ -1170,7 +1539,7 @@ pub fn extract_text_shadows( }, ) in text_layout_info.glyphs.iter().enumerate() { - extracted_uinodes.glyphs.push(ExtractedGlyph { + glyphs.push(ExtractedGlyph { color: shadow.color.into(), translation: *position, rect: atlas_info.rect, @@ -1180,20 +1549,24 @@ pub fn extract_text_shadows( info.section_index != *section_index || info.atlas_info.texture != atlas_info.texture }) { - extracted_uinodes.uinodes.push(ExtractedUiNode { - transform: node_transform, - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - image: atlas_info.texture, - clip, - extracted_camera_entity, - item: ExtractedUiItem::Glyphs { range: start..end }, - main_entity: entity.into(), - }); - start = end; + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .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, + extracted_camera_entity, + item: ExtractedUiItem::Glyphs { + glyphs: mem::take(&mut glyphs), + }, + }, + ); } - - end += 1; } for run in text_layout_info.run_geometry.iter() { @@ -1210,54 +1583,65 @@ pub fn extract_text_shadows( }; if has_strikethrough { - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - clip, - image: AssetId::default(), - extracted_camera_entity, - 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(), + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, + clip, + image: AssetId::default(), + extracted_camera_entity, + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } if has_underline { - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - clip, - image: AssetId::default(), - extracted_camera_entity, - 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(), + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, + clip, + image: AssetId::default(), + extracted_camera_entity, + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } } } @@ -1265,7 +1649,7 @@ pub fn extract_text_shadows( pub fn extract_text_decorations( mut commands: Commands, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, uinode_query: Extract< Query<( Entity, @@ -1290,7 +1674,9 @@ pub fn extract_text_decorations( >, camera_map: Extract, ) { + let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); + for ( entity, uinode, @@ -1302,7 +1688,10 @@ pub fn extract_text_decorations( camera, text_layout_info, text_scroll, - ) in &uinode_query + ) in extracted_uinodes + .changed + .iter() + .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`) if !inherited_visibility.get() || uinode.is_empty() { @@ -1348,28 +1737,33 @@ pub fn extract_text_decorations( }; if let Some(text_background_color) = text_background_color { - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - clip, - image: AssetId::default(), - extracted_camera_entity, - 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(), + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, + clip, + image: AssetId::default(), + extracted_camera_entity, + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } if maybe_strikethrough.is_some() { @@ -1378,28 +1772,34 @@ pub fn extract_text_decorations( .unwrap_or(text_color.0) .to_linear(); - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - clip, - image: AssetId::default(), - extracted_camera_entity, - transform: transform * Affine2::from_translation(run.strikethrough_position()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: run.strikethrough_size(), + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, + clip, + image: AssetId::default(), + extracted_camera_entity, + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } if maybe_underline.is_some() { @@ -1408,35 +1808,41 @@ pub fn extract_text_decorations( .unwrap_or(text_color.0) .to_linear(); - extracted_uinodes.uinodes.push(ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - clip, - image: AssetId::default(), - extracted_camera_entity, - transform: transform * Affine2::from_translation(run.underline_position()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: run.underline_size(), + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, + clip, + image: AssetId::default(), + extracted_camera_entity, + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } } } } #[repr(C)] -#[derive(Copy, Clone, Pod, Zeroable)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] struct UiVertex { pub position: [f32; 3], pub uv: [f32; 2], @@ -1482,7 +1888,7 @@ pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; -#[derive(Component)] +#[derive(Component, Debug)] pub struct UiBatch { pub range: Range, pub image: AssetId, @@ -1522,48 +1928,51 @@ pub fn queue_uinodes( let mut current_camera_entity = Entity::PLACEHOLDER; let mut current_phase = None; - for (index, extracted_uinode) in extracted_uinodes.uinodes.iter().enumerate() { - if current_camera_entity != extracted_uinode.extracted_camera_entity { - current_phase = render_views - .get(extracted_uinode.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, ui_anti_alias, transparent_phase)) - }) - }); - current_camera_entity = extracted_uinode.extracted_camera_entity; - } + for (main_entity, extracted_sub_uinodes) in extracted_uinodes.uinodes.iter() { + for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() { + if current_camera_entity != extracted_uinode.extracted_camera_entity { + current_phase = render_views + .get(extracted_uinode.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, ui_anti_alias, transparent_phase) + }) + }) + }); + current_camera_entity = extracted_uinode.extracted_camera_entity; + } - let Some((view, ui_anti_alias, transparent_phase)) = current_phase.as_mut() else { - continue; - }; + let Some((view, ui_anti_alias, transparent_phase)) = current_phase.as_mut() else { + continue; + }; - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_pipeline, - UiPipelineKey { - target_format: view.target_format, - anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), - }, - ); + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_pipeline, + UiPipelineKey { + target_format: view.target_format, + anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), + }, + ); - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline, - entity: (extracted_uinode.render_entity, extracted_uinode.main_entity), - sort_key: FloatOrd(extracted_uinode.z_order), - index, - // batch_range will be calculated in prepare_uinodes - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - indexed: true, - }); + transparent_phase.add_transient(TransparentUi { + draw_function, + 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, + }); + } } } @@ -1578,7 +1987,7 @@ pub fn prepare_uinodes( render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut, - mut extracted_uinodes: ResMut, + extracted_uinodes: Res, view_uniforms: Res, ui_pipeline: Res, mut image_bind_groups: ResMut, @@ -1623,13 +2032,17 @@ pub fn prepare_uinodes( let item = &mut ui_phase.items[item_index]; let Some(extracted_uinode) = extracted_uinodes .uinodes - .get(item.index) - .filter(|n| item.entity() == n.render_entity) + .get(&item.main_entity()) + .and_then(|sub_uinodes| sub_uinodes.get(&item.entity())) else { batch_image_handle = None; continue; }; + // Initialize the batch range to be zero-length initially. + // We'll extend it as we accumulate items into this batch. + item.batch_range = (item_index as u32)..(item_index as u32); + let mut existing_batch = batches.last_mut(); if batch_image_handle.is_none() @@ -1646,7 +2059,6 @@ pub fn prepare_uinodes( range: vertices_index..vertices_index, image: extracted_uinode.image, }; - batches.push((item.entity(), new_batch)); image_bind_groups @@ -1790,14 +2202,14 @@ pub fn prepare_uinodes( .map(|scaling| image.size_2d().as_vec2() * scaling) .unwrap_or(uinode_rect.max); if *flip_x { - core::mem::swap(&mut uinode_rect.max.x, &mut uinode_rect.min.x); + mem::swap(&mut uinode_rect.max.x, &mut uinode_rect.min.x); positions_diff[0].x *= -1.; positions_diff[1].x *= -1.; positions_diff[2].x *= -1.; positions_diff[3].x *= -1.; } if *flip_y { - core::mem::swap(&mut uinode_rect.max.y, &mut uinode_rect.min.y); + mem::swap(&mut uinode_rect.max.y, &mut uinode_rect.min.y); positions_diff[0].y *= -1.; positions_diff[1].y *= -1.; positions_diff[2].y *= -1.; @@ -1836,7 +2248,7 @@ pub fn prepare_uinodes( } for i in 0..4 { - ui_meta.vertices.push(UiVertex { + let ui_vertex = UiVertex { position: positions_clipped[i].into(), uv: uvs[i].into(), color, @@ -1850,7 +2262,8 @@ pub fn prepare_uinodes( ], size: rect_size.into(), point: points[i].into(), - }); + }; + ui_meta.vertices.push(ui_vertex); } for &i in &QUAD_INDICES { @@ -1860,14 +2273,14 @@ pub fn prepare_uinodes( vertices_index += 6; indices_index += 4; } - ExtractedUiItem::Glyphs { range } => { + ExtractedUiItem::Glyphs { glyphs } => { let image = gpu_images .get(extracted_uinode.image) .expect("Image was checked during batching and should still exist"); let atlas_extent = image.size_2d().as_vec2(); - for glyph in &extracted_uinodes.glyphs[range.clone()] { + for glyph in glyphs { let color = glyph.color.to_f32_array(); let glyph_rect = glyph.rect; let rect_size = glyph_rect.size(); @@ -1974,5 +2387,16 @@ pub fn prepare_uinodes( *previous_len = batches.len(); commands.try_insert_batch(batches); } - extracted_uinodes.clear(); +} + +/// A render-world system that removes all [`UiBatch`] components. +/// +/// They're currently rebuilt from scratch every frame, so we have to remove +/// them. +/// +/// This is run during the render cleanup phase. +pub fn clear_batches(mut commands: Commands, batches_query: Query>) { + for entity in &batches_query { + commands.entity(entity).remove::(); + } } diff --git a/crates/bevy_ui_render/src/render_pass.rs b/crates/bevy_ui_render/src/render_pass.rs index 116c68c171405..90ea1d454513b 100644 --- a/crates/bevy_ui_render/src/render_pass.rs +++ b/crates/bevy_ui_render/src/render_pass.rs @@ -74,6 +74,7 @@ pub fn ui_pass( pass_span.end(&mut render_pass); } +#[derive(Debug)] pub struct TransparentUi { pub sort_key: FloatOrd, pub entity: (Entity, MainEntity), @@ -81,7 +82,6 @@ pub struct TransparentUi { pub draw_function: DrawFunctionId, pub batch_range: Range, pub extra_index: PhaseItemExtraIndex, - pub index: usize, pub indexed: bool, } diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index 50735ef30cb27..950d5d793cc93 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::{sync_world::TemporaryRenderEntity, Extract}; +use bevy_render::Extract; use bevy_sprite::BorderRect; use bevy_text::{EditableText, TextColor, TextCursorStyle, TextLayoutInfo}; use bevy_ui::{ @@ -18,7 +18,7 @@ use crate::{ pub fn extract_text_cursor( mut commands: Commands, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, text_node_query: Extract< Query<( Entity, @@ -36,6 +36,7 @@ pub fn extract_text_cursor( camera_map: Extract, input_focus: Extract>>, ) { + let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); for ( @@ -49,7 +50,10 @@ pub fn extract_text_cursor( text_layout_info, cursor_style, text_scroll, - ) in text_node_query.iter() + ) in extracted_uinodes + .changed + .iter() + .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`) if !inherited_visibility.get() || uinode.is_empty() { @@ -136,28 +140,33 @@ pub fn extract_text_cursor( } } - extracted_uinodes.uinodes.push(ExtractedUiNode { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SELECTION, - clip, - image: AssetId::default(), - extracted_camera_entity, - transform: transform * Affine2::from_translation(selection.center()), - item: ExtractedUiItem::Node { - color: selection_color, - rect: Rect { - min: Vec2::ZERO, - max: selection.size(), + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SELECTION, + clip, + image: AssetId::default(), + extracted_camera_entity, + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius, - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } } @@ -165,35 +174,40 @@ pub fn extract_text_cursor( && !cursor_rect.is_empty() && !cursor_style.color.is_fully_transparent() { - extracted_uinodes.uinodes.push(ExtractedUiNode { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_CURSOR, - clip, - image: AssetId::default(), - extracted_camera_entity, - 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(), + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_CURSOR, + clip, + image: AssetId::default(), + extracted_camera_entity, + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius: ResolvedBorderRadius::default(), - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } } } pub fn extract_preedit_underlines( mut commands: Commands, - mut extracted_uinodes: ResMut, + extracted_uinodes: ResMut, text_node_query: Extract< Query< ( @@ -213,6 +227,7 @@ pub fn extract_preedit_underlines( >, camera_map: Extract, ) { + let extracted_uinodes = extracted_uinodes.into_inner(); let mut camera_mapper = camera_map.get_mapper(); for ( @@ -226,7 +241,10 @@ pub fn extract_preedit_underlines( target_camera, stack_index, text_scroll, - ) in text_node_query.iter() + ) in extracted_uinodes + .changed + .iter() + .flat_map(|main_entity| text_node_query.get(main_entity.entity()).ok()) { if !inherited_visibility.get() || uinode.is_empty() @@ -258,28 +276,33 @@ 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.push(ExtractedUiNode { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, - clip, - image: AssetId::default(), - extracted_camera_entity, - transform: transform * Affine2::from_translation(rect.center()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: rect.size(), + extracted_uinodes + .uinodes + .entry(entity.into()) + .or_default() + .insert( + commands.spawn_empty().id(), + ExtractedUiNode { + z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, + clip, + image: AssetId::default(), + extracted_camera_entity, + 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, + }, }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius: ResolvedBorderRadius::default(), - node_type: NodeType::Rect, - }, - main_entity: entity.into(), - }); + ); } } } diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index d9cbbb520c333..4a6d00f1dff7a 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -17,7 +17,7 @@ use bevy_render::{ render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, - sync_world::{MainEntity, TemporaryRenderEntity}, + sync_world::MainEntity, view::*, Extract, ExtractSchedule, Render, RenderSystems, }; @@ -301,13 +301,16 @@ pub struct ExtractedUiMaterialNode { // it is defaulted to a single camera if only one exists. // Nodes with ambiguous camera will be ignored. pub extracted_camera_entity: Entity, - pub main_entity: MainEntity, - pub render_entity: Entity, } +/// A render-world resource that stores all material nodes in the scene. #[derive(Resource)] pub struct ExtractedUiMaterialNodes { - pub uinodes: Vec>, + /// The list of material nodes. + /// + /// 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>>, } impl Default for ExtractedUiMaterialNodes { @@ -323,19 +326,49 @@ pub fn extract_ui_material_nodes( mut extracted_uinodes: ResMut>, materials: Extract>>, uinode_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &MaterialNode, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - )>, + Query< + ( + Entity, + &ComputedNode, + &ComputedStackIndex, + &UiGlobalTransform, + &MaterialNode, + &InheritedVisibility, + Option<&CalculatedClip>, + &ComputedUiTargetCamera, + ), + Or<( + Changed, + Changed, + Changed, + Changed>, + Changed, + Changed, + Changed, + )>, + >, >, camera_map: Extract, + ( + mut removed_computed_node_query, + mut removed_computed_stack_index_query, + mut removed_ui_global_transform_query, + mut removed_material_node_query, + mut removed_inherited_visibility_query, + mut removed_calculated_clip_query, + mut removed_computed_ui_target_camera_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>>, + Extract>, + Extract>, + Extract>, + ), + mut nodes_processed_this_frame: Local, ) { + nodes_processed_this_frame.clear(); let mut camera_mapper = camera_map.get_mapper(); for ( @@ -363,21 +396,53 @@ pub fn extract_ui_material_nodes( continue; }; - extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - 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_camera_entity, - main_entity: entity.into(), - }); + let main_entity = MainEntity::from(entity); + nodes_processed_this_frame.insert(main_entity); + + extracted_uinodes + .uinodes + .entry(main_entity) + .or_default() + .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_camera_entity, + }, + ); + } + + // Only remove the render-world data if we didn't handle the node above. + // It's possible that a relevant component was removed and added in the same + // frame. + for main_entity in removed_computed_node_query + .read() + .chain(removed_computed_stack_index_query.read()) + .chain(removed_ui_global_transform_query.read()) + .chain(removed_material_node_query.read()) + .chain(removed_inherited_visibility_query.read()) + .chain(removed_calculated_clip_query.read()) + .chain(removed_computed_ui_target_camera_query.read()) + { + let main_entity = MainEntity::from(main_entity); + if nodes_processed_this_frame.contains(&main_entity) { + continue; + } + let Some(mut extracted_nodes) = extracted_uinodes.uinodes.remove(&main_entity) else { + continue; + }; + for (render_entity, _) in extracted_nodes.drain(..) { + commands.entity(render_entity).despawn(); + } } } @@ -387,7 +452,7 @@ pub fn prepare_uimaterial_nodes( render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut>, - mut extracted_uinodes: ResMut>, + extracted_uinodes: Res>, view_uniforms: Res, globals_buffer: Res, ui_material_pipeline: Res>, @@ -416,9 +481,13 @@ pub fn prepare_uimaterial_nodes( let item = &mut ui_phase.items[item_index]; if let Some(extracted_uinode) = extracted_uinodes .uinodes - .get(item.index) - .filter(|n| item.entity() == n.render_entity) + .get(&item.main_entity()) + .and_then(|subnodes| subnodes.get(&item.entity())) { + // Initialize the batch range to be zero-length initially. + // We'll extend it as we accumulate items into this batch. + item.batch_range = (item_index as u32)..(item_index as u32); + let mut existing_batch = batches .last_mut() .filter(|_| batch_shader_handle == Some(extracted_uinode.material)); @@ -544,7 +613,6 @@ pub fn prepare_uimaterial_nodes( *previous_len = batches.len(); commands.try_insert_batch(batches); } - extracted_uinodes.uinodes.clear(); } pub struct PreparedUiMaterial { @@ -606,48 +674,50 @@ pub fn queue_ui_material_nodes( { let draw_function = draw_functions.read().id::>(); - for (index, extracted_uinode) in extracted_uinodes.uinodes.iter().enumerate() { - let Some(material) = render_materials.get(extracted_uinode.material) else { - continue; - }; - - let Ok(default_camera_view) = - render_views.get_mut(extracted_uinode.extracted_camera_entity) - else { - continue; - }; - - let Ok(view) = camera_views.get(default_camera_view.0) else { - continue; - }; - - let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) - else { - continue; - }; - - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_material_pipeline, - UiMaterialKey { - target_format: view.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(), + for (main_entity, extracted_sub_uinodes) in extracted_uinodes.uinodes.iter() { + for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() { + let Some(material) = render_materials.get(extracted_uinode.material) else { + continue; + }; + + let Ok(default_camera_view) = + render_views.get_mut(extracted_uinode.extracted_camera_entity) + else { + continue; + }; + + let Ok(view) = camera_views.get(default_camera_view.0) else { + continue; + }; + + let Some(transparent_phase) = + transparent_render_phases.get_mut(&view.retained_view_entity) + else { + continue; + }; + + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_material_pipeline, + UiMaterialKey { + target_format: view.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, + }); } - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline, - entity: (extracted_uinode.render_entity, extracted_uinode.main_entity), - sort_key: FloatOrd(extracted_uinode.stack_index as f32 + M::stack_z_offset()), - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - index, - 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 fa6f4f3aff48b..3b01e84971ae7 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -203,13 +203,16 @@ pub struct ExtractedUiTextureSlice { pub flip_x: bool, pub flip_y: bool, pub inverse_scale_factor: f32, - pub main_entity: MainEntity, - pub render_entity: Entity, } +/// A render-world resource that stores all texture slices in the scene. #[derive(Resource, Default)] pub struct ExtractedUiTextureSlices { - pub slices: Vec, + /// The list of texture slices. + /// + /// 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>, } pub fn extract_ui_texture_slices( @@ -217,24 +220,61 @@ pub fn extract_ui_texture_slices( mut extracted_ui_slicers: ResMut, texture_atlases: Extract>>, slicers_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &ImageNode, - )>, + Query< + ( + Entity, + &ComputedNode, + &ComputedStackIndex, + &UiGlobalTransform, + &InheritedVisibility, + Option<&CalculatedClip>, + &ComputedUiTargetCamera, + &ImageNode, + ), + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + // The `bevy_ui::widget::update_image_content_size_system` marks + // `ImageNodeSize` as changed to indicate that the image metrics + // and/or texture atlas layout changed, so we need to watch for + // changes to that component, even though we don't read it. + Changed, + )>, + >, >, camera_map: Extract, + ( + mut removed_computed_node_query, + mut removed_computed_stack_index_query, + mut removed_ui_global_transform_query, + mut removed_inherited_visibility_query, + mut removed_calculated_clip_query, + mut removed_computed_ui_target_camera_query, + mut removed_image_node_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), + mut nodes_processed_this_frame: Local, ) { + nodes_processed_this_frame.clear(); let mut camera_mapper = camera_map.get_mapper(); for (entity, uinode, stack_index, transform, inherited_visibility, clip, camera, image) in &slicers_query { + let main_entity = MainEntity::from(entity); + let visual_box = match image.visual_box { VisualBox::ContentBox => uinode.content_box(), VisualBox::PaddingBox => uinode.padding_box(), @@ -268,6 +308,8 @@ pub fn extract_ui_texture_slices( continue; }; + nodes_processed_this_frame.insert(main_entity); + let atlas_rect = image .texture_atlas .as_ref() @@ -285,25 +327,55 @@ pub fn extract_ui_texture_slices( } }; - extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { - render_entity: commands.spawn(TemporaryRenderEntity::default()).id(), - 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(), - extracted_camera_entity, - image_scale_mode, - atlas_rect, - flip_x: image.flip_x, - flip_y: image.flip_y, - inverse_scale_factor: uinode.inverse_scale_factor, - main_entity: entity.into(), - }); + extracted_ui_slicers + .slices + .entry(main_entity) + .or_default() + .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(), + extracted_camera_entity, + 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. + // It's possible that a relevant component was removed and added in the same + // frame. + for main_entity in removed_computed_node_query + .read() + .chain(removed_computed_stack_index_query.read()) + .chain(removed_ui_global_transform_query.read()) + .chain(removed_inherited_visibility_query.read()) + .chain(removed_calculated_clip_query.read()) + .chain(removed_computed_ui_target_camera_query.read()) + .chain(removed_image_node_query.read()) + { + let main_entity = MainEntity::from(main_entity); + if nodes_processed_this_frame.contains(&main_entity) { + continue; + } + let Some(mut extracted_nodes) = extracted_ui_slicers.slices.remove(&main_entity) else { + continue; + }; + for (render_entity, _) in extracted_nodes.drain(..) { + commands.entity(render_entity).despawn(); + } } } @@ -322,40 +394,42 @@ pub fn queue_ui_slices( draw_functions: Res>, ) { let draw_function = draw_functions.read().id::(); - for (index, extracted_slicer) in extracted_ui_slicers.slices.iter().enumerate() { - let Ok(default_camera_view) = - render_views.get_mut(extracted_slicer.extracted_camera_entity) - else { - continue; - }; - - let Ok(view) = camera_views.get(default_camera_view.0) else { - continue; - }; - - let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) - else { - continue; - }; - - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_slicer_pipeline, - UiTextureSlicePipelineKey { - target_format: view.target_format, - }, - ); - - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline, - entity: (extracted_slicer.render_entity, extracted_slicer.main_entity), - sort_key: FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::IMAGE), - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - index, - indexed: true, - }); + for (main_entity, subslices) in extracted_ui_slicers.slices.iter() { + for (render_entity, extracted_slicer) in subslices.iter() { + let Ok(default_camera_view) = + render_views.get_mut(extracted_slicer.extracted_camera_entity) + else { + continue; + }; + + let Ok(view) = camera_views.get(default_camera_view.0) else { + continue; + }; + + let Some(transparent_phase) = + transparent_render_phases.get_mut(&view.retained_view_entity) + else { + continue; + }; + + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_slicer_pipeline, + UiTextureSlicePipelineKey { + target_format: view.target_format, + }, + ); + + transparent_phase.add_transient(TransparentUi { + draw_function, + 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, + }); + } } } @@ -365,7 +439,7 @@ pub fn prepare_ui_slices( render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut, - mut extracted_slices: ResMut, + extracted_slices: Res, view_uniforms: Res, texture_slicer_pipeline: Res, mut image_bind_groups: ResMut, @@ -411,9 +485,13 @@ pub fn prepare_ui_slices( let item = &mut ui_phase.items[item_index]; if let Some(texture_slices) = extracted_slices .slices - .get(item.index) - .filter(|n| item.entity() == n.render_entity) + .get(&item.main_entity()) + .and_then(|subslices| subslices.get(&item.entity())) { + // Initialize the batch range to be zero-length initially. + // We'll extend it as we accumulate items into this batch. + item.batch_range = (item_index as u32)..(item_index as u32); + let mut existing_batch = batches.last_mut(); if batch_image_handle.is_none() @@ -633,7 +711,6 @@ pub fn prepare_ui_slices( *previous_len = batches.len(); commands.try_insert_batch(batches); } - extracted_slices.slices.clear(); } pub type DrawUiTextureSlices = (