Skip to content

Commit

Permalink
Fix sprite performance regression since retained render world (#17078)
Browse files Browse the repository at this point in the history
# Objective

- Fix sprite rendering performance regression since retained render
world changes
- The retained render world changes moved `ExtractedSprites` from using
the highly-optimised `EntityHasher` with an `Entity` to using
`FixedHasher` with `(Entity, MainEntity)`. This was enough to regress
framerate in bevymark by 25%.

## Solution

- Move the render world entity into a member of `ExtractedSprite` and
change `ExtractedSprites` to use `MainEntityHashMap` for its storage
- Disable sprite picking in bevymark

## Testing

M4 Max. `bevymark --waves 100 --per-wave 1000 --benchmark`. main in
yellow vs PR in red:

<img width="590" alt="Screenshot 2025-01-01 at 16 36 22"
src="https://github.com/user-attachments/assets/1e4ed6ec-3811-4abf-8b30-336153737f89"
/>

20.2% median frame time reduction.

<img width="594" alt="Screenshot 2025-01-01 at 16 38 37"
src="https://github.com/user-attachments/assets/157c2022-cda6-4cf2-bc63-d0bc40528cf0"
/>

49.7% median extract_sprites execution time reduction.

Comparing 0.14.2 yellow vs PR red:
<img width="593" alt="Screenshot 2025-01-01 at 16 40 06"
src="https://github.com/user-attachments/assets/abd59b6f-290a-4eb6-8835-ed110af995f3"
/>

~6.1% median frame time reduction.

---

## Migration Guide

- `ExtractedSprites` is now using `MainEntityHashMap` for storage, which
is keyed on `MainEntity`.
- The render world entity corresponding to an `ExtractedSprite` is now
stored in the `render_entity` member of it.
  • Loading branch information
superdump authored Jan 1, 2025
1 parent 0141bd0 commit fd330c8
Show file tree
Hide file tree
Showing 4 changed files with 37 additions and 31 deletions.
37 changes: 19 additions & 18 deletions crates/bevy_sprite/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ use bevy_ecs::{
};
use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo};
use bevy_math::{Affine3A, FloatOrd, Quat, Rect, Vec2, Vec4};
use bevy_render::sync_world::MainEntity;
use bevy_render::view::RenderVisibleEntities;
use bevy_render::{
render_asset::RenderAssets,
Expand All @@ -32,7 +31,7 @@ use bevy_render::{
*,
},
renderer::{RenderDevice, RenderQueue},
sync_world::{RenderEntity, TemporaryRenderEntity},
sync_world::{MainEntityHashMap, RenderEntity, TemporaryRenderEntity},
texture::{DefaultImageSampler, FallbackImage, GpuImage},
view::{
ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms,
Expand Down Expand Up @@ -341,11 +340,12 @@ pub struct ExtractedSprite {
/// For cases where additional [`ExtractedSprites`] are created during extraction, this stores the
/// entity that caused that creation for use in determining visibility.
pub original_entity: Option<Entity>,
pub render_entity: Entity,
}

#[derive(Resource, Default)]
pub struct ExtractedSprites {
pub sprites: HashMap<(Entity, MainEntity), ExtractedSprite>,
pub sprites: MainEntityHashMap<ExtractedSprite>,
}

#[derive(Resource, Default)]
Expand Down Expand Up @@ -390,16 +390,13 @@ pub fn extract_sprites(
if let Some(slices) = slices {
extracted_sprites.sprites.extend(
slices
.extract_sprites(transform, original_entity, sprite)
.map(|e| {
(
(
commands.spawn(TemporaryRenderEntity).id(),
original_entity.into(),
),
e,
)
}),
.extract_sprites(
transform,
original_entity,
commands.spawn(TemporaryRenderEntity).id(),
sprite,
)
.map(|e| (original_entity.into(), e)),
);
} else {
let atlas_rect = sprite
Expand All @@ -420,7 +417,7 @@ pub fn extract_sprites(

// PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive
extracted_sprites.sprites.insert(
(entity, original_entity.into()),
original_entity.into(),
ExtractedSprite {
color: sprite.color.into(),
transform: *transform,
Expand All @@ -432,6 +429,7 @@ pub fn extract_sprites(
image_handle_id: sprite.image.id(),
anchor: sprite.anchor.as_vec(),
original_entity: Some(original_entity),
render_entity: entity,
},
);
}
Expand Down Expand Up @@ -558,8 +556,11 @@ pub fn queue_sprites(
.items
.reserve(extracted_sprites.sprites.len());

for ((entity, main_entity), extracted_sprite) in extracted_sprites.sprites.iter() {
let index = extracted_sprite.original_entity.unwrap_or(*entity).index();
for (main_entity, extracted_sprite) in extracted_sprites.sprites.iter() {
let index = extracted_sprite
.original_entity
.unwrap_or(extracted_sprite.render_entity)
.index();

if !view_entities.contains(index as usize) {
continue;
Expand All @@ -572,7 +573,7 @@ pub fn queue_sprites(
transparent_phase.add(Transparent2d {
draw_function: draw_sprite_function,
pipeline,
entity: (*entity, *main_entity),
entity: (extracted_sprite.render_entity, *main_entity),
sort_key,
// batch_range and dynamic_offset will be calculated in prepare_sprites
batch_range: 0..0,
Expand Down Expand Up @@ -662,7 +663,7 @@ pub fn prepare_sprite_image_bind_groups(
// Compatible items share the same entity.
for item_index in 0..transparent_phase.items.len() {
let item = &transparent_phase.items[item_index];
let Some(extracted_sprite) = extracted_sprites.sprites.get(&item.entity) else {
let Some(extracted_sprite) = extracted_sprites.sprites.get(&item.entity.1) else {
// If there is a phase item that is not a sprite, then we must start a new
// batch to draw the other phase item(s) and to respect draw order. This can be
// done by invalidating the batch_image_handle
Expand Down
2 changes: 2 additions & 0 deletions crates/bevy_sprite/src/texture_slice/computed_slices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ impl ComputedTextureSlices {
&'a self,
transform: &'a GlobalTransform,
original_entity: Entity,
render_entity: Entity,
sprite: &'a Sprite,
) -> impl ExactSizeIterator<Item = ExtractedSprite> + 'a {
let mut flip = Vec2::ONE;
Expand All @@ -53,6 +54,7 @@ impl ComputedTextureSlices {
flip_y,
image_handle_id: sprite.image.id(),
anchor: Self::redepend_anchor_from_sprite_to_slice(sprite, slice),
render_entity,
}
})
}
Expand Down
6 changes: 2 additions & 4 deletions crates/bevy_text/src/text2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,7 @@ pub fn extract_text2d_sprite(
let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();

extracted_sprites.sprites.insert(
(
commands.spawn(TemporaryRenderEntity).id(),
original_entity.into(),
),
original_entity.into(),
ExtractedSprite {
transform: transform * GlobalTransform::from_translation(position.extend(0.)),
color,
Expand All @@ -220,6 +217,7 @@ pub fn extract_text2d_sprite(
flip_y: false,
anchor: Anchor::Center.as_vec(),
original_entity: Some(original_entity),
render_entity: commands.spawn(TemporaryRenderEntity).id(),
},
);
}
Expand Down
23 changes: 14 additions & 9 deletions examples/stress_tests/bevymark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use bevy::{
render_asset::RenderAssetUsages,
render_resource::{Extent3d, TextureDimension, TextureFormat},
},
sprite::AlphaMode2d,
sprite::{AlphaMode2d, SpritePlugin},
utils::Duration,
window::{PresentMode, WindowResolution},
winit::{UpdateMode, WinitSettings},
Expand Down Expand Up @@ -132,16 +132,21 @@ fn main() {

App::new()
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "BevyMark".into(),
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
present_mode: PresentMode::AutoNoVsync,
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
title: "BevyMark".into(),
resolution: WindowResolution::new(1920.0, 1080.0)
.with_scale_factor_override(1.0),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
})
.set(SpritePlugin {
#[cfg(feature = "bevy_sprite_picking_backend")]
add_picking: false,
}),
..default()
}),
FrameTimeDiagnosticsPlugin,
LogDiagnosticsPlugin::default(),
))
Expand Down

0 comments on commit fd330c8

Please sign in to comment.