Skip to content

Commit f292a76

Browse files
ickshonpemrchantey
authored andcommitted
Improved UI camera mapping (bevyengine#17244)
# Objective Two more optimisations for UI extraction: * We only need to query for the camera's render entity when the target camera changes. If the target camera is the same as for the previous UI node we can use the previous render entity. * The cheap checks for visibility and zero size should be performed first before the camera queries. ## Solution Add a new system param `UiCameraMap` that resolves the correct render camera entity and only queries when necessary. <img width="506" alt="tracee" src="https://github.com/user-attachments/assets/f57d1e0d-f3a7-49ee-8287-4f01ffc8ba24" /> I don't like the `UiCameraMap` + `UiCameraMapper` implementation very much, maybe someone else can suggest a better construction. This is partly motivated by bevyengine#16942 which adds further indirection and these changes would ameliorate that performance regression.
1 parent f4800d1 commit f292a76

File tree

6 files changed

+111
-106
lines changed

6 files changed

+111
-106
lines changed

crates/bevy_ui/src/render/box_shadow.rs

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
use core::{hash::Hash, ops::Range};
44

55
use crate::{
6-
BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, DefaultUiCamera, RenderUiSystem,
6+
BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, RenderUiSystem,
77
ResolvedBorderRadius, TransparentUi, UiTargetCamera, Val,
88
};
99
use bevy_app::prelude::*;
@@ -27,14 +27,14 @@ use bevy_render::{
2727
render_phase::*,
2828
render_resource::{binding_types::uniform_buffer, *},
2929
renderer::{RenderDevice, RenderQueue},
30-
sync_world::{RenderEntity, TemporaryRenderEntity},
30+
sync_world::TemporaryRenderEntity,
3131
view::*,
3232
Extract, ExtractSchedule, Render, RenderSet,
3333
};
3434
use bevy_transform::prelude::GlobalTransform;
3535
use bytemuck::{Pod, Zeroable};
3636

37-
use super::{stack_z_offsets, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
37+
use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS};
3838

3939
pub const BOX_SHADOW_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(17717747047134343426);
4040

@@ -236,7 +236,6 @@ pub struct ExtractedBoxShadows {
236236
pub fn extract_shadows(
237237
mut commands: Commands,
238238
mut extracted_box_shadows: ResMut<ExtractedBoxShadows>,
239-
default_ui_camera: Extract<DefaultUiCamera>,
240239
camera_query: Extract<Query<(Entity, &Camera)>>,
241240
box_shadow_query: Extract<
242241
Query<(
@@ -249,27 +248,22 @@ pub fn extract_shadows(
249248
Option<&UiTargetCamera>,
250249
)>,
251250
>,
252-
mapping: Extract<Query<RenderEntity>>,
251+
camera_map: Extract<UiCameraMap>,
253252
) {
254-
let default_camera_entity = default_ui_camera.get();
253+
let mut camera_mapper = camera_map.get_mapper();
255254

256255
for (entity, uinode, transform, visibility, box_shadow, clip, camera) in &box_shadow_query {
257-
let Some(camera_entity) = camera.map(UiTargetCamera::entity).or(default_camera_entity)
258-
else {
259-
continue;
260-
};
261-
262-
let Ok(extracted_camera_entity) = mapping.get(camera_entity) else {
263-
continue;
264-
};
265-
266256
// Skip if no visible shadows
267257
if !visibility.get() || box_shadow.is_empty() || uinode.is_empty() {
268258
continue;
269259
}
270260

261+
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
262+
continue;
263+
};
264+
271265
let ui_physical_viewport_size = camera_query
272-
.get(extracted_camera_entity)
266+
.get(camera_mapper.current_camera())
273267
.ok()
274268
.and_then(|(_, c)| {
275269
c.physical_viewport_size()
@@ -392,6 +386,10 @@ pub fn queue_shadows(
392386
}
393387
}
394388

389+
#[expect(
390+
clippy::too_many_arguments,
391+
reason = "Could be rewritten with less arguments using a QueryData-implementing struct, but doesn't need to be."
392+
)]
395393
pub fn prepare_shadows(
396394
mut commands: Commands,
397395
render_device: Res<RenderDevice>,

crates/bevy_ui/src/render/debug_overlay.rs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use crate::CalculatedClip;
22
use crate::ComputedNode;
3-
use crate::DefaultUiCamera;
43
use crate::UiTargetCamera;
54
use bevy_asset::AssetId;
65
use bevy_color::Hsla;
@@ -12,7 +11,6 @@ use bevy_ecs::system::Res;
1211
use bevy_ecs::system::ResMut;
1312
use bevy_math::Rect;
1413
use bevy_math::Vec2;
15-
use bevy_render::sync_world::RenderEntity;
1614
use bevy_render::sync_world::TemporaryRenderEntity;
1715
use bevy_render::view::InheritedVisibility;
1816
use bevy_render::Extract;
@@ -23,6 +21,7 @@ use super::ExtractedUiItem;
2321
use super::ExtractedUiNode;
2422
use super::ExtractedUiNodes;
2523
use super::NodeType;
24+
use super::UiCameraMap;
2625

2726
/// Configuration for the UI debug overlay
2827
#[derive(Resource)]
@@ -58,7 +57,6 @@ pub fn extract_debug_overlay(
5857
mut commands: Commands,
5958
debug_options: Extract<Res<UiDebugOptions>>,
6059
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
61-
default_ui_camera: Extract<DefaultUiCamera>,
6260
uinode_query: Extract<
6361
Query<(
6462
Entity,
@@ -69,25 +67,20 @@ pub fn extract_debug_overlay(
6967
Option<&UiTargetCamera>,
7068
)>,
7169
>,
72-
mapping: Extract<Query<RenderEntity>>,
70+
camera_map: Extract<UiCameraMap>,
7371
) {
7472
if !debug_options.enabled {
7573
return;
7674
}
7775

78-
let default_camera_entity = default_ui_camera.get();
76+
let mut camera_mapper = camera_map.get_mapper();
7977

8078
for (entity, uinode, visibility, maybe_clip, transform, camera) in &uinode_query {
8179
if !debug_options.show_hidden && !visibility.get() {
8280
continue;
8381
}
8482

85-
let Some(camera_entity) = camera.map(UiTargetCamera::entity).or(default_camera_entity)
86-
else {
87-
continue;
88-
};
89-
90-
let Ok(extracted_camera_entity) = mapping.get(camera_entity) else {
83+
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
9184
continue;
9285
};
9386

crates/bevy_ui/src/render/mod.rs

Lines changed: 72 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d};
2020
use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
2121
use bevy_ecs::entity::hash_map::EntityHashMap;
2222
use bevy_ecs::prelude::*;
23+
use bevy_ecs::system::SystemParam;
2324
use bevy_image::prelude::*;
2425
use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles};
2526
use bevy_render::render_graph::{NodeRunError, RenderGraphContext};
@@ -251,6 +252,54 @@ impl ExtractedUiNodes {
251252
}
252253
}
253254

255+
#[derive(SystemParam)]
256+
pub struct UiCameraMap<'w, 's> {
257+
default: DefaultUiCamera<'w, 's>,
258+
mapping: Query<'w, 's, RenderEntity>,
259+
}
260+
261+
impl<'w, 's> UiCameraMap<'w, 's> {
262+
/// Get the default camera and create the mapper
263+
pub fn get_mapper(&'w self) -> UiCameraMapper<'w, 's> {
264+
let default_camera_entity = self.default.get();
265+
UiCameraMapper {
266+
mapping: &self.mapping,
267+
default_camera_entity,
268+
camera_entity: Entity::PLACEHOLDER,
269+
render_entity: Entity::PLACEHOLDER,
270+
}
271+
}
272+
}
273+
274+
pub struct UiCameraMapper<'w, 's> {
275+
mapping: &'w Query<'w, 's, RenderEntity>,
276+
default_camera_entity: Option<Entity>,
277+
camera_entity: Entity,
278+
render_entity: Entity,
279+
}
280+
281+
impl<'w, 's> UiCameraMapper<'w, 's> {
282+
/// Returns the render entity corresponding to the given `UiTargetCamera` or the default camera if `None`.
283+
pub fn map(&mut self, camera: Option<&UiTargetCamera>) -> Option<Entity> {
284+
let camera_entity = camera
285+
.map(UiTargetCamera::entity)
286+
.or(self.default_camera_entity)?;
287+
if self.camera_entity != camera_entity {
288+
let Ok(new_render_camera_entity) = self.mapping.get(camera_entity) else {
289+
return None;
290+
};
291+
self.render_entity = new_render_camera_entity;
292+
self.camera_entity = camera_entity;
293+
}
294+
295+
Some(self.render_entity)
296+
}
297+
298+
pub fn current_camera(&self) -> Entity {
299+
self.camera_entity
300+
}
301+
}
302+
254303
/// A [`RenderGraphNode`] that executes the UI rendering subgraph on the UI
255304
/// view.
256305
struct RunUiSubgraphOnUiViewNode;
@@ -279,7 +328,6 @@ impl RenderGraphNode for RunUiSubgraphOnUiViewNode {
279328
pub fn extract_uinode_background_colors(
280329
mut commands: Commands,
281330
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
282-
default_ui_camera: Extract<DefaultUiCamera>,
283331
uinode_query: Extract<
284332
Query<(
285333
Entity,
@@ -291,21 +339,13 @@ pub fn extract_uinode_background_colors(
291339
&BackgroundColor,
292340
)>,
293341
>,
294-
mapping: Extract<Query<RenderEntity>>,
342+
camera_map: Extract<UiCameraMap>,
295343
) {
296-
let default_camera_entity = default_ui_camera.get();
344+
let mut camera_mapper = camera_map.get_mapper();
345+
297346
for (entity, uinode, transform, inherited_visibility, clip, camera, background_color) in
298347
&uinode_query
299348
{
300-
let Some(camera_entity) = camera.map(UiTargetCamera::entity).or(default_camera_entity)
301-
else {
302-
continue;
303-
};
304-
305-
let Ok(extracted_camera_entity) = mapping.get(camera_entity) else {
306-
continue;
307-
};
308-
309349
// Skip invisible backgrounds
310350
if !inherited_visibility.get()
311351
|| background_color.0.is_fully_transparent()
@@ -314,6 +354,10 @@ pub fn extract_uinode_background_colors(
314354
continue;
315355
}
316356

357+
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
358+
continue;
359+
};
360+
317361
extracted_uinodes.uinodes.insert(
318362
commands.spawn(TemporaryRenderEntity).id(),
319363
ExtractedUiNode {
@@ -345,7 +389,6 @@ pub fn extract_uinode_images(
345389
mut commands: Commands,
346390
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
347391
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
348-
default_ui_camera: Extract<DefaultUiCamera>,
349392
uinode_query: Extract<
350393
Query<(
351394
Entity,
@@ -357,19 +400,10 @@ pub fn extract_uinode_images(
357400
&ImageNode,
358401
)>,
359402
>,
360-
mapping: Extract<Query<RenderEntity>>,
403+
camera_map: Extract<UiCameraMap>,
361404
) {
362-
let default_camera_entity = default_ui_camera.get();
405+
let mut camera_mapper = camera_map.get_mapper();
363406
for (entity, uinode, transform, inherited_visibility, clip, camera, image) in &uinode_query {
364-
let Some(camera_entity) = camera.map(UiTargetCamera::entity).or(default_camera_entity)
365-
else {
366-
continue;
367-
};
368-
369-
let Ok(extracted_camera_entity) = mapping.get(camera_entity) else {
370-
continue;
371-
};
372-
373407
// Skip invisible images
374408
if !inherited_visibility.get()
375409
|| image.color.is_fully_transparent()
@@ -380,6 +414,10 @@ pub fn extract_uinode_images(
380414
continue;
381415
}
382416

417+
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
418+
continue;
419+
};
420+
383421
let atlas_rect = image
384422
.texture_atlas
385423
.as_ref()
@@ -436,7 +474,6 @@ pub fn extract_uinode_images(
436474
pub fn extract_uinode_borders(
437475
mut commands: Commands,
438476
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
439-
default_ui_camera: Extract<DefaultUiCamera>,
440477
uinode_query: Extract<
441478
Query<(
442479
Entity,
@@ -449,10 +486,11 @@ pub fn extract_uinode_borders(
449486
AnyOf<(&BorderColor, &Outline)>,
450487
)>,
451488
>,
452-
mapping: Extract<Query<RenderEntity>>,
489+
camera_map: Extract<UiCameraMap>,
453490
) {
454491
let image = AssetId::<Image>::default();
455-
let default_camera_entity = default_ui_camera.get();
492+
let mut camera_mapper = camera_map.get_mapper();
493+
456494
for (
457495
entity,
458496
node,
@@ -464,22 +502,15 @@ pub fn extract_uinode_borders(
464502
(maybe_border_color, maybe_outline),
465503
) in &uinode_query
466504
{
467-
let Some(camera_entity) = maybe_camera
468-
.map(UiTargetCamera::entity)
469-
.or(default_camera_entity)
470-
else {
471-
continue;
472-
};
473-
474-
let Ok(extracted_camera_entity) = mapping.get(camera_entity) else {
475-
continue;
476-
};
477-
478505
// Skip invisible borders and removed nodes
479506
if !inherited_visibility.get() || node.display == Display::None {
480507
continue;
481508
}
482509

510+
let Some(extracted_camera_entity) = camera_mapper.map(maybe_camera) else {
511+
continue;
512+
};
513+
483514
// Don't extract borders with zero width along all edges
484515
if computed_node.border() != BorderRect::ZERO {
485516
if let Some(border_color) = maybe_border_color.filter(|bc| !bc.0.is_fully_transparent())
@@ -675,7 +706,6 @@ pub fn extract_ui_camera_view(
675706
pub fn extract_text_sections(
676707
mut commands: Commands,
677708
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
678-
default_ui_camera: Extract<DefaultUiCamera>,
679709
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
680710
uinode_query: Extract<
681711
Query<(
@@ -690,12 +720,12 @@ pub fn extract_text_sections(
690720
)>,
691721
>,
692722
text_styles: Extract<Query<&TextColor>>,
693-
mapping: Extract<Query<RenderEntity>>,
723+
camera_map: Extract<UiCameraMap>,
694724
) {
695725
let mut start = 0;
696726
let mut end = 1;
697727

698-
let default_ui_camera = default_ui_camera.get();
728+
let mut camera_mapper = camera_map.get_mapper();
699729
for (
700730
entity,
701731
uinode,
@@ -707,16 +737,12 @@ pub fn extract_text_sections(
707737
text_layout_info,
708738
) in &uinode_query
709739
{
710-
let Some(camera_entity) = camera.map(UiTargetCamera::entity).or(default_ui_camera) else {
711-
continue;
712-
};
713-
714740
// Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`)
715741
if !inherited_visibility.get() || uinode.is_empty() {
716742
continue;
717743
}
718744

719-
let Ok(extracted_camera_entity) = mapping.get(camera_entity) else {
745+
let Some(extracted_camera_entity) = camera_mapper.map(camera) else {
720746
continue;
721747
};
722748

0 commit comments

Comments
 (0)