Use BinnedRenderPhase for Opaque2d (#13091)

Based on top of #12982  and #13069 

# Objective

- Opaque2d was implemented with SortedRenderPhase but BinnedRenderPhase
should be much faster

## Solution

- Implement BinnedRenderPhase for Opaque2d

## Notes

While testing this PR, before the change I had ~14 fps in bevymark with
100k entities. After this change I get ~71 fps, compared to using
sprites where I only get ~63 fps. This means that after this PR mesh2d
with opaque meshes will be faster than the sprite path. This is not a 1
to 1 comparison since sprites do alpha blending.
This commit is contained in:
IceSentry 2024-08-12 11:38:24 -04:00 committed by GitHub
parent 7f658cabf7
commit 9d6a4fbc85
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 167 additions and 53 deletions

View file

@ -4,7 +4,7 @@ use bevy_render::{
camera::ExtractedCamera, camera::ExtractedCamera,
diagnostic::RecordDiagnostics, diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode}, render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewSortedRenderPhases}, render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp}, render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp},
renderer::RenderContext, renderer::RenderContext,
view::{ViewDepthTexture, ViewTarget}, view::{ViewDepthTexture, ViewTarget},
@ -13,7 +13,7 @@ use bevy_utils::tracing::error;
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
use bevy_utils::tracing::info_span; use bevy_utils::tracing::info_span;
/// A [`bevy_render::render_graph::Node`] that runs the [`Opaque2d`] [`ViewSortedRenderPhases`] /// A [`bevy_render::render_graph::Node`] that runs the [`Opaque2d`] [`ViewBinnedRenderPhases`]
#[derive(Default)] #[derive(Default)]
pub struct MainOpaquePass2dNode; pub struct MainOpaquePass2dNode;
impl ViewNode for MainOpaquePass2dNode { impl ViewNode for MainOpaquePass2dNode {
@ -30,7 +30,7 @@ impl ViewNode for MainOpaquePass2dNode {
(camera, target, depth): QueryItem<'w, Self::ViewQuery>, (camera, target, depth): QueryItem<'w, Self::ViewQuery>,
world: &'w World, world: &'w World,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
let Some(opaque_phases) = world.get_resource::<ViewSortedRenderPhases<Opaque2d>>() else { let Some(opaque_phases) = world.get_resource::<ViewBinnedRenderPhases<Opaque2d>>() else {
return Ok(()); return Ok(());
}; };
@ -69,7 +69,7 @@ impl ViewNode for MainOpaquePass2dNode {
} }
// Opaque draws // Opaque draws
if !opaque_phase.items.is_empty() { if !opaque_phase.is_empty() {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _opaque_main_pass_2d_span = info_span!("opaque_main_pass_2d").entered(); let _opaque_main_pass_2d_span = info_span!("opaque_main_pass_2d").entered();
if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) { if let Err(err) = opaque_phase.render(&mut render_pass, world, view_entity) {

View file

@ -32,6 +32,7 @@ pub mod graph {
use std::ops::Range; use std::ops::Range;
use bevy_asset::UntypedAssetId;
use bevy_utils::HashMap; use bevy_utils::HashMap;
pub use camera_2d::*; pub use camera_2d::*;
pub use main_opaque_pass_2d_node::*; pub use main_opaque_pass_2d_node::*;
@ -45,12 +46,13 @@ use bevy_render::{
extract_component::ExtractComponentPlugin, extract_component::ExtractComponentPlugin,
render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner}, render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner},
render_phase::{ render_phase::{
sort_phase_system, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem, sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId,
PhaseItemExtraIndex, SortedPhaseItem, ViewSortedRenderPhases, DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases,
ViewSortedRenderPhases,
}, },
render_resource::{ render_resource::{
CachedRenderPipelineId, Extent3d, TextureDescriptor, TextureDimension, TextureFormat, BindGroupId, CachedRenderPipelineId, Extent3d, TextureDescriptor, TextureDimension,
TextureUsages, TextureFormat, TextureUsages,
}, },
renderer::RenderDevice, renderer::RenderDevice,
texture::TextureCache, texture::TextureCache,
@ -78,12 +80,11 @@ impl Plugin for Core2dPlugin {
.init_resource::<DrawFunctions<Opaque2d>>() .init_resource::<DrawFunctions<Opaque2d>>()
.init_resource::<DrawFunctions<Transparent2d>>() .init_resource::<DrawFunctions<Transparent2d>>()
.init_resource::<ViewSortedRenderPhases<Transparent2d>>() .init_resource::<ViewSortedRenderPhases<Transparent2d>>()
.init_resource::<ViewSortedRenderPhases<Opaque2d>>() .init_resource::<ViewBinnedRenderPhases<Opaque2d>>()
.add_systems(ExtractSchedule, extract_core_2d_camera_phases) .add_systems(ExtractSchedule, extract_core_2d_camera_phases)
.add_systems( .add_systems(
Render, Render,
( (
sort_phase_system::<Opaque2d>.in_set(RenderSet::PhaseSort),
sort_phase_system::<Transparent2d>.in_set(RenderSet::PhaseSort), sort_phase_system::<Transparent2d>.in_set(RenderSet::PhaseSort),
prepare_core_2d_depth_textures.in_set(RenderSet::PrepareResources), prepare_core_2d_depth_textures.in_set(RenderSet::PrepareResources),
), ),
@ -119,24 +120,47 @@ impl Plugin for Core2dPlugin {
} }
} }
/// Opaque 2D [`SortedPhaseItem`]s. /// Opaque 2D [`BinnedPhaseItem`]s.
pub struct Opaque2d { pub struct Opaque2d {
pub sort_key: FloatOrd, /// The key, which determines which can be batched.
pub entity: Entity, pub key: Opaque2dBinKey,
pub pipeline: CachedRenderPipelineId, /// An entity from which data will be fetched, including the mesh if
pub draw_function: DrawFunctionId, /// applicable.
pub representative_entity: Entity,
/// The ranges of instances.
pub batch_range: Range<u32>, pub batch_range: Range<u32>,
/// An extra index, which is either a dynamic offset or an index in the
/// indirect parameters list.
pub extra_index: PhaseItemExtraIndex, pub extra_index: PhaseItemExtraIndex,
} }
/// Data that must be identical in order to batch phase items together.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Opaque2dBinKey {
/// The identifier of the render pipeline.
pub pipeline: CachedRenderPipelineId,
/// The function used to draw.
pub draw_function: DrawFunctionId,
/// The asset that this phase item is associated with.
///
/// Normally, this is the ID of the mesh, but for non-mesh items it might be
/// the ID of another type of asset.
pub asset_id: UntypedAssetId,
/// The ID of a bind group specific to the material.
///
/// In the case of PBR, this is the `MaterialBindGroupId`.
pub material_bind_group_id: Option<BindGroupId>,
}
impl PhaseItem for Opaque2d { impl PhaseItem for Opaque2d {
#[inline] #[inline]
fn entity(&self) -> Entity { fn entity(&self) -> Entity {
self.entity self.representative_entity
} }
#[inline] #[inline]
fn draw_function(&self) -> DrawFunctionId { fn draw_function(&self) -> DrawFunctionId {
self.draw_function self.key.draw_function
} }
#[inline] #[inline]
@ -158,25 +182,28 @@ impl PhaseItem for Opaque2d {
} }
} }
impl SortedPhaseItem for Opaque2d { impl BinnedPhaseItem for Opaque2d {
type SortKey = FloatOrd; type BinKey = Opaque2dBinKey;
#[inline] fn new(
fn sort_key(&self) -> Self::SortKey { key: Self::BinKey,
self.sort_key representative_entity: Entity,
} batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
#[inline] ) -> Self {
fn sort(items: &mut [Self]) { Opaque2d {
// radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`. key,
radsort::sort_by_key(items, |item| item.sort_key().0); representative_entity,
batch_range,
extra_index,
}
} }
} }
impl CachedRenderPipelinePhaseItem for Opaque2d { impl CachedRenderPipelinePhaseItem for Opaque2d {
#[inline] #[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId { fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.pipeline self.key.pipeline
} }
} }
@ -246,7 +273,7 @@ impl CachedRenderPipelinePhaseItem for Transparent2d {
pub fn extract_core_2d_camera_phases( pub fn extract_core_2d_camera_phases(
mut commands: Commands, mut commands: Commands,
mut transparent_2d_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>, mut transparent_2d_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
mut opaque_2d_phases: ResMut<ViewSortedRenderPhases<Opaque2d>>, mut opaque_2d_phases: ResMut<ViewBinnedRenderPhases<Opaque2d>>,
cameras_2d: Extract<Query<(Entity, &Camera), With<Camera2d>>>, cameras_2d: Extract<Query<(Entity, &Camera), With<Camera2d>>>,
mut live_entities: Local<EntityHashSet>, mut live_entities: Local<EntityHashSet>,
) { ) {
@ -273,13 +300,13 @@ pub fn prepare_core_2d_depth_textures(
mut commands: Commands, mut commands: Commands,
mut texture_cache: ResMut<TextureCache>, mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>, render_device: Res<RenderDevice>,
transparent_2d_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>, transparent_2d_phases: Res<ViewSortedRenderPhases<Transparent2d>>,
opaque_2d_phases: ResMut<ViewSortedRenderPhases<Opaque2d>>, opaque_2d_phases: Res<ViewBinnedRenderPhases<Opaque2d>>,
views_2d: Query<(Entity, &ExtractedCamera, &Msaa), (With<Camera2d>,)>, views_2d: Query<(Entity, &ExtractedCamera, &Msaa), (With<Camera2d>,)>,
) { ) {
let mut textures = HashMap::default(); let mut textures = HashMap::default();
for (entity, camera, msaa) in &views_2d { for (view, camera, msaa) in &views_2d {
if !opaque_2d_phases.contains_key(&entity) || !transparent_2d_phases.contains_key(&entity) { if !opaque_2d_phases.contains_key(&view) || !transparent_2d_phases.contains_key(&view) {
continue; continue;
}; };
@ -313,7 +340,7 @@ pub fn prepare_core_2d_depth_textures(
.clone(); .clone();
commands commands
.entity(entity) .entity(view)
.insert(ViewDepthTexture::new(cached_texture, Some(0.0))); .insert(ViewDepthTexture::new(cached_texture, Some(0.0)));
} }
} }

View file

@ -29,13 +29,14 @@ bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
# other # other
bytemuck = { version = "1.5", features = ["derive"] } bytemuck = { version = "1", features = ["derive", "must_cast"] }
fixedbitset = "0.5" fixedbitset = "0.5"
guillotiere = "0.6.0" guillotiere = "0.6.0"
thiserror = "1.0" thiserror = "1.0"
rectangle-pack = "0.4" rectangle-pack = "0.4"
bitflags = "2.3" bitflags = "2.3"
radsort = "0.1" radsort = "0.1"
nonmax = "0.5"
[lints] [lints]
workspace = true workspace = true

View file

@ -1,7 +1,7 @@
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
use bevy_asset::{Asset, AssetApp, AssetId, AssetServer, Handle}; use bevy_asset::{Asset, AssetApp, AssetId, AssetServer, Handle};
use bevy_core_pipeline::{ use bevy_core_pipeline::{
core_2d::{Opaque2d, Transparent2d}, core_2d::{Opaque2d, Opaque2dBinKey, Transparent2d},
tonemapping::{DebandDither, Tonemapping}, tonemapping::{DebandDither, Tonemapping},
}; };
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
@ -18,8 +18,9 @@ use bevy_render::{
prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets, prepare_assets, PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets,
}, },
render_phase::{ render_phase::{
AddRenderCommand, DrawFunctions, PhaseItem, PhaseItemExtraIndex, RenderCommand, AddRenderCommand, BinnedRenderPhaseType, DrawFunctions, PhaseItem, PhaseItemExtraIndex,
RenderCommandResult, SetItemPipeline, TrackedRenderPass, ViewSortedRenderPhases, RenderCommand, RenderCommandResult, SetItemPipeline, TrackedRenderPass,
ViewBinnedRenderPhases, ViewSortedRenderPhases,
}, },
render_resource::{ render_resource::{
AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout,
@ -404,7 +405,7 @@ pub fn queue_material2d_meshes<M: Material2d>(
mut render_mesh_instances: ResMut<RenderMesh2dInstances>, mut render_mesh_instances: ResMut<RenderMesh2dInstances>,
render_material_instances: Res<RenderMaterial2dInstances<M>>, render_material_instances: Res<RenderMaterial2dInstances<M>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>, mut transparent_render_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
mut opaque_render_phases: ResMut<ViewSortedRenderPhases<Opaque2d>>, mut opaque_render_phases: ResMut<ViewBinnedRenderPhases<Opaque2d>>,
mut views: Query<( mut views: Query<(
Entity, Entity,
&ExtractedView, &ExtractedView,
@ -484,16 +485,17 @@ pub fn queue_material2d_meshes<M: Material2d>(
match material_2d.properties.alpha_mode { match material_2d.properties.alpha_mode {
AlphaMode2d::Opaque => { AlphaMode2d::Opaque => {
opaque_phase.add(Opaque2d { let bin_key = Opaque2dBinKey {
entity: *visible_entity,
draw_function: draw_opaque_2d,
pipeline: pipeline_id, pipeline: pipeline_id,
// Front-to-back ordering draw_function: draw_opaque_2d,
sort_key: -FloatOrd(mesh_z + material_2d.properties.depth_bias), asset_id: mesh_instance.mesh_asset_id.into(),
// Batching is done in batch_and_prepare_render_phase material_bind_group_id: material_2d.get_bind_group_id().0,
batch_range: 0..1, };
extra_index: PhaseItemExtraIndex::NONE, opaque_phase.add(
}); bin_key,
*visible_entity,
BinnedRenderPhaseType::mesh(mesh_instance.automatic_batching),
);
} }
AlphaMode2d::Blend => { AlphaMode2d::Blend => {
transparent_phase.add(Transparent2d { transparent_phase.add(Transparent2d {

View file

@ -14,10 +14,13 @@ use bevy_ecs::{
}; };
use bevy_math::{Affine3, Vec4}; use bevy_math::{Affine3, Vec4};
use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::batching::gpu_preprocessing::IndirectParameters;
use bevy_render::batching::no_gpu_preprocessing::batch_and_prepare_binned_render_phase;
use bevy_render::batching::no_gpu_preprocessing::{ use bevy_render::batching::no_gpu_preprocessing::{
self, batch_and_prepare_sorted_render_phase, write_batched_instance_buffer, self, batch_and_prepare_sorted_render_phase, write_batched_instance_buffer,
BatchedInstanceBuffer, BatchedInstanceBuffer,
}; };
use bevy_render::batching::GetFullBatchData;
use bevy_render::mesh::allocator::MeshAllocator; use bevy_render::mesh::allocator::MeshAllocator;
use bevy_render::mesh::{MeshVertexBufferLayoutRef, RenderMesh}; use bevy_render::mesh::{MeshVertexBufferLayoutRef, RenderMesh};
use bevy_render::texture::FallbackImage; use bevy_render::texture::FallbackImage;
@ -38,6 +41,8 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderApp, RenderSet, Extract, ExtractSchedule, Render, RenderApp, RenderSet,
}; };
use bevy_transform::components::GlobalTransform; use bevy_transform::components::GlobalTransform;
use bevy_utils::tracing::error;
use nonmax::NonMaxU32;
use crate::Material2dBindGroupId; use crate::Material2dBindGroupId;
@ -107,7 +112,7 @@ impl Plugin for Mesh2dRenderPlugin {
.add_systems( .add_systems(
Render, Render,
( (
batch_and_prepare_sorted_render_phase::<Opaque2d, Mesh2dPipeline> batch_and_prepare_binned_render_phase::<Opaque2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources), .in_set(RenderSet::PrepareResources),
batch_and_prepare_sorted_render_phase::<Transparent2d, Mesh2dPipeline> batch_and_prepare_sorted_render_phase::<Transparent2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources), .in_set(RenderSet::PrepareResources),
@ -163,7 +168,7 @@ pub struct Mesh2dTransforms {
pub flags: u32, pub flags: u32,
} }
#[derive(ShaderType, Clone)] #[derive(ShaderType, Clone, Copy)]
pub struct Mesh2dUniform { pub struct Mesh2dUniform {
// Affine 4x3 matrix transposed to 3x4 // Affine 4x3 matrix transposed to 3x4
pub world_from_local: [Vec4; 3], pub world_from_local: [Vec4; 3],
@ -360,12 +365,16 @@ impl Mesh2dPipeline {
} }
impl GetBatchData for Mesh2dPipeline { impl GetBatchData for Mesh2dPipeline {
type Param = SRes<RenderMesh2dInstances>; type Param = (
SRes<RenderMesh2dInstances>,
SRes<RenderAssets<RenderMesh>>,
SRes<MeshAllocator>,
);
type CompareData = (Material2dBindGroupId, AssetId<Mesh>); type CompareData = (Material2dBindGroupId, AssetId<Mesh>);
type BufferData = Mesh2dUniform; type BufferData = Mesh2dUniform;
fn get_batch_data( fn get_batch_data(
mesh_instances: &SystemParamItem<Self::Param>, (mesh_instances, _, _): &SystemParamItem<Self::Param>,
entity: Entity, entity: Entity,
) -> Option<(Self::BufferData, Option<Self::CompareData>)> { ) -> Option<(Self::BufferData, Option<Self::CompareData>)> {
let mesh_instance = mesh_instances.get(&entity)?; let mesh_instance = mesh_instances.get(&entity)?;
@ -379,6 +388,81 @@ impl GetBatchData for Mesh2dPipeline {
} }
} }
impl GetFullBatchData for Mesh2dPipeline {
type BufferInputData = ();
fn get_binned_batch_data(
(mesh_instances, _, _): &SystemParamItem<Self::Param>,
entity: Entity,
) -> Option<Self::BufferData> {
let mesh_instance = mesh_instances.get(&entity)?;
Some((&mesh_instance.transforms).into())
}
fn get_index_and_compare_data(
_: &SystemParamItem<Self::Param>,
_query_item: Entity,
) -> Option<(NonMaxU32, Option<Self::CompareData>)> {
error!(
"`get_index_and_compare_data` is only intended for GPU mesh uniform building, \
but this is not yet implemented for 2d meshes"
);
None
}
fn get_binned_index(
_: &SystemParamItem<Self::Param>,
_query_item: Entity,
) -> Option<NonMaxU32> {
error!(
"`get_binned_index` is only intended for GPU mesh uniform building, \
but this is not yet implemented for 2d meshes"
);
None
}
fn get_batch_indirect_parameters_index(
(mesh_instances, meshes, mesh_allocator): &SystemParamItem<Self::Param>,
indirect_parameters_buffer: &mut bevy_render::batching::gpu_preprocessing::IndirectParametersBuffer,
entity: Entity,
instance_index: u32,
) -> Option<NonMaxU32> {
let mesh_instance = mesh_instances.get(&entity)?;
let mesh = meshes.get(mesh_instance.mesh_asset_id)?;
let vertex_buffer_slice = mesh_allocator.mesh_vertex_slice(&mesh_instance.mesh_asset_id)?;
// Note that `IndirectParameters` covers both of these structures, even
// though they actually have distinct layouts. See the comment above that
// type for more information.
let indirect_parameters = match mesh.buffer_info {
RenderMeshBufferInfo::Indexed {
count: index_count, ..
} => {
let index_buffer_slice =
mesh_allocator.mesh_index_slice(&mesh_instance.mesh_asset_id)?;
IndirectParameters {
vertex_or_index_count: index_count,
instance_count: 0,
first_vertex_or_first_index: index_buffer_slice.range.start,
base_vertex_or_first_instance: vertex_buffer_slice.range.start,
first_instance: instance_index,
}
}
RenderMeshBufferInfo::NonIndexed => IndirectParameters {
vertex_or_index_count: mesh.vertex_count,
instance_count: 0,
first_vertex_or_first_index: vertex_buffer_slice.range.start,
base_vertex_or_first_instance: instance_index,
first_instance: instance_index,
},
};
(indirect_parameters_buffer.push(indirect_parameters) as u32)
.try_into()
.ok()
}
}
bitflags::bitflags! { bitflags::bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)] #[repr(transparent)]