Skip to content

Commit e80978c

Browse files
committed
Create diagnostics for material allocations
1 parent 42b2d5e commit e80978c

File tree

4 files changed

+193
-1
lines changed

4 files changed

+193
-1
lines changed

crates/bevy_pbr/src/diagnostic.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
use core::{any::type_name, marker::PhantomData};
2+
3+
use bevy_app::{Plugin, PreUpdate};
4+
use bevy_diagnostic::{Diagnostic, DiagnosticPath, Diagnostics, RegisterDiagnostic};
5+
use bevy_ecs::{resource::Resource, system::Res};
6+
use bevy_platform::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
7+
use bevy_render::{Extract, ExtractSchedule, RenderApp};
8+
9+
use crate::{Material, MaterialBindGroupAllocator};
10+
11+
#[derive(Default)]
12+
pub struct MaterialAllocatorDiagnosticPlugin<M: Material> {
13+
_phantom: PhantomData<M>,
14+
}
15+
16+
impl<M: Material> MaterialAllocatorDiagnosticPlugin<M> {
17+
/// Get the [`DiagnosticPath`] for slab count
18+
pub fn slabs_diagnostic_path() -> DiagnosticPath {
19+
DiagnosticPath::from_components(["material_allocator_slabs", type_name::<M>()])
20+
}
21+
/// Get the [`DiagnosticPath`] for total slabs size
22+
pub fn slabs_size_diagnostic_path() -> DiagnosticPath {
23+
DiagnosticPath::from_components(["material_allocator_slabs_size", type_name::<M>()])
24+
}
25+
/// Get the [`DiagnosticPath`] for material allocations
26+
pub fn allocations_diagnostic_path() -> DiagnosticPath {
27+
DiagnosticPath::from_components(["material_allocator_allocations", type_name::<M>()])
28+
}
29+
}
30+
31+
impl<M: Material> Plugin for MaterialAllocatorDiagnosticPlugin<M> {
32+
fn build(&self, app: &mut bevy_app::App) {
33+
app.register_diagnostic(
34+
Diagnostic::new(Self::slabs_diagnostic_path()).with_suffix(" slabs"),
35+
)
36+
.register_diagnostic(
37+
Diagnostic::new(Self::slabs_size_diagnostic_path()).with_suffix(" bytes"),
38+
)
39+
.register_diagnostic(
40+
Diagnostic::new(Self::allocations_diagnostic_path()).with_suffix(" meshes"),
41+
)
42+
.init_resource::<MaterialAllocatorMeasurements<M>>()
43+
.add_systems(PreUpdate, add_material_allocator_measurement::<M>);
44+
45+
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
46+
render_app.add_systems(ExtractSchedule, measure_allocator::<M>);
47+
}
48+
}
49+
}
50+
51+
#[derive(Debug, Resource)]
52+
struct MaterialAllocatorMeasurements<M: Material> {
53+
slabs: AtomicUsize,
54+
slabs_size: AtomicUsize,
55+
allocations: AtomicU64,
56+
_phantom: PhantomData<M>,
57+
}
58+
59+
impl<M: Material> Default for MaterialAllocatorMeasurements<M> {
60+
fn default() -> Self {
61+
Self {
62+
slabs: AtomicUsize::default(),
63+
slabs_size: AtomicUsize::default(),
64+
allocations: AtomicU64::default(),
65+
_phantom: PhantomData,
66+
}
67+
}
68+
}
69+
70+
fn add_material_allocator_measurement<M: Material>(
71+
mut diagnostics: Diagnostics,
72+
measurements: Res<MaterialAllocatorMeasurements<M>>,
73+
) {
74+
diagnostics.add_measurement(
75+
&MaterialAllocatorDiagnosticPlugin::<M>::slabs_diagnostic_path(),
76+
|| measurements.slabs.load(Ordering::Relaxed) as f64,
77+
);
78+
diagnostics.add_measurement(
79+
&MaterialAllocatorDiagnosticPlugin::<M>::slabs_size_diagnostic_path(),
80+
|| measurements.slabs_size.load(Ordering::Relaxed) as f64,
81+
);
82+
diagnostics.add_measurement(
83+
&MaterialAllocatorDiagnosticPlugin::<M>::allocations_diagnostic_path(),
84+
|| measurements.allocations.load(Ordering::Relaxed) as f64,
85+
);
86+
}
87+
88+
fn measure_allocator<M: Material>(
89+
measurements: Extract<Res<MaterialAllocatorMeasurements<M>>>,
90+
allocator: Res<MaterialBindGroupAllocator<M>>,
91+
) {
92+
measurements
93+
.slabs
94+
.store(allocator.slab_count(), Ordering::Relaxed);
95+
measurements
96+
.slabs_size
97+
.store(allocator.slabs_size(), Ordering::Relaxed);
98+
measurements
99+
.allocations
100+
.store(allocator.allocations(), Ordering::Relaxed);
101+
}

