Skip to content

Commit 306aaed

Browse files
authored
Prevent panic in check_dir_light_mesh_visibility (#24807)
# Objective Fixes #24804. In some edge cases `check_dir_light_mesh_visibility` reuses stale data from previous frames, causing a panic. ## Solution `check_dir_light_mesh_visibility` uses `view_visible_entities_queue` (a `Local<Parallel<Vec<Vec<Entity>>>>`) as a per-thread buffer, where the outer `Vec` has one slot per shadow cascade. Being `Local`, the buffers persist across frames. Each frame, the buffers should be resized to the current cascade count in the `par_iter` init closure. But that init only runs for threads that actually receive work. If the number of meshes goes down, some threads stay idle and never run their init, leaving their outer `Vec` at whatever length it had the previous frame. If the cascade count then increases in the same frame, those buffers get indexed with the new higher cascade count, which is out-of-bounds. This fixes the issue by resizing all buffers before the `par_iter` loop. ## Testing Made a minimal repro for #24804 that panics on main but not with this PR. <details> <summary>Click to view showcase</summary> ```rust //! Repro for #24804 use bevy::{ light::{CascadeShadowConfig, CascadeShadowConfigBuilder}, prelude::*, }; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, trigger_cascade_change) .run(); } fn setup( mut commands: Commands, mut meshes: ResMut<Assets<Mesh>>, mut materials: ResMut<Assets<StandardMaterial>>, ) { commands.spawn(( Camera3d::default(), Transform::from_xyz(0.0, 20.0, 40.0).looking_at(Vec3::ZERO, Vec3::Y), )); // One cascade so thread-local buffers all get sized to 1 let shadow_config: CascadeShadowConfig = CascadeShadowConfigBuilder { num_cascades: 1, ..default() } .into(); commands.spawn(( DirectionalLight { illuminance: 10_000.0, shadow_maps_enabled: true, ..default() }, shadow_config, Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -1.0, 0.5, 0.0)), )); // Spawn lots of cubes to use multiple threads from the pool let mesh = meshes.add(Cuboid::new(1.0, 1.0, 1.0)); let mat = materials.add(Color::WHITE); for x in -10..10_i32 { for z in -5..5_i32 { commands.spawn(( Mesh3d(mesh.clone()), MeshMaterial3d(mat.clone()), Transform::from_xyz(x as f32 * 2.0, 0.0, z as f32 * 2.0), )); } } } fn trigger_cascade_change( mut commands: Commands, mut frame: Local<u32>, meshes: Query<Entity, With<Mesh3d>>, mut light: Query<&mut CascadeShadowConfig, With<DirectionalLight>>, ) { *frame += 1; if *frame != 200 { return; } // Only keep one cube so most threadsdon't work this frame let mut iter = meshes.iter(); iter.next(); // keep one for entity in iter { commands.entity(entity).despawn(); } // Switch to 4 cascades so idle threads cause a panic for mut config in light.iter_mut() { *config = CascadeShadowConfigBuilder { num_cascades: 4, ..default() } .into(); } } ``` </details>
1 parent 5558f7b commit 306aaed

1 file changed

Lines changed: 7 additions & 0 deletions

File tree

crates/bevy_light/src/lib.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,13 @@ pub fn check_dir_light_mesh_visibility(
400400
let view_mask = maybe_view_mask.unwrap_or_default();
401401

402402
for (view, view_frusta) in &frusta.frusta {
403+
// Resize any per-thread buffer left at a stale state from a prior frame.
404+
// Threads receiving no work this frame won't run the `init` closure from `par_iter`, and without this
405+
// their buffer might be indexed out-of-bounds during collection if the number of cascades has increased.
406+
for thread_queue in view_visible_entities_queue.iter_mut() {
407+
thread_queue.resize(view_frusta.len(), Vec::default());
408+
}
409+
403410
visible_entity_query.par_iter().for_each_init(
404411
|| {
405412
let mut entities = view_visible_entities_queue.borrow_local_mut();

0 commit comments

Comments
 (0)