Extract sprites into a Vec (#17619)

# Objective

Extract sprites into a `Vec` instead of a `HashMap`.

## Solution

Extract UI nodes into a `Vec` instead of an `EntityHashMap`.
Add an index into the `Vec` to `Transparent2d`.
Compare both the index and render entity in prepare so there aren't any
collisions.

## Showcase
yellow this PR, red main

```
cargo run --example many_sprites --release --features "trace_tracy"
```

`extract_sprites`
<img width="452" alt="extract_sprites"
src="https://github.com/user-attachments/assets/66c60406-7c2b-4367-907d-4a71d3630296"
/>

`queue_sprites`
<img width="463" alt="queue_sprites"
src="https://github.com/user-attachments/assets/54b903bd-4137-4772-9f87-e10e1e050d69"
/>

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
ickshonpe 2025-03-18 00:48:33 +00:00 committed by GitHub
parent 958c9bb652
commit 4d8bc6161b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 64 additions and 59 deletions

View File

@ -349,6 +349,7 @@ pub struct Transparent2d {
pub pipeline: CachedRenderPipelineId, pub pipeline: CachedRenderPipelineId,
pub draw_function: DrawFunctionId, pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>, pub batch_range: Range<u32>,
pub extracted_index: usize,
pub extra_index: PhaseItemExtraIndex, pub extra_index: PhaseItemExtraIndex,
/// Whether the mesh in question is indexed (uses an index buffer in /// Whether the mesh in question is indexed (uses an index buffer in
/// addition to its vertex buffer). /// addition to its vertex buffer).

View File

@ -341,6 +341,7 @@ fn queue_line_gizmos_2d(
sort_key: FloatOrd(f32::INFINITY), sort_key: FloatOrd(f32::INFINITY),
batch_range: 0..1, batch_range: 0..1,
extra_index: PhaseItemExtraIndex::None, extra_index: PhaseItemExtraIndex::None,
extracted_index: usize::MAX,
indexed: false, indexed: false,
}); });
} }
@ -362,6 +363,7 @@ fn queue_line_gizmos_2d(
sort_key: FloatOrd(f32::INFINITY), sort_key: FloatOrd(f32::INFINITY),
batch_range: 0..1, batch_range: 0..1,
extra_index: PhaseItemExtraIndex::None, extra_index: PhaseItemExtraIndex::None,
extracted_index: usize::MAX,
indexed: false, indexed: false,
}); });
} }
@ -421,6 +423,7 @@ fn queue_line_joint_gizmos_2d(
sort_key: FloatOrd(f32::INFINITY), sort_key: FloatOrd(f32::INFINITY),
batch_range: 0..1, batch_range: 0..1,
extra_index: PhaseItemExtraIndex::None, extra_index: PhaseItemExtraIndex::None,
extracted_index: usize::MAX,
indexed: false, indexed: false,
}); });
} }

View File

@ -880,6 +880,7 @@ pub fn queue_material2d_meshes<M: Material2d>(
// Batching is done in batch_and_prepare_render_phase // Batching is done in batch_and_prepare_render_phase
batch_range: 0..1, batch_range: 0..1,
extra_index: PhaseItemExtraIndex::None, extra_index: PhaseItemExtraIndex::None,
extracted_index: usize::MAX,
indexed: mesh.indexed(), indexed: mesh.indexed(),
}); });
} }

View File

