This PR makes Bevy keep entities in bins from frame to frame if they haven't changed. This reduces the time spent in `queue_material_meshes` and related functions to near zero for static geometry. This patch uses the same change tick technique that #17567 uses to detect when meshes have changed in such a way as to require re-binning. In order to quickly find the relevant bin for an entity when that entity has changed, we introduce a new type of cache, the *bin key cache*. This cache stores a mapping from main world entity ID to cached bin key, as well as the tick of the most recent change to the entity. As we iterate through the visible entities in `queue_material_meshes`, we check the cache to see whether the entity needs to be re-binned. If it doesn't, then we mark it as clean in the `valid_cached_entity_bin_keys` bit set. If it does, then we insert it into the correct bin, and then mark the entity as clean. At the end, all entities not marked as clean are removed from the bins. This patch has a dramatic effect on the rendering performance of most benchmarks, as it effectively eliminates `queue_material_meshes` from the profile. Note, however, that it generally simultaneously regresses `batch_and_prepare_binned_render_phase` by a bit (not by enough to outweigh the win, however). I believe that's because, before this patch, `queue_material_meshes` put the bins in the CPU cache for `batch_and_prepare_binned_render_phase` to use, while with this patch, `batch_and_prepare_binned_render_phase` must load the bins into the CPU cache itself. On Caldera, this reduces the time spent in `queue_material_meshes` from 5+ ms to 0.2ms-0.3ms. Note that benchmarking on that scene is very noisy right now because of https://github.com/bevyengine/bevy/issues/17535. 
375 lines
12 KiB
Rust
375 lines
12 KiB
Rust
#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
|
|
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
|
#![forbid(unsafe_code)]
|
|
#![doc(
|
|
html_logo_url = "https://bevyengine.org/assets/icon.png",
|
|
html_favicon_url = "https://bevyengine.org/assets/icon.png"
|
|
)]
|
|
|
|
//! Provides 2D sprite rendering functionality.
|
|
|
|
extern crate alloc;
|
|
|
|
mod mesh2d;
|
|
#[cfg(feature = "bevy_sprite_picking_backend")]
|
|
mod picking_backend;
|
|
mod render;
|
|
mod sprite;
|
|
mod texture_slice;
|
|
|
|
/// The sprite prelude.
|
|
///
|
|
/// This includes the most common types in this crate, re-exported for your convenience.
|
|
pub mod prelude {
|
|
#[doc(hidden)]
|
|
pub use crate::{
|
|
sprite::{Sprite, SpriteImageMode},
|
|
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
|
|
ColorMaterial, MeshMaterial2d, ScalingMode,
|
|
};
|
|
}
|
|
|
|
pub use mesh2d::*;
|
|
#[cfg(feature = "bevy_sprite_picking_backend")]
|
|
pub use picking_backend::*;
|
|
pub use render::*;
|
|
pub use sprite::*;
|
|
pub use texture_slice::*;
|
|
|
|
use bevy_app::prelude::*;
|
|
use bevy_asset::{load_internal_asset, weak_handle, AssetEvents, Assets, Handle};
|
|
use bevy_core_pipeline::core_2d::{AlphaMask2d, Opaque2d, Transparent2d};
|
|
use bevy_ecs::prelude::*;
|
|
use bevy_image::{prelude::*, TextureAtlasPlugin};
|
|
use bevy_render::{
|
|
batching::sort_binned_render_phase,
|
|
mesh::{Mesh, Mesh2d, MeshAabb},
|
|
primitives::Aabb,
|
|
render_phase::AddRenderCommand,
|
|
render_resource::{Shader, SpecializedRenderPipelines},
|
|
view::{NoFrustumCulling, VisibilitySystems},
|
|
ExtractSchedule, Render, RenderApp, RenderSet,
|
|
};
|
|
|
|
/// Adds support for 2D sprite rendering.
|
|
pub struct SpritePlugin {
|
|
/// Whether to add the sprite picking backend to the app.
|
|
#[cfg(feature = "bevy_sprite_picking_backend")]
|
|
pub add_picking: bool,
|
|
}
|
|
|
|
#[expect(
|
|
clippy::allow_attributes,
|
|
reason = "clippy::derivable_impls is not always linted"
|
|
)]
|
|
#[allow(
|
|
clippy::derivable_impls,
|
|
reason = "Known false positive with clippy: <https://github.com/rust-lang/rust-clippy/issues/13160>"
|
|
)]
|
|
impl Default for SpritePlugin {
|
|
fn default() -> Self {
|
|
Self {
|
|
#[cfg(feature = "bevy_sprite_picking_backend")]
|
|
add_picking: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub const SPRITE_SHADER_HANDLE: Handle<Shader> =
|
|
weak_handle!("ed996613-54c0-49bd-81be-1c2d1a0d03c2");
|
|
pub const SPRITE_VIEW_BINDINGS_SHADER_HANDLE: Handle<Shader> =
|
|
weak_handle!("43947210-8df6-459a-8f2a-12f350d174cc");
|
|
|
|
/// System set for sprite rendering.
|
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
|
pub enum SpriteSystem {
|
|
ExtractSprites,
|
|
ComputeSlices,
|
|
}
|
|
|
|
impl Plugin for SpritePlugin {
|
|
fn build(&self, app: &mut App) {
|
|
load_internal_asset!(
|
|
app,
|
|
SPRITE_SHADER_HANDLE,
|
|
"render/sprite.wgsl",
|
|
Shader::from_wgsl
|
|
);
|
|
load_internal_asset!(
|
|
app,
|
|
SPRITE_VIEW_BINDINGS_SHADER_HANDLE,
|
|
"render/sprite_view_bindings.wgsl",
|
|
Shader::from_wgsl
|
|
);
|
|
|
|
if !app.is_plugin_added::<TextureAtlasPlugin>() {
|
|
app.add_plugins(TextureAtlasPlugin);
|
|
}
|
|
|
|
app.register_type::<Sprite>()
|
|
.register_type::<SpriteImageMode>()
|
|
.register_type::<TextureSlicer>()
|
|
.register_type::<Anchor>()
|
|
.register_type::<Mesh2d>()
|
|
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
|
|
.add_systems(
|
|
PostUpdate,
|
|
(
|
|
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
|
|
(
|
|
compute_slices_on_asset_event.before(AssetEvents),
|
|
compute_slices_on_sprite_change,
|
|
)
|
|
.in_set(SpriteSystem::ComputeSlices),
|
|
),
|
|
);
|
|
|
|
#[cfg(feature = "bevy_sprite_picking_backend")]
|
|
if self.add_picking {
|
|
app.add_plugins(SpritePickingPlugin);
|
|
}
|
|
|
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
|
render_app
|
|
.init_resource::<ImageBindGroups>()
|
|
.init_resource::<SpecializedRenderPipelines<SpritePipeline>>()
|
|
.init_resource::<SpriteMeta>()
|
|
.init_resource::<ExtractedSprites>()
|
|
.init_resource::<SpriteAssetEvents>()
|
|
.add_render_command::<Transparent2d, DrawSprite>()
|
|
.add_systems(
|
|
ExtractSchedule,
|
|
(
|
|
extract_sprites.in_set(SpriteSystem::ExtractSprites),
|
|
extract_sprite_events,
|
|
),
|
|
)
|
|
.add_systems(
|
|
Render,
|
|
(
|
|
queue_sprites
|
|
.in_set(RenderSet::Queue)
|
|
.ambiguous_with(queue_material2d_meshes::<ColorMaterial>),
|
|
prepare_sprite_image_bind_groups.in_set(RenderSet::PrepareBindGroups),
|
|
prepare_sprite_view_bind_groups.in_set(RenderSet::PrepareBindGroups),
|
|
sort_binned_render_phase::<Opaque2d>.in_set(RenderSet::PhaseSort),
|
|
sort_binned_render_phase::<AlphaMask2d>.in_set(RenderSet::PhaseSort),
|
|
),
|
|
);
|
|
};
|
|
}
|
|
|
|
fn finish(&self, app: &mut App) {
|
|
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
|
|
render_app
|
|
.init_resource::<SpriteBatches>()
|
|
.init_resource::<SpritePipeline>();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// System calculating and inserting an [`Aabb`] component to entities with either:
|
|
/// - a `Mesh2d` component,
|
|
/// - a `Sprite` and `Handle<Image>` components,
|
|
/// and without a [`NoFrustumCulling`] component.
|
|
///
|
|
/// Used in system set [`VisibilitySystems::CalculateBounds`].
|
|
pub fn calculate_bounds_2d(
|
|
mut commands: Commands,
|
|
meshes: Res<Assets<Mesh>>,
|
|
images: Res<Assets<Image>>,
|
|
atlases: Res<Assets<TextureAtlasLayout>>,
|
|
meshes_without_aabb: Query<(Entity, &Mesh2d), (Without<Aabb>, Without<NoFrustumCulling>)>,
|
|
sprites_to_recalculate_aabb: Query<
|
|
(Entity, &Sprite),
|
|
(
|
|
Or<(Without<Aabb>, Changed<Sprite>)>,
|
|
Without<NoFrustumCulling>,
|
|
),
|
|
>,
|
|
) {
|
|
for (entity, mesh_handle) in &meshes_without_aabb {
|
|
if let Some(mesh) = meshes.get(&mesh_handle.0) {
|
|
if let Some(aabb) = mesh.compute_aabb() {
|
|
commands.entity(entity).try_insert(aabb);
|
|
}
|
|
}
|
|
}
|
|
for (entity, sprite) in &sprites_to_recalculate_aabb {
|
|
if let Some(size) = sprite
|
|
.custom_size
|
|
.or_else(|| sprite.rect.map(|rect| rect.size()))
|
|
.or_else(|| match &sprite.texture_atlas {
|
|
// We default to the texture size for regular sprites
|
|
None => images.get(&sprite.image).map(Image::size_f32),
|
|
// We default to the drawn rect for atlas sprites
|
|
Some(atlas) => atlas
|
|
.texture_rect(&atlases)
|
|
.map(|rect| rect.size().as_vec2()),
|
|
})
|
|
{
|
|
let aabb = Aabb {
|
|
center: (-sprite.anchor.as_vec() * size).extend(0.0).into(),
|
|
half_extents: (0.5 * size).extend(0.0).into(),
|
|
};
|
|
commands.entity(entity).try_insert(aabb);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
|
|
use bevy_math::{Rect, Vec2, Vec3A};
|
|
use bevy_utils::default;
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn calculate_bounds_2d_create_aabb_for_image_sprite_entity() {
|
|
// Setup app
|
|
let mut app = App::new();
|
|
|
|
// Add resources and get handle to image
|
|
let mut image_assets = Assets::<Image>::default();
|
|
let image_handle = image_assets.add(Image::default());
|
|
app.insert_resource(image_assets);
|
|
let mesh_assets = Assets::<Mesh>::default();
|
|
app.insert_resource(mesh_assets);
|
|
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
|
|
app.insert_resource(texture_atlas_assets);
|
|
|
|
// Add system
|
|
app.add_systems(Update, calculate_bounds_2d);
|
|
|
|
// Add entities
|
|
let entity = app.world_mut().spawn(Sprite::from_image(image_handle)).id();
|
|
|
|
// Verify that the entity does not have an AABB
|
|
assert!(!app
|
|
.world()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.contains::<Aabb>());
|
|
|
|
// Run system
|
|
app.update();
|
|
|
|
// Verify the AABB exists
|
|
assert!(app
|
|
.world()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.contains::<Aabb>());
|
|
}
|
|
|
|
#[test]
|
|
fn calculate_bounds_2d_update_aabb_when_sprite_custom_size_changes_to_some() {
|
|
// Setup app
|
|
let mut app = App::new();
|
|
|
|
// Add resources and get handle to image
|
|
let mut image_assets = Assets::<Image>::default();
|
|
let image_handle = image_assets.add(Image::default());
|
|
app.insert_resource(image_assets);
|
|
let mesh_assets = Assets::<Mesh>::default();
|
|
app.insert_resource(mesh_assets);
|
|
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
|
|
app.insert_resource(texture_atlas_assets);
|
|
|
|
// Add system
|
|
app.add_systems(Update, calculate_bounds_2d);
|
|
|
|
// Add entities
|
|
let entity = app
|
|
.world_mut()
|
|
.spawn(Sprite {
|
|
custom_size: Some(Vec2::ZERO),
|
|
image: image_handle,
|
|
..default()
|
|
})
|
|
.id();
|
|
|
|
// Create initial AABB
|
|
app.update();
|
|
|
|
// Get the initial AABB
|
|
let first_aabb = *app
|
|
.world()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.get::<Aabb>()
|
|
.expect("Could not find initial AABB");
|
|
|
|
// Change `custom_size` of sprite
|
|
let mut binding = app
|
|
.world_mut()
|
|
.get_entity_mut(entity)
|
|
.expect("Could not find entity");
|
|
let mut sprite = binding
|
|
.get_mut::<Sprite>()
|
|
.expect("Could not find sprite component of entity");
|
|
sprite.custom_size = Some(Vec2::ONE);
|
|
|
|
// Re-run the `calculate_bounds_2d` system to get the new AABB
|
|
app.update();
|
|
|
|
// Get the re-calculated AABB
|
|
let second_aabb = *app
|
|
.world()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.get::<Aabb>()
|
|
.expect("Could not find second AABB");
|
|
|
|
// Check that the AABBs are not equal
|
|
assert_ne!(first_aabb, second_aabb);
|
|
}
|
|
|
|
#[test]
|
|
fn calculate_bounds_2d_correct_aabb_for_sprite_with_custom_rect() {
|
|
// Setup app
|
|
let mut app = App::new();
|
|
|
|
// Add resources and get handle to image
|
|
let mut image_assets = Assets::<Image>::default();
|
|
let image_handle = image_assets.add(Image::default());
|
|
app.insert_resource(image_assets);
|
|
let mesh_assets = Assets::<Mesh>::default();
|
|
app.insert_resource(mesh_assets);
|
|
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
|
|
app.insert_resource(texture_atlas_assets);
|
|
|
|
// Add system
|
|
app.add_systems(Update, calculate_bounds_2d);
|
|
|
|
// Add entities
|
|
let entity = app
|
|
.world_mut()
|
|
.spawn(Sprite {
|
|
rect: Some(Rect::new(0., 0., 0.5, 1.)),
|
|
anchor: Anchor::TopRight,
|
|
image: image_handle,
|
|
..default()
|
|
})
|
|
.id();
|
|
|
|
// Create AABB
|
|
app.update();
|
|
|
|
// Get the AABB
|
|
let aabb = *app
|
|
.world_mut()
|
|
.get_entity(entity)
|
|
.expect("Could not find entity")
|
|
.get::<Aabb>()
|
|
.expect("Could not find AABB");
|
|
|
|
// Verify that the AABB is at the expected position
|
|
assert_eq!(aabb.center, Vec3A::new(-0.25, -0.5, 0.));
|
|
|
|
// Verify that the AABB has the expected size
|
|
assert_eq!(aabb.half_extents, Vec3A::new(0.25, 0.5, 0.));
|
|
}
|
|
}
|