crates/bevy_pbr/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ mod cluster;
2929
mod components;
3030
pub mod decal;
3131
pub mod deferred;
32+
pub mod diagnostic;
3233
mod extended_material;
3334
mod fog;
3435
mod light;

crates/bevy_pbr/src/material_bind_groups.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,45 @@ where
611611
}
612612
}
613613
}
614+
615+
/// Get number of allocated slabs for bindless material, returns 0 if it is
616+
/// [`Self::NonBindless`].
617+
pub fn slab_count(&self) -> usize {
618+
match self {
619+
Self::Bindless(bless) => bless.slabs.len(),
620+
Self::NonBindless(_) => 0,
621+
}
622+
}
623+
624+
/// Get total size of slabs allocated for bindless material, returns 0 if it is
625+
/// [`Self::NonBindless`].
626+
pub fn slabs_size(&self) -> usize {
627+
match self {
628+
Self::Bindless(bless) => bless
629+
.slabs
630+
.iter()
631+
.flat_map(|slab| {
632+
slab.data_buffers
633+
.iter()
634+
.map(|(_, buffer)| buffer.buffer.len())
635+
})
636+
.sum(),
637+
Self::NonBindless(_) => 0,
638+
}
639+
}
640+
641+
/// Get number of bindless material allocations in slabs, returns 0 if it is
642+
/// [`Self::NonBindless`].
643+
pub fn allocations(&self) -> u64 {
644+
match self {
645+
Self::Bindless(bless) => bless
646+
.slabs
647+
.iter()
648+
.map(|slab| u64::from(slab.allocated_resource_count))
649+
.sum(),
650+
Self::NonBindless(_) => 0,
651+
}
652+
}
614653
}
615654

616655
impl<M> MaterialBindlessIndexTable<M>

tests/get_mut_leak.rs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@ use bevy::{
1111
diagnostic::{DiagnosticsStore, LogDiagnosticsPlugin},
1212
ecs::system::{Commands, Local, Res, ResMut},
1313
math::primitives::Sphere,
14-
pbr::{MeshMaterial3d, StandardMaterial},
14+
pbr::{
15+
diagnostic::MaterialAllocatorDiagnosticPlugin, Material, MeshMaterial3d, PreparedMaterial,
16+
StandardMaterial,
17+
},
1518
render::{
1619
diagnostic::{MeshAllocatorDiagnosticPlugin, RenderAssetDiagnosticPlugin},
1720
mesh::{Mesh, Mesh3d, Meshable, RenderMesh},
@@ -51,6 +54,39 @@ fn check_mesh_leak() {
5154
}
5255
}
5356

57+
#[test]
58+
fn check_standard_material_leak() {
59+
let mut app = App::new();
60+
app.add_plugins((
61+
DefaultPlugins
62+
.build()
63+
.disable::<AudioPlugin>()
64+
.disable::<WinitPlugin>()
65+
.disable::<WindowPlugin>(),
66+
LogDiagnosticsPlugin {
67+
wait_duration: Duration::ZERO,
68+
..Default::default()
69+
},
70+
RenderAssetDiagnosticPlugin::<PreparedMaterial<StandardMaterial>>::new(" materials"),
71+
MaterialAllocatorDiagnosticPlugin::<StandardMaterial>::default(),
72+
))
73+
.add_systems(Startup, mesh_setup)
74+
.add_systems(
75+
Update,
76+
(
77+
touch_mutably::<Mesh>,
78+
crash_on_material_leak_detection::<StandardMaterial>,
79+
),
80+
);
81+
82+
app.finish();
83+
app.cleanup();
84+
85+
for _ in 0..100 {
86+
app.update();
87+
}
88+
}
89+
5490
fn mesh_setup(
5591
mut commands: Commands,
5692
mut meshes: ResMut<Assets<Mesh>>,
@@ -93,3 +129,18 @@ fn crash_on_mesh_leak_detection(diagnostic_store: Res<DiagnosticsStore>) {
93129
);
94130
}
95131
}
132+
133+
fn crash_on_material_leak_detection<M: Material>(diagnostic_store: Res<DiagnosticsStore>) {
134+
if let (Some(materials), Some(allocations)) = (
135+
diagnostic_store
136+
.get_measurement(
137+
&RenderAssetDiagnosticPlugin::<PreparedMaterial<M>>::render_asset_diagnostic_path(),
138+
)
139+
.filter(|diag| diag.value > 0.),
140+
diagnostic_store
141+
.get_measurement(&MaterialAllocatorDiagnosticPlugin::<M>::allocations_diagnostic_path())
142+
.filter(|diag| diag.value > 0.),
143+
) {
144+
assert!(materials.value < allocations.value * 10., "Detected leak");
145+
}
146+
}

0 commit comments

Comments
 (0)