diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index d584cd9317b1ea..ce9cfa6bd41f63 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -15,6 +15,8 @@ pub use main_pass_2d::*; pub use main_pass_3d::*; pub use main_pass_driver::*; +use std::ops::Range; + use bevy_app::{App, Plugin}; use bevy_core::FloatOrd; use bevy_ecs::prelude::*; @@ -23,8 +25,8 @@ use bevy_render::{ color::Color, render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType}, render_phase::{ - sort_phase_system, CachedPipelinePhaseItem, DrawFunctionId, DrawFunctions, EntityPhaseItem, - PhaseItem, RenderPhase, + batch_phase_system, sort_phase_system, BatchedPhaseItem, CachedPipelinePhaseItem, + DrawFunctionId, DrawFunctions, EntityPhaseItem, PhaseItem, RenderPhase, }, render_resource::*, renderer::RenderDevice, @@ -84,6 +86,11 @@ pub mod clear_graph { #[derive(Default)] pub struct CorePipelinePlugin; +#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] +pub enum CorePipelineRenderSystems { + SortTransparent2d, +} + impl Plugin for CorePipelinePlugin { fn build(&self, app: &mut App) { app.init_resource::(); @@ -97,7 +104,16 @@ impl Plugin for CorePipelinePlugin { .add_system_to_stage(RenderStage::Extract, extract_clear_color) .add_system_to_stage(RenderStage::Extract, extract_core_pipeline_camera_phases) .add_system_to_stage(RenderStage::Prepare, prepare_core_views_system) - .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) + .add_system_to_stage( + RenderStage::PhaseSort, + sort_phase_system:: + .label(CorePipelineRenderSystems::SortTransparent2d), + ) + .add_system_to_stage( + RenderStage::PhaseSort, + batch_phase_system:: + .after(CorePipelineRenderSystems::SortTransparent2d), + ) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::) .add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::); @@ -160,6 +176,8 @@ pub struct Transparent2d { pub entity: Entity, pub pipeline: CachedPipelineId, pub draw_function: DrawFunctionId, + /// Range in the index buffer of this item + pub batch_range: Option>, } impl PhaseItem for Transparent2d { @@ -190,6 +208,16 @@ impl CachedPipelinePhaseItem for Transparent2d { } } +impl BatchedPhaseItem for Transparent2d { + fn batch_range(&self) -> &Option> { + &self.batch_range + } + + fn batch_range_mut(&mut self) -> &mut Option> { + &mut self.batch_range + } +} + pub struct Opaque3d { pub distance: f32, pub pipeline: CachedPipelineId, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index fdd3b9c5852964..ab796439fc0b89 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -8,7 +8,7 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, }; -use bevy_math::Mat4; +use bevy_math::{Mat4, Size}; use bevy_reflect::TypeUuid; use bevy_render::{ mesh::{GpuBufferInfo, Mesh}, @@ -328,6 +328,10 @@ impl FromWorld for MeshPipeline { texture, texture_view, sampler, + size: Size::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ), } }; MeshPipeline { diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index 7c41569a72d81e..6e8679fb4fd786 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -13,7 +13,7 @@ use bevy_ecs::{ }; use bevy_utils::HashMap; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use std::{any::TypeId, fmt::Debug, hash::Hash}; +use std::{any::TypeId, fmt::Debug, hash::Hash, ops::Range}; /// A draw function which is used to draw a specific [`PhaseItem`]. /// @@ -166,6 +166,40 @@ pub trait CachedPipelinePhaseItem: PhaseItem { fn cached_pipeline(&self) -> CachedPipelineId; } +pub trait BatchedPhaseItem: EntityPhaseItem { + /// Range in the index buffer of this item + fn batch_range(&self) -> &Option>; + + /// Range in the index buffer of this item + fn batch_range_mut(&mut self) -> &mut Option>; + + /// Batches another item within this item if they are compatible. + /// Items can be batched together if they have the same entity, and consecutive ranges. + /// If batching is successful, the `other` item should be discarded from the render pass. + #[inline] + fn add_to_batch(&mut self, other: &Self) -> BatchResult { + let self_entity = self.entity(); + if let (Some(self_batch_range), Some(other_batch_range)) = ( + self.batch_range_mut().as_mut(), + other.batch_range().as_ref(), + ) { + if self_entity == other.entity() && self_batch_range.end == other_batch_range.start { + // If the items are compatible, join their range into `self` + self_batch_range.end = other_batch_range.end; + return BatchResult::Success; + } + } + BatchResult::IncompatibleItems + } +} + +pub enum BatchResult { + /// The `other` item was batched into `self` + Success, + /// `self` and `other` cannot be batched together + IncompatibleItems, +} + impl RenderCommand

for E { type Param = E::Param; diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 594b9bbfe084e5..49c722689de38f 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -31,9 +31,44 @@ impl RenderPhase { } } +impl RenderPhase { + /// Batches the compatible [`BatchedPhaseItem`]s of this render phase + pub fn batch(&mut self) { + // TODO: this could be done in-place + let mut items = std::mem::take(&mut self.items); + let mut items = items.drain(..); + + self.items.reserve(items.len()); + + // Start the first batch from the first item + if let Some(mut current_batch) = items.next() { + // Batch following items until we find an incompatible item + for next_item in items { + if matches!( + current_batch.add_to_batch(&next_item), + BatchResult::IncompatibleItems + ) { + // Store the completed batch, and start a new one from the incompatible item + self.items.push(current_batch); + current_batch = next_item; + } + } + // Store the last batch + self.items.push(current_batch); + } + } +} + /// This system sorts all [`RenderPhases`](RenderPhase) for the [`PhaseItem`] type. pub fn sort_phase_system(mut render_phases: Query<&mut RenderPhase>) { for mut phase in render_phases.iter_mut() { phase.sort(); } } + +/// This batches the [`PhaseItem`]s of a [`RenderPhases`](RenderPhase). +pub fn batch_phase_system(mut render_phases: Query<&mut RenderPhase>) { + for mut phase in render_phases.iter_mut() { + phase.batch(); + } +} diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_render/src/texture/image.rs index 4d8b1f47a1c3fe..29063905ae82c2 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_render/src/texture/image.rs @@ -7,6 +7,7 @@ use crate::{ }; use bevy_asset::HandleUntyped; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_math::Size; use bevy_reflect::TypeUuid; use thiserror::Error; use wgpu::{ @@ -373,12 +374,13 @@ impl TextureFormatPixelInfo for TextureFormat { } /// The GPU-representation of an [`Image`]. -/// Consists of the [`Texture`], its [`TextureView`] and the corresponding [`Sampler`]. +/// Consists of the [`Texture`], its [`TextureView`] and the corresponding [`Sampler`], and the texture's [`Size`]. #[derive(Debug, Clone)] pub struct GpuImage { pub texture: Texture, pub texture_view: TextureView, pub sampler: Sampler, + pub size: Size, } impl RenderAsset for Image { @@ -426,10 +428,15 @@ impl RenderAsset for Image { ); let texture_view = texture.create_view(&TextureViewDescriptor::default()); + let size = Size::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ); Ok(GpuImage { texture, texture_view, sampler, + size, }) } } diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index d38e45231b337d..61da101d02d3e4 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -28,7 +28,7 @@ impl Default for Visibility { } /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering -#[derive(Component, Clone, Reflect)] +#[derive(Component, Clone, Reflect, Debug)] #[reflect(Component)] pub struct ComputedVisibility { pub is_visible: bool, diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 72935e48ae8b1a..bad48702089857 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -48,7 +48,6 @@ pub const SPRITE_SHADER_HANDLE: HandleUntyped = #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemLabel)] pub enum SpriteSystem { ExtractSprites, - PrepareSprites, } impl Plugin for SpritePlugin { @@ -66,7 +65,7 @@ impl Plugin for SpritePlugin { .init_resource::() .init_resource::>() .init_resource::() - .init_resource::() + .init_resource::() .init_resource::() .add_render_command::() .add_system_to_stage( @@ -74,10 +73,6 @@ impl Plugin for SpritePlugin { render::extract_sprites.label(SpriteSystem::ExtractSprites), ) .add_system_to_stage(RenderStage::Extract, render::extract_sprite_events) - .add_system_to_stage( - RenderStage::Prepare, - render::prepare_sprites.label(SpriteSystem::PrepareSprites), - ) .add_system_to_stage(RenderStage::Queue, queue_sprites); } } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 5afca8a0d8a0cc..0e3ceaae55cc08 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -24,7 +24,7 @@ use bevy_render::{ SpecializedPipeline, SpecializedPipelines, }, renderer::RenderDevice, - view::{ComputedVisibility, ExtractedView, Msaa, Visibility, VisibleEntities}, + view::{ComputedVisibility, Msaa, Visibility, VisibleEntities}, RenderApp, RenderStage, }; use bevy_transform::components::{GlobalTransform, Transform}; @@ -265,20 +265,17 @@ pub fn queue_material2d_meshes( render_meshes: Res>, render_materials: Res>, material2d_meshes: Query<(&Handle, &Mesh2dHandle, &Mesh2dUniform)>, - mut views: Query<( - &ExtractedView, - &VisibleEntities, - &mut RenderPhase, - )>, + mut views: Query<(&VisibleEntities, &mut RenderPhase)>, ) { - for (view, visible_entities, mut transparent_phase) in views.iter_mut() { + if material2d_meshes.is_empty() { + return; + } + for (visible_entities, mut transparent_phase) in views.iter_mut() { let draw_transparent_pbr = transparent_draw_functions .read() .get_id::>() .unwrap(); - let inverse_view_matrix = view.transform.compute_matrix().inverse(); - let inverse_view_row_2 = inverse_view_matrix.row(2); let mesh_key = Mesh2dPipelineKey::from_msaa_samples(msaa.samples); for visible_entity in &visible_entities.entities { @@ -302,9 +299,7 @@ pub fn queue_material2d_meshes( (mesh2d_key, specialized_key), ); - // NOTE: row 2 of the inverse view matrix dotted with column 3 of the model matrix - // gives the z component of translation of the mesh in view space - let mesh_z = inverse_view_row_2.dot(mesh2d_uniform.transform.col(3)); + let mesh_z = mesh2d_uniform.transform.w_axis.z; transparent_phase.add(Transparent2d { entity: *visible_entity, draw_function: draw_transparent_pbr, @@ -314,6 +309,8 @@ pub fn queue_material2d_meshes( // -z in front of the camera, the largest distance is -far with values increasing toward the // camera. As such we can just use mesh_z as the distance sort_key: FloatOrd(mesh_z), + // This material is not batched + batch_range: None, }); } } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 8bcb0e512bb341..8f432320f94385 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -4,7 +4,7 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, }; -use bevy_math::Mat4; +use bevy_math::{Mat4, Size}; use bevy_reflect::TypeUuid; use bevy_render::{ mesh::{GpuBufferInfo, Mesh}, @@ -15,12 +15,10 @@ use bevy_render::{ renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, GpuImage, Image, TextureFormatPixelInfo}, view::{ComputedVisibility, ExtractedView, ViewUniform, ViewUniformOffset, ViewUniforms}, - RenderApp, RenderStage, RenderWorld, + RenderApp, RenderStage, }; use bevy_transform::components::GlobalTransform; -use crate::{Extracted2dItem, Extracted2dItems, SpriteSystem}; - #[derive(Default, Clone, Component)] pub struct Mesh2dHandle(pub Handle); @@ -63,10 +61,7 @@ impl Plugin for Mesh2dRenderPlugin { app.sub_app_mut(RenderApp) .init_resource::() .init_resource::>() - .add_system_to_stage( - RenderStage::Extract, - extract_mesh2d.after(SpriteSystem::ExtractSprites), - ) + .add_system_to_stage(RenderStage::Extract, extract_mesh2d) .add_system_to_stage(RenderStage::Queue, queue_mesh2d_bind_group) .add_system_to_stage(RenderStage::Queue, queue_mesh2d_view_bind_groups); } @@ -90,19 +85,16 @@ bitflags::bitflags! { } pub fn extract_mesh2d( - mut render_world: ResMut, mut commands: Commands, mut previous_len: Local, query: Query<(Entity, &ComputedVisibility, &GlobalTransform, &Mesh2dHandle)>, ) { - let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); let mut values = Vec::with_capacity(*previous_len); for (entity, computed_visibility, transform, handle) in query.iter() { if !computed_visibility.is_visible { continue; } let transform = transform.compute_matrix(); - let z = transform.col(3).z; values.push(( entity, ( @@ -114,9 +106,6 @@ pub fn extract_mesh2d( }, ), )); - // Break sprite batches - // FIXME: this doesn't account for camera position - extracted_sprites.items.push(Extracted2dItem::Other { z }); } *previous_len = values.len(); commands.insert_or_spawn_batch(values); @@ -202,6 +191,10 @@ impl FromWorld for Mesh2dPipeline { texture, texture_view, sampler, + size: Size::new( + image.texture_descriptor.size.width as f32, + image.texture_descriptor.size.height as f32, + ), } }; Mesh2dPipeline { diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index f91beb66011de1..7bb9d0ef8b1212 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,28 +1,29 @@ -use std::{cmp::Ordering, ops::Range}; +use std::cmp::Ordering; use crate::{ texture_atlas::{TextureAtlas, TextureAtlasSprite}, Rect, Sprite, SPRITE_SHADER_HANDLE, }; -use bevy_asset::{AssetEvent, Assets, Handle}; +use bevy_asset::{AssetEvent, Assets, Handle, HandleId}; use bevy_core::FloatOrd; use bevy_core_pipeline::Transparent2d; use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, }; -use bevy_math::{const_vec3, Mat4, Vec2, Vec3, Vec4, Vec4Swizzles}; +use bevy_math::{const_vec2, Vec2}; +use bevy_reflect::Uuid; use bevy_render::{ color::Color, render_asset::RenderAssets, render_phase::{ - DrawFunctions, EntityRenderCommand, RenderCommandResult, RenderPhase, SetItemPipeline, - TrackedRenderPass, + BatchedPhaseItem, DrawFunctions, EntityRenderCommand, RenderCommand, RenderCommandResult, + RenderPhase, SetItemPipeline, TrackedRenderPass, }, render_resource::{std140::AsStd140, *}, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, Image}, - view::{ComputedVisibility, ExtractedView, Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, + view::{ComputedVisibility, Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, RenderWorld, }; use bevy_transform::components::GlobalTransform; @@ -176,26 +177,24 @@ impl SpecializedPipeline for SpritePipeline { } } +#[derive(Component, Clone, Copy)] pub struct ExtractedSprite { - pub transform: Mat4, + pub transform: GlobalTransform, pub color: Color, - pub rect: Rect, - pub handle: Handle, - pub atlas_size: Option, + /// Select an area of the texture + pub rect: Option, + /// Change the on-screen size of the sprite + pub custom_size: Option, + /// Handle to the `Image` of this sprite + /// PERF: storing a `HandleId` instead of `Handle` enables some optimizations (`ExtractedSprite` becomes `Copy` and doesn't need to be dropped) + pub image_handle_id: HandleId, pub flip_x: bool, pub flip_y: bool, } -pub enum Extracted2dItem { - // Sprites have their full data stored here so they can be batched - Sprite(ExtractedSprite), - // Other 2d items should have their z coordinate stored here to break sprite batches - Other { z: f32 }, -} - #[derive(Default)] -pub struct Extracted2dItems { - pub items: Vec, +pub struct ExtractedSprites { + pub sprites: Vec, } #[derive(Default)] @@ -231,7 +230,6 @@ pub fn extract_sprite_events( pub fn extract_sprites( mut render_world: ResMut, - images: Res>, texture_atlases: Res>, sprite_query: Query<( &ComputedVisibility, @@ -246,52 +244,42 @@ pub fn extract_sprites( &Handle, )>, ) { - let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); - extracted_sprites.items.clear(); + let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); + extracted_sprites.sprites.clear(); for (computed_visibility, sprite, transform, handle) in sprite_query.iter() { if !computed_visibility.is_visible { continue; } - if let Some(image) = images.get(handle) { - let size = image.texture_descriptor.size; - - extracted_sprites - .items - .push(Extracted2dItem::Sprite(ExtractedSprite { - atlas_size: None, - color: sprite.color, - transform: transform.compute_matrix(), - rect: Rect { - min: Vec2::ZERO, - max: sprite - .custom_size - .unwrap_or_else(|| Vec2::new(size.width as f32, size.height as f32)), - }, - flip_x: sprite.flip_x, - flip_y: sprite.flip_y, - handle: handle.clone_weak(), - })); - }; + // 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.push(ExtractedSprite { + color: sprite.color, + transform: *transform, + // Use the full texture + rect: None, + // Pass the custom size + custom_size: sprite.custom_size, + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + image_handle_id: handle.id, + }); } for (computed_visibility, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { if !computed_visibility.is_visible { continue; } if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) { - if images.contains(&texture_atlas.texture) { - let rect = texture_atlas.textures[atlas_sprite.index as usize]; - extracted_sprites - .items - .push(Extracted2dItem::Sprite(ExtractedSprite { - atlas_size: Some(texture_atlas.size), - color: atlas_sprite.color, - transform: transform.compute_matrix(), - rect, - flip_x: atlas_sprite.flip_x, - flip_y: atlas_sprite.flip_y, - handle: texture_atlas.texture.clone_weak(), - })); - } + let rect = Some(texture_atlas.textures[atlas_sprite.index as usize]); + extracted_sprites.sprites.push(ExtractedSprite { + color: atlas_sprite.color, + transform: *transform, + // Select the area in the texture atlas + rect, + // Pass the custom size + custom_size: atlas_sprite.custom_size, + flip_x: atlas_sprite.flip_x, + flip_y: atlas_sprite.flip_y, + image_handle_id: texture_atlas.texture.id, + }); } } } @@ -327,212 +315,28 @@ impl Default for SpriteMeta { } } -const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ - const_vec3!([-0.5, -0.5, 0.0]), - const_vec3!([0.5, -0.5, 0.0]), - const_vec3!([0.5, 0.5, 0.0]), - const_vec3!([-0.5, 0.5, 0.0]), +const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; + +const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + const_vec2!([-0.5, -0.5]), + const_vec2!([0.5, -0.5]), + const_vec2!([0.5, 0.5]), + const_vec2!([-0.5, 0.5]), ]; -const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; +const QUAD_UVS: [Vec2; 4] = [ + const_vec2!([0., 1.]), + const_vec2!([1., 1.]), + const_vec2!([1., 0.]), + const_vec2!([0., 0.]), +]; -#[derive(Component)] +#[derive(Component, Eq, PartialEq, Copy, Clone)] pub struct SpriteBatch { - range: Range, - handle: Handle, - z: f32, + image_handle_id: HandleId, colored: bool, } -pub fn prepare_sprites( - mut commands: Commands, - render_device: Res, - render_queue: Res, - mut sprite_meta: ResMut, - mut extracted_sprites: ResMut, -) { - sprite_meta.vertices.clear(); - sprite_meta.colored_vertices.clear(); - - // `Extracted2dItem::Sprite`s are sorted first by z and then by handle. this ensures that, when possible, batches span multiple z layers - // batches won't span z-layers if there is another batch or an `Extracted2dItem::Other` between them - extracted_sprites.items.sort_by(|a, b| match (a, b) { - (Extracted2dItem::Sprite(a), Extracted2dItem::Sprite(b)) => { - match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(b.transform.w_axis[2])) { - Ordering::Equal => a.handle.cmp(&b.handle), - other => other, - } - } - // Group sprites of same depth before other items of same depth to increase batching potential - (Extracted2dItem::Sprite(a), Extracted2dItem::Other { z: b }) => { - match FloatOrd(a.transform.w_axis[2]).cmp(&FloatOrd(*b)) { - Ordering::Equal => Ordering::Less, - other => other, - } - } - (Extracted2dItem::Other { z: b }, Extracted2dItem::Sprite(a)) => { - match FloatOrd(*b).cmp(&FloatOrd(a.transform.w_axis[2])) { - Ordering::Equal => Ordering::Greater, - other => other, - } - } - (Extracted2dItem::Other { z: a }, Extracted2dItem::Other { z: b }) => { - FloatOrd(*a).cmp(&FloatOrd(*b)) - } - }); - - let mut start = 0; - let mut end = 0; - let mut colored_start = 0; - let mut colored_end = 0; - let mut current_batch_handle: Option> = None; - let mut current_batch_colored = false; - let mut last_z = 0.0; - let mut break_batch = false; - for extracted_item in extracted_sprites.items.iter() { - match extracted_item { - Extracted2dItem::Sprite(extracted_sprite) => { - let colored = extracted_sprite.color != Color::WHITE; - if let Some(current_batch_handle) = ¤t_batch_handle { - if *current_batch_handle != extracted_sprite.handle - || current_batch_colored != colored - || break_batch - { - break_batch = false; - if current_batch_colored { - commands.spawn_bundle((SpriteBatch { - range: colored_start..colored_end, - handle: current_batch_handle.clone_weak(), - z: last_z, - colored: true, - },)); - colored_start = colored_end; - } else { - commands.spawn_bundle((SpriteBatch { - range: start..end, - handle: current_batch_handle.clone_weak(), - z: last_z, - colored: false, - },)); - start = end; - } - } - } - current_batch_handle = Some(extracted_sprite.handle.clone_weak()); - current_batch_colored = colored; - let sprite_rect = extracted_sprite.rect; - - // Specify the corners of the sprite - let mut bottom_left = Vec2::new(sprite_rect.min.x, sprite_rect.max.y); - let mut top_left = sprite_rect.min; - let mut top_right = Vec2::new(sprite_rect.max.x, sprite_rect.min.y); - let mut bottom_right = sprite_rect.max; - - if extracted_sprite.flip_x { - bottom_left.x = sprite_rect.max.x; - top_left.x = sprite_rect.max.x; - bottom_right.x = sprite_rect.min.x; - top_right.x = sprite_rect.min.x; - } - - if extracted_sprite.flip_y { - bottom_left.y = sprite_rect.min.y; - bottom_right.y = sprite_rect.min.y; - top_left.y = sprite_rect.max.y; - top_right.y = sprite_rect.max.y; - } - - let atlas_extent = extracted_sprite.atlas_size.unwrap_or(sprite_rect.max); - bottom_left /= atlas_extent; - bottom_right /= atlas_extent; - top_left /= atlas_extent; - top_right /= atlas_extent; - - let uvs: [[f32; 2]; 4] = [ - bottom_left.into(), - bottom_right.into(), - top_right.into(), - top_left.into(), - ]; - - let rect_size = extracted_sprite.rect.size().extend(1.0); - if current_batch_colored { - let color = extracted_sprite.color.as_linear_rgba_f32(); - // encode color as a single u32 to save space - let color = (color[0] * 255.0) as u32 - | ((color[1] * 255.0) as u32) << 8 - | ((color[2] * 255.0) as u32) << 16 - | ((color[3] * 255.0) as u32) << 24; - for index in QUAD_INDICES.iter() { - let mut final_position = QUAD_VERTEX_POSITIONS[*index] * rect_size; - final_position = - (extracted_sprite.transform * final_position.extend(1.0)).xyz(); - sprite_meta.colored_vertices.push(ColoredSpriteVertex { - position: final_position.into(), - uv: uvs[*index], - color, - }); - } - } else { - for index in QUAD_INDICES.iter() { - let mut final_position = QUAD_VERTEX_POSITIONS[*index] * rect_size; - final_position = - (extracted_sprite.transform * final_position.extend(1.0)).xyz(); - sprite_meta.vertices.push(SpriteVertex { - position: final_position.into(), - uv: uvs[*index], - }); - } - } - - last_z = extracted_sprite.transform.w_axis[2]; - if current_batch_colored { - colored_end += QUAD_INDICES.len() as u32; - } else { - end += QUAD_INDICES.len() as u32; - } - } - Extracted2dItem::Other { z: _ } => { - if current_batch_colored { - if colored_start != colored_end { - break_batch = true; - } - } else if start != end { - break_batch = true; - } - } - }; - } - - // if start != end, there is one last batch to process - if start != end { - if let Some(current_batch_handle) = current_batch_handle { - commands.spawn_bundle((SpriteBatch { - range: start..end, - handle: current_batch_handle, - colored: false, - z: last_z, - },)); - } - } else if colored_start != colored_end { - if let Some(current_batch_handle) = current_batch_handle { - commands.spawn_bundle((SpriteBatch { - range: colored_start..colored_end, - handle: current_batch_handle, - colored: true, - z: last_z, - },)); - } - } - - sprite_meta - .vertices - .write_buffer(&render_device, &render_queue); - sprite_meta - .colored_vertices - .write_buffer(&render_device, &render_queue); -} - #[derive(Default)] pub struct ImageBindGroups { values: HashMap, BindGroup>, @@ -540,8 +344,10 @@ pub struct ImageBindGroups { #[allow(clippy::too_many_arguments)] pub fn queue_sprites( + mut commands: Commands, draw_functions: Res>, render_device: Res, + render_queue: Res, mut sprite_meta: ResMut, view_uniforms: Res, sprite_pipeline: Res, @@ -550,8 +356,8 @@ pub fn queue_sprites( mut image_bind_groups: ResMut, gpu_images: Res>, msaa: Res, - sprite_batches: Query<(Entity, &SpriteBatch)>, - mut views: Query<(&ExtractedView, &mut RenderPhase)>, + mut extracted_sprites: ResMut, + mut views: Query<&mut RenderPhase>, events: Res, ) { // If an image has changed, the GpuImage has (probably) changed @@ -564,6 +370,12 @@ pub fn queue_sprites( } if let Some(view_binding) = view_uniforms.uniforms.binding() { + let sprite_meta = &mut sprite_meta; + + // Clear the vertex buffers + sprite_meta.vertices.clear(); + sprite_meta.colored_vertices.clear(); + sprite_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor { entries: &[BindGroupEntry { binding: 0, @@ -572,6 +384,7 @@ pub fn queue_sprites( label: Some("sprite_view_bind_group"), layout: &sprite_pipeline.view_layout, })); + let draw_sprite_function = draw_functions.read().get_id::().unwrap(); let key = SpritePipelineKey::from_msaa_samples(msaa.samples); let pipeline = pipelines.specialize(&mut pipeline_cache, &sprite_pipeline, key); @@ -580,45 +393,170 @@ pub fn queue_sprites( &sprite_pipeline, key | SpritePipelineKey::COLORED, ); - for (view, mut transparent_phase) in views.iter_mut() { - let inverse_view_matrix = view.transform.compute_matrix().inverse(); - let inverse_view_row_2 = inverse_view_matrix.row(2); - for (entity, batch) in sprite_batches.iter() { - // NOTE: row 2 of the inverse view matrix dotted with column 3 of the model matrix - // gives the z component of translation of the mesh in view space - let batch_z = inverse_view_row_2.dot(Vec4::new(0., 0., batch.z, 1.)); - image_bind_groups - .values - .entry(batch.handle.clone_weak()) - .or_insert_with(|| { - let gpu_image = gpu_images.get(&batch.handle).unwrap(); - render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(&gpu_image.texture_view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&gpu_image.sampler), - }, - ], - label: Some("sprite_material_bind_group"), - layout: &sprite_pipeline.material_layout, - }) - }); - transparent_phase.add(Transparent2d { - draw_function: draw_sprite_function, - pipeline: if batch.colored { - colored_pipeline + + // Vertex buffer indices + let mut index = 0; + let mut colored_index = 0; + + // FIXME: VisibleEntities is ignored + for mut transparent_phase in views.iter_mut() { + let extracted_sprites = &mut extracted_sprites.sprites; + let image_bind_groups = &mut *image_bind_groups; + + transparent_phase.items.reserve(extracted_sprites.len()); + + // Sort sprites by z for correct transparency and then by handle to improve batching + extracted_sprites.sort_unstable_by(|a, b| { + match FloatOrd(a.transform.translation.z).cmp(&FloatOrd(b.transform.translation.z)) + { + Ordering::Equal => a.image_handle_id.cmp(&b.image_handle_id), + other => other, + } + }); + + // Impossible starting values that will be replaced on the first iteration + let mut current_batch = SpriteBatch { + image_handle_id: HandleId::Id(Uuid::nil(), u64::MAX), + colored: false, + }; + let mut current_batch_entity = Entity::new(u32::MAX); + let mut current_image_size = Vec2::ZERO; + // Add a phase item for each sprite, and detect when succesive items can be batched. + // Spawn an entity with a `SpriteBatch` component for each possible batch. + // Compatible items share the same entity. + // Batches are merged later (in `batch_phase_system()`), so that they can be interrupted + // by any other phase item (and they can interrupt other items from batching). + for extracted_sprite in extracted_sprites.iter() { + let new_batch = SpriteBatch { + image_handle_id: extracted_sprite.image_handle_id, + colored: extracted_sprite.color != Color::WHITE, + }; + if new_batch != current_batch { + // Set-up a new possible batch + if let Some(gpu_image) = + gpu_images.get(&Handle::weak(new_batch.image_handle_id)) + { + current_batch = new_batch; + current_image_size = Vec2::new(gpu_image.size.width, gpu_image.size.height); + current_batch_entity = commands.spawn_bundle((current_batch,)).id(); + + image_bind_groups + .values + .entry(Handle::weak(current_batch.image_handle_id)) + .or_insert_with(|| { + render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView( + &gpu_image.texture_view, + ), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler(&gpu_image.sampler), + }, + ], + label: Some("sprite_material_bind_group"), + layout: &sprite_pipeline.material_layout, + }) + }); } else { - pipeline - }, - entity, - sort_key: FloatOrd(batch_z), + // Skip this item if the texture is not ready + continue; + } + } + + // Calculate vertex data for this item + + let mut uvs = QUAD_UVS; + if extracted_sprite.flip_x { + uvs = [uvs[1], uvs[0], uvs[3], uvs[2]]; + } + if extracted_sprite.flip_y { + uvs = [uvs[3], uvs[2], uvs[1], uvs[0]]; + } + + // By default, the size of the quad is the size of the texture + let mut quad_size = current_image_size; + + // If a rect is specified, adjust UVs and the size of the quad + if let Some(rect) = extracted_sprite.rect { + let rect_size = rect.size(); + for uv in &mut uvs { + *uv = (rect.min + *uv * rect_size) / current_image_size; + } + quad_size = rect_size; + } + + // Override the size if a custom one is specified + if let Some(custom_size) = extracted_sprite.custom_size { + quad_size = custom_size; + } + + // Apply size and global transform + let positions = QUAD_VERTEX_POSITIONS.map(|quad_pos| { + extracted_sprite + .transform + .mul_vec3((quad_pos * quad_size).extend(0.)).into() }); + + // These items will be sorted by depth with other phase items + let sort_key = FloatOrd(extracted_sprite.transform.translation.z); + + // Store the vertex data and add the item to the render phase + if current_batch.colored { + let color = extracted_sprite.color.as_linear_rgba_f32(); + // encode color as a single u32 to save space + let color = (color[0] * 255.0) as u32 + | ((color[1] * 255.0) as u32) << 8 + | ((color[2] * 255.0) as u32) << 16 + | ((color[3] * 255.0) as u32) << 24; + for i in QUAD_INDICES.iter() { + sprite_meta.colored_vertices.push(ColoredSpriteVertex { + position: positions[*i], + uv: uvs[*i].into(), + color, + }); + } + let item_start = colored_index; + colored_index += QUAD_INDICES.len() as u32; + let item_end = colored_index; + + transparent_phase.add(Transparent2d { + draw_function: draw_sprite_function, + pipeline: colored_pipeline, + entity: current_batch_entity, + sort_key, + batch_range: Some(item_start..item_end), + }); + } else { + for i in QUAD_INDICES.iter() { + sprite_meta.vertices.push(SpriteVertex { + position: positions[*i], + uv: uvs[*i].into(), + }); + } + let item_start = index; + index += QUAD_INDICES.len() as u32; + let item_end = index; + + transparent_phase.add(Transparent2d { + draw_function: draw_sprite_function, + pipeline, + entity: current_batch_entity, + sort_key, + batch_range: Some(item_start..item_end), + }); + } } } + sprite_meta + .vertices + .write_buffer(&render_device, &render_queue); + sprite_meta + .colored_vertices + .write_buffer(&render_device, &render_queue); } } @@ -663,30 +601,34 @@ impl EntityRenderCommand for SetSpriteTextureBindGroup { pass.set_bind_group( 1, - image_bind_groups.values.get(&sprite_batch.handle).unwrap(), + image_bind_groups + .values + .get(&Handle::weak(sprite_batch.image_handle_id)) + .unwrap(), &[], ); RenderCommandResult::Success } } + pub struct DrawSpriteBatch; -impl EntityRenderCommand for DrawSpriteBatch { +impl RenderCommand

for DrawSpriteBatch { type Param = (SRes, SQuery>); fn render<'w>( _view: Entity, - item: Entity, + item: &P, (sprite_meta, query_batch): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { - let sprite_batch = query_batch.get(item).unwrap(); + let sprite_batch = query_batch.get(item.entity()).unwrap(); let sprite_meta = sprite_meta.into_inner(); if sprite_batch.colored { pass.set_vertex_buffer(0, sprite_meta.colored_vertices.buffer().unwrap().slice(..)); } else { pass.set_vertex_buffer(0, sprite_meta.vertices.buffer().unwrap().slice(..)); } - pass.draw(sprite_batch.range.clone(), 0..1); + pass.draw(item.batch_range().as_ref().unwrap().clone(), 0..1); RenderCommandResult::Success } } diff --git a/crates/bevy_sprite/src/texture_atlas.rs b/crates/bevy_sprite/src/texture_atlas.rs index 84d2e3872a363a..49d366fa64ff12 100644 --- a/crates/bevy_sprite/src/texture_atlas.rs +++ b/crates/bevy_sprite/src/texture_atlas.rs @@ -28,6 +28,9 @@ pub struct TextureAtlasSprite { pub index: usize, pub flip_x: bool, pub flip_y: bool, + /// An optional custom size for the sprite that will be used when rendering, instead of the size + /// of the sprite's image in the atlas + pub custom_size: Option, } impl Default for TextureAtlasSprite { @@ -37,6 +40,7 @@ impl Default for TextureAtlasSprite { color: Color::WHITE, flip_x: false, flip_y: false, + custom_size: None, } } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index cdfa38e590790b..9e387c52a1c05b 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -5,9 +5,13 @@ use bevy_ecs::{ query::{Changed, QueryState, With}, system::{Local, Query, QuerySet, Res, ResMut}, }; -use bevy_math::{Mat4, Size, Vec3}; -use bevy_render::{texture::Image, RenderWorld}; -use bevy_sprite::{Extracted2dItem, Extracted2dItems, ExtractedSprite, TextureAtlas}; +use bevy_math::{Size, Vec3}; +use bevy_render::{ + texture::Image, + view::{ComputedVisibility, Visibility}, + RenderWorld, +}; +use bevy_sprite::{ExtractedSprite, ExtractedSprites, TextureAtlas}; use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_window::Windows; @@ -24,6 +28,8 @@ pub struct Text2dBundle { pub transform: Transform, pub global_transform: GlobalTransform, pub text_2d_size: Text2dSize, + pub visibility: Visibility, + pub computed_visibility: ComputedVisibility, } impl Default for Text2dBundle { @@ -35,6 +41,8 @@ impl Default for Text2dBundle { text_2d_size: Text2dSize { size: Size::default(), }, + visibility: Default::default(), + computed_visibility: Default::default(), } } } @@ -44,16 +52,26 @@ pub fn extract_text2d_sprite( texture_atlases: Res>, text_pipeline: Res, windows: Res, - text2d_query: Query<(Entity, &Text, &GlobalTransform, &Text2dSize)>, + text2d_query: Query<( + Entity, + &ComputedVisibility, + &Text, + &GlobalTransform, + &Text2dSize, + )>, ) { - let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); + let mut extracted_sprites = render_world.get_resource_mut::().unwrap(); + let scale_factor = if let Some(window) = windows.get_primary() { window.scale_factor() as f32 } else { 1. }; - for (entity, text, transform, calculated_size) in text2d_query.iter() { + for (entity, computed_visibility, text, transform, calculated_size) in text2d_query.iter() { + if !computed_visibility.is_visible { + continue; + } let (width, height) = (calculated_size.size.width, calculated_size.size.height); if let Some(text_layout) = text_pipeline.get_glyphs(&entity) { @@ -68,6 +86,9 @@ pub fn extract_text2d_sprite( HorizontalAlign::Right => Vec3::new(-width, 0.0, 0.0), }; + let mut text_transform = *transform; + text_transform.scale /= scale_factor; + for text_glyph in text_glyphs { let color = text.sections[text_glyph.section_index] .style @@ -78,27 +99,23 @@ pub fn extract_text2d_sprite( .unwrap(); let handle = atlas.texture.clone_weak(); let index = text_glyph.atlas_info.glyph_index as usize; - let rect = atlas.textures[index]; - let atlas_size = Some(atlas.size); - - let transform = - Mat4::from_rotation_translation(transform.rotation, transform.translation) - * Mat4::from_scale(transform.scale / scale_factor) - * Mat4::from_translation( - alignment_offset * scale_factor + text_glyph.position.extend(0.), - ); - - extracted_sprites - .items - .push(Extracted2dItem::Sprite(ExtractedSprite { - transform, - color, - rect, - handle, - atlas_size, - flip_x: false, - flip_y: false, - })); + let rect = Some(atlas.textures[index]); + + let glyph_transform = Transform::from_translation( + alignment_offset * scale_factor + text_glyph.position.extend(0.), + ); + + let transform = text_transform.mul_transform(glyph_transform); + + extracted_sprites.sprites.push(ExtractedSprite { + transform, + color, + rect, + custom_size: None, + image_handle_id: handle.id, + flip_x: false, + flip_y: false, + }); } } }