Retain RenderMaterialInstances and RenderMeshMaterialIds from frame to frame. (#16985)

This commit makes Bevy use change detection to only update
`RenderMaterialInstances` and `RenderMeshMaterialIds` when meshes have
been added, changed, or removed. `extract_mesh_materials`, the system
that extracts these, now follows the pattern that
`extract_meshes_for_gpu_building` established.

This improves frame time of `many_cubes` from 3.9ms to approximately
3.1ms, which slightly surpasses the performance of Bevy 0.14.

(Resubmitted from #16878 to clean up history.)

![Screenshot 2024-12-17
182109](https://github.com/user-attachments/assets/dfb26e20-b314-4c67-a59a-dc9623fabb62)

---------

Co-authored-by: Charlotte McElwain <charlotte.c.mcelwain@gmail.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Patrick Walton 2025-01-21 19:35:46 -08:00 committed by GitHub
parent 93e5e6cb95
commit 72ddac140a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 207 additions and 89 deletions

View File

@ -439,7 +439,8 @@ impl Plugin for PbrPlugin {
prepare_clusters.in_set(RenderSet::PrepareResources),
),
)
.init_resource::<LightMeta>();
.init_resource::<LightMeta>()
.init_resource::<RenderMaterialBindings>();
render_app.world_mut().add_observer(add_light_view_entities);
render_app

View File

@ -818,7 +818,9 @@ pub fn check_dir_light_mesh_visibility(
for entities in defer_queue.iter_mut() {
let mut iter = query.iter_many_mut(world, entities.iter());
while let Some(mut view_visibility) = iter.fetch_next() {
view_visibility.set();
if !**view_visibility {
view_visibility.set();
}
}
}
});
@ -940,12 +942,16 @@ pub fn check_point_light_mesh_visibility(
if has_no_frustum_culling
|| frustum.intersects_obb(aabb, &model_to_world, true, true)
{
view_visibility.set();
if !**view_visibility {
view_visibility.set();
}
visible_entities.push(entity);
}
}
} else {
view_visibility.set();
if !**view_visibility {
view_visibility.set();
}
for visible_entities in cubemap_visible_entities_local_queue.iter_mut()
{
visible_entities.push(entity);
@ -1025,11 +1031,15 @@ pub fn check_point_light_mesh_visibility(
if has_no_frustum_culling
|| frustum.intersects_obb(aabb, &model_to_world, true, true)
{
view_visibility.set();
if !**view_visibility {
view_visibility.set();
}
spot_visible_entities_local_queue.push(entity);
}
} else {
view_visibility.set();
if !**view_visibility {
view_visibility.set();
}
spot_visible_entities_local_queue.push(entity);
}
},

View File

@ -6,7 +6,7 @@ use crate::meshlet::{
InstanceManager,
};
use crate::*;
use bevy_asset::{Asset, AssetId, AssetServer};
use bevy_asset::{Asset, AssetId, AssetServer, UntypedAssetId};
use bevy_core_pipeline::{
core_3d::{
AlphaMask3d, Camera3d, Opaque3d, Opaque3dBatchSetKey, Opaque3dBinKey,
@ -33,17 +33,18 @@ use bevy_render::{
batching::gpu_preprocessing::GpuPreprocessingSupport,
camera::TemporalJitter,
extract_resource::ExtractResource,
mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh},
mesh::{self, Mesh3d, MeshVertexBufferLayoutRef, RenderMesh},
render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets},
render_phase::*,
render_resource::*,
renderer::RenderDevice,
sync_world::MainEntity,
view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility},
Extract,
};
use bevy_render::{mesh::allocator::MeshAllocator, sync_world::MainEntityHashMap};
use bevy_render::{texture::FallbackImage, view::RenderVisibleEntities};
use bevy_utils::hashbrown::hash_map::Entry;
use bevy_utils::HashMap;
use core::{hash::Hash, marker::PhantomData};
use tracing::error;
@ -270,7 +271,13 @@ where
fn build(&self, app: &mut App) {
app.init_asset::<M>()
.register_type::<MeshMaterial3d<M>>()
.add_plugins(RenderAssetPlugin::<PreparedMaterial<M>>::default());
.add_plugins(RenderAssetPlugin::<PreparedMaterial<M>>::default())
.add_systems(
PostUpdate,
mark_meshes_as_changed_if_their_materials_changed::<M>
.ambiguous_with_all()
.after(mesh::mark_3d_meshes_as_changed_if_their_assets_changed),
);
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
@ -282,7 +289,10 @@ where
.add_render_command::<Opaque3d, DrawMaterial<M>>()
.add_render_command::<AlphaMask3d, DrawMaterial<M>>()
.init_resource::<SpecializedMeshPipelines<MaterialPipeline<M>>>()
.add_systems(ExtractSchedule, extract_mesh_materials::<M>)
.add_systems(
ExtractSchedule,
extract_mesh_materials::<M>.before(ExtractMeshesSet),
)
.add_systems(
Render,
queue_material_meshes::<M>
@ -581,26 +591,64 @@ pub const fn screen_space_specular_transmission_pipeline_key(
}
}
pub fn extract_mesh_materials<M: Material>(
/// A system that ensures that
/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts meshes
/// whose materials changed.
///
/// As [`crate::render::mesh::collect_meshes_for_gpu_building`] only considers
/// meshes that were newly extracted, and it writes information from the
/// [`RenderMeshMaterialIds`] into the
/// [`crate::render::mesh::MeshInputUniform`], we must tell
/// [`crate::render::mesh::extract_meshes_for_gpu_building`] to re-extract a
/// mesh if its material changed. Otherwise, the material binding information in
/// the [`crate::render::mesh::MeshInputUniform`] might not be updated properly.
/// The easiest way to ensure that
/// [`crate::render::mesh::extract_meshes_for_gpu_building`] re-extracts a mesh
/// is to mark its [`Mesh3d`] as changed, so that's what this system does.
fn mark_meshes_as_changed_if_their_materials_changed<M>(
mut changed_meshes_query: Query<&mut Mesh3d, Changed<MeshMaterial3d<M>>>,
) where
M: Material,
{
for mut mesh in &mut changed_meshes_query {
mesh.set_changed();
}
}
/// Fills the [`RenderMaterialInstances`] and [`RenderMeshMaterialIds`]
/// resources from the meshes in the scene.
fn extract_mesh_materials<M: Material>(
mut material_instances: ResMut<RenderMaterialInstances<M>>,
mut material_ids: ResMut<RenderMeshMaterialIds>,
mut material_bind_group_allocator: ResMut<MaterialBindGroupAllocator<M>>,
query: Extract<Query<(Entity, &ViewVisibility, &MeshMaterial3d<M>)>>,
changed_meshes_query: Extract<
Query<
(Entity, &ViewVisibility, &MeshMaterial3d<M>),
Or<(Changed<ViewVisibility>, Changed<MeshMaterial3d<M>>)>,
>,
>,
mut removed_visibilities_query: Extract<RemovedComponents<ViewVisibility>>,
mut removed_materials_query: Extract<RemovedComponents<MeshMaterial3d<M>>>,
) {
material_instances.clear();
for (entity, view_visibility, material) in &query {
for (entity, view_visibility, material) in &changed_meshes_query {
if view_visibility.get() {
material_instances.insert(entity.into(), material.id());
material_ids.insert(entity.into(), material.id().into());
} else {
material_instances.remove(&MainEntity::from(entity));
material_ids.remove(entity.into());
}
}
// Allocate a slot for this material in the bind group.
let material_id = material.id().untyped();
material_ids
.mesh_to_material
.insert(entity.into(), material_id);
if let Entry::Vacant(entry) = material_ids.material_to_binding.entry(material_id) {
entry.insert(material_bind_group_allocator.allocate());
}
for entity in removed_visibilities_query
.read()
.chain(removed_materials_query.read())
{
// Only queue a mesh for removal if we didn't pick it up above.
// It's possible that a necessary component was removed and re-added in
// the same frame.
if !changed_meshes_query.contains(entity) {
material_instances.remove(&MainEntity::from(entity));
material_ids.remove(entity.into());
}
}
}
@ -1019,6 +1067,14 @@ pub struct MaterialProperties {
pub reads_view_transmission_texture: bool,
}
/// A resource that maps each untyped material ID to its binding.
///
/// This duplicates information in `RenderAssets<M>`, but it doesn't have the
/// `M` type parameter, so it can be used in untyped contexts like
/// [`crate::render::mesh::collect_meshes_for_gpu_building`].
#[derive(Resource, Default, Deref, DerefMut)]
pub struct RenderMaterialBindings(HashMap<UntypedAssetId, MaterialBindingId>);
/// Data prepared for a [`Material`] instance.
pub struct PreparedMaterial<M: Material> {
pub binding: MaterialBindingId,
@ -1033,8 +1089,8 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
SRes<RenderDevice>,
SRes<MaterialPipeline<M>>,
SRes<DefaultOpaqueRendererMethod>,
SRes<RenderMeshMaterialIds>,
SResMut<MaterialBindGroupAllocator<M>>,
SResMut<RenderMaterialBindings>,
M::Param,
);
@ -1045,19 +1101,15 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
render_device,
pipeline,
default_opaque_render_method,
mesh_material_ids,
ref mut bind_group_allocator,
ref mut render_material_bindings,
ref mut material_param,
): &mut SystemParamItem<Self::Param>,
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
// Fetch the material binding ID, so that we can write it in to the
// `PreparedMaterial`.
let Some(material_binding_id) = mesh_material_ids
.material_to_binding
.get(&material_id.untyped())
else {
return Err(PrepareAssetError::RetryNextUpdate(material));
};
// Allocate a material binding ID if needed.
let material_binding_id = *render_material_bindings
.entry(material_id.into())
.or_insert_with(|| bind_group_allocator.allocate());
let method = match material.opaque_render_method() {
OpaqueRendererMethod::Forward => OpaqueRendererMethod::Forward,
@ -1077,10 +1129,10 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
false,
) {
Ok(unprepared) => {
bind_group_allocator.init(render_device, *material_binding_id, unprepared);
bind_group_allocator.init(render_device, material_binding_id, unprepared);
Ok(PreparedMaterial {
binding: *material_binding_id,
binding: material_binding_id,
properties: MaterialProperties {
alpha_mode: material.alpha_mode(),
depth_bias: material.depth_bias(),
@ -1110,13 +1162,13 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
Ok(prepared_bind_group) => {
// Store the resulting bind group directly in the slot.
bind_group_allocator.init_custom(
*material_binding_id,
material_binding_id,
prepared_bind_group.bind_group,
prepared_bind_group.data,
);
Ok(PreparedMaterial {
binding: *material_binding_id,
binding: material_binding_id,
properties: MaterialProperties {
alpha_mode: material.alpha_mode(),
depth_bias: material.depth_bias(),
@ -1142,21 +1194,21 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
}
fn unload_asset(
asset_id: AssetId<Self::SourceAsset>,
(_, _, _, mesh_material_ids, ref mut bind_group_allocator, _): &mut SystemParamItem<
Self::Param,
>,
source_asset: AssetId<Self::SourceAsset>,
(
_,
_,
_,
ref mut bind_group_allocator,
ref mut render_material_bindings,
_,
): &mut SystemParamItem<Self::Param>,
) {
// Mark this material's slot in the binding array as free.
let Some(material_binding_id) = mesh_material_ids
.material_to_binding
.get(&asset_id.untyped())
let Some(material_binding_id) = render_material_bindings.remove(&source_asset.untyped())
else {
return;
};
bind_group_allocator.free(*material_binding_id);
bind_group_allocator.free(material_binding_id);
}
}

View File

@ -1,7 +1,8 @@
use super::{meshlet_mesh_manager::MeshletMeshManager, MeshletMesh, MeshletMesh3d};
use crate::{
Material, MeshFlags, MeshTransforms, MeshUniform, NotShadowCaster, NotShadowReceiver,
PreviousGlobalTransform, RenderMaterialInstances, RenderMeshMaterialIds,
PreviousGlobalTransform, RenderMaterialBindings, RenderMaterialInstances,
RenderMeshMaterialIds,
};
use bevy_asset::{AssetEvent, AssetServer, Assets, UntypedAssetId};
use bevy_ecs::{
@ -90,6 +91,7 @@ impl InstanceManager {
previous_transform: Option<&PreviousGlobalTransform>,
render_layers: Option<&RenderLayers>,
mesh_material_ids: &RenderMeshMaterialIds,
render_material_bindings: &RenderMaterialBindings,
not_shadow_receiver: bool,
not_shadow_caster: bool,
) {
@ -110,15 +112,11 @@ impl InstanceManager {
flags: flags.bits(),
};
let Some(mesh_material_asset_id) = mesh_material_ids.mesh_to_material.get(&instance) else {
return;
};
let Some(mesh_material_binding_id) = mesh_material_ids
.material_to_binding
.get(mesh_material_asset_id)
else {
return;
};
let mesh_material = mesh_material_ids.mesh_material(instance);
let mesh_material_binding_id = render_material_bindings
.get(&mesh_material)
.cloned()
.unwrap_or_default();
let mesh_uniform = MeshUniform::new(
&transforms,
@ -190,6 +188,7 @@ pub fn extract_meshlet_mesh_entities(
// TODO: Replace main_world and system_state when Extract<ResMut<Assets<MeshletMesh>>> is possible
mut main_world: ResMut<MainWorld>,
mesh_material_ids: Res<RenderMeshMaterialIds>,
render_material_bindings: Res<RenderMaterialBindings>,
mut system_state: Local<
Option<
SystemState<(
@ -259,6 +258,7 @@ pub fn extract_meshlet_mesh_entities(
previous_transform,
render_layers,
&mesh_material_ids,
&render_material_bindings,
not_shadow_receiver,
not_shadow_caster,
);

View File

@ -38,8 +38,8 @@ use bevy_render::{
renderer::{RenderAdapter, RenderDevice, RenderQueue},
texture::DefaultImageSampler,
view::{
NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, ViewTarget, ViewUniformOffset,
ViewVisibility, VisibilityRange,
self, NoFrustumCulling, NoIndirectDrawing, RenderVisibilityRanges, ViewTarget,
ViewUniformOffset, ViewVisibility, VisibilityRange,
},
Extract,
};
@ -160,6 +160,10 @@ impl Plugin for MeshRenderPlugin {
.init_resource::<MorphIndices>()
.init_resource::<MeshCullingDataBuffer>()
.init_resource::<RenderMeshMaterialIds>()
.configure_sets(
ExtractSchedule,
ExtractMeshesSet.after(view::extract_visibility_ranges),
)
.add_systems(
ExtractSchedule,
(
@ -172,7 +176,7 @@ impl Plugin for MeshRenderPlugin {
.add_systems(
Render,
(
set_mesh_motion_vector_flags.in_set(RenderSet::PrepareAssets),
set_mesh_motion_vector_flags.in_set(RenderSet::PrepareMeshes),
prepare_skins.in_set(RenderSet::PrepareResources),
prepare_morphs.in_set(RenderSet::PrepareResources),
prepare_mesh_bind_group.in_set(RenderSet::PrepareBindGroups),
@ -220,9 +224,7 @@ impl Plugin for MeshRenderPlugin {
gpu_preprocessing::delete_old_work_item_buffers::<MeshPipeline>
.in_set(RenderSet::PrepareResources),
collect_meshes_for_gpu_building
.in_set(RenderSet::PrepareAssets)
.after(allocator::allocate_and_free_meshes)
.after(extract_skins)
.in_set(RenderSet::PrepareMeshes)
// This must be before
// `set_mesh_motion_vector_flags` so it doesn't
// overwrite those flags.
@ -696,10 +698,7 @@ pub struct RenderMeshInstancesGpu(MainEntityHashMap<RenderMeshInstanceGpu>);
#[derive(Resource, Default)]
pub struct RenderMeshMaterialIds {
/// Maps the mesh instance to the material ID.
pub(crate) mesh_to_material: MainEntityHashMap<UntypedAssetId>,
/// Maps the material ID to the binding ID, which describes the location of
/// that material bind group data in memory.
pub(crate) material_to_binding: HashMap<UntypedAssetId, MaterialBindingId>,
mesh_to_material: MainEntityHashMap<UntypedAssetId>,
}
impl RenderMeshMaterialIds {
@ -709,15 +708,19 @@ impl RenderMeshMaterialIds {
/// Meshes almost always have materials, but in very specific circumstances
/// involving custom pipelines they won't. (See the
/// `specialized_mesh_pipelines` example.)
fn mesh_material_binding_id(&self, entity: MainEntity) -> MaterialBindingId {
pub(crate) fn mesh_material(&self, entity: MainEntity) -> UntypedAssetId {
self.mesh_to_material
.get(&entity)
.and_then(|mesh_material_asset_id| {
self.material_to_binding
.get(mesh_material_asset_id)
.cloned()
})
.unwrap_or_default()
.cloned()
.unwrap_or(AssetId::<StandardMaterial>::invalid().into())
}
pub(crate) fn insert(&mut self, mesh_entity: MainEntity, material_id: UntypedAssetId) {
self.mesh_to_material.insert(mesh_entity, material_id);
}
pub(crate) fn remove(&mut self, main_entity: MainEntity) {
self.mesh_to_material.remove(&main_entity);
}
}
@ -920,6 +923,7 @@ impl RenderMeshInstanceGpuBuilder {
previous_input_buffer: &mut InstanceInputUniformBuffer<MeshInputUniform>,
mesh_allocator: &MeshAllocator,
mesh_material_ids: &RenderMeshMaterialIds,
render_material_bindings: &RenderMaterialBindings,
render_lightmaps: &RenderLightmaps,
skin_indices: &SkinIndices,
) -> u32 {
@ -951,7 +955,11 @@ impl RenderMeshInstanceGpuBuilder {
};
// Look up the material index.
let mesh_material_binding_id = mesh_material_ids.mesh_material_binding_id(entity);
let mesh_material = mesh_material_ids.mesh_material(entity);
let mesh_material_binding_id = render_material_bindings
.get(&mesh_material)
.cloned()
.unwrap_or_default();
self.shared.material_bindings_index = mesh_material_binding_id;
let lightmap_slot = match render_lightmaps.render_lightmaps.get(&entity) {
@ -1394,6 +1402,7 @@ pub fn collect_meshes_for_gpu_building(
mut render_mesh_instance_queues: ResMut<RenderMeshInstanceGpuQueues>,
mesh_allocator: Res<MeshAllocator>,
mesh_material_ids: Res<RenderMeshMaterialIds>,
render_material_bindings: Res<RenderMaterialBindings>,
render_lightmaps: Res<RenderLightmaps>,
skin_indices: Res<SkinIndices>,
) {
@ -1432,6 +1441,7 @@ pub fn collect_meshes_for_gpu_building(
previous_input_buffer,
&mesh_allocator,
&mesh_material_ids,
&render_material_bindings,
&render_lightmaps,
&skin_indices,
);
@ -1458,6 +1468,7 @@ pub fn collect_meshes_for_gpu_building(
previous_input_buffer,
&mesh_allocator,
&mesh_material_ids,
&render_material_bindings,
&render_lightmaps,
&skin_indices,
);

View File

@ -126,6 +126,8 @@ pub enum RenderSet {
ExtractCommands,
/// Prepare assets that have been created/modified/removed this frame.
PrepareAssets,
/// Prepares extracted meshes.
PrepareMeshes,
/// Create any additional views such as those used for shadow mapping.
ManageViews,
/// Queue drawable entities as phase items in render phases ready for
@ -174,6 +176,7 @@ impl Render {
schedule.configure_sets(
(
ExtractCommands,
PrepareMeshes,
ManageViews,
Queue,
PhaseSort,
@ -185,7 +188,7 @@ impl Render {
.chain(),
);
schedule.configure_sets((ExtractCommands, PrepareAssets, Prepare).chain());
schedule.configure_sets((ExtractCommands, PrepareAssets, PrepareMeshes, Prepare).chain());
schedule.configure_sets(
QueueMeshes
.in_set(Queue)

View File

@ -21,7 +21,7 @@ use bevy_ecs::{
SystemParamItem,
},
};
pub use components::{Mesh2d, Mesh3d};
pub use components::{mark_3d_meshes_as_changed_if_their_assets_changed, Mesh2d, Mesh3d};
use wgpu::IndexFormat;
/// Adds the [`Mesh`] as an asset and makes sure that they are extracted and prepared for the GPU.
@ -40,7 +40,7 @@ impl Plugin for MeshPlugin {
.add_plugins(MeshAllocatorPlugin)
.add_systems(
PostUpdate,
components::mark_3d_meshes_as_changed_if_their_assets_changed
mark_3d_meshes_as_changed_if_their_assets_changed
.ambiguous_with(VisibilitySystems::CalculateBounds),
);

View File

@ -28,15 +28,15 @@ use bevy_render::{
ViewBinnedRenderPhases, ViewSortedRenderPhases,
},
render_resource::{
AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, PipelineCache,
RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline,
AsBindGroup, AsBindGroupError, BindGroup, BindGroupId, BindGroupLayout, BindingResources,
PipelineCache, RenderPipelineDescriptor, Shader, ShaderRef, SpecializedMeshPipeline,
SpecializedMeshPipelineError, SpecializedMeshPipelines,
},
renderer::RenderDevice,
sync_world::{MainEntity, MainEntityHashMap},
view::{ExtractedView, Msaa, RenderVisibleEntities, ViewVisibility},
Extract, ExtractSchedule, Render, RenderApp, RenderSet,
};
use bevy_render::{render_resource::BindingResources, sync_world::MainEntityHashMap};
use core::{hash::Hash, marker::PhantomData};
use derive_more::derive::From;
use tracing::error;
@ -281,15 +281,56 @@ impl<M: Material2d> Default for RenderMaterial2dInstances<M> {
pub fn extract_mesh_materials_2d<M: Material2d>(
mut material_instances: ResMut<RenderMaterial2dInstances<M>>,
query: Extract<Query<(Entity, &ViewVisibility, &MeshMaterial2d<M>), With<Mesh2d>>>,
changed_meshes_query: Extract<
Query<
(Entity, &ViewVisibility, &MeshMaterial2d<M>),
Or<(Changed<ViewVisibility>, Changed<MeshMaterial2d<M>>)>,
>,
>,
mut removed_visibilities_query: Extract<RemovedComponents<ViewVisibility>>,
mut removed_materials_query: Extract<RemovedComponents<MeshMaterial2d<M>>>,
) {
material_instances.clear();
for (entity, view_visibility, material) in &query {
for (entity, view_visibility, material) in &changed_meshes_query {
if view_visibility.get() {
material_instances.insert(entity.into(), material.id());
add_mesh_instance(entity, material, &mut material_instances);
} else {
remove_mesh_instance(entity, &mut material_instances);
}
}
for entity in removed_visibilities_query
.read()
.chain(removed_materials_query.read())
{
// Only queue a mesh for removal if we didn't pick it up above.
// It's possible that a necessary component was removed and re-added in
// the same frame.
if !changed_meshes_query.contains(entity) {
remove_mesh_instance(entity, &mut material_instances);
}
}
// Adds or updates a mesh instance in the [`RenderMaterial2dInstances`]
// array.
fn add_mesh_instance<M>(
entity: Entity,
material: &MeshMaterial2d<M>,
material_instances: &mut RenderMaterial2dInstances<M>,
) where
M: Material2d,
{
material_instances.insert(entity.into(), material.id());
}
// Removes a mesh instance from the [`RenderMaterial2dInstances`] array.
fn remove_mesh_instance<M>(
entity: Entity,
material_instances: &mut RenderMaterial2dInstances<M>,
) where
M: Material2d,
{
material_instances.remove(&MainEntity::from(entity));
}
}
/// Render pipeline data for a given [`Material2d`]