Skip to content

Memory leak in EntitySpecializationTicks when a material uses NotShadowCaster #21526

@EmbersArc

Description

@EmbersArc

Bevy version and features

0.17.2

What you did

This came out of a longer debugging session I "documented" in a bluesky thread.

EDIT: This has been fixed in #21410. See further down for a case that still leaks memory.

//! Example to reproduce memory leak issue.
//!
use bevy::{
    pbr::{ExtendedMaterial, MaterialExtension},
    prelude::*,
};
use bevy_render::render_resource::AsBindGroup;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(MaterialPlugin::<MyExtendedMaterial1>::default())
        // Issue goes away when only a single material is present.
        .add_plugins(MaterialPlugin::<MyExtendedMaterial2>::default())
        .init_resource::<Handles>()
        .add_systems(Startup, setup)
        .add_systems(Update, update)
        .run();
}

#[derive(Resource)]
struct Handles(Handle<MyExtendedMaterial1>, Handle<Mesh>);

type MyExtendedMaterial1 = ExtendedMaterial<StandardMaterial, MyExtension1>;

#[derive(Default, Clone, AsBindGroup, Asset, TypePath)]
struct MyExtension1 {}

impl MaterialExtension for MyExtension1 {}

type MyExtendedMaterial2 = ExtendedMaterial<StandardMaterial, MyExtension2>;

#[derive(Default, Clone, AsBindGroup, Asset, TypePath)]
struct MyExtension2 {}

impl MaterialExtension for MyExtension2 {}

impl FromWorld for Handles {
    fn from_world(world: &mut World) -> Self {
        let material1_handle = world
            .resource_mut::<Assets<MyExtendedMaterial1>>()
            .add(MyExtendedMaterial1::default());
        let mesh_handle = world.resource_mut::<Assets<Mesh>>().add(Cuboid::default());
        Self(material1_handle, mesh_handle)
    }
}

fn update(
    mut commands: Commands,
    existing_meshes: Query<Entity, With<MeshMaterial3d<MyExtendedMaterial1>>>,
    handles: Res<Handles>,
) {
    dbg!(existing_meshes.count());

    const COUNT: usize = 100;

    for _ in 0..COUNT {
        commands.spawn((Mesh3d(handles.1.clone()), MeshMaterial3d(handles.0.clone())));
    }

    existing_meshes
        .iter()
        .take(COUNT)
        .for_each(|entity| commands.entity(entity).despawn());
}

fn setup(mut commands: Commands) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
    ));
}

What went wrong

EntitySpecializationTicks and SpecializedMaterialPipelineCache grow with each set of spawned/despawned entities, eventually slowing the systems that use them to a crawl.

Running extract_entities_needs_specialization explicitly after early_sweep_material_instances fixes the leaks.

However

In the presence of entities with a NotShadowCaster component, the EntitySpecializationTicks keep growing:

//! Example to reproduce memory leak issue.
//!
use bevy::{
    light::NotShadowCaster,
    pbr::{ExtendedMaterial, MaterialExtension},
    prelude::*,
};
use bevy_render::render_resource::AsBindGroup;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins(MaterialPlugin::<MyExtendedMaterial1>::default())
        // Issue goes away when only a single material is present.
        .add_plugins(MaterialPlugin::<MyExtendedMaterial2>::default())
        .init_resource::<Handles>()
        .add_systems(Startup, setup)
        .add_systems(Update, update)
        .run();
}

#[derive(Resource)]
struct Handles(
    Handle<MyExtendedMaterial1>,
    Handle<MyExtendedMaterial2>,
    Handle<Mesh>,
);

type MyExtendedMaterial1 = ExtendedMaterial<StandardMaterial, MyExtension1>;

#[derive(Default, Clone, AsBindGroup, Asset, TypePath)]
struct MyExtension1 {}

impl MaterialExtension for MyExtension1 {}

type MyExtendedMaterial2 = ExtendedMaterial<StandardMaterial, MyExtension2>;

#[derive(Default, Clone, AsBindGroup, Asset, TypePath)]
struct MyExtension2 {}

impl MaterialExtension for MyExtension2 {}

impl FromWorld for Handles {
    fn from_world(world: &mut World) -> Self {
        let material1_handle = world
            .resource_mut::<Assets<MyExtendedMaterial1>>()
            .add(MyExtendedMaterial1::default());
        let material2_handle = world
            .resource_mut::<Assets<MyExtendedMaterial2>>()
            .add(MyExtendedMaterial2::default());
        let mesh_handle = world.resource_mut::<Assets<Mesh>>().add(Cuboid::default());
        Self(material1_handle, material2_handle, mesh_handle)
    }
}

fn update(
    mut commands: Commands,
    existing_meshes1: Query<Entity, With<MeshMaterial3d<MyExtendedMaterial1>>>,
    existing_meshes2: Query<Entity, With<MeshMaterial3d<MyExtendedMaterial2>>>,
    handles: Res<Handles>,
) {
    dbg!(existing_meshes1.count());
    dbg!(existing_meshes2.count());

    const COUNT: usize = 100;

    for _ in 0..COUNT {
        commands.spawn((Mesh3d(handles.2.clone()), MeshMaterial3d(handles.0.clone())));
        commands.spawn((
            Mesh3d(handles.2.clone()),
            MeshMaterial3d(handles.1.clone()),
            NotShadowCaster,
        ));
    }

    existing_meshes1
        .iter()
        .take(COUNT)
        .for_each(|entity| commands.entity(entity).despawn());
    existing_meshes2
        .iter()
        .take(COUNT)
        .for_each(|entity| commands.entity(entity).despawn());
}

fn setup(mut commands: Commands) {
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y),
    ));
}

Additional context

Related: #21410

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-RenderingDrawing game state to the screenC-BugAn unexpected or incorrect behaviorC-PerformanceA change motivated by improving speed, memory usage or compile timesD-ModestA "normal" level of difficulty; suitable for simple features or challenging fixesS-Ready-For-ImplementationThis issue is ready for an implementation PR. Go for it!

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions