Skip to content

Commit f32ce12

Browse files
committed
Retain UI phase items from frame to frame, and consolidate the UI
queuing systems into one. Although PR #24893 added retention for UI render world instances themselves, in order to avoid re-extracting them from the main world ECS every frame, we still recreate the `TransparentUi` phase items every frame via `add_transient()`, which additionally removes them from the phase at the end of every frame. This is a significant CPU time sink and isn't the preferred pattern in Bevy nowadays. This commit makes the UI-related phase items retained just as 3D meshes are. All `queue_` methods in `bevy_ui_render` have been updated to walk the list of changed and removed meshes and update elements in the `SortedRenderPhase` only as necessary. The calls to `add_transient()` have been removed in favor of the more modern `add_retained()`. Additionally, all the custom queuing systems have been consolidated into a single generic system, `queue_ui_items`. The resources that hold extracted UI items have likewise been consolidated into a generic `UiRenderObjects` resource. The behavior specific to each individual item type (normal UI nodes, box shadows, gradients, etc.) has been factored into a trait named `UiRenderObject`. This has resulted in dramatic simplifications throughout UI rendering. See the documentation for more information. On `many_buttons`, this PR reduces the median frame time from 36.95 ms to 22.78 ms, or 27 FPS to 44 FPS. The `queue_uinodes` system has gone from 6.21 ms/frame to 15.2 μs/frame, a 409× speedup. And, because the Rust standard library's sorting algorithm is good at sorting data that's close to already sorted, the `sort_phase_system` time decreases from 5.17 ms/frame to 1.29 ms/frame, a 4.01× speedup.
1 parent 1f2feb6 commit f32ce12

8 files changed

Lines changed: 581 additions & 456 deletions

File tree

crates/bevy_ui_render/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ bytemuck = { version = "1.5", features = ["derive"] }
4242
derive_more = { version = "2", default-features = false, features = ["from"] }
4343
tracing = { version = "0.1", default-features = false, features = ["std"] }
4444
indexmap = { version = "2" }
45+
smallvec = { version = "1" }
4546

4647
[features]
4748
default = ["bevy_ui_debug"]

crates/bevy_ui_render/src/box_shadow.rs

