From c1229398aefbec2cc8aad192a7c524df10f511fe Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 10:13:24 +0100 Subject: [PATCH 01/99] Added extraction module with ExtractedUiNodeGeometry type and a skeleton system --- crates/bevy_ui_render/src/extraction.rs | 133 ++++++++++++++++++++++++ crates/bevy_ui_render/src/lib.rs | 1 + 2 files changed, 134 insertions(+) create mode 100644 crates/bevy_ui_render/src/extraction.rs diff --git a/crates/bevy_ui_render/src/extraction.rs b/crates/bevy_ui_render/src/extraction.rs new file mode 100644 index 0000000000000..3767431a798d7 --- /dev/null +++ b/crates/bevy_ui_render/src/extraction.rs @@ -0,0 +1,133 @@ +use bevy_camera::visibility::InheritedVisibility; +use bevy_ecs::entity::Entity; +use bevy_ecs::entity::EntityIndexMap; +use bevy_ecs::lifecycle::RemovedComponents; +use bevy_ecs::query::Changed; +use bevy_ecs::query::Or; +use bevy_ecs::resource::Resource; +use bevy_ecs::system::Query; +use bevy_math::Affine2; +use bevy_math::Rect; +use bevy_math::Vec2; +use bevy_render::sync_world::MainEntityHashMap; +use bevy_render::sync_world::MainEntityHashSet; +use bevy_render::Extract; +use bevy_sprite::BorderRect; +use bevy_ui::CalculatedClip; +use bevy_ui::ComputedNode; +use bevy_ui::ComputedStackIndex; +use bevy_ui::ComputedUiTargetCamera; +use bevy_ui::ResolvedBorderRadius; +use bevy_ui::UiGlobalTransform; + +use crate::ExtractedGlyph; + +// pub struct ExtractedUiNode { +// pub z_order: f32, +// pub image: AssetId, +// pub clip: Option, +// pub item: ExtractedUiItem, +// pub transform: Affine2, +// } + +// /// The type of UI node. +// /// This is used to determine how to render the UI node. +// #[derive(Clone, Copy, Debug, PartialEq)] +// pub enum NodeType { +// Rect, +// Inverted, +// Border(u32), // shader flags +// } + +// pub enum ExtractedUiItem { +// Node { +// color: LinearRgba, +// rect: Rect, +// atlas_scaling: Option, +// flip_x: bool, +// flip_y: bool, +// /// Border radius of the UI node. +// /// Ordering: top left, top right, bottom right, bottom left. +// border_radius: ResolvedBorderRadius, +// /// Border thickness of the UI node. +// /// Ordering: left, top, right, bottom. +// border: BorderRect, +// node_type: NodeType, +// }, +// /// A contiguous sequence of text glyphs from the same section +// Glyphs { +// /// The color, position, and UV rect of each glyph. +// glyphs: Vec, +// }, +// } + +pub struct ExtractedUiNodeGeometry { + pub computed_node: ComputedNode, + pub transform: Affine2, + pub clip: Option, + pub atlas_scaling: Option, + pub flip_x: bool, + pub flip_y: bool, + pub border_radius: ResolvedBorderRadius, + pub border: BorderRect, +} + +pub struct ExtractedGlyphLayout { + pub glyphs: Vec, +} + +/// The list of UI nodes, as well as the set of nodes that changed. +/// +/// This is a two-level data structure so that we can quickly remove all +/// gradients associated with a main-world entity when it changes. +#[derive(Resource, Default)] +pub struct ExtractedUiGeometries { + /// The list of UI nodes grouped by their main-world entity, along with + /// each group's target camera entity. + /// + /// This is a two-level data structure so that we can quickly remove all UI + /// nodes associated with a main-world entity when it changes. + pub uinodes: MainEntityHashMap<(Entity, EntityIndexMap)>, + /// UI nodes that changed this frame. + pub changed: MainEntityHashSet, +} + +pub fn extract_uinode_geometry( + changed_geometry_query: Extract< + Query< + ( + Entity, + &ComputedNode, + &ComputedStackIndex, + &ComputedUiTargetCamera, + &InheritedVisibility, + &UiGlobalTransform, + Option<&CalculatedClip>, + ), + Or<( + 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_ui_global_transform_query, + mut removed_inherited_visibility_query, + mut removed_calculated_clip_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), +) { +} diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 9820b4a8a021a..6ce2ac04a5bee 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -19,6 +19,7 @@ pub mod ui_texture_slice_pipeline; #[cfg(feature = "bevy_ui_debug")] mod debug_overlay; +pub mod extraction; use bevy_a11y::AccessibilitySystems; use bevy_camera::visibility::InheritedVisibility; From 60ae4f39100feead380cedc60c8c5a0830c56a57 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 10:27:31 +0100 Subject: [PATCH 02/99] Add camera refs --- crates/bevy_ui_render/src/extraction.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/crates/bevy_ui_render/src/extraction.rs b/crates/bevy_ui_render/src/extraction.rs index 3767431a798d7..3add4c5a9859a 100644 --- a/crates/bevy_ui_render/src/extraction.rs +++ b/crates/bevy_ui_render/src/extraction.rs @@ -21,6 +21,7 @@ use bevy_ui::ResolvedBorderRadius; use bevy_ui::UiGlobalTransform; use crate::ExtractedGlyph; +use crate::UiCameraMap; // pub struct ExtractedUiNode { // pub z_order: f32, @@ -76,23 +77,17 @@ pub struct ExtractedGlyphLayout { pub glyphs: Vec, } -/// The list of UI nodes, as well as the set of nodes that changed. -/// -/// This is a two-level data structure so that we can quickly remove all -/// gradients associated with a main-world entity when it changes. +/// Uinode geometries and list of geometries that changed this frame. #[derive(Resource, Default)] pub struct ExtractedUiGeometries { - /// The list of UI nodes grouped by their main-world entity, along with - /// each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all UI - /// nodes associated with a main-world entity when it changes. - pub uinodes: MainEntityHashMap<(Entity, EntityIndexMap)>, - /// UI nodes that changed this frame. + /// map from extracted camera entity -> main world entity -> node geometry + pub uinodes: MainEntityHashMap>, + /// Main world entities with UI node geometry that changed this frame. pub changed: MainEntityHashSet, } pub fn extract_uinode_geometry( + camera_map: Extract, changed_geometry_query: Extract< Query< ( From 30c1006f1f3e3f3718f4cd605e705449021688f9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 10:29:30 +0100 Subject: [PATCH 03/99] Remove border as already in ComputedNode --- crates/bevy_ui_render/src/extraction.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bevy_ui_render/src/extraction.rs b/crates/bevy_ui_render/src/extraction.rs index 3add4c5a9859a..041dab9d6bb8e 100644 --- a/crates/bevy_ui_render/src/extraction.rs +++ b/crates/bevy_ui_render/src/extraction.rs @@ -69,8 +69,6 @@ pub struct ExtractedUiNodeGeometry { pub atlas_scaling: Option, pub flip_x: bool, pub flip_y: bool, - pub border_radius: ResolvedBorderRadius, - pub border: BorderRect, } pub struct ExtractedGlyphLayout { From faf4dc20177affd813c4e0798fd996599cbb57a4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 10:55:21 +0100 Subject: [PATCH 04/99] Renamed extraction module to extract_geometry. Added rough geometry extraction impl. --- .../{extraction.rs => extract_geometry.rs} | 83 ++++++++++++++++++- 1 file changed, 80 insertions(+), 3 deletions(-) rename crates/bevy_ui_render/src/{extraction.rs => extract_geometry.rs} (58%) diff --git a/crates/bevy_ui_render/src/extraction.rs b/crates/bevy_ui_render/src/extract_geometry.rs similarity index 58% rename from crates/bevy_ui_render/src/extraction.rs rename to crates/bevy_ui_render/src/extract_geometry.rs index 041dab9d6bb8e..9827a443b8159 100644 --- a/crates/bevy_ui_render/src/extraction.rs +++ b/crates/bevy_ui_render/src/extract_geometry.rs @@ -79,12 +79,13 @@ pub struct ExtractedGlyphLayout { #[derive(Resource, Default)] pub struct ExtractedUiGeometries { /// map from extracted camera entity -> main world entity -> node geometry - pub uinodes: MainEntityHashMap>, + pub uinode_geometries: MainEntityHashMap>, /// Main world entities with UI node geometry that changed this frame. pub changed: MainEntityHashSet, } pub fn extract_uinode_geometry( + mut extracted_geometries: bevy_ecs::system::ResMut, camera_map: Extract, changed_geometry_query: Extract< Query< @@ -111,16 +112,92 @@ pub fn extract_uinode_geometry( mut removed_computed_node_query, mut removed_computed_stack_index_query, mut removed_computed_ui_target_camera_query, - mut removed_ui_global_transform_query, mut removed_inherited_visibility_query, + mut removed_ui_global_transform_query, mut removed_calculated_clip_query, ): ( Extract>, Extract>, Extract>, - Extract>, Extract>, + Extract>, Extract>, ), ) { + // Changed list is cleared each frame, then repopulated + extracted_geometries.changed.clear(); + + // If UI Entities are missing any of these components their retained data will be deleted from the render + // world and they will be not be rendered: + // - ComputedNode + // - ComputedUiStackIndex + // - ComputedUiTargetCamera + // - InheritedVisibility + // - UiGlobalTransform + for entity in removed_computed_node_query + .read() + .chain(removed_computed_stack_index_query.read()) + .chain(removed_computed_ui_target_camera_query.read()) + .chain(removed_ui_global_transform_query.read()) + .chain(removed_inherited_visibility_query.read()) + { + let main_entity = entity.into(); + extracted_geometries.changed.insert(main_entity); + for uinodes in extracted_geometries.uinode_geometries.values_mut() { + uinodes.remove(&main_entity); + } + } + + for entity in removed_calculated_clip_query.read() { + let main_entity = entity.into(); + extracted_geometries.changed.insert(main_entity); + for uinodes in extracted_geometries.uinode_geometries.values_mut() { + if let Some(geometry) = uinodes.get_mut(&main_entity) { + geometry.clip = None; + } + } + } + + let mut camera_mapper = camera_map.get_mapper(); + + for ( + entity, + computed_node, + _stack_index, + computed_target, + inherited_visibility, + transform, + clip, + ) in &changed_geometry_query + { + let main_entity = entity.into(); + extracted_geometries.changed.insert(main_entity); + for uinodes in extracted_geometries.uinode_geometries.values_mut() { + uinodes.remove(&main_entity); + } + + if !inherited_visibility.get() { + continue; + } + + let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else { + continue; + }; + + extracted_geometries + .uinode_geometries + .entry(extracted_camera_entity.into()) + .or_default() + .insert( + main_entity, + ExtractedUiNodeGeometry { + computed_node: *computed_node, + transform: transform.into(), + clip: clip.map(|clip| clip.clip), + atlas_scaling: None, + flip_x: false, + flip_y: false, + }, + ); + } } From 37a806c9b2f949afa12c84c94fe58aafd2768e13 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 11:32:33 +0100 Subject: [PATCH 05/99] fix module import --- crates/bevy_ui_render/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 6ce2ac04a5bee..92998becd82d3 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -19,7 +19,7 @@ pub mod ui_texture_slice_pipeline; #[cfg(feature = "bevy_ui_debug")] mod debug_overlay; -pub mod extraction; +pub mod extract_geometry; use bevy_a11y::AccessibilitySystems; use bevy_camera::visibility::InheritedVisibility; From 79974d3c652b1f41d71aa624a5a2552f6a9f6581 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 11:33:36 +0100 Subject: [PATCH 06/99] Move removal to end of extract_uinode_geometry --- crates/bevy_ui_render/src/extract_geometry.rs | 126 +++++++++++------- 1 file changed, 75 insertions(+), 51 deletions(-) diff --git a/crates/bevy_ui_render/src/extract_geometry.rs b/crates/bevy_ui_render/src/extract_geometry.rs index 9827a443b8159..3759589dbb2cd 100644 --- a/crates/bevy_ui_render/src/extract_geometry.rs +++ b/crates/bevy_ui_render/src/extract_geometry.rs @@ -1,5 +1,6 @@ use bevy_camera::visibility::InheritedVisibility; use bevy_ecs::entity::Entity; +use bevy_ecs::entity::EntityHashMap; use bevy_ecs::entity::EntityIndexMap; use bevy_ecs::lifecycle::RemovedComponents; use bevy_ecs::query::Changed; @@ -69,17 +70,16 @@ pub struct ExtractedUiNodeGeometry { pub atlas_scaling: Option, pub flip_x: bool, pub flip_y: bool, -} - -pub struct ExtractedGlyphLayout { - pub glyphs: Vec, + pub stack_index: u32, } /// Uinode geometries and list of geometries that changed this frame. #[derive(Resource, Default)] pub struct ExtractedUiGeometries { - /// map from extracted camera entity -> main world entity -> node geometry - pub uinode_geometries: MainEntityHashMap>, + /// Map from extracted camera view render world entity -> UI node main world entity + pub camera_to_uinode_map: EntityHashMap, + /// Map from main world entity -> node geometry + pub uinode_geometries: MainEntityHashMap, /// Main world entities with UI node geometry that changed this frame. pub changed: MainEntityHashSet, } @@ -108,6 +108,18 @@ pub fn extract_uinode_geometry( )>, >, >, + unfiltered_geometry_query: Extract< + Query<( + Entity, + &ComputedNode, + &ComputedStackIndex, + &ComputedUiTargetCamera, + &InheritedVisibility, + &UiGlobalTransform, + Option<&CalculatedClip>, + )>, + >, + ( mut removed_computed_node_query, mut removed_computed_stack_index_query, @@ -127,27 +139,6 @@ pub fn extract_uinode_geometry( // Changed list is cleared each frame, then repopulated extracted_geometries.changed.clear(); - // If UI Entities are missing any of these components their retained data will be deleted from the render - // world and they will be not be rendered: - // - ComputedNode - // - ComputedUiStackIndex - // - ComputedUiTargetCamera - // - InheritedVisibility - // - UiGlobalTransform - for entity in removed_computed_node_query - .read() - .chain(removed_computed_stack_index_query.read()) - .chain(removed_computed_ui_target_camera_query.read()) - .chain(removed_ui_global_transform_query.read()) - .chain(removed_inherited_visibility_query.read()) - { - let main_entity = entity.into(); - extracted_geometries.changed.insert(main_entity); - for uinodes in extracted_geometries.uinode_geometries.values_mut() { - uinodes.remove(&main_entity); - } - } - for entity in removed_calculated_clip_query.read() { let main_entity = entity.into(); extracted_geometries.changed.insert(main_entity); @@ -163,41 +154,74 @@ pub fn extract_uinode_geometry( for ( entity, computed_node, - _stack_index, + stack_index, computed_target, inherited_visibility, transform, clip, - ) in &changed_geometry_query - { + ) in changed_geometry_query.iter().chain( + removed_calculated_clip_query + .read() + .filter_map(|entity| unfiltered_geometry_query.get(entity).ok()), + ) { let main_entity = entity.into(); extracted_geometries.changed.insert(main_entity); - for uinodes in extracted_geometries.uinode_geometries.values_mut() { - uinodes.remove(&main_entity); - } + // Find extracted camera entity, otherwise remove both camera and UI node entity. + let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else { + extracted_geometries.uinode_geometries.remove(&main_entity); + continue; + }; + + // Remove non-visible from all maps if !inherited_visibility.get() { + extracted_geometries.uinode_geometries.remove(&main_entity); + extracted_geometries.camera_to_uinode_map[&extracted_camera_entity] + .remove(&main_entity); continue; } - let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else { - continue; - }; + extracted_geometries.uinode_geometries.insert( + main_entity, + ExtractedUiNodeGeometry { + computed_node: *computed_node, + transform: transform.into(), + clip: clip.map(|clip| clip.clip), + atlas_scaling: None, + flip_x: false, + flip_y: false, + stack_index: stack_index.0, + }, + ); + } + + // If UI Entities are missing any of these components their retained data will be deleted from the render + // world and they will be not be rendered: + // - ComputedNode + // - ComputedUiStackIndex + // - ComputedUiTargetCamera + // - InheritedVisibility + // - UiGlobalTransform + for entity in removed_computed_node_query + .read() + .chain(removed_computed_stack_index_query.read()) + .chain(removed_computed_ui_target_camera_query.read()) + .chain(removed_ui_global_transform_query.read()) + .chain(removed_inherited_visibility_query.read()) + { + let main_entity = entity.into(); - extracted_geometries - .uinode_geometries - .entry(extracted_camera_entity.into()) - .or_default() - .insert( - main_entity, - ExtractedUiNodeGeometry { - computed_node: *computed_node, - transform: transform.into(), - clip: clip.map(|clip| clip.clip), - atlas_scaling: None, - flip_x: false, - flip_y: false, - }, - ); + match extracted_geometries.changed.entry(main_entity) { + bevy_platform::collections::hash_set::Entry::Occupied(_) => { + // UI node entity + } + bevy_platform::collections::hash_set::Entry::Vacant(_) => {} + } + + extracted_geometries.changed.insert(main_entity); + + for uinodes in extracted_geometries.uinode_geometries.values_mut() { + uinodes.remove(&main_entity); + } } } From 66430e6020f454176f73db75ce7d08c3bc8e9aa7 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 12:00:56 +0100 Subject: [PATCH 07/99] Store camera on ExtractedUiNodeGeometry --- crates/bevy_ui_render/src/extract_geometry.rs | 39 ++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/crates/bevy_ui_render/src/extract_geometry.rs b/crates/bevy_ui_render/src/extract_geometry.rs index 3759589dbb2cd..e8c4e02ab11cd 100644 --- a/crates/bevy_ui_render/src/extract_geometry.rs +++ b/crates/bevy_ui_render/src/extract_geometry.rs @@ -1,6 +1,7 @@ use bevy_camera::visibility::InheritedVisibility; use bevy_ecs::entity::Entity; use bevy_ecs::entity::EntityHashMap; +use bevy_ecs::entity::EntityHashSet; use bevy_ecs::entity::EntityIndexMap; use bevy_ecs::lifecycle::RemovedComponents; use bevy_ecs::query::Changed; @@ -64,6 +65,7 @@ use crate::UiCameraMap; // } pub struct ExtractedUiNodeGeometry { + pub extracted_camera: Entity, pub computed_node: ComputedNode, pub transform: Affine2, pub clip: Option, @@ -76,8 +78,7 @@ pub struct ExtractedUiNodeGeometry { /// Uinode geometries and list of geometries that changed this frame. #[derive(Resource, Default)] pub struct ExtractedUiGeometries { - /// Map from extracted camera view render world entity -> UI node main world entity - pub camera_to_uinode_map: EntityHashMap, + pub extracted_camera_to_main_uinode_map: EntityHashMap, /// Map from main world entity -> node geometry pub uinode_geometries: MainEntityHashMap, /// Main world entities with UI node geometry that changed this frame. @@ -139,16 +140,6 @@ pub fn extract_uinode_geometry( // Changed list is cleared each frame, then repopulated extracted_geometries.changed.clear(); - for entity in removed_calculated_clip_query.read() { - let main_entity = entity.into(); - extracted_geometries.changed.insert(main_entity); - for uinodes in extracted_geometries.uinode_geometries.values_mut() { - if let Some(geometry) = uinodes.get_mut(&main_entity) { - geometry.clip = None; - } - } - } - let mut camera_mapper = camera_map.get_mapper(); for ( @@ -160,6 +151,7 @@ pub fn extract_uinode_geometry( transform, clip, ) in changed_geometry_query.iter().chain( + // `CalculatedClip` is optional, also update geometry if it is removed. removed_calculated_clip_query .read() .filter_map(|entity| unfiltered_geometry_query.get(entity).ok()), @@ -176,14 +168,17 @@ pub fn extract_uinode_geometry( // Remove non-visible from all maps if !inherited_visibility.get() { extracted_geometries.uinode_geometries.remove(&main_entity); - extracted_geometries.camera_to_uinode_map[&extracted_camera_entity] - .remove(&main_entity); + extracted_geometries + .extracted_camera_to_main_uinode_map + .get_mut(&extracted_camera_entity) + .map(|main_entities| main_entities.remove(&main_entity)); continue; } extracted_geometries.uinode_geometries.insert( main_entity, ExtractedUiNodeGeometry { + extracted_camera: extracted_camera_entity, computed_node: *computed_node, transform: transform.into(), clip: clip.map(|clip| clip.clip), @@ -213,15 +208,15 @@ pub fn extract_uinode_geometry( match extracted_geometries.changed.entry(main_entity) { bevy_platform::collections::hash_set::Entry::Occupied(_) => { - // UI node entity + // If changed, it passed the geometry query so the component must have been removed and reinserted in the same frame. + // Do nothing. + } + bevy_platform::collections::hash_set::Entry::Vacant(entry) => { + // Failed the geometry queries, does not have at least one of the components (if reinserted would be `Changed`). + // Add to changed list and remove from maps + entry.insert(); + extracted_geometries.uinode_geometries.remove(&main_entity); } - bevy_platform::collections::hash_set::Entry::Vacant(_) => {} - } - - extracted_geometries.changed.insert(main_entity); - - for uinodes in extracted_geometries.uinode_geometries.values_mut() { - uinodes.remove(&main_entity); } } } From ca4fa9a0858ef57027d0bf19edd54dcfa2c47d04 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 12:08:34 +0100 Subject: [PATCH 08/99] Fixed removal detection --- crates/bevy_ui_render/src/extract_geometry.rs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui_render/src/extract_geometry.rs b/crates/bevy_ui_render/src/extract_geometry.rs index e8c4e02ab11cd..869d4223c355d 100644 --- a/crates/bevy_ui_render/src/extract_geometry.rs +++ b/crates/bevy_ui_render/src/extract_geometry.rs @@ -206,16 +206,17 @@ pub fn extract_uinode_geometry( { let main_entity = entity.into(); - match extracted_geometries.changed.entry(main_entity) { - bevy_platform::collections::hash_set::Entry::Occupied(_) => { - // If changed, it passed the geometry query so the component must have been removed and reinserted in the same frame. - // Do nothing. - } - bevy_platform::collections::hash_set::Entry::Vacant(entry) => { - // Failed the geometry queries, does not have at least one of the components (if reinserted would be `Changed`). - // Add to changed list and remove from maps - entry.insert(); - extracted_geometries.uinode_geometries.remove(&main_entity); + // If already in changed, entity passed the geometry query so the component must have been removed and reinserted in the same frame, so do nothing. + if extracted_geometries.changed.insert(main_entity) { + // Failed the geometry queries, does not have at least one of the components (if reinserted would be `Changed`). + // Add to changed list and remove from maps + if let Some(extracted_geometry) = + extracted_geometries.uinode_geometries.remove(&main_entity) + { + extracted_geometries + .extracted_camera_to_main_uinode_map + .get_mut(&extracted_geometry.extracted_camera) + .map(|main_entities| main_entities.remove(&main_entity)); } } } From 39423fd68a399c861ef0cdf5629fffa77db40b7a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 12:12:18 +0100 Subject: [PATCH 09/99] Added extract_uinode_geometry to ExtractSchedule --- crates/bevy_ui_render/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 92998becd82d3..63c21bb8e9a5a 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -258,6 +258,7 @@ impl Plugin for UiRenderPlugin { .add_systems( ExtractSchedule, ( + extract_geometry::extract_uinode_geometry, extract_uinode_changes.in_set(RenderUiSystems::ExtractChanges), extract_ui_camera_view .after(extract_cameras) From f2301d976dad291d07f529bcfc303b48d725a426 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 12:39:18 +0100 Subject: [PATCH 10/99] Update camera map added debug system --- crates/bevy_ui_render/src/extract_geometry.rs | 72 +++++++++++++++---- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ui_render/src/extract_geometry.rs b/crates/bevy_ui_render/src/extract_geometry.rs index 869d4223c355d..d8d9812206061 100644 --- a/crates/bevy_ui_render/src/extract_geometry.rs +++ b/crates/bevy_ui_render/src/extract_geometry.rs @@ -175,19 +175,45 @@ pub fn extract_uinode_geometry( continue; } - extracted_geometries.uinode_geometries.insert( - main_entity, - ExtractedUiNodeGeometry { - extracted_camera: extracted_camera_entity, - computed_node: *computed_node, - transform: transform.into(), - clip: clip.map(|clip| clip.clip), - atlas_scaling: None, - flip_x: false, - flip_y: false, - stack_index: stack_index.0, - }, - ); + let uinode_geometry = ExtractedUiNodeGeometry { + extracted_camera: extracted_camera_entity, + computed_node: *computed_node, + transform: transform.into(), + clip: clip.map(|clip| clip.clip), + atlas_scaling: None, + flip_x: false, + flip_y: false, + stack_index: stack_index.0, + }; + + let ExtractedUiGeometries { + extracted_camera_to_main_uinode_map, + uinode_geometries, + .. + } = &mut *extracted_geometries; + + match uinode_geometries.entry(main_entity) { + bevy_platform::collections::hash_map::Entry::Occupied(mut entry) => { + let old_extracted_camera = entry.get().extracted_camera; + if old_extracted_camera != extracted_camera_entity { + extracted_camera_to_main_uinode_map + .get_mut(&old_extracted_camera) + .map(|main_entities| main_entities.remove(&main_entity)); + extracted_camera_to_main_uinode_map + .entry(extracted_camera_entity) + .or_default() + .insert(main_entity); + } + entry.insert(uinode_geometry); + } + bevy_platform::collections::hash_map::Entry::Vacant(entry) => { + extracted_camera_to_main_uinode_map + .entry(extracted_camera_entity) + .or_default() + .insert(main_entity); + entry.insert(uinode_geometry); + } + } } // If UI Entities are missing any of these components their retained data will be deleted from the render @@ -221,3 +247,23 @@ pub fn extract_uinode_geometry( } } } + +pub fn extract_uinode_geometry_debug( + extracted_geometries: bevy_ecs::system::Res, +) { + if 0 < extracted_geometries.changed.len() { + println!("\n"); + println!( + "camera count: {}", + extracted_geometries + .extracted_camera_to_main_uinode_map + .len() + ); + println!( + "node count: {}", + extracted_geometries.uinode_geometries.len() + ); + println!("changed count: {}", extracted_geometries.changed.len()); + println!("\n"); + } +} From 0bfebb15e32f3570d3a65cf8a2be4d80dfa2111f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 12:55:17 +0100 Subject: [PATCH 11/99] Added camera sync checking to debug system --- crates/bevy_ui_render/src/extract_geometry.rs | 20 +++++++++++++++++++ crates/bevy_ui_render/src/lib.rs | 2 ++ 2 files changed, 22 insertions(+) diff --git a/crates/bevy_ui_render/src/extract_geometry.rs b/crates/bevy_ui_render/src/extract_geometry.rs index d8d9812206061..32fabd0755605 100644 --- a/crates/bevy_ui_render/src/extract_geometry.rs +++ b/crates/bevy_ui_render/src/extract_geometry.rs @@ -264,6 +264,26 @@ pub fn extract_uinode_geometry_debug( extracted_geometries.uinode_geometries.len() ); println!("changed count: {}", extracted_geometries.changed.len()); + + for (e, g) in &extracted_geometries.uinode_geometries { + if let Some(es) = extracted_geometries + .extracted_camera_to_main_uinode_map + .get(&g.extracted_camera) + { + if !es.contains(e) { + println!( + "UI node {e:?} has extracted geometry for camera {:?}, but is missing from that camera's UI node map", + g.extracted_camera + ); + } + } else { + println!( + "UI node {e:?} has extracted geometry for camera {:?}, but that camera is missing from the camera-to-UI-node map", + g.extracted_camera + ); + } + } + println!("\n"); } } diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 63c21bb8e9a5a..4bab21bf73c07 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -259,6 +259,8 @@ impl Plugin for UiRenderPlugin { ExtractSchedule, ( extract_geometry::extract_uinode_geometry, + extract_geometry::extract_uinode_geometry_debug + .after(extract_geometry::extract_uinode_geometry), extract_uinode_changes.in_set(RenderUiSystems::ExtractChanges), extract_ui_camera_view .after(extract_cameras) From 7c476eb1bcd665438b49f2a9cc9f464245683ea6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 13:05:05 +0100 Subject: [PATCH 12/99] added init for ExtractedUiGeometries resource --- crates/bevy_ui_render/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 4bab21bf73c07..4481f782d17c4 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -84,6 +84,7 @@ pub use render_pass::*; pub use ui_material_pipeline::*; use ui_texture_slice_pipeline::UiTextureSlicerPlugin; +use crate::extract_geometry::ExtractedUiGeometries; use crate::shader_flags::INVERT; use crate::text::{extract_preedit_underlines, extract_text_cursor}; @@ -229,6 +230,7 @@ impl Plugin for UiRenderPlugin { .init_gpu_resource::>() .init_gpu_resource::() .init_gpu_resource::() + .init_resource::() .init_resource::() .allow_ambiguous_resource::() .init_resource::>() From 168eb3c43fa15244e8d64c7ab5a6676fc14d7e78 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 13:20:18 +0100 Subject: [PATCH 13/99] clean up --- crates/bevy_ui_render/src/extract_geometry.rs | 67 +++++-------------- 1 file changed, 16 insertions(+), 51 deletions(-) diff --git a/crates/bevy_ui_render/src/extract_geometry.rs b/crates/bevy_ui_render/src/extract_geometry.rs index 32fabd0755605..b2c2728363411 100644 --- a/crates/bevy_ui_render/src/extract_geometry.rs +++ b/crates/bevy_ui_render/src/extract_geometry.rs @@ -1,8 +1,7 @@ +use crate::UiCameraMap; use bevy_camera::visibility::InheritedVisibility; use bevy_ecs::entity::Entity; use bevy_ecs::entity::EntityHashMap; -use bevy_ecs::entity::EntityHashSet; -use bevy_ecs::entity::EntityIndexMap; use bevy_ecs::lifecycle::RemovedComponents; use bevy_ecs::query::Changed; use bevy_ecs::query::Or; @@ -14,56 +13,12 @@ use bevy_math::Vec2; use bevy_render::sync_world::MainEntityHashMap; use bevy_render::sync_world::MainEntityHashSet; use bevy_render::Extract; -use bevy_sprite::BorderRect; use bevy_ui::CalculatedClip; use bevy_ui::ComputedNode; use bevy_ui::ComputedStackIndex; use bevy_ui::ComputedUiTargetCamera; -use bevy_ui::ResolvedBorderRadius; use bevy_ui::UiGlobalTransform; -use crate::ExtractedGlyph; -use crate::UiCameraMap; - -// pub struct ExtractedUiNode { -// pub z_order: f32, -// pub image: AssetId, -// pub clip: Option, -// pub item: ExtractedUiItem, -// pub transform: Affine2, -// } - -// /// The type of UI node. -// /// This is used to determine how to render the UI node. -// #[derive(Clone, Copy, Debug, PartialEq)] -// pub enum NodeType { -// Rect, -// Inverted, -// Border(u32), // shader flags -// } - -// pub enum ExtractedUiItem { -// Node { -// color: LinearRgba, -// rect: Rect, -// atlas_scaling: Option, -// flip_x: bool, -// flip_y: bool, -// /// Border radius of the UI node. -// /// Ordering: top left, top right, bottom right, bottom left. -// border_radius: ResolvedBorderRadius, -// /// Border thickness of the UI node. -// /// Ordering: left, top, right, bottom. -// border: BorderRect, -// node_type: NodeType, -// }, -// /// A contiguous sequence of text glyphs from the same section -// Glyphs { -// /// The color, position, and UV rect of each glyph. -// glyphs: Vec, -// }, -// } - pub struct ExtractedUiNodeGeometry { pub extracted_camera: Entity, pub computed_node: ComputedNode, @@ -83,6 +38,8 @@ pub struct ExtractedUiGeometries { pub uinode_geometries: MainEntityHashMap, /// Main world entities with UI node geometry that changed this frame. pub changed: MainEntityHashSet, + /// Main world entities that were removed from rendering + pub removed: MainEntityHashSet, } pub fn extract_uinode_geometry( @@ -137,8 +94,12 @@ pub fn extract_uinode_geometry( Extract>, ), ) { + // Probably need to consolidate changed & removed sets somehow for efficiency + // Do we also need to track added? // Changed list is cleared each frame, then repopulated extracted_geometries.changed.clear(); + // Removed list is cleared each frame, then repopulated + extracted_geometries.removed.clear(); let mut camera_mapper = camera_map.get_mapper(); @@ -162,12 +123,14 @@ pub fn extract_uinode_geometry( // Find extracted camera entity, otherwise remove both camera and UI node entity. let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else { extracted_geometries.uinode_geometries.remove(&main_entity); + extracted_geometries.removed.insert(main_entity); continue; }; // Remove non-visible from all maps if !inherited_visibility.get() { extracted_geometries.uinode_geometries.remove(&main_entity); + extracted_geometries.removed.insert(main_entity); extracted_geometries .extracted_camera_to_main_uinode_map .get_mut(&extracted_camera_entity) @@ -218,11 +181,11 @@ pub fn extract_uinode_geometry( // If UI Entities are missing any of these components their retained data will be deleted from the render // world and they will be not be rendered: - // - ComputedNode - // - ComputedUiStackIndex - // - ComputedUiTargetCamera - // - InheritedVisibility - // - UiGlobalTransform + // * ComputedNode + // * ComputedUiStackIndex + // * ComputedUiTargetCamera + // * InheritedVisibility + // * UiGlobalTransform for entity in removed_computed_node_query .read() .chain(removed_computed_stack_index_query.read()) @@ -239,6 +202,7 @@ pub fn extract_uinode_geometry( if let Some(extracted_geometry) = extracted_geometries.uinode_geometries.remove(&main_entity) { + extracted_geometries.removed.insert(main_entity); extracted_geometries .extracted_camera_to_main_uinode_map .get_mut(&extracted_geometry.extracted_camera) @@ -263,6 +227,7 @@ pub fn extract_uinode_geometry_debug( "node count: {}", extracted_geometries.uinode_geometries.len() ); + println!("removed count: {}", extracted_geometries.removed.len()); println!("changed count: {}", extracted_geometries.changed.len()); for (e, g) in &extracted_geometries.uinode_geometries { From c31478189fc7d7f85816b64e45dbe2ca054fec5b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 13:33:56 +0100 Subject: [PATCH 14/99] Change extract_ui_material_nodes to use extracted geometry --- .../src/ui_material_pipeline.rs | 115 +++--------------- 1 file changed, 14 insertions(+), 101 deletions(-) diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index b04757cb866df..d907da7f43660 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -291,14 +291,8 @@ impl RenderCommand

for DrawUiMaterialNode { } } -pub struct ExtractedUiMaterialNode { - pub stack_index: u32, - pub transform: Affine2, - pub rect: Rect, - pub border: BorderRect, - pub border_radius: [[f32; 4]; 2], +pub struct ExtractedUiMaterial { pub material: AssetId, - pub clip: Option, } /// A render-world resource that stores all material nodes in the scene. @@ -309,7 +303,7 @@ pub struct ExtractedUiMaterialNodes { /// /// This is a two-level data structure so that we can quickly remove all /// material nodes associated with a main-world entity when it changes. - pub uinodes: MainEntityHashMap<(Entity, EntityIndexMap>)>, + pub uinodes: MainEntityHashMap>>, } impl Default for ExtractedUiMaterialNodes { @@ -324,47 +318,9 @@ pub fn extract_ui_material_nodes( mut commands: Commands, mut extracted_uinodes: ResMut>, materials: Extract>>, - uinode_query: Extract< - Query< - ( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &MaterialNode, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - ), - Or<( - Changed, - Changed, - Changed, - Changed>, - Changed, - Changed, - Changed, - )>, - >, - >, + uinode_query: Extract), 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 removed_material_node_query,): (Extract>>,), mut nodes_to_reextract_next_frame: Local, mut nodes_processed_this_frame: Local, ) { @@ -372,16 +328,7 @@ pub fn extract_ui_material_nodes( let mut camera_mapper = camera_map.get_mapper(); let nodes_to_reextract = mem::take(&mut *nodes_to_reextract_next_frame); - for ( - entity, - computed_node, - stack_index, - transform, - handle, - inherited_visibility, - clip, - camera, - ) in uinode_query.iter().chain( + for (entity, handle) in uinode_query.iter().chain( nodes_to_reextract .into_iter() .filter_map(|main_entity| uinode_query.get(main_entity.entity()).ok()), @@ -399,16 +346,11 @@ pub fn extract_ui_material_nodes( .uinodes .get_mut(&main_entity) .iter_mut() - .flat_map(|(_, nodes)| nodes.drain(..)) + .flat_map(|nodes| nodes.drain(..)) { commands.entity(render_entity).despawn(); } - // skip invisible nodes - if !inherited_visibility.get() || computed_node.is_empty() { - continue; - } - // If the material hasn't finished loading, skip the entity, and // remember that we did so that we reextract the node next frame. if !materials.contains(handle) { @@ -416,54 +358,25 @@ pub fn extract_ui_material_nodes( continue; } - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; - }; - if let Some((camera_entity, _)) = extracted_uinodes.uinodes.get_mut(&main_entity) { - *camera_entity = extracted_camera_entity; - } - nodes_processed_this_frame.insert(main_entity); - extracted_uinodes - .uinodes - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiMaterialNode { - stack_index: stack_index.0, - transform: transform.into(), - material: handle.id(), - rect: Rect { - min: Vec2::ZERO, - max: computed_node.size(), - }, - border: computed_node.border(), - border_radius: computed_node.border_radius().into(), - clip: clip.map(|clip| clip.clip), - }, - ); + extracted_uinodes.uinodes.entry(main_entity).insert(( + commands.spawn_empty().id(), + ExtractedUiMaterial { + material: handle.id(), + }, + )); } // 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()) - { + for main_entity in removed_material_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_uinodes.uinodes.remove(&main_entity) else { + let Some(mut extracted_nodes) = extracted_uinodes.uinodes.remove(&main_entity) else { continue; }; for (render_entity, _) in extracted_nodes.drain(..) { From 8a1c7eebb1363c83f43ef525329b3609790d5e46 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 13:48:29 +0100 Subject: [PATCH 15/99] update materials map --- .../src/ui_material_pipeline.rs | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index d907da7f43660..0f152ab6b312c 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -11,6 +11,7 @@ use bevy_ecs::{ }; use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_mesh::VertexBufferLayout; +use bevy_platform::collections::hash_map::{OccupiedEntry, VacantEntry}; use bevy_render::{ globals::{GlobalsBuffer, GlobalsUniform}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, @@ -298,12 +299,8 @@ pub struct ExtractedUiMaterial { /// A render-world resource that stores all material nodes in the scene. #[derive(Resource)] pub struct ExtractedUiMaterialNodes { - /// The list of material nodes grouped by their main-world entity, along with - /// each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all - /// material nodes associated with a main-world entity when it changes. - pub uinodes: MainEntityHashMap>>, + /// Map from main-world UI entity to render entity and UI material node + pub uinodes: MainEntityHashMap<(Entity, ExtractedUiMaterial)>, } impl Default for ExtractedUiMaterialNodes { @@ -316,7 +313,7 @@ impl Default for ExtractedUiMaterialNodes { pub fn extract_ui_material_nodes( mut commands: Commands, - mut extracted_uinodes: ResMut>, + mut extracted_uinodes: Res>, materials: Extract>>, uinode_query: Extract), Changed>>>, camera_map: Extract, @@ -360,12 +357,21 @@ pub fn extract_ui_material_nodes( nodes_processed_this_frame.insert(main_entity); - extracted_uinodes.uinodes.entry(main_entity).insert(( - commands.spawn_empty().id(), - ExtractedUiMaterial { - material: handle.id(), - }, - )); + match extracted_uinodes.uinodes.entry(main_entity) { + bevy_platform::collections::hash_map::Entry::Occupied(mut entry) => { + let (_, material) = entry.get_mut(); + material.material = handle.id(); + } + bevy_platform::collections::hash_map::Entry::Vacant(entry) => { + let entity = commands.spawn_empty().id(); + entry.insert(( + entity, + ExtractedUiMaterial { + material: handle.id(), + }, + )); + } + } } // Only remove the render-world data if we didn't handle the node above. @@ -376,12 +382,10 @@ pub fn extract_ui_material_nodes( if nodes_processed_this_frame.contains(&main_entity) { continue; } - let Some(mut extracted_nodes) = extracted_uinodes.uinodes.remove(&main_entity) else { + let Some((entity, _)) = extracted_uinodes.uinodes.remove(&main_entity) else { continue; }; - for (render_entity, _) in extracted_nodes.drain(..) { - commands.entity(render_entity).despawn(); - } + commands.entity(entity).despawn(); } } From cdbfc1a25519fad01981e08ee432a39fef6eb34e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 13:54:33 +0100 Subject: [PATCH 16/99] updated queue_uinodes function --- .../src/ui_material_pipeline.rs | 67 ++++++++++--------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 0f152ab6b312c..97c82e2db7826 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -604,6 +604,7 @@ impl RenderAsset for PreparedUiMaterial { pub fn queue_ui_material_nodes( extracted_uinodes: Res>, + extracted_geometry: Res, draw_functions: Res>, ui_material_pipeline: Res>, mut pipelines: ResMut>>, @@ -619,13 +620,16 @@ pub fn queue_ui_material_nodes( let mut current_camera_entity = Entity::PLACEHOLDER; let mut current_phase = None; - for (main_entity, (extracted_camera_entity, extracted_sub_uinodes)) in - extracted_uinodes.uinodes.iter() - { - if current_camera_entity != *extracted_camera_entity { + for (main_entity, (render_entity, extracted_uinode)) in extracted_uinodes.uinodes.iter() { + let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + continue; + }; + let extracted_camera_entity = geometry.extracted_camera; + + if current_camera_entity != extracted_camera_entity { current_phase = render_views - .get(*extracted_camera_entity) + .get(extracted_camera_entity) .ok() .and_then(|default_camera_view| { camera_views @@ -639,39 +643,38 @@ pub fn queue_ui_material_nodes( }) }) }); - current_camera_entity = *extracted_camera_entity; + current_camera_entity = extracted_camera_entity; } let Some((target_format, transparent_phase)) = current_phase.as_mut() else { continue; }; - for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() { - let Some(material) = render_materials.get(extracted_uinode.material) else { - continue; - }; - - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_material_pipeline, - UiMaterialKey { - target_format: *target_format, - bind_group_data: material.key.clone(), - }, + + let Some(material) = render_materials.get(extracted_uinode.material) else { + continue; + }; + + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_material_pipeline, + UiMaterialKey { + target_format: *target_format, + bind_group_data: material.key.clone(), + }, + ); + if transparent_phase.items.capacity() < extracted_uinodes.uinodes.len() { + transparent_phase.items.reserve_exact( + extracted_uinodes.uinodes.len() - transparent_phase.items.capacity(), ); - 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: (*render_entity, *main_entity), + sort_key: FloatOrd(geometry.stack_index as f32 + M::stack_z_offset()), + batch_range: 0..0, + extra_index: PhaseItemExtraIndex::None, + indexed: false, + }); } } From c4a442385505d976fb62cdde94210f271e6d0bf1 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 14:08:57 +0100 Subject: [PATCH 17/99] update prepare_uimaterial_nodes --- .../src/ui_material_pipeline.rs | 63 ++++++++----------- 1 file changed, 26 insertions(+), 37 deletions(-) diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 97c82e2db7826..e7726793bd6ff 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -9,9 +9,8 @@ use bevy_ecs::{ *, }, }; -use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; +use bevy_math::{FloatOrd, Rect, Vec2}; use bevy_mesh::VertexBufferLayout; -use bevy_platform::collections::hash_map::{OccupiedEntry, VacantEntry}; use bevy_render::{ globals::{GlobalsBuffer, GlobalsUniform}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, @@ -24,8 +23,6 @@ use bevy_render::{ }; use bevy_render::{GpuResourceAppExt, RenderApp, RenderStartup}; use bevy_shader::{load_shader_library, Shader, ShaderRef}; -use bevy_sprite::BorderRect; -use bevy_ui::ComputedStackIndex; use bevy_utils::default; use bytemuck::{Pod, Zeroable}; use core::{hash::Hash, marker::PhantomData, ops::Range}; @@ -313,16 +310,14 @@ impl Default for ExtractedUiMaterialNodes { pub fn extract_ui_material_nodes( mut commands: Commands, - mut extracted_uinodes: Res>, + mut extracted_uinodes: ResMut>, materials: Extract>>, uinode_query: Extract), Changed>>>, - camera_map: Extract, (mut removed_material_node_query,): (Extract>>,), mut nodes_to_reextract_next_frame: Local, mut nodes_processed_this_frame: Local, ) { nodes_processed_this_frame.clear(); - let mut camera_mapper = camera_map.get_mapper(); let nodes_to_reextract = mem::take(&mut *nodes_to_reextract_next_frame); for (entity, handle) in uinode_query.iter().chain( @@ -338,15 +333,6 @@ pub fn extract_ui_material_nodes( if nodes_processed_this_frame.contains(&main_entity) { continue; } - // If there were any previous UI nodes for this entity, despawn them. - for (render_entity, _) in extracted_uinodes - .uinodes - .get_mut(&main_entity) - .iter_mut() - .flat_map(|nodes| nodes.drain(..)) - { - commands.entity(render_entity).despawn(); - } // If the material hasn't finished loading, skip the entity, and // remember that we did so that we reextract the node next frame. @@ -358,11 +344,11 @@ pub fn extract_ui_material_nodes( nodes_processed_this_frame.insert(main_entity); match extracted_uinodes.uinodes.entry(main_entity) { - bevy_platform::collections::hash_map::Entry::Occupied(mut entry) => { + Entry::Occupied(mut entry) => { let (_, material) = entry.get_mut(); material.material = handle.id(); } - bevy_platform::collections::hash_map::Entry::Vacant(entry) => { + Entry::Vacant(entry) => { let entity = commands.spawn_empty().id(); entry.insert(( entity, @@ -396,6 +382,7 @@ pub fn prepare_uimaterial_nodes( pipeline_cache: Res, mut ui_meta: ResMut>, extracted_uinodes: Res>, + extracted_geometry: Res, view_uniforms: Res, globals_buffer: Res, ui_material_pipeline: Res>, @@ -422,11 +409,12 @@ pub fn prepare_uimaterial_nodes( for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; - if let Some(extracted_uinode) = extracted_uinodes - .uinodes - .get(&item.main_entity()) - .and_then(|(_, subnodes)| subnodes.get(&item.entity())) - { + if let (Some((_, extracted_uinode)), Some(geometry)) = ( + extracted_uinodes.uinodes.get(&item.main_entity()), + extracted_geometry + .uinode_geometries + .get(&item.main_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); @@ -449,18 +437,21 @@ pub fn prepare_uimaterial_nodes( existing_batch = batches.last_mut(); } - let uinode_rect = extracted_uinode.rect; + let uinode_rect = Rect { + min: Vec2::ZERO, + max: geometry.computed_node.size(), + }; let rect_size = uinode_rect.size(); let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - extracted_uinode + geometry .transform .transform_point2(pos * rect_size) .extend(1.0) }); - let positions_diff = if let Some(clip) = extracted_uinode.clip { + let positions_diff = if let Some(clip) = geometry.clip { [ Vec2::new( f32::max(clip.min.x - positions[0].x, 0.), @@ -490,10 +481,8 @@ pub fn prepare_uimaterial_nodes( positions[3] + positions_diff[3].extend(0.), ]; - let transformed_rect_size = extracted_uinode - .transform - .transform_vector2(rect_size) - .abs(); + let transformed_rect_size = + geometry.transform.transform_vector2(rect_size).abs(); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -501,7 +490,7 @@ pub fn prepare_uimaterial_nodes( // horizontal / vertical lines // For all other angles, bypass the culling check // This does not properly handles all rotations on all axis - if extracted_uinode.transform.x_axis[1] == 0.0 { + if geometry.transform.x_axis[1] == 0.0 { // Cull nodes that are completely clipped if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y @@ -533,13 +522,13 @@ pub fn prepare_uimaterial_nodes( ui_meta.vertices.push(UiMaterialVertex { position: positions_clipped[i].into(), uv: uvs[i].into(), - size: extracted_uinode.rect.size().into(), - radius: extracted_uinode.border_radius, + size: uinode_rect.size().into(), + radius: geometry.computed_node.border_radius.into(), border: [ - extracted_uinode.border.min_inset.x, - extracted_uinode.border.min_inset.y, - extracted_uinode.border.max_inset.x, - extracted_uinode.border.max_inset.y, + geometry.computed_node.border.min_inset.x, + geometry.computed_node.border.min_inset.y, + geometry.computed_node.border.max_inset.x, + geometry.computed_node.border.max_inset.y, ], }); } From 5922a5197947ce0305502aaa1545cc894315b50c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 14:27:12 +0100 Subject: [PATCH 18/99] Check render_entity against item entity in prepare_uimaterial_nodes. UiMaterial rendering function now. --- crates/bevy_ui_render/src/ui_material_pipeline.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index e7726793bd6ff..2120a3dd30a2d 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -381,7 +381,7 @@ pub fn prepare_uimaterial_nodes( render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut>, - extracted_uinodes: Res>, + extracted_materials: Res>, extracted_geometry: Res, view_uniforms: Res, globals_buffer: Res, @@ -409,12 +409,13 @@ pub fn prepare_uimaterial_nodes( for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; - if let (Some((_, extracted_uinode)), Some(geometry)) = ( - extracted_uinodes.uinodes.get(&item.main_entity()), - extracted_geometry + if let Some((render_entity, extracted_uinode)) = + extracted_materials.uinodes.get(&item.main_entity()) + && *render_entity == item.entity() + && let Some(geometry) = extracted_geometry .uinode_geometries - .get(&item.main_entity()), - ) { + .get(&item.main_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); From c119f9d853d07c4e1276b37ea406a47a8d213186 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 14:54:33 +0100 Subject: [PATCH 19/99] updated ui_texture_slice_pipeline to use the extracted geometry --- .../src/ui_texture_slice_pipeline.rs | 240 +++++++----------- 1 file changed, 90 insertions(+), 150 deletions(-) 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 c29ef86b5d03d..b1b6a1b53e2e4 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -28,7 +28,7 @@ use bevy_shader::Shader; use bevy_sprite::{SliceScaleMode, SpriteImageMode, TextureSlicer}; use bevy_sprite_render::SpriteAssetEvents; use bevy_ui::widget::NodeImageMode; -use bevy_ui::{ComputedStackIndex, VisualBox}; +use bevy_ui::VisualBox; use bevy_utils::default; use binding_types::{sampler, texture_2d}; use bytemuck::{Pod, Zeroable}; @@ -191,28 +191,20 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { } pub struct ExtractedUiTextureSlice { - pub stack_index: u32, - pub transform: Affine2, - pub rect: Rect, pub atlas_rect: Option, pub image: AssetId, - pub clip: Option, pub color: LinearRgba, pub image_scale_mode: SpriteImageMode, pub flip_x: bool, pub flip_y: bool, - pub inverse_scale_factor: f32, + pub visual_box: VisualBox, } /// A render-world resource that stores all texture slices in the scene. #[derive(Resource, Default)] pub struct ExtractedUiTextureSlices { - /// The list of texture slices grouped by their main-world entity, along with - /// each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all - /// texture slices associated with a main-world entity when it changes. - pub slices: MainEntityHashMap<(Entity, EntityIndexMap)>, + /// Map from main-world UI entity to render entity and texture slice. + pub slices: MainEntityHashMap<(Entity, ExtractedUiTextureSlice)>, } pub fn extract_ui_texture_slices( @@ -221,23 +213,8 @@ pub fn extract_ui_texture_slices( texture_atlases: Extract>>, slicers_query: Extract< Query< - ( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &ImageNode, - ), + (Entity, &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 @@ -247,56 +224,19 @@ pub fn extract_ui_texture_slices( )>, >, >, - 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 removed_image_node_query,): (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 - { + for (entity, image) in &slicers_query { let main_entity = MainEntity::from(entity); + nodes_processed_this_frame.insert(main_entity); - // If there were any previous UI slices for this entity, despawn them. - for (render_entity, _) in extracted_ui_slicers - .slices - .get_mut(&main_entity) - .iter_mut() - .flat_map(|(_, slices)| slices.drain(..)) - { - commands.entity(render_entity).despawn(); - } - - let visual_box = match image.visual_box { - VisualBox::ContentBox => uinode.content_box(), - VisualBox::PaddingBox => uinode.padding_box(), - VisualBox::BorderBox => uinode.border_box(), - }; - - // Skip invisible images - if !inherited_visibility.get() - || image.color.is_fully_transparent() - || image.image.id() == TRANSPARENT_IMAGE_HANDLE.id() - || visual_box.size().cmple(Vec2::ZERO).any() - { + if image.color.is_fully_transparent() || image.image.id() == TRANSPARENT_IMAGE_HANDLE.id() { + if let Some((render_entity, _)) = extracted_ui_slicers.slices.remove(&main_entity) { + commands.entity(render_entity).despawn(); + } continue; } @@ -311,17 +251,13 @@ pub fn extract_ui_texture_slices( tile_y, stretch_value, }, - _ => continue, - }; - - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; + _ => { + if let Some((render_entity, _)) = extracted_ui_slicers.slices.remove(&main_entity) { + commands.entity(render_entity).despawn(); + } + continue; + } }; - if let Some((camera_entity, _)) = extracted_ui_slicers.slices.get_mut(&main_entity) { - *camera_entity = extracted_camera_entity; - } - - nodes_processed_this_frame.insert(main_entity); let atlas_rect = image .texture_atlas @@ -340,56 +276,40 @@ pub fn extract_ui_texture_slices( } }; - extracted_ui_slicers - .slices - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiTextureSlice { - stack_index: stack_index.0, - transform: Affine2::from(*transform) - * Affine2::from_translation(visual_box.center()), - color: image.color.into(), - rect: Rect { - min: Vec2::ZERO, - max: visual_box.size(), - }, - clip: clip.map(|clip| clip.clip), - image: image.image.id(), - image_scale_mode, - atlas_rect, - flip_x: image.flip_x, - flip_y: image.flip_y, - inverse_scale_factor: uinode.inverse_scale_factor, - }, - ); + let texture_slice = ExtractedUiTextureSlice { + color: image.color.into(), + image: image.image.id(), + image_scale_mode, + atlas_rect, + flip_x: image.flip_x, + flip_y: image.flip_y, + visual_box: image.visual_box, + }; + + match extracted_ui_slicers.slices.entry(main_entity) { + Entry::Occupied(mut entry) => { + let (_, extracted_slice) = entry.get_mut(); + *extracted_slice = texture_slice; + } + Entry::Vacant(entry) => { + let entity = commands.spawn_empty().id(); + entry.insert((entity, texture_slice)); + } + } } // 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()) - { + for main_entity in 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 { + let Some((render_entity, _)) = extracted_ui_slicers.slices.remove(&main_entity) else { continue; }; - for (render_entity, _) in extracted_nodes.drain(..) { - commands.entity(render_entity).despawn(); - } + commands.entity(render_entity).despawn(); } } @@ -398,7 +318,8 @@ pub fn extract_ui_texture_slices( reason = "it's a system that needs a lot of them" )] pub fn queue_ui_slices( - extracted_ui_slicers: ResMut, + extracted_ui_slicers: Res, + extracted_geometry: Res, ui_slicer_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -411,11 +332,16 @@ pub fn queue_ui_slices( let mut current_camera_entity = Entity::PLACEHOLDER; let mut current_phase = None; - for (main_entity, (extracted_camera_entity, subslices)) in extracted_ui_slicers.slices.iter() { - if current_camera_entity != *extracted_camera_entity { + for (main_entity, (render_entity, _)) in extracted_ui_slicers.slices.iter() { + let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + continue; + }; + let extracted_camera_entity = geometry.extracted_camera; + + if current_camera_entity != extracted_camera_entity { current_phase = render_views - .get(*extracted_camera_entity) + .get(extracted_camera_entity) .ok() .and_then(|default_camera_view| { camera_views @@ -436,23 +362,21 @@ pub fn queue_ui_slices( }) }) }); - current_camera_entity = *extracted_camera_entity; + current_camera_entity = extracted_camera_entity; } let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { continue; }; - for (render_entity, extracted_slicer) in subslices.iter() { - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline: *pipeline, - entity: (*render_entity, *main_entity), - sort_key: FloatOrd(extracted_slicer.stack_index as f32 + stack_z_offsets::IMAGE), - batch_range: 0..0, - extra_index: PhaseItemExtraIndex::None, - indexed: true, - }); - } + transparent_phase.add_transient(TransparentUi { + draw_function, + pipeline: *pipeline, + entity: (*render_entity, *main_entity), + sort_key: FloatOrd(geometry.stack_index as f32 + stack_z_offsets::IMAGE), + batch_range: 0..0, + extra_index: PhaseItemExtraIndex::None, + indexed: true, + }); } } @@ -463,6 +387,7 @@ pub fn prepare_ui_slices( pipeline_cache: Res, mut ui_meta: ResMut, extracted_slices: Res, + extracted_geometry: Res, view_uniforms: Res, texture_slicer_pipeline: Res, mut image_bind_groups: ResMut, @@ -506,11 +431,23 @@ pub fn prepare_ui_slices( for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; - if let Some(texture_slices) = extracted_slices - .slices - .get(&item.main_entity()) - .and_then(|(_, subslices)| subslices.get(&item.entity())) + if let Some((render_entity, texture_slices)) = + extracted_slices.slices.get(&item.main_entity()) + && *render_entity == item.entity() + && let Some(geometry) = extracted_geometry + .uinode_geometries + .get(&item.main_entity()) { + let visual_box = match texture_slices.visual_box { + VisualBox::ContentBox => geometry.computed_node.content_box(), + VisualBox::PaddingBox => geometry.computed_node.padding_box(), + VisualBox::BorderBox => geometry.computed_node.border_box(), + }; + if visual_box.size().cmple(Vec2::ZERO).any() { + 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); @@ -584,18 +521,22 @@ pub fn prepare_ui_slices( } } - let uinode_rect = texture_slices.rect; + let uinode_rect = Rect { + min: Vec2::ZERO, + max: visual_box.size(), + }; let rect_size = uinode_rect.size(); + let transform = + geometry.transform * Affine2::from_translation(visual_box.center()); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (texture_slices.transform.transform_point2(pos * rect_size)).extend(0.) - }); + let positions = QUAD_VERTEX_POSITIONS + .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) - let positions_diff = if let Some(clip) = texture_slices.clip { + let positions_diff = if let Some(clip) = geometry.clip { [ Vec2::new( f32::max(clip.min.x - positions[0].x, 0.), @@ -625,8 +566,7 @@ pub fn prepare_ui_slices( positions[3] + positions_diff[3].extend(0.), ]; - let transformed_rect_size = - texture_slices.transform.transform_vector2(rect_size).abs(); + let transformed_rect_size = transform.transform_vector2(rect_size).abs(); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -634,7 +574,7 @@ pub fn prepare_ui_slices( // horizontal / vertical lines // For all other angles, bypass the culling check // This does not properly handles all rotations on all axis - if texture_slices.transform.x_axis[1] == 0.0 { + if transform.x_axis[1] == 0.0 { // Cull nodes that are completely clipped if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y @@ -699,7 +639,7 @@ pub fn prepare_ui_slices( let [slices, border, repeat] = compute_texture_slices( image_size, - uinode_rect.size() * texture_slices.inverse_scale_factor, + uinode_rect.size() * geometry.computed_node.inverse_scale_factor, &texture_slices.image_scale_mode, ); From 11b9bc1ce449494936a0f2d9bed725fa153712c4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 15:01:35 +0100 Subject: [PATCH 20/99] Removed unneeded fields from ExtractedBoxShadow --- crates/bevy_ui_render/src/box_shadow.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 4164eef859e7f..1d92abf32da75 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -185,9 +185,7 @@ impl SpecializedRenderPipeline for BoxShadowPipeline { /// Description of a shadow to be sorted and queued for rendering pub struct ExtractedBoxShadow { pub stack_index: u32, - pub transform: Affine2, pub bounds: Vec2, - pub clip: Option, pub color: LinearRgba, pub radius: ResolvedBorderRadius, pub blur_radius: f32, From 7e60461101e588eaccdb298d9360259d2462afee Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 15:32:17 +0100 Subject: [PATCH 21/99] Updated box_shadow module. Shadow geometry is now resolved in prepare_shadows. --- crates/bevy_ui_render/src/box_shadow.rs | 295 +++++++++--------------- 1 file changed, 110 insertions(+), 185 deletions(-) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 1d92abf32da75..f6d9c8f56c70e 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -4,7 +4,6 @@ use core::{hash::Hash, ops::Range}; 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::*; @@ -15,7 +14,7 @@ use bevy_ecs::{ *, }, }; -use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2}; +use bevy_math::{vec2, Affine2, FloatOrd, Vec2}; use bevy_mesh::VertexBufferLayout; use bevy_render::sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}; use bevy_render::{ @@ -27,14 +26,11 @@ use bevy_render::{ }; use bevy_render::{GpuResourceAppExt, RenderApp, RenderStartup}; use bevy_shader::{Shader, ShaderDefVal}; -use bevy_ui::{ - BoxShadow, CalculatedClip, ComputedNode, ComputedStackIndex, ComputedUiRenderTargetInfo, - ComputedUiTargetCamera, ResolvedBorderRadius, UiGlobalTransform, Val, -}; +use bevy_ui::{BoxShadow, ComputedUiRenderTargetInfo, ResolvedBorderRadius, ShadowStyle, Val}; use bevy_utils::default; use bytemuck::{Pod, Zeroable}; -use crate::{BoxShadowSamples, RenderUiSystems, TransparentUi, UiCameraMap}; +use crate::{BoxShadowSamples, ExtractedUiGeometries, RenderUiSystems, TransparentUi}; use super::{stack_z_offsets, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; @@ -184,23 +180,15 @@ impl SpecializedRenderPipeline for BoxShadowPipeline { /// Description of a shadow to be sorted and queued for rendering pub struct ExtractedBoxShadow { - pub stack_index: u32, - pub bounds: Vec2, - pub color: LinearRgba, - pub radius: ResolvedBorderRadius, - pub blur_radius: f32, - pub size: Vec2, + pub style: ShadowStyle, + pub target: ComputedUiRenderTargetInfo, } /// List of extracted shadows to be sorted and queued for rendering #[derive(Resource, Default)] pub struct ExtractedBoxShadows { - /// The list of box shadows grouped by their main-world entity, along with - /// each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all box - /// shadows associated with a main-world entity when it changes. - pub box_shadows: MainEntityHashMap<(Entity, EntityIndexMap)>, + /// Box shadows grouped by their main-world UI entity. + pub box_shadows: MainEntityHashMap>, } pub fn extract_shadows( @@ -208,164 +196,66 @@ pub fn extract_shadows( mut extracted_box_shadows: ResMut, box_shadow_query: Extract< Query< - ( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - &BoxShadow, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &ComputedUiRenderTargetInfo, - ), - Or<( - Changed, - Changed, - Changed, - Changed, - Changed, - Changed, - Changed, - Changed, - )>, + (Entity, &BoxShadow, &ComputedUiRenderTargetInfo), + Or<(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>, + (mut removed_box_shadow_query, mut removed_computed_ui_render_target_info_query): ( 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 - { + for (entity, box_shadow, target) in &box_shadow_query { let main_entity = MainEntity::from(entity); + nodes_processed_this_frame.insert(main_entity); - // If there were any previous box shadows for this entity, despawn them. - for (render_entity, _) in extracted_box_shadows + let shadows = extracted_box_shadows .box_shadows - .get_mut(&main_entity) - .iter_mut() - .flat_map(|(_, shadows)| shadows.drain(..)) - { - commands.entity(render_entity).despawn(); - } - - // Skip if no visible shadows - if !visibility.get() || box_shadow.is_empty() || uinode.is_empty() { - continue; - } - - let Some(extracted_camera_entity) = mapping.map(camera) else { - continue; - }; - if let Some((camera_entity, _)) = extracted_box_shadows.box_shadows.get_mut(&main_entity) { - *camera_entity = extracted_camera_entity; - } - - let ui_physical_viewport_size = target.physical_size().as_vec2(); - let scale_factor = target.scale_factor(); + .entry(main_entity) + .or_default(); + let mut shadow_count = 0; for drop_shadow in box_shadow.iter() { if drop_shadow.color.is_fully_transparent() { continue; } - let resolve_val = |val, base, scale_factor| match val { - Val::Auto => 0., - Val::Px(px) => px * scale_factor, - Val::Percent(percent) => percent / 100. * base, - Val::Vw(percent) => percent / 100. * ui_physical_viewport_size.x, - Val::Vh(percent) => percent / 100. * ui_physical_viewport_size.y, - Val::VMin(percent) => percent / 100. * ui_physical_viewport_size.min_element(), - Val::VMax(percent) => percent / 100. * ui_physical_viewport_size.max_element(), + let extracted_shadow = ExtractedBoxShadow { + style: *drop_shadow, + target: *target, }; - - let spread_x = resolve_val(drop_shadow.spread_radius, uinode.size().x, scale_factor); - let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x; - - let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y); - - let blur_radius = resolve_val(drop_shadow.blur_radius, uinode.size().x, scale_factor); - let offset = vec2( - resolve_val(drop_shadow.x_offset, uinode.size().x, scale_factor), - resolve_val(drop_shadow.y_offset, uinode.size().y, scale_factor), - ); - - let shadow_size = uinode.size() + spread; - if shadow_size.cmple(Vec2::ZERO).any() { - continue; + if let Some((_, shadow)) = shadows.get_index_mut(shadow_count) { + *shadow = extracted_shadow; + } else { + shadows.insert(commands.spawn_empty().id(), extracted_shadow); } + shadow_count += 1; + } - 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, - bottom_left: uinode.border_radius.bottom_left * spread_ratio, - bottom_right: uinode.border_radius.bottom_right * spread_ratio, - }; + for (render_entity, _) in shadows.drain(shadow_count..) { + commands.entity(render_entity).despawn(); + } - extracted_box_shadows - .box_shadows - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedBoxShadow { - stack_index: stack_index.0, - transform: Affine2::from(transform) * Affine2::from_translation(offset), - color: drop_shadow.color.into(), - bounds: shadow_size + 6. * blur_radius, - clip: clip.map(|clip| clip.clip), - radius, - blur_radius, - size: shadow_size, - }, - ); + if shadows.is_empty() { + extracted_box_shadows.box_shadows.remove(&main_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 + for main_entity in removed_box_shadow_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) + let Some(mut extracted_nodes) = extracted_box_shadows.box_shadows.remove(&main_entity) else { continue; }; @@ -380,7 +270,8 @@ pub fn extract_shadows( reason = "it's a system that needs a lot of them" )] pub fn queue_shadows( - extracted_box_shadows: ResMut, + extracted_box_shadows: Res, + extracted_geometry: Res, box_shadow_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -393,11 +284,13 @@ pub fn queue_shadows( let mut current_camera_entity = Entity::PLACEHOLDER; let mut current_phase = None; - for (main_entity, (extracted_camera_entity, extracted_sub_shadows)) in - extracted_box_shadows.box_shadows.iter() - { - if current_camera_entity != *extracted_camera_entity { - current_phase = render_views.get(*extracted_camera_entity).ok().and_then( + for (main_entity, extracted_sub_shadows) in extracted_box_shadows.box_shadows.iter() { + let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + continue; + }; + + if current_camera_entity != geometry.extracted_camera { + current_phase = render_views.get(geometry.extracted_camera).ok().and_then( |(default_camera_view, shadow_samples)| { camera_views .get(default_camera_view.0) @@ -419,20 +312,18 @@ pub fn queue_shadows( }) }, ); - current_camera_entity = *extracted_camera_entity; + current_camera_entity = geometry.extracted_camera; } let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { continue; }; - for (entity, extracted_shadow) in extracted_sub_shadows.iter() { + for (entity, _) in extracted_sub_shadows.iter() { transparent_phase.add_transient(TransparentUi { draw_function, pipeline: *pipeline, entity: (*entity, *main_entity), - sort_key: FloatOrd( - extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW, - ), + sort_key: FloatOrd(geometry.stack_index as f32 + stack_z_offsets::BOX_SHADOW), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, @@ -449,6 +340,7 @@ pub fn prepare_shadows( pipeline_cache: Res, mut ui_meta: ResMut, extracted_shadows: Res, + extracted_geometry: Res, view_uniforms: Res, box_shadow_pipeline: Res, mut phases: ResMut>, @@ -472,34 +364,72 @@ pub fn prepare_shadows( for ui_phase in phases.values_mut() { for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; - let Some((extracted_camera_entity, box_shadow)) = extracted_shadows + let Some(shadow) = extracted_shadows .box_shadows .get(&item.main_entity()) - .and_then(|(extracted_camera_entity, sub_shadows)| { - sub_shadows - .get(&item.entity()) - .map(|box_shadow| (extracted_camera_entity, box_shadow)) - }) + .and_then(|shadows| shadows.get(&item.entity())) else { continue; }; + let Some(geometry) = extracted_geometry + .uinode_geometries + .get(&item.main_entity()) + else { + continue; + }; + + let uinode = &geometry.computed_node; + if uinode.is_empty() { + continue; + } + + let physical_viewport_size = shadow.target.physical_size().as_vec2(); + let scale_factor = shadow.target.scale_factor(); + let resolve_val = |val, base, scale_factor| match val { + Val::Auto => 0., + Val::Px(px) => px * scale_factor, + Val::Percent(percent) => percent / 100. * base, + Val::Vw(percent) => percent / 100. * physical_viewport_size.x, + Val::Vh(percent) => percent / 100. * physical_viewport_size.y, + Val::VMin(percent) => percent / 100. * physical_viewport_size.min_element(), + Val::VMax(percent) => percent / 100. * physical_viewport_size.max_element(), + }; + let spread_x = + resolve_val(shadow.style.spread_radius, uinode.size().x, scale_factor); + let spread_ratio = (spread_x + uinode.size().x) / uinode.size().x; + let spread = vec2(spread_x, uinode.size().y * spread_ratio - uinode.size().y); + let blur_radius = + resolve_val(shadow.style.blur_radius, uinode.size().x, scale_factor); + let offset = vec2( + resolve_val(shadow.style.x_offset, uinode.size().x, scale_factor), + resolve_val(shadow.style.y_offset, uinode.size().y, scale_factor), + ); + let shadow_size = uinode.size() + spread; + if shadow_size.cmple(Vec2::ZERO).any() { + continue; + } + + let radius = ResolvedBorderRadius { + top_left: uinode.border_radius.top_left * spread_ratio, + top_right: uinode.border_radius.top_right * spread_ratio, + bottom_left: uinode.border_radius.bottom_left * spread_ratio, + bottom_right: uinode.border_radius.bottom_right * spread_ratio, + }; + let bounds = shadow_size + 6. * blur_radius; + let transform = geometry.transform * Affine2::from_translation(offset); // 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; + let rect_size = bounds; // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - box_shadow - .transform - .transform_point2(pos * rect_size) - .extend(0.) - }); + let positions = QUAD_VERTEX_POSITIONS + .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) - let positions_diff = if let Some(clip) = box_shadow.clip { + let positions_diff = if let Some(clip) = geometry.clip { [ Vec2::new( f32::max(clip.min.x - positions[0].x, 0.), @@ -529,7 +459,7 @@ pub fn prepare_shadows( positions[3] + positions_diff[3].extend(0.), ]; - let transformed_rect_size = box_shadow.transform.transform_vector2(rect_size).abs(); + let transformed_rect_size = transform.transform_vector2(rect_size).abs(); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -537,7 +467,7 @@ pub fn prepare_shadows( // horizontal / vertical lines // For all other angles, bypass the culling check // This does not properly handles all rotations on all axis - if box_shadow.transform.x_axis[1] == 0.0 { + if transform.x_axis[1] == 0.0 { // Cull nodes that are completely clipped if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y @@ -548,29 +478,24 @@ pub fn prepare_shadows( let uvs = [ Vec2::new(positions_diff[0].x, positions_diff[0].y), + Vec2::new(bounds.x + positions_diff[1].x, positions_diff[1].y), Vec2::new( - box_shadow.bounds.x + positions_diff[1].x, - positions_diff[1].y, - ), - Vec2::new( - box_shadow.bounds.x + positions_diff[2].x, - box_shadow.bounds.y + positions_diff[2].y, - ), - Vec2::new( - positions_diff[3].x, - box_shadow.bounds.y + positions_diff[3].y, + bounds.x + positions_diff[2].x, + bounds.y + positions_diff[2].y, ), + Vec2::new(positions_diff[3].x, bounds.y + positions_diff[3].y), ] - .map(|pos| pos / box_shadow.bounds); + .map(|pos| pos / bounds); + let color: LinearRgba = shadow.style.color.into(); for i in 0..4 { ui_meta.vertices.push(BoxShadowVertex { position: positions_clipped[i].into(), uvs: uvs[i].into(), - vertex_color: box_shadow.color.to_f32_array(), - size: box_shadow.size.into(), - radius: box_shadow.radius.into(), - blur: box_shadow.blur_radius, + vertex_color: color.to_f32_array(), + size: shadow_size.into(), + radius: radius.into(), + blur: blur_radius, bounds: rect_size.into(), }); } @@ -583,7 +508,7 @@ pub fn prepare_shadows( item.entity(), UiShadowsBatch { range: vertices_index..vertices_index + 6, - camera: *extracted_camera_entity, + camera: geometry.extracted_camera, }, )); From 354b5369f969d8904072ed406aae7282ddd0e1dc Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 15:44:06 +0100 Subject: [PATCH 22/99] removed extract_uinode_geometry_debug system from schedule --- crates/bevy_ui_render/src/lib.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 4481f782d17c4..392df9154c88c 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -261,8 +261,6 @@ impl Plugin for UiRenderPlugin { ExtractSchedule, ( extract_geometry::extract_uinode_geometry, - extract_geometry::extract_uinode_geometry_debug - .after(extract_geometry::extract_uinode_geometry), extract_uinode_changes.in_set(RenderUiSystems::ExtractChanges), extract_ui_camera_view .after(extract_cameras) From 48dccf5af5dad54604804a4d807444e7d56f6e9d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 15:50:26 +0100 Subject: [PATCH 23/99] updated gradient module to use extracted geometry, like with shadows the gradient geometry is resolved in prepare, not extraction. --- crates/bevy_ui_render/src/gradient.rs | 492 ++++++++++---------------- 1 file changed, 178 insertions(+), 314 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 887a6cebb2d8d..f9ee8fc495bcc 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -15,11 +15,11 @@ use bevy_ecs::{ *, }, }; +use bevy_math::Vec2Swizzles; use bevy_math::{ ops::{cos, sin}, FloatOrd, Rect, Vec2, }; -use bevy_math::{Affine2, Vec2Swizzles}; use bevy_mesh::VertexBufferLayout; use bevy_render::{ render_phase::*, @@ -30,11 +30,9 @@ use bevy_render::{ }; use bevy_render::{GpuResourceAppExt, RenderStartup}; use bevy_shader::Shader; -use bevy_sprite::BorderRect; use bevy_ui::{ - BackgroundGradient, BorderGradient, ColorStop, ComputedStackIndex, ComputedUiRenderTargetInfo, - ConicGradient, Gradient, InterpolationColorSpace, LinearGradient, RadialGradient, - ResolvedBorderRadius, Val, + BackgroundGradient, BorderGradient, ColorStop, ComputedUiRenderTargetInfo, ConicGradient, + Gradient, InterpolationColorSpace, LinearGradient, RadialGradient, Val, }; use bevy_utils::default; use bytemuck::{Pod, Zeroable}; @@ -227,30 +225,19 @@ pub enum ResolvedGradient { } pub struct ExtractedGradient { - pub stack_index: u32, - pub transform: Affine2, - pub rect: Rect, - pub clip: Option, - pub stops: Vec<(LinearRgba, f32, f32)>, + pub gradient: Gradient, pub node_type: NodeType, - /// Border radius of the UI node. - /// Ordering: top left, top right, bottom right, bottom left. - pub border_radius: ResolvedBorderRadius, - /// Border thickness of the UI node. - /// Ordering: left, top, right, bottom. - pub border: BorderRect, - pub resolved_gradient: ResolvedGradient, - pub color_space: InterpolationColorSpace, + pub target: ComputedUiRenderTargetInfo, } /// A render-world resource that stores all gradients in the scene. #[derive(Resource, Default)] pub struct ExtractedGradients { - /// The list of gradients grouped by their main-world entity, along with each group's target camera entity. + /// The list of gradients grouped by their main-world entity. /// /// This is a two-level data structure so that we can quickly remove all /// gradients associated with a main-world entity when it changes. - pub items: MainEntityHashMap<(Entity, EntityIndexMap)>, + pub items: MainEntityHashMap>, } // Interpolate implicit stops (where position is `f32::NAN`) @@ -341,315 +328,76 @@ pub fn extract_gradients( 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![]; - - for ( - entity, - uinode, - stack_index, - camera, - target, - transform, - inherited_visibility, - clip, - (gradient, gradient_border), - ) in &gradients_query - { - let main_entity = MainEntity::from(entity); - // If there were any previous gradients for this entity, despawn them. - for (render_entity, _) in extracted_gradients - .items - .get_mut(&main_entity) - .iter_mut() - .flat_map(|(_, gradients)| gradients.drain(..)) - { - commands.entity(render_entity).despawn(); - } - - // Skip invisible images - if !inherited_visibility.get() { - continue; - } + for (entity, target, (gradient, gradient_border)) in &gradients_query { + let main_entity = MainEntity::from(entity); + nodes_processed_this_frame.insert(main_entity); - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; - }; - if let Some((camera_entity, _)) = extracted_gradients.items.get_mut(&main_entity) { - *camera_entity = extracted_camera_entity; - } + let gradients = extracted_gradients.items.entry(main_entity).or_default(); + let mut gradient_count = 0; - for (gradients, node_type) in [ + for (node_gradients, node_type) in [ (gradient.map(|g| &g.0), NodeType::Rect), (gradient_border.map(|g| &g.0), NodeType::Border(BORDER_ALL)), ] .iter() .filter_map(|(g, n)| g.map(|g| (g, *n))) { - for gradient in gradients.iter() { + for gradient in node_gradients.iter() { 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 - let length = compute_gradient_line_length(0.0, uinode.size); - let extracted_stops = compute_color_stops( - &[ - ColorStop::new(color, Val::Percent(0.0)), - ColorStop::new(color, Val::Percent(100.0)), - ], - target.scale_factor(), - length, - target.physical_size().as_vec2(), - &mut sorted_stops, - ); - extracted_gradients - .items - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedGradient { - stack_index: stack_index.0, - transform: transform.into(), - stops: extracted_stops, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Linear { angle: 0.0 }, - color_space: gradient.get_color_space(), - }, - ); - continue; - } - match gradient { - Gradient::Linear(LinearGradient { - color_space, - angle, - stops, - }) => { - let length = compute_gradient_line_length(*angle, uinode.size); - - let extracted_stops = compute_color_stops( - stops, - target.scale_factor(), - length, - target.physical_size().as_vec2(), - &mut sorted_stops, - ); - - extracted_gradients - .items - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedGradient { - stack_index: stack_index.0, - transform: transform.into(), - stops: extracted_stops, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Linear { angle: *angle }, - color_space: *color_space, - }, - ); - } - Gradient::Radial(RadialGradient { - color_space, - position: center, - shape, - stops, - }) => { - let c = center.resolve( - target.scale_factor(), - uinode.size, - target.physical_size().as_vec2(), - ); - - let size = shape.resolve( - c, - target.scale_factor(), - uinode.size, - target.physical_size().as_vec2(), - ); - - let length = size.x; - - let computed_stops = compute_color_stops( - stops, - target.scale_factor(), - length, - target.physical_size().as_vec2(), - &mut sorted_stops, - ); - - extracted_gradients - .items - .entry(main_entity) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedGradient { - stack_index: stack_index.0, - transform: transform.into(), - stops: computed_stops, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Radial { center: c, size }, - color_space: *color_space, - }, - ); - } - Gradient::Conic(ConicGradient { - color_space, - start, - position: center, - stops, - }) => { - let g_start = center.resolve( - target.scale_factor(), - uinode.size, - target.physical_size().as_vec2(), - ); - - // sort the explicit stops - sorted_stops.extend(stops.iter().filter_map(|stop| { - stop.angle.map(|angle| { - (stop.color.to_linear(), angle.clamp(0., TAU), stop.hint) - }) - })); - sorted_stops.sort_by_key(|(_, angle, _)| FloatOrd(*angle)); - let mut sorted_stops_drain = sorted_stops.drain(..); - - // fill the extracted stops buffer - 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_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedGradient { - stack_index: stack_index.0, - transform: transform.into(), - stops: extracted_color_stops, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - clip: clip.map(|clip| clip.clip), - node_type, - border_radius: uinode.border_radius, - border: uinode.border, - resolved_gradient: ResolvedGradient::Conic { - start: *start, - center: g_start, - }, - color_space: *color_space, - }, - ); - } + let extracted_gradient = ExtractedGradient { + gradient: gradient.clone(), + node_type, + target: *target, + }; + if let Some((_, gradient)) = gradients.get_index_mut(gradient_count) { + *gradient = extracted_gradient; + } else { + gradients.insert(commands.spawn_empty().id(), extracted_gradient); } + gradient_count += 1; } } + + for (render_entity, _) in gradients.drain(gradient_count..) { + commands.entity(render_entity).despawn(); + } + + if gradients.is_empty() { + extracted_gradients.items.remove(&main_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 + for main_entity in removed_computed_ui_render_target_info_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()) { @@ -657,7 +405,7 @@ pub fn extract_gradients( if nodes_processed_this_frame.contains(&main_entity) { continue; } - let Some((_, mut extracted_nodes)) = extracted_gradients.items.remove(&main_entity) else { + let Some(mut extracted_nodes) = extracted_gradients.items.remove(&main_entity) else { continue; }; for (render_entity, _) in extracted_nodes.drain(..) { @@ -671,7 +419,8 @@ pub fn extract_gradients( reason = "it's a system that needs a lot of them" )] pub fn queue_gradient( - extracted_gradients: ResMut, + extracted_gradients: Res, + extracted_geometry: Res, gradients_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -684,10 +433,13 @@ pub fn queue_gradient( let mut current_camera_entity = Entity::PLACEHOLDER; let mut current_phase = None; - for (main_entity, (extracted_camera_entity, sub_gradients)) in extracted_gradients.items.iter() - { - if current_camera_entity != *extracted_camera_entity { - current_phase = render_views.get(*extracted_camera_entity).ok().and_then( + for (main_entity, sub_gradients) in extracted_gradients.items.iter() { + let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + continue; + }; + + if current_camera_entity != geometry.extracted_camera { + current_phase = render_views.get(geometry.extracted_camera).ok().and_then( |(default_camera_view, ui_anti_alias)| { camera_views .get(default_camera_view.0) @@ -701,7 +453,7 @@ pub fn queue_gradient( }) }, ); - current_camera_entity = *extracted_camera_entity; + current_camera_entity = geometry.extracted_camera; } let Some((target_format, ui_anti_alias, transparent_phase)) = current_phase.as_mut() else { @@ -713,7 +465,7 @@ pub fn queue_gradient( &gradients_pipeline, UiGradientPipelineKey { anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), - color_space: gradient.color_space, + color_space: gradient.gradient.get_color_space(), target_format: *target_format, }, ); @@ -723,7 +475,7 @@ pub fn queue_gradient( pipeline, entity: (*render_entity, *main_entity), sort_key: FloatOrd( - gradient.stack_index as f32 + geometry.stack_index as f32 + match gradient.node_type { NodeType::Rect | NodeType::Inverted => stack_z_offsets::GRADIENT, NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, @@ -797,6 +549,7 @@ pub fn prepare_gradient( pipeline_cache: Res, mut ui_meta: ResMut, extracted_gradients: Res, + extracted_geometry: Res, view_uniforms: Res, gradients_pipeline: Res, mut phases: ResMut>, @@ -816,6 +569,7 @@ pub fn prepare_gradient( // Buffer indexes let mut vertices_index = 0; let mut indices_index = 0; + let mut sorted_stops = vec![]; for ui_phase in phases.values_mut() { for item_index in 0..ui_phase.items.len() { @@ -823,16 +577,127 @@ pub fn prepare_gradient( if let Some(gradient) = extracted_gradients .items .get(&item.main_entity()) - .and_then(|(_, subgradients)| subgradients.get(&item.entity())) + .and_then(|subgradients| subgradients.get(&item.entity())) { + let Some(geometry) = extracted_geometry + .uinode_geometries + .get(&item.main_entity()) + else { + continue; + }; + let uinode = &geometry.computed_node; + if uinode.is_empty() { + continue; + } + + let target_size = gradient.target.physical_size().as_vec2(); + let scale_factor = gradient.target.scale_factor(); + let color_space = gradient.gradient.get_color_space(); + let (stops, resolved_gradient) = if let Some(color) = + gradient.gradient.get_single() + { + // With a single color stop there's no gradient, fill the node with the color + let length = compute_gradient_line_length(0.0, uinode.size); + ( + compute_color_stops( + &[ + ColorStop::new(color, Val::Percent(0.0)), + ColorStop::new(color, Val::Percent(100.0)), + ], + scale_factor, + length, + target_size, + &mut sorted_stops, + ), + ResolvedGradient::Linear { angle: 0.0 }, + ) + } else { + match &gradient.gradient { + Gradient::Linear(LinearGradient { angle, stops, .. }) => { + let length = compute_gradient_line_length(*angle, uinode.size); + ( + compute_color_stops( + stops, + scale_factor, + length, + target_size, + &mut sorted_stops, + ), + ResolvedGradient::Linear { angle: *angle }, + ) + } + Gradient::Radial(RadialGradient { + position: center, + shape, + stops, + .. + }) => { + let center = center.resolve(scale_factor, uinode.size, target_size); + let size = + shape.resolve(center, scale_factor, uinode.size, target_size); + ( + compute_color_stops( + stops, + scale_factor, + size.x, + target_size, + &mut sorted_stops, + ), + ResolvedGradient::Radial { center, size }, + ) + } + Gradient::Conic(ConicGradient { + start, + position: center, + stops, + .. + }) => { + let center = center.resolve(scale_factor, uinode.size, target_size); + + // sort the explicit stops + sorted_stops.extend(stops.iter().filter_map(|stop| { + stop.angle.map(|angle| { + (stop.color.to_linear(), angle.clamp(0., TAU), stop.hint) + }) + })); + sorted_stops.sort_by_key(|(_, angle, _)| FloatOrd(*angle)); + let mut sorted_stops_drain = sorted_stops.drain(..); + + // fill the extracted stops buffer + 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_color_stops, + ResolvedGradient::Conic { + start: *start, + center, + }, + ) + } + } + }; + *item.batch_range_mut() = item_index as u32..item_index as u32 + 1; - let uinode_rect = gradient.rect; + let uinode_rect = Rect { + min: Vec2::ZERO, + max: uinode.size, + }; let rect_size = uinode_rect.size(); // Specify the corners of the node let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - gradient + geometry .transform .transform_point2(pos * rect_size) .extend(0.) @@ -841,7 +706,7 @@ pub fn prepare_gradient( // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) - let positions_diff = if let Some(clip) = gradient.clip { + let positions_diff = if let Some(clip) = geometry.clip { [ Vec2::new( f32::max(clip.min.x - positions[0].x, 0.), @@ -879,7 +744,7 @@ pub fn prepare_gradient( ]; let transformed_rect_size = - gradient.transform.transform_vector2(rect_size).abs(); + geometry.transform.transform_vector2(rect_size).abs(); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -887,7 +752,7 @@ pub fn prepare_gradient( // horizontal / vertical lines // For all other angles, bypass the culling check // This does not properly handles all rotations on all axis - if gradient.transform.x_axis[1] == 0.0 { + if geometry.transform.x_axis[1] == 0.0 { // Cull nodes that are completely clipped if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y @@ -904,7 +769,7 @@ pub fn prepare_gradient( 0 }; - let (g_start, g_dir, g_flags) = match gradient.resolved_gradient { + let (g_start, g_dir, g_flags) = match resolved_gradient { ResolvedGradient::Linear { angle } => { let corner_index = (angle - FRAC_PI_2).rem_euclid(TAU) / FRAC_PI_2; ( @@ -926,14 +791,14 @@ pub fn prepare_gradient( flags |= g_flags; - let range = 0..gradient.stops.len() - 1; + let range = 0..stops.len() - 1; let mut segment_count = 0; for stop_index in range { - let mut start_stop = gradient.stops[stop_index]; - let end_stop = gradient.stops[stop_index + 1]; + let mut start_stop = stops[stop_index]; + let end_stop = stops[stop_index + 1]; if start_stop.1 == end_stop.1 { - if stop_index == gradient.stops.len() - 2 { + if stop_index == stops.len() - 2 { if 0 < segment_count { start_stop.0 = LinearRgba::NONE; } @@ -941,14 +806,13 @@ pub fn prepare_gradient( continue; } } - let start_color = - convert_color_to_space(start_stop.0, gradient.color_space); - let end_color = convert_color_to_space(end_stop.0, gradient.color_space); + let start_color = convert_color_to_space(start_stop.0, color_space); + let end_color = convert_color_to_space(end_stop.0, color_space); let mut stop_flags = flags; if 0. < start_stop.1 && (stop_index == 0 || segment_count == 0) { stop_flags |= shader_flags::FILL_START; } - if stop_index == gradient.stops.len() - 2 { + if stop_index == stops.len() - 2 { stop_flags |= shader_flags::FILL_END; } @@ -957,12 +821,12 @@ pub fn prepare_gradient( position: positions_clipped[i].into(), uv: uvs[i].into(), flags: stop_flags | shader_flags::CORNERS[i], - radius: gradient.border_radius.into(), + radius: uinode.border_radius.into(), border: [ - gradient.border.min_inset.x, - gradient.border.min_inset.y, - gradient.border.max_inset.x, - gradient.border.max_inset.y, + uinode.border.min_inset.x, + uinode.border.min_inset.y, + uinode.border.max_inset.x, + uinode.border.max_inset.y, ], size: rect_size.xy().into(), g_start, From 3652fdce2af522d2194af225092065dc82b960bb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 16:13:25 +0100 Subject: [PATCH 24/99] New ExtractedUiNode and ExtractedGlyphLayout types to replace the old Extraction buffer types --- crates/bevy_ui_render/src/lib.rs | 50 +++++-------------------------- crates/bevy_ui_render/src/text.rs | 30 ++++++++++++++++--- 2 files changed, 34 insertions(+), 46 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 392df9154c88c..b0cdc33529a27 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -350,48 +350,14 @@ impl<'w, 's> UiCameraMapper<'w, 's> { } pub struct ExtractedUiNode { - pub z_order: f32, - pub image: AssetId, - pub clip: Option, - pub item: ExtractedUiItem, - pub transform: Affine2, -} - -/// The type of UI node. -/// This is used to determine how to render the UI node. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum NodeType { - Rect, - Inverted, - Border(u32), // shader flags -} - -pub enum ExtractedUiItem { - Node { - color: LinearRgba, - rect: Rect, - atlas_scaling: Option, - flip_x: bool, - flip_y: bool, - /// Border radius of the UI node. - /// Ordering: top left, top right, bottom right, bottom left. - border_radius: ResolvedBorderRadius, - /// Border thickness of the UI node. - /// Ordering: left, top, right, bottom. - border: BorderRect, - node_type: NodeType, - }, - /// A contiguous sequence of text glyphs from the same section - Glyphs { - /// The color, position, and UV rect of each glyph. - glyphs: Vec, - }, -} - -pub struct ExtractedGlyph { - pub color: LinearRgba, - pub translation: Vec2, - pub rect: Rect, + pub background_color: LinearRgba, + pub border_color: LinearRgba, + pub outer_color: LinearRgba, + pub image_color: LinearRgba, + pub outline_color: LinearRgba, + pub image: Option>, + pub flip_x: bool, + pub flip_y: bool, } /// The list of UI nodes, as well as the set of nodes that changed. diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index bb77b09008950..b6c18aa10a6a3 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -1,7 +1,11 @@ +use crate::{ + stack_z_offsets, ExtractedUiItem, ExtractedUiNode, ExtractedUiNodes, NodeType, UiCameraMap, +}; use bevy_asset::AssetId; use bevy_camera::visibility::InheritedVisibility; -use bevy_color::Alpha; +use bevy_color::{Alpha, LinearRgba}; use bevy_ecs::prelude::*; +use bevy_image::Image; use bevy_input_focus::InputFocus; use bevy_math::{Affine2, Rect, Vec2}; use bevy_render::Extract; @@ -11,10 +15,28 @@ use bevy_ui::{ CalculatedClip, ComputedNode, ComputedStackIndex, ComputedUiTargetCamera, ResolvedBorderRadius, UiGlobalTransform, }; +use core::range::Range; -use crate::{ - stack_z_offsets, ExtractedUiItem, ExtractedUiNode, ExtractedUiNodes, NodeType, UiCameraMap, -}; +pub struct ExtractedGlyph { + pub color: LinearRgba, + pub translation: Vec2, + pub rect: Rect, +} + +pub struct Sections { + pub range: Range, + pub atlas_texture: AssetId, +} + +pub struct ExtractedGlyphLayout { + pub shadow_color: LinearRgba, + pub shadow_offset: Vec2, + pub cursor_style: TextCursorStyle, + pub glyphs: Vec, + pub strike_through: Vec<(Rect, LinearRgba)>, + pub underline: Vec<(Rect, LinearRgba)>, + pub sections: Vec, +} pub fn extract_text_cursor( mut commands: Commands, From dd23426e434ccb2338ac53c560d4d9703d845c71 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 16:14:14 +0100 Subject: [PATCH 25/99] Removed unused --- crates/bevy_ui_render/src/text.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index b6c18aa10a6a3..5b92f87bc7b3d 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -1,6 +1,4 @@ -use crate::{ - stack_z_offsets, ExtractedUiItem, ExtractedUiNode, ExtractedUiNodes, NodeType, UiCameraMap, -}; +use crate::{stack_z_offsets, ExtractedUiNode, ExtractedUiNodes, UiCameraMap}; use bevy_asset::AssetId; use bevy_camera::visibility::InheritedVisibility; use bevy_color::{Alpha, LinearRgba}; From 8cbbfb29a23a7e0b62fd32904ab87be97d24c7fb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 16:21:28 +0100 Subject: [PATCH 26/99] Added new text extraction buffer --- crates/bevy_ui_render/src/text.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index 5b92f87bc7b3d..ea978f7e97037 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -2,11 +2,15 @@ use crate::{stack_z_offsets, ExtractedUiNode, ExtractedUiNodes, UiCameraMap}; use bevy_asset::AssetId; use bevy_camera::visibility::InheritedVisibility; use bevy_color::{Alpha, LinearRgba}; +use bevy_ecs::entity::EntityIndexMap; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_input_focus::InputFocus; use bevy_math::{Affine2, Rect, Vec2}; -use bevy_render::Extract; +use bevy_render::{ + sync_world::{MainEntityHashMap, MainEntityHashSet}, + Extract, +}; use bevy_sprite::BorderRect; use bevy_text::{EditableText, TextColor, TextCursorStyle, TextLayoutInfo}; use bevy_ui::{ @@ -36,6 +40,12 @@ pub struct ExtractedGlyphLayout { pub sections: Vec, } +#[derive(Resource, Default)] +pub struct ExtractedGlyphLayouts { + pub uinodes: MainEntityHashMap<(Entity, EntityIndexMap)>, + pub changed: MainEntityHashSet, +} + pub fn extract_text_cursor( mut commands: Commands, extracted_uinodes: ResMut, From ff3485ec5435f12590ee7a18e2a267f00a5bb618 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 19:19:07 +0100 Subject: [PATCH 27/99] Moved text, borders , backgrounds, and outlines to using extracted geometry --- crates/bevy_ui_render/src/debug_overlay.rs | 157 -- crates/bevy_ui_render/src/lib.rs | 1902 +++++--------------- crates/bevy_ui_render/src/render_pass.rs | 12 +- crates/bevy_ui_render/src/text.rs | 944 +++++++--- 4 files changed, 1185 insertions(+), 1830 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 99aa2ed6ab106..ba77645aed845 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -1,36 +1,9 @@ -use super::ExtractedUiItem; -use super::ExtractedUiNode; -use super::ExtractedUiNodes; -use super::NodeType; -use super::UiCameraMap; -use crate::shader_flags; -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; use bevy_ecs::prelude::ReflectResource; use bevy_ecs::resource::Resource; -use bevy_ecs::system::Commands; -use bevy_ecs::system::Query; -use bevy_ecs::system::Res; -use bevy_ecs::system::ResMut; -use bevy_math::Affine2; -use bevy_math::Rect; -use bevy_math::Vec2; use bevy_reflect::Reflect; -use bevy_render::Extract; -use bevy_sprite::BorderRect; -use bevy_ui::ui_transform::UiGlobalTransform; -use bevy_ui::CalculatedClip; -use bevy_ui::ComputedNode; -use bevy_ui::ComputedStackIndex; -use bevy_ui::ComputedUiTargetCamera; -use bevy_ui::ResolvedBorderRadius; -use bevy_ui::UiStack; /// Configuration for the UI debug overlay /// @@ -168,133 +141,3 @@ impl From for GlobalUiDebugOptions { } } } - -pub fn extract_debug_overlay( - mut commands: Commands, - debug_options: Extract>, - extracted_uinodes: ResMut, - uinode_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - Option<&UiDebugOptions>, - )>, - >, - 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 - 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 { - continue; - } - if !debug_options.show_hidden && !visibility.get() { - continue; - } - - let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else { - continue; - }; - - let color = debug_options - .line_color_override - .unwrap_or_else(|| Hsla::sequential_dispersed(entity.index_u32()).into()); - let z_order = (ui_stack.uinodes.len() as u32 + stack_index.0) as f32; - let border = BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()); - let transform = transform.affine(); - - let mut push_outline = |rect: Rect, radius: ResolvedBorderRadius| { - if rect.is_empty() { - return; - } - - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - // Keep all overlays above UI, and nudge each type slightly in Z so ordering is stable. - z_order, - clip: maybe_clip - .filter(|_| !debug_options.show_clipped) - .map(|clip| clip.clip), - image: AssetId::default(), - transform: transform * Affine2::from_translation(rect.center()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: rect.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border, - border_radius: radius, - node_type: NodeType::Border(shader_flags::BORDER_ALL), - }, - }, - ); - }; - - let border_box = Rect::from_center_size(Vec2::ZERO, uinode.size); - - if debug_options.outline_border_box { - push_outline(border_box, uinode.border_radius()); - } - - if debug_options.outline_padding_box { - let mut padding_box = border_box; - padding_box.min += uinode.border.min_inset; - padding_box.max -= uinode.border.max_inset; - push_outline(padding_box, uinode.inner_radius()); - } - - if debug_options.outline_content_box { - let mut content_box = border_box; - let content_inset = uinode.content_inset(); - content_box.min += content_inset.min_inset; - content_box.max -= content_inset.max_inset; - push_outline(content_box, ResolvedBorderRadius::ZERO); - } - - if debug_options.outline_scrollbars { - if let Some((gutter, [thumb_min, thumb_max])) = uinode.horizontal_scrollbar() { - push_outline(gutter, ResolvedBorderRadius::ZERO); - push_outline( - Rect { - min: Vec2::new(thumb_min, gutter.min.y), - max: Vec2::new(thumb_max, gutter.max.y), - }, - ResolvedBorderRadius::ZERO, - ); - } - if let Some((gutter, [thumb_min, thumb_max])) = uinode.vertical_scrollbar() { - push_outline(gutter, ResolvedBorderRadius::ZERO); - push_outline( - Rect { - min: Vec2::new(gutter.min.x, thumb_min), - max: Vec2::new(gutter.max.x, thumb_max), - }, - ResolvedBorderRadius::ZERO, - ); - } - } - } -} diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index b0cdc33529a27..c24921db9ce3f 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -22,7 +22,6 @@ mod debug_overlay; pub mod extract_geometry; use bevy_a11y::AccessibilitySystems; -use bevy_camera::visibility::InheritedVisibility; use bevy_camera::{Camera, Camera2d, Camera3d, RenderTarget}; use bevy_ecs::entity::EntityIndexMap; use bevy_reflect::prelude::ReflectDefault; @@ -31,11 +30,10 @@ 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, Text, TextShadow, ViewportNode}; +use bevy_ui::widget::{ImageNode, ImageNodeSize, NodeImageMode, ViewportNode}; use bevy_ui::{ - BackgroundColor, BackgroundGradient, BorderColor, BorderGradient, BoxShadow, CalculatedClip, - ComputedNode, ComputedStackIndex, ComputedUiTargetCamera, Display, Node, OuterColor, Outline, - ResolvedBorderRadius, UiGlobalTransform, UiSystems, VisualBox, + BackgroundColor, BorderColor, ComputedUiTargetCamera, Node, OuterColor, Outline, UiSystems, + VisualBox, }; use bevy_app::prelude::*; @@ -68,11 +66,6 @@ pub use debug_overlay::{GlobalUiDebugOptions, UiDebugOptions}; use gradient::GradientPlugin; use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet}; -use bevy_text::{ - 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}; @@ -86,7 +79,7 @@ use ui_texture_slice_pipeline::UiTextureSlicerPlugin; use crate::extract_geometry::ExtractedUiGeometries; use crate::shader_flags::INVERT; -use crate::text::{extract_preedit_underlines, extract_text_cursor}; +use crate::text::{extract_text, prepare_text, queue_text, ExtractedGlyphLayouts}; pub mod prelude { #[cfg(feature = "bevy_ui_debug")] @@ -233,6 +226,7 @@ impl Plugin for UiRenderPlugin { .init_resource::() .init_resource::() .allow_ambiguous_resource::() + .init_resource::() .init_resource::>() .init_resource::>() .allow_ambiguous_resource::>() @@ -261,29 +255,23 @@ impl Plugin for UiRenderPlugin { ExtractSchedule, ( extract_geometry::extract_uinode_geometry, - extract_uinode_changes.in_set(RenderUiSystems::ExtractChanges), extract_ui_camera_view .after(extract_cameras) .in_set(RenderUiSystems::ExtractCameraViews), extract_uinode_background_colors.in_set(RenderUiSystems::ExtractBackgrounds), - extract_uinode_images.in_set(RenderUiSystems::ExtractImages), - extract_uinode_borders.in_set(RenderUiSystems::ExtractBorders), - extract_viewport_nodes.in_set(RenderUiSystems::ExtractViewportNodes), - extract_text_decorations.in_set(RenderUiSystems::ExtractTextBackgrounds), - extract_text_shadows.in_set(RenderUiSystems::ExtractTextShadows), - extract_text_sections.in_set(RenderUiSystems::ExtractText), - extract_text_cursor.in_set(RenderUiSystems::ExtractCursor), - extract_preedit_underlines.in_set(RenderUiSystems::ExtractCursor), - #[cfg(feature = "bevy_ui_debug")] - debug_overlay::extract_debug_overlay.in_set(RenderUiSystems::ExtractDebug), + extract_text.in_set(RenderUiSystems::ExtractText), ), ) .add_systems( Render, ( queue_uinodes.in_set(RenderSystems::Queue), + queue_text.in_set(RenderSystems::Queue), sort_phase_system::.in_set(RenderSystems::PhaseSort), prepare_uinodes.in_set(RenderSystems::PrepareBindGroups), + prepare_text + .after(prepare_uinodes) + .in_set(RenderSystems::PrepareBindGroups), clear_batches.in_set(RenderSystems::Cleanup), ), ) @@ -351,217 +339,92 @@ impl<'w, 's> UiCameraMapper<'w, 's> { pub struct ExtractedUiNode { pub background_color: LinearRgba, - pub border_color: LinearRgba, + pub border_color: [LinearRgba; 4], pub outer_color: LinearRgba, pub image_color: LinearRgba, pub outline_color: LinearRgba, pub image: Option>, pub flip_x: bool, pub flip_y: bool, + pub image_rect: Option, + pub image_size: Vec2, + pub auto_sized: bool, + pub visual_box: VisualBox, + pub use_node_border: bool, +} + +/// The type of UI node. +/// This is used to determine how to render the UI node. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum NodeType { + Rect, + Inverted, + Border(u32), // shader flags } /// The list of UI nodes, as well as the set of nodes that changed. /// -/// This is a two-level data structure so that we can quickly remove all -/// gradients associated with a main-world entity when it changes. #[derive(Resource, Default)] pub struct ExtractedUiNodes { - /// The list of UI nodes grouped by their main-world entity, along with - /// each group's target camera entity. - /// - /// This is a two-level data structure so that we can quickly remove all UI - /// nodes associated with a main-world entity when it changes. - pub uinodes: MainEntityHashMap<(Entity, EntityIndexMap)>, + pub uinodes: MainEntityHashMap<(EntityIndexMap, ExtractedUiNode)>, /// UI nodes that changed this frame. pub changed: MainEntityHashSet, } -/// A query filter that matches all UI nodes. -type UiNodeQueryFilter = ( - With, - With, - With, - With, - With, -); - -// 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( +pub fn extract_uinode_background_colors( mut commands: Commands, mut extracted_uinodes: ResMut, - all_uinodes_query: Extract>, + texture_atlases: Extract>>, + camera_query: Extract>, changed_uinodes_query: Extract< Query< Entity, - ( - UiNodeQueryFilter, - 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, - )>, - Or<(Changed, Changed)>, - )>, - ), + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, >, >, - #[cfg(feature = "bevy_ui_debug")] changed_debug_options_query: Extract< - Query)>, - >, - text_span_query: Extract< - Query< + uinode_query: Extract< + Query<( Entity, - ( - With, - Or<( - Changed, - Changed, - Changed, - Changed, - Changed, - Changed, - )>, - ), - >, + &BackgroundColor, + Option<&OuterColor>, + Option<&ImageNode>, + Option<&ImageNodeSize>, + AnyOf<(&BorderColor, &Outline)>, + Option<&ViewportNode>, + )>, >, - 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>, - Extract>, - Extract>, - ), - ( - mut removed_text_cursor_style_query, - mut removed_text_shadow_query, - mut removed_background_gradient_query, - mut removed_border_gradient_query, - mut removed_box_shadow_query, - mut removed_editable_text_query, - mut removed_underline_query, - mut removed_strikethrough_query, - ): ( - Extract>, - Extract>, - Extract>, - Extract>, - Extract>, - Extract>, - Extract>, - Extract>, ), - (mut removed_strikethrough_color_query, mut removed_underline_color_query): ( - Extract>, - Extract>, - ), - #[cfg(feature = "bevy_ui_debug")] mut removed_debug_options_query: Extract< - RemovedComponents, - >, - #[cfg(feature = "bevy_ui_debug")] global_ui_debug_options: Extract>, - mut extra_nodes_to_invalidate: Local, + mut nodes_to_extract: Local, ) { extracted_uinodes.changed.clear(); - - // If the debug options changed, we wipe everything. - // That's a bit coarse-grained, but having the debug options change is rare - // and should only happen in, well, debugging. - #[cfg(feature = "bevy_ui_debug")] - let must_wipe_all_nodes = global_ui_debug_options.is_changed(); - #[cfg(not(feature = "bevy_ui_debug"))] - let must_wipe_all_nodes = false; - - if must_wipe_all_nodes { - for main_entity in &all_uinodes_query { - process_changed_entity( - main_entity.into(), - &mut commands, - &text_span_parent_query, - &text_query, - &mut extracted_uinodes, - Some(&mut extra_nodes_to_invalidate), - ); - } - } else { - // Go through all nodes that have changed and invalidate any render world - // data associated with them. - for main_entity in changed_uinodes_query + nodes_to_extract.clear(); + nodes_to_extract.extend( + changed_uinodes_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()) @@ -569,481 +432,164 @@ pub fn extract_uinode_changes( .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_cursor_style_query.read()) - .chain(removed_text_shadow_query.read()) - .chain(removed_background_gradient_query.read()) - .chain(removed_border_gradient_query.read()) - .chain(removed_box_shadow_query.read()) - .chain(removed_editable_text_query.read()) - .chain(removed_underline_query.read()) - .chain(removed_strikethrough_query.read()) - .chain(removed_strikethrough_color_query.read()) - .chain(removed_underline_color_query.read()) - { - process_changed_entity( - main_entity.into(), - &mut commands, - &text_span_parent_query, - &text_query, - &mut extracted_uinodes, - Some(&mut extra_nodes_to_invalidate), - ); - } - - // Process nodes that have changed debug options too, if that feature is - // enabled. - #[cfg(feature = "bevy_ui_debug")] - for main_entity in changed_debug_options_query - .iter() - .chain(removed_debug_options_query.read()) - { - process_changed_entity( - main_entity.into(), - &mut commands, - &text_span_parent_query, - &text_query, - &mut extracted_uinodes, - Some(&mut extra_nodes_to_invalidate), - ); - } - } + .map(MainEntity::from), + ); - 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. + for main_entity in nodes_to_extract.drain() { 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(..) { + let Ok(( + entity, + background_color, + maybe_outer_color, + maybe_image, + maybe_image_size, + (maybe_border_color, maybe_outline), + maybe_viewport_node, + )) = uinode_query.get(main_entity.entity()) + else { + let Some((mut render_items, _)) = extracted_uinodes.uinodes.remove(&main_entity) else { + continue; + }; + for (render_entity, _) in render_items.drain(..) { commands.entity(render_entity).despawn(); } - } + continue; + }; - // 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()) + let mut image_color = LinearRgba::NONE; + let mut image_asset = None; + let mut flip_x = false; + let mut flip_y = false; + let mut image_rect = None; + let mut image_size = Vec2::ZERO; + let mut auto_sized = false; + let mut visual_box = VisualBox::default(); + let mut use_node_border = false; + + if let Some(image) = maybe_image + && !image.color.is_fully_transparent() + && image.image.id() != TRANSPARENT_IMAGE_HANDLE.id() + && !image.image_mode.uses_slices() { - 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, + let atlas_rect = image + .texture_atlas + .as_ref() + .and_then(|s| s.texture_rect(&texture_atlases)) + .map(|r| r.as_rect()); + + image_rect = match (atlas_rect, image.rect) { + (None, None) => None, + (None, Some(image_rect)) => Some(image_rect), + (Some(atlas_rect), None) => Some(atlas_rect), + (Some(atlas_rect), Some(mut image_rect)) => { + image_rect.min += atlas_rect.min; + image_rect.max += atlas_rect.min; + Some(image_rect) } - } + }; + image_color = image.color.into(); + image_asset = Some(image.image.id()); + flip_x = image.flip_x; + flip_y = image.flip_y; + image_size = maybe_image_size + .map(|image_size| image_size.size().as_vec2()) + .unwrap_or_default(); + auto_sized = matches!(image.image_mode, NodeImageMode::Auto); + visual_box = image.visual_box; } - } -} -pub fn extract_uinode_background_colors( - mut commands: Commands, - extracted_uinodes: ResMut, - uinode_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &BackgroundColor, - Option<&OuterColor>, - )>, - >, - camera_map: Extract, -) { - let extracted_uinodes = extracted_uinodes.into_inner(); - let mut camera_mapper = camera_map.get_mapper(); - - for ( - entity, - uinode, - stack_index, - transform, - inherited_visibility, - clip, - camera, - background_color, - maybe_outer_color, - ) in extracted_uinodes - .changed - .iter() - .flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok()) - { - // Skip invisible backgrounds - if !inherited_visibility.get() - || (background_color.is_fully_transparent() - && maybe_outer_color.is_none_or(|outer| outer.is_fully_transparent())) - || uinode.is_empty() + let border_color = maybe_border_color + .map(|border_color| { + [ + border_color.left.to_linear(), + border_color.top.to_linear(), + border_color.right.to_linear(), + border_color.bottom.to_linear(), + ] + }) + .unwrap_or([LinearRgba::NONE; 4]); + let outline_color = maybe_outline + .map(|outline| outline.color.into()) + .unwrap_or(LinearRgba::NONE); + + if let Some(viewport_node) = maybe_viewport_node + && let Some(camera_entity) = viewport_node.camera + && let Some(image) = camera_query + .get(camera_entity) + .ok() + .and_then(|(_, render_target)| render_target.as_image()) { - continue; + image_color = LinearRgba::WHITE; + image_asset = Some(image.id()); + flip_x = false; + flip_y = false; + image_rect = None; + image_size = Vec2::ZERO; + auto_sized = false; + visual_box = VisualBox::BorderBox; + use_node_border = true; } - let extracted_sub_uinodes = match extracted_uinodes.uinodes.entry(entity.into()) { - Entry::Occupied(entry) => &mut entry.into_mut().1, - Entry::Vacant(entry) => { - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; - }; - &mut entry - .insert((extracted_camera_entity, Default::default())) - .1 - } + let extracted_uinode = ExtractedUiNode { + background_color: background_color.0.into(), + border_color, + outer_color: maybe_outer_color + .map(|outer_color| outer_color.0.into()) + .unwrap_or(LinearRgba::NONE), + image_color, + outline_color, + image: image_asset, + flip_x, + flip_y, + image_rect, + image_size, + auto_sized, + visual_box, + use_node_border, }; - if !background_color.is_fully_transparent() { - extracted_sub_uinodes.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(), - 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, - }, - }, - ); - } - - if let Some(outer_color) = maybe_outer_color - && !outer_color.0.is_fully_transparent() + let mut layers = vec![]; + if !extracted_uinode.background_color.is_fully_transparent() + || !extracted_uinode.outer_color.is_fully_transparent() { - extracted_sub_uinodes.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(), - 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, - }, - }, - ); + layers.push(stack_z_offsets::BACKGROUND_COLOR); } - } -} - -pub fn extract_uinode_images( - mut commands: Commands, - extracted_uinodes: ResMut, - texture_atlases: Extract>>, - uinode_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &ImageNode, - &ImageNodeSize, - )>, - >, - camera_map: Extract, -) { - let extracted_uinodes = extracted_uinodes.into_inner(); - let mut camera_mapper = camera_map.get_mapper(); - - for ( - entity, - uinode, - stack_index, - transform, - inherited_visibility, - clip, - camera, - image, - image_size, - ) 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(), - VisualBox::PaddingBox => uinode.padding_box(), - VisualBox::BorderBox => uinode.border_box(), - }; - // Skip invisible images - if !inherited_visibility.get() - || image.color.is_fully_transparent() - || image.image.id() == TRANSPARENT_IMAGE_HANDLE.id() - || image.image_mode.uses_slices() - || visual_box.size().cmple(Vec2::ZERO).any() + if extracted_uinode + .border_color + .iter() + .any(|color| !color.is_fully_transparent()) + || !extracted_uinode.outline_color.is_fully_transparent() { - continue; + layers.push(stack_z_offsets::BORDER); } - - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; - }; - - let size = if matches!(image.image_mode, NodeImageMode::Auto) { - let source = image_size.size().as_vec2(); - if source.cmple(Vec2::ZERO).any() { - visual_box.size() - } else { - source * (visual_box.size() / source).min_element() - } - } else { - visual_box.size() - }; - - let atlas_rect = image - .texture_atlas - .as_ref() - .and_then(|s| s.texture_rect(&texture_atlases)) - .map(|r| r.as_rect()); - - let mut rect = match (atlas_rect, image.rect) { - (None, None) => Rect { - min: Vec2::ZERO, - max: size, - }, - (None, Some(image_rect)) => image_rect, - (Some(atlas_rect), None) => atlas_rect, - (Some(atlas_rect), Some(mut image_rect)) => { - image_rect.min += atlas_rect.min; - image_rect.max += atlas_rect.min; - image_rect - } - }; - - let atlas_scaling = if atlas_rect.is_some() || image.rect.is_some() { - let atlas_scaling = size / rect.size(); - rect.min *= atlas_scaling; - rect.max *= atlas_scaling; - Some(atlas_scaling) - } else { - None - }; - - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::IMAGE, - clip: clip.map(|clip| clip.clip), - image: image.image.id(), - transform: Affine2::from(*transform) - * Affine2::from_translation(visual_box.center()), - item: ExtractedUiItem::Node { - color: image.color.into(), - rect, - atlas_scaling, - flip_x: image.flip_x, - flip_y: image.flip_y, - border: BorderRect::ZERO, - border_radius: uinode.border_radius, - node_type: NodeType::Rect, - }, - }, - ); - } -} - -pub fn extract_uinode_borders( - mut commands: Commands, - extracted_uinodes: ResMut, - uinode_query: Extract< - Query<( - Entity, - Option<&Node>, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - AnyOf<(&BorderColor, &Outline)>, - )>, - >, - camera_map: Extract, -) { - let extracted_uinodes = extracted_uinodes.into_inner(); - let image = AssetId::::default(); - let mut camera_mapper = camera_map.get_mapper(); - - for ( - entity, - node, - computed_node, - stack_index, - transform, - inherited_visibility, - maybe_clip, - camera, - (maybe_border_color, maybe_outline), - ) 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) { - continue; + if extracted_uinode.image.is_some() { + layers.push(stack_z_offsets::IMAGE); } - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; + let render_items = match extracted_uinodes.uinodes.entry(entity.into()) { + Entry::Occupied(mut entry) => { + entry.get_mut().1 = extracted_uinode; + &mut entry.into_mut().0 + } + Entry::Vacant(entry) => &mut entry.insert((Default::default(), extracted_uinode)).0, }; - // Don't extract borders with zero width along all edges - if computed_node.border() != BorderRect::ZERO - && let Some(border_color) = maybe_border_color - { - let border_colors = [ - border_color.left.to_linear(), - border_color.top.to_linear(), - border_color.right.to_linear(), - border_color.bottom.to_linear(), - ]; - - const BORDER_FLAGS: [u32; 4] = [ - shader_flags::BORDER_LEFT, - shader_flags::BORDER_TOP, - shader_flags::BORDER_RIGHT, - shader_flags::BORDER_BOTTOM, - ]; - let mut completed_flags = 0; - - for (i, &color) in border_colors.iter().enumerate() { - if color.is_fully_transparent() { - continue; - } - - let mut border_flags = BORDER_FLAGS[i]; - - if completed_flags & border_flags != 0 { - continue; - } - - for j in i + 1..4 { - if color == border_colors[j] { - border_flags |= BORDER_FLAGS[j]; - } - } - completed_flags |= border_flags; - - let node = ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::BORDER, - image, - clip: maybe_clip.map(|clip| clip.clip), - transform: transform.into(), - item: ExtractedUiItem::Node { - color, - rect: Rect { - max: computed_node.size(), - ..Default::default() - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: computed_node.border(), - border_radius: computed_node.border_radius(), - node_type: NodeType::Border(border_flags), - }, - }; - - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert(commands.spawn_empty().id(), node); + let layer_count = layers.len(); + for (index, layer) in layers.into_iter().enumerate() { + if let Some((_, old_layer)) = render_items.get_index_mut(index) { + *old_layer = layer; + } else { + render_items.insert(commands.spawn_empty().id(), layer); } } - if computed_node.outline_width() <= 0. { - continue; + for (render_entity, _) in render_items.drain(layer_count..) { + commands.entity(render_entity).despawn(); } - if let Some(outline) = maybe_outline.filter(|outline| !outline.color.is_fully_transparent()) - { - let outline_size = computed_node.outlined_node_size(); - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::BORDER, - image, - clip: maybe_clip.map(|clip| clip.clip), - transform: transform.into(), - item: ExtractedUiItem::Node { - color: outline.color.into(), - rect: Rect { - max: outline_size, - ..Default::default() - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::all(computed_node.outline_width()), - border_radius: computed_node.outline_radius(), - node_type: NodeType::Border(shader_flags::BORDER_ALL), - }, - }, - ); + if render_items.is_empty() { + extracted_uinodes.uinodes.remove(&main_entity); } } } @@ -1232,628 +778,9 @@ pub fn extract_ui_camera_view( transparent_render_phases.retain(|entity, _| live_entities.contains(entity)); } -pub fn extract_viewport_nodes( - mut commands: Commands, - extracted_uinodes: ResMut, - camera_query: Extract>, - uinode_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &ViewportNode, - )>, - >, - camera_map: Extract, -) { - let extracted_uinodes = extracted_uinodes.into_inner(); - let mut camera_mapper = camera_map.get_mapper(); - - for ( - entity, - uinode, - stack_index, - transform, - inherited_visibility, - clip, - camera, - viewport_node, - ) 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() { - continue; - } - - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; - }; - let Some(camera_entity) = viewport_node.camera else { - continue; - }; - - let Some(image) = camera_query - .get(camera_entity) - .ok() - .and_then(|(_, render_target)| render_target.as_image()) - else { - continue; - }; - - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::IMAGE, - clip: clip.map(|clip| clip.clip), - image: image.id(), - transform: transform.into(), - item: ExtractedUiItem::Node { - color: LinearRgba::WHITE, - rect: Rect { - min: Vec2::ZERO, - max: uinode.size, - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: uinode.border(), - border_radius: uinode.border_radius(), - node_type: NodeType::Rect, - }, - }, - ); - } -} - -pub fn extract_text_sections( - mut commands: Commands, - extracted_uinodes: ResMut, - uinode_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &ComputedTextBlock, - &TextColor, - &TextLayoutInfo, - Option<&EditableText>, - Option<&TextCursorStyle>, - )>, - >, - text_styles: Extract>, - camera_map: Extract, -) { - let extracted_uinodes = extracted_uinodes.into_inner(); - let mut camera_mapper = camera_map.get_mapper(); - - let mut glyphs = vec![]; - - for ( - entity, - uinode, - stack_index, - global_transform, - inherited_visibility, - maybe_clip, - camera, - computed_block, - text_color, - text_layout_info, - editable_text, - cursor_style, - ) 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() { - continue; - } - - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; - }; - - let transform = Affine2::from(*global_transform) - * Affine2::from_translation( - uinode.content_box().min - - editable_text.map_or(Vec2::ZERO, |text| text.viewport.offset), - ); - - let clip = if editable_text.is_some() { - let content_box = uinode.content_box(); - let text_clip = Rect::from_center_size( - global_transform.affine().translation + content_box.center(), - content_box.size(), - ); - Some(maybe_clip.map_or(text_clip, |clip| clip.clip.intersect(text_clip))) - } else { - maybe_clip.map(|clip| clip.clip) - }; - - let mut color = text_color.0.to_linear(); - - let selected_text_color = cursor_style - .and_then(|cursor_style| cursor_style.selected_text_color) - .map(|selected_text_color| selected_text_color.to_linear()); - - let mut current_section_index = 0; - - for ( - i, - PositionedGlyph { - position, - atlas_info, - section_index, - .. - }, - ) in text_layout_info.glyphs.iter().enumerate() - { - if current_section_index != *section_index - && let Some(section_entity) = computed_block - .entities() - .get(*section_index as usize) - .map(|t| t.entity) - { - color = text_styles - .get(section_entity) - .map(|text_color| LinearRgba::from(text_color.0)) - .unwrap_or_default(); - current_section_index = *section_index; - } - - let color = if !atlas_info.is_alpha_mask { - LinearRgba::WHITE - } else if let Some(selected_text_color) = selected_text_color - && text_layout_info - .selection_rects - .iter() - .any(|selection_rect| { - let glyph_rect = Rect::from_center_size(*position, atlas_info.rect.size()); - selection_rect.contains(glyph_rect.min) - && selection_rect.contains(glyph_rect.max) - }) - { - selected_text_color - } else { - color - }; - - glyphs.push(ExtractedGlyph { - color, - translation: *position, - rect: atlas_info.rect, - }); - - if text_layout_info - .glyphs - .get(i + 1) - .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) - { - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - image: atlas_info.texture, - clip, - item: ExtractedUiItem::Glyphs { - glyphs: mem::take(&mut glyphs), - }, - transform, - }, - ); - } - } - } -} - -pub fn extract_text_shadows( - mut commands: Commands, - extracted_uinodes: ResMut, - uinode_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &ComputedUiTargetCamera, - &InheritedVisibility, - Option<&CalculatedClip>, - &TextLayoutInfo, - &TextShadow, - &ComputedTextBlock, - Option<&EditableText>, - )>, - >, - text_decoration_query: Extract, Has)>>, - camera_map: Extract, -) { - let extracted_uinodes = extracted_uinodes.into_inner(); - let mut camera_mapper = camera_map.get_mapper(); - - let mut glyphs = vec![]; - - for ( - entity, - uinode, - stack_index, - global_transform, - target, - inherited_visibility, - maybe_clip, - text_layout_info, - shadow, - computed_block, - editable_text, - ) 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() { - continue; - } - - let Some(extracted_camera_entity) = camera_mapper.map(target) else { - continue; - }; - - let node_transform = Affine2::from(*global_transform) - * Affine2::from_translation( - uinode.content_box().min + shadow.offset / uinode.inverse_scale_factor() - - editable_text.map_or(Vec2::ZERO, |text| text.viewport.offset), - ); - - let clip = if editable_text.is_some() { - let content_box = uinode.content_box(); - let text_clip = Rect::from_center_size( - global_transform.affine().translation + content_box.center(), - content_box.size(), - ); - Some(maybe_clip.map_or(text_clip, |clip| clip.clip.intersect(text_clip))) - } else { - maybe_clip.map(|clip| clip.clip) - }; - - for ( - i, - PositionedGlyph { - position, - atlas_info, - section_index, - .. - }, - ) in text_layout_info.glyphs.iter().enumerate() - { - glyphs.push(ExtractedGlyph { - color: shadow.color.into(), - translation: *position, - rect: atlas_info.rect, - }); - - if text_layout_info.glyphs.get(i + 1).is_none_or(|info| { - info.section_index != *section_index - || info.atlas_info.texture != atlas_info.texture - }) { - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - transform: node_transform, - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - image: atlas_info.texture, - clip, - item: ExtractedUiItem::Glyphs { - glyphs: mem::take(&mut glyphs), - }, - }, - ); - } - } - - for run in text_layout_info.run_geometry.iter() { - let Some(section_entity) = computed_block - .entities() - .get(run.section_index as usize) - .map(|t| t.entity) - else { - continue; - }; - let Ok((has_strikethrough, has_underline)) = text_decoration_query.get(section_entity) - else { - continue; - }; - - if has_strikethrough { - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - clip, - image: AssetId::default(), - transform: node_transform - * Affine2::from_translation(run.strikethrough_position()), - item: ExtractedUiItem::Node { - color: shadow.color.into(), - rect: Rect { - min: Vec2::ZERO, - max: run.strikethrough_size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - }, - ); - } - - if has_underline { - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - clip, - image: AssetId::default(), - transform: node_transform - * Affine2::from_translation(run.underline_position()), - item: ExtractedUiItem::Node { - color: shadow.color.into(), - rect: Rect { - min: Vec2::ZERO, - max: run.underline_size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - }, - ); - } - } - } -} - -pub fn extract_text_decorations( - mut commands: Commands, - extracted_uinodes: ResMut, - uinode_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &ComputedTextBlock, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &TextLayoutInfo, - Option<&EditableText>, - )>, - >, - text_background_colors_query: Extract< - Query<( - AnyOf<(&TextBackgroundColor, &Strikethrough, &Underline)>, - &TextColor, - Option<&StrikethroughColor>, - Option<&UnderlineColor>, - )>, - >, - camera_map: Extract, -) { - let extracted_uinodes = extracted_uinodes.into_inner(); - let mut camera_mapper = camera_map.get_mapper(); - - for ( - entity, - uinode, - stack_index, - computed_block, - global_transform, - inherited_visibility, - maybe_clip, - camera, - text_layout_info, - editable_text, - ) 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() { - continue; - } - - let Some(extracted_camera_entity) = camera_mapper.map(camera) else { - continue; - }; - - let transform = Affine2::from(global_transform) - * Affine2::from_translation( - uinode.content_box().min - - editable_text.map_or(Vec2::ZERO, |text| text.viewport.offset), - ); - - let clip = if editable_text.is_some() { - let content_box = uinode.content_box(); - let text_clip = Rect::from_center_size( - global_transform.affine().translation + content_box.center(), - content_box.size(), - ); - Some(maybe_clip.map_or(text_clip, |clip| clip.clip.intersect(text_clip))) - } else { - maybe_clip.map(|clip| clip.clip) - }; - - for run in text_layout_info.run_geometry.iter() { - let Some(section_entity) = computed_block - .entities() - .get(run.section_index as usize) - .map(|t| t.entity) - else { - continue; - }; - let Ok(( - (text_background_color, maybe_strikethrough, maybe_underline), - text_color, - maybe_strikethrough_color, - maybe_underline_color, - )) = text_background_colors_query.get(section_entity) - else { - continue; - }; - - if let Some(text_background_color) = text_background_color { - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT, - clip, - image: AssetId::default(), - transform: transform * Affine2::from_translation(run.bounds.center()), - item: ExtractedUiItem::Node { - color: text_background_color.0.to_linear(), - rect: Rect { - min: Vec2::ZERO, - max: run.bounds.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - }, - ); - } - - if maybe_strikethrough.is_some() { - let color = maybe_strikethrough_color - .map(|sc| sc.0) - .unwrap_or(text_color.0) - .to_linear(); - - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, - clip, - image: AssetId::default(), - transform: transform - * Affine2::from_translation(run.strikethrough_position()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: run.strikethrough_size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - }, - ); - } - - if maybe_underline.is_some() { - let color = maybe_underline_color - .map(|uc| uc.0) - .unwrap_or(text_color.0) - .to_linear(); - - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, - clip, - image: AssetId::default(), - transform: transform - * Affine2::from_translation(run.underline_position()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: run.underline_size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::ZERO, - border_radius: ResolvedBorderRadius::ZERO, - node_type: NodeType::Rect, - }, - }, - ); - } - } - } -} - #[repr(C)] #[derive(Copy, Clone, Debug, Pod, Zeroable)] -struct UiVertex { +pub(crate) struct UiVertex { pub position: [f32; 3], pub uv: [f32; 2], pub color: [f32; 4], @@ -1874,8 +801,8 @@ struct UiVertex { #[derive(Resource)] pub struct UiMeta { - vertices: RawBufferVec, - indices: RawBufferVec, + pub(crate) vertices: RawBufferVec, + pub(crate) indices: RawBufferVec, view_bind_group: Option, } @@ -1902,6 +829,7 @@ pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; pub struct UiBatch { pub range: Range, pub image: AssetId, + pub texture_ranges: Vec<(Range, AssetId)>, } /// The values here should match the values for the constants in `ui.wgsl` @@ -1926,6 +854,7 @@ pub mod shader_flags { pub fn queue_uinodes( extracted_uinodes: Res, + extracted_geometry: Res, ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -1938,47 +867,52 @@ pub fn queue_uinodes( let mut current_camera_entity = Entity::PLACEHOLDER; let mut current_phase = None; - for (main_entity, (extracted_camera_entity, extracted_sub_uinodes)) in - extracted_uinodes.uinodes.iter() - { - if current_camera_entity != *extracted_camera_entity { - current_phase = render_views.get(*extracted_camera_entity).ok().and_then( - |(default_camera_view, ui_anti_alias)| { - camera_views - .get(default_camera_view.0) - .ok() - .and_then(|view| { - transparent_render_phases - .get_mut(&view.retained_view_entity) - .map(|transparent_phase| { - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_pipeline, - UiPipelineKey { - target_format: view.target_format, - anti_alias: matches!( - ui_anti_alias, - None | Some(UiAntiAlias::On) - ), - }, - ); - (pipeline, transparent_phase) - }) - }) - }, - ); - current_camera_entity = *extracted_camera_entity; - } - - let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { + for (main_entity, (render_items, _)) in extracted_uinodes.uinodes.iter() { + let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { continue; }; - for (render_entity, extracted_uinode) in extracted_sub_uinodes.iter() { + + for (render_entity, stack_z_offset) in render_items.iter() { + let extracted_camera_entity = geometry.extracted_camera; + + if current_camera_entity != extracted_camera_entity { + current_phase = render_views.get(extracted_camera_entity).ok().and_then( + |(default_camera_view, ui_anti_alias)| { + camera_views + .get(default_camera_view.0) + .ok() + .and_then(|view| { + transparent_render_phases + .get_mut(&view.retained_view_entity) + .map(|transparent_phase| { + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_pipeline, + UiPipelineKey { + target_format: view.target_format, + anti_alias: matches!( + ui_anti_alias, + None | Some(UiAntiAlias::On) + ), + }, + ); + (pipeline, transparent_phase) + }) + }) + }, + ); + current_camera_entity = extracted_camera_entity; + } + + let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { + continue; + }; + transparent_phase.add_transient(TransparentUi { draw_function, pipeline: *pipeline, entity: (*render_entity, *main_entity), - sort_key: FloatOrd(extracted_uinode.z_order), + sort_key: FloatOrd(geometry.stack_index as f32 + stack_z_offset), // batch_range will be calculated in prepare_uinodes batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, @@ -2000,6 +934,7 @@ pub fn prepare_uinodes( pipeline_cache: Res, mut ui_meta: ResMut, extracted_uinodes: Res, + extracted_geometry: Res, view_uniforms: Res, ui_pipeline: Res, mut image_bind_groups: ResMut, @@ -2042,14 +977,31 @@ pub fn prepare_uinodes( for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; - let Some(extracted_uinode) = extracted_uinodes - .uinodes + let extracted_uinode = extracted_uinodes.uinodes.get(&item.main_entity()).and_then( + |(render_items, extracted_uinode)| { + render_items + .get(&item.entity()) + .map(|stack_z_offset| (extracted_uinode, *stack_z_offset)) + }, + ); + if extracted_uinode.is_none() { + batch_image_handle = None; + continue; + } + let Some(geometry) = extracted_geometry + .uinode_geometries .get(&item.main_entity()) - .and_then(|(_, sub_uinodes)| sub_uinodes.get(&item.entity())) else { batch_image_handle = None; continue; }; + let image = extracted_uinode + .and_then(|(extracted_uinode, stack_z_offset)| { + (stack_z_offset == stack_z_offsets::IMAGE) + .then_some(extracted_uinode.image) + .flatten() + }) + .unwrap_or_default(); // Initialize the batch range to be zero-length initially. // We'll extend it as we accumulate items into this batch. @@ -2060,88 +1012,223 @@ pub fn prepare_uinodes( if batch_image_handle.is_none() || existing_batch.is_none() || (batch_image_handle != Some(AssetId::default()) - && extracted_uinode.image != AssetId::default() - && batch_image_handle != Some(extracted_uinode.image)) + && image != AssetId::default() + && batch_image_handle != Some(image)) { - if let Some(gpu_image) = gpu_images.get(extracted_uinode.image) { + if let Some(gpu_image) = gpu_images.get(image) { batch_item_index = item_index; - batch_image_handle = Some(extracted_uinode.image); + batch_image_handle = Some(image); let new_batch = UiBatch { range: vertices_index..vertices_index, - image: extracted_uinode.image, + image, + texture_ranges: vec![], }; batches.push((item.entity(), new_batch)); - image_bind_groups - .values - .entry(extracted_uinode.image) - .or_insert_with(|| { - render_device.create_bind_group( - "ui_material_bind_group", - &pipeline_cache - .get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential(( - &gpu_image.texture_view, - &gpu_image.sampler, - )), - ) - }); + image_bind_groups.values.entry(image).or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); existing_batch = batches.last_mut(); } else { continue; } } else if batch_image_handle == Some(AssetId::default()) - && extracted_uinode.image != AssetId::default() + && image != AssetId::default() { if let Some(ref mut existing_batch) = existing_batch - && let Some(gpu_image) = gpu_images.get(extracted_uinode.image) + && let Some(gpu_image) = gpu_images.get(image) { - batch_image_handle = Some(extracted_uinode.image); - existing_batch.1.image = extracted_uinode.image; - - image_bind_groups - .values - .entry(extracted_uinode.image) - .or_insert_with(|| { - render_device.create_bind_group( - "ui_material_bind_group", - &pipeline_cache - .get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential(( - &gpu_image.texture_view, - &gpu_image.sampler, - )), - ) - }); + batch_image_handle = Some(image); + existing_batch.1.image = image; + + image_bind_groups.values.entry(image).or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); } else { continue; } } - match &extracted_uinode.item { - ExtractedUiItem::Node { + if let Some((extracted_uinode, stack_z_offset)) = extracted_uinode { + let uinode = &geometry.computed_node; + let mut nodes = vec![]; + + if stack_z_offset == stack_z_offsets::BACKGROUND_COLOR { + if !extracted_uinode.background_color.is_fully_transparent() { + nodes.push(( + extracted_uinode.background_color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + geometry.transform, + geometry.clip, + uinode.border_radius(), + uinode.border(), + NodeType::Rect, + false, + )); + } + if !extracted_uinode.outer_color.is_fully_transparent() { + nodes.push(( + extracted_uinode.outer_color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + geometry.transform, + geometry.clip, + uinode.border_radius(), + BorderRect::ZERO, + NodeType::Inverted, + false, + )); + } + } else if stack_z_offset == stack_z_offsets::BORDER { + if uinode.border() != BorderRect::ZERO { + const BORDER_FLAGS: [u32; 4] = [ + shader_flags::BORDER_LEFT, + shader_flags::BORDER_TOP, + shader_flags::BORDER_RIGHT, + shader_flags::BORDER_BOTTOM, + ]; + let mut completed_flags = 0; + + for (i, &color) in extracted_uinode.border_color.iter().enumerate() { + if color.is_fully_transparent() + || completed_flags & BORDER_FLAGS[i] != 0 + { + continue; + } + + let mut border_flags = BORDER_FLAGS[i]; + for (j, &other_color) in + extracted_uinode.border_color.iter().enumerate().skip(i + 1) + { + if color == other_color { + border_flags |= BORDER_FLAGS[j]; + } + } + completed_flags |= border_flags; + + nodes.push(( + color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + geometry.transform, + geometry.clip, + uinode.border_radius(), + uinode.border(), + NodeType::Border(border_flags), + false, + )); + } + } + + if !extracted_uinode.outline_color.is_fully_transparent() + && uinode.outline_width() > 0. + { + nodes.push(( + extracted_uinode.outline_color, + Rect { + min: Vec2::ZERO, + max: uinode.outlined_node_size(), + }, + None, + geometry.transform, + geometry.clip, + uinode.outline_radius(), + BorderRect::all(uinode.outline_width()), + NodeType::Border(shader_flags::BORDER_ALL), + false, + )); + } + } else { + let visual_box = match extracted_uinode.visual_box { + VisualBox::ContentBox => uinode.content_box(), + VisualBox::PaddingBox => uinode.padding_box(), + VisualBox::BorderBox => uinode.border_box(), + }; + if visual_box.size().cmple(Vec2::ZERO).any() { + continue; + } + + let size = if extracted_uinode.auto_sized + && !extracted_uinode.image_size.cmple(Vec2::ZERO).any() + { + extracted_uinode.image_size + * (visual_box.size() / extracted_uinode.image_size).min_element() + } else { + visual_box.size() + }; + let mut rect = extracted_uinode.image_rect.unwrap_or(Rect { + min: Vec2::ZERO, + max: size, + }); + let atlas_scaling = extracted_uinode.image_rect.map(|_| { + let atlas_scaling = size / rect.size(); + rect.min *= atlas_scaling; + rect.max *= atlas_scaling; + atlas_scaling + }); + + nodes.push(( + extracted_uinode.image_color, + rect, + atlas_scaling, + geometry.transform * Affine2::from_translation(visual_box.center()), + geometry.clip, + uinode.border_radius(), + if extracted_uinode.use_node_border { + uinode.border() + } else { + BorderRect::ZERO + }, + NodeType::Rect, + true, + )); + } + + for ( + color, + mut uinode_rect, atlas_scaling, - flip_x, - flip_y, + transform, + clip, border_radius, border, node_type, - rect, - color, - } => { - let mut flags = if extracted_uinode.image != AssetId::default() { + textured, + ) in nodes + { + let mut flags = if textured { shader_flags::TEXTURED } else { shader_flags::UNTEXTURED }; - let mut uinode_rect = *rect; - let rect_size = uinode_rect.size(); - let transform = extracted_uinode.transform; - // Specify the corners of the node let positions = QUAD_VERTEX_POSITIONS .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); @@ -2149,7 +1236,7 @@ pub fn prepare_uinodes( // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) - let mut positions_diff = if let Some(clip) = extracted_uinode.clip { + let mut positions_diff = if let Some(clip) = clip { [ Vec2::new( f32::max(clip.min.x - positions[0].x, 0.), @@ -2207,20 +1294,20 @@ pub fn prepare_uinodes( [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] } else { let image = gpu_images - .get(extracted_uinode.image) + .get(image) .expect("Image was checked during batching and should still exist"); // Rescale atlases. This is done here because we need texture data that might not be available in Extract. let atlas_extent = atlas_scaling .map(|scaling| image.size_2d().as_vec2() * scaling) .unwrap_or(uinode_rect.max); - if *flip_x { + if extracted_uinode.flip_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 { + if extracted_uinode.flip_y { mem::swap(&mut uinode_rect.max.y, &mut uinode_rect.min.y); positions_diff[0].y *= -1.; positions_diff[1].y *= -1.; @@ -2249,7 +1336,7 @@ pub fn prepare_uinodes( }; let color = color.to_f32_array(); - match *node_type { + match node_type { NodeType::Border(border_flags) => { flags |= border_flags; } @@ -2265,7 +1352,7 @@ pub fn prepare_uinodes( uv: uvs[i].into(), color, flags: flags | shader_flags::CORNERS[i], - radius: (*border_radius).into(), + radius: border_radius.into(), border: [ border.min_inset.x, border.min_inset.y, @@ -2285,114 +1372,9 @@ pub fn prepare_uinodes( vertices_index += 6; indices_index += 4; } - 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 glyphs { - let color = glyph.color.to_f32_array(); - let glyph_rect = glyph.rect; - let rect_size = glyph_rect.size(); - - // Specify the corners of the glyph - let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - extracted_uinode - .transform - .transform_point2(glyph.translation + pos * glyph_rect.size()) - .extend(0.) - }); - - let positions_diff = if let Some(clip) = extracted_uinode.clip { - [ - Vec2::new( - f32::max(clip.min.x - positions[0].x, 0.), - f32::max(clip.min.y - positions[0].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[1].x, 0.), - f32::max(clip.min.y - positions[1].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[2].x, 0.), - f32::min(clip.max.y - positions[2].y, 0.), - ), - Vec2::new( - f32::max(clip.min.x - positions[3].x, 0.), - f32::min(clip.max.y - positions[3].y, 0.), - ), - ] - } else { - [Vec2::ZERO; 4] - }; - - let positions_clipped = [ - positions[0] + positions_diff[0].extend(0.), - positions[1] + positions_diff[1].extend(0.), - positions[2] + positions_diff[2].extend(0.), - positions[3] + positions_diff[3].extend(0.), - ]; - - // cull nodes that are completely clipped - let transformed_rect_size = extracted_uinode - .transform - .transform_vector2(rect_size) - .abs(); - // Don't try to cull glyphs that have a rotation. - if extracted_uinode.transform.x_axis[1] == 0.0 - && (positions_diff[0].x - positions_diff[1].x - >= transformed_rect_size.x - || positions_diff[1].y - positions_diff[2].y - >= transformed_rect_size.y) - { - continue; - } - - let uvs = [ - Vec2::new( - glyph.rect.min.x + positions_diff[0].x, - glyph.rect.min.y + positions_diff[0].y, - ), - Vec2::new( - glyph.rect.max.x + positions_diff[1].x, - glyph.rect.min.y + positions_diff[1].y, - ), - Vec2::new( - glyph.rect.max.x + positions_diff[2].x, - glyph.rect.max.y + positions_diff[2].y, - ), - Vec2::new( - glyph.rect.min.x + positions_diff[3].x, - glyph.rect.max.y + positions_diff[3].y, - ), - ] - .map(|pos| pos / atlas_extent); - - for i in 0..4 { - ui_meta.vertices.push(UiVertex { - position: positions_clipped[i].into(), - uv: uvs[i].into(), - color, - flags: shader_flags::TEXTURED | shader_flags::CORNERS[i], - radius: [[0.0; 4]; 2], - border: [0.0; 4], - size: rect_size.into(), - point: [0.0; 2], - }); - } - - for &i in &QUAD_INDICES { - ui_meta.indices.push(indices_index + i as u32); - } - - vertices_index += 6; - indices_index += 4; - } - } } - existing_batch.unwrap().1.range.end = vertices_index; + let existing_batch = existing_batch.unwrap(); + existing_batch.1.range.end = vertices_index; ui_phase.items[batch_item_index].batch_range_mut().end += 1; } } diff --git a/crates/bevy_ui_render/src/render_pass.rs b/crates/bevy_ui_render/src/render_pass.rs index 90ea1d454513b..48793d396062a 100644 --- a/crates/bevy_ui_render/src/render_pass.rs +++ b/crates/bevy_ui_render/src/render_pass.rs @@ -207,7 +207,7 @@ impl RenderCommand

for SetUiTextureBindGroup pub struct DrawUiNode; impl RenderCommand

for DrawUiNode { - type Param = SRes; + type Param = (SRes, SRes); type ViewQuery = (); type ItemQuery = Read; @@ -216,7 +216,7 @@ impl RenderCommand

for DrawUiNode { _item: &P, _view: (), batch: Option<&'w UiBatch>, - ui_meta: SystemParamItem<'w, '_, Self::Param>, + (ui_meta, image_bind_groups): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let Some(batch) = batch else { @@ -237,6 +237,14 @@ impl RenderCommand

for DrawUiNode { indices.slice(..), bevy_render::render_resource::IndexFormat::Uint32, ); + if !batch.texture_ranges.is_empty() { + let image_bind_groups = image_bind_groups.into_inner(); + for (range, image) in &batch.texture_ranges { + pass.set_bind_group(1, image_bind_groups.values.get(image).unwrap(), &[]); + pass.draw_indexed(range.clone(), 0, 0..1); + } + return RenderCommandResult::Success; + } // Draw the vertices pass.draw_indexed(batch.range.clone(), 0, 0..1); RenderCommandResult::Success diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index ea978f7e97037..ff4072e0840b0 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -1,23 +1,36 @@ -use crate::{stack_z_offsets, ExtractedUiNode, ExtractedUiNodes, UiCameraMap}; use bevy_asset::AssetId; -use bevy_camera::visibility::InheritedVisibility; -use bevy_color::{Alpha, LinearRgba}; -use bevy_ecs::entity::EntityIndexMap; +use bevy_color::{Alpha, ColorToComponents, LinearRgba}; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_input_focus::InputFocus; -use bevy_math::{Affine2, Rect, Vec2}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; +use bevy_platform::collections::hash_map::Entry; use bevy_render::{ - sync_world::{MainEntityHashMap, MainEntityHashSet}, + render_asset::RenderAssets, + render_phase::{DrawFunctions, PhaseItem, PhaseItemExtraIndex, ViewSortedRenderPhases}, + render_resource::{BindGroupEntries, PipelineCache, SpecializedRenderPipelines}, + renderer::{RenderDevice, RenderQueue}, + sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}, + texture::GpuImage, + view::{ExtractedView, ViewUniforms}, Extract, }; -use bevy_sprite::BorderRect; -use bevy_text::{EditableText, TextColor, TextCursorStyle, TextLayoutInfo}; +use bevy_text::{ + ComputedTextBlock, EditableText, PositionedGlyph, Strikethrough, StrikethroughColor, + TextBackgroundColor, TextColor, TextCursorStyle, TextLayoutInfo, TextSpan, Underline, + UnderlineColor, +}; use bevy_ui::{ - CalculatedClip, ComputedNode, ComputedStackIndex, ComputedUiTargetCamera, ResolvedBorderRadius, - UiGlobalTransform, + widget::{Text, TextShadow}, + ResolvedBorderRadius, +}; +use core::ops::Range; + +use crate::{ + extract_geometry::ExtractedUiGeometries, shader_flags, stack_z_offsets, DrawUi, + ImageNodeBindGroups, TransparentUi, UiAntiAlias, UiBatch, UiCameraView, UiMeta, UiPipeline, + UiPipelineKey, UiVertex, QUAD_INDICES, QUAD_VERTEX_POSITIONS, }; -use core::range::Range; pub struct ExtractedGlyph { pub color: LinearRgba, @@ -38,95 +51,202 @@ pub struct ExtractedGlyphLayout { pub strike_through: Vec<(Rect, LinearRgba)>, pub underline: Vec<(Rect, LinearRgba)>, pub sections: Vec, + pub backgrounds: Vec<(Rect, LinearRgba)>, + pub selections: Vec<(Rect, ResolvedBorderRadius)>, + pub cursor: Option, + pub focused: bool, + pub viewport_offset: Vec2, + pub clip_to_content_box: bool, } #[derive(Resource, Default)] pub struct ExtractedGlyphLayouts { - pub uinodes: MainEntityHashMap<(Entity, EntityIndexMap)>, + pub uinodes: MainEntityHashMap<(Entity, ExtractedGlyphLayout)>, pub changed: MainEntityHashSet, } -pub fn extract_text_cursor( +pub fn extract_text( mut commands: Commands, - extracted_uinodes: ResMut, - text_node_query: Extract< + mut extracted_glyph_layouts: ResMut, + changed_text_query: Extract< + Query< + Entity, + ( + With, + Or<( + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + )>, + ), + >, + >, + changed_text_span_query: Extract< + Query< + Entity, + ( + With, + Or<( + Changed, + Changed, + Changed, + Changed, + Changed, + Changed, + )>, + ), + >, + >, + text_query: Extract< Query<( Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, + &ComputedTextBlock, + &TextColor, &TextLayoutInfo, - &TextCursorStyle, Option<&EditableText>, + Option<&TextCursorStyle>, + Option<&TextShadow>, + )>, + >, + text_styles: Extract< + Query<( + AnyOf<(&TextBackgroundColor, &Strikethrough, &Underline)>, + &TextColor, + Option<&StrikethroughColor>, + Option<&UnderlineColor>, )>, >, - camera_map: Extract, + text_span_parent_query: Extract>>, + text_root_query: Extract>>, + ( + mut removed_text_query, + mut removed_computed_text_block_query, + mut removed_text_color_query, + mut removed_text_layout_info_query, + mut removed_text_cursor_style_query, + mut removed_text_shadow_query, + mut removed_editable_text_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), + ( + mut removed_text_background_color_query, + mut removed_strikethrough_query, + mut removed_underline_query, + mut removed_strikethrough_color_query, + mut removed_underline_color_query, + ): ( + Extract>, + Extract>, + Extract>, + Extract>, + Extract>, + ), input_focus: Extract>>, + mut nodes_to_extract: Local, ) { - let extracted_uinodes = extracted_uinodes.into_inner(); - let mut camera_mapper = camera_map.get_mapper(); - - for ( - entity, - uinode, - stack_index, - global_transform, - inherited_visibility, - maybe_clip, - target_camera, - text_layout_info, - cursor_style, - editable_text, - ) in extracted_uinodes - .changed + extracted_glyph_layouts.changed.clear(); + nodes_to_extract.clear(); + nodes_to_extract.extend(changed_text_query.iter().map(MainEntity::from)); + if input_focus.as_ref().is_some_and(DetectChanges::is_changed) { + nodes_to_extract.extend(text_root_query.iter().map(MainEntity::from)); + } + nodes_to_extract.extend( + removed_text_query + .read() + .chain(removed_computed_text_block_query.read()) + .chain(removed_text_layout_info_query.read()) + .chain(removed_text_cursor_style_query.read()) + .chain(removed_text_shadow_query.read()) + .chain(removed_editable_text_query.read()) + .map(MainEntity::from), + ); + + for entity in changed_text_span_query .iter() - .flat_map(|main_entity| text_node_query.get(main_entity.entity()).ok()) + .chain(removed_text_color_query.read()) + .chain(removed_text_background_color_query.read()) + .chain(removed_strikethrough_query.read()) + .chain(removed_underline_query.read()) + .chain(removed_strikethrough_color_query.read()) + .chain(removed_underline_color_query.read()) { - // 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() { - continue; + let mut entity = entity; + loop { + if text_root_query.contains(entity) { + nodes_to_extract.insert(entity.into()); + break; + } + match text_span_parent_query.get(entity) { + Ok(parent) => entity = parent.parent(), + Err(_) => break, + } } + } - let Some(extracted_camera_entity) = camera_mapper.map(target_camera) else { - continue; - }; - - let transform = Affine2::from(global_transform) - * Affine2::from_translation( - uinode.content_box().min - - editable_text.map_or(Vec2::ZERO, |editor| editor.viewport.offset), - ); + for main_entity in nodes_to_extract.drain() { + extracted_glyph_layouts.changed.insert(main_entity); - let clip = if editable_text.is_some() { - let content_box = uinode.content_box(); - let text_clip = Rect::from_center_size( - global_transform.affine().translation + content_box.center(), - content_box.size(), - ); - Some(maybe_clip.map_or(text_clip, |clip| clip.clip.intersect(text_clip))) - } else { - maybe_clip.map(|clip| clip.clip) + let Ok(( + entity, + computed_block, + text_color, + text_layout_info, + editable_text, + cursor_style, + shadow, + )) = text_query.get(main_entity.entity()) + else { + let Some((render_entity, _)) = extracted_glyph_layouts.uinodes.remove(&main_entity) + else { + continue; + }; + commands.entity(render_entity).despawn(); + continue; }; - let mut focused = false; - - if let Some(input_focus) = input_focus.as_ref() - && Some(entity) == input_focus.get() - { - focused = true; - } + let has_cursor_style = cursor_style.is_some(); + let cursor_style = cursor_style.copied().unwrap_or_default(); + let (shadow_color, shadow_offset) = shadow + .filter(|shadow| !shadow.color.is_fully_transparent()) + .map_or((LinearRgba::NONE, Vec2::ZERO), |shadow| { + (shadow.color.into(), shadow.offset) + }); + let viewport_offset = + editable_text.map_or(Vec2::ZERO, |editable_text| editable_text.viewport.offset); + let clip_to_content_box = editable_text.is_some(); + let focused = input_focus + .as_ref() + .is_some_and(|input_focus| input_focus.get() == Some(entity)); - let sc = if focused { + let selection_color = if focused { cursor_style.selection_color } else { cursor_style.unfocused_selection_color }; - - if !text_layout_info.selection_rects.is_empty() && !sc.is_fully_transparent() { - let selection_color = sc.to_linear(); + let mut selections = vec![]; + if has_cursor_style + && !text_layout_info.selection_rects.is_empty() + && !selection_color.is_fully_transparent() + { let selection_radius = cursor_style.selection_radius.clamp(0.0, 0.5); for (prev, selection, next) in @@ -138,8 +258,8 @@ pub fn extract_text_cursor( ( i.checked_sub(1) .map(|i| text_layout_info.selection_rects[i]), - current, - text_layout_info.selection_rects.get(i + 1), + *current, + text_layout_info.selection_rects.get(i + 1).copied(), ) }) { @@ -172,162 +292,564 @@ pub fn extract_text_cursor( } } - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_SELECTION, - clip, - image: AssetId::default(), - transform: transform * Affine2::from_translation(selection.center()), - item: ExtractedUiItem::Node { - color: selection_color, - rect: Rect { - min: Vec2::ZERO, - max: selection.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius, - node_type: NodeType::Rect, - }, - }, - ); + selections.push((selection, border_radius)); } } - if let Some((true, cursor_rect)) = text_layout_info.cursor - && !cursor_rect.is_empty() + let mut backgrounds = vec![]; + let mut strike_through = vec![]; + let mut underline = vec![]; + + for run in text_layout_info.run_geometry.iter() { + let Some(section_entity) = computed_block + .entities() + .get(run.section_index as usize) + .map(|text_entity| text_entity.entity) + else { + continue; + }; + let Ok(( + (text_background_color, maybe_strikethrough, maybe_underline), + run_text_color, + maybe_strikethrough_color, + maybe_underline_color, + )) = text_styles.get(section_entity) + else { + continue; + }; + + if let Some(text_background_color) = text_background_color { + backgrounds.push((run.bounds, text_background_color.0.to_linear())); + } + + if maybe_strikethrough.is_some() { + let rect = + Rect::from_center_size(run.strikethrough_position(), run.strikethrough_size()); + strike_through.push(( + rect, + maybe_strikethrough_color + .map(|color| color.0) + .unwrap_or(run_text_color.0) + .to_linear(), + )); + } + + if maybe_underline.is_some() { + let rect = Rect::from_center_size(run.underline_position(), run.underline_size()); + underline.push(( + rect, + maybe_underline_color + .map(|color| color.0) + .unwrap_or(run_text_color.0) + .to_linear(), + )); + } + } + + let mut color = text_color.0.to_linear(); + let selected_text_color = cursor_style + .selected_text_color + .map(|selected_text_color| selected_text_color.to_linear()); + let mut current_section_index = 0; + let mut section_start = 0; + let mut glyphs = vec![]; + let mut sections = vec![]; + + for ( + i, + PositionedGlyph { + position, + atlas_info, + section_index, + .. + }, + ) in text_layout_info.glyphs.iter().enumerate() + { + if current_section_index != *section_index + && let Some(section_entity) = computed_block + .entities() + .get(*section_index as usize) + .map(|text_entity| text_entity.entity) + { + color = text_styles + .get(section_entity) + .map(|(_, text_color, _, _)| LinearRgba::from(text_color.0)) + .unwrap_or_default(); + current_section_index = *section_index; + } + + let color = if !atlas_info.is_alpha_mask { + LinearRgba::WHITE + } else if let Some(selected_text_color) = selected_text_color + && text_layout_info + .selection_rects + .iter() + .any(|selection_rect| { + let glyph_rect = Rect::from_center_size(*position, atlas_info.rect.size()); + selection_rect.contains(glyph_rect.min) + && selection_rect.contains(glyph_rect.max) + }) + { + selected_text_color + } else { + color + }; + + glyphs.push(ExtractedGlyph { + color, + translation: *position, + rect: atlas_info.rect, + }); + + if text_layout_info + .glyphs + .get(i + 1) + .is_none_or(|info| info.atlas_info.texture != atlas_info.texture) + { + let glyph_count = glyphs.len() as u32; + sections.push(Sections { + range: section_start..glyph_count, + atlas_texture: atlas_info.texture, + }); + section_start = glyph_count; + } + } + + underline.extend( + text_layout_info + .preedit_underline_rects + .iter() + .map(|rect| (*rect, text_color.0.to_linear())), + ); + + let cursor = if has_cursor_style + && let Some((true, cursor)) = text_layout_info.cursor + && !cursor.is_empty() && !cursor_style.color.is_fully_transparent() { - extracted_uinodes - .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_CURSOR, - clip, - image: AssetId::default(), - transform: transform * Affine2::from_translation(cursor_rect.center()), - item: ExtractedUiItem::Node { - color: cursor_style.color.to_linear(), - rect: Rect { - min: Vec2::ZERO, - max: cursor_rect.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius: ResolvedBorderRadius::default(), - node_type: NodeType::Rect, - }, - }, - ); + Some(cursor) + } else { + None + }; + + if glyphs.is_empty() + && strike_through.is_empty() + && underline.is_empty() + && backgrounds.is_empty() + && selections.is_empty() + && cursor.is_none() + { + if let Some((render_entity, _)) = extracted_glyph_layouts.uinodes.remove(&main_entity) { + commands.entity(render_entity).despawn(); + } + continue; + } + + let layout = ExtractedGlyphLayout { + shadow_color, + shadow_offset, + cursor_style, + glyphs, + strike_through, + underline, + sections, + backgrounds, + selections, + cursor, + focused, + viewport_offset, + clip_to_content_box, + }; + + match extracted_glyph_layouts.uinodes.entry(entity.into()) { + Entry::Occupied(mut entry) => entry.get_mut().1 = layout, + Entry::Vacant(entry) => { + entry.insert((commands.spawn_empty().id(), layout)); + } } } } -pub fn extract_preedit_underlines( - mut commands: Commands, - extracted_uinodes: ResMut, - text_node_query: Extract< - Query< - ( - Entity, - &ComputedNode, - &TextColor, - &TextLayoutInfo, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - &ComputedStackIndex, - &EditableText, - ), - With, - >, - >, - camera_map: Extract, +pub fn queue_text( + extracted_glyph_layouts: Res, + extracted_geometry: Res, + ui_pipeline: Res, + mut pipelines: ResMut>, + mut transparent_render_phases: ResMut>, + render_views: Query<(&UiCameraView, Option<&UiAntiAlias>), With>, + camera_views: Query<&ExtractedView>, + pipeline_cache: Res, + draw_functions: Res>, ) { - let extracted_uinodes = extracted_uinodes.into_inner(); - let mut camera_mapper = camera_map.get_mapper(); - - for ( - entity, - uinode, - text_color, - text_layout_info, - global_transform, - inherited_visibility, - maybe_clip, - target_camera, - stack_index, - editable_text, - ) in extracted_uinodes - .changed - .iter() - .flat_map(|main_entity| text_node_query.get(main_entity.entity()).ok()) - { - if !inherited_visibility.get() - || uinode.is_empty() - || text_layout_info.preedit_underline_rects.is_empty() - { - continue; - } + let draw_function = draw_functions.read().id::(); - let Some(extracted_camera_entity) = camera_mapper.map(target_camera) else { + for (main_entity, (render_entity, _)) in extracted_glyph_layouts.uinodes.iter() { + let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + continue; + }; + let Some((default_camera_view, ui_anti_alias)) = + render_views.get(geometry.extracted_camera).ok() + else { + continue; + }; + let Some(view) = camera_views.get(default_camera_view.0).ok() else { + continue; + }; + let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) + 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 transform = Affine2::from(global_transform) - * Affine2::from_translation(uinode.content_box().min - editable_text.viewport.offset); + transparent_phase.add_transient(TransparentUi { + draw_function, + pipeline, + entity: (*render_entity, *main_entity), + sort_key: FloatOrd(geometry.stack_index as f32 + stack_z_offsets::TEXT), + batch_range: 0..0, + extra_index: PhaseItemExtraIndex::None, + indexed: true, + }); + } +} - let text_clip = Rect::from_center_size( - global_transform.affine().translation + uinode.content_box().center(), - uinode.content_box().size(), - ); - let clip = Some(maybe_clip.map_or(text_clip, |clip| clip.clip.intersect(text_clip))); +pub fn prepare_text( + mut commands: Commands, + render_device: Res, + render_queue: Res, + pipeline_cache: Res, + mut ui_meta: ResMut, + extracted_glyph_layouts: Res, + extracted_geometry: Res, + view_uniforms: Res, + ui_pipeline: Res, + mut image_bind_groups: ResMut, + gpu_images: Res>, + mut phases: ResMut>, + mut previous_len: Local, +) { + if extracted_glyph_layouts.uinodes.is_empty() || view_uniforms.uniforms.binding().is_none() { + return; + } - let color = text_color.0.to_linear(); + let mut batches = Vec::with_capacity(*previous_len); - for rect in text_layout_info.preedit_underline_rects.iter() { - extracted_uinodes + for ui_phase in phases.values_mut() { + for item_index in 0..ui_phase.items.len() { + let item = &mut ui_phase.items[item_index]; + let Some(layout) = extracted_glyph_layouts .uinodes - .entry(entity.into()) - .or_insert_with(|| (extracted_camera_entity, Default::default())) - .1 - .insert( - commands.spawn_empty().id(), - ExtractedUiNode { - z_order: stack_index.0 as f32 + stack_z_offsets::TEXT_STRIKETHROUGH, - clip, - image: AssetId::default(), - transform: transform * Affine2::from_translation(rect.center()), - item: ExtractedUiItem::Node { - color, - rect: Rect { - min: Vec2::ZERO, - max: rect.size(), - }, - atlas_scaling: None, - flip_x: false, - flip_y: false, - border: BorderRect::default(), - border_radius: ResolvedBorderRadius::default(), - node_type: NodeType::Rect, - }, - }, + .get(&item.main_entity()) + .and_then(|(render_entity, layout)| { + (*render_entity == item.entity()).then_some(layout) + }) + else { + continue; + }; + let Some(geometry) = extracted_geometry + .uinode_geometries + .get(&item.main_entity()) + else { + continue; + }; + let image = layout + .sections + .iter() + .find_map(|section| { + gpu_images + .get(section.atlas_texture) + .map(|_| section.atlas_texture) + }) + .unwrap_or_default(); + let Some(gpu_image) = gpu_images.get(image) else { + continue; + }; + + image_bind_groups.values.entry(image).or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential((&gpu_image.texture_view, &gpu_image.sampler)), + ) + }); + + let uinode = &geometry.computed_node; + let shadow_offset = layout.shadow_offset / uinode.inverse_scale_factor(); + let transform = geometry.transform + * Affine2::from_translation(uinode.content_box().min - layout.viewport_offset); + let clip = if layout.clip_to_content_box { + let content_box = uinode.content_box(); + let text_clip = Rect::from_center_size( + geometry.transform.translation + content_box.center(), + content_box.size(), ); + Some( + geometry + .clip + .map_or(text_clip, |clip| clip.intersect(text_clip)), + ) + } else { + geometry.clip + }; + + let mut quads: Vec<( + Rect, + LinearRgba, + ResolvedBorderRadius, + Option<(Rect, AssetId)>, + )> = Vec::with_capacity( + layout.selections.len() + + layout.backgrounds.len() + + layout.glyphs.len() + * if layout.shadow_color.is_fully_transparent() { + 1 + } else { + 2 + } + + layout.strike_through.len() + * if layout.shadow_color.is_fully_transparent() { + 1 + } else { + 2 + } + + layout.underline.len() + * if layout.shadow_color.is_fully_transparent() { + 1 + } else { + 2 + } + + usize::from(layout.cursor.is_some()), + ); + let selection_color = if layout.focused { + layout.cursor_style.selection_color + } else { + layout.cursor_style.unfocused_selection_color + } + .to_linear(); + quads.extend( + layout + .selections + .iter() + .map(|(rect, radius)| (*rect, selection_color, *radius, None)), + ); + quads.extend( + layout + .backgrounds + .iter() + .map(|(rect, color)| (*rect, *color, ResolvedBorderRadius::ZERO, None)), + ); + + if !layout.shadow_color.is_fully_transparent() { + for section in &layout.sections { + quads.extend( + layout.glyphs[section.range.start as usize..section.range.end as usize] + .iter() + .map(|glyph| { + ( + Rect::from_center_size( + glyph.translation + shadow_offset, + glyph.rect.size(), + ), + layout.shadow_color, + ResolvedBorderRadius::ZERO, + Some((glyph.rect, section.atlas_texture)), + ) + }), + ); + } + quads.extend( + layout + .strike_through + .iter() + .chain(layout.underline.iter()) + .map(|(rect, _)| { + ( + Rect::from_center_size(rect.center() + shadow_offset, rect.size()), + layout.shadow_color, + ResolvedBorderRadius::ZERO, + None, + ) + }), + ); + } + + for section in &layout.sections { + quads.extend( + layout.glyphs[section.range.start as usize..section.range.end as usize] + .iter() + .map(|glyph| { + ( + Rect::from_center_size(glyph.translation, glyph.rect.size()), + glyph.color, + ResolvedBorderRadius::ZERO, + Some((glyph.rect, section.atlas_texture)), + ) + }), + ); + } + quads.extend( + layout + .strike_through + .iter() + .chain(layout.underline.iter()) + .map(|(rect, color)| (*rect, *color, ResolvedBorderRadius::ZERO, None)), + ); + if let Some(cursor) = layout.cursor { + quads.push(( + cursor, + layout.cursor_style.color.to_linear(), + ResolvedBorderRadius::ZERO, + None, + )); + } + + let range_start = ui_meta.indices.len() as u32; + let mut indices_index = range_start; + let mut vertices_index = ui_meta.vertices.len() as u32; + let mut texture_range_start = range_start; + let mut current_texture = image; + let mut texture_ranges = vec![]; + + for (rect, color, border_radius, glyph) in quads { + let rect_size = rect.size(); + let rect_transform = transform * Affine2::from_translation(rect.center()); + let positions = QUAD_VERTEX_POSITIONS + .map(|pos| rect_transform.transform_point2(pos * rect_size).extend(0.)); + let positions_diff = if let Some(clip) = clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = rect_transform.transform_vector2(rect_size).abs(); + if rect_transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + continue; + } + + let (uvs, flags) = if let Some((glyph_rect, atlas_texture)) = glyph { + let Some(gpu_image) = gpu_images.get(atlas_texture) else { + continue; + }; + if current_texture != atlas_texture { + if texture_range_start < indices_index { + texture_ranges + .push((texture_range_start..indices_index, current_texture)); + } + texture_range_start = indices_index; + current_texture = atlas_texture; + } + image_bind_groups + .values + .entry(atlas_texture) + .or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + let atlas_extent = gpu_image.size_2d().as_vec2(); + ( + [ + Vec2::new( + glyph_rect.min.x + positions_diff[0].x, + glyph_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + glyph_rect.max.x + positions_diff[1].x, + glyph_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + glyph_rect.max.x + positions_diff[2].x, + glyph_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + glyph_rect.min.x + positions_diff[3].x, + glyph_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent), + shader_flags::TEXTURED, + ) + } else { + ( + [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y], + shader_flags::UNTEXTURED, + ) + }; + + let color = color.to_f32_array(); + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + flags: flags | shader_flags::CORNERS[i], + radius: border_radius.into(), + border: [0.; 4], + size: rect_size.into(), + point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), + }); + } + for &i in &QUAD_INDICES { + ui_meta.indices.push(vertices_index + i as u32); + } + indices_index += 6; + vertices_index += 4; + } + + if texture_range_start < indices_index { + texture_ranges.push((texture_range_start..indices_index, current_texture)); + } + item.batch_range = item_index as u32..item_index as u32 + 1; + batches.push(( + item.entity(), + UiBatch { + range: range_start..indices_index, + image, + texture_ranges, + }, + )); } } + + ui_meta.vertices.write_buffer(&render_device, &render_queue); + ui_meta.indices.write_buffer(&render_device, &render_queue); + *previous_len = batches.len(); + commands.try_insert_batch(batches); } From 1289f4222bb80c21c939fb6d3f46fd7afc102f77 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 19:21:39 +0100 Subject: [PATCH 28/99] Renamings geometry to layout --- crates/bevy_ui_render/src/box_shadow.rs | 12 +++--- ...{extract_geometry.rs => extract_layout.rs} | 37 ++++++++----------- crates/bevy_ui_render/src/gradient.rs | 10 ++--- crates/bevy_ui_render/src/lib.rs | 21 +++++------ crates/bevy_ui_render/src/text.rs | 16 ++++---- .../src/ui_material_pipeline.rs | 20 +++++----- .../src/ui_texture_slice_pipeline.rs | 16 ++++---- 7 files changed, 62 insertions(+), 70 deletions(-) rename crates/bevy_ui_render/src/{extract_geometry.rs => extract_layout.rs} (90%) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index f6d9c8f56c70e..ba7877c88bd93 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -30,7 +30,7 @@ use bevy_ui::{BoxShadow, ComputedUiRenderTargetInfo, ResolvedBorderRadius, Shado use bevy_utils::default; use bytemuck::{Pod, Zeroable}; -use crate::{BoxShadowSamples, ExtractedUiGeometries, RenderUiSystems, TransparentUi}; +use crate::{BoxShadowSamples, ExtractedUiLayout, RenderUiSystems, TransparentUi}; use super::{stack_z_offsets, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; @@ -271,7 +271,7 @@ pub fn extract_shadows( )] pub fn queue_shadows( extracted_box_shadows: Res, - extracted_geometry: Res, + extracted_geometry: Res, box_shadow_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -285,7 +285,7 @@ pub fn queue_shadows( let mut current_phase = None; for (main_entity, extracted_sub_shadows) in extracted_box_shadows.box_shadows.iter() { - let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + let Some(geometry) = extracted_geometry.layout.get(main_entity) else { continue; }; @@ -340,7 +340,7 @@ pub fn prepare_shadows( pipeline_cache: Res, mut ui_meta: ResMut, extracted_shadows: Res, - extracted_geometry: Res, + extracted_geometry: Res, view_uniforms: Res, box_shadow_pipeline: Res, mut phases: ResMut>, @@ -372,13 +372,13 @@ pub fn prepare_shadows( continue; }; let Some(geometry) = extracted_geometry - .uinode_geometries + .layout .get(&item.main_entity()) else { continue; }; - let uinode = &geometry.computed_node; + let uinode = &geometry.uinode; if uinode.is_empty() { continue; } diff --git a/crates/bevy_ui_render/src/extract_geometry.rs b/crates/bevy_ui_render/src/extract_layout.rs similarity index 90% rename from crates/bevy_ui_render/src/extract_geometry.rs rename to crates/bevy_ui_render/src/extract_layout.rs index b2c2728363411..52f552f91fc7f 100644 --- a/crates/bevy_ui_render/src/extract_geometry.rs +++ b/crates/bevy_ui_render/src/extract_layout.rs @@ -19,9 +19,9 @@ use bevy_ui::ComputedStackIndex; use bevy_ui::ComputedUiTargetCamera; use bevy_ui::UiGlobalTransform; -pub struct ExtractedUiNodeGeometry { +pub struct ExtractedUiNodeLayout { pub extracted_camera: Entity, - pub computed_node: ComputedNode, + pub uinode: ComputedNode, pub transform: Affine2, pub clip: Option, pub atlas_scaling: Option, @@ -32,18 +32,18 @@ pub struct ExtractedUiNodeGeometry { /// Uinode geometries and list of geometries that changed this frame. #[derive(Resource, Default)] -pub struct ExtractedUiGeometries { +pub struct ExtractedUiLayout { pub extracted_camera_to_main_uinode_map: EntityHashMap, /// Map from main world entity -> node geometry - pub uinode_geometries: MainEntityHashMap, + pub layout: MainEntityHashMap, /// Main world entities with UI node geometry that changed this frame. pub changed: MainEntityHashSet, /// Main world entities that were removed from rendering pub removed: MainEntityHashSet, } -pub fn extract_uinode_geometry( - mut extracted_geometries: bevy_ecs::system::ResMut, +pub fn extract_ui_layout( + mut extracted_geometries: bevy_ecs::system::ResMut, camera_map: Extract, changed_geometry_query: Extract< Query< @@ -122,14 +122,14 @@ pub fn extract_uinode_geometry( // Find extracted camera entity, otherwise remove both camera and UI node entity. let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else { - extracted_geometries.uinode_geometries.remove(&main_entity); + extracted_geometries.layout.remove(&main_entity); extracted_geometries.removed.insert(main_entity); continue; }; // Remove non-visible from all maps if !inherited_visibility.get() { - extracted_geometries.uinode_geometries.remove(&main_entity); + extracted_geometries.layout.remove(&main_entity); extracted_geometries.removed.insert(main_entity); extracted_geometries .extracted_camera_to_main_uinode_map @@ -138,9 +138,9 @@ pub fn extract_uinode_geometry( continue; } - let uinode_geometry = ExtractedUiNodeGeometry { + let uinode_geometry = ExtractedUiNodeLayout { extracted_camera: extracted_camera_entity, - computed_node: *computed_node, + uinode: *computed_node, transform: transform.into(), clip: clip.map(|clip| clip.clip), atlas_scaling: None, @@ -149,9 +149,9 @@ pub fn extract_uinode_geometry( stack_index: stack_index.0, }; - let ExtractedUiGeometries { + let ExtractedUiLayout { extracted_camera_to_main_uinode_map, - uinode_geometries, + layout: uinode_geometries, .. } = &mut *extracted_geometries; @@ -199,9 +199,7 @@ pub fn extract_uinode_geometry( if extracted_geometries.changed.insert(main_entity) { // Failed the geometry queries, does not have at least one of the components (if reinserted would be `Changed`). // Add to changed list and remove from maps - if let Some(extracted_geometry) = - extracted_geometries.uinode_geometries.remove(&main_entity) - { + if let Some(extracted_geometry) = extracted_geometries.layout.remove(&main_entity) { extracted_geometries.removed.insert(main_entity); extracted_geometries .extracted_camera_to_main_uinode_map @@ -213,7 +211,7 @@ pub fn extract_uinode_geometry( } pub fn extract_uinode_geometry_debug( - extracted_geometries: bevy_ecs::system::Res, + extracted_geometries: bevy_ecs::system::Res, ) { if 0 < extracted_geometries.changed.len() { println!("\n"); @@ -223,14 +221,11 @@ pub fn extract_uinode_geometry_debug( .extracted_camera_to_main_uinode_map .len() ); - println!( - "node count: {}", - extracted_geometries.uinode_geometries.len() - ); + println!("node count: {}", extracted_geometries.layout.len()); println!("removed count: {}", extracted_geometries.removed.len()); println!("changed count: {}", extracted_geometries.changed.len()); - for (e, g) in &extracted_geometries.uinode_geometries { + for (e, g) in &extracted_geometries.layout { if let Some(es) = extracted_geometries .extracted_camera_to_main_uinode_map .get(&g.extracted_camera) diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index f9ee8fc495bcc..bcceaa88af313 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -420,7 +420,7 @@ pub fn extract_gradients( )] pub fn queue_gradient( extracted_gradients: Res, - extracted_geometry: Res, + extracted_geometry: Res, gradients_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -434,7 +434,7 @@ pub fn queue_gradient( let mut current_phase = None; for (main_entity, sub_gradients) in extracted_gradients.items.iter() { - let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + let Some(geometry) = extracted_geometry.layout.get(main_entity) else { continue; }; @@ -549,7 +549,7 @@ pub fn prepare_gradient( pipeline_cache: Res, mut ui_meta: ResMut, extracted_gradients: Res, - extracted_geometry: Res, + extracted_geometry: Res, view_uniforms: Res, gradients_pipeline: Res, mut phases: ResMut>, @@ -580,12 +580,12 @@ pub fn prepare_gradient( .and_then(|subgradients| subgradients.get(&item.entity())) { let Some(geometry) = extracted_geometry - .uinode_geometries + .layout .get(&item.main_entity()) else { continue; }; - let uinode = &geometry.computed_node; + let uinode = &geometry.uinode; if uinode.is_empty() { continue; } diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index c24921db9ce3f..4acc1232a1bdf 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -19,7 +19,7 @@ pub mod ui_texture_slice_pipeline; #[cfg(feature = "bevy_ui_debug")] mod debug_overlay; -pub mod extract_geometry; +pub mod extract_layout; use bevy_a11y::AccessibilitySystems; use bevy_camera::{Camera, Camera2d, Camera3d, RenderTarget}; @@ -77,7 +77,7 @@ pub use render_pass::*; pub use ui_material_pipeline::*; use ui_texture_slice_pipeline::UiTextureSlicerPlugin; -use crate::extract_geometry::ExtractedUiGeometries; +use crate::extract_layout::ExtractedUiLayout; use crate::shader_flags::INVERT; use crate::text::{extract_text, prepare_text, queue_text, ExtractedGlyphLayouts}; @@ -223,7 +223,7 @@ impl Plugin for UiRenderPlugin { .init_gpu_resource::>() .init_gpu_resource::() .init_gpu_resource::() - .init_resource::() + .init_resource::() .init_resource::() .allow_ambiguous_resource::() .init_resource::() @@ -254,7 +254,7 @@ impl Plugin for UiRenderPlugin { .add_systems( ExtractSchedule, ( - extract_geometry::extract_uinode_geometry, + extract_layout::extract_ui_layout, extract_ui_camera_view .after(extract_cameras) .in_set(RenderUiSystems::ExtractCameraViews), @@ -854,7 +854,7 @@ pub mod shader_flags { pub fn queue_uinodes( extracted_uinodes: Res, - extracted_geometry: Res, + extracted_geometry: Res, ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -868,7 +868,7 @@ pub fn queue_uinodes( let mut current_phase = None; for (main_entity, (render_items, _)) in extracted_uinodes.uinodes.iter() { - let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + let Some(geometry) = extracted_geometry.layout.get(main_entity) else { continue; }; @@ -934,7 +934,7 @@ pub fn prepare_uinodes( pipeline_cache: Res, mut ui_meta: ResMut, extracted_uinodes: Res, - extracted_geometry: Res, + extracted_geometry: Res, view_uniforms: Res, ui_pipeline: Res, mut image_bind_groups: ResMut, @@ -988,10 +988,7 @@ pub fn prepare_uinodes( batch_image_handle = None; continue; } - let Some(geometry) = extracted_geometry - .uinode_geometries - .get(&item.main_entity()) - else { + let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) else { batch_image_handle = None; continue; }; @@ -1065,7 +1062,7 @@ pub fn prepare_uinodes( } } if let Some((extracted_uinode, stack_z_offset)) = extracted_uinode { - let uinode = &geometry.computed_node; + let uinode = &geometry.uinode; let mut nodes = vec![]; if stack_z_offset == stack_z_offsets::BACKGROUND_COLOR { diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index ff4072e0840b0..4222217a01c1c 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -27,9 +27,9 @@ use bevy_ui::{ use core::ops::Range; use crate::{ - extract_geometry::ExtractedUiGeometries, shader_flags, stack_z_offsets, DrawUi, - ImageNodeBindGroups, TransparentUi, UiAntiAlias, UiBatch, UiCameraView, UiMeta, UiPipeline, - UiPipelineKey, UiVertex, QUAD_INDICES, QUAD_VERTEX_POSITIONS, + extract_layout::ExtractedUiLayout, shader_flags, stack_z_offsets, DrawUi, ImageNodeBindGroups, + TransparentUi, UiAntiAlias, UiBatch, UiCameraView, UiMeta, UiPipeline, UiPipelineKey, UiVertex, + QUAD_INDICES, QUAD_VERTEX_POSITIONS, }; pub struct ExtractedGlyph { @@ -472,7 +472,7 @@ pub fn extract_text( pub fn queue_text( extracted_glyph_layouts: Res, - extracted_geometry: Res, + extracted_geometry: Res, ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -484,7 +484,7 @@ pub fn queue_text( let draw_function = draw_functions.read().id::(); for (main_entity, (render_entity, _)) in extracted_glyph_layouts.uinodes.iter() { - let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + let Some(geometry) = extracted_geometry.layout.get(main_entity) else { continue; }; let Some((default_camera_view, ui_anti_alias)) = @@ -527,7 +527,7 @@ pub fn prepare_text( pipeline_cache: Res, mut ui_meta: ResMut, extracted_glyph_layouts: Res, - extracted_geometry: Res, + extracted_geometry: Res, view_uniforms: Res, ui_pipeline: Res, mut image_bind_groups: ResMut, @@ -554,7 +554,7 @@ pub fn prepare_text( continue; }; let Some(geometry) = extracted_geometry - .uinode_geometries + .layout .get(&item.main_entity()) else { continue; @@ -580,7 +580,7 @@ pub fn prepare_text( ) }); - let uinode = &geometry.computed_node; + let uinode = &geometry.uinode; let shadow_offset = layout.shadow_offset / uinode.inverse_scale_factor(); let transform = geometry.transform * Affine2::from_translation(uinode.content_box().min - layout.viewport_offset); diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 2120a3dd30a2d..0855b1700a90d 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -382,7 +382,7 @@ pub fn prepare_uimaterial_nodes( pipeline_cache: Res, mut ui_meta: ResMut>, extracted_materials: Res>, - extracted_geometry: Res, + extracted_geometry: Res, view_uniforms: Res, globals_buffer: Res, ui_material_pipeline: Res>, @@ -413,7 +413,7 @@ pub fn prepare_uimaterial_nodes( extracted_materials.uinodes.get(&item.main_entity()) && *render_entity == item.entity() && let Some(geometry) = extracted_geometry - .uinode_geometries + .layout .get(&item.main_entity()) { // Initialize the batch range to be zero-length initially. @@ -440,7 +440,7 @@ pub fn prepare_uimaterial_nodes( let uinode_rect = Rect { min: Vec2::ZERO, - max: geometry.computed_node.size(), + max: geometry.uinode.size(), }; let rect_size = uinode_rect.size(); @@ -524,12 +524,12 @@ pub fn prepare_uimaterial_nodes( position: positions_clipped[i].into(), uv: uvs[i].into(), size: uinode_rect.size().into(), - radius: geometry.computed_node.border_radius.into(), + radius: geometry.uinode.border_radius.into(), border: [ - geometry.computed_node.border.min_inset.x, - geometry.computed_node.border.min_inset.y, - geometry.computed_node.border.max_inset.x, - geometry.computed_node.border.max_inset.y, + geometry.uinode.border.min_inset.x, + geometry.uinode.border.min_inset.y, + geometry.uinode.border.max_inset.x, + geometry.uinode.border.max_inset.y, ], }); } @@ -594,7 +594,7 @@ impl RenderAsset for PreparedUiMaterial { pub fn queue_ui_material_nodes( extracted_uinodes: Res>, - extracted_geometry: Res, + extracted_geometry: Res, draw_functions: Res>, ui_material_pipeline: Res>, mut pipelines: ResMut>>, @@ -611,7 +611,7 @@ pub fn queue_ui_material_nodes( let mut current_phase = None; for (main_entity, (render_entity, extracted_uinode)) in extracted_uinodes.uinodes.iter() { - let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + let Some(geometry) = extracted_geometry.layout.get(main_entity) else { continue; }; let extracted_camera_entity = geometry.extracted_camera; 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 b1b6a1b53e2e4..ee215451048d4 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -319,7 +319,7 @@ pub fn extract_ui_texture_slices( )] pub fn queue_ui_slices( extracted_ui_slicers: Res, - extracted_geometry: Res, + extracted_geometry: Res, ui_slicer_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -333,7 +333,7 @@ pub fn queue_ui_slices( let mut current_phase = None; for (main_entity, (render_entity, _)) in extracted_ui_slicers.slices.iter() { - let Some(geometry) = extracted_geometry.uinode_geometries.get(main_entity) else { + let Some(geometry) = extracted_geometry.layout.get(main_entity) else { continue; }; let extracted_camera_entity = geometry.extracted_camera; @@ -387,7 +387,7 @@ pub fn prepare_ui_slices( pipeline_cache: Res, mut ui_meta: ResMut, extracted_slices: Res, - extracted_geometry: Res, + extracted_geometry: Res, view_uniforms: Res, texture_slicer_pipeline: Res, mut image_bind_groups: ResMut, @@ -435,13 +435,13 @@ pub fn prepare_ui_slices( extracted_slices.slices.get(&item.main_entity()) && *render_entity == item.entity() && let Some(geometry) = extracted_geometry - .uinode_geometries + .layout .get(&item.main_entity()) { let visual_box = match texture_slices.visual_box { - VisualBox::ContentBox => geometry.computed_node.content_box(), - VisualBox::PaddingBox => geometry.computed_node.padding_box(), - VisualBox::BorderBox => geometry.computed_node.border_box(), + VisualBox::ContentBox => geometry.uinode.content_box(), + VisualBox::PaddingBox => geometry.uinode.padding_box(), + VisualBox::BorderBox => geometry.uinode.border_box(), }; if visual_box.size().cmple(Vec2::ZERO).any() { batch_image_handle = None; @@ -639,7 +639,7 @@ pub fn prepare_ui_slices( let [slices, border, repeat] = compute_texture_slices( image_size, - uinode_rect.size() * geometry.computed_node.inverse_scale_factor, + uinode_rect.size() * geometry.uinode.inverse_scale_factor, &texture_slices.image_scale_mode, ); From 80f96e1d2cbfd755bb848e13c18046c28392119a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 19:24:58 +0100 Subject: [PATCH 29/99] Fix uinode query, removed AnyOf --- crates/bevy_ui_render/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 4acc1232a1bdf..25f0fe746222d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -397,7 +397,8 @@ pub fn extract_uinode_background_colors( Option<&OuterColor>, Option<&ImageNode>, Option<&ImageNodeSize>, - AnyOf<(&BorderColor, &Outline)>, + Option<&BorderColor>, + Option<&Outline>, Option<&ViewportNode>, )>, >, @@ -444,7 +445,8 @@ pub fn extract_uinode_background_colors( maybe_outer_color, maybe_image, maybe_image_size, - (maybe_border_color, maybe_outline), + maybe_border_color, + maybe_outline, maybe_viewport_node, )) = uinode_query.get(main_entity.entity()) else { From 330893c609c41ded900946394a9d4c2dda37bef2 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 19:51:59 +0100 Subject: [PATCH 30/99] more tweaks --- crates/bevy_ui_render/src/box_shadow.rs | 5 +- crates/bevy_ui_render/src/gradient.rs | 5 +- crates/bevy_ui_render/src/lib.rs | 83 ++++++++++++------- crates/bevy_ui_render/src/text.rs | 5 +- .../src/ui_material_pipeline.rs | 4 +- .../src/ui_texture_slice_pipeline.rs | 4 +- 6 files changed, 59 insertions(+), 47 deletions(-) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index ba7877c88bd93..161eabac3b613 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -371,10 +371,7 @@ pub fn prepare_shadows( else { continue; }; - let Some(geometry) = extracted_geometry - .layout - .get(&item.main_entity()) - else { + let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) else { continue; }; diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index bcceaa88af313..f5f0f01b20606 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -579,10 +579,7 @@ pub fn prepare_gradient( .get(&item.main_entity()) .and_then(|subgradients| subgradients.get(&item.entity())) { - let Some(geometry) = extracted_geometry - .layout - .get(&item.main_entity()) - else { + let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) else { continue; }; let uinode = &geometry.uinode; diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 25f0fe746222d..1b050a3d99fca 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -376,9 +376,18 @@ pub fn extract_uinode_background_colors( mut extracted_uinodes: ResMut, texture_atlases: Extract>>, camera_query: Extract>, - changed_uinodes_query: Extract< + changed_uinode_query: Extract< Query< - Entity, + ( + Entity, + &BackgroundColor, + Option<&OuterColor>, + Option<&ImageNode>, + Option<&ImageNodeSize>, + Option<&BorderColor>, + Option<&Outline>, + Option<&ViewportNode>, + ), Or<( Changed, Changed, @@ -419,14 +428,13 @@ pub fn extract_uinode_background_colors( Extract>, Extract>, ), - mut nodes_to_extract: Local, + mut removed_uinodes: Local, ) { extracted_uinodes.changed.clear(); - nodes_to_extract.clear(); - nodes_to_extract.extend( - changed_uinodes_query - .iter() - .chain(removed_background_color_query.read()) + removed_uinodes.clear(); + removed_uinodes.extend( + removed_background_color_query + .read() .chain(removed_outer_color_query.read()) .chain(removed_image_node_query.read()) .chain(removed_image_node_size_query.read()) @@ -436,28 +444,32 @@ pub fn extract_uinode_background_colors( .map(MainEntity::from), ); - for main_entity in nodes_to_extract.drain() { - extracted_uinodes.changed.insert(main_entity); - - let Ok(( - entity, - background_color, - maybe_outer_color, - maybe_image, - maybe_image_size, - maybe_border_color, - maybe_outline, - maybe_viewport_node, - )) = uinode_query.get(main_entity.entity()) - else { - let Some((mut render_items, _)) = extracted_uinodes.uinodes.remove(&main_entity) else { - continue; - }; - for (render_entity, _) in render_items.drain(..) { - commands.entity(render_entity).despawn(); - } + for ( + entity, + background_color, + maybe_outer_color, + maybe_image, + maybe_image_size, + maybe_border_color, + maybe_outline, + maybe_viewport_node, + ) in changed_uinode_query.iter().chain( + uinode_query.iter_many( + 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()), + ), + ) { + let main_entity = entity.into(); + + // Already updated this entity + if !extracted_uinodes.changed.insert(main_entity) { continue; - }; + } let mut image_color = LinearRgba::NONE; let mut image_asset = None; @@ -594,6 +606,19 @@ pub fn extract_uinode_background_colors( extracted_uinodes.uinodes.remove(&main_entity); } } + + for main_entity in removed_uinodes.drain() { + if uinode_query.contains(main_entity.entity()) { + continue; + } + extracted_uinodes.changed.insert(main_entity); + let Some((mut render_items, _)) = extracted_uinodes.uinodes.remove(&main_entity) else { + continue; + }; + for (render_entity, _) in render_items.drain(..) { + commands.entity(render_entity).despawn(); + } + } } /// The UI camera is "moved back" by this many units (plus the [`UI_CAMERA_TRANSFORM_OFFSET`]) and also has a view diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index 4222217a01c1c..231c2ebd402a6 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -553,10 +553,7 @@ pub fn prepare_text( else { continue; }; - let Some(geometry) = extracted_geometry - .layout - .get(&item.main_entity()) - else { + let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) else { continue; }; let image = layout diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 0855b1700a90d..f92a7ab40d6ab 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -412,9 +412,7 @@ pub fn prepare_uimaterial_nodes( if let Some((render_entity, extracted_uinode)) = extracted_materials.uinodes.get(&item.main_entity()) && *render_entity == item.entity() - && let Some(geometry) = extracted_geometry - .layout - .get(&item.main_entity()) + && let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) { // Initialize the batch range to be zero-length initially. // We'll extend it as we accumulate items into this batch. 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 ee215451048d4..6847ee47d372d 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -434,9 +434,7 @@ pub fn prepare_ui_slices( if let Some((render_entity, texture_slices)) = extracted_slices.slices.get(&item.main_entity()) && *render_entity == item.entity() - && let Some(geometry) = extracted_geometry - .layout - .get(&item.main_entity()) + && let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) { let visual_box = match texture_slices.visual_box { VisualBox::ContentBox => geometry.uinode.content_box(), From be8ca66c060d7997cee82ddcc8fe9d91141f1bbf Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 22:28:08 +0100 Subject: [PATCH 31/99] fixed double UiMeta writes --- crates/bevy_ui_render/src/lib.rs | 5 +---- crates/bevy_ui_render/src/text.rs | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 1b050a3d99fca..93c5251889126 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -53,7 +53,7 @@ use bevy_render::{ ViewSortedRenderPhases, }, render_resource::*, - renderer::{RenderDevice, RenderQueue}, + renderer::RenderDevice, sync_world::{MainEntity, RenderEntity}, texture::GpuImage, view::{ExtractedView, RetainedViewEntity, ViewUniforms}, @@ -957,7 +957,6 @@ pub struct ImageNodeBindGroups { pub fn prepare_uinodes( mut commands: Commands, render_device: Res, - render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut, extracted_uinodes: Res, @@ -1403,8 +1402,6 @@ pub fn prepare_uinodes( } } - ui_meta.vertices.write_buffer(&render_device, &render_queue); - ui_meta.indices.write_buffer(&render_device, &render_queue); *previous_len = batches.len(); commands.try_insert_batch(batches); } diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index 231c2ebd402a6..73cdb43cc80c9 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -535,7 +535,7 @@ pub fn prepare_text( mut phases: ResMut>, mut previous_len: Local, ) { - if extracted_glyph_layouts.uinodes.is_empty() || view_uniforms.uniforms.binding().is_none() { + if view_uniforms.uniforms.binding().is_none() { return; } From 3b2a37965d67ff18a59423562c822749b8c0436e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 22:32:34 +0100 Subject: [PATCH 32/99] Remove unused render system sets --- crates/bevy_ui_render/src/lib.rs | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 93c5251889126..695a388ccd490 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -120,18 +120,11 @@ pub mod stack_z_offsets { #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum RenderUiSystems { - ExtractChanges, ExtractCameraViews, ExtractBoxShadows, ExtractBackgrounds, - ExtractImages, ExtractTextureSlice, - ExtractBorders, - ExtractViewportNodes, - ExtractTextBackgrounds, - ExtractTextShadows, ExtractText, - ExtractCursor, ExtractDebug, ExtractGradient, } @@ -234,18 +227,11 @@ impl Plugin for UiRenderPlugin { .configure_sets( ExtractSchedule, ( - RenderUiSystems::ExtractChanges, RenderUiSystems::ExtractCameraViews, RenderUiSystems::ExtractBoxShadows, RenderUiSystems::ExtractBackgrounds, - RenderUiSystems::ExtractViewportNodes, - RenderUiSystems::ExtractImages, RenderUiSystems::ExtractTextureSlice, - RenderUiSystems::ExtractBorders, - RenderUiSystems::ExtractTextBackgrounds, - RenderUiSystems::ExtractTextShadows, RenderUiSystems::ExtractText, - RenderUiSystems::ExtractCursor, RenderUiSystems::ExtractDebug, ) .chain_weak(), From 967b0494808b972e2062ce45d90840feaca2ed9c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 22:44:49 +0100 Subject: [PATCH 33/99] fix removed cleared --- crates/bevy_ui_render/src/lib.rs | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 695a388ccd490..2032057f4a44d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -23,7 +23,7 @@ pub mod extract_layout; use bevy_a11y::AccessibilitySystems; use bevy_camera::{Camera, Camera2d, Camera3d, RenderTarget}; -use bevy_ecs::entity::EntityIndexMap; +use bevy_ecs::entity::{EntityHashSet, EntityIndexMap}; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::camera::{extract_cameras, CameraMainPassTextureFormats}; @@ -414,7 +414,7 @@ pub fn extract_uinode_background_colors( Extract>, Extract>, ), - mut removed_uinodes: Local, + mut removed_uinodes: Local, ) { extracted_uinodes.changed.clear(); removed_uinodes.clear(); @@ -426,8 +426,7 @@ pub fn extract_uinode_background_colors( .chain(removed_image_node_size_query.read()) .chain(removed_border_color_query.read()) .chain(removed_outline_query.read()) - .chain(removed_viewport_node_query.read()) - .map(MainEntity::from), + .chain(removed_viewport_node_query.read()), ); for ( @@ -439,17 +438,10 @@ pub fn extract_uinode_background_colors( maybe_border_color, maybe_outline, maybe_viewport_node, - ) in changed_uinode_query.iter().chain( - uinode_query.iter_many( - 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()), - ), - ) { + ) in changed_uinode_query + .iter() + .chain(uinode_query.iter_many(removed_uinodes.iter())) + { let main_entity = entity.into(); // Already updated this entity @@ -593,7 +585,8 @@ pub fn extract_uinode_background_colors( } } - for main_entity in removed_uinodes.drain() { + for entity in removed_uinodes.drain() { + let main_entity = MainEntity::from(entity); if uinode_query.contains(main_entity.entity()) { continue; } From 90e8d57a076a2e5c8b27d31eca5a0c7916f66080 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Wed, 29 Jul 2026 23:27:36 +0100 Subject: [PATCH 34/99] fixed gradients bugs missing removals --- crates/bevy_ui_render/src/gradient.rs | 38 ++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index f5f0f01b20606..9cc8ca6f716db 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -324,7 +324,7 @@ fn compute_color_stops( pub fn extract_gradients( mut commands: Commands, mut extracted_gradients: ResMut, - gradients_query: Extract< + changed_gradients_query: Extract< Query< ( Entity, @@ -338,6 +338,13 @@ pub fn extract_gradients( )>, >, >, + gradients_query: Extract< + Query<( + Entity, + &ComputedUiRenderTargetInfo, + AnyOf<(&BackgroundGradient, &BorderGradient)>, + )>, + >, ( mut removed_computed_ui_render_target_info_query, mut removed_background_gradient_query, @@ -347,13 +354,22 @@ pub fn extract_gradients( Extract>, Extract>, ), - mut nodes_processed_this_frame: Local, + mut removed_gradients: Local, ) { - nodes_processed_this_frame.clear(); + removed_gradients.clear(); + removed_gradients.extend( + removed_computed_ui_render_target_info_query + .read() + .chain(removed_background_gradient_query.read()) + .chain(removed_border_gradient_query.read()) + .map(MainEntity::from), + ); - for (entity, target, (gradient, gradient_border)) in &gradients_query { + for (entity, target, (gradient, gradient_border)) in changed_gradients_query + .iter() + .chain(gradients_query.iter_many(removed_gradients.iter().map(ContainsEntity::entity))) + { let main_entity = MainEntity::from(entity); - nodes_processed_this_frame.insert(main_entity); let gradients = extracted_gradients.items.entry(main_entity).or_default(); let mut gradient_count = 0; @@ -393,16 +409,8 @@ pub fn extract_gradients( } } - // 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_ui_render_target_info_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) { + for main_entity in removed_gradients.drain() { + if gradients_query.contains(main_entity.entity()) { continue; } let Some(mut extracted_nodes) = extracted_gradients.items.remove(&main_entity) else { From 4e002cdd50cb21ccf60403b66cf927563992918e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 30 Jul 2026 13:55:00 +0100 Subject: [PATCH 35/99] Added From for [LinearRgba; 4] impl --- crates/bevy_ui/src/ui_node.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 73073716199b9..0ba2ec1573dcf 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -3,7 +3,7 @@ use crate::{ ComputedStackIndex, ContentSize, FocusPolicy, UiRect, Val, Val2, }; use bevy_camera::{visibility::Visibility, Camera, RenderTarget}; -use bevy_color::{Alpha, Color}; +use bevy_color::{Alpha, Color, LinearRgba}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_math::{BVec2, Rect, UVec2, Vec2, Vec4, Vec4Swizzles}; @@ -2306,6 +2306,17 @@ impl Default for BorderColor { } } +impl From for [LinearRgba; 4] { + fn from(value: BorderColor) -> Self { + [ + value.top.to_linear(), + value.right.to_linear(), + value.bottom.to_linear(), + value.left.to_linear(), + ] + } +} + #[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] #[reflect(Component, Default, Debug, PartialEq, Clone)] #[cfg_attr( From a6d54fed88994ec7ff365544b009970ccab3379f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 30 Jul 2026 15:09:40 +0100 Subject: [PATCH 36/99] single extract --- crates/bevy_ui_render/src/lib.rs | 430 ++++++++++++++----------------- 1 file changed, 195 insertions(+), 235 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 2032057f4a44d..4700c6ca15674 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -323,7 +323,7 @@ impl<'w, 's> UiCameraMapper<'w, 's> { } } -pub struct ExtractedUiNode { +pub struct ExtractedUiNodeStyle { pub background_color: LinearRgba, pub border_color: [LinearRgba; 4], pub outer_color: LinearRgba, @@ -352,7 +352,7 @@ pub enum NodeType { /// #[derive(Resource, Default)] pub struct ExtractedUiNodes { - pub uinodes: MainEntityHashMap<(EntityIndexMap, ExtractedUiNode)>, + pub uinodes: MainEntityHashMap<(Entity, ExtractedUiNodeStyle)>, /// UI nodes that changed this frame. pub changed: MainEntityHashSet, } @@ -491,20 +491,6 @@ pub fn extract_uinode_background_colors( visual_box = image.visual_box; } - let border_color = maybe_border_color - .map(|border_color| { - [ - border_color.left.to_linear(), - border_color.top.to_linear(), - border_color.right.to_linear(), - border_color.bottom.to_linear(), - ] - }) - .unwrap_or([LinearRgba::NONE; 4]); - let outline_color = maybe_outline - .map(|outline| outline.color.into()) - .unwrap_or(LinearRgba::NONE); - if let Some(viewport_node) = maybe_viewport_node && let Some(camera_entity) = viewport_node.camera && let Some(image) = camera_query @@ -523,14 +509,18 @@ pub fn extract_uinode_background_colors( use_node_border = true; } - let extracted_uinode = ExtractedUiNode { - background_color: background_color.0.into(), - border_color, + let extracted_uinode = ExtractedUiNodeStyle { + background_color: background_color.to_linear(), + border_color: maybe_border_color + .map(|&border_color| border_color.into()) + .unwrap_or([LinearRgba::NONE; 4]), outer_color: maybe_outer_color - .map(|outer_color| outer_color.0.into()) + .map(|outer_color| outer_color.0.to_linear()) .unwrap_or(LinearRgba::NONE), image_color, - outline_color, + outline_color: maybe_outline + .map(|outline| outline.color.to_linear()) + .unwrap_or(LinearRgba::NONE), image: image_asset, flip_x, flip_y, @@ -541,48 +531,25 @@ pub fn extract_uinode_background_colors( use_node_border, }; - let mut layers = vec![]; - if !extracted_uinode.background_color.is_fully_transparent() + let has_content = !extracted_uinode.background_color.is_fully_transparent() || !extracted_uinode.outer_color.is_fully_transparent() - { - layers.push(stack_z_offsets::BACKGROUND_COLOR); - } - if extracted_uinode - .border_color - .iter() - .any(|color| !color.is_fully_transparent()) + || extracted_uinode + .border_color + .iter() + .any(|color| !color.is_fully_transparent()) || !extracted_uinode.outline_color.is_fully_transparent() - { - layers.push(stack_z_offsets::BORDER); - } - if extracted_uinode.image.is_some() { - layers.push(stack_z_offsets::IMAGE); - } + || extracted_uinode.image.is_some(); - let render_items = match extracted_uinodes.uinodes.entry(entity.into()) { - Entry::Occupied(mut entry) => { - entry.get_mut().1 = extracted_uinode; - &mut entry.into_mut().0 - } - Entry::Vacant(entry) => &mut entry.insert((Default::default(), extracted_uinode)).0, - }; - - let layer_count = layers.len(); - for (index, layer) in layers.into_iter().enumerate() { - if let Some((_, old_layer)) = render_items.get_index_mut(index) { - *old_layer = layer; - } else { - render_items.insert(commands.spawn_empty().id(), layer); + if has_content { + match extracted_uinodes.uinodes.entry(main_entity) { + Entry::Occupied(mut entry) => entry.get_mut().1 = extracted_uinode, + Entry::Vacant(entry) => { + entry.insert((commands.spawn_empty().id(), extracted_uinode)); + } } - } - - for (render_entity, _) in render_items.drain(layer_count..) { + } else if let Some((render_entity, _)) = extracted_uinodes.uinodes.remove(&main_entity) { commands.entity(render_entity).despawn(); } - - if render_items.is_empty() { - extracted_uinodes.uinodes.remove(&main_entity); - } } for entity in removed_uinodes.drain() { @@ -591,12 +558,10 @@ pub fn extract_uinode_background_colors( continue; } extracted_uinodes.changed.insert(main_entity); - let Some((mut render_items, _)) = extracted_uinodes.uinodes.remove(&main_entity) else { + let Some((render_entity, _)) = extracted_uinodes.uinodes.remove(&main_entity) else { continue; }; - for (render_entity, _) in render_items.drain(..) { - commands.entity(render_entity).despawn(); - } + commands.entity(render_entity).despawn(); } } @@ -873,58 +838,56 @@ pub fn queue_uinodes( let mut current_camera_entity = Entity::PLACEHOLDER; let mut current_phase = None; - for (main_entity, (render_items, _)) in extracted_uinodes.uinodes.iter() { + for (main_entity, (render_entity, _)) in extracted_uinodes.uinodes.iter() { let Some(geometry) = extracted_geometry.layout.get(main_entity) else { continue; }; - for (render_entity, stack_z_offset) in render_items.iter() { - let extracted_camera_entity = geometry.extracted_camera; - - if current_camera_entity != extracted_camera_entity { - current_phase = render_views.get(extracted_camera_entity).ok().and_then( - |(default_camera_view, ui_anti_alias)| { - camera_views - .get(default_camera_view.0) - .ok() - .and_then(|view| { - transparent_render_phases - .get_mut(&view.retained_view_entity) - .map(|transparent_phase| { - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_pipeline, - UiPipelineKey { - target_format: view.target_format, - anti_alias: matches!( - ui_anti_alias, - None | Some(UiAntiAlias::On) - ), - }, - ); - (pipeline, transparent_phase) - }) - }) - }, - ); - current_camera_entity = extracted_camera_entity; - } + let extracted_camera_entity = geometry.extracted_camera; + + if current_camera_entity != extracted_camera_entity { + current_phase = render_views.get(extracted_camera_entity).ok().and_then( + |(default_camera_view, ui_anti_alias)| { + camera_views + .get(default_camera_view.0) + .ok() + .and_then(|view| { + transparent_render_phases + .get_mut(&view.retained_view_entity) + .map(|transparent_phase| { + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_pipeline, + UiPipelineKey { + target_format: view.target_format, + anti_alias: matches!( + ui_anti_alias, + None | Some(UiAntiAlias::On) + ), + }, + ); + (pipeline, transparent_phase) + }) + }) + }, + ); + current_camera_entity = extracted_camera_entity; + } - let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { - continue; - }; + let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { + continue; + }; - transparent_phase.add_transient(TransparentUi { - draw_function, - pipeline: *pipeline, - entity: (*render_entity, *main_entity), - sort_key: FloatOrd(geometry.stack_index as f32 + stack_z_offset), - // 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: *pipeline, + entity: (*render_entity, *main_entity), + sort_key: FloatOrd(geometry.stack_index as f32), + // batch_range will be calculated in prepare_uinodes + batch_range: 0..0, + extra_index: PhaseItemExtraIndex::None, + indexed: true, + }); } } @@ -983,10 +946,8 @@ pub fn prepare_uinodes( for item_index in 0..ui_phase.items.len() { let item = &mut ui_phase.items[item_index]; let extracted_uinode = extracted_uinodes.uinodes.get(&item.main_entity()).and_then( - |(render_items, extracted_uinode)| { - render_items - .get(&item.entity()) - .map(|stack_z_offset| (extracted_uinode, *stack_z_offset)) + |(render_entity, extracted_uinode)| { + (*render_entity == item.entity()).then_some(extracted_uinode) }, ); if extracted_uinode.is_none() { @@ -998,10 +959,10 @@ pub fn prepare_uinodes( continue; }; let image = extracted_uinode - .and_then(|(extracted_uinode, stack_z_offset)| { - (stack_z_offset == stack_z_offsets::IMAGE) - .then_some(extracted_uinode.image) - .flatten() + .and_then(|extracted_uinode| { + extracted_uinode + .image + .filter(|image| gpu_images.get(*image).is_some()) }) .unwrap_or_default(); @@ -1066,14 +1027,71 @@ pub fn prepare_uinodes( continue; } } - if let Some((extracted_uinode, stack_z_offset)) = extracted_uinode { + if let Some(extracted_uinode) = extracted_uinode { let uinode = &geometry.uinode; - let mut nodes = vec![]; + let mut nodes = Vec::with_capacity(8); + + if !extracted_uinode.background_color.is_fully_transparent() { + nodes.push(( + extracted_uinode.background_color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + geometry.transform, + geometry.clip, + uinode.border_radius(), + uinode.border(), + NodeType::Rect, + false, + )); + } + if !extracted_uinode.outer_color.is_fully_transparent() { + nodes.push(( + extracted_uinode.outer_color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + geometry.transform, + geometry.clip, + uinode.border_radius(), + BorderRect::ZERO, + NodeType::Inverted, + false, + )); + } + + if uinode.border() != BorderRect::ZERO { + const BORDER_FLAGS: [u32; 4] = [ + shader_flags::BORDER_TOP, + shader_flags::BORDER_RIGHT, + shader_flags::BORDER_BOTTOM, + shader_flags::BORDER_LEFT, + ]; + let mut completed_flags = 0; + + for (i, &color) in extracted_uinode.border_color.iter().enumerate() { + if color.is_fully_transparent() + || completed_flags & BORDER_FLAGS[i] != 0 + { + continue; + } + + let mut border_flags = BORDER_FLAGS[i]; + for (j, &other_color) in + extracted_uinode.border_color.iter().enumerate().skip(i + 1) + { + if color == other_color { + border_flags |= BORDER_FLAGS[j]; + } + } + completed_flags |= border_flags; - if stack_z_offset == stack_z_offsets::BACKGROUND_COLOR { - if !extracted_uinode.background_color.is_fully_transparent() { nodes.push(( - extracted_uinode.background_color, + color, Rect { min: Vec2::ZERO, max: uinode.size(), @@ -1083,132 +1101,74 @@ pub fn prepare_uinodes( geometry.clip, uinode.border_radius(), uinode.border(), - NodeType::Rect, - false, - )); - } - if !extracted_uinode.outer_color.is_fully_transparent() { - nodes.push(( - extracted_uinode.outer_color, - Rect { - min: Vec2::ZERO, - max: uinode.size(), - }, - None, - geometry.transform, - geometry.clip, - uinode.border_radius(), - BorderRect::ZERO, - NodeType::Inverted, + NodeType::Border(border_flags), false, )); } - } else if stack_z_offset == stack_z_offsets::BORDER { - if uinode.border() != BorderRect::ZERO { - const BORDER_FLAGS: [u32; 4] = [ - shader_flags::BORDER_LEFT, - shader_flags::BORDER_TOP, - shader_flags::BORDER_RIGHT, - shader_flags::BORDER_BOTTOM, - ]; - let mut completed_flags = 0; - - for (i, &color) in extracted_uinode.border_color.iter().enumerate() { - if color.is_fully_transparent() - || completed_flags & BORDER_FLAGS[i] != 0 - { - continue; - } + } - let mut border_flags = BORDER_FLAGS[i]; - for (j, &other_color) in - extracted_uinode.border_color.iter().enumerate().skip(i + 1) - { - if color == other_color { - border_flags |= BORDER_FLAGS[j]; - } - } - completed_flags |= border_flags; - - nodes.push(( - color, - Rect { - min: Vec2::ZERO, - max: uinode.size(), - }, - None, - geometry.transform, - geometry.clip, - uinode.border_radius(), - uinode.border(), - NodeType::Border(border_flags), - false, - )); - } - } + if !extracted_uinode.outline_color.is_fully_transparent() + && uinode.outline_width() > 0. + { + nodes.push(( + extracted_uinode.outline_color, + Rect { + min: Vec2::ZERO, + max: uinode.outlined_node_size(), + }, + None, + geometry.transform, + geometry.clip, + uinode.outline_radius(), + BorderRect::all(uinode.outline_width()), + NodeType::Border(shader_flags::BORDER_ALL), + false, + )); + } - if !extracted_uinode.outline_color.is_fully_transparent() - && uinode.outline_width() > 0. - { - nodes.push(( - extracted_uinode.outline_color, - Rect { - min: Vec2::ZERO, - max: uinode.outlined_node_size(), - }, - None, - geometry.transform, - geometry.clip, - uinode.outline_radius(), - BorderRect::all(uinode.outline_width()), - NodeType::Border(shader_flags::BORDER_ALL), - false, - )); - } - } else { + if extracted_uinode.image == Some(image) { let visual_box = match extracted_uinode.visual_box { VisualBox::ContentBox => uinode.content_box(), VisualBox::PaddingBox => uinode.padding_box(), VisualBox::BorderBox => uinode.border_box(), }; - if visual_box.size().cmple(Vec2::ZERO).any() { - continue; - } - - let size = if extracted_uinode.auto_sized - && !extracted_uinode.image_size.cmple(Vec2::ZERO).any() - { - extracted_uinode.image_size - * (visual_box.size() / extracted_uinode.image_size).min_element() - } else { - visual_box.size() - }; - let mut rect = extracted_uinode.image_rect.unwrap_or(Rect { - min: Vec2::ZERO, - max: size, - }); - let atlas_scaling = extracted_uinode.image_rect.map(|_| { - let atlas_scaling = size / rect.size(); - rect.min *= atlas_scaling; - rect.max *= atlas_scaling; - atlas_scaling - }); - - nodes.push(( - extracted_uinode.image_color, - rect, - atlas_scaling, - geometry.transform * Affine2::from_translation(visual_box.center()), - geometry.clip, - uinode.border_radius(), - if extracted_uinode.use_node_border { - uinode.border() + if !visual_box.size().cmple(Vec2::ZERO).any() { + let size = if extracted_uinode.auto_sized + && !extracted_uinode.image_size.cmple(Vec2::ZERO).any() + { + extracted_uinode.image_size + * (visual_box.size() / extracted_uinode.image_size) + .min_element() } else { - BorderRect::ZERO - }, - NodeType::Rect, - true, - )); + visual_box.size() + }; + let mut rect = extracted_uinode.image_rect.unwrap_or(Rect { + min: Vec2::ZERO, + max: size, + }); + let atlas_scaling = extracted_uinode.image_rect.map(|_| { + let atlas_scaling = size / rect.size(); + rect.min *= atlas_scaling; + rect.max *= atlas_scaling; + atlas_scaling + }); + + nodes.push(( + extracted_uinode.image_color, + rect, + atlas_scaling, + geometry.transform * Affine2::from_translation(visual_box.center()), + geometry.clip, + uinode.border_radius(), + if extracted_uinode.use_node_border { + uinode.border() + } else { + BorderRect::ZERO + }, + NodeType::Rect, + true, + )); + } } for ( From f02c40660e8d2d2f6423cbf7b7a83d77e20fe43a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 30 Jul 2026 22:09:02 +0100 Subject: [PATCH 37/99] enabled debug overlay --- crates/bevy_ui_render/src/debug_overlay.rs | 378 ++++++++++++++++++++- crates/bevy_ui_render/src/lib.rs | 22 ++ 2 files changed, 395 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index ba77645aed845..261988b9fc30e 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -1,9 +1,32 @@ -use bevy_color::LinearRgba; -use bevy_ecs::prelude::Component; -use bevy_ecs::prelude::ReflectComponent; -use bevy_ecs::prelude::ReflectResource; -use bevy_ecs::resource::Resource; +use crate::{ + extract_layout::ExtractedUiLayout, shader_flags, DrawUi, ImageNodeBindGroups, TransparentUi, + UiAntiAlias, UiBatch, UiCameraMap, UiCameraView, UiMeta, UiPipeline, UiPipelineKey, UiVertex, + QUAD_INDICES, QUAD_VERTEX_POSITIONS, +}; +use bevy_asset::AssetId; +use bevy_camera::visibility::InheritedVisibility; +use bevy_color::{ColorToComponents, Hsla, LinearRgba}; +use bevy_ecs::{ + entity::Entity, lifecycle::RemovedComponents, prelude::*, query::Changed, resource::Resource, +}; +use bevy_image::Image; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_reflect::Reflect; +use bevy_render::{ + render_asset::RenderAssets, + render_phase::{DrawFunctions, PhaseItem, PhaseItemExtraIndex, ViewSortedRenderPhases}, + render_resource::{BindGroupEntries, PipelineCache, SpecializedRenderPipelines}, + renderer::RenderDevice, + sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}, + texture::GpuImage, + view::ExtractedView, + Extract, +}; +use bevy_sprite::BorderRect; +use bevy_ui::{ + ui_transform::UiGlobalTransform, CalculatedClip, ComputedNode, ComputedStackIndex, + ComputedUiTargetCamera, ResolvedBorderRadius, UiStack, +}; /// Configuration for the UI debug overlay /// @@ -141,3 +164,348 @@ impl From for GlobalUiDebugOptions { } } } + +struct ExtractedUiDebugOverlay { + extracted_camera: Entity, + transform: Affine2, + clip: Option, + color: LinearRgba, + border: BorderRect, + outlines: Vec<(Rect, ResolvedBorderRadius)>, + z_order: f32, +} + +#[derive(Resource, Default)] +pub(super) struct ExtractedUiDebugOverlays { + overlays: MainEntityHashMap<(Entity, ExtractedUiDebugOverlay)>, +} + +pub(super) fn extract_debug_overlay( + mut commands: Commands, + debug_options: Extract>, + extracted_layout: Res, + mut extracted_overlays: ResMut, + changed_debug_options_query: Extract>>, + uinode_query: Extract< + Query<( + Entity, + &ComputedNode, + &ComputedStackIndex, + &UiGlobalTransform, + &InheritedVisibility, + Option<&CalculatedClip>, + &ComputedUiTargetCamera, + Option<&UiDebugOptions>, + )>, + >, + ui_stack: Extract>, + camera_map: Extract, + mut removed_debug_options: Extract>, + mut nodes_to_extract: Local, +) { + nodes_to_extract.clear(); + nodes_to_extract.extend(extracted_layout.changed.iter().copied()); + nodes_to_extract.extend(changed_debug_options_query.iter().map(MainEntity::from)); + nodes_to_extract.extend(removed_debug_options.read().map(MainEntity::from)); + if debug_options.is_changed() || ui_stack.is_changed() { + nodes_to_extract.extend( + uinode_query + .iter() + .map(|(entity, ..)| MainEntity::from(entity)), + ); + } + + let mut camera_mapper = camera_map.get_mapper(); + + for main_entity in nodes_to_extract.drain() { + let Ok(( + entity, + uinode, + stack_index, + transform, + visibility, + maybe_clip, + computed_target, + local_debug_options, + )) = uinode_query.get(main_entity.entity()) + else { + if let Some((render_entity, _)) = extracted_overlays.overlays.remove(&main_entity) { + commands.entity(render_entity).despawn(); + } + continue; + }; + + let debug_options = local_debug_options + .copied() + .unwrap_or((*debug_options.as_ref()).into()); + if !debug_options.enabled || (!debug_options.show_hidden && !visibility.get()) { + if let Some((render_entity, _)) = extracted_overlays.overlays.remove(&main_entity) { + commands.entity(render_entity).despawn(); + } + continue; + } + + let Some(extracted_camera) = camera_mapper.map(computed_target) else { + if let Some((render_entity, _)) = extracted_overlays.overlays.remove(&main_entity) { + commands.entity(render_entity).despawn(); + } + continue; + }; + + let mut outlines = Vec::with_capacity(7); + let border_box = Rect::from_center_size(Vec2::ZERO, uinode.size()); + if debug_options.outline_border_box && !border_box.is_empty() { + outlines.push((border_box, uinode.border_radius())); + } + if debug_options.outline_padding_box { + let mut padding_box = border_box; + padding_box.min += uinode.border().min_inset; + padding_box.max -= uinode.border().max_inset; + if !padding_box.is_empty() { + outlines.push((padding_box, uinode.inner_radius())); + } + } + if debug_options.outline_content_box { + let mut content_box = border_box; + let content_inset = uinode.content_inset(); + content_box.min += content_inset.min_inset; + content_box.max -= content_inset.max_inset; + if !content_box.is_empty() { + outlines.push((content_box, ResolvedBorderRadius::ZERO)); + } + } + if debug_options.outline_scrollbars { + if let Some((gutter, [thumb_min, thumb_max])) = uinode.horizontal_scrollbar() { + if !gutter.is_empty() { + outlines.push((gutter, ResolvedBorderRadius::ZERO)); + } + let thumb = Rect { + min: Vec2::new(thumb_min, gutter.min.y), + max: Vec2::new(thumb_max, gutter.max.y), + }; + if !thumb.is_empty() { + outlines.push((thumb, ResolvedBorderRadius::ZERO)); + } + } + if let Some((gutter, [thumb_min, thumb_max])) = uinode.vertical_scrollbar() { + if !gutter.is_empty() { + outlines.push((gutter, ResolvedBorderRadius::ZERO)); + } + let thumb = Rect { + min: Vec2::new(gutter.min.x, thumb_min), + max: Vec2::new(gutter.max.x, thumb_max), + }; + if !thumb.is_empty() { + outlines.push((thumb, ResolvedBorderRadius::ZERO)); + } + } + } + + if outlines.is_empty() { + if let Some((render_entity, _)) = extracted_overlays.overlays.remove(&main_entity) { + commands.entity(render_entity).despawn(); + } + continue; + } + + let overlay = ExtractedUiDebugOverlay { + extracted_camera, + transform: transform.affine(), + clip: maybe_clip + .filter(|_| !debug_options.show_clipped) + .map(|clip| clip.clip), + color: debug_options + .line_color_override + .unwrap_or_else(|| Hsla::sequential_dispersed(entity.index_u32()).into()), + border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), + outlines, + z_order: (ui_stack.uinodes.len() as u32 + stack_index.0) as f32, + }; + + match extracted_overlays.overlays.entry(main_entity) { + bevy_platform::collections::hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().1 = overlay; + } + bevy_platform::collections::hash_map::Entry::Vacant(entry) => { + entry.insert((commands.spawn_empty().id(), overlay)); + } + } + } +} + +pub(super) fn queue_debug_overlay( + extracted_overlays: Res, + ui_pipeline: Res, + mut pipelines: ResMut>, + mut transparent_render_phases: ResMut>, + render_views: Query<(&UiCameraView, Option<&UiAntiAlias>), With>, + camera_views: Query<&ExtractedView>, + pipeline_cache: Res, + draw_functions: Res>, +) { + let draw_function = draw_functions.read().id::(); + + for (main_entity, (render_entity, overlay)) in &extracted_overlays.overlays { + let Ok((default_camera_view, ui_anti_alias)) = render_views.get(overlay.extracted_camera) + 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_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: (*render_entity, *main_entity), + sort_key: FloatOrd(overlay.z_order), + batch_range: 0..0, + extra_index: PhaseItemExtraIndex::None, + indexed: true, + }); + } +} + +pub(super) fn prepare_debug_overlay( + mut commands: Commands, + render_device: Res, + pipeline_cache: Res, + ui_pipeline: Res, + mut ui_meta: ResMut, + extracted_overlays: Res, + mut image_bind_groups: ResMut, + gpu_images: Res>, + mut phases: ResMut>, +) { + if extracted_overlays.overlays.is_empty() { + return; + } + + let image = AssetId::::default(); + let Some(gpu_image) = gpu_images.get(image) else { + return; + }; + image_bind_groups.values.entry(image).or_insert_with(|| { + render_device.create_bind_group( + "ui_debug_overlay_image_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential((&gpu_image.texture_view, &gpu_image.sampler)), + ) + }); + + let mut batches = Vec::with_capacity(extracted_overlays.overlays.len()); + + for ui_phase in phases.values_mut() { + for item_index in 0..ui_phase.items.len() { + let item = &mut ui_phase.items[item_index]; + let Some(overlay) = extracted_overlays + .overlays + .get(&item.main_entity()) + .and_then(|(render_entity, overlay)| { + (*render_entity == item.entity()).then_some(overlay) + }) + else { + continue; + }; + + let range_start = ui_meta.indices.len() as u32; + let color = overlay.color.to_f32_array(); + + for (rect, radius) in &overlay.outlines { + let rect_size = rect.size(); + let transform = overlay.transform * Affine2::from_translation(rect.center()); + let positions = QUAD_VERTEX_POSITIONS + .map(|position| transform.transform_point2(position * rect_size).extend(0.)); + let positions_diff = if let Some(clip) = overlay.clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = transform.transform_vector2(rect_size).abs(); + if transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + continue; + } + + let vertex_start = ui_meta.vertices.len() as u32; + let points = QUAD_VERTEX_POSITIONS.map(|position| position * rect_size); + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y][i].into(), + color, + flags: shader_flags::UNTEXTURED + | shader_flags::BORDER_ALL + | shader_flags::CORNERS[i], + radius: (*radius).into(), + border: [ + overlay.border.min_inset.x, + overlay.border.min_inset.y, + overlay.border.max_inset.x, + overlay.border.max_inset.y, + ], + size: rect_size.into(), + point: (points[i] + positions_diff[i]).into(), + }); + } + for &index in &QUAD_INDICES { + ui_meta.indices.push(vertex_start + index as u32); + } + } + + let range_end = ui_meta.indices.len() as u32; + if range_start == range_end { + continue; + } + + item.batch_range = item_index as u32..item_index as u32 + 1; + batches.push(( + item.entity(), + UiBatch { + range: range_start..range_end, + image, + texture_ranges: vec![], + }, + )); + } + } + + commands.try_insert_batch(batches); +} diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 4700c6ca15674..595215db2f32a 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -212,6 +212,9 @@ impl Plugin for UiRenderPlugin { return; }; + #[cfg(feature = "bevy_ui_debug")] + render_app.init_resource::(); + render_app .init_gpu_resource::>() .init_gpu_resource::() @@ -270,6 +273,25 @@ impl Plugin for UiRenderPlugin { ui_pass.after(Core3dSystems::PostProcess).before(upscaling), ); + #[cfg(feature = "bevy_ui_debug")] + render_app + .add_systems( + ExtractSchedule, + debug_overlay::extract_debug_overlay + .after(extract_layout::extract_ui_layout) + .in_set(RenderUiSystems::ExtractDebug), + ) + .add_systems( + Render, + ( + debug_overlay::queue_debug_overlay.in_set(RenderSystems::Queue), + debug_overlay::prepare_debug_overlay + .after(prepare_uinodes) + .before(prepare_text) + .in_set(RenderSystems::PrepareBindGroups), + ), + ); + app.add_plugins(UiTextureSlicerPlugin); app.add_plugins(GradientPlugin); app.add_plugins(BoxShadowPlugin); From 42a24b00b374c773be580ad1fddc42d468440355 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 31 Jul 2026 11:05:30 +0100 Subject: [PATCH 38/99] Added skeleton for new prepare --- crates/bevy_ui_render/src/lib.rs | 92 +++++++++++++++++++++++++++++++- 1 file changed, 91 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 595215db2f32a..bd8b70f3900cb 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -27,6 +27,7 @@ use bevy_ecs::entity::{EntityHashSet, EntityIndexMap}; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::camera::{extract_cameras, CameraMainPassTextureFormats}; +use bevy_render::renderer::RenderQueue; use bevy_render::sync_world::{MainEntityHashMap, MainEntityHashSet}; use bevy_shader::load_shader_library; use bevy_sprite_render::SpriteAssetEvents; @@ -79,7 +80,9 @@ use ui_texture_slice_pipeline::UiTextureSlicerPlugin; use crate::extract_layout::ExtractedUiLayout; use crate::shader_flags::INVERT; -use crate::text::{extract_text, prepare_text, queue_text, ExtractedGlyphLayouts}; +use crate::text::{ + extract_text, prepare_text, queue_text, ExtractedGlyphLayout, ExtractedGlyphLayouts, +}; pub mod prelude { #[cfg(feature = "bevy_ui_debug")] @@ -1379,3 +1382,90 @@ pub fn clear_batches(mut commands: Commands, batches_query: Query(); } } + +// new prepare function design +// draws ui nodes, text layouts and debug outlines +pub fn prepare_uinodes_new( + mut commands: Commands, + render_device: Res, + render_queue: Res, + pipeline_cache: Res, + mut ui_meta: ResMut, + extracted_uinodes: Res, + extracted_geometry: Res, + view_uniforms: Res, + ui_pipeline: Res, + mut image_bind_groups: ResMut, + gpu_images: Res>, + mut phases: ResMut>, + events: Res, + mut previous_len: Local, +) { + // If an image has changed, the GpuImage has (probably) changed + for event in &events.images { + match event { + AssetEvent::Added { .. } | + AssetEvent::Unused { .. } | + // Images don't have dependencies + AssetEvent::LoadedWithDependencies { .. } => {} + AssetEvent::Modified { id } | AssetEvent::Removed { id } => { + image_bind_groups.values.remove(id); + } + }; + } + + let Some(view_binding) = view_uniforms.uniforms.binding() else { + // If there are no view bindings, nothing to draw to + return; + }; + + let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len); + + ui_meta.vertices.clear(); + ui_meta.indices.clear(); + ui_meta.view_bind_group = Some(render_device.create_bind_group( + "ui_view_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.view_layout), + &BindGroupEntries::single(view_binding), + )); + + // Buffer indexes + let mut vertices_index = 0; + let mut indices_index = 0; + + // for each sorted render phase corresponding to a UI view + for ui_phase in phases.values_mut() { + for item_index in 0..ui_phase.items.len() { + let item = &mut ui_phase.items[item_index]; + + // get layout for this phase item, if no layout continue + let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) else { + continue; + }; + + let mut batch_item_index = 0; + let mut batch_image_handle = None; + + // node item + + // text item + + // debug outlines item + } + } + + *previous_len = batches.len(); + commands.try_insert_batch(batches); + + ui_meta.vertices.write_buffer(&render_device, &render_queue); + ui_meta.indices.write_buffer(&render_device, &render_queue); +} + +/// generates quads from extracted uinode syle and layout +pub fn generate_uinodes_quads(layout: &ExtractedUiLayout, style: &ExtractedUiNodeStyle) {} + +/// generates quads from extracted uinode syle and glyph layout +pub fn generate_text_quads(layout: &ExtractedUiLayout, style: &ExtractedGlyphLayout) {} + +/// generate quads from extracted layout and debug options +pub fn generate_debug_quads(layout: &ExtractedUiLayout, style: &UiDebugOptions) {} From a8027c3425476d87d8153cc9d6f1850e25fe7382 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 31 Jul 2026 14:17:03 +0100 Subject: [PATCH 39/99] cleanup --- crates/bevy_ui_render/src/debug_overlay.rs | 15 +- crates/bevy_ui_render/src/lib.rs | 764 ++++++++++++++++++++- crates/bevy_ui_render/src/text.rs | 1 + 3 files changed, 738 insertions(+), 42 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 261988b9fc30e..8f911a7691dae 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -165,19 +165,19 @@ impl From for GlobalUiDebugOptions { } } -struct ExtractedUiDebugOverlay { +pub(super) struct ExtractedUiDebugOverlay { extracted_camera: Entity, - transform: Affine2, - clip: Option, - color: LinearRgba, - border: BorderRect, - outlines: Vec<(Rect, ResolvedBorderRadius)>, + pub(super) transform: Affine2, + pub(super) clip: Option, + pub(super) color: LinearRgba, + pub(super) border: BorderRect, + pub(super) outlines: Vec<(Rect, ResolvedBorderRadius)>, z_order: f32, } #[derive(Resource, Default)] pub(super) struct ExtractedUiDebugOverlays { - overlays: MainEntityHashMap<(Entity, ExtractedUiDebugOverlay)>, + pub(super) overlays: MainEntityHashMap<(Entity, ExtractedUiDebugOverlay)>, } pub(super) fn extract_debug_overlay( @@ -378,6 +378,7 @@ pub(super) fn queue_debug_overlay( } } +#[expect(dead_code, reason = "retained while using prepare_uinodes_new")] pub(super) fn prepare_debug_overlay( mut commands: Commands, render_device: Res, diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index bd8b70f3900cb..a6f470a869bcc 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -33,8 +33,8 @@ use bevy_shader::load_shader_library; use bevy_sprite_render::SpriteAssetEvents; use bevy_ui::widget::{ImageNode, ImageNodeSize, NodeImageMode, ViewportNode}; use bevy_ui::{ - BackgroundColor, BorderColor, ComputedUiTargetCamera, Node, OuterColor, Outline, UiSystems, - VisualBox, + BackgroundColor, BorderColor, ComputedUiTargetCamera, Node, OuterColor, Outline, + ResolvedBorderRadius, UiSystems, VisualBox, }; use bevy_app::prelude::*; @@ -78,11 +78,9 @@ pub use render_pass::*; pub use ui_material_pipeline::*; use ui_texture_slice_pipeline::UiTextureSlicerPlugin; -use crate::extract_layout::ExtractedUiLayout; +use crate::extract_layout::{ExtractedUiLayout, ExtractedUiNodeLayout}; use crate::shader_flags::INVERT; -use crate::text::{ - extract_text, prepare_text, queue_text, ExtractedGlyphLayout, ExtractedGlyphLayouts, -}; +use crate::text::{extract_text, queue_text, ExtractedGlyphLayout, ExtractedGlyphLayouts}; pub mod prelude { #[cfg(feature = "bevy_ui_debug")] @@ -260,10 +258,7 @@ impl Plugin for UiRenderPlugin { queue_uinodes.in_set(RenderSystems::Queue), queue_text.in_set(RenderSystems::Queue), sort_phase_system::.in_set(RenderSystems::PhaseSort), - prepare_uinodes.in_set(RenderSystems::PrepareBindGroups), - prepare_text - .after(prepare_uinodes) - .in_set(RenderSystems::PrepareBindGroups), + prepare_uinodes_new.in_set(RenderSystems::PrepareBindGroups), clear_batches.in_set(RenderSystems::Cleanup), ), ) @@ -286,13 +281,7 @@ impl Plugin for UiRenderPlugin { ) .add_systems( Render, - ( - debug_overlay::queue_debug_overlay.in_set(RenderSystems::Queue), - debug_overlay::prepare_debug_overlay - .after(prepare_uinodes) - .before(prepare_text) - .in_set(RenderSystems::PrepareBindGroups), - ), + debug_overlay::queue_debug_overlay.in_set(RenderSystems::Queue), ); app.add_plugins(UiTextureSlicerPlugin); @@ -1385,13 +1374,17 @@ pub fn clear_batches(mut commands: Commands, batches_query: Query, render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut, extracted_uinodes: Res, + extracted_glyph_layouts: Res, + #[cfg(feature = "bevy_ui_debug")] extracted_debug_overlays: Res< + debug_overlay::ExtractedUiDebugOverlays, + >, extracted_geometry: Res, view_uniforms: Res, ui_pipeline: Res, @@ -1429,28 +1422,204 @@ pub fn prepare_uinodes_new( &BindGroupEntries::single(view_binding), )); - // Buffer indexes - let mut vertices_index = 0; - let mut indices_index = 0; - // for each sorted render phase corresponding to a UI view for ui_phase in phases.values_mut() { + let mut batch_item_index = 0; + let mut batch_image_handle = None; + for item_index in 0..ui_phase.items.len() { - let item = &mut ui_phase.items[item_index]; + let main_entity = ui_phase.items[item_index].main_entity(); + let entity = ui_phase.items[item_index].entity(); + + if let Some(style) = extracted_uinodes + .uinodes + .get(&main_entity) + .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) + { + let Some(layout) = extracted_geometry.layout.get(&main_entity) else { + batch_image_handle = None; + continue; + }; + let image = style + .image + .filter(|image| gpu_images.get(*image).is_some()) + .unwrap_or_default(); - // get layout for this phase item, if no layout continue - let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) else { - continue; - }; + ui_phase.items[item_index].batch_range = item_index as u32..item_index as u32; + let mut existing_batch = batches.last_mut(); + if batch_image_handle.is_none() + || existing_batch.is_none() + || (batch_image_handle != Some(AssetId::default()) + && image != AssetId::default() + && batch_image_handle != Some(image)) + { + let Some(gpu_image) = gpu_images.get(image) else { + batch_image_handle = None; + continue; + }; + batch_item_index = item_index; + batch_image_handle = Some(image); + batches.push(( + entity, + UiBatch { + range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, + image, + texture_ranges: vec![], + }, + )); + image_bind_groups.values.entry(image).or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + existing_batch = batches.last_mut(); + } else if batch_image_handle == Some(AssetId::default()) + && image != AssetId::default() + { + if let Some(ref mut existing_batch) = existing_batch + && let Some(gpu_image) = gpu_images.get(image) + { + batch_image_handle = Some(image); + existing_batch.1.image = image; + image_bind_groups.values.entry(image).or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + } else { + batch_image_handle = None; + continue; + } + } - let mut batch_item_index = 0; - let mut batch_image_handle = None; + let gpu_image = gpu_images + .get(image) + .expect("Image was checked during batching and should still exist"); + generate_uinodes_quads(&mut ui_meta, layout, style, image, gpu_image); + existing_batch.unwrap().1.range.end = ui_meta.indices.len() as u32; + ui_phase.items[batch_item_index].batch_range_mut().end += 1; + continue; + } - // node item + batch_image_handle = None; - // text item + if let Some(style) = extracted_glyph_layouts + .uinodes + .get(&main_entity) + .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) + { + let Some(layout) = extracted_geometry.layout.get(&main_entity) else { + batch_image_handle = None; + continue; + }; + let image = style + .sections + .iter() + .find_map(|section| { + gpu_images + .get(section.atlas_texture) + .map(|_| section.atlas_texture) + }) + .unwrap_or_default(); + let Some(gpu_image) = gpu_images.get(image) else { + continue; + }; + image_bind_groups.values.entry(image).or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + + let range_start = ui_meta.indices.len() as u32; + let mut texture_ranges = + Vec::with_capacity(style.sections.len().saturating_mul(2).saturating_add(1)); + generate_text_quads( + &mut ui_meta, + layout, + style, + &gpu_images, + image, + &mut texture_ranges, + ); + for (_, texture) in &texture_ranges { + let gpu_image = gpu_images + .get(*texture) + .expect("Image was checked during quad generation and should still exist"); + image_bind_groups.values.entry(*texture).or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + } + ui_phase.items[item_index].batch_range = item_index as u32..item_index as u32 + 1; + batches.push(( + entity, + UiBatch { + range: range_start..ui_meta.indices.len() as u32, + image, + texture_ranges, + }, + )); + continue; + } - // debug outlines item + #[cfg(feature = "bevy_ui_debug")] + if let Some(style) = extracted_debug_overlays + .overlays + .get(&main_entity) + .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) + { + let image = AssetId::::default(); + let Some(gpu_image) = gpu_images.get(image) else { + continue; + }; + image_bind_groups.values.entry(image).or_insert_with(|| { + render_device.create_bind_group( + "ui_debug_overlay_image_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); + + let range_start = ui_meta.indices.len() as u32; + generate_debug_quads(&mut ui_meta, style); + let range_end = ui_meta.indices.len() as u32; + if range_start == range_end { + continue; + } + ui_phase.items[item_index].batch_range = item_index as u32..item_index as u32 + 1; + batches.push(( + entity, + UiBatch { + range: range_start..range_end, + image, + texture_ranges: vec![], + }, + )); + } } } @@ -1462,10 +1631,535 @@ pub fn prepare_uinodes_new( } /// generates quads from extracted uinode syle and layout -pub fn generate_uinodes_quads(layout: &ExtractedUiLayout, style: &ExtractedUiNodeStyle) {} +fn generate_uinodes_quads( + ui_meta: &mut UiMeta, + layout: &ExtractedUiNodeLayout, + style: &ExtractedUiNodeStyle, + image: AssetId, + gpu_image: &GpuImage, +) { + let uinode = &layout.uinode; + let mut push_quad = |color: LinearRgba, + mut rect: Rect, + atlas_scaling: Option, + transform: Affine2, + clip: Option, + border_radius: ResolvedBorderRadius, + border: BorderRect, + node_type: NodeType, + textured: bool| { + let mut flags = if textured { + shader_flags::TEXTURED + } else { + shader_flags::UNTEXTURED + }; + let rect_size = rect.size(); + let positions = + QUAD_VERTEX_POSITIONS.map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + let mut positions_diff = if let Some(clip) = clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = transform.transform_vector2(rect_size).abs(); + if transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + return; + } + let uvs = if textured { + let atlas_extent = atlas_scaling + .map(|scaling| gpu_image.size_2d().as_vec2() * scaling) + .unwrap_or(rect.max); + if style.flip_x { + mem::swap(&mut rect.max.x, &mut rect.min.x); + positions_diff[0].x *= -1.; + positions_diff[1].x *= -1.; + positions_diff[2].x *= -1.; + positions_diff[3].x *= -1.; + } + if style.flip_y { + mem::swap(&mut rect.max.y, &mut rect.min.y); + positions_diff[0].y *= -1.; + positions_diff[1].y *= -1.; + positions_diff[2].y *= -1.; + positions_diff[3].y *= -1.; + } + [ + Vec2::new( + rect.min.x + positions_diff[0].x, + rect.min.y + positions_diff[0].y, + ), + Vec2::new( + rect.max.x + positions_diff[1].x, + rect.min.y + positions_diff[1].y, + ), + Vec2::new( + rect.max.x + positions_diff[2].x, + rect.max.y + positions_diff[2].y, + ), + Vec2::new( + rect.min.x + positions_diff[3].x, + rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent) + } else { + [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] + }; + match node_type { + NodeType::Border(border_flags) => flags |= border_flags, + NodeType::Inverted => flags |= INVERT, + NodeType::Rect => {} + } + + let vertex_start = ui_meta.vertices.len() as u32; + let color = color.to_f32_array(); + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + flags: flags | shader_flags::CORNERS[i], + radius: border_radius.into(), + border: [ + border.min_inset.x, + border.min_inset.y, + border.max_inset.x, + border.max_inset.y, + ], + size: rect_size.into(), + point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), + }); + } + for &index in &QUAD_INDICES { + ui_meta.indices.push(vertex_start + index as u32); + } + }; + + if !style.background_color.is_fully_transparent() { + push_quad( + style.background_color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + layout.transform, + layout.clip, + uinode.border_radius(), + uinode.border(), + NodeType::Rect, + false, + ); + } + if !style.outer_color.is_fully_transparent() { + push_quad( + style.outer_color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + layout.transform, + layout.clip, + uinode.border_radius(), + BorderRect::ZERO, + NodeType::Inverted, + false, + ); + } + + if uinode.border() != BorderRect::ZERO { + const BORDER_FLAGS: [u32; 4] = [ + shader_flags::BORDER_TOP, + shader_flags::BORDER_RIGHT, + shader_flags::BORDER_BOTTOM, + shader_flags::BORDER_LEFT, + ]; + let mut completed_flags = 0; + + for (i, &color) in style.border_color.iter().enumerate() { + if color.is_fully_transparent() || completed_flags & BORDER_FLAGS[i] != 0 { + continue; + } + + let mut border_flags = BORDER_FLAGS[i]; + for (j, &other_color) in style.border_color.iter().enumerate().skip(i + 1) { + if color == other_color { + border_flags |= BORDER_FLAGS[j]; + } + } + completed_flags |= border_flags; + + push_quad( + color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + layout.transform, + layout.clip, + uinode.border_radius(), + uinode.border(), + NodeType::Border(border_flags), + false, + ); + } + } + + if !style.outline_color.is_fully_transparent() && uinode.outline_width() > 0. { + push_quad( + style.outline_color, + Rect { + min: Vec2::ZERO, + max: uinode.outlined_node_size(), + }, + None, + layout.transform, + layout.clip, + uinode.outline_radius(), + BorderRect::all(uinode.outline_width()), + NodeType::Border(shader_flags::BORDER_ALL), + false, + ); + } + + if style.image == Some(image) { + let visual_box = match style.visual_box { + VisualBox::ContentBox => uinode.content_box(), + VisualBox::PaddingBox => uinode.padding_box(), + VisualBox::BorderBox => uinode.border_box(), + }; + if !visual_box.size().cmple(Vec2::ZERO).any() { + let size = if style.auto_sized && !style.image_size.cmple(Vec2::ZERO).any() { + style.image_size * (visual_box.size() / style.image_size).min_element() + } else { + visual_box.size() + }; + let mut rect = style.image_rect.unwrap_or(Rect { + min: Vec2::ZERO, + max: size, + }); + let atlas_scaling = style.image_rect.map(|_| { + let atlas_scaling = size / rect.size(); + rect.min *= atlas_scaling; + rect.max *= atlas_scaling; + atlas_scaling + }); + push_quad( + style.image_color, + rect, + atlas_scaling, + layout.transform * Affine2::from_translation(visual_box.center()), + layout.clip, + uinode.border_radius(), + if style.use_node_border { + uinode.border() + } else { + BorderRect::ZERO + }, + NodeType::Rect, + true, + ); + } + } +} /// generates quads from extracted uinode syle and glyph layout -pub fn generate_text_quads(layout: &ExtractedUiLayout, style: &ExtractedGlyphLayout) {} +fn generate_text_quads( + ui_meta: &mut UiMeta, + layout: &ExtractedUiNodeLayout, + style: &ExtractedGlyphLayout, + gpu_images: &RenderAssets, + image: AssetId, + texture_ranges: &mut Vec<(Range, AssetId)>, +) { + let uinode = &layout.uinode; + let shadow_offset = style.shadow_offset / uinode.inverse_scale_factor(); + let transform = layout.transform + * Affine2::from_translation(uinode.content_box().min - style.viewport_offset); + let clip = if style.clip_to_content_box { + let content_box = uinode.content_box(); + let text_clip = Rect::from_center_size( + layout.transform.translation + content_box.center(), + content_box.size(), + ); + Some( + layout + .clip + .map_or(text_clip, |clip| clip.intersect(text_clip)), + ) + } else { + layout.clip + }; + let mut texture_range_start = ui_meta.indices.len() as u32; + let mut current_texture = image; + { + let mut push_quad = |rect: Rect, + color: LinearRgba, + border_radius: ResolvedBorderRadius, + glyph: Option<(Rect, AssetId)>| { + let rect_size = rect.size(); + let rect_transform = transform * Affine2::from_translation(rect.center()); + let positions = QUAD_VERTEX_POSITIONS + .map(|pos| rect_transform.transform_point2(pos * rect_size).extend(0.)); + let positions_diff = if let Some(clip) = clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = rect_transform.transform_vector2(rect_size).abs(); + if rect_transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + return; + } + + let (uvs, flags) = if let Some((glyph_rect, atlas_texture)) = glyph { + let Some(gpu_image) = gpu_images.get(atlas_texture) else { + return; + }; + let range_end = ui_meta.indices.len() as u32; + if current_texture != atlas_texture { + if texture_range_start < range_end { + texture_ranges.push((texture_range_start..range_end, current_texture)); + } + texture_range_start = range_end; + current_texture = atlas_texture; + } + let atlas_extent = gpu_image.size_2d().as_vec2(); + ( + [ + Vec2::new( + glyph_rect.min.x + positions_diff[0].x, + glyph_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + glyph_rect.max.x + positions_diff[1].x, + glyph_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + glyph_rect.max.x + positions_diff[2].x, + glyph_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + glyph_rect.min.x + positions_diff[3].x, + glyph_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent), + shader_flags::TEXTURED, + ) + } else { + ( + [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y], + shader_flags::UNTEXTURED, + ) + }; + + let vertex_start = ui_meta.vertices.len() as u32; + let color = color.to_f32_array(); + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + flags: flags | shader_flags::CORNERS[i], + radius: border_radius.into(), + border: [0.; 4], + size: rect_size.into(), + point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), + }); + } + for &index in &QUAD_INDICES { + ui_meta.indices.push(vertex_start + index as u32); + } + }; + + let selection_color = if style.focused { + style.cursor_style.selection_color + } else { + style.cursor_style.unfocused_selection_color + } + .to_linear(); + for (rect, radius) in &style.selections { + push_quad(*rect, selection_color, *radius, None); + } + for (rect, color) in &style.backgrounds { + push_quad(*rect, *color, ResolvedBorderRadius::ZERO, None); + } + + if !style.shadow_color.is_fully_transparent() { + for section in &style.sections { + for glyph in &style.glyphs[section.range.start as usize..section.range.end as usize] + { + push_quad( + Rect::from_center_size( + glyph.translation + shadow_offset, + glyph.rect.size(), + ), + style.shadow_color, + ResolvedBorderRadius::ZERO, + Some((glyph.rect, section.atlas_texture)), + ); + } + } + for (rect, _) in style.strike_through.iter().chain(style.underline.iter()) { + push_quad( + Rect::from_center_size(rect.center() + shadow_offset, rect.size()), + style.shadow_color, + ResolvedBorderRadius::ZERO, + None, + ); + } + } + + for section in &style.sections { + for glyph in &style.glyphs[section.range.start as usize..section.range.end as usize] { + push_quad( + Rect::from_center_size(glyph.translation, glyph.rect.size()), + glyph.color, + ResolvedBorderRadius::ZERO, + Some((glyph.rect, section.atlas_texture)), + ); + } + } + for (rect, color) in style.strike_through.iter().chain(style.underline.iter()) { + push_quad(*rect, *color, ResolvedBorderRadius::ZERO, None); + } + if let Some(cursor) = style.cursor { + push_quad( + cursor, + style.cursor_style.color.to_linear(), + ResolvedBorderRadius::ZERO, + None, + ); + } + } + let range_end = ui_meta.indices.len() as u32; + if texture_range_start < range_end { + texture_ranges.push((texture_range_start..range_end, current_texture)); + } +} /// generate quads from extracted layout and debug options -pub fn generate_debug_quads(layout: &ExtractedUiLayout, style: &UiDebugOptions) {} +#[cfg(feature = "bevy_ui_debug")] +fn generate_debug_quads(ui_meta: &mut UiMeta, style: &debug_overlay::ExtractedUiDebugOverlay) { + let color = style.color.to_f32_array(); + for (rect, radius) in &style.outlines { + let rect_size = rect.size(); + let transform = style.transform * Affine2::from_translation(rect.center()); + let positions = QUAD_VERTEX_POSITIONS + .map(|position| transform.transform_point2(position * rect_size).extend(0.)); + let positions_diff = if let Some(clip) = style.clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = transform.transform_vector2(rect_size).abs(); + if transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + continue; + } + + let vertex_start = ui_meta.vertices.len() as u32; + let points = QUAD_VERTEX_POSITIONS.map(|position| position * rect_size); + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y][i].into(), + color, + flags: shader_flags::UNTEXTURED + | shader_flags::BORDER_ALL + | shader_flags::CORNERS[i], + radius: (*radius).into(), + border: [ + style.border.min_inset.x, + style.border.min_inset.y, + style.border.max_inset.x, + style.border.max_inset.y, + ], + size: rect_size.into(), + point: (points[i] + positions_diff[i]).into(), + }); + } + for &index in &QUAD_INDICES { + ui_meta.indices.push(vertex_start + index as u32); + } + } +} diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index 73cdb43cc80c9..1af507ee853f9 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -520,6 +520,7 @@ pub fn queue_text( } } +#[expect(dead_code, reason = "retained while using prepare_uinodes_new")] pub fn prepare_text( mut commands: Commands, render_device: Res, From 410fffb2b475a1b24e592ff8a948e467dd76e727 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 31 Jul 2026 14:47:52 +0100 Subject: [PATCH 40/99] comments and renamings --- crates/bevy_ui_render/src/lib.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index a6f470a869bcc..300abb367e200 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -784,6 +784,7 @@ pub(crate) struct UiVertex { pub point: [f32; 2], } +// Indexed buffers for UI vertices #[derive(Resource)] pub struct UiMeta { pub(crate) vertices: RawBufferVec, @@ -950,8 +951,8 @@ pub fn prepare_uinodes( )); // Buffer indexes - let mut vertices_index = 0; - let mut indices_index = 0; + let mut vertices_index = 0; // indexes into ui_meta.indices + let mut indices_index = 0; // indexes into ui_meta.vertices for ui_phase in phases.values_mut() { let mut batch_item_index = 0; @@ -1408,12 +1409,13 @@ fn prepare_uinodes_new( } let Some(view_binding) = view_uniforms.uniforms.binding() else { - // If there are no view bindings, nothing to draw to + // If there are no view bindings, give up. return; }; let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len); + // UiMeta contains the vertex and index buffer data and the current view ui_meta.vertices.clear(); ui_meta.indices.clear(); ui_meta.view_bind_group = Some(render_device.create_bind_group( @@ -1424,12 +1426,13 @@ fn prepare_uinodes_new( // for each sorted render phase corresponding to a UI view for ui_phase in phases.values_mut() { + // used to store the index of the first phase item in a batch let mut batch_item_index = 0; let mut batch_image_handle = None; - for item_index in 0..ui_phase.items.len() { - let main_entity = ui_phase.items[item_index].main_entity(); - let entity = ui_phase.items[item_index].entity(); + for phase_item_index in 0..ui_phase.items.len() { + let main_entity = ui_phase.items[phase_item_index].main_entity(); + let entity = ui_phase.items[phase_item_index].entity(); if let Some(style) = extracted_uinodes .uinodes @@ -1445,7 +1448,8 @@ fn prepare_uinodes_new( .filter(|image| gpu_images.get(*image).is_some()) .unwrap_or_default(); - ui_phase.items[item_index].batch_range = item_index as u32..item_index as u32; + ui_phase.items[phase_item_index].batch_range = + phase_item_index as u32..phase_item_index as u32; let mut existing_batch = batches.last_mut(); if batch_image_handle.is_none() || existing_batch.is_none() @@ -1457,7 +1461,7 @@ fn prepare_uinodes_new( batch_image_handle = None; continue; }; - batch_item_index = item_index; + batch_item_index = phase_item_index; batch_image_handle = Some(image); batches.push(( entity, @@ -1571,7 +1575,8 @@ fn prepare_uinodes_new( ) }); } - ui_phase.items[item_index].batch_range = item_index as u32..item_index as u32 + 1; + ui_phase.items[phase_item_index].batch_range = + phase_item_index as u32..phase_item_index as u32 + 1; batches.push(( entity, UiBatch { @@ -1610,7 +1615,8 @@ fn prepare_uinodes_new( if range_start == range_end { continue; } - ui_phase.items[item_index].batch_range = item_index as u32..item_index as u32 + 1; + ui_phase.items[phase_item_index].batch_range = + phase_item_index as u32..phase_item_index as u32 + 1; batches.push(( entity, UiBatch { From 53d60fe25dfe0244e3f3940b2a53fa4072483831 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 31 Jul 2026 14:50:48 +0100 Subject: [PATCH 41/99] Removed old prepare_uinodes --- crates/bevy_ui_render/src/lib.rs | 454 +------------------------------ 1 file changed, 2 insertions(+), 452 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 300abb367e200..30869fa973469 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -258,7 +258,7 @@ impl Plugin for UiRenderPlugin { queue_uinodes.in_set(RenderSystems::Queue), queue_text.in_set(RenderSystems::Queue), sort_phase_system::.in_set(RenderSystems::PhaseSort), - prepare_uinodes_new.in_set(RenderSystems::PrepareBindGroups), + prepare_uinodes.in_set(RenderSystems::PrepareBindGroups), clear_batches.in_set(RenderSystems::Cleanup), ), ) @@ -911,456 +911,6 @@ pub struct ImageNodeBindGroups { pub values: HashMap, BindGroup>, } -pub fn prepare_uinodes( - mut commands: Commands, - render_device: Res, - pipeline_cache: Res, - mut ui_meta: ResMut, - extracted_uinodes: Res, - extracted_geometry: Res, - view_uniforms: Res, - ui_pipeline: Res, - mut image_bind_groups: ResMut, - gpu_images: Res>, - mut phases: ResMut>, - events: Res, - mut previous_len: Local, -) { - // If an image has changed, the GpuImage has (probably) changed - for event in &events.images { - match event { - AssetEvent::Added { .. } | - AssetEvent::Unused { .. } | - // Images don't have dependencies - AssetEvent::LoadedWithDependencies { .. } => {} - AssetEvent::Modified { id } | AssetEvent::Removed { id } => { - image_bind_groups.values.remove(id); - } - }; - } - - if let Some(view_binding) = view_uniforms.uniforms.binding() { - let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len); - - ui_meta.vertices.clear(); - ui_meta.indices.clear(); - ui_meta.view_bind_group = Some(render_device.create_bind_group( - "ui_view_bind_group", - &pipeline_cache.get_bind_group_layout(&ui_pipeline.view_layout), - &BindGroupEntries::single(view_binding), - )); - - // Buffer indexes - let mut vertices_index = 0; // indexes into ui_meta.indices - let mut indices_index = 0; // indexes into ui_meta.vertices - - for ui_phase in phases.values_mut() { - let mut batch_item_index = 0; - let mut batch_image_handle = None; - - for item_index in 0..ui_phase.items.len() { - let item = &mut ui_phase.items[item_index]; - let extracted_uinode = extracted_uinodes.uinodes.get(&item.main_entity()).and_then( - |(render_entity, extracted_uinode)| { - (*render_entity == item.entity()).then_some(extracted_uinode) - }, - ); - if extracted_uinode.is_none() { - batch_image_handle = None; - continue; - } - let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) else { - batch_image_handle = None; - continue; - }; - let image = extracted_uinode - .and_then(|extracted_uinode| { - extracted_uinode - .image - .filter(|image| gpu_images.get(*image).is_some()) - }) - .unwrap_or_default(); - - // 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() - || existing_batch.is_none() - || (batch_image_handle != Some(AssetId::default()) - && image != AssetId::default() - && batch_image_handle != Some(image)) - { - if let Some(gpu_image) = gpu_images.get(image) { - batch_item_index = item_index; - batch_image_handle = Some(image); - - let new_batch = UiBatch { - range: vertices_index..vertices_index, - image, - texture_ranges: vec![], - }; - batches.push((item.entity(), new_batch)); - - image_bind_groups.values.entry(image).or_insert_with(|| { - render_device.create_bind_group( - "ui_material_bind_group", - &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential(( - &gpu_image.texture_view, - &gpu_image.sampler, - )), - ) - }); - - existing_batch = batches.last_mut(); - } else { - continue; - } - } else if batch_image_handle == Some(AssetId::default()) - && image != AssetId::default() - { - if let Some(ref mut existing_batch) = existing_batch - && let Some(gpu_image) = gpu_images.get(image) - { - batch_image_handle = Some(image); - existing_batch.1.image = image; - - image_bind_groups.values.entry(image).or_insert_with(|| { - render_device.create_bind_group( - "ui_material_bind_group", - &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential(( - &gpu_image.texture_view, - &gpu_image.sampler, - )), - ) - }); - } else { - continue; - } - } - if let Some(extracted_uinode) = extracted_uinode { - let uinode = &geometry.uinode; - let mut nodes = Vec::with_capacity(8); - - if !extracted_uinode.background_color.is_fully_transparent() { - nodes.push(( - extracted_uinode.background_color, - Rect { - min: Vec2::ZERO, - max: uinode.size(), - }, - None, - geometry.transform, - geometry.clip, - uinode.border_radius(), - uinode.border(), - NodeType::Rect, - false, - )); - } - if !extracted_uinode.outer_color.is_fully_transparent() { - nodes.push(( - extracted_uinode.outer_color, - Rect { - min: Vec2::ZERO, - max: uinode.size(), - }, - None, - geometry.transform, - geometry.clip, - uinode.border_radius(), - BorderRect::ZERO, - NodeType::Inverted, - false, - )); - } - - if uinode.border() != BorderRect::ZERO { - const BORDER_FLAGS: [u32; 4] = [ - shader_flags::BORDER_TOP, - shader_flags::BORDER_RIGHT, - shader_flags::BORDER_BOTTOM, - shader_flags::BORDER_LEFT, - ]; - let mut completed_flags = 0; - - for (i, &color) in extracted_uinode.border_color.iter().enumerate() { - if color.is_fully_transparent() - || completed_flags & BORDER_FLAGS[i] != 0 - { - continue; - } - - let mut border_flags = BORDER_FLAGS[i]; - for (j, &other_color) in - extracted_uinode.border_color.iter().enumerate().skip(i + 1) - { - if color == other_color { - border_flags |= BORDER_FLAGS[j]; - } - } - completed_flags |= border_flags; - - nodes.push(( - color, - Rect { - min: Vec2::ZERO, - max: uinode.size(), - }, - None, - geometry.transform, - geometry.clip, - uinode.border_radius(), - uinode.border(), - NodeType::Border(border_flags), - false, - )); - } - } - - if !extracted_uinode.outline_color.is_fully_transparent() - && uinode.outline_width() > 0. - { - nodes.push(( - extracted_uinode.outline_color, - Rect { - min: Vec2::ZERO, - max: uinode.outlined_node_size(), - }, - None, - geometry.transform, - geometry.clip, - uinode.outline_radius(), - BorderRect::all(uinode.outline_width()), - NodeType::Border(shader_flags::BORDER_ALL), - false, - )); - } - - if extracted_uinode.image == Some(image) { - let visual_box = match extracted_uinode.visual_box { - VisualBox::ContentBox => uinode.content_box(), - VisualBox::PaddingBox => uinode.padding_box(), - VisualBox::BorderBox => uinode.border_box(), - }; - if !visual_box.size().cmple(Vec2::ZERO).any() { - let size = if extracted_uinode.auto_sized - && !extracted_uinode.image_size.cmple(Vec2::ZERO).any() - { - extracted_uinode.image_size - * (visual_box.size() / extracted_uinode.image_size) - .min_element() - } else { - visual_box.size() - }; - let mut rect = extracted_uinode.image_rect.unwrap_or(Rect { - min: Vec2::ZERO, - max: size, - }); - let atlas_scaling = extracted_uinode.image_rect.map(|_| { - let atlas_scaling = size / rect.size(); - rect.min *= atlas_scaling; - rect.max *= atlas_scaling; - atlas_scaling - }); - - nodes.push(( - extracted_uinode.image_color, - rect, - atlas_scaling, - geometry.transform * Affine2::from_translation(visual_box.center()), - geometry.clip, - uinode.border_radius(), - if extracted_uinode.use_node_border { - uinode.border() - } else { - BorderRect::ZERO - }, - NodeType::Rect, - true, - )); - } - } - - for ( - color, - mut uinode_rect, - atlas_scaling, - transform, - clip, - border_radius, - border, - node_type, - textured, - ) in nodes - { - let mut flags = if textured { - shader_flags::TEXTURED - } else { - shader_flags::UNTEXTURED - }; - - let rect_size = uinode_rect.size(); - - // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); - let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); - - // Calculate the effect of clipping - // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) - let mut positions_diff = if let Some(clip) = clip { - [ - Vec2::new( - f32::max(clip.min.x - positions[0].x, 0.), - f32::max(clip.min.y - positions[0].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[1].x, 0.), - f32::max(clip.min.y - positions[1].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[2].x, 0.), - f32::min(clip.max.y - positions[2].y, 0.), - ), - Vec2::new( - f32::max(clip.min.x - positions[3].x, 0.), - f32::min(clip.max.y - positions[3].y, 0.), - ), - ] - } else { - [Vec2::ZERO; 4] - }; - - let positions_clipped = [ - positions[0] + positions_diff[0].extend(0.), - positions[1] + positions_diff[1].extend(0.), - positions[2] + positions_diff[2].extend(0.), - positions[3] + positions_diff[3].extend(0.), - ]; - - let points = [ - points[0] + positions_diff[0], - points[1] + positions_diff[1], - points[2] + positions_diff[2], - points[3] + positions_diff[3], - ]; - - let transformed_rect_size = transform.transform_vector2(rect_size).abs(); - - // Don't try to cull nodes that have a rotation - // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π - // In those two cases, the culling check can proceed normally as corners will be on - // horizontal / vertical lines - // For all other angles, bypass the culling check - // This does not properly handles all rotations on all axis - if transform.x_axis[1] == 0.0 { - // Cull nodes that are completely clipped - if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x - || positions_diff[1].y - positions_diff[2].y - >= transformed_rect_size.y - { - continue; - } - } - let uvs = if flags == shader_flags::UNTEXTURED { - [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] - } else { - let image = gpu_images - .get(image) - .expect("Image was checked during batching and should still exist"); - // Rescale atlases. This is done here because we need texture data that might not be available in Extract. - let atlas_extent = atlas_scaling - .map(|scaling| image.size_2d().as_vec2() * scaling) - .unwrap_or(uinode_rect.max); - if extracted_uinode.flip_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 extracted_uinode.flip_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.; - positions_diff[3].y *= -1.; - } - [ - Vec2::new( - uinode_rect.min.x + positions_diff[0].x, - uinode_rect.min.y + positions_diff[0].y, - ), - Vec2::new( - uinode_rect.max.x + positions_diff[1].x, - uinode_rect.min.y + positions_diff[1].y, - ), - Vec2::new( - uinode_rect.max.x + positions_diff[2].x, - uinode_rect.max.y + positions_diff[2].y, - ), - Vec2::new( - uinode_rect.min.x + positions_diff[3].x, - uinode_rect.max.y + positions_diff[3].y, - ), - ] - .map(|pos| pos / atlas_extent) - }; - - let color = color.to_f32_array(); - match node_type { - NodeType::Border(border_flags) => { - flags |= border_flags; - } - NodeType::Inverted => { - flags |= INVERT; - } - _ => {} - } - - for i in 0..4 { - let ui_vertex = UiVertex { - position: positions_clipped[i].into(), - uv: uvs[i].into(), - color, - flags: flags | shader_flags::CORNERS[i], - radius: border_radius.into(), - border: [ - border.min_inset.x, - border.min_inset.y, - border.max_inset.x, - border.max_inset.y, - ], - size: rect_size.into(), - point: points[i].into(), - }; - ui_meta.vertices.push(ui_vertex); - } - - for &i in &QUAD_INDICES { - ui_meta.indices.push(indices_index + i as u32); - } - - vertices_index += 6; - indices_index += 4; - } - } - let existing_batch = existing_batch.unwrap(); - existing_batch.1.range.end = vertices_index; - ui_phase.items[batch_item_index].batch_range_mut().end += 1; - } - } - - *previous_len = batches.len(); - commands.try_insert_batch(batches); - } -} - /// A render-world system that removes all [`UiBatch`] components. /// /// They're currently rebuilt from scratch every frame, so we have to remove @@ -1375,7 +925,7 @@ pub fn clear_batches(mut commands: Commands, batches_query: Query, render_queue: Res, From 8081002e634b5287d81f4221ddaf90b0570626a0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 31 Jul 2026 14:59:10 +0100 Subject: [PATCH 42/99] delete prepare from debug_overlay --- crates/bevy_ui_render/src/debug_overlay.rs | 133 --------------------- 1 file changed, 133 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 8f911a7691dae..877423cbd996b 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -377,136 +377,3 @@ pub(super) fn queue_debug_overlay( }); } } - -#[expect(dead_code, reason = "retained while using prepare_uinodes_new")] -pub(super) fn prepare_debug_overlay( - mut commands: Commands, - render_device: Res, - pipeline_cache: Res, - ui_pipeline: Res, - mut ui_meta: ResMut, - extracted_overlays: Res, - mut image_bind_groups: ResMut, - gpu_images: Res>, - mut phases: ResMut>, -) { - if extracted_overlays.overlays.is_empty() { - return; - } - - let image = AssetId::::default(); - let Some(gpu_image) = gpu_images.get(image) else { - return; - }; - image_bind_groups.values.entry(image).or_insert_with(|| { - render_device.create_bind_group( - "ui_debug_overlay_image_bind_group", - &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential((&gpu_image.texture_view, &gpu_image.sampler)), - ) - }); - - let mut batches = Vec::with_capacity(extracted_overlays.overlays.len()); - - for ui_phase in phases.values_mut() { - for item_index in 0..ui_phase.items.len() { - let item = &mut ui_phase.items[item_index]; - let Some(overlay) = extracted_overlays - .overlays - .get(&item.main_entity()) - .and_then(|(render_entity, overlay)| { - (*render_entity == item.entity()).then_some(overlay) - }) - else { - continue; - }; - - let range_start = ui_meta.indices.len() as u32; - let color = overlay.color.to_f32_array(); - - for (rect, radius) in &overlay.outlines { - let rect_size = rect.size(); - let transform = overlay.transform * Affine2::from_translation(rect.center()); - let positions = QUAD_VERTEX_POSITIONS - .map(|position| transform.transform_point2(position * rect_size).extend(0.)); - let positions_diff = if let Some(clip) = overlay.clip { - [ - Vec2::new( - f32::max(clip.min.x - positions[0].x, 0.), - f32::max(clip.min.y - positions[0].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[1].x, 0.), - f32::max(clip.min.y - positions[1].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[2].x, 0.), - f32::min(clip.max.y - positions[2].y, 0.), - ), - Vec2::new( - f32::max(clip.min.x - positions[3].x, 0.), - f32::min(clip.max.y - positions[3].y, 0.), - ), - ] - } else { - [Vec2::ZERO; 4] - }; - let positions_clipped = [ - positions[0] + positions_diff[0].extend(0.), - positions[1] + positions_diff[1].extend(0.), - positions[2] + positions_diff[2].extend(0.), - positions[3] + positions_diff[3].extend(0.), - ]; - let transformed_rect_size = transform.transform_vector2(rect_size).abs(); - if transform.x_axis[1] == 0.0 - && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x - || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) - { - continue; - } - - let vertex_start = ui_meta.vertices.len() as u32; - let points = QUAD_VERTEX_POSITIONS.map(|position| position * rect_size); - for i in 0..4 { - ui_meta.vertices.push(UiVertex { - position: positions_clipped[i].into(), - uv: [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y][i].into(), - color, - flags: shader_flags::UNTEXTURED - | shader_flags::BORDER_ALL - | shader_flags::CORNERS[i], - radius: (*radius).into(), - border: [ - overlay.border.min_inset.x, - overlay.border.min_inset.y, - overlay.border.max_inset.x, - overlay.border.max_inset.y, - ], - size: rect_size.into(), - point: (points[i] + positions_diff[i]).into(), - }); - } - for &index in &QUAD_INDICES { - ui_meta.indices.push(vertex_start + index as u32); - } - } - - let range_end = ui_meta.indices.len() as u32; - if range_start == range_end { - continue; - } - - item.batch_range = item_index as u32..item_index as u32 + 1; - batches.push(( - item.entity(), - UiBatch { - range: range_start..range_end, - image, - texture_ranges: vec![], - }, - )); - } - } - - commands.try_insert_batch(batches); -} From e2ac948cd5d4c680ef76a653603f531ec4087865 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 31 Jul 2026 15:35:38 +0100 Subject: [PATCH 43/99] renaming --- crates/bevy_ui_render/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 30869fa973469..f1ceb8421b23d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -977,7 +977,7 @@ fn prepare_uinodes( // for each sorted render phase corresponding to a UI view for ui_phase in phases.values_mut() { // used to store the index of the first phase item in a batch - let mut batch_item_index = 0; + let mut batch_start_item_index = 0; let mut batch_image_handle = None; for phase_item_index in 0..ui_phase.items.len() { @@ -1011,7 +1011,7 @@ fn prepare_uinodes( batch_image_handle = None; continue; }; - batch_item_index = phase_item_index; + batch_start_item_index = phase_item_index; batch_image_handle = Some(image); batches.push(( entity, @@ -1061,7 +1061,7 @@ fn prepare_uinodes( .expect("Image was checked during batching and should still exist"); generate_uinodes_quads(&mut ui_meta, layout, style, image, gpu_image); existing_batch.unwrap().1.range.end = ui_meta.indices.len() as u32; - ui_phase.items[batch_item_index].batch_range_mut().end += 1; + ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; continue; } From ef0894bf1911ef1e755e0541b6201ee527a193cf Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 31 Jul 2026 15:50:32 +0100 Subject: [PATCH 44/99] Improved batching --- crates/bevy_ui_render/src/lib.rs | 91 +++++++++++++++++++++++++------- 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index f1ceb8421b23d..f5278a168a1ef 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1059,14 +1059,25 @@ fn prepare_uinodes( let gpu_image = gpu_images .get(image) .expect("Image was checked during batching and should still exist"); + let range_start = ui_meta.indices.len() as u32; generate_uinodes_quads(&mut ui_meta, layout, style, image, gpu_image); - existing_batch.unwrap().1.range.end = ui_meta.indices.len() as u32; + let range_end = ui_meta.indices.len() as u32; + let existing_batch = existing_batch.unwrap(); + existing_batch.1.range.end = range_end; + if let Some((range, texture)) = existing_batch.1.texture_ranges.last_mut() { + if image == AssetId::default() || *texture == image { + range.end = range_end; + } else if range_start < range_end { + existing_batch + .1 + .texture_ranges + .push((range_start..range_end, image)); + } + } ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; continue; } - batch_image_handle = None; - if let Some(style) = extracted_glyph_layouts .uinodes .get(&main_entity) @@ -1086,8 +1097,39 @@ fn prepare_uinodes( }) .unwrap_or_default(); let Some(gpu_image) = gpu_images.get(image) else { + batch_image_handle = None; continue; }; + + ui_phase.items[phase_item_index].batch_range = + phase_item_index as u32..phase_item_index as u32; + let mut existing_batch = batches.last_mut(); + if batch_image_handle.is_none() + || existing_batch.is_none() + || (batch_image_handle != Some(AssetId::default()) + && image != AssetId::default() + && batch_image_handle != Some(image)) + { + batch_start_item_index = phase_item_index; + batch_image_handle = Some(image); + batches.push(( + entity, + UiBatch { + range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, + image, + texture_ranges: Vec::with_capacity( + style.sections.len().saturating_mul(2).saturating_add(1), + ), + }, + )); + existing_batch = batches.last_mut(); + } else if batch_image_handle == Some(AssetId::default()) + && image != AssetId::default() + && let Some(existing_batch) = &mut existing_batch + { + batch_image_handle = Some(image); + existing_batch.1.image = image; + } image_bind_groups.values.entry(image).or_insert_with(|| { render_device.create_bind_group( "ui_material_bind_group", @@ -1099,18 +1141,27 @@ fn prepare_uinodes( ) }); - let range_start = ui_meta.indices.len() as u32; - let mut texture_ranges = - Vec::with_capacity(style.sections.len().saturating_mul(2).saturating_add(1)); + let existing_batch = existing_batch.unwrap(); + existing_batch + .1 + .texture_ranges + .reserve(style.sections.len().saturating_mul(2).saturating_add(1)); + if existing_batch.1.texture_ranges.is_empty() && !existing_batch.1.range.is_empty() + { + existing_batch + .1 + .texture_ranges + .push((existing_batch.1.range.clone(), existing_batch.1.image)); + } generate_text_quads( &mut ui_meta, layout, style, &gpu_images, image, - &mut texture_ranges, + &mut existing_batch.1.texture_ranges, ); - for (_, texture) in &texture_ranges { + for (_, texture) in &existing_batch.1.texture_ranges { let gpu_image = gpu_images .get(*texture) .expect("Image was checked during quad generation and should still exist"); @@ -1125,19 +1176,16 @@ fn prepare_uinodes( ) }); } - ui_phase.items[phase_item_index].batch_range = - phase_item_index as u32..phase_item_index as u32 + 1; - batches.push(( - entity, - UiBatch { - range: range_start..ui_meta.indices.len() as u32, - image, - texture_ranges, - }, - )); + existing_batch.1.range.end = ui_meta.indices.len() as u32; + if let Some((_, texture)) = existing_batch.1.texture_ranges.last() { + batch_image_handle = Some(*texture); + } + ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; continue; } + batch_image_handle = None; + #[cfg(feature = "bevy_ui_debug")] if let Some(style) = extracted_debug_overlays .overlays @@ -1474,8 +1522,11 @@ fn generate_text_quads( } else { layout.clip }; - let mut texture_range_start = ui_meta.indices.len() as u32; - let mut current_texture = image; + let (mut texture_range_start, mut current_texture) = texture_ranges + .pop() + .map_or((ui_meta.indices.len() as u32, image), |(range, texture)| { + (range.start, texture) + }); { let mut push_quad = |rect: Rect, color: LinearRgba, From 78aed9e86eb426869933f61969f9e5a617f391db Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 31 Jul 2026 17:47:42 +0100 Subject: [PATCH 45/99] renamed extract_uinode_background_colors --- crates/bevy_ui_render/src/gradient.rs | 2 +- crates/bevy_ui_render/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 9cc8ca6f716db..20aec8b05748d 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -54,7 +54,7 @@ impl Plugin for GradientPlugin { ExtractSchedule, extract_gradients .in_set(RenderUiSystems::ExtractGradient) - .after(extract_uinode_background_colors), + .after(extract_uinode_styles), ) .add_systems( Render, diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index f5278a168a1ef..c556042e1cf8d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -248,7 +248,7 @@ impl Plugin for UiRenderPlugin { extract_ui_camera_view .after(extract_cameras) .in_set(RenderUiSystems::ExtractCameraViews), - extract_uinode_background_colors.in_set(RenderUiSystems::ExtractBackgrounds), + extract_uinode_styles.in_set(RenderUiSystems::ExtractBackgrounds), extract_text.in_set(RenderUiSystems::ExtractText), ), ) @@ -371,7 +371,7 @@ pub struct ExtractedUiNodes { pub changed: MainEntityHashSet, } -pub fn extract_uinode_background_colors( +pub fn extract_uinode_styles( mut commands: Commands, mut extracted_uinodes: ResMut, texture_atlases: Extract>>, From 02dfd5d1da08df6aa395feb6d4c57f3ab1db840c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Fri, 31 Jul 2026 18:16:52 +0100 Subject: [PATCH 46/99] cleanup further --- crates/bevy_ui_render/src/lib.rs | 118 ++++++++++++++----------------- 1 file changed, 52 insertions(+), 66 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index c556042e1cf8d..55573bcbda41b 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -221,8 +221,8 @@ impl Plugin for UiRenderPlugin { .init_gpu_resource::() .init_gpu_resource::() .init_resource::() - .init_resource::() - .allow_ambiguous_resource::() + .init_resource::() + .allow_ambiguous_resource::() .init_resource::() .init_resource::>() .init_resource::>() @@ -337,6 +337,7 @@ impl<'w, 's> UiCameraMapper<'w, 's> { } } +#[derive(Default)] pub struct ExtractedUiNodeStyle { pub background_color: LinearRgba, pub border_color: [LinearRgba; 4], @@ -353,6 +354,19 @@ pub struct ExtractedUiNodeStyle { pub use_node_border: bool, } +impl ExtractedUiNodeStyle { + pub fn is_visible(&self) -> bool { + !self.background_color.is_fully_transparent() + || !self.outer_color.is_fully_transparent() + || self + .border_color + .iter() + .any(|color| !color.is_fully_transparent()) + || !self.outline_color.is_fully_transparent() + || self.image.is_some() + } +} + /// The type of UI node. /// This is used to determine how to render the UI node. #[derive(Clone, Copy, Debug, PartialEq)] @@ -365,7 +379,7 @@ pub enum NodeType { /// The list of UI nodes, as well as the set of nodes that changed. /// #[derive(Resource, Default)] -pub struct ExtractedUiNodes { +pub struct ExtractedUiNodeStyles { pub uinodes: MainEntityHashMap<(Entity, ExtractedUiNodeStyle)>, /// UI nodes that changed this frame. pub changed: MainEntityHashSet, @@ -373,7 +387,7 @@ pub struct ExtractedUiNodes { pub fn extract_uinode_styles( mut commands: Commands, - mut extracted_uinodes: ResMut, + mut extracted_uinodes: ResMut, texture_atlases: Extract>>, camera_query: Extract>, changed_uinode_query: Extract< @@ -463,15 +477,7 @@ pub fn extract_uinode_styles( continue; } - let mut image_color = LinearRgba::NONE; - let mut image_asset = None; - let mut flip_x = false; - let mut flip_y = false; - let mut image_rect = None; - let mut image_size = Vec2::ZERO; - let mut auto_sized = false; - let mut visual_box = VisualBox::default(); - let mut use_node_border = false; + let mut style = ExtractedUiNodeStyle::default(); if let Some(image) = maybe_image && !image.color.is_fully_transparent() @@ -484,7 +490,7 @@ pub fn extract_uinode_styles( .and_then(|s| s.texture_rect(&texture_atlases)) .map(|r| r.as_rect()); - image_rect = match (atlas_rect, image.rect) { + style.image_rect = match (atlas_rect, image.rect) { (None, None) => None, (None, Some(image_rect)) => Some(image_rect), (Some(atlas_rect), None) => Some(atlas_rect), @@ -494,15 +500,15 @@ pub fn extract_uinode_styles( Some(image_rect) } }; - image_color = image.color.into(); - image_asset = Some(image.image.id()); - flip_x = image.flip_x; - flip_y = image.flip_y; - image_size = maybe_image_size + style.image_color = image.color.into(); + style.image = Some(image.image.id()); + style.flip_x = image.flip_x; + style.flip_y = image.flip_y; + style.image_size = maybe_image_size .map(|image_size| image_size.size().as_vec2()) .unwrap_or_default(); - auto_sized = matches!(image.image_mode, NodeImageMode::Auto); - visual_box = image.visual_box; + style.auto_sized = matches!(image.image_mode, NodeImageMode::Auto); + style.visual_box = image.visual_box; } if let Some(viewport_node) = maybe_viewport_node @@ -512,53 +518,33 @@ pub fn extract_uinode_styles( .ok() .and_then(|(_, render_target)| render_target.as_image()) { - image_color = LinearRgba::WHITE; - image_asset = Some(image.id()); - flip_x = false; - flip_y = false; - image_rect = None; - image_size = Vec2::ZERO; - auto_sized = false; - visual_box = VisualBox::BorderBox; - use_node_border = true; + style.image_color = LinearRgba::WHITE; + style.image = Some(image.id()); + style.flip_x = false; + style.flip_y = false; + style.image_rect = None; + style.image_size = Vec2::ZERO; + style.auto_sized = false; + style.visual_box = VisualBox::BorderBox; + style.use_node_border = true; } - let extracted_uinode = ExtractedUiNodeStyle { - background_color: background_color.to_linear(), - border_color: maybe_border_color - .map(|&border_color| border_color.into()) - .unwrap_or([LinearRgba::NONE; 4]), - outer_color: maybe_outer_color - .map(|outer_color| outer_color.0.to_linear()) - .unwrap_or(LinearRgba::NONE), - image_color, - outline_color: maybe_outline - .map(|outline| outline.color.to_linear()) - .unwrap_or(LinearRgba::NONE), - image: image_asset, - flip_x, - flip_y, - image_rect, - image_size, - auto_sized, - visual_box, - use_node_border, - }; - - let has_content = !extracted_uinode.background_color.is_fully_transparent() - || !extracted_uinode.outer_color.is_fully_transparent() - || extracted_uinode - .border_color - .iter() - .any(|color| !color.is_fully_transparent()) - || !extracted_uinode.outline_color.is_fully_transparent() - || extracted_uinode.image.is_some(); - - if has_content { + style.background_color = background_color.to_linear(); + style.border_color = maybe_border_color + .map(|&border_color| border_color.into()) + .unwrap_or([LinearRgba::NONE; 4]); + style.outer_color = maybe_outer_color + .map(|outer_color| outer_color.0.to_linear()) + .unwrap_or(LinearRgba::NONE); + style.outline_color = maybe_outline + .map(|outline| outline.color.to_linear()) + .unwrap_or(LinearRgba::NONE); + + if style.is_visible() { match extracted_uinodes.uinodes.entry(main_entity) { - Entry::Occupied(mut entry) => entry.get_mut().1 = extracted_uinode, + Entry::Occupied(mut entry) => entry.get_mut().1 = style, Entry::Vacant(entry) => { - entry.insert((commands.spawn_empty().id(), extracted_uinode)); + entry.insert((commands.spawn_empty().id(), style)); } } } else if let Some((render_entity, _)) = extracted_uinodes.uinodes.remove(&main_entity) { @@ -839,7 +825,7 @@ pub mod shader_flags { } pub fn queue_uinodes( - extracted_uinodes: Res, + extracted_uinodes: Res, extracted_geometry: Res, ui_pipeline: Res, mut pipelines: ResMut>, @@ -931,7 +917,7 @@ fn prepare_uinodes( render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut, - extracted_uinodes: Res, + extracted_uinodes: Res, extracted_glyph_layouts: Res, #[cfg(feature = "bevy_ui_debug")] extracted_debug_overlays: Res< debug_overlay::ExtractedUiDebugOverlays, From c6ba7b4c054c71fadfdb15ef9197c90a2cd9f290 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Sat, 1 Aug 2026 22:51:59 +0100 Subject: [PATCH 47/99] Add debug_overlay queue function --- crates/bevy_ui_render/src/debug_overlay.rs | 325 ++++++--------------- crates/bevy_ui_render/src/lib.rs | 6 +- 2 files changed, 86 insertions(+), 245 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 877423cbd996b..ed3125374f2d4 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -1,3 +1,5 @@ +use std::arch::global_asm; + use crate::{ extract_layout::ExtractedUiLayout, shader_flags, DrawUi, ImageNodeBindGroups, TransparentUi, UiAntiAlias, UiBatch, UiCameraMap, UiCameraView, UiMeta, UiPipeline, UiPipelineKey, UiVertex, @@ -6,11 +8,17 @@ use crate::{ use bevy_asset::AssetId; use bevy_camera::visibility::InheritedVisibility; use bevy_color::{ColorToComponents, Hsla, LinearRgba}; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - entity::Entity, lifecycle::RemovedComponents, prelude::*, query::Changed, resource::Resource, + entity::{hash_map, Entity}, + lifecycle::RemovedComponents, + prelude::*, + query::{Changed, EcsAccessLevel}, + resource::Resource, }; use bevy_image::Image; use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; +use bevy_platform::collections::hash_table::Entry; use bevy_reflect::Reflect; use bevy_render::{ render_asset::RenderAssets, @@ -32,7 +40,7 @@ use bevy_ui::{ /// /// Can be added as a `Component` to individual UI node entities. /// This overwrites the default [`GlobalUiDebugOptions`] resource. -#[derive(Component, Reflect, Copy, Clone)] +#[derive(Component, Reflect, Clone)] #[reflect(Component)] pub struct UiDebugOptions { /// Set to true to enable the UI debug overlay @@ -82,259 +90,72 @@ impl Default for UiDebugOptions { impl From for UiDebugOptions { fn from(other: GlobalUiDebugOptions) -> Self { - Self { - enabled: other.enabled, - outline_border_box: other.outline_border_box, - outline_padding_box: other.outline_padding_box, - outline_content_box: other.outline_content_box, - outline_scrollbars: other.outline_scrollbars, - line_width: other.line_width, - line_color_override: other.line_color_override, - show_hidden: other.show_hidden, - show_clipped: other.show_clipped, - ignore_border_radius: other.ignore_border_radius, - } + other.0.clone() } } /// Configuration for the UI debug overlay /// /// A global `resource` that can be overridden by local component [`UiDebugOptions`] override on individual UI node entities -#[derive(Resource, Reflect, Copy, Clone)] +#[derive(Default, Resource, Reflect, Clone, Deref, DerefMut)] #[reflect(Resource)] -pub struct GlobalUiDebugOptions { - /// Set to true to enable the UI debug overlay - pub enabled: bool, - /// Show outlines for the border boxes of UI nodes - pub outline_border_box: bool, - /// Show outlines for the padding boxes of UI nodes - pub outline_padding_box: bool, - /// Show outlines for the content boxes of UI nodes - pub outline_content_box: bool, - /// Show outlines for the scrollbar regions of UI nodes - pub outline_scrollbars: bool, - /// Width of the overlay's lines in logical pixels - pub line_width: f32, - /// Override Color for the overlay's lines - pub line_color_override: Option, - /// Show outlines for non-visible UI nodes - pub show_hidden: bool, - /// Show outlines for clipped sections of UI nodes - pub show_clipped: bool, - /// Draw outlines with sharp corners even if the UI nodes have border radii - pub ignore_border_radius: bool, -} - -impl GlobalUiDebugOptions { - pub fn toggle(&mut self) { - self.enabled = !self.enabled; - } -} - -impl Default for GlobalUiDebugOptions { - fn default() -> Self { - Self { - enabled: false, - line_width: 1., - line_color_override: None, - show_hidden: false, - show_clipped: false, - ignore_border_radius: false, - outline_border_box: true, - outline_padding_box: false, - outline_content_box: false, - outline_scrollbars: false, - } - } -} +pub struct GlobalUiDebugOptions(pub UiDebugOptions); impl From for GlobalUiDebugOptions { fn from(other: UiDebugOptions) -> Self { - Self { - enabled: other.enabled, - outline_border_box: other.outline_border_box, - outline_padding_box: other.outline_padding_box, - outline_content_box: other.outline_content_box, - outline_scrollbars: other.outline_scrollbars, - line_width: other.line_width, - line_color_override: other.line_color_override, - show_hidden: other.show_hidden, - show_clipped: other.show_clipped, - ignore_border_radius: other.ignore_border_radius, - } + Self(other) } } - -pub(super) struct ExtractedUiDebugOverlay { - extracted_camera: Entity, - pub(super) transform: Affine2, - pub(super) clip: Option, - pub(super) color: LinearRgba, - pub(super) border: BorderRect, - pub(super) outlines: Vec<(Rect, ResolvedBorderRadius)>, - z_order: f32, -} - #[derive(Resource, Default)] -pub(super) struct ExtractedUiDebugOverlays { - pub(super) overlays: MainEntityHashMap<(Entity, ExtractedUiDebugOverlay)>, +pub struct ExtractedUiDebugOptions { + pub stack_offset: f32, + pub global: UiDebugOptions, + pub local: MainEntityHashMap<(Entity, UiDebugOptions)>, } -pub(super) fn extract_debug_overlay( +pub fn extract_debug_overlay( mut commands: Commands, - debug_options: Extract>, - extracted_layout: Res, - mut extracted_overlays: ResMut, - changed_debug_options_query: Extract>>, - uinode_query: Extract< - Query<( - Entity, - &ComputedNode, - &ComputedStackIndex, - &UiGlobalTransform, - &InheritedVisibility, - Option<&CalculatedClip>, - &ComputedUiTargetCamera, - Option<&UiDebugOptions>, - )>, - >, - ui_stack: Extract>, - camera_map: Extract, + global_debug_options: Extract>, + mut extracted_debug_options: ResMut, + ui_debug_options_query: Extract>>, mut removed_debug_options: Extract>, - mut nodes_to_extract: Local, + mut nodes_processed_this_frame: Local, + ui_stack: Res, ) { - nodes_to_extract.clear(); - nodes_to_extract.extend(extracted_layout.changed.iter().copied()); - nodes_to_extract.extend(changed_debug_options_query.iter().map(MainEntity::from)); - nodes_to_extract.extend(removed_debug_options.read().map(MainEntity::from)); - if debug_options.is_changed() || ui_stack.is_changed() { - nodes_to_extract.extend( - uinode_query - .iter() - .map(|(entity, ..)| MainEntity::from(entity)), - ); - } + extracted_debug_options.stack_offset = ui_stack.uinodes.len() as f32; + nodes_processed_this_frame.clear(); - let mut camera_mapper = camera_map.get_mapper(); + extracted_debug_options.global = global_debug_options.0.clone(); - for main_entity in nodes_to_extract.drain() { - let Ok(( - entity, - uinode, - stack_index, - transform, - visibility, - maybe_clip, - computed_target, - local_debug_options, - )) = uinode_query.get(main_entity.entity()) - else { - if let Some((render_entity, _)) = extracted_overlays.overlays.remove(&main_entity) { - commands.entity(render_entity).despawn(); - } - continue; - }; - - let debug_options = local_debug_options - .copied() - .unwrap_or((*debug_options.as_ref()).into()); - if !debug_options.enabled || (!debug_options.show_hidden && !visibility.get()) { - if let Some((render_entity, _)) = extracted_overlays.overlays.remove(&main_entity) { - commands.entity(render_entity).despawn(); - } - continue; - } + // iter through all nodes with UiDebugOptions + // add to processed this frame list, so they aren't removed if tagged by removal detection + for (entity, local_debug_options) in ui_debug_options_query.iter() { + let main_entity = MainEntity::from(entity); + nodes_processed_this_frame.insert(main_entity); - let Some(extracted_camera) = camera_mapper.map(computed_target) else { - if let Some((render_entity, _)) = extracted_overlays.overlays.remove(&main_entity) { - commands.entity(render_entity).despawn(); - } - continue; - }; - - let mut outlines = Vec::with_capacity(7); - let border_box = Rect::from_center_size(Vec2::ZERO, uinode.size()); - if debug_options.outline_border_box && !border_box.is_empty() { - outlines.push((border_box, uinode.border_radius())); - } - if debug_options.outline_padding_box { - let mut padding_box = border_box; - padding_box.min += uinode.border().min_inset; - padding_box.max -= uinode.border().max_inset; - if !padding_box.is_empty() { - outlines.push((padding_box, uinode.inner_radius())); - } - } - if debug_options.outline_content_box { - let mut content_box = border_box; - let content_inset = uinode.content_inset(); - content_box.min += content_inset.min_inset; - content_box.max -= content_inset.max_inset; - if !content_box.is_empty() { - outlines.push((content_box, ResolvedBorderRadius::ZERO)); - } - } - if debug_options.outline_scrollbars { - if let Some((gutter, [thumb_min, thumb_max])) = uinode.horizontal_scrollbar() { - if !gutter.is_empty() { - outlines.push((gutter, ResolvedBorderRadius::ZERO)); - } - let thumb = Rect { - min: Vec2::new(thumb_min, gutter.min.y), - max: Vec2::new(thumb_max, gutter.max.y), - }; - if !thumb.is_empty() { - outlines.push((thumb, ResolvedBorderRadius::ZERO)); - } + match extracted_debug_options.local.entry(main_entity) { + bevy_platform::collections::hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().1 = local_debug_options.clone(); } - if let Some((gutter, [thumb_min, thumb_max])) = uinode.vertical_scrollbar() { - if !gutter.is_empty() { - outlines.push((gutter, ResolvedBorderRadius::ZERO)); - } - let thumb = Rect { - min: Vec2::new(gutter.min.x, thumb_min), - max: Vec2::new(gutter.max.x, thumb_max), - }; - if !thumb.is_empty() { - outlines.push((thumb, ResolvedBorderRadius::ZERO)); - } + bevy_platform::collections::hash_map::Entry::Vacant(entry) => { + entry.insert((commands.spawn_empty().id(), local_debug_options.clone())); } } + } - if outlines.is_empty() { - if let Some((render_entity, _)) = extracted_overlays.overlays.remove(&main_entity) { - commands.entity(render_entity).despawn(); - } + for main_entity in removed_debug_options.read().map(MainEntity::from) { + if nodes_processed_this_frame.contains(&main_entity) { continue; } - - let overlay = ExtractedUiDebugOverlay { - extracted_camera, - transform: transform.affine(), - clip: maybe_clip - .filter(|_| !debug_options.show_clipped) - .map(|clip| clip.clip), - color: debug_options - .line_color_override - .unwrap_or_else(|| Hsla::sequential_dispersed(entity.index_u32()).into()), - border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), - outlines, - z_order: (ui_stack.uinodes.len() as u32 + stack_index.0) as f32, - }; - - match extracted_overlays.overlays.entry(main_entity) { - bevy_platform::collections::hash_map::Entry::Occupied(mut entry) => { - entry.get_mut().1 = overlay; - } - bevy_platform::collections::hash_map::Entry::Vacant(entry) => { - entry.insert((commands.spawn_empty().id(), overlay)); - } + if let Some((render_entity, _)) = extracted_debug_options.local.remove(&main_entity) { + commands.entity(render_entity).despawn(); } } } -pub(super) fn queue_debug_overlay( - extracted_overlays: Res, +pub fn queue_debug_overlay( + extracted_overlays: Res, + extracted_layout: Res, ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -344,36 +165,56 @@ pub(super) fn queue_debug_overlay( draw_functions: Res>, ) { let draw_function = draw_functions.read().id::(); + let mut current_camera_entity = Entity::PLACEHOLDER; + let mut current_phase = None; - for (main_entity, (render_entity, overlay)) in &extracted_overlays.overlays { - let Ok((default_camera_view, ui_anti_alias)) = render_views.get(overlay.extracted_camera) - else { + for (main_entity, (render_entity, overlay)) in &extracted_overlays.local { + let Some(geometry) = extracted_layout.layout.get(main_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 { + + if current_camera_entity != geometry.extracted_camera { + current_phase = render_views.get(geometry.extracted_camera).ok().and_then( + |(default_camera_view, ui_anti_alias)| { + camera_views + .get(default_camera_view.0) + .ok() + .and_then(|view| { + transparent_render_phases + .get_mut(&view.retained_view_entity) + .map(|transparent_phase| { + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_pipeline, + UiPipelineKey { + target_format: view.target_format, + anti_alias: matches!( + ui_anti_alias, + None | Some(UiAntiAlias::On) + ), + }, + ); + (pipeline, transparent_phase) + }) + }) + }, + ); + current_camera_entity = geometry.extracted_camera; + } + let Some((pipeline, 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)), - }, - ); transparent_phase.add_transient(TransparentUi { draw_function, - pipeline, + pipeline: *pipeline, entity: (*render_entity, *main_entity), - sort_key: FloatOrd(overlay.z_order), + sort_key: FloatOrd(extracted_overlays.stack_offset + geometry.stack_index as f32), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, indexed: true, }); } } + +pub fn prepare_debug_overlay() {} diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 55573bcbda41b..996d620df28d6 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -214,7 +214,7 @@ impl Plugin for UiRenderPlugin { }; #[cfg(feature = "bevy_ui_debug")] - render_app.init_resource::(); + render_app.init_resource::(); render_app .init_gpu_resource::>() @@ -920,7 +920,7 @@ fn prepare_uinodes( extracted_uinodes: Res, extracted_glyph_layouts: Res, #[cfg(feature = "bevy_ui_debug")] extracted_debug_overlays: Res< - debug_overlay::ExtractedUiDebugOverlays, + debug_overlay::ExtractedUiDebugOptions, >, extracted_geometry: Res, view_uniforms: Res, @@ -1174,7 +1174,7 @@ fn prepare_uinodes( #[cfg(feature = "bevy_ui_debug")] if let Some(style) = extracted_debug_overlays - .overlays + .local .get(&main_entity) .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) { From b58d835a20280be73452c6691d469c7f583e5696 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 09:18:05 +0100 Subject: [PATCH 48/99] One phase item per camera debug overlay --- crates/bevy_ui_render/src/debug_overlay.rs | 356 +++++++++++++++++---- crates/bevy_ui_render/src/lib.rs | 83 +---- 2 files changed, 305 insertions(+), 134 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index ed3125374f2d4..62ebcfa54c48a 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -10,7 +10,7 @@ use bevy_camera::visibility::InheritedVisibility; use bevy_color::{ColorToComponents, Hsla, LinearRgba}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - entity::{hash_map, Entity}, + entity::{hash_map, Entity, EntityHashMap}, lifecycle::RemovedComponents, prelude::*, query::{Changed, EcsAccessLevel}, @@ -33,7 +33,7 @@ use bevy_render::{ use bevy_sprite::BorderRect; use bevy_ui::{ ui_transform::UiGlobalTransform, CalculatedClip, ComputedNode, ComputedStackIndex, - ComputedUiTargetCamera, ResolvedBorderRadius, UiStack, + ComputedUiTargetCamera, ResolvedBorderRadius, UiStack, UiTargetCamera, }; /// Configuration for the UI debug overlay @@ -106,11 +106,15 @@ impl From for GlobalUiDebugOptions { Self(other) } } + +/// The debug visualization is just outlines, so it can be extracted as a single phase item for each extracted camera. #[derive(Resource, Default)] pub struct ExtractedUiDebugOptions { - pub stack_offset: f32, - pub global: UiDebugOptions, - pub local: MainEntityHashMap<(Entity, UiDebugOptions)>, + /// z_index of whole visualization. + pub z_offset: f32, + pub global_debug_options: UiDebugOptions, + /// Map from extracted view -> (render entity for debug phase item, Node MainEntity -> UiDebugOptions) + pub local_debug_options: MainEntityHashMap, } pub fn extract_debug_overlay( @@ -122,10 +126,10 @@ pub fn extract_debug_overlay( mut nodes_processed_this_frame: Local, ui_stack: Res, ) { - extracted_debug_options.stack_offset = ui_stack.uinodes.len() as f32; + extracted_debug_options.z_offset = ui_stack.uinodes.len() as f32; nodes_processed_this_frame.clear(); - extracted_debug_options.global = global_debug_options.0.clone(); + extracted_debug_options.global_debug_options = global_debug_options.0.clone(); // iter through all nodes with UiDebugOptions // add to processed this frame list, so they aren't removed if tagged by removal detection @@ -133,74 +137,53 @@ pub fn extract_debug_overlay( let main_entity = MainEntity::from(entity); nodes_processed_this_frame.insert(main_entity); - match extracted_debug_options.local.entry(main_entity) { - bevy_platform::collections::hash_map::Entry::Occupied(mut entry) => { - entry.get_mut().1 = local_debug_options.clone(); - } - bevy_platform::collections::hash_map::Entry::Vacant(entry) => { - entry.insert((commands.spawn_empty().id(), local_debug_options.clone())); - } - } + extracted_debug_options + .local_debug_options + .insert(main_entity, local_debug_options.clone()); } for main_entity in removed_debug_options.read().map(MainEntity::from) { if nodes_processed_this_frame.contains(&main_entity) { continue; } - if let Some((render_entity, _)) = extracted_debug_options.local.remove(&main_entity) { - commands.entity(render_entity).despawn(); - } + extracted_debug_options + .local_debug_options + .remove(&main_entity); } } pub fn queue_debug_overlay( extracted_overlays: Res, - extracted_layout: Res, ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, - render_views: Query<(&UiCameraView, Option<&UiAntiAlias>), With>, + render_views: Query<(Entity, &UiCameraView, Option<&UiAntiAlias>), With>, camera_views: Query<&ExtractedView>, pipeline_cache: Res, draw_functions: Res>, ) { let draw_function = draw_functions.read().id::(); - let mut current_camera_entity = Entity::PLACEHOLDER; - let mut current_phase = None; - for (main_entity, (render_entity, overlay)) in &extracted_overlays.local { - let Some(geometry) = extracted_layout.layout.get(main_entity) else { - continue; - }; + for (entity, default_camera_view, ui_anti_alias) in render_views.iter() { + let mut current_phase = camera_views + .get(default_camera_view.0) + .ok() + .and_then(|view| { + transparent_render_phases + .get_mut(&view.retained_view_entity) + .map(|transparent_phase| { + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_pipeline, + UiPipelineKey { + target_format: view.target_format, + anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), + }, + ); + (pipeline, transparent_phase) + }) + }); - if current_camera_entity != geometry.extracted_camera { - current_phase = render_views.get(geometry.extracted_camera).ok().and_then( - |(default_camera_view, ui_anti_alias)| { - camera_views - .get(default_camera_view.0) - .ok() - .and_then(|view| { - transparent_render_phases - .get_mut(&view.retained_view_entity) - .map(|transparent_phase| { - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_pipeline, - UiPipelineKey { - target_format: view.target_format, - anti_alias: matches!( - ui_anti_alias, - None | Some(UiAntiAlias::On) - ), - }, - ); - (pipeline, transparent_phase) - }) - }) - }, - ); - current_camera_entity = geometry.extracted_camera; - } let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { continue; }; @@ -208,8 +191,8 @@ pub fn queue_debug_overlay( transparent_phase.add_transient(TransparentUi { draw_function, pipeline: *pipeline, - entity: (*render_entity, *main_entity), - sort_key: FloatOrd(extracted_overlays.stack_offset + geometry.stack_index as f32), + entity: (entity, entity.into()), + sort_key: FloatOrd(extracted_overlays.z_offset), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, indexed: true, @@ -217,4 +200,263 @@ pub fn queue_debug_overlay( } } -pub fn prepare_debug_overlay() {} +/// Debug layout can just +fn generate_debug_layout_quads( + ui_meta: &mut UiMeta, + layout: &ExtractedUiNodeLayout, + style: &ExtractedUiNodeStyle, + image: AssetId, + gpu_image: &GpuImage, +) { + let uinode = &layout.uinode; + let mut push_quad = |color: LinearRgba, + mut rect: Rect, + atlas_scaling: Option, + transform: Affine2, + clip: Option, + border_radius: ResolvedBorderRadius, + border: BorderRect, + node_type: NodeType, + textured: bool| { + let mut flags = if textured { + shader_flags::TEXTURED + } else { + shader_flags::UNTEXTURED + }; + let rect_size = rect.size(); + let positions = + QUAD_VERTEX_POSITIONS.map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + let mut positions_diff = if let Some(clip) = clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = transform.transform_vector2(rect_size).abs(); + if transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + return; + } + let uvs = if textured { + let atlas_extent = atlas_scaling + .map(|scaling| gpu_image.size_2d().as_vec2() * scaling) + .unwrap_or(rect.max); + if style.flip_x { + mem::swap(&mut rect.max.x, &mut rect.min.x); + positions_diff[0].x *= -1.; + positions_diff[1].x *= -1.; + positions_diff[2].x *= -1.; + positions_diff[3].x *= -1.; + } + if style.flip_y { + mem::swap(&mut rect.max.y, &mut rect.min.y); + positions_diff[0].y *= -1.; + positions_diff[1].y *= -1.; + positions_diff[2].y *= -1.; + positions_diff[3].y *= -1.; + } + [ + Vec2::new( + rect.min.x + positions_diff[0].x, + rect.min.y + positions_diff[0].y, + ), + Vec2::new( + rect.max.x + positions_diff[1].x, + rect.min.y + positions_diff[1].y, + ), + Vec2::new( + rect.max.x + positions_diff[2].x, + rect.max.y + positions_diff[2].y, + ), + Vec2::new( + rect.min.x + positions_diff[3].x, + rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent) + } else { + [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] + }; + match node_type { + NodeType::Border(border_flags) => flags |= border_flags, + NodeType::Inverted => flags |= INVERT, + NodeType::Rect => {} + } + + let vertex_start = ui_meta.vertices.len() as u32; + let color = color.to_f32_array(); + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + flags: flags | shader_flags::CORNERS[i], + radius: border_radius.into(), + border: [ + border.min_inset.x, + border.min_inset.y, + border.max_inset.x, + border.max_inset.y, + ], + size: rect_size.into(), + point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), + }); + } + for &index in &QUAD_INDICES { + ui_meta.indices.push(vertex_start + index as u32); + } + }; + + if !style.background_color.is_fully_transparent() { + push_quad( + style.background_color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + layout.transform, + layout.clip, + uinode.border_radius(), + uinode.border(), + NodeType::Rect, + false, + ); + } + if !style.outer_color.is_fully_transparent() { + push_quad( + style.outer_color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + layout.transform, + layout.clip, + uinode.border_radius(), + BorderRect::ZERO, + NodeType::Inverted, + false, + ); + } + + if uinode.border() != BorderRect::ZERO { + const BORDER_FLAGS: [u32; 4] = [ + shader_flags::BORDER_TOP, + shader_flags::BORDER_RIGHT, + shader_flags::BORDER_BOTTOM, + shader_flags::BORDER_LEFT, + ]; + let mut completed_flags = 0; + + for (i, &color) in style.border_color.iter().enumerate() { + if color.is_fully_transparent() || completed_flags & BORDER_FLAGS[i] != 0 { + continue; + } + + let mut border_flags = BORDER_FLAGS[i]; + for (j, &other_color) in style.border_color.iter().enumerate().skip(i + 1) { + if color == other_color { + border_flags |= BORDER_FLAGS[j]; + } + } + completed_flags |= border_flags; + + push_quad( + color, + Rect { + min: Vec2::ZERO, + max: uinode.size(), + }, + None, + layout.transform, + layout.clip, + uinode.border_radius(), + uinode.border(), + NodeType::Border(border_flags), + false, + ); + } + } + + if !style.outline_color.is_fully_transparent() && uinode.outline_width() > 0. { + push_quad( + style.outline_color, + Rect { + min: Vec2::ZERO, + max: uinode.outlined_node_size(), + }, + None, + layout.transform, + layout.clip, + uinode.outline_radius(), + BorderRect::all(uinode.outline_width()), + NodeType::Border(shader_flags::BORDER_ALL), + false, + ); + } + + if style.image == Some(image) { + let visual_box = match style.visual_box { + VisualBox::ContentBox => uinode.content_box(), + VisualBox::PaddingBox => uinode.padding_box(), + VisualBox::BorderBox => uinode.border_box(), + }; + if !visual_box.size().cmple(Vec2::ZERO).any() { + let size = if style.auto_sized && !style.image_size.cmple(Vec2::ZERO).any() { + style.image_size * (visual_box.size() / style.image_size).min_element() + } else { + visual_box.size() + }; + let mut rect = style.image_rect.unwrap_or(Rect { + min: Vec2::ZERO, + max: size, + }); + let atlas_scaling = style.image_rect.map(|_| { + let atlas_scaling = size / rect.size(); + rect.min *= atlas_scaling; + rect.max *= atlas_scaling; + atlas_scaling + }); + push_quad( + style.image_color, + rect, + atlas_scaling, + layout.transform * Affine2::from_translation(visual_box.center()), + layout.clip, + uinode.border_radius(), + if style.use_node_border { + uinode.border() + } else { + BorderRect::ZERO + }, + NodeType::Rect, + true, + ); + } + } +} diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 996d620df28d6..80b407da4abe0 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -610,6 +610,7 @@ pub struct CachedUiViewData { /// The unique, stable identifier for the view across frames. retained_view_entity: RetainedViewEntity, } +pub struct UiExtractedView; /// Extracts all UI elements associated with a camera into the render world. pub fn extract_ui_camera_view( @@ -826,7 +827,7 @@ pub mod shader_flags { pub fn queue_uinodes( extracted_uinodes: Res, - extracted_geometry: Res, + extracted_layout: Res, ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -840,11 +841,11 @@ pub fn queue_uinodes( let mut current_phase = None; for (main_entity, (render_entity, _)) in extracted_uinodes.uinodes.iter() { - let Some(geometry) = extracted_geometry.layout.get(main_entity) else { + let Some(layout) = extracted_layout.layout.get(main_entity) else { continue; }; - let extracted_camera_entity = geometry.extracted_camera; + let extracted_camera_entity = layout.extracted_camera; if current_camera_entity != extracted_camera_entity { current_phase = render_views.get(extracted_camera_entity).ok().and_then( @@ -883,7 +884,7 @@ pub fn queue_uinodes( draw_function, pipeline: *pipeline, entity: (*render_entity, *main_entity), - sort_key: FloatOrd(geometry.stack_index as f32), + sort_key: FloatOrd(layout.stack_index as f32), // batch_range will be calculated in prepare_uinodes batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, @@ -1174,7 +1175,7 @@ fn prepare_uinodes( #[cfg(feature = "bevy_ui_debug")] if let Some(style) = extracted_debug_overlays - .local + .local_debug_options .get(&main_entity) .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) { @@ -1684,75 +1685,3 @@ fn generate_text_quads( texture_ranges.push((texture_range_start..range_end, current_texture)); } } - -/// generate quads from extracted layout and debug options -#[cfg(feature = "bevy_ui_debug")] -fn generate_debug_quads(ui_meta: &mut UiMeta, style: &debug_overlay::ExtractedUiDebugOverlay) { - let color = style.color.to_f32_array(); - for (rect, radius) in &style.outlines { - let rect_size = rect.size(); - let transform = style.transform * Affine2::from_translation(rect.center()); - let positions = QUAD_VERTEX_POSITIONS - .map(|position| transform.transform_point2(position * rect_size).extend(0.)); - let positions_diff = if let Some(clip) = style.clip { - [ - Vec2::new( - f32::max(clip.min.x - positions[0].x, 0.), - f32::max(clip.min.y - positions[0].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[1].x, 0.), - f32::max(clip.min.y - positions[1].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[2].x, 0.), - f32::min(clip.max.y - positions[2].y, 0.), - ), - Vec2::new( - f32::max(clip.min.x - positions[3].x, 0.), - f32::min(clip.max.y - positions[3].y, 0.), - ), - ] - } else { - [Vec2::ZERO; 4] - }; - let positions_clipped = [ - positions[0] + positions_diff[0].extend(0.), - positions[1] + positions_diff[1].extend(0.), - positions[2] + positions_diff[2].extend(0.), - positions[3] + positions_diff[3].extend(0.), - ]; - let transformed_rect_size = transform.transform_vector2(rect_size).abs(); - if transform.x_axis[1] == 0.0 - && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x - || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) - { - continue; - } - - let vertex_start = ui_meta.vertices.len() as u32; - let points = QUAD_VERTEX_POSITIONS.map(|position| position * rect_size); - for i in 0..4 { - ui_meta.vertices.push(UiVertex { - position: positions_clipped[i].into(), - uv: [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y][i].into(), - color, - flags: shader_flags::UNTEXTURED - | shader_flags::BORDER_ALL - | shader_flags::CORNERS[i], - radius: (*radius).into(), - border: [ - style.border.min_inset.x, - style.border.min_inset.y, - style.border.max_inset.x, - style.border.max_inset.y, - ], - size: rect_size.into(), - point: (points[i] + positions_diff[i]).into(), - }); - } - for &index in &QUAD_INDICES { - ui_meta.indices.push(vertex_start + index as u32); - } - } -} From 89ed5b9bebec2668bb760a3a7f0ecdbf362684c6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 10:22:01 +0100 Subject: [PATCH 49/99] Renamed UiDebugOptions -> UiDebugOutline Extract debug options per node. Queue one phase item per debug layer. Skeleton for new rendering. --- crates/bevy_ui_render/src/debug_overlay.rs | 438 ++++++++------------ crates/bevy_ui_render/src/extract_layout.rs | 35 +- crates/bevy_ui_render/src/lib.rs | 86 ++-- examples/testbed/ui.rs | 20 +- examples/ui/images/image_node_resizing.rs | 2 +- examples/ui/scroll_and_overflow/scroll.rs | 2 +- 6 files changed, 232 insertions(+), 351 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 62ebcfa54c48a..c5a91c22d571b 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -1,9 +1,9 @@ use std::arch::global_asm; use crate::{ - extract_layout::ExtractedUiLayout, shader_flags, DrawUi, ImageNodeBindGroups, TransparentUi, - UiAntiAlias, UiBatch, UiCameraMap, UiCameraView, UiMeta, UiPipeline, UiPipelineKey, UiVertex, - QUAD_INDICES, QUAD_VERTEX_POSITIONS, + extract_layout::ExtractedUiLayout, shader_flags, DrawUi, ImageNodeBindGroups, NodeType, + TransparentUi, UiAntiAlias, UiBatch, UiCameraMap, UiCameraView, UiMeta, UiPipeline, + UiPipelineKey, UiVertex, QUAD_INDICES, QUAD_UVS, QUAD_VERTEX_POSITIONS, }; use bevy_asset::AssetId; use bevy_camera::visibility::InheritedVisibility; @@ -39,10 +39,10 @@ use bevy_ui::{ /// Configuration for the UI debug overlay /// /// Can be added as a `Component` to individual UI node entities. -/// This overwrites the default [`GlobalUiDebugOptions`] resource. +/// This overwrites the default [`GlobalUiDebugOutline`] resource. #[derive(Component, Reflect, Clone)] #[reflect(Component)] -pub struct UiDebugOptions { +pub struct UiDebugOutline { /// Set to true to enable the UI debug overlay pub enabled: bool, /// Show outlines for the border boxes of UI nodes @@ -65,13 +65,13 @@ pub struct UiDebugOptions { pub ignore_border_radius: bool, } -impl UiDebugOptions { +impl UiDebugOutline { pub fn toggle(&mut self) { self.enabled = !self.enabled; } } -impl Default for UiDebugOptions { +impl Default for UiDebugOutline { fn default() -> Self { Self { enabled: false, @@ -88,7 +88,7 @@ impl Default for UiDebugOptions { } } -impl From for UiDebugOptions { +impl From for UiDebugOutline { fn from(other: GlobalUiDebugOptions) -> Self { other.0.clone() } @@ -99,61 +99,68 @@ impl From for UiDebugOptions { /// A global `resource` that can be overridden by local component [`UiDebugOptions`] override on individual UI node entities #[derive(Default, Resource, Reflect, Clone, Deref, DerefMut)] #[reflect(Resource)] -pub struct GlobalUiDebugOptions(pub UiDebugOptions); +pub struct GlobalUiDebugOptions(pub UiDebugOutline); -impl From for GlobalUiDebugOptions { - fn from(other: UiDebugOptions) -> Self { +impl From for GlobalUiDebugOptions { + fn from(other: UiDebugOutline) -> Self { Self(other) } } /// The debug visualization is just outlines, so it can be extracted as a single phase item for each extracted camera. #[derive(Resource, Default)] -pub struct ExtractedUiDebugOptions { +pub struct ExtractedUiDebugLayer { /// z_index of whole visualization. pub z_offset: f32, - pub global_debug_options: UiDebugOptions, - /// Map from extracted view -> (render entity for debug phase item, Node MainEntity -> UiDebugOptions) - pub local_debug_options: MainEntityHashMap, + pub default_outline: UiDebugOutline, + pub per_node_outline: MainEntityHashMap, + pub changed_this_frame: MainEntityHashSet, +} + +impl ExtractedUiDebugLayer { + pub fn get(&self, main_entity: &MainEntity) -> &UiDebugOutline { + &self + .per_node_outline + .get(main_entity) + .unwrap_or(&self.default_outline) + } } pub fn extract_debug_overlay( - mut commands: Commands, global_debug_options: Extract>, - mut extracted_debug_options: ResMut, - ui_debug_options_query: Extract>>, - mut removed_debug_options: Extract>, - mut nodes_processed_this_frame: Local, + mut extracted_debug_layer: ResMut, + ui_debug_outlines_query: Extract>>, + mut removed_debug_options: Extract>, ui_stack: Res, ) { - extracted_debug_options.z_offset = ui_stack.uinodes.len() as f32; - nodes_processed_this_frame.clear(); - - extracted_debug_options.global_debug_options = global_debug_options.0.clone(); + extracted_debug_layer.changed_this_frame.clear(); + extracted_debug_layer.z_offset = ui_stack.uinodes.len() as f32; + extracted_debug_layer.default_outline = global_debug_options.0.clone(); // iter through all nodes with UiDebugOptions // add to processed this frame list, so they aren't removed if tagged by removal detection - for (entity, local_debug_options) in ui_debug_options_query.iter() { + for (entity, debug_outlines) in ui_debug_outlines_query.iter() { let main_entity = MainEntity::from(entity); - nodes_processed_this_frame.insert(main_entity); + extracted_debug_layer.changed_this_frame.insert(main_entity); - extracted_debug_options - .local_debug_options - .insert(main_entity, local_debug_options.clone()); + extracted_debug_layer + .per_node_outline + .insert(main_entity, debug_outlines.clone()); } for main_entity in removed_debug_options.read().map(MainEntity::from) { - if nodes_processed_this_frame.contains(&main_entity) { + if extracted_debug_layer + .changed_this_frame + .contains(&main_entity) + { continue; } - extracted_debug_options - .local_debug_options - .remove(&main_entity); + extracted_debug_layer.per_node_outline.remove(&main_entity); } } pub fn queue_debug_overlay( - extracted_overlays: Res, + extracted_overlays: Res, ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -200,262 +207,143 @@ pub fn queue_debug_overlay( } } -/// Debug layout can just -fn generate_debug_layout_quads( +/// Clip each debug outline quad, compute its vertices, and push them onto the vertex buffer +pub fn push_debug_outline( + rect: Rect, + line_color: LinearRgba, + line_width: f32, + border_radius: ResolvedBorderRadius, + clip: Option, + transform: Affine2, ui_meta: &mut UiMeta, - layout: &ExtractedUiNodeLayout, - style: &ExtractedUiNodeStyle, - image: AssetId, - gpu_image: &GpuImage, ) { - let uinode = &layout.uinode; - let mut push_quad = |color: LinearRgba, - mut rect: Rect, - atlas_scaling: Option, - transform: Affine2, - clip: Option, - border_radius: ResolvedBorderRadius, - border: BorderRect, - node_type: NodeType, - textured: bool| { - let mut flags = if textured { - shader_flags::TEXTURED - } else { - shader_flags::UNTEXTURED - }; - let rect_size = rect.size(); - let positions = - QUAD_VERTEX_POSITIONS.map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); - let mut positions_diff = if let Some(clip) = clip { - [ - Vec2::new( - f32::max(clip.min.x - positions[0].x, 0.), - f32::max(clip.min.y - positions[0].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[1].x, 0.), - f32::max(clip.min.y - positions[1].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[2].x, 0.), - f32::min(clip.max.y - positions[2].y, 0.), - ), - Vec2::new( - f32::max(clip.min.x - positions[3].x, 0.), - f32::min(clip.max.y - positions[3].y, 0.), - ), - ] - } else { - [Vec2::ZERO; 4] - }; - let positions_clipped = [ - positions[0] + positions_diff[0].extend(0.), - positions[1] + positions_diff[1].extend(0.), - positions[2] + positions_diff[2].extend(0.), - positions[3] + positions_diff[3].extend(0.), - ]; - let transformed_rect_size = transform.transform_vector2(rect_size).abs(); - if transform.x_axis[1] == 0.0 - && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x - || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) - { - return; - } - let uvs = if textured { - let atlas_extent = atlas_scaling - .map(|scaling| gpu_image.size_2d().as_vec2() * scaling) - .unwrap_or(rect.max); - if style.flip_x { - mem::swap(&mut rect.max.x, &mut rect.min.x); - positions_diff[0].x *= -1.; - positions_diff[1].x *= -1.; - positions_diff[2].x *= -1.; - positions_diff[3].x *= -1.; - } - if style.flip_y { - mem::swap(&mut rect.max.y, &mut rect.min.y); - positions_diff[0].y *= -1.; - positions_diff[1].y *= -1.; - positions_diff[2].y *= -1.; - positions_diff[3].y *= -1.; - } - [ - Vec2::new( - rect.min.x + positions_diff[0].x, - rect.min.y + positions_diff[0].y, - ), - Vec2::new( - rect.max.x + positions_diff[1].x, - rect.min.y + positions_diff[1].y, - ), - Vec2::new( - rect.max.x + positions_diff[2].x, - rect.max.y + positions_diff[2].y, - ), - Vec2::new( - rect.min.x + positions_diff[3].x, - rect.max.y + positions_diff[3].y, - ), - ] - .map(|pos| pos / atlas_extent) - } else { - [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] - }; - match node_type { - NodeType::Border(border_flags) => flags |= border_flags, - NodeType::Inverted => flags |= INVERT, - NodeType::Rect => {} - } - - let vertex_start = ui_meta.vertices.len() as u32; - let color = color.to_f32_array(); - for i in 0..4 { - ui_meta.vertices.push(UiVertex { - position: positions_clipped[i].into(), - uv: uvs[i].into(), - color, - flags: flags | shader_flags::CORNERS[i], - radius: border_radius.into(), - border: [ - border.min_inset.x, - border.min_inset.y, - border.max_inset.x, - border.max_inset.y, - ], - size: rect_size.into(), - point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), - }); - } - for &index in &QUAD_INDICES { - ui_meta.indices.push(vertex_start + index as u32); - } + let rect_size = rect.size(); + let positions = + QUAD_VERTEX_POSITIONS.map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + let positions_diff = if let Some(clip) = clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = transform.transform_vector2(rect_size).abs(); + if transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + return; + } + + let flags = shader_flags::UNTEXTURED | shader_flags::BORDER_ALL; + let vertex_start = ui_meta.vertices.len() as u32; + let color = line_color.to_f32_array(); - if !style.background_color.is_fully_transparent() { - push_quad( - style.background_color, - Rect { - min: Vec2::ZERO, - max: uinode.size(), - }, - None, - layout.transform, - layout.clip, - uinode.border_radius(), - uinode.border(), - NodeType::Rect, - false, - ); + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: QUAD_UVS[i].into(), + color, + flags: flags | shader_flags::CORNERS[i], + radius: border_radius.into(), + border: [line_width, line_width, line_width, line_width], + size: rect_size.into(), + point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), + }); } - if !style.outer_color.is_fully_transparent() { - push_quad( - style.outer_color, - Rect { - min: Vec2::ZERO, - max: uinode.size(), - }, - None, - layout.transform, - layout.clip, - uinode.border_radius(), - BorderRect::ZERO, - NodeType::Inverted, - false, - ); + for &index in &QUAD_INDICES { + ui_meta.indices.push(vertex_start + index as u32); } +} - if uinode.border() != BorderRect::ZERO { - const BORDER_FLAGS: [u32; 4] = [ - shader_flags::BORDER_TOP, - shader_flags::BORDER_RIGHT, - shader_flags::BORDER_BOTTOM, - shader_flags::BORDER_LEFT, - ]; - let mut completed_flags = 0; +/// Debug layout can just +fn generate_debug_layout_quads( + ui_meta: &mut UiMeta, + extracted_ui_layout: &ExtractedUiLayout, + extracted_ui_debug_layer: &ExtractedUiDebugLayer, +) { + if !extracted_ui_debug_layer.default_outline.enabled + && extracted_ui_debug_layer.per_node_outline.is_empty() + { + return; + } - for (i, &color) in style.border_color.iter().enumerate() { - if color.is_fully_transparent() || completed_flags & BORDER_FLAGS[i] != 0 { - continue; - } + for (main_entity, layout) in extracted_ui_layout.layout.iter() { + let debug_outline = extracted_ui_debug_layer.get(main_entity); + if !debug_outline.enabled { + continue; + } - let mut border_flags = BORDER_FLAGS[i]; - for (j, &other_color) in style.border_color.iter().enumerate().skip(i + 1) { - if color == other_color { - border_flags |= BORDER_FLAGS[j]; - } - } - completed_flags |= border_flags; + let line_color = debug_outline + .line_color_override + .unwrap_or_else(|| Hsla::sequential_dispersed(main_entity.index_u32()).into()); - push_quad( - color, - Rect { - min: Vec2::ZERO, - max: uinode.size(), - }, - None, - layout.transform, - layout.clip, - uinode.border_radius(), - uinode.border(), - NodeType::Border(border_flags), - false, + if debug_outline.outline_border_box { + push_debug_outline( + rect, + line_color, + debug_outline.line_width, + border_radius, + clip, + transform, + ui_meta, ); } - } - if !style.outline_color.is_fully_transparent() && uinode.outline_width() > 0. { - push_quad( - style.outline_color, - Rect { - min: Vec2::ZERO, - max: uinode.outlined_node_size(), - }, - None, - layout.transform, - layout.clip, - uinode.outline_radius(), - BorderRect::all(uinode.outline_width()), - NodeType::Border(shader_flags::BORDER_ALL), - false, - ); - } + if debug_outline.outline_content_box { + push_debug_outline( + rect, + line_color, + debug_outline.line_width, + border_radius, + clip, + transform, + ui_meta, + ); + } - if style.image == Some(image) { - let visual_box = match style.visual_box { - VisualBox::ContentBox => uinode.content_box(), - VisualBox::PaddingBox => uinode.padding_box(), - VisualBox::BorderBox => uinode.border_box(), - }; - if !visual_box.size().cmple(Vec2::ZERO).any() { - let size = if style.auto_sized && !style.image_size.cmple(Vec2::ZERO).any() { - style.image_size * (visual_box.size() / style.image_size).min_element() - } else { - visual_box.size() - }; - let mut rect = style.image_rect.unwrap_or(Rect { - min: Vec2::ZERO, - max: size, - }); - let atlas_scaling = style.image_rect.map(|_| { - let atlas_scaling = size / rect.size(); - rect.min *= atlas_scaling; - rect.max *= atlas_scaling; - atlas_scaling - }); - push_quad( - style.image_color, + if debug_outline.outline_padding_box { + push_debug_outline( + rect, + line_color, + debug_outline.line_width, + border_radius, + clip, + transform, + ui_meta, + ); + } + + if debug_outline.outline_scrollbars { + push_debug_outline( rect, - atlas_scaling, - layout.transform * Affine2::from_translation(visual_box.center()), - layout.clip, - uinode.border_radius(), - if style.use_node_border { - uinode.border() - } else { - BorderRect::ZERO - }, - NodeType::Rect, - true, + line_color, + debug_outline.line_width, + border_radius, + clip, + transform, + ui_meta, ); } } diff --git a/crates/bevy_ui_render/src/extract_layout.rs b/crates/bevy_ui_render/src/extract_layout.rs index 52f552f91fc7f..6e32db34c4482 100644 --- a/crates/bevy_ui_render/src/extract_layout.rs +++ b/crates/bevy_ui_render/src/extract_layout.rs @@ -28,6 +28,7 @@ pub struct ExtractedUiNodeLayout { pub flip_x: bool, pub flip_y: bool, pub stack_index: u32, + pub is_visible: bool, } /// Uinode geometries and list of geometries that changed this frame. @@ -43,7 +44,7 @@ pub struct ExtractedUiLayout { } pub fn extract_ui_layout( - mut extracted_geometries: bevy_ecs::system::ResMut, + mut extracted_ui_layout: bevy_ecs::system::ResMut, camera_map: Extract, changed_geometry_query: Extract< Query< @@ -97,9 +98,9 @@ pub fn extract_ui_layout( // Probably need to consolidate changed & removed sets somehow for efficiency // Do we also need to track added? // Changed list is cleared each frame, then repopulated - extracted_geometries.changed.clear(); + extracted_ui_layout.changed.clear(); // Removed list is cleared each frame, then repopulated - extracted_geometries.removed.clear(); + extracted_ui_layout.removed.clear(); let mut camera_mapper = camera_map.get_mapper(); @@ -118,26 +119,15 @@ pub fn extract_ui_layout( .filter_map(|entity| unfiltered_geometry_query.get(entity).ok()), ) { let main_entity = entity.into(); - extracted_geometries.changed.insert(main_entity); + extracted_ui_layout.changed.insert(main_entity); // Find extracted camera entity, otherwise remove both camera and UI node entity. let Some(extracted_camera_entity) = camera_mapper.map(computed_target) else { - extracted_geometries.layout.remove(&main_entity); - extracted_geometries.removed.insert(main_entity); + extracted_ui_layout.layout.remove(&main_entity); + extracted_ui_layout.removed.insert(main_entity); continue; }; - // Remove non-visible from all maps - if !inherited_visibility.get() { - extracted_geometries.layout.remove(&main_entity); - extracted_geometries.removed.insert(main_entity); - extracted_geometries - .extracted_camera_to_main_uinode_map - .get_mut(&extracted_camera_entity) - .map(|main_entities| main_entities.remove(&main_entity)); - continue; - } - let uinode_geometry = ExtractedUiNodeLayout { extracted_camera: extracted_camera_entity, uinode: *computed_node, @@ -147,13 +137,14 @@ pub fn extract_ui_layout( flip_x: false, flip_y: false, stack_index: stack_index.0, + is_visible: inherited_visibility.get(), }; let ExtractedUiLayout { extracted_camera_to_main_uinode_map, layout: uinode_geometries, .. - } = &mut *extracted_geometries; + } = &mut *extracted_ui_layout; match uinode_geometries.entry(main_entity) { bevy_platform::collections::hash_map::Entry::Occupied(mut entry) => { @@ -196,12 +187,12 @@ pub fn extract_ui_layout( let main_entity = entity.into(); // If already in changed, entity passed the geometry query so the component must have been removed and reinserted in the same frame, so do nothing. - if extracted_geometries.changed.insert(main_entity) { + if extracted_ui_layout.changed.insert(main_entity) { // Failed the geometry queries, does not have at least one of the components (if reinserted would be `Changed`). // Add to changed list and remove from maps - if let Some(extracted_geometry) = extracted_geometries.layout.remove(&main_entity) { - extracted_geometries.removed.insert(main_entity); - extracted_geometries + if let Some(extracted_geometry) = extracted_ui_layout.layout.remove(&main_entity) { + extracted_ui_layout.removed.insert(main_entity); + extracted_ui_layout .extracted_camera_to_main_uinode_map .get_mut(&extracted_geometry.extracted_camera) .map(|main_entities| main_entities.remove(&main_entity)); diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 80b407da4abe0..2ca0220ea524f 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -62,7 +62,7 @@ use bevy_render::{ }; use bevy_sprite::BorderRect; #[cfg(feature = "bevy_ui_debug")] -pub use debug_overlay::{GlobalUiDebugOptions, UiDebugOptions}; +pub use debug_overlay::{GlobalUiDebugOptions, UiDebugOutline}; use gradient::GradientPlugin; @@ -84,7 +84,7 @@ use crate::text::{extract_text, queue_text, ExtractedGlyphLayout, ExtractedGlyph pub mod prelude { #[cfg(feature = "bevy_ui_debug")] - pub use crate::debug_overlay::{GlobalUiDebugOptions, UiDebugOptions}; + pub use crate::debug_overlay::{GlobalUiDebugOptions, UiDebugOutline}; pub use crate::{ ui_material::*, ui_material_pipeline::UiMaterialPlugin, BoxShadowSamples, UiAntiAlias, @@ -214,7 +214,7 @@ impl Plugin for UiRenderPlugin { }; #[cfg(feature = "bevy_ui_debug")] - render_app.init_resource::(); + render_app.init_resource::(); render_app .init_gpu_resource::>() @@ -796,6 +796,8 @@ pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ Vec2::new(-0.5, 0.5), ]; +pub(crate) const QUAD_UVS: [Vec2; 4] = [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]; + pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; #[derive(Component, Debug)] @@ -921,7 +923,7 @@ fn prepare_uinodes( extracted_uinodes: Res, extracted_glyph_layouts: Res, #[cfg(feature = "bevy_ui_debug")] extracted_debug_overlays: Res< - debug_overlay::ExtractedUiDebugOptions, + debug_overlay::ExtractedUiDebugLayer, >, extracted_geometry: Res, view_uniforms: Res, @@ -1173,44 +1175,44 @@ fn prepare_uinodes( batch_image_handle = None; - #[cfg(feature = "bevy_ui_debug")] - if let Some(style) = extracted_debug_overlays - .local_debug_options - .get(&main_entity) - .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) - { - let image = AssetId::::default(); - let Some(gpu_image) = gpu_images.get(image) else { - continue; - }; - image_bind_groups.values.entry(image).or_insert_with(|| { - render_device.create_bind_group( - "ui_debug_overlay_image_bind_group", - &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential(( - &gpu_image.texture_view, - &gpu_image.sampler, - )), - ) - }); - - let range_start = ui_meta.indices.len() as u32; - generate_debug_quads(&mut ui_meta, style); - let range_end = ui_meta.indices.len() as u32; - if range_start == range_end { - continue; - } - ui_phase.items[phase_item_index].batch_range = - phase_item_index as u32..phase_item_index as u32 + 1; - batches.push(( - entity, - UiBatch { - range: range_start..range_end, - image, - texture_ranges: vec![], - }, - )); - } + // #[cfg(feature = "bevy_ui_debug")] + // if let Some(style) = extracted_debug_overlays + // .per_node_debug_options + // .get(&main_entity) + // .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) + // { + // let image = AssetId::::default(); + // let Some(gpu_image) = gpu_images.get(image) else { + // continue; + // }; + // image_bind_groups.values.entry(image).or_insert_with(|| { + // render_device.create_bind_group( + // "ui_debug_overlay_image_bind_group", + // &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + // &BindGroupEntries::sequential(( + // &gpu_image.texture_view, + // &gpu_image.sampler, + // )), + // ) + // }); + + // let range_start = ui_meta.indices.len() as u32; + // generate_debug_quads(&mut ui_meta, style); + // let range_end = ui_meta.indices.len() as u32; + // if range_start == range_end { + // continue; + // } + // ui_phase.items[phase_item_index].batch_range = + // phase_item_index as u32..phase_item_index as u32 + 1; + // batches.push(( + // entity, + // UiBatch { + // range: range_start..range_end, + // image, + // texture_ranges: vec![], + // }, + // )); + // } } } diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 2bb33561c70d0..f510847a153ba 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -2242,7 +2242,7 @@ mod debug_outlines { use bevy::{ color::palettes::css::{BLUE, GRAY, RED}, prelude::*, - ui_render::UiDebugOptions, + ui_render::UiDebugOutline, }; pub fn setup(mut commands: Commands, mut debug_options: ResMut) { @@ -2252,7 +2252,7 @@ mod debug_outlines { debug_options.show_hidden = true; debug_options.show_clipped = true; - let debug_options: UiDebugOptions = (*debug_options.as_ref()).into(); + let debug_options: UiDebugOutline = (*debug_options.as_ref()).into(); commands.spawn((Camera2d, DespawnOnExit(super::Scene::DebugOutlines))); commands @@ -2358,7 +2358,7 @@ mod debug_outlines { ..default() } )], - UiDebugOptions { + UiDebugOutline { outline_border_box: true, outline_padding_box: true, outline_content_box: true, @@ -2378,7 +2378,7 @@ mod debug_outlines { ..default() }, ScrollPosition(Vec2::new(180., 180.)), - UiDebugOptions { + UiDebugOutline { line_width: 3., outline_scrollbars: true, show_hidden: false, @@ -2395,7 +2395,7 @@ mod debug_outlines { ..default() } )], - UiDebugOptions { + UiDebugOutline { enabled: false, ..default() }, @@ -2413,7 +2413,7 @@ mod debug_outlines { scrollbar_width: 10., ..default() }, - UiDebugOptions { + UiDebugOutline { line_width: 3., outline_scrollbars: true, show_hidden: false, @@ -2430,7 +2430,7 @@ mod debug_outlines { ..default() } )], - UiDebugOptions { + UiDebugOutline { enabled: false, ..default() }, @@ -2449,7 +2449,7 @@ mod debug_outlines { ..default() }, ScrollPosition(Vec2::new(300., 0.)), - UiDebugOptions { + UiDebugOutline { line_width: 3., outline_scrollbars: true, show_hidden: false, @@ -2466,14 +2466,14 @@ mod debug_outlines { move |j| { ( Text(format!("Item {}", (i * 5) + j)), - UiDebugOptions { + UiDebugOutline { enabled: false, ..default() }, ) } }))), - UiDebugOptions { + UiDebugOutline { enabled: false, ..default() }, diff --git a/examples/ui/images/image_node_resizing.rs b/examples/ui/images/image_node_resizing.rs index e8ab791b42fc4..04a1f471ddd3a 100644 --- a/examples/ui/images/image_node_resizing.rs +++ b/examples/ui/images/image_node_resizing.rs @@ -95,7 +95,7 @@ fn setup(mut commands: Commands, asset_server: Res) { padding: px(6).all(), ..default() }, - UiDebugOptions { + UiDebugOutline { enabled: false, ..default() }, diff --git a/examples/ui/scroll_and_overflow/scroll.rs b/examples/ui/scroll_and_overflow/scroll.rs index 83b89003d17e1..8814b6b8cc957 100644 --- a/examples/ui/scroll_and_overflow/scroll.rs +++ b/examples/ui/scroll_and_overflow/scroll.rs @@ -235,7 +235,7 @@ fn vertically_scrolling_list(font_handle: Handle) -> impl Bundle { ..default() }, #[cfg(feature = "bevy_ui_debug")] - UiDebugOptions { + UiDebugOutline { enabled: true, outline_border_box: false, outline_padding_box: false, From 3488686e54d93c4643130dce15284a34f99affc1 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 10:40:44 +0100 Subject: [PATCH 50/99] completed generate_debug_layer_quads --- crates/bevy_ui_render/src/debug_overlay.rs | 96 +++++++++++++++------- 1 file changed, 67 insertions(+), 29 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index c5a91c22d571b..17a0b00d88ef5 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -217,9 +217,10 @@ pub fn push_debug_outline( transform: Affine2, ui_meta: &mut UiMeta, ) { - let rect_size = rect.size(); + let size = rect.size(); + let transform = transform * Affine2::from_translation(rect.center()); let positions = - QUAD_VERTEX_POSITIONS.map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + QUAD_VERTEX_POSITIONS.map(|pos| transform.transform_point2(pos * size).extend(0.)); let positions_diff = if let Some(clip) = clip { [ Vec2::new( @@ -248,7 +249,7 @@ pub fn push_debug_outline( positions[2] + positions_diff[2].extend(0.), positions[3] + positions_diff[3].extend(0.), ]; - let transformed_rect_size = transform.transform_vector2(rect_size).abs(); + let transformed_rect_size = transform.transform_vector2(size).abs(); if transform.x_axis[1] == 0.0 && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) @@ -268,8 +269,8 @@ pub fn push_debug_outline( flags: flags | shader_flags::CORNERS[i], radius: border_radius.into(), border: [line_width, line_width, line_width, line_width], - size: rect_size.into(), - point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), + size: size.into(), + point: (QUAD_VERTEX_POSITIONS[i] * size + positions_diff[i]).into(), }); } for &index in &QUAD_INDICES { @@ -278,7 +279,7 @@ pub fn push_debug_outline( } /// Debug layout can just -fn generate_debug_layout_quads( +fn generate_debug_layer_quads( ui_meta: &mut UiMeta, extracted_ui_layout: &ExtractedUiLayout, extracted_ui_debug_layer: &ExtractedUiDebugLayer, @@ -301,50 +302,87 @@ fn generate_debug_layout_quads( if debug_outline.outline_border_box { push_debug_outline( - rect, + layout.uinode.border_box(), line_color, debug_outline.line_width, - border_radius, - clip, - transform, + layout.uinode.border_radius, + layout.clip, + layout.transform, ui_meta, ); } - if debug_outline.outline_content_box { + if debug_outline.outline_padding_box { push_debug_outline( - rect, + layout.ui_node.padding_box(), line_color, debug_outline.line_width, - border_radius, - clip, - transform, + layout.uinode.inner_radius(), + layout.clip, + layout.transform, ui_meta, ); } - if debug_outline.outline_padding_box { + if debug_outline.outline_content_box { push_debug_outline( - rect, + layout.uinode.content_box(), line_color, debug_outline.line_width, - border_radius, - clip, - transform, + ResolvedBorderRadius::ZERO, + layout.clip, + layout.transform, ui_meta, ); } if debug_outline.outline_scrollbars { - push_debug_outline( - rect, - line_color, - debug_outline.line_width, - border_radius, - clip, - transform, - ui_meta, - ); + if let Some((gutter, [thumb_min, thumb_max])) = layout.uinode.horizontal_scrollbar() { + push_debug_outline( + gutter, + line_color, + debug_outline.line_width, + ResolvedBorderRadius::ZERO, + layout.clip, + layout.transform, + ui_meta, + ); + push_debug_outline( + Rect { + min: Vec2::new(thumb_min, gutter.min.y), + max: Vec2::new(thumb_max, gutter.max.y), + }, + line_color, + debug_outline.line_width, + ResolvedBorderRadius::ZERO, + layout.clip, + layout.transform, + ui_meta, + ); + } + if let Some((gutter, [thumb_min, thumb_max])) = layout.uinode.vertical_scrollbar() { + push_debug_outline( + gutter, + line_color, + debug_outline.line_width, + ResolvedBorderRadius::ZERO, + layout.clip, + layout.transform, + ui_meta, + ); + push_debug_outline( + Rect { + min: Vec2::new(gutter.min.x, thumb_min), + max: Vec2::new(gutter.max.x, thumb_max), + }, + line_color, + debug_outline.line_width, + ResolvedBorderRadius::ZERO, + layout.clip, + layout.transform, + ui_meta, + ); + } } } } From b53efaa027c8b126c7b94a811b2ad0a480b2d109 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 11:18:06 +0100 Subject: [PATCH 51/99] GlobalUiDebugOptions -> UiDebugOverlay --- crates/bevy_ui_render/src/debug_overlay.rs | 35 ++++----- crates/bevy_ui_render/src/lib.rs | 87 ++++++++++------------ examples/testbed/full_ui.rs | 2 +- examples/testbed/ui.rs | 6 +- examples/ui/images/image_node_resizing.rs | 2 +- 5 files changed, 61 insertions(+), 71 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 17a0b00d88ef5..87b0b59f1f091 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -88,8 +88,8 @@ impl Default for UiDebugOutline { } } -impl From for UiDebugOutline { - fn from(other: GlobalUiDebugOptions) -> Self { +impl From for UiDebugOutline { + fn from(other: UiDebugOverlay) -> Self { other.0.clone() } } @@ -99,9 +99,9 @@ impl From for UiDebugOutline { /// A global `resource` that can be overridden by local component [`UiDebugOptions`] override on individual UI node entities #[derive(Default, Resource, Reflect, Clone, Deref, DerefMut)] #[reflect(Resource)] -pub struct GlobalUiDebugOptions(pub UiDebugOutline); +pub struct UiDebugOverlay(pub UiDebugOutline); -impl From for GlobalUiDebugOptions { +impl From for UiDebugOverlay { fn from(other: UiDebugOutline) -> Self { Self(other) } @@ -109,7 +109,7 @@ impl From for GlobalUiDebugOptions { /// The debug visualization is just outlines, so it can be extracted as a single phase item for each extracted camera. #[derive(Resource, Default)] -pub struct ExtractedUiDebugLayer { +pub struct ExtractedUiDebugOverlay { /// z_index of whole visualization. pub z_offset: f32, pub default_outline: UiDebugOutline, @@ -117,7 +117,7 @@ pub struct ExtractedUiDebugLayer { pub changed_this_frame: MainEntityHashSet, } -impl ExtractedUiDebugLayer { +impl ExtractedUiDebugOverlay { pub fn get(&self, main_entity: &MainEntity) -> &UiDebugOutline { &self .per_node_outline @@ -127,8 +127,8 @@ impl ExtractedUiDebugLayer { } pub fn extract_debug_overlay( - global_debug_options: Extract>, - mut extracted_debug_layer: ResMut, + global_debug_options: Extract>, + mut extracted_debug_layer: ResMut, ui_debug_outlines_query: Extract>>, mut removed_debug_options: Extract>, ui_stack: Res, @@ -160,7 +160,7 @@ pub fn extract_debug_overlay( } pub fn queue_debug_overlay( - extracted_overlays: Res, + extracted_overlays: Res, ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -208,7 +208,7 @@ pub fn queue_debug_overlay( } /// Clip each debug outline quad, compute its vertices, and push them onto the vertex buffer -pub fn push_debug_outline( +fn push_debug_outline( rect: Rect, line_color: LinearRgba, line_width: f32, @@ -278,20 +278,21 @@ pub fn push_debug_outline( } } -/// Debug layout can just -fn generate_debug_layer_quads( +/// The debug overlay consists of just outlines drawn above the UI, so one phase item can hold +/// all the outlines per camera +pub fn push_debug_overlay_vertices( ui_meta: &mut UiMeta, extracted_ui_layout: &ExtractedUiLayout, - extracted_ui_debug_layer: &ExtractedUiDebugLayer, + extracted_ui_debug_overlay: &ExtractedUiDebugOverlay, ) { - if !extracted_ui_debug_layer.default_outline.enabled - && extracted_ui_debug_layer.per_node_outline.is_empty() + if !extracted_ui_debug_overlay.default_outline.enabled + && extracted_ui_debug_overlay.per_node_outline.is_empty() { return; } for (main_entity, layout) in extracted_ui_layout.layout.iter() { - let debug_outline = extracted_ui_debug_layer.get(main_entity); + let debug_outline = extracted_ui_debug_overlay.get(main_entity); if !debug_outline.enabled { continue; } @@ -314,7 +315,7 @@ fn generate_debug_layer_quads( if debug_outline.outline_padding_box { push_debug_outline( - layout.ui_node.padding_box(), + layout.uinode.padding_box(), line_color, debug_outline.line_width, layout.uinode.inner_radius(), diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 2ca0220ea524f..66979a0005a11 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -62,7 +62,7 @@ use bevy_render::{ }; use bevy_sprite::BorderRect; #[cfg(feature = "bevy_ui_debug")] -pub use debug_overlay::{GlobalUiDebugOptions, UiDebugOutline}; +pub use debug_overlay::{UiDebugOutline, UiDebugOverlay}; use gradient::GradientPlugin; @@ -84,7 +84,7 @@ use crate::text::{extract_text, queue_text, ExtractedGlyphLayout, ExtractedGlyph pub mod prelude { #[cfg(feature = "bevy_ui_debug")] - pub use crate::debug_overlay::{GlobalUiDebugOptions, UiDebugOutline}; + pub use crate::debug_overlay::{UiDebugOutline, UiDebugOverlay}; pub use crate::{ ui_material::*, ui_material_pipeline::UiMaterialPlugin, BoxShadowSamples, UiAntiAlias, @@ -195,7 +195,7 @@ impl Plugin for UiRenderPlugin { load_shader_library!(app, "ui.wgsl"); #[cfg(feature = "bevy_ui_debug")] - app.init_resource::(); + app.init_resource::(); app.add_systems( PostUpdate, @@ -214,7 +214,7 @@ impl Plugin for UiRenderPlugin { }; #[cfg(feature = "bevy_ui_debug")] - render_app.init_resource::(); + render_app.init_resource::(); render_app .init_gpu_resource::>() @@ -922,10 +922,7 @@ fn prepare_uinodes( mut ui_meta: ResMut, extracted_uinodes: Res, extracted_glyph_layouts: Res, - #[cfg(feature = "bevy_ui_debug")] extracted_debug_overlays: Res< - debug_overlay::ExtractedUiDebugLayer, - >, - extracted_geometry: Res, + extracted_ui_layout: Res, view_uniforms: Res, ui_pipeline: Res, mut image_bind_groups: ResMut, @@ -933,6 +930,9 @@ fn prepare_uinodes( mut phases: ResMut>, events: Res, mut previous_len: Local, + #[cfg(feature = "bevy_ui_debug")] extracted_ui_debug_overlay: Res< + debug_overlay::ExtractedUiDebugOverlay, + >, ) { // If an image has changed, the GpuImage has (probably) changed for event in &events.images { @@ -967,6 +967,8 @@ fn prepare_uinodes( for ui_phase in phases.values_mut() { // used to store the index of the first phase item in a batch let mut batch_start_item_index = 0; + + // Setting batch_image_handle to None is used to signal the end of the current batch. let mut batch_image_handle = None; for phase_item_index in 0..ui_phase.items.len() { @@ -978,7 +980,7 @@ fn prepare_uinodes( .get(&main_entity) .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) { - let Some(layout) = extracted_geometry.layout.get(&main_entity) else { + let Some(layout) = extracted_ui_layout.layout.get(&main_entity) else { batch_image_handle = None; continue; }; @@ -1072,7 +1074,7 @@ fn prepare_uinodes( .get(&main_entity) .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) { - let Some(layout) = extracted_geometry.layout.get(&main_entity) else { + let Some(layout) = extracted_ui_layout.layout.get(&main_entity) else { batch_image_handle = None; continue; }; @@ -1173,46 +1175,33 @@ fn prepare_uinodes( continue; } - batch_image_handle = None; + #[cfg(feature = "bevy_ui_debug")] + { + let mut existing_batch = batches.last_mut(); + if batch_image_handle.is_none() || existing_batch.is_none() { + batch_start_item_index = phase_item_index; + batches.push(( + ui_phase.items[batch_start_item_index].entity(), + UiBatch { + range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, + image: AssetId::default(), + texture_ranges: vec![], + }, + )); + existing_batch = batches.last_mut(); + } - // #[cfg(feature = "bevy_ui_debug")] - // if let Some(style) = extracted_debug_overlays - // .per_node_debug_options - // .get(&main_entity) - // .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) - // { - // let image = AssetId::::default(); - // let Some(gpu_image) = gpu_images.get(image) else { - // continue; - // }; - // image_bind_groups.values.entry(image).or_insert_with(|| { - // render_device.create_bind_group( - // "ui_debug_overlay_image_bind_group", - // &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), - // &BindGroupEntries::sequential(( - // &gpu_image.texture_view, - // &gpu_image.sampler, - // )), - // ) - // }); - - // let range_start = ui_meta.indices.len() as u32; - // generate_debug_quads(&mut ui_meta, style); - // let range_end = ui_meta.indices.len() as u32; - // if range_start == range_end { - // continue; - // } - // ui_phase.items[phase_item_index].batch_range = - // phase_item_index as u32..phase_item_index as u32 + 1; - // batches.push(( - // entity, - // UiBatch { - // range: range_start..range_end, - // image, - // texture_ranges: vec![], - // }, - // )); - // } + debug_overlay::push_debug_overlay_vertices( + &mut ui_meta, + &extracted_ui_layout, + &extracted_ui_debug_overlay, + ); + + existing_batch.unwrap().1.range.end = ui_meta.indices.len() as u32; + ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; + } + + batch_image_handle = None; } } diff --git a/examples/testbed/full_ui.rs b/examples/testbed/full_ui.rs index 092f2723c98a6..b87f37ae5d19a 100644 --- a/examples/testbed/full_ui.rs +++ b/examples/testbed/full_ui.rs @@ -408,7 +408,7 @@ fn setup(mut commands: Commands, asset_server: Res) { // The system that will enable/disable the debug outlines around the nodes fn toggle_debug_overlay( input: Res>, - mut debug_options: ResMut, + mut debug_options: ResMut, mut root_node_query: Query<&mut Visibility, (With, Without)>, ) { info_once!("The debug outlines are enabled, press Space to turn them on/off"); diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index f510847a153ba..50ebfa0169511 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -2245,7 +2245,7 @@ mod debug_outlines { ui_render::UiDebugOutline, }; - pub fn setup(mut commands: Commands, mut debug_options: ResMut) { + pub fn setup(mut commands: Commands, mut debug_options: ResMut) { debug_options.enabled = true; debug_options.line_width = 5.; debug_options.line_color_override = Some(LinearRgba::GREEN); @@ -2483,8 +2483,8 @@ mod debug_outlines { }); } - pub fn teardown(mut debug_options: ResMut) { - *debug_options = GlobalUiDebugOptions::default(); + pub fn teardown(mut debug_options: ResMut) { + *debug_options = UiDebugOverlay::default(); } } diff --git a/examples/ui/images/image_node_resizing.rs b/examples/ui/images/image_node_resizing.rs index 04a1f471ddd3a..7cb6f5e9f4b76 100644 --- a/examples/ui/images/image_node_resizing.rs +++ b/examples/ui/images/image_node_resizing.rs @@ -18,7 +18,7 @@ fn main() { App::new() .add_plugins(DefaultPlugins) // Enable for image outline - .insert_resource(GlobalUiDebugOptions { + .insert_resource(UiDebugOverlay { enabled: true, ..default() }) From e2714a1c14476d57a0c670d41a118b6ab0b5b210 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 11:23:06 +0100 Subject: [PATCH 52/99] store changed shadows in extracted_box_shadows --- crates/bevy_ui_render/src/box_shadow.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 161eabac3b613..96994f4f930c5 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -189,6 +189,8 @@ pub struct ExtractedBoxShadow { pub struct ExtractedBoxShadows { /// Box shadows grouped by their main-world UI entity. pub box_shadows: MainEntityHashMap>, + /// List of UI entities with changed shadows, cleared each frame before extraction. + pub changed_this_frame: MainEntityHashSet, } pub fn extract_shadows( @@ -204,13 +206,12 @@ pub fn extract_shadows( Extract>, Extract>, ), - mut nodes_processed_this_frame: Local, ) { - nodes_processed_this_frame.clear(); + extracted_box_shadows.changed_this_frame.clear(); for (entity, box_shadow, target) in &box_shadow_query { let main_entity = MainEntity::from(entity); - nodes_processed_this_frame.insert(main_entity); + extracted_box_shadows.changed_this_frame.insert(main_entity); let shadows = extracted_box_shadows .box_shadows @@ -252,7 +253,10 @@ pub fn extract_shadows( .chain(removed_computed_ui_render_target_info_query.read()) { let main_entity = MainEntity::from(main_entity); - if nodes_processed_this_frame.contains(&main_entity) { + if extracted_box_shadows + .changed_this_frame + .contains(&main_entity) + { continue; } let Some(mut extracted_nodes) = extracted_box_shadows.box_shadows.remove(&main_entity) From 4479517b3d0db5bef20a47428aaa25d3ab585764 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 11:39:28 +0100 Subject: [PATCH 53/99] track changed in ExtractedGradients, not locally in system --- crates/bevy_ui_render/src/gradient.rs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 20aec8b05748d..8bbb73cd9a53c 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -238,6 +238,7 @@ pub struct ExtractedGradients { /// 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>, + pub changed_this_frame: MainEntityHashSet, } // Interpolate implicit stops (where position is `f32::NAN`) @@ -356,6 +357,7 @@ pub fn extract_gradients( ), mut removed_gradients: Local, ) { + extracted_gradients.changed_this_frame.clear(); removed_gradients.clear(); removed_gradients.extend( removed_computed_ui_render_target_info_query @@ -365,18 +367,23 @@ pub fn extract_gradients( .map(MainEntity::from), ); - for (entity, target, (gradient, gradient_border)) in changed_gradients_query + for (entity, target, (node_gradient, border_gradient)) in changed_gradients_query .iter() .chain(gradients_query.iter_many(removed_gradients.iter().map(ContainsEntity::entity))) { + if node_gradient.is_none() && border_gradient.is_none() { + continue; + } + let main_entity = MainEntity::from(entity); + extracted_gradients.changed_this_frame.insert(main_entity); - let gradients = extracted_gradients.items.entry(main_entity).or_default(); + let extracted_gradients = extracted_gradients.items.entry(main_entity).or_default(); let mut gradient_count = 0; for (node_gradients, node_type) in [ - (gradient.map(|g| &g.0), NodeType::Rect), - (gradient_border.map(|g| &g.0), NodeType::Border(BORDER_ALL)), + (node_gradient.map(|g| &g.0), NodeType::Rect), + (border_gradient.map(|g| &g.0), NodeType::Border(BORDER_ALL)), ] .iter() .filter_map(|(g, n)| g.map(|g| (g, *n))) @@ -391,22 +398,14 @@ pub fn extract_gradients( node_type, target: *target, }; - if let Some((_, gradient)) = gradients.get_index_mut(gradient_count) { + if let Some((_, gradient)) = extracted_gradients.get_index_mut(gradient_count) { *gradient = extracted_gradient; } else { - gradients.insert(commands.spawn_empty().id(), extracted_gradient); + extracted_gradients.insert(commands.spawn_empty().id(), extracted_gradient); } gradient_count += 1; } } - - for (render_entity, _) in gradients.drain(gradient_count..) { - commands.entity(render_entity).despawn(); - } - - if gradients.is_empty() { - extracted_gradients.items.remove(&main_entity); - } } for main_entity in removed_gradients.drain() { From 2c5c59c350e9f84116c704ec7f3ab601793bd885 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 11:46:19 +0100 Subject: [PATCH 54/99] Made changed_this_frame chagnes for materials and slicers --- crates/bevy_ui_render/src/ui_material_pipeline.rs | 11 ++++++----- .../bevy_ui_render/src/ui_texture_slice_pipeline.rs | 11 +++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index f92a7ab40d6ab..3033a6c4811ab 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -298,6 +298,7 @@ pub struct ExtractedUiMaterial { pub struct ExtractedUiMaterialNodes { /// Map from main-world UI entity to render entity and UI material node pub uinodes: MainEntityHashMap<(Entity, ExtractedUiMaterial)>, + pub changed_this_frame: MainEntityHashSet, } impl Default for ExtractedUiMaterialNodes { @@ -315,9 +316,9 @@ pub fn extract_ui_material_nodes( uinode_query: Extract), Changed>>>, (mut removed_material_node_query,): (Extract>>,), mut nodes_to_reextract_next_frame: Local, - mut nodes_processed_this_frame: Local, ) { - nodes_processed_this_frame.clear(); + extracted_uinodes.changed_this_frame.clear(); + let nodes_to_reextract = mem::take(&mut *nodes_to_reextract_next_frame); for (entity, handle) in uinode_query.iter().chain( @@ -330,7 +331,7 @@ pub fn extract_ui_material_nodes( // Make sure we don't process the same node more than once. // This is possible if the node was marked for reextraction on the // previous frame and was also otherwise changed on this frame. - if nodes_processed_this_frame.contains(&main_entity) { + if extracted_uinodes.changed_this_frame.contains(&main_entity) { continue; } @@ -341,7 +342,7 @@ pub fn extract_ui_material_nodes( continue; } - nodes_processed_this_frame.insert(main_entity); + extracted_uinodes.changed_this_frame.insert(main_entity); match extracted_uinodes.uinodes.entry(main_entity) { Entry::Occupied(mut entry) => { @@ -365,7 +366,7 @@ pub fn extract_ui_material_nodes( // frame. for main_entity in removed_material_node_query.read() { let main_entity = MainEntity::from(main_entity); - if nodes_processed_this_frame.contains(&main_entity) { + if extracted_uinodes.changed_this_frame.contains(&main_entity) { continue; } let Some((entity, _)) = extracted_uinodes.uinodes.remove(&main_entity) else { 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 6847ee47d372d..86d675f20721a 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -205,6 +205,7 @@ pub struct ExtractedUiTextureSlice { pub struct ExtractedUiTextureSlices { /// Map from main-world UI entity to render entity and texture slice. pub slices: MainEntityHashMap<(Entity, ExtractedUiTextureSlice)>, + pub changed_this_frame: MainEntityHashSet, } pub fn extract_ui_texture_slices( @@ -225,13 +226,12 @@ pub fn extract_ui_texture_slices( >, >, (mut removed_image_node_query,): (Extract>,), - mut nodes_processed_this_frame: Local, ) { - nodes_processed_this_frame.clear(); + extracted_ui_slicers.changed_this_frame.clear(); for (entity, image) in &slicers_query { let main_entity = MainEntity::from(entity); - nodes_processed_this_frame.insert(main_entity); + extracted_ui_slicers.changed_this_frame.insert(main_entity); if image.color.is_fully_transparent() || image.image.id() == TRANSPARENT_IMAGE_HANDLE.id() { if let Some((render_entity, _)) = extracted_ui_slicers.slices.remove(&main_entity) { @@ -303,7 +303,10 @@ pub fn extract_ui_texture_slices( // frame. for main_entity in removed_image_node_query.read() { let main_entity = MainEntity::from(main_entity); - if nodes_processed_this_frame.contains(&main_entity) { + if extracted_ui_slicers + .changed_this_frame + .contains(&main_entity) + { continue; } let Some((render_entity, _)) = extracted_ui_slicers.slices.remove(&main_entity) else { From 31017d2dc8e59d5d6935d2fc34c0681d5893a7c8 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 11:49:11 +0100 Subject: [PATCH 55/99] ExtractedUiNodeStyles changed -> changed_this_frame --- crates/bevy_ui_render/src/lib.rs | 8 ++++---- crates/bevy_ui_render/src/ui_material_pipeline.rs | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 66979a0005a11..af488d0b4a858 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -382,7 +382,7 @@ pub enum NodeType { pub struct ExtractedUiNodeStyles { pub uinodes: MainEntityHashMap<(Entity, ExtractedUiNodeStyle)>, /// UI nodes that changed this frame. - pub changed: MainEntityHashSet, + pub changed_this_frame: MainEntityHashSet, } pub fn extract_uinode_styles( @@ -444,7 +444,7 @@ pub fn extract_uinode_styles( ), mut removed_uinodes: Local, ) { - extracted_uinodes.changed.clear(); + extracted_uinodes.changed_this_frame.clear(); removed_uinodes.clear(); removed_uinodes.extend( removed_background_color_query @@ -473,7 +473,7 @@ pub fn extract_uinode_styles( let main_entity = entity.into(); // Already updated this entity - if !extracted_uinodes.changed.insert(main_entity) { + if !extracted_uinodes.changed_this_frame.insert(main_entity) { continue; } @@ -557,7 +557,7 @@ pub fn extract_uinode_styles( if uinode_query.contains(main_entity.entity()) { continue; } - extracted_uinodes.changed.insert(main_entity); + extracted_uinodes.changed_this_frame.insert(main_entity); let Some((render_entity, _)) = extracted_uinodes.uinodes.remove(&main_entity) else { continue; }; diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 3033a6c4811ab..d2e689c87e5e5 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -305,6 +305,7 @@ impl Default for ExtractedUiMaterialNodes { fn default() -> Self { Self { uinodes: Default::default(), + changed_this_frame: Default::default(), } } } From 88c2c4e75727b5e577bbf6454ad235887855c6e6 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 11:56:26 +0100 Subject: [PATCH 56/99] Cleanup, Extract UiStack resource --- crates/bevy_ui_render/src/debug_overlay.rs | 37 +++++++--------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 87b0b59f1f091..a0afe976824bc 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -1,40 +1,27 @@ -use std::arch::global_asm; - use crate::{ - extract_layout::ExtractedUiLayout, shader_flags, DrawUi, ImageNodeBindGroups, NodeType, - TransparentUi, UiAntiAlias, UiBatch, UiCameraMap, UiCameraView, UiMeta, UiPipeline, - UiPipelineKey, UiVertex, QUAD_INDICES, QUAD_UVS, QUAD_VERTEX_POSITIONS, + extract_layout::ExtractedUiLayout, shader_flags, DrawUi, TransparentUi, UiAntiAlias, + UiCameraView, UiMeta, UiPipeline, UiPipelineKey, UiVertex, QUAD_INDICES, QUAD_UVS, + QUAD_VERTEX_POSITIONS, }; -use bevy_asset::AssetId; -use bevy_camera::visibility::InheritedVisibility; + use bevy_color::{ColorToComponents, Hsla, LinearRgba}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - entity::{hash_map, Entity, EntityHashMap}, - lifecycle::RemovedComponents, - prelude::*, - query::{Changed, EcsAccessLevel}, - resource::Resource, + entity::Entity, lifecycle::RemovedComponents, prelude::*, query::Changed, resource::Resource, }; -use bevy_image::Image; + use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; -use bevy_platform::collections::hash_table::Entry; + use bevy_reflect::Reflect; use bevy_render::{ - render_asset::RenderAssets, - render_phase::{DrawFunctions, PhaseItem, PhaseItemExtraIndex, ViewSortedRenderPhases}, - render_resource::{BindGroupEntries, PipelineCache, SpecializedRenderPipelines}, - renderer::RenderDevice, + render_phase::{DrawFunctions, PhaseItemExtraIndex, ViewSortedRenderPhases}, + render_resource::{PipelineCache, SpecializedRenderPipelines}, sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}, - texture::GpuImage, view::ExtractedView, Extract, }; -use bevy_sprite::BorderRect; -use bevy_ui::{ - ui_transform::UiGlobalTransform, CalculatedClip, ComputedNode, ComputedStackIndex, - ComputedUiTargetCamera, ResolvedBorderRadius, UiStack, UiTargetCamera, -}; + +use bevy_ui::{ResolvedBorderRadius, UiStack}; /// Configuration for the UI debug overlay /// @@ -131,7 +118,7 @@ pub fn extract_debug_overlay( mut extracted_debug_layer: ResMut, ui_debug_outlines_query: Extract>>, mut removed_debug_options: Extract>, - ui_stack: Res, + ui_stack: Extract>, ) { extracted_debug_layer.changed_this_frame.clear(); extracted_debug_layer.z_offset = ui_stack.uinodes.len() as f32; From 7cfc24cd14de505ec27901410c66ded04093597b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 12:17:13 +0100 Subject: [PATCH 57/99] fix rwmode stuff --- crates/bevy_ui_render/src/text.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index fca47d8f7ad34..269d692bd04ac 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -162,7 +162,7 @@ pub fn extract_text( Extract>, Extract>, Extract>, - Extract, + Extract>, ), input_focus: Extract>>, mut nodes_to_extract: Local, @@ -181,6 +181,7 @@ pub fn extract_text( .chain(removed_text_cursor_style_query.read()) .chain(removed_text_shadow_query.read()) .chain(removed_editable_text_query.read()) + .chain(removed_read_write_mode_query.read()) .map(MainEntity::from), ); @@ -242,11 +243,12 @@ pub fn extract_text( .as_ref() .is_some_and(|input_focus| input_focus.get() == Some(entity)); - let selection_color = if focused && rwmode.is_some_and(|rwmode| *rwmode == TextReadWriteMode::Editable) { - cursor_style.selection_color - } else { - cursor_style.unfocused_selection_color - }; + let selection_color = + if focused && rwmode.is_some_and(|rwmode| *rwmode == TextReadWriteMode::Editable) { + cursor_style.selection_color + } else { + cursor_style.unfocused_selection_color + }; let mut selections = vec![]; if has_cursor_style && !text_layout_info.selection_rects.is_empty() @@ -432,7 +434,7 @@ pub fn extract_text( && let Some((true, cursor)) = text_layout_info.cursor && !cursor.is_empty() && !cursor_style.color.is_fully_transparent() - && rwmode.is_some_and(|rwmode| rwmode != TextReadWriteMode::Static) + && rwmode.is_some_and(|rwmode| *rwmode != TextReadWriteMode::Static) { Some(cursor) } else { From 5304357b5a034ab06b781508eb211327b5fa881b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 12:18:01 +0100 Subject: [PATCH 58/99] Removed old text prepare --- crates/bevy_ui_render/src/text.rs | 332 ------------------------------ 1 file changed, 332 deletions(-) diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index 269d692bd04ac..0fd01ea25d6ea 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -528,335 +528,3 @@ pub fn queue_text( }); } } - -#[expect(dead_code, reason = "retained while using prepare_uinodes_new")] -pub fn prepare_text( - mut commands: Commands, - render_device: Res, - render_queue: Res, - pipeline_cache: Res, - mut ui_meta: ResMut, - extracted_glyph_layouts: Res, - extracted_geometry: Res, - view_uniforms: Res, - ui_pipeline: Res, - mut image_bind_groups: ResMut, - gpu_images: Res>, - mut phases: ResMut>, - mut previous_len: Local, -) { - if view_uniforms.uniforms.binding().is_none() { - return; - } - - let mut batches = Vec::with_capacity(*previous_len); - - for ui_phase in phases.values_mut() { - for item_index in 0..ui_phase.items.len() { - let item = &mut ui_phase.items[item_index]; - let Some(layout) = extracted_glyph_layouts - .uinodes - .get(&item.main_entity()) - .and_then(|(render_entity, layout)| { - (*render_entity == item.entity()).then_some(layout) - }) - else { - continue; - }; - let Some(geometry) = extracted_geometry.layout.get(&item.main_entity()) else { - continue; - }; - let image = layout - .sections - .iter() - .find_map(|section| { - gpu_images - .get(section.atlas_texture) - .map(|_| section.atlas_texture) - }) - .unwrap_or_default(); - let Some(gpu_image) = gpu_images.get(image) else { - continue; - }; - - image_bind_groups.values.entry(image).or_insert_with(|| { - render_device.create_bind_group( - "ui_material_bind_group", - &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential((&gpu_image.texture_view, &gpu_image.sampler)), - ) - }); - - let uinode = &geometry.uinode; - let shadow_offset = layout.shadow_offset / uinode.inverse_scale_factor(); - let transform = geometry.transform - * Affine2::from_translation(uinode.content_box().min - layout.viewport_offset); - let clip = if layout.clip_to_content_box { - let content_box = uinode.content_box(); - let text_clip = Rect::from_center_size( - geometry.transform.translation + content_box.center(), - content_box.size(), - ); - Some( - geometry - .clip - .map_or(text_clip, |clip| clip.intersect(text_clip)), - ) - } else { - geometry.clip - }; - - let mut quads: Vec<( - Rect, - LinearRgba, - ResolvedBorderRadius, - Option<(Rect, AssetId)>, - )> = Vec::with_capacity( - layout.selections.len() - + layout.backgrounds.len() - + layout.glyphs.len() - * if layout.shadow_color.is_fully_transparent() { - 1 - } else { - 2 - } - + layout.strike_through.len() - * if layout.shadow_color.is_fully_transparent() { - 1 - } else { - 2 - } - + layout.underline.len() - * if layout.shadow_color.is_fully_transparent() { - 1 - } else { - 2 - } - + usize::from(layout.cursor.is_some()), - ); - let selection_color = if layout.focused { - layout.cursor_style.selection_color - } else { - layout.cursor_style.unfocused_selection_color - } - .to_linear(); - quads.extend( - layout - .selections - .iter() - .map(|(rect, radius)| (*rect, selection_color, *radius, None)), - ); - quads.extend( - layout - .backgrounds - .iter() - .map(|(rect, color)| (*rect, *color, ResolvedBorderRadius::ZERO, None)), - ); - - if !layout.shadow_color.is_fully_transparent() { - for section in &layout.sections { - quads.extend( - layout.glyphs[section.range.start as usize..section.range.end as usize] - .iter() - .map(|glyph| { - ( - Rect::from_center_size( - glyph.translation + shadow_offset, - glyph.rect.size(), - ), - layout.shadow_color, - ResolvedBorderRadius::ZERO, - Some((glyph.rect, section.atlas_texture)), - ) - }), - ); - } - quads.extend( - layout - .strike_through - .iter() - .chain(layout.underline.iter()) - .map(|(rect, _)| { - ( - Rect::from_center_size(rect.center() + shadow_offset, rect.size()), - layout.shadow_color, - ResolvedBorderRadius::ZERO, - None, - ) - }), - ); - } - - for section in &layout.sections { - quads.extend( - layout.glyphs[section.range.start as usize..section.range.end as usize] - .iter() - .map(|glyph| { - ( - Rect::from_center_size(glyph.translation, glyph.rect.size()), - glyph.color, - ResolvedBorderRadius::ZERO, - Some((glyph.rect, section.atlas_texture)), - ) - }), - ); - } - quads.extend( - layout - .strike_through - .iter() - .chain(layout.underline.iter()) - .map(|(rect, color)| (*rect, *color, ResolvedBorderRadius::ZERO, None)), - ); - if let Some(cursor) = layout.cursor { - quads.push(( - cursor, - layout.cursor_style.color.to_linear(), - ResolvedBorderRadius::ZERO, - None, - )); - } - - let range_start = ui_meta.indices.len() as u32; - let mut indices_index = range_start; - let mut vertices_index = ui_meta.vertices.len() as u32; - let mut texture_range_start = range_start; - let mut current_texture = image; - let mut texture_ranges = vec![]; - - for (rect, color, border_radius, glyph) in quads { - let rect_size = rect.size(); - let rect_transform = transform * Affine2::from_translation(rect.center()); - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| rect_transform.transform_point2(pos * rect_size).extend(0.)); - let positions_diff = if let Some(clip) = clip { - [ - Vec2::new( - f32::max(clip.min.x - positions[0].x, 0.), - f32::max(clip.min.y - positions[0].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[1].x, 0.), - f32::max(clip.min.y - positions[1].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[2].x, 0.), - f32::min(clip.max.y - positions[2].y, 0.), - ), - Vec2::new( - f32::max(clip.min.x - positions[3].x, 0.), - f32::min(clip.max.y - positions[3].y, 0.), - ), - ] - } else { - [Vec2::ZERO; 4] - }; - let positions_clipped = [ - positions[0] + positions_diff[0].extend(0.), - positions[1] + positions_diff[1].extend(0.), - positions[2] + positions_diff[2].extend(0.), - positions[3] + positions_diff[3].extend(0.), - ]; - let transformed_rect_size = rect_transform.transform_vector2(rect_size).abs(); - if rect_transform.x_axis[1] == 0.0 - && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x - || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) - { - continue; - } - - let (uvs, flags) = if let Some((glyph_rect, atlas_texture)) = glyph { - let Some(gpu_image) = gpu_images.get(atlas_texture) else { - continue; - }; - if current_texture != atlas_texture { - if texture_range_start < indices_index { - texture_ranges - .push((texture_range_start..indices_index, current_texture)); - } - texture_range_start = indices_index; - current_texture = atlas_texture; - } - image_bind_groups - .values - .entry(atlas_texture) - .or_insert_with(|| { - render_device.create_bind_group( - "ui_material_bind_group", - &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential(( - &gpu_image.texture_view, - &gpu_image.sampler, - )), - ) - }); - let atlas_extent = gpu_image.size_2d().as_vec2(); - ( - [ - Vec2::new( - glyph_rect.min.x + positions_diff[0].x, - glyph_rect.min.y + positions_diff[0].y, - ), - Vec2::new( - glyph_rect.max.x + positions_diff[1].x, - glyph_rect.min.y + positions_diff[1].y, - ), - Vec2::new( - glyph_rect.max.x + positions_diff[2].x, - glyph_rect.max.y + positions_diff[2].y, - ), - Vec2::new( - glyph_rect.min.x + positions_diff[3].x, - glyph_rect.max.y + positions_diff[3].y, - ), - ] - .map(|pos| pos / atlas_extent), - shader_flags::TEXTURED, - ) - } else { - ( - [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y], - shader_flags::UNTEXTURED, - ) - }; - - let color = color.to_f32_array(); - for i in 0..4 { - ui_meta.vertices.push(UiVertex { - position: positions_clipped[i].into(), - uv: uvs[i].into(), - color, - flags: flags | shader_flags::CORNERS[i], - radius: border_radius.into(), - border: [0.; 4], - size: rect_size.into(), - point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), - }); - } - for &i in &QUAD_INDICES { - ui_meta.indices.push(vertices_index + i as u32); - } - indices_index += 6; - vertices_index += 4; - } - - if texture_range_start < indices_index { - texture_ranges.push((texture_range_start..indices_index, current_texture)); - } - item.batch_range = item_index as u32..item_index as u32 + 1; - batches.push(( - item.entity(), - UiBatch { - range: range_start..indices_index, - image, - texture_ranges, - }, - )); - } - } - - ui_meta.vertices.write_buffer(&render_device, &render_queue); - ui_meta.indices.write_buffer(&render_device, &render_queue); - *previous_len = batches.len(); - commands.try_insert_batch(batches); -} From fe9e1c379f53e1d9a63641092d77943be70c7217 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 12:25:25 +0100 Subject: [PATCH 59/99] rename generate_*_quads fns to push_*_vertices --- crates/bevy_ui_render/src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index b7cec89f88bb1..bd6e3ba7b422a 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1053,7 +1053,7 @@ fn prepare_uinodes( .get(image) .expect("Image was checked during batching and should still exist"); let range_start = ui_meta.indices.len() as u32; - generate_uinodes_quads(&mut ui_meta, layout, style, image, gpu_image); + push_uinode_vertices(&mut ui_meta, layout, style, image, gpu_image); let range_end = ui_meta.indices.len() as u32; let existing_batch = existing_batch.unwrap(); existing_batch.1.range.end = range_end; @@ -1146,7 +1146,7 @@ fn prepare_uinodes( .texture_ranges .push((existing_batch.1.range.clone(), existing_batch.1.image)); } - generate_text_quads( + push_text_vertices( &mut ui_meta, layout, style, @@ -1214,8 +1214,8 @@ fn prepare_uinodes( ui_meta.indices.write_buffer(&render_device, &render_queue); } -/// generates quads from extracted uinode syle and layout -fn generate_uinodes_quads( +/// generates vertices from extracted uinode syle and layout +fn push_uinode_vertices( ui_meta: &mut UiMeta, layout: &ExtractedUiNodeLayout, style: &ExtractedUiNodeStyle, @@ -1318,7 +1318,7 @@ fn generate_uinodes_quads( match node_type { NodeType::Border(border_flags) => flags |= border_flags, NodeType::Inverted => flags |= INVERT, - NodeType::Rect => {} + _ => {} } let vertex_start = ui_meta.vertices.len() as u32; @@ -1475,8 +1475,8 @@ fn generate_uinodes_quads( } } -/// generates quads from extracted uinode syle and glyph layout -fn generate_text_quads( +/// generates vertices from extracted uinode syle and glyph layout +fn push_text_vertices( ui_meta: &mut UiMeta, layout: &ExtractedUiNodeLayout, style: &ExtractedGlyphLayout, From e2e93d21c44762e2363146bc83110a67c42232fb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 16:01:22 +0100 Subject: [PATCH 60/99] Removed unused imports --- crates/bevy_ui_render/src/text.rs | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index 0fd01ea25d6ea..fef5dc10bfa44 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -1,18 +1,19 @@ +use crate::{ + extract_layout::ExtractedUiLayout, stack_z_offsets, DrawUi, TransparentUi, UiAntiAlias, + UiCameraView, UiPipeline, UiPipelineKey, +}; use bevy_asset::AssetId; -use bevy_color::{Alpha, ColorToComponents, LinearRgba}; +use bevy_color::{Alpha, LinearRgba}; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_input_focus::InputFocus; -use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; +use bevy_math::{FloatOrd, Rect, Vec2}; use bevy_platform::collections::hash_map::Entry; use bevy_render::{ - render_asset::RenderAssets, - render_phase::{DrawFunctions, PhaseItem, PhaseItemExtraIndex, ViewSortedRenderPhases}, - render_resource::{BindGroupEntries, PipelineCache, SpecializedRenderPipelines}, - renderer::{RenderDevice, RenderQueue}, + render_phase::{DrawFunctions, PhaseItemExtraIndex, ViewSortedRenderPhases}, + render_resource::{PipelineCache, SpecializedRenderPipelines}, sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}, - texture::GpuImage, - view::{ExtractedView, ViewUniforms}, + view::ExtractedView, Extract, }; use bevy_text::{ @@ -26,12 +27,6 @@ use bevy_ui::{ }; use core::ops::Range; -use crate::{ - extract_layout::ExtractedUiLayout, shader_flags, stack_z_offsets, DrawUi, ImageNodeBindGroups, - TransparentUi, UiAntiAlias, UiBatch, UiCameraView, UiMeta, UiPipeline, UiPipelineKey, UiVertex, - QUAD_INDICES, QUAD_VERTEX_POSITIONS, -}; - pub struct ExtractedGlyph { pub color: LinearRgba, pub translation: Vec2, From 1daba58b49f491d88971706a3e02cc422a355131 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 16:10:56 +0100 Subject: [PATCH 61/99] Fixed debug outlines for hidden and clipped nodes. --- crates/bevy_ui_render/src/debug_overlay.rs | 19 ++++++++++++------- crates/bevy_ui_render/src/extract_layout.rs | 4 ++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index a0afe976824bc..d1c8ef917ddf6 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -284,6 +284,11 @@ pub fn push_debug_overlay_vertices( continue; } + if !layout.visible && !debug_outline.show_hidden { + continue; + } + + let clip = layout.clip.filter(|_| !debug_outline.show_clipped); let line_color = debug_outline .line_color_override .unwrap_or_else(|| Hsla::sequential_dispersed(main_entity.index_u32()).into()); @@ -294,7 +299,7 @@ pub fn push_debug_overlay_vertices( line_color, debug_outline.line_width, layout.uinode.border_radius, - layout.clip, + clip, layout.transform, ui_meta, ); @@ -306,7 +311,7 @@ pub fn push_debug_overlay_vertices( line_color, debug_outline.line_width, layout.uinode.inner_radius(), - layout.clip, + clip, layout.transform, ui_meta, ); @@ -318,7 +323,7 @@ pub fn push_debug_overlay_vertices( line_color, debug_outline.line_width, ResolvedBorderRadius::ZERO, - layout.clip, + clip, layout.transform, ui_meta, ); @@ -331,7 +336,7 @@ pub fn push_debug_overlay_vertices( line_color, debug_outline.line_width, ResolvedBorderRadius::ZERO, - layout.clip, + clip, layout.transform, ui_meta, ); @@ -343,7 +348,7 @@ pub fn push_debug_overlay_vertices( line_color, debug_outline.line_width, ResolvedBorderRadius::ZERO, - layout.clip, + clip, layout.transform, ui_meta, ); @@ -354,7 +359,7 @@ pub fn push_debug_overlay_vertices( line_color, debug_outline.line_width, ResolvedBorderRadius::ZERO, - layout.clip, + clip, layout.transform, ui_meta, ); @@ -366,7 +371,7 @@ pub fn push_debug_overlay_vertices( line_color, debug_outline.line_width, ResolvedBorderRadius::ZERO, - layout.clip, + clip, layout.transform, ui_meta, ); diff --git a/crates/bevy_ui_render/src/extract_layout.rs b/crates/bevy_ui_render/src/extract_layout.rs index 6e32db34c4482..82eb7a6d18919 100644 --- a/crates/bevy_ui_render/src/extract_layout.rs +++ b/crates/bevy_ui_render/src/extract_layout.rs @@ -28,7 +28,7 @@ pub struct ExtractedUiNodeLayout { pub flip_x: bool, pub flip_y: bool, pub stack_index: u32, - pub is_visible: bool, + pub visible: bool, } /// Uinode geometries and list of geometries that changed this frame. @@ -137,7 +137,7 @@ pub fn extract_ui_layout( flip_x: false, flip_y: false, stack_index: stack_index.0, - is_visible: inherited_visibility.get(), + visible: inherited_visibility.get(), }; let ExtractedUiLayout { From 5dd9f25e4011f2b5329b35c59ba03d2decf38c29 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 16:19:55 +0100 Subject: [PATCH 62/99] Fixed visibility by checking the visible flag in the extracted layout in the queue functions and skipping if not visible. This means that the data persists in the render world for UI elements that aren't visible, which may not be ideal, I'm not sure. --- crates/bevy_ui_render/src/box_shadow.rs | 16 ++++++++++------ crates/bevy_ui_render/src/gradient.rs | 16 ++++++++++------ crates/bevy_ui_render/src/lib.rs | 4 ++++ crates/bevy_ui_render/src/text.rs | 13 +++++++++---- .../bevy_ui_render/src/ui_material_pipeline.rs | 13 +++++++++---- .../src/ui_texture_slice_pipeline.rs | 11 ++++++++--- 6 files changed, 50 insertions(+), 23 deletions(-) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 96994f4f930c5..5f10556dbbc50 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -275,7 +275,7 @@ pub fn extract_shadows( )] pub fn queue_shadows( extracted_box_shadows: Res, - extracted_geometry: Res, + extracted_layout: Res, box_shadow_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -289,12 +289,16 @@ pub fn queue_shadows( let mut current_phase = None; for (main_entity, extracted_sub_shadows) in extracted_box_shadows.box_shadows.iter() { - let Some(geometry) = extracted_geometry.layout.get(main_entity) else { + let Some(layout) = extracted_layout.layout.get(main_entity) else { continue; }; - if current_camera_entity != geometry.extracted_camera { - current_phase = render_views.get(geometry.extracted_camera).ok().and_then( + if !layout.visible { + continue; + } + + if current_camera_entity != layout.extracted_camera { + current_phase = render_views.get(layout.extracted_camera).ok().and_then( |(default_camera_view, shadow_samples)| { camera_views .get(default_camera_view.0) @@ -316,7 +320,7 @@ pub fn queue_shadows( }) }, ); - current_camera_entity = geometry.extracted_camera; + current_camera_entity = layout.extracted_camera; } let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { @@ -327,7 +331,7 @@ pub fn queue_shadows( draw_function, pipeline: *pipeline, entity: (*entity, *main_entity), - sort_key: FloatOrd(geometry.stack_index as f32 + stack_z_offsets::BOX_SHADOW), + sort_key: FloatOrd(layout.stack_index as f32 + stack_z_offsets::BOX_SHADOW), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 8bbb73cd9a53c..24a47fdf791ff 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -427,7 +427,7 @@ pub fn extract_gradients( )] pub fn queue_gradient( extracted_gradients: Res, - extracted_geometry: Res, + extracted_layout: Res, gradients_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -441,12 +441,16 @@ pub fn queue_gradient( let mut current_phase = None; for (main_entity, sub_gradients) in extracted_gradients.items.iter() { - let Some(geometry) = extracted_geometry.layout.get(main_entity) else { + let Some(layout) = extracted_layout.layout.get(main_entity) else { continue; }; - if current_camera_entity != geometry.extracted_camera { - current_phase = render_views.get(geometry.extracted_camera).ok().and_then( + if !layout.visible { + continue; + } + + if current_camera_entity != layout.extracted_camera { + current_phase = render_views.get(layout.extracted_camera).ok().and_then( |(default_camera_view, ui_anti_alias)| { camera_views .get(default_camera_view.0) @@ -460,7 +464,7 @@ pub fn queue_gradient( }) }, ); - current_camera_entity = geometry.extracted_camera; + current_camera_entity = layout.extracted_camera; } let Some((target_format, ui_anti_alias, transparent_phase)) = current_phase.as_mut() else { @@ -482,7 +486,7 @@ pub fn queue_gradient( pipeline, entity: (*render_entity, *main_entity), sort_key: FloatOrd( - geometry.stack_index as f32 + layout.stack_index as f32 + match gradient.node_type { NodeType::Rect | NodeType::Inverted => stack_z_offsets::GRADIENT, NodeType::Border(_) => stack_z_offsets::BORDER_GRADIENT, diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index bd6e3ba7b422a..8479fddd65257 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -849,6 +849,10 @@ pub fn queue_uinodes( continue; }; + if !layout.visible { + continue; + } + let extracted_camera_entity = layout.extracted_camera; if current_camera_entity != extracted_camera_entity { diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index fef5dc10bfa44..cbf876be1f742 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -476,7 +476,7 @@ pub fn extract_text( pub fn queue_text( extracted_glyph_layouts: Res, - extracted_geometry: Res, + extracted_layout: Res, ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, @@ -488,11 +488,16 @@ pub fn queue_text( let draw_function = draw_functions.read().id::(); for (main_entity, (render_entity, _)) in extracted_glyph_layouts.uinodes.iter() { - let Some(geometry) = extracted_geometry.layout.get(main_entity) else { + let Some(layout) = extracted_layout.layout.get(main_entity) else { continue; }; + + if !layout.visible { + continue; + } + let Some((default_camera_view, ui_anti_alias)) = - render_views.get(geometry.extracted_camera).ok() + render_views.get(layout.extracted_camera).ok() else { continue; }; @@ -516,7 +521,7 @@ pub fn queue_text( draw_function, pipeline, entity: (*render_entity, *main_entity), - sort_key: FloatOrd(geometry.stack_index as f32 + stack_z_offsets::TEXT), + sort_key: FloatOrd(layout.stack_index as f32 + stack_z_offsets::TEXT), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, indexed: true, diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index d2e689c87e5e5..1d8a30a553391 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -594,7 +594,7 @@ impl RenderAsset for PreparedUiMaterial { pub fn queue_ui_material_nodes( extracted_uinodes: Res>, - extracted_geometry: Res, + extracted_layout: Res, draw_functions: Res>, ui_material_pipeline: Res>, mut pipelines: ResMut>>, @@ -611,10 +611,15 @@ pub fn queue_ui_material_nodes( let mut current_phase = None; for (main_entity, (render_entity, extracted_uinode)) in extracted_uinodes.uinodes.iter() { - let Some(geometry) = extracted_geometry.layout.get(main_entity) else { + let Some(layout) = extracted_layout.layout.get(main_entity) else { continue; }; - let extracted_camera_entity = geometry.extracted_camera; + + if !layout.visible { + continue; + } + + let extracted_camera_entity = layout.extracted_camera; if current_camera_entity != extracted_camera_entity { current_phase = @@ -661,7 +666,7 @@ pub fn queue_ui_material_nodes( draw_function, pipeline, entity: (*render_entity, *main_entity), - sort_key: FloatOrd(geometry.stack_index as f32 + M::stack_z_offset()), + sort_key: FloatOrd(layout.stack_index as f32 + M::stack_z_offset()), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, indexed: false, diff --git a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs index 86d675f20721a..86a34c2b0adcb 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -336,10 +336,15 @@ pub fn queue_ui_slices( let mut current_phase = None; for (main_entity, (render_entity, _)) in extracted_ui_slicers.slices.iter() { - let Some(geometry) = extracted_geometry.layout.get(main_entity) else { + let Some(layout) = extracted_geometry.layout.get(main_entity) else { continue; }; - let extracted_camera_entity = geometry.extracted_camera; + + if !layout.visible { + continue; + } + + let extracted_camera_entity = layout.extracted_camera; if current_camera_entity != extracted_camera_entity { current_phase = @@ -375,7 +380,7 @@ pub fn queue_ui_slices( draw_function, pipeline: *pipeline, entity: (*render_entity, *main_entity), - sort_key: FloatOrd(geometry.stack_index as f32 + stack_z_offsets::IMAGE), + sort_key: FloatOrd(layout.stack_index as f32 + stack_z_offsets::IMAGE), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, indexed: true, From 08597bbde651dbbf12de58f55ed2eadb84fefcc1 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 16:24:01 +0100 Subject: [PATCH 63/99] use [line_width; 4] instead of repeating four times --- crates/bevy_ui_render/src/debug_overlay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index d1c8ef917ddf6..e3771dc2b2e3d 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -255,7 +255,7 @@ fn push_debug_outline( color, flags: flags | shader_flags::CORNERS[i], radius: border_radius.into(), - border: [line_width, line_width, line_width, line_width], + border: [line_width; 4], size: size.into(), point: (QUAD_VERTEX_POSITIONS[i] * size + positions_diff[i]).into(), }); From 3bce767a68006655213521c9cd37ddadc79eb3f4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 17:12:17 +0100 Subject: [PATCH 64/99] extend texture range for debug outlines --- crates/bevy_ui_render/src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 8479fddd65257..b16217c86ac85 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1203,7 +1203,11 @@ fn prepare_uinodes( &extracted_ui_debug_overlay, ); - existing_batch.unwrap().1.range.end = ui_meta.indices.len() as u32; + let existing_batch = existing_batch.unwrap(); + existing_batch.1.range.end = ui_meta.indices.len() as u32; + if let Some((range, _)) = existing_batch.1.texture_ranges.last_mut() { + range.end = ui_meta.indices.len() as u32; + } ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; } From b783f2a313a0e2840f8ebe26c71f9b1d67a7ca0a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 17:55:18 +0100 Subject: [PATCH 65/99] store only indices where textures are changed in UiMeta, so batches can contain multiple textures --- crates/bevy_ui_render/src/lib.rs | 28 +++++++++++++----------- crates/bevy_ui_render/src/render_pass.rs | 10 ++++++--- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index b16217c86ac85..185c82a2f5ba6 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -779,6 +779,7 @@ pub struct UiMeta { pub(crate) vertices: RawBufferVec, pub(crate) indices: RawBufferVec, view_bind_group: Option, + texture_changes: Vec<(Range, AssetId)>, } impl Default for UiMeta { @@ -787,6 +788,7 @@ impl Default for UiMeta { vertices: RawBufferVec::new(BufferUsages::VERTEX), indices: RawBufferVec::new(BufferUsages::INDEX), view_bind_group: None, + texture_changes: Vec::default(), } } } @@ -806,7 +808,7 @@ pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; pub struct UiBatch { pub range: Range, pub image: AssetId, - pub texture_ranges: Vec<(Range, AssetId)>, + pub texture_changes: Range, } /// The values here should match the values for the constants in `ui.wgsl` @@ -1015,7 +1017,7 @@ fn prepare_uinodes( UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, image, - texture_ranges: vec![], + texture_changes: vec![], }, )); image_bind_groups.values.entry(image).or_insert_with(|| { @@ -1061,13 +1063,13 @@ fn prepare_uinodes( let range_end = ui_meta.indices.len() as u32; let existing_batch = existing_batch.unwrap(); existing_batch.1.range.end = range_end; - if let Some((range, texture)) = existing_batch.1.texture_ranges.last_mut() { + if let Some((range, texture)) = existing_batch.1.texture_changes.last_mut() { if image == AssetId::default() || *texture == image { range.end = range_end; } else if range_start < range_end { existing_batch .1 - .texture_ranges + .texture_changes .push((range_start..range_end, image)); } } @@ -1114,7 +1116,7 @@ fn prepare_uinodes( UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, image, - texture_ranges: Vec::with_capacity( + texture_changes: Vec::with_capacity( style.sections.len().saturating_mul(2).saturating_add(1), ), }, @@ -1141,13 +1143,13 @@ fn prepare_uinodes( let existing_batch = existing_batch.unwrap(); existing_batch .1 - .texture_ranges + .texture_changes .reserve(style.sections.len().saturating_mul(2).saturating_add(1)); - if existing_batch.1.texture_ranges.is_empty() && !existing_batch.1.range.is_empty() + if existing_batch.1.texture_changes.is_empty() && !existing_batch.1.range.is_empty() { existing_batch .1 - .texture_ranges + .texture_changes .push((existing_batch.1.range.clone(), existing_batch.1.image)); } push_text_vertices( @@ -1156,9 +1158,9 @@ fn prepare_uinodes( style, &gpu_images, image, - &mut existing_batch.1.texture_ranges, + &mut existing_batch.1.texture_changes, ); - for (_, texture) in &existing_batch.1.texture_ranges { + for (_, texture) in &existing_batch.1.texture_changes { let gpu_image = gpu_images .get(*texture) .expect("Image was checked during quad generation and should still exist"); @@ -1174,7 +1176,7 @@ fn prepare_uinodes( }); } existing_batch.1.range.end = ui_meta.indices.len() as u32; - if let Some((_, texture)) = existing_batch.1.texture_ranges.last() { + if let Some((_, texture)) = existing_batch.1.texture_changes.last() { batch_image_handle = Some(*texture); } ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; @@ -1191,7 +1193,7 @@ fn prepare_uinodes( UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, image: AssetId::default(), - texture_ranges: vec![], + texture_changes: vec![], }, )); existing_batch = batches.last_mut(); @@ -1205,7 +1207,7 @@ fn prepare_uinodes( let existing_batch = existing_batch.unwrap(); existing_batch.1.range.end = ui_meta.indices.len() as u32; - if let Some((range, _)) = existing_batch.1.texture_ranges.last_mut() { + if let Some((range, _)) = existing_batch.1.texture_changes.last_mut() { range.end = ui_meta.indices.len() as u32; } ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; diff --git a/crates/bevy_ui_render/src/render_pass.rs b/crates/bevy_ui_render/src/render_pass.rs index 48793d396062a..88892d2d38240 100644 --- a/crates/bevy_ui_render/src/render_pass.rs +++ b/crates/bevy_ui_render/src/render_pass.rs @@ -237,11 +237,15 @@ impl RenderCommand

for DrawUiNode { indices.slice(..), bevy_render::render_resource::IndexFormat::Uint32, ); - if !batch.texture_ranges.is_empty() { + if !batch.texture_changes.is_empty() { let image_bind_groups = image_bind_groups.into_inner(); - for (range, image) in &batch.texture_ranges { + + for range_index in batch.texture_changes.clone() { + let (texture_range, image) = &ui_meta.texture_changes[range_index as usize]; + let start = texture_range.start.max(batch.range.start); + let end = texture_range.end.min(batch.range.end); pass.set_bind_group(1, image_bind_groups.values.get(image).unwrap(), &[]); - pass.draw_indexed(range.clone(), 0, 0..1); + pass.draw_indexed(start..end, 0, 0..1); } return RenderCommandResult::Success; } From f0784518a6fd8db9723dd17399abc5206f1d0203 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 18:08:09 +0100 Subject: [PATCH 66/99] migrated prepare_uinodes to use new texture changes list --- crates/bevy_ui_render/src/lib.rs | 96 +++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 33 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 185c82a2f5ba6..fa27cf104662e 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -965,6 +965,7 @@ fn prepare_uinodes( // UiMeta contains the vertex and index buffer data and the current view ui_meta.vertices.clear(); ui_meta.indices.clear(); + ui_meta.texture_changes.clear(); ui_meta.view_bind_group = Some(render_device.create_bind_group( "ui_view_bind_group", &pipeline_cache.get_bind_group_layout(&ui_pipeline.view_layout), @@ -1012,12 +1013,14 @@ fn prepare_uinodes( }; batch_start_item_index = phase_item_index; batch_image_handle = Some(image); + + let changes_start = ui_meta.texture_changes.len() as u32; batches.push(( entity, UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, image, - texture_changes: vec![], + texture_changes: changes_start..changes_start, }, )); image_bind_groups.values.entry(image).or_insert_with(|| { @@ -1063,16 +1066,27 @@ fn prepare_uinodes( let range_end = ui_meta.indices.len() as u32; let existing_batch = existing_batch.unwrap(); existing_batch.1.range.end = range_end; - if let Some((range, texture)) = existing_batch.1.texture_changes.last_mut() { + if let Some((range, texture)) = ui_meta.texture_changes.last_mut() { if image == AssetId::default() || *texture == image { range.end = range_end; } else if range_start < range_end { - existing_batch - .1 + ui_meta .texture_changes .push((range_start..range_end, image)); } + } else if range_start < range_end { + ui_meta + .texture_changes + .push((range_start..range_end, image)); + } + let texture_changes_start = existing_batch.1.texture_changes.start as usize; + if texture_changes_start > 0 + && ui_meta.texture_changes[texture_changes_start - 1].0.end + > existing_batch.1.range.start + { + existing_batch.1.texture_changes.start -= 1; } + existing_batch.1.texture_changes.end = ui_meta.texture_changes.len() as u32; ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; continue; } @@ -1116,9 +1130,8 @@ fn prepare_uinodes( UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, image, - texture_changes: Vec::with_capacity( - style.sections.len().saturating_mul(2).saturating_add(1), - ), + texture_changes: ui_meta.texture_changes.len() as u32 + ..ui_meta.texture_changes.len() as u32, }, )); existing_batch = batches.last_mut(); @@ -1141,26 +1154,23 @@ fn prepare_uinodes( }); let existing_batch = existing_batch.unwrap(); - existing_batch - .1 + ui_meta .texture_changes .reserve(style.sections.len().saturating_mul(2).saturating_add(1)); - if existing_batch.1.texture_changes.is_empty() && !existing_batch.1.range.is_empty() + push_text_vertices(&mut ui_meta, layout, style, &gpu_images, image); + existing_batch.1.range.end = ui_meta.indices.len() as u32; + let texture_changes_start = existing_batch.1.texture_changes.start as usize; + if texture_changes_start > 0 + && ui_meta.texture_changes[texture_changes_start - 1].0.end + > existing_batch.1.range.start { - existing_batch - .1 - .texture_changes - .push((existing_batch.1.range.clone(), existing_batch.1.image)); + existing_batch.1.texture_changes.start -= 1; } - push_text_vertices( - &mut ui_meta, - layout, - style, - &gpu_images, - image, - &mut existing_batch.1.texture_changes, - ); - for (_, texture) in &existing_batch.1.texture_changes { + existing_batch.1.texture_changes.end = ui_meta.texture_changes.len() as u32; + for (_, texture) in &ui_meta.texture_changes[existing_batch.1.texture_changes.start + as usize + ..existing_batch.1.texture_changes.end as usize] + { let gpu_image = gpu_images .get(*texture) .expect("Image was checked during quad generation and should still exist"); @@ -1175,8 +1185,7 @@ fn prepare_uinodes( ) }); } - existing_batch.1.range.end = ui_meta.indices.len() as u32; - if let Some((_, texture)) = existing_batch.1.texture_changes.last() { + if let Some((_, texture)) = ui_meta.texture_changes.last() { batch_image_handle = Some(*texture); } ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; @@ -1193,12 +1202,14 @@ fn prepare_uinodes( UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, image: AssetId::default(), - texture_changes: vec![], + texture_changes: ui_meta.texture_changes.len() as u32 + ..ui_meta.texture_changes.len() as u32, }, )); existing_batch = batches.last_mut(); } + let range_start = ui_meta.indices.len() as u32; debug_overlay::push_debug_overlay_vertices( &mut ui_meta, &extracted_ui_layout, @@ -1206,10 +1217,25 @@ fn prepare_uinodes( ); let existing_batch = existing_batch.unwrap(); - existing_batch.1.range.end = ui_meta.indices.len() as u32; - if let Some((range, _)) = existing_batch.1.texture_changes.last_mut() { - range.end = ui_meta.indices.len() as u32; + let range_end = ui_meta.indices.len() as u32; + existing_batch.1.range.end = range_end; + if range_start < range_end { + if let Some((range, _)) = ui_meta.texture_changes.last_mut() { + range.end = range_end; + } else { + ui_meta + .texture_changes + .push((range_start..range_end, AssetId::default())); + } } + let texture_changes_start = existing_batch.1.texture_changes.start as usize; + if texture_changes_start > 0 + && ui_meta.texture_changes[texture_changes_start - 1].0.end + > existing_batch.1.range.start + { + existing_batch.1.texture_changes.start -= 1; + } + existing_batch.1.texture_changes.end = ui_meta.texture_changes.len() as u32; ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; } @@ -1492,7 +1518,6 @@ fn push_text_vertices( style: &ExtractedGlyphLayout, gpu_images: &RenderAssets, image: AssetId, - texture_ranges: &mut Vec<(Range, AssetId)>, ) { let uinode = &layout.uinode; let shadow_offset = style.shadow_offset / uinode.inverse_scale_factor(); @@ -1512,7 +1537,8 @@ fn push_text_vertices( } else { layout.clip }; - let (mut texture_range_start, mut current_texture) = texture_ranges + let (mut texture_range_start, mut current_texture) = ui_meta + .texture_changes .pop() .map_or((ui_meta.indices.len() as u32, image), |(range, texture)| { (range.start, texture) @@ -1569,7 +1595,9 @@ fn push_text_vertices( let range_end = ui_meta.indices.len() as u32; if current_texture != atlas_texture { if texture_range_start < range_end { - texture_ranges.push((texture_range_start..range_end, current_texture)); + ui_meta + .texture_changes + .push((texture_range_start..range_end, current_texture)); } texture_range_start = range_end; current_texture = atlas_texture; @@ -1685,6 +1713,8 @@ fn push_text_vertices( } let range_end = ui_meta.indices.len() as u32; if texture_range_start < range_end { - texture_ranges.push((texture_range_start..range_end, current_texture)); + ui_meta + .texture_changes + .push((texture_range_start..range_end, current_texture)); } } From 6808026dc7d13fe712b00ced8a3e7c82f8d98d36 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 18:15:38 +0100 Subject: [PATCH 67/99] Removed SetUiTextureBindGroup --- crates/bevy_ui_render/src/render_pass.rs | 34 +++--------------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/crates/bevy_ui_render/src/render_pass.rs b/crates/bevy_ui_render/src/render_pass.rs index 88892d2d38240..4891740841fb5 100644 --- a/crates/bevy_ui_render/src/render_pass.rs +++ b/crates/bevy_ui_render/src/render_pass.rs @@ -154,12 +154,7 @@ impl CachedRenderPipelinePhaseItem for TransparentUi { } } -pub type DrawUi = ( - SetItemPipeline, - SetUiViewBindGroup<0>, - SetUiTextureBindGroup<1>, - DrawUiNode, -); +pub type DrawUi = (SetItemPipeline, SetUiViewBindGroup<0>, DrawUiNode); pub struct SetUiViewBindGroup; impl RenderCommand

for SetUiViewBindGroup { @@ -181,29 +176,6 @@ impl RenderCommand

for SetUiViewBindGroup { RenderCommandResult::Success } } -pub struct SetUiTextureBindGroup; -impl RenderCommand

for SetUiTextureBindGroup { - type Param = SRes; - type ViewQuery = (); - type ItemQuery = Read; - - #[inline] - fn render<'w>( - _item: &P, - _view: (), - batch: Option<&'w UiBatch>, - image_bind_groups: SystemParamItem<'w, '_, Self::Param>, - pass: &mut TrackedRenderPass<'w>, - ) -> RenderCommandResult { - let image_bind_groups = image_bind_groups.into_inner(); - let Some(batch) = batch else { - return RenderCommandResult::Skip; - }; - - pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]); - RenderCommandResult::Success - } -} pub struct DrawUiNode; impl RenderCommand

for DrawUiNode { @@ -237,9 +209,8 @@ impl RenderCommand

for DrawUiNode { indices.slice(..), bevy_render::render_resource::IndexFormat::Uint32, ); + let image_bind_groups = image_bind_groups.into_inner(); if !batch.texture_changes.is_empty() { - let image_bind_groups = image_bind_groups.into_inner(); - for range_index in batch.texture_changes.clone() { let (texture_range, image) = &ui_meta.texture_changes[range_index as usize]; let start = texture_range.start.max(batch.range.start); @@ -250,6 +221,7 @@ impl RenderCommand

for DrawUiNode { return RenderCommandResult::Success; } // Draw the vertices + pass.set_bind_group(1, image_bind_groups.values.get(&batch.image).unwrap(), &[]); pass.draw_indexed(batch.range.clone(), 0, 0..1); RenderCommandResult::Success } From 3e16dc9a134dee3e2b029735ef5f37881c10fdd9 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 18:22:08 +0100 Subject: [PATCH 68/99] prepare_uinodes batch_open flag --- crates/bevy_ui_render/src/lib.rs | 94 ++++++++------------------------ 1 file changed, 24 insertions(+), 70 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index fa27cf104662e..088c1659b495d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -977,8 +977,7 @@ fn prepare_uinodes( // used to store the index of the first phase item in a batch let mut batch_start_item_index = 0; - // Setting batch_image_handle to None is used to signal the end of the current batch. - let mut batch_image_handle = None; + let mut batch_open = false; for phase_item_index in 0..ui_phase.items.len() { let main_entity = ui_phase.items[phase_item_index].main_entity(); @@ -990,29 +989,24 @@ fn prepare_uinodes( .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) { let Some(layout) = extracted_ui_layout.layout.get(&main_entity) else { - batch_image_handle = None; + batch_open = false; continue; }; let image = style .image .filter(|image| gpu_images.get(*image).is_some()) .unwrap_or_default(); + let Some(gpu_image) = gpu_images.get(image) else { + batch_open = false; + continue; + }; ui_phase.items[phase_item_index].batch_range = phase_item_index as u32..phase_item_index as u32; let mut existing_batch = batches.last_mut(); - if batch_image_handle.is_none() - || existing_batch.is_none() - || (batch_image_handle != Some(AssetId::default()) - && image != AssetId::default() - && batch_image_handle != Some(image)) - { - let Some(gpu_image) = gpu_images.get(image) else { - batch_image_handle = None; - continue; - }; + if !batch_open || existing_batch.is_none() { batch_start_item_index = phase_item_index; - batch_image_handle = Some(image); + batch_open = true; let changes_start = ui_meta.texture_changes.len() as u32; batches.push(( @@ -1023,44 +1017,18 @@ fn prepare_uinodes( texture_changes: changes_start..changes_start, }, )); - image_bind_groups.values.entry(image).or_insert_with(|| { - render_device.create_bind_group( - "ui_material_bind_group", - &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential(( - &gpu_image.texture_view, - &gpu_image.sampler, - )), - ) - }); existing_batch = batches.last_mut(); - } else if batch_image_handle == Some(AssetId::default()) - && image != AssetId::default() - { - if let Some(ref mut existing_batch) = existing_batch - && let Some(gpu_image) = gpu_images.get(image) - { - batch_image_handle = Some(image); - existing_batch.1.image = image; - image_bind_groups.values.entry(image).or_insert_with(|| { - render_device.create_bind_group( - "ui_material_bind_group", - &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), - &BindGroupEntries::sequential(( - &gpu_image.texture_view, - &gpu_image.sampler, - )), - ) - }); - } else { - batch_image_handle = None; - continue; - } } - - let gpu_image = gpu_images - .get(image) - .expect("Image was checked during batching and should still exist"); + image_bind_groups.values.entry(image).or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential(( + &gpu_image.texture_view, + &gpu_image.sampler, + )), + ) + }); let range_start = ui_meta.indices.len() as u32; push_uinode_vertices(&mut ui_meta, layout, style, image, gpu_image); let range_end = ui_meta.indices.len() as u32; @@ -1097,7 +1065,7 @@ fn prepare_uinodes( .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) { let Some(layout) = extracted_ui_layout.layout.get(&main_entity) else { - batch_image_handle = None; + batch_open = false; continue; }; let image = style @@ -1110,21 +1078,16 @@ fn prepare_uinodes( }) .unwrap_or_default(); let Some(gpu_image) = gpu_images.get(image) else { - batch_image_handle = None; + batch_open = false; continue; }; ui_phase.items[phase_item_index].batch_range = phase_item_index as u32..phase_item_index as u32; let mut existing_batch = batches.last_mut(); - if batch_image_handle.is_none() - || existing_batch.is_none() - || (batch_image_handle != Some(AssetId::default()) - && image != AssetId::default() - && batch_image_handle != Some(image)) - { + if !batch_open || existing_batch.is_none() { batch_start_item_index = phase_item_index; - batch_image_handle = Some(image); + batch_open = true; batches.push(( entity, UiBatch { @@ -1135,12 +1098,6 @@ fn prepare_uinodes( }, )); existing_batch = batches.last_mut(); - } else if batch_image_handle == Some(AssetId::default()) - && image != AssetId::default() - && let Some(existing_batch) = &mut existing_batch - { - batch_image_handle = Some(image); - existing_batch.1.image = image; } image_bind_groups.values.entry(image).or_insert_with(|| { render_device.create_bind_group( @@ -1185,9 +1142,6 @@ fn prepare_uinodes( ) }); } - if let Some((_, texture)) = ui_meta.texture_changes.last() { - batch_image_handle = Some(*texture); - } ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; continue; } @@ -1195,7 +1149,7 @@ fn prepare_uinodes( #[cfg(feature = "bevy_ui_debug")] { let mut existing_batch = batches.last_mut(); - if batch_image_handle.is_none() || existing_batch.is_none() { + if !batch_open || existing_batch.is_none() { batch_start_item_index = phase_item_index; batches.push(( ui_phase.items[batch_start_item_index].entity(), @@ -1239,7 +1193,7 @@ fn prepare_uinodes( ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; } - batch_image_handle = None; + batch_open = false; } } From ec99b86335679ec4b8923742dd713d0e367b45d1 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 18:24:42 +0100 Subject: [PATCH 69/99] Made UiBatch exclusively range driven --- crates/bevy_ui_render/src/lib.rs | 4 ---- crates/bevy_ui_render/src/render_pass.rs | 18 ++++++------------ 2 files changed, 6 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 088c1659b495d..9ae7c030b4f20 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -807,7 +807,6 @@ pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; #[derive(Component, Debug)] pub struct UiBatch { pub range: Range, - pub image: AssetId, pub texture_changes: Range, } @@ -1013,7 +1012,6 @@ fn prepare_uinodes( entity, UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, - image, texture_changes: changes_start..changes_start, }, )); @@ -1092,7 +1090,6 @@ fn prepare_uinodes( entity, UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, - image, texture_changes: ui_meta.texture_changes.len() as u32 ..ui_meta.texture_changes.len() as u32, }, @@ -1155,7 +1152,6 @@ fn prepare_uinodes( ui_phase.items[batch_start_item_index].entity(), UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, - image: AssetId::default(), texture_changes: ui_meta.texture_changes.len() as u32 ..ui_meta.texture_changes.len() as u32, }, diff --git a/crates/bevy_ui_render/src/render_pass.rs b/crates/bevy_ui_render/src/render_pass.rs index 4891740841fb5..18eb9a3cb3856 100644 --- a/crates/bevy_ui_render/src/render_pass.rs +++ b/crates/bevy_ui_render/src/render_pass.rs @@ -210,19 +210,13 @@ impl RenderCommand

for DrawUiNode { bevy_render::render_resource::IndexFormat::Uint32, ); let image_bind_groups = image_bind_groups.into_inner(); - if !batch.texture_changes.is_empty() { - for range_index in batch.texture_changes.clone() { - let (texture_range, image) = &ui_meta.texture_changes[range_index as usize]; - let start = texture_range.start.max(batch.range.start); - let end = texture_range.end.min(batch.range.end); - pass.set_bind_group(1, image_bind_groups.values.get(image).unwrap(), &[]); - pass.draw_indexed(start..end, 0, 0..1); - } - return RenderCommandResult::Success; + for range_index in batch.texture_changes.clone() { + let (texture_range, image) = &ui_meta.texture_changes[range_index as usize]; + let start = texture_range.start.max(batch.range.start); + let end = texture_range.end.min(batch.range.end); + pass.set_bind_group(1, image_bind_groups.values.get(image).unwrap(), &[]); + pass.draw_indexed(start..end, 0, 0..1); } - // Draw the vertices - pass.set_bind_group(1, image_bind_groups.values.get(&batch.image).unwrap(), &[]); - pass.draw_indexed(batch.range.clone(), 0, 0..1); RenderCommandResult::Success } } From d3c0614847a7ec2d36ddc1f4f6fc225ae8b38ee4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 18:34:10 +0100 Subject: [PATCH 70/99] added get helper on ExtractedUiNodeStyles --- crates/bevy_ui_render/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 9ae7c030b4f20..096b1cc42005d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -387,6 +387,20 @@ pub struct ExtractedUiNodeStyles { pub changed_this_frame: MainEntityHashSet, } +impl ExtractedUiNodeStyles { + pub fn get_style( + &self, + main_entity: MainEntity, + render_entity: Entity, + ) -> Option<&ExtractedUiNodeStyle> { + self.uinodes + .get(&main_entity) + .and_then(|(style_render_entity, style)| { + (render_entity == *style_render_entity).then_some(style) + }) + } +} + pub fn extract_uinode_styles( mut commands: Commands, mut extracted_uinodes: ResMut, From b1c75e6d7b4fab4eb204a31a023e9c2cfd86de1a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 18:37:54 +0100 Subject: [PATCH 71/99] added get_style method to ExtractedUiNodeStyles --- crates/bevy_ui_render/src/lib.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 096b1cc42005d..f7469292d2c39 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -941,9 +941,9 @@ fn prepare_uinodes( render_queue: Res, pipeline_cache: Res, mut ui_meta: ResMut, - extracted_uinodes: Res, + extracted_styles: Res, extracted_glyph_layouts: Res, - extracted_ui_layout: Res, + extracted_layout: Res, view_uniforms: Res, ui_pipeline: Res, mut image_bind_groups: ResMut, @@ -996,12 +996,8 @@ fn prepare_uinodes( let main_entity = ui_phase.items[phase_item_index].main_entity(); let entity = ui_phase.items[phase_item_index].entity(); - if let Some(style) = extracted_uinodes - .uinodes - .get(&main_entity) - .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) - { - let Some(layout) = extracted_ui_layout.layout.get(&main_entity) else { + if let Some(style) = extracted_styles.get_style(main_entity, entity) { + let Some(layout) = extracted_layout.layout.get(&main_entity) else { batch_open = false; continue; }; @@ -1076,7 +1072,7 @@ fn prepare_uinodes( .get(&main_entity) .and_then(|(render_entity, style)| (*render_entity == entity).then_some(style)) { - let Some(layout) = extracted_ui_layout.layout.get(&main_entity) else { + let Some(layout) = extracted_layout.layout.get(&main_entity) else { batch_open = false; continue; }; @@ -1176,7 +1172,7 @@ fn prepare_uinodes( let range_start = ui_meta.indices.len() as u32; debug_overlay::push_debug_overlay_vertices( &mut ui_meta, - &extracted_ui_layout, + &extracted_layout, &extracted_ui_debug_overlay, ); From 01441c8a9a1ba19face49584cc20d7d87fe752ad Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 18:42:01 +0100 Subject: [PATCH 72/99] reordered comparisons ltr --- crates/bevy_ui_render/src/lib.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index f7469292d2c39..daf262b89a314 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1056,9 +1056,9 @@ fn prepare_uinodes( .push((range_start..range_end, image)); } let texture_changes_start = existing_batch.1.texture_changes.start as usize; - if texture_changes_start > 0 - && ui_meta.texture_changes[texture_changes_start - 1].0.end - > existing_batch.1.range.start + if 0 < texture_changes_start + && existing_batch.1.range.start + < ui_meta.texture_changes[texture_changes_start - 1].0.end { existing_batch.1.texture_changes.start -= 1; } @@ -1124,9 +1124,9 @@ fn prepare_uinodes( push_text_vertices(&mut ui_meta, layout, style, &gpu_images, image); existing_batch.1.range.end = ui_meta.indices.len() as u32; let texture_changes_start = existing_batch.1.texture_changes.start as usize; - if texture_changes_start > 0 - && ui_meta.texture_changes[texture_changes_start - 1].0.end - > existing_batch.1.range.start + if 0 < texture_changes_start + && existing_batch.1.range.start + < ui_meta.texture_changes[texture_changes_start - 1].0.end { existing_batch.1.texture_changes.start -= 1; } @@ -1189,9 +1189,9 @@ fn prepare_uinodes( } } let texture_changes_start = existing_batch.1.texture_changes.start as usize; - if texture_changes_start > 0 - && ui_meta.texture_changes[texture_changes_start - 1].0.end - > existing_batch.1.range.start + if 0 < texture_changes_start + && existing_batch.1.range.start + < ui_meta.texture_changes[texture_changes_start - 1].0.end { existing_batch.1.texture_changes.start -= 1; } @@ -1413,7 +1413,7 @@ fn push_uinode_vertices( } } - if !style.outline_color.is_fully_transparent() && uinode.outline_width() > 0. { + if !style.outline_color.is_fully_transparent() && 0. < uinode.outline_width() { push_quad( style.outline_color, Rect { From cb921bbacc961d0b622182ffc0e3ddc5040cc53f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 18:49:45 +0100 Subject: [PATCH 73/99] removed redundant comparison --- crates/bevy_ui_render/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index daf262b89a314..5294984138c0c 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1013,16 +1013,16 @@ fn prepare_uinodes( ui_phase.items[phase_item_index].batch_range = phase_item_index as u32..phase_item_index as u32; let mut existing_batch = batches.last_mut(); - if !batch_open || existing_batch.is_none() { + if !batch_open { batch_start_item_index = phase_item_index; batch_open = true; - let changes_start = ui_meta.texture_changes.len() as u32; batches.push(( entity, UiBatch { range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, - texture_changes: changes_start..changes_start, + texture_changes: ui_meta.texture_changes.len() as u32 + ..ui_meta.texture_changes.len() as u32, }, )); existing_batch = batches.last_mut(); From 01906d2881153dd47d40f1f00f6352796919911c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 18:53:45 +0100 Subject: [PATCH 74/99] added `UiBatch::empty` helper --- crates/bevy_ui_render/src/lib.rs | 35 ++++++++++++-------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 5294984138c0c..3e3e48b52d40f 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -824,6 +824,16 @@ pub struct UiBatch { pub texture_changes: Range, } +impl UiBatch { + fn empty(ui_meta: &UiMeta) -> Self { + UiBatch { + range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, + texture_changes: ui_meta.texture_changes.len() as u32 + ..ui_meta.texture_changes.len() as u32, + } + } +} + /// The values here should match the values for the constants in `ui.wgsl` pub mod shader_flags { /// Texture should be ignored @@ -1016,15 +1026,7 @@ fn prepare_uinodes( if !batch_open { batch_start_item_index = phase_item_index; batch_open = true; - - batches.push(( - entity, - UiBatch { - range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, - texture_changes: ui_meta.texture_changes.len() as u32 - ..ui_meta.texture_changes.len() as u32, - }, - )); + batches.push((entity, UiBatch::empty(&ui_meta))); existing_batch = batches.last_mut(); } image_bind_groups.values.entry(image).or_insert_with(|| { @@ -1096,14 +1098,7 @@ fn prepare_uinodes( if !batch_open || existing_batch.is_none() { batch_start_item_index = phase_item_index; batch_open = true; - batches.push(( - entity, - UiBatch { - range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, - texture_changes: ui_meta.texture_changes.len() as u32 - ..ui_meta.texture_changes.len() as u32, - }, - )); + batches.push((entity, UiBatch::empty(&ui_meta))); existing_batch = batches.last_mut(); } image_bind_groups.values.entry(image).or_insert_with(|| { @@ -1160,11 +1155,7 @@ fn prepare_uinodes( batch_start_item_index = phase_item_index; batches.push(( ui_phase.items[batch_start_item_index].entity(), - UiBatch { - range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, - texture_changes: ui_meta.texture_changes.len() as u32 - ..ui_meta.texture_changes.len() as u32, - }, + UiBatch::empty(&ui_meta), )); existing_batch = batches.last_mut(); } From 77f502c9d8cad4a35601f12d559070eb9f2e350b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 19:01:19 +0100 Subject: [PATCH 75/99] cleanup and consolidate existing batch unwraps --- crates/bevy_ui_render/src/lib.rs | 52 +++++++++++++++----------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 3e3e48b52d40f..1be3f61a48bc5 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1022,12 +1022,11 @@ fn prepare_uinodes( ui_phase.items[phase_item_index].batch_range = phase_item_index as u32..phase_item_index as u32; - let mut existing_batch = batches.last_mut(); + if !batch_open { batch_start_item_index = phase_item_index; batch_open = true; batches.push((entity, UiBatch::empty(&ui_meta))); - existing_batch = batches.last_mut(); } image_bind_groups.values.entry(image).or_insert_with(|| { render_device.create_bind_group( @@ -1042,8 +1041,8 @@ fn prepare_uinodes( let range_start = ui_meta.indices.len() as u32; push_uinode_vertices(&mut ui_meta, layout, style, image, gpu_image); let range_end = ui_meta.indices.len() as u32; - let existing_batch = existing_batch.unwrap(); - existing_batch.1.range.end = range_end; + let existing_batch = &mut batches.last_mut().unwrap().1; + existing_batch.range.end = range_end; if let Some((range, texture)) = ui_meta.texture_changes.last_mut() { if image == AssetId::default() || *texture == image { range.end = range_end; @@ -1057,14 +1056,14 @@ fn prepare_uinodes( .texture_changes .push((range_start..range_end, image)); } - let texture_changes_start = existing_batch.1.texture_changes.start as usize; + let texture_changes_start = existing_batch.texture_changes.start as usize; if 0 < texture_changes_start - && existing_batch.1.range.start + && existing_batch.range.start < ui_meta.texture_changes[texture_changes_start - 1].0.end { - existing_batch.1.texture_changes.start -= 1; + existing_batch.texture_changes.start -= 1; } - existing_batch.1.texture_changes.end = ui_meta.texture_changes.len() as u32; + existing_batch.texture_changes.end = ui_meta.texture_changes.len() as u32; ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; continue; } @@ -1094,12 +1093,11 @@ fn prepare_uinodes( ui_phase.items[phase_item_index].batch_range = phase_item_index as u32..phase_item_index as u32; - let mut existing_batch = batches.last_mut(); - if !batch_open || existing_batch.is_none() { + + if !batch_open { batch_start_item_index = phase_item_index; batch_open = true; batches.push((entity, UiBatch::empty(&ui_meta))); - existing_batch = batches.last_mut(); } image_bind_groups.values.entry(image).or_insert_with(|| { render_device.create_bind_group( @@ -1112,23 +1110,23 @@ fn prepare_uinodes( ) }); - let existing_batch = existing_batch.unwrap(); + let existing_batch = &mut batches.last_mut().unwrap().1; ui_meta .texture_changes .reserve(style.sections.len().saturating_mul(2).saturating_add(1)); push_text_vertices(&mut ui_meta, layout, style, &gpu_images, image); - existing_batch.1.range.end = ui_meta.indices.len() as u32; - let texture_changes_start = existing_batch.1.texture_changes.start as usize; + existing_batch.range.end = ui_meta.indices.len() as u32; + let texture_changes_start = existing_batch.texture_changes.start as usize; if 0 < texture_changes_start - && existing_batch.1.range.start + && existing_batch.range.start < ui_meta.texture_changes[texture_changes_start - 1].0.end { - existing_batch.1.texture_changes.start -= 1; + existing_batch.texture_changes.start -= 1; } - existing_batch.1.texture_changes.end = ui_meta.texture_changes.len() as u32; - for (_, texture) in &ui_meta.texture_changes[existing_batch.1.texture_changes.start + existing_batch.texture_changes.end = ui_meta.texture_changes.len() as u32; + for (_, texture) in &ui_meta.texture_changes[existing_batch.texture_changes.start as usize - ..existing_batch.1.texture_changes.end as usize] + ..existing_batch.texture_changes.end as usize] { let gpu_image = gpu_images .get(*texture) @@ -1150,14 +1148,12 @@ fn prepare_uinodes( #[cfg(feature = "bevy_ui_debug")] { - let mut existing_batch = batches.last_mut(); - if !batch_open || existing_batch.is_none() { + if !batch_open { batch_start_item_index = phase_item_index; batches.push(( ui_phase.items[batch_start_item_index].entity(), UiBatch::empty(&ui_meta), )); - existing_batch = batches.last_mut(); } let range_start = ui_meta.indices.len() as u32; @@ -1167,9 +1163,9 @@ fn prepare_uinodes( &extracted_ui_debug_overlay, ); - let existing_batch = existing_batch.unwrap(); + let existing_batch = &mut batches.last_mut().unwrap().1; let range_end = ui_meta.indices.len() as u32; - existing_batch.1.range.end = range_end; + existing_batch.range.end = range_end; if range_start < range_end { if let Some((range, _)) = ui_meta.texture_changes.last_mut() { range.end = range_end; @@ -1179,14 +1175,14 @@ fn prepare_uinodes( .push((range_start..range_end, AssetId::default())); } } - let texture_changes_start = existing_batch.1.texture_changes.start as usize; + let texture_changes_start = existing_batch.texture_changes.start as usize; if 0 < texture_changes_start - && existing_batch.1.range.start + && existing_batch.range.start < ui_meta.texture_changes[texture_changes_start - 1].0.end { - existing_batch.1.texture_changes.start -= 1; + existing_batch.texture_changes.start -= 1; } - existing_batch.1.texture_changes.end = ui_meta.texture_changes.len() as u32; + existing_batch.texture_changes.end = ui_meta.texture_changes.len() as u32; ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; } From 4587b52ff2ce2980cdc84716d5bcca8cf3913849 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 19:19:05 +0100 Subject: [PATCH 76/99] associate cameras and debug overlays --- crates/bevy_ui_render/src/debug_overlay.rs | 38 ++++++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index e3771dc2b2e3d..4b0d59410a11b 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -4,10 +4,15 @@ use crate::{ QUAD_VERTEX_POSITIONS, }; +use bevy_camera::{Camera2d, Camera3d}; use bevy_color::{ColorToComponents, Hsla, LinearRgba}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - entity::Entity, lifecycle::RemovedComponents, prelude::*, query::Changed, resource::Resource, + entity::{Entity, EntityHashMap, EntityHashSet}, + lifecycle::RemovedComponents, + prelude::*, + query::Changed, + resource::Resource, }; use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; @@ -16,7 +21,7 @@ use bevy_reflect::Reflect; use bevy_render::{ render_phase::{DrawFunctions, PhaseItemExtraIndex, ViewSortedRenderPhases}, render_resource::{PipelineCache, SpecializedRenderPipelines}, - sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}, + sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet, RenderEntity}, view::ExtractedView, Extract, }; @@ -100,6 +105,7 @@ pub struct ExtractedUiDebugOverlay { /// z_index of whole visualization. pub z_offset: f32, pub default_outline: UiDebugOutline, + pub extracted_camera_view_to_ids: EntityHashMap<(Entity, MainEntity)>, pub per_node_outline: MainEntityHashMap, pub changed_this_frame: MainEntityHashSet, } @@ -114,16 +120,38 @@ impl ExtractedUiDebugOverlay { } pub fn extract_debug_overlay( + mut commands: Commands, global_debug_options: Extract>, mut extracted_debug_layer: ResMut, + camera_query: Extract, With)>>>, ui_debug_outlines_query: Extract>>, mut removed_debug_options: Extract>, ui_stack: Extract>, + mut live_camera_views: Local, ) { extracted_debug_layer.changed_this_frame.clear(); extracted_debug_layer.z_offset = ui_stack.uinodes.len() as f32; extracted_debug_layer.default_outline = global_debug_options.0.clone(); + live_camera_views.clear(); + for (main_entity, extracted_camera_view) in camera_query.iter() { + live_camera_views.insert(extracted_camera_view); + extracted_debug_layer + .extracted_camera_view_to_ids + .entry(extracted_camera_view) + .or_insert_with(|| (commands.spawn_empty().id(), main_entity.into())); + } + extracted_debug_layer.extracted_camera_view_to_ids.retain( + |extracted_camera_view, (render_entity, _)| { + if live_camera_views.contains(extracted_camera_view) { + true + } else { + commands.entity(*render_entity).despawn(); + false + } + }, + ); + // iter through all nodes with UiDebugOptions // add to processed this frame list, so they aren't removed if tagged by removal detection for (entity, debug_outlines) in ui_debug_outlines_query.iter() { @@ -182,10 +210,14 @@ pub fn queue_debug_overlay( continue; }; + let Some(item_ids) = extracted_overlays.extracted_camera_view_to_ids.get(&entity) else { + continue; + }; + transparent_phase.add_transient(TransparentUi { draw_function, pipeline: *pipeline, - entity: (entity, entity.into()), + entity: *item_ids, sort_key: FloatOrd(extracted_overlays.z_offset), batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, From 200f91664f2bed04c4a797f8a7a73392833b2e40 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 19:33:16 +0100 Subject: [PATCH 77/99] Close the current texture range at each batch boundary. --- crates/bevy_ui_render/src/lib.rs | 71 ++++++++++++------------ crates/bevy_ui_render/src/render_pass.rs | 4 +- 2 files changed, 35 insertions(+), 40 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 1be3f61a48bc5..ac0ff776fb143 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1043,7 +1043,13 @@ fn prepare_uinodes( let range_end = ui_meta.indices.len() as u32; let existing_batch = &mut batches.last_mut().unwrap().1; existing_batch.range.end = range_end; - if let Some((range, texture)) = ui_meta.texture_changes.last_mut() { + if existing_batch.texture_changes.is_empty() { + if range_start < range_end { + ui_meta + .texture_changes + .push((range_start..range_end, image)); + } + } else if let Some((range, texture)) = ui_meta.texture_changes.last_mut() { if image == AssetId::default() || *texture == image { range.end = range_end; } else if range_start < range_end { @@ -1051,17 +1057,6 @@ fn prepare_uinodes( .texture_changes .push((range_start..range_end, image)); } - } else if range_start < range_end { - ui_meta - .texture_changes - .push((range_start..range_end, image)); - } - let texture_changes_start = existing_batch.texture_changes.start as usize; - if 0 < texture_changes_start - && existing_batch.range.start - < ui_meta.texture_changes[texture_changes_start - 1].0.end - { - existing_batch.texture_changes.start -= 1; } existing_batch.texture_changes.end = ui_meta.texture_changes.len() as u32; ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; @@ -1114,15 +1109,15 @@ fn prepare_uinodes( ui_meta .texture_changes .reserve(style.sections.len().saturating_mul(2).saturating_add(1)); - push_text_vertices(&mut ui_meta, layout, style, &gpu_images, image); + push_text_vertices( + &mut ui_meta, + layout, + style, + &gpu_images, + image, + existing_batch.texture_changes.start as usize, + ); existing_batch.range.end = ui_meta.indices.len() as u32; - let texture_changes_start = existing_batch.texture_changes.start as usize; - if 0 < texture_changes_start - && existing_batch.range.start - < ui_meta.texture_changes[texture_changes_start - 1].0.end - { - existing_batch.texture_changes.start -= 1; - } existing_batch.texture_changes.end = ui_meta.texture_changes.len() as u32; for (_, texture) in &ui_meta.texture_changes[existing_batch.texture_changes.start as usize @@ -1167,21 +1162,18 @@ fn prepare_uinodes( let range_end = ui_meta.indices.len() as u32; existing_batch.range.end = range_end; if range_start < range_end { - if let Some((range, _)) = ui_meta.texture_changes.last_mut() { - range.end = range_end; - } else { + if existing_batch.texture_changes.is_empty() { + let image = ui_meta + .texture_changes + .last() + .map_or(AssetId::default(), |(_, image)| *image); ui_meta .texture_changes - .push((range_start..range_end, AssetId::default())); + .push((range_start..range_end, image)); + } else if let Some((range, _)) = ui_meta.texture_changes.last_mut() { + range.end = range_end; } } - let texture_changes_start = existing_batch.texture_changes.start as usize; - if 0 < texture_changes_start - && existing_batch.range.start - < ui_meta.texture_changes[texture_changes_start - 1].0.end - { - existing_batch.texture_changes.start -= 1; - } existing_batch.texture_changes.end = ui_meta.texture_changes.len() as u32; ui_phase.items[batch_start_item_index].batch_range_mut().end += 1; } @@ -1465,6 +1457,7 @@ fn push_text_vertices( style: &ExtractedGlyphLayout, gpu_images: &RenderAssets, image: AssetId, + texture_changes_start: usize, ) { let uinode = &layout.uinode; let shadow_offset = style.shadow_offset / uinode.inverse_scale_factor(); @@ -1484,12 +1477,16 @@ fn push_text_vertices( } else { layout.clip }; - let (mut texture_range_start, mut current_texture) = ui_meta - .texture_changes - .pop() - .map_or((ui_meta.indices.len() as u32, image), |(range, texture)| { - (range.start, texture) - }); + let (mut texture_range_start, mut current_texture) = + if texture_changes_start < ui_meta.texture_changes.len() { + ui_meta + .texture_changes + .pop() + .map(|(range, texture)| (range.start, texture)) + .unwrap() + } else { + (ui_meta.indices.len() as u32, image) + }; { let mut push_quad = |rect: Rect, color: LinearRgba, diff --git a/crates/bevy_ui_render/src/render_pass.rs b/crates/bevy_ui_render/src/render_pass.rs index 18eb9a3cb3856..a4d82b28660b9 100644 --- a/crates/bevy_ui_render/src/render_pass.rs +++ b/crates/bevy_ui_render/src/render_pass.rs @@ -212,10 +212,8 @@ impl RenderCommand

for DrawUiNode { let image_bind_groups = image_bind_groups.into_inner(); for range_index in batch.texture_changes.clone() { let (texture_range, image) = &ui_meta.texture_changes[range_index as usize]; - let start = texture_range.start.max(batch.range.start); - let end = texture_range.end.min(batch.range.end); pass.set_bind_group(1, image_bind_groups.values.get(image).unwrap(), &[]); - pass.draw_indexed(start..end, 0, 0..1); + pass.draw_indexed(texture_range.clone(), 0, 0..1); } RenderCommandResult::Success } From 233aea18d36ce23b60ab4ae2090231ec7f38e693 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 19:50:46 +0100 Subject: [PATCH 78/99] simplify debug overlay rendering params --- crates/bevy_ui_render/src/debug_overlay.rs | 64 ++++++---------------- 1 file changed, 17 insertions(+), 47 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 4b0d59410a11b..54c094f9a112f 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -229,11 +229,8 @@ pub fn queue_debug_overlay( /// Clip each debug outline quad, compute its vertices, and push them onto the vertex buffer fn push_debug_outline( rect: Rect, - line_color: LinearRgba, - line_width: f32, border_radius: ResolvedBorderRadius, - clip: Option, - transform: Affine2, + (line_color, line_width, clip, transform): &(LinearRgba, f32, Option, Affine2), ui_meta: &mut UiMeta, ) { let size = rect.size(); @@ -287,7 +284,7 @@ fn push_debug_outline( color, flags: flags | shader_flags::CORNERS[i], radius: border_radius.into(), - border: [line_width; 4], + border: [*line_width; 4], size: size.into(), point: (QUAD_VERTEX_POSITIONS[i] * size + positions_diff[i]).into(), }); @@ -320,19 +317,20 @@ pub fn push_debug_overlay_vertices( continue; } - let clip = layout.clip.filter(|_| !debug_outline.show_clipped); - let line_color = debug_outline - .line_color_override - .unwrap_or_else(|| Hsla::sequential_dispersed(main_entity.index_u32()).into()); + let style = &( + debug_outline + .line_color_override + .unwrap_or_else(|| Hsla::sequential_dispersed(main_entity.index_u32()).into()), + debug_outline.line_width, + layout.clip.filter(|_| !debug_outline.show_clipped), + layout.transform, + ); if debug_outline.outline_border_box { push_debug_outline( layout.uinode.border_box(), - line_color, - debug_outline.line_width, layout.uinode.border_radius, - clip, - layout.transform, + style, ui_meta, ); } @@ -340,11 +338,8 @@ pub fn push_debug_overlay_vertices( if debug_outline.outline_padding_box { push_debug_outline( layout.uinode.padding_box(), - line_color, - debug_outline.line_width, layout.uinode.inner_radius(), - clip, - layout.transform, + style, ui_meta, ); } @@ -352,59 +347,34 @@ pub fn push_debug_overlay_vertices( if debug_outline.outline_content_box { push_debug_outline( layout.uinode.content_box(), - line_color, - debug_outline.line_width, ResolvedBorderRadius::ZERO, - clip, - layout.transform, + style, ui_meta, ); } if debug_outline.outline_scrollbars { if let Some((gutter, [thumb_min, thumb_max])) = layout.uinode.horizontal_scrollbar() { - push_debug_outline( - gutter, - line_color, - debug_outline.line_width, - ResolvedBorderRadius::ZERO, - clip, - layout.transform, - ui_meta, - ); + push_debug_outline(gutter, ResolvedBorderRadius::ZERO, style, ui_meta); push_debug_outline( Rect { min: Vec2::new(thumb_min, gutter.min.y), max: Vec2::new(thumb_max, gutter.max.y), }, - line_color, - debug_outline.line_width, ResolvedBorderRadius::ZERO, - clip, - layout.transform, + style, ui_meta, ); } if let Some((gutter, [thumb_min, thumb_max])) = layout.uinode.vertical_scrollbar() { - push_debug_outline( - gutter, - line_color, - debug_outline.line_width, - ResolvedBorderRadius::ZERO, - clip, - layout.transform, - ui_meta, - ); + push_debug_outline(gutter, ResolvedBorderRadius::ZERO, style, ui_meta); push_debug_outline( Rect { min: Vec2::new(gutter.min.x, thumb_min), max: Vec2::new(gutter.max.x, thumb_max), }, - line_color, - debug_outline.line_width, ResolvedBorderRadius::ZERO, - clip, - layout.transform, + style, ui_meta, ); } From 84a91c4dffde06b9b56b2d5e7f77e46d015589c0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 19:52:18 +0100 Subject: [PATCH 79/99] Formatting --- crates/bevy_ui_render/src/box_shadow.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index 5f10556dbbc50..e5fd4675a2bdb 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -332,7 +332,6 @@ pub fn queue_shadows( pipeline: *pipeline, entity: (*entity, *main_entity), sort_key: FloatOrd(layout.stack_index as f32 + stack_z_offsets::BOX_SHADOW), - batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, indexed: true, From 59a0d02b653522e6bb78917409df8e3c2829481d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 19:57:56 +0100 Subject: [PATCH 80/99] apply scale factor to debug overlay's line width --- crates/bevy_ui_render/src/debug_overlay.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 54c094f9a112f..5a56bdc877536 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -321,7 +321,7 @@ pub fn push_debug_overlay_vertices( debug_outline .line_color_override .unwrap_or_else(|| Hsla::sequential_dispersed(main_entity.index_u32()).into()), - debug_outline.line_width, + debug_outline.line_width / layout.uinode.inverse_scale_factor(), layout.clip.filter(|_| !debug_outline.show_clipped), layout.transform, ); From 615e234d861085827aadb121836295bb67fc40fb Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 20:01:25 +0100 Subject: [PATCH 81/99] Removed extract_uinode_geometry_debug --- crates/bevy_ui_render/src/extract_layout.rs | 38 --------------------- 1 file changed, 38 deletions(-) diff --git a/crates/bevy_ui_render/src/extract_layout.rs b/crates/bevy_ui_render/src/extract_layout.rs index 82eb7a6d18919..dc2fa5804be67 100644 --- a/crates/bevy_ui_render/src/extract_layout.rs +++ b/crates/bevy_ui_render/src/extract_layout.rs @@ -200,41 +200,3 @@ pub fn extract_ui_layout( } } } - -pub fn extract_uinode_geometry_debug( - extracted_geometries: bevy_ecs::system::Res, -) { - if 0 < extracted_geometries.changed.len() { - println!("\n"); - println!( - "camera count: {}", - extracted_geometries - .extracted_camera_to_main_uinode_map - .len() - ); - println!("node count: {}", extracted_geometries.layout.len()); - println!("removed count: {}", extracted_geometries.removed.len()); - println!("changed count: {}", extracted_geometries.changed.len()); - - for (e, g) in &extracted_geometries.layout { - if let Some(es) = extracted_geometries - .extracted_camera_to_main_uinode_map - .get(&g.extracted_camera) - { - if !es.contains(e) { - println!( - "UI node {e:?} has extracted geometry for camera {:?}, but is missing from that camera's UI node map", - g.extracted_camera - ); - } - } else { - println!( - "UI node {e:?} has extracted geometry for camera {:?}, but that camera is missing from the camera-to-UI-node map", - g.extracted_camera - ); - } - } - - println!("\n"); - } -} From 8e6f434fdb37e0578c441571827d2773085f15b1 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 20:07:55 +0100 Subject: [PATCH 82/99] Only extracted debug overlay once per camera view --- crates/bevy_ui_render/src/debug_overlay.rs | 9 +++++++-- crates/bevy_ui_render/src/lib.rs | 7 +++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui_render/src/debug_overlay.rs b/crates/bevy_ui_render/src/debug_overlay.rs index 5a56bdc877536..7c23122ea5113 100644 --- a/crates/bevy_ui_render/src/debug_overlay.rs +++ b/crates/bevy_ui_render/src/debug_overlay.rs @@ -31,7 +31,7 @@ use bevy_ui::{ResolvedBorderRadius, UiStack}; /// Configuration for the UI debug overlay /// /// Can be added as a `Component` to individual UI node entities. -/// This overwrites the default [`GlobalUiDebugOutline`] resource. +/// This overwrites the default [`UiDebugOverlay`] resource. #[derive(Component, Reflect, Clone)] #[reflect(Component)] pub struct UiDebugOutline { @@ -88,7 +88,7 @@ impl From for UiDebugOutline { /// Configuration for the UI debug overlay /// -/// A global `resource` that can be overridden by local component [`UiDebugOptions`] override on individual UI node entities +/// A global `resource` that can be overridden by local component [`UiDebugOutline`] override on individual UI node entities #[derive(Default, Resource, Reflect, Clone, Deref, DerefMut)] #[reflect(Resource)] pub struct UiDebugOverlay(pub UiDebugOutline); @@ -300,6 +300,7 @@ pub fn push_debug_overlay_vertices( ui_meta: &mut UiMeta, extracted_ui_layout: &ExtractedUiLayout, extracted_ui_debug_overlay: &ExtractedUiDebugOverlay, + extracted_camera: Entity, ) { if !extracted_ui_debug_overlay.default_outline.enabled && extracted_ui_debug_overlay.per_node_outline.is_empty() @@ -308,6 +309,10 @@ pub fn push_debug_overlay_vertices( } for (main_entity, layout) in extracted_ui_layout.layout.iter() { + if layout.extracted_camera != extracted_camera { + continue; + } + let debug_outline = extracted_ui_debug_overlay.get(main_entity); if !debug_outline.enabled { continue; diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index ac0ff776fb143..3aca4aae3b749 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1142,6 +1142,12 @@ fn prepare_uinodes( } #[cfg(feature = "bevy_ui_debug")] + if let Some(extracted_camera) = extracted_ui_debug_overlay + .extracted_camera_view_to_ids + .iter() + .find_map(|(extracted_camera, item_ids)| { + (*item_ids == (entity, main_entity)).then_some(*extracted_camera) + }) { if !batch_open { batch_start_item_index = phase_item_index; @@ -1156,6 +1162,7 @@ fn prepare_uinodes( &mut ui_meta, &extracted_layout, &extracted_ui_debug_overlay, + extracted_camera, ); let existing_batch = &mut batches.last_mut().unwrap().1; From 6bf515c9244f7a900c6e1f1ecce6290128bd64a1 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 20:12:35 +0100 Subject: [PATCH 83/99] lookup debug overlay ids before phase item loop --- crates/bevy_ui_render/src/lib.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 3aca4aae3b749..0cd2d09466cf9 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -996,7 +996,16 @@ fn prepare_uinodes( )); // for each sorted render phase corresponding to a UI view - for ui_phase in phases.values_mut() { + for (retained_view_entity, ui_phase) in phases.iter_mut() { + #[cfg(feature = "bevy_ui_debug")] + let extracted_debug_overlay = extracted_ui_debug_overlay + .extracted_camera_view_to_ids + .iter() + .find_map(|(extracted_camera, item_ids)| { + (item_ids.1 == retained_view_entity.main_entity) + .then_some((*extracted_camera, *item_ids)) + }); + // used to store the index of the first phase item in a batch let mut batch_start_item_index = 0; @@ -1142,12 +1151,8 @@ fn prepare_uinodes( } #[cfg(feature = "bevy_ui_debug")] - if let Some(extracted_camera) = extracted_ui_debug_overlay - .extracted_camera_view_to_ids - .iter() - .find_map(|(extracted_camera, item_ids)| { - (*item_ids == (entity, main_entity)).then_some(*extracted_camera) - }) + if let Some((extracted_camera, item_ids)) = extracted_debug_overlay + && item_ids == (entity, main_entity) { if !batch_open { batch_start_item_index = phase_item_index; From 2f2d7d3c2d0955cdfac082d56be214e66dbb9620 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 20:19:06 +0100 Subject: [PATCH 84/99] Order ExtractGradient between ExtractTextureSlice and ExtractText --- crates/bevy_ui_render/src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 0cd2d09466cf9..c19d1d0d515a0 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -126,9 +126,9 @@ pub enum RenderUiSystems { ExtractBoxShadows, ExtractBackgrounds, ExtractTextureSlice, + ExtractGradient, ExtractText, ExtractDebug, - ExtractGradient, } /// Marker for controlling whether UI is rendered with or without anti-aliasing @@ -237,6 +237,7 @@ impl Plugin for UiRenderPlugin { RenderUiSystems::ExtractBoxShadows, RenderUiSystems::ExtractBackgrounds, RenderUiSystems::ExtractTextureSlice, + RenderUiSystems::ExtractGradient, RenderUiSystems::ExtractText, RenderUiSystems::ExtractDebug, ) From 49a67d1d0b8ae03ab5d973b87684456fe0c1000b Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 20:39:01 +0100 Subject: [PATCH 85/99] clear stale gradients from the gradients index map --- crates/bevy_ui_render/src/gradient.rs | 32 +++++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 24a47fdf791ff..73b5f70768fa8 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -371,14 +371,21 @@ pub fn extract_gradients( .iter() .chain(gradients_query.iter_many(removed_gradients.iter().map(ContainsEntity::entity))) { + let main_entity = MainEntity::from(entity); + + if !extracted_gradients.changed_this_frame.insert(main_entity) { + continue; + } if node_gradient.is_none() && border_gradient.is_none() { + if let Some(mut gradients) = extracted_gradients.items.remove(&main_entity) { + for (render_entity, _) in gradients.drain(..) { + commands.entity(render_entity).despawn(); + } + } continue; } - let main_entity = MainEntity::from(entity); - extracted_gradients.changed_this_frame.insert(main_entity); - - let extracted_gradients = extracted_gradients.items.entry(main_entity).or_default(); + let gradients_map = extracted_gradients.items.entry(main_entity).or_default(); let mut gradient_count = 0; for (node_gradients, node_type) in [ @@ -386,7 +393,7 @@ pub fn extract_gradients( (border_gradient.map(|g| &g.0), NodeType::Border(BORDER_ALL)), ] .iter() - .filter_map(|(g, n)| g.map(|g| (g, *n))) + .filter_map(|(gradients, node_type)| gradients.map(|gradients| (gradients, *node_type))) { for gradient in node_gradients.iter() { if gradient.is_empty() { @@ -398,14 +405,25 @@ pub fn extract_gradients( node_type, target: *target, }; - if let Some((_, gradient)) = extracted_gradients.get_index_mut(gradient_count) { + + if let Some((_, gradient)) = gradients_map.get_index_mut(gradient_count) { *gradient = extracted_gradient; } else { - extracted_gradients.insert(commands.spawn_empty().id(), extracted_gradient); + gradients_map.insert(commands.spawn_empty().id(), extracted_gradient); } + gradient_count += 1; } } + + // Despawn the retained entries that no longer correspond to a gradient + for (render_entity, _) in gradients_map.drain(gradient_count..) { + commands.entity(render_entity).despawn(); + } + + if gradients_map.is_empty() { + extracted_gradients.items.remove(&main_entity); + } } for main_entity in removed_gradients.drain() { From 2294bbd0c9af048e03baebb0857c3fbcd6640c68 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 20:41:38 +0100 Subject: [PATCH 86/99] renamed variables for consistancy --- crates/bevy_ui_render/src/extract_layout.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui_render/src/extract_layout.rs b/crates/bevy_ui_render/src/extract_layout.rs index dc2fa5804be67..3c51f9f4deba9 100644 --- a/crates/bevy_ui_render/src/extract_layout.rs +++ b/crates/bevy_ui_render/src/extract_layout.rs @@ -142,11 +142,11 @@ pub fn extract_ui_layout( let ExtractedUiLayout { extracted_camera_to_main_uinode_map, - layout: uinode_geometries, + layout: uinode_layouts, .. } = &mut *extracted_ui_layout; - match uinode_geometries.entry(main_entity) { + match uinode_layouts.entry(main_entity) { bevy_platform::collections::hash_map::Entry::Occupied(mut entry) => { let old_extracted_camera = entry.get().extracted_camera; if old_extracted_camera != extracted_camera_entity { From 0171dc138bd4f1fd914b4480d033719315936188 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 20:46:52 +0100 Subject: [PATCH 87/99] use a Or<(With, With)> query filter in extract_text, EditableText doesn't use Text --- crates/bevy_ui_render/src/text.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index cbf876be1f742..ce3d2f08d35f2 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -67,7 +67,7 @@ pub fn extract_text( Query< Entity, ( - With, + Or<(With, With)>, Or<( Or<( Changed, From f4ce6c5ee6282acbaf037e8251586b95f5e3c97e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 21:11:47 +0100 Subject: [PATCH 88/99] Huge combined batch in many_buttons doing too many image lookups during glyph layout, only do it for the ones in the current glyph layout, not the whole batch --- crates/bevy_ui_render/src/lib.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index c19d1d0d515a0..6a46f40540298 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1116,9 +1116,9 @@ fn prepare_uinodes( }); let existing_batch = &mut batches.last_mut().unwrap().1; - ui_meta - .texture_changes - .reserve(style.sections.len().saturating_mul(2).saturating_add(1)); + + let new_texture_changes_start = ui_meta.texture_changes.len(); + push_text_vertices( &mut ui_meta, layout, @@ -1127,15 +1127,13 @@ fn prepare_uinodes( image, existing_batch.texture_changes.start as usize, ); + existing_batch.range.end = ui_meta.indices.len() as u32; existing_batch.texture_changes.end = ui_meta.texture_changes.len() as u32; - for (_, texture) in &ui_meta.texture_changes[existing_batch.texture_changes.start - as usize - ..existing_batch.texture_changes.end as usize] - { + for (_, texture) in &ui_meta.texture_changes[new_texture_changes_start..] { let gpu_image = gpu_images .get(*texture) - .expect("Image was checked during quad generation and should still exist"); + .expect("Image was checked in push_text_vertices"); image_bind_groups.values.entry(*texture).or_insert_with(|| { render_device.create_bind_group( "ui_material_bind_group", From 75b4ead071251ec2c514867a9de88e8fa4d70d71 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 21:28:14 +0100 Subject: [PATCH 89/99] moved push_text_vertices into text module --- crates/bevy_ui_render/src/lib.rs | 215 +--------------------------- crates/bevy_ui_render/src/text.rs | 223 +++++++++++++++++++++++++++++- 2 files changed, 220 insertions(+), 218 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 6a46f40540298..43cfe7fef5618 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -81,7 +81,7 @@ use ui_texture_slice_pipeline::UiTextureSlicerPlugin; use crate::extract_layout::{ExtractedUiLayout, ExtractedUiNodeLayout}; use crate::shader_flags::INVERT; -use crate::text::{extract_text, queue_text, ExtractedGlyphLayout, ExtractedGlyphLayouts}; +use crate::text::{extract_text, push_text_vertices, queue_text, ExtractedGlyphLayouts}; pub mod prelude { #[cfg(feature = "bevy_ui_debug")] @@ -1460,216 +1460,3 @@ fn push_uinode_vertices( } } } - -/// generates vertices from extracted uinode syle and glyph layout -fn push_text_vertices( - ui_meta: &mut UiMeta, - layout: &ExtractedUiNodeLayout, - style: &ExtractedGlyphLayout, - gpu_images: &RenderAssets, - image: AssetId, - texture_changes_start: usize, -) { - let uinode = &layout.uinode; - let shadow_offset = style.shadow_offset / uinode.inverse_scale_factor(); - let transform = layout.transform - * Affine2::from_translation(uinode.content_box().min - style.viewport_offset); - let clip = if style.clip_to_content_box { - let content_box = uinode.content_box(); - let text_clip = Rect::from_center_size( - layout.transform.translation + content_box.center(), - content_box.size(), - ); - Some( - layout - .clip - .map_or(text_clip, |clip| clip.intersect(text_clip)), - ) - } else { - layout.clip - }; - let (mut texture_range_start, mut current_texture) = - if texture_changes_start < ui_meta.texture_changes.len() { - ui_meta - .texture_changes - .pop() - .map(|(range, texture)| (range.start, texture)) - .unwrap() - } else { - (ui_meta.indices.len() as u32, image) - }; - { - let mut push_quad = |rect: Rect, - color: LinearRgba, - border_radius: ResolvedBorderRadius, - glyph: Option<(Rect, AssetId)>| { - let rect_size = rect.size(); - let rect_transform = transform * Affine2::from_translation(rect.center()); - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| rect_transform.transform_point2(pos * rect_size).extend(0.)); - let positions_diff = if let Some(clip) = clip { - [ - Vec2::new( - f32::max(clip.min.x - positions[0].x, 0.), - f32::max(clip.min.y - positions[0].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[1].x, 0.), - f32::max(clip.min.y - positions[1].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[2].x, 0.), - f32::min(clip.max.y - positions[2].y, 0.), - ), - Vec2::new( - f32::max(clip.min.x - positions[3].x, 0.), - f32::min(clip.max.y - positions[3].y, 0.), - ), - ] - } else { - [Vec2::ZERO; 4] - }; - let positions_clipped = [ - positions[0] + positions_diff[0].extend(0.), - positions[1] + positions_diff[1].extend(0.), - positions[2] + positions_diff[2].extend(0.), - positions[3] + positions_diff[3].extend(0.), - ]; - let transformed_rect_size = rect_transform.transform_vector2(rect_size).abs(); - if rect_transform.x_axis[1] == 0.0 - && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x - || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) - { - return; - } - - let (uvs, flags) = if let Some((glyph_rect, atlas_texture)) = glyph { - let Some(gpu_image) = gpu_images.get(atlas_texture) else { - return; - }; - let range_end = ui_meta.indices.len() as u32; - if current_texture != atlas_texture { - if texture_range_start < range_end { - ui_meta - .texture_changes - .push((texture_range_start..range_end, current_texture)); - } - texture_range_start = range_end; - current_texture = atlas_texture; - } - let atlas_extent = gpu_image.size_2d().as_vec2(); - ( - [ - Vec2::new( - glyph_rect.min.x + positions_diff[0].x, - glyph_rect.min.y + positions_diff[0].y, - ), - Vec2::new( - glyph_rect.max.x + positions_diff[1].x, - glyph_rect.min.y + positions_diff[1].y, - ), - Vec2::new( - glyph_rect.max.x + positions_diff[2].x, - glyph_rect.max.y + positions_diff[2].y, - ), - Vec2::new( - glyph_rect.min.x + positions_diff[3].x, - glyph_rect.max.y + positions_diff[3].y, - ), - ] - .map(|pos| pos / atlas_extent), - shader_flags::TEXTURED, - ) - } else { - ( - [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y], - shader_flags::UNTEXTURED, - ) - }; - - let vertex_start = ui_meta.vertices.len() as u32; - let color = color.to_f32_array(); - for i in 0..4 { - ui_meta.vertices.push(UiVertex { - position: positions_clipped[i].into(), - uv: uvs[i].into(), - color, - flags: flags | shader_flags::CORNERS[i], - radius: border_radius.into(), - border: [0.; 4], - size: rect_size.into(), - point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), - }); - } - for &index in &QUAD_INDICES { - ui_meta.indices.push(vertex_start + index as u32); - } - }; - - let selection_color = if style.focused { - style.cursor_style.selection_color - } else { - style.cursor_style.unfocused_selection_color - } - .to_linear(); - for (rect, radius) in &style.selections { - push_quad(*rect, selection_color, *radius, None); - } - for (rect, color) in &style.backgrounds { - push_quad(*rect, *color, ResolvedBorderRadius::ZERO, None); - } - - if !style.shadow_color.is_fully_transparent() { - for section in &style.sections { - for glyph in &style.glyphs[section.range.start as usize..section.range.end as usize] - { - push_quad( - Rect::from_center_size( - glyph.translation + shadow_offset, - glyph.rect.size(), - ), - style.shadow_color, - ResolvedBorderRadius::ZERO, - Some((glyph.rect, section.atlas_texture)), - ); - } - } - for (rect, _) in style.strike_through.iter().chain(style.underline.iter()) { - push_quad( - Rect::from_center_size(rect.center() + shadow_offset, rect.size()), - style.shadow_color, - ResolvedBorderRadius::ZERO, - None, - ); - } - } - - for section in &style.sections { - for glyph in &style.glyphs[section.range.start as usize..section.range.end as usize] { - push_quad( - Rect::from_center_size(glyph.translation, glyph.rect.size()), - glyph.color, - ResolvedBorderRadius::ZERO, - Some((glyph.rect, section.atlas_texture)), - ); - } - } - for (rect, color) in style.strike_through.iter().chain(style.underline.iter()) { - push_quad(*rect, *color, ResolvedBorderRadius::ZERO, None); - } - if let Some(cursor) = style.cursor { - push_quad( - cursor, - style.cursor_style.color.to_linear(), - ResolvedBorderRadius::ZERO, - None, - ); - } - } - let range_end = ui_meta.indices.len() as u32; - if texture_range_start < range_end { - ui_meta - .texture_changes - .push((texture_range_start..range_end, current_texture)); - } -} diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index ce3d2f08d35f2..50ad68958acd0 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -1,18 +1,21 @@ use crate::{ - extract_layout::ExtractedUiLayout, stack_z_offsets, DrawUi, TransparentUi, UiAntiAlias, - UiCameraView, UiPipeline, UiPipelineKey, + extract_layout::{ExtractedUiLayout, ExtractedUiNodeLayout}, + shader_flags, stack_z_offsets, DrawUi, TransparentUi, UiAntiAlias, UiCameraView, UiMeta, + UiPipeline, UiPipelineKey, UiVertex, QUAD_INDICES, QUAD_VERTEX_POSITIONS, }; use bevy_asset::AssetId; -use bevy_color::{Alpha, LinearRgba}; +use bevy_color::{Alpha, ColorToComponents, LinearRgba}; use bevy_ecs::prelude::*; use bevy_image::Image; use bevy_input_focus::InputFocus; -use bevy_math::{FloatOrd, Rect, Vec2}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_platform::collections::hash_map::Entry; use bevy_render::{ + render_asset::RenderAssets, render_phase::{DrawFunctions, PhaseItemExtraIndex, ViewSortedRenderPhases}, render_resource::{PipelineCache, SpecializedRenderPipelines}, sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet}, + texture::GpuImage, view::ExtractedView, Extract, }; @@ -528,3 +531,215 @@ pub fn queue_text( }); } } + +pub(crate) fn push_text_vertices( + ui_meta: &mut UiMeta, + layout: &ExtractedUiNodeLayout, + style: &ExtractedGlyphLayout, + gpu_images: &RenderAssets, + image: AssetId, + texture_changes_start: usize, +) { + let uinode = &layout.uinode; + let shadow_offset = style.shadow_offset / uinode.inverse_scale_factor(); + let transform = layout.transform + * Affine2::from_translation(uinode.content_box().min - style.viewport_offset); + let clip = if style.clip_to_content_box { + let content_box = uinode.content_box(); + let text_clip = Rect::from_center_size( + layout.transform.translation + content_box.center(), + content_box.size(), + ); + Some( + layout + .clip + .map_or(text_clip, |clip| clip.intersect(text_clip)), + ) + } else { + layout.clip + }; + let (mut texture_range_start, mut current_texture) = + if texture_changes_start < ui_meta.texture_changes.len() { + ui_meta + .texture_changes + .pop() + .map(|(range, texture)| (range.start, texture)) + .unwrap() + } else { + (ui_meta.indices.len() as u32, image) + }; + { + let mut push_quad = |rect: Rect, + color: LinearRgba, + border_radius: ResolvedBorderRadius, + glyph: Option<(Rect, AssetId)>| { + let rect_size = rect.size(); + let rect_transform = transform * Affine2::from_translation(rect.center()); + let positions = QUAD_VERTEX_POSITIONS + .map(|pos| rect_transform.transform_point2(pos * rect_size).extend(0.)); + let positions_diff = if let Some(clip) = clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = rect_transform.transform_vector2(rect_size).abs(); + if rect_transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + return; + } + + let (uvs, flags) = if let Some((glyph_rect, atlas_texture)) = glyph { + let Some(gpu_image) = gpu_images.get(atlas_texture) else { + return; + }; + let range_end = ui_meta.indices.len() as u32; + if current_texture != atlas_texture { + if texture_range_start < range_end { + ui_meta + .texture_changes + .push((texture_range_start..range_end, current_texture)); + } + texture_range_start = range_end; + current_texture = atlas_texture; + } + let atlas_extent = gpu_image.size_2d().as_vec2(); + ( + [ + Vec2::new( + glyph_rect.min.x + positions_diff[0].x, + glyph_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + glyph_rect.max.x + positions_diff[1].x, + glyph_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + glyph_rect.max.x + positions_diff[2].x, + glyph_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + glyph_rect.min.x + positions_diff[3].x, + glyph_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent), + shader_flags::TEXTURED, + ) + } else { + ( + [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y], + shader_flags::UNTEXTURED, + ) + }; + + let vertex_start = ui_meta.vertices.len() as u32; + let color = color.to_f32_array(); + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + flags: flags | shader_flags::CORNERS[i], + radius: border_radius.into(), + border: [0.; 4], + size: rect_size.into(), + point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), + }); + } + for &index in &QUAD_INDICES { + ui_meta.indices.push(vertex_start + index as u32); + } + }; + + let selection_color = if style.focused { + style.cursor_style.selection_color + } else { + style.cursor_style.unfocused_selection_color + } + .to_linear(); + for (rect, radius) in &style.selections { + push_quad(*rect, selection_color, *radius, None); + } + for (rect, color) in &style.backgrounds { + push_quad(*rect, *color, ResolvedBorderRadius::ZERO, None); + } + + if !style.shadow_color.is_fully_transparent() { + for section in &style.sections { + for glyph in &style.glyphs[section.range.start as usize..section.range.end as usize] + { + push_quad( + Rect::from_center_size( + glyph.translation + shadow_offset, + glyph.rect.size(), + ), + style.shadow_color, + ResolvedBorderRadius::ZERO, + Some((glyph.rect, section.atlas_texture)), + ); + } + } + for (rect, _) in style.strike_through.iter().chain(style.underline.iter()) { + push_quad( + Rect::from_center_size(rect.center() + shadow_offset, rect.size()), + style.shadow_color, + ResolvedBorderRadius::ZERO, + None, + ); + } + } + + for section in &style.sections { + for glyph in &style.glyphs[section.range.start as usize..section.range.end as usize] { + push_quad( + Rect::from_center_size(glyph.translation, glyph.rect.size()), + glyph.color, + ResolvedBorderRadius::ZERO, + Some((glyph.rect, section.atlas_texture)), + ); + } + } + for (rect, color) in style.strike_through.iter().chain(style.underline.iter()) { + push_quad(*rect, *color, ResolvedBorderRadius::ZERO, None); + } + if let Some(cursor) = style.cursor { + push_quad( + cursor, + style.cursor_style.color.to_linear(), + ResolvedBorderRadius::ZERO, + None, + ); + } + } + let range_end = ui_meta.indices.len() as u32; + if texture_range_start < range_end { + ui_meta + .texture_changes + .push((texture_range_start..range_end, current_texture)); + } +} From 64dbf4464cb8d77c00195cd6b656313b69f88496 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 21:42:57 +0100 Subject: [PATCH 90/99] look up image only if needed --- crates/bevy_ui_render/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 43cfe7fef5618..aad24aa893c1d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -1131,10 +1131,10 @@ fn prepare_uinodes( existing_batch.range.end = ui_meta.indices.len() as u32; existing_batch.texture_changes.end = ui_meta.texture_changes.len() as u32; for (_, texture) in &ui_meta.texture_changes[new_texture_changes_start..] { - let gpu_image = gpu_images - .get(*texture) - .expect("Image was checked in push_text_vertices"); image_bind_groups.values.entry(*texture).or_insert_with(|| { + let gpu_image = gpu_images + .get(*texture) + .expect("Image was checked in push_text_vertices"); render_device.create_bind_group( "ui_material_bind_group", &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), From 562924012430587c184a06143464eff340d784e7 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 21:45:01 +0100 Subject: [PATCH 91/99] split up push_text_vertices --- crates/bevy_ui_render/src/text.rs | 445 +++++++++++++++++++----------- 1 file changed, 277 insertions(+), 168 deletions(-) diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index 50ad68958acd0..08d57bbdbbad5 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -534,29 +534,30 @@ pub fn queue_text( pub(crate) fn push_text_vertices( ui_meta: &mut UiMeta, - layout: &ExtractedUiNodeLayout, - style: &ExtractedGlyphLayout, + node_layout: &ExtractedUiNodeLayout, + glyph_layout: &ExtractedGlyphLayout, gpu_images: &RenderAssets, - image: AssetId, + initial_texture: AssetId, texture_changes_start: usize, ) { - let uinode = &layout.uinode; - let shadow_offset = style.shadow_offset / uinode.inverse_scale_factor(); - let transform = layout.transform - * Affine2::from_translation(uinode.content_box().min - style.viewport_offset); - let clip = if style.clip_to_content_box { - let content_box = uinode.content_box(); + let shadow_offset = glyph_layout.shadow_offset / node_layout.uinode.inverse_scale_factor(); + let transform = node_layout.transform + * Affine2::from_translation( + node_layout.uinode.content_box().min - glyph_layout.viewport_offset, + ); + let clip = if glyph_layout.clip_to_content_box { + let content_box = node_layout.uinode.content_box(); let text_clip = Rect::from_center_size( - layout.transform.translation + content_box.center(), + node_layout.transform.translation + content_box.center(), content_box.size(), ); Some( - layout + node_layout .clip .map_or(text_clip, |clip| clip.intersect(text_clip)), ) } else { - layout.clip + node_layout.clip }; let (mut texture_range_start, mut current_texture) = if texture_changes_start < ui_meta.texture_changes.len() { @@ -566,176 +567,128 @@ pub(crate) fn push_text_vertices( .map(|(range, texture)| (range.start, texture)) .unwrap() } else { - (ui_meta.indices.len() as u32, image) - }; - { - let mut push_quad = |rect: Rect, - color: LinearRgba, - border_radius: ResolvedBorderRadius, - glyph: Option<(Rect, AssetId)>| { - let rect_size = rect.size(); - let rect_transform = transform * Affine2::from_translation(rect.center()); - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| rect_transform.transform_point2(pos * rect_size).extend(0.)); - let positions_diff = if let Some(clip) = clip { - [ - Vec2::new( - f32::max(clip.min.x - positions[0].x, 0.), - f32::max(clip.min.y - positions[0].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[1].x, 0.), - f32::max(clip.min.y - positions[1].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[2].x, 0.), - f32::min(clip.max.y - positions[2].y, 0.), - ), - Vec2::new( - f32::max(clip.min.x - positions[3].x, 0.), - f32::min(clip.max.y - positions[3].y, 0.), - ), - ] - } else { - [Vec2::ZERO; 4] - }; - let positions_clipped = [ - positions[0] + positions_diff[0].extend(0.), - positions[1] + positions_diff[1].extend(0.), - positions[2] + positions_diff[2].extend(0.), - positions[3] + positions_diff[3].extend(0.), - ]; - let transformed_rect_size = rect_transform.transform_vector2(rect_size).abs(); - if rect_transform.x_axis[1] == 0.0 - && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x - || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) - { - return; - } - - let (uvs, flags) = if let Some((glyph_rect, atlas_texture)) = glyph { - let Some(gpu_image) = gpu_images.get(atlas_texture) else { - return; - }; - let range_end = ui_meta.indices.len() as u32; - if current_texture != atlas_texture { - if texture_range_start < range_end { - ui_meta - .texture_changes - .push((texture_range_start..range_end, current_texture)); - } - texture_range_start = range_end; - current_texture = atlas_texture; - } - let atlas_extent = gpu_image.size_2d().as_vec2(); - ( - [ - Vec2::new( - glyph_rect.min.x + positions_diff[0].x, - glyph_rect.min.y + positions_diff[0].y, - ), - Vec2::new( - glyph_rect.max.x + positions_diff[1].x, - glyph_rect.min.y + positions_diff[1].y, - ), - Vec2::new( - glyph_rect.max.x + positions_diff[2].x, - glyph_rect.max.y + positions_diff[2].y, - ), - Vec2::new( - glyph_rect.min.x + positions_diff[3].x, - glyph_rect.max.y + positions_diff[3].y, - ), - ] - .map(|pos| pos / atlas_extent), - shader_flags::TEXTURED, - ) - } else { - ( - [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y], - shader_flags::UNTEXTURED, - ) - }; - - let vertex_start = ui_meta.vertices.len() as u32; - let color = color.to_f32_array(); - for i in 0..4 { - ui_meta.vertices.push(UiVertex { - position: positions_clipped[i].into(), - uv: uvs[i].into(), - color, - flags: flags | shader_flags::CORNERS[i], - radius: border_radius.into(), - border: [0.; 4], - size: rect_size.into(), - point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), - }); - } - for &index in &QUAD_INDICES { - ui_meta.indices.push(vertex_start + index as u32); - } + (ui_meta.indices.len() as u32, initial_texture) }; - let selection_color = if style.focused { - style.cursor_style.selection_color - } else { - style.cursor_style.unfocused_selection_color - } - .to_linear(); - for (rect, radius) in &style.selections { - push_quad(*rect, selection_color, *radius, None); - } - for (rect, color) in &style.backgrounds { - push_quad(*rect, *color, ResolvedBorderRadius::ZERO, None); - } + let selection_color = if glyph_layout.focused { + glyph_layout.cursor_style.selection_color + } else { + glyph_layout.cursor_style.unfocused_selection_color + } + .to_linear(); + for (rect, radius) in &glyph_layout.selections { + push_untextured_vertices(ui_meta, transform, clip, *rect, selection_color, *radius); + } + for (rect, color) in &glyph_layout.backgrounds { + push_untextured_vertices( + ui_meta, + transform, + clip, + *rect, + *color, + ResolvedBorderRadius::ZERO, + ); + } - if !style.shadow_color.is_fully_transparent() { - for section in &style.sections { - for glyph in &style.glyphs[section.range.start as usize..section.range.end as usize] - { - push_quad( - Rect::from_center_size( - glyph.translation + shadow_offset, - glyph.rect.size(), - ), - style.shadow_color, - ResolvedBorderRadius::ZERO, - Some((glyph.rect, section.atlas_texture)), - ); + if !glyph_layout.shadow_color.is_fully_transparent() { + for section in &glyph_layout.sections { + let Some(gpu_image) = gpu_images.get(section.atlas_texture) else { + continue; + }; + let range_end = ui_meta.indices.len() as u32; + if current_texture != section.atlas_texture { + if texture_range_start < range_end { + ui_meta + .texture_changes + .push((texture_range_start..range_end, current_texture)); } + texture_range_start = range_end; + current_texture = section.atlas_texture; } - for (rect, _) in style.strike_through.iter().chain(style.underline.iter()) { - push_quad( - Rect::from_center_size(rect.center() + shadow_offset, rect.size()), - style.shadow_color, - ResolvedBorderRadius::ZERO, - None, + let atlas_extent = gpu_image.size_2d().as_vec2(); + for glyph in + &glyph_layout.glyphs[section.range.start as usize..section.range.end as usize] + { + push_glyph_vertices( + ui_meta, + transform, + clip, + Rect::from_center_size(glyph.translation + shadow_offset, glyph.rect.size()), + glyph_layout.shadow_color, + glyph.rect, + atlas_extent, ); } } + for (rect, _) in glyph_layout + .strike_through + .iter() + .chain(glyph_layout.underline.iter()) + { + push_untextured_vertices( + ui_meta, + transform, + clip, + Rect::from_center_size(rect.center() + shadow_offset, rect.size()), + glyph_layout.shadow_color, + ResolvedBorderRadius::ZERO, + ); + } + } - for section in &style.sections { - for glyph in &style.glyphs[section.range.start as usize..section.range.end as usize] { - push_quad( - Rect::from_center_size(glyph.translation, glyph.rect.size()), - glyph.color, - ResolvedBorderRadius::ZERO, - Some((glyph.rect, section.atlas_texture)), - ); + for section in &glyph_layout.sections { + let Some(gpu_image) = gpu_images.get(section.atlas_texture) else { + continue; + }; + let range_end = ui_meta.indices.len() as u32; + if current_texture != section.atlas_texture { + if texture_range_start < range_end { + ui_meta + .texture_changes + .push((texture_range_start..range_end, current_texture)); } + texture_range_start = range_end; + current_texture = section.atlas_texture; } - for (rect, color) in style.strike_through.iter().chain(style.underline.iter()) { - push_quad(*rect, *color, ResolvedBorderRadius::ZERO, None); - } - if let Some(cursor) = style.cursor { - push_quad( - cursor, - style.cursor_style.color.to_linear(), - ResolvedBorderRadius::ZERO, - None, + let atlas_extent = gpu_image.size_2d().as_vec2(); + for glyph in &glyph_layout.glyphs[section.range.start as usize..section.range.end as usize] + { + push_glyph_vertices( + ui_meta, + transform, + clip, + Rect::from_center_size(glyph.translation, glyph.rect.size()), + glyph.color, + glyph.rect, + atlas_extent, ); } } + for (rect, color) in glyph_layout + .strike_through + .iter() + .chain(glyph_layout.underline.iter()) + { + push_untextured_vertices( + ui_meta, + transform, + clip, + *rect, + *color, + ResolvedBorderRadius::ZERO, + ); + } + if let Some(cursor) = glyph_layout.cursor { + push_untextured_vertices( + ui_meta, + transform, + clip, + cursor, + glyph_layout.cursor_style.color.to_linear(), + ResolvedBorderRadius::ZERO, + ); + } + let range_end = ui_meta.indices.len() as u32; if texture_range_start < range_end { ui_meta @@ -743,3 +696,159 @@ pub(crate) fn push_text_vertices( .push((texture_range_start..range_end, current_texture)); } } + +fn push_untextured_vertices( + ui_meta: &mut UiMeta, + transform: Affine2, + clip: Option, + rect: Rect, + color: LinearRgba, + border_radius: ResolvedBorderRadius, +) { + let rect_size = rect.size(); + let rect_transform = transform * Affine2::from_translation(rect.center()); + let positions = QUAD_VERTEX_POSITIONS + .map(|pos| rect_transform.transform_point2(pos * rect_size).extend(0.)); + let positions_diff = if let Some(clip) = clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = rect_transform.transform_vector2(rect_size).abs(); + if rect_transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + return; + } + + let vertex_start = ui_meta.vertices.len() as u32; + let color = color.to_f32_array(); + let uvs = [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y]; + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + flags: shader_flags::UNTEXTURED | shader_flags::CORNERS[i], + radius: border_radius.into(), + border: [0.; 4], + size: rect_size.into(), + point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), + }); + } + for &index in &QUAD_INDICES { + ui_meta.indices.push(vertex_start + index as u32); + } +} + +fn push_glyph_vertices( + ui_meta: &mut UiMeta, + transform: Affine2, + clip: Option, + rect: Rect, + color: LinearRgba, + glyph_rect: Rect, + atlas_extent: Vec2, +) { + let rect_size = rect.size(); + let rect_transform = transform * Affine2::from_translation(rect.center()); + let positions = QUAD_VERTEX_POSITIONS + .map(|pos| rect_transform.transform_point2(pos * rect_size).extend(0.)); + let positions_diff = if let Some(clip) = clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + let transformed_rect_size = rect_transform.transform_vector2(rect_size).abs(); + if rect_transform.x_axis[1] == 0.0 + && (positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y) + { + return; + } + + let uvs = [ + Vec2::new( + glyph_rect.min.x + positions_diff[0].x, + glyph_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + glyph_rect.max.x + positions_diff[1].x, + glyph_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + glyph_rect.max.x + positions_diff[2].x, + glyph_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + glyph_rect.min.x + positions_diff[3].x, + glyph_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent); + + let vertex_start = ui_meta.vertices.len() as u32; + let color = color.to_f32_array(); + for i in 0..4 { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + flags: shader_flags::TEXTURED | shader_flags::CORNERS[i], + radius: ResolvedBorderRadius::ZERO.into(), + border: [0.; 4], + size: rect_size.into(), + point: (QUAD_VERTEX_POSITIONS[i] * rect_size + positions_diff[i]).into(), + }); + } + for &index in &QUAD_INDICES { + ui_meta.indices.push(vertex_start + index as u32); + } +} From 6fd9db8196c2dc524c3f969306ea883bd172c5ec Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 22:04:55 +0100 Subject: [PATCH 92/99] cache pipeline specialization and camera lookup in queue_text --- crates/bevy_ui_render/src/lib.rs | 8 ++--- crates/bevy_ui_render/src/text.rs | 52 ++++++++++++++++++++----------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index aad24aa893c1d..7450f26846515 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -879,10 +879,8 @@ pub fn queue_uinodes( continue; } - let extracted_camera_entity = layout.extracted_camera; - - if current_camera_entity != extracted_camera_entity { - current_phase = render_views.get(extracted_camera_entity).ok().and_then( + if current_camera_entity != layout.extracted_camera { + current_phase = render_views.get(layout.extracted_camera).ok().and_then( |(default_camera_view, ui_anti_alias)| { camera_views .get(default_camera_view.0) @@ -907,7 +905,7 @@ pub fn queue_uinodes( }) }, ); - current_camera_entity = extracted_camera_entity; + current_camera_entity = layout.extracted_camera; } let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { diff --git a/crates/bevy_ui_render/src/text.rs b/crates/bevy_ui_render/src/text.rs index 08d57bbdbbad5..25dad40626f7d 100644 --- a/crates/bevy_ui_render/src/text.rs +++ b/crates/bevy_ui_render/src/text.rs @@ -489,6 +489,8 @@ pub fn queue_text( draw_functions: Res>, ) { let draw_function = draw_functions.read().id::(); + let mut current_camera_entity = Entity::PLACEHOLDER; + let mut current_phase = None; for (main_entity, (render_entity, _)) in extracted_glyph_layouts.uinodes.iter() { let Some(layout) = extracted_layout.layout.get(main_entity) else { @@ -499,30 +501,42 @@ pub fn queue_text( continue; } - let Some((default_camera_view, ui_anti_alias)) = - render_views.get(layout.extracted_camera).ok() - else { - continue; - }; - let Some(view) = camera_views.get(default_camera_view.0).ok() else { - continue; - }; - let Some(transparent_phase) = transparent_render_phases.get_mut(&view.retained_view_entity) - else { + if current_camera_entity != layout.extracted_camera { + current_phase = render_views.get(layout.extracted_camera).ok().and_then( + |(default_camera_view, ui_anti_alias)| { + camera_views + .get(default_camera_view.0) + .ok() + .and_then(|view| { + transparent_render_phases + .get_mut(&view.retained_view_entity) + .map(|transparent_phase| { + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_pipeline, + UiPipelineKey { + target_format: view.target_format, + anti_alias: matches!( + ui_anti_alias, + None | Some(UiAntiAlias::On) + ), + }, + ); + (pipeline, transparent_phase) + }) + }) + }, + ); + current_camera_entity = layout.extracted_camera; + } + + let Some((pipeline, 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)), - }, - ); transparent_phase.add_transient(TransparentUi { draw_function, - pipeline, + pipeline: *pipeline, entity: (*render_entity, *main_entity), sort_key: FloatOrd(layout.stack_index as f32 + stack_z_offsets::TEXT), batch_range: 0..0, From 0ba8a50816d6d4c2ef805b1af0eed907706e4614 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 22:15:48 +0100 Subject: [PATCH 93/99] Removed redundant UiBatch::range --- crates/bevy_ui_render/src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 7450f26846515..e778ff014a291 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -821,14 +821,12 @@ pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; #[derive(Component, Debug)] pub struct UiBatch { - pub range: Range, pub texture_changes: Range, } impl UiBatch { fn empty(ui_meta: &UiMeta) -> Self { UiBatch { - range: ui_meta.indices.len() as u32..ui_meta.indices.len() as u32, texture_changes: ui_meta.texture_changes.len() as u32 ..ui_meta.texture_changes.len() as u32, } @@ -1050,7 +1048,6 @@ fn prepare_uinodes( push_uinode_vertices(&mut ui_meta, layout, style, image, gpu_image); let range_end = ui_meta.indices.len() as u32; let existing_batch = &mut batches.last_mut().unwrap().1; - existing_batch.range.end = range_end; if existing_batch.texture_changes.is_empty() { if range_start < range_end { ui_meta @@ -1126,7 +1123,6 @@ fn prepare_uinodes( existing_batch.texture_changes.start as usize, ); - existing_batch.range.end = ui_meta.indices.len() as u32; existing_batch.texture_changes.end = ui_meta.texture_changes.len() as u32; for (_, texture) in &ui_meta.texture_changes[new_texture_changes_start..] { image_bind_groups.values.entry(*texture).or_insert_with(|| { @@ -1169,7 +1165,6 @@ fn prepare_uinodes( let existing_batch = &mut batches.last_mut().unwrap().1; let range_end = ui_meta.indices.len() as u32; - existing_batch.range.end = range_end; if range_start < range_end { if existing_batch.texture_changes.is_empty() { let image = ui_meta From e35560d1364c023fdb9e3453819897d6c5ea97ad Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 22:20:28 +0100 Subject: [PATCH 94/99] Removed redundant fields from ExtractedUiNodeLayout --- crates/bevy_ui_render/src/extract_layout.rs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ui_render/src/extract_layout.rs b/crates/bevy_ui_render/src/extract_layout.rs index 3c51f9f4deba9..f6df6a1816c77 100644 --- a/crates/bevy_ui_render/src/extract_layout.rs +++ b/crates/bevy_ui_render/src/extract_layout.rs @@ -9,7 +9,6 @@ use bevy_ecs::resource::Resource; use bevy_ecs::system::Query; use bevy_math::Affine2; use bevy_math::Rect; -use bevy_math::Vec2; use bevy_render::sync_world::MainEntityHashMap; use bevy_render::sync_world::MainEntityHashSet; use bevy_render::Extract; @@ -24,9 +23,6 @@ pub struct ExtractedUiNodeLayout { pub uinode: ComputedNode, pub transform: Affine2, pub clip: Option, - pub atlas_scaling: Option, - pub flip_x: bool, - pub flip_y: bool, pub stack_index: u32, pub visible: bool, } @@ -128,14 +124,11 @@ pub fn extract_ui_layout( continue; }; - let uinode_geometry = ExtractedUiNodeLayout { + let uinode_layout = ExtractedUiNodeLayout { extracted_camera: extracted_camera_entity, uinode: *computed_node, transform: transform.into(), clip: clip.map(|clip| clip.clip), - atlas_scaling: None, - flip_x: false, - flip_y: false, stack_index: stack_index.0, visible: inherited_visibility.get(), }; @@ -158,14 +151,14 @@ pub fn extract_ui_layout( .or_default() .insert(main_entity); } - entry.insert(uinode_geometry); + entry.insert(uinode_layout); } bevy_platform::collections::hash_map::Entry::Vacant(entry) => { extracted_camera_to_main_uinode_map .entry(extracted_camera_entity) .or_default() .insert(main_entity); - entry.insert(uinode_geometry); + entry.insert(uinode_layout); } } } From c630c50a89302cc41f6823c05ecc71a898477787 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 3 Aug 2026 22:30:17 +0100 Subject: [PATCH 95/99] fixed debug overlay fallback image --- crates/bevy_ui_render/src/lib.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index e778ff014a291..28912c6ae04a8 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -992,6 +992,20 @@ fn prepare_uinodes( &BindGroupEntries::single(view_binding), )); + #[cfg(feature = "bevy_ui_debug")] + if let Some(gpu_image) = gpu_images.get(AssetId::::default()) { + image_bind_groups + .values + .entry(AssetId::default()) + .or_insert_with(|| { + render_device.create_bind_group( + "ui_material_bind_group", + &pipeline_cache.get_bind_group_layout(&ui_pipeline.image_layout), + &BindGroupEntries::sequential((&gpu_image.texture_view, &gpu_image.sampler)), + ) + }); + } + // for each sorted render phase corresponding to a UI view for (retained_view_entity, ui_phase) in phases.iter_mut() { #[cfg(feature = "bevy_ui_debug")] From e2b3df2c18f4a2cc87317b76a119c5367898fdcd Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 6 Aug 2026 19:44:04 +0100 Subject: [PATCH 96/99] Early return from prepare if the extraction buffers are empty. --- crates/bevy_ui_render/src/box_shadow.rs | 4 ++++ crates/bevy_ui_render/src/gradient.rs | 4 ++++ crates/bevy_ui_render/src/lib.rs | 18 ++++++++++++++++++ .../bevy_ui_render/src/ui_material_pipeline.rs | 4 ++++ 4 files changed, 30 insertions(+) diff --git a/crates/bevy_ui_render/src/box_shadow.rs b/crates/bevy_ui_render/src/box_shadow.rs index e5fd4675a2bdb..41ee54b85ac55 100644 --- a/crates/bevy_ui_render/src/box_shadow.rs +++ b/crates/bevy_ui_render/src/box_shadow.rs @@ -353,6 +353,10 @@ pub fn prepare_shadows( mut phases: ResMut>, mut previous_len: Local, ) { + if extracted_shadows.box_shadows.is_empty() || extracted_geometry.layout.is_empty() { + return; + } + if let Some(view_binding) = view_uniforms.uniforms.binding() { let mut batches: Vec<(Entity, UiShadowsBatch)> = Vec::with_capacity(*previous_len); diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 73b5f70768fa8..a0d2e24685755 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -584,6 +584,10 @@ pub fn prepare_gradient( mut phases: ResMut>, mut previous_len: Local, ) { + if extracted_gradients.items.is_empty() || extracted_geometry.layout.is_empty() { + return; + } + if let Some(view_binding) = view_uniforms.uniforms.binding() { let mut batches: Vec<(Entity, GradientBatch)> = Vec::with_capacity(*previous_len); diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 28912c6ae04a8..82e76cf01d262 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -975,6 +975,24 @@ fn prepare_uinodes( }; } + if extracted_layout.layout.is_empty() { + return; + } + + #[cfg(not(feature = "bevy_ui_debug"))] + if extracted_glyph_layouts.uinodes.is_empty() && extracted_styles.uinodes.is_empty() { + return; + } + + #[cfg(feature = "bevy_ui_debug")] + if extracted_glyph_layouts.uinodes.is_empty() + && extracted_styles.uinodes.is_empty() + && !extracted_ui_debug_overlay.default_outline.enabled + && extracted_ui_debug_overlay.per_node_outline.is_empty() + { + return; + } + let Some(view_binding) = view_uniforms.uniforms.binding() else { // If there are no view bindings, give up. return; diff --git a/crates/bevy_ui_render/src/ui_material_pipeline.rs b/crates/bevy_ui_render/src/ui_material_pipeline.rs index 1d8a30a553391..ed127005b90d0 100644 --- a/crates/bevy_ui_render/src/ui_material_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_material_pipeline.rs @@ -391,6 +391,10 @@ pub fn prepare_uimaterial_nodes( mut phases: ResMut>, mut previous_len: Local, ) { + if extracted_materials.uinodes.is_empty() || extracted_geometry.layout.is_empty() { + return; + } + if let (Some(view_binding), Some(globals_binding)) = ( view_uniforms.uniforms.binding(), globals_buffer.buffer.binding(), From 99fd735dea34a90441f085510454ecfd3d6401c0 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 6 Aug 2026 19:46:39 +0100 Subject: [PATCH 97/99] retain uinode phaseitems --- crates/bevy_ui_render/src/lib.rs | 99 +++++++++++++++++++------------- 1 file changed, 60 insertions(+), 39 deletions(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 82e76cf01d262..68589abee29c9 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -12,6 +12,7 @@ mod gradient; mod image; pub use image::ImageNodeAssetChangedSystems; mod pipeline; +pub mod render_object; pub mod render_pass; mod text; pub mod ui_material; @@ -24,7 +25,7 @@ pub mod extract_layout; use bevy_a11y::AccessibilitySystems; use bevy_camera::{Camera, Camera2d, Camera3d, RenderTarget}; -use bevy_ecs::entity::{EntityHashSet, EntityIndexMap}; +use bevy_ecs::entity::{EntityHashMap, EntityHashSet, EntityIndexMap}; use bevy_reflect::prelude::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::camera::{extract_cameras, CameraMainPassTextureFormats}; @@ -859,67 +860,87 @@ pub fn queue_uinodes( ui_pipeline: Res, mut pipelines: ResMut>, mut transparent_render_phases: ResMut>, - render_views: Query<(&UiCameraView, Option<&UiAntiAlias>), With>, + render_views: Query<(Entity, &UiCameraView, Option<&UiAntiAlias>), With>, camera_views: Query<&ExtractedView>, pipeline_cache: Res, draw_functions: Res>, + mut queued_uinodes: Local>, + mut view_keys: Local>, + mut changed_uinodes: Local, ) { let draw_function = draw_functions.read().id::(); - let mut current_camera_entity = Entity::PLACEHOLDER; - let mut current_phase = None; - for (main_entity, (render_entity, _)) in extracted_uinodes.uinodes.iter() { - let Some(layout) = extracted_layout.layout.get(main_entity) else { + changed_uinodes.clear(); + changed_uinodes.extend(extracted_uinodes.changed_this_frame.iter().copied()); + changed_uinodes.extend(extracted_layout.changed.iter().copied()); + + let mut current_views = EntityHashMap::default(); + let mut live_views: HashSet = HashSet::default(); + + for (camera_entity, ui_camera_view, ui_anti_alias) in &render_views { + let Ok(view) = camera_views.get(ui_camera_view.0) else { continue; }; - if !layout.visible { - continue; + let key = UiPipelineKey { + target_format: view.target_format, + anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), + }; + let retained_view_entity = view.retained_view_entity; + + live_views.insert(retained_view_entity); + + if view_keys.insert(retained_view_entity, key) != Some(key) + && let Some(entities) = extracted_layout + .extracted_camera_to_main_uinode_map + .get(&camera_entity) + { + changed_uinodes.extend(entities.iter().copied()); } - if current_camera_entity != layout.extracted_camera { - current_phase = render_views.get(layout.extracted_camera).ok().and_then( - |(default_camera_view, ui_anti_alias)| { - camera_views - .get(default_camera_view.0) - .ok() - .and_then(|view| { - transparent_render_phases - .get_mut(&view.retained_view_entity) - .map(|transparent_phase| { - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_pipeline, - UiPipelineKey { - target_format: view.target_format, - anti_alias: matches!( - ui_anti_alias, - None | Some(UiAntiAlias::On) - ), - }, - ); - (pipeline, transparent_phase) - }) - }) - }, - ); - current_camera_entity = layout.extracted_camera; + let pipeline = pipelines.specialize(&pipeline_cache, &ui_pipeline, key); + current_views.insert(camera_entity, (retained_view_entity, pipeline)); + } + + view_keys.retain(|view, _| live_views.contains(view)); + + for main_entity in changed_uinodes.drain() { + if let Some((render_entity, retained_view_entity)) = queued_uinodes.remove(&main_entity) + && let Some(phase) = transparent_render_phases.get_mut(&retained_view_entity) + { + phase.remove(render_entity, main_entity); } - let Some((pipeline, transparent_phase)) = current_phase.as_mut() else { + let Some((render_entity, _)) = extracted_uinodes.uinodes.get(&main_entity) else { + continue; + }; + let Some(layout) = extracted_layout + .layout + .get(&main_entity) + .filter(|layout| layout.visible) + else { + continue; + }; + let Some(&(retained_view_entity, pipeline)) = current_views.get(&layout.extracted_camera) + else { + continue; + }; + let Some(phase) = transparent_render_phases.get_mut(&retained_view_entity) else { continue; }; - transparent_phase.add_transient(TransparentUi { + phase.add_retained(TransparentUi { draw_function, - pipeline: *pipeline, - entity: (*render_entity, *main_entity), + pipeline, + entity: (*render_entity, main_entity), sort_key: FloatOrd(layout.stack_index as f32), // batch_range will be calculated in prepare_uinodes batch_range: 0..0, extra_index: PhaseItemExtraIndex::None, indexed: true, }); + + queued_uinodes.insert(main_entity, (*render_entity, retained_view_entity)); } } From e97ae0daaa3a3fa09e5249ec92864a85686af6ca Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 6 Aug 2026 19:50:11 +0100 Subject: [PATCH 98/99] Fixed imports --- crates/bevy_ui_render/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_ui_render/src/lib.rs b/crates/bevy_ui_render/src/lib.rs index 68589abee29c9..a916404a4606d 100644 --- a/crates/bevy_ui_render/src/lib.rs +++ b/crates/bevy_ui_render/src/lib.rs @@ -12,7 +12,6 @@ mod gradient; mod image; pub use image::ImageNodeAssetChangedSystems; mod pipeline; -pub mod render_object; pub mod render_pass; mod text; pub mod ui_material; From ffd1f70581c0c874a2376d31838497a393f34b5c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 6 Aug 2026 21:44:22 +0100 Subject: [PATCH 99/99] skip prepare slices if no slices --- crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs | 4 ++++ 1 file changed, 4 insertions(+) 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 86a34c2b0adcb..2cdc481e09170 100644 --- a/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui_render/src/ui_texture_slice_pipeline.rs @@ -417,6 +417,10 @@ pub fn prepare_ui_slices( }; } + if extracted_slices.slices.is_empty() || extracted_geometry.layout.is_empty() { + return; + } + if let Some(view_binding) = view_uniforms.uniforms.binding() { let mut batches: Vec<(Entity, UiTextureSlicerBatch)> = Vec::with_capacity(*previous_len);