bevy icon indicating copy to clipboard operation
bevy copied to clipboard

Meshes flicker when retrieving mutable reference from AssetServer

Open Affinator opened this issue 5 months ago • 3 comments
trafficstars

Bevy version

0.16

Relevant system information

2025-05-28T13:21:01.933062Z  INFO bevy_render::renderer: AdapterInfo { name: "NVIDIA RTX A2000 Laptop GPU", vendor: 4318, device: 9656, device_type: DiscreteGpu, driver: "NVIDIA", driver_info: "553.62", backend: Vulkan }
2025-05-28T13:21:02.086063Z  INFO bevy_render::batching::gpu_preprocessing: GPU preprocessing is fully supported on this device.

What you did

I upgraded from 0.15 to 0.16. This worked without flicker before.

What went wrong

I wrote a clustered terrain generator for a strategy game (WIP). The terrain can be changed during gameplay. After a cluster has been updated, I retrieve the meshes of the adjacent terrain clusters to average the normals at the borders between the clusters.

Starting with 0.16 just retrieving the mesh (independent if I change the mesh or not) to modify it by if let Some(other_mesh) = meshes.get_mut(other_mesh_handle) {[....]} creates a flickering mesh. It looks like that another mesh is displayed for a single frame, before the correct mesh is displayed again.

Additional information

https://github.com/user-attachments/assets/236a0603-b7af-4879-9e14-341849524fe1

In this example I am not altering the adjacent meshes. I am just retrieving a mutable borrow from the AssetServer. If I only retrieve an immutable borrow the flicker disappears.

Affinator avatar May 28 '25 13:05 Affinator

This is probably fixed by #19083

rparrett avatar May 30 '25 13:05 rparrett

Unfortunately this is not fixed by 0.16.1.

PS: I'll try to better reproduce this on Wednesday.

Affinator avatar Jun 02 '25 07:06 Affinator

OK, this must be some kind of race condition.

I was able to reproduce the flicker on a simple example:

  • I spawn three cubes in a setup_system and I try to let one of the outer cubes (left or right) flicker
  • In a system in the Update schedule
    • I retrieve mutable references to the meshes left and right (and i read them just to make sure the compiler does not optimize away any dead code)
    • I update/overwrite the mesh in the middle (alternating between a cube and a capsule)
    • Note1: I do this each second but then several times in a row (if time.elapsed_secs() % 1.0 < 0.1 {), but I am not sure if this is 100% needed. I got the flicker reproduced this way.
    • Note2: I do need to force the AssetServer to remove the old middle mesh (meshes.remove(debug_resource.middle_mesh.id());) to see the flicker. In my main project I do not need to force_remove to see the flicker. So I assume this may not be needed on another machine.

Example video (only visible just at the beginning, look at the outer cubes):

https://github.com/user-attachments/assets/b51aee1b-4b73-46e8-98f2-b672b9870035

Example project:

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, flicker)
        .run();
}

#[derive(Resource)]
struct DebugResource {
    middle_entity: Entity,
    middle_mesh: Mesh3d,
    right_mesh: Mesh3d,
    left_mesh: Mesh3d,
    middle_mesh_is_currently_cube: bool
}

/// set up a simple 3D scene
fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // circular base
    commands.spawn((
        Mesh3d(meshes.add(Circle::new(4.0))),
        MeshMaterial3d(materials.add(Color::WHITE)),
        Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
    ));

    // middle cube
    let middle_mesh = Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0)));
    let middle_entity = commands.spawn((
        middle_mesh.clone(),
        MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
        Transform::from_xyz(0.0, 0.5, 0.0),
    )).id();

    // right cube
    let right_mesh = Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0)));
    commands.spawn((
        right_mesh.clone(),
        MeshMaterial3d(materials.add(Color::srgb_u8(87, 175, 64))),
        Transform::from_xyz(1.5, 0.5, 0.0),
    ));

    // left cube
    let left_mesh = Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0)));
    commands.spawn((
        left_mesh.clone(),
        MeshMaterial3d(materials.add(Color::srgb_u8(87, 175, 64))),
        Transform::from_xyz(-1.5, 0.5, 0.0),
    ));

    // light
    commands.spawn((
        PointLight {
            shadows_enabled: true,
            ..default()
        },
        Transform::from_xyz(4.0, 8.0, 4.0),
    ));

    // camera
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    let debug_resource = DebugResource {
        middle_entity,
        middle_mesh,
        right_mesh,
        left_mesh,
        middle_mesh_is_currently_cube: true        
    };

    commands.insert_resource(debug_resource);
}