@ -19,7 +19,6 @@ use bevy_ecs::{
use bevy_image::{BevyDefault, Image, ImageSampler, TextureAtlasLayout, TextureFormatPixelInfo}; use bevy_image::{BevyDefault, Image, ImageSampler, TextureAtlasLayout, TextureFormatPixelInfo};
use bevy_math::{Affine3A, FloatOrd, Quat, Rect, Vec2, Vec4}; use bevy_math::{Affine3A, FloatOrd, Quat, Rect, Vec2, Vec4};
use bevy_platform_support::collections::HashMap; use bevy_platform_support::collections::HashMap;
use bevy_render::sync_world::MainEntity;
use bevy_render::view::{RenderVisibleEntities, RetainedViewEntity}; use bevy_render::view::{RenderVisibleEntities, RetainedViewEntity};
use bevy_render::{ use bevy_render::{
render_asset::RenderAssets, render_asset::RenderAssets,
@ -32,7 +31,7 @@ use bevy_render::{
*, *,
}, },
renderer::{RenderDevice, RenderQueue}, renderer::{RenderDevice, RenderQueue},
sync_world::{RenderEntity, TemporaryRenderEntity}, sync_world::RenderEntity,
texture::{DefaultImageSampler, FallbackImage, GpuImage}, texture::{DefaultImageSampler, FallbackImage, GpuImage},
view::{ view::{
ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms,
@ -339,13 +338,15 @@ pub struct ExtractedSprite {
pub anchor: Vec2, pub anchor: Vec2,
/// For cases where additional [`ExtractedSprites`] are created during extraction, this stores the /// For cases where additional [`ExtractedSprites`] are created during extraction, this stores the
/// entity that caused that creation for use in determining visibility. /// entity that caused that creation for use in determining visibility.
pub original_entity: Option<Entity>, pub original_entity: Entity,
pub scaling_mode: Option<ScalingMode>, pub scaling_mode: Option<ScalingMode>,
pub render_entity: Entity,
} }
#[derive(Resource, Default)] #[derive(Resource, Default)]
pub struct ExtractedSprites { pub struct ExtractedSprites {
pub sprites: HashMap<(Entity, MainEntity), ExtractedSprite>, //pub sprites: HashMap<(Entity, MainEntity), ExtractedSprite>,
pub sprites: Vec<ExtractedSprite>,
} }
#[derive(Resource, Default)] #[derive(Resource, Default)]
@ -388,19 +389,12 @@ pub fn extract_sprites(
} }
if let Some(slices) = slices { if let Some(slices) = slices {
extracted_sprites.sprites.extend( extracted_sprites.sprites.extend(slices.extract_sprites(
slices &mut commands,
.extract_sprites(transform, original_entity, sprite) transform,
.map(|e| { original_entity,
( sprite,
( ));
commands.spawn(TemporaryRenderEntity).id(),
original_entity.into(),
),
e,
)
}),
);
} else { } else {
let atlas_rect = sprite let atlas_rect = sprite
.texture_atlas .texture_atlas
@ -419,22 +413,20 @@ 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 // 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( extracted_sprites.sprites.push(ExtractedSprite {
(entity, original_entity.into()), render_entity: entity,
ExtractedSprite { color: sprite.color.into(),
color: sprite.color.into(), transform: *transform,
transform: *transform, rect,
rect, // Pass the custom size
// Pass the custom size custom_size: sprite.custom_size,
custom_size: sprite.custom_size, flip_x: sprite.flip_x,
flip_x: sprite.flip_x, flip_y: sprite.flip_y,
flip_y: sprite.flip_y, image_handle_id: sprite.image.id(),
image_handle_id: sprite.image.id(), anchor: sprite.anchor.as_vec(),
anchor: sprite.anchor.as_vec(), original_entity,
original_entity: Some(original_entity), scaling_mode: sprite.image_mode.scale(),
scaling_mode: sprite.image_mode.scale(), });
},
);
} }
} }
} }
@ -561,10 +553,10 @@ pub fn queue_sprites(
.items .items
.reserve(extracted_sprites.sprites.len()); .reserve(extracted_sprites.sprites.len());
for ((entity, main_entity), extracted_sprite) in extracted_sprites.sprites.iter() { for (index, extracted_sprite) in extracted_sprites.sprites.iter().enumerate() {
let index = extracted_sprite.original_entity.unwrap_or(*entity).index(); let view_index = extracted_sprite.original_entity.index();
if !view_entities.contains(index as usize) { if !view_entities.contains(view_index as usize) {
continue; continue;
} }
@ -575,11 +567,15 @@ pub fn queue_sprites(
transparent_phase.add(Transparent2d { transparent_phase.add(Transparent2d {
draw_function: draw_sprite_function, draw_function: draw_sprite_function,
pipeline, pipeline,
entity: (*entity, *main_entity), entity: (
extracted_sprite.render_entity,
extracted_sprite.original_entity.into(),
),
sort_key, sort_key,
// `batch_range` is calculated in `prepare_sprite_image_bind_groups` // `batch_range` is calculated in `prepare_sprite_image_bind_groups`
batch_range: 0..0, batch_range: 0..0,
extra_index: PhaseItemExtraIndex::None, extra_index: PhaseItemExtraIndex::None,
extracted_index: index,
indexed: true, indexed: true,
}); });
} }
@ -664,7 +660,12 @@ pub fn prepare_sprite_image_bind_groups(
// Compatible items share the same entity. // Compatible items share the same entity.
for item_index in 0..transparent_phase.items.len() { for item_index in 0..transparent_phase.items.len() {
let item = &transparent_phase.items[item_index]; 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.extracted_index)
.filter(|extracted_sprite| extracted_sprite.render_entity == item.entity())
else {
// If there is a phase item that is not a sprite, then we must start a new // 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 // batch to draw the other phase item(s) and to respect draw order. This can be
// done by invalidating the batch_image_handle // done by invalidating the batch_image_handle

View File

@ -6,6 +6,7 @@ use bevy_ecs::prelude::*;
use bevy_image::Image; use bevy_image::Image;
use bevy_math::{Rect, Vec2}; use bevy_math::{Rect, Vec2};
use bevy_platform_support::collections::HashSet; use bevy_platform_support::collections::HashSet;
use bevy_render::sync_world::TemporaryRenderEntity;
use bevy_transform::prelude::*; use bevy_transform::prelude::*;
/// Component storing texture slices for tiled or sliced sprite entities /// Component storing texture slices for tiled or sliced sprite entities
@ -24,12 +25,13 @@ impl ComputedTextureSlices {
/// * `sprite` - The sprite component /// * `sprite` - The sprite component
/// * `handle` - The sprite texture handle /// * `handle` - The sprite texture handle
#[must_use] #[must_use]
pub(crate) fn extract_sprites<'a>( pub(crate) fn extract_sprites<'a, 'w, 's>(
&'a self, &'a self,
commands: &'a mut Commands<'w, 's>,
transform: &'a GlobalTransform, transform: &'a GlobalTransform,
original_entity: Entity, original_entity: Entity,
sprite: &'a Sprite, sprite: &'a Sprite,
) -> impl ExactSizeIterator<Item = ExtractedSprite> + 'a { ) -> impl ExactSizeIterator<Item = ExtractedSprite> + 'a + use<'a, 'w, 's> {
let mut flip = Vec2::ONE; let mut flip = Vec2::ONE;
let [mut flip_x, mut flip_y] = [false; 2]; let [mut flip_x, mut flip_y] = [false; 2];
if sprite.flip_x { if sprite.flip_x {
@ -44,7 +46,8 @@ impl ComputedTextureSlices {
let offset = (slice.offset * flip).extend(0.0); let offset = (slice.offset * flip).extend(0.0);
let transform = transform.mul_transform(Transform::from_translation(offset)); let transform = transform.mul_transform(Transform::from_translation(offset));
ExtractedSprite { ExtractedSprite {
original_entity: Some(original_entity), render_entity: commands.spawn(TemporaryRenderEntity).id(),
original_entity,
color: sprite.color.into(), color: sprite.color.into(),
transform, transform,
rect: Some(slice.texture_rect), rect: Some(slice.texture_rect),

View File

@ -204,24 +204,19 @@ pub fn extract_text2d_sprite(
} }
let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap();
extracted_sprites.sprites.insert( extracted_sprites.sprites.push(ExtractedSprite {
( render_entity: commands.spawn(TemporaryRenderEntity).id(),
commands.spawn(TemporaryRenderEntity).id(), transform: transform * GlobalTransform::from_translation(position.extend(0.)),
original_entity.into(), color,
), rect: Some(atlas.textures[atlas_info.location.glyph_index].as_rect()),
ExtractedSprite { custom_size: None,
transform: transform * GlobalTransform::from_translation(position.extend(0.)), image_handle_id: atlas_info.texture.id(),
color, flip_x: false,
rect: Some(atlas.textures[atlas_info.location.glyph_index].as_rect()), flip_y: false,
custom_size: None, anchor: Anchor::Center.as_vec(),
image_handle_id: atlas_info.texture.id(), original_entity,
flip_x: false, scaling_mode: None,
flip_y: false, });
anchor: Anchor::Center.as_vec(),
original_entity: Some(original_entity),
scaling_mode: None,
},
);
} }
} }
} }

View File

@ -412,6 +412,7 @@ pub fn queue_colored_mesh2d(
// This material is not batched // This material is not batched
batch_range: 0..1, batch_range: 0..1,
extra_index: PhaseItemExtraIndex::None, extra_index: PhaseItemExtraIndex::None,
extracted_index: usize::MAX,
indexed: mesh.indexed(), indexed: mesh.indexed(),
}); });
} }