From a788e31ad55861f9bf4423ca179b8445f0864001 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Fri, 25 Aug 2023 05:34:24 -0700 Subject: [PATCH 01/58] Fix CI for Rust 1.72 (#9562) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective [Rust 1.72.0](https://blog.rust-lang.org/2023/08/24/Rust-1.72.0.html) is now stable. # Notes - `let-else` formatting has arrived! - I chose to allow `explicit_iter_loop` due to https://github.com/rust-lang/rust-clippy/issues/11074. We didn't hit any of the false positives that prevent compilation, but fixing this did produce a lot of the "symbol soup" mentioned, e.g. `for image in &mut *image_events {`. Happy to undo this if there's consensus the other way. --------- Co-authored-by: François --- crates/bevy_animation/src/lib.rs | 16 +++++++--- crates/bevy_asset/src/asset_server.rs | 14 +++++--- crates/bevy_asset/src/io/file_asset_io.rs | 4 ++- crates/bevy_core_pipeline/src/blit/mod.rs | 2 +- crates/bevy_core_pipeline/src/bloom/mod.rs | 5 ++- .../src/contrast_adaptive_sharpening/node.rs | 9 ++++-- .../bevy_core_pipeline/src/msaa_writeback.rs | 2 +- crates/bevy_core_pipeline/src/taa/mod.rs | 14 ++++---- crates/bevy_ecs/macros/src/fetch.rs | 2 +- crates/bevy_ecs/macros/src/lib.rs | 15 ++++++--- crates/bevy_ecs/src/schedule/schedule.rs | 4 +-- crates/bevy_ecs/src/schedule/set.rs | 2 +- crates/bevy_ecs/src/world/mod.rs | 5 +-- .../query_exact_sized_iterator_safety.stderr | 32 +++++++++---------- ...er_combinations_mut_iterator_safety.stderr | 16 +++++----- ...query_iter_many_mut_iterator_safety.stderr | 16 +++++----- .../ui/system_param_derive_readonly.stderr | 16 +++++----- .../tests/ui/world_query_derive.stderr | 32 +++++++++---------- crates/bevy_gizmos/src/gizmos.rs | 2 +- crates/bevy_gizmos/src/lib.rs | 8 +++-- crates/bevy_gizmos/src/pipeline_2d.rs | 12 +++++-- crates/bevy_gizmos/src/pipeline_3d.rs | 12 +++++-- crates/bevy_math/src/cubic_splines.rs | 5 +-- crates/bevy_pbr/src/prepass/mod.rs | 4 ++- crates/bevy_pbr/src/ssao/mod.rs | 11 +++++-- .../bevy_reflect_derive/src/derive_data.rs | 6 ++-- crates/bevy_reflect/src/map.rs | 2 +- crates/bevy_reflect/src/struct_trait.rs | 2 +- .../tests/reflect_derive/generics.fail.stderr | 16 +++++----- crates/bevy_render/src/color/mod.rs | 12 +++++-- crates/bevy_render/src/mesh/morph.rs | 7 ++-- crates/bevy_render/src/pipelined_rendering.rs | 4 ++- crates/bevy_render/src/render_graph/graph.rs | 4 ++- crates/bevy_render/src/render_graph/node.rs | 7 ++-- crates/bevy_ui/src/render/render_pass.rs | 8 ++--- crates/bevy_winit/src/lib.rs | 24 +++++++------- examples/2d/texture_atlas.rs | 5 ++- examples/3d/bloom_3d.rs | 2 +- examples/animation/morph_targets.rs | 8 +++-- examples/games/breakout.rs | 5 --- examples/shader/post_processing.rs | 3 +- examples/shader/texture_binding_array.rs | 2 +- examples/stress_tests/many_gizmos.rs | 5 ++- .../tools/scene_viewer/morph_viewer_plugin.rs | 8 +++-- examples/ui/overflow_debug.rs | 2 +- tools/ci/src/main.rs | 2 +- 46 files changed, 235 insertions(+), 159 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index b7b8cb1a494b9..b84e871f042d8 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -341,9 +341,13 @@ fn verify_no_ancestor_player( player_parent: Option<&Parent>, parents: &Query<(Option>, Option<&Parent>)>, ) -> bool { - let Some(mut current) = player_parent.map(Parent::get) else { return true }; + let Some(mut current) = player_parent.map(Parent::get) else { + return true; + }; loop { - let Ok((maybe_player, parent)) = parents.get(current) else { return true }; + let Ok((maybe_player, parent)) = parents.get(current) else { + return true; + }; if maybe_player.is_some() { return false; } @@ -506,7 +510,9 @@ fn apply_animation( for (path, bone_id) in &animation_clip.paths { let cached_path = &mut animation.path_cache[*bone_id]; let curves = animation_clip.get_curves(*bone_id).unwrap(); - let Some(target) = entity_from_path(root, path, children, names, cached_path) else { continue }; + let Some(target) = entity_from_path(root, path, children, names, cached_path) else { + continue; + }; // SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias // any of their descendant Transforms. // @@ -519,7 +525,9 @@ fn apply_animation( // This means only the AnimationPlayers closest to the root of the hierarchy will be able // to run their animation. Any players in the children or descendants will log a warning // and do nothing. - let Ok(mut transform) = (unsafe { transforms.get_unchecked(target) }) else { continue }; + let Ok(mut transform) = (unsafe { transforms.get_unchecked(target) }) else { + continue; + }; let mut morphs = unsafe { morphs.get_unchecked(target) }; for curve in curves { // Some curves have only one keyframe used to set a transform diff --git a/crates/bevy_asset/src/asset_server.rs b/crates/bevy_asset/src/asset_server.rs index e5daf11126777..11dbd21b37e28 100644 --- a/crates/bevy_asset/src/asset_server.rs +++ b/crates/bevy_asset/src/asset_server.rs @@ -790,7 +790,8 @@ mod test { let asset_server = setup("."); asset_server.add_loader(FakePngLoader); - let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.png", true) else { + let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.png", true) + else { panic!(); }; @@ -802,7 +803,8 @@ mod test { let asset_server = setup("."); asset_server.add_loader(FakePngLoader); - let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.PNG", true) else { + let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.PNG", true) + else { panic!(); }; assert_eq!(t.extensions()[0], "png"); @@ -855,7 +857,9 @@ mod test { let asset_server = setup("."); asset_server.add_loader(FakePngLoader); - let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test-v1.2.3.png", true) else { + let Ok(MaybeAssetLoader::Ready(t)) = + asset_server.get_path_asset_loader("test-v1.2.3.png", true) + else { panic!(); }; assert_eq!(t.extensions()[0], "png"); @@ -866,7 +870,9 @@ mod test { let asset_server = setup("."); asset_server.add_loader(FakeMultipleDotLoader); - let Ok(MaybeAssetLoader::Ready(t)) = asset_server.get_path_asset_loader("test.test.png", true) else { + let Ok(MaybeAssetLoader::Ready(t)) = + asset_server.get_path_asset_loader("test.test.png", true) + else { panic!(); }; assert_eq!(t.extensions()[0], "test.png"); diff --git a/crates/bevy_asset/src/io/file_asset_io.rs b/crates/bevy_asset/src/io/file_asset_io.rs index 9dabb1b1b5a94..1c4f56f28ac4b 100644 --- a/crates/bevy_asset/src/io/file_asset_io.rs +++ b/crates/bevy_asset/src/io/file_asset_io.rs @@ -201,7 +201,9 @@ pub fn filesystem_watcher_system( } = event { for path in &paths { - let Some(set) = watcher.path_map.get(path) else {continue}; + let Some(set) = watcher.path_map.get(path) else { + continue; + }; for to_reload in set { // When an asset is modified, note down the timestamp (overriding any previous modification events) changed.insert(to_reload.to_owned(), Instant::now()); diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index 2bc2f63ea90e8..98442953bd712 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -19,7 +19,7 @@ impl Plugin for BlitPlugin { fn finish(&self, app: &mut App) { let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { - return + return; }; render_app diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index e5271544e2d78..5ec42d0c2e5cd 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -163,7 +163,10 @@ impl ViewNode for BloomNode { pipeline_cache.get_render_pipeline(downsampling_pipeline_ids.main), pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_main), pipeline_cache.get_render_pipeline(upsampling_pipeline_ids.id_final), - ) else { return Ok(()) }; + ) + else { + return Ok(()); + }; render_context.command_encoder().push_debug_group("bloom"); diff --git a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs index c49d876f2214d..0dd3f086ee27c 100644 --- a/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs +++ b/crates/bevy_core_pipeline/src/contrast_adaptive_sharpening/node.rs @@ -53,10 +53,15 @@ impl Node for CASNode { let sharpening_pipeline = world.resource::(); let uniforms = world.resource::>(); - let Ok((target, pipeline, uniform_index)) = self.query.get_manual(world, view_entity) else { return Ok(()) }; + let Ok((target, pipeline, uniform_index)) = self.query.get_manual(world, view_entity) + else { + return Ok(()); + }; let uniforms_id = uniforms.buffer().unwrap().id(); - let Some(uniforms) = uniforms.binding() else { return Ok(()) }; + let Some(uniforms) = uniforms.binding() else { + return Ok(()); + }; let pipeline = pipeline_cache.get_render_pipeline(pipeline.0).unwrap(); diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index cf881b07b608e..cd925c37225d5 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -21,7 +21,7 @@ pub struct MsaaWritebackPlugin; impl Plugin for MsaaWritebackPlugin { fn build(&self, app: &mut App) { let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { - return + return; }; render_app.add_systems( diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index 01e75ddd15f50..66097d3beeb20 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -57,7 +57,9 @@ impl Plugin for TemporalAntiAliasPlugin { app.insert_resource(Msaa::Off) .register_type::(); - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; render_app .init_resource::>() @@ -86,7 +88,9 @@ impl Plugin for TemporalAntiAliasPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; render_app.init_resource::(); } @@ -188,11 +192,7 @@ impl ViewNode for TAANode { ) else { return Ok(()); }; - let ( - Some(taa_pipeline), - Some(prepass_motion_vectors_texture), - Some(prepass_depth_texture), - ) = ( + let (Some(taa_pipeline), Some(prepass_motion_vectors_texture), Some(prepass_depth_texture)) = ( pipeline_cache.get_render_pipeline(taa_pipeline_id.0), &prepass_textures.motion_vectors, &prepass_textures.depth, diff --git a/crates/bevy_ecs/macros/src/fetch.rs b/crates/bevy_ecs/macros/src/fetch.rs index 987ee67dc4cae..e28d33475b778 100644 --- a/crates/bevy_ecs/macros/src/fetch.rs +++ b/crates/bevy_ecs/macros/src/fetch.rs @@ -135,7 +135,7 @@ pub fn derive_world_query_impl(input: TokenStream) -> TokenStream { "#[derive(WorldQuery)]` only supports structs", ) .into_compile_error() - .into() + .into(); }; let mut field_attrs = Vec::new(); diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 110df68260981..5bfbb3cfee1e6 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -251,10 +251,17 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { pub fn derive_system_param(input: TokenStream) -> TokenStream { let token_stream = input.clone(); let ast = parse_macro_input!(input as DeriveInput); - let syn::Data::Struct(syn::DataStruct { fields: field_definitions, .. }) = ast.data else { - return syn::Error::new(ast.span(), "Invalid `SystemParam` type: expected a `struct`") - .into_compile_error() - .into(); + let syn::Data::Struct(syn::DataStruct { + fields: field_definitions, + .. + }) = ast.data + else { + return syn::Error::new( + ast.span(), + "Invalid `SystemParam` type: expected a `struct`", + ) + .into_compile_error() + .into(); }; let path = bevy_ecs_path(); diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index cb9ab0a978335..4f1b90ff9050d 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -550,8 +550,8 @@ impl ScheduleGraph { let Some(prev) = config_iter.next() else { return AddSystemsInnerResult { nodes: Vec::new(), - densely_chained: true - } + densely_chained: true, + }; }; let mut previous_result = self.add_systems_inner(prev, true); densely_chained = previous_result.densely_chained; diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index c34745b9f34b6..6e0caaba0937c 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -83,7 +83,7 @@ impl Hash for SystemTypeSet { } impl Clone for SystemTypeSet { fn clone(&self) -> Self { - Self(PhantomData) + *self } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 82f2456b58322..0e79b19ce8568 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1770,8 +1770,9 @@ impl World { f: impl FnOnce(&mut World, &mut Schedule) -> R, ) -> Result { let label = label.as_ref(); - let Some((extracted_label, mut schedule)) - = self.get_resource_mut::().and_then(|mut s| s.remove_entry(label)) + let Some((extracted_label, mut schedule)) = self + .get_resource_mut::() + .and_then(|mut s| s.remove_entry(label)) else { return Err(TryRunScheduleError(label.dyn_clone())); }; diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr index ad2d61d015fe7..e4c9574ad49d2 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_exact_sized_iterator_safety.stderr @@ -7,14 +7,14 @@ error[E0277]: the trait bound `bevy_ecs::query::Changed: ArchetypeFilter` i | required by a bound introduced by this call | = help: the following other types implement trait `ArchetypeFilter`: - () - (F0, F1) - (F0, F1, F2) - (F0, F1, F2, F3) - (F0, F1, F2, F3, F4) - (F0, F1, F2, F3, F4, F5) - (F0, F1, F2, F3, F4, F5, F6) - (F0, F1, F2, F3, F4, F5, F6, F7) + With + Without + Or<()> + Or<(F0,)> + Or<(F0, F1)> + Or<(F0, F1, F2)> + Or<(F0, F1, F2, F3)> + Or<(F0, F1, F2, F3, F4)> and $N others = note: required for `QueryIter<'_, '_, &Foo, bevy_ecs::query::Changed>` to implement `ExactSizeIterator` note: required by a bound in `is_exact_size_iterator` @@ -32,14 +32,14 @@ error[E0277]: the trait bound `bevy_ecs::query::Added: ArchetypeFilter` is | required by a bound introduced by this call | = help: the following other types implement trait `ArchetypeFilter`: - () - (F0, F1) - (F0, F1, F2) - (F0, F1, F2, F3) - (F0, F1, F2, F3, F4) - (F0, F1, F2, F3, F4, F5) - (F0, F1, F2, F3, F4, F5, F6) - (F0, F1, F2, F3, F4, F5, F6, F7) + With + Without + Or<()> + Or<(F0,)> + Or<(F0, F1)> + Or<(F0, F1, F2)> + Or<(F0, F1, F2, F3)> + Or<(F0, F1, F2, F3, F4)> and $N others = note: required for `QueryIter<'_, '_, &Foo, bevy_ecs::query::Added>` to implement `ExactSizeIterator` note: required by a bound in `is_exact_size_iterator` diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr index 0b1474d28959e..570cab0a54d91 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_combinations_mut_iterator_safety.stderr @@ -7,14 +7,14 @@ error[E0277]: the trait bound `&mut A: ReadOnlyWorldQuery` is not satisfied | required by a bound introduced by this call | = help: the following other types implement trait `ReadOnlyWorldQuery`: - &T - () - (F0, F1) - (F0, F1, F2) - (F0, F1, F2, F3) - (F0, F1, F2, F3, F4) - (F0, F1, F2, F3, F4, F5) - (F0, F1, F2, F3, F4, F5, F6) + bevy_ecs::change_detection::Ref<'__w, T> + Has + AnyOf<()> + AnyOf<(F0,)> + AnyOf<(F0, F1)> + AnyOf<(F0, F1, F2)> + AnyOf<(F0, F1, F2, F3)> + AnyOf<(F0, F1, F2, F3, F4)> and $N others = note: `ReadOnlyWorldQuery` is implemented for `&A`, but not for `&mut A` = note: required for `QueryCombinationIter<'_, '_, &mut A, (), _>` to implement `Iterator` diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr index 855f36173da12..6fa4ef005cbaf 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/query_iter_many_mut_iterator_safety.stderr @@ -7,14 +7,14 @@ error[E0277]: the trait bound `&mut A: ReadOnlyWorldQuery` is not satisfied | required by a bound introduced by this call | = help: the following other types implement trait `ReadOnlyWorldQuery`: - &T - () - (F0, F1) - (F0, F1, F2) - (F0, F1, F2, F3) - (F0, F1, F2, F3, F4) - (F0, F1, F2, F3, F4, F5) - (F0, F1, F2, F3, F4, F5, F6) + bevy_ecs::change_detection::Ref<'__w, T> + Has + AnyOf<()> + AnyOf<(F0,)> + AnyOf<(F0, F1)> + AnyOf<(F0, F1, F2)> + AnyOf<(F0, F1, F2, F3)> + AnyOf<(F0, F1, F2, F3, F4)> and $N others = note: `ReadOnlyWorldQuery` is implemented for `&A`, but not for `&mut A` = note: required for `QueryManyIter<'_, '_, &mut A, (), std::array::IntoIter>` to implement `Iterator` diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr index 4be9e86f8f15d..7d952a9324877 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/system_param_derive_readonly.stderr @@ -13,14 +13,14 @@ error[E0277]: the trait bound `&'static mut Foo: ReadOnlyWorldQuery` is not sati | ^^^^^^^ the trait `ReadOnlyWorldQuery` is not implemented for `&'static mut Foo` | = help: the following other types implement trait `ReadOnlyWorldQuery`: - &T - () - (F0, F1) - (F0, F1, F2) - (F0, F1, F2, F3) - (F0, F1, F2, F3, F4) - (F0, F1, F2, F3, F4, F5) - (F0, F1, F2, F3, F4, F5, F6) + bevy_ecs::change_detection::Ref<'__w, T> + Has + AnyOf<()> + AnyOf<(F0,)> + AnyOf<(F0, F1)> + AnyOf<(F0, F1, F2)> + AnyOf<(F0, F1, F2, F3)> + AnyOf<(F0, F1, F2, F3, F4)> and $N others = note: `ReadOnlyWorldQuery` is implemented for `&'static Foo`, but not for `&'static mut Foo` = note: required for `bevy_ecs::system::Query<'_, '_, &'static mut Foo>` to implement `ReadOnlySystemParam` diff --git a/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr b/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr index 158be3f9d7e05..830a130e50731 100644 --- a/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr +++ b/crates/bevy_ecs_compile_fail_tests/tests/ui/world_query_derive.stderr @@ -5,14 +5,14 @@ error[E0277]: the trait bound `&'static mut Foo: ReadOnlyWorldQuery` is not sati | ^^^^^^^^^^^^^^^^ the trait `ReadOnlyWorldQuery` is not implemented for `&'static mut Foo` | = help: the following other types implement trait `ReadOnlyWorldQuery`: - &T - () - (F0, F1) - (F0, F1, F2) - (F0, F1, F2, F3) - (F0, F1, F2, F3, F4) - (F0, F1, F2, F3, F4, F5) - (F0, F1, F2, F3, F4, F5, F6) + MutableUnmarked + MutableMarkedReadOnly + NestedMutableUnmarked + bevy_ecs::change_detection::Ref<'__w, T> + Has + AnyOf<()> + AnyOf<(F0,)> + AnyOf<(F0, F1)> and $N others note: required by a bound in `_::assert_readonly` --> tests/ui/world_query_derive.rs:7:10 @@ -28,14 +28,14 @@ error[E0277]: the trait bound `MutableMarked: ReadOnlyWorldQuery` is not satisfi | ^^^^^^^^^^^^^ the trait `ReadOnlyWorldQuery` is not implemented for `MutableMarked` | = help: the following other types implement trait `ReadOnlyWorldQuery`: - &T - () - (F0, F1) - (F0, F1, F2) - (F0, F1, F2, F3) - (F0, F1, F2, F3, F4) - (F0, F1, F2, F3, F4, F5) - (F0, F1, F2, F3, F4, F5, F6) + MutableUnmarked + MutableMarkedReadOnly + NestedMutableUnmarked + bevy_ecs::change_detection::Ref<'__w, T> + Has + AnyOf<()> + AnyOf<(F0,)> + AnyOf<(F0, F1)> and $N others note: required by a bound in `_::assert_readonly` --> tests/ui/world_query_derive.rs:18:10 diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index a7e7769a58252..c2258bc0673f3 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -152,7 +152,7 @@ impl<'s> Gizmos<'s> { /// ``` #[inline] pub fn linestrip(&mut self, positions: impl IntoIterator, color: Color) { - self.extend_strip_positions(positions.into_iter()); + self.extend_strip_positions(positions); let len = self.buffer.strip_positions.len(); self.buffer .strip_colors diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 5d21be330a2cb..22ca7291c38ff 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -96,7 +96,9 @@ impl Plugin for GizmoPlugin { .after(TransformSystem::TransformPropagate), ); - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; render_app .add_systems(ExtractSchedule, extract_gizmo_data) @@ -109,7 +111,9 @@ impl Plugin for GizmoPlugin { } fn finish(&self, app: &mut bevy_app::App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; let render_device = render_app.world.resource::(); let layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { diff --git a/crates/bevy_gizmos/src/pipeline_2d.rs b/crates/bevy_gizmos/src/pipeline_2d.rs index 06f21138d8a55..128998b0b834b 100644 --- a/crates/bevy_gizmos/src/pipeline_2d.rs +++ b/crates/bevy_gizmos/src/pipeline_2d.rs @@ -27,7 +27,9 @@ pub struct LineGizmo2dPlugin; impl Plugin for LineGizmo2dPlugin { fn build(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; render_app .add_render_command::() @@ -36,7 +38,9 @@ impl Plugin for LineGizmo2dPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; render_app.init_resource::(); } @@ -151,7 +155,9 @@ fn queue_line_gizmos_2d( | Mesh2dPipelineKey::from_hdr(view.hdr); for (entity, handle) in &line_gizmos { - let Some(line_gizmo) = line_gizmo_assets.get(handle) else { continue }; + let Some(line_gizmo) = line_gizmo_assets.get(handle) else { + continue; + }; let pipeline = pipelines.specialize( &pipeline_cache, diff --git a/crates/bevy_gizmos/src/pipeline_3d.rs b/crates/bevy_gizmos/src/pipeline_3d.rs index 14f033f30455f..9d015f0f047f7 100644 --- a/crates/bevy_gizmos/src/pipeline_3d.rs +++ b/crates/bevy_gizmos/src/pipeline_3d.rs @@ -25,7 +25,9 @@ use bevy_render::{ pub struct LineGizmo3dPlugin; impl Plugin for LineGizmo3dPlugin { fn build(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; render_app .add_render_command::() @@ -34,7 +36,9 @@ impl Plugin for LineGizmo3dPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; render_app.init_resource::(); } @@ -164,7 +168,9 @@ fn queue_line_gizmos_3d( | MeshPipelineKey::from_hdr(view.hdr); for (entity, handle) in &line_gizmos { - let Some(line_gizmo) = line_gizmo_assets.get(handle) else { continue }; + let Some(line_gizmo) = line_gizmo_assets.get(handle) else { + continue; + }; let pipeline = pipelines.specialize( &pipeline_cache, diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines.rs index c0ff6cf272f72..544e815878c53 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines.rs @@ -133,10 +133,7 @@ impl Hermite

{ tangents: impl IntoIterator, ) -> Self { Self { - control_points: control_points - .into_iter() - .zip(tangents.into_iter()) - .collect(), + control_points: control_points.into_iter().zip(tangents).collect(), } } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index bfdd3ff0032cd..c402b30e0aa6f 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -809,7 +809,9 @@ pub fn queue_prepass_material_meshes( let rangefinder = view.rangefinder3d(); for visible_entity in &visible_entities.entities { - let Ok((material_handle, mesh_handle, mesh_transforms, batch_indices)) = material_meshes.get(*visible_entity) else { + let Ok((material_handle, mesh_handle, mesh_transforms, batch_indices)) = + material_meshes.get(*visible_entity) + else { continue; }; diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 276ee261a5e01..e2d016b95738a 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -86,7 +86,9 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { } fn finish(&self, app: &mut App) { - let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { return }; + let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; if !render_app .world @@ -226,7 +228,8 @@ impl ViewNode for SsaoNode { pipeline_cache.get_compute_pipeline(pipelines.preprocess_depth_pipeline), pipeline_cache.get_compute_pipeline(pipelines.spatial_denoise_pipeline), pipeline_cache.get_compute_pipeline(pipeline_id.0), - ) else { + ) + else { return Ok(()); }; @@ -640,7 +643,9 @@ fn prepare_ssao_textures( views: Query<(Entity, &ExtractedCamera), With>, ) { for (entity, camera) in &views { - let Some(physical_viewport_size) = camera.physical_viewport_size else { continue }; + let Some(physical_viewport_size) = camera.physical_viewport_size else { + continue; + }; let size = Extent3d { width: physical_viewport_size.x, height: physical_viewport_size.y, diff --git a/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs b/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs index ada309aacfde4..27a298044ebaa 100644 --- a/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs +++ b/crates/bevy_reflect/bevy_reflect_derive/src/derive_data.rs @@ -195,7 +195,8 @@ impl<'a> ReflectDerive<'a> { let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. - }) = &pair.value else { + }) = &pair.value + else { return Err(syn::Error::new( pair.span(), format_args!("`#[{TYPE_PATH_ATTRIBUTE_NAME} = \"...\"]` must be a string literal"), @@ -211,7 +212,8 @@ impl<'a> ReflectDerive<'a> { let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. - }) = &pair.value else { + }) = &pair.value + else { return Err(syn::Error::new( pair.span(), format_args!("`#[{TYPE_NAME_ATTRIBUTE_NAME} = \"...\"]` must be a string literal"), diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index e31f4280c2aa6..0ddc85a562763 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -513,7 +513,7 @@ mod tests { #[test] fn test_into_iter() { - let expected = vec!["foo", "bar", "baz"]; + let expected = ["foo", "bar", "baz"]; let mut map = DynamicMap::default(); map.insert(0usize, expected[0].to_string()); diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index 36e3aaa56a830..f2bb3a4563fd1 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -507,7 +507,7 @@ impl Debug for DynamicStruct { /// Returns [`None`] if the comparison couldn't even be performed. #[inline] pub fn struct_partial_eq(a: &S, b: &dyn Reflect) -> Option { - let ReflectRef::Struct(struct_value) = b.reflect_ref() else { + let ReflectRef::Struct(struct_value) = b.reflect_ref() else { return Some(false); }; diff --git a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr b/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr index 2c503d8a0188c..40d36dcf977ae 100644 --- a/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr +++ b/crates/bevy_reflect_compile_fail_tests/tests/reflect_derive/generics.fail.stderr @@ -11,14 +11,14 @@ error[E0277]: the trait bound `NoReflect: Reflect` is not satisfied | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Reflect` is not implemented for `NoReflect` | = help: the following other types implement trait `Reflect`: - &'static Path - () - (A, B) - (A, B, C) - (A, B, C, D) - (A, B, C, D, E) - (A, B, C, D, E, F) - (A, B, C, D, E, F, G) + bool + char + isize + i8 + i16 + i32 + i64 + i128 and $N others note: required for `Foo` to implement `Reflect` --> tests/reflect_derive/generics.fail.rs:3:10 diff --git a/crates/bevy_render/src/color/mod.rs b/crates/bevy_render/src/color/mod.rs index c5839b7cd4266..deb642a131396 100644 --- a/crates/bevy_render/src/color/mod.rs +++ b/crates/bevy_render/src/color/mod.rs @@ -1990,8 +1990,14 @@ mod tests { let hsla = Color::hsla(0., 0., 0., 0.); let lcha = Color::lcha(0., 0., 0., 0.); assert_eq!(rgba_l, rgba_l.as_rgba_linear()); - let Color::RgbaLinear { .. } = rgba.as_rgba_linear() else { panic!("from Rgba") }; - let Color::RgbaLinear { .. } = hsla.as_rgba_linear() else { panic!("from Hsla") }; - let Color::RgbaLinear { .. } = lcha.as_rgba_linear() else { panic!("from Lcha") }; + let Color::RgbaLinear { .. } = rgba.as_rgba_linear() else { + panic!("from Rgba") + }; + let Color::RgbaLinear { .. } = hsla.as_rgba_linear() else { + panic!("from Hsla") + }; + let Color::RgbaLinear { .. } = lcha.as_rgba_linear() else { + panic!("from Lcha") + }; } } diff --git a/crates/bevy_render/src/mesh/morph.rs b/crates/bevy_render/src/mesh/morph.rs index be52d1f59b495..cb523113be231 100644 --- a/crates/bevy_render/src/mesh/morph.rs +++ b/crates/bevy_render/src/mesh/morph.rs @@ -74,8 +74,11 @@ impl MorphTargetImage { return Err(MorphBuildError::TooManyTargets { target_count }); } let component_count = (vertex_count * MorphAttributes::COMPONENT_COUNT) as u32; - let Some((Rect(width, height), padding)) = lowest_2d(component_count , max) else { - return Err(MorphBuildError::TooManyAttributes { vertex_count, component_count }); + let Some((Rect(width, height), padding)) = lowest_2d(component_count, max) else { + return Err(MorphBuildError::TooManyAttributes { + vertex_count, + component_count, + }); }; let data = targets .flat_map(|mut attributes| { diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index 0e2417e51d730..f8fcfe53e978e 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -111,7 +111,9 @@ impl Plugin for PipelinedRenderingPlugin { s.spawn(async { app_to_render_receiver.recv().await }); }) .pop(); - let Some(Ok(mut render_app)) = sent_app else { break }; + let Some(Ok(mut render_app)) = sent_app else { + break; + }; { #[cfg(feature = "trace")] diff --git a/crates/bevy_render/src/render_graph/graph.rs b/crates/bevy_render/src/render_graph/graph.rs index 87fc063ee13f9..e0bdc9283ce1d 100644 --- a/crates/bevy_render/src/render_graph/graph.rs +++ b/crates/bevy_render/src/render_graph/graph.rs @@ -125,7 +125,9 @@ impl RenderGraph { /// It simply won't create a new edge. pub fn add_node_edges(&mut self, edges: &[&'static str]) { for window in edges.windows(2) { - let [a, b] = window else { break; }; + let [a, b] = window else { + break; + }; if let Err(err) = self.try_add_node_edge(*a, *b) { match err { // Already existing edges are very easy to produce with this api diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 9307c235a062e..0e4630e3c72ef 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -398,10 +398,9 @@ where render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let Ok(view) = self - .view_query - .get_manual(world, graph.view_entity()) - else { return Ok(()); }; + let Ok(view) = self.view_query.get_manual(world, graph.view_entity()) else { + return Ok(()); + }; ViewNode::run(&self.node, graph, render_context, view, world)?; Ok(()) diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 90e7b6059cecc..80985b2f26db2 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -49,10 +49,10 @@ impl Node for UiPassNode { let input_view_entity = graph.view_entity(); let Ok((transparent_phase, target, camera_ui)) = - self.ui_view_query.get_manual(world, input_view_entity) - else { - return Ok(()); - }; + self.ui_view_query.get_manual(world, input_view_entity) + else { + return Ok(()); + }; if transparent_phase.items.is_empty() { return Ok(()); } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 504c70ac43ab6..8c79494dec350 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -422,20 +422,20 @@ pub fn winit_runner(mut app: App) { event_writer_system_state.get_mut(&mut app.world); let Some(window_entity) = winit_windows.get_window_entity(window_id) else { - warn!( - "Skipped event {:?} for unknown winit Window Id {:?}", - event, window_id - ); - return; - }; + warn!( + "Skipped event {:?} for unknown winit Window Id {:?}", + event, window_id + ); + return; + }; let Ok((mut window, mut cache)) = windows.get_mut(window_entity) else { - warn!( - "Window {:?} is missing `Window` component, skipping event {:?}", - window_entity, event - ); - return; - }; + warn!( + "Window {:?} is missing `Window` component, skipping event {:?}", + window_entity, event + ); + return; + }; runner_state.window_event_received = true; diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index b1f6cb21137fc..14fda578398d4 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -56,7 +56,10 @@ fn setup( for handle in &rpg_sprite_handles.handles { let handle = handle.typed_weak(); let Some(texture) = textures.get(&handle) else { - warn!("{:?} did not resolve to an `Image` asset.", asset_server.get_handle_path(handle)); + warn!( + "{:?} did not resolve to an `Image` asset.", + asset_server.get_handle_path(handle) + ); continue; }; diff --git a/examples/3d/bloom_3d.rs b/examples/3d/bloom_3d.rs index 7c6f14052630c..4b68878fcdfc0 100644 --- a/examples/3d/bloom_3d.rs +++ b/examples/3d/bloom_3d.rs @@ -75,7 +75,7 @@ fn setup_scene( 0 => material_emissive1.clone(), 1 => material_emissive2.clone(), 2 => material_emissive3.clone(), - 3 | 4 | 5 => material_non_emissive.clone(), + 3..=5 => material_non_emissive.clone(), _ => unreachable!(), }; diff --git a/examples/animation/morph_targets.rs b/examples/animation/morph_targets.rs index e773fb6cf9b32..83f216fc33dc7 100644 --- a/examples/animation/morph_targets.rs +++ b/examples/animation/morph_targets.rs @@ -88,8 +88,12 @@ fn name_morphs( return; } - let Some(mesh) = meshes.get(&morph_data.mesh) else { return }; - let Some(names) = mesh.morph_target_names() else { return }; + let Some(mesh) = meshes.get(&morph_data.mesh) else { + return; + }; + let Some(names) = mesh.morph_target_names() else { + return; + }; for name in names { println!(" {name}"); } diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index d582856cc52dc..739caf92a1952 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -249,11 +249,6 @@ fn setup( commands.spawn(WallBundle::new(WallLocation::Top)); // Bricks - // Negative scales result in flipped sprites / meshes, - // which is definitely not what we want here - assert!(BRICK_SIZE.x > 0.0); - assert!(BRICK_SIZE.y > 0.0); - let total_width_of_bricks = (RIGHT_WALL - LEFT_WALL) - 2. * GAP_BETWEEN_BRICKS_AND_SIDES; let bottom_edge_of_bricks = paddle_y + GAP_BETWEEN_PADDLE_AND_BRICKS; let total_height_of_bricks = TOP_WALL - bottom_edge_of_bricks - GAP_BETWEEN_BRICKS_AND_CEILING; diff --git a/examples/shader/post_processing.rs b/examples/shader/post_processing.rs index 88d197ae72bf7..7aed8e8f85d8c 100644 --- a/examples/shader/post_processing.rs +++ b/examples/shader/post_processing.rs @@ -158,7 +158,8 @@ impl ViewNode for PostProcessNode { let pipeline_cache = world.resource::(); // Get the pipeline from the cache - let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id) else { + let Some(pipeline) = pipeline_cache.get_render_pipeline(post_process_pipeline.pipeline_id) + else { return Ok(()); }; diff --git a/examples/shader/texture_binding_array.rs b/examples/shader/texture_binding_array.rs index c4affd57c1d2f..90bf00afc705b 100644 --- a/examples/shader/texture_binding_array.rs +++ b/examples/shader/texture_binding_array.rs @@ -37,7 +37,7 @@ impl Plugin for GpuFeatureSupportChecker { fn finish(&self, app: &mut App) { let Ok(render_app) = app.get_sub_app_mut(RenderApp) else { - return + return; }; let render_device = render_app.world.resource::(); diff --git a/examples/stress_tests/many_gizmos.rs b/examples/stress_tests/many_gizmos.rs index 472b25d5e4028..f0a72f451e2db 100644 --- a/examples/stress_tests/many_gizmos.rs +++ b/examples/stress_tests/many_gizmos.rs @@ -91,7 +91,10 @@ fn setup(mut commands: Commands) { fn ui_system(mut query: Query<&mut Text>, config: Res, diag: Res) { let mut text = query.single_mut(); - let Some(fps) = diag.get(FrameTimeDiagnosticsPlugin::FPS).and_then(|fps| fps.smoothed()) else { + let Some(fps) = diag + .get(FrameTimeDiagnosticsPlugin::FPS) + .and_then(|fps| fps.smoothed()) + else { return; }; diff --git a/examples/tools/scene_viewer/morph_viewer_plugin.rs b/examples/tools/scene_viewer/morph_viewer_plugin.rs index e71a41db95fbf..b82bef0f20dda 100644 --- a/examples/tools/scene_viewer/morph_viewer_plugin.rs +++ b/examples/tools/scene_viewer/morph_viewer_plugin.rs @@ -181,7 +181,9 @@ fn update_text( mut text: Query<&mut Text>, morphs: Query<&MorphWeights>, ) { - let Some(mut controls) = controls else { return; }; + let Some(mut controls) = controls else { + return; + }; for (i, target) in controls.weights.iter_mut().enumerate() { let Ok(weights) = morphs.get(target.entity) else { continue; @@ -203,7 +205,9 @@ fn update_morphs( input: Res>, time: Res

for SetItemPipeline { } } -/// A [`PhaseItem`] that can be batched dynamically. -/// -/// Batching is an optimization that regroups multiple items in the same vertex buffer -/// to render them in a single draw call. -/// -/// If this is implemented on a type, the implementation of [`PhaseItem::sort`] should -/// be changed to implement a stable sort, or incorrect/suboptimal batching may result. -pub trait BatchedPhaseItem: PhaseItem { - /// Range in the vertex buffer of this item. - fn batch_range(&self) -> &Option>; - - /// Range in the vertex buffer of this item. - fn batch_range_mut(&mut self) -> &mut Option>; - - /// Batches another item within this item if they are compatible. - /// Items can be batched together if they have the same entity, and consecutive ranges. - /// If batching is successful, the `other` item should be discarded from the render pass. - #[inline] - fn add_to_batch(&mut self, other: &Self) -> BatchResult { - let self_entity = self.entity(); - if let (Some(self_batch_range), Some(other_batch_range)) = ( - self.batch_range_mut().as_mut(), - other.batch_range().as_ref(), - ) { - // If the items are compatible, join their range into `self` - if self_entity == other.entity() { - if self_batch_range.end == other_batch_range.start { - self_batch_range.end = other_batch_range.end; - return BatchResult::Success; - } else if self_batch_range.start == other_batch_range.end { - self_batch_range.start = other_batch_range.start; - return BatchResult::Success; - } - } - } - BatchResult::IncompatibleItems - } -} - -/// The result of a batching operation. -pub enum BatchResult { - /// The `other` item was batched into `self` - Success, - /// `self` and `other` cannot be batched together - IncompatibleItems, -} - /// This system sorts the [`PhaseItem`]s of all [`RenderPhase`]s of this type. pub fn sort_phase_system(mut render_phases: Query<&mut RenderPhase>) { for mut phase in &mut render_phases { phase.sort(); } } - -/// This system batches the [`PhaseItem`]s of all [`RenderPhase`]s of this type. -pub fn batch_phase_system(mut render_phases: Query<&mut RenderPhase>) { - for mut phase in &mut render_phases { - phase.batch(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bevy_ecs::entity::Entity; - use std::ops::Range; - - #[test] - fn batching() { - #[derive(Debug, PartialEq)] - struct TestPhaseItem { - entity: Entity, - batch_range: Option>, - } - impl PhaseItem for TestPhaseItem { - type SortKey = (); - - fn entity(&self) -> Entity { - self.entity - } - - fn sort_key(&self) -> Self::SortKey {} - - fn draw_function(&self) -> DrawFunctionId { - unimplemented!(); - } - } - impl BatchedPhaseItem for TestPhaseItem { - fn batch_range(&self) -> &Option> { - &self.batch_range - } - - fn batch_range_mut(&mut self) -> &mut Option> { - &mut self.batch_range - } - } - let mut render_phase = RenderPhase::::default(); - let items = [ - TestPhaseItem { - entity: Entity::from_raw(0), - batch_range: Some(0..5), - }, - // This item should be batched - TestPhaseItem { - entity: Entity::from_raw(0), - batch_range: Some(5..10), - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(0..5), - }, - TestPhaseItem { - entity: Entity::from_raw(0), - batch_range: Some(10..15), - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(5..10), - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: None, - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(10..15), - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(20..25), - }, - // This item should be batched - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(25..30), - }, - // This item should be batched - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(30..35), - }, - ]; - for item in items { - render_phase.add(item); - } - render_phase.batch(); - let items_batched = [ - TestPhaseItem { - entity: Entity::from_raw(0), - batch_range: Some(0..10), - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(0..5), - }, - TestPhaseItem { - entity: Entity::from_raw(0), - batch_range: Some(10..15), - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(5..10), - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: None, - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(10..15), - }, - TestPhaseItem { - entity: Entity::from_raw(1), - batch_range: Some(20..35), - }, - ]; - assert_eq!(&*render_phase.items, items_batched); - } -} diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 350e31338889f..c086fff99262a 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -31,9 +31,7 @@ pub use image_texture_loader::*; pub use texture_cache::*; use crate::{ - render_asset::{PrepareAssetSet, RenderAssetPlugin}, - renderer::RenderDevice, - Render, RenderApp, RenderSet, + render_asset::RenderAssetPlugin, renderer::RenderDevice, Render, RenderApp, RenderSet, }; use bevy_app::{App, Plugin}; use bevy_asset::{AddAsset, Assets}; @@ -80,12 +78,10 @@ impl Plugin for ImagePlugin { app.init_asset_loader::(); } - app.add_plugins(RenderAssetPlugin::::with_prepare_asset_set( - PrepareAssetSet::PreAssetPrepare, - )) - .register_type::() - .add_asset::() - .register_asset_reflect::(); + app.add_plugins(RenderAssetPlugin::::default()) + .register_type::() + .add_asset::() + .register_asset_reflect::(); app.world .resource_mut::>() .set_untracked(DEFAULT_IMAGE_HANDLE, Image::default()); diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 9167279081b2d..487c3ef415479 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -53,19 +53,16 @@ impl Plugin for ViewPlugin { .add_plugins((ExtractResourcePlugin::::default(), VisibilityPlugin)); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::() - .configure_set(Render, ViewSet::PrepareUniforms.in_set(RenderSet::Prepare)) - .add_systems( - Render, - ( - prepare_view_uniforms.in_set(ViewSet::PrepareUniforms), - prepare_view_targets - .after(WindowSystem::Prepare) - .in_set(RenderSet::Prepare) - .after(crate::render_asset::prepare_assets::), - ), - ); + render_app.init_resource::().add_systems( + Render, + ( + prepare_view_targets + .in_set(RenderSet::ManageViews) + .after(prepare_windows) + .after(crate::render_asset::prepare_assets::), + prepare_view_uniforms.in_set(RenderSet::PrepareResources), + ), + ); } } } @@ -517,10 +514,3 @@ fn prepare_view_targets( } } } - -/// System sets for the [`view`](crate::view) module. -#[derive(SystemSet, PartialEq, Eq, Hash, Debug, Clone)] -pub enum ViewSet { - /// Prepares view uniforms - PrepareUniforms, -} diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index b8e77f519767c..911214ecbaf01 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -27,11 +27,6 @@ pub struct NonSendMarker; pub struct WindowRenderPlugin; -#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] -pub enum WindowSystem { - Prepare, -} - impl Plugin for WindowRenderPlugin { fn build(&self, app: &mut App) { app.add_plugins(ScreenshotPlugin); @@ -42,8 +37,7 @@ impl Plugin for WindowRenderPlugin { .init_resource::() .init_non_send_resource::() .add_systems(ExtractSchedule, extract_windows) - .configure_set(Render, WindowSystem::Prepare.in_set(RenderSet::Prepare)) - .add_systems(Render, prepare_windows.in_set(WindowSystem::Prepare)); + .add_systems(Render, prepare_windows.in_set(RenderSet::ManageViews)); } } diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 545cd19d4768e..72038b0427442 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -91,9 +91,12 @@ impl Plugin for SpritePlugin { ) .add_systems( Render, - queue_sprites - .in_set(RenderSet::Queue) - .ambiguous_with(queue_material2d_meshes::), + ( + queue_sprites + .in_set(RenderSet::Queue) + .ambiguous_with(queue_material2d_meshes::), + prepare_sprites.in_set(RenderSet::PrepareBindGroups), + ), ); }; } diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 2ba6765e720df..ffe8ae670e0cd 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -19,7 +19,7 @@ use bevy_render::{ extract_component::ExtractComponentPlugin, mesh::{Mesh, MeshVertexBufferLayout}, prelude::Image, - render_asset::{PrepareAssetSet, RenderAssets}, + render_asset::{prepare_assets, RenderAssets}, render_phase::{ AddRenderCommand, DrawFunctions, PhaseItem, RenderCommand, RenderCommandResult, RenderPhase, SetItemPipeline, TrackedRenderPass, @@ -165,9 +165,11 @@ where Render, ( prepare_materials_2d:: - .in_set(RenderSet::Prepare) - .after(PrepareAssetSet::PreAssetPrepare), - queue_material2d_meshes::.in_set(RenderSet::Queue), + .in_set(RenderSet::PrepareAssets) + .after(prepare_assets::), + queue_material2d_meshes:: + .in_set(RenderSet::QueueMeshes) + .after(prepare_materials_2d::), ), ); } @@ -415,7 +417,7 @@ pub fn queue_material2d_meshes( // camera. As such we can just use mesh_z as the distance sort_key: FloatOrd(mesh_z), // This material is not batched - batch_range: None, + batch_size: 1, }); } } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 3e6c4ce068bca..e614c58e8495e 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -106,8 +106,8 @@ impl Plugin for Mesh2dRenderPlugin { .add_systems( Render, ( - queue_mesh2d_bind_group.in_set(RenderSet::Queue), - queue_mesh2d_view_bind_groups.in_set(RenderSet::Queue), + prepare_mesh2d_bind_group.in_set(RenderSet::PrepareBindGroups), + prepare_mesh2d_view_bind_groups.in_set(RenderSet::PrepareBindGroups), ), ); } @@ -480,7 +480,7 @@ pub struct Mesh2dBindGroup { pub value: BindGroup, } -pub fn queue_mesh2d_bind_group( +pub fn prepare_mesh2d_bind_group( mut commands: Commands, mesh2d_pipeline: Res, render_device: Res, @@ -505,7 +505,7 @@ pub struct Mesh2dViewBindGroup { pub value: BindGroup, } -pub fn queue_mesh2d_view_bind_groups( +pub fn prepare_mesh2d_view_bind_groups( mut commands: Commands, render_device: Res, mesh2d_pipeline: Res, diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 8bce8ac998ba4..f00f63f1a69a9 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,4 +1,4 @@ -use std::cmp::Ordering; +use std::ops::Range; use crate::{ texture_atlas::{TextureAtlas, TextureAtlasSprite}, @@ -11,16 +11,16 @@ use bevy_core_pipeline::{ }; use bevy_ecs::{ prelude::*, + storage::SparseSet, system::{lifetimeless::*, SystemParamItem, SystemState}, }; use bevy_math::{Rect, Vec2}; -use bevy_reflect::Uuid; use bevy_render::{ color::Color, render_asset::RenderAssets, render_phase::{ - BatchedPhaseItem, DrawFunctions, PhaseItem, RenderCommand, RenderCommandResult, - RenderPhase, SetItemPipeline, TrackedRenderPass, + DrawFunctions, PhaseItem, RenderCommand, RenderCommandResult, RenderPhase, SetItemPipeline, + TrackedRenderPass, }, render_resource::*, renderer::{RenderDevice, RenderQueue}, @@ -34,8 +34,7 @@ use bevy_render::{ Extract, }; use bevy_transform::components::GlobalTransform; -use bevy_utils::FloatOrd; -use bevy_utils::HashMap; +use bevy_utils::{FloatOrd, HashMap, Uuid}; use bytemuck::{Pod, Zeroable}; use fixedbitset::FixedBitSet; @@ -296,9 +295,7 @@ impl SpecializedRenderPipeline for SpritePipeline { } } -#[derive(Component, Clone, Copy)] pub struct ExtractedSprite { - pub entity: Entity, pub transform: GlobalTransform, pub color: Color, /// Select an area of the texture @@ -315,7 +312,7 @@ pub struct ExtractedSprite { #[derive(Resource, Default)] pub struct ExtractedSprites { - pub sprites: Vec, + pub sprites: SparseSet, } #[derive(Resource, Default)] @@ -368,24 +365,25 @@ pub fn extract_sprites( )>, >, ) { - extracted_sprites.sprites.clear(); for (entity, visibility, sprite, transform, handle) in sprite_query.iter() { if !visibility.is_visible() { continue; } // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive - extracted_sprites.sprites.push(ExtractedSprite { + extracted_sprites.sprites.insert( entity, - color: sprite.color, - transform: *transform, - rect: sprite.rect, - // Pass the custom size - custom_size: sprite.custom_size, - flip_x: sprite.flip_x, - flip_y: sprite.flip_y, - image_handle_id: handle.id(), - anchor: sprite.anchor.as_vec(), - }); + ExtractedSprite { + color: sprite.color, + transform: *transform, + rect: sprite.rect, + // Pass the custom size + custom_size: sprite.custom_size, + flip_x: sprite.flip_x, + flip_y: sprite.flip_y, + image_handle_id: handle.id(), + anchor: sprite.anchor.as_vec(), + }, + ); } for (entity, visibility, atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() { if !visibility.is_visible() { @@ -404,19 +402,21 @@ pub fn extract_sprites( ) }), ); - extracted_sprites.sprites.push(ExtractedSprite { + extracted_sprites.sprites.insert( entity, - color: atlas_sprite.color, - transform: *transform, - // Select the area in the texture atlas - rect, - // Pass the custom size - custom_size: atlas_sprite.custom_size, - flip_x: atlas_sprite.flip_x, - flip_y: atlas_sprite.flip_y, - image_handle_id: texture_atlas.texture.id(), - anchor: atlas_sprite.anchor.as_vec(), - }); + ExtractedSprite { + color: atlas_sprite.color, + transform: *transform, + // Select the area in the texture atlas + rect, + // Pass the custom size + custom_size: atlas_sprite.custom_size, + flip_x: atlas_sprite.flip_x, + flip_y: atlas_sprite.flip_y, + image_handle_id: texture_atlas.texture.id(), + anchor: atlas_sprite.anchor.as_vec(), + }, + ); } } } @@ -469,8 +469,9 @@ const QUAD_UVS: [Vec2; 4] = [ Vec2::new(0., 0.), ]; -#[derive(Component, Eq, PartialEq, Copy, Clone)] +#[derive(Component)] pub struct SpriteBatch { + range: Range, image_handle_id: HandleId, colored: bool, } @@ -482,20 +483,13 @@ pub struct ImageBindGroups { #[allow(clippy::too_many_arguments)] pub fn queue_sprites( - mut commands: Commands, mut view_entities: Local, draw_functions: Res>, - render_device: Res, - render_queue: Res, - mut sprite_meta: ResMut, - view_uniforms: Res, sprite_pipeline: Res, mut pipelines: ResMut>, pipeline_cache: Res, - mut image_bind_groups: ResMut, - gpu_images: Res>, msaa: Res, - mut extracted_sprites: ResMut, + extracted_sprites: Res, mut views: Query<( &mut RenderPhase, &VisibleEntities, @@ -503,6 +497,100 @@ pub fn queue_sprites( Option<&Tonemapping>, Option<&DebandDither>, )>, +) { + let msaa_key = SpritePipelineKey::from_msaa_samples(msaa.samples()); + + let draw_sprite_function = draw_functions.read().id::(); + + for (mut transparent_phase, visible_entities, view, tonemapping, dither) in &mut views { + let mut view_key = SpritePipelineKey::from_hdr(view.hdr) | msaa_key; + + if !view.hdr { + if let Some(tonemapping) = tonemapping { + view_key |= SpritePipelineKey::TONEMAP_IN_SHADER; + view_key |= match tonemapping { + Tonemapping::None => SpritePipelineKey::TONEMAP_METHOD_NONE, + Tonemapping::Reinhard => SpritePipelineKey::TONEMAP_METHOD_REINHARD, + Tonemapping::ReinhardLuminance => { + SpritePipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE + } + Tonemapping::AcesFitted => SpritePipelineKey::TONEMAP_METHOD_ACES_FITTED, + Tonemapping::AgX => SpritePipelineKey::TONEMAP_METHOD_AGX, + Tonemapping::SomewhatBoringDisplayTransform => { + SpritePipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM + } + Tonemapping::TonyMcMapface => SpritePipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE, + Tonemapping::BlenderFilmic => SpritePipelineKey::TONEMAP_METHOD_BLENDER_FILMIC, + }; + } + if let Some(DebandDither::Enabled) = dither { + view_key |= SpritePipelineKey::DEBAND_DITHER; + } + } + + let pipeline = pipelines.specialize( + &pipeline_cache, + &sprite_pipeline, + view_key | SpritePipelineKey::from_colored(false), + ); + let colored_pipeline = pipelines.specialize( + &pipeline_cache, + &sprite_pipeline, + view_key | SpritePipelineKey::from_colored(true), + ); + + view_entities.clear(); + view_entities.extend(visible_entities.entities.iter().map(|e| e.index() as usize)); + + transparent_phase + .items + .reserve(extracted_sprites.sprites.len()); + + for (entity, extracted_sprite) in extracted_sprites.sprites.iter() { + if !view_entities.contains(entity.index() as usize) { + continue; + } + + // These items will be sorted by depth with other phase items + let sort_key = FloatOrd(extracted_sprite.transform.translation().z); + + // Add the item to the render phase + if extracted_sprite.color != Color::WHITE { + transparent_phase.add(Transparent2d { + draw_function: draw_sprite_function, + pipeline: colored_pipeline, + entity: *entity, + sort_key, + // batch_size will be calculated in prepare_sprites + batch_size: 0, + }); + } else { + transparent_phase.add(Transparent2d { + draw_function: draw_sprite_function, + pipeline, + entity: *entity, + sort_key, + // batch_size will be calculated in prepare_sprites + batch_size: 0, + }); + } + } + } +} + +#[allow(clippy::too_many_arguments)] +pub fn prepare_sprites( + mut commands: Commands, + mut previous_len: Local, + render_device: Res, + render_queue: Res, + mut sprite_meta: ResMut, + view_uniforms: Res, + sprite_pipeline: Res, + mut image_bind_groups: ResMut, + gpu_images: Res>, + mut extracted_sprites: ResMut, + mut phases: Query<&mut RenderPhase>, events: Res, ) { // If an image has changed, the GpuImage has (probably) changed @@ -515,9 +603,8 @@ pub fn queue_sprites( }; } - let msaa_key = SpritePipelineKey::from_msaa_samples(msaa.samples()); - if let Some(view_binding) = view_uniforms.uniforms.binding() { + let mut batches: Vec<(Entity, SpriteBatch)> = Vec::with_capacity(*previous_len); let sprite_meta = &mut sprite_meta; // Clear the vertex buffers @@ -533,210 +620,141 @@ pub fn queue_sprites( layout: &sprite_pipeline.view_layout, })); - let draw_sprite_function = draw_functions.read().id::(); - // Vertex buffer indices let mut index = 0; let mut colored_index = 0; - // FIXME: VisibleEntities is ignored - - let extracted_sprites = &mut extracted_sprites.sprites; - // Sort sprites by z for correct transparency and then by handle to improve batching - // NOTE: This can be done independent of views by reasonably assuming that all 2D views look along the negative-z axis in world space - extracted_sprites.sort_unstable_by(|a, b| { - match a - .transform - .translation() - .z - .partial_cmp(&b.transform.translation().z) - { - Some(Ordering::Equal) | None => a.image_handle_id.cmp(&b.image_handle_id), - Some(other) => other, - } - }); let image_bind_groups = &mut *image_bind_groups; - for (mut transparent_phase, visible_entities, view, tonemapping, dither) in &mut views { - let mut view_key = SpritePipelineKey::from_hdr(view.hdr) | msaa_key; - - if !view.hdr { - if let Some(tonemapping) = tonemapping { - view_key |= SpritePipelineKey::TONEMAP_IN_SHADER; - view_key |= match tonemapping { - Tonemapping::None => SpritePipelineKey::TONEMAP_METHOD_NONE, - Tonemapping::Reinhard => SpritePipelineKey::TONEMAP_METHOD_REINHARD, - Tonemapping::ReinhardLuminance => { - SpritePipelineKey::TONEMAP_METHOD_REINHARD_LUMINANCE - } - Tonemapping::AcesFitted => SpritePipelineKey::TONEMAP_METHOD_ACES_FITTED, - Tonemapping::AgX => SpritePipelineKey::TONEMAP_METHOD_AGX, - Tonemapping::SomewhatBoringDisplayTransform => { - SpritePipelineKey::TONEMAP_METHOD_SOMEWHAT_BORING_DISPLAY_TRANSFORM - } - Tonemapping::TonyMcMapface => { - SpritePipelineKey::TONEMAP_METHOD_TONY_MC_MAPFACE - } - Tonemapping::BlenderFilmic => { - SpritePipelineKey::TONEMAP_METHOD_BLENDER_FILMIC - } - }; - } - if let Some(DebandDither::Enabled) = dither { - view_key |= SpritePipelineKey::DEBAND_DITHER; - } - } - - let pipeline = pipelines.specialize( - &pipeline_cache, - &sprite_pipeline, - view_key | SpritePipelineKey::from_colored(false), - ); - let colored_pipeline = pipelines.specialize( - &pipeline_cache, - &sprite_pipeline, - view_key | SpritePipelineKey::from_colored(true), - ); - - view_entities.clear(); - view_entities.extend(visible_entities.entities.iter().map(|e| e.index() as usize)); - transparent_phase.items.reserve(extracted_sprites.len()); + for mut transparent_phase in &mut phases { + let mut batch_item_index = 0; + let mut batch_image_size = Vec2::ZERO; + let mut batch_image_handle = HandleId::Id(Uuid::nil(), u64::MAX); + let mut batch_colored = false; - // Impossible starting values that will be replaced on the first iteration - let mut current_batch = SpriteBatch { - image_handle_id: HandleId::Id(Uuid::nil(), u64::MAX), - colored: false, - }; - let mut current_batch_entity = Entity::PLACEHOLDER; - let mut current_image_size = Vec2::ZERO; - // Add a phase item for each sprite, and detect when successive items can be batched. + // Iterate through the phase items and detect when successive sprites that can be batched. // Spawn an entity with a `SpriteBatch` component for each possible batch. // Compatible items share the same entity. - // Batches are merged later (in `batch_phase_system()`), so that they can be interrupted - // by any other phase item (and they can interrupt other items from batching). - for extracted_sprite in extracted_sprites.iter() { - if !view_entities.contains(extracted_sprite.entity.index() as usize) { - continue; - } - let new_batch = SpriteBatch { - image_handle_id: extracted_sprite.image_handle_id, - colored: extracted_sprite.color != Color::WHITE, - }; - if new_batch != current_batch { - // Set-up a new possible batch - if let Some(gpu_image) = - gpu_images.get(&Handle::weak(new_batch.image_handle_id)) - { - current_batch = new_batch; - current_image_size = Vec2::new(gpu_image.size.x, gpu_image.size.y); - current_batch_entity = commands.spawn(current_batch).id(); - - image_bind_groups - .values - .entry(Handle::weak(current_batch.image_handle_id)) - .or_insert_with(|| { - render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView( - &gpu_image.texture_view, - ), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&gpu_image.sampler), - }, - ], - label: Some("sprite_material_bind_group"), - layout: &sprite_pipeline.material_layout, - }) - }); - } else { - // Skip this item if the texture is not ready - continue; - } - } + for item_index in 0..transparent_phase.items.len() { + let item = &mut transparent_phase.items[item_index]; + if let Some(extracted_sprite) = extracted_sprites.sprites.get(item.entity) { + // Take a reference to an existing compatible batch if one exists + let mut existing_batch = batches.last_mut().filter(|_| { + batch_image_handle == extracted_sprite.image_handle_id + && batch_colored == (extracted_sprite.color != Color::WHITE) + }); - // Calculate vertex data for this item + if existing_batch.is_none() { + if let Some(gpu_image) = + gpu_images.get(&Handle::weak(extracted_sprite.image_handle_id)) + { + batch_item_index = item_index; + batch_image_size = Vec2::new(gpu_image.size.x, gpu_image.size.y); + batch_image_handle = extracted_sprite.image_handle_id; + batch_colored = extracted_sprite.color != Color::WHITE; + + let new_batch = SpriteBatch { + range: if batch_colored { + colored_index..colored_index + } else { + index..index + }, + colored: batch_colored, + image_handle_id: batch_image_handle, + }; + + batches.push((item.entity, new_batch)); + + image_bind_groups + .values + .entry(Handle::weak(batch_image_handle)) + .or_insert_with(|| { + render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView( + &gpu_image.texture_view, + ), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler( + &gpu_image.sampler, + ), + }, + ], + label: Some("sprite_material_bind_group"), + layout: &sprite_pipeline.material_layout, + }) + }); + existing_batch = batches.last_mut(); + } else { + continue; + } + } - let mut uvs = QUAD_UVS; - if extracted_sprite.flip_x { - uvs = [uvs[1], uvs[0], uvs[3], uvs[2]]; - } - if extracted_sprite.flip_y { - uvs = [uvs[3], uvs[2], uvs[1], uvs[0]]; - } + // Calculate vertex data for this item + let mut uvs = QUAD_UVS; + if extracted_sprite.flip_x { + uvs = [uvs[1], uvs[0], uvs[3], uvs[2]]; + } + if extracted_sprite.flip_y { + uvs = [uvs[3], uvs[2], uvs[1], uvs[0]]; + } - // By default, the size of the quad is the size of the texture - let mut quad_size = current_image_size; + // By default, the size of the quad is the size of the texture + let mut quad_size = batch_image_size; - // If a rect is specified, adjust UVs and the size of the quad - if let Some(rect) = extracted_sprite.rect { - let rect_size = rect.size(); - for uv in &mut uvs { - *uv = (rect.min + *uv * rect_size) / current_image_size; + // If a rect is specified, adjust UVs and the size of the quad + if let Some(rect) = extracted_sprite.rect { + let rect_size = rect.size(); + for uv in &mut uvs { + *uv = (rect.min + *uv * rect_size) / batch_image_size; + } + quad_size = rect_size; } - quad_size = rect_size; - } - // Override the size if a custom one is specified - if let Some(custom_size) = extracted_sprite.custom_size { - quad_size = custom_size; - } + // Override the size if a custom one is specified + if let Some(custom_size) = extracted_sprite.custom_size { + quad_size = custom_size; + } - // Apply size and global transform - let positions = QUAD_VERTEX_POSITIONS.map(|quad_pos| { - extracted_sprite - .transform - .transform_point( - ((quad_pos - extracted_sprite.anchor) * quad_size).extend(0.), - ) - .into() - }); + // Apply size and global transform + let positions = QUAD_VERTEX_POSITIONS.map(|quad_pos| { + extracted_sprite + .transform + .transform_point( + ((quad_pos - extracted_sprite.anchor) * quad_size).extend(0.), + ) + .into() + }); - // These items will be sorted by depth with other phase items - let sort_key = FloatOrd(extracted_sprite.transform.translation().z); - - // Store the vertex data and add the item to the render phase - if current_batch.colored { - let vertex_color = extracted_sprite.color.as_linear_rgba_f32(); - for i in QUAD_INDICES { - sprite_meta.colored_vertices.push(ColoredSpriteVertex { - position: positions[i], - uv: uvs[i].into(), - color: vertex_color, - }); + // Store the vertex data and add the item to the render phase + if batch_colored { + let vertex_color = extracted_sprite.color.as_linear_rgba_f32(); + for i in QUAD_INDICES { + sprite_meta.colored_vertices.push(ColoredSpriteVertex { + position: positions[i], + uv: uvs[i].into(), + color: vertex_color, + }); + } + colored_index += QUAD_INDICES.len() as u32; + existing_batch.unwrap().1.range.end = colored_index; + } else { + for i in QUAD_INDICES { + sprite_meta.vertices.push(SpriteVertex { + position: positions[i], + uv: uvs[i].into(), + }); + } + index += QUAD_INDICES.len() as u32; + existing_batch.unwrap().1.range.end = index; } - let item_start = colored_index; - colored_index += QUAD_INDICES.len() as u32; - let item_end = colored_index; - - transparent_phase.add(Transparent2d { - draw_function: draw_sprite_function, - pipeline: colored_pipeline, - entity: current_batch_entity, - sort_key, - batch_range: Some(item_start..item_end), - }); + transparent_phase.items[batch_item_index].batch_size += 1; } else { - for i in QUAD_INDICES { - sprite_meta.vertices.push(SpriteVertex { - position: positions[i], - uv: uvs[i].into(), - }); - } - let item_start = index; - index += QUAD_INDICES.len() as u32; - let item_end = index; - - transparent_phase.add(Transparent2d { - draw_function: draw_sprite_function, - pipeline, - entity: current_batch_entity, - sort_key, - batch_range: Some(item_start..item_end), - }); + batch_image_handle = HandleId::Id(Uuid::nil(), u64::MAX); } } } @@ -746,7 +764,10 @@ pub fn queue_sprites( sprite_meta .colored_vertices .write_buffer(&render_device, &render_queue); + *previous_len = batches.len(); + commands.insert_or_spawn_batch(batches); } + extracted_sprites.sprites.clear(); } pub type DrawSprite = ( @@ -786,7 +807,7 @@ impl RenderCommand

for SetSpriteTextureBindGrou fn render<'w>( _item: &P, _view: (), - sprite_batch: &'_ SpriteBatch, + batch: &'_ SpriteBatch, image_bind_groups: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -796,7 +817,7 @@ impl RenderCommand

for SetSpriteTextureBindGrou I, image_bind_groups .values - .get(&Handle::weak(sprite_batch.image_handle_id)) + .get(&Handle::weak(batch.image_handle_id)) .unwrap(), &[], ); @@ -805,25 +826,25 @@ impl RenderCommand

for SetSpriteTextureBindGrou } pub struct DrawSpriteBatch; -impl RenderCommand

for DrawSpriteBatch { +impl RenderCommand

for DrawSpriteBatch { type Param = SRes; type ViewWorldQuery = (); type ItemWorldQuery = Read; fn render<'w>( - item: &P, + _item: &P, _view: (), - sprite_batch: &'_ SpriteBatch, + batch: &'_ SpriteBatch, sprite_meta: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let sprite_meta = sprite_meta.into_inner(); - if sprite_batch.colored { + if batch.colored { pass.set_vertex_buffer(0, sprite_meta.colored_vertices.buffer().unwrap().slice(..)); } else { pass.set_vertex_buffer(0, sprite_meta.vertices.buffer().unwrap().slice(..)); } - pass.draw(item.batch_range().as_ref().unwrap().clone(), 0..1); + pass.draw(batch.range.clone(), 0..1); RenderCommandResult::Success } } diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index a7c72c5d44f86..5587355d90d2c 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ event::EventReader, prelude::With, reflect::ReflectComponent, - system::{Local, Query, Res, ResMut}, + system::{Commands, Local, Query, Res, ResMut}, }; use bevy_math::{Vec2, Vec3}; use bevy_reflect::Reflect; @@ -77,12 +77,12 @@ pub struct Text2dBundle { } pub fn extract_text2d_sprite( + mut commands: Commands, mut extracted_sprites: ResMut, texture_atlases: Extract>>, windows: Extract>>, text2d_query: Extract< Query<( - Entity, &ComputedVisibility, &Text, &TextLayoutInfo, @@ -98,7 +98,7 @@ pub fn extract_text2d_sprite( .unwrap_or(1.0); let scaling = GlobalTransform::from_scale(Vec3::splat(scale_factor.recip())); - for (entity, computed_visibility, text, text_layout_info, anchor, global_transform) in + for (computed_visibility, text, text_layout_info, anchor, global_transform) in text2d_query.iter() { if !computed_visibility.is_visible() { @@ -125,17 +125,19 @@ pub fn extract_text2d_sprite( } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - extracted_sprites.sprites.push(ExtractedSprite { - entity, - transform: transform * GlobalTransform::from_translation(position.extend(0.)), - color, - rect: Some(atlas.textures[atlas_info.glyph_index]), - custom_size: None, - image_handle_id: atlas.texture.id(), - flip_x: false, - flip_y: false, - anchor: Anchor::Center.as_vec(), - }); + extracted_sprites.sprites.insert( + commands.spawn_empty().id(), + ExtractedSprite { + transform: transform * GlobalTransform::from_translation(position.extend(0.)), + color, + rect: Some(atlas.textures[atlas_info.glyph_index]), + custom_size: None, + image_handle_id: atlas.texture.id(), + flip_x: false, + flip_y: false, + anchor: Anchor::Center.as_vec(), + }, + ); } } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index c2e82a7440671..1305093c05047 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -2,6 +2,7 @@ mod pipeline; mod render_pass; use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; +use bevy_ecs::storage::SparseSet; use bevy_hierarchy::Parent; use bevy_render::{ExtractSchedule, Render}; use bevy_window::{PrimaryWindow, Window}; @@ -14,7 +15,7 @@ use crate::{ }; use bevy_app::prelude::*; -use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped}; +use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleId, HandleUntyped}; use bevy_ecs::prelude::*; use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec4Swizzles}; use bevy_reflect::TypeUuid; @@ -36,8 +37,8 @@ use bevy_sprite::TextureAtlas; #[cfg(feature = "bevy_text")] use bevy_text::{PositionedGlyph, Text, TextLayoutInfo}; use bevy_transform::components::GlobalTransform; -use bevy_utils::FloatOrd; use bevy_utils::HashMap; +use bevy_utils::{FloatOrd, Uuid}; use bytemuck::{Pod, Zeroable}; use std::ops::Range; @@ -93,9 +94,9 @@ pub fn build_ui_render(app: &mut App) { .add_systems( Render, ( - prepare_uinodes.in_set(RenderSet::Prepare), queue_uinodes.in_set(RenderSet::Queue), sort_phase_system::.in_set(RenderSet::PhaseSort), + prepare_uinodes.in_set(RenderSet::PrepareBindGroups), ), ); @@ -166,7 +167,7 @@ pub struct ExtractedUiNode { #[derive(Resource, Default)] pub struct ExtractedUiNodes { - pub uinodes: Vec, + pub uinodes: SparseSet, } pub fn extract_atlas_uinodes( @@ -177,6 +178,7 @@ pub fn extract_atlas_uinodes( uinode_query: Extract< Query< ( + Entity, &Node, &GlobalTransform, &BackgroundColor, @@ -190,8 +192,16 @@ pub fn extract_atlas_uinodes( >, ) { for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((uinode, transform, color, visibility, clip, texture_atlas_handle, atlas_image)) = - uinode_query.get(*entity) + if let Ok(( + entity, + uinode, + transform, + color, + visibility, + clip, + texture_atlas_handle, + atlas_image, + )) = uinode_query.get(*entity) { // Skip invisible and completely transparent nodes if !visibility.is_visible() || color.0.a() == 0.0 { @@ -230,17 +240,20 @@ pub fn extract_atlas_uinodes( atlas_rect.max *= scale; atlas_size *= scale; - extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index, - transform: transform.compute_matrix(), - color: color.0, - rect: atlas_rect, - clip: clip.map(|clip| clip.clip), - image, - atlas_size: Some(atlas_size), - flip_x: atlas_image.flip_x, - flip_y: atlas_image.flip_y, - }); + extracted_uinodes.uinodes.insert( + entity, + ExtractedUiNode { + stack_index, + transform: transform.compute_matrix(), + color: color.0, + rect: atlas_rect, + clip: clip.map(|clip| clip.clip), + image, + atlas_size: Some(atlas_size), + flip_x: atlas_image.flip_x, + flip_y: atlas_image.flip_y, + }, + ); } } } @@ -258,6 +271,7 @@ fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) } pub fn extract_uinode_borders( + mut commands: Commands, mut extracted_uinodes: ResMut, windows: Extract>>, ui_scale: Extract>, @@ -355,21 +369,24 @@ pub fn extract_uinode_borders( for edge in border_rects { if edge.min.x < edge.max.x && edge.min.y < edge.max.y { - extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index, - // This translates the uinode's transform to the center of the current border rectangle - transform: transform * Mat4::from_translation(edge.center().extend(0.)), - color: border_color.0, - rect: Rect { - max: edge.size(), - ..Default::default() + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index, + // This translates the uinode's transform to the center of the current border rectangle + transform: transform * Mat4::from_translation(edge.center().extend(0.)), + color: border_color.0, + rect: Rect { + max: edge.size(), + ..Default::default() + }, + image: image.clone_weak(), + atlas_size: None, + clip: clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, }, - image: image.clone_weak(), - atlas_size: None, - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, - }); + ); } } } @@ -383,6 +400,7 @@ pub fn extract_uinodes( uinode_query: Extract< Query< ( + Entity, &Node, &GlobalTransform, &BackgroundColor, @@ -395,7 +413,7 @@ pub fn extract_uinodes( >, ) { for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((uinode, transform, color, maybe_image, visibility, clip)) = + if let Ok((entity, uinode, transform, color, maybe_image, visibility, clip)) = uinode_query.get(*entity) { // Skip invisible and completely transparent nodes @@ -413,20 +431,23 @@ pub fn extract_uinodes( (DEFAULT_IMAGE_HANDLE.typed(), false, false) }; - extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index, - transform: transform.compute_matrix(), - color: color.0, - rect: Rect { - min: Vec2::ZERO, - max: uinode.calculated_size, + extracted_uinodes.uinodes.insert( + entity, + ExtractedUiNode { + stack_index, + transform: transform.compute_matrix(), + color: color.0, + rect: Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, + }, + clip: clip.map(|clip| clip.clip), + image, + atlas_size: None, + flip_x, + flip_y, }, - clip: clip.map(|clip| clip.clip), - image, - atlas_size: None, - flip_x, - flip_y, - }); + ); }; } } @@ -506,6 +527,7 @@ pub fn extract_default_ui_camera_view( #[cfg(feature = "bevy_text")] pub fn extract_text_uinodes( + mut commands: Commands, mut extracted_uinodes: ResMut, texture_atlases: Extract>>, windows: Extract>>, @@ -560,18 +582,21 @@ pub fn extract_text_uinodes( let mut rect = atlas.textures[atlas_info.glyph_index]; rect.min *= inverse_scale_factor; rect.max *= inverse_scale_factor; - extracted_uinodes.uinodes.push(ExtractedUiNode { - stack_index, - transform: transform - * Mat4::from_translation(position.extend(0.) * inverse_scale_factor), - color, - rect, - image: atlas.texture.clone_weak(), - atlas_size: Some(atlas.size * inverse_scale_factor), - clip: clip.map(|clip| clip.clip), - flip_x: false, - flip_y: false, - }); + extracted_uinodes.uinodes.insert( + commands.spawn_empty().id(), + ExtractedUiNode { + stack_index, + transform: transform + * Mat4::from_translation(position.extend(0.) * inverse_scale_factor), + color, + rect, + image: atlas.texture.clone_weak(), + atlas_size: Some(atlas.size * inverse_scale_factor), + clip: clip.map(|clip| clip.clip), + flip_x: false, + flip_y: false, + }, + ); } } } @@ -613,176 +638,42 @@ const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; #[derive(Component)] pub struct UiBatch { pub range: Range, - pub image: Handle, - pub z: f32, + pub image_handle_id: HandleId, } const TEXTURED_QUAD: u32 = 0; const UNTEXTURED_QUAD: u32 = 1; -pub fn prepare_uinodes( - mut commands: Commands, - render_device: Res, - render_queue: Res, - mut ui_meta: ResMut, - mut extracted_uinodes: ResMut, +#[allow(clippy::too_many_arguments)] +pub fn queue_uinodes( + extracted_uinodes: Res, + ui_pipeline: Res, + mut pipelines: ResMut>, + mut views: Query<(&ExtractedView, &mut RenderPhase)>, + pipeline_cache: Res, + draw_functions: Res>, ) { - ui_meta.vertices.clear(); - - // sort by ui stack index, starting from the deepest node - extracted_uinodes - .uinodes - .sort_by_key(|node| node.stack_index); - - let mut start = 0; - let mut end = 0; - let mut current_batch_image = DEFAULT_IMAGE_HANDLE.typed(); - let mut last_z = 0.0; - - #[inline] - fn is_textured(image: &Handle) -> bool { - image.id() != DEFAULT_IMAGE_HANDLE.id() - } - - for extracted_uinode in extracted_uinodes.uinodes.drain(..) { - let mode = if is_textured(&extracted_uinode.image) { - if current_batch_image.id() != extracted_uinode.image.id() { - if is_textured(¤t_batch_image) && start != end { - commands.spawn(UiBatch { - range: start..end, - image: current_batch_image, - z: last_z, - }); - start = end; - } - current_batch_image = extracted_uinode.image.clone_weak(); - } - TEXTURED_QUAD - } else { - // Untextured `UiBatch`es are never spawned within the loop. - // If all the `extracted_uinodes` are untextured a single untextured UiBatch will be spawned after the loop terminates. - UNTEXTURED_QUAD - }; - - let mut uinode_rect = extracted_uinode.rect; - - let rect_size = uinode_rect.size().extend(1.0); - - // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (extracted_uinode.transform * (pos * rect_size).extend(1.)).xyz()); - - // Calculate the effect of clipping - // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) - let mut positions_diff = if let Some(clip) = extracted_uinode.clip { - [ - Vec2::new( - f32::max(clip.min.x - positions[0].x, 0.), - f32::max(clip.min.y - positions[0].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[1].x, 0.), - f32::max(clip.min.y - positions[1].y, 0.), - ), - Vec2::new( - f32::min(clip.max.x - positions[2].x, 0.), - f32::min(clip.max.y - positions[2].y, 0.), - ), - Vec2::new( - f32::max(clip.min.x - positions[3].x, 0.), - f32::min(clip.max.y - positions[3].y, 0.), - ), - ] - } else { - [Vec2::ZERO; 4] - }; - - let positions_clipped = [ - positions[0] + positions_diff[0].extend(0.), - positions[1] + positions_diff[1].extend(0.), - positions[2] + positions_diff[2].extend(0.), - positions[3] + positions_diff[3].extend(0.), - ]; - - let transformed_rect_size = extracted_uinode.transform.transform_vector3(rect_size); - - // Don't try to cull nodes that have a rotation - // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π - // In those two cases, the culling check can proceed normally as corners will be on - // horizontal / vertical lines - // For all other angles, bypass the culling check - // This does not properly handles all rotations on all axis - if extracted_uinode.transform.x_axis[1] == 0.0 { - // Cull nodes that are completely clipped - if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x - || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y - { - continue; - } - } - let uvs = if mode == UNTEXTURED_QUAD { - [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] - } else { - let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max); - if extracted_uinode.flip_x { - std::mem::swap(&mut uinode_rect.max.x, &mut uinode_rect.min.x); - positions_diff[0].x *= -1.; - positions_diff[1].x *= -1.; - positions_diff[2].x *= -1.; - positions_diff[3].x *= -1.; - } - if extracted_uinode.flip_y { - std::mem::swap(&mut uinode_rect.max.y, &mut uinode_rect.min.y); - positions_diff[0].y *= -1.; - positions_diff[1].y *= -1.; - positions_diff[2].y *= -1.; - positions_diff[3].y *= -1.; - } - [ - Vec2::new( - uinode_rect.min.x + positions_diff[0].x, - uinode_rect.min.y + positions_diff[0].y, - ), - Vec2::new( - uinode_rect.max.x + positions_diff[1].x, - uinode_rect.min.y + positions_diff[1].y, - ), - Vec2::new( - uinode_rect.max.x + positions_diff[2].x, - uinode_rect.max.y + positions_diff[2].y, - ), - Vec2::new( - uinode_rect.min.x + positions_diff[3].x, - uinode_rect.max.y + positions_diff[3].y, - ), - ] - .map(|pos| pos / atlas_extent) - }; - - let color = extracted_uinode.color.as_linear_rgba_f32(); - for i in QUAD_INDICES { - ui_meta.vertices.push(UiVertex { - position: positions_clipped[i].into(), - uv: uvs[i].into(), - color, - mode, + let draw_function = draw_functions.read().id::(); + for (view, mut transparent_phase) in &mut views { + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_pipeline, + UiPipelineKey { hdr: view.hdr }, + ); + transparent_phase + .items + .reserve(extracted_uinodes.uinodes.len()); + for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() { + transparent_phase.add(TransparentUi { + draw_function, + pipeline, + entity: *entity, + sort_key: FloatOrd(extracted_uinode.stack_index as f32), + // batch_size will be calculated in prepare_uinodes + batch_size: 0, }); } - - last_z = extracted_uinode.transform.w_axis[2]; - end += QUAD_INDICES.len() as u32; } - - // if start != end, there is one last batch to process - if start != end { - commands.spawn(UiBatch { - range: start..end, - image: current_batch_image, - z: last_z, - }); - } - - ui_meta.vertices.write_buffer(&render_device, &render_queue); } #[derive(Resource, Default)] @@ -791,19 +682,19 @@ pub struct UiImageBindGroups { } #[allow(clippy::too_many_arguments)] -pub fn queue_uinodes( - draw_functions: Res>, +pub fn prepare_uinodes( + mut commands: Commands, render_device: Res, + render_queue: Res, mut ui_meta: ResMut, + mut extracted_uinodes: ResMut, view_uniforms: Res, ui_pipeline: Res, - mut pipelines: ResMut>, - pipeline_cache: Res, mut image_bind_groups: ResMut, gpu_images: Res>, - ui_batches: Query<(Entity, &UiBatch)>, - mut views: Query<(&ExtractedView, &mut RenderPhase)>, + mut phases: Query<&mut RenderPhase>, events: Res, + mut previous_len: Local, ) { // If an image has changed, the GpuImage has (probably) changed for event in &events.images { @@ -815,7 +706,15 @@ pub fn queue_uinodes( }; } + #[inline] + fn is_textured(image: &Handle) -> bool { + image.id() != DEFAULT_IMAGE_HANDLE.id() + } + if let Some(view_binding) = view_uniforms.uniforms.binding() { + let mut batches: Vec<(Entity, UiBatch)> = Vec::with_capacity(*previous_len); + + ui_meta.vertices.clear(); ui_meta.view_bind_group = Some(render_device.create_bind_group(&BindGroupDescriptor { entries: &[BindGroupEntry { binding: 0, @@ -824,41 +723,186 @@ pub fn queue_uinodes( label: Some("ui_view_bind_group"), layout: &ui_pipeline.view_layout, })); - let draw_ui_function = draw_functions.read().id::(); - for (view, mut transparent_phase) in &mut views { - let pipeline = pipelines.specialize( - &pipeline_cache, - &ui_pipeline, - UiPipelineKey { hdr: view.hdr }, - ); - for (entity, batch) in &ui_batches { - image_bind_groups - .values - .entry(batch.image.clone_weak()) - .or_insert_with(|| { - let gpu_image = gpu_images.get(&batch.image).unwrap(); - render_device.create_bind_group(&BindGroupDescriptor { - entries: &[ - BindGroupEntry { - binding: 0, - resource: BindingResource::TextureView(&gpu_image.texture_view), - }, - BindGroupEntry { - binding: 1, - resource: BindingResource::Sampler(&gpu_image.sampler), - }, - ], - label: Some("ui_material_bind_group"), - layout: &ui_pipeline.image_layout, - }) + + // Vertex buffer index + let mut index = 0; + + for mut ui_phase in &mut phases { + let mut batch_item_index = 0; + let mut batch_image_handle = HandleId::Id(Uuid::nil(), u64::MAX); + + for item_index in 0..ui_phase.items.len() { + let item = &mut ui_phase.items[item_index]; + if let Some(extracted_uinode) = extracted_uinodes.uinodes.get(item.entity) { + let mut existing_batch = batches + .last_mut() + .filter(|_| batch_image_handle == extracted_uinode.image.id()); + + if existing_batch.is_none() { + if let Some(gpu_image) = gpu_images.get(&extracted_uinode.image) { + batch_item_index = item_index; + batch_image_handle = extracted_uinode.image.id(); + + let new_batch = UiBatch { + range: index..index, + image_handle_id: extracted_uinode.image.id(), + }; + + batches.push((item.entity, new_batch)); + + image_bind_groups + .values + .entry(Handle::weak(batch_image_handle)) + .or_insert_with(|| { + render_device.create_bind_group(&BindGroupDescriptor { + entries: &[ + BindGroupEntry { + binding: 0, + resource: BindingResource::TextureView( + &gpu_image.texture_view, + ), + }, + BindGroupEntry { + binding: 1, + resource: BindingResource::Sampler( + &gpu_image.sampler, + ), + }, + ], + label: Some("ui_material_bind_group"), + layout: &ui_pipeline.image_layout, + }) + }); + + existing_batch = batches.last_mut(); + } else { + continue; + } + } + + let mode = if is_textured(&extracted_uinode.image) { + TEXTURED_QUAD + } else { + UNTEXTURED_QUAD + }; + + let mut uinode_rect = extracted_uinode.rect; + + let rect_size = uinode_rect.size().extend(1.0); + + // Specify the corners of the node + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (extracted_uinode.transform * (pos * rect_size).extend(1.)).xyz() }); - transparent_phase.add(TransparentUi { - draw_function: draw_ui_function, - pipeline, - entity, - sort_key: FloatOrd(batch.z), - }); + + // Calculate the effect of clipping + // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) + let mut positions_diff = if let Some(clip) = extracted_uinode.clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + + let transformed_rect_size = + extracted_uinode.transform.transform_vector3(rect_size); + + // Don't try to cull nodes that have a rotation + // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π + // In those two cases, the culling check can proceed normally as corners will be on + // horizontal / vertical lines + // For all other angles, bypass the culling check + // This does not properly handles all rotations on all axis + if extracted_uinode.transform.x_axis[1] == 0.0 { + // Cull nodes that are completely clipped + if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y + { + continue; + } + } + let uvs = if mode == UNTEXTURED_QUAD { + [Vec2::ZERO, Vec2::X, Vec2::ONE, Vec2::Y] + } else { + let atlas_extent = extracted_uinode.atlas_size.unwrap_or(uinode_rect.max); + if extracted_uinode.flip_x { + std::mem::swap(&mut uinode_rect.max.x, &mut uinode_rect.min.x); + positions_diff[0].x *= -1.; + positions_diff[1].x *= -1.; + positions_diff[2].x *= -1.; + positions_diff[3].x *= -1.; + } + if extracted_uinode.flip_y { + std::mem::swap(&mut uinode_rect.max.y, &mut uinode_rect.min.y); + positions_diff[0].y *= -1.; + positions_diff[1].y *= -1.; + positions_diff[2].y *= -1.; + positions_diff[3].y *= -1.; + } + [ + Vec2::new( + uinode_rect.min.x + positions_diff[0].x, + uinode_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[1].x, + uinode_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[2].x, + uinode_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + uinode_rect.min.x + positions_diff[3].x, + uinode_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / atlas_extent) + }; + + let color = extracted_uinode.color.as_linear_rgba_f32(); + for i in QUAD_INDICES { + ui_meta.vertices.push(UiVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + color, + mode, + }); + } + index += QUAD_INDICES.len() as u32; + existing_batch.unwrap().1.range.end = index; + ui_phase.items[batch_item_index].batch_size += 1; + } else { + batch_image_handle = HandleId::Id(Uuid::nil(), u64::MAX); + } } } + ui_meta.vertices.write_buffer(&render_device, &render_queue); + *previous_len = batches.len(); + commands.insert_or_spawn_batch(batches); } + extracted_uinodes.uinodes.clear(); } diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 80985b2f26db2..c464b2a5853ea 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -1,5 +1,6 @@ use super::{UiBatch, UiImageBindGroups, UiMeta}; use crate::{prelude::UiCameraConfig, DefaultCameraView}; +use bevy_asset::Handle; use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, @@ -90,6 +91,7 @@ pub struct TransparentUi { pub entity: Entity, pub pipeline: CachedRenderPipelineId, pub draw_function: DrawFunctionId, + pub batch_size: usize, } impl PhaseItem for TransparentUi { @@ -109,6 +111,11 @@ impl PhaseItem for TransparentUi { fn draw_function(&self) -> DrawFunctionId { self.draw_function } + + #[inline] + fn batch_size(&self) -> usize { + self.batch_size + } } impl CachedRenderPipelinePhaseItem for TransparentUi { @@ -161,7 +168,14 @@ impl RenderCommand

for SetUiTextureBindGroup pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { let image_bind_groups = image_bind_groups.into_inner(); - pass.set_bind_group(I, image_bind_groups.values.get(&batch.image).unwrap(), &[]); + pass.set_bind_group( + I, + image_bind_groups + .values + .get(&Handle::weak(batch.image_handle_id)) + .unwrap(), + &[], + ); RenderCommandResult::Success } } diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index f2fb481388dfa..b5fa7c306303e 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -279,7 +279,7 @@ impl Plugin for ColoredMesh2dPlugin { .add_render_command::() .init_resource::>() .add_systems(ExtractSchedule, extract_colored_mesh2d) - .add_systems(Render, queue_colored_mesh2d.in_set(RenderSet::Queue)); + .add_systems(Render, queue_colored_mesh2d.in_set(RenderSet::QueueMeshes)); } fn finish(&self, app: &mut App) { @@ -357,7 +357,7 @@ pub fn queue_colored_mesh2d( // in order to get correct transparency sort_key: FloatOrd(mesh_z), // This material is not batched - batch_range: None, + batch_size: 1, }); } } diff --git a/examples/shader/compute_shader_game_of_life.rs b/examples/shader/compute_shader_game_of_life.rs index d737fb029c339..6d8383ee0c532 100644 --- a/examples/shader/compute_shader_game_of_life.rs +++ b/examples/shader/compute_shader_game_of_life.rs @@ -74,7 +74,10 @@ impl Plugin for GameOfLifeComputePlugin { // for operation on by the compute shader and display on the sprite. app.add_plugins(ExtractResourcePlugin::::default()); let render_app = app.sub_app_mut(RenderApp); - render_app.add_systems(Render, queue_bind_group.in_set(RenderSet::Queue)); + render_app.add_systems( + Render, + prepare_bind_group.in_set(RenderSet::PrepareBindGroups), + ); let mut render_graph = render_app.world.resource_mut::(); render_graph.add_node("game_of_life", GameOfLifeNode::default()); @@ -96,7 +99,7 @@ struct GameOfLifeImage(Handle); #[derive(Resource)] struct GameOfLifeImageBindGroup(BindGroup); -fn queue_bind_group( +fn prepare_bind_group( mut commands: Commands, pipeline: Res, gpu_images: Res>, diff --git a/examples/shader/shader_instancing.rs b/examples/shader/shader_instancing.rs index 925c0c14d3404..326183d917a0b 100644 --- a/examples/shader/shader_instancing.rs +++ b/examples/shader/shader_instancing.rs @@ -86,8 +86,8 @@ impl Plugin for CustomMaterialPlugin { .add_systems( Render, ( - queue_custom.in_set(RenderSet::Queue), - prepare_instance_buffers.in_set(RenderSet::Prepare), + queue_custom.in_set(RenderSet::QueueMeshes), + prepare_instance_buffers.in_set(RenderSet::PrepareResources), ), ); } @@ -136,6 +136,7 @@ fn queue_custom( draw_function: draw_custom, distance: rangefinder .distance_translation(&mesh_transforms.transform.translation), + batch_size: 1, }); } } From 9d804a231e99f0ea2f0825d89285be98d668f138 Mon Sep 17 00:00:00 2001 From: st0rmbtw <61053971+st0rmbtw@users.noreply.github.com> Date: Sun, 27 Aug 2023 20:53:37 +0300 Subject: [PATCH 10/58] Make `run_if_inner` public and rename to `run_if_dyn` (#9576) # Objective Sometimes you want to create a plugin with a custom run condition. In a function, you take the `Condition` trait and then make a `BoxedCondition` from it to store it. And then you want to add that condition to a system, but you can't, because there is only the `run_if` function available which takes `impl Condition` instead of `BoxedCondition`. So you have to create a wrapper type for the `BoxedCondition` and implement the `System` and `ReadOnlySystem` traits for the wrapper (Like it's done in the picture below). It's very inconvenient and boilerplate. But there is an easy solution for that: make the `run_if_inner` system that takes a `BoxedCondition` public. Also, it makes sense to make `in_set_inner` function public as well with the same motivation. ![image](https://github.com/bevyengine/bevy/assets/61053971/a4455180-7e0c-4c2b-9372-cd8b4a9e682e) A chunk of the source code of the `bevy-inspector-egui` crate. ## Solution Make `run_if_inner` function public. Rename `run_if_inner` to `run_if_dyn`. Make `in_set_inner` function public. Rename `in_set_inner` to `in_set_dyn`. ## Changelog Changed visibility of `run_if_inner` from `pub(crate)` to `pub`. Renamed `run_if_inner` to `run_if_dyn`. Changed visibility of `in_set_inner` from `pub(crate)` to `pub`. Renamed `in_set_inner` to `in_set_dyn`. ## Migration Guide --------- Co-authored-by: Alice Cecile Co-authored-by: Joseph <21144246+JoJoJet@users.noreply.github.com> --- crates/bevy_ecs/src/schedule/config.rs | 15 ++++++++++----- crates/bevy_ecs/src/schedule/schedule.rs | 4 ++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index 708661035ed55..47b72232b1179 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -83,14 +83,15 @@ impl SystemConfigs { }) } - pub(crate) fn in_set_inner(&mut self, set: BoxedSystemSet) { + /// Adds a new boxed system set to the systems. + pub fn in_set_dyn(&mut self, set: BoxedSystemSet) { match self { SystemConfigs::SystemConfig(config) => { config.graph_info.sets.push(set); } SystemConfigs::Configs { configs, .. } => { for config in configs { - config.in_set_inner(set.dyn_clone()); + config.in_set_dyn(set.dyn_clone()); } } } @@ -167,7 +168,11 @@ impl SystemConfigs { } } - pub(crate) fn run_if_inner(&mut self, condition: BoxedCondition) { + /// Adds a new boxed run condition to the systems. + /// + /// This is useful if you have a run condition whose concrete type is unknown. + /// Prefer `run_if` for run conditions whose type is known at compile time. + pub fn run_if_dyn(&mut self, condition: BoxedCondition) { match self { SystemConfigs::SystemConfig(config) => { config.conditions.push(condition); @@ -307,7 +312,7 @@ impl IntoSystemConfigs<()> for SystemConfigs { "adding arbitrary systems to a system type set is not allowed" ); - self.in_set_inner(set.dyn_clone()); + self.in_set_dyn(set.dyn_clone()); self } @@ -341,7 +346,7 @@ impl IntoSystemConfigs<()> for SystemConfigs { } fn run_if(mut self, condition: impl Condition) -> SystemConfigs { - self.run_if_inner(new_condition(condition)); + self.run_if_dyn(new_condition(condition)); self } diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 4f1b90ff9050d..18e230114e69d 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -532,14 +532,14 @@ impl ScheduleGraph { if more_than_one_entry { let set = AnonymousSet::new(); for config in &mut configs { - config.in_set_inner(set.dyn_clone()); + config.in_set_dyn(set.dyn_clone()); } let mut set_config = set.into_config(); set_config.conditions.extend(collective_conditions); self.configure_set(set_config); } else { for condition in collective_conditions { - configs[0].run_if_inner(condition); + configs[0].run_if_dyn(condition); } } } From 7b6f8659f8e85056273ccc41388ef0620aef35cd Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 27 Aug 2023 10:54:59 -0700 Subject: [PATCH 11/58] Refactor build_schedule and related errors (#9579) # Objective - break up large build_schedule system to make it easier to read - Clean up related error messages. - I have a follow up PR that adds the schedule name to the error messages, but wanted to break this up from that. ## Changelog - refactor `build_schedule` to be easier to read ## Sample Error Messages Dependency Cycle ```text thread 'main' panicked at 'System dependencies contain cycle(s). schedule has 1 before/after cycle(s): cycle 1: system set 'A' must run before itself system set 'A' ... which must run before system set 'B' ... which must run before system set 'A' ', crates\bevy_ecs\src\schedule\schedule.rs:228:13 ``` ```text thread 'main' panicked at 'System dependencies contain cycle(s). schedule has 1 before/after cycle(s): cycle 1: system 'foo' must run before itself system 'foo' ... which must run before system 'bar' ... which must run before system 'foo' ', crates\bevy_ecs\src\schedule\schedule.rs:228:13 ``` Hierarchy Cycle ```text thread 'main' panicked at 'System set hierarchy contains cycle(s). schedule has 1 in_set cycle(s): cycle 1: set 'A' contains itself set 'A' ... which contains set 'B' ... which contains set 'A' ', crates\bevy_ecs\src\schedule\schedule.rs:230:13 ``` System Type Set ```text thread 'main' panicked at 'Tried to order against `SystemTypeSet(fn foo())` in a schedule that has more than one `SystemTypeSet(fn foo())` instance. `SystemTypeSet(fn foo())` is a `SystemTypeSet` and cannot be used for ordering if ambiguous. Use a different set without this restriction.', crates\bevy_ecs\src\schedule\schedule.rs:230:13 ``` Hierarchy Redundancy ```text thread 'main' panicked at 'System set hierarchy contains redundant edges. hierarchy contains redundant edge(s) -- system set 'X' cannot be child of set 'A', longer path exists ', crates\bevy_ecs\src\schedule\schedule.rs:230:13 ``` Systems have ordering but interset ```text thread 'main' panicked at '`A` and `C` have a `before`-`after` relationship (which may be transitive) but share systems.', crates\bevy_ecs\src\schedule\schedule.rs:227:51 ``` Cross Dependency ```text thread 'main' panicked at '`A` and `B` have both `in_set` and `before`-`after` relationships (these might be transitive). This combination is unsolvable as a system cannot run before or after a set it belongs to.', crates\bevy_ecs\src\schedule\schedule.rs:230:13 ``` Ambiguity ```text thread 'main' panicked at 'Systems with conflicting access have indeterminate run order. 1 pairs of systems with conflicting data access have indeterminate execution order. Consider adding `before`, `after`, or `ambiguous_with` relationships between these: -- res_mut and res_ref conflict on: ["bevymark::ambiguity::X"] ', crates\bevy_ecs\src\schedule\schedule.rs:230:13 ``` --- crates/bevy_ecs/src/schedule/mod.rs | 16 +- crates/bevy_ecs/src/schedule/schedule.rs | 386 ++++++++++++++--------- crates/bevy_ecs/src/schedule/set.rs | 2 +- 3 files changed, 244 insertions(+), 160 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index c585fb508fb72..1982ce03dae3d 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -541,7 +541,10 @@ mod tests { schedule.configure_set(TestSet::B.after(TestSet::A)); let result = schedule.initialize(&mut world); - assert!(matches!(result, Err(ScheduleBuildError::DependencyCycle))); + assert!(matches!( + result, + Err(ScheduleBuildError::DependencyCycle(_)) + )); fn foo() {} fn bar() {} @@ -551,7 +554,10 @@ mod tests { schedule.add_systems((foo.after(bar), bar.after(foo))); let result = schedule.initialize(&mut world); - assert!(matches!(result, Err(ScheduleBuildError::DependencyCycle))); + assert!(matches!( + result, + Err(ScheduleBuildError::DependencyCycle(_)) + )); } #[test] @@ -570,7 +576,7 @@ mod tests { schedule.configure_set(TestSet::B.in_set(TestSet::A)); let result = schedule.initialize(&mut world); - assert!(matches!(result, Err(ScheduleBuildError::HierarchyCycle))); + assert!(matches!(result, Err(ScheduleBuildError::HierarchyCycle(_)))); } #[test] @@ -645,7 +651,7 @@ mod tests { let result = schedule.initialize(&mut world); assert!(matches!( result, - Err(ScheduleBuildError::HierarchyRedundancy) + Err(ScheduleBuildError::HierarchyRedundancy(_)) )); } @@ -707,7 +713,7 @@ mod tests { schedule.add_systems((res_ref, res_mut)); let result = schedule.initialize(&mut world); - assert!(matches!(result, Err(ScheduleBuildError::Ambiguity))); + assert!(matches!(result, Err(ScheduleBuildError::Ambiguity(_)))); } } } diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 18e230114e69d..2db71e2dc19e2 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -382,9 +382,7 @@ pub struct ScheduleGraph { uninit: Vec<(NodeId, usize)>, hierarchy: Dag, dependency: Dag, - dependency_flattened: Dag, ambiguous_with: UnGraphMap, - ambiguous_with_flattened: UnGraphMap, ambiguous_with_all: HashSet, conflicting_systems: Vec<(NodeId, NodeId, Vec)>, changed: bool, @@ -403,9 +401,7 @@ impl ScheduleGraph { uninit: Vec::new(), hierarchy: Dag::new(), dependency: Dag::new(), - dependency_flattened: Dag::new(), ambiguous_with: UnGraphMap::new(), - ambiguous_with_flattened: UnGraphMap::new(), ambiguous_with_all: HashSet::new(), conflicting_systems: Vec::new(), changed: false, @@ -863,45 +859,70 @@ impl ScheduleGraph { components: &Components, ) -> Result { // check hierarchy for cycles - self.hierarchy.topsort = self - .topsort_graph(&self.hierarchy.graph, ReportCycles::Hierarchy) - .map_err(|_| ScheduleBuildError::HierarchyCycle)?; + self.hierarchy.topsort = + self.topsort_graph(&self.hierarchy.graph, ReportCycles::Hierarchy)?; let hier_results = check_graph(&self.hierarchy.graph, &self.hierarchy.topsort); - if self.settings.hierarchy_detection != LogLevel::Ignore - && self.contains_hierarchy_conflicts(&hier_results.transitive_edges) - { - self.report_hierarchy_conflicts(&hier_results.transitive_edges); - if matches!(self.settings.hierarchy_detection, LogLevel::Error) { - return Err(ScheduleBuildError::HierarchyRedundancy); - } - } + self.optionally_check_hierarchy_conflicts(&hier_results.transitive_edges)?; // remove redundant edges self.hierarchy.graph = hier_results.transitive_reduction; // check dependencies for cycles - self.dependency.topsort = self - .topsort_graph(&self.dependency.graph, ReportCycles::Dependency) - .map_err(|_| ScheduleBuildError::DependencyCycle)?; + self.dependency.topsort = + self.topsort_graph(&self.dependency.graph, ReportCycles::Dependency)?; // check for systems or system sets depending on sets they belong to let dep_results = check_graph(&self.dependency.graph, &self.dependency.topsort); - for &(a, b) in dep_results.connected.iter() { - if hier_results.connected.contains(&(a, b)) || hier_results.connected.contains(&(b, a)) - { - let name_a = self.get_node_name(&a); - let name_b = self.get_node_name(&b); - return Err(ScheduleBuildError::CrossDependency(name_a, name_b)); - } - } + self.check_for_cross_dependencies(&dep_results, &hier_results.connected)?; // map all system sets to their systems // go in reverse topological order (bottom-up) for efficiency + let (set_systems, set_system_bitsets) = + self.map_sets_to_systems(&self.hierarchy.topsort, &self.hierarchy.graph); + self.check_order_but_intersect(&dep_results.connected, &set_system_bitsets)?; + + // check that there are no edges to system-type sets that have multiple instances + self.check_system_type_set_ambiguity(&set_systems)?; + + let dependency_flattened = self.get_dependency_flattened(&set_systems); + + // topsort + let mut dependency_flattened_dag = Dag { + topsort: self.topsort_graph(&dependency_flattened, ReportCycles::Dependency)?, + graph: dependency_flattened, + }; + + let flat_results = check_graph( + &dependency_flattened_dag.graph, + &dependency_flattened_dag.topsort, + ); + + // remove redundant edges + dependency_flattened_dag.graph = flat_results.transitive_reduction; + + // flatten: combine `in_set` with `ambiguous_with` information + let ambiguous_with_flattened = self.get_ambiguous_with_flattened(&set_systems); + + // check for conflicts + let conflicting_systems = + self.get_conflicting_systems(&flat_results.disconnected, &ambiguous_with_flattened); + self.optionally_check_conflicts(&conflicting_systems, components)?; + self.conflicting_systems = conflicting_systems; + + // build the schedule + Ok(self.build_schedule_inner(dependency_flattened_dag, hier_results.reachable)) + } + + fn map_sets_to_systems( + &self, + hierarchy_topsort: &[NodeId], + hierarchy_graph: &GraphMap, + ) -> (HashMap>, HashMap) { let mut set_systems: HashMap> = HashMap::with_capacity(self.system_sets.len()); let mut set_system_bitsets = HashMap::with_capacity(self.system_sets.len()); - for &id in self.hierarchy.topsort.iter().rev() { + for &id in hierarchy_topsort.iter().rev() { if id.is_system() { continue; } @@ -909,11 +930,7 @@ impl ScheduleGraph { let mut systems = Vec::new(); let mut system_bitset = FixedBitSet::with_capacity(self.systems.len()); - for child in self - .hierarchy - .graph - .neighbors_directed(id, Direction::Outgoing) - { + for child in hierarchy_graph.neighbors_directed(id, Direction::Outgoing) { match child { NodeId::System(_) => { systems.push(child); @@ -931,47 +948,13 @@ impl ScheduleGraph { set_systems.insert(id, systems); set_system_bitsets.insert(id, system_bitset); } + (set_systems, set_system_bitsets) + } - // check that there is no ordering between system sets that intersect - for (a, b) in dep_results.connected.iter() { - if !(a.is_set() && b.is_set()) { - continue; - } - - let a_systems = set_system_bitsets.get(a).unwrap(); - let b_systems = set_system_bitsets.get(b).unwrap(); - - if !(a_systems.is_disjoint(b_systems)) { - return Err(ScheduleBuildError::SetsHaveOrderButIntersect( - self.get_node_name(a), - self.get_node_name(b), - )); - } - } - - // check that there are no edges to system-type sets that have multiple instances - for (&id, systems) in set_systems.iter() { - let set = &self.system_sets[id.index()]; - if set.is_system_type() { - let instances = systems.len(); - let ambiguous_with = self.ambiguous_with.edges(id); - let before = self - .dependency - .graph - .edges_directed(id, Direction::Incoming); - let after = self - .dependency - .graph - .edges_directed(id, Direction::Outgoing); - let relations = before.count() + after.count() + ambiguous_with.count(); - if instances > 1 && relations > 0 { - return Err(ScheduleBuildError::SystemTypeSetAmbiguity( - self.get_node_name(&id), - )); - } - } - } - + fn get_dependency_flattened( + &self, + set_systems: &HashMap>, + ) -> GraphMap { // flatten: combine `in_set` with `before` and `after` information // have to do it like this to preserve transitivity let mut dependency_flattened = self.dependency.graph.clone(); @@ -1003,21 +986,13 @@ impl ScheduleGraph { } } - // topsort - self.dependency_flattened.topsort = self - .topsort_graph(&dependency_flattened, ReportCycles::Dependency) - .map_err(|_| ScheduleBuildError::DependencyCycle)?; - self.dependency_flattened.graph = dependency_flattened; - - let flat_results = check_graph( - &self.dependency_flattened.graph, - &self.dependency_flattened.topsort, - ); - - // remove redundant edges - self.dependency_flattened.graph = flat_results.transitive_reduction; + dependency_flattened + } - // flatten: combine `in_set` with `ambiguous_with` information + fn get_ambiguous_with_flattened( + &self, + set_systems: &HashMap>, + ) -> GraphMap { let mut ambiguous_with_flattened = UnGraphMap::new(); for (lhs, rhs, _) in self.ambiguous_with.all_edges() { match (lhs, rhs) { @@ -1044,12 +1019,17 @@ impl ScheduleGraph { } } - self.ambiguous_with_flattened = ambiguous_with_flattened; + ambiguous_with_flattened + } - // check for conflicts + fn get_conflicting_systems( + &self, + flat_results_disconnected: &Vec<(NodeId, NodeId)>, + ambiguous_with_flattened: &GraphMap, + ) -> Vec<(NodeId, NodeId, Vec)> { let mut conflicting_systems = Vec::new(); - for &(a, b) in &flat_results.disconnected { - if self.ambiguous_with_flattened.contains_edge(a, b) + for &(a, b) in flat_results_disconnected { + if ambiguous_with_flattened.contains_edge(a, b) || self.ambiguous_with_all.contains(&a) || self.ambiguous_with_all.contains(&b) { @@ -1070,18 +1050,15 @@ impl ScheduleGraph { } } - if self.settings.ambiguity_detection != LogLevel::Ignore - && self.contains_conflicts(&conflicting_systems) - { - self.report_conflicts(&conflicting_systems, components); - if matches!(self.settings.ambiguity_detection, LogLevel::Error) { - return Err(ScheduleBuildError::Ambiguity); - } - } - self.conflicting_systems = conflicting_systems; + conflicting_systems + } - // build the schedule - let dg_system_ids = self.dependency_flattened.topsort.clone(); + fn build_schedule_inner( + &self, + dependency_flattened_dag: Dag, + hier_results_reachable: FixedBitSet, + ) -> SystemSchedule { + let dg_system_ids = dependency_flattened_dag.topsort.clone(); let dg_system_idx_map = dg_system_ids .iter() .cloned() @@ -1120,14 +1097,12 @@ impl ScheduleGraph { let mut system_dependencies = Vec::with_capacity(sys_count); let mut system_dependents = Vec::with_capacity(sys_count); for &sys_id in &dg_system_ids { - let num_dependencies = self - .dependency_flattened + let num_dependencies = dependency_flattened_dag .graph .neighbors_directed(sys_id, Direction::Incoming) .count(); - let dependents = self - .dependency_flattened + let dependents = dependency_flattened_dag .graph .neighbors_directed(sys_id, Direction::Outgoing) .map(|dep_id| dg_system_idx_map[&dep_id]) @@ -1145,7 +1120,7 @@ impl ScheduleGraph { let bitset = &mut systems_in_sets_with_conditions[i]; for &(col, sys_id) in &hg_systems { let idx = dg_system_idx_map[&sys_id]; - let is_descendant = hier_results.reachable[index(row, col, hg_node_count)]; + let is_descendant = hier_results_reachable[index(row, col, hg_node_count)]; bitset.set(idx, is_descendant); } } @@ -1160,12 +1135,12 @@ impl ScheduleGraph { .enumerate() .take_while(|&(_idx, &row)| row < col) { - let is_ancestor = hier_results.reachable[index(row, col, hg_node_count)]; + let is_ancestor = hier_results_reachable[index(row, col, hg_node_count)]; bitset.set(idx, is_ancestor); } } - Ok(SystemSchedule { + SystemSchedule { systems: Vec::with_capacity(sys_count), system_conditions: Vec::with_capacity(sys_count), set_conditions: Vec::with_capacity(set_with_conditions_count), @@ -1175,7 +1150,7 @@ impl ScheduleGraph { system_dependents, sets_with_conditions_of_systems, systems_in_sets_with_conditions, - }) + } } fn update_schedule( @@ -1292,20 +1267,36 @@ impl ScheduleGraph { } } - fn contains_hierarchy_conflicts(&self, transitive_edges: &[(NodeId, NodeId)]) -> bool { - if transitive_edges.is_empty() { - return false; + /// If [`ScheduleBuildSettings::hierarchy_detection`] is [`LogLevel::Ignore`] this check + /// is skipped. + fn optionally_check_hierarchy_conflicts( + &self, + transitive_edges: &[(NodeId, NodeId)], + ) -> Result<(), ScheduleBuildError> { + if self.settings.hierarchy_detection == LogLevel::Ignore || transitive_edges.is_empty() { + return Ok(()); } - true + let message = self.get_hierarchy_conflicts_error_message(transitive_edges); + match self.settings.hierarchy_detection { + LogLevel::Ignore => unreachable!(), + LogLevel::Warn => { + error!("{}", message); + Ok(()) + } + LogLevel::Error => Err(ScheduleBuildError::HierarchyRedundancy(message)), + } } - fn report_hierarchy_conflicts(&self, transitive_edges: &[(NodeId, NodeId)]) { + fn get_hierarchy_conflicts_error_message( + &self, + transitive_edges: &[(NodeId, NodeId)], + ) -> String { let mut message = String::from("hierarchy contains redundant edge(s)"); for (parent, child) in transitive_edges { writeln!( message, - " -- {:?} '{:?}' cannot be child of set '{:?}', longer path exists", + " -- {} `{}` cannot be child of set `{}`, longer path exists", self.get_node_kind(child), self.get_node_name(child), self.get_node_name(parent), @@ -1313,7 +1304,7 @@ impl ScheduleGraph { .unwrap(); } - error!("{}", message); + message } /// Tries to topologically sort `graph`. @@ -1329,7 +1320,7 @@ impl ScheduleGraph { &self, graph: &DiGraphMap, report: ReportCycles, - ) -> Result, Vec>> { + ) -> Result, ScheduleBuildError> { // Tarjan's SCC algorithm returns elements in *reverse* topological order. let mut tarjan_scc = TarjanScc::new(); let mut top_sorted_nodes = Vec::with_capacity(graph.node_count()); @@ -1355,39 +1346,43 @@ impl ScheduleGraph { cycles.append(&mut simple_cycles_in_component(graph, scc)); } - match report { - ReportCycles::Hierarchy => self.report_hierarchy_cycles(&cycles), - ReportCycles::Dependency => self.report_dependency_cycles(&cycles), - } + let error = match report { + ReportCycles::Hierarchy => ScheduleBuildError::HierarchyCycle( + self.get_hierarchy_cycles_error_message(&cycles), + ), + ReportCycles::Dependency => ScheduleBuildError::DependencyCycle( + self.get_dependency_cycles_error_message(&cycles), + ), + }; - Err(sccs_with_cycles) + Err(error) } } /// Logs details of cycles in the hierarchy graph. - fn report_hierarchy_cycles(&self, cycles: &[Vec]) { + fn get_hierarchy_cycles_error_message(&self, cycles: &[Vec]) -> String { let mut message = format!("schedule has {} in_set cycle(s):\n", cycles.len()); for (i, cycle) in cycles.iter().enumerate() { let mut names = cycle.iter().map(|id| self.get_node_name(id)); let first_name = names.next().unwrap(); writeln!( message, - "cycle {}: set '{first_name}' contains itself", + "cycle {}: set `{first_name}` contains itself", i + 1, ) .unwrap(); - writeln!(message, "set '{first_name}'").unwrap(); + writeln!(message, "set `{first_name}`").unwrap(); for name in names.chain(std::iter::once(first_name)) { - writeln!(message, " ... which contains set '{name}'").unwrap(); + writeln!(message, " ... which contains set `{name}`").unwrap(); } writeln!(message).unwrap(); } - error!("{}", message); + message } /// Logs details of cycles in the dependency graph. - fn report_dependency_cycles(&self, cycles: &[Vec]) { + fn get_dependency_cycles_error_message(&self, cycles: &[Vec]) -> String { let mut message = format!("schedule has {} before/after cycle(s):\n", cycles.len()); for (i, cycle) in cycles.iter().enumerate() { let mut names = cycle @@ -1396,36 +1391,119 @@ impl ScheduleGraph { let (first_kind, first_name) = names.next().unwrap(); writeln!( message, - "cycle {}: {first_kind} '{first_name}' must run before itself", + "cycle {}: {first_kind} `{first_name}` must run before itself", i + 1, ) .unwrap(); - writeln!(message, "{first_kind} '{first_name}'").unwrap(); + writeln!(message, "{first_kind} `{first_name}`").unwrap(); for (kind, name) in names.chain(std::iter::once((first_kind, first_name))) { - writeln!(message, " ... which must run before {kind} '{name}'").unwrap(); + writeln!(message, " ... which must run before {kind} `{name}`").unwrap(); } writeln!(message).unwrap(); } - error!("{}", message); + message } - fn contains_conflicts(&self, conflicts: &[(NodeId, NodeId, Vec)]) -> bool { - if conflicts.is_empty() { - return false; + fn check_for_cross_dependencies( + &self, + dep_results: &CheckGraphResults, + hier_results_connected: &HashSet<(NodeId, NodeId)>, + ) -> Result<(), ScheduleBuildError> { + for &(a, b) in dep_results.connected.iter() { + if hier_results_connected.contains(&(a, b)) || hier_results_connected.contains(&(b, a)) + { + let name_a = self.get_node_name(&a); + let name_b = self.get_node_name(&b); + return Err(ScheduleBuildError::CrossDependency(name_a, name_b)); + } } - true + Ok(()) + } + + fn check_order_but_intersect( + &self, + dep_results_connected: &HashSet<(NodeId, NodeId)>, + set_system_bitsets: &HashMap, + ) -> Result<(), ScheduleBuildError> { + // check that there is no ordering between system sets that intersect + for (a, b) in dep_results_connected.iter() { + if !(a.is_set() && b.is_set()) { + continue; + } + + let a_systems = set_system_bitsets.get(a).unwrap(); + let b_systems = set_system_bitsets.get(b).unwrap(); + + if !(a_systems.is_disjoint(b_systems)) { + return Err(ScheduleBuildError::SetsHaveOrderButIntersect( + self.get_node_name(a), + self.get_node_name(b), + )); + } + } + + Ok(()) + } + + fn check_system_type_set_ambiguity( + &self, + set_systems: &HashMap>, + ) -> Result<(), ScheduleBuildError> { + for (&id, systems) in set_systems.iter() { + let set = &self.system_sets[id.index()]; + if set.is_system_type() { + let instances = systems.len(); + let ambiguous_with = self.ambiguous_with.edges(id); + let before = self + .dependency + .graph + .edges_directed(id, Direction::Incoming); + let after = self + .dependency + .graph + .edges_directed(id, Direction::Outgoing); + let relations = before.count() + after.count() + ambiguous_with.count(); + if instances > 1 && relations > 0 { + return Err(ScheduleBuildError::SystemTypeSetAmbiguity( + self.get_node_name(&id), + )); + } + } + } + Ok(()) + } + + /// if [`ScheduleBuildSettings::ambiguity_detection`] is [`LogLevel::Ignore`], this check is skipped + fn optionally_check_conflicts( + &self, + conflicts: &[(NodeId, NodeId, Vec)], + components: &Components, + ) -> Result<(), ScheduleBuildError> { + if self.settings.ambiguity_detection == LogLevel::Ignore || conflicts.is_empty() { + return Ok(()); + } + + let message = self.get_conflicts_error_message(conflicts, components); + match self.settings.ambiguity_detection { + LogLevel::Ignore => Ok(()), + LogLevel::Warn => { + warn!("{}", message); + Ok(()) + } + LogLevel::Error => Err(ScheduleBuildError::Ambiguity(message)), + } } - fn report_conflicts( + fn get_conflicts_error_message( &self, ambiguities: &[(NodeId, NodeId, Vec)], components: &Components, - ) { + ) -> String { let n_ambiguities = ambiguities.len(); - let mut string = format!( + let mut message = format!( "{n_ambiguities} pairs of systems with conflicting data access have indeterminate execution order. \ Consider adding `before`, `after`, or `ambiguous_with` relationships between these:\n", ); @@ -1437,22 +1515,22 @@ impl ScheduleGraph { debug_assert!(system_a.is_system(), "{name_a} is not a system."); debug_assert!(system_b.is_system(), "{name_b} is not a system."); - writeln!(string, " -- {name_a} and {name_b}").unwrap(); + writeln!(message, " -- {name_a} and {name_b}").unwrap(); if !conflicts.is_empty() { let conflict_names: Vec<_> = conflicts .iter() .map(|id| components.get_name(*id).unwrap()) .collect(); - writeln!(string, " conflict on: {conflict_names:?}").unwrap(); + writeln!(message, " conflict on: {conflict_names:?}").unwrap(); } else { // one or both systems must be exclusive let world = std::any::type_name::(); - writeln!(string, " conflict on: {world}").unwrap(); + writeln!(message, " conflict on: {world}").unwrap(); } } - warn!("{}", string); + message } fn traverse_sets_containing_node(&self, id: NodeId, f: &mut impl FnMut(NodeId) -> bool) { @@ -1485,33 +1563,33 @@ pub enum ScheduleBuildError { #[error("`{0:?}` contains itself.")] HierarchyLoop(String), /// The hierarchy of system sets contains a cycle. - #[error("System set hierarchy contains cycle(s).")] - HierarchyCycle, + #[error("System set hierarchy contains cycle(s).\n{0}")] + HierarchyCycle(String), /// The hierarchy of system sets contains redundant edges. /// /// This error is disabled by default, but can be opted-in using [`ScheduleBuildSettings`]. - #[error("System set hierarchy contains redundant edges.")] - HierarchyRedundancy, + #[error("System set hierarchy contains redundant edges.\n{0}")] + HierarchyRedundancy(String), /// A system (set) has been told to run before itself. #[error("`{0:?}` depends on itself.")] DependencyLoop(String), /// The dependency graph contains a cycle. - #[error("System dependencies contain cycle(s).")] - DependencyCycle, + #[error("System dependencies contain cycle(s).\n{0}")] + DependencyCycle(String), /// Tried to order a system (set) relative to a system set it belongs to. - #[error("`{0:?}` and `{1:?}` have both `in_set` and `before`-`after` relationships (these might be transitive). This combination is unsolvable as a system cannot run before or after a set it belongs to.")] + #[error("`{0}` and `{1}` have both `in_set` and `before`-`after` relationships (these might be transitive). This combination is unsolvable as a system cannot run before or after a set it belongs to.")] CrossDependency(String, String), /// Tried to order system sets that share systems. - #[error("`{0:?}` and `{1:?}` have a `before`-`after` relationship (which may be transitive) but share systems.")] + #[error("`{0}` and `{1}` have a `before`-`after` relationship (which may be transitive) but share systems.")] SetsHaveOrderButIntersect(String, String), /// Tried to order a system (set) relative to all instances of some system function. - #[error("Tried to order against `fn {0:?}` in a schedule that has more than one `{0:?}` instance. `fn {0:?}` is a `SystemTypeSet` and cannot be used for ordering if ambiguous. Use a different set without this restriction.")] + #[error("Tried to order against `{0}` in a schedule that has more than one `{0}` instance. `{0}` is a `SystemTypeSet` and cannot be used for ordering if ambiguous. Use a different set without this restriction.")] SystemTypeSetAmbiguity(String), /// Systems with conflicting access have indeterminate run order. /// /// This error is disabled by default, but can be opted-in using [`ScheduleBuildSettings`]. - #[error("Systems with conflicting access have indeterminate run order.")] - Ambiguity, + #[error("Systems with conflicting access have indeterminate run order.\n{0}")] + Ambiguity(String), /// Tried to run a schedule before all of its systems have been initialized. #[error("Systems in schedule have not been initialized.")] Uninitialized, diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 6e0caaba0937c..1277f4b54dc2b 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -71,7 +71,7 @@ impl SystemTypeSet { impl Debug for SystemTypeSet { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_tuple("SystemTypeSet") - .field(&std::any::type_name::()) + .field(&format_args!("fn {}()", &std::any::type_name::())) .finish() } } From aa489eced0ecfe0de38a9afca5a56c7934b949ab Mon Sep 17 00:00:00 2001 From: James O'Brien Date: Sun, 27 Aug 2023 13:37:17 -0700 Subject: [PATCH 12/58] Swap TransparentUi to use a stable sort (#9598) # Objective - Fix the `borders`, `ui` and `text_wrap_debug` examples. ## Solution - Swap `TransparentUi` to use a stable sort --- This is the smallest change to fix the examples but ideally this is fixed by setting better sort keys for the UI elements such that we can swap back to an unstable sort. --- crates/bevy_ui/src/render/render_pass.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index c464b2a5853ea..697aa11104c7e 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -112,6 +112,11 @@ impl PhaseItem for TransparentUi { self.draw_function } + #[inline] + fn sort(items: &mut [Self]) { + items.sort_by_key(|item| item.sort_key()); + } + #[inline] fn batch_size(&self) -> usize { self.batch_size From 474b55a29c0dc0875735e51c0e994bf27c8fc5ab Mon Sep 17 00:00:00 2001 From: Joseph <21144246+JoJoJet@users.noreply.github.com> Date: Mon, 28 Aug 2023 09:36:46 -0700 Subject: [PATCH 13/58] Add `system.map(...)` for transforming the output of a system (#8526) # Objective Any time we wish to transform the output of a system, we currently use system piping to do so: ```rust my_system.pipe(|In(x)| do_something(x)) ``` Unfortunately, system piping is not a zero cost abstraction. Each call to `.pipe` requires allocating two extra access sets: one for the second system and one for the combined accesses of both systems. This also adds extra work to each call to `update_archetype_component_access`, which stacks as one adds multiple layers of system piping. ## Solution Add the `AdapterSystem` abstraction: similar to `CombinatorSystem`, this allows you to implement a trait to generically control how a system is run and how its inputs and outputs are processed. Unlike `CombinatorSystem`, this does not have any overhead when computing world accesses which makes it ideal for simple operations such as inverting or ignoring the output of a system. Add the extension method `.map(...)`: this is similar to `.pipe(...)`, only it accepts a closure as an argument instead of an `In` system. ```rust my_system.map(do_something) ``` This has the added benefit of making system names less messy: a system that ignores its output will just be called `my_system`, instead of `Pipe(my_system, ignore)` --- ## Changelog TODO ## Migration Guide The `system_adapter` functions have been deprecated: use `.map` instead, which is a lightweight alternative to `.pipe`. ```rust // Before: my_system.pipe(system_adapter::ignore) my_system.pipe(system_adapter::unwrap) my_system.pipe(system_adapter::new(T::from)) // After: my_system.map(std::mem::drop) my_system.map(Result::unwrap) my_system.map(T::from) // Before: my_system.pipe(system_adapter::info) my_system.pipe(system_adapter::dbg) my_system.pipe(system_adapter::warn) my_system.pipe(system_adapter::error) // After: my_system.map(bevy_utils::info) my_system.map(bevy_utils::dbg) my_system.map(bevy_utils::warn) my_system.map(bevy_utils::error) ``` --------- Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/lib.rs | 6 +- crates/bevy_ecs/src/schedule/condition.rs | 99 ++--------- crates/bevy_ecs/src/system/adapter_system.rs | 170 +++++++++++++++++++ crates/bevy_ecs/src/system/mod.rs | 48 +++++- crates/bevy_utils/src/lib.rs | 24 +++ examples/ecs/system_piping.rs | 12 +- 6 files changed, 261 insertions(+), 98 deletions(-) create mode 100644 crates/bevy_ecs/src/system/adapter_system.rs diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index cb91dae60351e..c60356f4da91c 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -30,6 +30,10 @@ pub mod prelude { #[doc(hidden)] #[cfg(feature = "bevy_reflect")] pub use crate::reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}; + #[allow(deprecated)] + pub use crate::system::adapter::{ + self as system_adapter, dbg, error, ignore, info, unwrap, warn, + }; #[doc(hidden)] pub use crate::{ bundle::Bundle, @@ -45,8 +49,6 @@ pub mod prelude { OnEnter, OnExit, OnTransition, Schedule, Schedules, State, States, SystemSet, }, system::{ - adapter as system_adapter, - adapter::{dbg, error, ignore, info, unwrap, warn}, Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, SystemParamFunction, }, diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index abaf9bbf62c2d..8186819ccdb63 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -1,12 +1,9 @@ -use std::any::TypeId; use std::borrow::Cow; use std::ops::Not; -use crate::component::{self, ComponentId}; -use crate::query::Access; -use crate::system::{CombinatorSystem, Combine, IntoSystem, ReadOnlySystem, System}; -use crate::world::unsafe_world_cell::UnsafeWorldCell; -use crate::world::World; +use crate::system::{ + Adapt, AdapterSystem, CombinatorSystem, Combine, IntoSystem, ReadOnlySystem, System, +}; /// A type-erased run condition stored in a [`Box`]. pub type BoxedCondition = Box>; @@ -181,8 +178,6 @@ mod sealed { /// A collection of [run conditions](Condition) that may be useful in any bevy app. pub mod common_conditions { - use std::borrow::Cow; - use super::NotSystem; use crate::{ change_detection::DetectChanges, @@ -987,98 +982,32 @@ pub mod common_conditions { { let condition = IntoSystem::into_system(condition); let name = format!("!{}", condition.name()); - NotSystem:: { - condition, - name: Cow::Owned(name), - } + NotSystem::new(super::NotMarker, condition, name.into()) } } /// Invokes [`Not`] with the output of another system. /// /// See [`common_conditions::not`] for examples. -#[derive(Clone)] -pub struct NotSystem -where - T::Out: Not, -{ - condition: T, - name: Cow<'static, str>, -} -impl System for NotSystem +pub type NotSystem = AdapterSystem; + +/// Used with [`AdapterSystem`] to negate the output of a system via the [`Not`] operator. +#[doc(hidden)] +#[derive(Clone, Copy)] +pub struct NotMarker; + +impl Adapt for NotMarker where T::Out: Not, { type In = T::In; type Out = ::Output; - fn name(&self) -> Cow<'static, str> { - self.name.clone() - } - - fn type_id(&self) -> TypeId { - TypeId::of::() - } - - fn component_access(&self) -> &Access { - self.condition.component_access() - } - - fn archetype_component_access(&self) -> &Access { - self.condition.archetype_component_access() - } - - fn is_send(&self) -> bool { - self.condition.is_send() - } - - fn is_exclusive(&self) -> bool { - self.condition.is_exclusive() - } - - unsafe fn run_unsafe(&mut self, input: Self::In, world: UnsafeWorldCell) -> Self::Out { - // SAFETY: The inner condition system asserts its own safety. - !self.condition.run_unsafe(input, world) - } - - fn run(&mut self, input: Self::In, world: &mut World) -> Self::Out { - !self.condition.run(input, world) - } - - fn apply_deferred(&mut self, world: &mut World) { - self.condition.apply_deferred(world); - } - - fn initialize(&mut self, world: &mut World) { - self.condition.initialize(world); - } - - fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { - self.condition.update_archetype_component_access(world); - } - - fn check_change_tick(&mut self, change_tick: component::Tick) { - self.condition.check_change_tick(change_tick); - } - - fn get_last_run(&self) -> component::Tick { - self.condition.get_last_run() - } - - fn set_last_run(&mut self, last_run: component::Tick) { - self.condition.set_last_run(last_run); + fn adapt(&mut self, input: Self::In, run_system: impl FnOnce(T::In) -> T::Out) -> Self::Out { + !run_system(input) } } -// SAFETY: This trait is only implemented when the inner system is read-only. -// The `Not` condition does not modify access, and thus cannot transform a read-only system into one that is not. -unsafe impl ReadOnlySystem for NotSystem -where - T: ReadOnlySystem, - T::Out: Not, -{ -} - /// Combines the outputs of two systems using the `&&` operator. pub type AndThen = CombinatorSystem; diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs new file mode 100644 index 0000000000000..288e91138868b --- /dev/null +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -0,0 +1,170 @@ +use std::borrow::Cow; + +use super::{ReadOnlySystem, System}; +use crate::world::unsafe_world_cell::UnsafeWorldCell; + +/// Customizes the behavior of an [`AdapterSystem`] +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// use bevy_ecs::system::{Adapt, AdapterSystem}; +/// +/// // A system adapter that inverts the result of a system. +/// // NOTE: Instead of manually implementing this, you can just use `bevy_ecs::schedule::common_conditions::not`. +/// pub type NotSystem = AdapterSystem; +/// +/// // This struct is used to customize the behavior of our adapter. +/// pub struct NotMarker; +/// +/// impl Adapt for NotMarker +/// where +/// S: System, +/// S::Out: std::ops::Not, +/// { +/// type In = S::In; +/// type Out = ::Output; +/// +/// fn adapt( +/// &mut self, +/// input: Self::In, +/// run_system: impl FnOnce(S::In) -> S::Out, +/// ) -> Self::Out { +/// !run_system(input) +/// } +/// } +/// # let mut world = World::new(); +/// # let mut system = NotSystem::new(NotMarker, IntoSystem::into_system(|| false), "".into()); +/// # system.initialize(&mut world); +/// # assert!(system.run((), &mut world)); +/// ``` +pub trait Adapt: Send + Sync + 'static { + /// The [input](System::In) type for an [`AdapterSystem`]. + type In; + /// The [output](System::Out) type for an [`AdapterSystem`]. + type Out; + + /// When used in an [`AdapterSystem`], this function customizes how the system + /// is run and how its inputs/outputs are adapted. + fn adapt(&mut self, input: Self::In, run_system: impl FnOnce(S::In) -> S::Out) -> Self::Out; +} + +/// A [`System`] that takes the output of `S` and transforms it by applying `Func` to it. +#[derive(Clone)] +pub struct AdapterSystem { + func: Func, + system: S, + name: Cow<'static, str>, +} + +impl AdapterSystem +where + Func: Adapt, + S: System, +{ + /// Creates a new [`System`] that uses `func` to adapt `system`, via the [`Adapt`] trait. + pub const fn new(func: Func, system: S, name: Cow<'static, str>) -> Self { + Self { func, system, name } + } +} + +impl System for AdapterSystem +where + Func: Adapt, + S: System, +{ + type In = Func::In; + type Out = Func::Out; + + fn name(&self) -> Cow<'static, str> { + self.name.clone() + } + + fn type_id(&self) -> std::any::TypeId { + std::any::TypeId::of::() + } + + fn component_access(&self) -> &crate::query::Access { + self.system.component_access() + } + + #[inline] + fn archetype_component_access( + &self, + ) -> &crate::query::Access { + self.system.archetype_component_access() + } + + fn is_send(&self) -> bool { + self.system.is_send() + } + + fn is_exclusive(&self) -> bool { + self.system.is_exclusive() + } + + #[inline] + unsafe fn run_unsafe(&mut self, input: Self::In, world: UnsafeWorldCell) -> Self::Out { + // SAFETY: `system.run_unsafe` has the same invariants as `self.run_unsafe`. + self.func + .adapt(input, |input| self.system.run_unsafe(input, world)) + } + + #[inline] + fn run(&mut self, input: Self::In, world: &mut crate::prelude::World) -> Self::Out { + self.func + .adapt(input, |input| self.system.run(input, world)) + } + + #[inline] + fn apply_deferred(&mut self, world: &mut crate::prelude::World) { + self.system.apply_deferred(world); + } + + fn initialize(&mut self, world: &mut crate::prelude::World) { + self.system.initialize(world); + } + + #[inline] + fn update_archetype_component_access(&mut self, world: UnsafeWorldCell) { + self.system.update_archetype_component_access(world); + } + + fn check_change_tick(&mut self, change_tick: crate::component::Tick) { + self.system.check_change_tick(change_tick); + } + + fn get_last_run(&self) -> crate::component::Tick { + self.system.get_last_run() + } + + fn set_last_run(&mut self, last_run: crate::component::Tick) { + self.system.set_last_run(last_run); + } + + fn default_system_sets(&self) -> Vec> { + self.system.default_system_sets() + } +} + +// SAFETY: The inner system is read-only. +unsafe impl ReadOnlySystem for AdapterSystem +where + Func: Adapt, + S: ReadOnlySystem, +{ +} + +impl Adapt for F +where + S: System, + F: Send + Sync + 'static + FnMut(S::Out) -> Out, +{ + type In = S::In; + type Out = Out; + + fn adapt(&mut self, input: S::In, run_system: impl FnOnce(S::In) -> S::Out) -> Out { + self(run_system(input)) + } +} diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 3900dd68ac6ba..051795fd566eb 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -102,6 +102,7 @@ //! - All tuples between 1 to 16 elements where each element implements [`SystemParam`] //! - [`()` (unit primitive type)](https://doc.rust-lang.org/stable/std/primitive.unit.html) +mod adapter_system; mod combinator; mod commands; mod exclusive_function_system; @@ -114,6 +115,7 @@ mod system_param; use std::borrow::Cow; +pub use adapter_system::*; pub use combinator::*; pub use commands::*; pub use exclusive_function_system::*; @@ -162,6 +164,34 @@ pub trait IntoSystem: Sized { let name = format!("Pipe({}, {})", system_a.name(), system_b.name()); PipeSystem::new(system_a, system_b, Cow::Owned(name)) } + + /// Pass the output of this system into the passed function `f`, creating a new system that + /// outputs the value returned from the function. + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # let mut schedule = Schedule::new(); + /// // Ignores the output of a system that may fail. + /// schedule.add_systems(my_system.map(std::mem::drop)); + /// # let mut world = World::new(); + /// # world.insert_resource(T); + /// # schedule.run(&mut world); + /// + /// # #[derive(Resource)] struct T; + /// # type Err = (); + /// fn my_system(res: Res) -> Result<(), Err> { + /// // ... + /// # Err(()) + /// } + /// ``` + fn map(self, f: F) -> AdapterSystem + where + F: Send + Sync + 'static + FnMut(Out) -> T, + { + let system = Self::into_system(self); + let name = system.name(); + AdapterSystem::new(f, system, name) + } } // All systems implicitly implement IntoSystem. @@ -201,6 +231,7 @@ impl IntoSystem for T { pub struct In(pub In); /// A collection of common adapters for [piping](crate::system::PipeSystem) the result of a system. +#[deprecated = "this form of system adapter has been deprecated in favor of `system.map(...)`"] pub mod adapter { use crate::system::In; use bevy_utils::tracing; @@ -223,6 +254,7 @@ pub mod adapter { /// println!("{x:?}"); /// } /// ``` + #[deprecated = "use `.map(...)` instead"] pub fn new(mut f: impl FnMut(T) -> U) -> impl FnMut(In) -> U { move |In(x)| f(x) } @@ -260,6 +292,7 @@ pub mod adapter { /// # fn open_file(name: &str) -> Result<&'static str, std::io::Error> /// # { Ok("hello world") } /// ``` + #[deprecated = "use `.map(Result::unwrap)` instead"] pub fn unwrap(In(res): In>) -> T { res.unwrap() } @@ -287,6 +320,7 @@ pub mod adapter { /// "42".to_string() /// } /// ``` + #[deprecated = "use `.map(bevy_utils::info)` instead"] pub fn info(In(data): In) { tracing::info!("{:?}", data); } @@ -314,6 +348,7 @@ pub mod adapter { /// Ok("42".parse()?) /// } /// ``` + #[deprecated = "use `.map(bevy_utils::dbg)` instead"] pub fn dbg(In(data): In) { tracing::debug!("{:?}", data); } @@ -341,6 +376,7 @@ pub mod adapter { /// Err("Got to rusty?".to_string()) /// } /// ``` + #[deprecated = "use `.map(bevy_utils::warn)` instead"] pub fn warn(In(res): In>) { if let Err(warn) = res { tracing::warn!("{:?}", warn); @@ -369,6 +405,7 @@ pub mod adapter { /// Err("Some error".to_owned()) /// } /// ``` + #[deprecated = "use `.map(bevy_utils::error)` instead"] pub fn error(In(res): In>) { if let Err(error) = res { tracing::error!("{:?}", error); @@ -409,6 +446,7 @@ pub mod adapter { /// Some(()) /// } /// ``` + #[deprecated = "use `.map(std::mem::drop)` instead"] pub fn ignore(In(_): In) {} } @@ -510,7 +548,7 @@ mod tests { Schedule, }, system::{ - adapter::new, Commands, In, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, + Commands, In, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, QueryComponentError, Res, ResMut, Resource, System, SystemState, }, world::{FromWorld, World}, @@ -1785,10 +1823,10 @@ mod tests { !val } - assert_is_system(returning::>.pipe(unwrap)); - assert_is_system(returning::>.pipe(ignore)); - assert_is_system(returning::<&str>.pipe(new(u64::from_str)).pipe(unwrap)); - assert_is_system(exclusive_in_out::<(), Result<(), std::io::Error>>.pipe(error)); + assert_is_system(returning::>.map(Result::unwrap)); + assert_is_system(returning::>.map(std::mem::drop)); + assert_is_system(returning::<&str>.map(u64::from_str).map(Result::unwrap)); + assert_is_system(exclusive_in_out::<(), Result<(), std::io::Error>>.map(bevy_utils::error)); assert_is_system(returning::.pipe(exclusive_in_out::)); returning::<()>.run_if(returning::.pipe(not)); diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 3dc32e81d8904..1c3152a043f04 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -298,6 +298,30 @@ impl Drop for OnDrop { } } +/// Calls the [`tracing::info!`] macro on a value. +pub fn info(data: T) { + tracing::info!("{:?}", data); +} + +/// Calls the [`tracing::debug!`] macro on a value. +pub fn dbg(data: T) { + tracing::debug!("{:?}", data); +} + +/// Processes a [`Result`] by calling the [`tracing::warn!`] macro in case of an [`Err`] value. +pub fn warn(result: Result<(), E>) { + if let Err(warn) = result { + tracing::warn!("{:?}", warn); + } +} + +/// Processes a [`Result`] by calling the [`tracing::error!`] macro in case of an [`Err`] value. +pub fn error(result: Result<(), E>) { + if let Err(error) = result { + tracing::error!("{:?}", error); + } +} + /// Like [`tracing::trace`], but conditional on cargo feature `detailed_trace`. #[macro_export] macro_rules! detailed_trace { diff --git a/examples/ecs/system_piping.rs b/examples/ecs/system_piping.rs index f2584c46ad8a5..0863daa700797 100644 --- a/examples/ecs/system_piping.rs +++ b/examples/ecs/system_piping.rs @@ -5,7 +5,7 @@ use bevy::prelude::*; use std::num::ParseIntError; use bevy::log::LogPlugin; -use bevy::utils::tracing::Level; +use bevy::utils::{dbg, error, info, tracing::Level, warn}; fn main() { App::new() @@ -19,11 +19,11 @@ fn main() { Update, ( parse_message_system.pipe(handler_system), - data_pipe_system.pipe(info), - parse_message_system.pipe(dbg), - warning_pipe_system.pipe(warn), - parse_error_message_system.pipe(error), - parse_message_system.pipe(ignore), + data_pipe_system.map(info), + parse_message_system.map(dbg), + warning_pipe_system.map(warn), + parse_error_message_system.map(error), + parse_message_system.map(std::mem::drop), ), ) .run(); From 3cf94e7c9d434a7fb21be1ff266c3fa43d13ea3d Mon Sep 17 00:00:00 2001 From: Aceeri Date: Mon, 28 Aug 2023 09:37:44 -0700 Subject: [PATCH 14/58] Check cursor position for out of bounds of the window (#8855) # Objective Fixes #8840 Make the cursor position more consistent, right now the cursor position is *sometimes* outside of the window and returns the position and *sometimes* `None`. Even in the cases where someone might be using that position that is outside of the window, it'll probably require some manual transformations for it to actually be useful. ## Solution Check the windows width and height for out of bounds positions. --- ## Changelog - Cursor position is now always `None` when outside of the window. --------- Co-authored-by: ickshonpe --- crates/bevy_window/src/window.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index d5d191d70d2ca..10420f44c0cdb 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -2,7 +2,7 @@ use bevy_ecs::{ entity::{Entity, EntityMapper, MapEntities}, prelude::{Component, ReflectComponent}, }; -use bevy_math::{DVec2, IVec2, Vec2}; +use bevy_math::{DVec2, IVec2, Rect, Vec2}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; #[cfg(feature = "serialize")] @@ -316,9 +316,8 @@ impl Window { /// See [`WindowResolution`] for an explanation about logical/physical sizes. #[inline] pub fn cursor_position(&self) -> Option { - self.internal - .physical_cursor_position - .map(|position| (position / self.scale_factor()).as_vec2()) + self.physical_cursor_position() + .map(|position| (position.as_dvec2() / self.scale_factor()).as_vec2()) } /// The cursor position in this window in physical pixels. @@ -328,9 +327,17 @@ impl Window { /// See [`WindowResolution`] for an explanation about logical/physical sizes. #[inline] pub fn physical_cursor_position(&self) -> Option { - self.internal - .physical_cursor_position - .map(|position| position.as_vec2()) + match self.internal.physical_cursor_position { + Some(position) => { + let position = position.as_vec2(); + if Rect::new(0., 0., self.width(), self.height()).contains(position) { + Some(position) + } else { + None + } + } + None => None, + } } /// Set the cursor position in this window in logical pixels. From db5f80b2beb7ddcf6d666ac0522b96a98429f3a2 Mon Sep 17 00:00:00 2001 From: DevinLeamy Date: Mon, 28 Aug 2023 12:43:04 -0400 Subject: [PATCH 15/58] API updates to the AnimationPlayer (#9002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Added `AnimationPlayer` API UX improvements. - Succestor to https://github.com/bevyengine/bevy/pull/5912 - Fixes https://github.com/bevyengine/bevy/issues/5848 _(Credits to @asafigan for filing #5848, creating the initial pull request, and the discussion in #5912)_ ## Solution - Created `RepeatAnimation` enum to describe an animation repetition behavior. - Added `is_finished()`, `set_repeat()`, and `is_playback_reversed()` methods to the animation player. - ~~Made the animation clip optional as per the comment from #5912~~ > ~~My problem is that the default handle [used the initialize a `PlayingAnimation`] could actually refer to an actual animation if an AnimationClip is set for the default handle, which leads me to ask, "Should animation_clip should be an Option?"~~ - Added an accessor for the animation clip `animation_clip()` to the animation player. To determine if an animation is finished, we use the number of times the animation has completed and the repetition behavior. If the animation is playing in reverse then `elapsed < 0.0` counts as a completion. Otherwise, `elapsed > animation.duration` counts as a completion. This is what I would expect, personally. If there's any ambiguity, perhaps we could add some `AnimationCompletionBehavior`, to specify that kind of completion behavior to use. Update: Previously `PlayingAnimation::elapsed` was being used as the seek time into the animation clip. This was misleading because if you increased the speed of the animation it would also increase (or decrease) the elapsed time. In other words, the elapsed time was not actually the elapsed time. To solve this, we introduce `PlayingAnimation::seek_time` to serve as the value we manipulate the move between keyframes. Consequently, `elapsed()` now returns the actual elapsed time, and is not effected by the animation speed. Because `set_elapsed` was being used to manipulate the displayed keyframe, we introduce `AnimationPlayer::seek_to` and `AnimationPlayer::replay` to provide this functionality. ## Migration Guide - Removed `set_elapsed`. - Removed `stop_repeating` in favour of `AnimationPlayer::set_repeat(RepeatAnimation::Never)`. - Introduced `seek_to` to seek to a given timestamp inside of the animation. - Introduced `seek_time` accessor for the `PlayingAnimation::seek_to`. - Introduced `AnimationPlayer::replay` to reset the `PlayingAnimation` to a state where no time has elapsed. --------- Co-authored-by: Hennadii Chernyshchyk Co-authored-by: François --- crates/bevy_animation/src/lib.rs | 162 ++++++++++++++---- examples/animation/animated_fox.rs | 30 +++- examples/stress_tests/many_foxes.rs | 8 +- .../tools/scene_viewer/animation_plugin.rs | 2 +- 4 files changed, 162 insertions(+), 40 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index b84e871f042d8..59eab3f92bdae 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -102,12 +102,6 @@ impl AnimationClip { self.duration } - /// The bone ids mapped by their [`EntityPath`]. - #[inline] - pub fn paths(&self) -> &HashMap { - &self.paths - } - /// Add a [`VariableCurve`] to an [`EntityPath`]. pub fn add_curve_to_path(&mut self, path: EntityPath, curve: VariableCurve) { // Update the duration of the animation by this curve duration if it's longer @@ -129,25 +123,94 @@ impl AnimationClip { } } +/// Repetition behavior of an animation. +#[derive(Reflect, Copy, Clone, Default)] +pub enum RepeatAnimation { + /// The animation will finish after running once. + #[default] + Never, + /// The animation will finish after running "n" times. + Count(u32), + /// The animation will never finish. + Forever, +} + #[derive(Reflect)] struct PlayingAnimation { - repeat: bool, + repeat: RepeatAnimation, speed: f32, + /// Total time the animation has been played. + /// + /// Note: Time does not increase when the animation is paused or after it has completed. elapsed: f32, + /// The timestamp inside of the animation clip. + /// + /// Note: This will always be in the range [0.0, animation clip duration] + seek_time: f32, animation_clip: Handle, path_cache: Vec>>, + /// Number of times the animation has completed. + /// If the animation is playing in reverse, this increments when the animation passes the start. + completions: u32, } impl Default for PlayingAnimation { fn default() -> Self { Self { - repeat: false, + repeat: RepeatAnimation::default(), speed: 1.0, elapsed: 0.0, + seek_time: 0.0, animation_clip: Default::default(), path_cache: Vec::new(), + completions: 0, + } + } +} + +impl PlayingAnimation { + /// Check if the animation has finished, based on its repetition behavior and the number of times it has repeated. + /// + /// Note: An animation with `RepeatAnimation::Forever` will never finish. + #[inline] + pub fn is_finished(&self) -> bool { + match self.repeat { + RepeatAnimation::Forever => false, + RepeatAnimation::Never => self.completions >= 1, + RepeatAnimation::Count(n) => self.completions >= n, + } + } + + /// Update the animation given the delta time and the duration of the clip being played. + #[inline] + fn update(&mut self, delta: f32, clip_duration: f32) { + if self.is_finished() { + return; + } + + self.elapsed += delta; + self.seek_time += delta * self.speed; + + if (self.seek_time > clip_duration && self.speed > 0.0) + || (self.seek_time < 0.0 && self.speed < 0.0) + { + self.completions += 1; + } + + if self.seek_time >= clip_duration { + self.seek_time %= clip_duration; + } + if self.seek_time < 0.0 { + self.seek_time += clip_duration; } } + + /// Reset back to the initial state as if no time has elapsed. + fn replay(&mut self) { + self.completions = 0; + self.elapsed = 0.0; + self.seek_time = 0.0; + } } /// An animation that is being faded out as part of a transition @@ -222,7 +285,7 @@ impl AnimationPlayer { /// If `transition_duration` is set, this will use a linear blending /// between the previous and the new animation to make a smooth transition pub fn play(&mut self, handle: Handle) -> &mut Self { - if self.animation.animation_clip != handle || self.is_paused() { + if !self.is_playing_clip(&handle) || self.is_paused() { self.start(handle); } self @@ -235,24 +298,56 @@ impl AnimationPlayer { handle: Handle, transition_duration: Duration, ) -> &mut Self { - if self.animation.animation_clip != handle || self.is_paused() { + if !self.is_playing_clip(&handle) || self.is_paused() { self.start_with_transition(handle, transition_duration); } self } - /// Set the animation to repeat + /// Handle to the animation clip being played. + pub fn animation_clip(&self) -> &Handle { + &self.animation.animation_clip + } + + /// Check if the given animation clip is being played. + pub fn is_playing_clip(&self, handle: &Handle) -> bool { + self.animation_clip() == handle + } + + /// Check if the playing animation has finished, according to the repetition behavior. + pub fn is_finished(&self) -> bool { + self.animation.is_finished() + } + + /// Sets repeat to [`RepeatAnimation::Forever`]. + /// + /// See also [`Self::set_repeat`]. pub fn repeat(&mut self) -> &mut Self { - self.animation.repeat = true; + self.animation.repeat = RepeatAnimation::Forever; self } - /// Stop the animation from repeating - pub fn stop_repeating(&mut self) -> &mut Self { - self.animation.repeat = false; + /// Set the repetition behaviour of the animation. + pub fn set_repeat(&mut self, repeat: RepeatAnimation) -> &mut Self { + self.animation.repeat = repeat; self } + /// Repetition behavior of the animation. + pub fn repeat_mode(&self) -> RepeatAnimation { + self.animation.repeat + } + + /// Number of times the animation has completed. + pub fn completions(&self) -> u32 { + self.animation.completions + } + + /// Check if the animation is playing in reverse. + pub fn is_playback_reversed(&self) -> bool { + self.animation.speed < 0.0 + } + /// Pause the animation pub fn pause(&mut self) { self.paused = true; @@ -284,11 +379,21 @@ impl AnimationPlayer { self.animation.elapsed } - /// Seek to a specific time in the animation - pub fn set_elapsed(&mut self, elapsed: f32) -> &mut Self { - self.animation.elapsed = elapsed; + /// Seek time inside of the animation. Always within the range [0.0, clip duration]. + pub fn seek_time(&self) -> f32 { + self.animation.seek_time + } + + /// Seek to a specific time in the animation. + pub fn seek_to(&mut self, seek_time: f32) -> &mut Self { + self.animation.seek_time = seek_time; self } + + /// Reset the animation to its initial state, as if no time has elapsed. + pub fn replay(&mut self) { + self.animation.replay(); + } } fn entity_from_path( @@ -489,16 +594,12 @@ fn apply_animation( children: &Query<&Children>, ) { if let Some(animation_clip) = animations.get(&animation.animation_clip) { - if !paused { - animation.elapsed += time.delta_seconds() * animation.speed; - } - let mut elapsed = animation.elapsed; - if animation.repeat { - elapsed %= animation_clip.duration; - } - if elapsed < 0.0 { - elapsed += animation_clip.duration; - } + // We don't return early because seek_to() may have been called on the animation player. + animation.update( + if paused { 0.0 } else { time.delta_seconds() }, + animation_clip.duration, + ); + if animation.path_cache.len() != animation_clip.paths.len() { animation.path_cache = vec![Vec::new(); animation_clip.paths.len()]; } @@ -556,7 +657,7 @@ fn apply_animation( // PERF: finding the current keyframe can be optimised let step_start = match curve .keyframe_timestamps - .binary_search_by(|probe| probe.partial_cmp(&elapsed).unwrap()) + .binary_search_by(|probe| probe.partial_cmp(&animation.seek_time).unwrap()) { Ok(n) if n >= curve.keyframe_timestamps.len() - 1 => continue, // this curve is finished Ok(i) => i, @@ -566,7 +667,7 @@ fn apply_animation( }; let ts_start = curve.keyframe_timestamps[step_start]; let ts_end = curve.keyframe_timestamps[step_start + 1]; - let lerp = (elapsed - ts_start) / (ts_end - ts_start); + let lerp = (animation.seek_time - ts_start) / (ts_end - ts_start); // Apply the keyframe match &curve.keyframes { @@ -620,7 +721,6 @@ impl Plugin for AnimationPlugin { app.add_asset::() .register_asset_reflect::() .register_type::() - .register_type::() .add_systems( PostUpdate, animation_player.before(TransformSystem::TransformPropagate), diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 0052cbce75a9a..808d04adf13f7 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -5,6 +5,7 @@ use std::time::Duration; use bevy::pbr::CascadeShadowConfigBuilder; use bevy::prelude::*; +use bevy_internal::animation::RepeatAnimation; fn main() { App::new() @@ -77,6 +78,8 @@ fn setup( println!(" - spacebar: play / pause"); println!(" - arrow up / down: speed up / slow down animation playback"); println!(" - arrow left / right: seek backward / forward"); + println!(" - digit 1 / 3 / 5: play the animation times"); + println!(" - L: loop the animation forever"); println!(" - return: change animation"); } @@ -116,13 +119,13 @@ fn keyboard_animation_control( } if keyboard_input.just_pressed(KeyCode::Left) { - let elapsed = player.elapsed(); - player.set_elapsed(elapsed - 0.1); + let elapsed = player.seek_time(); + player.seek_to(elapsed - 0.1); } if keyboard_input.just_pressed(KeyCode::Right) { - let elapsed = player.elapsed(); - player.set_elapsed(elapsed + 0.1); + let elapsed = player.seek_time(); + player.seek_to(elapsed + 0.1); } if keyboard_input.just_pressed(KeyCode::Return) { @@ -134,5 +137,24 @@ fn keyboard_animation_control( ) .repeat(); } + + if keyboard_input.just_pressed(KeyCode::Key1) { + player.set_repeat(RepeatAnimation::Count(1)); + player.replay(); + } + + if keyboard_input.just_pressed(KeyCode::Key3) { + player.set_repeat(RepeatAnimation::Count(3)); + player.replay(); + } + + if keyboard_input.just_pressed(KeyCode::Key5) { + player.set_repeat(RepeatAnimation::Count(5)); + player.replay(); + } + + if keyboard_input.just_pressed(KeyCode::L) { + player.set_repeat(RepeatAnimation::Forever); + } } } diff --git a/examples/stress_tests/many_foxes.rs b/examples/stress_tests/many_foxes.rs index 18dee869bcf79..194100658bd6e 100644 --- a/examples/stress_tests/many_foxes.rs +++ b/examples/stress_tests/many_foxes.rs @@ -270,13 +270,13 @@ fn keyboard_animation_control( } if keyboard_input.just_pressed(KeyCode::Left) { - let elapsed = player.elapsed(); - player.set_elapsed(elapsed - 0.1); + let elapsed = player.seek_time(); + player.seek_to(elapsed - 0.1); } if keyboard_input.just_pressed(KeyCode::Right) { - let elapsed = player.elapsed(); - player.set_elapsed(elapsed + 0.1); + let elapsed = player.seek_time(); + player.seek_to(elapsed + 0.1); } if keyboard_input.just_pressed(KeyCode::Return) { diff --git a/examples/tools/scene_viewer/animation_plugin.rs b/examples/tools/scene_viewer/animation_plugin.rs index 4c4e032a3871f..e1f2970547961 100644 --- a/examples/tools/scene_viewer/animation_plugin.rs +++ b/examples/tools/scene_viewer/animation_plugin.rs @@ -91,7 +91,7 @@ fn handle_inputs( let resume = !player.is_paused(); // set the current animation to its start and pause it to reset to its starting state - player.set_elapsed(0.0).pause(); + player.seek_to(0.0).pause(); clips.advance_to_next(); let current_clip = clips.current(); From afcb1fee909e8cb06519135257089778fef06fcc Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Mon, 28 Aug 2023 18:46:16 +0200 Subject: [PATCH 16/58] Cleanup some bevy_text pipeline.rs (#9111) ## Objective - `bevy_text/src/pipeline.rs` had some crufty code. ## Solution Remove the cruft. - `&mut self` argument was unused by `TextPipeline::create_text_measure`, so we replace it with a constructor `TextMeasureInfo::from_text`. - We also pass a `&Text` to `from_text` since there is no reason to split the struct before passing it as argument. - from_text also checks beforehand that every Font exist in the Assets. This allows rust to skip the drop code on the Vecs we create in the method, since there is no early exit. - We also remove the scaled_fonts field on `TextMeasureInfo`. This avoids an additional allocation. We can re-use the font on `fonts` instead in `compute_size`. Building a `ScaledFont` seems fairly cheap, when looking at the ab_glyph internals. - We also implement ToSectionText on TextMeasureSection, this let us skip creating a whole new Vec each time we call compute_size. - This let us remove compute_size_from_section_text, since its only purpose was to not have to allocate the Vec we just made redundant. - Make some immutabe `Vec` into `Box<[T]>` and `String` into `Box` - `{min,max}_width_content_size` fields of `TextMeasureInfo` have name `width` in them, yet the contain information on both width and height. - `TextMeasureInfo::linebreak_behaviour` -> `linebreak_behavior` ## Migration Guide - The `ResMut` argument to `measure_text_system` doesn't exist anymore. If you were calling this system manually, you should remove the argument. - The `{min,max}_width_content_size` fields of `TextMeasureInfo` are renamed to `min` and `max` respectively - Other changes to `TextMeasureInfo` may also break your code if you were manually building it. Please consider using the new `TextMeasureInfo::from_text` to build one instead. - `TextPipeline::create_text_measure` has been removed in favor of `TextMeasureInfo::from_text` --- crates/bevy_text/src/glyph_brush.rs | 8 +- crates/bevy_text/src/pipeline.rs | 165 +++++++++++++--------------- crates/bevy_text/src/text.rs | 3 +- crates/bevy_ui/src/widget/text.rs | 45 ++------ 4 files changed, 91 insertions(+), 130 deletions(-) diff --git a/crates/bevy_text/src/glyph_brush.rs b/crates/bevy_text/src/glyph_brush.rs index 70fea30322e18..d9d04a1ba125e 100644 --- a/crates/bevy_text/src/glyph_brush.rs +++ b/crates/bevy_text/src/glyph_brush.rs @@ -84,7 +84,7 @@ impl GlyphBrush { }) .collect::, _>>()?; - let text_bounds = compute_text_bounds(&glyphs, |index| §ions_data[index].3); + let text_bounds = compute_text_bounds(&glyphs, |index| sections_data[index].3); let mut positioned_glyphs = Vec::new(); for sg in glyphs { @@ -203,12 +203,12 @@ impl GlyphPlacementAdjuster { /// Computes the minimal bounding rectangle for a block of text. /// Ignores empty trailing lines. -pub(crate) fn compute_text_bounds<'a, T>( +pub(crate) fn compute_text_bounds( section_glyphs: &[SectionGlyph], - get_scaled_font: impl Fn(usize) -> &'a PxScaleFont, + get_scaled_font: impl Fn(usize) -> PxScaleFont, ) -> bevy_math::Rect where - T: ab_glyph::Font + 'a, + T: ab_glyph::Font, { let mut text_bounds = Rect { min: Vec2::splat(std::f32::MAX), diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 2d16ce03c3356..acde2c23ce612 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -1,4 +1,4 @@ -use ab_glyph::PxScale; +use ab_glyph::{Font as AbglyphFont, PxScale}; use bevy_asset::{Assets, Handle, HandleId}; use bevy_ecs::component::Component; use bevy_ecs::system::Resource; @@ -7,12 +7,12 @@ use bevy_render::texture::Image; use bevy_sprite::TextureAtlas; use bevy_utils::HashMap; -use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText}; +use glyph_brush_layout::{FontId, GlyphPositioner, SectionGeometry, SectionText, ToSectionText}; use crate::{ compute_text_bounds, error::TextError, glyph_brush::GlyphBrush, scale_value, BreakLineOn, Font, - FontAtlasSet, FontAtlasWarning, PositionedGlyph, TextAlignment, TextSection, TextSettings, - YAxisOrientation, + FontAtlasSet, FontAtlasWarning, PositionedGlyph, Text, TextAlignment, TextSection, + TextSettings, YAxisOrientation, }; #[derive(Default, Resource)] @@ -85,7 +85,7 @@ impl TextPipeline { return Ok(TextLayoutInfo::default()); } - let size = compute_text_bounds(§ion_glyphs, |index| &scaled_fonts[index]).size(); + let size = compute_text_bounds(§ion_glyphs, |index| scaled_fonts[index]).size(); let glyphs = self.brush.process_glyphs( section_glyphs, @@ -101,123 +101,110 @@ impl TextPipeline { Ok(TextLayoutInfo { glyphs, size }) } +} - pub fn create_text_measure( - &mut self, +#[derive(Debug, Clone)] +pub struct TextMeasureSection { + pub text: Box, + pub scale: f32, + pub font_id: FontId, +} + +#[derive(Debug, Clone, Default)] +pub struct TextMeasureInfo { + pub fonts: Box<[ab_glyph::FontArc]>, + pub sections: Box<[TextMeasureSection]>, + pub text_alignment: TextAlignment, + pub linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker, + pub min: Vec2, + pub max: Vec2, +} + +impl TextMeasureInfo { + pub fn from_text( + text: &Text, fonts: &Assets, - sections: &[TextSection], scale_factor: f64, - text_alignment: TextAlignment, - linebreak_behaviour: BreakLineOn, ) -> Result { - let mut auto_fonts = Vec::with_capacity(sections.len()); - let mut scaled_fonts = Vec::with_capacity(sections.len()); - let sections = sections + let sections = &text.sections; + for section in sections { + if !fonts.contains(§ion.style.font) { + return Err(TextError::NoSuchFont); + } + } + let (auto_fonts, sections) = sections .iter() .enumerate() .map(|(i, section)| { - let font = fonts - .get(§ion.style.font) - .ok_or(TextError::NoSuchFont)?; - let font_size = scale_value(section.style.font_size, scale_factor); - auto_fonts.push(font.font.clone()); - let px_scale_font = ab_glyph::Font::into_scaled(font.font.clone(), font_size); - scaled_fonts.push(px_scale_font); - - let section = TextMeasureSection { - font_id: FontId(i), - scale: PxScale::from(font_size), - text: section.value.clone(), - }; - - Ok(section) + // SAFETY: we exited early earlier in this function if + // one of the fonts was missing. + let font = unsafe { fonts.get(§ion.style.font).unwrap_unchecked() }; + ( + font.font.clone(), + TextMeasureSection { + font_id: FontId(i), + scale: scale_value(section.style.font_size, scale_factor), + text: section.value.clone().into_boxed_str(), + }, + ) }) - .collect::, _>>()?; + .unzip(); - Ok(TextMeasureInfo::new( + Ok(Self::new( auto_fonts, - scaled_fonts, sections, - text_alignment, - linebreak_behaviour.into(), + text.alignment, + text.linebreak_behavior.into(), )) } -} - -#[derive(Debug, Clone)] -pub struct TextMeasureSection { - pub text: String, - pub scale: PxScale, - pub font_id: FontId, -} - -#[derive(Debug, Clone)] -pub struct TextMeasureInfo { - pub fonts: Vec, - pub scaled_fonts: Vec>, - pub sections: Vec, - pub text_alignment: TextAlignment, - pub linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, - pub min_width_content_size: Vec2, - pub max_width_content_size: Vec2, -} - -impl TextMeasureInfo { fn new( fonts: Vec, - scaled_fonts: Vec>, sections: Vec, text_alignment: TextAlignment, - linebreak_behaviour: glyph_brush_layout::BuiltInLineBreaker, + linebreak_behavior: glyph_brush_layout::BuiltInLineBreaker, ) -> Self { let mut info = Self { - fonts, - scaled_fonts, - sections, + fonts: fonts.into_boxed_slice(), + sections: sections.into_boxed_slice(), text_alignment, - linebreak_behaviour, - min_width_content_size: Vec2::ZERO, - max_width_content_size: Vec2::ZERO, + linebreak_behavior, + min: Vec2::ZERO, + max: Vec2::ZERO, }; - let section_texts = info.prepare_section_texts(); - let min = - info.compute_size_from_section_texts(§ion_texts, Vec2::new(0.0, f32::INFINITY)); - let max = info.compute_size_from_section_texts( - §ion_texts, - Vec2::new(f32::INFINITY, f32::INFINITY), - ); - info.min_width_content_size = min; - info.max_width_content_size = max; + let min = info.compute_size(Vec2::new(0.0, f32::INFINITY)); + let max = info.compute_size(Vec2::INFINITY); + info.min = min; + info.max = max; info } - fn prepare_section_texts(&self) -> Vec { - self.sections - .iter() - .map(|section| SectionText { - font_id: section.font_id, - scale: section.scale, - text: §ion.text, - }) - .collect::>() - } - - fn compute_size_from_section_texts(&self, sections: &[SectionText], bounds: Vec2) -> Vec2 { + pub fn compute_size(&self, bounds: Vec2) -> Vec2 { + let sections = &self.sections; let geom = SectionGeometry { bounds: (bounds.x, bounds.y), ..Default::default() }; let section_glyphs = glyph_brush_layout::Layout::default() .h_align(self.text_alignment.into()) - .line_breaker(self.linebreak_behaviour) + .line_breaker(self.linebreak_behavior) .calculate_glyphs(&self.fonts, &geom, sections); - compute_text_bounds(§ion_glyphs, |index| &self.scaled_fonts[index]).size() + compute_text_bounds(§ion_glyphs, |index| { + let font = &self.fonts[index]; + let font_size = self.sections[index].scale; + font.into_scaled(font_size) + }) + .size() } - - pub fn compute_size(&self, bounds: Vec2) -> Vec2 { - let sections = self.prepare_section_texts(); - self.compute_size_from_section_texts(§ions, bounds) +} +impl ToSectionText for TextMeasureSection { + #[inline(always)] + fn to_section_text(&self) -> SectionText<'_> { + SectionText { + text: &self.text, + scale: PxScale::from(self.scale), + font_id: self.font_id, + } } } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index c231b6ed994f7..92e2bd0cce213 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -140,11 +140,12 @@ impl TextSection { } /// Describes horizontal alignment preference for positioning & bounds. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] pub enum TextAlignment { /// Leftmost character is immediately to the right of the render position.
/// Bounds start from the render position and advance rightwards. + #[default] Left, /// Leftmost & rightmost characters are equidistant to the render position.
/// Bounds start from the render position and advance equally left & right. diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index d9baccdcc2471..22c7285a806d6 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -53,20 +53,17 @@ impl Measure for TextMeasure { _available_height: AvailableSpace, ) -> Vec2 { let x = width.unwrap_or_else(|| match available_width { - AvailableSpace::Definite(x) => x.clamp( - self.info.min_width_content_size.x, - self.info.max_width_content_size.x, - ), - AvailableSpace::MinContent => self.info.min_width_content_size.x, - AvailableSpace::MaxContent => self.info.max_width_content_size.x, + AvailableSpace::Definite(x) => x.clamp(self.info.min.x, self.info.max.x), + AvailableSpace::MinContent => self.info.min.x, + AvailableSpace::MaxContent => self.info.max.x, }); height .map_or_else( || match available_width { AvailableSpace::Definite(_) => self.info.compute_size(Vec2::new(x, f32::MAX)), - AvailableSpace::MinContent => Vec2::new(x, self.info.min_width_content_size.y), - AvailableSpace::MaxContent => Vec2::new(x, self.info.max_width_content_size.y), + AvailableSpace::MinContent => Vec2::new(x, self.info.min.y), + AvailableSpace::MaxContent => Vec2::new(x, self.info.max.y), }, |y| Vec2::new(x, y), ) @@ -77,24 +74,15 @@ impl Measure for TextMeasure { #[inline] fn create_text_measure( fonts: &Assets, - text_pipeline: &mut TextPipeline, scale_factor: f64, text: Ref, mut content_size: Mut, mut text_flags: Mut, ) { - match text_pipeline.create_text_measure( - fonts, - &text.sections, - scale_factor, - text.alignment, - text.linebreak_behavior, - ) { + match TextMeasureInfo::from_text(&text, fonts, scale_factor) { Ok(measure) => { if text.linebreak_behavior == BreakLineOn::NoWrap { - content_size.set(FixedMeasure { - size: measure.max_width_content_size, - }); + content_size.set(FixedMeasure { size: measure.max }); } else { content_size.set(TextMeasure { info: measure }); } @@ -127,7 +115,6 @@ pub fn measure_text_system( fonts: Res>, windows: Query<&Window, With>, ui_scale: Res, - mut text_pipeline: ResMut, mut text_query: Query<(Ref, &mut ContentSize, &mut TextFlags), With>, ) { let window_scale_factor = windows @@ -142,14 +129,7 @@ pub fn measure_text_system( // scale factor unchanged, only create new measure funcs for modified text for (text, content_size, text_flags) in text_query.iter_mut() { if text.is_changed() || text_flags.needs_new_measure_func { - create_text_measure( - &fonts, - &mut text_pipeline, - scale_factor, - text, - content_size, - text_flags, - ); + create_text_measure(&fonts, scale_factor, text, content_size, text_flags); } } } else { @@ -157,14 +137,7 @@ pub fn measure_text_system( *last_scale_factor = scale_factor; for (text, content_size, text_flags) in text_query.iter_mut() { - create_text_measure( - &fonts, - &mut text_pipeline, - scale_factor, - text, - content_size, - text_flags, - ); + create_text_measure(&fonts, scale_factor, text, content_size, text_flags); } } } From 44f677a38a6c99b8dcf79426d5b615a1266dde2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9l=C3=A8ne=20Amanita?= <134181069+Selene-Amanita@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:47:25 +0100 Subject: [PATCH 17/58] Improve documentation relating to `Frustum` and `HalfSpace` (#9136) # Objective This PR's first aim is to fix a mistake in `HalfSpace`'s documentation. When defining a `Frustum` myself in bevy_basic_portals, I realised that the "distance" of the `HalfSpace` is not, as the current doc defines, the "distance from the origin along the normal", but actually the opposite of that. See the example I gave in this PR. This means one of two things: 1. The documentation about `HalfSpace` is wrong (it is either way because of the `n.p + d > 0` formula given later anyway, which is how it behaves, but in that formula `d` is indeed the opposite of the "distance from the origin along the normal", otherwise it should be `n.p > d`) 2. The distance is supposed to be the "distance from the origin along the normal" but when used in a Frustum it's used as the opposite, and it is a mistake 3. Same as 2, but it is somehow intended Since I think `HalfSpace` is only used for `Frustum`, and it's easier to fix documentation than code, I assumed for this PR we're in case number 1. If we're in case number 3, the documentation of `Frustum` needs to change, and in case number 2, the code needs to be fixed. While I was at it, I also : - Tried to improve the documentation for `Frustum`, `Aabb`, and `VisibilitySystems`, among others, since they're all related to `Frustum`. - Fixed documentation about frustum culling not applying to 2d objects, which is not true since https://github.com/bevyengine/bevy/pull/7885 ## Remarks and questions - What about a `HalfSpace` with an infinite distance, is it allowed and does it represents the whole space? If so it should probably be mentioned. - I referenced the `update_frusta` system in `bevy_render::view::visibility` directly instead of referencing its system set, should I reference the system set instead? It's a bit annoying since it's in 3 sets. - `visibility_propagate` is not public for some reason, I think it probably should be, but for now I only documented its system set, should I make it public? I don't think that would count as a breaking change? - Why is `Aabb` inserted by a system, with `NoFrustumCulling` as an opt-out, instead of having it inserted by default in `PbrBundle` for example and then the system calculating it when it's added? Is it because there is still no way to have an optional component inside a bundle? --------- Co-authored-by: SpecificProtagonist Co-authored-by: Alice Cecile --- crates/bevy_render/src/primitives/mod.rs | 91 ++++++++++++++++--- crates/bevy_render/src/view/visibility/mod.rs | 38 ++++++-- crates/bevy_sprite/src/lib.rs | 7 ++ 3 files changed, 114 insertions(+), 22 deletions(-) diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 2cb067ded19f5..d150fe08e997f 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -3,7 +3,32 @@ use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles}; use bevy_reflect::Reflect; use bevy_utils::HashMap; -/// An axis-aligned bounding box. +/// An axis-aligned bounding box, defined by: +/// - a center, +/// - the distances from the center to each faces along the axis, +/// the faces are orthogonal to the axis. +/// +/// It is typically used as a component on an entity to represent the local space +/// occupied by this entity, with faces orthogonal to its local axis. +/// +/// This component is notably used during "frustum culling", a process to determine +/// if an entity should be rendered by a [`Camera`] if its bounding box intersects +/// with the camera's [`Frustum`]. +/// +/// It will be added automatically by the systems in [`CalculateBounds`] to entities that: +/// - could be subject to frustum culling, for example with a [`Handle`] +/// or `Sprite` component, +/// - don't have the [`NoFrustumCulling`] component. +/// +/// It won't be updated automatically if the space occupied by the entity changes, +/// for example if the vertex positions of a [`Mesh`] inside a `Handle` are +/// updated. +/// +/// [`Camera`]: crate::camera::Camera +/// [`NoFrustumCulling`]: crate::view::visibility::NoFrustumCulling +/// [`CalculateBounds`]: crate::view::visibility::VisibilitySystems::CalculateBounds +/// [`Mesh`]: crate::mesh::Mesh +/// [`Handle`]: crate::mesh::Mesh #[derive(Component, Clone, Copy, Debug, Default, Reflect)] #[reflect(Component)] pub struct Aabb { @@ -76,11 +101,28 @@ impl Sphere { } } -/// A bisecting plane that partitions 3D space into two regions. +/// A region of 3D space, specifically an open set whose border is a bisecting 2D plane. +/// This bisecting plane partitions 3D space into two infinite regions, +/// the half-space is one of those regions and excludes the bisecting plane. +/// +/// Each instance of this type is characterized by: +/// - the bisecting plane's unit normal, normalized and pointing "inside" the half-space, +/// - the signed distance along the normal from the bisecting plane to the origin of 3D space. +/// +/// The distance can also be seen as: +/// - the distance along the inverse of the normal from the origin of 3D space to the bisecting plane, +/// - the opposite of the distance along the normal from the origin of 3D space to the bisecting plane. +/// +/// Any point `p` is considered to be within the `HalfSpace` when the length of the projection +/// of p on the normal is greater or equal than the opposite of the distance, +/// meaning: if the equation `normal.dot(p) + distance > 0.` is satisfied. /// -/// Each instance of this type is characterized by the bisecting plane's unit normal and distance from the origin along the normal. -/// Any point `p` is considered to be within the `HalfSpace` when the distance is positive, -/// meaning: if the equation `n.p + d > 0` is satisfied. +/// For example, the half-space containing all the points with a z-coordinate lesser +/// or equal than `8.0` would be defined by: `HalfSpace::new(Vec3::NEG_Z.extend(-8.0))`. +/// It includes all the points from the bisecting plane towards `NEG_Z`, and the distance +/// from the plane to the origin is `-8.0` along `NEG_Z`. +/// +/// It is used to define a [`Frustum`], but is also a useful mathematical primitive for rendering tasks such as light computation. #[derive(Clone, Copy, Debug, Default)] pub struct HalfSpace { normal_d: Vec4, @@ -88,8 +130,8 @@ pub struct HalfSpace { impl HalfSpace { /// Constructs a `HalfSpace` from a 4D vector whose first 3 components - /// represent the bisecting plane's unit normal, and the last component signifies - /// the distance from the origin to the plane along the normal. + /// represent the bisecting plane's unit normal, and the last component is + /// the signed distance along the normal from the plane to the origin. /// The constructor ensures the normal vector is normalized and the distance is appropriately scaled. #[inline] pub fn new(normal_d: Vec4) -> Self { @@ -104,23 +146,46 @@ impl HalfSpace { Vec3A::from(self.normal_d) } - /// Returns the distance from the origin to the bisecting plane along the plane's unit normal vector. - /// This distance helps determine the position of a point `p` on the bisecting plane, as per the equation `n.p + d = 0`. + /// Returns the signed distance from the bisecting plane to the origin along + /// the plane's unit normal vector. #[inline] pub fn d(&self) -> f32 { self.normal_d.w } - /// Returns the bisecting plane's unit normal vector and the distance from the origin to the plane. + /// Returns the bisecting plane's unit normal vector and the signed distance + /// from the plane to the origin. #[inline] pub fn normal_d(&self) -> Vec4 { self.normal_d } } -/// A frustum made up of the 6 defining half spaces. -/// Half spaces are ordered left, right, top, bottom, near, far. -/// The normal vectors of the half spaces point towards the interior of the frustum. +/// A region of 3D space defined by the intersection of 6 [`HalfSpace`]s. +/// +/// Frustums are typically an apex-truncated square pyramid (a pyramid without the top) or a cuboid. +/// +/// Half spaces are ordered left, right, top, bottom, near, far. The normal vectors +/// of the half-spaces point towards the interior of the frustum. +/// +/// A frustum component is used on an entity with a [`Camera`] component to +/// determine which entities will be considered for rendering by this camera. +/// All entities with an [`Aabb`] component that are not contained by (or crossing +/// the boundary of) the frustum will not be rendered, and not be used in rendering computations. +/// +/// This process is called frustum culling, and entities can opt out of it using +/// the [`NoFrustumCulling`] component. +/// +/// The frustum component is typically added from a bundle, either the `Camera2dBundle` +/// or the `Camera3dBundle`. +/// It is usually updated automatically by [`update_frusta`] from the +/// [`CameraProjection`] component and [`GlobalTransform`] of the camera entity. +/// +/// [`Camera`]: crate::camera::Camera +/// [`NoFrustumCulling`]: crate::view::visibility::NoFrustumCulling +/// [`update_frusta`]: crate::view::visibility::update_frusta +/// [`CameraProjection`]: crate::camera::CameraProjection +/// [`GlobalTransform`]: bevy_transform::components::GlobalTransform #[derive(Component, Clone, Copy, Debug, Default, Reflect)] #[reflect(Component)] pub struct Frustum { diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 9a58b4ada7aa2..538b6a03ee7e4 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -153,7 +153,13 @@ pub struct VisibilityBundle { pub computed: ComputedVisibility, } -/// Use this component to opt-out of built-in frustum culling for Mesh entities +/// Use this component to opt-out of built-in frustum culling for entities, see +/// [`Frustum`]. +/// +/// It can be used for example: +/// - when a [`Mesh`] is updated but its [`Aabb`] is not, which might happen with animations, +/// - when using some light effects, like wanting a [`Mesh`] out of the [`Frustum`] +/// to appear in the reflection of a [`Mesh`] within. #[derive(Component, Default, Reflect)] #[reflect(Component, Default)] pub struct NoFrustumCulling; @@ -161,15 +167,12 @@ pub struct NoFrustumCulling; /// Collection of entities visible from the current view. /// /// This component contains all entities which are visible from the currently -/// rendered view. The collection is updated automatically by the [`check_visibility()`] -/// system, and renderers can use it to optimize rendering of a particular view, to +/// rendered view. The collection is updated automatically by the [`VisibilitySystems::CheckVisibility`] +/// system set, and renderers can use it to optimize rendering of a particular view, to /// prevent drawing items not visible from that view. /// /// This component is intended to be attached to the same entity as the [`Camera`] and /// the [`Frustum`] defining the view. -/// -/// Currently this component is ignored by the sprite renderer, so sprite rendering -/// is not optimized per view. #[derive(Clone, Component, Default, Debug, Reflect)] #[reflect(Component)] pub struct VisibleEntities { @@ -193,13 +196,21 @@ impl VisibleEntities { #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum VisibilitySystems { + /// Label for the [`calculate_bounds`] and `calculate_bounds_2d` systems, + /// calculating and inserting an [`Aabb`] to relevant entities. CalculateBounds, + /// Label for the [`apply_deferred`] call after [`VisibilitySystems::CalculateBounds`] CalculateBoundsFlush, + /// Label for the [`update_frusta`] system. UpdateOrthographicFrusta, + /// Label for the [`update_frusta`] system. UpdatePerspectiveFrusta, + /// Label for the [`update_frusta`] system. UpdateProjectionFrusta, + /// Label for the system propagating the [`ComputedVisibility`] in a + /// [`hierarchy`](bevy_hierarchy). VisibilityPropagate, - /// Label for the [`check_visibility()`] system updating each frame the [`ComputedVisibility`] + /// Label for the [`check_visibility`] system updating [`ComputedVisibility`] /// of each entity and the [`VisibleEntities`] of each view. CheckVisibility, } @@ -253,6 +264,10 @@ impl Plugin for VisibilityPlugin { } } +/// Computes and adds an [`Aabb`] component to entities with a +/// [`Handle`](Mesh) component and without a [`NoFrustumCulling`] component. +/// +/// This system is used in system set [`VisibilitySystems::CalculateBounds`]. pub fn calculate_bounds( mut commands: Commands, meshes: Res>, @@ -267,6 +282,11 @@ pub fn calculate_bounds( } } +/// Updates [`Frustum`]. +/// +/// This system is used in system sets [`VisibilitySystems::UpdateProjectionFrusta`], +/// [`VisibilitySystems::UpdatePerspectiveFrusta`], and +/// [`VisibilitySystems::UpdateOrthographicFrusta`]. pub fn update_frusta( mut views: Query< (&GlobalTransform, &T, &mut Frustum), @@ -345,9 +365,9 @@ fn propagate_recursive( Ok(()) } -/// System updating the visibility of entities each frame. +/// Updates the visibility of entities each frame. /// -/// The system is part of the [`VisibilitySystems::CheckVisibility`] set. Each frame, it updates the +/// This system is part of the [`VisibilitySystems::CheckVisibility`] set. Each frame, it updates the /// [`ComputedVisibility`] of all entities, and for each view also compute the [`VisibleEntities`] /// for that view. pub fn check_visibility( diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 72038b0427442..b337df92d9687 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -108,6 +108,13 @@ impl Plugin for SpritePlugin { } } +/// System calculating and inserting an [`Aabb`] component to entities with either: +/// - a `Mesh2dHandle` component, +/// - a `Sprite` and `Handle` components, +/// - a `TextureAtlasSprite` and `Handle` components, +/// and without a [`NoFrustumCulling`] component. +/// +/// Used in system set [`VisibilitySystems::CalculateBounds`]. pub fn calculate_bounds_2d( mut commands: Commands, meshes: Res>, From fb9a65fa0dcb14646d3f3680fe850f3a7519e521 Mon Sep 17 00:00:00 2001 From: Hennadii Chernyshchyk Date: Mon, 28 Aug 2023 19:50:43 +0300 Subject: [PATCH 18/58] bevy_scene: Add `ReflectBundle` (#9165) # Objective Similar to #6344, but contains only `ReflectBundle` changes. Useful for scripting. The implementation has also been updated to look exactly like `ReflectComponent`. --- ## Changelog ### Added - Reflection for bundles. --------- Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com> --- crates/bevy_ecs/src/reflect/bundle.rs | 218 ++++++++++++++++++++++++++ crates/bevy_ecs/src/reflect/mod.rs | 2 + 2 files changed, 220 insertions(+) create mode 100644 crates/bevy_ecs/src/reflect/bundle.rs diff --git a/crates/bevy_ecs/src/reflect/bundle.rs b/crates/bevy_ecs/src/reflect/bundle.rs new file mode 100644 index 0000000000000..7fc07d855e6b3 --- /dev/null +++ b/crates/bevy_ecs/src/reflect/bundle.rs @@ -0,0 +1,218 @@ +//! Definitions for [`Bundle`] reflection. +//! +//! This module exports two types: [`ReflectBundleFns`] and [`ReflectBundle`]. +//! +//! Same as [`super::component`], but for bundles. +use std::any::TypeId; + +use crate::{ + prelude::Bundle, + world::{EntityMut, FromWorld, World}, +}; +use bevy_reflect::{FromType, Reflect, ReflectRef, TypeRegistry}; + +use super::ReflectComponent; + +/// A struct used to operate on reflected [`Bundle`] of a type. +/// +/// A [`ReflectBundle`] for type `T` can be obtained via +/// [`bevy_reflect::TypeRegistration::data`]. +#[derive(Clone)] +pub struct ReflectBundle(ReflectBundleFns); + +/// The raw function pointers needed to make up a [`ReflectBundle`]. +/// +/// The also [`super::component::ReflectComponentFns`]. +#[derive(Clone)] +pub struct ReflectBundleFns { + /// Function pointer implementing [`ReflectBundle::from_world()`]. + pub from_world: fn(&mut World) -> Box, + /// Function pointer implementing [`ReflectBundle::insert()`]. + pub insert: fn(&mut EntityMut, &dyn Reflect), + /// Function pointer implementing [`ReflectBundle::apply()`]. + pub apply: fn(&mut EntityMut, &dyn Reflect, &TypeRegistry), + /// Function pointer implementing [`ReflectBundle::apply_or_insert()`]. + pub apply_or_insert: fn(&mut EntityMut, &dyn Reflect, &TypeRegistry), + /// Function pointer implementing [`ReflectBundle::remove()`]. + pub remove: fn(&mut EntityMut), +} + +impl ReflectBundleFns { + /// Get the default set of [`ReflectBundleFns`] for a specific bundle type using its + /// [`FromType`] implementation. + /// + /// This is useful if you want to start with the default implementation before overriding some + /// of the functions to create a custom implementation. + pub fn new() -> Self { + >::from_type().0 + } +} + +impl ReflectBundle { + /// Constructs default reflected [`Bundle`] from world using [`from_world()`](FromWorld::from_world). + pub fn from_world(&self, world: &mut World) -> Box { + (self.0.from_world)(world) + } + + /// Insert a reflected [`Bundle`] into the entity like [`insert()`](crate::world::EntityMut::insert). + pub fn insert(&self, entity: &mut EntityMut, bundle: &dyn Reflect) { + (self.0.insert)(entity, bundle); + } + + /// Uses reflection to set the value of this [`Bundle`] type in the entity to the given value. + /// + /// # Panics + /// + /// Panics if there is no [`Bundle`] of the given type. + pub fn apply(&self, entity: &mut EntityMut, bundle: &dyn Reflect, registry: &TypeRegistry) { + (self.0.apply)(entity, bundle, registry); + } + + /// Uses reflection to set the value of this [`Bundle`] type in the entity to the given value or insert a new one if it does not exist. + pub fn apply_or_insert( + &self, + entity: &mut EntityMut, + bundle: &dyn Reflect, + registry: &TypeRegistry, + ) { + (self.0.apply_or_insert)(entity, bundle, registry); + } + + /// Removes this [`Bundle`] type from the entity. Does nothing if it doesn't exist. + pub fn remove(&self, entity: &mut EntityMut) { + (self.0.remove)(entity); + } + + /// Create a custom implementation of [`ReflectBundle`]. + /// + /// This is an advanced feature, + /// useful for scripting implementations, + /// that should not be used by most users + /// unless you know what you are doing. + /// + /// Usually you should derive [`Reflect`] and add the `#[reflect(Bundle)]` bundle + /// to generate a [`ReflectBundle`] implementation automatically. + /// + /// See [`ReflectBundleFns`] for more information. + pub fn new(fns: ReflectBundleFns) -> Self { + Self(fns) + } + + /// The underlying function pointers implementing methods on `ReflectBundle`. + /// + /// This is useful when you want to keep track locally of an individual + /// function pointer. + /// + /// Calling [`TypeRegistry::get`] followed by + /// [`TypeRegistration::data::`] can be costly if done several + /// times per frame. Consider cloning [`ReflectBundle`] and keeping it + /// between frames, cloning a `ReflectBundle` is very cheap. + /// + /// If you only need a subset of the methods on `ReflectBundle`, + /// use `fn_pointers` to get the underlying [`ReflectBundleFns`] + /// and copy the subset of function pointers you care about. + /// + /// [`TypeRegistration::data::`]: bevy_reflect::TypeRegistration::data + /// [`TypeRegistry::get`]: bevy_reflect::TypeRegistry::get + pub fn fn_pointers(&self) -> &ReflectBundleFns { + &self.0 + } +} + +impl FromType for ReflectBundle { + fn from_type() -> Self { + ReflectBundle(ReflectBundleFns { + from_world: |world| Box::new(B::from_world(world)), + insert: |entity, reflected_bundle| { + let mut bundle = entity.world_scope(|world| B::from_world(world)); + bundle.apply(reflected_bundle); + entity.insert(bundle); + }, + apply: |entity, reflected_bundle, registry| { + let mut bundle = entity.world_scope(|world| B::from_world(world)); + bundle.apply(reflected_bundle); + + match bundle.reflect_ref() { + ReflectRef::Struct(bundle) => bundle + .iter_fields() + .for_each(|field| insert_field::(entity, field, registry)), + ReflectRef::Tuple(bundle) => bundle + .iter_fields() + .for_each(|field| insert_field::(entity, field, registry)), + _ => panic!( + "expected bundle `{}` to be named struct or tuple", + std::any::type_name::() + ), + } + }, + apply_or_insert: |entity, reflected_bundle, registry| { + let mut bundle = entity.world_scope(|world| B::from_world(world)); + bundle.apply(reflected_bundle); + + match bundle.reflect_ref() { + ReflectRef::Struct(bundle) => bundle + .iter_fields() + .for_each(|field| apply_or_insert_field::(entity, field, registry)), + ReflectRef::Tuple(bundle) => bundle + .iter_fields() + .for_each(|field| apply_or_insert_field::(entity, field, registry)), + _ => panic!( + "expected bundle `{}` to be named struct or tuple", + std::any::type_name::() + ), + } + }, + remove: |entity| { + entity.remove::(); + }, + }) + } +} + +fn insert_field(entity: &mut EntityMut, field: &dyn Reflect, registry: &TypeRegistry) { + if let Some(reflect_component) = registry.get_type_data::(field.type_id()) { + reflect_component.apply(entity, field); + } else if let Some(reflect_bundle) = registry.get_type_data::(field.type_id()) { + reflect_bundle.apply(entity, field, registry); + } else { + entity.world_scope(|world| { + if world.components().get_id(TypeId::of::()).is_some() { + panic!( + "no `ReflectComponent` registration found for `{}`", + field.type_name() + ); + }; + }); + + panic!( + "no `ReflectBundle` registration found for `{}`", + field.type_name() + ) + } +} + +fn apply_or_insert_field( + entity: &mut EntityMut, + field: &dyn Reflect, + registry: &TypeRegistry, +) { + if let Some(reflect_component) = registry.get_type_data::(field.type_id()) { + reflect_component.apply_or_insert(entity, field); + } else if let Some(reflect_bundle) = registry.get_type_data::(field.type_id()) { + reflect_bundle.apply_or_insert(entity, field, registry); + } else { + entity.world_scope(|world| { + if world.components().get_id(TypeId::of::()).is_some() { + panic!( + "no `ReflectComponent` registration found for `{}`", + field.type_name() + ); + }; + }); + + panic!( + "no `ReflectBundle` registration found for `{}`", + field.type_name() + ) + } +} diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index bcfb1770b770a..1c7da802becc8 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -6,10 +6,12 @@ use crate as bevy_ecs; use crate::{entity::Entity, system::Resource}; use bevy_reflect::{impl_reflect_value, ReflectDeserialize, ReflectSerialize, TypeRegistryArc}; +mod bundle; mod component; mod map_entities; mod resource; +pub use bundle::{ReflectBundle, ReflectBundleFns}; pub use component::{ReflectComponent, ReflectComponentFns}; pub use map_entities::ReflectMapEntities; pub use resource::{ReflectResource, ReflectResourceFns}; From f38549c68de2859d14118acb961ecabe76ce631b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9l=C3=A8ne=20Amanita?= <134181069+Selene-Amanita@users.noreply.github.com> Date: Mon, 28 Aug 2023 17:56:22 +0100 Subject: [PATCH 19/58] Reorganize `Events` and `EventSequence` code (#9306) # Objective Make code relating to event more readable. Currently the `impl` block of `Events` is split in two, and the big part of its implementations are put at the end of the file, far from the definition of the `struct`. ## Solution - Move and merge the `impl` blocks of `Events` next to its definition. - Move the `EventSequence` definition and implementations before the `Events`, because they're pretty trivial and help understand how `Events` work, rather than being buried bellow `Events`. I separated those two steps in two commits to not be too confusing. I didn't modify any code of documentation. I want to do a second PR with such modifications after this one is merged. --- crates/bevy_ecs/src/event.rs | 320 +++++++++++++++++------------------ 1 file changed, 159 insertions(+), 161 deletions(-) diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 5307ebdb2a513..95ef6ebbe47b9 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -188,6 +188,165 @@ impl Events { .start_event_count .min(self.events_b.start_event_count) } + + /// "Sends" an `event` by writing it to the current event buffer. [`EventReader`]s can then read + /// the event. + pub fn send(&mut self, event: E) { + let event_id = EventId { + id: self.event_count, + _marker: PhantomData, + }; + detailed_trace!("Events::send() -> id: {}", event_id); + + let event_instance = EventInstance { event_id, event }; + + self.events_b.push(event_instance); + self.event_count += 1; + } + + /// Sends the default value of the event. Useful when the event is an empty struct. + pub fn send_default(&mut self) + where + E: Default, + { + self.send(Default::default()); + } + + /// Gets a new [`ManualEventReader`]. This will include all events already in the event buffers. + pub fn get_reader(&self) -> ManualEventReader { + ManualEventReader::default() + } + + /// Gets a new [`ManualEventReader`]. This will ignore all events already in the event buffers. + /// It will read all future events. + pub fn get_reader_current(&self) -> ManualEventReader { + ManualEventReader { + last_event_count: self.event_count, + ..Default::default() + } + } + + /// Swaps the event buffers and clears the oldest event buffer. In general, this should be + /// called once per frame/update. + pub fn update(&mut self) { + std::mem::swap(&mut self.events_a, &mut self.events_b); + self.events_b.clear(); + self.events_b.start_event_count = self.event_count; + debug_assert_eq!( + self.events_a.start_event_count + self.events_a.len(), + self.events_b.start_event_count + ); + } + + /// A system that calls [`Events::update`] once per frame. + pub fn update_system(mut events: ResMut) { + events.update(); + } + + #[inline] + fn reset_start_event_count(&mut self) { + self.events_a.start_event_count = self.event_count; + self.events_b.start_event_count = self.event_count; + } + + /// Removes all events. + #[inline] + pub fn clear(&mut self) { + self.reset_start_event_count(); + self.events_a.clear(); + self.events_b.clear(); + } + + /// Returns the number of events currently stored in the event buffer. + #[inline] + pub fn len(&self) -> usize { + self.events_a.len() + self.events_b.len() + } + + /// Returns true if there are no events currently stored in the event buffer. + #[inline] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Creates a draining iterator that removes all events. + pub fn drain(&mut self) -> impl Iterator + '_ { + self.reset_start_event_count(); + + // Drain the oldest events first, then the newest + self.events_a + .drain(..) + .chain(self.events_b.drain(..)) + .map(|i| i.event) + } + + /// Iterates over events that happened since the last "update" call. + /// WARNING: You probably don't want to use this call. In most cases you should use an + /// [`EventReader`]. You should only use this if you know you only need to consume events + /// between the last `update()` call and your call to `iter_current_update_events`. + /// If events happen outside that window, they will not be handled. For example, any events that + /// happen after this call and before the next `update()` call will be dropped. + pub fn iter_current_update_events(&self) -> impl ExactSizeIterator { + self.events_b.iter().map(|i| &i.event) + } + + /// Get a specific event by id if it still exists in the events buffer. + pub fn get_event(&self, id: usize) -> Option<(&E, EventId)> { + if id < self.oldest_id() { + return None; + } + + let sequence = self.sequence(id); + let index = id.saturating_sub(sequence.start_event_count); + + sequence + .get(index) + .map(|instance| (&instance.event, instance.event_id)) + } + + /// Oldest id still in the events buffer. + pub fn oldest_id(&self) -> usize { + self.events_a.start_event_count + } + + /// Which event buffer is this event id a part of. + fn sequence(&self, id: usize) -> &EventSequence { + if id < self.events_b.start_event_count { + &self.events_a + } else { + &self.events_b + } + } +} + +impl std::iter::Extend for Events { + fn extend(&mut self, iter: I) + where + I: IntoIterator, + { + let old_count = self.event_count; + let mut event_count = self.event_count; + let events = iter.into_iter().map(|event| { + let event_id = EventId { + id: event_count, + _marker: PhantomData, + }; + event_count += 1; + EventInstance { event_id, event } + }); + + self.events_b.extend(events); + + if old_count != event_count { + detailed_trace!( + "Events::extend() -> ids: ({}..{})", + self.event_count, + event_count + ); + } + + self.event_count = event_count; + } } #[derive(Debug)] @@ -553,167 +712,6 @@ impl<'a, E: Event> ExactSizeIterator for ManualEventIteratorWithId<'a, E> { } } -impl Events { - /// "Sends" an `event` by writing it to the current event buffer. [`EventReader`]s can then read - /// the event. - pub fn send(&mut self, event: E) { - let event_id = EventId { - id: self.event_count, - _marker: PhantomData, - }; - detailed_trace!("Events::send() -> id: {}", event_id); - - let event_instance = EventInstance { event_id, event }; - - self.events_b.push(event_instance); - self.event_count += 1; - } - - /// Sends the default value of the event. Useful when the event is an empty struct. - pub fn send_default(&mut self) - where - E: Default, - { - self.send(Default::default()); - } - - /// Gets a new [`ManualEventReader`]. This will include all events already in the event buffers. - pub fn get_reader(&self) -> ManualEventReader { - ManualEventReader::default() - } - - /// Gets a new [`ManualEventReader`]. This will ignore all events already in the event buffers. - /// It will read all future events. - pub fn get_reader_current(&self) -> ManualEventReader { - ManualEventReader { - last_event_count: self.event_count, - ..Default::default() - } - } - - /// Swaps the event buffers and clears the oldest event buffer. In general, this should be - /// called once per frame/update. - pub fn update(&mut self) { - std::mem::swap(&mut self.events_a, &mut self.events_b); - self.events_b.clear(); - self.events_b.start_event_count = self.event_count; - debug_assert_eq!( - self.events_a.start_event_count + self.events_a.len(), - self.events_b.start_event_count - ); - } - - /// A system that calls [`Events::update`] once per frame. - pub fn update_system(mut events: ResMut) { - events.update(); - } - - #[inline] - fn reset_start_event_count(&mut self) { - self.events_a.start_event_count = self.event_count; - self.events_b.start_event_count = self.event_count; - } - - /// Removes all events. - #[inline] - pub fn clear(&mut self) { - self.reset_start_event_count(); - self.events_a.clear(); - self.events_b.clear(); - } - - /// Returns the number of events currently stored in the event buffer. - #[inline] - pub fn len(&self) -> usize { - self.events_a.len() + self.events_b.len() - } - - /// Returns true if there are no events currently stored in the event buffer. - #[inline] - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - /// Creates a draining iterator that removes all events. - pub fn drain(&mut self) -> impl Iterator + '_ { - self.reset_start_event_count(); - - // Drain the oldest events first, then the newest - self.events_a - .drain(..) - .chain(self.events_b.drain(..)) - .map(|i| i.event) - } - - /// Iterates over events that happened since the last "update" call. - /// WARNING: You probably don't want to use this call. In most cases you should use an - /// [`EventReader`]. You should only use this if you know you only need to consume events - /// between the last `update()` call and your call to `iter_current_update_events`. - /// If events happen outside that window, they will not be handled. For example, any events that - /// happen after this call and before the next `update()` call will be dropped. - pub fn iter_current_update_events(&self) -> impl ExactSizeIterator { - self.events_b.iter().map(|i| &i.event) - } - - /// Get a specific event by id if it still exists in the events buffer. - pub fn get_event(&self, id: usize) -> Option<(&E, EventId)> { - if id < self.oldest_id() { - return None; - } - - let sequence = self.sequence(id); - let index = id.saturating_sub(sequence.start_event_count); - - sequence - .get(index) - .map(|instance| (&instance.event, instance.event_id)) - } - - /// Oldest id still in the events buffer. - pub fn oldest_id(&self) -> usize { - self.events_a.start_event_count - } - - /// Which event buffer is this event id a part of. - fn sequence(&self, id: usize) -> &EventSequence { - if id < self.events_b.start_event_count { - &self.events_a - } else { - &self.events_b - } - } -} - -impl std::iter::Extend for Events { - fn extend(&mut self, iter: I) - where - I: IntoIterator, - { - let old_count = self.event_count; - let mut event_count = self.event_count; - let events = iter.into_iter().map(|event| { - let event_id = EventId { - id: event_count, - _marker: PhantomData, - }; - event_count += 1; - EventInstance { event_id, event } - }); - - self.events_b.extend(events); - - if old_count != event_count { - detailed_trace!( - "Events::extend() -> ids: ({}..{})", - self.event_count, - event_count - ); - } - - self.event_count = event_count; - } -} - #[cfg(test)] mod tests { use crate::{prelude::World, system::SystemState}; From b28f6334da3b0d08b54945cf3b0778f4a54b2112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Mon, 28 Aug 2023 18:58:45 +0200 Subject: [PATCH 20/58] only take up to the max number of joints (#9351) # Objective - Meshes with a higher number of joints than `MAX_JOINTS` are crashing - Fixes partly #9021 (doesn't crash anymore, but the mesh is not correctly displayed) ## Solution - Only take up to `MAX_JOINTS` joints when extending the buffer --- crates/bevy_gltf/src/loader.rs | 13 ++++++++++++- crates/bevy_pbr/src/render/mesh.rs | 4 +++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 7b737fec0ed74..b5c1fae3c66a6 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -11,7 +11,7 @@ use bevy_log::warn; use bevy_math::{Mat4, Vec3}; use bevy_pbr::{ AlphaMode, DirectionalLight, DirectionalLightBundle, PbrBundle, PointLight, PointLightBundle, - SpotLight, SpotLightBundle, StandardMaterial, + SpotLight, SpotLightBundle, StandardMaterial, MAX_JOINTS, }; use bevy_render::{ camera::{Camera, OrthographicProjection, PerspectiveProjection, Projection, ScalingMode}, @@ -489,6 +489,7 @@ async fn load_gltf<'a, 'b>( } } + let mut warned_about_max_joints = HashSet::new(); for (&entity, &skin_index) in &entity_to_skin_index_map { let mut entity = world.entity_mut(entity); let skin = gltf.skins().nth(skin_index).unwrap(); @@ -497,6 +498,16 @@ async fn load_gltf<'a, 'b>( .map(|node| node_index_to_entity_map[&node.index()]) .collect(); + if joint_entities.len() > MAX_JOINTS && warned_about_max_joints.insert(skin_index) { + warn!( + "The glTF skin {:?} has {} joints, but the maximum supported is {}", + skin.name() + .map(|name| name.to_string()) + .unwrap_or_else(|| skin.index().to_string()), + joint_entities.len(), + MAX_JOINTS + ); + } entity.insert(SkinnedMesh { inverse_bindposes: skinned_mesh_inverse_bindposes[skin_index].clone(), joints: joint_entities, diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index c1f0fed803d3d..a52c54557ed82 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -52,7 +52,8 @@ use crate::render::{ #[derive(Default)] pub struct MeshRenderPlugin; -const MAX_JOINTS: usize = 256; +/// Maximum number of joints supported for skinned meshes. +pub const MAX_JOINTS: usize = 256; const JOINT_SIZE: usize = std::mem::size_of::(); pub(crate) const JOINT_BUFFER_SIZE: usize = MAX_JOINTS * JOINT_SIZE; @@ -323,6 +324,7 @@ impl SkinnedMeshJoints { joints .iter_many(&skin.joints) .zip(inverse_bindposes.iter()) + .take(MAX_JOINTS) .map(|(joint, bindpose)| joint.affine() * *bindpose), ); // iter_many will skip any failed fetches. This will cause it to assign the wrong bones, From 25e5239d2efa3fbdb17ab0abd1d4ce3f1d01baa4 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Mon, 28 Aug 2023 18:06:12 +0100 Subject: [PATCH 21/58] check root node for animations (#9407) # Objective fixes #8357 gltf animations can affect multiple "root" nodes (i.e. top level nodes within a gltf scene). the current loader adds an AnimationPlayer to each root node which is affected by any animation. when a clip which affects multiple root nodes is played on a root node player, the root node name is not checked, leading to potentially incorrect weights being applied. also, the `AnimationClip::compatible_with` method will never return true for those clips, as it checks that all paths start with the root node name - not all paths start with the same name so it can't return true. ## Solution - check the first path node name matches the given root - change compatible_with to return true if `any` match is found a better alternative would probably be to attach the player to the scene root instead of the first child, and then walk the full path from there. this would be breaking (and would stop multiple animations that *don't* overlap from being played concurrently), but i'm happy to modify to that if it's preferred. --------- Co-authored-by: Nicola Papale --- crates/bevy_animation/src/lib.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 59eab3f92bdae..b39bfa42b7f22 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -119,7 +119,7 @@ impl AnimationClip { /// Whether this animation clip can run on entity with given [`Name`]. pub fn compatible_with(&self, name: &Name) -> bool { - self.paths.keys().all(|path| &path.parts[0] == name) + self.paths.keys().any(|path| &path.parts[0] == name) } } @@ -406,8 +406,18 @@ fn entity_from_path( // PERF: finding the target entity can be optimised let mut current_entity = root; path_cache.resize(path.parts.len(), None); - // Ignore the first name, it is the root node which we already have - for (idx, part) in path.parts.iter().enumerate().skip(1) { + + let mut parts = path.parts.iter().enumerate(); + + // check the first name is the root node which we already have + let Some((_, root_name)) = parts.next() else { + return None; + }; + if names.get(current_entity) != Ok(root_name) { + return None; + } + + for (idx, part) in parts { let mut found = false; let children = children.get(current_entity).ok()?; if let Some(cached) = path_cache[idx] { @@ -608,12 +618,14 @@ fn apply_animation( return; } + let mut any_path_found = false; for (path, bone_id) in &animation_clip.paths { let cached_path = &mut animation.path_cache[*bone_id]; let curves = animation_clip.get_curves(*bone_id).unwrap(); let Some(target) = entity_from_path(root, path, children, names, cached_path) else { continue; }; + any_path_found = true; // SAFETY: The verify_no_ancestor_player check above ensures that two animation players cannot alias // any of their descendant Transforms. // @@ -702,6 +714,10 @@ fn apply_animation( } } } + + if !any_path_found { + warn!("Animation player on {root:?} did not match any entity paths."); + } } } From 36f29a933fa82de18388813cf6f45ee0dbd8eb89 Mon Sep 17 00:00:00 2001 From: Pixelstorm Date: Mon, 28 Aug 2023 18:13:02 +0100 Subject: [PATCH 22/58] Remove redundant check for `AppExit` events in `ScheduleRunnerPlugin` (#9421) # Objective Fixes #9420 ## Solution Remove one of the two `AppExit` event checks in the `ScheduleRunnerPlugin`'s main loop. Specificially, the check that happens immediately before calling `App.update()`, to be consistent with the `WinitPlugin`. --- crates/bevy_app/src/schedule_runner.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/crates/bevy_app/src/schedule_runner.rs b/crates/bevy_app/src/schedule_runner.rs index d303769f92696..b8f45d87802f0 100644 --- a/crates/bevy_app/src/schedule_runner.rs +++ b/crates/bevy_app/src/schedule_runner.rs @@ -89,15 +89,6 @@ impl Plugin for ScheduleRunnerPlugin { -> Result, AppExit> { let start_time = Instant::now(); - if let Some(app_exit_events) = - app.world.get_resource_mut::>() - { - if let Some(exit) = app_exit_event_reader.iter(&app_exit_events).last() - { - return Err(exit.clone()); - } - } - app.update(); if let Some(app_exit_events) = From 394e2b0c9185476aab4d61fd7d0602ab39e1033b Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Tue, 29 Aug 2023 03:18:16 +1000 Subject: [PATCH 23/58] Replaced EntityMap with HashMap (#9461) # Objective - Fixes #9321 ## Solution - `EntityMap` has been replaced by a simple `HashMap`. --- ## Changelog - `EntityMap::world_scope` has been replaced with `World::world_scope` to avoid creating a new trait. This is a public facing change to the call semantics, but has no effect on results or behaviour. - `EntityMap`, as a `HashMap`, now operates on `&Entity` rather than `Entity`. This changes many standard access functions (e.g, `.get`) in a public-facing way. ## Migration Guide - Calls to `EntityMap::world_scope` can be directly replaced with the following: `map.world_scope(&mut world)` -> `world.world_scope(&mut map)` - Calls to legacy `EntityMap` methods such as `EntityMap::get` must explicitly include de/reference symbols: `let entity = map.get(parent);` -> `let &entity = map.get(&parent);` --- crates/bevy_ecs/src/entity/map_entities.rs | 154 ++++++-------------- crates/bevy_ecs/src/reflect/map_entities.rs | 32 ++-- crates/bevy_scene/src/dynamic_scene.rs | 15 +- crates/bevy_scene/src/scene.rs | 6 +- crates/bevy_scene/src/scene_spawner.rs | 15 +- crates/bevy_scene/src/serde.rs | 7 +- 6 files changed, 90 insertions(+), 139 deletions(-) diff --git a/crates/bevy_ecs/src/entity/map_entities.rs b/crates/bevy_ecs/src/entity/map_entities.rs index 4db2bc59bc15d..122e418179301 100644 --- a/crates/bevy_ecs/src/entity/map_entities.rs +++ b/crates/bevy_ecs/src/entity/map_entities.rs @@ -1,11 +1,11 @@ use crate::{entity::Entity, world::World}; -use bevy_utils::{Entry, HashMap}; +use bevy_utils::HashMap; /// Operation to map all contained [`Entity`] fields in a type to new values. /// /// As entity IDs are valid only for the [`World`] they're sourced from, using [`Entity`] /// as references in components copied from another world will be invalid. This trait -/// allows defining custom mappings for these references via [`EntityMap`]. +/// allows defining custom mappings for these references via [`HashMap`] /// /// Implementing this trait correctly is required for properly loading components /// with entity references from scenes. @@ -32,112 +32,29 @@ use bevy_utils::{Entry, HashMap}; /// /// [`World`]: crate::world::World pub trait MapEntities { - /// Updates all [`Entity`] references stored inside using `entity_map`. + /// Updates all [`Entity`] references stored inside using `entity_mapper`. /// /// Implementors should look up any and all [`Entity`] values stored within and /// update them to the mapped values via `entity_mapper`. fn map_entities(&mut self, entity_mapper: &mut EntityMapper); } -/// A mapping from one set of entities to another. -/// -/// The API generally follows [`HashMap`], but each [`Entity`] is returned by value, as they are [`Copy`]. -/// -/// This is typically used to coordinate data transfer between sets of entities, such as between a scene and the world -/// or over the network. This is required as [`Entity`] identifiers are opaque; you cannot and do not want to reuse -/// identifiers directly. -/// -/// On its own, an `EntityMap` is not capable of allocating new entity identifiers, which is needed to map references -/// to entities that lie outside the source entity set. To do this, an `EntityMap` can be wrapped in an -/// [`EntityMapper`] which scopes it to a particular destination [`World`] and allows new identifiers to be allocated. -/// This functionality can be accessed through [`Self::world_scope()`]. -#[derive(Default, Debug)] -pub struct EntityMap { - map: HashMap, -} - -impl EntityMap { - /// Inserts an entities pair into the map. - /// - /// If the map did not have `from` present, [`None`] is returned. - /// - /// If the map did have `from` present, the value is updated, and the old value is returned. - pub fn insert(&mut self, from: Entity, to: Entity) -> Option { - self.map.insert(from, to) - } - - /// Removes an `entity` from the map, returning the mapped value of it if the `entity` was previously in the map. - pub fn remove(&mut self, entity: Entity) -> Option { - self.map.remove(&entity) - } - - /// Gets the given entity's corresponding entry in the map for in-place manipulation. - pub fn entry(&mut self, entity: Entity) -> Entry<'_, Entity, Entity> { - self.map.entry(entity) - } - - /// Returns the corresponding mapped entity. - pub fn get(&self, entity: Entity) -> Option { - self.map.get(&entity).copied() - } - - /// An iterator visiting all keys in arbitrary order. - pub fn keys(&self) -> impl Iterator + '_ { - self.map.keys().cloned() - } - - /// An iterator visiting all values in arbitrary order. - pub fn values(&self) -> impl Iterator + '_ { - self.map.values().cloned() - } - - /// Returns the number of elements in the map. - pub fn len(&self) -> usize { - self.map.len() - } - - /// Returns true if the map contains no elements. - pub fn is_empty(&self) -> bool { - self.map.is_empty() - } - - /// An iterator visiting all (key, value) pairs in arbitrary order. - pub fn iter(&self) -> impl Iterator + '_ { - self.map.iter().map(|(from, to)| (*from, *to)) - } - - /// Clears the map, removing all entity pairs. Keeps the allocated memory for reuse. - pub fn clear(&mut self) { - self.map.clear(); - } - - /// Creates an [`EntityMapper`] from this [`EntityMap`] and scoped to the provided [`World`], then calls the - /// provided function with it. This allows one to allocate new entity references in the provided `World` that are - /// guaranteed to never point at a living entity now or in the future. This functionality is useful for safely - /// mapping entity identifiers that point at entities outside the source world. The passed function, `f`, is called - /// within the scope of the passed world. Its return value is then returned from `world_scope` as the generic type - /// parameter `R`. - pub fn world_scope( - &mut self, - world: &mut World, - f: impl FnOnce(&mut World, &mut EntityMapper) -> R, - ) -> R { - let mut mapper = EntityMapper::new(self, world); - let result = f(world, &mut mapper); - mapper.finish(world); - result - } -} - -/// A wrapper for [`EntityMap`], augmenting it with the ability to allocate new [`Entity`] references in a destination +/// A wrapper for [`HashMap`], augmenting it with the ability to allocate new [`Entity`] references in a destination /// world. These newly allocated references are guaranteed to never point to any living entity in that world. /// /// References are allocated by returning increasing generations starting from an internally initialized base /// [`Entity`]. After it is finished being used by [`MapEntities`] implementations, this entity is despawned and the /// requisite number of generations reserved. pub struct EntityMapper<'m> { - /// The wrapped [`EntityMap`]. - map: &'m mut EntityMap, + /// A mapping from one set of entities to another. + /// + /// This is typically used to coordinate data transfer between sets of entities, such as between a scene and the world + /// or over the network. This is required as [`Entity`] identifiers are opaque; you cannot and do not want to reuse + /// identifiers directly. + /// + /// On its own, a [`HashMap`] is not capable of allocating new entity identifiers, which is needed to map references + /// to entities that lie outside the source entity set. This functionality can be accessed through [`EntityMapper::world_scope()`]. + map: &'m mut HashMap, /// A base [`Entity`] used to allocate new references. dead_start: Entity, /// The number of generations this mapper has allocated thus far. @@ -147,7 +64,7 @@ pub struct EntityMapper<'m> { impl<'m> EntityMapper<'m> { /// Returns the corresponding mapped entity or reserves a new dead entity ID if it is absent. pub fn get_or_reserve(&mut self, entity: Entity) -> Entity { - if let Some(mapped) = self.map.get(entity) { + if let Some(&mapped) = self.map.get(&entity) { return mapped; } @@ -163,21 +80,21 @@ impl<'m> EntityMapper<'m> { new } - /// Gets a reference to the underlying [`EntityMap`]. - pub fn get_map(&'m self) -> &'m EntityMap { + /// Gets a reference to the underlying [`HashMap`]. + pub fn get_map(&'m self) -> &'m HashMap { self.map } - /// Gets a mutable reference to the underlying [`EntityMap`] - pub fn get_map_mut(&'m mut self) -> &'m mut EntityMap { + /// Gets a mutable reference to the underlying [`HashMap`]. + pub fn get_map_mut(&'m mut self) -> &'m mut HashMap { self.map } /// Creates a new [`EntityMapper`], spawning a temporary base [`Entity`] in the provided [`World`] - fn new(map: &'m mut EntityMap, world: &mut World) -> Self { + fn new(map: &'m mut HashMap, world: &mut World) -> Self { Self { map, - // SAFETY: Entities data is kept in a valid state via `EntityMap::world_scope` + // SAFETY: Entities data is kept in a valid state via `EntityMapper::world_scope` dead_start: unsafe { world.entities_mut().alloc() }, generations: 0, } @@ -193,19 +110,40 @@ impl<'m> EntityMapper<'m> { assert!(entities.free(self.dead_start).is_some()); assert!(entities.reserve_generations(self.dead_start.index, self.generations)); } + + /// Creates an [`EntityMapper`] from a provided [`World`] and [`HashMap`], then calls the + /// provided function with it. This allows one to allocate new entity references in this [`World`] that are + /// guaranteed to never point at a living entity now or in the future. This functionality is useful for safely + /// mapping entity identifiers that point at entities outside the source world. The passed function, `f`, is called + /// within the scope of this world. Its return value is then returned from `world_scope` as the generic type + /// parameter `R`. + pub fn world_scope( + entity_map: &'m mut HashMap, + world: &mut World, + f: impl FnOnce(&mut World, &mut Self) -> R, + ) -> R { + let mut mapper = Self::new(entity_map, world); + let result = f(world, &mut mapper); + mapper.finish(world); + result + } } #[cfg(test)] mod tests { - use super::{EntityMap, EntityMapper}; - use crate::{entity::Entity, world::World}; + use bevy_utils::HashMap; + + use crate::{ + entity::{Entity, EntityMapper}, + world::World, + }; #[test] fn entity_mapper() { const FIRST_IDX: u32 = 1; const SECOND_IDX: u32 = 2; - let mut map = EntityMap::default(); + let mut map = HashMap::default(); let mut world = World::new(); let mut mapper = EntityMapper::new(&mut map, &mut world); @@ -232,10 +170,10 @@ mod tests { #[test] fn world_scope_reserves_generations() { - let mut map = EntityMap::default(); + let mut map = HashMap::default(); let mut world = World::new(); - let dead_ref = map.world_scope(&mut world, |_, mapper| { + let dead_ref = EntityMapper::world_scope(&mut map, &mut world, |_, mapper| { mapper.get_or_reserve(Entity::new(0, 0)) }); diff --git a/crates/bevy_ecs/src/reflect/map_entities.rs b/crates/bevy_ecs/src/reflect/map_entities.rs index bd09bd97b64ad..7a5fe4257f0e5 100644 --- a/crates/bevy_ecs/src/reflect/map_entities.rs +++ b/crates/bevy_ecs/src/reflect/map_entities.rs @@ -1,9 +1,10 @@ use crate::{ component::Component, - entity::{Entity, EntityMap, EntityMapper, MapEntities}, + entity::{Entity, EntityMapper, MapEntities}, world::World, }; use bevy_reflect::FromType; +use bevy_utils::HashMap; /// For a specific type of component, this maps any fields with values of type [`Entity`] to a new world. /// Since a given `Entity` ID is only valid for the world it came from, when performing deserialization @@ -17,27 +18,32 @@ pub struct ReflectMapEntities { } impl ReflectMapEntities { - /// A general method for applying [`MapEntities`] behavior to all elements in an [`EntityMap`]. + /// A general method for applying [`MapEntities`] behavior to all elements in an [`HashMap`]. /// - /// Be mindful in its usage: Works best in situations where the entities in the [`EntityMap`] are newly + /// Be mindful in its usage: Works best in situations where the entities in the [`HashMap`] are newly /// created, before systems have a chance to add new components. If some of the entities referred to - /// by the [`EntityMap`] might already contain valid entity references, you should use [`map_entities`](Self::map_entities). + /// by the [`HashMap`] might already contain valid entity references, you should use [`map_entities`](Self::map_entities). /// /// An example of this: A scene can be loaded with `Parent` components, but then a `Parent` component can be added /// to these entities after they have been loaded. If you reload the scene using [`map_all_entities`](Self::map_all_entities), those `Parent` /// components with already valid entity references could be updated to point at something else entirely. - pub fn map_all_entities(&self, world: &mut World, entity_map: &mut EntityMap) { - entity_map.world_scope(world, self.map_all_entities); + pub fn map_all_entities(&self, world: &mut World, entity_map: &mut HashMap) { + EntityMapper::world_scope(entity_map, world, self.map_all_entities); } - /// A general method for applying [`MapEntities`] behavior to elements in an [`EntityMap`]. Unlike + /// A general method for applying [`MapEntities`] behavior to elements in an [`HashMap`]. Unlike /// [`map_all_entities`](Self::map_all_entities), this is applied to specific entities, not all values - /// in the [`EntityMap`]. + /// in the [`HashMap`]. /// /// This is useful mostly for when you need to be careful not to update components that already contain valid entity /// values. See [`map_all_entities`](Self::map_all_entities) for more details. - pub fn map_entities(&self, world: &mut World, entity_map: &mut EntityMap, entities: &[Entity]) { - entity_map.world_scope(world, |world, mapper| { + pub fn map_entities( + &self, + world: &mut World, + entity_map: &mut HashMap, + entities: &[Entity], + ) { + EntityMapper::world_scope(entity_map, world, |world, mapper| { (self.map_entities)(world, mapper, entities); }); } @@ -54,7 +60,11 @@ impl FromType for ReflectMapEntities { } }, map_all_entities: |world, entity_mapper| { - let entities = entity_mapper.get_map().values().collect::>(); + let entities = entity_mapper + .get_map() + .values() + .copied() + .collect::>(); for entity in &entities { if let Some(mut component) = world.get_mut::(*entity) { component.map_entities(entity_mapper); diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index 6ede572834283..e063f034d694c 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -3,7 +3,7 @@ use std::any::TypeId; use crate::{DynamicSceneBuilder, Scene, SceneSpawnError}; use anyhow::Result; use bevy_ecs::{ - entity::{Entity, EntityMap}, + entity::Entity, reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities}, world::World, }; @@ -67,7 +67,7 @@ impl DynamicScene { pub fn write_to_world_with( &self, world: &mut World, - entity_map: &mut EntityMap, + entity_map: &mut HashMap, type_registry: &AppTypeRegistry, ) -> Result<(), SceneSpawnError> { let type_registry = type_registry.read(); @@ -155,7 +155,7 @@ impl DynamicScene { pub fn write_to_world( &self, world: &mut World, - entity_map: &mut EntityMap, + entity_map: &mut HashMap, ) -> Result<(), SceneSpawnError> { let registry = world.resource::().clone(); self.write_to_world_with(world, entity_map, ®istry) @@ -183,8 +183,9 @@ where #[cfg(test)] mod tests { - use bevy_ecs::{entity::EntityMap, reflect::AppTypeRegistry, system::Command, world::World}; + use bevy_ecs::{reflect::AppTypeRegistry, system::Command, world::World}; use bevy_hierarchy::{AddChild, Parent}; + use bevy_utils::HashMap; use crate::dynamic_scene_builder::DynamicSceneBuilder; @@ -213,11 +214,11 @@ mod tests { scene_builder.extract_entity(original_parent_entity); scene_builder.extract_entity(original_child_entity); let scene = scene_builder.build(); - let mut entity_map = EntityMap::default(); + let mut entity_map = HashMap::default(); scene.write_to_world(&mut world, &mut entity_map).unwrap(); - let from_scene_parent_entity = entity_map.get(original_parent_entity).unwrap(); - let from_scene_child_entity = entity_map.get(original_child_entity).unwrap(); + let &from_scene_parent_entity = entity_map.get(&original_parent_entity).unwrap(); + let &from_scene_child_entity = entity_map.get(&original_child_entity).unwrap(); // We then add the parent from the scene as a child of the original child // Hierarchy should look like: diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index e49b3fc8bcb00..c47db7e749988 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -1,9 +1,9 @@ use bevy_ecs::{ - entity::EntityMap, reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities, ReflectResource}, world::World, }; use bevy_reflect::{TypePath, TypeUuid}; +use bevy_utils::HashMap; use crate::{DynamicScene, InstanceInfo, SceneSpawnError}; @@ -30,7 +30,7 @@ impl Scene { type_registry: &AppTypeRegistry, ) -> Result { let mut world = World::new(); - let mut entity_map = EntityMap::default(); + let mut entity_map = HashMap::default(); dynamic_scene.write_to_world_with(&mut world, &mut entity_map, type_registry)?; Ok(Self { world }) @@ -56,7 +56,7 @@ impl Scene { type_registry: &AppTypeRegistry, ) -> Result { let mut instance_info = InstanceInfo { - entity_map: EntityMap::default(), + entity_map: HashMap::default(), }; let type_registry = type_registry.read(); diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 4816ee668212c..3496e867598a8 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -1,7 +1,7 @@ use crate::{DynamicScene, Scene}; use bevy_asset::{AssetEvent, Assets, Handle}; use bevy_ecs::{ - entity::{Entity, EntityMap}, + entity::Entity, event::{Event, Events, ManualEventReader}, reflect::AppTypeRegistry, system::{Command, Resource}, @@ -25,7 +25,7 @@ pub struct SceneInstanceReady { #[derive(Debug)] pub struct InstanceInfo { /// Mapping of entities from the scene world to the instance world. - pub entity_map: EntityMap, + pub entity_map: HashMap, } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] @@ -120,7 +120,7 @@ impl SceneSpawner { pub fn despawn_instance_sync(&mut self, world: &mut World, instance_id: &InstanceId) { if let Some(instance) = self.spawned_instances.remove(instance_id) { - for entity in instance.entity_map.values() { + for &entity in instance.entity_map.values() { let _ = world.despawn(entity); } } @@ -131,7 +131,7 @@ impl SceneSpawner { world: &mut World, scene_handle: &Handle, ) -> Result<(), SceneSpawnError> { - let mut entity_map = EntityMap::default(); + let mut entity_map = HashMap::default(); Self::spawn_dynamic_internal(world, scene_handle, &mut entity_map)?; let instance_id = InstanceId::new(); self.spawned_instances @@ -147,7 +147,7 @@ impl SceneSpawner { fn spawn_dynamic_internal( world: &mut World, scene_handle: &Handle, - entity_map: &mut EntityMap, + entity_map: &mut HashMap, ) -> Result<(), SceneSpawnError> { world.resource_scope(|world, scenes: Mut>| { let scene = @@ -237,7 +237,7 @@ impl SceneSpawner { let scenes_to_spawn = std::mem::take(&mut self.dynamic_scenes_to_spawn); for (scene_handle, instance_id) in scenes_to_spawn { - let mut entity_map = EntityMap::default(); + let mut entity_map = HashMap::default(); match Self::spawn_dynamic_internal(world, &scene_handle, &mut entity_map) { Ok(_) => { @@ -277,7 +277,7 @@ impl SceneSpawner { for (instance_id, parent) in scenes_with_parent { if let Some(instance) = self.spawned_instances.get(&instance_id) { - for entity in instance.entity_map.values() { + for &entity in instance.entity_map.values() { // Add the `Parent` component to the scene root, and update the `Children` component of // the scene parent if !world @@ -322,6 +322,7 @@ impl SceneSpawner { .map(|instance| instance.entity_map.values()) .into_iter() .flatten() + .copied() } } diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index 492c6ac33077e..b23f4b8510065 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -424,12 +424,13 @@ impl<'a, 'de> Visitor<'de> for SceneMapVisitor<'a> { mod tests { use crate::serde::{SceneDeserializer, SceneSerializer}; use crate::{DynamicScene, DynamicSceneBuilder}; - use bevy_ecs::entity::{Entity, EntityMap, EntityMapper, MapEntities}; + use bevy_ecs::entity::{Entity, EntityMapper, MapEntities}; use bevy_ecs::prelude::{Component, ReflectComponent, ReflectResource, Resource, World}; use bevy_ecs::query::{With, Without}; use bevy_ecs::reflect::{AppTypeRegistry, ReflectMapEntities}; use bevy_ecs::world::FromWorld; use bevy_reflect::{Reflect, ReflectSerialize}; + use bevy_utils::HashMap; use bincode::Options; use serde::de::DeserializeSeed; use serde::Serialize; @@ -603,7 +604,7 @@ mod tests { "expected `entities` to contain 3 entities" ); - let mut map = EntityMap::default(); + let mut map = HashMap::default(); let mut dst_world = create_world(); scene.write_to_world(&mut dst_world, &mut map).unwrap(); @@ -642,7 +643,7 @@ mod tests { let deserialized_scene = scene_deserializer.deserialize(&mut deserializer).unwrap(); - let mut map = EntityMap::default(); + let mut map = HashMap::default(); let mut dst_world = create_world(); deserialized_scene .write_to_world(&mut dst_world, &mut map) From a2bd93aedc18909b52531fe88796f72a01a0bee7 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 28 Aug 2023 18:21:08 +0100 Subject: [PATCH 24/58] Make `GridPlacement`'s fields non-zero and add accessor functions. (#9486) # Objective * There is no way to read the fields of `GridPlacement` once set. * Values of `0` for `GridPlacement`'s fields are invalid but can be set. * A non-zero representation would be half the size. fixes #9474 ## Solution * Add `get_start`, `get_end` and `get_span` accessor methods. * Change`GridPlacement`'s constructor functions to panic on arguments of zero. * Use non-zero types instead of primitives for `GridPlacement`'s fields. --- ## Changelog `bevy_ui::ui_node::GridPlacement`: * Field types have been changed to `Option` and `Option`. This is because zero values are not valid for `GridPlacement`. Previously, Taffy interpreted these as auto variants. * Constructor functions for `GridPlacement` panic on arguments of `0`. * Added accessor functions: `get_start`, `get_end`, and `get_span`. These return the inner primitive value (if present) of the respective fields. ## Migration Guide `GridPlacement`'s constructor functions no longer accept values of `0`. Given any argument of `0` they will panic with a `GridPlacementError`. --- crates/bevy_ui/src/layout/convert.rs | 4 +- crates/bevy_ui/src/ui_node.rs | 152 ++++++++++++++++++++++----- 2 files changed, 127 insertions(+), 29 deletions(-) diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 1f5237a85da49..ce424ba44e3a7 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -278,8 +278,8 @@ impl From for taffy::style::GridAutoFlow { impl From for taffy::geometry::Line { fn from(value: GridPlacement) -> Self { - let span = value.span.unwrap_or(1).max(1); - match (value.start, value.end) { + let span = value.get_span().unwrap_or(1); + match (value.get_start(), value.get_end()) { (Some(start), Some(end)) => taffy::geometry::Line { start: style_helpers::line(start), end: style_helpers::line(end), diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 435a14b1c01f8..93bb9a698a22c 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -10,7 +10,10 @@ use bevy_render::{ use bevy_transform::prelude::GlobalTransform; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; -use std::ops::{Div, DivAssign, Mul, MulAssign}; +use std::{ + num::{NonZeroI16, NonZeroU16}, + ops::{Div, DivAssign, Mul, MulAssign}, +}; use thiserror::Error; /// Describes the size of a UI node @@ -1470,100 +1473,145 @@ impl From for Vec { /// pub struct GridPlacement { /// The grid line at which the item should start. Lines are 1-indexed. Negative indexes count backwards from the end of the grid. Zero is not a valid index. - pub(crate) start: Option, + pub(crate) start: Option, /// How many grid tracks the item should span. Defaults to 1. - pub(crate) span: Option, - /// The grid line at which the node should end. Lines are 1-indexed. Negative indexes count backwards from the end of the grid. Zero is not a valid index. - pub(crate) end: Option, + pub(crate) span: Option, + /// The grid line at which the item should end. Lines are 1-indexed. Negative indexes count backwards from the end of the grid. Zero is not a valid index. + pub(crate) end: Option, } impl GridPlacement { pub const DEFAULT: Self = Self { start: None, - span: Some(1), + span: Some(unsafe { NonZeroU16::new_unchecked(1) }), end: None, }; /// Place the grid item automatically (letting the `span` default to `1`). pub fn auto() -> Self { - Self { - start: None, - end: None, - span: Some(1), - } + Self::DEFAULT } /// Place the grid item automatically, specifying how many tracks it should `span`. + /// + /// # Panics + /// + /// Panics if `span` is `0` pub fn span(span: u16) -> Self { Self { start: None, end: None, - span: Some(span), + span: try_into_grid_span(span).expect("Invalid span value of 0."), } } /// Place the grid item specifying the `start` grid line (letting the `span` default to `1`). + /// + /// # Panics + /// + /// Panics if `start` is `0` pub fn start(start: i16) -> Self { Self { - start: Some(start), - end: None, - span: Some(1), + start: try_into_grid_index(start).expect("Invalid start value of 0."), + ..Self::DEFAULT } } /// Place the grid item specifying the `end` grid line (letting the `span` default to `1`). + /// + /// # Panics + /// + /// Panics if `end` is `0` pub fn end(end: i16) -> Self { Self { - start: None, - end: Some(end), - span: Some(1), + end: try_into_grid_index(end).expect("Invalid end value of 0."), + ..Self::DEFAULT } } /// Place the grid item specifying the `start` grid line and how many tracks it should `span`. + /// + /// # Panics + /// + /// Panics if `start` or `span` is `0` pub fn start_span(start: i16, span: u16) -> Self { Self { - start: Some(start), + start: try_into_grid_index(start).expect("Invalid start value of 0."), end: None, - span: Some(span), + span: try_into_grid_span(span).expect("Invalid span value of 0."), } } /// Place the grid item specifying `start` and `end` grid lines (`span` will be inferred) + /// + /// # Panics + /// + /// Panics if `start` or `end` is `0` pub fn start_end(start: i16, end: i16) -> Self { Self { - start: Some(start), - end: Some(end), + start: try_into_grid_index(start).expect("Invalid start value of 0."), + end: try_into_grid_index(end).expect("Invalid end value of 0."), span: None, } } /// Place the grid item specifying the `end` grid line and how many tracks it should `span`. + /// + /// # Panics + /// + /// Panics if `end` or `span` is `0` pub fn end_span(end: i16, span: u16) -> Self { Self { start: None, - end: Some(end), - span: Some(span), + end: try_into_grid_index(end).expect("Invalid end value of 0."), + span: try_into_grid_span(span).expect("Invalid span value of 0."), } } /// Mutate the item, setting the `start` grid line + /// + /// # Panics + /// + /// Panics if `start` is `0` pub fn set_start(mut self, start: i16) -> Self { - self.start = Some(start); + self.start = try_into_grid_index(start).expect("Invalid start value of 0."); self } /// Mutate the item, setting the `end` grid line + /// + /// # Panics + /// + /// Panics if `end` is `0` pub fn set_end(mut self, end: i16) -> Self { - self.end = Some(end); + self.end = try_into_grid_index(end).expect("Invalid end value of 0."); self } /// Mutate the item, setting the number of tracks the item should `span` + /// + /// # Panics + /// + /// Panics if `span` is `0` pub fn set_span(mut self, span: u16) -> Self { - self.span = Some(span); + self.span = try_into_grid_span(span).expect("Invalid span value of 0."); self } + + /// Returns the grid line at which the item should start, or `None` if not set. + pub fn get_start(self) -> Option { + self.start.map(NonZeroI16::get) + } + + /// Returns the grid line at which the item should end, or `None` if not set. + pub fn get_end(self) -> Option { + self.end.map(NonZeroI16::get) + } + + /// Returns span for this grid item, or `None` if not set. + pub fn get_span(self) -> Option { + self.span.map(NonZeroU16::get) + } } impl Default for GridPlacement { @@ -1572,6 +1620,29 @@ impl Default for GridPlacement { } } +/// Convert an `i16` to `NonZeroI16`, fails on `0` and returns the `InvalidZeroIndex` error. +fn try_into_grid_index(index: i16) -> Result, GridPlacementError> { + Ok(Some( + NonZeroI16::new(index).ok_or(GridPlacementError::InvalidZeroIndex)?, + )) +} + +/// Convert a `u16` to `NonZeroU16`, fails on `0` and returns the `InvalidZeroSpan` error. +fn try_into_grid_span(span: u16) -> Result, GridPlacementError> { + Ok(Some( + NonZeroU16::new(span).ok_or(GridPlacementError::InvalidZeroSpan)?, + )) +} + +/// Errors that occur when setting contraints for a `GridPlacement` +#[derive(Debug, Eq, PartialEq, Clone, Copy, Error)] +pub enum GridPlacementError { + #[error("Zero is not a valid grid position")] + InvalidZeroIndex, + #[error("Spans cannot be zero length")] + InvalidZeroSpan, +} + /// The background color of the node /// /// This serves as the "fill" color. @@ -1719,6 +1790,7 @@ impl Default for ZIndex { #[cfg(test)] mod tests { + use crate::GridPlacement; use crate::ValArithmeticError; use super::Val; @@ -1867,4 +1939,30 @@ mod tests { fn default_val_equals_const_default_val() { assert_eq!(Val::default(), Val::DEFAULT); } + + #[test] + fn invalid_grid_placement_values() { + assert!(std::panic::catch_unwind(|| GridPlacement::span(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::end(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start_end(0, 1)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start_end(-1, 0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start_span(1, 0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::start_span(0, 1)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::end_span(0, 1)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::end_span(1, 0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::default().set_start(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::default().set_end(0)).is_err()); + assert!(std::panic::catch_unwind(|| GridPlacement::default().set_span(0)).is_err()); + } + + #[test] + fn grid_placement_accessors() { + assert_eq!(GridPlacement::start(5).get_start(), Some(5)); + assert_eq!(GridPlacement::end(-4).get_end(), Some(-4)); + assert_eq!(GridPlacement::span(2).get_span(), Some(2)); + assert_eq!(GridPlacement::start_end(11, 21).get_span(), None); + assert_eq!(GridPlacement::start_span(3, 5).get_end(), None); + assert_eq!(GridPlacement::end_span(-4, 12).get_start(), None); + } } From 94c67afa4bd18603ff91d93dbb0f04a09f842b54 Mon Sep 17 00:00:00 2001 From: Alex Kocharin Date: Mon, 28 Aug 2023 21:23:44 +0400 Subject: [PATCH 25/58] Fix panic when using `.load_folder()` with absolute paths (#9490) Fixes https://github.com/bevyengine/bevy/issues/9458. On case-insensitive filesystems (Windows, Mac, NTFS mounted in Linux, etc.), a path can be represented in a multiple ways: - `c:\users\user\rust\assets\hello\world` - `c:/users/user/rust/assets/hello/world` - `C:\USERS\USER\rust\assets\hello\world` If user specifies a path variant that doesn't match asset folder path bevy calculates, `path.strip_prefix()` will fail, as demonstrated below: ```rs dbg!(Path::new("c:/foo/bar/baz").strip_prefix("c:/foo")); // Ok("bar/baz") dbg!(Path::new("c:/FOO/bar/baz").strip_prefix("c:/foo")); // StripPrefixError(()) ``` This commit rewrites the code in question in a way that prefix stripping is no longer necessary. I've tested with the following paths on my computer: ```rs let res = asset_server.load_folder("C:\\Users\\user\\rust\\assets\\foo\\bar"); dbg!(res); let res = asset_server.load_folder("c:\\users\\user\\rust\\assets\\foo\\bar"); dbg!(res); let res = asset_server.load_folder("C:/Users/user/rust/assets/foo/bar"); dbg!(res); ``` --- crates/bevy_asset/src/io/file_asset_io.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_asset/src/io/file_asset_io.rs b/crates/bevy_asset/src/io/file_asset_io.rs index 1c4f56f28ac4b..ced308064074a 100644 --- a/crates/bevy_asset/src/io/file_asset_io.rs +++ b/crates/bevy_asset/src/io/file_asset_io.rs @@ -114,10 +114,11 @@ impl AssetIo for FileAssetIo { path: &Path, ) -> Result>, AssetIoError> { let root_path = self.root_path.to_owned(); - Ok(Box::new(fs::read_dir(root_path.join(path))?.map( + let path = path.to_owned(); + Ok(Box::new(fs::read_dir(root_path.join(&path))?.map( move |entry| { - let path = entry.unwrap().path(); - path.strip_prefix(&root_path).unwrap().to_owned() + let file_name = entry.unwrap().file_name(); + path.join(file_name) }, ))) } From aa20565f75e839adb3f8846aa15fbdaeb73c79f7 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 28 Aug 2023 18:29:46 +0100 Subject: [PATCH 26/58] Add `Without` filter to `sync_simple_transforms`' orphaned entities query (#9518) # Objective `sync_simple_transforms` only checks for removed parents and doesn't filter for `Without`, so it overwrites the `GlobalTransform` of non-orphan entities that were orphaned and then reparented since the last update. Introduced by #7264 ## Solution Filter for `Without`. Fixes #9517, #9492 ## Changelog `sync_simple_transforms`: * Added a `Without` filter to the orphaned entities query. --- crates/bevy_transform/src/systems.rs | 58 +++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 2 deletions(-) diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index bf60b58922dfa..48d76fa14e1ab 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -21,7 +21,7 @@ pub fn sync_simple_transforms( Without, ), >, - Query<(Ref, &mut GlobalTransform), Without>, + Query<(Ref, &mut GlobalTransform), (Without, Without)>, )>, mut orphaned: RemovedComponents, ) { @@ -183,7 +183,7 @@ mod test { use bevy_app::prelude::*; use bevy_ecs::prelude::*; use bevy_ecs::system::CommandQueue; - use bevy_math::vec3; + use bevy_math::{vec3, Vec3}; use bevy_tasks::{ComputeTaskPool, TaskPool}; use crate::components::{GlobalTransform, Transform}; @@ -481,4 +481,58 @@ mod test { app.update(); } + + #[test] + fn global_transform_should_not_be_overwritten_after_reparenting() { + let translation = Vec3::ONE; + let mut world = World::new(); + + // Create transform propagation schedule + let mut schedule = Schedule::default(); + schedule.add_systems((sync_simple_transforms, propagate_transforms)); + + // Spawn a `TransformBundle` entity with a local translation of `Vec3::ONE` + let mut spawn_transform_bundle = || { + world + .spawn(TransformBundle::from_transform( + Transform::from_translation(translation), + )) + .id() + }; + + // Spawn parent and child with identical transform bundles + let parent = spawn_transform_bundle(); + let child = spawn_transform_bundle(); + world.entity_mut(parent).add_child(child); + + // Run schedule to propagate transforms + schedule.run(&mut world); + + // Child should be positioned relative to its parent + let parent_global_transform = *world.entity(parent).get::().unwrap(); + let child_global_transform = *world.entity(child).get::().unwrap(); + assert!(parent_global_transform + .translation() + .abs_diff_eq(translation, 0.1)); + assert!(child_global_transform + .translation() + .abs_diff_eq(2. * translation, 0.1)); + + // Reparent child + world.entity_mut(child).remove_parent(); + world.entity_mut(parent).add_child(child); + + // Run schedule to propagate transforms + schedule.run(&mut world); + + // Translations should be unchanged after update + assert_eq!( + parent_global_transform, + *world.entity(parent).get::().unwrap() + ); + assert_eq!( + child_global_transform, + *world.entity(child).get::().unwrap() + ); + } } From 5012a0fd57748ab6f146776368b4cf988bba1eaa Mon Sep 17 00:00:00 2001 From: Ray Redondo Date: Mon, 28 Aug 2023 12:31:56 -0500 Subject: [PATCH 27/58] Update defaults for OrthographicProjection (#9537) # Objective These new defaults match what is used by `Camera2dBundle::default()`, removing a potential footgun from overriding a field in the projection component of the bundle. ## Solution Adjusted the near clipping plane of `OrthographicProjection::default()` to `-1000.`. --- ## Changelog Changed: `OrthographicProjection::default()` now matches the value used in `Camera2dBundle::default()` ## Migration Guide Workarounds used to keep the projection consistent with the bundle defaults are no longer required. Meanwhile, uses of `OrthographicProjection` in 2D scenes may need to be adjusted; the `near` clipping plane default was changed from `0.0` to `-1000.0`. --- crates/bevy_render/src/camera/projection.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 7b62385cac792..a824d886c80aa 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -197,6 +197,8 @@ pub enum ScalingMode { /// /// Note that the scale of the projection and the apparent size of objects are inversely proportional. /// As the size of the projection increases, the size of objects decreases. +/// +/// Note also that the view frustum is centered at the origin. #[derive(Component, Debug, Clone, Reflect)] #[reflect(Component, Default)] pub struct OrthographicProjection { @@ -204,7 +206,7 @@ pub struct OrthographicProjection { /// /// Objects closer than this will not be rendered. /// - /// Defaults to `0.0` + /// Defaults to `-1000.0` pub near: f32, /// The distance of the far clipping plane in world units. /// @@ -315,7 +317,7 @@ impl Default for OrthographicProjection { fn default() -> Self { OrthographicProjection { scale: 1.0, - near: 0.0, + near: -1000.0, far: 1000.0, viewport_origin: Vec2::new(0.5, 0.5), scaling_mode: ScalingMode::WindowSize(1.0), From b7d68873eca08d88169c8acb4891b5c5044d1406 Mon Sep 17 00:00:00 2001 From: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Date: Mon, 28 Aug 2023 10:36:18 -0700 Subject: [PATCH 28/58] bevy_derive: Fix `#[deref]` breaking other attributes (#9551) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Fixes #9550 ## Solution Removes a check that asserts that _all_ attribute metas are path-only, rather than just the `#[deref]` attribute itself. --- ## Changelog - Fixes an issue where deriving `Deref` with `#[deref]` on a field causes other attributes to sometimes result in a compile error --------- Co-authored-by: François --- crates/bevy_derive/src/derefs.rs | 4 +++- .../tests/deref_derive/multiple_fields.pass.rs | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/bevy_derive/src/derefs.rs b/crates/bevy_derive/src/derefs.rs index 0e3dfc1bf0f75..90b00f7b71c33 100644 --- a/crates/bevy_derive/src/derefs.rs +++ b/crates/bevy_derive/src/derefs.rs @@ -68,10 +68,12 @@ fn get_deref_field(ast: &DeriveInput, is_mut: bool) -> syn::Result<(Member, &Typ let mut selected_field: Option<(Member, &Type)> = None; for (index, field) in data_struct.fields.iter().enumerate() { for attr in &field.attrs { - if !attr.meta.require_path_only()?.is_ident(DEREF_ATTR) { + if !attr.meta.path().is_ident(DEREF_ATTR) { continue; } + attr.meta.require_path_only()?; + if selected_field.is_some() { return Err(syn::Error::new_spanned( attr, diff --git a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_fields.pass.rs b/crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_fields.pass.rs index 2244e89b78385..96662054f266c 100644 --- a/crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_fields.pass.rs +++ b/crates/bevy_macros_compile_fail_tests/tests/deref_derive/multiple_fields.pass.rs @@ -5,9 +5,13 @@ struct TupleStruct(usize, #[deref] String); #[derive(Deref)] struct Struct { + // Works with other attributes + #[cfg(test)] foo: usize, #[deref] bar: String, + /// Also works with doc comments. + baz: i32, } fn main() { @@ -15,8 +19,10 @@ fn main() { let _: &String = &*value; let value = Struct { + #[cfg(test)] foo: 123, bar: "Hello world!".to_string(), + baz: 321, }; let _: &String = &*value; } From 087a345579384fc389fd80e37deb57318903bddc Mon Sep 17 00:00:00 2001 From: jnhyatt <48692273+jnhyatt@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:37:42 -0600 Subject: [PATCH 29/58] Rename `Bezier` to `CubicBezier` for clarity (#9554) # Objective A Bezier curve is a curve defined by two or more control points. In the simplest form, it's just a line. The (arguably) most common type of Bezier curve is a cubic Bezier, defined by four control points. These are often used in animation, etc. Bevy has a Bezier curve struct called `Bezier`. However, this is technically a misnomer as it only represents cubic Bezier curves. ## Solution This PR changes the struct name to `CubicBezier` to more accurately reflect the struct's usage. Since it's exposed in Bevy's prelude, it can potentially collide with other `Bezier` implementations. While that might instead be an argument for removing it from the prelude, there's also something to be said for adding a more general `Bezier` into Bevy, in which case we'd likely want to use the name `Bezier`. As a final motivator, not only is the struct located in `cubic_spines.rs`, there are also several other spline-related structs which follow the `CubicXxx` naming convention where applicable. For example, `CubicSegment` represents a cubic Bezier curve (with coefficients pre-baked). --- ## Migration Guide - Change all `Bezier` references to `CubicBezier` --- benches/benches/bevy_math/bezier.rs | 10 +++++----- crates/bevy_math/src/cubic_splines.rs | 18 +++++++++--------- crates/bevy_math/src/lib.rs | 4 +++- examples/animation/cubic_curve.rs | 2 +- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/benches/benches/bevy_math/bezier.rs b/benches/benches/bevy_math/bezier.rs index 8f9c866d32c50..10645a59977a7 100644 --- a/benches/benches/bevy_math/bezier.rs +++ b/benches/benches/bevy_math/bezier.rs @@ -14,7 +14,7 @@ fn easing(c: &mut Criterion) { } fn cubic_2d(c: &mut Criterion) { - let bezier = Bezier::new([[ + let bezier = CubicBezier::new([[ vec2(0.0, 0.0), vec2(0.0, 1.0), vec2(1.0, 0.0), @@ -27,7 +27,7 @@ fn cubic_2d(c: &mut Criterion) { } fn cubic(c: &mut Criterion) { - let bezier = Bezier::new([[ + let bezier = CubicBezier::new([[ vec3a(0.0, 0.0, 0.0), vec3a(0.0, 1.0, 0.0), vec3a(1.0, 0.0, 0.0), @@ -40,7 +40,7 @@ fn cubic(c: &mut Criterion) { } fn cubic_vec3(c: &mut Criterion) { - let bezier = Bezier::new([[ + let bezier = CubicBezier::new([[ vec3(0.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), vec3(1.0, 0.0, 0.0), @@ -53,7 +53,7 @@ fn cubic_vec3(c: &mut Criterion) { } fn build_pos_cubic(c: &mut Criterion) { - let bezier = Bezier::new([[ + let bezier = CubicBezier::new([[ vec3a(0.0, 0.0, 0.0), vec3a(0.0, 1.0, 0.0), vec3a(1.0, 0.0, 0.0), @@ -66,7 +66,7 @@ fn build_pos_cubic(c: &mut Criterion) { } fn build_accel_cubic(c: &mut Criterion) { - let bezier = Bezier::new([[ + let bezier = CubicBezier::new([[ vec3a(0.0, 0.0, 0.0), vec3a(0.0, 1.0, 0.0), vec3a(1.0, 0.0, 0.0), diff --git a/crates/bevy_math/src/cubic_splines.rs b/crates/bevy_math/src/cubic_splines.rs index 544e815878c53..dc89a00e1a902 100644 --- a/crates/bevy_math/src/cubic_splines.rs +++ b/crates/bevy_math/src/cubic_splines.rs @@ -28,7 +28,7 @@ impl Point for Vec3A {} impl Point for Vec2 {} impl Point for f32 {} -/// A spline composed of a series of cubic Bezier curves. +/// A spline composed of a single cubic Bezier curve. /// /// Useful for user-drawn curves with local control, or animation easing. See /// [`CubicSegment::new_bezier`] for use in easing. @@ -53,22 +53,22 @@ impl Point for f32 {} /// vec2(5.0, 3.0), /// vec2(9.0, 8.0), /// ]]; -/// let bezier = Bezier::new(points).to_curve(); +/// let bezier = CubicBezier::new(points).to_curve(); /// let positions: Vec<_> = bezier.iter_positions(100).collect(); /// ``` -pub struct Bezier { +pub struct CubicBezier { control_points: Vec<[P; 4]>, } -impl Bezier

{ - /// Create a new Bezier curve from sets of control points. +impl CubicBezier

{ + /// Create a new cubic Bezier curve from sets of control points. pub fn new(control_points: impl Into>) -> Self { Self { control_points: control_points.into(), } } } -impl CubicGenerator

for Bezier

{ +impl CubicGenerator

for CubicBezier

{ #[inline] fn to_curve(&self) -> CubicCurve

{ let char_matrix = [ @@ -337,7 +337,7 @@ impl CubicSegment { /// example, the ubiquitous "ease-in-out" is defined as `(0.25, 0.1), (0.25, 1.0)`. pub fn new_bezier(p1: impl Into, p2: impl Into) -> Self { let (p0, p3) = (Vec2::ZERO, Vec2::ONE); - let bezier = Bezier::new([[p0, p1.into(), p2.into(), p3]]).to_curve(); + let bezier = CubicBezier::new([[p0, p1.into(), p2.into(), p3]]).to_curve(); bezier.segments[0].clone() } @@ -550,7 +550,7 @@ impl CubicCurve

{ mod tests { use glam::{vec2, Vec2}; - use crate::cubic_splines::{Bezier, CubicGenerator, CubicSegment}; + use crate::cubic_splines::{CubicBezier, CubicGenerator, CubicSegment}; /// How close two floats can be and still be considered equal const FLOAT_EQ: f32 = 1e-5; @@ -565,7 +565,7 @@ mod tests { vec2(5.0, 3.0), vec2(9.0, 8.0), ]]; - let bezier = Bezier::new(points).to_curve(); + let bezier = CubicBezier::new(points).to_curve(); for i in 0..=N_SAMPLES { let t = i as f32 / N_SAMPLES as f32; // Check along entire length assert!(bezier.position(t).distance(cubic_manual(t, points[0])) <= FLOAT_EQ); diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 1c75dc015d01b..b88de2e3a6dc9 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -20,7 +20,9 @@ pub use rects::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - cubic_splines::{BSpline, Bezier, CardinalSpline, CubicGenerator, CubicSegment, Hermite}, + cubic_splines::{ + BSpline, CardinalSpline, CubicBezier, CubicGenerator, CubicSegment, Hermite, + }, BVec2, BVec3, BVec4, EulerRot, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, Quat, Ray, Rect, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, diff --git a/examples/animation/cubic_curve.rs b/examples/animation/cubic_curve.rs index d6aff04536faa..92a003edaa723 100644 --- a/examples/animation/cubic_curve.rs +++ b/examples/animation/cubic_curve.rs @@ -33,7 +33,7 @@ fn setup( ]]; // Make a CubicCurve - let bezier = Bezier::new(points).to_curve(); + let bezier = CubicBezier::new(points).to_curve(); // Spawning a cube to experiment on commands.spawn(( From 4e59671ae73b8fa2937b76de3b1aef059b98e0f1 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 28 Aug 2023 10:44:52 -0700 Subject: [PATCH 30/58] clean up configure_set(s) erroring (#9577) # Objective - have errors in configure_set and configure_sets show the line number of the user calling location rather than pointing to schedule.rs - use display formatting for the errors ## Example Error Text ```text // dependency loop // before thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: DependencyLoop("A")', crates\bevy_ecs\src\schedule\schedule.rs:682:39 // after thread 'main' panicked at 'System set `A` depends on itself.', examples/stress_tests/bevymark.rs:16:9 // hierarchy loop // before thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: HierarchyLoop("A")', crates\bevy_ecs\src\schedule\schedule.rs:682:3 // after thread 'main' panicked at 'System set `A` contains itself.', examples/stress_tests/bevymark.rs:16:9 // configuring a system type set // before thread 'main' panicked at 'configuring system type sets is not allowed', crates\bevy_ecs\src\schedule\config.rs:394:9 //after thread 'main' panicked at 'configuring system type sets is not allowed', examples/stress_tests/bevymark.rs:16:9 ``` Code to produce errors: ```rust use bevy::prelude::*; #[derive(SystemSet, Clone, Debug, PartialEq, Eq, Hash)] enum TestSet { A, } fn main() { fn foo() {} let mut app = App::empty(); // Hierarchy Loop app.configure_set(Main, TestSet::A.in_set(TestSet::A)); // Dependency Loop app.configure_set(Main, TestSet::A.after(TestSet::A)); // Configure System Type Set app.configure_set(Main, foo.into_system_set()); } ``` --- crates/bevy_app/src/app.rs | 2 ++ crates/bevy_ecs/src/schedule/config.rs | 2 ++ crates/bevy_ecs/src/schedule/schedule.rs | 21 +++++++++++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 39ed373a785e2..2bc076d205f12 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -393,6 +393,7 @@ impl App { } /// Configures a system set in the default schedule, adding the set if it does not exist. + #[track_caller] pub fn configure_set( &mut self, schedule: impl ScheduleLabel, @@ -410,6 +411,7 @@ impl App { } /// Configures a collection of system sets in the default schedule, adding any sets that do not exist. + #[track_caller] pub fn configure_sets( &mut self, schedule: impl ScheduleLabel, diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index 47b72232b1179..9817feec6d4ca 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -393,6 +393,7 @@ pub struct SystemSetConfig { } impl SystemSetConfig { + #[track_caller] fn new(set: BoxedSystemSet) -> Self { // system type sets are automatically populated // to avoid unintentionally broad changes, they cannot be configured @@ -449,6 +450,7 @@ pub trait IntoSystemSetConfig: Sized { } impl IntoSystemSetConfig for S { + #[track_caller] fn into_config(self) -> SystemSetConfig { SystemSetConfig::new(Box::new(self)) } diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 2db71e2dc19e2..81bc44f23a7ee 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -181,12 +181,14 @@ impl Schedule { } /// Configures a system set in this schedule, adding it if it does not exist. + #[track_caller] pub fn configure_set(&mut self, set: impl IntoSystemSetConfig) -> &mut Self { self.graph.configure_set(set); self } /// Configures a collection of system sets in this schedule, adding them if they does not exist. + #[track_caller] pub fn configure_sets(&mut self, sets: impl IntoSystemSetConfigs) -> &mut Self { self.graph.configure_sets(sets); self @@ -656,6 +658,7 @@ impl ScheduleGraph { Ok(id) } + #[track_caller] fn configure_sets(&mut self, sets: impl IntoSystemSetConfigs) { let SystemSetConfigs { sets, chained } = sets.into_configs(); let mut set_iter = sets.into_iter(); @@ -669,15 +672,25 @@ impl ScheduleGraph { } } else { for set in set_iter { - self.configure_set_inner(set).unwrap(); + if let Err(e) = self.configure_set_inner(set) { + // using `unwrap_or_else(panic!)` led to the error being reported + // from this line instead of in the user code + panic!("{e}"); + }; } } } + #[track_caller] fn configure_set(&mut self, set: impl IntoSystemSetConfig) { - self.configure_set_inner(set).unwrap(); + if let Err(e) = self.configure_set_inner(set) { + // using `unwrap_or_else(panic!)` led to the error being reported + // from this line instead of in the user code + panic!("{e}"); + }; } + #[track_caller] fn configure_set_inner( &mut self, set: impl IntoSystemSetConfig, @@ -1560,7 +1573,7 @@ impl ScheduleGraph { #[non_exhaustive] pub enum ScheduleBuildError { /// A system set contains itself. - #[error("`{0:?}` contains itself.")] + #[error("System set `{0}` contains itself.")] HierarchyLoop(String), /// The hierarchy of system sets contains a cycle. #[error("System set hierarchy contains cycle(s).\n{0}")] @@ -1571,7 +1584,7 @@ pub enum ScheduleBuildError { #[error("System set hierarchy contains redundant edges.\n{0}")] HierarchyRedundancy(String), /// A system (set) has been told to run before itself. - #[error("`{0:?}` depends on itself.")] + #[error("System set `{0}` depends on itself.")] DependencyLoop(String), /// The dependency graph contains a cycle. #[error("System dependencies contain cycle(s).\n{0}")] From da8ab16d83559b94ef24822ecf34ef45974b3060 Mon Sep 17 00:00:00 2001 From: Joseph <21144246+JoJoJet@users.noreply.github.com> Date: Mon, 28 Aug 2023 10:47:15 -0700 Subject: [PATCH 31/58] Relax more `Sync` bounds on `Local` (#9589) # Objective #5483 allows for the creation of non-`Sync` locals. However, it's not actually possible to use these types as there is a `Sync` bound on the `Deref` impls. ## Solution Remove the unnecessary bounds. --- crates/bevy_ecs/src/system/system_param.rs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 9684a41f1ad12..ef7a8252bf153 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -685,7 +685,7 @@ pub struct Local<'s, T: FromWorld + Send + 'static>(pub(crate) &'s mut T); // SAFETY: Local only accesses internal state unsafe impl<'s, T: FromWorld + Send + 'static> ReadOnlySystemParam for Local<'s, T> {} -impl<'s, T: FromWorld + Send + Sync + 'static> Deref for Local<'s, T> { +impl<'s, T: FromWorld + Send + 'static> Deref for Local<'s, T> { type Target = T; #[inline] @@ -694,7 +694,7 @@ impl<'s, T: FromWorld + Send + Sync + 'static> Deref for Local<'s, T> { } } -impl<'s, T: FromWorld + Send + Sync + 'static> DerefMut for Local<'s, T> { +impl<'s, T: FromWorld + Send + 'static> DerefMut for Local<'s, T> { #[inline] fn deref_mut(&mut self) -> &mut Self::Target { self.0 @@ -1560,7 +1560,7 @@ mod tests { query::{ReadOnlyWorldQuery, WorldQuery}, system::{assert_is_system, Query}, }; - use std::marker::PhantomData; + use std::{cell::RefCell, marker::PhantomData}; // Compile test for https://github.com/bevyengine/bevy/pull/2838. #[test] @@ -1726,4 +1726,17 @@ mod tests { fn my_system(_: InvariantParam) {} assert_is_system(my_system); } + + // Compile test for https://github.com/bevyengine/bevy/pull/9589. + #[test] + fn non_sync_local() { + fn non_sync_system(cell: Local>) { + assert_eq!(*cell.borrow(), 0); + } + + let mut world = World::new(); + let mut schedule = crate::schedule::Schedule::new(); + schedule.add_systems(non_sync_system); + schedule.run(&mut world); + } } From a47a5b30fea3a7c9f953b908e13ce6ee13afb836 Mon Sep 17 00:00:00 2001 From: Joseph <21144246+JoJoJet@users.noreply.github.com> Date: Mon, 28 Aug 2023 10:49:25 -0700 Subject: [PATCH 32/58] Rename `ManualEventIterator` (#9592) # Objective The name `ManualEventIterator` is long and unnecessary, as this is the iterator type used for both `EventReader` and `ManualEventReader`. ## Solution Rename `ManualEventIterator` to `EventIterator`. To ease migration, add a deprecated type alias with the old name. --- ## Changelog - The types `ManualEventIterator{WithId}` have been renamed to `EventIterator{WithId}`. ## Migration Guide The type `ManualEventIterator` has been renamed to `EventIterator`. Additonally, `ManualEventIteratorWithId` has been renamed to `EventIteratorWithId`. --- crates/bevy_ecs/src/event.rs | 49 +++++++++++++++--------- crates/bevy_ecs/src/removal_detection.rs | 8 ++-- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 95ef6ebbe47b9..9693bc18fa2d8 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -390,12 +390,12 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// Iterates over the events this [`EventReader`] has not seen yet. This updates the /// [`EventReader`]'s event counter, which means subsequent event reads will not include events /// that happened before now. - pub fn iter(&mut self) -> ManualEventIterator<'_, E> { + pub fn iter(&mut self) -> EventIterator<'_, E> { self.reader.iter(&self.events) } /// Like [`iter`](Self::iter), except also returning the [`EventId`] of the events. - pub fn iter_with_id(&mut self) -> ManualEventIteratorWithId<'_, E> { + pub fn iter_with_id(&mut self) -> EventIteratorWithId<'_, E> { self.reader.iter_with_id(&self.events) } @@ -442,7 +442,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { impl<'a, 'w, 's, E: Event> IntoIterator for &'a mut EventReader<'w, 's, E> { type Item = &'a E; - type IntoIter = ManualEventIterator<'a, E>; + type IntoIter = EventIterator<'a, E>; fn into_iter(self) -> Self::IntoIter { self.iter() } @@ -541,16 +541,13 @@ impl Default for ManualEventReader { #[allow(clippy::len_without_is_empty)] // Check fails since the is_empty implementation has a signature other than `(&self) -> bool` impl ManualEventReader { /// See [`EventReader::iter`] - pub fn iter<'a>(&'a mut self, events: &'a Events) -> ManualEventIterator<'a, E> { + pub fn iter<'a>(&'a mut self, events: &'a Events) -> EventIterator<'a, E> { self.iter_with_id(events).without_id() } /// See [`EventReader::iter_with_id`] - pub fn iter_with_id<'a>( - &'a mut self, - events: &'a Events, - ) -> ManualEventIteratorWithId<'a, E> { - ManualEventIteratorWithId::new(self, events) + pub fn iter_with_id<'a>(&'a mut self, events: &'a Events) -> EventIteratorWithId<'a, E> { + EventIteratorWithId::new(self, events) } /// See [`EventReader::len`] @@ -585,11 +582,18 @@ impl ManualEventReader { /// An iterator that yields any unread events from an [`EventReader`] or [`ManualEventReader`]. #[derive(Debug)] -pub struct ManualEventIterator<'a, E: Event> { - iter: ManualEventIteratorWithId<'a, E>, +pub struct EventIterator<'a, E: Event> { + iter: EventIteratorWithId<'a, E>, } -impl<'a, E: Event> Iterator for ManualEventIterator<'a, E> { +/// An iterator that yields any unread events from an [`EventReader`] or [`ManualEventReader`]. +/// +/// This is a type alias for [`EventIterator`], which used to be called `ManualEventIterator`. +/// This type alias will be removed in the next release of bevy, so you should use [`EventIterator`] directly instead. +#[deprecated = "This type has been renamed to `EventIterator`."] +pub type ManualEventIterator<'a, E> = EventIterator<'a, E>; + +impl<'a, E: Event> Iterator for EventIterator<'a, E> { type Item = &'a E; fn next(&mut self) -> Option { self.iter.next().map(|(event, _)| event) @@ -615,7 +619,7 @@ impl<'a, E: Event> Iterator for ManualEventIterator<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for ManualEventIterator<'a, E> { +impl<'a, E: Event> ExactSizeIterator for EventIterator<'a, E> { fn len(&self) -> usize { self.iter.len() } @@ -623,13 +627,20 @@ impl<'a, E: Event> ExactSizeIterator for ManualEventIterator<'a, E> { /// An iterator that yields any unread events (and their IDs) from an [`EventReader`] or [`ManualEventReader`]. #[derive(Debug)] -pub struct ManualEventIteratorWithId<'a, E: Event> { +pub struct EventIteratorWithId<'a, E: Event> { reader: &'a mut ManualEventReader, chain: Chain>, Iter<'a, EventInstance>>, unread: usize, } -impl<'a, E: Event> ManualEventIteratorWithId<'a, E> { +/// An iterator that yields any unread events (and their IDs) from an [`EventReader`] or [`ManualEventReader`]. +/// +/// This is a type alias for [`EventIteratorWithId`], which used to be called `ManualEventIteratorWithId`. +/// This type alias will be removed in the next release of bevy, so you should use [`EventIteratorWithId`] directly instead. +#[deprecated = "This type has been renamed to `EventIteratorWithId`."] +pub type ManualEventIteratorWithId<'a, E> = EventIteratorWithId<'a, E>; + +impl<'a, E: Event> EventIteratorWithId<'a, E> { /// Creates a new iterator that yields any `events` that have not yet been seen by `reader`. pub fn new(reader: &'a mut ManualEventReader, events: &'a Events) -> Self { let a_index = (reader.last_event_count).saturating_sub(events.events_a.start_event_count); @@ -652,12 +663,12 @@ impl<'a, E: Event> ManualEventIteratorWithId<'a, E> { } /// Iterate over only the events. - pub fn without_id(self) -> ManualEventIterator<'a, E> { - ManualEventIterator { iter: self } + pub fn without_id(self) -> EventIterator<'a, E> { + EventIterator { iter: self } } } -impl<'a, E: Event> Iterator for ManualEventIteratorWithId<'a, E> { +impl<'a, E: Event> Iterator for EventIteratorWithId<'a, E> { type Item = (&'a E, EventId); fn next(&mut self) -> Option { match self @@ -706,7 +717,7 @@ impl<'a, E: Event> Iterator for ManualEventIteratorWithId<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for ManualEventIteratorWithId<'a, E> { +impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> { fn len(&self) -> usize { self.unread } diff --git a/crates/bevy_ecs/src/removal_detection.rs b/crates/bevy_ecs/src/removal_detection.rs index 5546f17eacc50..cd58cfd019343 100644 --- a/crates/bevy_ecs/src/removal_detection.rs +++ b/crates/bevy_ecs/src/removal_detection.rs @@ -4,9 +4,7 @@ use crate::{ self as bevy_ecs, component::{Component, ComponentId, ComponentIdFor, Tick}, entity::Entity, - event::{ - Event, EventId, Events, ManualEventIterator, ManualEventIteratorWithId, ManualEventReader, - }, + event::{Event, EventId, EventIterator, EventIteratorWithId, Events, ManualEventReader}, prelude::Local, storage::SparseSet, system::{ReadOnlySystemParam, SystemMeta, SystemParam}, @@ -145,7 +143,7 @@ pub struct RemovedComponents<'w, 's, T: Component> { /// /// See [`RemovedComponents`]. pub type RemovedIter<'a> = iter::Map< - iter::Flatten>>>, + iter::Flatten>>>, fn(RemovedComponentEntity) -> Entity, >; @@ -153,7 +151,7 @@ pub type RemovedIter<'a> = iter::Map< /// /// See [`RemovedComponents`]. pub type RemovedIterWithId<'a> = iter::Map< - iter::Flatten>>, + iter::Flatten>>, fn( (&RemovedComponentEntity, EventId), ) -> (Entity, EventId), From 1839ff7e2ab4f6fc77b3927e85653f80c1e08864 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Tue, 29 Aug 2023 04:08:53 +1000 Subject: [PATCH 33/58] Replaced `EntityCommand` Implementation for `FnOnce` (#9604) # Objective - Fixes #4917 - Replaces #9602 ## Solution - Replaced `EntityCommand` implementation for `FnOnce` to apply to `FnOnce(EntityMut)` instead of `FnOnce(Entity, &mut World)` --- ## Changelog - `FnOnce(Entity, &mut World)` no longer implements `EntityCommand`. This is a breaking change. ## Migration Guide ### 1. New-Type `FnOnce` Create an `EntityCommand` type which implements the method you previously wrote: ```rust pub struct ClassicEntityCommand(pub F); impl EntityCommand for ClassicEntityCommand where F: FnOnce(Entity, &mut World) + Send + 'static, { fn apply(self, id: Entity, world: &mut World) { (self.0)(id, world); } } commands.add(ClassicEntityCommand(|id: Entity, world: &mut World| { /* ... */ })); ``` ### 2. Extract `(Entity, &mut World)` from `EntityMut` The method `into_world_mut` can be used to gain access to the `World` from an `EntityMut`. ```rust let old = |id: Entity, world: &mut World| { /* ... */ }; let new = |mut entity: EntityMut| { let id = entity.id(); let world = entity.into_world_mut(); /* ... */ }; ``` --- crates/bevy_ecs/src/system/commands/mod.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index fac21c7c0ffc2..ed6677ea9f7e7 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -5,7 +5,7 @@ use crate::{ self as bevy_ecs, bundle::Bundle, entity::{Entities, Entity}, - world::{FromWorld, World}, + world::{EntityMut, FromWorld, World}, }; use bevy_ecs_macros::SystemParam; use bevy_utils::tracing::{error, info}; @@ -805,12 +805,13 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { /// /// ``` /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::world::EntityMut; /// # fn my_system(mut commands: Commands) { /// commands /// .spawn_empty() /// // Closures with this signature implement `EntityCommand`. - /// .add(|id: Entity, world: &mut World| { - /// println!("Executed an EntityCommand for {id:?}"); + /// .add(|entity: EntityMut| { + /// println!("Executed an EntityCommand for {:?}", entity.id()); /// }); /// # } /// # bevy_ecs::system::assert_is_system(my_system); @@ -848,10 +849,10 @@ where impl EntityCommand for F where - F: FnOnce(Entity, &mut World) + Send + 'static, + F: FnOnce(EntityMut) + Send + 'static, { fn apply(self, id: Entity, world: &mut World) { - self(id, world); + self(world.entity_mut(id)); } } From 9f27f011c117875380a8476d414c1bc834b02d8c Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 28 Aug 2023 19:11:30 +0100 Subject: [PATCH 34/58] Remove `Val`'s `try_*` arithmetic methods (#9609) # Objective Remove `Val`'s `try_*` arithmetic methods. fixes #9571 ## Changelog Removed these methods from `bevy_ui::ui_node::Val`: - `try_add` - `try_sub` - `try_add_assign_with_size` - `try_sub_assign_with_size` - `try_add_assign` - `try_sub_assign` - `try_add_assign_with_size` - `try_sub_assign_with_size` ## Migration Guide `Val`'s `try_*` arithmetic methods have been removed. To perform arithmetic on `Val`s deconstruct them using pattern matching. --- crates/bevy_ui/src/ui_node.rs | 180 ---------------------------------- 1 file changed, 180 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index 93bb9a698a22c..f7c18f397d627 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -237,42 +237,6 @@ pub enum ValArithmeticError { } impl Val { - /// Tries to add the values of two [`Val`]s. - /// Returns [`ValArithmeticError::NonIdenticalVariants`] if two [`Val`]s are of different variants. - /// When adding non-numeric [`Val`]s, it returns the value unchanged. - pub fn try_add(&self, rhs: Val) -> Result { - match (self, rhs) { - (Val::Auto, Val::Auto) => Ok(*self), - (Val::Px(value), Val::Px(rhs_value)) => Ok(Val::Px(value + rhs_value)), - (Val::Percent(value), Val::Percent(rhs_value)) => Ok(Val::Percent(value + rhs_value)), - _ => Err(ValArithmeticError::NonIdenticalVariants), - } - } - - /// Adds `rhs` to `self` and assigns the result to `self` (see [`Val::try_add`]) - pub fn try_add_assign(&mut self, rhs: Val) -> Result<(), ValArithmeticError> { - *self = self.try_add(rhs)?; - Ok(()) - } - - /// Tries to subtract the values of two [`Val`]s. - /// Returns [`ValArithmeticError::NonIdenticalVariants`] if two [`Val`]s are of different variants. - /// When adding non-numeric [`Val`]s, it returns the value unchanged. - pub fn try_sub(&self, rhs: Val) -> Result { - match (self, rhs) { - (Val::Auto, Val::Auto) => Ok(*self), - (Val::Px(value), Val::Px(rhs_value)) => Ok(Val::Px(value - rhs_value)), - (Val::Percent(value), Val::Percent(rhs_value)) => Ok(Val::Percent(value - rhs_value)), - _ => Err(ValArithmeticError::NonIdenticalVariants), - } - } - - /// Subtracts `rhs` from `self` and assigns the result to `self` (see [`Val::try_sub`]) - pub fn try_sub_assign(&mut self, rhs: Val) -> Result<(), ValArithmeticError> { - *self = self.try_sub(rhs)?; - Ok(()) - } - /// A convenience function for simple evaluation of [`Val::Percent`] variant into a concrete [`Val::Px`] value. /// Returns a [`ValArithmeticError::NonEvaluateable`] if the [`Val`] is impossible to evaluate into [`Val::Px`]. /// Otherwise it returns an [`f32`] containing the evaluated value in pixels. @@ -285,46 +249,6 @@ impl Val { _ => Err(ValArithmeticError::NonEvaluateable), } } - - /// Similar to [`Val::try_add`], but performs [`Val::evaluate`] on both values before adding. - /// Returns an [`f32`] value in pixels. - pub fn try_add_with_size(&self, rhs: Val, size: f32) -> Result { - let lhs = self.evaluate(size)?; - let rhs = rhs.evaluate(size)?; - - Ok(lhs + rhs) - } - - /// Similar to [`Val::try_add_assign`], but performs [`Val::evaluate`] on both values before adding. - /// The value gets converted to [`Val::Px`]. - pub fn try_add_assign_with_size( - &mut self, - rhs: Val, - size: f32, - ) -> Result<(), ValArithmeticError> { - *self = Val::Px(self.evaluate(size)? + rhs.evaluate(size)?); - Ok(()) - } - - /// Similar to [`Val::try_sub`], but performs [`Val::evaluate`] on both values before subtracting. - /// Returns an [`f32`] value in pixels. - pub fn try_sub_with_size(&self, rhs: Val, size: f32) -> Result { - let lhs = self.evaluate(size)?; - let rhs = rhs.evaluate(size)?; - - Ok(lhs - rhs) - } - - /// Similar to [`Val::try_sub_assign`], but performs [`Val::evaluate`] on both values before adding. - /// The value gets converted to [`Val::Px`]. - pub fn try_sub_assign_with_size( - &mut self, - rhs: Val, - size: f32, - ) -> Result<(), ValArithmeticError> { - *self = Val::Px(self.try_add_with_size(rhs, size)?); - Ok(()) - } } /// Describes the style of a UI container node @@ -1795,67 +1719,6 @@ mod tests { use super::Val; - #[test] - fn val_try_add() { - let auto_sum = Val::Auto.try_add(Val::Auto).unwrap(); - let px_sum = Val::Px(20.).try_add(Val::Px(22.)).unwrap(); - let percent_sum = Val::Percent(50.).try_add(Val::Percent(50.)).unwrap(); - - assert_eq!(auto_sum, Val::Auto); - assert_eq!(px_sum, Val::Px(42.)); - assert_eq!(percent_sum, Val::Percent(100.)); - } - - #[test] - fn val_try_add_to_self() { - let mut val = Val::Px(5.); - - val.try_add_assign(Val::Px(3.)).unwrap(); - - assert_eq!(val, Val::Px(8.)); - } - - #[test] - fn val_try_sub() { - let auto_sum = Val::Auto.try_sub(Val::Auto).unwrap(); - let px_sum = Val::Px(72.).try_sub(Val::Px(30.)).unwrap(); - let percent_sum = Val::Percent(100.).try_sub(Val::Percent(50.)).unwrap(); - - assert_eq!(auto_sum, Val::Auto); - assert_eq!(px_sum, Val::Px(42.)); - assert_eq!(percent_sum, Val::Percent(50.)); - } - - #[test] - fn different_variant_val_try_add() { - let different_variant_sum_1 = Val::Px(50.).try_add(Val::Percent(50.)); - let different_variant_sum_2 = Val::Percent(50.).try_add(Val::Auto); - - assert_eq!( - different_variant_sum_1, - Err(ValArithmeticError::NonIdenticalVariants) - ); - assert_eq!( - different_variant_sum_2, - Err(ValArithmeticError::NonIdenticalVariants) - ); - } - - #[test] - fn different_variant_val_try_sub() { - let different_variant_diff_1 = Val::Px(50.).try_sub(Val::Percent(50.)); - let different_variant_diff_2 = Val::Percent(50.).try_sub(Val::Auto); - - assert_eq!( - different_variant_diff_1, - Err(ValArithmeticError::NonIdenticalVariants) - ); - assert_eq!( - different_variant_diff_2, - Err(ValArithmeticError::NonIdenticalVariants) - ); - } - #[test] fn val_evaluate() { let size = 250.; @@ -1880,49 +1743,6 @@ mod tests { assert_eq!(evaluate_auto, Err(ValArithmeticError::NonEvaluateable)); } - #[test] - fn val_try_add_with_size() { - let size = 250.; - - let px_sum = Val::Px(21.).try_add_with_size(Val::Px(21.), size).unwrap(); - let percent_sum = Val::Percent(20.) - .try_add_with_size(Val::Percent(30.), size) - .unwrap(); - let mixed_sum = Val::Px(20.) - .try_add_with_size(Val::Percent(30.), size) - .unwrap(); - - assert_eq!(px_sum, 42.); - assert_eq!(percent_sum, 0.5 * size); - assert_eq!(mixed_sum, 20. + 0.3 * size); - } - - #[test] - fn val_try_sub_with_size() { - let size = 250.; - - let px_sum = Val::Px(60.).try_sub_with_size(Val::Px(18.), size).unwrap(); - let percent_sum = Val::Percent(80.) - .try_sub_with_size(Val::Percent(30.), size) - .unwrap(); - let mixed_sum = Val::Percent(50.) - .try_sub_with_size(Val::Px(30.), size) - .unwrap(); - - assert_eq!(px_sum, 42.); - assert_eq!(percent_sum, 0.5 * size); - assert_eq!(mixed_sum, 0.5 * size - 30.); - } - - #[test] - fn val_try_add_non_numeric_with_size() { - let size = 250.; - - let percent_sum = Val::Auto.try_add_with_size(Val::Auto, size); - - assert_eq!(percent_sum, Err(ValArithmeticError::NonEvaluateable)); - } - #[test] fn val_arithmetic_error_messages() { assert_eq!( From 2e3900f6a9453e6c1fb260e7636ff3e4a984b482 Mon Sep 17 00:00:00 2001 From: lelo <15314665+hate@users.noreply.github.com> Date: Mon, 28 Aug 2023 14:21:12 -0400 Subject: [PATCH 35/58] Clarify what happens when setting the audio volume (#9480) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Fixes [#8835](https://github.com/bevyengine/bevy/issues/8835) ## Solution - Added a note to the `set_volume` docstring which explains how volume is interpreted. --------- Co-authored-by: Alice Cecile Co-authored-by: GitGhillie Co-authored-by: François --- crates/bevy_audio/src/sinks.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/bevy_audio/src/sinks.rs b/crates/bevy_audio/src/sinks.rs index f81b817d698f2..e0730cbebd60f 100644 --- a/crates/bevy_audio/src/sinks.rs +++ b/crates/bevy_audio/src/sinks.rs @@ -15,6 +15,14 @@ pub trait AudioSinkPlayback { /// /// The value `1.0` is the "normal" volume (unfiltered input). Any value other than `1.0` /// will multiply each sample by this value. + /// + /// # Note on Audio Volume + /// + /// An increase of 10 decibels (dB) roughly corresponds to the perceived volume doubling in intensity. + /// As this function scales not the volume but the amplitude, a conversion might be necessary. + /// For example, to halve the perceived volume you need to decrease the volume by 10 dB. + /// This corresponds to 20log(x) = -10dB, solving x = 10^(-10/20) = 0.316. + /// Multiply the current volume by 0.316 to halve the perceived volume. fn set_volume(&self, volume: f32); /// Gets the speed of the sound. From 72fc63e594386d838641e7ce2e8b196148f4bd64 Mon Sep 17 00:00:00 2001 From: Noah Date: Mon, 28 Aug 2023 14:21:20 -0400 Subject: [PATCH 36/58] implement insert and remove reflected entity commands (#8895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective To enable non exclusive system usage of reflected components and make reflection more ergonomic to use by making it more in line with standard entity commands. ## Solution - Implements a new `EntityCommands` extension trait for reflection related functions in the reflect module of bevy_ecs. - Implements 4 new commands, `insert_reflect`, `insert_reflect_with_registry`, `remove_reflect`, and `remove_reflect_with_registry`. Both insert commands take a `Box` component while the remove commands take the component type name. - Made `EntityCommands` fields pub(crate) to allow access in the reflect module. (Might be worth making these just public to enable user end custom entity commands in a different pr) - Added basic tests to ensure the commands are actually working. - Documentation of functions. --- ## Changelog Added: - Implements 4 new commands on the new entity commands extension. - `insert_reflect` - `remove_reflect` - `insert_reflect_with_registry` - `remove_reflect_with_registry` The commands operate the same except the with_registry commands take a generic parameter for a resource that implements `AsRef`. Otherwise the default commands use the `AppTypeRegistry` for reflection data. Changed: - Made `EntityCommands` fields pub(crate) to allow access in the reflect module. > Hopefully this time it works. Please don't make me rebase again ☹ --- .../bevy_ecs/src/reflect/entity_commands.rs | 452 ++++++++++++++++++ crates/bevy_ecs/src/reflect/mod.rs | 2 + crates/bevy_ecs/src/system/commands/mod.rs | 4 +- 3 files changed, 456 insertions(+), 2 deletions(-) create mode 100644 crates/bevy_ecs/src/reflect/entity_commands.rs diff --git a/crates/bevy_ecs/src/reflect/entity_commands.rs b/crates/bevy_ecs/src/reflect/entity_commands.rs new file mode 100644 index 0000000000000..da9374bbf8a06 --- /dev/null +++ b/crates/bevy_ecs/src/reflect/entity_commands.rs @@ -0,0 +1,452 @@ +use crate::prelude::Mut; +use crate::reflect::AppTypeRegistry; +use crate::system::{Command, EntityCommands, Resource}; +use crate::{entity::Entity, reflect::ReflectComponent, world::World}; +use bevy_reflect::{Reflect, TypeRegistry}; +use std::borrow::Cow; +use std::marker::PhantomData; + +/// An extension trait for [`EntityCommands`] for reflection related functions +pub trait ReflectCommandExt { + /// Adds the given boxed reflect component to the entity using the reflection data in + /// [`AppTypeRegistry`]. + /// + /// This will overwrite any previous component of the same type. + /// + /// # Panics + /// + /// - If the entity doesn't exist. + /// - If [`AppTypeRegistry`] does not have the reflection data for the given [`Component`](crate::component::Component). + /// - If the component data is invalid. See [`Reflect::apply`] for further details. + /// - If [`AppTypeRegistry`] is not present in the [`World`]. + /// + /// # Note + /// + /// Prefer to use the typed [`EntityCommands::insert`] if possible. Adding a reflected component + /// is much slower. + /// + /// # Example + /// + /// ```rust + /// // Note that you need to register the component type in the AppTypeRegistry prior to using + /// // reflection. You can use the helpers on the App with `app.register_type::()` + /// // or write to the TypeRegistry directly to register all your components + /// + /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::reflect::ReflectCommandExt; + /// # use bevy_reflect::{FromReflect, FromType, Reflect, TypeRegistry}; + /// // A resource that can hold any component that implements reflect as a boxed reflect component + /// #[derive(Resource)] + /// struct Prefab{ + /// component: Box, + /// } + /// #[derive(Component, Reflect, Default)] + /// #[reflect(Component)] + /// struct ComponentA(u32); + /// + /// #[derive(Component, Reflect, Default)] + /// #[reflect(Component)] + /// struct ComponentB(String); + /// + /// fn insert_reflect_component( + /// mut commands: Commands, + /// mut prefab: ResMut + /// ) { + /// // Create a set of new boxed reflect components to use + /// let boxed_reflect_component_a: Box = Box::new(ComponentA(916)); + /// let boxed_reflect_component_b: Box = Box::new(ComponentB("NineSixteen".to_string())); + /// + /// // You can overwrite the component in the resource with either ComponentA or ComponentB + /// prefab.component = boxed_reflect_component_a; + /// prefab.component = boxed_reflect_component_b; + /// + /// // No matter which component is in the resource and without knowing the exact type, you can + /// // use the insert_reflect entity command to insert that component into an entity. + /// commands + /// .spawn_empty() + /// .insert_reflect(prefab.component.clone_value()); + /// } + /// + /// ``` + fn insert_reflect(&mut self, component: Box) -> &mut Self; + + /// Same as [`insert_reflect`](ReflectCommandExt::insert_reflect), but using the `T` resource as type registry instead of + /// `AppTypeRegistry`. + /// + /// # Panics + /// + /// - If the given [`Resource`] is not present in the [`World`]. + /// + /// # Note + /// + /// - The given [`Resource`] is removed from the [`World`] before the command is applied. + fn insert_reflect_with_registry>( + &mut self, + component: Box, + ) -> &mut Self; + + /// Removes from the entity the component with the given type name registered in [`AppTypeRegistry`]. + /// + /// Does nothing if the entity does not have a component of the same type, if [`AppTypeRegistry`] + /// does not contain the reflection data for the given component, or if the entity does not exist. + /// + /// # Note + /// + /// Prefer to use the typed [`EntityCommands::remove`] if possible. Removing a reflected component + /// is much slower. + /// + /// # Example + /// + /// ```rust + /// // Note that you need to register the component type in the AppTypeRegistry prior to using + /// // reflection. You can use the helpers on the App with `app.register_type::()` + /// // or write to the TypeRegistry directly to register all your components + /// + /// # use bevy_ecs::prelude::*; + /// # use bevy_ecs::reflect::ReflectCommandExt; + /// # use bevy_reflect::{FromReflect, FromType, Reflect, TypeRegistry}; + /// + /// // A resource that can hold any component that implements reflect as a boxed reflect component + /// #[derive(Resource)] + /// struct Prefab{ + /// entity: Entity, + /// component: Box, + /// } + /// #[derive(Component, Reflect, Default)] + /// #[reflect(Component)] + /// struct ComponentA(u32); + /// #[derive(Component, Reflect, Default)] + /// #[reflect(Component)] + /// struct ComponentB(String); + /// + /// fn remove_reflect_component( + /// mut commands: Commands, + /// prefab: Res + /// ) { + /// // Prefab can hold any boxed reflect component. In this case either + /// // ComponentA or ComponentB. No matter which component is in the resource though, + /// // we can attempt to remove any component of that same type from an entity. + /// commands.entity(prefab.entity) + /// .remove_reflect(prefab.component.type_name().to_owned()); + /// } + /// + /// ``` + fn remove_reflect(&mut self, component_type_name: impl Into>) -> &mut Self; + /// Same as [`remove_reflect`](ReflectCommandExt::remove_reflect), but using the `T` resource as type registry instead of + /// `AppTypeRegistry`. + fn remove_reflect_with_registry>( + &mut self, + component_type_name: impl Into>, + ) -> &mut Self; +} + +impl<'w, 's, 'a> ReflectCommandExt for EntityCommands<'w, 's, 'a> { + fn insert_reflect(&mut self, component: Box) -> &mut Self { + self.commands.add(InsertReflect { + entity: self.entity, + component, + }); + self + } + + fn insert_reflect_with_registry>( + &mut self, + component: Box, + ) -> &mut Self { + self.commands.add(InsertReflectWithRegistry:: { + entity: self.entity, + _t: PhantomData, + component, + }); + self + } + + fn remove_reflect(&mut self, component_type_name: impl Into>) -> &mut Self { + self.commands.add(RemoveReflect { + entity: self.entity, + component_type_name: component_type_name.into(), + }); + self + } + + fn remove_reflect_with_registry>( + &mut self, + component_type_name: impl Into>, + ) -> &mut Self { + self.commands.add(RemoveReflectWithRegistry:: { + entity: self.entity, + _t: PhantomData, + component_type_name: component_type_name.into(), + }); + self + } +} + +/// Helper function to add a reflect component to a given entity +fn insert_reflect( + world: &mut World, + entity: Entity, + type_registry: &TypeRegistry, + component: Box, +) { + let type_info = component.type_name(); + let Some(mut entity) = world.get_entity_mut(entity) else { + panic!("error[B0003]: Could not insert a reflected component (of type {}) for entity {entity:?} because it doesn't exist in this World.", component.type_name()); + }; + let Some(type_registration) = type_registry.get_with_name(type_info) else { + panic!("Could not get type registration (for component type {}) because it doesn't exist in the TypeRegistry.", component.type_name()); + }; + let Some(reflect_component) = type_registration.data::() else { + panic!("Could not get ReflectComponent data (for component type {}) because it doesn't exist in this TypeRegistration.", component.type_name()); + }; + reflect_component.insert(&mut entity, &*component); +} + +/// A [`Command`] that adds the boxed reflect component to an entity using the data in +/// [`AppTypeRegistry`]. +/// +/// See [`ReflectCommandExt::insert_reflect`] for details. +pub struct InsertReflect { + /// The entity on which the component will be inserted. + pub entity: Entity, + /// The reflect [Component](crate::component::Component) that will be added to the entity. + pub component: Box, +} + +impl Command for InsertReflect { + fn apply(self, world: &mut World) { + let registry = world.get_resource::().unwrap().clone(); + insert_reflect(world, self.entity, ®istry.read(), self.component); + } +} + +/// A [`Command`] that adds the boxed reflect component to an entity using the data in the provided +/// [`Resource`] that implements [`AsRef`]. +/// +/// See [`ReflectCommandExt::insert_reflect_with_registry`] for details. +pub struct InsertReflectWithRegistry> { + /// The entity on which the component will be inserted. + pub entity: Entity, + pub _t: PhantomData, + /// The reflect [Component](crate::component::Component) that will be added to the entity. + pub component: Box, +} + +impl> Command for InsertReflectWithRegistry { + fn apply(self, world: &mut World) { + world.resource_scope(|world, registry: Mut| { + let registry: &TypeRegistry = registry.as_ref().as_ref(); + insert_reflect(world, self.entity, registry, self.component); + }); + } +} + +/// Helper function to remove a reflect component from a given entity +fn remove_reflect( + world: &mut World, + entity: Entity, + type_registry: &TypeRegistry, + component_type_name: Cow<'static, str>, +) { + let Some(mut entity) = world.get_entity_mut(entity) else { + return; + }; + let Some(type_registration) = type_registry.get_with_name(&component_type_name) else { + return; + }; + let Some(reflect_component) = type_registration.data::() else { + return; + }; + reflect_component.remove(&mut entity); +} + +/// A [`Command`] that removes the component of the same type as the given component type name from +/// the provided entity. +/// +/// See [`ReflectCommandExt::remove_reflect`] for details. +pub struct RemoveReflect { + /// The entity from which the component will be removed. + pub entity: Entity, + /// The [Component](crate::component::Component) type name that will be used to remove a component + /// of the same type from the entity. + pub component_type_name: Cow<'static, str>, +} + +impl Command for RemoveReflect { + fn apply(self, world: &mut World) { + let registry = world.get_resource::().unwrap().clone(); + remove_reflect( + world, + self.entity, + ®istry.read(), + self.component_type_name, + ); + } +} + +/// A [`Command`] that removes the component of the same type as the given component type name from +/// the provided entity using the provided [`Resource`] that implements [`AsRef`]. +/// +/// See [`ReflectCommandExt::remove_reflect_with_registry`] for details. +pub struct RemoveReflectWithRegistry> { + /// The entity from which the component will be removed. + pub entity: Entity, + pub _t: PhantomData, + /// The [Component](crate::component::Component) type name that will be used to remove a component + /// of the same type from the entity. + pub component_type_name: Cow<'static, str>, +} + +impl> Command for RemoveReflectWithRegistry { + fn apply(self, world: &mut World) { + world.resource_scope(|world, registry: Mut| { + let registry: &TypeRegistry = registry.as_ref().as_ref(); + remove_reflect(world, self.entity, registry, self.component_type_name); + }); + } +} + +#[cfg(test)] +mod tests { + use crate::prelude::{AppTypeRegistry, ReflectComponent}; + use crate::reflect::ReflectCommandExt; + use crate::system::{Commands, SystemState}; + use crate::{self as bevy_ecs, component::Component, world::World}; + use bevy_ecs_macros::Resource; + use bevy_reflect::{Reflect, TypeRegistry}; + + #[derive(Resource)] + struct TypeRegistryResource { + type_registry: TypeRegistry, + } + + impl AsRef for TypeRegistryResource { + fn as_ref(&self) -> &TypeRegistry { + &self.type_registry + } + } + + #[derive(Component, Reflect, Default, PartialEq, Eq, Debug)] + #[reflect(Component)] + struct ComponentA(u32); + + #[test] + fn insert_reflected() { + let mut world = World::new(); + + let type_registry = AppTypeRegistry::default(); + { + let mut registry = type_registry.write(); + registry.register::(); + registry.register_type_data::(); + } + world.insert_resource(type_registry); + + let mut system_state: SystemState = SystemState::new(&mut world); + let mut commands = system_state.get_mut(&mut world); + + let entity = commands.spawn_empty().id(); + + let boxed_reflect_component_a = Box::new(ComponentA(916)) as Box; + + commands + .entity(entity) + .insert_reflect(boxed_reflect_component_a); + system_state.apply(&mut world); + + assert_eq!( + world.entity(entity).get::(), + Some(&ComponentA(916)) + ); + } + + #[test] + fn insert_reflected_with_registry() { + let mut world = World::new(); + + let mut type_registry = TypeRegistryResource { + type_registry: TypeRegistry::new(), + }; + + type_registry.type_registry.register::(); + type_registry + .type_registry + .register_type_data::(); + world.insert_resource(type_registry); + + let mut system_state: SystemState = SystemState::new(&mut world); + let mut commands = system_state.get_mut(&mut world); + + let entity = commands.spawn_empty().id(); + + let boxed_reflect_component_a = Box::new(ComponentA(916)) as Box; + + commands + .entity(entity) + .insert_reflect_with_registry::(boxed_reflect_component_a); + system_state.apply(&mut world); + + assert_eq!( + world.entity(entity).get::(), + Some(&ComponentA(916)) + ); + } + + #[test] + fn remove_reflected() { + let mut world = World::new(); + + let type_registry = AppTypeRegistry::default(); + { + let mut registry = type_registry.write(); + registry.register::(); + registry.register_type_data::(); + } + world.insert_resource(type_registry); + + let mut system_state: SystemState = SystemState::new(&mut world); + let mut commands = system_state.get_mut(&mut world); + + let entity = commands.spawn(ComponentA(0)).id(); + + let boxed_reflect_component_a = Box::new(ComponentA(916)) as Box; + + commands + .entity(entity) + .remove_reflect(boxed_reflect_component_a.type_name().to_owned()); + system_state.apply(&mut world); + + assert_eq!(world.entity(entity).get::(), None); + } + + #[test] + fn remove_reflected_with_registry() { + let mut world = World::new(); + + let mut type_registry = TypeRegistryResource { + type_registry: TypeRegistry::new(), + }; + + type_registry.type_registry.register::(); + type_registry + .type_registry + .register_type_data::(); + world.insert_resource(type_registry); + + let mut system_state: SystemState = SystemState::new(&mut world); + let mut commands = system_state.get_mut(&mut world); + + let entity = commands.spawn(ComponentA(0)).id(); + + let boxed_reflect_component_a = Box::new(ComponentA(916)) as Box; + + commands + .entity(entity) + .remove_reflect_with_registry::( + boxed_reflect_component_a.type_name().to_owned(), + ); + system_state.apply(&mut world); + + assert_eq!(world.entity(entity).get::(), None); + } +} diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index 1c7da802becc8..7dd481840ac86 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -8,11 +8,13 @@ use bevy_reflect::{impl_reflect_value, ReflectDeserialize, ReflectSerialize, Typ mod bundle; mod component; +mod entity_commands; mod map_entities; mod resource; pub use bundle::{ReflectBundle, ReflectBundleFns}; pub use component::{ReflectComponent, ReflectComponentFns}; +pub use entity_commands::ReflectCommandExt; pub use map_entities::ReflectMapEntities; pub use resource::{ReflectResource, ReflectResourceFns}; diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index ed6677ea9f7e7..3c404a551c97e 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -642,8 +642,8 @@ impl Command for WithEntity { /// A list of commands that will be run to modify an [entity](crate::entity). pub struct EntityCommands<'w, 's, 'a> { - entity: Entity, - commands: &'a mut Commands<'w, 's>, + pub(crate) entity: Entity, + pub(crate) commands: &'a mut Commands<'w, 's>, } impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { From a8dc8350c657817fd10192e9ddcbc84a9de3e5c8 Mon Sep 17 00:00:00 2001 From: DevinLeamy Date: Mon, 28 Aug 2023 14:54:45 -0400 Subject: [PATCH 37/58] Add configure_schedules to App and Schedules to apply `ScheduleBuildSettings` to all schedules (#9514) # Objective - Fixes: #9508 - Fixes: #9526 ## Solution - Adds ```rust fn configure_schedules(&mut self, schedule_build_settings: ScheduleBuildSettings) ``` to `Schedules`, and `App` to simplify applying `ScheduleBuildSettings` to all schedules. --- ## Migration Guide - No breaking changes. - Adds `Schedule::get_build_settings()` getter for the schedule's `ScheduleBuildSettings`. - Can replaced manual configuration of all schedules: ```rust // Old for (_, schedule) in app.world.resource_mut::().iter_mut() { schedule.set_build_settings(build_settings); } // New app.configure_schedules(build_settings); ``` --- crates/bevy_app/src/app.rs | 13 ++++++++++++- crates/bevy_ecs/src/schedule/schedule.rs | 12 ++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 2bc076d205f12..eadbdaad13f29 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -5,7 +5,7 @@ use bevy_ecs::{ schedule::{ apply_state_transition, common_conditions::run_once as run_once_condition, run_enter_schedule, BoxedScheduleLabel, IntoSystemConfigs, IntoSystemSetConfigs, - ScheduleLabel, + ScheduleBuildSettings, ScheduleLabel, }, }; use bevy_utils::{tracing::debug, HashMap, HashSet}; @@ -850,6 +850,17 @@ impl App { self } + + /// Applies the provided [`ScheduleBuildSettings`] to all schedules. + pub fn configure_schedules( + &mut self, + schedule_build_settings: ScheduleBuildSettings, + ) -> &mut Self { + self.world + .resource_mut::() + .configure_schedules(schedule_build_settings); + self + } } fn run_once(mut app: App) { diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 81bc44f23a7ee..b45504b77998f 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -103,6 +103,13 @@ impl Schedules { schedule.check_change_ticks(change_tick); } } + + /// Applies the provided [`ScheduleBuildSettings`] to all schedules. + pub fn configure_schedules(&mut self, schedule_build_settings: ScheduleBuildSettings) { + for (_, schedule) in self.inner.iter_mut() { + schedule.set_build_settings(schedule_build_settings.clone()); + } + } } fn make_executor(kind: ExecutorKind) -> Box { @@ -200,6 +207,11 @@ impl Schedule { self } + /// Returns the schedule's current `ScheduleBuildSettings`. + pub fn get_build_settings(&self) -> ScheduleBuildSettings { + self.graph.settings.clone() + } + /// Returns the schedule's current execution strategy. pub fn get_executor_kind(&self) -> ExecutorKind { self.executor.kind() From 05b7f60ae528a6ac3fcc5beee1b2588bdadfec11 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Mon, 28 Aug 2023 19:55:35 +0100 Subject: [PATCH 38/58] UI node bundle comment fix (#9404) # Objective Doc comment for the `global_transform` field in `NodeBundle` says: ``` /// This field is automatically managed by the UI layout system. ``` The `GlobalTransform` component is the thing being managed, not the `global_transform` field, and the `TransformPropagate` systems do the managing, not the UI layout system. --- crates/bevy_ui/src/node_bundles.rs | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 84f7940fe1734..f43f4642d6b7d 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -36,12 +36,12 @@ pub struct NodeBundle { pub focus_policy: FocusPolicy, /// The transform of the node /// - /// This field is automatically managed by the UI layout system. + /// This component is automatically managed by the UI layout system. /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component. pub transform: Transform, /// The global transform of the node /// - /// This field is automatically managed by the UI layout system. + /// This component is automatically updated by the [`TransformPropagate`](`bevy_transform::TransformSystem::TransformPropagate`) systems. /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component. pub global_transform: GlobalTransform, /// Describes the visibility properties of the node @@ -88,19 +88,18 @@ pub struct ImageBundle { pub image: UiImage, /// The size of the image in pixels /// - /// This field is set automatically + /// This component is set automatically pub image_size: UiImageSize, /// Whether this node should block interaction with lower nodes pub focus_policy: FocusPolicy, /// The transform of the node /// - /// This field is automatically managed by the UI layout system. + /// This component is automatically managed by the UI layout system. /// To alter the position of the `ImageBundle`, use the properties of the [`Style`] component. pub transform: Transform, /// The global transform of the node /// - /// This field is automatically managed by the UI layout system. - /// To alter the position of the `ImageBundle`, use the properties of the [`Style`] component. + /// This component is automatically updated by the [`TransformPropagate`](`bevy_transform::TransformSystem::TransformPropagate`) systems. pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, @@ -132,17 +131,16 @@ pub struct AtlasImageBundle { pub focus_policy: FocusPolicy, /// The size of the image in pixels /// - /// This field is set automatically + /// This component is set automatically pub image_size: UiImageSize, /// The transform of the node /// - /// This field is automatically managed by the UI layout system. + /// This component is automatically managed by the UI layout system. /// To alter the position of the `AtlasImageBundle`, use the properties of the [`Style`] component. pub transform: Transform, /// The global transform of the node /// - /// This field is automatically managed by the UI layout system. - /// To alter the position of the `AtlasImageBundle`, use the properties of the [`Style`] component. + /// This component is automatically updated by the [`TransformPropagate`](`bevy_transform::TransformSystem::TransformPropagate`) systems. pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, @@ -173,13 +171,12 @@ pub struct TextBundle { pub focus_policy: FocusPolicy, /// The transform of the node /// - /// This field is automatically managed by the UI layout system. + /// This component is automatically managed by the UI layout system. /// To alter the position of the `TextBundle`, use the properties of the [`Style`] component. pub transform: Transform, /// The global transform of the node /// - /// This field is automatically managed by the UI layout system. - /// To alter the position of the `TextBundle`, use the properties of the [`Style`] component. + /// This component is automatically updated by the [`TransformPropagate`](`bevy_transform::TransformSystem::TransformPropagate`) systems. pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, @@ -285,13 +282,12 @@ pub struct ButtonBundle { pub image: UiImage, /// The transform of the node /// - /// This field is automatically managed by the UI layout system. + /// This component is automatically managed by the UI layout system. /// To alter the position of the `ButtonBundle`, use the properties of the [`Style`] component. pub transform: Transform, /// The global transform of the node /// - /// This field is automatically managed by the UI layout system. - /// To alter the position of the `ButtonBundle`, use the properties of the [`Style`] component. + /// This component is automatically updated by the [`TransformPropagate`](`bevy_transform::TransformSystem::TransformPropagate`) systems. pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, From c440de06f1387dfaee43e15a559b312a27b44283 Mon Sep 17 00:00:00 2001 From: Joseph <21144246+JoJoJet@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:55:59 -0700 Subject: [PATCH 39/58] Add a variant of `Events::update` that returns the removed events (#9542) # Objective Every frame, `Events::update` gets called, which clears out any old events from the buffer. There should be a way of taking ownership of these old events instead of throwing them away. My use-case is dumping old events into a debug menu so they can be inspected later. One potential workaround is to just have a system that clones any incoming events and stores them in a list -- however, this requires the events to implement `Clone`. ## Solution Add `Events::update_drain`, which returns an iterator of the events that were removed from the buffer. --- crates/bevy_ecs/src/event.rs | 53 +++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 9693bc18fa2d8..25ae9ebb7275e 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -228,14 +228,27 @@ impl Events { /// Swaps the event buffers and clears the oldest event buffer. In general, this should be /// called once per frame/update. + /// + /// If you need access to the events that were removed, consider using [`Events::update_drain`]. pub fn update(&mut self) { + let _ = self.update_drain(); + } + + /// Swaps the event buffers and drains the oldest event buffer, returning an iterator + /// of all events that were removed. In general, this should be called once per frame/update. + /// + /// If you do not need to take ownership of the removed events, use [`Events::update`] instead. + #[must_use = "If you do not need the returned events, call .update() instead."] + pub fn update_drain(&mut self) -> impl Iterator + '_ { std::mem::swap(&mut self.events_a, &mut self.events_b); - self.events_b.clear(); + let iter = self.events_b.events.drain(..); self.events_b.start_event_count = self.event_count; debug_assert_eq!( self.events_a.start_event_count + self.events_a.len(), self.events_b.start_event_count ); + + iter.map(|e| e.event) } /// A system that calls [`Events::update`] once per frame. @@ -725,7 +738,7 @@ impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> { #[cfg(test)] mod tests { - use crate::{prelude::World, system::SystemState}; + use crate::system::assert_is_read_only_system; use super::*; @@ -982,6 +995,31 @@ mod tests { assert!(is_empty, "EventReader should be empty"); } + #[test] + fn test_update_drain() { + let mut events = Events::::default(); + let mut reader = events.get_reader(); + + events.send(TestEvent { i: 0 }); + events.send(TestEvent { i: 1 }); + assert_eq!(reader.iter(&events).count(), 2); + + let mut old_events = Vec::from_iter(events.update_drain()); + assert!(old_events.is_empty()); + + events.send(TestEvent { i: 2 }); + assert_eq!(reader.iter(&events).count(), 1); + + old_events.extend(events.update_drain()); + assert_eq!(old_events.len(), 2); + + old_events.extend(events.update_drain()); + assert_eq!( + old_events, + &[TestEvent { i: 0 }, TestEvent { i: 1 }, TestEvent { i: 2 }] + ); + } + #[allow(clippy::iter_nth_zero)] #[test] fn test_event_iter_nth() { @@ -1053,13 +1091,8 @@ mod tests { #[test] fn ensure_reader_readonly() { - fn read_for() { - let mut world = World::new(); - world.init_resource::>(); - let mut state = SystemState::>::new(&mut world); - // This can only work if EventReader only reads the world - let _reader = state.get(&world); - } - read_for::(); + fn reader_system(_: EventReader) {} + + assert_is_read_only_system(reader_system); } } From 33fdc5f5db73c4d1f78459a8160cbf93b7370eb3 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 28 Aug 2023 13:44:48 -0700 Subject: [PATCH 40/58] Move schedule name into `Schedule` (#9600) # Objective - Move schedule name into `Schedule` to allow the schedule name to be used for errors and tracing in Schedule methods - Fixes #9510 ## Solution - Move label onto `Schedule` and adjust api's on `World` and `Schedule` to not pass explicit label where it makes sense to. - add name to errors and tracing. - `Schedule::new` now takes a label so either add the label or use `Schedule::default` which uses a default label. `default` is mostly used in doc examples and tests. --- ## Changelog - move label onto `Schedule` to improve error message and logging for schedules. ## Migration Guide `Schedule::new` and `App::add_schedule` ```rust // old let schedule = Schedule::new(); app.add_schedule(MyLabel, schedule); // new let schedule = Schedule::new(MyLabel); app.add_schedule(schedule); ``` if you aren't using a label and are using the schedule struct directly you can use the default constructor. ```rust // old let schedule = Schedule::new(); schedule.run(world); // new let schedule = Schedule::default(); schedule.run(world); ``` `Schedules:insert` ```rust // old let schedule = Schedule::new(); schedules.insert(MyLabel, schedule); // new let schedule = Schedule::new(MyLabel); schedules.insert(schedule); ``` `World::add_schedule` ```rust // old let schedule = Schedule::new(); world.add_schedule(MyLabel, schedule); // new let schedule = Schedule::new(MyLabel); world.add_schedule(schedule); ``` --- .../bevy_ecs/components/archetype_updates.rs | 2 +- benches/benches/bevy_ecs/empty_archetypes.rs | 2 +- .../bevy_ecs/scheduling/run_condition.rs | 8 +-- .../bevy_ecs/scheduling/running_systems.rs | 8 +-- .../benches/bevy_ecs/scheduling/schedule.rs | 2 +- crates/bevy_app/src/app.rs | 20 +++---- crates/bevy_app/src/main_schedule.rs | 8 +-- crates/bevy_ecs/src/change_detection.rs | 4 +- crates/bevy_ecs/src/event.rs | 2 +- crates/bevy_ecs/src/query/mod.rs | 2 +- crates/bevy_ecs/src/schedule/condition.rs | 54 +++++++++---------- crates/bevy_ecs/src/schedule/config.rs | 4 +- crates/bevy_ecs/src/schedule/mod.rs | 28 +++++----- crates/bevy_ecs/src/schedule/schedule.rs | 44 ++++++++++----- crates/bevy_ecs/src/schedule/set.rs | 4 +- crates/bevy_ecs/src/system/combinator.rs | 2 +- crates/bevy_ecs/src/system/commands/mod.rs | 4 +- crates/bevy_ecs/src/system/mod.rs | 6 +-- crates/bevy_ecs/src/system/system_param.rs | 6 +-- crates/bevy_ecs/src/world/mod.rs | 19 +++---- crates/bevy_render/src/lib.rs | 8 +-- crates/bevy_time/src/time.rs | 2 +- crates/bevy_transform/src/systems.rs | 8 +-- 23 files changed, 131 insertions(+), 116 deletions(-) diff --git a/benches/benches/bevy_ecs/components/archetype_updates.rs b/benches/benches/bevy_ecs/components/archetype_updates.rs index 30f8ca6fccb97..ec7703a0b1d49 100644 --- a/benches/benches/bevy_ecs/components/archetype_updates.rs +++ b/benches/benches/bevy_ecs/components/archetype_updates.rs @@ -7,7 +7,7 @@ struct A(f32); fn setup(system_count: usize) -> (World, Schedule) { let mut world = World::new(); fn empty() {} - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); for _ in 0..system_count { schedule.add_systems(empty); } diff --git a/benches/benches/bevy_ecs/empty_archetypes.rs b/benches/benches/bevy_ecs/empty_archetypes.rs index 315a2d2ce2b68..d6521303f6fc0 100644 --- a/benches/benches/bevy_ecs/empty_archetypes.rs +++ b/benches/benches/bevy_ecs/empty_archetypes.rs @@ -77,7 +77,7 @@ fn par_for_each( fn setup(parallel: bool, setup: impl FnOnce(&mut Schedule)) -> (World, Schedule) { let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); if parallel { world.insert_resource(ComputeTaskPool(TaskPool::default())); } diff --git a/benches/benches/bevy_ecs/scheduling/run_condition.rs b/benches/benches/bevy_ecs/scheduling/run_condition.rs index a18bdd8c97f40..7f0100633d8f5 100644 --- a/benches/benches/bevy_ecs/scheduling/run_condition.rs +++ b/benches/benches/bevy_ecs/scheduling/run_condition.rs @@ -18,7 +18,7 @@ pub fn run_condition_yes(criterion: &mut Criterion) { group.measurement_time(std::time::Duration::from_secs(3)); fn empty() {} for amount in 0..21 { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems(empty.run_if(yes)); for _ in 0..amount { schedule.add_systems((empty, empty, empty, empty, empty).distributive_run_if(yes)); @@ -41,7 +41,7 @@ pub fn run_condition_no(criterion: &mut Criterion) { group.measurement_time(std::time::Duration::from_secs(3)); fn empty() {} for amount in 0..21 { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems(empty.run_if(no)); for _ in 0..amount { schedule.add_systems((empty, empty, empty, empty, empty).distributive_run_if(no)); @@ -71,7 +71,7 @@ pub fn run_condition_yes_with_query(criterion: &mut Criterion) { query.single().0 } for amount in 0..21 { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems(empty.run_if(yes_with_query)); for _ in 0..amount { schedule.add_systems( @@ -100,7 +100,7 @@ pub fn run_condition_yes_with_resource(criterion: &mut Criterion) { res.0 } for amount in 0..21 { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems(empty.run_if(yes_with_resource)); for _ in 0..amount { schedule.add_systems( diff --git a/benches/benches/bevy_ecs/scheduling/running_systems.rs b/benches/benches/bevy_ecs/scheduling/running_systems.rs index 151b96d4a6af4..00c5b3ec0ee27 100644 --- a/benches/benches/bevy_ecs/scheduling/running_systems.rs +++ b/benches/benches/bevy_ecs/scheduling/running_systems.rs @@ -21,7 +21,7 @@ pub fn empty_systems(criterion: &mut Criterion) { group.measurement_time(std::time::Duration::from_secs(3)); fn empty() {} for amount in 0..5 { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); for _ in 0..amount { schedule.add_systems(empty); } @@ -33,7 +33,7 @@ pub fn empty_systems(criterion: &mut Criterion) { }); } for amount in 1..21 { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); for _ in 0..amount { schedule.add_systems((empty, empty, empty, empty, empty)); } @@ -73,7 +73,7 @@ pub fn busy_systems(criterion: &mut Criterion) { world.spawn_batch((0..ENTITY_BUNCH).map(|_| (A(0.0), B(0.0), C(0.0), D(0.0)))); world.spawn_batch((0..ENTITY_BUNCH).map(|_| (A(0.0), B(0.0), C(0.0), E(0.0)))); for system_amount in 0..5 { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems((ab, cd, ce)); for _ in 0..system_amount { schedule.add_systems((ab, cd, ce)); @@ -124,7 +124,7 @@ pub fn contrived(criterion: &mut Criterion) { world.spawn_batch((0..ENTITY_BUNCH).map(|_| (A(0.0), B(0.0)))); world.spawn_batch((0..ENTITY_BUNCH).map(|_| (C(0.0), D(0.0)))); for system_amount in 0..5 { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems((s_0, s_1, s_2)); for _ in 0..system_amount { schedule.add_systems((s_0, s_1, s_2)); diff --git a/benches/benches/bevy_ecs/scheduling/schedule.rs b/benches/benches/bevy_ecs/scheduling/schedule.rs index e670c23d74b37..99f8b20f2bc05 100644 --- a/benches/benches/bevy_ecs/scheduling/schedule.rs +++ b/benches/benches/bevy_ecs/scheduling/schedule.rs @@ -46,7 +46,7 @@ pub fn schedule(c: &mut Criterion) { world.spawn_batch((0..10000).map(|_| (A(0.0), B(0.0), C(0.0), E(0.0)))); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems((ab, cd, ce)); schedule.run(&mut world); diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index eadbdaad13f29..426c045dce28e 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -384,9 +384,9 @@ impl App { if let Some(schedule) = schedules.get_mut(&schedule) { schedule.add_systems(systems); } else { - let mut new_schedule = Schedule::new(); + let mut new_schedule = Schedule::new(schedule); new_schedule.add_systems(systems); - schedules.insert(schedule, new_schedule); + schedules.insert(new_schedule); } self @@ -403,9 +403,9 @@ impl App { if let Some(schedule) = schedules.get_mut(&schedule) { schedule.configure_set(set); } else { - let mut new_schedule = Schedule::new(); + let mut new_schedule = Schedule::new(schedule); new_schedule.configure_set(set); - schedules.insert(schedule, new_schedule); + schedules.insert(new_schedule); } self } @@ -421,9 +421,9 @@ impl App { if let Some(schedule) = schedules.get_mut(&schedule) { schedule.configure_sets(sets); } else { - let mut new_schedule = Schedule::new(); + let mut new_schedule = Schedule::new(schedule); new_schedule.configure_sets(sets); - schedules.insert(schedule, new_schedule); + schedules.insert(new_schedule); } self } @@ -798,9 +798,9 @@ impl App { /// # Warning /// This method will overwrite any existing schedule at that label. /// To avoid this behavior, use the `init_schedule` method instead. - pub fn add_schedule(&mut self, label: impl ScheduleLabel, schedule: Schedule) -> &mut Self { + pub fn add_schedule(&mut self, schedule: Schedule) -> &mut Self { let mut schedules = self.world.resource_mut::(); - schedules.insert(label, schedule); + schedules.insert(schedule); self } @@ -811,7 +811,7 @@ impl App { pub fn init_schedule(&mut self, label: impl ScheduleLabel) -> &mut Self { let mut schedules = self.world.resource_mut::(); if !schedules.contains(&label) { - schedules.insert(label, Schedule::new()); + schedules.insert(Schedule::new(label)); } self } @@ -841,7 +841,7 @@ impl App { let mut schedules = self.world.resource_mut::(); if schedules.get(&label).is_none() { - schedules.insert(label.dyn_clone(), Schedule::new()); + schedules.insert(Schedule::new(label.dyn_clone())); } let schedule = schedules.get_mut(&label).unwrap(); diff --git a/crates/bevy_app/src/main_schedule.rs b/crates/bevy_app/src/main_schedule.rs index f72ac80fb00cc..004aba0a6fc0a 100644 --- a/crates/bevy_app/src/main_schedule.rs +++ b/crates/bevy_app/src/main_schedule.rs @@ -161,13 +161,13 @@ pub struct MainSchedulePlugin; impl Plugin for MainSchedulePlugin { fn build(&self, app: &mut App) { // simple "facilitator" schedules benefit from simpler single threaded scheduling - let mut main_schedule = Schedule::new(); + let mut main_schedule = Schedule::new(Main); main_schedule.set_executor_kind(ExecutorKind::SingleThreaded); - let mut fixed_update_loop_schedule = Schedule::new(); + let mut fixed_update_loop_schedule = Schedule::new(RunFixedUpdateLoop); fixed_update_loop_schedule.set_executor_kind(ExecutorKind::SingleThreaded); - app.add_schedule(Main, main_schedule) - .add_schedule(RunFixedUpdateLoop, fixed_update_loop_schedule) + app.add_schedule(main_schedule) + .add_schedule(fixed_update_loop_schedule) .init_resource::() .add_systems(Main, Main::run_main); } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index ff5f9bf3958c1..a00a5cd6bb66b 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -149,7 +149,7 @@ pub trait DetectChangesMut: DetectChanges { /// # score_changed.initialize(&mut world); /// # score_changed.run((), &mut world); /// # - /// # let mut schedule = Schedule::new(); + /// # let mut schedule = Schedule::default(); /// # schedule.add_systems(reset_score); /// # /// # // first time `reset_score` runs, the score is changed. @@ -214,7 +214,7 @@ pub trait DetectChangesMut: DetectChanges { /// # score_changed_event.initialize(&mut world); /// # score_changed_event.run((), &mut world); /// # - /// # let mut schedule = Schedule::new(); + /// # let mut schedule = Schedule::default(); /// # schedule.add_systems(reset_score); /// # /// # // first time `reset_score` runs, the score is changed. diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 25ae9ebb7275e..7f0887eb4cc90 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -1034,7 +1034,7 @@ mod tests { world.send_event(TestEvent { i: 3 }); world.send_event(TestEvent { i: 4 }); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems(|mut events: EventReader| { let mut iter = events.iter(); diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index 0c4106d189ff9..863122d90a8b6 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -789,7 +789,7 @@ mod tests { } } - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems((propagate_system, modify_system).chain()); schedule.run(&mut world); world.clear_trackers(); diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 8186819ccdb63..f8ad50a5bbf71 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -26,7 +26,7 @@ pub type BoxedCondition = Box>; /// /// # #[derive(Resource)] struct DidRun(bool); /// # fn my_system(mut did_run: ResMut) { did_run.0 = true; } -/// # let mut schedule = Schedule::new(); +/// # let mut schedule = Schedule::default(); /// schedule.add_systems(my_system.run_if(every_other_time())); /// # let mut world = World::new(); /// # world.insert_resource(DidRun(false)); @@ -46,13 +46,13 @@ pub type BoxedCondition = Box>; /// } /// /// # fn always_true() -> bool { true } -/// # let mut schedule = Schedule::new(); +/// # let mut app = Schedule::default(); /// # #[derive(Resource)] struct DidRun(bool); /// # fn my_system(mut did_run: ResMut) { did_run.0 = true; } -/// schedule.add_systems(my_system.run_if(always_true.pipe(identity()))); +/// app.add_systems(my_system.run_if(always_true.pipe(identity()))); /// # let mut world = World::new(); /// # world.insert_resource(DidRun(false)); -/// # schedule.run(&mut world); +/// # app.run(&mut world); /// # assert!(world.resource::().0); pub trait Condition: sealed::Condition { /// Returns a new run condition that only returns `true` @@ -69,7 +69,7 @@ pub trait Condition: sealed::Condition { /// #[derive(Resource, PartialEq)] /// struct R(u32); /// - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # fn my_system() {} /// app.add_systems( @@ -86,7 +86,7 @@ pub trait Condition: sealed::Condition { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, PartialEq)] /// # struct R(u32); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # fn my_system() {} /// app.add_systems( @@ -123,7 +123,7 @@ pub trait Condition: sealed::Condition { /// #[derive(Resource, PartialEq)] /// struct B(u32); /// - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # #[derive(Resource)] struct C(bool); /// # fn my_system(mut c: ResMut) { c.0 = true; } @@ -197,7 +197,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// app.add_systems( @@ -238,7 +238,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// app.add_systems( /// // `resource_exists` will only return true if the given resource exists in the world @@ -277,7 +277,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default, PartialEq)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// app.add_systems( @@ -315,7 +315,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default, PartialEq)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// app.add_systems( /// // `resource_exists_and_equals` will only return true @@ -358,7 +358,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// app.add_systems( /// // `resource_added` will only return true if the @@ -408,7 +408,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// app.add_systems( @@ -462,7 +462,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// app.add_systems( /// // `resource_exists_and_changed` will only return true if the @@ -523,7 +523,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// app.add_systems( @@ -593,7 +593,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// app.add_systems( @@ -648,7 +648,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] @@ -695,7 +695,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] @@ -747,7 +747,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] @@ -813,7 +813,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// #[derive(States, Clone, Copy, Default, Eq, PartialEq, Hash, Debug)] @@ -863,7 +863,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// # world.init_resource::>(); @@ -907,7 +907,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// app.add_systems( @@ -954,7 +954,7 @@ pub mod common_conditions { /// # use bevy_ecs::prelude::*; /// # #[derive(Resource, Default)] /// # struct Counter(u8); - /// # let mut app = Schedule::new(); + /// # let mut app = Schedule::default(); /// # let mut world = World::new(); /// # world.init_resource::(); /// app.add_systems( @@ -1084,7 +1084,7 @@ mod tests { fn run_condition() { let mut world = World::new(); world.init_resource::(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); // Run every other cycle schedule.add_systems(increment_counter.run_if(every_other_time)); @@ -1111,7 +1111,7 @@ mod tests { fn run_condition_combinators() { let mut world = World::new(); world.init_resource::(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); // Always run schedule.add_systems(increment_counter.run_if(every_other_time.or_else(|| true))); @@ -1128,7 +1128,7 @@ mod tests { fn multiple_run_conditions() { let mut world = World::new(); world.init_resource::(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); // Run every other cycle schedule.add_systems(increment_counter.run_if(every_other_time).run_if(|| true)); @@ -1146,7 +1146,7 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); // This should never run, if multiple run conditions worked // like an OR condition then it would always run diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index 9817feec6d4ca..5ef4428ecc948 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -225,7 +225,7 @@ where /// /// ``` /// # use bevy_ecs::prelude::*; - /// # let mut schedule = Schedule::new(); + /// # let mut schedule = Schedule::default(); /// # fn a() {} /// # fn b() {} /// # fn condition() -> bool { true } @@ -258,7 +258,7 @@ where /// /// ``` /// # use bevy_ecs::prelude::*; - /// # let mut schedule = Schedule::new(); + /// # let mut schedule = Schedule::default(); /// # fn a() {} /// # fn b() {} /// # fn condition() -> bool { true } diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 1982ce03dae3d..faba4851e95c9 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -180,7 +180,7 @@ mod tests { #[test] fn add_systems_correct_order() { let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); world.init_resource::(); @@ -201,7 +201,7 @@ mod tests { #[test] fn add_systems_correct_order_nested() { let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); world.init_resource::(); @@ -528,14 +528,14 @@ mod tests { #[test] #[should_panic] fn dependency_loop() { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.configure_set(TestSet::X.after(TestSet::X)); } #[test] fn dependency_cycle() { let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.configure_set(TestSet::A.after(TestSet::B)); schedule.configure_set(TestSet::B.after(TestSet::A)); @@ -550,7 +550,7 @@ mod tests { fn bar() {} let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems((foo.after(bar), bar.after(foo))); let result = schedule.initialize(&mut world); @@ -563,14 +563,14 @@ mod tests { #[test] #[should_panic] fn hierarchy_loop() { - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.configure_set(TestSet::X.in_set(TestSet::X)); } #[test] fn hierarchy_cycle() { let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.configure_set(TestSet::A.in_set(TestSet::B)); schedule.configure_set(TestSet::B.in_set(TestSet::A)); @@ -586,7 +586,7 @@ mod tests { fn bar() {} let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); // Schedule `bar` to run after `foo`. schedule.add_systems((foo, bar.after(foo))); @@ -607,7 +607,7 @@ mod tests { )); // same goes for `ambiguous_with` - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems(foo); schedule.add_systems(bar.ambiguous_with(foo)); let result = schedule.initialize(&mut world); @@ -624,14 +624,14 @@ mod tests { #[should_panic] fn configure_system_type_set() { fn foo() {} - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.configure_set(foo.into_system_set()); } #[test] fn hierarchy_redundancy() { let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.set_build_settings(ScheduleBuildSettings { hierarchy_detection: LogLevel::Error, @@ -658,7 +658,7 @@ mod tests { #[test] fn cross_dependency() { let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); // Add `B` and give it both kinds of relationships with `A`. schedule.configure_set(TestSet::B.in_set(TestSet::A)); @@ -673,7 +673,7 @@ mod tests { #[test] fn sets_have_order_but_intersect() { let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); fn foo() {} @@ -704,7 +704,7 @@ mod tests { fn res_mut(_x: ResMut) {} let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.set_build_settings(ScheduleBuildSettings { ambiguity_detection: LogLevel::Error, diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index b45504b77998f..33d5c5647f936 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -41,8 +41,8 @@ impl Schedules { /// /// If the map already had an entry for `label`, `schedule` is inserted, /// and the old schedule is returned. Otherwise, `None` is returned. - pub fn insert(&mut self, label: impl ScheduleLabel, schedule: Schedule) -> Option { - let label = label.dyn_clone(); + pub fn insert(&mut self, schedule: Schedule) -> Option { + let label = schedule.name.dyn_clone(); self.inner.insert(label, schedule) } @@ -158,22 +158,31 @@ fn make_executor(kind: ExecutorKind) -> Box { /// } /// ``` pub struct Schedule { + name: BoxedScheduleLabel, graph: ScheduleGraph, executable: SystemSchedule, executor: Box, executor_initialized: bool, } +#[derive(ScheduleLabel, Hash, PartialEq, Eq, Debug, Clone)] +struct DefaultSchedule; + +/// Creates a schedule with a default label. Only use in situations where +/// where you don't care about the [`ScheduleLabel`]. Inserting a default schedule +/// into the world risks overwriting another schedule. For most situations you should use +/// [`Schedule::new`]. impl Default for Schedule { fn default() -> Self { - Self::new() + Self::new(DefaultSchedule) } } impl Schedule { /// Constructs an empty `Schedule`. - pub fn new() -> Self { + pub fn new(label: impl ScheduleLabel) -> Self { Self { + name: label.dyn_clone(), graph: ScheduleGraph::new(), executable: SystemSchedule::new(), executor: make_executor(ExecutorKind::default()), @@ -237,8 +246,12 @@ impl Schedule { /// Runs all systems in this schedule on the `world`, using its current execution strategy. pub fn run(&mut self, world: &mut World) { + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!("schedule", name = ?self.name).entered(); + world.check_change_ticks(); - self.initialize(world).unwrap_or_else(|e| panic!("{e}")); + self.initialize(world) + .unwrap_or_else(|e| panic!("Error when intializing schedule {:?}: {e}", self.name)); self.executor.run(&mut self.executable, world); } @@ -250,7 +263,7 @@ impl Schedule { if self.graph.changed { self.graph.initialize(world); self.graph - .update_schedule(&mut self.executable, world.components())?; + .update_schedule(&mut self.executable, world.components(), &self.name)?; self.graph.changed = false; self.executor_initialized = false; } @@ -882,13 +895,14 @@ impl ScheduleGraph { pub fn build_schedule( &mut self, components: &Components, + schedule_label: &BoxedScheduleLabel, ) -> Result { // check hierarchy for cycles self.hierarchy.topsort = self.topsort_graph(&self.hierarchy.graph, ReportCycles::Hierarchy)?; let hier_results = check_graph(&self.hierarchy.graph, &self.hierarchy.topsort); - self.optionally_check_hierarchy_conflicts(&hier_results.transitive_edges)?; + self.optionally_check_hierarchy_conflicts(&hier_results.transitive_edges, schedule_label)?; // remove redundant edges self.hierarchy.graph = hier_results.transitive_reduction; @@ -932,7 +946,7 @@ impl ScheduleGraph { // check for conflicts let conflicting_systems = self.get_conflicting_systems(&flat_results.disconnected, &ambiguous_with_flattened); - self.optionally_check_conflicts(&conflicting_systems, components)?; + self.optionally_check_conflicts(&conflicting_systems, components, schedule_label)?; self.conflicting_systems = conflicting_systems; // build the schedule @@ -1182,6 +1196,7 @@ impl ScheduleGraph { &mut self, schedule: &mut SystemSchedule, components: &Components, + schedule_label: &BoxedScheduleLabel, ) -> Result<(), ScheduleBuildError> { if !self.uninit.is_empty() { return Err(ScheduleBuildError::Uninitialized); @@ -1206,7 +1221,7 @@ impl ScheduleGraph { self.system_set_conditions[id.index()] = conditions; } - *schedule = self.build_schedule(components)?; + *schedule = self.build_schedule(components, schedule_label)?; // move systems into new schedule for &id in &schedule.system_ids { @@ -1297,6 +1312,7 @@ impl ScheduleGraph { fn optionally_check_hierarchy_conflicts( &self, transitive_edges: &[(NodeId, NodeId)], + schedule_label: &BoxedScheduleLabel, ) -> Result<(), ScheduleBuildError> { if self.settings.hierarchy_detection == LogLevel::Ignore || transitive_edges.is_empty() { return Ok(()); @@ -1306,7 +1322,10 @@ impl ScheduleGraph { match self.settings.hierarchy_detection { LogLevel::Ignore => unreachable!(), LogLevel::Warn => { - error!("{}", message); + error!( + "Schedule {schedule_label:?} has redundant edges:\n {}", + message + ); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::HierarchyRedundancy(message)), @@ -1505,6 +1524,7 @@ impl ScheduleGraph { &self, conflicts: &[(NodeId, NodeId, Vec)], components: &Components, + schedule_label: &BoxedScheduleLabel, ) -> Result<(), ScheduleBuildError> { if self.settings.ambiguity_detection == LogLevel::Ignore || conflicts.is_empty() { return Ok(()); @@ -1514,7 +1534,7 @@ impl ScheduleGraph { match self.settings.ambiguity_detection { LogLevel::Ignore => Ok(()), LogLevel::Warn => { - warn!("{}", message); + warn!("Schedule {schedule_label:?} has ambiguities.\n{}", message); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::Ambiguity(message)), @@ -1689,7 +1709,7 @@ mod tests { struct Set; let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.configure_set(Set.run_if(|| false)); schedule.add_systems( diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 1277f4b54dc2b..bdbc16ff7d905 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -198,9 +198,9 @@ mod tests { let mut world = World::new(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::new(A); schedule.add_systems(|mut flag: ResMut| flag.0 = true); - world.add_schedule(schedule, A); + world.add_schedule(schedule); let boxed: Box = Box::new(A); diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 13c2b7869303d..1d20927ef995b 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -50,7 +50,7 @@ use super::{ReadOnlySystem, System}; /// # let mut world = World::new(); /// # world.init_resource::(); /// # -/// # let mut app = Schedule::new(); +/// # let mut app = Schedule::default(); /// app.add_systems(my_system.run_if(Xor::new( /// IntoSystem::into_system(resource_equals(A(1))), /// IntoSystem::into_system(resource_equals(B(1))), diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 3c404a551c97e..927b608cd35cb 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -596,9 +596,9 @@ impl<'w, 's> Commands<'w, 's> { /// # let mut world = World::new(); /// # world.init_resource::(); /// # -/// # let mut setup_schedule = Schedule::new(); +/// # let mut setup_schedule = Schedule::default(); /// # setup_schedule.add_systems(setup); -/// # let mut assert_schedule = Schedule::new(); +/// # let mut assert_schedule = Schedule::default(); /// # assert_schedule.add_systems(assert_names); /// # /// # setup_schedule.run(&mut world); diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 051795fd566eb..fd4b3027c4b90 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -55,7 +55,7 @@ //! //! ``` //! # use bevy_ecs::prelude::*; -//! # let mut schedule = Schedule::new(); +//! # let mut schedule = Schedule::default(); //! # let mut world = World::new(); //! // Configure these systems to run in order using `chain()`. //! schedule.add_systems((print_first, print_last).chain()); @@ -170,7 +170,7 @@ pub trait IntoSystem: Sized { /// /// ``` /// # use bevy_ecs::prelude::*; - /// # let mut schedule = Schedule::new(); + /// # let mut schedule = Schedule::default(); /// // Ignores the output of a system that may fail. /// schedule.add_systems(my_system.map(std::mem::drop)); /// # let mut world = World::new(); @@ -1916,7 +1916,7 @@ mod tests { world.insert_resource(A); world.insert_resource(C(0)); - let mut sched = Schedule::new(); + let mut sched = Schedule::default(); sched.add_systems( ( (|mut res: ResMut| { diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index ef7a8252bf153..e5343002df95a 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -353,7 +353,7 @@ impl_param_set!(); /// /// ``` /// # let mut world = World::default(); -/// # let mut schedule = Schedule::new(); +/// # let mut schedule = Schedule::default(); /// # use bevy_ecs::prelude::*; /// #[derive(Resource)] /// struct MyResource { value: u32 } @@ -854,7 +854,7 @@ pub trait SystemBuffer: FromWorld + Send + 'static { /// // ... /// }); /// -/// let mut schedule = Schedule::new(); +/// let mut schedule = Schedule::default(); /// // These two systems have no conflicts and will run in parallel. /// schedule.add_systems((alert_criminal, alert_monster)); /// @@ -1735,7 +1735,7 @@ mod tests { } let mut world = World::new(); - let mut schedule = crate::schedule::Schedule::new(); + let mut schedule = crate::schedule::Schedule::default(); schedule.add_systems(non_sync_system); schedule.run(&mut world); } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 0e79b19ce8568..93217aca5ebf9 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1748,9 +1748,9 @@ impl World { /// accessing the [`Schedules`] resource. /// /// The `Schedules` resource will be initialized if it does not already exist. - pub fn add_schedule(&mut self, schedule: Schedule, label: impl ScheduleLabel) { + pub fn add_schedule(&mut self, schedule: Schedule) { let mut schedules = self.get_resource_or_insert_with(Schedules::default); - schedules.insert(label, schedule); + schedules.insert(schedule); } /// Temporarily removes the schedule associated with `label` from the world, @@ -1770,21 +1770,16 @@ impl World { f: impl FnOnce(&mut World, &mut Schedule) -> R, ) -> Result { let label = label.as_ref(); - let Some((extracted_label, mut schedule)) = self + let Some(mut schedule) = self .get_resource_mut::() - .and_then(|mut s| s.remove_entry(label)) + .and_then(|mut s| s.remove(label)) else { return Err(TryRunScheduleError(label.dyn_clone())); }; - // TODO: move this span to Schedule::run - #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!("schedule", name = ?extracted_label).entered(); let value = f(self, &mut schedule); - let old = self - .resource_mut::() - .insert(extracted_label, schedule); + let old = self.resource_mut::().insert(schedule); if old.is_some() { warn!("Schedule `{label:?}` was inserted during a call to `World::schedule_scope`: its value has been overwritten"); } @@ -1809,10 +1804,10 @@ impl World { /// # /// # let mut world = World::new(); /// # world.insert_resource(Counter(0)); - /// # let mut schedule = Schedule::new(); + /// # let mut schedule = Schedule::new(MySchedule); /// # schedule.add_systems(tick_counter); /// # world.init_resource::(); - /// # world.add_schedule(schedule, MySchedule); + /// # world.add_schedule(schedule); /// # fn tick_counter(mut counter: ResMut) { counter.0 += 1; } /// // Run the schedule five times. /// world.schedule_scope(MySchedule, |world, schedule| { diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index c467a8623726b..473f75fc3aba4 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -132,7 +132,7 @@ impl Render { pub fn base_schedule() -> Schedule { use RenderSet::*; - let mut schedule = Schedule::new(); + let mut schedule = Schedule::new(Self); // Create "stage-like" structure using buffer flushes + ordering schedule.add_systems(( @@ -295,12 +295,12 @@ impl Plugin for RenderPlugin { let mut render_app = App::empty(); render_app.main_schedule_label = Box::new(Render); - let mut extract_schedule = Schedule::new(); + let mut extract_schedule = Schedule::new(ExtractSchedule); extract_schedule.set_apply_final_deferred(false); render_app - .add_schedule(ExtractSchedule, extract_schedule) - .add_schedule(Render, Render::base_schedule()) + .add_schedule(extract_schedule) + .add_schedule(Render::base_schedule()) .init_resource::() .insert_resource(app.world.resource::().clone()) .add_systems(ExtractSchedule, PipelineCache::extract_shaders) diff --git a/crates/bevy_time/src/time.rs b/crates/bevy_time/src/time.rs index 0602e141dda16..260910c46bde8 100644 --- a/crates/bevy_time/src/time.rs +++ b/crates/bevy_time/src/time.rs @@ -124,7 +124,7 @@ impl Time { /// world.insert_resource(time); /// world.insert_resource(Health { health_value: 0.2 }); /// - /// let mut schedule = Schedule::new(); + /// let mut schedule = Schedule::default(); /// schedule.add_systems(health_system); /// /// // Simulate that 30 ms have passed diff --git a/crates/bevy_transform/src/systems.rs b/crates/bevy_transform/src/systems.rs index 48d76fa14e1ab..76ba509f6743b 100644 --- a/crates/bevy_transform/src/systems.rs +++ b/crates/bevy_transform/src/systems.rs @@ -200,7 +200,7 @@ mod test { let offset_transform = |offset| TransformBundle::from_transform(Transform::from_xyz(offset, offset, offset)); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems((sync_simple_transforms, propagate_transforms)); let mut command_queue = CommandQueue::default(); @@ -251,7 +251,7 @@ mod test { ComputeTaskPool::init(TaskPool::default); let mut world = World::default(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems((sync_simple_transforms, propagate_transforms)); // Root entity @@ -289,7 +289,7 @@ mod test { fn did_propagate_command_buffer() { let mut world = World::default(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems((sync_simple_transforms, propagate_transforms)); // Root entity @@ -329,7 +329,7 @@ mod test { ComputeTaskPool::init(TaskPool::default); let mut world = World::default(); - let mut schedule = Schedule::new(); + let mut schedule = Schedule::default(); schedule.add_systems((sync_simple_transforms, propagate_transforms)); // Add parent entities From bc8bf348186d0d3a3683614fe49102e7d115dd3f Mon Sep 17 00:00:00 2001 From: Joseph <21144246+JoJoJet@users.noreply.github.com> Date: Mon, 28 Aug 2023 16:30:59 -0700 Subject: [PATCH 41/58] Allow disjoint mutable world access via `EntityMut` (#9419) # Objective Fix #4278 Fix #5504 Fix #9422 Provide safe ways to borrow an entire entity, while allowing disjoint mutable access. `EntityRef` and `EntityMut` are not suitable for this, since they provide access to the entire world -- they are just helper types for working with `&World`/`&mut World`. This has potential uses for reflection and serialization ## Solution Remove `EntityRef::world`, which allows it to soundly be used within queries. `EntityMut` no longer supports structural world mutations, which allows multiple instances of it to exist for different entities at once. Structural world mutations are performed using the new type `EntityWorldMut`. ```rust fn disjoint_system( q2: Query<&mut A>, q1: Query>, ) { ... } let [entity1, entity2] = world.many_entities_mut([id1, id2]); *entity1.get_mut::().unwrap() = *entity2.get().unwrap(); for entity in world.iter_entities_mut() { ... } ``` --- ## Changelog - Removed `EntityRef::world`, to fix a soundness issue with queries. + Removed the ability to structurally mutate the world using `EntityMut`, which allows it to be used in queries. + Added `EntityWorldMut`, which is used to perform structural mutations that are no longer allowed using `EntityMut`. ## Migration Guide **Note for maintainers: ensure that the guide for #9604 is updated accordingly.** Removed the method `EntityRef::world`, to fix a soundness issue with queries. If you need access to `&World` while using an `EntityRef`, consider passing the world as a separate parameter. `EntityMut` can no longer perform 'structural' world mutations, such as adding or removing components, or despawning the entity. Additionally, `EntityMut::world`, `EntityMut::world_mut` , and `EntityMut::world_scope` have been removed. Instead, use the newly-added type `EntityWorldMut`, which is a helper type for working with `&mut World`. --------- Co-authored-by: Alice Cecile --- crates/bevy_ecs/src/archetype.rs | 24 +- crates/bevy_ecs/src/entity/mod.rs | 14 +- crates/bevy_ecs/src/lib.rs | 2 +- crates/bevy_ecs/src/query/access.rs | 58 ++- crates/bevy_ecs/src/query/fetch.rs | 94 +++- crates/bevy_ecs/src/reflect/bundle.rs | 33 +- crates/bevy_ecs/src/reflect/component.rs | 29 +- crates/bevy_ecs/src/system/commands/mod.rs | 9 +- crates/bevy_ecs/src/world/entity_ref.rs | 580 ++++++++++++++++----- crates/bevy_ecs/src/world/mod.rs | 319 +++++++++++- crates/bevy_hierarchy/src/child_builder.rs | 8 +- crates/bevy_hierarchy/src/hierarchy.rs | 4 +- 12 files changed, 947 insertions(+), 227 deletions(-) diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index a64023489674c..7fb308dadae7f 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -158,12 +158,12 @@ pub struct Edges { impl Edges { /// Checks the cache for the target archetype when adding a bundle to the - /// source archetype. For more information, see [`EntityMut::insert`]. + /// source archetype. For more information, see [`EntityWorldMut::insert`]. /// /// If this returns `None`, it means there has not been a transition from /// the source archetype via the provided bundle. /// - /// [`EntityMut::insert`]: crate::world::EntityMut::insert + /// [`EntityWorldMut::insert`]: crate::world::EntityWorldMut::insert #[inline] pub fn get_add_bundle(&self, bundle_id: BundleId) -> Option { self.get_add_bundle_internal(bundle_id) @@ -177,9 +177,9 @@ impl Edges { } /// Caches the target archetype when adding a bundle to the source archetype. - /// For more information, see [`EntityMut::insert`]. + /// For more information, see [`EntityWorldMut::insert`]. /// - /// [`EntityMut::insert`]: crate::world::EntityMut::insert + /// [`EntityWorldMut::insert`]: crate::world::EntityWorldMut::insert #[inline] pub(crate) fn insert_add_bundle( &mut self, @@ -197,7 +197,7 @@ impl Edges { } /// Checks the cache for the target archetype when removing a bundle to the - /// source archetype. For more information, see [`EntityMut::remove`]. + /// source archetype. For more information, see [`EntityWorldMut::remove`]. /// /// If this returns `None`, it means there has not been a transition from /// the source archetype via the provided bundle. @@ -205,16 +205,16 @@ impl Edges { /// If this returns `Some(None)`, it means that the bundle cannot be removed /// from the source archetype. /// - /// [`EntityMut::remove`]: crate::world::EntityMut::remove + /// [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove #[inline] pub fn get_remove_bundle(&self, bundle_id: BundleId) -> Option> { self.remove_bundle.get(bundle_id).cloned() } /// Caches the target archetype when removing a bundle to the source archetype. - /// For more information, see [`EntityMut::remove`]. + /// For more information, see [`EntityWorldMut::remove`]. /// - /// [`EntityMut::remove`]: crate::world::EntityMut::remove + /// [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove #[inline] pub(crate) fn insert_remove_bundle( &mut self, @@ -225,21 +225,21 @@ impl Edges { } /// Checks the cache for the target archetype when removing a bundle to the - /// source archetype. For more information, see [`EntityMut::remove`]. + /// source archetype. For more information, see [`EntityWorldMut::remove`]. /// /// If this returns `None`, it means there has not been a transition from /// the source archetype via the provided bundle. /// - /// [`EntityMut::remove`]: crate::world::EntityMut::remove + /// [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove #[inline] pub fn get_take_bundle(&self, bundle_id: BundleId) -> Option> { self.take_bundle.get(bundle_id).cloned() } /// Caches the target archetype when removing a bundle to the source archetype. - /// For more information, see [`EntityMut::take`]. + /// For more information, see [`EntityWorldMut::take`]. /// - /// [`EntityMut::take`]: crate::world::EntityMut::take + /// [`EntityWorldMut::take`]: crate::world::EntityWorldMut::take #[inline] pub(crate) fn insert_take_bundle( &mut self, diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 99dd7fe791a30..3a49c99aedcf6 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -21,8 +21,8 @@ //! |Spawn an entity with components|[`Commands::spawn`]|[`World::spawn`]| //! |Spawn an entity without components|[`Commands::spawn_empty`]|[`World::spawn_empty`]| //! |Despawn an entity|[`EntityCommands::despawn`]|[`World::despawn`]| -//! |Insert a component, bundle, or tuple of components and bundles to an entity|[`EntityCommands::insert`]|[`EntityMut::insert`]| -//! |Remove a component, bundle, or tuple of components and bundles from an entity|[`EntityCommands::remove`]|[`EntityMut::remove`]| +//! |Insert a component, bundle, or tuple of components and bundles to an entity|[`EntityCommands::insert`]|[`EntityWorldMut::insert`]| +//! |Remove a component, bundle, or tuple of components and bundles from an entity|[`EntityCommands::remove`]|[`EntityWorldMut::remove`]| //! //! [`World`]: crate::world::World //! [`Commands::spawn`]: crate::system::Commands::spawn @@ -33,8 +33,8 @@ //! [`World::spawn`]: crate::world::World::spawn //! [`World::spawn_empty`]: crate::world::World::spawn_empty //! [`World::despawn`]: crate::world::World::despawn -//! [`EntityMut::insert`]: crate::world::EntityMut::insert -//! [`EntityMut::remove`]: crate::world::EntityMut::remove +//! [`EntityWorldMut::insert`]: crate::world::EntityWorldMut::insert +//! [`EntityWorldMut::remove`]: crate::world::EntityWorldMut::remove mod map_entities; pub use map_entities::*; @@ -72,7 +72,7 @@ type IdCursor = isize; /// # Usage /// /// This data type is returned by iterating a `Query` that has `Entity` as part of its query fetch type parameter ([learn more]). -/// It can also be obtained by calling [`EntityCommands::id`] or [`EntityMut::id`]. +/// It can also be obtained by calling [`EntityCommands::id`] or [`EntityWorldMut::id`]. /// /// ``` /// # use bevy_ecs::prelude::*; @@ -84,7 +84,7 @@ type IdCursor = isize; /// } /// /// fn exclusive_system(world: &mut World) { -/// // Calling `spawn` returns `EntityMut`. +/// // Calling `spawn` returns `EntityWorldMut`. /// let entity = world.spawn(SomeComponent).id(); /// } /// # @@ -111,7 +111,7 @@ type IdCursor = isize; /// /// [learn more]: crate::system::Query#entity-id-access /// [`EntityCommands::id`]: crate::system::EntityCommands::id -/// [`EntityMut::id`]: crate::world::EntityMut::id +/// [`EntityWorldMut::id`]: crate::world::EntityWorldMut::id /// [`EntityCommands`]: crate::system::EntityCommands /// [`Query::get`]: crate::system::Query::get /// [`World`]: crate::world::World diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index c60356f4da91c..b9b8718094abe 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -52,7 +52,7 @@ pub mod prelude { Commands, Deferred, In, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, ParamSet, Query, ReadOnlySystem, Res, ResMut, Resource, System, SystemParamFunction, }, - world::{EntityRef, FromWorld, World}, + world::{EntityMut, EntityRef, EntityWorldMut, FromWorld, World}, }; } diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 41a23020ac5f4..808d31d2c48e7 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -53,9 +53,12 @@ pub struct Access { reads_and_writes: FixedBitSet, /// The exclusively-accessed elements. writes: FixedBitSet, - /// Is `true` if this has access to all elements in the collection? + /// Is `true` if this has access to all elements in the collection. /// This field is a performance optimization for `&World` (also harder to mess up for soundness). reads_all: bool, + /// Is `true` if this has mutable access to all elements in the collection. + /// If this is true, then `reads_all` must also be true. + writes_all: bool, marker: PhantomData, } @@ -68,6 +71,7 @@ impl fmt::Debug for Access { ) .field("writes", &FormattedBitSet::::new(&self.writes)) .field("reads_all", &self.reads_all) + .field("writes_all", &self.writes_all) .finish() } } @@ -83,6 +87,7 @@ impl Access { pub const fn new() -> Self { Self { reads_all: false, + writes_all: false, reads_and_writes: FixedBitSet::new(), writes: FixedBitSet::new(), marker: PhantomData, @@ -116,14 +121,19 @@ impl Access { self.reads_all || self.reads_and_writes.contains(index.sparse_set_index()) } + /// Returns `true` if this can access anything. + pub fn has_any_read(&self) -> bool { + self.reads_all || !self.reads_and_writes.is_clear() + } + /// Returns `true` if this can exclusively access the element given by `index`. pub fn has_write(&self, index: T) -> bool { - self.writes.contains(index.sparse_set_index()) + self.writes_all || self.writes.contains(index.sparse_set_index()) } /// Returns `true` if this accesses anything mutably. pub fn has_any_write(&self) -> bool { - !self.writes.is_clear() + self.writes_all || !self.writes.is_clear() } /// Sets this as having access to all indexed elements (i.e. `&World`). @@ -131,14 +141,26 @@ impl Access { self.reads_all = true; } + /// Sets this as having mutable access to all indexed elements (i.e. `EntityMut`). + pub fn write_all(&mut self) { + self.reads_all = true; + self.writes_all = true; + } + /// Returns `true` if this has access to all indexed elements (i.e. `&World`). pub fn has_read_all(&self) -> bool { self.reads_all } + /// Returns `true` if this has write access to all indexed elements (i.e. `EntityMut`). + pub fn has_write_all(&self) -> bool { + self.writes_all + } + /// Removes all accesses. pub fn clear(&mut self) { self.reads_all = false; + self.writes_all = false; self.reads_and_writes.clear(); self.writes.clear(); } @@ -146,6 +168,7 @@ impl Access { /// Adds all access from `other`. pub fn extend(&mut self, other: &Access) { self.reads_all = self.reads_all || other.reads_all; + self.writes_all = self.writes_all || other.writes_all; self.reads_and_writes.union_with(&other.reads_and_writes); self.writes.union_with(&other.writes); } @@ -155,13 +178,20 @@ impl Access { /// [`Access`] instances are incompatible if one can write /// an element that the other can read or write. pub fn is_compatible(&self, other: &Access) -> bool { - // Only systems that do not write data are compatible with systems that operate on `&World`. + if self.writes_all { + return !other.has_any_read(); + } + + if other.writes_all { + return !self.has_any_read(); + } + if self.reads_all { - return other.writes.count_ones(..) == 0; + return !other.has_any_write(); } if other.reads_all { - return self.writes.count_ones(..) == 0; + return !self.has_any_write(); } self.writes.is_disjoint(&other.reads_and_writes) @@ -172,12 +202,23 @@ impl Access { pub fn get_conflicts(&self, other: &Access) -> Vec { let mut conflicts = FixedBitSet::default(); if self.reads_all { + // QUESTION: How to handle `other.writes_all`? conflicts.extend(other.writes.ones()); } if other.reads_all { + // QUESTION: How to handle `self.writes_all`. conflicts.extend(self.writes.ones()); } + + if self.writes_all { + conflicts.extend(other.reads_and_writes.ones()); + } + + if other.writes_all { + conflicts.extend(self.reads_and_writes.ones()); + } + conflicts.extend(self.writes.intersection(&other.reads_and_writes)); conflicts.extend(self.reads_and_writes.intersection(&other.writes)); conflicts @@ -377,6 +418,11 @@ impl FilteredAccess { pub fn read_all(&mut self) { self.access.read_all(); } + + /// Sets the underlying unfiltered access as having mutable access to all indexed elements. + pub fn write_all(&mut self) { + self.access.write_all(); + } } #[derive(Clone, Eq, PartialEq)] diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 240124632248d..c477e01597e86 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -5,7 +5,7 @@ use crate::{ entity::Entity, query::{Access, DebugCheckedUnwrap, FilteredAccess}, storage::{ComponentSparseSet, Table, TableRow}, - world::{unsafe_world_cell::UnsafeWorldCell, EntityRef, Mut, Ref, World}, + world::{unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityRef, Mut, Ref, World}, }; pub use bevy_ecs_macros::WorldQuery; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; @@ -524,8 +524,8 @@ unsafe impl WorldQuery for Entity { unsafe impl ReadOnlyWorldQuery for Entity {} /// SAFETY: `Self` is the same as `Self::ReadOnly` -unsafe impl<'a> WorldQuery for EntityRef<'a> { - type Fetch<'w> = &'w World; +unsafe impl WorldQuery for EntityRef<'_> { + type Fetch<'w> = UnsafeWorldCell<'w>; type Item<'w> = EntityRef<'w>; type ReadOnly = Self; type State = (); @@ -544,8 +544,7 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { - // SAFE: EntityRef has permission to access the whole world immutably thanks to update_component_access and update_archetype_component_access - world.world() + world } #[inline] @@ -568,7 +567,9 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { _table_row: TableRow, ) -> Self::Item<'w> { // SAFETY: `fetch` must be called with an entity that exists in the world - unsafe { world.get_entity(entity).debug_checked_unwrap() } + let cell = world.get_entity(entity).debug_checked_unwrap(); + // SAFETY: Read-only access to every component has been registered. + EntityRef::new(cell) } fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { @@ -599,8 +600,85 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { } } -/// SAFETY: access is read only -unsafe impl<'a> ReadOnlyWorldQuery for EntityRef<'a> {} +/// SAFETY: Access is read-only. +unsafe impl ReadOnlyWorldQuery for EntityRef<'_> {} + +/// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` +unsafe impl<'a> WorldQuery for EntityMut<'a> { + type Fetch<'w> = UnsafeWorldCell<'w>; + type Item<'w> = EntityMut<'w>; + type ReadOnly = EntityRef<'a>; + type State = (); + + fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + item + } + + const IS_DENSE: bool = true; + + const IS_ARCHETYPAL: bool = true; + + unsafe fn init_fetch<'w>( + world: UnsafeWorldCell<'w>, + _state: &Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w> { + world + } + + #[inline] + unsafe fn set_archetype<'w>( + _fetch: &mut Self::Fetch<'w>, + _state: &Self::State, + _archetype: &'w Archetype, + _table: &Table, + ) { + } + + #[inline] + unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + } + + #[inline(always)] + unsafe fn fetch<'w>( + world: &mut Self::Fetch<'w>, + entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w> { + // SAFETY: `fetch` must be called with an entity that exists in the world + let cell = world.get_entity(entity).debug_checked_unwrap(); + // SAFETY: mutable access to every component has been registered. + EntityMut::new(cell) + } + + fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { + assert!( + !access.access().has_any_read(), + "EntityMut conflicts with a previous access in this query. Exclusive access cannot coincide with any other accesses.", + ); + access.write_all(); + } + + fn update_archetype_component_access( + _state: &Self::State, + archetype: &Archetype, + access: &mut Access, + ) { + for component_id in archetype.components() { + access.add_write(archetype.get_archetype_component_id(component_id).unwrap()); + } + } + + fn init_state(_world: &mut World) {} + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } +} #[doc(hidden)] pub struct ReadFetch<'w, T> { diff --git a/crates/bevy_ecs/src/reflect/bundle.rs b/crates/bevy_ecs/src/reflect/bundle.rs index 7fc07d855e6b3..8b5f5e20843c8 100644 --- a/crates/bevy_ecs/src/reflect/bundle.rs +++ b/crates/bevy_ecs/src/reflect/bundle.rs @@ -7,7 +7,7 @@ use std::any::TypeId; use crate::{ prelude::Bundle, - world::{EntityMut, FromWorld, World}, + world::{EntityWorldMut, FromWorld, World}, }; use bevy_reflect::{FromType, Reflect, ReflectRef, TypeRegistry}; @@ -28,13 +28,13 @@ pub struct ReflectBundleFns { /// Function pointer implementing [`ReflectBundle::from_world()`]. pub from_world: fn(&mut World) -> Box, /// Function pointer implementing [`ReflectBundle::insert()`]. - pub insert: fn(&mut EntityMut, &dyn Reflect), + pub insert: fn(&mut EntityWorldMut, &dyn Reflect), /// Function pointer implementing [`ReflectBundle::apply()`]. - pub apply: fn(&mut EntityMut, &dyn Reflect, &TypeRegistry), + pub apply: fn(&mut EntityWorldMut, &dyn Reflect, &TypeRegistry), /// Function pointer implementing [`ReflectBundle::apply_or_insert()`]. - pub apply_or_insert: fn(&mut EntityMut, &dyn Reflect, &TypeRegistry), + pub apply_or_insert: fn(&mut EntityWorldMut, &dyn Reflect, &TypeRegistry), /// Function pointer implementing [`ReflectBundle::remove()`]. - pub remove: fn(&mut EntityMut), + pub remove: fn(&mut EntityWorldMut), } impl ReflectBundleFns { @@ -54,8 +54,8 @@ impl ReflectBundle { (self.0.from_world)(world) } - /// Insert a reflected [`Bundle`] into the entity like [`insert()`](crate::world::EntityMut::insert). - pub fn insert(&self, entity: &mut EntityMut, bundle: &dyn Reflect) { + /// Insert a reflected [`Bundle`] into the entity like [`insert()`](crate::world::EntityWorldMut::insert). + pub fn insert(&self, entity: &mut EntityWorldMut, bundle: &dyn Reflect) { (self.0.insert)(entity, bundle); } @@ -64,14 +64,19 @@ impl ReflectBundle { /// # Panics /// /// Panics if there is no [`Bundle`] of the given type. - pub fn apply(&self, entity: &mut EntityMut, bundle: &dyn Reflect, registry: &TypeRegistry) { + pub fn apply( + &self, + entity: &mut EntityWorldMut, + bundle: &dyn Reflect, + registry: &TypeRegistry, + ) { (self.0.apply)(entity, bundle, registry); } /// Uses reflection to set the value of this [`Bundle`] type in the entity to the given value or insert a new one if it does not exist. pub fn apply_or_insert( &self, - entity: &mut EntityMut, + entity: &mut EntityWorldMut, bundle: &dyn Reflect, registry: &TypeRegistry, ) { @@ -79,7 +84,7 @@ impl ReflectBundle { } /// Removes this [`Bundle`] type from the entity. Does nothing if it doesn't exist. - pub fn remove(&self, entity: &mut EntityMut) { + pub fn remove(&self, entity: &mut EntityWorldMut) { (self.0.remove)(entity); } @@ -169,7 +174,11 @@ impl FromType for ReflectBundle { } } -fn insert_field(entity: &mut EntityMut, field: &dyn Reflect, registry: &TypeRegistry) { +fn insert_field( + entity: &mut EntityWorldMut, + field: &dyn Reflect, + registry: &TypeRegistry, +) { if let Some(reflect_component) = registry.get_type_data::(field.type_id()) { reflect_component.apply(entity, field); } else if let Some(reflect_bundle) = registry.get_type_data::(field.type_id()) { @@ -192,7 +201,7 @@ fn insert_field(entity: &mut EntityMut, field: &dyn Reflect, registr } fn apply_or_insert_field( - entity: &mut EntityMut, + entity: &mut EntityWorldMut, field: &dyn Reflect, registry: &TypeRegistry, ) { diff --git a/crates/bevy_ecs/src/reflect/component.rs b/crates/bevy_ecs/src/reflect/component.rs index 94b36d0953fbf..f91ac8e900225 100644 --- a/crates/bevy_ecs/src/reflect/component.rs +++ b/crates/bevy_ecs/src/reflect/component.rs @@ -25,7 +25,7 @@ //! implementation of `ReflectComponent`. //! //! The `FromType` impl creates a function per field of [`ReflectComponentFns`]. -//! In those functions, we call generic methods on [`World`] and [`EntityMut`]. +//! In those functions, we call generic methods on [`World`] and [`EntityWorldMut`]. //! //! The result is a `ReflectComponent` completely independent of `C`, yet capable //! of using generic ECS methods such as `entity.get::()` to get `&dyn Reflect` @@ -50,7 +50,7 @@ use crate::{ change_detection::Mut, component::Component, entity::Entity, - world::{unsafe_world_cell::UnsafeEntityCell, EntityMut, EntityRef, FromWorld, World}, + world::{unsafe_world_cell::UnsafeEntityCell, EntityRef, EntityWorldMut, FromWorld, World}, }; use bevy_reflect::{FromType, Reflect}; @@ -86,19 +86,19 @@ pub struct ReflectComponentFns { /// Function pointer implementing [`ReflectComponent::from_world()`]. pub from_world: fn(&mut World) -> Box, /// Function pointer implementing [`ReflectComponent::insert()`]. - pub insert: fn(&mut EntityMut, &dyn Reflect), + pub insert: fn(&mut EntityWorldMut, &dyn Reflect), /// Function pointer implementing [`ReflectComponent::apply()`]. - pub apply: fn(&mut EntityMut, &dyn Reflect), + pub apply: fn(&mut EntityWorldMut, &dyn Reflect), /// Function pointer implementing [`ReflectComponent::apply_or_insert()`]. - pub apply_or_insert: fn(&mut EntityMut, &dyn Reflect), + pub apply_or_insert: fn(&mut EntityWorldMut, &dyn Reflect), /// Function pointer implementing [`ReflectComponent::remove()`]. - pub remove: fn(&mut EntityMut), + pub remove: fn(&mut EntityWorldMut), /// Function pointer implementing [`ReflectComponent::contains()`]. pub contains: fn(EntityRef) -> bool, /// Function pointer implementing [`ReflectComponent::reflect()`]. pub reflect: fn(EntityRef) -> Option<&dyn Reflect>, /// Function pointer implementing [`ReflectComponent::reflect_mut()`]. - pub reflect_mut: for<'a> fn(&'a mut EntityMut<'_>) -> Option>, + pub reflect_mut: for<'a> fn(&'a mut EntityWorldMut<'_>) -> Option>, /// Function pointer implementing [`ReflectComponent::reflect_unchecked_mut()`]. /// /// # Safety @@ -125,8 +125,8 @@ impl ReflectComponent { (self.0.from_world)(world) } - /// Insert a reflected [`Component`] into the entity like [`insert()`](crate::world::EntityMut::insert). - pub fn insert(&self, entity: &mut EntityMut, component: &dyn Reflect) { + /// Insert a reflected [`Component`] into the entity like [`insert()`](crate::world::EntityWorldMut::insert). + pub fn insert(&self, entity: &mut EntityWorldMut, component: &dyn Reflect) { (self.0.insert)(entity, component); } @@ -135,17 +135,17 @@ impl ReflectComponent { /// # Panics /// /// Panics if there is no [`Component`] of the given type. - pub fn apply(&self, entity: &mut EntityMut, component: &dyn Reflect) { + pub fn apply(&self, entity: &mut EntityWorldMut, component: &dyn Reflect) { (self.0.apply)(entity, component); } /// Uses reflection to set the value of this [`Component`] type in the entity to the given value or insert a new one if it does not exist. - pub fn apply_or_insert(&self, entity: &mut EntityMut, component: &dyn Reflect) { + pub fn apply_or_insert(&self, entity: &mut EntityWorldMut, component: &dyn Reflect) { (self.0.apply_or_insert)(entity, component); } /// Removes this [`Component`] type from the entity. Does nothing if it doesn't exist. - pub fn remove(&self, entity: &mut EntityMut) { + pub fn remove(&self, entity: &mut EntityWorldMut) { (self.0.remove)(entity); } @@ -160,7 +160,10 @@ impl ReflectComponent { } /// Gets the value of this [`Component`] type from the entity as a mutable reflected reference. - pub fn reflect_mut<'a>(&self, entity: &'a mut EntityMut<'_>) -> Option> { + pub fn reflect_mut<'a>( + &self, + entity: &'a mut EntityWorldMut<'_>, + ) -> Option> { (self.0.reflect_mut)(entity) } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 927b608cd35cb..9009d63859be3 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -5,7 +5,7 @@ use crate::{ self as bevy_ecs, bundle::Bundle, entity::{Entities, Entity}, - world::{EntityMut, FromWorld, World}, + world::{EntityWorldMut, FromWorld, World}, }; use bevy_ecs_macros::SystemParam; use bevy_utils::tracing::{error, info}; @@ -724,7 +724,7 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { /// Removes a [`Bundle`] of components from the entity. /// - /// See [`EntityMut::remove`](crate::world::EntityMut::remove) for more + /// See [`EntityWorldMut::remove`](crate::world::EntityWorldMut::remove) for more /// details. /// /// # Example @@ -805,12 +805,11 @@ impl<'w, 's, 'a> EntityCommands<'w, 's, 'a> { /// /// ``` /// # use bevy_ecs::prelude::*; - /// # use bevy_ecs::world::EntityMut; /// # fn my_system(mut commands: Commands) { /// commands /// .spawn_empty() /// // Closures with this signature implement `EntityCommand`. - /// .add(|entity: EntityMut| { + /// .add(|entity: EntityWorldMut| { /// println!("Executed an EntityCommand for {:?}", entity.id()); /// }); /// # } @@ -849,7 +848,7 @@ where impl EntityCommand for F where - F: FnOnce(EntityMut) + Send + 'static, + F: FnOnce(EntityWorldMut) + Send + 'static, { fn apply(self, id: Entity, world: &mut World) { self(world.entity_mut(id)); diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 29a63c2220d60..52534604990a7 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -14,64 +14,54 @@ use std::any::TypeId; use super::{unsafe_world_cell::UnsafeEntityCell, Ref}; -/// A read-only reference to a particular [`Entity`] and all of its components +/// A read-only reference to a particular [`Entity`] and all of its components. +/// +/// # Examples +/// +/// Read-only access disjoint with mutable access. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component)] pub struct A; +/// # #[derive(Component)] pub struct B; +/// fn disjoint_system( +/// query1: Query<&mut A>, +/// query2: Query>, +/// ) { +/// // ... +/// } +/// # bevy_ecs::system::assert_is_system(disjoint_system); +/// ``` #[derive(Copy, Clone)] -pub struct EntityRef<'w> { - world: &'w World, - entity: Entity, - location: EntityLocation, -} +pub struct EntityRef<'w>(UnsafeEntityCell<'w>); impl<'w> EntityRef<'w> { /// # Safety - /// - /// - `entity` must be valid for `world`: the generation should match that of the entity at the same index. - /// - `location` must be sourced from `world`'s `Entities` and must exactly match the location for `entity` - /// - /// The above is trivially satisfied if `location` was sourced from `world.entities().get(entity)`. + /// - `cell` must have permission to read every component of the entity. + /// - No mutable accesses to any of the entity's components may exist + /// at the same time as the returned [`EntityRef`]. #[inline] - pub(crate) unsafe fn new(world: &'w World, entity: Entity, location: EntityLocation) -> Self { - debug_assert!(world.entities().contains(entity)); - debug_assert_eq!(world.entities().get(entity), Some(location)); - - Self { - world, - entity, - location, - } - } - - fn as_unsafe_world_cell_readonly(&self) -> UnsafeEntityCell<'w> { - UnsafeEntityCell::new( - self.world.as_unsafe_world_cell_readonly(), - self.entity, - self.location, - ) + pub(crate) unsafe fn new(cell: UnsafeEntityCell<'w>) -> Self { + Self(cell) } /// Returns the [ID](Entity) of the current entity. #[inline] #[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."] pub fn id(&self) -> Entity { - self.entity + self.0.id() } /// Gets metadata indicating the location where the current entity is stored. #[inline] pub fn location(&self) -> EntityLocation { - self.location + self.0.location() } /// Returns the archetype that the current entity belongs to. #[inline] pub fn archetype(&self) -> &Archetype { - &self.world.archetypes[self.location.archetype_id] - } - - /// Gets read-only access to the world that the current entity belongs to. - #[inline] - pub fn world(&self) -> &'w World { - self.world + self.0.archetype() } /// Returns `true` if the current entity has a component of type `T`. @@ -96,8 +86,7 @@ impl<'w> EntityRef<'w> { /// [`Self::contains_type_id`]. #[inline] pub fn contains_id(&self, component_id: ComponentId) -> bool { - self.as_unsafe_world_cell_readonly() - .contains_id(component_id) + self.0.contains_id(component_id) } /// Returns `true` if the current entity has a component with the type identified by `type_id`. @@ -109,16 +98,15 @@ impl<'w> EntityRef<'w> { /// - If you have a [`ComponentId`] instead of a [`TypeId`], consider using [`Self::contains_id`]. #[inline] pub fn contains_type_id(&self, type_id: TypeId) -> bool { - self.as_unsafe_world_cell_readonly() - .contains_type_id(type_id) + self.0.contains_type_id(type_id) } /// Gets access to the component of type `T` for the current entity. /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get(&self) -> Option<&'w T> { - // SAFETY: &self implies shared access for duration of returned value - unsafe { self.as_unsafe_world_cell_readonly().get::() } + // SAFETY: We have read-only access to all components of this entity. + unsafe { self.0.get::() } } /// Gets access to the component of type `T` for the current entity, @@ -127,16 +115,16 @@ impl<'w> EntityRef<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get_ref(&self) -> Option> { - // SAFETY: &self implies shared access for duration of returned value - unsafe { self.as_unsafe_world_cell_readonly().get_ref::() } + // SAFETY: We have read-only access to all components of this entity. + unsafe { self.0.get_ref::() } } /// Retrieves the change ticks for the given component. This can be useful for implementing change /// detection in custom runtimes. #[inline] pub fn get_change_ticks(&self) -> Option { - // SAFETY: &self implies shared access - unsafe { self.as_unsafe_world_cell_readonly().get_change_ticks::() } + // SAFETY: We have read-only access to all components of this entity. + unsafe { self.0.get_change_ticks::() } } /// Retrieves the change ticks for the given [`ComponentId`]. This can be useful for implementing change @@ -147,15 +135,10 @@ impl<'w> EntityRef<'w> { /// compile time.** #[inline] pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { - // SAFETY: &self implies shared access - unsafe { - self.as_unsafe_world_cell_readonly() - .get_change_ticks_by_id(component_id) - } + // SAFETY: We have read-only access to all components of this entity. + unsafe { self.0.get_change_ticks_by_id(component_id) } } -} -impl<'w> EntityRef<'w> { /// Gets the component of the given [`ComponentId`] from the entity. /// /// **You should prefer to use the typed API where possible and only @@ -166,35 +149,263 @@ impl<'w> EntityRef<'w> { /// which is only valid while the `'w` borrow of the lifetime is active. #[inline] pub fn get_by_id(&self, component_id: ComponentId) -> Option> { - // SAFETY: &self implies shared access for duration of returned value - unsafe { self.as_unsafe_world_cell_readonly().get_by_id(component_id) } + // SAFETY: We have read-only access to all components of this entity. + unsafe { self.0.get_by_id(component_id) } + } +} + +impl<'w> From> for EntityRef<'w> { + fn from(entity_mut: EntityWorldMut<'w>) -> EntityRef<'w> { + // SAFETY: + // - `EntityWorldMut` guarantees exclusive access to the entire world. + unsafe { EntityRef::new(entity_mut.into_unsafe_entity_cell()) } + } +} + +impl<'a> From<&'a EntityWorldMut<'_>> for EntityRef<'a> { + fn from(value: &'a EntityWorldMut<'_>) -> Self { + // SAFETY: + // - `EntityWorldMut` guarantees exclusive access to the entire world. + // - `&value` ensures no mutable accesses are active. + unsafe { EntityRef::new(value.as_unsafe_entity_cell_readonly()) } } } impl<'w> From> for EntityRef<'w> { - fn from(entity_mut: EntityMut<'w>) -> EntityRef<'w> { - // SAFETY: the safety invariants on EntityMut and EntityRef are identical - // and EntityMut is promised to be valid by construction. - unsafe { EntityRef::new(entity_mut.world, entity_mut.entity, entity_mut.location) } + fn from(value: EntityMut<'w>) -> Self { + // SAFETY: + // - `EntityMut` gurantees exclusive access to all of the entity's components. + unsafe { EntityRef::new(value.0) } } } -/// A mutable reference to a particular [`Entity`] and all of its components -pub struct EntityMut<'w> { +impl<'a> From<&'a EntityMut<'_>> for EntityRef<'a> { + fn from(value: &'a EntityMut<'_>) -> Self { + // SAFETY: + // - `EntityMut` gurantees exclusive access to all of the entity's components. + // - `&value` ensures there are no mutable accesses. + unsafe { EntityRef::new(value.0) } + } +} + +/// Provides mutable access to a single entity and all of its components. +/// +/// Contrast with [`EntityWorldMut`], with allows adding and removing components, +/// despawning the entity, and provides mutable access to the entire world. +/// Because of this, `EntityWorldMut` cannot coexist with any other world accesses. +/// +/// # Examples +/// +/// Disjoint mutable access. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component)] pub struct A; +/// fn disjoint_system( +/// query1: Query>, +/// query2: Query>, +/// ) { +/// // ... +/// } +/// # bevy_ecs::system::assert_is_system(disjoint_system); +/// ``` +pub struct EntityMut<'w>(UnsafeEntityCell<'w>); + +impl<'w> EntityMut<'w> { + /// # Safety + /// - `cell` must have permission to mutate every component of the entity. + /// - No accesses to any of the entity's components may exist + /// at the same time as the returned [`EntityMut`]. + pub(crate) unsafe fn new(cell: UnsafeEntityCell<'w>) -> Self { + Self(cell) + } + + /// Returns a new instance with a shorter lifetime. + /// This is useful if you have `&mut EntityMut`, but you need `EntityMut`. + pub fn reborrow(&mut self) -> EntityMut<'_> { + // SAFETY: We have exclusive access to the entire entity and its components. + unsafe { Self::new(self.0) } + } + + /// Gets read-only access to all of the entity's components. + pub fn as_readonly(&self) -> EntityRef<'_> { + EntityRef::from(self) + } + + /// Returns the [ID](Entity) of the current entity. + #[inline] + #[must_use = "Omit the .id() call if you do not need to store the `Entity` identifier."] + pub fn id(&self) -> Entity { + self.0.id() + } + + /// Gets metadata indicating the location where the current entity is stored. + #[inline] + pub fn location(&self) -> EntityLocation { + self.0.location() + } + + /// Returns the archetype that the current entity belongs to. + #[inline] + pub fn archetype(&self) -> &Archetype { + self.0.archetype() + } + + /// Returns `true` if the current entity has a component of type `T`. + /// Otherwise, this returns `false`. + /// + /// ## Notes + /// + /// If you do not know the concrete type of a component, consider using + /// [`Self::contains_id`] or [`Self::contains_type_id`]. + #[inline] + pub fn contains(&self) -> bool { + self.contains_type_id(TypeId::of::()) + } + + /// Returns `true` if the current entity has a component identified by `component_id`. + /// Otherwise, this returns false. + /// + /// ## Notes + /// + /// - If you know the concrete type of the component, you should prefer [`Self::contains`]. + /// - If you know the component's [`TypeId`] but not its [`ComponentId`], consider using + /// [`Self::contains_type_id`]. + #[inline] + pub fn contains_id(&self, component_id: ComponentId) -> bool { + self.0.contains_id(component_id) + } + + /// Returns `true` if the current entity has a component with the type identified by `type_id`. + /// Otherwise, this returns false. + /// + /// ## Notes + /// + /// - If you know the concrete type of the component, you should prefer [`Self::contains`]. + /// - If you have a [`ComponentId`] instead of a [`TypeId`], consider using [`Self::contains_id`]. + #[inline] + pub fn contains_type_id(&self, type_id: TypeId) -> bool { + self.0.contains_type_id(type_id) + } + + /// Gets access to the component of type `T` for the current entity. + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn get(&self) -> Option<&'_ T> { + self.as_readonly().get() + } + + /// Gets access to the component of type `T` for the current entity, + /// including change detection information as a [`Ref`]. + /// + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn get_ref(&self) -> Option> { + self.as_readonly().get_ref() + } + + /// Gets mutable access to the component of type `T` for the current entity. + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn get_mut(&mut self) -> Option> { + // SAFETY: &mut self implies exclusive access for duration of returned value + unsafe { self.0.get_mut() } + } + + /// Retrieves the change ticks for the given component. This can be useful for implementing change + /// detection in custom runtimes. + #[inline] + pub fn get_change_ticks(&self) -> Option { + self.as_readonly().get_change_ticks::() + } + + /// Retrieves the change ticks for the given [`ComponentId`]. This can be useful for implementing change + /// detection in custom runtimes. + /// + /// **You should prefer to use the typed API [`EntityWorldMut::get_change_ticks`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + #[inline] + pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { + self.as_readonly().get_change_ticks_by_id(component_id) + } + + /// Gets the component of the given [`ComponentId`] from the entity. + /// + /// **You should prefer to use the typed API [`EntityWorldMut::get`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + /// + /// Unlike [`EntityMut::get`], this returns a raw pointer to the component, + /// which is only valid while the [`EntityMut`] is alive. + #[inline] + pub fn get_by_id(&self, component_id: ComponentId) -> Option> { + self.as_readonly().get_by_id(component_id) + } + + /// Gets a [`MutUntyped`] of the component of the given [`ComponentId`] from the entity. + /// + /// **You should prefer to use the typed API [`EntityMut::get_mut`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + /// + /// Unlike [`EntityMut::get_mut`], this returns a raw pointer to the component, + /// which is only valid while the [`EntityMut`] is alive. + #[inline] + pub fn get_mut_by_id(&mut self, component_id: ComponentId) -> Option> { + // SAFETY: + // - `&mut self` ensures that no references exist to this entity's components. + // - `as_unsafe_world_cell` gives mutable permission for all components on this entity + unsafe { self.0.get_mut_by_id(component_id) } + } +} + +impl<'w> From> for EntityMut<'w> { + fn from(value: EntityWorldMut<'w>) -> Self { + // SAFETY: `EntityWorldMut` guarantees exclusive access to the entire world. + unsafe { EntityMut::new(value.into_unsafe_entity_cell()) } + } +} + +impl<'a> From<&'a mut EntityWorldMut<'_>> for EntityMut<'a> { + fn from(value: &'a mut EntityWorldMut<'_>) -> Self { + // SAFETY: `EntityWorldMut` guarantees exclusive access to the entire world. + unsafe { EntityMut::new(value.as_unsafe_entity_cell()) } + } +} + +/// A mutable reference to a particular [`Entity`], and the entire world. +/// This is essentially a performance-optimized `(Entity, &mut World)` tuple, +/// which caches the [`EntityLocation`] to reduce duplicate lookups. +/// +/// Since this type provides mutable access to the entire world, only one +/// [`EntityWorldMut`] can exist at a time for a given world. +/// +/// See also [`EntityMut`], which allows disjoint mutable access to multiple +/// entities at once. Unlike `EntityMut`, this type allows adding and +/// removing components, and despawning the entity. +pub struct EntityWorldMut<'w> { world: &'w mut World, entity: Entity, location: EntityLocation, } -impl<'w> EntityMut<'w> { - fn as_unsafe_world_cell_readonly(&self) -> UnsafeEntityCell<'_> { +impl<'w> EntityWorldMut<'w> { + fn as_unsafe_entity_cell_readonly(&self) -> UnsafeEntityCell<'_> { UnsafeEntityCell::new( self.world.as_unsafe_world_cell_readonly(), self.entity, self.location, ) } - fn as_unsafe_world_cell(&mut self) -> UnsafeEntityCell<'_> { + fn as_unsafe_entity_cell(&mut self) -> UnsafeEntityCell<'_> { + UnsafeEntityCell::new( + self.world.as_unsafe_world_cell(), + self.entity, + self.location, + ) + } + fn into_unsafe_entity_cell(self) -> UnsafeEntityCell<'w> { UnsafeEntityCell::new( self.world.as_unsafe_world_cell(), self.entity, @@ -217,7 +428,7 @@ impl<'w> EntityMut<'w> { debug_assert!(world.entities().contains(entity)); debug_assert_eq!(world.entities().get(entity), Some(location)); - EntityMut { + EntityWorldMut { world, entity, location, @@ -265,7 +476,7 @@ impl<'w> EntityMut<'w> { /// [`Self::contains_type_id`]. #[inline] pub fn contains_id(&self, component_id: ComponentId) -> bool { - self.as_unsafe_world_cell_readonly() + self.as_unsafe_entity_cell_readonly() .contains_id(component_id) } @@ -278,7 +489,7 @@ impl<'w> EntityMut<'w> { /// - If you have a [`ComponentId`] instead of a [`TypeId`], consider using [`Self::contains_id`]. #[inline] pub fn contains_type_id(&self, type_id: TypeId) -> bool { - self.as_unsafe_world_cell_readonly() + self.as_unsafe_entity_cell_readonly() .contains_type_id(type_id) } @@ -286,8 +497,16 @@ impl<'w> EntityMut<'w> { /// Returns `None` if the entity does not have a component of type `T`. #[inline] pub fn get(&self) -> Option<&'_ T> { - // SAFETY: &self implies shared access for duration of returned value - unsafe { self.as_unsafe_world_cell_readonly().get::() } + EntityRef::from(self).get() + } + + /// Gets access to the component of type `T` for the current entity, + /// including change detection information as a [`Ref`]. + /// + /// Returns `None` if the entity does not have a component of type `T`. + #[inline] + pub fn get_ref(&self) -> Option> { + EntityRef::from(self).get_ref() } /// Gets mutable access to the component of type `T` for the current entity. @@ -295,30 +514,54 @@ impl<'w> EntityMut<'w> { #[inline] pub fn get_mut(&mut self) -> Option> { // SAFETY: &mut self implies exclusive access for duration of returned value - unsafe { self.as_unsafe_world_cell().get_mut() } + unsafe { self.as_unsafe_entity_cell().get_mut() } } /// Retrieves the change ticks for the given component. This can be useful for implementing change /// detection in custom runtimes. #[inline] pub fn get_change_ticks(&self) -> Option { - // SAFETY: &self implies shared access - unsafe { self.as_unsafe_world_cell_readonly().get_change_ticks::() } + EntityRef::from(self).get_change_ticks::() } /// Retrieves the change ticks for the given [`ComponentId`]. This can be useful for implementing change /// detection in custom runtimes. /// - /// **You should prefer to use the typed API [`EntityMut::get_change_ticks`] where possible and only + /// **You should prefer to use the typed API [`EntityWorldMut::get_change_ticks`] where possible and only /// use this in cases where the actual component types are not known at /// compile time.** #[inline] pub fn get_change_ticks_by_id(&self, component_id: ComponentId) -> Option { - // SAFETY: &self implies shared access - unsafe { - self.as_unsafe_world_cell_readonly() - .get_change_ticks_by_id(component_id) - } + EntityRef::from(self).get_change_ticks_by_id(component_id) + } + + /// Gets the component of the given [`ComponentId`] from the entity. + /// + /// **You should prefer to use the typed API [`EntityWorldMut::get`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + /// + /// Unlike [`EntityWorldMut::get`], this returns a raw pointer to the component, + /// which is only valid while the [`EntityWorldMut`] is alive. + #[inline] + pub fn get_by_id(&self, component_id: ComponentId) -> Option> { + EntityRef::from(self).get_by_id(component_id) + } + + /// Gets a [`MutUntyped`] of the component of the given [`ComponentId`] from the entity. + /// + /// **You should prefer to use the typed API [`EntityWorldMut::get_mut`] where possible and only + /// use this in cases where the actual component types are not known at + /// compile time.** + /// + /// Unlike [`EntityWorldMut::get_mut`], this returns a raw pointer to the component, + /// which is only valid while the [`EntityWorldMut`] is alive. + #[inline] + pub fn get_mut_by_id(&mut self, component_id: ComponentId) -> Option> { + // SAFETY: + // - `&mut self` ensures that no references exist to this entity's components. + // - `as_unsafe_world_cell` gives mutable permission for all components on this entity + unsafe { self.as_unsafe_entity_cell().get_mut_by_id(component_id) } } /// Adds a [`Bundle`] of components to the entity. @@ -350,11 +593,11 @@ impl<'w> EntityMut<'w> { /// /// This will overwrite any previous value(s) of the same component type. /// - /// You should prefer to use the typed API [`EntityMut::insert`] where possible. + /// You should prefer to use the typed API [`EntityWorldMut::insert`] where possible. /// /// # Safety /// - /// - [`ComponentId`] must be from the same world as [`EntityMut`] + /// - [`ComponentId`] must be from the same world as [`EntityWorldMut`] /// - [`OwningPtr`] must be a valid reference to the type represented by [`ComponentId`] pub unsafe fn insert_by_id( &mut self, @@ -391,13 +634,13 @@ impl<'w> EntityMut<'w> { /// /// This will overwrite any previous value(s) of the same component type. /// - /// You should prefer to use the typed API [`EntityMut::insert`] where possible. - /// If your [`Bundle`] only has one component, use the cached API [`EntityMut::insert_by_id`]. + /// You should prefer to use the typed API [`EntityWorldMut::insert`] where possible. + /// If your [`Bundle`] only has one component, use the cached API [`EntityWorldMut::insert_by_id`]. /// /// If possible, pass a sorted slice of `ComponentId` to maximize caching potential. /// /// # Safety - /// - Each [`ComponentId`] must be from the same world as [`EntityMut`] + /// - Each [`ComponentId`] must be from the same world as [`EntityWorldMut`] /// - Each [`OwningPtr`] must be a valid reference to the type represented by [`ComponentId`] pub unsafe fn insert_by_ids<'a, I: Iterator>>( &mut self, @@ -719,27 +962,27 @@ impl<'w> EntityMut<'w> { self.world } - /// Returns this `EntityMut`'s world. + /// Returns this entity's world. /// - /// See [`EntityMut::world_scope`] or [`EntityMut::into_world_mut`] for a safe alternative. + /// See [`EntityWorldMut::world_scope`] or [`EntityWorldMut::into_world_mut`] for a safe alternative. /// /// # Safety /// Caller must not modify the world in a way that changes the current entity's location /// If the caller _does_ do something that could change the location, `self.update_location()` - /// must be called before using any other methods on this [`EntityMut`]. + /// must be called before using any other methods on this [`EntityWorldMut`]. #[inline] pub unsafe fn world_mut(&mut self) -> &mut World { self.world } - /// Returns this `EntityMut`'s [`World`], consuming itself. + /// Returns this entity's [`World`], consuming itself. #[inline] pub fn into_world_mut(self) -> &'w mut World { self.world } - /// Gives mutable access to this `EntityMut`'s [`World`] in a temporary scope. - /// This is a safe alternative to using [`Self::world_mut`]. + /// Gives mutable access to this entity's [`World`] in a temporary scope. + /// This is a safe alternative to using [`EntityWorldMut::world_mut`]. /// /// # Examples /// @@ -757,14 +1000,14 @@ impl<'w> EntityMut<'w> { /// let mut r = world.resource_mut::(); /// r.0 += 1; /// - /// // Return a value from the world before giving it back to the `EntityMut`. + /// // Return a value from the world before giving it back to the `EntityWorldMut`. /// *r /// }); /// # assert_eq!(new_r.0, 1); /// ``` pub fn world_scope(&mut self, f: impl FnOnce(&mut World) -> U) -> U { struct Guard<'w, 'a> { - entity_mut: &'a mut EntityMut<'w>, + entity_mut: &'a mut EntityWorldMut<'w>, } impl Drop for Guard<'_, '_> { @@ -784,47 +1027,13 @@ impl<'w> EntityMut<'w> { /// Updates the internal entity location to match the current location in the internal /// [`World`]. /// - /// This is *only* required when using the unsafe function [`EntityMut::world_mut`], + /// This is *only* required when using the unsafe function [`EntityWorldMut::world_mut`], /// which enables the location to change. pub fn update_location(&mut self) { self.location = self.world.entities().get(self.entity).unwrap(); } } -impl<'w> EntityMut<'w> { - /// Gets the component of the given [`ComponentId`] from the entity. - /// - /// **You should prefer to use the typed API [`EntityMut::get`] where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** - /// - /// Unlike [`EntityMut::get`], this returns a raw pointer to the component, - /// which is only valid while the [`EntityMut`] is alive. - #[inline] - pub fn get_by_id(&self, component_id: ComponentId) -> Option> { - // SAFETY: - // - `&self` ensures that no mutable references exist to this entity's components. - // - `as_unsafe_world_cell_readonly` gives read only permission for all components on this entity - unsafe { self.as_unsafe_world_cell_readonly().get_by_id(component_id) } - } - - /// Gets a [`MutUntyped`] of the component of the given [`ComponentId`] from the entity. - /// - /// **You should prefer to use the typed API [`EntityMut::get_mut`] where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** - /// - /// Unlike [`EntityMut::get_mut`], this returns a raw pointer to the component, - /// which is only valid while the [`EntityMut`] is alive. - #[inline] - pub fn get_mut_by_id(&mut self, component_id: ComponentId) -> Option> { - // SAFETY: - // - `&mut self` ensures that no references exist to this entity's components. - // - `as_unsafe_world_cell` gives mutable permission for all components on this entity - unsafe { self.as_unsafe_world_cell().get_mut_by_id(component_id) } - } -} - /// Inserts a dynamic [`Bundle`] into the entity. /// /// # Safety @@ -1027,9 +1236,7 @@ mod tests { use bevy_ptr::OwningPtr; use std::panic::AssertUnwindSafe; - use crate as bevy_ecs; - use crate::component::ComponentId; - use crate::prelude::*; // for the `#[derive(Component)]` + use crate::{self as bevy_ecs, component::ComponentId, prelude::*, system::assert_is_system}; #[test] fn sorted_remove() { @@ -1090,7 +1297,7 @@ mod tests { { test_component.set_changed(); let test_component = - // SAFETY: `test_component` has unique access of the `EntityMut` and is not used afterwards + // SAFETY: `test_component` has unique access of the `EntityWorldMut` and is not used afterwards unsafe { test_component.into_inner().deref_mut::() }; test_component.0 = 43; } @@ -1133,7 +1340,7 @@ mod tests { let id = entity.id(); let res = std::panic::catch_unwind(AssertUnwindSafe(|| { entity.world_scope(|w| { - // Change the entity's `EntityLocation`, which invalidates the original `EntityMut`. + // Change the entity's `EntityLocation`, which invalidates the original `EntityWorldMut`. // This will get updated at the end of the scope. w.entity_mut(id).insert(TestComponent(0)); @@ -1369,4 +1576,113 @@ mod tests { assert_eq!(dynamic_components, static_components); } + + #[derive(Component)] + struct A; + + #[derive(Resource)] + struct R; + + #[test] + fn disjoint_access() { + fn disjoint_readonly(_: Query>, _: Query>) {} + + fn disjoint_mutable(_: Query>, _: Query>) {} + + assert_is_system(disjoint_readonly); + assert_is_system(disjoint_mutable); + } + + #[test] + fn ref_compatible() { + fn borrow_system(_: Query<(EntityRef, &A)>, _: Query<&A>) {} + + assert_is_system(borrow_system); + } + + #[test] + fn ref_compatible_with_resource() { + fn borrow_system(_: Query, _: Res) {} + + assert_is_system(borrow_system); + } + + #[test] + #[ignore] // This should pass, but it currently fails due to limitations in our access model. + fn ref_compatible_with_resource_mut() { + fn borrow_system(_: Query, _: ResMut) {} + + assert_is_system(borrow_system); + } + + #[test] + #[should_panic] + fn ref_incompatible_with_mutable_component() { + fn incompatible_system(_: Query<(EntityRef, &mut A)>) {} + + assert_is_system(incompatible_system); + } + + #[test] + #[should_panic] + fn ref_incompatible_with_mutable_query() { + fn incompatible_system(_: Query, _: Query<&mut A>) {} + + assert_is_system(incompatible_system); + } + + #[test] + fn mut_compatible_with_entity() { + fn borrow_mut_system(_: Query<(Entity, EntityMut)>) {} + + assert_is_system(borrow_mut_system); + } + + #[test] + #[ignore] // This should pass, but it currently fails due to limitations in our access model. + fn mut_compatible_with_resource() { + fn borrow_mut_system(_: Res, _: Query) {} + + assert_is_system(borrow_mut_system); + } + + #[test] + #[ignore] // This should pass, but it currently fails due to limitations in our access model. + fn mut_compatible_with_resource_mut() { + fn borrow_mut_system(_: ResMut, _: Query) {} + + assert_is_system(borrow_mut_system); + } + + #[test] + #[should_panic] + fn mut_incompatible_with_read_only_component() { + fn incompatible_system(_: Query<(EntityMut, &A)>) {} + + assert_is_system(incompatible_system); + } + + #[test] + #[should_panic] + fn mut_incompatible_with_mutable_component() { + fn incompatible_system(_: Query<(EntityMut, &mut A)>) {} + + assert_is_system(incompatible_system); + } + + #[test] + #[should_panic] + fn mut_incompatible_with_read_only_query() { + fn incompatible_system(_: Query, _: Query<&A>) {} + + assert_is_system(incompatible_system); + } + + #[test] + #[should_panic] + fn mut_incompatible_with_mutable_query() { + fn incompatible_system(_: Query, _: Query<&mut A>) {} + + assert_is_system(incompatible_system); + } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 93217aca5ebf9..2d795a242c94b 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -7,7 +7,7 @@ pub mod unsafe_world_cell; mod world_cell; pub use crate::change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}; -pub use entity_ref::{EntityMut, EntityRef}; +pub use entity_ref::{EntityMut, EntityRef, EntityWorldMut}; pub use spawn_batch::*; pub use world_cell::*; @@ -18,7 +18,7 @@ use crate::{ component::{Component, ComponentDescriptor, ComponentId, ComponentInfo, Components, Tick}, entity::{AllocAtWithoutReplacement, Entities, Entity, EntityLocation}, event::{Event, Events}, - query::{DebugCheckedUnwrap, QueryState, ReadOnlyWorldQuery, WorldQuery}, + query::{DebugCheckedUnwrap, QueryEntityError, QueryState, ReadOnlyWorldQuery, WorldQuery}, removal_detection::RemovedComponentEvents, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, @@ -30,13 +30,14 @@ use bevy_utils::tracing::warn; use std::{ any::TypeId, fmt, + mem::MaybeUninit, sync::atomic::{AtomicU32, Ordering}, }; mod identifier; pub use identifier::WorldId; -use self::unsafe_world_cell::UnsafeWorldCell; +use self::unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}; /// Stores and exposes operations on [entities](Entity), [components](Component), resources, /// and their associated metadata. @@ -256,7 +257,7 @@ impl World { } } - /// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`. + /// Retrieves an [`EntityWorldMut`] that exposes read and write operations for the given `entity`. /// This will panic if the `entity` does not exist. Use [`World::get_entity_mut`] if you want /// to check for entity existence instead of implicitly panic-ing. /// @@ -277,7 +278,7 @@ impl World { /// ``` #[inline] #[track_caller] - pub fn entity_mut(&mut self, entity: Entity) -> EntityMut { + pub fn entity_mut(&mut self, entity: Entity) -> EntityWorldMut { #[inline(never)] #[cold] #[track_caller] @@ -291,6 +292,91 @@ impl World { } } + /// Gets an [`EntityRef`] for multiple entities at once. + /// + /// # Panics + /// + /// If any entity does not exist in the world. + /// + /// # Examples + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let id1 = world.spawn_empty().id(); + /// # let id2 = world.spawn_empty().id(); + /// // Getting multiple entities. + /// let [entity1, entity2] = world.many_entities([id1, id2]); + /// ``` + /// + /// ```should_panic + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let id1 = world.spawn_empty().id(); + /// # let id2 = world.spawn_empty().id(); + /// // Trying to get a despawned entity will fail. + /// world.despawn(id2); + /// world.many_entities([id1, id2]); + /// ``` + pub fn many_entities(&mut self, entities: [Entity; N]) -> [EntityRef<'_>; N] { + #[inline(never)] + #[cold] + #[track_caller] + fn panic_no_entity(entity: Entity) -> ! { + panic!("Entity {entity:?} does not exist"); + } + + match self.get_many_entities(entities) { + Ok(refs) => refs, + Err(entity) => panic_no_entity(entity), + } + } + + /// Gets mutable access to multiple entities at once. + /// + /// # Panics + /// + /// If any entities do not exist in the world, + /// or if the same entity is specified multiple times. + /// + /// # Examples + /// + /// Disjoint mutable access. + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let id1 = world.spawn_empty().id(); + /// # let id2 = world.spawn_empty().id(); + /// // Disjoint mutable access. + /// let [entity1, entity2] = world.many_entities_mut([id1, id2]); + /// ``` + /// + /// Trying to access the same entity multiple times will fail. + /// + /// ```should_panic + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let id = world.spawn_empty().id(); + /// world.many_entities_mut([id, id]); + /// ``` + pub fn many_entities_mut( + &mut self, + entities: [Entity; N], + ) -> [EntityMut<'_>; N] { + #[inline(never)] + #[cold] + #[track_caller] + fn panic_on_err(e: QueryEntityError) -> ! { + panic!("{e}"); + } + + match self.get_many_entities_mut(entities) { + Ok(borrows) => borrows, + Err(e) => panic_on_err(e), + } + } + /// Returns the components of an [`Entity`](crate::entity::Entity) through [`ComponentInfo`](crate::component::ComponentInfo). #[inline] pub fn inspect_entity(&self, entity: Entity) -> Vec<&ComponentInfo> { @@ -315,7 +401,7 @@ impl World { .collect() } - /// Returns an [`EntityMut`] for the given `entity` (if it exists) or spawns one if it doesn't exist. + /// Returns an [`EntityWorldMut`] for the given `entity` (if it exists) or spawns one if it doesn't exist. /// This will return [`None`] if the `entity` exists with a different generation. /// /// # Note @@ -323,12 +409,12 @@ impl World { /// This method should generally only be used for sharing entities across apps, and only when they have a /// scheme worked out to share an ID space (which doesn't happen by default). #[inline] - pub fn get_or_spawn(&mut self, entity: Entity) -> Option { + pub fn get_or_spawn(&mut self, entity: Entity) -> Option { self.flush(); match self.entities.alloc_at_without_replacement(entity) { AllocAtWithoutReplacement::Exists(location) => { // SAFETY: `entity` exists and `location` is that entity's location - Some(unsafe { EntityMut::new(self, entity, location) }) + Some(unsafe { EntityWorldMut::new(self, entity, location) }) } AllocAtWithoutReplacement::DidNotExist => { // SAFETY: entity was just allocated @@ -339,8 +425,8 @@ impl World { } /// Retrieves an [`EntityRef`] that exposes read-only operations for the given `entity`. - /// Returns [`None`] if the `entity` does not exist. Use [`World::entity`] if you don't want - /// to unwrap the [`EntityRef`] yourself. + /// Returns [`None`] if the `entity` does not exist. + /// Instead of unwrapping the value returned from this function, prefer [`World::entity`]. /// /// ``` /// use bevy_ecs::{component::Component, world::World}; @@ -362,10 +448,48 @@ impl World { let location = self.entities.get(entity)?; // SAFETY: if the Entity is invalid, the function returns early. // Additionally, Entities::get(entity) returns the correct EntityLocation if the entity exists. - let entity_ref = unsafe { EntityRef::new(self, entity, location) }; + let entity_cell = + UnsafeEntityCell::new(self.as_unsafe_world_cell_readonly(), entity, location); + // SAFETY: The UnsafeEntityCell has read access to the entire world. + let entity_ref = unsafe { EntityRef::new(entity_cell) }; Some(entity_ref) } + /// Gets an [`EntityRef`] for multiple entities at once. + /// + /// # Errors + /// + /// If any entity does not exist in the world. + /// + /// # Examples + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let id1 = world.spawn_empty().id(); + /// # let id2 = world.spawn_empty().id(); + /// // Getting multiple entities. + /// let [entity1, entity2] = world.get_many_entities([id1, id2]).unwrap(); + /// + /// // Trying to get a despawned entity will fail. + /// world.despawn(id2); + /// assert!(world.get_many_entities([id1, id2]).is_err()); + /// ``` + pub fn get_many_entities( + &self, + entities: [Entity; N], + ) -> Result<[EntityRef<'_>; N], Entity> { + let mut refs = [MaybeUninit::uninit(); N]; + for (r, id) in std::iter::zip(&mut refs, entities) { + *r = MaybeUninit::new(self.get_entity(id).ok_or(id)?); + } + + // SAFETY: Each item was initialized in the above loop. + let refs = refs.map(|r| unsafe { MaybeUninit::assume_init(r) }); + + Ok(refs) + } + /// Returns an [`Entity`] iterator of current entities. /// /// This is useful in contexts where you only have read-only access to the [`World`]. @@ -385,15 +509,47 @@ impl World { table_row: archetype_entity.table_row(), }; - // SAFETY: entity exists and location accurately specifies the archetype where the entity is stored - unsafe { EntityRef::new(self, entity, location) } + // SAFETY: entity exists and location accurately specifies the archetype where the entity is stored. + let cell = UnsafeEntityCell::new( + self.as_unsafe_world_cell_readonly(), + entity, + location, + ); + // SAFETY: `&self` gives read access to the entire world. + unsafe { EntityRef::new(cell) } + }) + }) + } + + /// Returns a mutable iterator over all entities in the `World`. + pub fn iter_entities_mut(&mut self) -> impl Iterator> + '_ { + let world_cell = self.as_unsafe_world_cell(); + world_cell.archetypes().iter().flat_map(move |archetype| { + archetype + .entities() + .iter() + .enumerate() + .map(move |(archetype_row, archetype_entity)| { + let entity = archetype_entity.entity(); + let location = EntityLocation { + archetype_id: archetype.id(), + archetype_row: ArchetypeRow::new(archetype_row), + table_id: archetype.table_id(), + table_row: archetype_entity.table_row(), + }; + + // SAFETY: entity exists and location accurately specifies the archetype where the entity is stored. + let cell = UnsafeEntityCell::new(world_cell, entity, location); + // SAFETY: We have exclusive access to the entire world. We only create one borrow for each entity, + // so none will conflict with one another. + unsafe { EntityMut::new(cell) } }) }) } - /// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`. - /// Returns [`None`] if the `entity` does not exist. Use [`World::entity_mut`] if you don't want - /// to unwrap the [`EntityMut`] yourself. + /// Retrieves an [`EntityWorldMut`] that exposes read and write operations for the given `entity`. + /// Returns [`None`] if the `entity` does not exist. + /// Instead of unwrapping the value returned from this function, prefer [`World::entity_mut`]. /// /// ``` /// use bevy_ecs::{component::Component, world::World}; @@ -411,13 +567,78 @@ impl World { /// position.x = 1.0; /// ``` #[inline] - pub fn get_entity_mut(&mut self, entity: Entity) -> Option { + pub fn get_entity_mut(&mut self, entity: Entity) -> Option { let location = self.entities.get(entity)?; // SAFETY: `entity` exists and `location` is that entity's location - Some(unsafe { EntityMut::new(self, entity, location) }) + Some(unsafe { EntityWorldMut::new(self, entity, location) }) } - /// Spawns a new [`Entity`] and returns a corresponding [`EntityMut`], which can be used + /// Gets mutable access to multiple entities. + /// + /// # Errors + /// + /// If any entities do not exist in the world, + /// or if the same entity is specified multiple times. + /// + /// # Examples + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let id1 = world.spawn_empty().id(); + /// # let id2 = world.spawn_empty().id(); + /// // Disjoint mutable access. + /// let [entity1, entity2] = world.get_many_entities_mut([id1, id2]).unwrap(); + /// + /// // Trying to access the same entity multiple times will fail. + /// assert!(world.get_many_entities_mut([id1, id1]).is_err()); + /// ``` + pub fn get_many_entities_mut( + &mut self, + entities: [Entity; N], + ) -> Result<[EntityMut<'_>; N], QueryEntityError> { + // Ensure each entity is unique. + for i in 0..N { + for j in 0..i { + if entities[i] == entities[j] { + return Err(QueryEntityError::AliasedMutability(entities[i])); + } + } + } + + // SAFETY: Each entity is unique. + unsafe { self.get_entities_mut_unchecked(entities) } + } + + /// # Safety + /// `entities` must contain no duplicate [`Entity`] IDs. + unsafe fn get_entities_mut_unchecked( + &mut self, + entities: [Entity; N], + ) -> Result<[EntityMut<'_>; N], QueryEntityError> { + let world_cell = self.as_unsafe_world_cell(); + + let mut cells = [MaybeUninit::uninit(); N]; + for (cell, id) in std::iter::zip(&mut cells, entities) { + *cell = MaybeUninit::new( + world_cell + .get_entity(id) + .ok_or(QueryEntityError::NoSuchEntity(id))?, + ); + } + // SAFETY: Each item was initialized in the loop above. + let cells = cells.map(|c| unsafe { MaybeUninit::assume_init(c) }); + + // SAFETY: + // - `world_cell` has exclusive access to the entire world. + // - The caller ensures that each entity is unique, so none + // of the borrows will conflict with one another. + let borrows = cells.map(|c| unsafe { EntityMut::new(c) }); + + Ok(borrows) + } + + /// Spawns a new [`Entity`] and returns a corresponding [`EntityWorldMut`], which can be used /// to add components to the entity or retrieve its id. /// /// ``` @@ -442,7 +663,7 @@ impl World { /// let position = world.entity(entity).get::().unwrap(); /// assert_eq!(position.x, 0.0); /// ``` - pub fn spawn_empty(&mut self) -> EntityMut { + pub fn spawn_empty(&mut self) -> EntityWorldMut { self.flush(); let entity = self.entities.alloc(); // SAFETY: entity was just allocated @@ -450,7 +671,7 @@ impl World { } /// Spawns a new [`Entity`] with a given [`Bundle`] of [components](`Component`) and returns - /// a corresponding [`EntityMut`], which can be used to add components to the entity or + /// a corresponding [`EntityWorldMut`], which can be used to add components to the entity or /// retrieve its id. /// /// ``` @@ -508,7 +729,7 @@ impl World { /// let position = world.entity(entity).get::().unwrap(); /// assert_eq!(position.x, 2.0); /// ``` - pub fn spawn(&mut self, bundle: B) -> EntityMut { + pub fn spawn(&mut self, bundle: B) -> EntityWorldMut { self.flush(); let change_tick = self.change_tick(); let entity = self.entities.alloc(); @@ -529,12 +750,12 @@ impl World { }; // SAFETY: entity and location are valid, as they were just created above - unsafe { EntityMut::new(self, entity, entity_location) } + unsafe { EntityWorldMut::new(self, entity, entity_location) } } /// # Safety /// must be called on an entity that was just allocated - unsafe fn spawn_at_empty_internal(&mut self, entity: Entity) -> EntityMut { + unsafe fn spawn_at_empty_internal(&mut self, entity: Entity) -> EntityWorldMut { let archetype = self.archetypes.empty_mut(); // PERF: consider avoiding allocating entities in the empty archetype unless needed let table_row = self.storages.tables[archetype.table_id()].allocate(entity); @@ -543,7 +764,7 @@ impl World { let location = archetype.allocate(entity, table_row); // SAFETY: entity index was just allocated self.entities.set(entity.index(), location); - EntityMut::new(self, entity, location) + EntityWorldMut::new(self, entity, location) } /// Spawns a batch of entities with the same component [Bundle] type. Takes a given [Bundle] @@ -2284,6 +2505,54 @@ mod tests { assert_eq!(entity_counters.len(), 0); } + #[test] + fn iterate_entities_mut() { + #[derive(Component, PartialEq, Debug)] + struct A(i32); + + #[derive(Component, PartialEq, Debug)] + struct B(i32); + + let mut world = World::new(); + + let a1 = world.spawn(A(1)).id(); + let a2 = world.spawn(A(2)).id(); + let b1 = world.spawn(B(1)).id(); + let b2 = world.spawn(B(2)).id(); + + for mut entity in world.iter_entities_mut() { + if let Some(mut a) = entity.get_mut::() { + a.0 -= 1; + } + } + assert_eq!(world.entity(a1).get(), Some(&A(0))); + assert_eq!(world.entity(a2).get(), Some(&A(1))); + assert_eq!(world.entity(b1).get(), Some(&B(1))); + assert_eq!(world.entity(b2).get(), Some(&B(2))); + + for mut entity in world.iter_entities_mut() { + if let Some(mut b) = entity.get_mut::() { + b.0 *= 2; + } + } + assert_eq!(world.entity(a1).get(), Some(&A(0))); + assert_eq!(world.entity(a2).get(), Some(&A(1))); + assert_eq!(world.entity(b1).get(), Some(&B(2))); + assert_eq!(world.entity(b2).get(), Some(&B(4))); + + let mut entities = world.iter_entities_mut().collect::>(); + entities.sort_by_key(|e| e.get::().map(|a| a.0).or(e.get::().map(|b| b.0))); + let (a, b) = entities.split_at_mut(2); + std::mem::swap( + &mut a[1].get_mut::().unwrap().0, + &mut b[0].get_mut::().unwrap().0, + ); + assert_eq!(world.entity(a1).get(), Some(&A(0))); + assert_eq!(world.entity(a2).get(), Some(&A(2))); + assert_eq!(world.entity(b1).get(), Some(&B(1))); + assert_eq!(world.entity(b2).get(), Some(&B(4))); + } + #[test] fn spawn_empty_bundle() { let mut world = World::new(); diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index 19bada425f03b..da52ac385faf2 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -4,7 +4,7 @@ use bevy_ecs::{ entity::Entity, prelude::Events, system::{Command, Commands, EntityCommands}, - world::{EntityMut, World}, + world::{EntityWorldMut, World}, }; use smallvec::SmallVec; @@ -460,7 +460,7 @@ pub struct WorldChildBuilder<'w> { impl<'w> WorldChildBuilder<'w> { /// Spawns an entity with the given bundle and inserts it into the parent entity's [`Children`]. /// Also adds [`Parent`] component to the created entity. - pub fn spawn(&mut self, bundle: impl Bundle + Send + Sync + 'static) -> EntityMut<'_> { + pub fn spawn(&mut self, bundle: impl Bundle + Send + Sync + 'static) -> EntityWorldMut<'_> { let entity = self.world.spawn((bundle, Parent(self.parent))).id(); push_child_unchecked(self.world, self.parent, entity); push_events( @@ -475,7 +475,7 @@ impl<'w> WorldChildBuilder<'w> { /// Spawns an [`Entity`] with no components and inserts it into the parent entity's [`Children`]. /// Also adds [`Parent`] component to the created entity. - pub fn spawn_empty(&mut self) -> EntityMut<'_> { + pub fn spawn_empty(&mut self) -> EntityWorldMut<'_> { let entity = self.world.spawn(Parent(self.parent)).id(); push_child_unchecked(self.world, self.parent, entity); push_events( @@ -554,7 +554,7 @@ pub trait BuildWorldChildren { fn remove_parent(&mut self) -> &mut Self; } -impl<'w> BuildWorldChildren for EntityMut<'w> { +impl<'w> BuildWorldChildren for EntityWorldMut<'w> { fn with_children(&mut self, spawn_children: impl FnOnce(&mut WorldChildBuilder)) -> &mut Self { let parent = self.id(); self.world_scope(|world| { diff --git a/crates/bevy_hierarchy/src/hierarchy.rs b/crates/bevy_hierarchy/src/hierarchy.rs index 32c2f788d63aa..79b56b9fd183a 100644 --- a/crates/bevy_hierarchy/src/hierarchy.rs +++ b/crates/bevy_hierarchy/src/hierarchy.rs @@ -2,7 +2,7 @@ use crate::components::{Children, Parent}; use bevy_ecs::{ entity::Entity, system::{Command, EntityCommands}, - world::{EntityMut, World}, + world::{EntityWorldMut, World}, }; use bevy_utils::tracing::debug; @@ -103,7 +103,7 @@ impl<'w, 's, 'a> DespawnRecursiveExt for EntityCommands<'w, 's, 'a> { } } -impl<'w> DespawnRecursiveExt for EntityMut<'w> { +impl<'w> DespawnRecursiveExt for EntityWorldMut<'w> { /// Despawns the provided entity and its children. fn despawn_recursive(self) { let entity = self.id(); From 9073d446dcb687dedacc5cdda76a91e9788e4539 Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Tue, 29 Aug 2023 12:49:40 +0200 Subject: [PATCH 42/58] Do not panic on non-UI child of UI entity (#9621) Legitimately, bevy emits a WARN when encountering entities in UI trees without NodeBunlde components. Bevy pretty much always panics when such a thing happens, due to the update_clipping system. However, sometimes, it's perfectly legitimate to have a child without UI nodes in a UI tree. For example, as a "seed" entity that is consumed by a 3rd party plugin, which will later spawn a valid UI tree. In loading scenarios, you are pretty much guaranteed to have incomplete children. The presence of the WARN hints that bevy does not intend to panic on such occasion (otherwise the warn! would be a panic!) so I assume panic is an unintended behavior, aka a bug. ## Solution Early-return instead of panicking. I did only test that it indeed fixed the panic, not checked for UI inconsistencies. Though on a logical level, it can only have changed code that would otherwise panic. ## Alternatives Instead of early-returning on invalid entity in `update_clipping`, do not call it with invalid entity in its recursive call. --- ## Changelog - Do not panic on non-UI child of UI entity --- crates/bevy_ui/src/update.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 1da4395b55196..dd524be5172e7 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -37,8 +37,10 @@ fn update_clipping( entity: Entity, maybe_inherited_clip: Option, ) { - let (node, global_transform, style, maybe_calculated_clip) = - node_query.get_mut(entity).unwrap(); + let Ok((node, global_transform, style, maybe_calculated_clip)) = node_query.get_mut(entity) + else { + return; + }; // Update this node's CalculatedClip component if let Some(mut calculated_clip) = maybe_calculated_clip { From 64dcaf002b1943b576830e4e82cd2f3ab33723a4 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 29 Aug 2023 12:12:23 +0100 Subject: [PATCH 43/58] Rename `Val` `evaluate` to `resolve` and implement viewport variant support (#9568) # Objective Rename `Val`'s `evaluate` method to `resolve`. Implement `resolve` support for `Val`'s viewport variants. fixes #9535 --- ## Changelog `bevy_ui::ui_node::Val`: * Renamed the following methods and added a `viewport_size` parameter: - `evaluate` to `resolve` - `try_add_with_size` to `try_add_with_context` - `try_add_assign_with_size` to `try_add_assign_with_context` - `try_sub_with_size` to `try_sub_with_context` - `try_sub_assign_with_size` to `try_sub_assign_with_context` * Implemented `resolve` support for `Val`'s viewport coordinate types ## Migration Guide * Renamed the following `Val` methods and added a `viewport_size` parameter: - `evaluate` to `resolve` - `try_add_with_size` to `try_add_with_context` - `try_add_assign_with_size` to `try_add_assign_with_context` - `try_sub_with_size` to `try_sub_with_context` - `try_sub_assign_with_size` to `try_sub_assign_with_context` --- crates/bevy_ui/src/ui_node.rs | 68 ++++++++++++++++++++++++++--------- 1 file changed, 52 insertions(+), 16 deletions(-) diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index f7c18f397d627..1dfbbcd0204a4 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -237,16 +237,19 @@ pub enum ValArithmeticError { } impl Val { - /// A convenience function for simple evaluation of [`Val::Percent`] variant into a concrete [`Val::Px`] value. - /// Returns a [`ValArithmeticError::NonEvaluateable`] if the [`Val`] is impossible to evaluate into [`Val::Px`]. - /// Otherwise it returns an [`f32`] containing the evaluated value in pixels. + /// Resolves a [`Val`] to its value in logical pixels and returns this as an [`f32`]. + /// Returns a [`ValArithmeticError::NonEvaluateable`] if the [`Val`] is impossible to resolve into a concrete value. /// - /// **Note:** If a [`Val::Px`] is evaluated, it's inner value returned unchanged. - pub fn evaluate(&self, size: f32) -> Result { + /// **Note:** If a [`Val::Px`] is resolved, it's inner value is returned unchanged. + pub fn resolve(self, parent_size: f32, viewport_size: Vec2) -> Result { match self { - Val::Percent(value) => Ok(size * value / 100.0), - Val::Px(value) => Ok(*value), - _ => Err(ValArithmeticError::NonEvaluateable), + Val::Percent(value) => Ok(parent_size * value / 100.0), + Val::Px(value) => Ok(value), + Val::Vw(value) => Ok(viewport_size.x * value / 100.0), + Val::Vh(value) => Ok(viewport_size.y * value / 100.0), + Val::VMin(value) => Ok(viewport_size.min_element() * value / 100.0), + Val::VMax(value) => Ok(viewport_size.max_element() * value / 100.0), + Val::Auto => Err(ValArithmeticError::NonEvaluateable), } } } @@ -1714,33 +1717,66 @@ impl Default for ZIndex { #[cfg(test)] mod tests { + use super::Val; use crate::GridPlacement; use crate::ValArithmeticError; - - use super::Val; + use bevy_math::vec2; #[test] fn val_evaluate() { let size = 250.; - let result = Val::Percent(80.).evaluate(size).unwrap(); + let viewport_size = vec2(1000., 500.); + let result = Val::Percent(80.).resolve(size, viewport_size).unwrap(); assert_eq!(result, size * 0.8); } #[test] - fn val_evaluate_px() { + fn val_resolve_px() { let size = 250.; - let result = Val::Px(10.).evaluate(size).unwrap(); + let viewport_size = vec2(1000., 500.); + let result = Val::Px(10.).resolve(size, viewport_size).unwrap(); assert_eq!(result, 10.); } #[test] - fn val_invalid_evaluation() { + fn val_resolve_viewport_coords() { + let size = 250.; + let viewport_size = vec2(500., 500.); + + for value in (-10..10).map(|value| value as f32) { + // for a square viewport there should be no difference between `Vw` and `Vh` and between `Vmin` and `Vmax`. + assert_eq!( + Val::Vw(value).resolve(size, viewport_size), + Val::Vh(value).resolve(size, viewport_size) + ); + assert_eq!( + Val::VMin(value).resolve(size, viewport_size), + Val::VMax(value).resolve(size, viewport_size) + ); + assert_eq!( + Val::VMin(value).resolve(size, viewport_size), + Val::Vw(value).resolve(size, viewport_size) + ); + } + + let viewport_size = vec2(1000., 500.); + assert_eq!(Val::Vw(100.).resolve(size, viewport_size).unwrap(), 1000.); + assert_eq!(Val::Vh(100.).resolve(size, viewport_size).unwrap(), 500.); + assert_eq!(Val::Vw(60.).resolve(size, viewport_size).unwrap(), 600.); + assert_eq!(Val::Vh(40.).resolve(size, viewport_size).unwrap(), 200.); + assert_eq!(Val::VMin(50.).resolve(size, viewport_size).unwrap(), 250.); + assert_eq!(Val::VMax(75.).resolve(size, viewport_size).unwrap(), 750.); + } + + #[test] + fn val_auto_is_non_resolveable() { let size = 250.; - let evaluate_auto = Val::Auto.evaluate(size); + let viewport_size = vec2(1000., 500.); + let resolve_auto = Val::Auto.resolve(size, viewport_size); - assert_eq!(evaluate_auto, Err(ValArithmeticError::NonEvaluateable)); + assert_eq!(resolve_auto, Err(ValArithmeticError::NonEvaluateable)); } #[test] From ce2ade2636b2d3a729e673b47f235ef4068a7328 Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Tue, 29 Aug 2023 05:28:24 -0700 Subject: [PATCH 44/58] Remove unused regex dep from bevy_render (#9613) # Objective As far as I can tell, this is no longer needed since the switch to fancier shader imports via `naga_oil`. This shouldn't have any affect on compile times because it's in our tree from `naga_oil`, `tracing-subscriber`, and `rodio`. --- crates/bevy_render/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 9db47cfe18254..dd2f90aef4683 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -71,7 +71,6 @@ futures-lite = "1.4.0" anyhow = "1.0" hexasphere = "9.0" parking_lot = "0.12.1" -regex = "1.5" ddsfile = { version = "0.5.0", optional = true } ktx2 = { version = "0.3.0", optional = true } naga_oil = "0.8" From 2b2abcef973f38d37f1dbe327dd5c2d18fa05d17 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 29 Aug 2023 13:30:02 +0100 Subject: [PATCH 45/58] Replace uses of `entity.insert` with tuple bundles in `game_menu` example (#9619) # Objective Change two places where `entity.insert` is used to add components individually to spawn a tuple bundle instead. --- examples/games/game_menu.rs | 41 +++++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/examples/games/game_menu.rs b/examples/games/game_menu.rs index cc96169601ec1..cbd300d83c0d3 100644 --- a/examples/games/game_menu.rs +++ b/examples/games/game_menu.rs @@ -654,16 +654,19 @@ mod menu { DisplayQuality::Medium, DisplayQuality::High, ] { - let mut entity = parent.spawn(ButtonBundle { - style: Style { - width: Val::Px(150.0), - height: Val::Px(65.0), - ..button_style.clone() + let mut entity = parent.spawn(( + ButtonBundle { + style: Style { + width: Val::Px(150.0), + height: Val::Px(65.0), + ..button_style.clone() + }, + background_color: NORMAL_BUTTON.into(), + ..default() }, - background_color: NORMAL_BUTTON.into(), - ..default() - }); - entity.insert(quality_setting).with_children(|parent| { + quality_setting, + )); + entity.with_children(|parent| { parent.spawn(TextBundle::from_section( format!("{quality_setting:?}"), button_text_style.clone(), @@ -746,16 +749,18 @@ mod menu { button_text_style.clone(), )); for volume_setting in [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] { - let mut entity = parent.spawn(ButtonBundle { - style: Style { - width: Val::Px(30.0), - height: Val::Px(65.0), - ..button_style.clone() + let mut entity = parent.spawn(( + ButtonBundle { + style: Style { + width: Val::Px(30.0), + height: Val::Px(65.0), + ..button_style.clone() + }, + background_color: NORMAL_BUTTON.into(), + ..default() }, - background_color: NORMAL_BUTTON.into(), - ..default() - }); - entity.insert(Volume(volume_setting)); + Volume(volume_setting), + )); if *volume == Volume(volume_setting) { entity.insert(SelectedOption); } From da9a070d6f70b47f9ace50c3e1f351e9436f06d1 Mon Sep 17 00:00:00 2001 From: Mike Date: Tue, 29 Aug 2023 07:53:26 -0700 Subject: [PATCH 46/58] port old ambiguity tests over (#9617) # Objective - Some of the old ambiguity tests didn't get ported over during schedule v3. ## Solution - Port over tests from https://github.com/bevyengine/bevy/blob/15ee98db8d1c6705111e0f11a8fc240ceaf9f2db/crates/bevy_ecs/src/schedule/ambiguity_detection.rs#L279-L612 with minimal changes - Make a method to convert the ambiguity conflicts to a string for easier verification of correct results. --- crates/bevy_ecs/src/schedule/mod.rs | 330 +++++++++++++++++++++++ crates/bevy_ecs/src/schedule/schedule.rs | 39 ++- 2 files changed, 356 insertions(+), 13 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index faba4851e95c9..5f0ae88a8f015 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -716,4 +716,334 @@ mod tests { assert!(matches!(result, Err(ScheduleBuildError::Ambiguity(_)))); } } + + mod system_ambiguity { + // Required to make the derive macro behave + use crate as bevy_ecs; + use crate::event::Events; + use crate::prelude::*; + + #[derive(Resource)] + struct R; + + #[derive(Component)] + struct A; + + #[derive(Component)] + struct B; + + // An event type + #[derive(Event)] + struct E; + + fn empty_system() {} + fn res_system(_res: Res) {} + fn resmut_system(_res: ResMut) {} + fn nonsend_system(_ns: NonSend) {} + fn nonsendmut_system(_ns: NonSendMut) {} + fn read_component_system(_query: Query<&A>) {} + fn write_component_system(_query: Query<&mut A>) {} + fn with_filtered_component_system(_query: Query<&mut A, With>) {} + fn without_filtered_component_system(_query: Query<&mut A, Without>) {} + fn event_reader_system(_reader: EventReader) {} + fn event_writer_system(_writer: EventWriter) {} + fn event_resource_system(_events: ResMut>) {} + fn read_world_system(_world: &World) {} + fn write_world_system(_world: &mut World) {} + + // Tests for conflict detection + + #[test] + fn one_of_everything() { + let mut world = World::new(); + world.insert_resource(R); + world.spawn(A); + world.init_resource::>(); + + let mut schedule = Schedule::default(); + schedule + // nonsendmut system deliberately conflicts with resmut system + .add_systems((resmut_system, write_component_system, event_writer_system)); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + #[test] + fn read_only() { + let mut world = World::new(); + world.insert_resource(R); + world.spawn(A); + world.init_resource::>(); + + let mut schedule = Schedule::default(); + schedule.add_systems(( + empty_system, + empty_system, + res_system, + res_system, + nonsend_system, + nonsend_system, + read_component_system, + read_component_system, + event_reader_system, + event_reader_system, + read_world_system, + read_world_system, + )); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + #[test] + fn read_world() { + let mut world = World::new(); + world.insert_resource(R); + world.spawn(A); + world.init_resource::>(); + + let mut schedule = Schedule::default(); + schedule.add_systems(( + resmut_system, + write_component_system, + event_writer_system, + read_world_system, + )); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 3); + } + + #[test] + fn resources() { + let mut world = World::new(); + world.insert_resource(R); + + let mut schedule = Schedule::default(); + schedule.add_systems((resmut_system, res_system)); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 1); + } + + #[test] + fn nonsend() { + let mut world = World::new(); + world.insert_resource(R); + + let mut schedule = Schedule::default(); + schedule.add_systems((nonsendmut_system, nonsend_system)); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 1); + } + + #[test] + fn components() { + let mut world = World::new(); + world.spawn(A); + + let mut schedule = Schedule::default(); + schedule.add_systems((read_component_system, write_component_system)); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 1); + } + + #[test] + #[ignore = "Known failing but fix is non-trivial: https://github.com/bevyengine/bevy/issues/4381"] + fn filtered_components() { + let mut world = World::new(); + world.spawn(A); + + let mut schedule = Schedule::default(); + schedule.add_systems(( + with_filtered_component_system, + without_filtered_component_system, + )); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + #[test] + fn events() { + let mut world = World::new(); + world.init_resource::>(); + + let mut schedule = Schedule::default(); + schedule.add_systems(( + // All of these systems clash + event_reader_system, + event_writer_system, + event_resource_system, + )); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 3); + } + + #[test] + fn exclusive() { + let mut world = World::new(); + world.insert_resource(R); + world.spawn(A); + world.init_resource::>(); + + let mut schedule = Schedule::default(); + schedule.add_systems(( + // All 3 of these conflict with each other + write_world_system, + write_world_system, + res_system, + )); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 3); + } + + // Tests for silencing and resolving ambiguities + #[test] + fn before_and_after() { + let mut world = World::new(); + world.init_resource::>(); + + let mut schedule = Schedule::default(); + schedule.add_systems(( + event_reader_system.before(event_writer_system), + event_writer_system, + event_resource_system.after(event_writer_system), + )); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + #[test] + fn ignore_all_ambiguities() { + let mut world = World::new(); + world.insert_resource(R); + + let mut schedule = Schedule::default(); + schedule.add_systems(( + resmut_system.ambiguous_with_all(), + res_system, + nonsend_system, + )); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + #[test] + fn ambiguous_with_label() { + let mut world = World::new(); + world.insert_resource(R); + + #[derive(SystemSet, Hash, PartialEq, Eq, Debug, Clone)] + struct IgnoreMe; + + let mut schedule = Schedule::default(); + schedule.add_systems(( + resmut_system.ambiguous_with(IgnoreMe), + res_system.in_set(IgnoreMe), + nonsend_system.in_set(IgnoreMe), + )); + + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + #[test] + fn ambiguous_with_system() { + let mut world = World::new(); + + let mut schedule = Schedule::default(); + schedule.add_systems(( + write_component_system.ambiguous_with(read_component_system), + read_component_system, + )); + let _ = schedule.initialize(&mut world); + + assert_eq!(schedule.graph().conflicting_systems().len(), 0); + } + + // Tests that the correct ambiguities were reported in the correct order. + #[test] + fn correct_ambiguities() { + use super::*; + + #[derive(ScheduleLabel, Hash, PartialEq, Eq, Debug, Clone)] + struct TestSchedule; + + fn system_a(_res: ResMut) {} + fn system_b(_res: ResMut) {} + fn system_c(_res: ResMut) {} + fn system_d(_res: ResMut) {} + fn system_e(_res: ResMut) {} + + let mut world = World::new(); + world.insert_resource(R); + + let mut schedule = Schedule::new(TestSchedule); + schedule.add_systems(( + system_a, + system_b, + system_c.ambiguous_with_all(), + system_d.ambiguous_with(system_b), + system_e.after(system_a), + )); + + schedule.graph_mut().initialize(&mut world); + let _ = schedule + .graph_mut() + .build_schedule(world.components(), &TestSchedule.dyn_clone()); + + let ambiguities: Vec<_> = schedule + .graph() + .conflicts_to_string(world.components()) + .collect(); + + let expected = &[ + ( + "system_d".to_string(), + "system_a".to_string(), + vec!["bevy_ecs::schedule::tests::system_ambiguity::R"], + ), + ( + "system_d".to_string(), + "system_e".to_string(), + vec!["bevy_ecs::schedule::tests::system_ambiguity::R"], + ), + ( + "system_b".to_string(), + "system_a".to_string(), + vec!["bevy_ecs::schedule::tests::system_ambiguity::R"], + ), + ( + "system_b".to_string(), + "system_e".to_string(), + vec!["bevy_ecs::schedule::tests::system_ambiguity::R"], + ), + ]; + + // ordering isn't stable so do this + for entry in expected { + assert!(ambiguities.contains(entry)); + } + } + } } diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 33d5c5647f936..0024360158a20 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -1553,21 +1553,11 @@ impl ScheduleGraph { Consider adding `before`, `after`, or `ambiguous_with` relationships between these:\n", ); - for (system_a, system_b, conflicts) in ambiguities { - let name_a = self.get_node_name(system_a); - let name_b = self.get_node_name(system_b); - - debug_assert!(system_a.is_system(), "{name_a} is not a system."); - debug_assert!(system_b.is_system(), "{name_b} is not a system."); - + for (name_a, name_b, conflicts) in self.conflicts_to_string(components) { writeln!(message, " -- {name_a} and {name_b}").unwrap(); - if !conflicts.is_empty() { - let conflict_names: Vec<_> = conflicts - .iter() - .map(|id| components.get_name(*id).unwrap()) - .collect(); - writeln!(message, " conflict on: {conflict_names:?}").unwrap(); + if !conflicts.is_empty() { + writeln!(message, " conflict on: {conflicts:?}").unwrap(); } else { // one or both systems must be exclusive let world = std::any::type_name::(); @@ -1578,6 +1568,29 @@ impl ScheduleGraph { message } + /// convert conflics to human readable format + pub fn conflicts_to_string<'a>( + &'a self, + components: &'a Components, + ) -> impl Iterator)> + 'a { + self.conflicting_systems + .iter() + .map(move |(system_a, system_b, conflicts)| { + let name_a = self.get_node_name(system_a); + let name_b = self.get_node_name(system_b); + + debug_assert!(system_a.is_system(), "{name_a} is not a system."); + debug_assert!(system_b.is_system(), "{name_b} is not a system."); + + let conflict_names: Vec<_> = conflicts + .iter() + .map(|id| components.get_name(*id).unwrap()) + .collect(); + + (name_a, name_b, conflict_names) + }) + } + fn traverse_sets_containing_node(&self, id: NodeId, f: &mut impl FnMut(NodeId) -> bool) { for (set_id, _, _) in self.hierarchy.graph.edges_directed(id, Direction::Incoming) { if f(set_id) { From 4f212a5b0cf8fc959bc23dd586b652069cab8f0d Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Tue, 29 Aug 2023 17:29:46 +0200 Subject: [PATCH 47/58] Remove `IntoIterator` impl for `&mut EventReader` (#9583) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective The latest `clippy` release has a much more aggressive application of the [`explicit_iter_loop`](https://rust-lang.github.io/rust-clippy/master/index.html#/explicit_into_iter_loop?groups=pedantic) pedantic lint. As a result, clippy now suggests the following: ```diff -for event in events.iter() { +for event in &mut events { ``` I'm generally in favor of this lint. Using `for mut item in &mut query` is also recommended over `for mut item in query.iter_mut()` for good reasons IMO. But, it is my personal belief that `&mut events` is much less clear than `events.iter()`. Why? The reason is that the events from `EventReader` **are not mutable**, they are immutable references to each event in the event reader. `&mut events` suggests we are getting mutable access to events — similarly to `&mut query` — which is not the case. Using `&mut events` is therefore misleading. `IntoIterator` requires a mutable `EventReader` because it updates the internal `last_event_count`, not because it let you mutate it. So clippy's suggested improvement is a downgrade. ## Solution Do not implement `IntoIterator` for `&mut events`. Without the impl, clippy won't suggest its "fix". This also prevents generally people from using `&mut events` for iterating `EventReader`s, which makes the ecosystem every-so-slightly better. --- ## Changelog - Removed `IntoIterator` impl for `&mut EventReader` ## Migration Guide - `&mut EventReader` does not implement `IntoIterator` anymore. replace `for foo in &mut events` by `for foo in events.iter()` --- crates/bevy_ecs/src/event.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 7f0887eb4cc90..8f3bf91d3fe7d 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -453,14 +453,6 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { } } -impl<'a, 'w, 's, E: Event> IntoIterator for &'a mut EventReader<'w, 's, E> { - type Item = &'a E; - type IntoIter = EventIterator<'a, E>; - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - /// Sends events of type `T`. /// /// # Usage From 1399078f127ba1cb304f115447d463241cdba6b9 Mon Sep 17 00:00:00 2001 From: "Ame :]" <104745335+ameknite@users.noreply.github.com> Date: Tue, 29 Aug 2023 12:59:10 -0600 Subject: [PATCH 48/58] remove VecSwizzles imports (#9629) # Objective - Since #9387, there is no need to import VecSwizzles separately, it is already included in the prelude. ## Solution - Remove the imports. --- examples/2d/rotation.rs | 2 +- examples/transforms/scale.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/2d/rotation.rs b/examples/2d/rotation.rs index 3447543b7541a..8f5e7fa6d6913 100644 --- a/examples/2d/rotation.rs +++ b/examples/2d/rotation.rs @@ -1,6 +1,6 @@ //! Demonstrates rotating entities in 2D using quaternions. -use bevy::{math::Vec3Swizzles, prelude::*}; +use bevy::prelude::*; const TIME_STEP: f32 = 1.0 / 60.0; const BOUNDS: Vec2 = Vec2::new(1200.0, 640.0); diff --git a/examples/transforms/scale.rs b/examples/transforms/scale.rs index 537a5c7137877..bbae8b71b9be2 100644 --- a/examples/transforms/scale.rs +++ b/examples/transforms/scale.rs @@ -2,7 +2,6 @@ use std::f32::consts::PI; -use bevy::math::Vec3Swizzles; use bevy::prelude::*; // Define a component to keep information for the scaled object. From f2f39c835af23d4ce521bce355502da06e166e0d Mon Sep 17 00:00:00 2001 From: "Ame :]" <104745335+ameknite@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:11:06 -0600 Subject: [PATCH 49/58] Check for bevy_internal imports in CI (#9612) # Objective - Avoid using bevy_internal imports in examples. ## Solution - Add CI to check for bevy_internal imports like suggested in https://github.com/bevyengine/bevy/pull/9547#issuecomment-1689377999 - Fix another import I don't know much about CI so I don't know if this is the better approach, but I think is better than doing a pull request every time I found this lol, any suggestion is welcome. --------- Co-authored-by: Rob Parrett --- .github/workflows/ci.yml | 22 ++++++++++++++++++++++ examples/animation/animated_fox.rs | 4 +--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9a135c6e4e662..6e9e82dc2bfc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -333,3 +333,25 @@ jobs: with: name: msrv path: msrv/ + + check-bevy-internal-imports: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v3 + - name: Check for bevy_internal imports + shell: bash + run: | + errors="" + for file in $(find examples tests -name '*.rs'); do + if grep -q "use bevy_internal" "$file"; then + errors+="ERROR: Detected 'use bevy_internal' in $file\n" + fi + done + if [ -n "$errors" ]; then + echo -e "$errors" + echo " Avoid importing bevy_internal, it should not be used directly" + echo " Fix the issue by replacing 'bevy_internal' with 'bevy'" + echo " Example: 'use bevy::sprite::MaterialMesh2dBundle;' instead of 'bevy_internal::sprite::MaterialMesh2dBundle;'" + exit 1 + fi \ No newline at end of file diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 808d04adf13f7..87fe9dd0989a7 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -3,9 +3,7 @@ use std::f32::consts::PI; use std::time::Duration; -use bevy::pbr::CascadeShadowConfigBuilder; -use bevy::prelude::*; -use bevy_internal::animation::RepeatAnimation; +use bevy::{animation::RepeatAnimation, pbr::CascadeShadowConfigBuilder, prelude::*}; fn main() { App::new() From fb094eab8793041444294e62df96dd7ae488a980 Mon Sep 17 00:00:00 2001 From: "Ame :]" <104745335+ameknite@users.noreply.github.com> Date: Tue, 29 Aug 2023 19:13:04 -0600 Subject: [PATCH 50/58] Move default docs (#9638) # Objective - Make the default docs more useful like suggested in https://github.com/bevyengine/bevy/pull/9600#issuecomment-1696452118 ## Solution - Move the documentation to the `fn default()` method instead of the `impl Default`. Allows you to view the docs directly on the function without having to go to the implementation. ### Before ![Screenshot 2023-08-29 at 18 21 03](https://github.com/bevyengine/bevy/assets/104745335/6d31591e-f190-4b8e-8bc3-a570ada294f0) ### After ![Screenshot 2023-08-29 at 18 19 54](https://github.com/bevyengine/bevy/assets/104745335/e2442ec1-593d-47f3-b539-8c77a170f0b6) --- crates/bevy_ecs/src/schedule/schedule.rs | 8 ++++---- crates/bevy_render/src/view/visibility/render_layers.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 0024360158a20..8f7da74f1e78c 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -168,11 +168,11 @@ pub struct Schedule { #[derive(ScheduleLabel, Hash, PartialEq, Eq, Debug, Clone)] struct DefaultSchedule; -/// Creates a schedule with a default label. Only use in situations where -/// where you don't care about the [`ScheduleLabel`]. Inserting a default schedule -/// into the world risks overwriting another schedule. For most situations you should use -/// [`Schedule::new`]. impl Default for Schedule { + /// Creates a schedule with a default label. Only use in situations where + /// you don't care about the [`ScheduleLabel`]. Inserting a default schedule + /// into the world risks overwriting another schedule. For most situations + /// you should use [`Schedule::new`]. fn default() -> Self { Self::new(DefaultSchedule) } diff --git a/crates/bevy_render/src/view/visibility/render_layers.rs b/crates/bevy_render/src/view/visibility/render_layers.rs index 677c0877777ba..0451e35818f15 100644 --- a/crates/bevy_render/src/view/visibility/render_layers.rs +++ b/crates/bevy_render/src/view/visibility/render_layers.rs @@ -38,8 +38,8 @@ impl std::iter::FromIterator for RenderLayers { } } -/// Defaults to containing to layer `0`, the first layer. impl Default for RenderLayers { + /// By default, this structure includes layer `0`, which represents the first layer. fn default() -> Self { RenderLayers::layer(0) } From 42e6dc8987d89a53a34db868cb6d90fcf49c0096 Mon Sep 17 00:00:00 2001 From: lelo <15314665+hate@users.noreply.github.com> Date: Wed, 30 Aug 2023 10:20:03 -0400 Subject: [PATCH 51/58] Refactor `EventReader::iter` to `read` (#9631) # Objective - The current `EventReader::iter` has been determined to cause confusion among new Bevy users. It was suggested by @JoJoJet to rename the method to better clarify its usage. - Solves #9624 ## Solution - Rename `EventReader::iter` to `EventReader::read`. - Rename `EventReader::iter_with_id` to `EventReader::read_with_id`. - Rename `ManualEventReader::iter` to `ManualEventReader::read`. - Rename `ManualEventReader::iter_with_id` to `ManualEventReader::read_with_id`. --- ## Changelog - `EventReader::iter` has been renamed to `EventReader::read`. - `EventReader::iter_with_id` has been renamed to `EventReader::read_with_id`. - `ManualEventReader::iter` has been renamed to `ManualEventReader::read`. - `ManualEventReader::iter_with_id` has been renamed to `ManualEventReader::read_with_id`. - Deprecated `EventReader::iter` - Deprecated `EventReader::iter_with_id` - Deprecated `ManualEventReader::iter` - Deprecated `ManualEventReader::iter_with_id` ## Migration Guide - Existing usages of `EventReader::iter` and `EventReader::iter_with_id` will have to be changed to `EventReader::read` and `EventReader::read_with_id` respectively. - Existing usages of `ManualEventReader::iter` and `ManualEventReader::iter_with_id` will have to be changed to `ManualEventReader::read` and `ManualEventReader::read_with_id` respectively. --- crates/bevy_app/src/schedule_runner.rs | 2 +- crates/bevy_ecs/examples/events.rs | 2 +- crates/bevy_ecs/src/event.rs | 56 ++++++++++++++----- crates/bevy_ecs/src/removal_detection.rs | 4 +- crates/bevy_ecs/src/schedule/condition.rs | 2 +- crates/bevy_gilrs/src/rumble.rs | 2 +- crates/bevy_input/src/gamepad.rs | 8 +-- crates/bevy_input/src/keyboard.rs | 2 +- crates/bevy_input/src/mouse.rs | 2 +- crates/bevy_input/src/touch.rs | 2 +- crates/bevy_pbr/src/material.rs | 2 +- crates/bevy_render/src/camera/camera.rs | 6 +- crates/bevy_render/src/render_asset.rs | 2 +- .../src/render_resource/pipeline_cache.rs | 2 +- crates/bevy_render/src/view/window/mod.rs | 2 +- crates/bevy_scene/src/scene_spawner.rs | 2 +- crates/bevy_sprite/src/mesh2d/material.rs | 2 +- crates/bevy_sprite/src/render/mod.rs | 2 +- crates/bevy_text/src/text2d.rs | 2 +- crates/bevy_ui/src/layout/mod.rs | 2 +- crates/bevy_window/src/system.rs | 2 +- crates/bevy_winit/src/accessibility.rs | 4 +- crates/bevy_winit/src/lib.rs | 6 +- examples/3d/shadow_biases.rs | 2 +- examples/3d/skybox.rs | 2 +- examples/3d/split_screen.rs | 2 +- examples/3d/tonemapping.rs | 4 +- examples/app/drag_and_drop.rs | 2 +- .../external_source_external_thread.rs | 2 +- examples/audio/pitch.rs | 2 +- examples/ecs/event.rs | 4 +- examples/input/char_input_events.rs | 2 +- examples/input/gamepad_input_events.rs | 10 ++-- examples/input/keyboard_input_events.rs | 2 +- examples/input/mouse_input_events.rs | 12 ++-- examples/input/text_input.rs | 6 +- examples/input/touch_input_events.rs | 2 +- examples/mobile/src/lib.rs | 2 +- examples/tools/gamepad_viewer.rs | 4 +- .../scene_viewer/camera_controller_plugin.rs | 2 +- examples/ui/size_constraints.rs | 2 +- examples/ui/ui.rs | 2 +- examples/window/window_resizing.rs | 2 +- tests/how_to_test_systems.rs | 4 +- 44 files changed, 108 insertions(+), 82 deletions(-) diff --git a/crates/bevy_app/src/schedule_runner.rs b/crates/bevy_app/src/schedule_runner.rs index b8f45d87802f0..0780a1b9a395a 100644 --- a/crates/bevy_app/src/schedule_runner.rs +++ b/crates/bevy_app/src/schedule_runner.rs @@ -94,7 +94,7 @@ impl Plugin for ScheduleRunnerPlugin { if let Some(app_exit_events) = app.world.get_resource_mut::>() { - if let Some(exit) = app_exit_event_reader.iter(&app_exit_events).last() + if let Some(exit) = app_exit_event_reader.read(&app_exit_events).last() { return Err(exit.clone()); } diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs index 637b0a2a81b5b..af342f1fca8f0 100644 --- a/crates/bevy_ecs/examples/events.rs +++ b/crates/bevy_ecs/examples/events.rs @@ -52,7 +52,7 @@ fn sending_system(mut event_writer: EventWriter) { // This system listens for events of the type MyEvent // If an event is received it will be printed to the console fn receiving_system(mut event_reader: EventReader) { - for my_event in event_reader.iter() { + for my_event in event_reader.read() { println!( " Received message {:?}, with random value of {}", my_event.message, my_event.random_value diff --git a/crates/bevy_ecs/src/event.rs b/crates/bevy_ecs/src/event.rs index 8f3bf91d3fe7d..0494555b04e65 100644 --- a/crates/bevy_ecs/src/event.rs +++ b/crates/bevy_ecs/src/event.rs @@ -403,13 +403,27 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// Iterates over the events this [`EventReader`] has not seen yet. This updates the /// [`EventReader`]'s event counter, which means subsequent event reads will not include events /// that happened before now. + pub fn read(&mut self) -> EventIterator<'_, E> { + self.reader.read(&self.events) + } + + /// Iterates over the events this [`EventReader`] has not seen yet. This updates the + /// [`EventReader`]'s event counter, which means subsequent event reads will not include events + /// that happened before now. + #[deprecated = "use `.read()` instead."] pub fn iter(&mut self) -> EventIterator<'_, E> { - self.reader.iter(&self.events) + self.reader.read(&self.events) + } + + /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. + pub fn read_with_id(&mut self) -> EventIteratorWithId<'_, E> { + self.reader.read_with_id(&self.events) } /// Like [`iter`](Self::iter), except also returning the [`EventId`] of the events. + #[deprecated = "use `.read_with_id() instead."] pub fn iter_with_id(&mut self) -> EventIteratorWithId<'_, E> { - self.reader.iter_with_id(&self.events) + self.reader.read_with_id(&self.events) } /// Determines the number of events available to be read from this [`EventReader`] without consuming any. @@ -545,12 +559,24 @@ impl Default for ManualEventReader { #[allow(clippy::len_without_is_empty)] // Check fails since the is_empty implementation has a signature other than `(&self) -> bool` impl ManualEventReader { + /// See [`EventReader::read`] + pub fn read<'a>(&'a mut self, events: &'a Events) -> EventIterator<'a, E> { + self.read_with_id(events).without_id() + } + /// See [`EventReader::iter`] + #[deprecated = "use `.read()` instead."] pub fn iter<'a>(&'a mut self, events: &'a Events) -> EventIterator<'a, E> { - self.iter_with_id(events).without_id() + self.read_with_id(events).without_id() + } + + /// See [`EventReader::read_with_id`] + pub fn read_with_id<'a>(&'a mut self, events: &'a Events) -> EventIteratorWithId<'a, E> { + EventIteratorWithId::new(self, events) } /// See [`EventReader::iter_with_id`] + #[deprecated = "use `.read_with_id() instead."] pub fn iter_with_id<'a>(&'a mut self, events: &'a Events) -> EventIteratorWithId<'a, E> { EventIteratorWithId::new(self, events) } @@ -834,7 +860,7 @@ mod tests { events: &Events, reader: &mut ManualEventReader, ) -> Vec { - reader.iter(events).cloned().collect::>() + reader.read(events).cloned().collect::>() } #[derive(Event, PartialEq, Eq, Debug)] @@ -844,21 +870,21 @@ mod tests { let mut events = Events::::default(); let mut reader = events.get_reader(); - assert!(reader.iter(&events).next().is_none()); + assert!(reader.read(&events).next().is_none()); events.send(E(0)); - assert_eq!(*reader.iter(&events).next().unwrap(), E(0)); - assert_eq!(reader.iter(&events).next(), None); + assert_eq!(*reader.read(&events).next().unwrap(), E(0)); + assert_eq!(reader.read(&events).next(), None); events.send(E(1)); clear_func(&mut events); - assert!(reader.iter(&events).next().is_none()); + assert!(reader.read(&events).next().is_none()); events.send(E(2)); events.update(); events.send(E(3)); - assert!(reader.iter(&events).eq([E(2), E(3)].iter())); + assert!(reader.read(&events).eq([E(2), E(3)].iter())); } #[test] @@ -880,7 +906,7 @@ mod tests { events.extend(vec![TestEvent { i: 0 }, TestEvent { i: 1 }]); assert!(reader - .iter(&events) + .read(&events) .eq([TestEvent { i: 0 }, TestEvent { i: 1 }].iter())); } @@ -923,7 +949,7 @@ mod tests { events.send(TestEvent { i: 1 }); events.send(TestEvent { i: 2 }); let mut reader = events.get_reader(); - let mut iter = reader.iter(&events); + let mut iter = reader.read(&events); assert_eq!(iter.len(), 3); iter.next(); assert_eq!(iter.len(), 2); @@ -994,13 +1020,13 @@ mod tests { events.send(TestEvent { i: 0 }); events.send(TestEvent { i: 1 }); - assert_eq!(reader.iter(&events).count(), 2); + assert_eq!(reader.read(&events).count(), 2); let mut old_events = Vec::from_iter(events.update_drain()); assert!(old_events.is_empty()); events.send(TestEvent { i: 2 }); - assert_eq!(reader.iter(&events).count(), 1); + assert_eq!(reader.read(&events).count(), 1); old_events.extend(events.update_drain()); assert_eq!(old_events.len(), 2); @@ -1028,7 +1054,7 @@ mod tests { let mut schedule = Schedule::default(); schedule.add_systems(|mut events: EventReader| { - let mut iter = events.iter(); + let mut iter = events.read(); assert_eq!(iter.next(), Some(&TestEvent { i: 0 })); assert_eq!(iter.nth(2), Some(&TestEvent { i: 3 })); @@ -1048,7 +1074,7 @@ mod tests { let mut reader = IntoSystem::into_system(|mut events: EventReader| -> Option { - events.iter().last().copied() + events.read().last().copied() }); reader.initialize(&mut world); diff --git a/crates/bevy_ecs/src/removal_detection.rs b/crates/bevy_ecs/src/removal_detection.rs index cd58cfd019343..0118ab8dedc55 100644 --- a/crates/bevy_ecs/src/removal_detection.rs +++ b/crates/bevy_ecs/src/removal_detection.rs @@ -202,7 +202,7 @@ impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { /// that happened before now. pub fn iter(&mut self) -> RemovedIter<'_> { self.reader_mut_with_events() - .map(|(reader, events)| reader.iter(events).cloned()) + .map(|(reader, events)| reader.read(events).cloned()) .into_iter() .flatten() .map(RemovedComponentEntity::into) @@ -211,7 +211,7 @@ impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { /// Like [`iter`](Self::iter), except also returning the [`EventId`] of the events. pub fn iter_with_id(&mut self) -> RemovedIterWithId<'_> { self.reader_mut_with_events() - .map(|(reader, events)| reader.iter_with_id(events)) + .map(|(reader, events)| reader.read_with_id(events)) .into_iter() .flatten() .map(map_id_events) diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index f8ad50a5bbf71..6c3b8e80fec2a 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -895,7 +895,7 @@ pub mod common_conditions { // calls of the run condition. Simply checking `is_empty` would not be enough. // PERF: note that `count` is efficient (not actually looping/iterating), // due to Bevy having a specialized implementation for events. - move |mut reader: EventReader| reader.iter().count() > 0 + move |mut reader: EventReader| reader.read().count() > 0 } /// Generates a [`Condition`](super::Condition)-satisfying closure that returns `true` diff --git a/crates/bevy_gilrs/src/rumble.rs b/crates/bevy_gilrs/src/rumble.rs index 3f3e8aa4c7f18..310a80d8f7f5a 100644 --- a/crates/bevy_gilrs/src/rumble.rs +++ b/crates/bevy_gilrs/src/rumble.rs @@ -136,7 +136,7 @@ pub(crate) fn play_gilrs_rumble( .retain(|_gamepad, rumbles| !rumbles.is_empty()); // Add new effects. - for rumble in requests.iter().cloned() { + for rumble in requests.read().cloned() { let gamepad = rumble.gamepad(); match handle_rumble_request(&mut running_rumbles, &mut gilrs, rumble, current_time) { Ok(()) => {} diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index 3e9857504c383..b3356544d8634 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -1023,7 +1023,7 @@ pub fn gamepad_connection_system( mut button_axis: ResMut>, mut button_input: ResMut>, ) { - for connection_event in connection_events.iter() { + for connection_event in connection_events.read() { let gamepad = connection_event.gamepad; if let GamepadConnection::Connected(info) = &connection_event.connection { @@ -1168,7 +1168,7 @@ pub fn gamepad_axis_event_system( mut gamepad_axis: ResMut>, mut axis_events: EventReader, ) { - for axis_event in axis_events.iter() { + for axis_event in axis_events.read() { let axis = GamepadAxis::new(axis_event.gamepad, axis_event.axis_type); gamepad_axis.set(axis, axis_event.value); } @@ -1181,7 +1181,7 @@ pub fn gamepad_button_event_system( mut button_input_events: EventWriter, settings: Res, ) { - for button_event in button_changed_events.iter() { + for button_event in button_changed_events.read() { let button = GamepadButton::new(button_event.gamepad, button_event.button_type); let value = button_event.value; let button_property = settings.get_button_settings(button); @@ -1258,7 +1258,7 @@ pub fn gamepad_event_system( mut button_input: ResMut>, ) { button_input.bypass_change_detection().clear(); - for gamepad_event in gamepad_events.iter() { + for gamepad_event in gamepad_events.read() { match gamepad_event { GamepadEvent::Connection(connection_event) => { connection_events.send(connection_event.clone()); diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index 1c5648c7687ac..b54b5dfdf4800 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -53,7 +53,7 @@ pub fn keyboard_input_system( // Avoid clearing if it's not empty to ensure change detection is not triggered. scan_input.bypass_change_detection().clear(); key_input.bypass_change_detection().clear(); - for event in keyboard_input_events.iter() { + for event in keyboard_input_events.read() { let KeyboardInput { scan_code, state, .. } = event; diff --git a/crates/bevy_input/src/mouse.rs b/crates/bevy_input/src/mouse.rs index 062a1ed7647aa..93bee34fc61bb 100644 --- a/crates/bevy_input/src/mouse.rs +++ b/crates/bevy_input/src/mouse.rs @@ -144,7 +144,7 @@ pub fn mouse_button_input_system( mut mouse_button_input_events: EventReader, ) { mouse_button_input.bypass_change_detection().clear(); - for event in mouse_button_input_events.iter() { + for event in mouse_button_input_events.read() { match event.state { ButtonState::Pressed => mouse_button_input.press(event.button), ButtonState::Released => mouse_button_input.release(event.button), diff --git a/crates/bevy_input/src/touch.rs b/crates/bevy_input/src/touch.rs index f3660233e66b5..0f61787c66c62 100644 --- a/crates/bevy_input/src/touch.rs +++ b/crates/bevy_input/src/touch.rs @@ -366,7 +366,7 @@ pub fn touch_screen_input_system( ) { touch_state.update(); - for event in touch_input_events.iter() { + for event in touch_input_events.read() { touch_state.process_touch_event(event); } } diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 9c4e82d5de7d7..68361938dec16 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -608,7 +608,7 @@ pub fn extract_materials( ) { let mut changed_assets = HashSet::default(); let mut removed = Vec::new(); - for event in events.iter() { + for event in events.read() { match event { AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { changed_assets.insert(handle.clone_weak()); diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index d6aa154dc174d..5fbf037d1fe1d 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -575,11 +575,11 @@ pub fn camera_system( let primary_window = primary_window.iter().next(); let mut changed_window_ids = HashSet::new(); - changed_window_ids.extend(window_created_events.iter().map(|event| event.window)); - changed_window_ids.extend(window_resized_events.iter().map(|event| event.window)); + changed_window_ids.extend(window_created_events.read().map(|event| event.window)); + changed_window_ids.extend(window_resized_events.read().map(|event| event.window)); let changed_image_handles: HashSet<&Handle> = image_asset_events - .iter() + .read() .filter_map(|event| { if let AssetEvent::Modified { handle } = event { Some(handle) diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index a39a7ea3bdc11..0b6f17048fe36 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -136,7 +136,7 @@ fn extract_render_asset( ) { let mut changed_assets = HashSet::default(); let mut removed = Vec::new(); - for event in events.iter() { + for event in events.read() { match event { AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { changed_assets.insert(handle.clone_weak()); diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index d1fed0fdba78e..2ab51da14ee77 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -832,7 +832,7 @@ impl PipelineCache { shaders: Extract>>, mut events: Extract>>, ) { - for event in events.iter() { + for event in events.read() { match event { AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { if let Some(shader) = shaders.get(handle) { diff --git a/crates/bevy_render/src/view/window/mod.rs b/crates/bevy_render/src/view/window/mod.rs index 911214ecbaf01..213f609e5c9e0 100644 --- a/crates/bevy_render/src/view/window/mod.rs +++ b/crates/bevy_render/src/view/window/mod.rs @@ -161,7 +161,7 @@ fn extract_windows( } } - for closed_window in closed.iter() { + for closed_window in closed.read() { extracted_windows.remove(&closed_window.window); } // This lock will never block because `callbacks` is `pub(crate)` and this is the singular callsite where it's locked. diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 3496e867598a8..cbcf7ca0d9022 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -354,7 +354,7 @@ pub fn scene_spawner_system(world: &mut World) { let scene_spawner = &mut *scene_spawner; for event in scene_spawner .scene_asset_event_reader - .iter(scene_asset_events) + .read(scene_asset_events) { if let AssetEvent::Modified { handle } = event { if scene_spawner.spawned_dynamic_scenes.contains_key(handle) { diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index ffe8ae670e0cd..8e7ca92bf79da 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -467,7 +467,7 @@ pub fn extract_materials_2d( ) { let mut changed_assets = HashSet::default(); let mut removed = Vec::new(); - for event in events.iter() { + for event in events.read() { match event { AssetEvent::Created { handle } | AssetEvent::Modified { handle } => { changed_assets.insert(handle.clone_weak()); diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index f00f63f1a69a9..1468e30a72998 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -327,7 +327,7 @@ pub fn extract_sprite_events( let SpriteAssetEvents { ref mut images } = *events; images.clear(); - for image in image_events.iter() { + for image in image_events.read() { // AssetEvent: !Clone images.push(match image { AssetEvent::Created { handle } => AssetEvent::Created { diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 5587355d90d2c..602e4c449c27e 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -165,7 +165,7 @@ pub fn update_text2d_layout( mut text_query: Query<(Entity, Ref, Ref, &mut TextLayoutInfo)>, ) { // We need to consume the entire iterator, hence `last` - let factor_changed = scale_factor_changed.iter().last().is_some(); + let factor_changed = scale_factor_changed.read().last().is_some(); // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 let scale_factor = windows diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index e48100091132d..4ed138c1f3681 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -248,7 +248,7 @@ pub fn ui_layout_system( }; let resized = resize_events - .iter() + .read() .any(|resized_window| resized_window.window == primary_window_entity); // update window root nodes diff --git a/crates/bevy_window/src/system.rs b/crates/bevy_window/src/system.rs index a077d93753625..c08ec306f2ba9 100644 --- a/crates/bevy_window/src/system.rs +++ b/crates/bevy_window/src/system.rs @@ -41,7 +41,7 @@ pub fn exit_on_primary_closed( /// /// [`WindowPlugin`]: crate::WindowPlugin pub fn close_when_requested(mut commands: Commands, mut closed: EventReader) { - for event in closed.iter() { + for event in closed.read() { commands.entity(event.window).despawn(); } } diff --git a/crates/bevy_winit/src/accessibility.rs b/crates/bevy_winit/src/accessibility.rs index 49e7a0f11c65b..49a4c0d859c6d 100644 --- a/crates/bevy_winit/src/accessibility.rs +++ b/crates/bevy_winit/src/accessibility.rs @@ -46,7 +46,7 @@ fn handle_window_focus( adapters: NonSend, mut focused: EventReader, ) { - for event in focused.iter() { + for event in focused.read() { if let Some(adapter) = adapters.get(&event.window) { adapter.update_if_active(|| { let focus_id = (*focus).unwrap_or_else(|| event.window); @@ -68,7 +68,7 @@ fn window_closed( mut receivers: ResMut, mut events: EventReader, ) { - for WindowClosed { window, .. } in events.iter() { + for WindowClosed { window, .. } in events.read() { adapters.remove(window); receivers.remove(window); } diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 8c79494dec350..8479697969250 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -356,7 +356,7 @@ pub fn winit_runner(mut app: App) { } if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader.iter(app_exit_events).last().is_some() { + if app_exit_event_reader.read(app_exit_events).last().is_some() { *control_flow = ControlFlow::Exit; return; } @@ -723,14 +723,14 @@ pub fn winit_runner(mut app: App) { if let Some(app_redraw_events) = app.world.get_resource::>() { - if redraw_event_reader.iter(app_redraw_events).last().is_some() { + if redraw_event_reader.read(app_redraw_events).last().is_some() { runner_state.redraw_requested = true; *control_flow = ControlFlow::Poll; } } if let Some(app_exit_events) = app.world.get_resource::>() { - if app_exit_event_reader.iter(app_exit_events).last().is_some() { + if app_exit_event_reader.read(app_exit_events).last().is_some() { *control_flow = ControlFlow::Exit; } } diff --git a/examples/3d/shadow_biases.rs b/examples/3d/shadow_biases.rs index 920a4cf111e03..af175cd8e9d88 100644 --- a/examples/3d/shadow_biases.rs +++ b/examples/3d/shadow_biases.rs @@ -277,7 +277,7 @@ fn camera_controller( // Handle mouse input let mut mouse_delta = Vec2::ZERO; - for mouse_event in mouse_events.iter() { + for mouse_event in mouse_events.read() { mouse_delta += mouse_event.delta; } diff --git a/examples/3d/skybox.rs b/examples/3d/skybox.rs index 7cc6b91c4b5f6..619550f56b4d5 100644 --- a/examples/3d/skybox.rs +++ b/examples/3d/skybox.rs @@ -291,7 +291,7 @@ pub fn camera_controller( // Handle mouse input let mut mouse_delta = Vec2::ZERO; if mouse_button_input.pressed(options.mouse_key_enable_mouse) || *move_toggled { - for mouse_event in mouse_events.iter() { + for mouse_event in mouse_events.read() { mouse_delta += mouse_event.delta; } } diff --git a/examples/3d/split_screen.rs b/examples/3d/split_screen.rs index 944812affe491..1a0f215d6464f 100644 --- a/examples/3d/split_screen.rs +++ b/examples/3d/split_screen.rs @@ -95,7 +95,7 @@ fn set_camera_viewports( // We need to dynamically resize the camera's viewports whenever the window size changes // so then each camera always takes up half the screen. // A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup. - for resize_event in resize_events.iter() { + for resize_event in resize_events.read() { let window = windows.get(resize_event.window).unwrap(); let mut left_camera = left_camera.single_mut(); left_camera.viewport = Some(Viewport { diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index d2f87b079a257..687efaaef5845 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -307,7 +307,7 @@ fn update_image_viewer( ) { let mut new_image: Option> = None; - for event in drop_events.iter() { + for event in drop_events.read() { match event { FileDragAndDrop::DroppedFile { path_buf, .. } => { new_image = Some(asset_server.load(path_buf.to_string_lossy().to_string())); @@ -328,7 +328,7 @@ fn update_image_viewer( } } - for event in image_events.iter() { + for event in image_events.read() { let image_changed_h = match event { AssetEvent::Created { handle } | AssetEvent::Modified { handle } => handle, _ => continue, diff --git a/examples/app/drag_and_drop.rs b/examples/app/drag_and_drop.rs index 0abf98ccefcf3..fcd02e1011b35 100644 --- a/examples/app/drag_and_drop.rs +++ b/examples/app/drag_and_drop.rs @@ -10,7 +10,7 @@ fn main() { } fn file_drag_and_drop_system(mut events: EventReader) { - for event in events.iter() { + for event in events.read() { info!("{:?}", event); } } diff --git a/examples/async_tasks/external_source_external_thread.rs b/examples/async_tasks/external_source_external_thread.rs index 841f6b920e21f..64b9ea6cdafff 100644 --- a/examples/async_tasks/external_source_external_thread.rs +++ b/examples/async_tasks/external_source_external_thread.rs @@ -57,7 +57,7 @@ fn spawn_text(mut commands: Commands, mut reader: EventReader) { ..default() }; - for (per_frame, event) in reader.iter().enumerate() { + for (per_frame, event) in reader.read().enumerate() { commands.spawn(Text2dBundle { text: Text::from_section(event.0.to_string(), text_style.clone()) .with_alignment(TextAlignment::Center), diff --git a/examples/audio/pitch.rs b/examples/audio/pitch.rs index 083e4a359dbab..218ae5500f602 100644 --- a/examples/audio/pitch.rs +++ b/examples/audio/pitch.rs @@ -28,7 +28,7 @@ fn play_pitch( mut events: EventReader, mut commands: Commands, ) { - for _ in events.iter() { + for _ in events.read() { info!("playing pitch with frequency: {}", frequency.0); commands.spawn(PitchBundle { source: pitch_assets.add(Pitch::new(frequency.0, Duration::new(1, 0))), diff --git a/examples/ecs/event.rs b/examples/ecs/event.rs index 8f7d68d5b0feb..7d5abd1856629 100644 --- a/examples/ecs/event.rs +++ b/examples/ecs/event.rs @@ -51,13 +51,13 @@ fn event_trigger( // prints events as they come in fn event_listener(mut events: EventReader) { - for my_event in events.iter() { + for my_event in events.read() { info!("{}", my_event.message); } } fn sound_player(mut play_sound_events: EventReader) { - for _ in play_sound_events.iter() { + for _ in play_sound_events.read() { info!("Playing a sound"); } } diff --git a/examples/input/char_input_events.rs b/examples/input/char_input_events.rs index 141f4ce806dd7..bdaefb7233688 100644 --- a/examples/input/char_input_events.rs +++ b/examples/input/char_input_events.rs @@ -11,7 +11,7 @@ fn main() { /// This system prints out all char events as they come in fn print_char_event_system(mut char_input_events: EventReader) { - for event in char_input_events.iter() { + for event in char_input_events.read() { info!("{:?}: '{}'", event, event.char); } } diff --git a/examples/input/gamepad_input_events.rs b/examples/input/gamepad_input_events.rs index cf3971c6a9cc2..73bd6ffef95c8 100644 --- a/examples/input/gamepad_input_events.rs +++ b/examples/input/gamepad_input_events.rs @@ -27,16 +27,16 @@ fn gamepad_events( // this event is emmitted. mut button_input_events: EventReader, ) { - for connection_event in connection_events.iter() { + for connection_event in connection_events.read() { info!("{:?}", connection_event); } - for axis_changed_event in axis_changed_events.iter() { + for axis_changed_event in axis_changed_events.read() { info!( "{:?} of {:?} is changed to {}", axis_changed_event.axis_type, axis_changed_event.gamepad, axis_changed_event.value ); } - for button_changed_event in button_changed_events.iter() { + for button_changed_event in button_changed_events.read() { info!( "{:?} of {:?} is changed to {}", button_changed_event.button_type, @@ -44,7 +44,7 @@ fn gamepad_events( button_changed_event.value ); } - for button_input_event in button_input_events.iter() { + for button_input_event in button_input_events.read() { info!("{:?}", button_input_event); } } @@ -53,7 +53,7 @@ fn gamepad_events( // stream directly. For standard use-cases, reading the events individually or using the // `Input` or `Axis` resources is preferable. fn gamepad_ordered_events(mut gamepad_events: EventReader) { - for gamepad_event in gamepad_events.iter() { + for gamepad_event in gamepad_events.read() { match gamepad_event { GamepadEvent::Connection(connection_event) => info!("{:?}", connection_event), GamepadEvent::Button(button_event) => info!("{:?}", button_event), diff --git a/examples/input/keyboard_input_events.rs b/examples/input/keyboard_input_events.rs index bac3b68316c9f..bba305a2d6af4 100644 --- a/examples/input/keyboard_input_events.rs +++ b/examples/input/keyboard_input_events.rs @@ -11,7 +11,7 @@ fn main() { /// This system prints out all keyboard events as they come in fn print_keyboard_event_system(mut keyboard_input_events: EventReader) { - for event in keyboard_input_events.iter() { + for event in keyboard_input_events.read() { info!("{:?}", event); } } diff --git a/examples/input/mouse_input_events.rs b/examples/input/mouse_input_events.rs index 61ba2a3d13703..f1081f41c4436 100644 --- a/examples/input/mouse_input_events.rs +++ b/examples/input/mouse_input_events.rs @@ -24,29 +24,29 @@ fn print_mouse_events_system( mut touchpad_magnify_events: EventReader, mut touchpad_rotate_events: EventReader, ) { - for event in mouse_button_input_events.iter() { + for event in mouse_button_input_events.read() { info!("{:?}", event); } - for event in mouse_motion_events.iter() { + for event in mouse_motion_events.read() { info!("{:?}", event); } - for event in cursor_moved_events.iter() { + for event in cursor_moved_events.read() { info!("{:?}", event); } - for event in mouse_wheel_events.iter() { + for event in mouse_wheel_events.read() { info!("{:?}", event); } // This event will only fire on macOS - for event in touchpad_magnify_events.iter() { + for event in touchpad_magnify_events.read() { info!("{:?}", event); } // This event will only fire on macOS - for event in touchpad_rotate_events.iter() { + for event in touchpad_rotate_events.read() { info!("{:?}", event); } } diff --git a/examples/input/text_input.rs b/examples/input/text_input.rs index 8140325094df2..8e317157f6334 100644 --- a/examples/input/text_input.rs +++ b/examples/input/text_input.rs @@ -142,7 +142,7 @@ fn listen_ime_events( mut status_text: Query<&mut Text, With>, mut edit_text: Query<&mut Text, (Without, Without)>, ) { - for event in events.iter() { + for event in events.read() { match event { Ime::Preedit { value, cursor, .. } if !cursor.is_none() => { status_text.single_mut().sections[5].value = format!("IME buffer: {value}"); @@ -168,7 +168,7 @@ fn listen_received_character_events( mut events: EventReader, mut edit_text: Query<&mut Text, (Without, Without)>, ) { - for event in events.iter() { + for event in events.read() { edit_text.single_mut().sections[0].value.push(event.char); } } @@ -178,7 +178,7 @@ fn listen_keyboard_input_events( mut events: EventReader, mut edit_text: Query<(Entity, &mut Text), (Without, Without)>, ) { - for event in events.iter() { + for event in events.read() { match event.key_code { Some(KeyCode::Return) => { let (entity, text) = edit_text.single(); diff --git a/examples/input/touch_input_events.rs b/examples/input/touch_input_events.rs index b8e918334e3cf..2c6d7e25b1d4e 100644 --- a/examples/input/touch_input_events.rs +++ b/examples/input/touch_input_events.rs @@ -10,7 +10,7 @@ fn main() { } fn touch_event_system(mut touch_events: EventReader) { - for event in touch_events.iter() { + for event in touch_events.read() { info!("{:?}", event); } } diff --git a/examples/mobile/src/lib.rs b/examples/mobile/src/lib.rs index 6e513942d19bf..e08e6804705cc 100644 --- a/examples/mobile/src/lib.rs +++ b/examples/mobile/src/lib.rs @@ -31,7 +31,7 @@ fn touch_camera( ) { let window = windows.single(); - for touch in touches.iter() { + for touch in touches.read() { if touch.phase == TouchPhase::Started { *last_position = None; } diff --git a/examples/tools/gamepad_viewer.rs b/examples/tools/gamepad_viewer.rs index d9e2771400ef4..9b2eb0e7b05f6 100644 --- a/examples/tools/gamepad_viewer.rs +++ b/examples/tools/gamepad_viewer.rs @@ -466,7 +466,7 @@ fn update_button_values( mut events: EventReader, mut query: Query<(&mut Text, &TextWithButtonValue)>, ) { - for button_event in events.iter() { + for button_event in events.read() { for (mut text, text_with_button_value) in query.iter_mut() { if button_event.button_type == **text_with_button_value { text.sections[0].value = format!("{:.3}", button_event.value); @@ -480,7 +480,7 @@ fn update_axes( mut query: Query<(&mut Transform, &MoveWithAxes)>, mut text_query: Query<(&mut Text, &TextWithAxes)>, ) { - for axis_event in axis_events.iter() { + for axis_event in axis_events.read() { let axis_type = axis_event.axis_type; let value = axis_event.value; for (mut transform, move_with) in query.iter_mut() { diff --git a/examples/tools/scene_viewer/camera_controller_plugin.rs b/examples/tools/scene_viewer/camera_controller_plugin.rs index f786b2f04b80d..20adab24f9465 100644 --- a/examples/tools/scene_viewer/camera_controller_plugin.rs +++ b/examples/tools/scene_viewer/camera_controller_plugin.rs @@ -174,7 +174,7 @@ fn camera_controller( window.cursor.visible = false; } - for mouse_event in mouse_events.iter() { + for mouse_event in mouse_events.read() { mouse_delta += mouse_event.delta; } } diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index 41a57224fc17d..5f0892c14afa8 100644 --- a/examples/ui/size_constraints.rs +++ b/examples/ui/size_constraints.rs @@ -364,7 +364,7 @@ fn update_radio_buttons_colors( mut text_query: Query<&mut Text>, children_query: Query<&Children>, ) { - for &ButtonActivatedEvent(button_id) in event_reader.iter() { + for &ButtonActivatedEvent(button_id) in event_reader.read() { let target_constraint = button_query.get_component::(button_id).unwrap(); for (id, constraint, interaction) in button_query.iter() { if target_constraint == constraint { diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index 2a0478b6589b0..2dea49e6eac6a 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -316,7 +316,7 @@ fn mouse_scroll( mut query_list: Query<(&mut ScrollingList, &mut Style, &Parent, &Node)>, query_node: Query<&Node>, ) { - for mouse_wheel_event in mouse_wheel_events.iter() { + for mouse_wheel_event in mouse_wheel_events.read() { for (mut scrolling_list, mut style, parent, list_node) in &mut query_list { let items_height = list_node.size().y; let container_height = query_node.get(parent.get()).unwrap().size().y; diff --git a/examples/window/window_resizing.rs b/examples/window/window_resizing.rs index 5281454765db6..35c7c6d17937c 100644 --- a/examples/window/window_resizing.rs +++ b/examples/window/window_resizing.rs @@ -86,7 +86,7 @@ fn on_resize_system( mut resize_reader: EventReader, ) { let mut text = q.single_mut(); - for e in resize_reader.iter() { + for e in resize_reader.read() { // When resolution is being changed text.sections[0].value = format!("{:.1} x {:.1}", e.width, e.height); } diff --git a/tests/how_to_test_systems.rs b/tests/how_to_test_systems.rs index 192aff7cd2f39..23b29bd550c2b 100644 --- a/tests/how_to_test_systems.rs +++ b/tests/how_to_test_systems.rs @@ -13,7 +13,7 @@ struct EnemyDied(u32); struct Score(u32); fn update_score(mut dead_enemies: EventReader, mut score: ResMut) { - for value in dead_enemies.iter() { + for value in dead_enemies.read() { score.0 += value.0; } } @@ -109,7 +109,7 @@ fn did_despawn_enemy() { // Get `EnemyDied` event reader let enemy_died_events = app.world.resource::>(); let mut enemy_died_reader = enemy_died_events.get_reader(); - let enemy_died = enemy_died_reader.iter(enemy_died_events).next().unwrap(); + let enemy_died = enemy_died_reader.read(enemy_died_events).next().unwrap(); // Check the event has been sent assert_eq!(enemy_died.0, 1); From 36eedbfa92023fad94fd218de195196f642935a4 Mon Sep 17 00:00:00 2001 From: Emi <95967983+EmiOnGit@users.noreply.github.com> Date: Wed, 30 Aug 2023 19:31:30 +0200 Subject: [PATCH 52/58] Change `Urect::width` & `Urect::height` to be const (#9640) # Objective The two functions [`Urect::height`](https://docs.rs/bevy_math/latest/bevy_math/struct.URect.html#method.height), [`Urect::width`](https://docs.rs/bevy_math/latest/bevy_math/struct.URect.html#method.width) are currently not const. Since the methods are very unlikely to change (ever) and are useful to be const for some games, they should be. Co-authored-by: Emi --- crates/bevy_math/src/rects/urect.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/rects/urect.rs b/crates/bevy_math/src/rects/urect.rs index fb74a6183e9b0..73517de3056e4 100644 --- a/crates/bevy_math/src/rects/urect.rs +++ b/crates/bevy_math/src/rects/urect.rs @@ -130,7 +130,7 @@ impl URect { /// assert_eq!(r.width(), 5); /// ``` #[inline] - pub fn width(&self) -> u32 { + pub const fn width(&self) -> u32 { self.max.x - self.min.x } @@ -144,7 +144,7 @@ impl URect { /// assert_eq!(r.height(), 1); /// ``` #[inline] - pub fn height(&self) -> u32 { + pub const fn height(&self) -> u32 { self.max.y - self.min.y } From 23598d7bec2904252c28cba5e9b57d56a9151ed4 Mon Sep 17 00:00:00 2001 From: Joseph <21144246+JoJoJet@users.noreply.github.com> Date: Wed, 30 Aug 2023 18:33:13 -0700 Subject: [PATCH 53/58] Add a method to compute a bounding box enclosing a set of points (#9630) # Objective Make it easier to create bounding boxes in user code by providing a constructor that computes a box surrounding an arbitrary number of points. ## Solution Add `Aabb::enclosing`, which accepts iterators, slices, or arrays. --------- Co-authored-by: Tristan Guichaoua <33934311+tguichaoua@users.noreply.github.com> --- crates/bevy_render/src/mesh/mesh/mod.rs | 27 +++--------- crates/bevy_render/src/primitives/mod.rs | 52 +++++++++++++++++++++++- 2 files changed, 56 insertions(+), 23 deletions(-) diff --git a/crates/bevy_render/src/mesh/mesh/mod.rs b/crates/bevy_render/src/mesh/mesh/mod.rs index 84ad6eb0deda2..49c87576915ec 100644 --- a/crates/bevy_render/src/mesh/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mesh/mod.rs @@ -464,27 +464,13 @@ impl Mesh { /// Compute the Axis-Aligned Bounding Box of the mesh vertices in model space pub fn compute_aabb(&self) -> Option { - if let Some(VertexAttributeValues::Float32x3(values)) = + let Some(VertexAttributeValues::Float32x3(values)) = self.attribute(Mesh::ATTRIBUTE_POSITION) - { - let mut minimum = VEC3_MAX; - let mut maximum = VEC3_MIN; - for p in values { - minimum = minimum.min(Vec3::from_slice(p)); - maximum = maximum.max(Vec3::from_slice(p)); - } - if minimum.x != std::f32::MAX - && minimum.y != std::f32::MAX - && minimum.z != std::f32::MAX - && maximum.x != std::f32::MIN - && maximum.y != std::f32::MIN - && maximum.z != std::f32::MIN - { - return Some(Aabb::from_min_max(minimum, maximum)); - } - } + else { + return None; + }; - None + Aabb::enclosing(values.iter().map(|p| Vec3::from_slice(p))) } /// Whether this mesh has morph targets. @@ -635,9 +621,6 @@ struct MeshAttributeData { values: VertexAttributeValues, } -const VEC3_MIN: Vec3 = Vec3::splat(std::f32::MIN); -const VEC3_MAX: Vec3 = Vec3::splat(std::f32::MAX); - fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { let (a, b, c) = (Vec3::from(a), Vec3::from(b), Vec3::from(c)); (b - a).cross(c - a).normalize().into() diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index d150fe08e997f..8b74959dc6c3f 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -1,3 +1,5 @@ +use std::borrow::Borrow; + use bevy_ecs::{component::Component, prelude::Entity, reflect::ReflectComponent}; use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles}; use bevy_reflect::Reflect; @@ -29,7 +31,7 @@ use bevy_utils::HashMap; /// [`CalculateBounds`]: crate::view::visibility::VisibilitySystems::CalculateBounds /// [`Mesh`]: crate::mesh::Mesh /// [`Handle`]: crate::mesh::Mesh -#[derive(Component, Clone, Copy, Debug, Default, Reflect)] +#[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)] #[reflect(Component)] pub struct Aabb { pub center: Vec3A, @@ -49,6 +51,30 @@ impl Aabb { } } + /// Returns a bounding box enclosing the specified set of points. + /// + /// Returns `None` if the iterator is empty. + /// + /// # Examples + /// + /// ``` + /// # use bevy_math::{Vec3, Vec3A}; + /// # use bevy_render::primitives::Aabb; + /// let bb = Aabb::enclosing([Vec3::X, Vec3::Z * 2.0, Vec3::Y * -0.5]).unwrap(); + /// assert_eq!(bb.min(), Vec3A::new(0.0, -0.5, 0.0)); + /// assert_eq!(bb.max(), Vec3A::new(1.0, 0.0, 2.0)); + /// ``` + pub fn enclosing>(iter: impl IntoIterator) -> Option { + let mut iter = iter.into_iter().map(|p| *p.borrow()); + let mut min = iter.next()?; + let mut max = min; + for v in iter { + min = Vec3::min(min, v); + max = Vec3::max(max, v); + } + Some(Self::from_min_max(min, max)) + } + /// Calculate the relative radius of the AABB with respect to a plane #[inline] pub fn relative_radius(&self, p_normal: &Vec3A, model: &Mat3A) -> f32 { @@ -455,4 +481,28 @@ mod tests { }; assert!(frustum.intersects_sphere(&sphere, true)); } + + #[test] + fn aabb_enclosing() { + assert_eq!(Aabb::enclosing(<[Vec3; 0]>::default()), None); + assert_eq!( + Aabb::enclosing(vec![Vec3::ONE]).unwrap(), + Aabb::from_min_max(Vec3::ONE, Vec3::ONE) + ); + assert_eq!( + Aabb::enclosing(&[Vec3::Y, Vec3::X, Vec3::Z][..]).unwrap(), + Aabb::from_min_max(Vec3::ZERO, Vec3::ONE) + ); + assert_eq!( + Aabb::enclosing([ + Vec3::NEG_X, + Vec3::X * 2.0, + Vec3::NEG_Y * 5.0, + Vec3::Z, + Vec3::ZERO + ]) + .unwrap(), + Aabb::from_min_max(Vec3::new(-1.0, -5.0, 0.0), Vec3::new(2.0, 0.0, 1.0)) + ); + } } From 4bf9b7bde3003bc9e9775e21f977fe448b49a489 Mon Sep 17 00:00:00 2001 From: ickk Date: Thu, 31 Aug 2023 17:20:53 +1000 Subject: [PATCH 54/58] fix typo (#9651) plural "windows" in message where it should be singular "window" --- crates/bevy_window/src/system.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_window/src/system.rs b/crates/bevy_window/src/system.rs index c08ec306f2ba9..b7dec35a102eb 100644 --- a/crates/bevy_window/src/system.rs +++ b/crates/bevy_window/src/system.rs @@ -28,7 +28,7 @@ pub fn exit_on_primary_closed( windows: Query<(), (With, With)>, ) { if windows.is_empty() { - bevy_utils::tracing::info!("Primary windows was closed, exiting"); + bevy_utils::tracing::info!("Primary window was closed, exiting"); app_exit_events.send(AppExit); } } From ee3cc8ca86a40cbfdad1883f29f1c4be975b8ac8 Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Thu, 31 Aug 2023 14:55:17 +0200 Subject: [PATCH 55/58] Fix erronenous glam version (#9653) # Objective - Fix compilation issue with wrongly specified glam version - bevy uses `Vec2::INFINITY`, depends on `0.24` (equivalent to `0.24.0`) yet it was only introduced in version `0.24.1` Context: https://discord.com/channels/691052431525675048/692572690833473578/1146586570787397794 ## Solution - Bump glam version. --- benches/Cargo.toml | 2 +- crates/bevy_math/Cargo.toml | 2 +- crates/bevy_mikktspace/Cargo.toml | 2 +- crates/bevy_reflect/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 5842072a4ac1f..f060d83ebfda8 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -7,7 +7,7 @@ publish = false license = "MIT OR Apache-2.0" [dev-dependencies] -glam = "0.24" +glam = "0.24.1" rand = "0.8" rand_chacha = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index ac74611a80715..f627ec9e6dfbc 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [dependencies] -glam = { version = "0.24", features = ["bytemuck"] } +glam = { version = "0.24.1", features = ["bytemuck"] } serde = { version = "1", features = ["derive"], optional = true } [features] diff --git a/crates/bevy_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index e5471e7bc3451..8bd4afcd7753b 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -11,7 +11,7 @@ license = "Zlib AND (MIT OR Apache-2.0)" keywords = ["bevy", "3D", "graphics", "algorithm", "tangent"] [dependencies] -glam = "0.24" +glam = "0.24.1" [[example]] name = "generate" diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 99c443a3cd0e1..33b4ad6a1cf0c 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -37,7 +37,7 @@ smallvec = { version = "1.6", features = [ "union", "const_generics", ], optional = true } -glam = { version = "0.24", features = ["serde"], optional = true } +glam = { version = "0.24.1", features = ["serde"], optional = true } smol_str = { version = "0.2.0", optional = true } [dev-dependencies] From 11567b31f947a00a56e788bf794ffac43ea23187 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 31 Aug 2023 20:02:33 +0100 Subject: [PATCH 56/58] Change `Window::physical_cursor_position` to use the physical size of the window (#9657) # Objective `Window::physical_cursor_position` checks to see if the cursor's position is inside the window but it constructs the bounding rect for the window using its logical size and then checks to see if it contains the cursor's physical position. When the physical size is smaller than the logical size, this leaves a dead zone where the cursor is over the window but its position is unreported. fixes: #9656 ## Solution Use the physical size of the window. --- crates/bevy_window/src/window.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 10420f44c0cdb..c4e06fbc56a41 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -330,7 +330,14 @@ impl Window { match self.internal.physical_cursor_position { Some(position) => { let position = position.as_vec2(); - if Rect::new(0., 0., self.width(), self.height()).contains(position) { + if Rect::new( + 0., + 0., + self.physical_width() as f32, + self.physical_height() as f32, + ) + .contains(position) + { Some(position) } else { None From 3527288342bd9a40c2c602576921cc70e34aedba Mon Sep 17 00:00:00 2001 From: Emi <95967983+EmiOnGit@users.noreply.github.com> Date: Thu, 31 Aug 2023 21:05:49 +0200 Subject: [PATCH 57/58] Fixing some doc comments (#9646) # Objective I've been collecting some mistakes in the documentation and fixed them --------- Co-authored-by: Emi Co-authored-by: Nicola Papale --- crates/bevy_animation/src/lib.rs | 8 ++++---- crates/bevy_asset/src/assets.rs | 2 +- crates/bevy_audio/src/audio.rs | 2 +- crates/bevy_audio/src/audio_source.rs | 2 +- crates/bevy_transform/src/commands.rs | 11 ++++------- crates/bevy_winit/src/lib.rs | 4 ++-- crates/bevy_winit/src/winit_windows.rs | 2 +- 7 files changed, 14 insertions(+), 17 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index b39bfa42b7f22..5e2b535c800af 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -241,8 +241,8 @@ pub struct AnimationPlayer { } impl AnimationPlayer { - /// Start playing an animation, resetting state of the player - /// This will use a linear blending between the previous and the new animation to make a smooth transition + /// Start playing an animation, resetting state of the player. + /// This will use a linear blending between the previous and the new animation to make a smooth transition. pub fn start(&mut self, handle: Handle) -> &mut Self { self.animation = PlayingAnimation { animation_clip: handle, @@ -256,8 +256,8 @@ impl AnimationPlayer { self } - /// Start playing an animation, resetting state of the player - /// This will use a linear blending between the previous and the new animation to make a smooth transition + /// Start playing an animation, resetting state of the player. + /// This will use a linear blending between the previous and the new animation to make a smooth transition. pub fn start_with_transition( &mut self, handle: Handle, diff --git a/crates/bevy_asset/src/assets.rs b/crates/bevy_asset/src/assets.rs index 8a91fd701ad8b..b586acaeb56ff 100644 --- a/crates/bevy_asset/src/assets.rs +++ b/crates/bevy_asset/src/assets.rs @@ -146,7 +146,7 @@ impl Assets { /// Get mutable access to the asset for the given handle. /// /// This is the main method for mutably accessing asset data from an [Assets] collection. If you - /// do not need mutable access to the asset, you may also use [get](Assets::get). + /// do not need mutable access to the asset, you may also use [`get`](Assets::get). pub fn get_mut(&mut self, handle: &Handle) -> Option<&mut T> { let id: HandleId = handle.into(); self.events.send(AssetEvent::Modified { diff --git a/crates/bevy_audio/src/audio.rs b/crates/bevy_audio/src/audio.rs index 546e158e3f906..c7d8cd6b82acb 100644 --- a/crates/bevy_audio/src/audio.rs +++ b/crates/bevy_audio/src/audio.rs @@ -53,7 +53,7 @@ impl VolumeLevel { } } -/// How should Bevy manage the sound playback? +/// The way Bevy manages the sound playback. #[derive(Debug, Clone, Copy)] pub enum PlaybackMode { /// Play the sound once. Do nothing when it ends. diff --git a/crates/bevy_audio/src/audio_source.rs b/crates/bevy_audio/src/audio_source.rs index 93b951e9f7b5e..367e36ecc2d73 100644 --- a/crates/bevy_audio/src/audio_source.rs +++ b/crates/bevy_audio/src/audio_source.rs @@ -68,7 +68,7 @@ impl AssetLoader for AudioLoader { /// in order to be registered. /// Types that implement this trait usually contain raw sound data that can be converted into an iterator of samples. /// This trait is implemented for [`AudioSource`]. -/// Check the example `audio/decodable` for how to implement this trait on a custom type. +/// Check the example [`decodable`](https://github.com/bevyengine/bevy/blob/latest/examples/audio/decodable.rs) for how to implement this trait on a custom type. pub trait Decodable: Send + Sync + 'static { /// The type of the audio samples. /// Usually a [`u16`], [`i16`] or [`f32`], as those implement [`rodio::Sample`]. diff --git a/crates/bevy_transform/src/commands.rs b/crates/bevy_transform/src/commands.rs index 27bf3400c1d89..abd1ab5d65440 100644 --- a/crates/bevy_transform/src/commands.rs +++ b/crates/bevy_transform/src/commands.rs @@ -1,12 +1,9 @@ -//! Extension to [`EntityCommands`] to modify [`bevy_hierarchy`] hierarchies +//! Extension to [`EntityCommands`] to modify `bevy_hierarchy` hierarchies //! while preserving [`GlobalTransform`]. use bevy_ecs::{prelude::Entity, system::Command, system::EntityCommands, world::World}; use bevy_hierarchy::{AddChild, RemoveParent}; -#[cfg(doc)] -use bevy_hierarchy::BuildChildren; - use crate::{GlobalTransform, Transform}; /// Command similar to [`AddChild`], but updating the child transform to keep @@ -63,13 +60,13 @@ impl Command for RemoveParentInPlace { update_transform(); } } -/// Collection of methods similar to [`BuildChildren`], but preserving each +/// Collection of methods similar to [`BuildChildren`](bevy_hierarchy::BuildChildren), but preserving each /// entity's [`GlobalTransform`]. pub trait BuildChildrenTransformExt { /// Change this entity's parent while preserving this entity's [`GlobalTransform`] /// by updating its [`Transform`]. /// - /// See [`BuildChildren::set_parent`] for a method that doesn't update the + /// See [`BuildChildren::set_parent`](bevy_hierarchy::BuildChildren::set_parent) for a method that doesn't update the /// [`Transform`]. /// /// Note that both the hierarchy and transform updates will only execute @@ -80,7 +77,7 @@ pub trait BuildChildrenTransformExt { /// Make this entity parentless while preserving this entity's [`GlobalTransform`] /// by updating its [`Transform`] to be equal to its current [`GlobalTransform`]. /// - /// See [`BuildChildren::remove_parent`] for a method that doesn't update the + /// See [`BuildChildren::remove_parent`](bevy_hierarchy::BuildChildren::remove_parent) for a method that doesn't update the /// [`Transform`]. /// /// Note that both the hierarchy and transform updates will only execute diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 8479697969250..d6ea22e368388 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -63,10 +63,10 @@ use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin #[cfg(target_os = "android")] pub static ANDROID_APP: std::sync::OnceLock = std::sync::OnceLock::new(); -/// A [`Plugin`] that uses [`winit`] to create and manage windows, and receive window and input +/// A [`Plugin`] that uses `winit` to create and manage windows, and receive window and input /// events. /// -/// This plugin will add systems and resources that sync with the [`winit`] backend and also +/// This plugin will add systems and resources that sync with the `winit` backend and also /// replace the exising [`App`] runner with one that constructs an [event loop](EventLoop) to /// receive window and input events from the OS. #[derive(Default)] diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 4c7f30df6fa82..a2d5b22c2c034 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -21,7 +21,7 @@ use crate::{ converters::{convert_enabled_buttons, convert_window_level, convert_window_theme}, }; -/// A resource mapping window entities to their [`winit`]-backend [`Window`](winit::window::Window) +/// A resource mapping window entities to their `winit`-backend [`Window`](winit::window::Window) /// states. #[derive(Debug, Default)] pub struct WinitWindows { From c2b85f9b52a3dc4c0d573e33107ee3ac9fd8f4e5 Mon Sep 17 00:00:00 2001 From: Mike Date: Thu, 31 Aug 2023 12:06:13 -0700 Subject: [PATCH 58/58] fix ambiguity reporting (#9648) # Objective - I broke ambiguity reporting in one of my refactors. `conflicts_to_string` should have been using the passed in parameter rather than the one stored on self. --- crates/bevy_ecs/src/schedule/mod.rs | 2 +- crates/bevy_ecs/src/schedule/schedule.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 5f0ae88a8f015..ad5bc617a6a2e 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -1014,7 +1014,7 @@ mod tests { let ambiguities: Vec<_> = schedule .graph() - .conflicts_to_string(world.components()) + .conflicts_to_string(schedule.graph().conflicting_systems(), world.components()) .collect(); let expected = &[ diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 8f7da74f1e78c..8b174abdcc7ff 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -1553,7 +1553,7 @@ impl ScheduleGraph { Consider adding `before`, `after`, or `ambiguous_with` relationships between these:\n", ); - for (name_a, name_b, conflicts) in self.conflicts_to_string(components) { + for (name_a, name_b, conflicts) in self.conflicts_to_string(ambiguities, components) { writeln!(message, " -- {name_a} and {name_b}").unwrap(); if !conflicts.is_empty() { @@ -1571,9 +1571,10 @@ impl ScheduleGraph { /// convert conflics to human readable format pub fn conflicts_to_string<'a>( &'a self, + ambiguities: &'a [(NodeId, NodeId, Vec)], components: &'a Components, ) -> impl Iterator)> + 'a { - self.conflicting_systems + ambiguities .iter() .map(move |(system_a, system_b, conflicts)| { let name_a = self.get_node_name(system_a);