Lines changed: 78 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use bevy_app::prelude::*;
66
use bevy_asset::*;
77
use bevy_camera::visibility::InheritedVisibility;
88
use bevy_color::{Alpha, ColorToComponents, LinearRgba};
9-
use bevy_ecs::entity::EntityIndexMap;
109
use bevy_ecs::prelude::*;
1110
use bevy_ecs::{
1211
prelude::Component,
@@ -17,7 +16,7 @@ use bevy_ecs::{
1716
};
1817
use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2};
1918
use bevy_mesh::VertexBufferLayout;
20-
use bevy_render::sync_world::{MainEntity, MainEntityHashMap, MainEntityHashSet};
19+
use bevy_render::sync_world::{MainEntity, MainEntityHashSet};
2120
use bevy_render::{
2221
render_phase::*,
2322
render_resource::{binding_types::uniform_buffer, *},
@@ -34,9 +33,12 @@ use bevy_ui::{
3433
use bevy_utils::default;
3534
use bytemuck::{Pod, Zeroable};
3635

37-
use crate::{BoxShadowSamples, RenderUiSystems, TransparentUi, UiCameraMap};
36+
use crate::{
37+
queue_ui_items, BoxShadowSamples, CachedCameraView, ChangedUiObject, RenderUiSystems,
38+
TransparentUi, UiCameraMap, UiRenderObject, UiRenderObjects,
39+
};
3840

39-
use super::{stack_z_offsets, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
41+
use super::{stack_z_offsets, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
4042

4143
/// A plugin that enables the rendering of box shadows.
4244
pub struct BoxShadowPlugin;
@@ -59,7 +61,7 @@ impl Plugin for BoxShadowPlugin {
5961
.add_systems(
6062
Render,
6163
(
62-
queue_shadows.in_set(RenderSystems::Queue),
64+
queue_ui_items::<ExtractedBoxShadow>.in_set(RenderSystems::Queue),
6365
prepare_shadows.in_set(RenderSystems::PrepareBindGroups),
6466
),
6567
);
@@ -194,17 +196,44 @@ pub struct ExtractedBoxShadow {
194196
pub size: Vec2,
195197
}
196198

197-
/// List of extracted shadows to be sorted and queued for rendering
198-
#[derive(Resource, Default)]
199-
pub struct ExtractedBoxShadows {
200-
/// The list of box shadows grouped by their main-world entity, along with
201-
/// each group's target camera entity.
202-
///
203-
/// This is a two-level data structure so that we can quickly remove all box
204-
/// shadows associated with a main-world entity when it changes.
205-
pub box_shadows: MainEntityHashMap<(Entity, EntityIndexMap<ExtractedBoxShadow>)>,
199+
impl UiRenderObject for ExtractedBoxShadow {
200+
type DrawFunctions = DrawBoxShadows;
201+
type ViewPipelineKeyBuilder = UiBoxShadowViewPipelineKeyBuilder;
202+
type ViewQueryData = Option<&'static BoxShadowSamples>;
203+
type SpecializedRenderPipeline = BoxShadowPipeline;
204+
type PipelineKeySystemParam = ();
205+
206+
fn get_sort_key(&self) -> FloatOrd {
207+
FloatOrd(self.stack_index as f32 + stack_z_offsets::BOX_SHADOW)
208+
}
209+
210+
fn create_view_pipeline_key_builder<'w, 's>(
211+
box_shadow_samples: Option<&BoxShadowSamples>,
212+
) -> Self::ViewPipelineKeyBuilder {
213+
UiBoxShadowViewPipelineKeyBuilder {
214+
box_shadow_samples: box_shadow_samples.cloned(),
215+
}
216+
}
217+
218+
fn create_pipeline_key(
219+
&self,
220+
cached_camera_view: &CachedCameraView<Self::ViewPipelineKeyBuilder>,
221+
_: &mut SystemParamItem<Self::PipelineKeySystemParam>,
222+
) -> Option<BoxShadowPipelineKey> {
223+
Some(BoxShadowPipelineKey {
224+
target_format: cached_camera_view.extracted_view.target_format,
225+
samples: cached_camera_view
226+
.pipeline_key_builder
227+
.box_shadow_samples
228+
.unwrap_or_default()
229+
.0,
230+
})
231+
}
206232
}
207233

234+
/// List of extracted shadows to be sorted and queued for rendering
235+
pub type ExtractedBoxShadows = UiRenderObjects<ExtractedBoxShadow>;
236+
208237
pub fn extract_shadows(
209238
mut commands: Commands,
210239
mut extracted_box_shadows: ResMut<ExtractedBoxShadows>,
@@ -269,6 +298,7 @@ pub fn extract_shadows(
269298
mut nodes_processed_this_frame: Local<MainEntityHashSet>,
270299
) {
271300
nodes_processed_this_frame.clear();
301+
extracted_box_shadows.changed.clear();
272302

273303
let mut mapping = camera_map.get_mapper();
274304

@@ -281,14 +311,22 @@ pub fn extract_shadows(
281311
{
282312
let main_entity = MainEntity::from(entity);
283313

284-
// If there were any previous box shadows for this entity, despawn them.
285-
for (render_entity, _) in extracted_box_shadows
286-
.box_shadows
287-
.get_mut(&main_entity)
288-
.iter_mut()
289-
.flat_map(|(_, shadows)| shadows.drain(..))
314+
// If there were any previous box shadows for this entity, despawn them
315+
// and record them as changed so the render phase entry can be removed.
316+
if let Some((prev_camera_entity, mut shadows)) =
317+
extracted_box_shadows.objects.remove(&main_entity)
290318
{
291-
commands.entity(render_entity).despawn();
319+
let changed = extracted_box_shadows
320+
.changed
321+
.entry(main_entity)
322+
.or_default();
323+
for (render_entity, _) in shadows.drain(..) {
324+
commands.entity(render_entity).despawn();
325+
changed.push(ChangedUiObject {
326+
render_entity,
327+
prev_camera_entity,
328+
});
329+
}
292330
}
293331

294332
// Skip if no visible shadows
@@ -299,7 +337,7 @@ pub fn extract_shadows(
299337
let Some(extracted_camera_entity) = mapping.map(camera) else {
300338
continue;
301339
};
302-
if let Some((camera_entity, _)) = extracted_box_shadows.box_shadows.get_mut(&main_entity) {
340+
if let Some((camera_entity, _)) = extracted_box_shadows.objects.get_mut(&main_entity) {
303341
*camera_entity = extracted_camera_entity;
304342
}
305343

@@ -347,7 +385,7 @@ pub fn extract_shadows(
347385
};
348386

349387
extracted_box_shadows
350-
.box_shadows
388+
.objects
351389
.entry(main_entity)
352390
.or_insert_with(|| (extracted_camera_entity, Default::default()))
353391
.1
@@ -383,81 +421,31 @@ pub fn extract_shadows(
383421
if nodes_processed_this_frame.contains(&main_entity) {
384422
continue;
385423
}
386-
let Some((_, mut extracted_nodes)) = extracted_box_shadows.box_shadows.remove(&main_entity)
424+
let Some((prev_camera_entity, mut extracted_nodes)) =
425+
extracted_box_shadows.objects.remove(&main_entity)
387426
else {
388427
continue;
389428
};
429+
let changed = extracted_box_shadows
430+
.changed
431+
.entry(main_entity)
432+
.or_default();
390433
for (render_entity, _) in extracted_nodes.drain(..) {
391434
commands.entity(render_entity).despawn();
435+
changed.push(ChangedUiObject {
436+
render_entity,
437+
prev_camera_entity,
438+
});
392439
}
393440
}
394441
}
395442

396-
#[expect(
397-
clippy::too_many_arguments,
398-
reason = "it's a system that needs a lot of them"
399-
)]
400-
pub fn queue_shadows(
401-
extracted_box_shadows: ResMut<ExtractedBoxShadows>,
402-
box_shadow_pipeline: Res<BoxShadowPipeline>,
403-
mut pipelines: ResMut<SpecializedRenderPipelines<BoxShadowPipeline>>,
404-
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
405-
render_views: Query<(&UiCameraView, Option<&BoxShadowSamples>), With<ExtractedView>>,
406-
camera_views: Query<&ExtractedView>,
407-
pipeline_cache: Res<PipelineCache>,
408-
draw_functions: Res<DrawFunctions<TransparentUi>>,
409-
) {
410-
let draw_function = draw_functions.read().id::<DrawBoxShadows>();
411-
let mut current_camera_entity = Entity::PLACEHOLDER;
412-
let mut current_phase = None;
413-
414-
for (main_entity, (extracted_camera_entity, extracted_sub_shadows)) in
415-
extracted_box_shadows.box_shadows.iter()
416-
{
417-
if current_camera_entity != *extracted_camera_entity {
418-
current_phase = render_views.get(*extracted_camera_entity).ok().and_then(
419-
|(default_camera_view, shadow_samples)| {
420-
camera_views
421-
.get(default_camera_view.0)
422-
.ok()
423-
.and_then(|view| {
424-
transparent_render_phases
425-
.get_mut(&view.retained_view_entity)
426-
.map(|transparent_phase| {
427-
let pipeline = pipelines.specialize(
428-
&pipeline_cache,
429-
&box_shadow_pipeline,
430-
BoxShadowPipelineKey {
431-
target_format: view.target_format,
432-
samples: shadow_samples.copied().unwrap_or_default().0,
433-
},
434-
);
435-
(pipeline, transparent_phase)
436-
})
437-
})
438-
},
439-
);
440-
current_camera_entity = *extracted_camera_entity;
441-
}
442-
443-
let Some((pipeline, transparent_phase)) = current_phase.as_mut() else {
444-
continue;
445-
};
446-
for (entity, extracted_shadow) in extracted_sub_shadows.iter() {
447-
transparent_phase.add_transient(TransparentUi {
448-
draw_function,
449-
pipeline: *pipeline,
450-
entity: (*entity, *main_entity),
451-
sort_key: FloatOrd(
452-
extracted_shadow.stack_index as f32 + stack_z_offsets::BOX_SHADOW,
453-
),
454-
455-
batch_range: 0..0,
456-
extra_index: PhaseItemExtraIndex::None,
457-
indexed: true,
458-
});
459-
}
460-
}
443+
/// Information that the box shadow renderer needs from each view to construct
444+
/// the pipeline key.
445+
pub struct UiBoxShadowViewPipelineKeyBuilder {
446+
/// The number of samples that this view requests to render box shadows
447+
/// with.
448+
box_shadow_samples: Option<BoxShadowSamples>,
461449
}
462450

463451
pub fn prepare_shadows(
@@ -491,7 +479,7 @@ pub fn prepare_shadows(
491479
for item_index in 0..ui_phase.items.len() {
492480
let item = &mut ui_phase.items[item_index];
493481
let Some((extracted_camera_entity, box_shadow)) = extracted_shadows
494-
.box_shadows
482+
.objects
495483
.get(&item.main_entity())
496484
.and_then(|(extracted_camera_entity, sub_shadows)| {
497485
sub_shadows

crates/bevy_ui_render/src/debug_overlay.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ pub fn extract_debug_overlay(
194194
for (entity, uinode, stack_index, transform, visibility, maybe_clip, computed_target, debug) in
195195
extracted_uinodes
196196
.changed
197-
.iter()
197+
.keys()
198198
.flat_map(|main_entity| uinode_query.get(main_entity.entity()).ok())
199199
{
200200
let debug_options = debug.copied().unwrap_or((*debug_options.as_ref()).into());
@@ -222,7 +222,7 @@ pub fn extract_debug_overlay(
222222
}
223223

224224
extracted_uinodes
225-
.uinodes
225+
.objects
226226
.entry(entity.into())
227227
.or_insert_with(|| (extracted_camera_entity, Default::default()))
228228
.1

0 commit comments

Comments
 (0)