fn flicker(
    time: Res<Time>,
    mut debug_resource: ResMut<DebugResource>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut commands: Commands
)
{
    if time.elapsed_secs() % 1.0 < 0.1 {
        // get mutable reference to the mesh we want to flicker (read it to force the compiler to not optimize it away)
        if let Some(right_mesh) = meshes.get_mut(debug_resource.right_mesh.id()) {
            println!("got mutable reference to right_mesh mesh: {}", right_mesh.get_vertex_size());
        }

        // get mutable reference to another mesh we want to flicker (to increase the chance of it happening, read it to force the compiler to not optimize it away)
        if let Some(left_mesh) = meshes.get_mut(debug_resource.left_mesh.id()) {
            println!("got mutable reference to left_mesh mesh: {}", left_mesh.get_vertex_size());
        }

        // update middle mesh
        let new_mesh = if debug_resource.middle_mesh_is_currently_cube {
            debug_resource.middle_mesh_is_currently_cube = false;
            Capsule3d::new(1.0, 2.0).mesh().build()
        }
        else {
            debug_resource.middle_mesh_is_currently_cube = true;
            Cuboid::new(1.0, 1.0, 1.0).mesh().build()
        };
        
        let mut middle_entity = commands.entity(debug_resource.middle_entity);
        let new_mesh3d = Mesh3d(meshes.add(new_mesh));        
        meshes.remove(debug_resource.middle_mesh.id()); // This is needed here to reproduce the flicker on my machine in this example. In my main project it flickers without manually removing the mesh from the AssetServer.
        middle_entity.insert(new_mesh3d.clone());
        debug_resource.middle_mesh = new_mesh3d;
        println!("updated/overwrote cube1 mesh");

    }
}

Affinator avatar Jun 04 '25 09:06 Affinator

I'm also seeing flickering when updating UVs in a mesh, and at the same time creating a new mesh elsewhere. The "flickered image" is that of the mesh I'm updating the UVs for rather than the mesh I'm creating, and it seems to have rotated transforms in the ficker even though the transforms are static for that entity.

peterellisjones avatar Jul 18 '25 10:07 peterellisjones

I'm seeing flickering too, after updating to 0.16, full code here

https://github.com/user-attachments/assets/5b512e9f-7582-42c0-ae25-c806343f5421

meshes.insert(&mut self.greedy_mesh, greedy_mesh);

aljen avatar Aug 07 '25 21:08 aljen

@tychedelia IIRC we discussed this problem on Discord and your opinion was that this was a complex fix with a workaround? I don't remember the details though.

alice-i-cecile avatar Aug 17 '25 22:08 alice-i-cecile

@tychedelia IIRC we discussed this problem on Discord and your opinion was that this was a complex fix with a workaround? I don't remember the details though.

I'm not sure I'd seen a reproduction posted. This seems bad. I'm not sure I can commit to fixing it before 0.17 but I'd really like to take a look at it with the repro.

tychedelia avatar Aug 17 '25 23:08 tychedelia

The repro posted doesnt repro on main on my m4 macbook nor on my windows 11 nvidia rtx 2070 intel i7 11th gen.

atlv24 avatar Aug 18 '25 03:08 atlv24

This repros on Linux, Ubuntu 22.04 10th gen intel i9 + rtx 2080 Ti

atlv24 avatar Aug 18 '25 03:08 atlv24

I can reproduce on my Linux box on main at 955e024a3828047382c4595970546bf3dc28316e, and on Bevy 0.16.1.

More cursedly, I can also reproduce this on Bevy 0.15.0. Time to go backwards in time...

I've attempted to reproduce this on Bevy 0.15, but got a black screen. Here's my compiling reproduction code for 0.14 at least.

use bevy::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, flicker)
        .run();
}

#[derive(Resource)]
struct DebugResource {
    middle_entity: Entity,
    middle_mesh: Handle<Mesh>,
    right_mesh: Handle<Mesh>,
    left_mesh: Handle<Mesh>,
    middle_mesh_is_currently_cube: bool,
}

/// set up a simple 3D scene
fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    // circular base
    commands.spawn((
        meshes.add(Circle::new(4.0)),
        materials.add(Color::WHITE),
        Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
    ));

    // middle cube
    let middle_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0));
    let middle_entity = commands
        .spawn((
            middle_mesh.clone(),
            materials.add(Color::srgb_u8(124, 144, 255)),
            Transform::from_xyz(0.0, 0.5, 0.0),
        ))
        .id();

    // right cube
    let right_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0));
    commands.spawn((
        right_mesh.clone(),
        materials.add(Color::srgb_u8(87, 175, 64)),
        Transform::from_xyz(1.5, 0.5, 0.0),
    ));

    // left cube
    let left_mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0));
    commands.spawn((
        left_mesh.clone(),
        materials.add(Color::srgb_u8(87, 175, 64)),
        Transform::from_xyz(-1.5, 0.5, 0.0),
    ));

    // light
    commands.spawn((
        PointLight {
            shadows_enabled: true,
            ..default()
        },
        Transform::from_xyz(4.0, 8.0, 4.0),
    ));

    // camera
    commands.spawn((
        Camera3d::default(),
        Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y),
    ));

    let debug_resource = DebugResource {
        middle_entity,
        middle_mesh,
        right_mesh,
        left_mesh,
        middle_mesh_is_currently_cube: true,
    };

    commands.insert_resource(debug_resource);
}

