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,
diagnostic::RecordDiagnostics,
render_graph::{NodeRunError, RenderGraphContext, ViewNode},
render_phase::{TrackedRenderPass, ViewSortedRenderPhases},
render_phase::{TrackedRenderPass, ViewBinnedRenderPhases},
render_resource::{CommandEncoderDescriptor, RenderPassDescriptor, StoreOp},
renderer::RenderContext,
view::{ViewDepthTexture, ViewTarget},
@ -13,7 +13,7 @@ use bevy_utils::tracing::error;
#[cfg(feature = "trace")]
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)]
pub struct MainOpaquePass2dNode;
impl ViewNode for MainOpaquePass2dNode {
@ -30,7 +30,7 @@ impl ViewNode for MainOpaquePass2dNode {
(camera, target, depth): QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let Some(opaque_phases) = world.get_resource::<ViewSortedRenderPhases<Opaque2d>>() else {
let Some(opaque_phases) = world.get_resource::<ViewBinnedRenderPhases<Opaque2d>>() else {
return Ok(());
};
@ -69,7 +69,7 @@ impl ViewNode for MainOpaquePass2dNode {
}
// Opaque draws
if !opaque_phase.items.is_empty() {
if !opaque_phase.is_empty() {
#[cfg(feature = "trace")]
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) {

View file

@ -32,6 +32,7 @@ pub mod graph {
use std::ops::Range;
use bevy_asset::UntypedAssetId;
use bevy_utils::HashMap;
pub use camera_2d::*;
pub use main_opaque_pass_2d_node::*;
@ -45,12 +46,13 @@ use bevy_render::{
extract_component::ExtractComponentPlugin,
render_graph::{EmptyNode, RenderGraphApp, ViewNodeRunner},
render_phase::{
sort_phase_system, CachedRenderPipelinePhaseItem, DrawFunctionId, DrawFunctions, PhaseItem,
PhaseItemExtraIndex, SortedPhaseItem, ViewSortedRenderPhases,
sort_phase_system, BinnedPhaseItem, CachedRenderPipelinePhaseItem, DrawFunctionId,
DrawFunctions, PhaseItem, PhaseItemExtraIndex, SortedPhaseItem, ViewBinnedRenderPhases,
ViewSortedRenderPhases,
},
render_resource::{
CachedRenderPipelineId, Extent3d, TextureDescriptor, TextureDimension, TextureFormat,
TextureUsages,
BindGroupId, CachedRenderPipelineId, Extent3d, TextureDescriptor, TextureDimension,
TextureFormat, TextureUsages,
},
renderer::RenderDevice,
texture::TextureCache,
@ -78,12 +80,11 @@ impl Plugin for Core2dPlugin {
.init_resource::<DrawFunctions<Opaque2d>>()
.init_resource::<DrawFunctions<Transparent2d>>()
.init_resource::<ViewSortedRenderPhases<Transparent2d>>()
.init_resource::<ViewSortedRenderPhases<Opaque2d>>()
.init_resource::<ViewBinnedRenderPhases<Opaque2d>>()
.add_systems(ExtractSchedule, extract_core_2d_camera_phases)
.add_systems(
Render,
(
sort_phase_system::<Opaque2d>.in_set(RenderSet::PhaseSort),
sort_phase_system::<Transparent2d>.in_set(RenderSet::PhaseSort),
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 sort_key: FloatOrd,
pub entity: Entity,
pub pipeline: CachedRenderPipelineId,
pub draw_function: DrawFunctionId,
/// The key, which determines which can be batched.
pub key: Opaque2dBinKey,
/// An entity from which data will be fetched, including the mesh if
/// applicable.
pub representative_entity: Entity,
/// The ranges of instances.
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,
}
/// 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 {
#[inline]
fn entity(&self) -> Entity {
self.entity
self.representative_entity
}
#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.draw_function
self.key.draw_function
}
#[inline]
@ -158,25 +182,28 @@ impl PhaseItem for Opaque2d {
}
}
impl SortedPhaseItem for Opaque2d {
type SortKey = FloatOrd;
impl BinnedPhaseItem for Opaque2d {
type BinKey = Opaque2dBinKey;
#[inline]
fn sort_key(&self) -> Self::SortKey {
self.sort_key
}
#[inline]
fn sort(items: &mut [Self]) {
// radsort is a stable radix sort that performed better than `slice::sort_by_key` or `slice::sort_unstable_by_key`.
radsort::sort_by_key(items, |item| item.sort_key().0);
fn new(
key: Self::BinKey,
representative_entity: Entity,
batch_range: Range<u32>,
extra_index: PhaseItemExtraIndex,
) -> Self {
Opaque2d {
key,
representative_entity,
batch_range,
extra_index,
}
}
}
impl CachedRenderPipelinePhaseItem for Opaque2d {
#[inline]
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(
mut commands: Commands,
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>>>,
mut live_entities: Local<EntityHashSet>,
) {
@ -273,13 +300,13 @@ pub fn prepare_core_2d_depth_textures(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
render_device: Res<RenderDevice>,
transparent_2d_phases: ResMut<ViewSortedRenderPhases<Transparent2d>>,
opaque_2d_phases: ResMut<ViewSortedRenderPhases<Opaque2d>>,
transparent_2d_phases: Res<ViewSortedRenderPhases<Transparent2d>>,
opaque_2d_phases: Res<ViewBinnedRenderPhases<Opaque2d>>,
views_2d: Query<(Entity, &ExtractedCamera, &Msaa), (With<Camera2d>,)>,
) {
let mut textures = HashMap::default();
for (entity, camera, msaa) in &views_2d {
if !opaque_2d_phases.contains_key(&entity) || !transparent_2d_phases.contains_key(&entity) {
for (view, camera, msaa) in &views_2d {
if !opaque_2d_phases.contains_key(&view) || !transparent_2d_phases.contains_key(&view) {
continue;
};
@ -313,7 +340,7 @@ pub fn prepare_core_2d_depth_textures(
.clone();
commands
.entity(entity)
.entity(view)
.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" }
# other
bytemuck = { version = "1.5", features = ["derive"] }
bytemuck = { version = "1", features = ["derive", "must_cast"] }
fixedbitset = "0.5"
guillotiere = "0.6.0"
thiserror = "1.0"
rectangle-pack = "0.4"
bitflags = "2.3"
radsort = "0.1"
nonmax = "0.5"
[lints]
workspace = true

View file

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

View file

@ -14,10 +14,13 @@ use bevy_ecs::{
};
use bevy_math::{Affine3, Vec4};
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::{
self, batch_and_prepare_sorted_render_phase, write_batched_instance_buffer,
BatchedInstanceBuffer,
};
use bevy_render::batching::GetFullBatchData;
use bevy_render::mesh::allocator::MeshAllocator;
use bevy_render::mesh::{MeshVertexBufferLayoutRef, RenderMesh};
use bevy_render::texture::FallbackImage;
@ -38,6 +41,8 @@ use bevy_render::{
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_transform::components::GlobalTransform;
use bevy_utils::tracing::error;
use nonmax::NonMaxU32;
use crate::Material2dBindGroupId;
@ -107,7 +112,7 @@ impl Plugin for Mesh2dRenderPlugin {
.add_systems(
Render,
(
batch_and_prepare_sorted_render_phase::<Opaque2d, Mesh2dPipeline>
batch_and_prepare_binned_render_phase::<Opaque2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
batch_and_prepare_sorted_render_phase::<Transparent2d, Mesh2dPipeline>
.in_set(RenderSet::PrepareResources),
@ -163,7 +168,7 @@ pub struct Mesh2dTransforms {
pub flags: u32,
}
#[derive(ShaderType, Clone)]
#[derive(ShaderType, Clone, Copy)]
pub struct Mesh2dUniform {
// Affine 4x3 matrix transposed to 3x4
pub world_from_local: [Vec4; 3],
@ -360,12 +365,16 @@ impl Mesh2dPipeline {
}
impl GetBatchData for Mesh2dPipeline {
type Param = SRes<RenderMesh2dInstances>;
type Param = (
SRes<RenderMesh2dInstances>,
SRes<RenderAssets<RenderMesh>>,
SRes<MeshAllocator>,
);
type CompareData = (Material2dBindGroupId, AssetId<Mesh>);
type BufferData = Mesh2dUniform;
fn get_batch_data(
mesh_instances: &SystemParamItem<Self::Param>,
(mesh_instances, _, _): &SystemParamItem<Self::Param>,
entity: Entity,
) -> Option<(Self::BufferData, Option<Self::CompareData>)> {
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! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[repr(transparent)]