From b4ee8981541618597417851108b232ceb4ff8eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Krzysztof=20=C5=BBywiecki?= Date: Sun, 27 Apr 2025 15:54:28 +0200 Subject: [PATCH 01/33] Removed conversion from pointer physical coordinates to viewport local coordinates in bevy_picking make_ray function (#18870) # Objective - Fixes #18856. ## Solution After PR #17633, `Camera::viewport_to_world` method corrects `viewport_position` passed in that input so that it's offset by camera's viewport. `Camera::viewport_to_world` is used by `make_ray` function which in turn also offsets pointer position by viewport position, which causes picking objects to be shifted by viewport position, and it wasn't removed in the aforementioned PR. This second offsetting in `make_ray` was removed. ## Testing - I tested simple_picking example by applying some horizontal offset to camera's viewport. - I tested my application that displayed a single rectangle with picking on two cameras arranged in a row. When using local bevy with this fix, both cameras can be used for picking correctly. - I modified split_screen example: I added observer to ground plane that changes color on hover, and removed UI as it interfered with picking both on master and my branch. On master, only top left camera was triggering the observer, and on my branch all cameras could change plane's color on hover. - I added viewport offset to mesh_picking, with my changes it works correctly, while on master picking ray is shifted. - Sprite picking with viewport offset doesn't work both on master and on this branch. These are the only scenarios I tested. I think other picking functions that use this function should be tested but I couldn't track more uses of it. Co-authored-by: Krzysztof Zywiecki --- crates/bevy_picking/src/backend.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 8c781d54e32f4..fb2f9d7d7aafd 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -229,11 +229,8 @@ pub mod ray { if !pointer_loc.is_in_viewport(camera, primary_window_entity) { return None; } - let mut viewport_pos = pointer_loc.position; - if let Some(viewport) = &camera.viewport { - let viewport_logical = camera.to_logical(viewport.physical_position)?; - viewport_pos -= viewport_logical; - } - camera.viewport_to_world(camera_tfm, viewport_pos).ok() + camera + .viewport_to_world(camera_tfm, pointer_loc.position) + .ok() } } From 227e1bbf34ec25bba330c27d7ab7ddec5b540764 Mon Sep 17 00:00:00 2001 From: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Date: Mon, 28 Apr 2025 12:26:53 -0700 Subject: [PATCH 02/33] bevy_reflect: Re-reflect `hashbrown` types (#18944) # Objective Fixes #18943 ## Solution Reintroduces support for `hashbrown`'s `HashMap` and `HashSet` types. These were inadvertently removed when `bevy_platform` newtyped the `hashbrown` types. Since we removed our `hashbrown` dependency, I gated these impls behind a `hashbrown` feature. Not entirely sure if this is necessary since we enabled it for `bevy_reflect` through `bevy_platform` anyways. (Complex features still confuse me a bit so let me know if I can just remove it!) I also went ahead and preemptively implemented `TypePath` for `PassHash` while I was here. ## Testing You can test that it works by adding the following to a Bevy example based on this PR (you'll also need to include `hashbrown` of course): ```rust #[derive(Reflect)] struct Foo(hashbrown::HashMap); ``` Then check it compiles with: ``` cargo check --example hello_world --no-default-features --features=bevy_reflect/hashbrown ``` --- crates/bevy_reflect/Cargo.toml | 4 +++ crates/bevy_reflect/src/impls/std.rs | 37 ++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index d2297ab716c76..f37865f9a3f62 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -33,6 +33,9 @@ debug_stack = ["std"] ## Adds reflection support to `glam` types. glam = ["dep:glam"] +## Adds reflection support to `hashbrown` types. +hashbrown = ["dep:hashbrown"] + ## Adds reflection support to `petgraph` types. petgraph = ["dep:petgraph", "std"] @@ -87,6 +90,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0", default-feature # used by bevy-utils, but it also needs reflect impls foldhash = { version = "0.1.3", default-features = false } +hashbrown = { version = "0.15.1", optional = true, default-features = false } # other erased-serde = { version = "0.4", default-features = false, features = [ diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 350527f91097d..6a752d187775e 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -1001,6 +1001,19 @@ crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashMap > ); +#[cfg(feature = "hashbrown")] +impl_reflect_for_hashmap!(hashbrown::hash_map::HashMap); +#[cfg(feature = "hashbrown")] +impl_type_path!(::hashbrown::hash_map::HashMap); +#[cfg(all(feature = "functions", feature = "hashbrown"))] +crate::func::macros::impl_function_traits!(::hashbrown::hash_map::HashMap; + < + K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Hash, + V: FromReflect + MaybeTyped + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + macro_rules! impl_reflect_for_hashset { ($ty:path) => { impl Set for $ty @@ -1208,6 +1221,7 @@ macro_rules! impl_reflect_for_hashset { impl_type_path!(::bevy_platform::hash::NoOpHash); impl_type_path!(::bevy_platform::hash::FixedHasher); +impl_type_path!(::bevy_platform::hash::PassHash); impl_reflect_opaque!(::core::net::SocketAddr( Clone, Debug, @@ -1239,6 +1253,18 @@ crate::func::macros::impl_function_traits!(::bevy_platform::collections::HashSet > ); +#[cfg(feature = "hashbrown")] +impl_reflect_for_hashset!(::hashbrown::hash_set::HashSet); +#[cfg(feature = "hashbrown")] +impl_type_path!(::hashbrown::hash_set::HashSet); +#[cfg(all(feature = "functions", feature = "hashbrown"))] +crate::func::macros::impl_function_traits!(::hashbrown::hash_set::HashSet; + < + V: Hash + Eq + FromReflect + TypePath + GetTypeRegistration, + S: TypePath + BuildHasher + Default + Send + Sync + > +); + impl Map for ::alloc::collections::BTreeMap where K: FromReflect + MaybeTyped + TypePath + GetTypeRegistration + Eq + Ord, @@ -2848,4 +2874,15 @@ mod tests { let output = <&'static str as FromReflect>::from_reflect(&expected).unwrap(); assert_eq!(expected, output); } + + #[test] + fn should_reflect_hashmaps() { + assert_impl_all!(std::collections::HashMap: Reflect); + assert_impl_all!(bevy_platform::collections::HashMap: Reflect); + + // We specify `foldhash::fast::RandomState` directly here since without the `default-hasher` + // feature, hashbrown uses an empty enum to force users to specify their own + #[cfg(feature = "hashbrown")] + assert_impl_all!(hashbrown::HashMap: Reflect); + } } From 92cda8b0bbc6cbc4223c728b37bde363465aa9ba Mon Sep 17 00:00:00 2001 From: Innokentiy Popov <65750797+kyon4ik@users.noreply.github.com> Date: Sun, 4 May 2025 18:05:27 +0500 Subject: [PATCH 03/33] Fix `rotate_by` implementation for `Aabb2d` (#19015) # Objective Fixes #18969 ## Solution Also updated `Aabb3d` implementation for consistency. ## Testing Added tests for `Aabb2d` and `Aabb3d` to verify correct rotation behavior for angles greater than 90 degrees. --- .../bevy_math/src/bounding/bounded2d/mod.rs | 23 +++++++++++++------ .../bevy_math/src/bounding/bounded3d/mod.rs | 22 +++++++++++++----- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/crates/bevy_math/src/bounding/bounded2d/mod.rs b/crates/bevy_math/src/bounding/bounded2d/mod.rs index bea18f5808481..5f11ad5233dc4 100644 --- a/crates/bevy_math/src/bounding/bounded2d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded2d/mod.rs @@ -243,13 +243,9 @@ impl BoundingVolume for Aabb2d { /// and consider storing the original AABB and rotating that every time instead. #[inline(always)] fn rotate_by(&mut self, rotation: impl Into) { - let rotation: Rot2 = rotation.into(); - let abs_rot_mat = Mat2::from_cols( - Vec2::new(rotation.cos, rotation.sin), - Vec2::new(rotation.sin, rotation.cos), - ); - let half_size = abs_rot_mat * self.half_size(); - *self = Self::new(rotation * self.center(), half_size); + let rot_mat = Mat2::from(rotation.into()); + let half_size = rot_mat.abs() * self.half_size(); + *self = Self::new(rot_mat * self.center(), half_size); } } @@ -274,6 +270,8 @@ impl IntersectsVolume for Aabb2d { #[cfg(test)] mod aabb2d_tests { + use approx::assert_relative_eq; + use super::Aabb2d; use crate::{ bounding::{BoundingCircle, BoundingVolume, IntersectsVolume}, @@ -394,6 +392,17 @@ mod aabb2d_tests { assert!(scaled.contains(&a)); } + #[test] + fn rotate() { + let a = Aabb2d { + min: Vec2::new(-2.0, -2.0), + max: Vec2::new(2.0, 2.0), + }; + let rotated = a.rotated_by(core::f32::consts::PI); + assert_relative_eq!(rotated.min, a.min); + assert_relative_eq!(rotated.max, a.max); + } + #[test] fn transform() { let a = Aabb2d { diff --git a/crates/bevy_math/src/bounding/bounded3d/mod.rs b/crates/bevy_math/src/bounding/bounded3d/mod.rs index 5a95b7711f647..ca3b3597984d9 100644 --- a/crates/bevy_math/src/bounding/bounded3d/mod.rs +++ b/crates/bevy_math/src/bounding/bounded3d/mod.rs @@ -250,12 +250,7 @@ impl BoundingVolume for Aabb3d { #[inline(always)] fn rotate_by(&mut self, rotation: impl Into) { let rot_mat = Mat3::from_quat(rotation.into()); - let abs_rot_mat = Mat3::from_cols( - rot_mat.x_axis.abs(), - rot_mat.y_axis.abs(), - rot_mat.z_axis.abs(), - ); - let half_size = abs_rot_mat * self.half_size(); + let half_size = rot_mat.abs() * self.half_size(); *self = Self::new(rot_mat * self.center(), half_size); } } @@ -279,6 +274,8 @@ impl IntersectsVolume for Aabb3d { #[cfg(test)] mod aabb3d_tests { + use approx::assert_relative_eq; + use super::Aabb3d; use crate::{ bounding::{BoundingSphere, BoundingVolume, IntersectsVolume}, @@ -398,6 +395,19 @@ mod aabb3d_tests { assert!(scaled.contains(&a)); } + #[test] + fn rotate() { + use core::f32::consts::PI; + let a = Aabb3d { + min: Vec3A::new(-2.0, -2.0, -2.0), + max: Vec3A::new(2.0, 2.0, 2.0), + }; + let rotation = Quat::from_euler(glam::EulerRot::XYZ, PI, PI, 0.0); + let rotated = a.rotated_by(rotation); + assert_relative_eq!(rotated.min, a.min); + assert_relative_eq!(rotated.max, a.max); + } + #[test] fn transform() { let a = Aabb3d { From c64f628bfb0f6dba03c77733783431fb8a7b3530 Mon Sep 17 00:00:00 2001 From: JaySpruce Date: Mon, 5 May 2025 12:42:36 -0500 Subject: [PATCH 04/33] Fix sparse set components ignoring `insert_if_new`/`InsertMode` (#19059) # Objective I've been tinkering with ECS insertion/removal lately, and noticed that sparse sets just... don't interact with `InsertMode` at all. Sure enough, using `insert_if_new` with a sparse component does the same thing as `insert`. # Solution - Add a check in `BundleInfo::write_components` to drop the new value if the entity already has the component and `InsertMode` is `Keep`. - Add necessary methods to sparse set internals to fetch the drop function. # Testing Minimal reproduction:
Code ``` use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(PostStartup, component_print) .run(); } #[derive(Component)] #[component(storage = "SparseSet")] struct SparseComponent(u32); fn setup(mut commands: Commands) { let mut entity = commands.spawn_empty(); entity.insert(SparseComponent(1)); entity.insert(SparseComponent(2)); let mut entity = commands.spawn_empty(); entity.insert(SparseComponent(3)); entity.insert_if_new(SparseComponent(4)); } fn component_print(query: Query<&SparseComponent>) { for component in &query { info!("{}", component.0); } } ```
Here it is on Bevy Playground (0.15.3): https://learnbevy.com/playground?share=2a96a68a81e804d3fdd644a833c1d51f7fa8dd33fc6192fbfd077b082a6b1a41 Output on `main`: ``` 2025-05-04T17:50:50.401328Z INFO system{name="fork::component_print"}: fork: 2 2025-05-04T17:50:50.401583Z INFO system{name="fork::component_print"}: fork: 4 ``` Output with this PR : ``` 2025-05-04T17:51:33.461835Z INFO system{name="fork::component_print"}: fork: 2 2025-05-04T17:51:33.462091Z INFO system{name="fork::component_print"}: fork: 3 ``` --- crates/bevy_ecs/src/bundle.rs | 22 +++++++++++++++------ crates/bevy_ecs/src/storage/blob_vec.rs | 7 +++++++ crates/bevy_ecs/src/storage/sparse_set.rs | 7 +++++++ crates/bevy_ecs/src/storage/table/column.rs | 7 +++++++ 4 files changed, 37 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 5666d90c53a42..32a91279cb467 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -630,13 +630,14 @@ impl BundleInfo { let mut bundle_component = 0; let after_effect = bundle.get_components(&mut |storage_type, component_ptr| { let component_id = *self.component_ids.get_unchecked(bundle_component); + // SAFETY: bundle_component is a valid index for this bundle + let status = unsafe { bundle_component_status.get_status(bundle_component) }; match storage_type { StorageType::Table => { - // SAFETY: bundle_component is a valid index for this bundle - let status = unsafe { bundle_component_status.get_status(bundle_component) }; - // SAFETY: If component_id is in self.component_ids, BundleInfo::new ensures that - // the target table contains the component. - let column = table.get_column_mut(component_id).debug_checked_unwrap(); + let column = + // SAFETY: If component_id is in self.component_ids, BundleInfo::new ensures that + // the target table contains the component. + unsafe { table.get_column_mut(component_id).debug_checked_unwrap() }; match (status, insert_mode) { (ComponentStatus::Added, _) => { column.initialize(table_row, component_ptr, change_tick, caller); @@ -656,7 +657,16 @@ impl BundleInfo { // SAFETY: If component_id is in self.component_ids, BundleInfo::new ensures that // a sparse set exists for the component. unsafe { sparse_sets.get_mut(component_id).debug_checked_unwrap() }; - sparse_set.insert(entity, component_ptr, change_tick, caller); + match (status, insert_mode) { + (ComponentStatus::Added, _) | (_, InsertMode::Replace) => { + sparse_set.insert(entity, component_ptr, change_tick, caller); + } + (ComponentStatus::Existing, InsertMode::Keep) => { + if let Some(drop_fn) = sparse_set.get_drop() { + drop_fn(component_ptr); + } + } + } } } bundle_component += 1; diff --git a/crates/bevy_ecs/src/storage/blob_vec.rs b/crates/bevy_ecs/src/storage/blob_vec.rs index 2451fccb140f8..85852a2bea81b 100644 --- a/crates/bevy_ecs/src/storage/blob_vec.rs +++ b/crates/bevy_ecs/src/storage/blob_vec.rs @@ -366,6 +366,13 @@ impl BlobVec { unsafe { core::slice::from_raw_parts(self.data.as_ptr() as *const UnsafeCell, self.len) } } + /// Returns the drop function for values stored in the vector, + /// or `None` if they don't need to be dropped. + #[inline] + pub fn get_drop(&self) -> Option)> { + self.drop + } + /// Clears the vector, removing (and dropping) all values. /// /// Note that this method has no effect on the allocated capacity of the vector. diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index bb79382e06a8d..6c809df849b3e 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -300,6 +300,13 @@ impl ComponentSparseSet { }) } + /// Returns the drop function for the component type stored in the sparse set, + /// or `None` if it doesn't need to be dropped. + #[inline] + pub fn get_drop(&self) -> Option)> { + self.dense.get_drop() + } + /// Removes the `entity` from this sparse set and returns a pointer to the associated value (if /// it exists). #[must_use = "The returned pointer must be used to drop the removed component."] diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index d4690d264cb32..522df222c6379 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -697,4 +697,11 @@ impl Column { changed_by.get_unchecked(row.as_usize()) }) } + + /// Returns the drop function for elements of the column, + /// or `None` if they don't need to be dropped. + #[inline] + pub fn get_drop(&self) -> Option)> { + self.data.get_drop() + } } From 79cbe845db1a5d59c0d76638b874bd2e24810b9c Mon Sep 17 00:00:00 2001 From: Greeble <166992735+greeble-dev@users.noreply.github.com> Date: Mon, 5 May 2025 19:04:43 +0100 Subject: [PATCH 05/33] Fix occlusion culling not respecting device limits (#18974) The occlusion culling plugin checks for a GPU feature by looking at `RenderAdapter`. This is wrong - it should be checking `RenderDevice`. See these notes for background: https://github.com/bevyengine/bevy/discussions/18973 I don't have any evidence that this was causing any bugs, so right now it's just a precaution. ## Testing ``` cargo run --example occlusion_culling ``` Tested on Win10/Nvidia across Vulkan, WebGL/Chrome, WebGPU/Chrome. --- examples/3d/occlusion_culling.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/3d/occlusion_culling.rs b/examples/3d/occlusion_culling.rs index 4c69db0a4a101..9268f1dc6a8d3 100644 --- a/examples/3d/occlusion_culling.rs +++ b/examples/3d/occlusion_culling.rs @@ -32,7 +32,7 @@ use bevy::{ experimental::occlusion_culling::OcclusionCulling, render_graph::{self, NodeRunError, RenderGraphApp, RenderGraphContext, RenderLabel}, render_resource::{Buffer, BufferDescriptor, BufferUsages, MapMode}, - renderer::{RenderAdapter, RenderContext, RenderDevice}, + renderer::{RenderContext, RenderDevice}, settings::WgpuFeatures, Render, RenderApp, RenderDebugFlags, RenderPlugin, RenderSet, }, @@ -140,7 +140,7 @@ struct SavedIndirectParametersData { impl FromWorld for SavedIndirectParameters { fn from_world(world: &mut World) -> SavedIndirectParameters { - let render_adapter = world.resource::(); + let render_device = world.resource::(); SavedIndirectParameters(Arc::new(Mutex::new(SavedIndirectParametersData { data: vec![], count: 0, @@ -152,7 +152,7 @@ impl FromWorld for SavedIndirectParameters { // supports `multi_draw_indirect_count`. So, if we don't have that // feature, then we don't bother to display how many meshes were // culled. - occlusion_culling_introspection_supported: render_adapter + occlusion_culling_introspection_supported: render_device .features() .contains(WgpuFeatures::MULTI_DRAW_INDIRECT_COUNT), }))) From f39320d0c772356f57079d321546eac80ffb31ee Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Tue, 6 May 2025 07:18:56 +0200 Subject: [PATCH 06/33] Add `world` and `world_mut` methods to `RelatedSpawner` (#18880) # Objective `RelatedSpawnerCommands` offers methods to get the underlying `Commands`. `RelatedSpawner` does not expose the inner `World` reference so far. I currently want to write extension traits for both of them but I need to duplicate the whole API for the latter because I cannot get it's `&mut World`. ## Solution Add methods for immutable and mutable `World` access --- crates/bevy_ecs/src/relationship/related_methods.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 98ef8d08321ac..b53fd43943752 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -519,6 +519,16 @@ impl<'w, R: Relationship> RelatedSpawner<'w, R> { pub fn target_entity(&self) -> Entity { self.target } + + /// Returns a reference to the underlying [`World`]. + pub fn world(&self) -> &World { + self.world + } + + /// Returns a mutable reference to the underlying [`World`]. + pub fn world_mut(&mut self) -> &mut World { + self.world + } } /// Uses commands to spawn related "source" entities with the given [`Relationship`], targeting From fd5c5e7cede71adeb474155783443902e6175d03 Mon Sep 17 00:00:00 2001 From: UkoeHB <37489173+UkoeHB@users.noreply.github.com> Date: Tue, 6 May 2025 00:23:48 -0500 Subject: [PATCH 07/33] Expose CustomCursorUrl (#19006) # Objective `CustomCursorUrl` is inaccessible. ## Solution Expose `CustomCursorUrl`. --- crates/bevy_winit/src/cursor.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index f45b7f00d6915..bdca3f85851ec 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -39,6 +39,13 @@ use tracing::warn; #[cfg(feature = "custom_cursor")] pub use crate::custom_cursor::{CustomCursor, CustomCursorImage}; +#[cfg(all( + feature = "custom_cursor", + target_family = "wasm", + target_os = "unknown" +))] +pub use crate::custom_cursor::CustomCursorUrl; + pub(crate) struct CursorPlugin; impl Plugin for CursorPlugin { From 289c51b5478b37f3685162c05d30c53238e5f804 Mon Sep 17 00:00:00 2001 From: databasedav <31483365+databasedav@users.noreply.github.com> Date: Fri, 9 May 2025 10:10:54 -0700 Subject: [PATCH 08/33] fix `.insert_related` index bound (#19134) # Objective resolves #19092 ## Solution - remove the `.saturating_sub` from the index transformation - add `.saturating_add` to the internal offset calculation ## Testing - added regression test, confirming 0 index order + testing max bound --- crates/bevy_ecs/src/hierarchy.rs | 37 +++++++++++++++++++ .../src/relationship/related_methods.rs | 2 +- .../relationship_source_collection.rs | 5 +-- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index 9f4b0d0f8f8da..53be7fc80b9a2 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -656,6 +656,43 @@ mod tests { ); } + // regression test for https://github.com/bevyengine/bevy/pull/19134 + #[test] + fn insert_children_index_bound() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + let child4 = world.spawn_empty().id(); + + let mut entity_world_mut = world.spawn_empty(); + + let first_children = entity_world_mut.add_children(&[child1, child2]).id(); + let hierarchy = get_hierarchy(&world, first_children); + assert_eq!( + hierarchy, + Node::new_with(first_children, vec![Node::new(child1), Node::new(child2)]) + ); + + let root = world + .entity_mut(first_children) + .insert_children(usize::MAX, &[child3, child4]) + .id(); + let hierarchy = get_hierarchy(&world, root); + assert_eq!( + hierarchy, + Node::new_with( + root, + vec![ + Node::new(child1), + Node::new(child2), + Node::new(child3), + Node::new(child4), + ] + ) + ); + } + #[test] fn remove_children() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index b53fd43943752..6f314ffc05ec2 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -81,7 +81,7 @@ impl<'w> EntityWorldMut<'w> { let id = self.id(); self.world_scope(|world| { for (offset, related) in related.iter().enumerate() { - let index = index + offset; + let index = index.saturating_add(offset); if world .get::(*related) .is_some_and(|relationship| relationship.get() == id) diff --git a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs index c2c9bd94d8235..c9a00c8be8a3b 100644 --- a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs +++ b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs @@ -213,15 +213,14 @@ impl OrderedRelationshipSourceCollection for Vec { fn place_most_recent(&mut self, index: usize) { if let Some(entity) = self.pop() { - let index = index.min(self.len().saturating_sub(1)); + let index = index.min(self.len()); self.insert(index, entity); } } fn place(&mut self, entity: Entity, index: usize) { if let Some(current) = <[Entity]>::iter(self).position(|e| *e == entity) { - // The len is at least 1, so the subtraction is safe. - let index = index.min(self.len().saturating_sub(1)); + let index = index.min(self.len()); Vec::remove(self, current); self.insert(index, entity); }; From 6826676e41f2ca0d12eb494a4c62e0ceeb2dde3e Mon Sep 17 00:00:00 2001 From: Jordan Dominion Date: Fri, 9 May 2025 22:45:25 -0400 Subject: [PATCH 09/33] Fix macro pollution in SystemParam derive (#19155) # Objective Fixes #19130 ## Solution Fully quality `Result::Ok` so as to not accidentally invoke the anyhow function of the same name ## Testing Tested on this minimal repro with and without change. main.rs ```rs use anyhow::Ok; use bevy::ecs::system::SystemParam; #[derive(SystemParam)] pub struct SomeParams; fn main() { } ``` Cargo.toml ```toml [package] name = "bevy-playground" version = "0.1.0" edition = "2024" [dependencies] anyhow = "1.0.98" bevy = { path = "../bevy" } ``` --- crates/bevy_ecs/macros/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index a657765ac23f9..410317a275075 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -455,7 +455,7 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { <#field_types as #path::system::SystemParam>::validate_param(#field_locals, _system_meta, _world) .map_err(|err| #path::system::SystemParamValidationError::new::(err.skipped, #field_messages, #field_names))?; )* - Ok(()) + Result::Ok(()) } #[inline] From efde13a827bb8fe1a8ed7908f2942798d571f75f Mon Sep 17 00:00:00 2001 From: Daniel Gallups <44790295+dsgallups@users.noreply.github.com> Date: Mon, 12 May 2025 14:11:14 -0400 Subject: [PATCH 10/33] Fix: Provide CPU mesh processing with MaterialBindingId (#19083) # Objective Fixes #19027 ## Solution Query for the material binding id if using fallback CPU processing ## Testing I've honestly no clue how to test for this, and I imagine that this isn't entirely failsafe :( but would highly appreciate a suggestion! To verify this works, please run the the texture.rs example using WebGL 2. Additionally, I'm extremely naive about the nuances of pbr. This PR is essentially to kinda *get the ball rolling* of sorts. Thanks :) --------- Co-authored-by: Gilles Henaux Co-authored-by: charlotte --- crates/bevy_pbr/src/render/mesh.rs | 40 ++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 4bae79b807417..0517c637fa4e7 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -837,12 +837,33 @@ pub struct RenderMeshInstanceGpuQueues(Parallel); pub struct MeshesToReextractNextFrame(MainEntityHashSet); impl RenderMeshInstanceShared { - fn from_components( + /// A gpu builder will provide the mesh instance id + /// during [`RenderMeshInstanceGpuBuilder::update`]. + fn for_gpu_building( previous_transform: Option<&PreviousGlobalTransform>, mesh: &Mesh3d, tag: Option<&MeshTag>, not_shadow_caster: bool, no_automatic_batching: bool, + ) -> Self { + Self::for_cpu_building( + previous_transform, + mesh, + tag, + default(), + not_shadow_caster, + no_automatic_batching, + ) + } + + /// The cpu builder does not have an equivalent [`RenderMeshInstanceGpuBuilder::update`]. + fn for_cpu_building( + previous_transform: Option<&PreviousGlobalTransform>, + mesh: &Mesh3d, + tag: Option<&MeshTag>, + material_bindings_index: MaterialBindingId, + not_shadow_caster: bool, + no_automatic_batching: bool, ) -> Self { let mut mesh_instance_flags = RenderMeshInstanceFlags::empty(); mesh_instance_flags.set(RenderMeshInstanceFlags::SHADOW_CASTER, !not_shadow_caster); @@ -858,8 +879,7 @@ impl RenderMeshInstanceShared { RenderMeshInstanceShared { mesh_asset_id: mesh.id(), flags: mesh_instance_flags, - // This gets filled in later, during `RenderMeshGpuBuilder::update`. - material_bindings_index: default(), + material_bindings_index, lightmap_slab_index: None, tag: tag.map_or(0, |i| **i), } @@ -1305,6 +1325,8 @@ pub struct ExtractMeshesSet; /// [`MeshUniform`] building. pub fn extract_meshes_for_cpu_building( mut render_mesh_instances: ResMut, + mesh_material_ids: Res, + render_material_bindings: Res, render_visibility_ranges: Res, mut render_mesh_instance_queues: Local>>, meshes_query: Extract< @@ -1358,10 +1380,18 @@ pub fn extract_meshes_for_cpu_building( transmitted_receiver, ); - let shared = RenderMeshInstanceShared::from_components( + let mesh_material = mesh_material_ids.mesh_material(MainEntity::from(entity)); + + let material_bindings_index = render_material_bindings + .get(&mesh_material) + .copied() + .unwrap_or_default(); + + let shared = RenderMeshInstanceShared::for_cpu_building( previous_transform, mesh, tag, + material_bindings_index, not_shadow_caster, no_automatic_batching, ); @@ -1566,7 +1596,7 @@ fn extract_mesh_for_gpu_building( transmitted_receiver, ); - let shared = RenderMeshInstanceShared::from_components( + let shared = RenderMeshInstanceShared::for_gpu_building( previous_transform, mesh, tag, From 82f193284ae013c6aac252fab7348825ff5610b4 Mon Sep 17 00:00:00 2001 From: atlv Date: Mon, 12 May 2025 15:14:13 -0400 Subject: [PATCH 11/33] Fix specular cutoff on lights with radius overlapping with mesh (#19157) # Objective - Fixes #13318 ## Solution - Clamp a dot product to be positive to avoid choosing a `centerToRay` which is not on the ray but behind it. ## Testing - Repro in #13318 Main: {DA2A2B99-27C7-4A76-83B6-CCB70FB57CAD} This PR: {2C4BC3E7-C6A6-4736-A916-0366FBB618DA} Eevee reference: ![329697008-ff28a5f3-27f3-4e98-9cee-d836a6c76aee](https://github.com/user-attachments/assets/a1b566ab-16ee-40d3-a0b6-ad179ca0fe3a) --- crates/bevy_pbr/src/render/pbr_lighting.wgsl | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/render/pbr_lighting.wgsl b/crates/bevy_pbr/src/render/pbr_lighting.wgsl index 4497b567e9ff8..01e09fe3b470e 100644 --- a/crates/bevy_pbr/src/render/pbr_lighting.wgsl +++ b/crates/bevy_pbr/src/render/pbr_lighting.wgsl @@ -278,7 +278,23 @@ fn compute_specular_layer_values_for_point_light( // Representative Point Area Lights. // see http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf p14-16 - let centerToRay = dot(light_to_frag, R) * R - light_to_frag; + var LtFdotR = dot(light_to_frag, R); + + // HACK: the following line is an amendment to fix a discontinuity when a surface + // intersects the light sphere. See https://github.com/bevyengine/bevy/issues/13318 + // + // This sentence in the reference is crux of the problem: "We approximate finding the point with the + // smallest angle to the reflection ray by finding the point with the smallest distance to the ray." + // This approximation turns out to be completely wrong for points inside or near the sphere. + // Clamping this dot product to be positive ensures `centerToRay` lies on ray and not behind it. + // Any non-zero epsilon works here, it just has to be positive to avoid a singularity at zero. + // However, this is still far from physically accurate. Deriving an exact solution would help, + // but really we should adopt a superior solution to area lighting, such as: + // Physically Based Area Lights by Michal Drobot, or + // Polygonal-Light Shading with Linearly Transformed Cosines by Eric Heitz et al. + LtFdotR = max(0.0001, LtFdotR); + + let centerToRay = LtFdotR * R - light_to_frag; let closestPoint = light_to_frag + centerToRay * saturate( light_position_radius * inverseSqrt(dot(centerToRay, centerToRay))); let LspecLengthInverse = inverseSqrt(dot(closestPoint, closestPoint)); From f47038f5c8de4d4bb083c2a81b7b40c9049ea2da Mon Sep 17 00:00:00 2001 From: atlv Date: Sat, 17 May 2025 15:03:47 -0400 Subject: [PATCH 12/33] fix(render): transitive shader imports now work consistently on web (#19266) # Objective - transitive shader imports sometimes fail to load silently and return Ok - Fixes #19226 ## Solution - Don't return Ok, return the appropriate error code which will retry the load later when the dependencies load ## Testing - `bevy run --example=3d_scene web --open` Note: this is was theoretically a problem before the hot reloading PR, but probably extremely unlikely to occur. --- crates/bevy_render/src/render_resource/pipeline_cache.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 653ae70b1c77c..706ebd23f2f1e 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -208,7 +208,11 @@ impl ShaderCache { } composer.add_composable_module(shader.into())?; + } else { + Err(PipelineCacheError::ShaderImportNotYetAvailable)?; } + } else { + Err(PipelineCacheError::ShaderImportNotYetAvailable)?; } // if we fail to add a module the composer will tell us what is missing } From 2811a034b8770de781b647fb688522f8b53c3c35 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Sun, 18 May 2025 09:24:37 +0300 Subject: [PATCH 13/33] Fix point light shadow glitches (#19265) # Objective Fixes #18945 ## Solution Entities that are not visible in any view (camera or light), get their render meshes removed. When they become visible somewhere again, the meshes get recreated and assigned possibly different ids. Point/spot light visible entities weren't cleared when the lights themseves went out of view, which caused them to try to queue these fake visible entities for rendering every frame. The shadow phase cache usually flushes non visible entites, but because of this bug it never flushed them and continued to queue meshes with outdated ids. The simple solution is to every frame clear all visible entities for all point/spot lights that may or may not be visible. The visible entities get repopulated directly afterwards. I also renamed the `global_point_lights` to `global_visible_clusterable` to make it clear that it includes only visible things. ## Testing - Tested with the code from the issue. --- crates/bevy_pbr/src/render/light.rs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index d71dccc71ac39..dfc7f679f312f 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -220,7 +220,8 @@ pub fn extract_lights( mut commands: Commands, point_light_shadow_map: Extract>, directional_light_shadow_map: Extract>, - global_point_lights: Extract>, + global_visible_clusterable: Extract>, + cubemap_visible_entities: Extract>>, point_lights: Extract< Query<( Entity, @@ -276,6 +277,16 @@ pub fn extract_lights( if directional_light_shadow_map.is_changed() { commands.insert_resource(directional_light_shadow_map.clone()); } + + // Clear previous visible entities for all cubemapped lights as they might not be in the + // `global_visible_clusterable` list anymore. + commands.try_insert_batch( + cubemap_visible_entities + .iter() + .map(|render_entity| (render_entity, RenderCubemapVisibleEntities::default())) + .collect::>(), + ); + // This is the point light shadow map texel size for one face of the cube as a distance of 1.0 // world unit from the light. // point_light_texel_size = 2.0 * 1.0 * tan(PI / 4.0) / cube face width in texels @@ -286,7 +297,7 @@ pub fn extract_lights( let point_light_texel_size = 2.0 / point_light_shadow_map.size as f32; let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len); - for entity in global_point_lights.iter().copied() { + for entity in global_visible_clusterable.iter().copied() { let Ok(( main_entity, render_entity, @@ -350,7 +361,7 @@ pub fn extract_lights( commands.try_insert_batch(point_lights_values); let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len); - for entity in global_point_lights.iter().copied() { + for entity in global_visible_clusterable.iter().copied() { if let Ok(( main_entity, render_entity, From aab1ae8457215b06bb98ee4049c86daed6a2bb86 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Sun, 18 May 2025 09:28:30 +0300 Subject: [PATCH 14/33] Make sure prepass notices changes in alpha mode (#19170) # Objective Fixes #19150 ## Solution Normally the `validate_cached_entity` in https://github.com/bevyengine/bevy/blob/86cc02dca229662bdfb371a0e5a1716560ea7baa/crates/bevy_pbr/src/prepass/mod.rs#L1109-L1126 marks unchanged entites as clean, which makes them remain in the phase. If a material is changed to an `alpha_mode` that isn't supposed to be added to the prepass pipeline, the specialization system just `continue`s and doesn't indicate to the cache that the entity is not clean anymore. I made these invalid entities get removed from the pipeline cache so that they are correctly not marked clean and then removed from the phase. ## Testing Tested with the example code from the issue. --- crates/bevy_pbr/src/prepass/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 77f874c168ddd..800ec72861457 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -983,12 +983,18 @@ pub fn specialize_prepass_material_meshes( AlphaMode::Blend | AlphaMode::Premultiplied | AlphaMode::Add - | AlphaMode::Multiply => continue, + | AlphaMode::Multiply => { + // In case this material was previously in a valid alpha_mode, remove it to + // stop the queue system from assuming its retained cache to be valid. + view_specialized_material_pipeline_cache.remove(visible_entity); + continue; + } } if material.properties.reads_view_transmission_texture { // No-op: Materials reading from `ViewTransmissionTexture` are not rendered in the `Opaque3d` // phase, and are therefore also excluded from the prepass much like alpha-blended materials. + view_specialized_material_pipeline_cache.remove(visible_entity); continue; } From 125fbb05d312abf3f2f5d152d5368d30ddb49ff3 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Mon, 19 May 2025 22:42:09 +0300 Subject: [PATCH 15/33] Fix spot light shadow glitches (#19273) # Objective Spot light shadows are still broken after fixing point lights in #19265 ## Solution Fix spot lights in the same way, just using the spot light specific visible entities component. I also changed the query to be directly in the render world instead of being extracted to be more accurate. ## Testing Tested with the same code but changing `PointLight` to `SpotLight`. --- crates/bevy_pbr/src/render/light.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index dfc7f679f312f..f57ba9adf343e 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -221,7 +221,17 @@ pub fn extract_lights( point_light_shadow_map: Extract>, directional_light_shadow_map: Extract>, global_visible_clusterable: Extract>, - cubemap_visible_entities: Extract>>, + previous_point_lights: Query< + Entity, + ( + With, + With, + ), + >, + previous_spot_lights: Query< + Entity, + (With, With), + >, point_lights: Extract< Query<( Entity, @@ -278,14 +288,20 @@ pub fn extract_lights( commands.insert_resource(directional_light_shadow_map.clone()); } - // Clear previous visible entities for all cubemapped lights as they might not be in the + // Clear previous visible entities for all point/spot lights as they might not be in the // `global_visible_clusterable` list anymore. commands.try_insert_batch( - cubemap_visible_entities + previous_point_lights .iter() .map(|render_entity| (render_entity, RenderCubemapVisibleEntities::default())) .collect::>(), ); + commands.try_insert_batch( + previous_spot_lights + .iter() + .map(|render_entity| (render_entity, RenderVisibleMeshEntities::default())) + .collect::>(), + ); // This is the point light shadow map texel size for one face of the cube as a distance of 1.0 // world unit from the light. From ca5712a7bf19f20a3cde6c6dd503f720d1be9db4 Mon Sep 17 00:00:00 2001 From: Lucas Date: Thu, 22 May 2025 20:30:14 +0200 Subject: [PATCH 16/33] feat: derive Serialize on Childof (#19336) # Objective allow serialization / deserialization on the `ChildOf` entity, for example in network usage. my usage was for the bevy_replicon crate, to replicate `ChildOf`. ## Solution same implementation of serde as other types in the bevy repo --------- Co-authored-by: Hennadii Chernyshchyk --- crates/bevy_ecs/src/hierarchy.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index 53be7fc80b9a2..6fd66b0bb6e08 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -19,6 +19,8 @@ use crate::{ use alloc::{format, string::String, vec::Vec}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::std_traits::ReflectDefault; +#[cfg(all(feature = "serialize", feature = "bevy_reflect"))] +use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use core::ops::Deref; use core::slice; use disqualified::ShortName; @@ -96,9 +98,14 @@ use log::warn; feature = "bevy_reflect", reflect(Component, PartialEq, Debug, FromWorld, Clone) )] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect(Serialize, Deserialize) +)] #[relationship(relationship_target = Children)] #[doc(alias = "IsChild", alias = "Parent")] -pub struct ChildOf(pub Entity); +pub struct ChildOf(#[entities] pub Entity); impl ChildOf { /// The parent entity of this child entity. From 8ff0c6481a3cab6fce2ae6f2ef8ccbed76a4fa6a Mon Sep 17 00:00:00 2001 From: NonbinaryCoder <108490895+NonbinaryCoder@users.noreply.github.com> Date: Thu, 22 May 2025 15:04:24 -0400 Subject: [PATCH 17/33] Diagnostic reset sum ema (#19337) # Objective Fix incorrect average returned by `Diagnostic` after `clear_history` is called. ## Solution Reset sum and ema values in `Diagnostic::clear_history`. ## Testing I have added a cargo test for `Diagnostic::clear_history` that checks average and smoothed average. This test passes, and should not be platform dependent. --- crates/bevy_diagnostic/src/diagnostic.rs | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/bevy_diagnostic/src/diagnostic.rs b/crates/bevy_diagnostic/src/diagnostic.rs index 00a758416be97..675f205a64f93 100644 --- a/crates/bevy_diagnostic/src/diagnostic.rs +++ b/crates/bevy_diagnostic/src/diagnostic.rs @@ -293,6 +293,8 @@ impl Diagnostic { /// Clear the history of this diagnostic. pub fn clear_history(&mut self) { self.history.clear(); + self.sum = 0.0; + self.ema = 0.0; } } @@ -420,3 +422,31 @@ impl RegisterDiagnostic for App { self } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_clear_history() { + const MEASUREMENT: f64 = 20.0; + + let mut diagnostic = + Diagnostic::new(DiagnosticPath::new("test")).with_max_history_length(5); + let mut now = Instant::now(); + + for _ in 0..3 { + for _ in 0..5 { + diagnostic.add_measurement(DiagnosticMeasurement { + time: now, + value: MEASUREMENT, + }); + // Increase time to test smoothed average. + now += Duration::from_secs(1); + } + assert!((diagnostic.average().unwrap() - MEASUREMENT).abs() < 0.1); + assert!((diagnostic.smoothed().unwrap() - MEASUREMENT).abs() < 0.1); + diagnostic.clear_history(); + } + } +} From ddc3264794fed95d7a58fca8d0878bed786546ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Job?= Date: Mon, 26 May 2025 17:26:46 +0200 Subject: [PATCH 18/33] Fix BRP query failing when specifying missing/invalid components (#18871) # Objective - Fixes #18869. ## Solution The issue was the `?` after a `Result` raising the error, instead of treating it. Instead it is handled with `ok`, `and_then`, `map` ... _Edit: I added the following logic._ On `bevy/query` remote requests, when `strict` is false: - Unregistered components in `option` and `without` are ignored. - Unregistered components in `has` are considered absent from the entity. - Unregistered components in `components` and `with` result in an empty response since they specify hard requirements. I made the `get_component_ids` function return a `AnyhowResult<(Vec<(TypeId, ComponentId)>, Vec)>` instead of the previous `AnyhowResult>`; that is I added the list of unregistered components. ## Testing I tested changes using the same procedure as in the linked issue: ```sh cargo run --example server --features="bevy_remote" ``` In another terminal: ```sh # Not strict: $ curl -X POST http://localhost:15702 -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "method": "bevy/query", "id": 0, "params": { "data": { "components": [ "foo::bar::MyComponent" ] } } }' {"jsonrpc":"2.0","id":0,"result":[]} # Strict: $ curl -X POST http://localhost:15702 -H "Content-Type: application/json" -d '{ "jsonrpc": "2.0", "method": "bevy/query", "id": 0, "params": { "data": { "components": [ "foo::bar::MyComponent" ] }, "strict": true } }' {"jsonrpc":"2.0","id":0,"error":{"code":-23402,"message":"Component `foo::bar::MyComponent` isn't registered or used in the world"}} ``` --- crates/bevy_remote/src/builtin_methods.rs | 88 ++++++++++++++++------- 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index ce5fa259a1a33..b980c0cc5305f 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -718,17 +718,27 @@ pub fn process_remote_query_request(In(params): In>, world: &mut W let app_type_registry = world.resource::().clone(); let type_registry = app_type_registry.read(); - let components = get_component_ids(&type_registry, world, components, strict) + let (components, unregistered_in_components) = + get_component_ids(&type_registry, world, components, strict) + .map_err(BrpError::component_error)?; + let (option, _) = get_component_ids(&type_registry, world, option, strict) .map_err(BrpError::component_error)?; - let option = get_component_ids(&type_registry, world, option, strict) - .map_err(BrpError::component_error)?; - let has = + let (has, unregistered_in_has) = get_component_ids(&type_registry, world, has, strict).map_err(BrpError::component_error)?; - let without = get_component_ids(&type_registry, world, without, strict) + let (without, _) = get_component_ids(&type_registry, world, without, strict) .map_err(BrpError::component_error)?; - let with = get_component_ids(&type_registry, world, with, strict) + let (with, unregistered_in_with) = get_component_ids(&type_registry, world, with, strict) .map_err(BrpError::component_error)?; + // When "strict" is false: + // - Unregistered components in "option" and "without" are ignored. + // - Unregistered components in "has" are considered absent from the entity. + // - Unregistered components in "components" and "with" result in an empty + // response since they specify hard requirements. + if !unregistered_in_components.is_empty() || !unregistered_in_with.is_empty() { + return serde_json::to_value(BrpQueryResponse::default()).map_err(BrpError::internal); + } + let mut query = QueryBuilder::::new(world); for (_, component) in &components { query.ref_id(*component); @@ -784,6 +794,7 @@ pub fn process_remote_query_request(In(params): In>, world: &mut W let has_map = build_has_map( row.clone(), has_paths_and_reflect_components.iter().copied(), + &unregistered_in_has, ); response.push(BrpQueryRow { entity: row.id(), @@ -1024,12 +1035,19 @@ pub fn process_remote_remove_request( let type_registry = app_type_registry.read(); let component_ids = get_component_ids(&type_registry, world, components, true) + .and_then(|(registered, unregistered)| { + if unregistered.is_empty() { + Ok(registered) + } else { + Err(anyhow!("Unregistered component types: {:?}", unregistered)) + } + }) .map_err(BrpError::component_error)?; // Remove the components. let mut entity_world_mut = get_entity_mut(world, entity)?; - for (_, component_id) in component_ids { - entity_world_mut.remove_by_id(component_id); + for (_, component_id) in component_ids.iter() { + entity_world_mut.remove_by_id(*component_id); } Ok(Value::Null) @@ -1264,8 +1282,9 @@ fn get_entity_mut(world: &mut World, entity: Entity) -> Result, strict: bool, -) -> AnyhowResult> { +) -> AnyhowResult<(Vec<(TypeId, ComponentId)>, Vec)> { let mut component_ids = vec![]; + let mut unregistered_components = vec![]; for component_path in component_paths { - let type_id = get_component_type_registration(type_registry, &component_path)?.type_id(); - let Some(component_id) = world.components().get_id(type_id) else { - if strict { - return Err(anyhow!( - "Component `{}` isn't used in the world", - component_path - )); - } - continue; - }; - - component_ids.push((type_id, component_id)); + let maybe_component_tuple = get_component_type_registration(type_registry, &component_path) + .ok() + .and_then(|type_registration| { + let type_id = type_registration.type_id(); + world + .components() + .get_id(type_id) + .map(|component_id| (type_id, component_id)) + }); + if let Some((type_id, component_id)) = maybe_component_tuple { + component_ids.push((type_id, component_id)); + } else if strict { + return Err(anyhow!( + "Component `{}` isn't registered or used in the world", + component_path + )); + } else { + unregistered_components.push(component_path); + } } - Ok(component_ids) + Ok((component_ids, unregistered_components)) } /// Given an entity (`entity_ref`) and a list of reflected component information @@ -1325,12 +1352,16 @@ fn build_components_map<'a>( Ok(serialized_components_map) } -/// Given an entity (`entity_ref`) and list of reflected component information -/// (`paths_and_reflect_components`), return a map which associates each component to -/// a boolean value indicating whether or not that component is present on the entity. +/// Given an entity (`entity_ref`), +/// a list of reflected component information (`paths_and_reflect_components`) +/// and a list of unregistered components, +/// return a map which associates each component to a boolean value indicating +/// whether or not that component is present on the entity. +/// Unregistered components are considered absent from the entity. fn build_has_map<'a>( entity_ref: FilteredEntityRef, paths_and_reflect_components: impl Iterator, + unregistered_components: &[String], ) -> HashMap { let mut has_map = >::default(); @@ -1338,6 +1369,9 @@ fn build_has_map<'a>( let has = reflect_component.contains(entity_ref.clone()); has_map.insert(type_path.to_owned(), Value::Bool(has)); } + unregistered_components.iter().for_each(|component| { + has_map.insert(component.to_owned(), Value::Bool(false)); + }); has_map } From 489dca774cc05dfa74d69b1068fd3fcd9705966c Mon Sep 17 00:00:00 2001 From: Jakob Hellermann Date: Mon, 26 May 2025 17:33:05 +0200 Subject: [PATCH 19/33] bevy_ecs: forward `type_id` in `InfallibleSystemWrapper` (#18931) similar to https://github.com/bevyengine/bevy/pull/12030 # Objective `bevy_mod_debugdump` uses the `SystemTypeSet::system_type` to look up constrains like `(system_1, system_2.after(system_1))`. For that it needs to find the type id in `schedule.graph().systems()` Now with systems being wrapped in an `InfallibleSystemWrapper` this association was no longer possible. ## Solution By forwarding the type id in `InfallibleSystemWrapper`, `bevy_mod_debugdump` can resolve the dependencies as before, and the wrapper is an unnoticable implementation detail. ## Testing - `cargo test -p bevy_ecs` I'm not sure what exactly could break otherwise. --- crates/bevy_ecs/src/system/schedule_system.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 75fad2b7e9af6..b117f2c38731f 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -30,6 +30,10 @@ impl> System for InfallibleSystemWrapper { self.0.name() } + fn type_id(&self) -> core::any::TypeId { + self.0.type_id() + } + #[inline] fn component_access(&self) -> &Access { self.0.component_access() From 27221622b04c92c39a2a97c1836ee932f91f0cbf Mon Sep 17 00:00:00 2001 From: Stepan Urazov <110625288+atlasgorn@users.noreply.github.com> Date: Mon, 26 May 2025 20:52:59 +0300 Subject: [PATCH 20/33] Added support for .wesl files to the regex pattern for examples (#19178) ## Objective [Shaders / Material - WESL](https://bevyengine.org/examples-webgpu/shaders/shader-material-wesl/) example doesn't have a WESL file tab ## Solution Added wesl to regex --------- Co-authored-by: Stepan Urazov <110625288+hg127@users.noreply.github.com> --- tools/example-showcase/src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tools/example-showcase/src/main.rs b/tools/example-showcase/src/main.rs index c8feff10503e1..6fa24b1a27359 100644 --- a/tools/example-showcase/src/main.rs +++ b/tools/example-showcase/src/main.rs @@ -773,9 +773,7 @@ fn parse_examples() -> Vec { let technical_name = val.get("name").unwrap().as_str().unwrap().to_string(); let source_code = fs::read_to_string(val["path"].as_str().unwrap()).unwrap(); - let shader_regex = - Regex::new(r"(shaders\/\w+\.wgsl)|(shaders\/\w+\.frag)|(shaders\/\w+\.vert)") - .unwrap(); + let shader_regex = Regex::new(r"shaders\/\w+\.(wgsl|frag|vert|wesl)").unwrap(); // Find all instances of references to shader files, and keep them in an ordered and deduped vec. let mut shader_paths = vec![]; From d0ed3ab379840847ed628c525583d35a1a98ee07 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Mon, 26 May 2025 14:07:17 -0400 Subject: [PATCH 21/33] Move trigger_screenshots to finish() (#19204) # Objective - The tigger_screenshots system gets added in `.build()` but relies on a resource that is only inserted in `.finish()` - This isn't a bug for most users, but when doing headless mode testing it can technically work without ever calling `.finish()` and did work before bevy 0.15 but while migrating my work codebase I had an issue of test failing because of this ## Solution - Move the trigger_screenshots system to `.finish()` ## Testing - I ran the screenshot example and it worked as expected --- crates/bevy_render/src/view/window/screenshot.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 6e223eedaf047..8e1e611d179a1 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -403,7 +403,6 @@ impl Plugin for ScreenshotPlugin { .after(event_update_system) .before(ApplyDeferred), ) - .add_systems(Update, trigger_screenshots) .register_type::() .register_type::(); @@ -417,7 +416,8 @@ impl Plugin for ScreenshotPlugin { fn finish(&self, app: &mut bevy_app::App) { let (tx, rx) = std::sync::mpsc::channel(); - app.insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx)))); + app.add_systems(Update, trigger_screenshots) + .insert_resource(CapturedScreenshots(Arc::new(Mutex::new(rx)))); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app From 41178347c14ef5297741b1a8af10bba7bbe35647 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Mon, 26 May 2025 15:26:26 -0400 Subject: [PATCH 22/33] Add WakeUp event to App (#19212) # Objective - The WakeUp event is never added to the app. If you need to use that event you currently need to add it yourself. ## Solution - Add the WakeUp event to the App in the WinitPlugin ## Testing - I tested the window_setting example and it compiled and worked --- crates/bevy_winit/src/state.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 33ad693c5ab4a..378426d9689a3 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -110,8 +110,9 @@ struct WinitAppRunnerState { impl WinitAppRunnerState { fn new(mut app: App) -> Self { + app.add_event::(); #[cfg(feature = "custom_cursor")] - app.add_event::().init_resource::(); + app.init_resource::(); let event_writer_system_state: SystemState<( EventWriter, From 1126949807fc8a403604744ece9e3be929c43494 Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Mon, 26 May 2025 21:28:56 +0200 Subject: [PATCH 23/33] No schedule build pass overwrite if build settings do not change `auto_insert_apply_deferred` from `true` (#19217) # Objective Fixes #18790. Simpler alternative to #19195. ## Solution As suggested by @PixelDust22, simply avoid overwriting the pass if the schedule already has auto sync points enabled. Leave pass logic untouched. It still is probably a bad idea to add systems/set configs before changing the build settings, but that is not important as long there are no more complex build passes. ## Testing Added a test. --------- Co-authored-by: Thierry Berger --- crates/bevy_ecs/src/schedule/schedule.rs | 54 +++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 584e621bf8a39..8015bde6c1f74 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -394,9 +394,21 @@ impl Schedule { } /// Changes miscellaneous build settings. + /// + /// If [`settings.auto_insert_apply_deferred`][ScheduleBuildSettings::auto_insert_apply_deferred] + /// is `false`, this clears `*_ignore_deferred` edge settings configured so far. + /// + /// Generally this method should be used before adding systems or set configurations to the schedule, + /// not after. pub fn set_build_settings(&mut self, settings: ScheduleBuildSettings) -> &mut Self { if settings.auto_insert_apply_deferred { - self.add_build_pass(passes::AutoInsertApplyDeferredPass::default()); + if !self + .graph + .passes + .contains_key(&TypeId::of::()) + { + self.add_build_pass(passes::AutoInsertApplyDeferredPass::default()); + } } else { self.remove_build_pass::(); } @@ -2076,6 +2088,46 @@ mod tests { #[derive(Resource)] struct Resource2; + #[test] + fn unchanged_auto_insert_apply_deferred_has_no_effect() { + use alloc::{vec, vec::Vec}; + + #[derive(PartialEq, Debug)] + enum Entry { + System(usize), + SyncPoint(usize), + } + + #[derive(Resource, Default)] + struct Log(Vec); + + fn system(mut res: ResMut, mut commands: Commands) { + res.0.push(Entry::System(N)); + commands + .queue(|world: &mut World| world.resource_mut::().0.push(Entry::SyncPoint(N))); + } + + let mut world = World::default(); + world.init_resource::(); + let mut schedule = Schedule::default(); + schedule.add_systems((system::<1>, system::<2>).chain_ignore_deferred()); + schedule.set_build_settings(ScheduleBuildSettings { + auto_insert_apply_deferred: true, + ..Default::default() + }); + schedule.run(&mut world); + let actual = world.remove_resource::().unwrap().0; + + let expected = vec![ + Entry::System(1), + Entry::System(2), + Entry::SyncPoint(1), + Entry::SyncPoint(2), + ]; + + assert_eq!(actual, expected); + } + // regression test for https://github.com/bevyengine/bevy/issues/9114 #[test] fn ambiguous_with_not_breaking_run_conditions() { From 6fc2e919b819fba31414c54e2afa7439c615f955 Mon Sep 17 00:00:00 2001 From: Benjamin Brienen Date: Mon, 26 May 2025 21:38:28 +0200 Subject: [PATCH 24/33] Make sure that `serde_json::Map::into_values` exists (#19229) # Objective cargo update was required to build because into_values was added in a patch version ## Solution Depend on the new patch ## Testing Builds locally now --- Cargo.toml | 2 +- crates/bevy_gltf/Cargo.toml | 2 +- crates/bevy_reflect/Cargo.toml | 2 +- crates/bevy_remote/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8a864307245f8..2a20b538044bf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -540,7 +540,7 @@ rand_chacha = "0.3.1" ron = "0.8.0" flate2 = "1.0" serde = { version = "1", features = ["derive"] } -serde_json = "1" +serde_json = "1.0.140" bytemuck = "1.7" bevy_render = { path = "crates/bevy_render", version = "0.16.0", default-features = false } # The following explicit dependencies are needed for proc macros to work inside of examples as they are part of the bevy crate itself. diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index ef99cc2bbd490..a31828f6c392c 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -62,7 +62,7 @@ fixedbitset = "0.5" itertools = "0.14" percent-encoding = "2.1" serde = { version = "1.0", features = ["derive"] } -serde_json = "1" +serde_json = "1.0.140" smallvec = "1.11" tracing = { version = "0.1", default-features = false, features = ["std"] } diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index f37865f9a3f62..483d2782f7d3f 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -123,7 +123,7 @@ wgpu-types = { version = "24", features = [ ron = "0.8.0" rmp-serde = "1.1" bincode = { version = "2.0", features = ["serde"] } -serde_json = "1.0" +serde_json = "1.0.140" serde = { version = "1", features = ["derive"] } static_assertions = "1.1.0" diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index 3ccbcbdc3e077..a2b8e7d994ee2 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -31,7 +31,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0", default-feature anyhow = "1" hyper = { version = "1", features = ["server", "http1"] } serde = { version = "1", features = ["derive"] } -serde_json = { version = "1" } +serde_json = "1.0.140" http-body-util = "0.1" async-channel = "2" From 4562bb484f74141fbaca8a5015bbeb1ce0664429 Mon Sep 17 00:00:00 2001 From: SpecificProtagonist Date: Mon, 26 May 2025 22:15:21 +0200 Subject: [PATCH 25/33] Fix spawn tracking for spawn commands (#19351) See also https://discord.com/channels/691052431525675048/1374187654425481266/1375553989185372292. Set spawn info in `Commands::spawn_empty`. Also added a benchmark for `Commands::spawn`. See added test. --- benches/benches/bevy_ecs/world/commands.rs | 25 +++++++++++ benches/benches/bevy_ecs/world/mod.rs | 1 + crates/bevy_ecs/src/entity/mod.rs | 23 ++++++++++- crates/bevy_ecs/src/system/commands/mod.rs | 48 +++++++++++++++++++--- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 8ad87862eba24..7ffd7c6773fea 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -62,6 +62,31 @@ pub fn spawn_commands(criterion: &mut Criterion) { group.finish(); } +pub fn nonempty_spawn_commands(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("nonempty_spawn_commands"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + + for entity_count in [100, 1_000, 10_000] { + group.bench_function(format!("{}_entities", entity_count), |bencher| { + let mut world = World::default(); + let mut command_queue = CommandQueue::default(); + + bencher.iter(|| { + let mut commands = Commands::new(&mut command_queue, &world); + for i in 0..entity_count { + if black_box(i % 2 == 0) { + commands.spawn(A); + } + } + command_queue.apply(&mut world); + }); + }); + } + + group.finish(); +} + #[derive(Default, Component)] struct Matrix([[f32; 4]; 4]); diff --git a/benches/benches/bevy_ecs/world/mod.rs b/benches/benches/bevy_ecs/world/mod.rs index e35dc999c2eb8..7158f2f033498 100644 --- a/benches/benches/bevy_ecs/world/mod.rs +++ b/benches/benches/bevy_ecs/world/mod.rs @@ -17,6 +17,7 @@ criterion_group!( benches, empty_commands, spawn_commands, + nonempty_spawn_commands, insert_commands, fake_commands, zero_sized_commands, diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 7bba07aac6017..c9ba9e2643dd1 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -74,6 +74,7 @@ pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; use crate::{ archetype::{ArchetypeId, ArchetypeRow}, change_detection::MaybeLocation, + component::Tick, identifier::{ error::IdentifierError, kinds::IdKind, @@ -84,7 +85,13 @@ use crate::{ }; use alloc::vec::Vec; use bevy_platform::sync::atomic::Ordering; -use core::{fmt, hash::Hash, mem, num::NonZero, panic::Location}; +use core::{ + fmt, + hash::Hash, + mem::{self}, + num::NonZero, + panic::Location, +}; use log::warn; #[cfg(feature = "serialize")] @@ -866,6 +873,20 @@ impl Entities { meta.location = location; } + /// # Safety + /// - `index` must be a valid entity index. + #[inline] + pub(crate) unsafe fn mark_spawn_despawn(&mut self, index: u32, by: MaybeLocation, _at: Tick) { + // // SAFETY: Caller guarantees that `index` a valid entity index + // let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; + // meta.spawned_or_despawned_by = MaybeUninit::new(SpawnedOrDespawned { by, at }); + by.map(|caller| { + // SAFETY: Caller guarantees that `index` a valid entity index + let meta = unsafe { self.meta.get_unchecked_mut(index as usize) }; + meta.spawned_or_despawned_by = MaybeLocation::new(Some(caller)); + }); + } + /// Increments the `generation` of a freed [`Entity`]. The next entity ID allocated with this /// `index` will count `generation` starting from the prior `generation` + the specified /// value + 1. diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 4cb6d61bc0e9a..3f87cf664c3d3 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -318,12 +318,24 @@ impl<'w, 's> Commands<'w, 's> { /// - [`spawn`](Self::spawn) to spawn an entity with components. /// - [`spawn_batch`](Self::spawn_batch) to spawn many entities /// with the same combination of components. + #[track_caller] pub fn spawn_empty(&mut self) -> EntityCommands { let entity = self.entities.reserve_entity(); - EntityCommands { + let mut entity_commands = EntityCommands { entity, commands: self.reborrow(), - } + }; + let caller = MaybeLocation::caller(); + entity_commands.queue(move |entity: EntityWorldMut| { + let index = entity.id().index(); + let world = entity.into_world_mut(); + let tick = world.change_tick(); + // SAFETY: Entity has been flushed + unsafe { + world.entities_mut().mark_spawn_despawn(index, caller, tick); + } + }); + entity_commands } /// Spawns a new [`Entity`] with the given components @@ -370,9 +382,35 @@ impl<'w, 's> Commands<'w, 's> { /// with the same combination of components. #[track_caller] pub fn spawn(&mut self, bundle: T) -> EntityCommands { - let mut entity = self.spawn_empty(); - entity.insert(bundle); - entity + let entity = self.entities.reserve_entity(); + let mut entity_commands = EntityCommands { + entity, + commands: self.reborrow(), + }; + let caller = MaybeLocation::caller(); + + entity_commands.queue(move |mut entity: EntityWorldMut| { + // Store metadata about the spawn operation. + // This is the same as in `spawn_empty`, but merged into + // the same command for better performance. + let index = entity.id().index(); + entity.world_scope(|world| { + let tick = world.change_tick(); + // SAFETY: Entity has been flushed + unsafe { + world.entities_mut().mark_spawn_despawn(index, caller, tick); + } + }); + + entity.insert_with_caller( + bundle, + InsertMode::Replace, + caller, + crate::relationship::RelationshipHookMode::Run, + ); + }); + // entity_command::insert(bundle, InsertMode::Replace) + entity_commands } /// Returns the [`EntityCommands`] for the given [`Entity`]. From b14f94ec340b2c1746112ec73802232ba243fb39 Mon Sep 17 00:00:00 2001 From: atlv Date: Tue, 27 May 2025 00:58:58 -0400 Subject: [PATCH 26/33] doc(render): fix incorrectly transposed view matrix docs (#19317) # Objective - Mend incorrect docs ## Solution - Mend them - add example use - clarify column major ## Testing - No code changes --- crates/bevy_render/src/view/mod.rs | 44 +++++++++++++++------------ crates/bevy_render/src/view/view.wgsl | 22 ++++++++------ 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index c392dcaaebe76..6dd9283d0db40 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -262,34 +262,36 @@ impl RetainedViewEntity { pub struct ExtractedView { /// The entity in the main world corresponding to this render world view. pub retained_view_entity: RetainedViewEntity, - /// Typically a right-handed projection matrix, one of either: + /// Typically a column-major right-handed projection matrix, one of either: /// /// Perspective (infinite reverse z) /// ```text /// f = 1 / tan(fov_y_radians / 2) /// - /// ⎡ f / aspect 0 0 0 ⎤ - /// ⎢ 0 f 0 0 ⎥ - /// ⎢ 0 0 0 -1 ⎥ - /// ⎣ 0 0 near 0 ⎦ + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 near ⎥ + /// ⎣ 0 0 -1 0 ⎦ /// ``` /// /// Orthographic /// ```text /// w = right - left /// h = top - bottom - /// d = near - far + /// d = far - near /// cw = -right - left /// ch = -top - bottom /// - /// ⎡ 2 / w 0 0 0 ⎤ - /// ⎢ 0 2 / h 0 0 ⎥ - /// ⎢ 0 0 1 / d 0 ⎥ - /// ⎣ cw / w ch / h near / d 1 ⎦ + /// ⎡ 2 / w 0 0 cw / w ⎤ + /// ⎢ 0 2 / h 0 ch / h ⎥ + /// ⎢ 0 0 1 / d far / d ⎥ + /// ⎣ 0 0 0 1 ⎦ /// ``` /// /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic /// + /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` + /// /// Custom projections are also possible however. pub clip_from_view: Mat4, pub world_from_view: GlobalTransform, @@ -529,34 +531,36 @@ pub struct ViewUniform { pub world_from_clip: Mat4, pub world_from_view: Mat4, pub view_from_world: Mat4, - /// Typically a right-handed projection matrix, one of either: + /// Typically a column-major right-handed projection matrix, one of either: /// /// Perspective (infinite reverse z) /// ```text /// f = 1 / tan(fov_y_radians / 2) /// - /// ⎡ f / aspect 0 0 0 ⎤ - /// ⎢ 0 f 0 0 ⎥ - /// ⎢ 0 0 0 -1 ⎥ - /// ⎣ 0 0 near 0 ⎦ + /// ⎡ f / aspect 0 0 0 ⎤ + /// ⎢ 0 f 0 0 ⎥ + /// ⎢ 0 0 0 near ⎥ + /// ⎣ 0 0 -1 0 ⎦ /// ``` /// /// Orthographic /// ```text /// w = right - left /// h = top - bottom - /// d = near - far + /// d = far - near /// cw = -right - left /// ch = -top - bottom /// - /// ⎡ 2 / w 0 0 0 ⎤ - /// ⎢ 0 2 / h 0 0 ⎥ - /// ⎢ 0 0 1 / d 0 ⎥ - /// ⎣ cw / w ch / h near / d 1 ⎦ + /// ⎡ 2 / w 0 0 cw / w ⎤ + /// ⎢ 0 2 / h 0 ch / h ⎥ + /// ⎢ 0 0 1 / d far / d ⎥ + /// ⎣ 0 0 0 1 ⎦ /// ``` /// /// `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic /// + /// Glam matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` + /// /// Custom projections are also possible however. pub clip_from_view: Mat4, pub view_from_clip: Mat4, diff --git a/crates/bevy_render/src/view/view.wgsl b/crates/bevy_render/src/view/view.wgsl index 317de2eb88073..7b14bab9e1ca7 100644 --- a/crates/bevy_render/src/view/view.wgsl +++ b/crates/bevy_render/src/view/view.wgsl @@ -19,33 +19,35 @@ struct View { world_from_clip: mat4x4, world_from_view: mat4x4, view_from_world: mat4x4, - // Typically a right-handed projection matrix, one of either: + // Typically a column-major right-handed projection matrix, one of either: // // Perspective (infinite reverse z) // ``` // f = 1 / tan(fov_y_radians / 2) // - // ⎡ f / aspect 0 0 0 ⎤ - // ⎢ 0 f 0 0 ⎥ - // ⎢ 0 0 0 -1 ⎥ - // ⎣ 0 0 near 0 ⎦ + // ⎡ f / aspect 0 0 0 ⎤ + // ⎢ 0 f 0 0 ⎥ + // ⎢ 0 0 0 near ⎥ + // ⎣ 0 0 -1 0 ⎦ // ``` // // Orthographic // ``` // w = right - left // h = top - bottom - // d = near - far + // d = far - near // cw = -right - left // ch = -top - bottom // - // ⎡ 2 / w 0 0 0 ⎤ - // ⎢ 0 2 / h 0 0 ⎥ - // ⎢ 0 0 1 / d 0 ⎥ - // ⎣ cw / w ch / h near / d 1 ⎦ + // ⎡ 2 / w 0 0 cw / w ⎤ + // ⎢ 0 2 / h 0 ch / h ⎥ + // ⎢ 0 0 1 / d far / d ⎥ + // ⎣ 0 0 0 1 ⎦ // ``` // // `clip_from_view[3][3] == 1.0` is the standard way to check if a projection is orthographic + // + // Wgsl matrices are column major, so for example getting the near plane of a perspective projection is `clip_from_view[3][2]` // // Custom projections are also possible however. clip_from_view: mat4x4, From 50f70ebb916162eecf394a5d6de3be92ee01afe7 Mon Sep 17 00:00:00 2001 From: HeartofPhos <43216176+HeartofPhos@users.noreply.github.com> Date: Tue, 27 May 2025 23:05:31 +0200 Subject: [PATCH 27/33] Fix custom relations panics with parent/child relations (#19341) # Objective Fixes #18905 ## Solution `world.commands().entity(target_entity).queue(command)` calls `commands.with_entity` without an error handler, instead queue on `Commands` with an error handler ## Testing Added unit test Co-authored-by: Heart <> --- crates/bevy_ecs/src/relationship/mod.rs | 81 ++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 10 deletions(-) diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 9a2a2a2d5a39a..3522118fbc820 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -158,19 +158,21 @@ pub trait Relationship: Component + Sized { { relationship_target.collection_mut_risky().remove(entity); if relationship_target.len() == 0 { - if let Ok(mut entity) = world.commands().get_entity(target_entity) { + let command = |mut entity: EntityWorldMut| { // this "remove" operation must check emptiness because in the event that an identical // relationship is inserted on top, this despawn would result in the removal of that identical // relationship ... not what we want! - entity.queue(|mut entity: EntityWorldMut| { - if entity - .get::() - .is_some_and(RelationshipTarget::is_empty) - { - entity.remove::(); - } - }); - } + if entity + .get::() + .is_some_and(RelationshipTarget::is_empty) + { + entity.remove::(); + } + }; + + world + .commands() + .queue(command.with_entity(target_entity).handle_error_with(ignore)); } } } @@ -424,4 +426,63 @@ mod tests { // No assert necessary, looking to make sure compilation works with the macros } + + #[test] + fn parent_child_relationship_with_custom_relationship() { + use crate::prelude::ChildOf; + + #[derive(Component)] + #[relationship(relationship_target = RelTarget)] + struct Rel(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Rel)] + struct RelTarget(Entity); + + let mut world = World::new(); + + // Rel on Parent + // Despawn Parent + let mut commands = world.commands(); + let child = commands.spawn_empty().id(); + let parent = commands.spawn(Rel(child)).add_child(child).id(); + commands.entity(parent).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(world.get_entity(parent).is_err()); + + // Rel on Parent + // Despawn Child + let mut commands = world.commands(); + let child = commands.spawn_empty().id(); + let parent = commands.spawn(Rel(child)).add_child(child).id(); + commands.entity(child).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(!world.entity(parent).contains::()); + + // Rel on Child + // Despawn Parent + let mut commands = world.commands(); + let parent = commands.spawn_empty().id(); + let child = commands.spawn((ChildOf(parent), Rel(parent))).id(); + commands.entity(parent).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(world.get_entity(parent).is_err()); + + // Rel on Child + // Despawn Child + let mut commands = world.commands(); + let parent = commands.spawn_empty().id(); + let child = commands.spawn((ChildOf(parent), Rel(parent))).id(); + commands.entity(child).despawn(); + world.flush(); + + assert!(world.get_entity(child).is_err()); + assert!(!world.entity(parent).contains::()); + } } From bc00178b596f637391d556f1e3cdbfc620f3bebe Mon Sep 17 00:00:00 2001 From: eugineerd <70062110+eugineerd@users.noreply.github.com> Date: Fri, 30 May 2025 22:28:53 +0300 Subject: [PATCH 28/33] Fix `EntityCloner` replacing required components. (#19326) # Objective Fix #19324 ## Solution `EntityCloner` replaces required components when filtering. This is unexpected when comparing with the way the rest of bevy handles required components. This PR separates required components from explicit components when filtering in `EntityClonerBuilder`. ## Testing Added a regression test for this case. --- crates/bevy_ecs/src/entity/clone_entities.rs | 67 +++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index 3337894066260..dd6cb360e915d 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -5,6 +5,7 @@ use bumpalo::Bump; use core::any::TypeId; use crate::{ + archetype::Archetype, bundle::Bundle, component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo}, entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper}, @@ -346,6 +347,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { pub struct EntityCloner { filter_allows_components: bool, filter: HashSet, + filter_required: HashSet, clone_behavior_overrides: HashMap, move_components: bool, linked_cloning: bool, @@ -362,6 +364,7 @@ impl Default for EntityCloner { linked_cloning: false, default_clone_fn: ComponentCloneBehavior::global_default_fn(), filter: Default::default(), + filter_required: Default::default(), clone_behavior_overrides: Default::default(), clone_queue: Default::default(), deferred_commands: Default::default(), @@ -465,6 +468,12 @@ impl EntityCloner { { let world = world.as_unsafe_world_cell(); let source_entity = world.get_entity(source).expect("Source entity must exist"); + let target_archetype = (!self.filter_required.is_empty()).then(|| { + world + .get_entity(target) + .expect("Target entity must exist") + .archetype() + }); #[cfg(feature = "bevy_reflect")] // SAFETY: we have unique access to `world`, nothing else accesses the registry at this moment, and we clone @@ -481,7 +490,7 @@ impl EntityCloner { bundle_scratch = BundleScratch::with_capacity(archetype.component_count()); for component in archetype.components() { - if !self.is_cloning_allowed(&component) { + if !self.is_cloning_allowed(&component, target_archetype) { continue; } @@ -605,9 +614,19 @@ impl EntityCloner { target } - fn is_cloning_allowed(&self, component: &ComponentId) -> bool { - (self.filter_allows_components && self.filter.contains(component)) - || (!self.filter_allows_components && !self.filter.contains(component)) + fn is_cloning_allowed( + &self, + component: &ComponentId, + target_archetype: Option<&Archetype>, + ) -> bool { + if self.filter_allows_components { + self.filter.contains(component) + || target_archetype.is_some_and(|archetype| { + !archetype.contains(*component) && self.filter_required.contains(component) + }) + } else { + !self.filter.contains(component) && !self.filter_required.contains(component) + } } } @@ -809,9 +828,9 @@ impl<'w> EntityClonerBuilder<'w> { if let Some(info) = self.world.components().get_info(id) { for required_id in info.required_components().iter_ids() { if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.insert(required_id); + self.entity_cloner.filter_required.insert(required_id); } else { - self.entity_cloner.filter.remove(&required_id); + self.entity_cloner.filter_required.remove(&required_id); } } } @@ -829,9 +848,9 @@ impl<'w> EntityClonerBuilder<'w> { if let Some(info) = self.world.components().get_info(id) { for required_id in info.required_components().iter_ids() { if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.remove(&required_id); + self.entity_cloner.filter_required.remove(&required_id); } else { - self.entity_cloner.filter.insert(required_id); + self.entity_cloner.filter_required.insert(required_id); } } } @@ -1406,4 +1425,36 @@ mod tests { ); assert!(world.resource::().0); } + + #[test] + fn cloning_with_required_components_preserves_existing() { + #[derive(Component, Clone, PartialEq, Debug, Default)] + #[require(B(5))] + struct A; + + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u32); + + let mut world = World::default(); + + let e = world.spawn((A, B(0))).id(); + let e_clone = world.spawn(B(1)).id(); + + EntityCloner::build(&mut world) + .deny_all() + .allow::() + .clone_entity(e, e_clone); + + assert_eq!(world.entity(e_clone).get::(), Some(&A)); + assert_eq!(world.entity(e_clone).get::(), Some(&B(1))); + + let e_clone2 = world.spawn(B(2)).id(); + + EntityCloner::build(&mut world) + .allow_all() + .deny::() + .clone_entity(e, e_clone2); + + assert_eq!(world.entity(e_clone2).get::(), Some(&B(2))); + } } From 56bdd5c3c1fe0cf4772afd0e4bd7bb585ee61c3f Mon Sep 17 00:00:00 2001 From: Manuel Brea Carreras Date: Fri, 30 May 2025 20:33:47 +0100 Subject: [PATCH 29/33] Fix #19219 by moving observer triggers out of resource_scope (#19221) # Objective Fixes #19219 ## Solution Instead of calling `world.commands().trigger` and `world.commands().trigger_targets` whenever each scene is spawned, save the `instance_id` and optional parent entity to perform all such calls at the end. This prevents the potential flush of the world command queue that can happen if `add_child` is called from causing the crash. ## Testing - Did you test these changes? If so, how? - Verified that I can no longer reproduce the bug with the instructions at #19219. - Ran `bevy_scene` tests - Visually verified that the following examples still run as expected `many_foxes`, `scene` . (should I test any more?) - Are there any parts that need more testing? - Pending to run `cargo test` at the root to test that all examples still build; I will update the PR when that's done - How can other people (reviewers) test your changes? Is there anything specific they need to know? - Run bevy as usual - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? - N/a (tested on Linux/wayland but it shouldn't be relevant) --- --- crates/bevy_scene/src/scene_spawner.rs | 27 +++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index dce5ad971e105..be9df93607d46 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -79,6 +79,7 @@ pub struct SceneSpawner { scenes_to_despawn: Vec>, instances_to_despawn: Vec, scenes_with_parent: Vec<(InstanceId, Entity)>, + instances_ready: Vec<(InstanceId, Option)>, } /// Errors that can occur when spawning a scene. @@ -337,8 +338,9 @@ impl SceneSpawner { // Scenes with parents need more setup before they are ready. // See `set_scene_instance_parent_sync()`. if parent.is_none() { - // Defer via commands otherwise SceneSpawner is not available in the observer. - world.commands().trigger(SceneInstanceReady { instance_id }); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, None)); } } Err(SceneSpawnError::NonExistentScene { .. }) => { @@ -362,8 +364,9 @@ impl SceneSpawner { // Scenes with parents need more setup before they are ready. // See `set_scene_instance_parent_sync()`. if parent.is_none() { - // Defer via commands otherwise SceneSpawner is not available in the observer. - world.commands().trigger(SceneInstanceReady { instance_id }); + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, None)); } } Err(SceneSpawnError::NonExistentRealScene { .. }) => { @@ -398,12 +401,25 @@ impl SceneSpawner { } } + // We trigger `SceneInstanceReady` events after processing all scenes + // SceneSpawner may not be available in the observer. + self.instances_ready.push((instance_id, Some(parent))); + } else { + self.scenes_with_parent.push((instance_id, parent)); + } + } + } + + fn trigger_scene_ready_events(&mut self, world: &mut World) { + for (instance_id, parent) in self.instances_ready.drain(..) { + if let Some(parent) = parent { // Defer via commands otherwise SceneSpawner is not available in the observer. world .commands() .trigger_targets(SceneInstanceReady { instance_id }, parent); } else { - self.scenes_with_parent.push((instance_id, parent)); + // Defer via commands otherwise SceneSpawner is not available in the observer. + world.commands().trigger(SceneInstanceReady { instance_id }); } } } @@ -477,6 +493,7 @@ pub fn scene_spawner_system(world: &mut World) { .update_spawned_scenes(world, &updated_spawned_scenes) .unwrap(); scene_spawner.set_scene_instance_parent_sync(world); + scene_spawner.trigger_scene_ready_events(world); }); } From 75d92f44349ccc74170e65bf0f959ba85abf4a58 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Fri, 30 May 2025 21:35:55 +0200 Subject: [PATCH 30/33] fix distinct directional lights per view (#19147) # Objective after #15156 it seems like using distinct directional lights on different views is broken (and will probably break spotlights too). fix them ## Solution the reason is a bit hairy so with an example: - camera 0 on layer 0 - camera 1 on layer 1 - dir light 0 on layer 0 (2 cascades) - dir light 1 on layer 1 (2 cascades) in render/lights.rs: - outside of any view loop, - we count the total number of shadow casting directional light cascades (4) and assign an incrementing `depth_texture_base_index` for each (0-1 for one light, 2-3 for the other, depending on iteration order) (line 1034) - allocate a texture array for the total number of cascades plus spotlight maps (4) (line 1106) - in the view loop, for directional lights we - skip lights that don't intersect on renderlayers (line 1440) - assign an incrementing texture layer to each light/cascade starting from 0 (resets to 0 per view) (assigning 0 and 1 each time for the 2 cascades of the intersecting light) (line 1509, init at 1421) then in the rendergraph: - camera 0 renders the shadow map for light 0 to texture indices 0 and 1 - camera 0 renders using shadows from the `depth_texture_base_index` (maybe 0-1, maybe 2-3 depending on the iteration order) - camera 1 renders the shadow map for light 1 to texture indices 0 and 1 - camera 0 renders using shadows from the `depth_texture_base_index` (maybe 0-1, maybe 2-3 depending on the iteration order) issues: - one of the views uses empty shadow maps (bug) - we allocated a texture layer per cascade per light, even though not all lights are used on all views (just inefficient) - I think we're allocating texture layers even for lights with `shadows_enabled: false` (just inefficient) solution: - calculate upfront the view with the largest number of directional cascades - allocate this many layers (plus layers for spotlights) in the texture array - keep using texture layers 0..n in the per-view loop, but build GpuLights.gpu_directional_lights within the loop too so it refers to the same layers we render to nice side effects: - we can now use `max_texture_array_layers / MAX_CASCADES_PER_LIGHT` shadow-casting directional lights per view, rather than overall. - we can remove the `GpuDirectionalLight::skip` field, since the gpu lights struct is constructed per view a simpler approach would be to keep everything the same, and just increment the texture layer index in the view loop even for non-intersecting lights. this pr reduces the total shadowmap vram used as well and isn't *much* extra complexity. but if we want something less risky/intrusive for 16.1 that would be the way. ## Testing i edited the split screen example to put separate lights on layer 1 and layer 2, and put the plane and fox on both layers (using lots of unrelated code for render layer propagation from #17575). without the fix the directional shadows will only render on one of the top 2 views even though there are directional lights on both layers. ```rs //! Renders two cameras to the same window to accomplish "split screen". use std::f32::consts::PI; use bevy::{ pbr::CascadeShadowConfigBuilder, prelude::*, render::camera::Viewport, window::WindowResized, }; use bevy_render::view::RenderLayers; fn main() { App::new() .add_plugins(DefaultPlugins) .add_plugins(HierarchyPropagatePlugin::::default()) .add_systems(Startup, setup) .add_systems(Update, (set_camera_viewports, button_system)) .run(); } /// set up a simple 3D scene fn setup( mut commands: Commands, asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, ) { let all_layers = RenderLayers::layer(1).with(2).with(3).with(4); // plane commands.spawn(( Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), all_layers.clone() )); commands.spawn(( SceneRoot( asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")), ), Propagate(all_layers.clone()), )); // Light commands.spawn(( Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)), DirectionalLight { shadows_enabled: true, ..default() }, CascadeShadowConfigBuilder { num_cascades: if cfg!(all( feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu") )) { // Limited to 1 cascade in WebGL 1 } else { 2 }, first_cascade_far_bound: 200.0, maximum_distance: 280.0, ..default() } .build(), RenderLayers::layer(1), )); commands.spawn(( Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)), DirectionalLight { shadows_enabled: true, ..default() }, CascadeShadowConfigBuilder { num_cascades: if cfg!(all( feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu") )) { // Limited to 1 cascade in WebGL 1 } else { 2 }, first_cascade_far_bound: 200.0, maximum_distance: 280.0, ..default() } .build(), RenderLayers::layer(2), )); // Cameras and their dedicated UI for (index, (camera_name, camera_pos)) in [ ("Player 1", Vec3::new(0.0, 200.0, -150.0)), ("Player 2", Vec3::new(150.0, 150., 50.0)), ("Player 3", Vec3::new(100.0, 150., -150.0)), ("Player 4", Vec3::new(-100.0, 80., 150.0)), ] .iter() .enumerate() { let camera = commands .spawn(( Camera3d::default(), Transform::from_translation(*camera_pos).looking_at(Vec3::ZERO, Vec3::Y), Camera { // Renders cameras with different priorities to prevent ambiguities order: index as isize, ..default() }, CameraPosition { pos: UVec2::new((index % 2) as u32, (index / 2) as u32), }, RenderLayers::layer(index+1) )) .id(); // Set up UI commands .spawn(( UiTargetCamera(camera), Node { width: Val::Percent(100.), height: Val::Percent(100.), ..default() }, )) .with_children(|parent| { parent.spawn(( Text::new(*camera_name), Node { position_type: PositionType::Absolute, top: Val::Px(12.), left: Val::Px(12.), ..default() }, )); buttons_panel(parent); }); } fn buttons_panel(parent: &mut ChildSpawnerCommands) { parent .spawn(Node { position_type: PositionType::Absolute, width: Val::Percent(100.), height: Val::Percent(100.), display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::SpaceBetween, align_items: AlignItems::Center, padding: UiRect::all(Val::Px(20.)), ..default() }) .with_children(|parent| { rotate_button(parent, "<", Direction::Left); rotate_button(parent, ">", Direction::Right); }); } fn rotate_button(parent: &mut ChildSpawnerCommands, caption: &str, direction: Direction) { parent .spawn(( RotateCamera(direction), Button, Node { width: Val::Px(40.), height: Val::Px(40.), border: UiRect::all(Val::Px(2.)), justify_content: JustifyContent::Center, align_items: AlignItems::Center, ..default() }, BorderColor(Color::WHITE), BackgroundColor(Color::srgb(0.25, 0.25, 0.25)), )) .with_children(|parent| { parent.spawn(Text::new(caption)); }); } } #[derive(Component)] struct CameraPosition { pos: UVec2, } #[derive(Component)] struct RotateCamera(Direction); enum Direction { Left, Right, } fn set_camera_viewports( windows: Query<&Window>, mut resize_events: EventReader, mut query: Query<(&CameraPosition, &mut Camera)>, ) { // 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.read() { let window = windows.get(resize_event.window).unwrap(); let size = window.physical_size() / 2; for (camera_position, mut camera) in &mut query { camera.viewport = Some(Viewport { physical_position: camera_position.pos * size, physical_size: size, ..default() }); } } } fn button_system( interaction_query: Query< (&Interaction, &ComputedNodeTarget, &RotateCamera), (Changed, With