Skip to content

Commit

Permalink
Use multidraw for opaque meshes when GPU culling is in use. (#16427)
Browse files Browse the repository at this point in the history
This commit adds support for *multidraw*, which is a feature that allows
multiple meshes to be drawn in a single drawcall. `wgpu` currently
implements multidraw on Vulkan, so this feature is only enabled there.
Multiple meshes can be drawn at once if they're in the same vertex and
index buffers and are otherwise placed in the same bin. (Thus, for
example, at present the materials and textures must be identical, but
see #16368.) Multidraw is a significant performance improvement during
the draw phase because it reduces the number of rebindings, as well as
the number of drawcalls.

This feature is currently only enabled when GPU culling is used: i.e.
when `GpuCulling` is present on a camera. Therefore, if you run for
example `scene_viewer`, you will not see any performance improvements,
because `scene_viewer` doesn't add the `GpuCulling` component to its
camera.

Additionally, the multidraw feature is only implemented for opaque 3D
meshes and not for shadows or 2D meshes. I plan to make GPU culling the
default and to extend the feature to shadows in the future. Also, in the
future I suspect that polyfilling multidraw on APIs that don't support
it will be fruitful, as even without driver-level support use of
multidraw allows us to avoid expensive `wgpu` rebindings.
  • Loading branch information
pcwalton authored Dec 6, 2024
1 parent 4d6b02a commit f5de3f0
Show file tree
Hide file tree
Showing 28 changed files with 669 additions and 310 deletions.
28 changes: 23 additions & 5 deletions crates/bevy_core_pipeline/src/core_2d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub mod graph {
use core::ops::Range;

use bevy_asset::UntypedAssetId;
use bevy_render::batching::gpu_preprocessing::GpuPreprocessingMode;
use bevy_render::render_phase::PhaseItemBinKey;
use bevy_utils::HashMap;
pub use camera_2d::*;
pub use main_opaque_pass_2d_node::*;
Expand Down Expand Up @@ -153,6 +155,14 @@ pub struct Opaque2dBinKey {
pub material_bind_group_id: Option<BindGroupId>,
}

impl PhaseItemBinKey for Opaque2dBinKey {
type BatchSetKey = ();

fn get_batch_set_key(&self) -> Option<Self::BatchSetKey> {
None
}
}

impl PhaseItem for Opaque2d {
#[inline]
fn entity(&self) -> Entity {
Expand All @@ -179,7 +189,7 @@ impl PhaseItem for Opaque2d {
}

fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
Expand Down Expand Up @@ -269,7 +279,7 @@ impl PhaseItem for AlphaMask2d {
}

fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
Expand All @@ -295,6 +305,14 @@ impl BinnedPhaseItem for AlphaMask2d {
}
}

impl PhaseItemBinKey for AlphaMask2dBinKey {
type BatchSetKey = ();

fn get_batch_set_key(&self) -> Option<Self::BatchSetKey> {
None
}
}

impl CachedRenderPipelinePhaseItem for AlphaMask2d {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
Expand Down Expand Up @@ -340,7 +358,7 @@ impl PhaseItem for Transparent2d {

#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

#[inline]
Expand Down Expand Up @@ -385,8 +403,8 @@ pub fn extract_core_2d_camera_phases(
continue;
}
transparent_2d_phases.insert_or_clear(entity);
opaque_2d_phases.insert_or_clear(entity);
alpha_mask_2d_phases.insert_or_clear(entity);
opaque_2d_phases.insert_or_clear(entity, GpuPreprocessingMode::None);
alpha_mask_2d_phases.insert_or_clear(entity, GpuPreprocessingMode::None);

live_entities.insert(entity);
}
Expand Down
107 changes: 87 additions & 20 deletions crates/bevy_core_pipeline/src/core_3d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ pub const DEPTH_TEXTURE_SAMPLING_SUPPORTED: bool = true;

use core::ops::Range;

use bevy_render::batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport};
use bevy_render::mesh::allocator::SlabId;
use bevy_render::render_phase::PhaseItemBinKey;
use bevy_render::view::GpuCulling;
pub use camera_3d::*;
pub use main_opaque_pass_3d_node::*;
pub use main_transparent_pass_3d_node::*;
Expand Down Expand Up @@ -224,9 +228,13 @@ pub struct Opaque3d {
pub extra_index: PhaseItemExtraIndex,
}

/// Data that must be identical in order to batch phase items together.
/// Information that must be identical in order to place opaque meshes in the
/// same *batch set*.
///
/// A batch set is a set of batches that can be multi-drawn together, if
/// multi-draw is in use.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Opaque3dBinKey {
pub struct Opaque3dBatchSetKey {
/// The identifier of the render pipeline.
pub pipeline: CachedRenderPipelineId,

Expand All @@ -238,14 +246,45 @@ pub struct Opaque3dBinKey {
/// In the case of PBR, this is the `MaterialBindGroupIndex`.
pub material_bind_group_index: Option<u32>,

/// The ID of the slab of GPU memory that contains vertex data.
///
/// For non-mesh items, you can fill this with 0 if your items can be
/// multi-drawn, or with a unique value if they can't.
pub vertex_slab: SlabId,

/// The ID of the slab of GPU memory that contains index data, if present.
///
/// For non-mesh items, you can safely fill this with `None`.
pub index_slab: Option<SlabId>,

/// The lightmap, if present.
pub lightmap_image: Option<AssetId<Image>>,
}

/// Data that must be identical in order to *batch* phase items together.
///
/// Note that a *batch set* (if multi-draw is in use) contains multiple batches.
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Opaque3dBinKey {
/// The key of the *batch set*.
///
/// As batches belong to a batch set, meshes in a batch must obviously be
/// able to be placed in a single batch set.
pub batch_set_key: Opaque3dBatchSetKey,

/// 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 lightmap, if present.
pub lightmap_image: Option<AssetId<Image>>,
impl PhaseItemBinKey for Opaque3dBinKey {
type BatchSetKey = Opaque3dBatchSetKey;

fn get_batch_set_key(&self) -> Option<Self::BatchSetKey> {
Some(self.batch_set_key.clone())
}
}

impl PhaseItem for Opaque3d {
Expand All @@ -261,7 +300,7 @@ impl PhaseItem for Opaque3d {

#[inline]
fn draw_function(&self) -> DrawFunctionId {
self.key.draw_function
self.key.batch_set_key.draw_function
}

#[inline]
Expand All @@ -275,7 +314,7 @@ impl PhaseItem for Opaque3d {
}

fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

fn batch_range_and_extra_index_mut(&mut self) -> (&mut Range<u32>, &mut PhaseItemExtraIndex) {
Expand Down Expand Up @@ -305,7 +344,7 @@ impl BinnedPhaseItem for Opaque3d {
impl CachedRenderPipelinePhaseItem for Opaque3d {
#[inline]
fn cached_pipeline(&self) -> CachedRenderPipelineId {
self.key.pipeline
self.key.batch_set_key.pipeline
}
}

Expand Down Expand Up @@ -343,7 +382,7 @@ impl PhaseItem for AlphaMask3d {

#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

#[inline]
Expand Down Expand Up @@ -426,7 +465,7 @@ impl PhaseItem for Transmissive3d {

#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

#[inline]
Expand Down Expand Up @@ -493,7 +532,7 @@ impl PhaseItem for Transparent3d {

#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

#[inline]
Expand Down Expand Up @@ -529,18 +568,27 @@ pub fn extract_core_3d_camera_phases(
mut alpha_mask_3d_phases: ResMut<ViewBinnedRenderPhases<AlphaMask3d>>,
mut transmissive_3d_phases: ResMut<ViewSortedRenderPhases<Transmissive3d>>,
mut transparent_3d_phases: ResMut<ViewSortedRenderPhases<Transparent3d>>,
cameras_3d: Extract<Query<(RenderEntity, &Camera), With<Camera3d>>>,
cameras_3d: Extract<Query<(RenderEntity, &Camera, Has<GpuCulling>), With<Camera3d>>>,
mut live_entities: Local<EntityHashSet>,
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
) {
live_entities.clear();

for (entity, camera) in &cameras_3d {
for (entity, camera, has_gpu_culling) in &cameras_3d {
if !camera.is_active {
continue;
}

opaque_3d_phases.insert_or_clear(entity);
alpha_mask_3d_phases.insert_or_clear(entity);
// If GPU culling is in use, use it (and indirect mode); otherwise, just
// preprocess the meshes.
let gpu_preprocessing_mode = gpu_preprocessing_support.min(if has_gpu_culling {
GpuPreprocessingMode::Culling
} else {
GpuPreprocessingMode::PreprocessingOnly
});

opaque_3d_phases.insert_or_clear(entity, gpu_preprocessing_mode);
alpha_mask_3d_phases.insert_or_clear(entity, gpu_preprocessing_mode);
transmissive_3d_phases.insert_or_clear(entity);
transparent_3d_phases.insert_or_clear(entity);

Expand All @@ -554,6 +602,8 @@ pub fn extract_core_3d_camera_phases(
}

// Extract the render phases for the prepass

#[allow(clippy::too_many_arguments)]
pub fn extract_camera_prepass_phase(
mut commands: Commands,
mut opaque_3d_prepass_phases: ResMut<ViewBinnedRenderPhases<Opaque3dPrepass>>,
Expand All @@ -565,6 +615,7 @@ pub fn extract_camera_prepass_phase(
(
RenderEntity,
&Camera,
Has<GpuCulling>,
Has<DepthPrepass>,
Has<NormalPrepass>,
Has<MotionVectorPrepass>,
Expand All @@ -574,27 +625,43 @@ pub fn extract_camera_prepass_phase(
>,
>,
mut live_entities: Local<EntityHashSet>,
gpu_preprocessing_support: Res<GpuPreprocessingSupport>,
) {
live_entities.clear();

for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass, deferred_prepass) in
cameras_3d.iter()
for (
entity,
camera,
gpu_culling,
depth_prepass,
normal_prepass,
motion_vector_prepass,
deferred_prepass,
) in cameras_3d.iter()
{
if !camera.is_active {
continue;
}

// If GPU culling is in use, use it (and indirect mode); otherwise, just
// preprocess the meshes.
let gpu_preprocessing_mode = gpu_preprocessing_support.min(if gpu_culling {
GpuPreprocessingMode::Culling
} else {
GpuPreprocessingMode::PreprocessingOnly
});

if depth_prepass || normal_prepass || motion_vector_prepass {
opaque_3d_prepass_phases.insert_or_clear(entity);
alpha_mask_3d_prepass_phases.insert_or_clear(entity);
opaque_3d_prepass_phases.insert_or_clear(entity, gpu_preprocessing_mode);
alpha_mask_3d_prepass_phases.insert_or_clear(entity, gpu_preprocessing_mode);
} else {
opaque_3d_prepass_phases.remove(&entity);
alpha_mask_3d_prepass_phases.remove(&entity);
}

if deferred_prepass {
opaque_3d_deferred_phases.insert_or_clear(entity);
alpha_mask_3d_deferred_phases.insert_or_clear(entity);
opaque_3d_deferred_phases.insert_or_clear(entity, gpu_preprocessing_mode);
alpha_mask_3d_deferred_phases.insert_or_clear(entity, gpu_preprocessing_mode);
} else {
opaque_3d_deferred_phases.remove(&entity);
alpha_mask_3d_deferred_phases.remove(&entity);
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_core_pipeline/src/deferred/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl PhaseItem for Opaque3dDeferred {

#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

#[inline]
Expand Down Expand Up @@ -133,7 +133,7 @@ impl PhaseItem for AlphaMask3dDeferred {

#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

#[inline]
Expand Down
13 changes: 11 additions & 2 deletions crates/bevy_core_pipeline/src/prepass/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ use bevy_asset::UntypedAssetId;
use bevy_ecs::prelude::*;
use bevy_math::Mat4;
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::render_phase::PhaseItemBinKey;
use bevy_render::sync_world::MainEntity;
use bevy_render::{
render_phase::{
Expand Down Expand Up @@ -167,6 +168,14 @@ pub struct OpaqueNoLightmap3dBinKey {
pub asset_id: UntypedAssetId,
}

impl PhaseItemBinKey for OpaqueNoLightmap3dBinKey {
type BatchSetKey = ();

fn get_batch_set_key(&self) -> Option<Self::BatchSetKey> {
None
}
}

impl PhaseItem for Opaque3dPrepass {
#[inline]
fn entity(&self) -> Entity {
Expand Down Expand Up @@ -194,7 +203,7 @@ impl PhaseItem for Opaque3dPrepass {

#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

#[inline]
Expand Down Expand Up @@ -268,7 +277,7 @@ impl PhaseItem for AlphaMask3dPrepass {

#[inline]
fn extra_index(&self) -> PhaseItemExtraIndex {
self.extra_index
self.extra_index.clone()
}

#[inline]
Expand Down
6 changes: 3 additions & 3 deletions crates/bevy_gizmos/src/pipeline_2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ fn queue_line_gizmos_2d(
pipeline,
sort_key: FloatOrd(f32::INFINITY),
batch_range: 0..1,
extra_index: PhaseItemExtraIndex::NONE,
extra_index: PhaseItemExtraIndex::None,
});
}

Expand All @@ -358,7 +358,7 @@ fn queue_line_gizmos_2d(
pipeline,
sort_key: FloatOrd(f32::INFINITY),
batch_range: 0..1,
extra_index: PhaseItemExtraIndex::NONE,
extra_index: PhaseItemExtraIndex::None,
});
}
}
Expand Down Expand Up @@ -417,7 +417,7 @@ fn queue_line_joint_gizmos_2d(
pipeline,
sort_key: FloatOrd(f32::INFINITY),
batch_range: 0..1,
extra_index: PhaseItemExtraIndex::NONE,
extra_index: PhaseItemExtraIndex::None,
});
}
}
Expand Down
Loading

0 comments on commit f5de3f0

Please sign in to comment.