bevy/tests/render_asset_leaks.rs
2025-06-27 15:22:31 -03:00

279 lines
7.6 KiB
Rust

//! Tests if touching mutably a asset that gets extracted to the render world
//! causes a leak
use std::time::Duration;
use bevy::{
app::{App, PluginGroup, Startup, Update},
asset::{Asset, Assets, Handle},
color::Color,
diagnostic::{DiagnosticsStore, LogDiagnosticsPlugin},
ecs::{
resource::Resource,
system::{Commands, Res, ResMut},
},
math::primitives::Sphere,
pbr::{
diagnostic::MaterialAllocatorDiagnosticPlugin, Material, PreparedMaterial, StandardMaterial,
},
render::{
diagnostic::{MeshAllocatorDiagnosticPlugin, RenderAssetDiagnosticPlugin},
mesh::{Mesh, Meshable, RenderMesh},
},
utils::default,
window::{ExitCondition, WindowPlugin},
winit::WinitPlugin,
DefaultPlugins,
};
use bevy_render::{
settings::{Backends, RenderCreation, WgpuSettings},
RenderPlugin,
};
fn base_app() -> App {
let mut app = App::new();
app.add_plugins((
DefaultPlugins
.build()
.set(WindowPlugin {
primary_window: None,
exit_condition: ExitCondition::DontExit,
..default()
})
.set(RenderPlugin {
render_creation: RenderCreation::Automatic(WgpuSettings {
backends: Some(Backends::NOOP),
..default()
}),
..default()
})
.disable::<WinitPlugin>(),
LogDiagnosticsPlugin {
wait_duration: Duration::ZERO,
..default()
},
));
app
}
#[test]
fn check_mesh_leak() {
let mut app = base_app();
app.add_plugins((
RenderAssetDiagnosticPlugin::<RenderMesh>::new(" meshes"),
MeshAllocatorDiagnosticPlugin,
))
.add_systems(Startup, mesh_setup)
.add_systems(
Update,
(touch_mutably::<Mesh>, crash_on_mesh_leak_detection),
);
app.finish();
app.cleanup();
for _ in 0..100 {
app.update();
}
}
#[test]
fn check_standard_material_leak() {
let mut app = base_app();
app.add_plugins((
RenderAssetDiagnosticPlugin::<PreparedMaterial<StandardMaterial>>::new(" materials"),
MaterialAllocatorDiagnosticPlugin::<StandardMaterial>::new(" standard materials"),
))
.add_systems(Startup, mesh_setup)
.add_systems(
Update,
(
touch_mutably::<StandardMaterial>,
crash_on_material_leak_detection::<StandardMaterial>,
),
);
app.finish();
app.cleanup();
for _ in 0..100 {
app.update();
}
}
#[test]
fn check_mesh_churn_leak() {
let mut app = base_app();
app.add_plugins((
RenderAssetDiagnosticPlugin::<RenderMesh>::new(" meshes"),
MeshAllocatorDiagnosticPlugin,
))
.add_systems(Startup, mesh_setup)
.add_systems(Update, (churn::<Mesh>, crash_on_mesh_leak_detection));
app.finish();
app.cleanup();
for _ in 0..100 {
app.update();
}
}
#[test]
fn check_standard_material_churn_leak() {
let mut app = base_app();
app.add_plugins((
RenderAssetDiagnosticPlugin::<PreparedMaterial<StandardMaterial>>::new(" materials"),
MaterialAllocatorDiagnosticPlugin::<StandardMaterial>::new(" standard materials"),
))
.add_systems(Startup, mesh_setup)
.add_systems(
Update,
(
churn::<StandardMaterial>,
crash_on_material_leak_detection::<StandardMaterial>,
),
);
app.finish();
app.cleanup();
for _ in 0..100 {
app.update();
}
}
#[ignore = "FIXME Issue #18808"]
#[test]
fn check_mesh_churn_insert_leak() {
let mut app = base_app();
app.add_plugins((
RenderAssetDiagnosticPlugin::<RenderMesh>::new(" meshes"),
MeshAllocatorDiagnosticPlugin,
))
.add_systems(Startup, mesh_setup)
.add_systems(
Update,
(churn_using_insert::<Mesh>, crash_on_mesh_leak_detection),
);
app.finish();
app.cleanup();
for _ in 0..100 {
app.update();
}
}
#[test]
fn check_standard_material_churn_insert_leak() {
let mut app = base_app();
app.add_plugins((
RenderAssetDiagnosticPlugin::<PreparedMaterial<StandardMaterial>>::new(" materials"),
MaterialAllocatorDiagnosticPlugin::<StandardMaterial>::new(" standard materials"),
))
.add_systems(Startup, mesh_setup)
.add_systems(
Update,
(
churn_using_insert::<StandardMaterial>,
crash_on_material_leak_detection::<StandardMaterial>,
),
);
app.finish();
app.cleanup();
for _ in 0..100 {
app.update();
}
}
#[derive(Resource)]
struct Leaker<A: Asset>(Vec<Handle<A>>);
fn mesh_setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
bevy::log::info!("Mesh setup");
let mut mesh_leaker = Vec::with_capacity(16);
for _ in 0..16 {
mesh_leaker.push(meshes.add(Sphere::new(1.).mesh().ico(79).unwrap()));
}
commands.insert_resource(Leaker(mesh_leaker));
let mut material_leaker = Vec::with_capacity(1000);
for _ in 0..1000 {
material_leaker.push(materials.add(Color::WHITE));
}
commands.insert_resource(Leaker(material_leaker));
}
fn touch_mutably<A: Asset>(mut assets: ResMut<Assets<A>>) {
for _ in assets.iter_mut() {}
}
fn churn<A: Asset>(mut assets: ResMut<Assets<A>>, mut leaker: ResMut<Leaker<A>>) {
let all_ids = leaker.0.drain(..).collect::<Vec<_>>();
for id in all_ids {
let asset = assets.remove(id.id()).unwrap();
leaker.0.push(assets.add(asset));
}
}
fn churn_using_insert<A: Asset>(mut assets: ResMut<Assets<A>>, leaker: Res<Leaker<A>>) {
for id in &leaker.0 {
let asset = assets.remove(id.id()).unwrap();
assets.insert(id.id(), asset);
}
}
fn crash_on_mesh_leak_detection(diagnostic_store: Res<DiagnosticsStore>) {
if let (Some(render_meshes), Some(slab_size), Some(allocations)) = (
diagnostic_store
.get_measurement(
&RenderAssetDiagnosticPlugin::<RenderMesh>::render_asset_diagnostic_path(),
)
.filter(|diag| diag.value > 0.),
diagnostic_store
.get_measurement(MeshAllocatorDiagnosticPlugin::slabs_size_diagnostic_path())
.filter(|diag| diag.value > 0.),
diagnostic_store
.get_measurement(MeshAllocatorDiagnosticPlugin::allocations_diagnostic_path())
.filter(|diag| diag.value > 0.),
) {
assert!(
allocations.value < render_meshes.value * 10.,
"Detected leak"
);
assert!(
slab_size.value < (1 << 30) as f64,
"Exceeded 1GB of allocations."
);
}
}
fn crash_on_material_leak_detection<M: Material>(diagnostic_store: Res<DiagnosticsStore>) {
if let (Some(materials), Some(slab_size), Some(allocations)) = (
diagnostic_store
.get_measurement(
&RenderAssetDiagnosticPlugin::<PreparedMaterial<M>>::render_asset_diagnostic_path(),
)
.filter(|diag| diag.value > 0.),
diagnostic_store
.get_measurement(&MaterialAllocatorDiagnosticPlugin::<M>::slabs_size_diagnostic_path())
.filter(|diag| diag.value > 0.),
diagnostic_store
.get_measurement(&MaterialAllocatorDiagnosticPlugin::<M>::allocations_diagnostic_path())
.filter(|diag| diag.value > 0.),
) {
assert!(allocations.value < materials.value * 10., "Detected leak");
assert!(
slab_size.value < (1 << 30) as f64,
"Exceeded 1GB of allocations."
);
}
}