fn flicker(
    time: Res<Time>,
    mut debug_resource: ResMut<DebugResource>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut commands: Commands,
) {
    if time.elapsed_seconds() % 1.0 < 0.1 {
        // get mutable reference to the mesh we want to flicker (read it to force the compiler to not optimize it away)
        if let Some(right_mesh) = meshes.get_mut(debug_resource.right_mesh.id()) {
            println!(
                "got mutable reference to right_mesh mesh: {}",
                right_mesh.get_vertex_size()
            );
        }

        // get mutable reference to another mesh we want to flicker (to increase the chance of it happening, read it to force the compiler to not optimize it away)
        if let Some(left_mesh) = meshes.get_mut(debug_resource.left_mesh.id()) {
            println!(
                "got mutable reference to left_mesh mesh: {}",
                left_mesh.get_vertex_size()
            );
        }

        // update middle mesh
        let new_mesh = if debug_resource.middle_mesh_is_currently_cube {
            debug_resource.middle_mesh_is_currently_cube = false;
            Capsule3d::new(1.0, 2.0).mesh().build()
        } else {
            debug_resource.middle_mesh_is_currently_cube = true;
            Cuboid::new(1.0, 1.0, 1.0).mesh().build()
        };

        let mut middle_entity = commands.entity(debug_resource.middle_entity);
        let new_mesh3d = meshes.add(new_mesh);
        meshes.remove(debug_resource.middle_mesh.id()); // This is needed here to reproduce the flicker on my machine in this example. In my main project it flickers without manually removing the mesh from the AssetServer.
        middle_entity.insert(new_mesh3d.clone());
        debug_resource.middle_mesh = new_mesh3d;
        println!("updated/overwrote cube1 mesh");
    }
}

alice-i-cecile avatar Aug 18 '25 04:08 alice-i-cecile

Cutting from the 0.17 milestone. This is a nasty bug, but very old, and not a release blocker. @atlv24 and I suspect the mesh allocator based on the timing and behavior, but that's just a hunch.

alice-i-cecile avatar Aug 18 '25 05:08 alice-i-cecile

Reproduced by @tychedelia on m2 Mac.

alice-i-cecile avatar Aug 18 '25 06:08 alice-i-cecile

I have a very good reproduction on what I believe is this issue, and I think it actually is a problem with Indirect drawing - I was able to eliminate the flicker by adding the NoIndirectDrawing to the rendering camera.

My reproduction was based on a cubic meshing scenario: I have a generated surface mesh based on voxel data, which is regenerated whenever the underlying voxel data changes (a typical minecraft scenario). The regeneration creates an entirely new mesh and replaces the old one via mesh_assets.insert(old_handle, new_mesh).

In the mesh allocator, this causes the old allocation to be discarded, a new allocation to be obtained, and the mesh data will be written to the new allocation space. Note that this also occurs whenever a mesh asset is mutably dereferenced, so the scenario in the OP applies.

When updating the mesh, I noticed bad flickers in two cases:

  1. When the allocation process causes a new slab to be allocated, the mesh disappears entirely for a single frame.
  2. When the new allocation has more vertexes than the previous version, the notable flashing occurs for a single frame: almost exactly as in the reproduction posted by @aljen.

My assumption is that something in the Indirect rendering system is holding onto the wrong indexes after a mesh change, and this doesn't get updated until the next frame. Looking back at the cube/capsule reproduction posted by @Affinator , the extremely warped third cube appears to be drawing from vertex data which matches the capsule shape, which would also fit this hypothesis.

I'm looking for the underlying held references, but again, adding the NoIndirectDrawing component to the Camera appears to be a workaround.

mrtracy avatar Sep 10 '25 18:09 mrtracy

I can confirm that NoIndirectDrawing fixes the flicker on my side.

Affinator avatar Sep 11 '25 06:09 Affinator

Okay, I believe I have found what is causing the issue, but I don't fully understand how it isn't a larger issue.

I fixed this by adding additional 'Changed' parameter to extract_meshes_for_gpu_building() in bevy_pbr/render/mesh.rs.

Image

GPU extraction of mesh instances is apparently optimized only for entities which changed, but in my case (and i'm guessing in every other case here), nothing about the actual entity was changing, only the underlying mesh was changing. Adding the 'AssetChanged<Mesh3D>' seems to capture this event.

What I can't understand is why it corrects itself after a single frame; something must be causing this entity to get picked up quickly, but I'm not sure what's changing.

mrtracy avatar Sep 12 '25 06:09 mrtracy

I've uploaded a PR with a different fix than the one I suggested above; there was indeed a system already in place to mark "Mesh3D" components as changed if the underlying mesh was changed, but there was a one-frame delay in this occurring because that system was scheduled before the AssetEvents SystemSet, when it should have been scheduled after.

This looks like a simple mistake to my eyes, there are a lot of moving parts here; but if there was an important reason for these to be in the previous order, there are other fixes possible.

mrtracy avatar Sep 13 '25 06:09 mrtracy