diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37db848558..526ac4c401 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: - name: CI job # To run the tests one item at a time for troubleshooting, use # cargo --quiet test --lib -- --list | sed 's/: test$//' | MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-disable-weak-memory-emulation" xargs -n1 cargo miri test -p bevy_ecs --lib -- --exact - run: cargo miri test -p bevy_ecs + run: cargo miri test -p bevy_ecs --features bevy_utils/debug env: # -Zrandomize-layout makes sure we dont rely on the layout of anything that might change RUSTFLAGS: -Zrandomize-layout @@ -247,7 +247,7 @@ jobs: - name: Check wasm run: cargo check --target wasm32-unknown-unknown -Z build-std=std,panic_abort env: - RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory -D warnings" + RUSTFLAGS: "-C target-feature=+atomics,+bulk-memory" markdownlint: runs-on: ubuntu-latest @@ -293,7 +293,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.32.0 + uses: crate-ci/typos@v1.33.1 - name: Typos info if: failure() run: | diff --git a/Cargo.toml b/Cargo.toml index 2d9524a527..363e85b3b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,7 +74,6 @@ allow_attributes_without_reason = "warn" [workspace.lints.rust] missing_docs = "warn" -mismatched_lifetime_syntaxes = "allow" unexpected_cfgs = { level = "warn", check-cfg = ['cfg(docsrs_dep)'] } unsafe_code = "deny" unsafe_op_in_unsafe_fn = "warn" @@ -136,6 +135,7 @@ default = [ "bevy_audio", "bevy_color", "bevy_core_pipeline", + "bevy_core_widgets", "bevy_anti_aliasing", "bevy_gilrs", "bevy_gizmos", @@ -167,6 +167,7 @@ default = [ "vorbis", "webgl2", "x11", + "debug", ] # Recommended defaults for no_std applications @@ -249,6 +250,15 @@ bevy_render = ["bevy_internal/bevy_render", "bevy_color"] # Provides scene functionality bevy_scene = ["bevy_internal/bevy_scene", "bevy_asset"] +# Provides raytraced lighting (experimental) +bevy_solari = [ + "bevy_internal/bevy_solari", + "bevy_asset", + "bevy_core_pipeline", + "bevy_pbr", + "bevy_render", +] + # Provides sprite functionality bevy_sprite = [ "bevy_internal/bevy_sprite", @@ -295,6 +305,9 @@ bevy_log = ["bevy_internal/bevy_log"] # Enable input focus subsystem bevy_input_focus = ["bevy_internal/bevy_input_focus"] +# Headless widget collection for Bevy UI. +bevy_core_widgets = ["bevy_internal/bevy_core_widgets"] + # Enable passthrough loading for SPIR-V shaders (Only supported on Vulkan, shader capabilities and extensions must agree with the platform implementation) spirv_shader_passthrough = ["bevy_internal/spirv_shader_passthrough"] @@ -497,7 +510,10 @@ file_watcher = ["bevy_internal/file_watcher"] embedded_watcher = ["bevy_internal/embedded_watcher"] # Enable stepping-based debugging of Bevy systems -bevy_debug_stepping = ["bevy_internal/bevy_debug_stepping"] +bevy_debug_stepping = [ + "bevy_internal/bevy_debug_stepping", + "bevy_internal/debug", +] # Enables the meshlet renderer for dense high-poly scenes (experimental) meshlet = ["bevy_internal/meshlet"] @@ -547,6 +563,9 @@ web = ["bevy_internal/web"] # Enable hotpatching of Bevy systems hotpatching = ["bevy_internal/hotpatching"] +# Enable collecting debug information about systems and components to help with diagnostics +debug = ["bevy_internal/debug"] + [dependencies] bevy_internal = { path = "crates/bevy_internal", version = "0.16.0-dev", default-features = false } tracing = { version = "0.1", default-features = false, optional = true } @@ -558,7 +577,7 @@ bevy_dylib = { path = "crates/bevy_dylib", version = "0.16.0-dev", default-featu [dev-dependencies] rand = "0.8.0" rand_chacha = "0.3.1" -ron = "0.8.0" +ron = "0.10" flate2 = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.140" @@ -597,7 +616,7 @@ web-sys = { version = "0.3", features = ["Window"] } [[example]] name = "context_menu" -path = "examples/usages/context_menu.rs" +path = "examples/usage/context_menu.rs" doc-scrape-examples = true [package.metadata.example.context_menu] @@ -1266,6 +1285,18 @@ description = "Load a cubemap texture onto a cube like a skybox and cycle throug category = "3D Rendering" wasm = false +[[example]] +name = "solari" +path = "examples/3d/solari.rs" +doc-scrape-examples = true +required-features = ["bevy_solari"] + +[package.metadata.example.solari] +name = "Solari" +description = "Demonstrates realtime dynamic global illumination rendering using Bevy Solari." +category = "3D Rendering" +wasm = false + [[example]] name = "spherical_area_lights" path = "examples/3d/spherical_area_lights.rs" @@ -2082,6 +2113,7 @@ wasm = false name = "dynamic" path = "examples/ecs/dynamic.rs" doc-scrape-examples = true +required-features = ["debug"] [package.metadata.example.dynamic] name = "Dynamic ECS" @@ -3563,6 +3595,17 @@ description = "Illustrates how to use 9 Slicing for TextureAtlases in UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_transform" +path = "examples/ui/ui_transform.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_transform] +name = "UI Transform" +description = "An example demonstrating how to translate, rotate and scale UI elements." +category = "UI (User Interface)" +wasm = true + [[example]] name = "viewport_debug" path = "examples/ui/viewport_debug.rs" @@ -4447,3 +4490,25 @@ name = "Hotpatching Systems" description = "Demonstrates how to hotpatch systems" category = "ECS (Entity Component System)" wasm = false + +[[example]] +name = "core_widgets" +path = "examples/ui/core_widgets.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets] +name = "Core Widgets" +description = "Demonstrates use of core (headless) widgets in Bevy UI" +category = "UI (User Interface)" +wasm = true + +[[example]] +name = "core_widgets_observers" +path = "examples/ui/core_widgets_observers.rs" +doc-scrape-examples = true + +[package.metadata.example.core_widgets_observers] +name = "Core Widgets (w/Observers)" +description = "Demonstrates use of core (headless) widgets in Bevy UI, with Observers" +category = "UI (User Interface)" +wasm = true diff --git a/assets/branding/bevy_solari.svg b/assets/branding/bevy_solari.svg new file mode 100644 index 0000000000..65b996493f --- /dev/null +++ b/assets/branding/bevy_solari.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/benches/benches/bevy_ecs/events/iter.rs b/benches/benches/bevy_ecs/events/iter.rs index dc20bc3395..9ad17ed8c8 100644 --- a/benches/benches/bevy_ecs/events/iter.rs +++ b/benches/benches/bevy_ecs/events/iter.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct BenchEvent([u8; SIZE]); pub struct Benchmark(Events>); diff --git a/benches/benches/bevy_ecs/events/send.rs b/benches/benches/bevy_ecs/events/send.rs index fa996b50aa..be8934e789 100644 --- a/benches/benches/bevy_ecs/events/send.rs +++ b/benches/benches/bevy_ecs/events/send.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct BenchEvent([u8; SIZE]); impl Default for BenchEvent { diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs index 65c15f7308..808c3727d5 100644 --- a/benches/benches/bevy_ecs/observers/propagation.rs +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -61,14 +61,10 @@ pub fn event_propagation(criterion: &mut Criterion) { group.finish(); } -#[derive(Clone, Component)] +#[derive(Event, EntityEvent, Clone, Component)] +#[entity_event(traversal = &'static ChildOf, auto_propagate)] struct TestEvent {} -impl Event for TestEvent { - type Traversal = &'static ChildOf; - const AUTO_PROPAGATE: bool = true; -} - fn send_events(world: &mut World, leaves: &[Entity]) { let target = leaves.iter().choose(&mut rand::thread_rng()).unwrap(); @@ -117,6 +113,6 @@ fn add_listeners_to_hierarchy( } } -fn empty_listener(trigger: Trigger>) { +fn empty_listener(trigger: On>) { black_box(trigger); } diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index 85207624e8..9c26b074e5 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -1,8 +1,8 @@ use core::hint::black_box; use bevy_ecs::{ - event::Event, - observer::{Trigger, TriggerTargets}, + event::{EntityEvent, Event}, + observer::{On, TriggerTargets}, world::World, }; @@ -13,7 +13,7 @@ fn deterministic_rand() -> ChaCha8Rng { ChaCha8Rng::seed_from_u64(42) } -#[derive(Clone, Event)] +#[derive(Clone, Event, EntityEvent)] struct EventBase; pub fn observe_simple(criterion: &mut Criterion) { @@ -46,7 +46,7 @@ pub fn observe_simple(criterion: &mut Criterion) { group.finish(); } -fn empty_listener_base(trigger: Trigger) { +fn empty_listener_base(trigger: On) { black_box(trigger); } diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index f8c46757dd..22b2f71f07 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -26,7 +26,8 @@ use accesskit::Node; use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - prelude::{Component, Event}, + component::Component, + event::{BufferedEvent, Event}, resource::Resource, schedule::SystemSet, }; @@ -44,7 +45,7 @@ use serde::{Deserialize, Serialize}; use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// Wrapper struct for [`accesskit::ActionRequest`]. Required to allow it to be used as an `Event`. -#[derive(Event, Deref, DerefMut)] +#[derive(Event, BufferedEvent, Deref, DerefMut)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct ActionRequest(pub accesskit::ActionRequest); diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 9f9cd26587..9db4a97fd0 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -32,7 +32,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea # other petgraph = { version = "0.7", features = ["serde-1"] } -ron = "0.8" +ron = "0.10" serde = "1" blake3 = { version = "1.0" } downcast-rs = { version = "2", default-features = false, features = ["std"] } diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index aa6d252fee..a5f4041ac7 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -1,10 +1,11 @@ //! The animation graph, which allows animations to be blended together. use core::{ + fmt::Write, iter, ops::{Index, IndexMut, Range}, }; -use std::io::{self, Write}; +use std::io; use bevy_asset::{ io::Reader, Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, Handle, LoadContext, diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index dd68595961..ae7ce42ed6 100644 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -324,13 +324,13 @@ impl AnimationClip { .push(variable_curve); } - /// Add a untargeted [`Event`] to this [`AnimationClip`]. + /// Add an [`EntityEvent`] with no [`AnimationTarget`] to this [`AnimationClip`]. /// /// The `event` will be cloned and triggered on the [`AnimationPlayer`] entity once the `time` (in seconds) /// is reached in the animation. /// /// See also [`add_event_to_target`](Self::add_event_to_target). - pub fn add_event(&mut self, time: f32, event: impl Event + Clone) { + pub fn add_event(&mut self, time: f32, event: impl EntityEvent + Clone) { self.add_event_fn( time, move |commands: &mut Commands, entity: Entity, _time: f32, _weight: f32| { @@ -339,7 +339,7 @@ impl AnimationClip { ); } - /// Add an [`Event`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. + /// Add an [`EntityEvent`] to an [`AnimationTarget`] named by an [`AnimationTargetId`]. /// /// The `event` will be cloned and triggered on the entity matching the target once the `time` (in seconds) /// is reached in the animation. @@ -349,7 +349,7 @@ impl AnimationClip { &mut self, target_id: AnimationTargetId, time: f32, - event: impl Event + Clone, + event: impl EntityEvent + Clone, ) { self.add_event_fn_to_target( target_id, @@ -360,19 +360,19 @@ impl AnimationClip { ); } - /// Add a untargeted event function to this [`AnimationClip`]. + /// Add an event function with no [`AnimationTarget`] to this [`AnimationClip`]. /// /// The `func` will trigger on the [`AnimationPlayer`] entity once the `time` (in seconds) /// is reached in the animation. /// - /// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event`]. + /// For a simpler [`EntityEvent`]-based alternative, see [`AnimationClip::add_event`]. /// See also [`add_event_to_target`](Self::add_event_to_target). /// /// ``` /// # use bevy_animation::AnimationClip; /// # let mut clip = AnimationClip::default(); /// clip.add_event_fn(1.0, |commands, entity, time, weight| { - /// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}"); + /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}"); /// }) /// ``` pub fn add_event_fn( @@ -388,14 +388,14 @@ impl AnimationClip { /// The `func` will trigger on the entity matching the target once the `time` (in seconds) /// is reached in the animation. /// - /// For a simpler [`Event`]-based alternative, see [`AnimationClip::add_event_to_target`]. + /// For a simpler [`EntityEvent`]-based alternative, see [`AnimationClip::add_event_to_target`]. /// Use [`add_event`](Self::add_event) instead if you don't have a specific target. /// /// ``` /// # use bevy_animation::{AnimationClip, AnimationTargetId}; /// # let mut clip = AnimationClip::default(); /// clip.add_event_fn_to_target(AnimationTargetId::from_iter(["Arm", "Hand"]), 1.0, |commands, entity, time, weight| { - /// println!("Animation Event Triggered {entity:#?} at time {time} with weight {weight}"); + /// println!("Animation event triggered {entity:#?} at time {time} with weight {weight}"); /// }) /// ``` pub fn add_event_fn_to_target( @@ -1534,7 +1534,7 @@ mod tests { use super::*; - #[derive(Event, Reflect, Clone)] + #[derive(Event, EntityEvent, Reflect, Clone)] struct A; #[track_caller] diff --git a/crates/bevy_anti_aliasing/src/smaa/mod.rs b/crates/bevy_anti_aliasing/src/smaa/mod.rs index 0947abc5f8..bb082c5a01 100644 --- a/crates/bevy_anti_aliasing/src/smaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/smaa/mod.rs @@ -847,7 +847,7 @@ impl ViewNode for SmaaNode { view_smaa_uniform_offset, smaa_textures, view_smaa_bind_groups, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 9f82b22073..fc16a499d1 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -106,6 +106,8 @@ impl Default for App { #[cfg(feature = "bevy_reflect")] { + use bevy_ecs::observer::ObservedBy; + #[cfg(not(feature = "reflect_auto_register"))] app.init_resource::(); @@ -115,6 +117,7 @@ impl Default for App { app.register_type::(); app.register_type::(); app.register_type::(); + app.register_type::(); } #[cfg(feature = "reflect_functions")] @@ -346,7 +349,7 @@ impl App { self } - /// Initializes `T` event handling by inserting an event queue resource ([`Events::`]) + /// Initializes [`BufferedEvent`] handling for `T` by inserting an event queue resource ([`Events::`]) /// and scheduling an [`event_update_system`] in [`First`]. /// /// See [`Events`] for information on how to define events. @@ -357,7 +360,7 @@ impl App { /// # use bevy_app::prelude::*; /// # use bevy_ecs::prelude::*; /// # - /// # #[derive(Event)] + /// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # let mut app = App::new(); /// # @@ -365,7 +368,7 @@ impl App { /// ``` pub fn add_event(&mut self) -> &mut Self where - T: Event, + T: BufferedEvent, { self.main_mut().add_event::(); self @@ -1311,7 +1314,7 @@ impl App { /// Spawns an [`Observer`] entity, which will watch for and respond to the given event. /// - /// `observer` can be any system whose first parameter is a [`Trigger`]. + /// `observer` can be any system whose first parameter is [`On`]. /// /// # Examples /// @@ -1327,14 +1330,14 @@ impl App { /// # friends_allowed: bool, /// # }; /// # - /// # #[derive(Event)] + /// # #[derive(Event, EntityEvent)] /// # struct Invite; /// # /// # #[derive(Component)] /// # struct Friend; /// # /// - /// app.add_observer(|trigger: Trigger, friends: Query>, mut commands: Commands| { + /// app.add_observer(|trigger: On, friends: Query>, mut commands: Commands| { /// if trigger.event().friends_allowed { /// for friend in friends.iter() { /// commands.trigger_targets(Invite, friend); @@ -1409,7 +1412,7 @@ fn run_once(mut app: App) -> AppExit { app.should_exit().unwrap_or(AppExit::Success) } -/// An event that indicates the [`App`] should exit. If one or more of these are present at the end of an update, +/// A [`BufferedEvent`] that indicates the [`App`] should exit. If one or more of these are present at the end of an update, /// the [runner](App::set_runner) will end and ([maybe](App::run)) return control to the caller. /// /// This event can be used to detect when an exit is requested. Make sure that systems listening @@ -1419,7 +1422,7 @@ fn run_once(mut app: App) -> AppExit { /// This type is roughly meant to map to a standard definition of a process exit code (0 means success, not 0 means error). Due to portability concerns /// (see [`ExitCode`](https://doc.rust-lang.org/std/process/struct.ExitCode.html) and [`process::exit`](https://doc.rust-lang.org/std/process/fn.exit.html#)) /// we only allow error codes between 1 and [255](u8::MAX). -#[derive(Event, Debug, Clone, Default, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Default, PartialEq, Eq)] pub enum AppExit { /// [`App`] exited without any problems. #[default] @@ -1487,9 +1490,9 @@ mod tests { change_detection::{DetectChanges, ResMut}, component::Component, entity::Entity, - event::{Event, EventWriter, Events}, + event::{BufferedEvent, Event, EventWriter, Events}, + lifecycle::RemovedComponents, query::With, - removal_detection::RemovedComponents, resource::Resource, schedule::{IntoScheduleConfigs, ScheduleLabel}, system::{Commands, Query}, @@ -1853,7 +1856,7 @@ mod tests { } #[test] fn events_should_be_updated_once_per_update() { - #[derive(Event, Clone)] + #[derive(Event, BufferedEvent, Clone)] struct TestEvent; let mut app = App::new(); diff --git a/crates/bevy_app/src/propagate.rs b/crates/bevy_app/src/propagate.rs index 5d766a626c..754ba3140e 100644 --- a/crates/bevy_app/src/propagate.rs +++ b/crates/bevy_app/src/propagate.rs @@ -6,9 +6,9 @@ use bevy_ecs::{ component::Component, entity::Entity, hierarchy::ChildOf, + lifecycle::RemovedComponents, query::{Changed, Or, QueryFilter, With, Without}, relationship::{Relationship, RelationshipTarget}, - removal_detection::RemovedComponents, schedule::{IntoScheduleConfigs, SystemSet}, system::{Commands, Local, Query}, }; diff --git a/crates/bevy_app/src/sub_app.rs b/crates/bevy_app/src/sub_app.rs index c340b80654..56d6b43d38 100644 --- a/crates/bevy_app/src/sub_app.rs +++ b/crates/bevy_app/src/sub_app.rs @@ -338,7 +338,7 @@ impl SubApp { /// See [`App::add_event`]. pub fn add_event(&mut self) -> &mut Self where - T: Event, + T: BufferedEvent, { if !self.world.contains_resource::>() { EventRegistry::register_event::(self.world_mut()); diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index cbb138b0f5..e91987f40a 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -54,7 +54,7 @@ parking_lot = { version = "0.12", default-features = false, features = [ "arc_lock", "send_guard", ] } -ron = { version = "0.8", default-features = false } +ron = { version = "0.10", default-features = false } serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2", default-features = false } derive_more = { version = "1", default-features = false, features = ["from"] } diff --git a/crates/bevy_asset/macros/src/lib.rs b/crates/bevy_asset/macros/src/lib.rs index 443bd09ab9..a7ea87b752 100644 --- a/crates/bevy_asset/macros/src/lib.rs +++ b/crates/bevy_asset/macros/src/lib.rs @@ -1,6 +1,7 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] +//! Macros for deriving asset traits. + use bevy_macro_utils::BevyManifest; use proc_macro::{Span, TokenStream}; use quote::{format_ident, quote}; @@ -12,6 +13,7 @@ pub(crate) fn bevy_asset_path() -> Path { const DEPENDENCY_ATTRIBUTE: &str = "dependency"; +/// Implement the `Asset` trait. #[proc_macro_derive(Asset, attributes(dependency))] pub fn derive_asset(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -30,6 +32,7 @@ pub fn derive_asset(input: TokenStream) -> TokenStream { }) } +/// Implement the `VisitAssetDependencies` trait. #[proc_macro_derive(VisitAssetDependencies, attributes(dependency))] pub fn derive_asset_dependency_visitor(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); diff --git a/crates/bevy_asset/src/asset_changed.rs b/crates/bevy_asset/src/asset_changed.rs index d314fa3fd6..b43a8625e7 100644 --- a/crates/bevy_asset/src/asset_changed.rs +++ b/crates/bevy_asset/src/asset_changed.rs @@ -158,9 +158,9 @@ unsafe impl WorldQuery for AssetChanged { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &Self::State, + state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -201,9 +201,9 @@ unsafe impl WorldQuery for AssetChanged { const IS_DENSE: bool = <&A>::IS_DENSE; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, ) { @@ -215,7 +215,11 @@ unsafe impl WorldQuery for AssetChanged { } } - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + state: &Self::State, + table: &'w Table, + ) { if let Some(inner) = &mut fetch.inner { // SAFETY: We delegate to the inner `set_table` for `A` unsafe { @@ -265,6 +269,7 @@ unsafe impl QueryFilter for AssetChanged { #[inline] unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -272,7 +277,7 @@ unsafe impl QueryFilter for AssetChanged { fetch.inner.as_mut().is_some_and(|inner| { // SAFETY: We delegate to the inner `fetch` for `A` unsafe { - let handle = <&A>::fetch(inner, entity, table_row); + let handle = <&A>::fetch(&state.asset_id, inner, entity, table_row); fetch.check.has_changed(handle) } }) diff --git a/crates/bevy_asset/src/event.rs b/crates/bevy_asset/src/event.rs index 087cb44b5a..42de19fe44 100644 --- a/crates/bevy_asset/src/event.rs +++ b/crates/bevy_asset/src/event.rs @@ -1,12 +1,12 @@ use crate::{Asset, AssetId, AssetLoadError, AssetPath, UntypedAssetId}; -use bevy_ecs::event::Event; +use bevy_ecs::event::{BufferedEvent, Event}; use bevy_reflect::Reflect; use core::fmt::Debug; -/// An event emitted when a specific [`Asset`] fails to load. +/// A [`BufferedEvent`] emitted when a specific [`Asset`] fails to load. /// /// For an untyped equivalent, see [`UntypedAssetLoadFailedEvent`]. -#[derive(Event, Clone, Debug)] +#[derive(Event, BufferedEvent, Clone, Debug)] pub struct AssetLoadFailedEvent { /// The stable identifier of the asset that failed to load. pub id: AssetId, @@ -24,7 +24,7 @@ impl AssetLoadFailedEvent { } /// An untyped version of [`AssetLoadFailedEvent`]. -#[derive(Event, Clone, Debug)] +#[derive(Event, BufferedEvent, Clone, Debug)] pub struct UntypedAssetLoadFailedEvent { /// The stable identifier of the asset that failed to load. pub id: UntypedAssetId, @@ -44,9 +44,9 @@ impl From<&AssetLoadFailedEvent> for UntypedAssetLoadFailedEvent { } } -/// Events that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events. +/// [`BufferedEvent`]s that occur for a specific loaded [`Asset`], such as "value changed" events and "dependency" events. #[expect(missing_docs, reason = "Documenting the id fields is unhelpful.")] -#[derive(Event, Reflect)] +#[derive(Event, BufferedEvent, Reflect)] pub enum AssetEvent { /// Emitted whenever an [`Asset`] is added. Added { id: AssetId }, diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index 50bbfb5dfc..24405f0657 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -344,7 +344,7 @@ impl<'a> LoadContext<'a> { /// Begins a new labeled asset load. Use the returned [`LoadContext`] to load /// dependencies for the new asset and call [`LoadContext::finish`] to finalize the asset load. - /// When finished, make sure you call [`LoadContext::add_labeled_asset`] to add the results back to the parent + /// When finished, make sure you call [`LoadContext::add_loaded_labeled_asset`] to add the results back to the parent /// context. /// Prefer [`LoadContext::labeled_asset_scope`] when possible, which will automatically add /// the labeled [`LoadContext`] back to the parent context. @@ -360,7 +360,7 @@ impl<'a> LoadContext<'a> { /// # let load_context: LoadContext = panic!(); /// let mut handles = Vec::new(); /// for i in 0..2 { - /// let mut labeled = load_context.begin_labeled_asset(); + /// let labeled = load_context.begin_labeled_asset(); /// handles.push(std::thread::spawn(move || { /// (i.to_string(), labeled.finish(Image::default())) /// })); @@ -385,7 +385,7 @@ impl<'a> LoadContext<'a> { /// [`LoadedAsset`], which is registered under the `label` label. /// /// This exists to remove the need to manually call [`LoadContext::begin_labeled_asset`] and then manually register the - /// result with [`LoadContext::add_labeled_asset`]. + /// result with [`LoadContext::add_loaded_labeled_asset`]. /// /// See [`AssetPath`] for more on labeled assets. pub fn labeled_asset_scope( diff --git a/crates/bevy_asset/src/reflect.rs b/crates/bevy_asset/src/reflect.rs index 5c436c1061..6c470891bd 100644 --- a/crates/bevy_asset/src/reflect.rs +++ b/crates/bevy_asset/src/reflect.rs @@ -18,16 +18,16 @@ pub struct ReflectAsset { handle_type_id: TypeId, assets_resource_type_id: TypeId, - get: fn(&World, UntypedHandle) -> Option<&dyn Reflect>, + get: fn(&World, UntypedAssetId) -> Option<&dyn Reflect>, // SAFETY: // - may only be called with an [`UnsafeWorldCell`] which can be used to access the corresponding `Assets` resource mutably // - may only be used to access **at most one** access at once - get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedHandle) -> Option<&mut dyn Reflect>, + get_unchecked_mut: unsafe fn(UnsafeWorldCell<'_>, UntypedAssetId) -> Option<&mut dyn Reflect>, add: fn(&mut World, &dyn PartialReflect) -> UntypedHandle, - insert: fn(&mut World, UntypedHandle, &dyn PartialReflect), + insert: fn(&mut World, UntypedAssetId, &dyn PartialReflect), len: fn(&World) -> usize, ids: for<'w> fn(&'w World) -> Box + 'w>, - remove: fn(&mut World, UntypedHandle) -> Option>, + remove: fn(&mut World, UntypedAssetId) -> Option>, } impl ReflectAsset { @@ -42,15 +42,19 @@ impl ReflectAsset { } /// Equivalent of [`Assets::get`] - pub fn get<'w>(&self, world: &'w World, handle: UntypedHandle) -> Option<&'w dyn Reflect> { - (self.get)(world, handle) + pub fn get<'w>( + &self, + world: &'w World, + asset_id: impl Into, + ) -> Option<&'w dyn Reflect> { + (self.get)(world, asset_id.into()) } /// Equivalent of [`Assets::get_mut`] pub fn get_mut<'w>( &self, world: &'w mut World, - handle: UntypedHandle, + asset_id: impl Into, ) -> Option<&'w mut dyn Reflect> { // SAFETY: unique world access #[expect( @@ -58,7 +62,7 @@ impl ReflectAsset { reason = "Use of unsafe `Self::get_unchecked_mut()` function." )] unsafe { - (self.get_unchecked_mut)(world.as_unsafe_world_cell(), handle) + (self.get_unchecked_mut)(world.as_unsafe_world_cell(), asset_id.into()) } } @@ -76,8 +80,8 @@ impl ReflectAsset { /// # let handle_1: UntypedHandle = unimplemented!(); /// # let handle_2: UntypedHandle = unimplemented!(); /// let unsafe_world_cell = world.as_unsafe_world_cell(); - /// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_1).unwrap() }; - /// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, handle_2).unwrap() }; + /// let a = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_1).unwrap() }; + /// let b = unsafe { reflect_asset.get_unchecked_mut(unsafe_world_cell, &handle_2).unwrap() }; /// // ^ not allowed, two mutable references through the same asset resource, even though the /// // handles are distinct /// @@ -96,10 +100,10 @@ impl ReflectAsset { pub unsafe fn get_unchecked_mut<'w>( &self, world: UnsafeWorldCell<'w>, - handle: UntypedHandle, + asset_id: impl Into, ) -> Option<&'w mut dyn Reflect> { // SAFETY: requirements are deferred to the caller - unsafe { (self.get_unchecked_mut)(world, handle) } + unsafe { (self.get_unchecked_mut)(world, asset_id.into()) } } /// Equivalent of [`Assets::add`] @@ -107,13 +111,22 @@ impl ReflectAsset { (self.add)(world, value) } /// Equivalent of [`Assets::insert`] - pub fn insert(&self, world: &mut World, handle: UntypedHandle, value: &dyn PartialReflect) { - (self.insert)(world, handle, value); + pub fn insert( + &self, + world: &mut World, + asset_id: impl Into, + value: &dyn PartialReflect, + ) { + (self.insert)(world, asset_id.into(), value); } /// Equivalent of [`Assets::remove`] - pub fn remove(&self, world: &mut World, handle: UntypedHandle) -> Option> { - (self.remove)(world, handle) + pub fn remove( + &self, + world: &mut World, + asset_id: impl Into, + ) -> Option> { + (self.remove)(world, asset_id.into()) } /// Equivalent of [`Assets::len`] @@ -137,17 +150,17 @@ impl FromType for ReflectAsset { ReflectAsset { handle_type_id: TypeId::of::>(), assets_resource_type_id: TypeId::of::>(), - get: |world, handle| { + get: |world, asset_id| { let assets = world.resource::>(); - let asset = assets.get(&handle.typed_debug_checked()); + let asset = assets.get(asset_id.typed_debug_checked()); asset.map(|asset| asset as &dyn Reflect) }, - get_unchecked_mut: |world, handle| { + get_unchecked_mut: |world, asset_id| { // SAFETY: `get_unchecked_mut` must be called with `UnsafeWorldCell` having access to `Assets`, // and must ensure to only have at most one reference to it live at all times. #[expect(unsafe_code, reason = "Uses `UnsafeWorldCell::get_resource_mut()`.")] let assets = unsafe { world.get_resource_mut::>().unwrap().into_inner() }; - let asset = assets.get_mut(&handle.typed_debug_checked()); + let asset = assets.get_mut(asset_id.typed_debug_checked()); asset.map(|asset| asset as &mut dyn Reflect) }, add: |world, value| { @@ -156,11 +169,11 @@ impl FromType for ReflectAsset { .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::add`"); assets.add(value).untyped() }, - insert: |world, handle, value| { + insert: |world, asset_id, value| { let mut assets = world.resource_mut::>(); let value: A = FromReflect::from_reflect(value) .expect("could not call `FromReflect::from_reflect` in `ReflectAsset::set`"); - assets.insert(&handle.typed_debug_checked(), value); + assets.insert(asset_id.typed_debug_checked(), value); }, len: |world| { let assets = world.resource::>(); @@ -170,9 +183,9 @@ impl FromType for ReflectAsset { let assets = world.resource::>(); Box::new(assets.ids().map(AssetId::untyped)) }, - remove: |world, handle| { + remove: |world, asset_id| { let mut assets = world.resource_mut::>(); - let value = assets.remove(&handle.typed_debug_checked()); + let value = assets.remove(asset_id.typed_debug_checked()); value.map(|value| Box::new(value) as Box) }, } @@ -200,7 +213,7 @@ impl FromType for ReflectAsset { /// let reflect_asset = type_registry.get_type_data::(reflect_handle.asset_type_id()).unwrap(); /// /// let handle = reflect_handle.downcast_handle_untyped(handle.as_any()).unwrap(); -/// let value = reflect_asset.get(world, handle).unwrap(); +/// let value = reflect_asset.get(world, &handle).unwrap(); /// println!("{value:?}"); /// } /// ``` @@ -247,7 +260,7 @@ mod tests { use alloc::{string::String, vec::Vec}; use core::any::TypeId; - use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset, UntypedHandle}; + use crate::{Asset, AssetApp, AssetPlugin, ReflectAsset}; use bevy_app::App; use bevy_ecs::reflect::AppTypeRegistry; use bevy_reflect::Reflect; @@ -281,7 +294,7 @@ mod tests { let handle = reflect_asset.add(app.world_mut(), &value); // struct is a reserved keyword, so we can't use it here let strukt = reflect_asset - .get_mut(app.world_mut(), handle) + .get_mut(app.world_mut(), &handle) .unwrap() .reflect_mut() .as_struct() @@ -294,16 +307,12 @@ mod tests { assert_eq!(reflect_asset.len(app.world()), 1); let ids: Vec<_> = reflect_asset.ids(app.world()).collect(); assert_eq!(ids.len(), 1); + let id = ids[0]; - let fetched_handle = UntypedHandle::Weak(ids[0]); - let asset = reflect_asset - .get(app.world(), fetched_handle.clone_weak()) - .unwrap(); + let asset = reflect_asset.get(app.world(), id).unwrap(); assert_eq!(asset.downcast_ref::().unwrap().field, "edited"); - reflect_asset - .remove(app.world_mut(), fetched_handle) - .unwrap(); + reflect_asset.remove(app.world_mut(), id).unwrap(); assert_eq!(reflect_asset.len(app.world()), 0); } } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 2b3898cd54..e120888616 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -1953,6 +1953,14 @@ impl AssetLoaderError { pub fn path(&self) -> &AssetPath<'static> { &self.path } + + /// The error the loader reported when attempting to load the asset. + /// + /// If you know the type of the error the asset loader returned, you can use + /// [`BevyError::downcast_ref()`] to get it. + pub fn error(&self) -> &BevyError { + &self.error + } } /// An error that occurs while resolving an asset added by `add_async`. diff --git a/crates/bevy_audio/Cargo.toml b/crates/bevy_audio/Cargo.toml index aff7f83b37..ae5385870d 100644 --- a/crates/bevy_audio/Cargo.toml +++ b/crates/bevy_audio/Cargo.toml @@ -19,12 +19,18 @@ bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } # other +# TODO: Remove `coreaudio-sys` dep below when updating `cpal`. rodio = { version = "0.20", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } [target.'cfg(target_os = "android")'.dependencies] cpal = { version = "0.15", optional = true } +[target.'cfg(target_vendor = "apple")'.dependencies] +# NOTE: Explicitly depend on this patch version to fix: +# https://github.com/bevyengine/bevy/issues/18893 +coreaudio-sys = { version = "0.2.17", default-features = false } + [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. rodio = { version = "0.20", default-features = false, features = [ diff --git a/crates/bevy_color/src/hsla.rs b/crates/bevy_color/src/hsla.rs index b29fce72ac..1579519274 100644 --- a/crates/bevy_color/src/hsla.rs +++ b/crates/bevy_color/src/hsla.rs @@ -92,7 +92,7 @@ impl Hsla { /// // Palette with 5 distinct hues /// let palette = (0..5).map(Hsla::sequential_dispersed).collect::>(); /// ``` - pub fn sequential_dispersed(index: u32) -> Self { + pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const RATIO_360: f32 = 360.0 / u32::MAX as f32; diff --git a/crates/bevy_color/src/lcha.rs b/crates/bevy_color/src/lcha.rs index e5f5ecab32..2a3b115bb3 100644 --- a/crates/bevy_color/src/lcha.rs +++ b/crates/bevy_color/src/lcha.rs @@ -96,7 +96,7 @@ impl Lcha { /// // Palette with 5 distinct hues /// let palette = (0..5).map(Lcha::sequential_dispersed).collect::>(); /// ``` - pub fn sequential_dispersed(index: u32) -> Self { + pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const RATIO_360: f32 = 360.0 / u32::MAX as f32; diff --git a/crates/bevy_color/src/oklcha.rs b/crates/bevy_color/src/oklcha.rs index 91ffe422c7..ba52a519ae 100644 --- a/crates/bevy_color/src/oklcha.rs +++ b/crates/bevy_color/src/oklcha.rs @@ -92,7 +92,7 @@ impl Oklcha { /// // Palette with 5 distinct hues /// let palette = (0..5).map(Oklcha::sequential_dispersed).collect::>(); /// ``` - pub fn sequential_dispersed(index: u32) -> Self { + pub const fn sequential_dispersed(index: u32) -> Self { const FRAC_U32MAX_GOLDEN_RATIO: u32 = 2654435769; // (u32::MAX / Φ) rounded up const RATIO_360: f32 = 360.0 / u32::MAX as f32; diff --git a/crates/bevy_core_pipeline/src/bloom/mod.rs b/crates/bevy_core_pipeline/src/bloom/mod.rs index 10ffdf9c63..65e51c8472 100644 --- a/crates/bevy_core_pipeline/src/bloom/mod.rs +++ b/crates/bevy_core_pipeline/src/bloom/mod.rs @@ -121,7 +121,7 @@ impl ViewNode for BloomNode { bloom_settings, upsampling_pipeline_ids, downsampling_pipeline_ids, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { if bloom_settings.intensity == 0.0 { diff --git a/crates/bevy_core_pipeline/src/bloom/settings.rs b/crates/bevy_core_pipeline/src/bloom/settings.rs index 195c2eb4c0..435ed037b5 100644 --- a/crates/bevy_core_pipeline/src/bloom/settings.rs +++ b/crates/bevy_core_pipeline/src/bloom/settings.rs @@ -227,7 +227,7 @@ impl ExtractComponent for Bloom { type QueryFilter = With; type Out = (Self, BloomUniforms); - fn extract_component((bloom, camera): QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component((bloom, camera): QueryItem<'_, '_, Self::QueryData>) -> Option { match ( camera.physical_viewport_rect(), camera.physical_viewport_size(), diff --git a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs index 60f355c115..e8cd0c65c6 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_opaque_pass_2d_node.rs @@ -31,7 +31,7 @@ impl ViewNode for MainOpaquePass2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (Some(opaque_phases), Some(alpha_mask_phases)) = ( diff --git a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs index 494d4d0f89..4054283a57 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_transparent_pass_2d_node.rs @@ -28,7 +28,7 @@ impl ViewNode for MainTransparentPass2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): bevy_ecs::query::QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): bevy_ecs::query::QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(transparent_phases) = diff --git a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs index 3b1bc96c90..b19268ac1f 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_opaque_pass_3d_node.rs @@ -45,7 +45,7 @@ impl ViewNode for MainOpaquePass3dNode { skybox_pipeline, skybox_bind_group, view_uniform_offset, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (Some(opaque_phases), Some(alpha_mask_phases)) = ( diff --git a/crates/bevy_core_pipeline/src/deferred/node.rs b/crates/bevy_core_pipeline/src/deferred/node.rs index ffac1eec6d..e786d2a222 100644 --- a/crates/bevy_core_pipeline/src/deferred/node.rs +++ b/crates/bevy_core_pipeline/src/deferred/node.rs @@ -36,7 +36,7 @@ impl ViewNode for EarlyDeferredGBufferPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { run_deferred_prepass( @@ -74,7 +74,7 @@ impl ViewNode for LateDeferredGBufferPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query; @@ -107,6 +107,7 @@ fn run_deferred_prepass<'w>( render_context: &mut RenderContext<'w>, (camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem< 'w, + '_, ::ViewQuery, >, is_late: bool, diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index 38f5e1e796..c27d81180d 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -352,7 +352,7 @@ impl ViewNode for DepthOfFieldNode { view_bind_group_layouts, depth_of_field_uniform_index, auxiliary_dof_texture, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 8dc51e4ed5..5f82e10599 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -61,7 +61,7 @@ impl ViewNode for MsaaWritebackNode { &self, _graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (target, blit_pipeline_id, msaa): QueryItem<'w, Self::ViewQuery>, + (target, blit_pipeline_id, msaa): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { if *msaa == Msaa::Off { diff --git a/crates/bevy_core_pipeline/src/oit/mod.rs b/crates/bevy_core_pipeline/src/oit/mod.rs index 3ae95d71cc..5b5d038fa0 100644 --- a/crates/bevy_core_pipeline/src/oit/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/mod.rs @@ -1,7 +1,7 @@ //! Order Independent Transparency (OIT) for 3d rendering. See [`OrderIndependentTransparencyPlugin`] for more details. use bevy_app::prelude::*; -use bevy_ecs::{component::*, prelude::*}; +use bevy_ecs::{component::*, lifecycle::ComponentHook, prelude::*}; use bevy_math::UVec2; use bevy_platform::collections::HashSet; use bevy_platform::time::Instant; diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs index 1ab03c5dfa..0f188d1d73 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -352,7 +352,7 @@ impl ViewNode for PostProcessingNode { &self, _: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, Self::ViewQuery>, + (view_target, pipeline_id, chromatic_aberration, post_processing_uniform_buffer_offsets): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); @@ -485,7 +485,7 @@ impl ExtractComponent for ChromaticAberration { type Out = ChromaticAberration; fn extract_component( - chromatic_aberration: QueryItem<'_, Self::QueryData>, + chromatic_aberration: QueryItem<'_, '_, Self::QueryData>, ) -> Option { // Skip the postprocessing phase entirely if the intensity is zero. if chromatic_aberration.intensity > 0.0 { diff --git a/crates/bevy_core_pipeline/src/prepass/mod.rs b/crates/bevy_core_pipeline/src/prepass/mod.rs index deea2a5fa8..880e2b6892 100644 --- a/crates/bevy_core_pipeline/src/prepass/mod.rs +++ b/crates/bevy_core_pipeline/src/prepass/mod.rs @@ -74,11 +74,16 @@ pub struct MotionVectorPrepass; #[reflect(Component, Default)] pub struct DeferredPrepass; +/// View matrices from the previous frame. +/// +/// Useful for temporal rendering techniques that need access to last frame's camera data. #[derive(Component, ShaderType, Clone)] pub struct PreviousViewData { pub view_from_world: Mat4, pub clip_from_world: Mat4, pub clip_from_view: Mat4, + pub world_from_clip: Mat4, + pub view_from_clip: Mat4, } #[derive(Resource, Default)] diff --git a/crates/bevy_core_pipeline/src/prepass/node.rs b/crates/bevy_core_pipeline/src/prepass/node.rs index 04cc1890b0..500cc0a42b 100644 --- a/crates/bevy_core_pipeline/src/prepass/node.rs +++ b/crates/bevy_core_pipeline/src/prepass/node.rs @@ -36,7 +36,7 @@ impl ViewNode for EarlyPrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { run_prepass(graph, render_context, view_query, world, "early prepass") @@ -73,7 +73,7 @@ impl ViewNode for LatePrepassNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - query: QueryItem<'w, Self::ViewQuery>, + query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // We only need a late prepass if we have occlusion culling and indirect @@ -112,7 +112,7 @@ fn run_prepass<'w>( _, _, has_deferred, - ): QueryItem<'w, ::ViewQuery>, + ): QueryItem<'w, '_, ::ViewQuery>, world: &'w World, label: &'static str, ) -> Result<(), NodeRunError> { diff --git a/crates/bevy_core_pipeline/src/skybox/mod.rs b/crates/bevy_core_pipeline/src/skybox/mod.rs index cb75df2053..51c6934ece 100644 --- a/crates/bevy_core_pipeline/src/skybox/mod.rs +++ b/crates/bevy_core_pipeline/src/skybox/mod.rs @@ -113,7 +113,9 @@ impl ExtractComponent for Skybox { type QueryFilter = (); type Out = (Self, SkyboxUniforms); - fn extract_component((skybox, exposure): QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component( + (skybox, exposure): QueryItem<'_, '_, Self::QueryData>, + ) -> Option { let exposure = exposure .map(Exposure::exposure) .unwrap_or_else(|| Exposure::default().exposure()); @@ -123,7 +125,7 @@ impl ExtractComponent for Skybox { SkyboxUniforms { brightness: skybox.brightness * exposure, transform: Transform::from_rotation(skybox.rotation) - .compute_matrix() + .to_matrix() .inverse(), #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] _wasm_padding_8b: 0, diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml new file mode 100644 index 0000000000..83ecd67ede --- /dev/null +++ b/crates/bevy_core_widgets/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "bevy_core_widgets" +version = "0.16.0-dev" +edition = "2024" +description = "Unstyled common widgets for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_a11y = { path = "../bevy_a11y", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } +bevy_log = { path = "../bevy_log", version = "0.16.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } +bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } +bevy_ui = { path = "../bevy_ui", version = "0.16.0-dev", features = [ + "bevy_ui_picking_backend", +] } + +# other +accesskit = "0.19" + +[features] +default = [] + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs new file mode 100644 index 0000000000..ec30b625f9 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -0,0 +1,141 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::query::Has; +use bevy_ecs::system::ResMut; +use bevy_ecs::{ + component::Component, + entity::Entity, + observer::On, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; +use bevy_ui::{InteractionDisabled, Pressed}; + +/// Headless button widget. This widget maintains a "pressed" state, which is used to +/// indicate whether the button is currently being pressed by the user. It emits a `ButtonClicked` +/// event when the button is un-pressed. +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::Button)))] +pub struct CoreButton { + /// Optional system to run when the button is clicked, or when the Enter or Space key + /// is pressed while the button is focused. If this field is `None`, the button will + /// emit a `ButtonClicked` event when clicked. + pub on_click: Option, +} + +fn button_on_key_event( + mut trigger: On>, + q_state: Query<(&CoreButton, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, disabled)) = q_state.get(trigger.target()) { + if !disabled { + let event = &trigger.event().input; + if !event.repeat + && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + { + if let Some(on_click) = bstate.on_click { + trigger.propagate(false); + commands.run_system(on_click); + } + } + } + } +} + +fn button_on_pointer_click( + mut trigger: On>, + mut q_state: Query<(&CoreButton, Has, Has)>, + mut commands: Commands, +) { + if let Ok((bstate, pressed, disabled)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if pressed && !disabled { + if let Some(on_click) = bstate.on_click { + commands.run_system(on_click); + } + } + } +} + +fn button_on_pointer_down( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + focus: Option>, + focus_visible: Option>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled { + if !pressed { + commands.entity(button).insert(Pressed); + } + // Clicking on a button makes it the focused input, + // and hides the focus ring if it was visible. + if let Some(mut focus) = focus { + focus.0 = (trigger.target() != Entity::PLACEHOLDER).then_some(trigger.target()); + } + if let Some(mut focus_visible) = focus_visible { + focus_visible.0 = false; + } + } + } +} + +fn button_on_pointer_up( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_drag_end( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +fn button_on_pointer_cancel( + mut trigger: On>, + mut q_state: Query<(Entity, Has, Has), With>, + mut commands: Commands, +) { + if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled && pressed { + commands.entity(button).remove::(); + } + } +} + +/// Plugin that adds the observers for the [`CoreButton`] widget. +pub struct CoreButtonPlugin; + +impl Plugin for CoreButtonPlugin { + fn build(&self, app: &mut App) { + app.add_observer(button_on_key_event) + .add_observer(button_on_pointer_down) + .add_observer(button_on_pointer_up) + .add_observer(button_on_pointer_click) + .add_observer(button_on_pointer_drag_end) + .add_observer(button_on_pointer_cancel); + } +} diff --git a/crates/bevy_core_widgets/src/core_checkbox.rs b/crates/bevy_core_widgets/src/core_checkbox.rs new file mode 100644 index 0000000000..fc12811055 --- /dev/null +++ b/crates/bevy_core_widgets/src/core_checkbox.rs @@ -0,0 +1,179 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::event::{EntityEvent, Event}; +use bevy_ecs::query::{Has, Without}; +use bevy_ecs::system::{In, ResMut}; +use bevy_ecs::{ + component::Component, + observer::On, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_picking::events::{Click, Pointer}; +use bevy_ui::{Checkable, Checked, InteractionDisabled}; + +/// Headless widget implementation for checkboxes. The [`Checked`] component represents the current +/// state of the checkbox. The `on_change` field is an optional system id that will be run when the +/// checkbox is clicked, or when the `Enter` or `Space` key is pressed while the checkbox is +/// focused. If the `on_change` field is `None`, then instead of calling a callback, the checkbox +/// will update its own [`Checked`] state directly. +/// +/// # Toggle switches +/// +/// The [`CoreCheckbox`] component can be used to implement other kinds of toggle widgets. If you +/// are going to do a toggle switch, you should override the [`AccessibilityNode`] component with +/// the `Switch` role instead of the `Checkbox` role. +#[derive(Component, Debug, Default)] +#[require(AccessibilityNode(accesskit::Node::new(Role::CheckBox)), Checkable)] +pub struct CoreCheckbox { + /// One-shot system that is run when the checkbox state needs to be changed. + pub on_change: Option>>, +} + +fn checkbox_on_key_input( + mut ev: On>, + q_checkbox: Query<(&CoreCheckbox, Has), Without>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked)) = q_checkbox.get(ev.target()) { + let event = &ev.event().input; + if event.state == ButtonState::Pressed + && !event.repeat + && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) + { + ev.propagate(false); + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } + } +} + +fn checkbox_on_pointer_click( + mut ev: On>, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + focus: Option>, + focus_visible: Option>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + // Clicking on a button makes it the focused input, + // and hides the focus ring if it was visible. + if let Some(mut focus) = focus { + focus.0 = Some(ev.target()); + } + if let Some(mut focus_visible) = focus_visible { + focus_visible.0 = false; + } + + ev.propagate(false); + if !disabled { + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } + } +} + +/// Event which can be triggered on a checkbox to set the checked state. This can be used to control +/// the checkbox via gamepad buttons or other inputs. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreCheckbox, SetChecked}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a checkbox +/// let checkbox = commands.spawn(( +/// CoreCheckbox::default(), +/// )).id(); +/// +/// // Set to checked +/// commands.trigger_targets(SetChecked(true), checkbox); +/// } +/// ``` +#[derive(Event, EntityEvent)] +pub struct SetChecked(pub bool); + +/// Event which can be triggered on a checkbox to toggle the checked state. This can be used to +/// control the checkbox via gamepad buttons or other inputs. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreCheckbox, ToggleChecked}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a checkbox +/// let checkbox = commands.spawn(( +/// CoreCheckbox::default(), +/// )).id(); +/// +/// // Set to checked +/// commands.trigger_targets(ToggleChecked, checkbox); +/// } +/// ``` +#[derive(Event, EntityEvent)] +pub struct ToggleChecked; + +fn checkbox_on_set_checked( + mut ev: On, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + ev.propagate(false); + if disabled { + return; + } + + let will_be_checked = ev.event().0; + if will_be_checked != is_checked { + set_checkbox_state(&mut commands, ev.target(), checkbox, will_be_checked); + } + } +} + +fn checkbox_on_toggle_checked( + mut ev: On, + q_checkbox: Query<(&CoreCheckbox, Has, Has)>, + mut commands: Commands, +) { + if let Ok((checkbox, is_checked, disabled)) = q_checkbox.get(ev.target()) { + ev.propagate(false); + if disabled { + return; + } + + set_checkbox_state(&mut commands, ev.target(), checkbox, !is_checked); + } +} + +fn set_checkbox_state( + commands: &mut Commands, + entity: impl Into, + checkbox: &CoreCheckbox, + new_state: bool, +) { + if let Some(on_change) = checkbox.on_change { + commands.run_system_with(on_change, new_state); + } else if new_state { + commands.entity(entity.into()).insert(Checked); + } else { + commands.entity(entity.into()).remove::(); + } +} + +/// Plugin that adds the observers for the [`CoreCheckbox`] widget. +pub struct CoreCheckboxPlugin; + +impl Plugin for CoreCheckboxPlugin { + fn build(&self, app: &mut App) { + app.add_observer(checkbox_on_key_input) + .add_observer(checkbox_on_pointer_click) + .add_observer(checkbox_on_set_checked) + .add_observer(checkbox_on_toggle_checked); + } +} diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs new file mode 100644 index 0000000000..ecd6d52fbe --- /dev/null +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -0,0 +1,514 @@ +use core::ops::RangeInclusive; + +use accesskit::{Orientation, Role}; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::entity::Entity; +use bevy_ecs::event::{EntityEvent, Event}; +use bevy_ecs::hierarchy::{ChildOf, Children}; +use bevy_ecs::lifecycle::Insert; +use bevy_ecs::query::Has; +use bevy_ecs::system::{In, Res, ResMut}; +use bevy_ecs::world::DeferredWorld; +use bevy_ecs::{ + component::Component, + observer::On, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_log::warn_once; +use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; +use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; + +/// Defines how the slider should behave when you click on the track (not the thumb). +#[derive(Debug, Default)] +pub enum TrackClick { + /// Clicking on the track lets you drag to edit the value, just like clicking on the thumb. + #[default] + Drag, + /// Clicking on the track increments or decrements the slider by [`SliderStep`]. + Step, + /// Clicking on the track snaps the value to the clicked position. + Snap, +} + +/// A headless slider widget, which can be used to build custom sliders. Sliders have a value +/// (represented by the [`SliderValue`] component) and a range (represented by [`SliderRange`]). An +/// optional step size can be specified via [`SliderStep`]. +/// +/// You can also control the slider remotely by triggering a [`SetSliderValue`] event on it. This +/// can be useful in a console environment for controlling the value gamepad inputs. +/// +/// The presence of the `on_change` property controls whether the slider uses internal or external +/// state management. If the `on_change` property is `None`, then the slider updates its own state +/// automatically. Otherwise, the `on_change` property contains the id of a one-shot system which is +/// passed the new slider value. In this case, the slider value is not modified, it is the +/// responsibility of the callback to trigger whatever data-binding mechanism is used to update the +/// slider's value. +/// +/// Typically a slider will contain entities representing the "track" and "thumb" elements. The core +/// slider makes no assumptions about the hierarchical structure of these elements, but expects that +/// the thumb will be marked with a [`CoreSliderThumb`] component. +/// +/// The core slider does not modify the visible position of the thumb: that is the responsibility of +/// the stylist. This can be done either in percent or pixel units as desired. To prevent overhang +/// at the ends of the slider, the positioning should take into account the thumb width, by reducing +/// the amount of travel. So for example, in a slider 100px wide, with a thumb that is 10px, the +/// amount of travel is 90px. The core slider's calculations for clicking and dragging assume this +/// is the case, and will reduce the travel by the measured size of the thumb entity, which allows +/// the movement of the thumb to be perfectly synchronized with the movement of the mouse. +/// +/// In cases where overhang is desired for artistic reasons, the thumb may have additional +/// decorative child elements, absolutely positioned, which don't affect the size measurement. +#[derive(Component, Debug, Default)] +#[require( + AccessibilityNode(accesskit::Node::new(Role::Slider)), + CoreSliderDragState, + SliderValue, + SliderRange, + SliderStep +)] +pub struct CoreSlider { + /// Callback which is called when the slider is dragged or the value is changed via other user + /// interaction. If this value is `None`, then the slider will self-update. + pub on_change: Option>>, + /// Set the track-clicking behavior for this slider. + pub track_click: TrackClick, + // TODO: Think about whether we want a "vertical" option. +} + +/// Marker component that identifies which descendant element is the slider thumb. +#[derive(Component, Debug, Default)] +pub struct CoreSliderThumb; + +/// A component which stores the current value of the slider. +#[derive(Component, Debug, Default, PartialEq, Clone, Copy)] +#[component(immutable)] +pub struct SliderValue(pub f32); + +/// A component which represents the allowed range of the slider value. Defaults to 0.0..=1.0. +#[derive(Component, Debug, PartialEq, Clone, Copy)] +#[component(immutable)] +pub struct SliderRange { + start: f32, + end: f32, +} + +impl SliderRange { + /// Creates a new slider range with the given start and end values. + pub fn new(start: f32, end: f32) -> Self { + if end < start { + warn_once!( + "Expected SliderRange::start ({}) <= SliderRange::end ({})", + start, + end + ); + } + Self { start, end } + } + + /// Creates a new slider range from a Rust range. + pub fn from_range(range: RangeInclusive) -> Self { + let (start, end) = range.into_inner(); + Self { start, end } + } + + /// Returns the minimum allowed value for this slider. + pub fn start(&self) -> f32 { + self.start + } + + /// Return a new instance of a `SliderRange` with a new start position. + pub fn with_start(&self, start: f32) -> Self { + Self::new(start, self.end) + } + + /// Returns the maximum allowed value for this slider. + pub fn end(&self) -> f32 { + self.end + } + + /// Return a new instance of a `SliderRange` with a new end position. + pub fn with_end(&self, end: f32) -> Self { + Self::new(self.start, end) + } + + /// Returns the full span of the range (max - min). + pub fn span(&self) -> f32 { + self.end - self.start + } + + /// Returns the center value of the range. + pub fn center(&self) -> f32 { + (self.start + self.end) / 2.0 + } + + /// Constrain a value between the minimum and maximum allowed values for this slider. + pub fn clamp(&self, value: f32) -> f32 { + value.clamp(self.start, self.end) + } + + /// Compute the position of the thumb on the slider, as a value between 0 and 1, taking + /// into account the proportion of the value between the minimum and maximum limits. + pub fn thumb_position(&self, value: f32) -> f32 { + if self.end > self.start { + (value - self.start) / (self.end - self.start) + } else { + 0.5 + } + } +} + +impl Default for SliderRange { + fn default() -> Self { + Self { + start: 0.0, + end: 1.0, + } + } +} + +/// Defines the amount by which to increment or decrement the slider value when using keyboard +/// shorctuts. Defaults to 1.0. +#[derive(Component, Debug, PartialEq, Clone)] +#[component(immutable)] +pub struct SliderStep(pub f32); + +impl Default for SliderStep { + fn default() -> Self { + Self(1.0) + } +} + +/// Component used to manage the state of a slider during dragging. +#[derive(Component, Default)] +pub struct CoreSliderDragState { + /// Whether the slider is currently being dragged. + pub dragging: bool, + + /// The value of the slider when dragging started. + offset: f32, +} + +pub(crate) fn slider_on_pointer_down( + mut trigger: On>, + q_slider: Query<( + &CoreSlider, + &SliderValue, + &SliderRange, + &SliderStep, + &ComputedNode, + &ComputedNodeTarget, + &UiGlobalTransform, + Has, + )>, + q_thumb: Query<&ComputedNode, With>, + q_children: Query<&Children>, + q_parents: Query<&ChildOf>, + focus: Option>, + focus_visible: Option>, + mut commands: Commands, + ui_scale: Res, +) { + if q_thumb.contains(trigger.target()) { + // Thumb click, stop propagation to prevent track click. + trigger.propagate(false); + + // Find the slider entity that's an ancestor of the thumb + if let Some(slider_entity) = q_parents + .iter_ancestors(trigger.target()) + .find(|entity| q_slider.contains(*entity)) + { + // Set focus to slider and hide focus ring + if let Some(mut focus) = focus { + focus.0 = Some(slider_entity); + } + if let Some(mut focus_visible) = focus_visible { + focus_visible.0 = false; + } + } + } else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) = + q_slider.get(trigger.target()) + { + // Track click + trigger.propagate(false); + + // Set focus to slider and hide focus ring + if let Some(mut focus) = focus { + focus.0 = (trigger.target() != Entity::PLACEHOLDER).then_some(trigger.target()); + } + if let Some(mut focus_visible) = focus_visible { + focus_visible.0 = false; + } + + if disabled { + return; + } + + // Find thumb size by searching descendants for the first entity with CoreSliderThumb + let thumb_size = q_children + .iter_descendants(trigger.target()) + .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) + .unwrap_or(0.0); + + // Detect track click. + let local_pos = transform.try_inverse().unwrap().transform_point2( + trigger.event().pointer_location.position * node_target.scale_factor() / ui_scale.0, + ); + let track_width = node.size().x - thumb_size; + // Avoid division by zero + let click_val = if track_width > 0. { + local_pos.x * range.span() / track_width + range.center() + } else { + 0. + }; + + // Compute new value from click position + let new_value = range.clamp(match slider.track_click { + TrackClick::Drag => { + return; + } + TrackClick::Step => { + if click_val < value.0 { + value.0 - step.0 + } else { + value.0 + step.0 + } + } + TrackClick::Snap => click_val, + }); + + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } +} + +pub(crate) fn slider_on_drag_start( + mut trigger: On>, + mut q_slider: Query< + ( + &SliderValue, + &mut CoreSliderDragState, + Has, + ), + With, + >, +) { + if let Ok((value, mut drag, disabled)) = q_slider.get_mut(trigger.target()) { + trigger.propagate(false); + if !disabled { + drag.dragging = true; + drag.offset = value.0; + } + } +} + +pub(crate) fn slider_on_drag( + mut trigger: On>, + mut q_slider: Query<( + &ComputedNode, + &CoreSlider, + &SliderRange, + &UiGlobalTransform, + &mut CoreSliderDragState, + Has, + )>, + q_thumb: Query<&ComputedNode, With>, + q_children: Query<&Children>, + mut commands: Commands, + ui_scale: Res, +) { + if let Ok((node, slider, range, transform, drag, disabled)) = q_slider.get_mut(trigger.target()) + { + trigger.propagate(false); + if drag.dragging && !disabled { + let mut distance = trigger.event().distance / ui_scale.0; + distance.y *= -1.; + let distance = transform.transform_vector2(distance); + // Find thumb size by searching descendants for the first entity with CoreSliderThumb + let thumb_size = q_children + .iter_descendants(trigger.target()) + .find_map(|child_id| q_thumb.get(child_id).ok().map(|thumb| thumb.size().x)) + .unwrap_or(0.0); + let slider_width = ((node.size().x - thumb_size) * node.inverse_scale_factor).max(1.0); + let span = range.span(); + let new_value = if span > 0. { + range.clamp(drag.offset + (distance.x * span) / slider_width) + } else { + range.start() + span * 0.5 + }; + + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } + } +} + +pub(crate) fn slider_on_drag_end( + mut trigger: On>, + mut q_slider: Query<(&CoreSlider, &mut CoreSliderDragState)>, +) { + if let Ok((_slider, mut drag)) = q_slider.get_mut(trigger.target()) { + trigger.propagate(false); + if drag.dragging { + drag.dragging = false; + } + } +} + +fn slider_on_key_input( + mut trigger: On>, + q_slider: Query<( + &CoreSlider, + &SliderValue, + &SliderRange, + &SliderStep, + Has, + )>, + mut commands: Commands, +) { + if let Ok((slider, value, range, step, disabled)) = q_slider.get(trigger.target()) { + let event = &trigger.event().input; + if !disabled && event.state == ButtonState::Pressed { + let new_value = match event.key_code { + KeyCode::ArrowLeft => range.clamp(value.0 - step.0), + KeyCode::ArrowRight => range.clamp(value.0 + step.0), + KeyCode::Home => range.start(), + KeyCode::End => range.end(), + _ => { + return; + } + }; + trigger.propagate(false); + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } + } +} + +pub(crate) fn slider_on_insert(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_orientation(Orientation::Horizontal); + } +} + +pub(crate) fn slider_on_insert_value(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let value = entity.get::().unwrap().0; + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_numeric_value(value.into()); + } +} + +pub(crate) fn slider_on_insert_range(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let range = *entity.get::().unwrap(); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_min_numeric_value(range.start().into()); + accessibility.set_max_numeric_value(range.end().into()); + } +} + +pub(crate) fn slider_on_insert_step(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let step = entity.get::().unwrap().0; + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_numeric_value_step(step.into()); + } +} + +/// An [`EntityEvent`] that can be triggered on a slider to modify its value (using the `on_change` callback). +/// This can be used to control the slider via gamepad buttons or other inputs. The value will be +/// clamped when the event is processed. +/// +/// # Example: +/// +/// ``` +/// use bevy_ecs::system::Commands; +/// use bevy_core_widgets::{CoreSlider, SliderRange, SliderValue, SetSliderValue}; +/// +/// fn setup(mut commands: Commands) { +/// // Create a slider +/// let slider = commands.spawn(( +/// CoreSlider::default(), +/// SliderValue(0.5), +/// SliderRange::new(0.0, 1.0), +/// )).id(); +/// +/// // Set to an absolute value +/// commands.trigger_targets(SetSliderValue::Absolute(0.75), slider); +/// +/// // Adjust relatively +/// commands.trigger_targets(SetSliderValue::Relative(-0.25), slider); +/// } +/// ``` +#[derive(Event, EntityEvent, Clone)] +pub enum SetSliderValue { + /// Set the slider value to a specific value. + Absolute(f32), + /// Add a delta to the slider value. + Relative(f32), + /// Add a delta to the slider value, multiplied by the step size. + RelativeStep(f32), +} + +fn slider_on_set_value( + mut trigger: On, + q_slider: Query<(&CoreSlider, &SliderValue, &SliderRange, Option<&SliderStep>)>, + mut commands: Commands, +) { + if let Ok((slider, value, range, step)) = q_slider.get(trigger.target()) { + trigger.propagate(false); + let new_value = match trigger.event() { + SetSliderValue::Absolute(new_value) => range.clamp(*new_value), + SetSliderValue::Relative(delta) => range.clamp(value.0 + *delta), + SetSliderValue::RelativeStep(delta) => { + range.clamp(value.0 + *delta * step.map(|s| s.0).unwrap_or_default()) + } + }; + if let Some(on_change) = slider.on_change { + commands.run_system_with(on_change, new_value); + } else { + commands + .entity(trigger.target()) + .insert(SliderValue(new_value)); + } + } +} + +/// Plugin that adds the observers for the [`CoreSlider`] widget. +pub struct CoreSliderPlugin; + +impl Plugin for CoreSliderPlugin { + fn build(&self, app: &mut App) { + app.add_observer(slider_on_pointer_down) + .add_observer(slider_on_drag_start) + .add_observer(slider_on_drag_end) + .add_observer(slider_on_drag) + .add_observer(slider_on_key_input) + .add_observer(slider_on_insert) + .add_observer(slider_on_insert_value) + .add_observer(slider_on_insert_range) + .add_observer(slider_on_insert_step) + .add_observer(slider_on_set_value); + } +} diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs new file mode 100644 index 0000000000..cdb9142b52 --- /dev/null +++ b/crates/bevy_core_widgets/src/lib.rs @@ -0,0 +1,38 @@ +//! This crate provides a set of core widgets for Bevy UI, such as buttons, checkboxes, and sliders. +//! These widgets have no inherent styling, it's the responsibility of the user to add styling +//! appropriate for their game or application. +//! +//! # State Management +//! +//! Most of the widgets use external state management: this means that the widgets do not +//! automatically update their own internal state, but instead rely on the app to update the widget +//! state (as well as any other related game state) in response to a change event emitted by the +//! widget. The primary motivation for this is to avoid two-way data binding in scenarios where the +//! user interface is showing a live view of dynamic data coming from deeper within the game engine. + +// Note on naming: the `Core` prefix is used on components that would normally be internal to the +// styled/opinionated widgets that use them. Components which are directly exposed to users above +// the widget level, like `SliderValue`, should not have the `Core` prefix. + +mod core_button; +mod core_checkbox; +mod core_slider; + +use bevy_app::{App, Plugin}; + +pub use core_button::{CoreButton, CoreButtonPlugin}; +pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; +pub use core_slider::{ + CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue, + SliderRange, SliderStep, SliderValue, TrackClick, +}; + +/// A plugin that registers the observers for all of the core widgets. If you don't want to +/// use all of the widgets, you can import the individual widget plugins instead. +pub struct CoreWidgetsPlugin; + +impl Plugin for CoreWidgetsPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((CoreButtonPlugin, CoreCheckboxPlugin, CoreSliderPlugin)); + } +} diff --git a/crates/bevy_dev_tools/Cargo.toml b/crates/bevy_dev_tools/Cargo.toml index 2250a35393..ab5a04f2b2 100644 --- a/crates/bevy_dev_tools/Cargo.toml +++ b/crates/bevy_dev_tools/Cargo.toml @@ -31,7 +31,7 @@ bevy_state = { path = "../bevy_state", version = "0.16.0-dev" } # other serde = { version = "1.0", features = ["derive"], optional = true } -ron = { version = "0.8.0", optional = true } +ron = { version = "0.10", optional = true } tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] diff --git a/crates/bevy_dev_tools/src/ci_testing/config.rs b/crates/bevy_dev_tools/src/ci_testing/config.rs index 6dc601f1cc..a2419dfaa5 100644 --- a/crates/bevy_dev_tools/src/ci_testing/config.rs +++ b/crates/bevy_dev_tools/src/ci_testing/config.rs @@ -49,7 +49,7 @@ pub enum CiTestingEvent { } /// A custom event that can be configured from a configuration file for CI testing. -#[derive(Event)] +#[derive(Event, BufferedEvent)] pub struct CiTestingCustomEvent(pub String); #[cfg(test)] diff --git a/crates/bevy_dev_tools/src/picking_debug.rs b/crates/bevy_dev_tools/src/picking_debug.rs index 79c1c8fff4..16233cd3dc 100644 --- a/crates/bevy_dev_tools/src/picking_debug.rs +++ b/crates/bevy_dev_tools/src/picking_debug.rs @@ -5,9 +5,9 @@ use bevy_color::prelude::*; use bevy_ecs::prelude::*; use bevy_picking::backend::HitData; use bevy_picking::hover::HoverMap; -use bevy_picking::pointer::{Location, PointerId, PointerPress}; +use bevy_picking::pointer::{Location, PointerId, PointerInput, PointerLocation, PointerPress}; use bevy_picking::prelude::*; -use bevy_picking::{pointer, PickingSystems}; +use bevy_picking::PickingSystems; use bevy_reflect::prelude::*; use bevy_render::prelude::*; use bevy_text::prelude::*; @@ -91,11 +91,11 @@ impl Plugin for DebugPickingPlugin { ( // This leaves room to easily change the log-level associated // with different events, should that be desired. - log_event_debug::.run_if(DebugPickingMode::is_noisy), + log_event_debug::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, log_pointer_event_debug::, - log_pointer_event_debug::, - log_pointer_event_debug::, + log_pointer_event_debug::, + log_pointer_event_debug::, log_pointer_event_debug::, log_pointer_event_trace::.run_if(DebugPickingMode::is_noisy), log_pointer_event_debug::, @@ -121,7 +121,7 @@ impl Plugin for DebugPickingPlugin { } /// Listen for any event and logs it at the debug level -pub fn log_event_debug(mut events: EventReader) { +pub fn log_event_debug(mut events: EventReader) { for event in events.read() { debug!("{event:?}"); } @@ -214,7 +214,7 @@ pub fn update_debug_data( entity_names: Query, mut pointers: Query<( &PointerId, - &pointer::PointerLocation, + &PointerLocation, &PointerPress, &mut PointerDebug, )>, diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index cb11b0adcd..6941b71f27 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -36,7 +36,7 @@ backtrace = ["std"] ## Enables `tracing` integration, allowing spans and other metrics to be reported ## through that framework. -trace = ["std", "dep:tracing"] +trace = ["std", "dep:tracing", "bevy_utils/debug"] ## Enables a more detailed set of traces which may be noisy if left on by default. detailed_trace = ["trace"] @@ -64,9 +64,9 @@ std = [ "bevy_reflect?/std", "bevy_tasks/std", "bevy_utils/parallel", + "bevy_utils/std", "bitflags/std", "concurrent-queue/std", - "disqualified/alloc", "fixedbitset/std", "indexmap/std", "serde?/std", @@ -99,7 +99,6 @@ bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-fea ] } bitflags = { version = "2.3", default-features = false } -disqualified = { version = "1.0", default-features = false } fixedbitset = { version = "0.5", default-features = false } serde = { version = "1", default-features = false, features = [ "alloc", diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index ba283d39b2..b085a79c21 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -277,26 +277,24 @@ world.spawn(PlayerBundle { }); ``` -### Events +### Buffered Events -Events offer a communication channel between one or more systems. Events can be sent using the system parameter `EventWriter` and received with `EventReader`. +Buffered events offer a communication channel between one or more systems. +They can be sent using the `EventWriter` system parameter and received with `EventReader`. ```rust use bevy_ecs::prelude::*; -#[derive(Event)] -struct MyEvent { - message: String, +#[derive(Event, BufferedEvent)] +struct Message(String); + +fn writer(mut writer: EventWriter) { + writer.write(Message("Hello!".to_string())); } -fn writer(mut writer: EventWriter) { - writer.write(MyEvent { - message: "hello!".to_string(), - }); -} - -fn reader(mut reader: EventReader) { - for event in reader.read() { +fn reader(mut reader: EventReader) { + for Message(message) in reader.read() { + println!("{}", message); } } ``` @@ -309,37 +307,39 @@ Observers are systems that listen for a "trigger" of a specific `Event`: use bevy_ecs::prelude::*; #[derive(Event)] -struct MyEvent { +struct Speak { message: String } let mut world = World::new(); -world.add_observer(|trigger: Trigger| { - println!("{}", trigger.event().message); +world.add_observer(|trigger: On| { + println!("{}", trigger.message); }); world.flush(); -world.trigger(MyEvent { - message: "hello!".to_string(), +world.trigger(Speak { + message: "Hello!".to_string(), }); ``` -These differ from `EventReader` and `EventWriter` in that they are "reactive". Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. Triggers can trigger other triggers, and they all will be evaluated at the same time! +These differ from `EventReader` and `EventWriter` in that they are "reactive". +Rather than happening at a specific point in a schedule, they happen _immediately_ whenever a trigger happens. +Triggers can trigger other triggers, and they all will be evaluated at the same time! -Events can also be triggered to target specific entities: +If the event is an `EntityEvent`, it can also be triggered to target specific entities: ```rust use bevy_ecs::prelude::*; -#[derive(Event)] +#[derive(Event, EntityEvent)] struct Explode; let mut world = World::new(); let entity = world.spawn_empty().id(); -world.add_observer(|trigger: Trigger, mut commands: Commands| { +world.add_observer(|trigger: On, mut commands: Commands| { println!("Entity {} goes BOOM!", trigger.target()); commands.entity(trigger.target()).despawn(); }); diff --git a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs index 4076819ee3..79c2158644 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs +++ b/crates/bevy_ecs/compile_fail/tests/ui/component_hook_relationship.rs @@ -60,4 +60,4 @@ mod case4 { pub struct BarTargetOf(Entity); } -fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::component::HookContext) {} +fn foo_hook(_world: bevy_ecs::world::DeferredWorld, _ctx: bevy_ecs::lifecycle::HookContext) {} diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs index fb01184048..ecdcb31a33 100644 --- a/crates/bevy_ecs/examples/events.rs +++ b/crates/bevy_ecs/examples/events.rs @@ -1,4 +1,4 @@ -//! In this example a system sends a custom event with a 50/50 chance during any frame. +//! In this example a system sends a custom buffered event with a 50/50 chance during any frame. //! If an event was sent, it will be printed by the console in a receiving system. #![expect(clippy::print_stdout, reason = "Allowed in examples.")] @@ -15,7 +15,7 @@ fn main() { // Create a schedule to store our systems let mut schedule = Schedule::default(); - // Events need to be updated in every frame in order to clear our buffers. + // Buffered events need to be updated every frame in order to clear our buffers. // This update should happen before we use the events. // Here, we use system sets to control the ordering. #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] @@ -37,7 +37,7 @@ fn main() { } // This is our event that we will send and receive in systems -#[derive(Event)] +#[derive(Event, BufferedEvent)] struct MyEvent { pub message: String, pub random_value: f32, diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index 00268cb680..53ba284588 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -13,11 +13,28 @@ use syn::{ LitStr, Member, Path, Result, Token, Type, Visibility, }; -pub const EVENT: &str = "event"; +pub const EVENT: &str = "entity_event"; pub const AUTO_PROPAGATE: &str = "auto_propagate"; pub const TRAVERSAL: &str = "traversal"; pub fn derive_event(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_ecs_path: Path = crate::bevy_ecs_path(); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Send + Sync + 'static }); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause {} + }) +} + +pub fn derive_entity_event(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let mut auto_propagate = false; let mut traversal: Type = parse_quote!(()); @@ -49,13 +66,30 @@ pub fn derive_event(input: TokenStream) -> TokenStream { let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); TokenStream::from(quote! { - impl #impl_generics #bevy_ecs_path::event::Event for #struct_name #type_generics #where_clause { + impl #impl_generics #bevy_ecs_path::event::EntityEvent for #struct_name #type_generics #where_clause { type Traversal = #traversal; const AUTO_PROPAGATE: bool = #auto_propagate; } }) } +pub fn derive_buffered_event(input: TokenStream) -> TokenStream { + let mut ast = parse_macro_input!(input as DeriveInput); + let bevy_ecs_path: Path = crate::bevy_ecs_path(); + + ast.generics + .make_where_clause() + .predicates + .push(parse_quote! { Self: Send + Sync + 'static }); + + let struct_name = &ast.ident; + let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl(); + + TokenStream::from(quote! { + impl #impl_generics #bevy_ecs_path::event::BufferedEvent for #struct_name #type_generics #where_clause {} + }) +} + pub fn derive_resource(input: TokenStream) -> TokenStream { let mut ast = parse_macro_input!(input as DeriveInput); let bevy_ecs_path: Path = crate::bevy_ecs_path(); @@ -434,7 +468,7 @@ impl HookAttributeKind { HookAttributeKind::Path(path) => path.to_token_stream(), HookAttributeKind::Call(call) => { quote!({ - fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::component::HookContext) { + fn _internal_hook(world: #bevy_ecs_path::world::DeferredWorld, ctx: #bevy_ecs_path::lifecycle::HookContext) { (#call)(world, ctx) } _internal_hook @@ -658,7 +692,7 @@ fn hook_register_function_call( ) -> Option { function.map(|meta| { quote! { - fn #hook() -> ::core::option::Option<#bevy_ecs_path::component::ComponentHook> { + fn #hook() -> ::core::option::Option<#bevy_ecs_path::lifecycle::ComponentHook> { ::core::option::Option::Some(#meta) } } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 114aff642b..7750f97259 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -1,4 +1,5 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] +//! Macros for deriving ECS traits. + #![cfg_attr(docsrs, feature(doc_auto_cfg))] extern crate proc_macro; @@ -15,7 +16,7 @@ use crate::{ use bevy_macro_utils::{derive_label, ensure_no_collision, get_struct_fields, BevyManifest}; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; -use quote::{format_ident, quote}; +use quote::{format_ident, quote, ToTokens}; use syn::{ parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, token::Comma, ConstParam, Data, DataStruct, DeriveInput, GenericParam, Index, TypeParam, @@ -28,12 +29,48 @@ enum BundleFieldKind { const BUNDLE_ATTRIBUTE_NAME: &str = "bundle"; const BUNDLE_ATTRIBUTE_IGNORE_NAME: &str = "ignore"; +const BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS: &str = "ignore_from_components"; +#[derive(Debug)] +struct BundleAttributes { + impl_from_components: bool, +} + +impl Default for BundleAttributes { + fn default() -> Self { + Self { + impl_from_components: true, + } + } +} + +/// Implement the `Bundle` trait. #[proc_macro_derive(Bundle, attributes(bundle))] pub fn derive_bundle(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); let ecs_path = bevy_ecs_path(); + let mut errors = vec![]; + + let mut attributes = BundleAttributes::default(); + + for attr in &ast.attrs { + if attr.path().is_ident(BUNDLE_ATTRIBUTE_NAME) { + let parsing = attr.parse_nested_meta(|meta| { + if meta.path.is_ident(BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS) { + attributes.impl_from_components = false; + return Ok(()); + } + + Err(meta.error(format!("Invalid bundle container attribute. Allowed attributes: `{BUNDLE_ATTRIBUTE_NO_FROM_COMPONENTS}`"))) + }); + + if let Err(error) = parsing { + errors.push(error.into_compile_error()); + } + } + } + let named_fields = match get_struct_fields(&ast.data, "derive(Bundle)") { Ok(fields) => fields, Err(e) => return e.into_compile_error().into(), @@ -42,6 +79,8 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { let mut field_kind = Vec::with_capacity(named_fields.len()); for field in named_fields { + let mut kind = BundleFieldKind::Component; + for attr in field .attrs .iter() @@ -49,7 +88,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { { if let Err(error) = attr.parse_nested_meta(|meta| { if meta.path.is_ident(BUNDLE_ATTRIBUTE_IGNORE_NAME) { - field_kind.push(BundleFieldKind::Ignore); + kind = BundleFieldKind::Ignore; Ok(()) } else { Err(meta.error(format!( @@ -61,7 +100,7 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { } } - field_kind.push(BundleFieldKind::Component); + field_kind.push(kind); } let field = named_fields @@ -74,61 +113,33 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { .map(|field| &field.ty) .collect::>(); - let mut field_component_ids = Vec::new(); - let mut field_get_component_ids = Vec::new(); - let mut field_get_components = Vec::new(); - let mut field_from_components = Vec::new(); - let mut field_required_components = Vec::new(); + let mut active_field_types = Vec::new(); + let mut active_field_tokens = Vec::new(); + let mut inactive_field_tokens = Vec::new(); for (((i, field_type), field_kind), field) in field_type .iter() .enumerate() .zip(field_kind.iter()) .zip(field.iter()) { + let field_tokens = match field { + Some(field) => field.to_token_stream(), + None => Index::from(i).to_token_stream(), + }; match field_kind { BundleFieldKind::Component => { - field_component_ids.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::component_ids(components, &mut *ids); - }); - field_required_components.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::register_required_components(components, required_components); - }); - field_get_component_ids.push(quote! { - <#field_type as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids); - }); - match field { - Some(field) => { - field_get_components.push(quote! { - self.#field.get_components(&mut *func); - }); - field_from_components.push(quote! { - #field: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), - }); - } - None => { - let index = Index::from(i); - field_get_components.push(quote! { - self.#index.get_components(&mut *func); - }); - field_from_components.push(quote! { - #index: <#field_type as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func), - }); - } - } + active_field_types.push(field_type); + active_field_tokens.push(field_tokens); } - BundleFieldKind::Ignore => { - field_from_components.push(quote! { - #field: ::core::default::Default::default(), - }); - } + BundleFieldKind::Ignore => inactive_field_tokens.push(field_tokens), } } let generics = ast.generics; let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); let struct_name = &ast.ident; - TokenStream::from(quote! { + let bundle_impl = quote! { // SAFETY: // - ComponentId is returned in field-definition-order. [get_components] uses field-definition-order // - `Bundle::get_components` is exactly once for each member. Rely's on the Component -> Bundle implementation to properly pass @@ -138,40 +149,27 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { fn component_ids( components: &mut #ecs_path::component::ComponentsRegistrator, ids: &mut impl FnMut(#ecs_path::component::ComponentId) - ){ - #(#field_component_ids)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::component_ids(components, ids);)* } fn get_component_ids( components: &#ecs_path::component::Components, ids: &mut impl FnMut(Option<#ecs_path::component::ComponentId>) - ){ - #(#field_get_component_ids)* + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::get_component_ids(components, &mut *ids);)* } fn register_required_components( components: &mut #ecs_path::component::ComponentsRegistrator, required_components: &mut #ecs_path::component::RequiredComponents - ){ - #(#field_required_components)* - } - } - - // SAFETY: - // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order - #[allow(deprecated)] - unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { - #[allow(unused_variables, non_snake_case)] - unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self - where - __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> - { - Self{ - #(#field_from_components)* - } + ) { + #(<#active_field_types as #ecs_path::bundle::Bundle>::register_required_components(components, required_components);)* } } + }; + let dynamic_bundle_impl = quote! { #[allow(deprecated)] impl #impl_generics #ecs_path::bundle::DynamicBundle for #struct_name #ty_generics #where_clause { type Effect = (); @@ -181,12 +179,40 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream { self, func: &mut impl FnMut(#ecs_path::component::StorageType, #ecs_path::ptr::OwningPtr<'_>) ) { - #(#field_get_components)* + #(<#active_field_types as #ecs_path::bundle::DynamicBundle>::get_components(self.#active_field_tokens, &mut *func);)* } } + }; + + let from_components_impl = attributes.impl_from_components.then(|| quote! { + // SAFETY: + // - ComponentId is returned in field-definition-order. [from_components] uses field-definition-order + #[allow(deprecated)] + unsafe impl #impl_generics #ecs_path::bundle::BundleFromComponents for #struct_name #ty_generics #where_clause { + #[allow(unused_variables, non_snake_case)] + unsafe fn from_components<__T, __F>(ctx: &mut __T, func: &mut __F) -> Self + where + __F: FnMut(&mut __T) -> #ecs_path::ptr::OwningPtr<'_> + { + Self { + #(#active_field_tokens: <#active_field_types as #ecs_path::bundle::BundleFromComponents>::from_components(ctx, &mut *func),)* + #(#inactive_field_tokens: ::core::default::Default::default(),)* + } + } + } + }); + + let attribute_errors = &errors; + + TokenStream::from(quote! { + #(#attribute_errors)* + #bundle_impl + #from_components_impl + #dynamic_bundle_impl }) } +/// Implement the `MapEntities` trait. #[proc_macro_derive(MapEntities, attributes(entities))] pub fn derive_map_entities(input: TokenStream) -> TokenStream { let ast = parse_macro_input!(input as DeriveInput); @@ -394,10 +420,10 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { > #path::system::SystemParamBuilder<#generic_struct> for #builder_name<#(#builder_type_parameters,)*> #where_clause { - fn build(self, world: &mut #path::world::World, meta: &mut #path::system::SystemMeta) -> <#generic_struct as #path::system::SystemParam>::State { + fn build(self, world: &mut #path::world::World) -> <#generic_struct as #path::system::SystemParam>::State { let #builder_name { #(#fields: #field_locals,)* } = self; #state_struct_name { - state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world, meta) + state: #path::system::SystemParamBuilder::build((#(#tuple_patterns,)*), world) } } } @@ -426,12 +452,16 @@ pub fn derive_system_param(input: TokenStream) -> TokenStream { type State = #state_struct_name<#punctuated_generic_idents>; type Item<'w, 's> = #struct_name #ty_generics; - fn init_state(world: &mut #path::world::World, system_meta: &mut #path::system::SystemMeta) -> Self::State { + fn init_state(world: &mut #path::world::World) -> Self::State { #state_struct_name { - state: <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_state(world, system_meta), + state: <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_state(world), } } + fn init_access(state: &Self::State, system_meta: &mut #path::system::SystemMeta, component_access_set: &mut #path::query::FilteredAccessSet<#path::component::ComponentId>, world: &mut #path::world::World) { + <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::init_access(&state.state, system_meta, component_access_set, world); + } + fn apply(state: &mut Self::State, system_meta: &#path::system::SystemMeta, world: &mut #path::world::World) { <#fields_alias::<'_, '_, #punctuated_generic_idents> as #path::system::SystemParam>::apply(&mut state.state, system_meta, world); } @@ -522,16 +552,31 @@ pub(crate) fn bevy_ecs_path() -> syn::Path { BevyManifest::shared().get_path("bevy_ecs") } -#[proc_macro_derive(Event, attributes(event))] +/// Implement the `Event` trait. +#[proc_macro_derive(Event)] pub fn derive_event(input: TokenStream) -> TokenStream { component::derive_event(input) } +/// Implement the `EntityEvent` trait. +#[proc_macro_derive(EntityEvent, attributes(entity_event))] +pub fn derive_entity_event(input: TokenStream) -> TokenStream { + component::derive_entity_event(input) +} + +/// Implement the `BufferedEvent` trait. +#[proc_macro_derive(BufferedEvent)] +pub fn derive_buffered_event(input: TokenStream) -> TokenStream { + component::derive_buffered_event(input) +} + +/// Implement the `Resource` trait. #[proc_macro_derive(Resource)] pub fn derive_resource(input: TokenStream) -> TokenStream { component::derive_resource(input) } +/// Implement the `Component` trait. #[proc_macro_derive( Component, attributes(component, require, relationship, relationship_target, entities) @@ -540,6 +585,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream { component::derive_component(input) } +/// Implement the `FromWorld` trait. #[proc_macro_derive(FromWorld, attributes(from_world))] pub fn derive_from_world(input: TokenStream) -> TokenStream { let bevy_ecs_path = bevy_ecs_path(); diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 4e4529e631..12d9c2bf1c 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -74,12 +74,23 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { let user_generics = ast.generics.clone(); let (user_impl_generics, user_ty_generics, user_where_clauses) = user_generics.split_for_impl(); let user_generics_with_world = { - let mut generics = ast.generics; + let mut generics = ast.generics.clone(); generics.params.insert(0, parse_quote!('__w)); generics }; let (user_impl_generics_with_world, user_ty_generics_with_world, user_where_clauses_with_world) = user_generics_with_world.split_for_impl(); + let user_generics_with_world_and_state = { + let mut generics = ast.generics; + generics.params.insert(0, parse_quote!('__w)); + generics.params.insert(1, parse_quote!('__s)); + generics + }; + let ( + user_impl_generics_with_world_and_state, + user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, + ) = user_generics_with_world_and_state.split_for_impl(); let struct_name = ast.ident; let read_only_struct_name = if attributes.is_mutable { @@ -164,13 +175,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { &visibility, &item_struct_name, &field_types, - &user_impl_generics_with_world, + &user_impl_generics_with_world_and_state, &field_attrs, &field_visibilities, &field_idents, &user_ty_generics, - &user_ty_generics_with_world, - user_where_clauses_with_world, + &user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, ); let mutable_world_query_impl = world_query_impl( &path, @@ -199,13 +210,13 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { &visibility, &read_only_item_struct_name, &read_only_field_types, - &user_impl_generics_with_world, + &user_impl_generics_with_world_and_state, &field_attrs, &field_visibilities, &field_idents, &user_ty_generics, - &user_ty_generics_with_world, - user_where_clauses_with_world, + &user_ty_generics_with_world_and_state, + user_where_clauses_with_world_and_state, ); let readonly_world_query_impl = world_query_impl( &path, @@ -256,11 +267,11 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { for #read_only_struct_name #user_ty_generics #user_where_clauses { const IS_READ_ONLY: bool = true; type ReadOnly = #read_only_struct_name #user_ty_generics; - type Item<'__w> = #read_only_item_struct_name #user_ty_generics_with_world; + type Item<'__w, '__s> = #read_only_item_struct_name #user_ty_generics_with_world_and_state; - fn shrink<'__wlong: '__wshort, '__wshort>( - item: Self::Item<'__wlong> - ) -> Self::Item<'__wshort> { + fn shrink<'__wlong: '__wshort, '__wshort, '__s>( + item: Self::Item<'__wlong, '__s> + ) -> Self::Item<'__wshort, '__s> { #read_only_item_struct_name { #( #field_idents: <#read_only_field_types>::shrink(item.#field_idents), @@ -278,13 +289,26 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { /// SAFETY: we call `fetch` for each member that implements `Fetch`. #[inline(always)] - unsafe fn fetch<'__w>( + unsafe fn fetch<'__w, '__s>( + _state: &'__s Self::State, _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, - ) -> Self::Item<'__w> { + ) -> Self::Item<'__w, '__s> { Self::Item { - #(#field_idents: <#read_only_field_types>::fetch(&mut _fetch.#named_field_idents, _entity, _table_row),)* + #(#field_idents: <#read_only_field_types>::fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row),)* + } + } + } + + impl #user_impl_generics #path::query::ReleaseStateQueryData + for #read_only_struct_name #user_ty_generics #user_where_clauses + // Make these HRTBs with an unused lifetime parameter to allow trivial constraints + // See https://github.com/rust-lang/rust/issues/48214 + where #(for<'__a> #field_types: #path::query::QueryData,)* { + fn release_state<'__w>(_item: Self::Item<'__w, '_>) -> Self::Item<'__w, 'static> { + Self::Item { + #(#field_idents: <#read_only_field_types>::release_state(_item.#field_idents),)* } } } @@ -301,11 +325,11 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { for #struct_name #user_ty_generics #user_where_clauses { const IS_READ_ONLY: bool = #is_read_only; type ReadOnly = #read_only_struct_name #user_ty_generics; - type Item<'__w> = #item_struct_name #user_ty_generics_with_world; + type Item<'__w, '__s> = #item_struct_name #user_ty_generics_with_world_and_state; - fn shrink<'__wlong: '__wshort, '__wshort>( - item: Self::Item<'__wlong> - ) -> Self::Item<'__wshort> { + fn shrink<'__wlong: '__wshort, '__wshort, '__s>( + item: Self::Item<'__wlong, '__s> + ) -> Self::Item<'__wshort, '__s> { #item_struct_name { #( #field_idents: <#field_types>::shrink(item.#field_idents), @@ -323,13 +347,26 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { /// SAFETY: we call `fetch` for each member that implements `Fetch`. #[inline(always)] - unsafe fn fetch<'__w>( + unsafe fn fetch<'__w, '__s>( + _state: &'__s Self::State, _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, - ) -> Self::Item<'__w> { + ) -> Self::Item<'__w, '__s> { Self::Item { - #(#field_idents: <#field_types>::fetch(&mut _fetch.#named_field_idents, _entity, _table_row),)* + #(#field_idents: <#field_types>::fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row),)* + } + } + } + + impl #user_impl_generics #path::query::ReleaseStateQueryData + for #struct_name #user_ty_generics #user_where_clauses + // Make these HRTBs with an unused lifetime parameter to allow trivial constraints + // See https://github.com/rust-lang/rust/issues/48214 + where #(for<'__a> #field_types: #path::query::ReleaseStateQueryData,)* { + fn release_state<'__w>(_item: Self::Item<'__w, '_>) -> Self::Item<'__w, 'static> { + Self::Item { + #(#field_idents: <#field_types>::release_state(_item.#field_idents),)* } } } diff --git a/crates/bevy_ecs/macros/src/query_filter.rs b/crates/bevy_ecs/macros/src/query_filter.rs index c7ddb9cc83..5ae2d2325f 100644 --- a/crates/bevy_ecs/macros/src/query_filter.rs +++ b/crates/bevy_ecs/macros/src/query_filter.rs @@ -102,11 +102,12 @@ pub fn derive_query_filter_impl(input: TokenStream) -> TokenStream { #[allow(unused_variables)] #[inline(always)] unsafe fn filter_fetch<'__w>( + _state: &Self::State, _fetch: &mut ::Fetch<'__w>, _entity: #path::entity::Entity, _table_row: #path::storage::TableRow, ) -> bool { - true #(&& <#field_types>::filter_fetch(&mut _fetch.#named_field_idents, _entity, _table_row))* + true #(&& <#field_types>::filter_fetch(&_state.#named_field_idents, &mut _fetch.#named_field_idents, _entity, _table_row))* } } }; diff --git a/crates/bevy_ecs/macros/src/world_query.rs b/crates/bevy_ecs/macros/src/world_query.rs index 5c4c0bff01..5a7d164b80 100644 --- a/crates/bevy_ecs/macros/src/world_query.rs +++ b/crates/bevy_ecs/macros/src/world_query.rs @@ -10,13 +10,13 @@ pub(crate) fn item_struct( visibility: &Visibility, item_struct_name: &Ident, field_types: &Vec, - user_impl_generics_with_world: &ImplGenerics, + user_impl_generics_with_world_and_state: &ImplGenerics, field_attrs: &Vec>, field_visibilities: &Vec, field_idents: &Vec, user_ty_generics: &TypeGenerics, - user_ty_generics_with_world: &TypeGenerics, - user_where_clauses_with_world: Option<&WhereClause>, + user_ty_generics_with_world_and_state: &TypeGenerics, + user_where_clauses_with_world_and_state: Option<&WhereClause>, ) -> proc_macro2::TokenStream { let item_attrs = quote! { #[doc = concat!( @@ -33,20 +33,20 @@ pub(crate) fn item_struct( Fields::Named(_) => quote! { #derive_macro_call #item_attrs - #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world { - #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::QueryData>::Item<'__w>,)* + #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state { + #(#(#field_attrs)* #field_visibilities #field_idents: <#field_types as #path::query::QueryData>::Item<'__w, '__s>,)* } }, Fields::Unnamed(_) => quote! { #derive_macro_call #item_attrs - #visibility struct #item_struct_name #user_impl_generics_with_world #user_where_clauses_with_world( - #( #field_visibilities <#field_types as #path::query::QueryData>::Item<'__w>, )* + #visibility struct #item_struct_name #user_impl_generics_with_world_and_state #user_where_clauses_with_world_and_state( + #( #field_visibilities <#field_types as #path::query::QueryData>::Item<'__w, '__s>, )* ); }, Fields::Unit => quote! { #item_attrs - #visibility type #item_struct_name #user_ty_generics_with_world = #struct_name #user_ty_generics; + #visibility type #item_struct_name #user_ty_generics_with_world_and_state = #struct_name #user_ty_generics; }, } } @@ -79,7 +79,7 @@ pub(crate) fn world_query_impl( #[automatically_derived] #visibility struct #fetch_struct_name #user_impl_generics_with_world #user_where_clauses_with_world { #(#named_field_idents: <#field_types as #path::query::WorldQuery>::Fetch<'__w>,)* - #marker_name: &'__w (), + #marker_name: &'__w(), } impl #user_impl_generics_with_world Clone for #fetch_struct_name #user_ty_generics_with_world @@ -110,9 +110,9 @@ pub(crate) fn world_query_impl( } } - unsafe fn init_fetch<'__w>( + unsafe fn init_fetch<'__w, '__s>( _world: #path::world::unsafe_world_cell::UnsafeWorldCell<'__w>, - state: &Self::State, + state: &'__s Self::State, _last_run: #path::component::Tick, _this_run: #path::component::Tick, ) -> ::Fetch<'__w> { @@ -133,9 +133,9 @@ pub(crate) fn world_query_impl( /// SAFETY: we call `set_archetype` for each member that implements `Fetch` #[inline] - unsafe fn set_archetype<'__w>( + unsafe fn set_archetype<'__w, '__s>( _fetch: &mut ::Fetch<'__w>, - _state: &Self::State, + _state: &'__s Self::State, _archetype: &'__w #path::archetype::Archetype, _table: &'__w #path::storage::Table ) { @@ -144,9 +144,9 @@ pub(crate) fn world_query_impl( /// SAFETY: we call `set_table` for each member that implements `Fetch` #[inline] - unsafe fn set_table<'__w>( + unsafe fn set_table<'__w, '__s>( _fetch: &mut ::Fetch<'__w>, - _state: &Self::State, + _state: &'__s Self::State, _table: &'__w #path::storage::Table ) { #(<#field_types>::set_table(&mut _fetch.#named_field_idents, &_state.#named_field_idents, _table);)* diff --git a/crates/bevy_ecs/src/archetype.rs b/crates/bevy_ecs/src/archetype.rs index 4e5b28dde8..34a2a4c813 100644 --- a/crates/bevy_ecs/src/archetype.rs +++ b/crates/bevy_ecs/src/archetype.rs @@ -691,41 +691,41 @@ impl Archetype { self.flags().contains(ArchetypeFlags::ON_DESPAWN_HOOK) } - /// Returns true if any of the components in this archetype have at least one [`OnAdd`] observer + /// Returns true if any of the components in this archetype have at least one [`Add`] observer /// - /// [`OnAdd`]: crate::world::OnAdd + /// [`Add`]: crate::lifecycle::Add #[inline] pub fn has_add_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_ADD_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnInsert`] observer + /// Returns true if any of the components in this archetype have at least one [`Insert`] observer /// - /// [`OnInsert`]: crate::world::OnInsert + /// [`Insert`]: crate::lifecycle::Insert #[inline] pub fn has_insert_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_INSERT_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnReplace`] observer + /// Returns true if any of the components in this archetype have at least one [`Replace`] observer /// - /// [`OnReplace`]: crate::world::OnReplace + /// [`Replace`]: crate::lifecycle::Replace #[inline] pub fn has_replace_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REPLACE_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnRemove`] observer + /// Returns true if any of the components in this archetype have at least one [`Remove`] observer /// - /// [`OnRemove`]: crate::world::OnRemove + /// [`Remove`]: crate::lifecycle::Remove #[inline] pub fn has_remove_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_REMOVE_OBSERVER) } - /// Returns true if any of the components in this archetype have at least one [`OnDespawn`] observer + /// Returns true if any of the components in this archetype have at least one [`Despawn`] observer /// - /// [`OnDespawn`]: crate::world::OnDespawn + /// [`Despawn`]: crate::lifecycle::Despawn #[inline] pub fn has_despawn_observer(&self) -> bool { self.flags().contains(ArchetypeFlags::ON_DESPAWN_OBSERVER) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index f0641ff80c..8efdc60ad9 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -2,6 +2,57 @@ //! //! This module contains the [`Bundle`] trait and some other helper types. +/// Derive the [`Bundle`] trait +/// +/// You can apply this derive macro to structs that are +/// composed of [`Component`]s or +/// other [`Bundle`]s. +/// +/// ## Attributes +/// +/// Sometimes parts of the Bundle should not be inserted. +/// Those can be marked with `#[bundle(ignore)]`, and they will be skipped. +/// In that case, the field needs to implement [`Default`] unless you also ignore +/// the [`BundleFromComponents`] implementation. +/// +/// ```rust +/// # use bevy_ecs::prelude::{Component, Bundle}; +/// # #[derive(Component)] +/// # struct Hitpoint; +/// # +/// #[derive(Bundle)] +/// struct HitpointMarker { +/// hitpoints: Hitpoint, +/// +/// #[bundle(ignore)] +/// creator: Option +/// } +/// ``` +/// +/// Some fields may be bundles that do not implement +/// [`BundleFromComponents`]. This happens for bundles that cannot be extracted. +/// For example with [`SpawnRelatedBundle`](bevy_ecs::spawn::SpawnRelatedBundle), see below for an +/// example usage. +/// In those cases you can either ignore it as above, +/// or you can opt out the whole Struct by marking it as ignored with +/// `#[bundle(ignore_from_components)]`. +/// +/// ```rust +/// # use bevy_ecs::prelude::{Component, Bundle, ChildOf, Spawn}; +/// # #[derive(Component)] +/// # struct Hitpoint; +/// # #[derive(Component)] +/// # struct Marker; +/// # +/// use bevy_ecs::spawn::SpawnRelatedBundle; +/// +/// #[derive(Bundle)] +/// #[bundle(ignore_from_components)] +/// struct HitpointMarker { +/// hitpoints: Hitpoint, +/// related_spawner: SpawnRelatedBundle>, +/// } +/// ``` pub use bevy_ecs_macros::Bundle; use crate::{ @@ -15,15 +66,13 @@ use crate::{ RequiredComponents, StorageType, Tick, }, entity::{Entities, Entity, EntityLocation}, + lifecycle::{ADD, INSERT, REMOVE, REPLACE}, observer::Observers, prelude::World, query::DebugCheckedUnwrap, relationship::RelationshipHookMode, storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow}, - world::{ - unsafe_world_cell::UnsafeWorldCell, EntityWorldMut, ON_ADD, ON_INSERT, ON_REMOVE, - ON_REPLACE, - }, + world::{unsafe_world_cell::UnsafeWorldCell, EntityWorldMut}, }; use alloc::{boxed::Box, vec, vec::Vec}; use bevy_platform::collections::{HashMap, HashSet}; @@ -501,10 +550,9 @@ impl BundleInfo { // SAFETY: the caller ensures component_id is valid. unsafe { components.get_info_unchecked(id).name() } }) - .collect::>() - .join(", "); + .collect::>(); - panic!("Bundle {bundle_type_name} has duplicate components: {names}"); + panic!("Bundle {bundle_type_name} has duplicate components: {names:?}"); } // handle explicit components @@ -1142,8 +1190,8 @@ impl<'w> BundleInserter<'w> { if insert_mode == InsertMode::Replace { if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, - entity, + REPLACE, + Some(entity), archetype_after_insert.iter_existing(), caller, ); @@ -1327,8 +1375,8 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, - entity, + ADD, + Some(entity), archetype_after_insert.iter_added(), caller, ); @@ -1345,8 +1393,8 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), archetype_after_insert.iter_inserted(), caller, ); @@ -1364,8 +1412,8 @@ impl<'w> BundleInserter<'w> { ); if new_archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), archetype_after_insert.iter_added(), caller, ); @@ -1518,8 +1566,8 @@ impl<'w> BundleRemover<'w> { }; if self.old_archetype.as_ref().has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, - entity, + REPLACE, + Some(entity), bundle_components_in_archetype(), caller, ); @@ -1533,8 +1581,8 @@ impl<'w> BundleRemover<'w> { ); if self.old_archetype.as_ref().has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, - entity, + REMOVE, + Some(entity), bundle_components_in_archetype(), caller, ); @@ -1784,8 +1832,8 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_add_observer() { deferred_world.trigger_observers( - ON_ADD, - entity, + ADD, + Some(entity), bundle_info.iter_contributed_components(), caller, ); @@ -1799,8 +1847,8 @@ impl<'w> BundleSpawner<'w> { ); if archetype.has_insert_observer() { deferred_world.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), bundle_info.iter_contributed_components(), caller, ); @@ -2072,7 +2120,7 @@ fn sorted_remove(source: &mut Vec, remove: &[T]) { #[cfg(test)] mod tests { use crate::{ - archetype::ArchetypeCreated, component::HookContext, prelude::*, world::DeferredWorld, + archetype::ArchetypeCreated, lifecycle::HookContext, prelude::*, world::DeferredWorld, }; use alloc::vec; @@ -2122,6 +2170,26 @@ mod tests { } } + #[derive(Bundle)] + #[bundle(ignore_from_components)] + struct BundleNoExtract { + b: B, + no_from_comp: crate::spawn::SpawnRelatedBundle>, + } + + #[test] + fn can_spawn_bundle_without_extract() { + let mut world = World::new(); + let id = world + .spawn(BundleNoExtract { + b: B, + no_from_comp: Children::spawn(Spawn(C)), + }) + .id(); + + assert!(world.entity(id).get::().is_some()); + } + #[test] fn component_hook_order_spawn_despawn() { let mut world = World::new(); @@ -2317,7 +2385,7 @@ mod tests { #[derive(Resource, Default)] struct Count(u32); world.init_resource::(); - world.add_observer(|_t: Trigger, mut count: ResMut| { + world.add_observer(|_t: On, mut count: ResMut| { count.0 += 1; }); @@ -2329,4 +2397,13 @@ mod tests { assert_eq!(world.resource::().0, 3); } + + #[derive(Bundle)] + #[expect(unused, reason = "tests the output of the derive macro is valid")] + struct Ignore { + #[bundle(ignore)] + foo: i32, + #[bundle(ignore)] + bar: i32, + } } diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 85219d44ca..006b738caf 100644 --- a/crates/bevy_ecs/src/change_detection.rs +++ b/crates/bevy_ecs/src/change_detection.rs @@ -230,7 +230,7 @@ pub trait DetectChangesMut: DetectChanges { /// #[derive(Resource, PartialEq, Eq)] /// pub struct Score(u32); /// - /// #[derive(Event, PartialEq, Eq)] + /// #[derive(Event, BufferedEvent, PartialEq, Eq)] /// pub struct ScoreChanged { /// current: u32, /// previous: u32, @@ -898,63 +898,39 @@ impl_debug!(Ref<'w, T>,); /// Unique mutable borrow of an entity's component or of a resource. /// -/// This can be used in queries to opt into change detection on both their mutable and immutable forms, as opposed to -/// `&mut T`, which only provides access to change detection while in its mutable form: +/// This can be used in queries to access change detection from immutable query methods, as opposed +/// to `&mut T` which only provides access to change detection from mutable query methods. /// /// ```rust /// # use bevy_ecs::prelude::*; /// # use bevy_ecs::query::QueryData; /// # -/// #[derive(Component, Clone)] +/// #[derive(Component, Clone, Debug)] /// struct Name(String); /// -/// #[derive(Component, Clone, Copy)] +/// #[derive(Component, Clone, Copy, Debug)] /// struct Health(f32); /// -/// #[derive(Component, Clone, Copy)] -/// struct Position { -/// x: f32, -/// y: f32, -/// }; +/// fn my_system(mut query: Query<(Mut, &mut Health)>) { +/// // Mutable access provides change detection information for both parameters: +/// // - `name` has type `Mut` +/// // - `health` has type `Mut` +/// for (name, health) in query.iter_mut() { +/// println!("Name: {:?} (last changed {:?})", name, name.last_changed()); +/// println!("Health: {:?} (last changed: {:?})", health, health.last_changed()); +/// # println!("{}{}", name.0, health.0); // Silence dead_code warning +/// } /// -/// #[derive(Component, Clone, Copy)] -/// struct Player { -/// id: usize, -/// }; -/// -/// #[derive(QueryData)] -/// #[query_data(mutable)] -/// struct PlayerQuery { -/// id: &'static Player, -/// -/// // Reacting to `PlayerName` changes is expensive, so we need to enable change detection when reading it. -/// name: Mut<'static, Name>, -/// -/// health: &'static mut Health, -/// position: &'static mut Position, -/// } -/// -/// fn update_player_avatars(players_query: Query) { -/// // The item returned by the iterator is of type `PlayerQueryReadOnlyItem`. -/// for player in players_query.iter() { -/// if player.name.is_changed() { -/// // Update the player's name. This clones a String, and so is more expensive. -/// update_player_name(player.id, player.name.clone()); -/// } -/// -/// // Update the health bar. -/// update_player_health(player.id, *player.health); -/// -/// // Update the player's position. -/// update_player_position(player.id, *player.position); +/// // Immutable access only provides change detection for `Name`: +/// // - `name` has type `Ref` +/// // - `health` has type `&Health` +/// for (name, health) in query.iter() { +/// println!("Name: {:?} (last changed {:?})", name, name.last_changed()); +/// println!("Health: {:?}", health); /// } /// } /// -/// # bevy_ecs::system::assert_is_system(update_player_avatars); -/// -/// # fn update_player_name(player: &Player, new_name: Name) {} -/// # fn update_player_health(player: &Player, new_health: Health) {} -/// # fn update_player_position(player: &Player, new_position: Position) {} +/// # bevy_ecs::system::assert_is_system(my_system); /// ``` pub struct Mut<'w, T: ?Sized> { pub(crate) value: &'w mut T, diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index d083901ccc..cfcde29ab2 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -5,16 +5,17 @@ use crate::{ bundle::BundleInfo, change_detection::{MaybeLocation, MAX_CHANGE_AGE}, entity::{ComponentCloneCtx, Entity, EntityMapper, SourceComponent}, + lifecycle::{ComponentHook, ComponentHooks}, query::DebugCheckedUnwrap, - relationship::RelationshipHookMode, resource::Resource, storage::{SparseSetIndex, SparseSets, Table, TableRow}, system::{Local, SystemParam}, - world::{DeferredWorld, FromWorld, World}, + world::{FromWorld, World}, }; use alloc::boxed::Box; use alloc::{borrow::Cow, format, vec::Vec}; pub use bevy_ecs_macros::Component; +use bevy_ecs_macros::Event; use bevy_platform::sync::Arc; use bevy_platform::{ collections::{HashMap, HashSet}, @@ -23,7 +24,7 @@ use bevy_platform::{ use bevy_ptr::{OwningPtr, UnsafeCellDeref}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; -use bevy_utils::TypeIdMap; +use bevy_utils::{prelude::DebugName, TypeIdMap}; use core::{ alloc::Layout, any::{Any, TypeId}, @@ -33,7 +34,6 @@ use core::{ mem::needs_drop, ops::{Deref, DerefMut}, }; -use disqualified::ShortName; use smallvec::SmallVec; use thiserror::Error; @@ -375,7 +375,8 @@ use thiserror::Error; /// - `#[component(on_remove = on_remove_function)]` /// /// ``` -/// # use bevy_ecs::component::{Component, HookContext}; +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::lifecycle::HookContext; /// # use bevy_ecs::world::DeferredWorld; /// # use bevy_ecs::entity::Entity; /// # use bevy_ecs::component::ComponentId; @@ -404,7 +405,8 @@ use thiserror::Error; /// This also supports function calls that yield closures /// /// ``` -/// # use bevy_ecs::component::{Component, HookContext}; +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::lifecycle::HookContext; /// # use bevy_ecs::world::DeferredWorld; /// # /// #[derive(Component)] @@ -595,7 +597,7 @@ mod private { /// `&mut ...`, created while inserted onto an entity. /// In all other ways, they are identical to mutable components. /// This restriction allows hooks to observe all changes made to an immutable -/// component, effectively turning the `OnInsert` and `OnReplace` hooks into a +/// component, effectively turning the `Insert` and `Replace` hooks into a /// `OnMutate` hook. /// This is not practical for mutable components, as the runtime cost of invoking /// a hook for every exclusive reference created would be far too high. @@ -656,244 +658,6 @@ pub enum StorageType { SparseSet, } -/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`. -pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext); - -/// Context provided to a [`ComponentHook`]. -#[derive(Clone, Copy, Debug)] -pub struct HookContext { - /// The [`Entity`] this hook was invoked for. - pub entity: Entity, - /// The [`ComponentId`] this hook was invoked for. - pub component_id: ComponentId, - /// The caller location is `Some` if the `track_caller` feature is enabled. - pub caller: MaybeLocation, - /// Configures how relationship hooks will run - pub relationship_hook_mode: RelationshipHookMode, -} - -/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. -/// -/// Hooks are functions that run when a component is added, overwritten, or removed from an entity. -/// These are intended to be used for structural side effects that need to happen when a component is added or removed, -/// and are not intended for general-purpose logic. -/// -/// For example, you might use a hook to update a cached index when a component is added, -/// to clean up resources when a component is removed, -/// or to keep hierarchical data structures across entities in sync. -/// -/// This information is stored in the [`ComponentInfo`] of the associated component. -/// -/// There is two ways of configuring hooks for a component: -/// 1. Defining the relevant hooks on the [`Component`] implementation -/// 2. Using the [`World::register_component_hooks`] method -/// -/// # Example 2 -/// -/// ``` -/// use bevy_ecs::prelude::*; -/// use bevy_platform::collections::HashSet; -/// -/// #[derive(Component)] -/// struct MyTrackedComponent; -/// -/// #[derive(Resource, Default)] -/// struct TrackedEntities(HashSet); -/// -/// let mut world = World::new(); -/// world.init_resource::(); -/// -/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks -/// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); -/// assert!(tracked_component_query.iter(&world).next().is_none()); -/// -/// world.register_component_hooks::().on_add(|mut world, context| { -/// let mut tracked_entities = world.resource_mut::(); -/// tracked_entities.0.insert(context.entity); -/// }); -/// -/// world.register_component_hooks::().on_remove(|mut world, context| { -/// let mut tracked_entities = world.resource_mut::(); -/// tracked_entities.0.remove(&context.entity); -/// }); -/// -/// let entity = world.spawn(MyTrackedComponent).id(); -/// let tracked_entities = world.resource::(); -/// assert!(tracked_entities.0.contains(&entity)); -/// -/// world.despawn(entity); -/// let tracked_entities = world.resource::(); -/// assert!(!tracked_entities.0.contains(&entity)); -/// ``` -#[derive(Debug, Clone, Default)] -pub struct ComponentHooks { - pub(crate) on_add: Option, - pub(crate) on_insert: Option, - pub(crate) on_replace: Option, - pub(crate) on_remove: Option, - pub(crate) on_despawn: Option, -} - -impl ComponentHooks { - pub(crate) fn update_from_component(&mut self) -> &mut Self { - if let Some(hook) = C::on_add() { - self.on_add(hook); - } - if let Some(hook) = C::on_insert() { - self.on_insert(hook); - } - if let Some(hook) = C::on_replace() { - self.on_replace(hook); - } - if let Some(hook) = C::on_remove() { - self.on_remove(hook); - } - if let Some(hook) = C::on_despawn() { - self.on_despawn(hook); - } - - self - } - - /// Register a [`ComponentHook`] that will be run when this component is added to an entity. - /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as - /// adding all of its components. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_add` hook - pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_add(hook) - .expect("Component already has an on_add hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`) - /// or replaced. - /// - /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component). - /// - /// # Warning - /// - /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. - /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_insert` hook - pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_insert(hook) - .expect("Component already has an on_insert hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is about to be dropped, - /// such as being replaced (with `.insert`) or removed. - /// - /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced, - /// allowing access to the previous data just before it is dropped. - /// This hook does *not* run if the entity did not already have this component. - /// - /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity). - /// - /// # Warning - /// - /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. - /// As a result, this is *not* an appropriate mechanism for reliably updating indexes and other caches. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_replace` hook - pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_replace(hook) - .expect("Component already has an on_replace hook") - } - - /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. - /// Despawning an entity counts as removing all of its components. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_remove` hook - pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_remove(hook) - .expect("Component already has an on_remove hook") - } - - /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. - /// - /// # Panics - /// - /// Will panic if the component already has an `on_despawn` hook - pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self { - self.try_on_despawn(hook) - .expect("Component already has an on_despawn hook") - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity. - /// - /// This is a fallible version of [`Self::on_add`]. - /// - /// Returns `None` if the component already has an `on_add` hook. - pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_add.is_some() { - return None; - } - self.on_add = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`) - /// - /// This is a fallible version of [`Self::on_insert`]. - /// - /// Returns `None` if the component already has an `on_insert` hook. - pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_insert.is_some() { - return None; - } - self.on_insert = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed - /// - /// This is a fallible version of [`Self::on_replace`]. - /// - /// Returns `None` if the component already has an `on_replace` hook. - pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_replace.is_some() { - return None; - } - self.on_replace = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. - /// - /// This is a fallible version of [`Self::on_remove`]. - /// - /// Returns `None` if the component already has an `on_remove` hook. - pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_remove.is_some() { - return None; - } - self.on_remove = Some(hook); - Some(self) - } - - /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. - /// - /// This is a fallible version of [`Self::on_despawn`]. - /// - /// Returns `None` if the component already has an `on_despawn` hook. - pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> { - if self.on_despawn.is_some() { - return None; - } - self.on_despawn = Some(hook); - Some(self) - } -} - /// Stores metadata for a type of component or resource stored in a specific [`World`]. #[derive(Debug, Clone)] pub struct ComponentInfo { @@ -913,8 +677,8 @@ impl ComponentInfo { /// Returns the name of the current component. #[inline] - pub fn name(&self) -> &str { - &self.descriptor.name + pub fn name(&self) -> DebugName { + self.descriptor.name.clone() } /// Returns `true` if the current component is mutable. @@ -1071,7 +835,7 @@ impl SparseSetIndex for ComponentId { /// A value describing a component or resource, which may or may not correspond to a Rust type. #[derive(Clone)] pub struct ComponentDescriptor { - name: Cow<'static, str>, + name: DebugName, // SAFETY: This must remain private. It must match the statically known StorageType of the // associated rust component type if one exists. storage_type: StorageType, @@ -1117,7 +881,7 @@ impl ComponentDescriptor { /// Create a new `ComponentDescriptor` for the type `T`. pub fn new() -> Self { Self { - name: Cow::Borrowed(core::any::type_name::()), + name: DebugName::type_name::(), storage_type: T::STORAGE_TYPE, is_send_and_sync: true, type_id: Some(TypeId::of::()), @@ -1142,7 +906,7 @@ impl ComponentDescriptor { clone_behavior: ComponentCloneBehavior, ) -> Self { Self { - name: name.into(), + name: name.into().into(), storage_type, is_send_and_sync: true, type_id: None, @@ -1158,7 +922,7 @@ impl ComponentDescriptor { /// The [`StorageType`] for resources is always [`StorageType::Table`]. pub fn new_resource() -> Self { Self { - name: Cow::Borrowed(core::any::type_name::()), + name: DebugName::type_name::(), // PERF: `SparseStorage` may actually be a more // reasonable choice as `storage_type` for resources. storage_type: StorageType::Table, @@ -1173,7 +937,7 @@ impl ComponentDescriptor { fn new_non_send(storage_type: StorageType) -> Self { Self { - name: Cow::Borrowed(core::any::type_name::()), + name: DebugName::type_name::(), storage_type, is_send_and_sync: false, type_id: Some(TypeId::of::()), @@ -1199,8 +963,8 @@ impl ComponentDescriptor { /// Returns the name of the current component. #[inline] - pub fn name(&self) -> &str { - self.name.as_ref() + pub fn name(&self) -> DebugName { + self.name.clone() } /// Returns whether this component is mutable. @@ -2052,7 +1816,7 @@ impl Components { } /// Gets the metadata associated with the given component, if it is registered. - /// This will return `None` if the id is not regiserted or is queued. + /// This will return `None` if the id is not registered or is queued. /// /// This will return an incorrect result if `id` did not come from the same world as `self`. It may return `None` or a garbage value. #[inline] @@ -2089,13 +1853,10 @@ impl Components { /// /// This will return an incorrect result if `id` did not come from the same world as `self`. It may return `None` or a garbage value. #[inline] - pub fn get_name<'a>(&'a self, id: ComponentId) -> Option> { + pub fn get_name<'a>(&'a self, id: ComponentId) -> Option { self.components .get(id.0) - .and_then(|info| { - info.as_ref() - .map(|info| Cow::Borrowed(info.descriptor.name())) - }) + .and_then(|info| info.as_ref().map(|info| info.descriptor.name())) .or_else(|| { let queued = self.queued.read().unwrap_or_else(PoisonError::into_inner); // first check components, then resources, then dynamic @@ -2616,12 +2377,12 @@ impl Tick { /// /// Returns `true` if wrapping was performed. Otherwise, returns `false`. #[inline] - pub(crate) fn check_tick(&mut self, tick: Tick) -> bool { - let age = tick.relative_to(*self); + pub fn check_tick(&mut self, check: CheckChangeTicks) -> bool { + let age = check.present_tick().relative_to(*self); // This comparison assumes that `age` has not overflowed `u32::MAX` before, which will be true // so long as this check always runs before that can happen. if age.get() > Self::MAX.get() { - *self = tick.relative_to(Self::MAX); + *self = check.present_tick().relative_to(Self::MAX); true } else { false @@ -2629,6 +2390,41 @@ impl Tick { } } +/// An observer [`Event`] that can be used to maintain [`Tick`]s in custom data structures, enabling to make +/// use of bevy's periodic checks that clamps ticks to a certain range, preventing overflows and thus +/// keeping methods like [`Tick::is_newer_than`] reliably return `false` for ticks that got too old. +/// +/// # Example +/// +/// Here a schedule is stored in a custom resource. This way the systems in it would not have their change +/// ticks automatically updated via [`World::check_change_ticks`], possibly causing `Tick`-related bugs on +/// long-running apps. +/// +/// To fix that, add an observer for this event that calls the schedule's +/// [`Schedule::check_change_ticks`](bevy_ecs::schedule::Schedule::check_change_ticks). +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_ecs::component::CheckChangeTicks; +/// +/// #[derive(Resource)] +/// struct CustomSchedule(Schedule); +/// +/// # let mut world = World::new(); +/// world.add_observer(|check: On, mut schedule: ResMut| { +/// schedule.0.check_change_ticks(*check); +/// }); +/// ``` +#[derive(Debug, Clone, Copy, Event)] +pub struct CheckChangeTicks(pub(crate) Tick); + +impl CheckChangeTicks { + /// Get the present `Tick` that other ticks get compared to. + pub fn present_tick(self) -> Tick { + self.0 + } +} + /// Interior-mutable access to the [`Tick`]s for a single component or resource. #[derive(Copy, Clone, Debug)] pub struct TickCells<'a> { @@ -3013,13 +2809,13 @@ pub fn enforce_no_required_components_recursion( "Recursive required components detected: {}\nhelp: {}", recursion_check_stack .iter() - .map(|id| format!("{}", ShortName(&components.get_name(*id).unwrap()))) + .map(|id| format!("{}", components.get_name(*id).unwrap().shortname())) .collect::>() .join(" → "), if direct_recursion { format!( "Remove require({}).", - ShortName(&components.get_name(requiree).unwrap()) + components.get_name(requiree).unwrap().shortname() ) } else { "If this is intentional, consider merging the components.".into() diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index b124055d16..02d2491b7a 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -1,6 +1,7 @@ use alloc::{borrow::ToOwned, boxed::Box, collections::VecDeque, vec::Vec}; use bevy_platform::collections::{HashMap, HashSet}; use bevy_ptr::{Ptr, PtrMut}; +use bevy_utils::prelude::DebugName; use bumpalo::Bump; use core::any::TypeId; @@ -171,7 +172,8 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// - `ComponentId` of component being written does not match expected `ComponentId`. pub fn write_target_component(&mut self, mut component: C) { C::map_entities(&mut component, &mut self.mapper); - let short_name = disqualified::ShortName::of::(); + let debug_name = DebugName::type_name::(); + let short_name = debug_name.shortname(); if self.target_component_written { panic!("Trying to write component '{short_name}' multiple times") } diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 0e22fddbc8..02d9698917 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -76,7 +76,7 @@ pub use unique_vec::{UniqueEntityEquivalentVec, UniqueEntityVec}; use crate::{ archetype::{ArchetypeId, ArchetypeRow}, change_detection::MaybeLocation, - component::Tick, + component::{CheckChangeTicks, Tick}, storage::{SparseSetIndex, TableId, TableRow}, }; use alloc::vec::Vec; @@ -1216,9 +1216,9 @@ impl Entities { } #[inline] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for meta in &mut self.meta { - meta.spawned_or_despawned.at.check_tick(change_tick); + meta.spawned_or_despawned.at.check_tick(check); } } diff --git a/crates/bevy_ecs/src/error/command_handling.rs b/crates/bevy_ecs/src/error/command_handling.rs index bf2741d376..c303b76d17 100644 --- a/crates/bevy_ecs/src/error/command_handling.rs +++ b/crates/bevy_ecs/src/error/command_handling.rs @@ -1,4 +1,6 @@ -use core::{any::type_name, fmt}; +use core::fmt; + +use bevy_utils::prelude::DebugName; use crate::{ entity::Entity, @@ -31,7 +33,7 @@ where Err(err) => (error_handler)( err.into(), ErrorContext::Command { - name: type_name::().into(), + name: DebugName::type_name::(), }, ), } @@ -43,7 +45,7 @@ where Err(err) => world.default_error_handler()( err.into(), ErrorContext::Command { - name: type_name::().into(), + name: DebugName::type_name::(), }, ), } diff --git a/crates/bevy_ecs/src/error/handler.rs b/crates/bevy_ecs/src/error/handler.rs index c89408b250..85a5a13297 100644 --- a/crates/bevy_ecs/src/error/handler.rs +++ b/crates/bevy_ecs/src/error/handler.rs @@ -1,7 +1,7 @@ use core::fmt::Display; use crate::{component::Tick, error::BevyError, prelude::Resource}; -use alloc::borrow::Cow; +use bevy_utils::prelude::DebugName; use derive_more::derive::{Deref, DerefMut}; /// Context for a [`BevyError`] to aid in debugging. @@ -10,26 +10,26 @@ pub enum ErrorContext { /// The error occurred in a system. System { /// The name of the system that failed. - name: Cow<'static, str>, + name: DebugName, /// The last tick that the system was run. last_run: Tick, }, /// The error occurred in a run condition. RunCondition { /// The name of the run condition that failed. - name: Cow<'static, str>, + name: DebugName, /// The last tick that the run condition was evaluated. last_run: Tick, }, /// The error occurred in a command. Command { /// The name of the command that failed. - name: Cow<'static, str>, + name: DebugName, }, /// The error occurred in an observer. Observer { /// The name of the observer that failed. - name: Cow<'static, str>, + name: DebugName, /// The last tick that the observer was run. last_run: Tick, }, @@ -54,12 +54,12 @@ impl Display for ErrorContext { impl ErrorContext { /// The name of the ECS construct that failed. - pub fn name(&self) -> &str { + pub fn name(&self) -> DebugName { match self { Self::System { name, .. } | Self::Command { name, .. } | Self::Observer { name, .. } - | Self::RunCondition { name, .. } => name, + | Self::RunCondition { name, .. } => name.clone(), } } diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index d525ba2e57..52839f369d 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -11,33 +11,81 @@ use core::{ marker::PhantomData, }; -/// Something that "happens" and might be read / observed by app logic. +/// Something that "happens" and can be processed by app logic. /// -/// Events can be stored in an [`Events`] resource -/// You can conveniently access events using the [`EventReader`] and [`EventWriter`] system parameter. +/// Events can be triggered on a [`World`] using a method like [`trigger`](World::trigger), +/// causing any global [`Observer`] watching that event to run. This allows for push-based +/// event handling where observers are immediately notified of events as they happen. /// -/// Events can also be "triggered" on a [`World`], which will then cause any [`Observer`] of that trigger to run. +/// Additional event handling behavior can be enabled by implementing the [`EntityEvent`] +/// and [`BufferedEvent`] traits: +/// +/// - [`EntityEvent`]s support targeting specific entities, triggering any observers watching those targets. +/// They are useful for entity-specific event handlers and can even be propagated from one entity to another. +/// - [`BufferedEvent`]s support a pull-based event handling system where events are written using an [`EventWriter`] +/// and read later using an [`EventReader`]. This is an alternative to observers that allows efficient batch processing +/// of events at fixed points in a schedule. /// /// Events must be thread-safe. /// -/// ## Derive -/// This trait can be derived. -/// Adding `auto_propagate` sets [`Self::AUTO_PROPAGATE`] to true. -/// Adding `traversal = "X"` sets [`Self::Traversal`] to be of type "X". +/// # Usage +/// +/// The [`Event`] trait can be derived: /// /// ``` -/// use bevy_ecs::prelude::*; -/// +/// # use bevy_ecs::prelude::*; +/// # /// #[derive(Event)] -/// #[event(auto_propagate)] -/// struct MyEvent; +/// struct Speak { +/// message: String, +/// } /// ``` /// +/// An [`Observer`] can then be added to listen for this event type: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// world.add_observer(|trigger: On| { +/// println!("{}", trigger.message); +/// }); +/// ``` +/// +/// The event can be triggered on the [`World`] using the [`trigger`](World::trigger) method: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event)] +/// # struct Speak { +/// # message: String, +/// # } +/// # +/// # let mut world = World::new(); +/// # +/// # world.add_observer(|trigger: On| { +/// # println!("{}", trigger.message); +/// # }); +/// # +/// # world.flush(); +/// # +/// world.trigger(Speak { +/// message: "Hello!".to_string(), +/// }); +/// ``` +/// +/// For events that additionally need entity targeting or buffering, consider also deriving +/// [`EntityEvent`] or [`BufferedEvent`], respectively. /// /// [`World`]: crate::world::World -/// [`ComponentId`]: crate::component::ComponentId /// [`Observer`]: crate::observer::Observer -/// [`Events`]: super::Events /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[diagnostic::on_unimplemented( @@ -46,18 +94,6 @@ use core::{ note = "consider annotating `{Self}` with `#[derive(Event)]`" )] pub trait Event: Send + Sync + 'static { - /// The component that describes which Entity to propagate this event to next, when [propagation] is enabled. - /// - /// [propagation]: crate::observer::Trigger::propagate - type Traversal: Traversal; - - /// When true, this event will always attempt to propagate when [triggered], without requiring a call - /// to [`Trigger::propagate`]. - /// - /// [triggered]: crate::system::Commands::trigger_targets - /// [`Trigger::propagate`]: crate::observer::Trigger::propagate - const AUTO_PROPAGATE: bool = false; - /// Generates the [`ComponentId`] for this event type. /// /// If this type has already been registered, @@ -68,7 +104,7 @@ pub trait Event: Send + Sync + 'static { /// /// # Warning /// - /// This method should not be overridden by implementors, + /// This method should not be overridden by implementers, /// and should always correspond to the implementation of [`component_id`](Event::component_id). fn register_component_id(world: &mut World) -> ComponentId { world.register_component::>() @@ -82,13 +118,216 @@ pub trait Event: Send + Sync + 'static { /// /// # Warning /// - /// This method should not be overridden by implementors, + /// This method should not be overridden by implementers, /// and should always correspond to the implementation of [`register_component_id`](Event::register_component_id). fn component_id(world: &World) -> Option { world.component_id::>() } } +/// An [`Event`] that can be targeted at specific entities. +/// +/// Entity events can be triggered on a [`World`] with specific entity targets using a method +/// like [`trigger_targets`](World::trigger_targets), causing any [`Observer`] watching the event +/// for those entities to run. +/// +/// Unlike basic [`Event`]s, entity events can optionally be propagated from one entity target to another +/// based on the [`EntityEvent::Traversal`] type associated with the event. This enables use cases +/// such as bubbling events to parent entities for UI purposes. +/// +/// Entity events must be thread-safe. +/// +/// # Usage +/// +/// The [`EntityEvent`] trait can be derived. The `event` attribute can be used to further configure +/// the propagation behavior: adding `auto_propagate` sets [`EntityEvent::AUTO_PROPAGATE`] to `true`, +/// while adding `traversal = X` sets [`EntityEvent::Traversal`] to be of type `X`. +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// // When the `Damage` event is triggered on an entity, bubble the event up to ancestors. +/// #[derive(Event, EntityEvent)] +/// #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// struct Damage { +/// amount: f32, +/// } +/// ``` +/// +/// An [`Observer`] can then be added to listen for this event type for the desired entity: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, EntityEvent)] +/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # struct Damage { +/// # amount: f32, +/// # } +/// # +/// # #[derive(Component)] +/// # struct Health(f32); +/// # +/// # #[derive(Component)] +/// # struct Enemy; +/// # +/// # #[derive(Component)] +/// # struct ArmorPiece; +/// # +/// # let mut world = World::new(); +/// # +/// // Spawn an enemy entity. +/// let enemy = world.spawn((Enemy, Health(100.0))).id(); +/// +/// // Spawn some armor as a child of the enemy entity. +/// // When the armor takes damage, it will bubble the event up to the enemy, +/// // which can then handle the event with its own observer. +/// let armor_piece = world +/// .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) +/// .observe(|trigger: On, mut query: Query<&mut Health>| { +/// // Note: `On::target` only exists because this is an `EntityEvent`. +/// let mut health = query.get_mut(trigger.target()).unwrap(); +/// health.0 -= trigger.amount; +/// }) +/// .id(); +/// ``` +/// +/// The event can be triggered on the [`World`] using the [`trigger_targets`](World::trigger_targets) method, +/// providing the desired entity target(s): +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, EntityEvent)] +/// # #[entity_event(traversal = &'static ChildOf, auto_propagate)] +/// # struct Damage { +/// # amount: f32, +/// # } +/// # +/// # #[derive(Component)] +/// # struct Health(f32); +/// # +/// # #[derive(Component)] +/// # struct Enemy; +/// # +/// # #[derive(Component)] +/// # struct ArmorPiece; +/// # +/// # let mut world = World::new(); +/// # +/// # let enemy = world.spawn((Enemy, Health(100.0))).id(); +/// # let armor_piece = world +/// # .spawn((ArmorPiece, Health(25.0), ChildOf(enemy))) +/// # .observe(|trigger: On, mut query: Query<&mut Health>| { +/// # // Note: `On::target` only exists because this is an `EntityEvent`. +/// # let mut health = query.get_mut(trigger.target()).unwrap(); +/// # health.0 -= trigger.amount; +/// # }) +/// # .id(); +/// # +/// # world.flush(); +/// # +/// world.trigger_targets(Damage { amount: 10.0 }, armor_piece); +/// ``` +/// +/// [`World`]: crate::world::World +/// [`TriggerTargets`]: crate::observer::TriggerTargets +/// [`Observer`]: crate::observer::Observer +/// [`Events`]: super::Events +/// [`EventReader`]: super::EventReader +/// [`EventWriter`]: super::EventWriter +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `EntityEvent`", + label = "invalid `EntityEvent`", + note = "consider annotating `{Self}` with `#[derive(Event, EntityEvent)]`" +)] +pub trait EntityEvent: Event { + /// The component that describes which [`Entity`] to propagate this event to next, when [propagation] is enabled. + /// + /// [`Entity`]: crate::entity::Entity + /// [propagation]: crate::observer::On::propagate + type Traversal: Traversal; + + /// When true, this event will always attempt to propagate when [triggered], without requiring a call + /// to [`On::propagate`]. + /// + /// [triggered]: crate::system::Commands::trigger_targets + /// [`On::propagate`]: crate::observer::On::propagate + const AUTO_PROPAGATE: bool = false; +} + +/// A buffered [`Event`] for pull-based event handling. +/// +/// Buffered events can be written with [`EventWriter`] and read using the [`EventReader`] system parameter. +/// These events are stored in the [`Events`] resource, and require periodically polling the world for new events, +/// typically in a system that runs as part of a schedule. +/// +/// While the polling imposes a small overhead, buffered events are useful for efficiently batch processing +/// a large number of events at once. This can make them more efficient than [`Event`]s used by [`Observer`]s +/// for events that happen at a high frequency or in large quantities. +/// +/// Unlike [`Event`]s triggered for observers, buffered events are evaluated at fixed points in the schedule +/// rather than immediately when they are sent. This allows for more predictable scheduling and deferring +/// event processing to a later point in time. +/// +/// Buffered events do *not* trigger observers automatically when they are written via an [`EventWriter`]. +/// However, they can still also be triggered on a [`World`] in case you want both buffered and immediate +/// event handling for the same event. +/// +/// Buffered events must be thread-safe. +/// +/// # Usage +/// +/// The [`BufferedEvent`] trait can be derived: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// #[derive(Event, BufferedEvent)] +/// struct Message(String); +/// ``` +/// +/// The event can then be written to the event buffer using an [`EventWriter`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, BufferedEvent)] +/// # struct Message(String); +/// # +/// fn write_hello(mut writer: EventWriter) { +/// writer.write(Message("Hello!".to_string())); +/// } +/// ``` +/// +/// Buffered events can be efficiently read using an [`EventReader`]: +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # +/// # #[derive(Event, BufferedEvent)] +/// # struct Message(String); +/// # +/// fn read_messages(mut reader: EventReader) { +/// // Process all buffered events of type `Message`. +/// for Message(message) in reader.read() { +/// println!("{message}"); +/// } +/// } +/// ``` +/// +/// [`World`]: crate::world::World +/// [`Observer`]: crate::observer::Observer +/// [`Events`]: super::Events +/// [`EventReader`]: super::EventReader +/// [`EventWriter`]: super::EventWriter +#[diagnostic::on_unimplemented( + message = "`{Self}` is not an `BufferedEvent`", + label = "invalid `BufferedEvent`", + note = "consider annotating `{Self}` with `#[derive(Event, BufferedEvent)]`" +)] +pub trait BufferedEvent: Event {} + /// An internal type that implements [`Component`] for a given [`Event`] type. /// /// This exists so we can easily get access to a unique [`ComponentId`] for each [`Event`] type, @@ -115,7 +354,7 @@ struct EventWrapperComponent(PhantomData); derive(Reflect), reflect(Clone, Debug, PartialEq, Hash) )] -pub struct EventId { +pub struct EventId { /// Uniquely identifies the event associated with this ID. // This value corresponds to the order in which each event was added to the world. pub id: usize, @@ -125,21 +364,21 @@ pub struct EventId { pub(super) _marker: PhantomData, } -impl Copy for EventId {} +impl Copy for EventId {} -impl Clone for EventId { +impl Clone for EventId { fn clone(&self) -> Self { *self } } -impl fmt::Display for EventId { +impl fmt::Display for EventId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { ::fmt(self, f) } } -impl fmt::Debug for EventId { +impl fmt::Debug for EventId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( f, @@ -150,27 +389,27 @@ impl fmt::Debug for EventId { } } -impl PartialEq for EventId { +impl PartialEq for EventId { fn eq(&self, other: &Self) -> bool { self.id == other.id } } -impl Eq for EventId {} +impl Eq for EventId {} -impl PartialOrd for EventId { +impl PartialOrd for EventId { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } -impl Ord for EventId { +impl Ord for EventId { fn cmp(&self, other: &Self) -> Ordering { self.id.cmp(&other.id) } } -impl Hash for EventId { +impl Hash for EventId { fn hash(&self, state: &mut H) { Hash::hash(&self.id, state); } @@ -178,7 +417,7 @@ impl Hash for EventId { #[derive(Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub(crate) struct EventInstance { +pub(crate) struct EventInstance { pub event_id: EventId, pub event: E, } diff --git a/crates/bevy_ecs/src/event/collections.rs b/crates/bevy_ecs/src/event/collections.rs index 66447b7de4..7d1854149e 100644 --- a/crates/bevy_ecs/src/event/collections.rs +++ b/crates/bevy_ecs/src/event/collections.rs @@ -1,7 +1,7 @@ use alloc::vec::Vec; use bevy_ecs::{ change_detection::MaybeLocation, - event::{Event, EventCursor, EventId, EventInstance}, + event::{BufferedEvent, EventCursor, EventId, EventInstance}, resource::Resource, }; use core::{ @@ -38,10 +38,11 @@ use { /// dropped silently. /// /// # Example -/// ``` -/// use bevy_ecs::event::{Event, Events}; /// -/// #[derive(Event)] +/// ``` +/// use bevy_ecs::event::{BufferedEvent, Event, Events}; +/// +/// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize /// } @@ -91,7 +92,7 @@ use { /// [`event_update_system`]: super::event_update_system #[derive(Debug, Resource)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource, Default))] -pub struct Events { +pub struct Events { /// Holds the oldest still active events. /// Note that `a.start_event_count + a.len()` should always be equal to `events_b.start_event_count`. pub(crate) events_a: EventSequence, @@ -101,7 +102,7 @@ pub struct Events { } // Derived Default impl would incorrectly require E: Default -impl Default for Events { +impl Default for Events { fn default() -> Self { Self { events_a: Default::default(), @@ -111,7 +112,7 @@ impl Default for Events { } } -impl Events { +impl Events { /// Returns the index of the oldest event stored in the event buffer. pub fn oldest_event_count(&self) -> usize { self.events_a.start_event_count @@ -286,7 +287,7 @@ impl Events { } } -impl Extend for Events { +impl Extend for Events { #[track_caller] fn extend(&mut self, iter: I) where @@ -321,13 +322,13 @@ impl Extend for Events { #[derive(Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default))] -pub(crate) struct EventSequence { +pub(crate) struct EventSequence { pub(crate) events: Vec>, pub(crate) start_event_count: usize, } // Derived Default impl would incorrectly require E: Default -impl Default for EventSequence { +impl Default for EventSequence { fn default() -> Self { Self { events: Default::default(), @@ -336,7 +337,7 @@ impl Default for EventSequence { } } -impl Deref for EventSequence { +impl Deref for EventSequence { type Target = Vec>; fn deref(&self) -> &Self::Target { @@ -344,7 +345,7 @@ impl Deref for EventSequence { } } -impl DerefMut for EventSequence { +impl DerefMut for EventSequence { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.events } @@ -357,7 +358,7 @@ pub struct SendBatchIds { _marker: PhantomData, } -impl Iterator for SendBatchIds { +impl Iterator for SendBatchIds { type Item = EventId; fn next(&mut self) -> Option { @@ -377,7 +378,7 @@ impl Iterator for SendBatchIds { } } -impl ExactSizeIterator for SendBatchIds { +impl ExactSizeIterator for SendBatchIds { fn len(&self) -> usize { self.event_count.saturating_sub(self.last_count) } @@ -385,12 +386,11 @@ impl ExactSizeIterator for SendBatchIds { #[cfg(test)] mod tests { - use crate::event::Events; - use bevy_ecs_macros::Event; + use crate::event::{BufferedEvent, Event, Events}; #[test] fn iter_current_update_events_iterates_over_current_events() { - #[derive(Event, Clone)] + #[derive(Event, BufferedEvent, Clone)] struct TestEvent; let mut test_events = Events::::default(); diff --git a/crates/bevy_ecs/src/event/event_cursor.rs b/crates/bevy_ecs/src/event/event_cursor.rs index ff15ef4931..70e19a732c 100644 --- a/crates/bevy_ecs/src/event/event_cursor.rs +++ b/crates/bevy_ecs/src/event/event_cursor.rs @@ -1,5 +1,6 @@ use bevy_ecs::event::{ - Event, EventIterator, EventIteratorWithId, EventMutIterator, EventMutIteratorWithId, Events, + BufferedEvent, EventIterator, EventIteratorWithId, EventMutIterator, EventMutIteratorWithId, + Events, }; #[cfg(feature = "multi_threaded")] use bevy_ecs::event::{EventMutParIter, EventParIter}; @@ -19,9 +20,9 @@ use core::marker::PhantomData; /// /// ``` /// use bevy_ecs::prelude::*; -/// use bevy_ecs::event::{Event, Events, EventCursor}; +/// use bevy_ecs::event::{BufferedEvent, Events, EventCursor}; /// -/// #[derive(Event, Clone, Debug)] +/// #[derive(Event, BufferedEvent, Clone, Debug)] /// struct MyEvent; /// /// /// A system that both sends and receives events using a [`Local`] [`EventCursor`]. @@ -50,12 +51,12 @@ use core::marker::PhantomData; /// [`EventReader`]: super::EventReader /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventCursor { +pub struct EventCursor { pub(super) last_event_count: usize, pub(super) _marker: PhantomData, } -impl Default for EventCursor { +impl Default for EventCursor { fn default() -> Self { EventCursor { last_event_count: 0, @@ -64,7 +65,7 @@ impl Default for EventCursor { } } -impl Clone for EventCursor { +impl Clone for EventCursor { fn clone(&self) -> Self { EventCursor { last_event_count: self.last_event_count, @@ -73,7 +74,7 @@ impl Clone for EventCursor { } } -impl EventCursor { +impl EventCursor { /// See [`EventReader::read`](super::EventReader::read) pub fn read<'a>(&'a mut self, events: &'a Events) -> EventIterator<'a, E> { self.read_with_id(events).without_id() diff --git a/crates/bevy_ecs/src/event/iterators.rs b/crates/bevy_ecs/src/event/iterators.rs index f9ee74b8b0..c90aed2a19 100644 --- a/crates/bevy_ecs/src/event/iterators.rs +++ b/crates/bevy_ecs/src/event/iterators.rs @@ -1,15 +1,15 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::batching::BatchingStrategy; -use bevy_ecs::event::{Event, EventCursor, EventId, EventInstance, Events}; +use bevy_ecs::event::{BufferedEvent, EventCursor, EventId, EventInstance, Events}; use core::{iter::Chain, slice::Iter}; /// An iterator that yields any unread events from an [`EventReader`](super::EventReader) or [`EventCursor`]. #[derive(Debug)] -pub struct EventIterator<'a, E: Event> { +pub struct EventIterator<'a, E: BufferedEvent> { iter: EventIteratorWithId<'a, E>, } -impl<'a, E: Event> Iterator for EventIterator<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventIterator<'a, E> { type Item = &'a E; fn next(&mut self) -> Option { self.iter.next().map(|(event, _)| event) @@ -35,7 +35,7 @@ impl<'a, E: Event> Iterator for EventIterator<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventIterator<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventIterator<'a, E> { fn len(&self) -> usize { self.iter.len() } @@ -43,13 +43,13 @@ impl<'a, E: Event> ExactSizeIterator for EventIterator<'a, E> { /// An iterator that yields any unread events (and their IDs) from an [`EventReader`](super::EventReader) or [`EventCursor`]. #[derive(Debug)] -pub struct EventIteratorWithId<'a, E: Event> { +pub struct EventIteratorWithId<'a, E: BufferedEvent> { reader: &'a mut EventCursor, chain: Chain>, Iter<'a, EventInstance>>, unread: usize, } -impl<'a, E: Event> EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> EventIteratorWithId<'a, E> { /// Creates a new iterator that yields any `events` that have not yet been seen by `reader`. pub fn new(reader: &'a mut EventCursor, events: &'a Events) -> Self { let a_index = reader @@ -81,7 +81,7 @@ impl<'a, E: Event> EventIteratorWithId<'a, E> { } } -impl<'a, E: Event> Iterator for EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventIteratorWithId<'a, E> { type Item = (&'a E, EventId); fn next(&mut self) -> Option { match self @@ -131,16 +131,16 @@ impl<'a, E: Event> Iterator for EventIteratorWithId<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventIteratorWithId<'a, E> { fn len(&self) -> usize { self.unread } } -/// A parallel iterator over `Event`s. +/// A parallel iterator over `BufferedEvent`s. #[cfg(feature = "multi_threaded")] #[derive(Debug)] -pub struct EventParIter<'a, E: Event> { +pub struct EventParIter<'a, E: BufferedEvent> { reader: &'a mut EventCursor, slices: [&'a [EventInstance]; 2], batching_strategy: BatchingStrategy, @@ -149,7 +149,7 @@ pub struct EventParIter<'a, E: Event> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> EventParIter<'a, E> { +impl<'a, E: BufferedEvent> EventParIter<'a, E> { /// Creates a new parallel iterator over `events` that have not yet been seen by `reader`. pub fn new(reader: &'a mut EventCursor, events: &'a Events) -> Self { let a_index = reader @@ -248,7 +248,7 @@ impl<'a, E: Event> EventParIter<'a, E> { } } - /// Returns the number of [`Event`]s to be iterated. + /// Returns the number of [`BufferedEvent`]s to be iterated. pub fn len(&self) -> usize { self.slices.iter().map(|s| s.len()).sum() } @@ -260,7 +260,7 @@ impl<'a, E: Event> EventParIter<'a, E> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> IntoIterator for EventParIter<'a, E> { +impl<'a, E: BufferedEvent> IntoIterator for EventParIter<'a, E> { type IntoIter = EventIteratorWithId<'a, E>; type Item = ::Item; diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 3bb422b7bb..fd624d1abf 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -11,8 +11,8 @@ mod update; mod writer; pub(crate) use base::EventInstance; -pub use base::{Event, EventId}; -pub use bevy_ecs_macros::Event; +pub use base::{BufferedEvent, EntityEvent, Event, EventId}; +pub use bevy_ecs_macros::{BufferedEvent, EntityEvent, Event}; pub use collections::{Events, SendBatchIds}; pub use event_cursor::EventCursor; #[cfg(feature = "multi_threaded")] @@ -38,17 +38,20 @@ pub use writer::EventWriter; mod tests { use alloc::{vec, vec::Vec}; use bevy_ecs::{event::*, system::assert_is_read_only_system}; - use bevy_ecs_macros::Event; + use bevy_ecs_macros::BufferedEvent; - #[derive(Event, Copy, Clone, PartialEq, Eq, Debug)] + #[derive(Event, BufferedEvent, Copy, Clone, PartialEq, Eq, Debug)] struct TestEvent { i: usize, } - #[derive(Event, Clone, PartialEq, Debug, Default)] + #[derive(Event, BufferedEvent, Clone, PartialEq, Debug, Default)] struct EmptyTestEvent; - fn get_events(events: &Events, cursor: &mut EventCursor) -> Vec { + fn get_events( + events: &Events, + cursor: &mut EventCursor, + ) -> Vec { cursor.read(events).cloned().collect::>() } diff --git a/crates/bevy_ecs/src/event/mut_iterators.rs b/crates/bevy_ecs/src/event/mut_iterators.rs index 3cb531ce78..3fa8378f23 100644 --- a/crates/bevy_ecs/src/event/mut_iterators.rs +++ b/crates/bevy_ecs/src/event/mut_iterators.rs @@ -1,17 +1,17 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::batching::BatchingStrategy; -use bevy_ecs::event::{Event, EventCursor, EventId, EventInstance, Events}; +use bevy_ecs::event::{BufferedEvent, EventCursor, EventId, EventInstance, Events}; use core::{iter::Chain, slice::IterMut}; /// An iterator that yields any unread events from an [`EventMutator`] or [`EventCursor`]. /// /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventMutIterator<'a, E: Event> { +pub struct EventMutIterator<'a, E: BufferedEvent> { iter: EventMutIteratorWithId<'a, E>, } -impl<'a, E: Event> Iterator for EventMutIterator<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventMutIterator<'a, E> { type Item = &'a mut E; fn next(&mut self) -> Option { self.iter.next().map(|(event, _)| event) @@ -37,7 +37,7 @@ impl<'a, E: Event> Iterator for EventMutIterator<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventMutIterator<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventMutIterator<'a, E> { fn len(&self) -> usize { self.iter.len() } @@ -47,13 +47,13 @@ impl<'a, E: Event> ExactSizeIterator for EventMutIterator<'a, E> { /// /// [`EventMutator`]: super::EventMutator #[derive(Debug)] -pub struct EventMutIteratorWithId<'a, E: Event> { +pub struct EventMutIteratorWithId<'a, E: BufferedEvent> { mutator: &'a mut EventCursor, chain: Chain>, IterMut<'a, EventInstance>>, unread: usize, } -impl<'a, E: Event> EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> EventMutIteratorWithId<'a, E> { /// Creates a new iterator that yields any `events` that have not yet been seen by `mutator`. pub fn new(mutator: &'a mut EventCursor, events: &'a mut Events) -> Self { let a_index = mutator @@ -84,7 +84,7 @@ impl<'a, E: Event> EventMutIteratorWithId<'a, E> { } } -impl<'a, E: Event> Iterator for EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> Iterator for EventMutIteratorWithId<'a, E> { type Item = (&'a mut E, EventId); fn next(&mut self) -> Option { match self @@ -134,16 +134,16 @@ impl<'a, E: Event> Iterator for EventMutIteratorWithId<'a, E> { } } -impl<'a, E: Event> ExactSizeIterator for EventMutIteratorWithId<'a, E> { +impl<'a, E: BufferedEvent> ExactSizeIterator for EventMutIteratorWithId<'a, E> { fn len(&self) -> usize { self.unread } } -/// A parallel iterator over `Event`s. +/// A parallel iterator over `BufferedEvent`s. #[derive(Debug)] #[cfg(feature = "multi_threaded")] -pub struct EventMutParIter<'a, E: Event> { +pub struct EventMutParIter<'a, E: BufferedEvent> { mutator: &'a mut EventCursor, slices: [&'a mut [EventInstance]; 2], batching_strategy: BatchingStrategy, @@ -152,7 +152,7 @@ pub struct EventMutParIter<'a, E: Event> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> EventMutParIter<'a, E> { +impl<'a, E: BufferedEvent> EventMutParIter<'a, E> { /// Creates a new parallel iterator over `events` that have not yet been seen by `mutator`. pub fn new(mutator: &'a mut EventCursor, events: &'a mut Events) -> Self { let a_index = mutator @@ -251,7 +251,7 @@ impl<'a, E: Event> EventMutParIter<'a, E> { } } - /// Returns the number of [`Event`]s to be iterated. + /// Returns the number of [`BufferedEvent`]s to be iterated. pub fn len(&self) -> usize { self.slices.iter().map(|s| s.len()).sum() } @@ -263,7 +263,7 @@ impl<'a, E: Event> EventMutParIter<'a, E> { } #[cfg(feature = "multi_threaded")] -impl<'a, E: Event> IntoIterator for EventMutParIter<'a, E> { +impl<'a, E: BufferedEvent> IntoIterator for EventMutParIter<'a, E> { type IntoIter = EventMutIteratorWithId<'a, E>; type Item = ::Item; diff --git a/crates/bevy_ecs/src/event/mutator.rs b/crates/bevy_ecs/src/event/mutator.rs index e95037af5b..a9c9459119 100644 --- a/crates/bevy_ecs/src/event/mutator.rs +++ b/crates/bevy_ecs/src/event/mutator.rs @@ -1,7 +1,7 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::event::EventMutParIter; use bevy_ecs::{ - event::{Event, EventCursor, EventMutIterator, EventMutIteratorWithId, Events}, + event::{BufferedEvent, EventCursor, EventMutIterator, EventMutIteratorWithId, Events}, system::{Local, ResMut, SystemParam}, }; @@ -15,7 +15,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event, Debug)] +/// #[derive(Event, BufferedEvent, Debug)] /// pub struct MyEvent(pub u32); // Custom event type. /// fn my_system(mut reader: EventMutator) { /// for event in reader.read() { @@ -42,13 +42,13 @@ use bevy_ecs::{ /// [`EventReader`]: super::EventReader /// [`EventWriter`]: super::EventWriter #[derive(SystemParam, Debug)] -pub struct EventMutator<'w, 's, E: Event> { +pub struct EventMutator<'w, 's, E: BufferedEvent> { pub(super) reader: Local<'s, EventCursor>, - #[system_param(validation_message = "Event not initialized")] + #[system_param(validation_message = "BufferedEvent not initialized")] events: ResMut<'w, Events>, } -impl<'w, 's, E: Event> EventMutator<'w, 's, E> { +impl<'w, 's, E: BufferedEvent> EventMutator<'w, 's, E> { /// Iterates over the events this [`EventMutator`] has not seen yet. This updates the /// [`EventMutator`]'s event counter, which means subsequent event reads will not include events /// that happened before now. @@ -69,7 +69,7 @@ impl<'w, 's, E: Event> EventMutator<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -116,7 +116,7 @@ impl<'w, 's, E: Event> EventMutator<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct CollisionEvent; /// /// fn play_collision_sound(mut events: EventMutator) { diff --git a/crates/bevy_ecs/src/event/reader.rs b/crates/bevy_ecs/src/event/reader.rs index 995e2ca9e9..e15b3ea9e7 100644 --- a/crates/bevy_ecs/src/event/reader.rs +++ b/crates/bevy_ecs/src/event/reader.rs @@ -1,11 +1,11 @@ #[cfg(feature = "multi_threaded")] use bevy_ecs::event::EventParIter; use bevy_ecs::{ - event::{Event, EventCursor, EventIterator, EventIteratorWithId, Events}, + event::{BufferedEvent, EventCursor, EventIterator, EventIteratorWithId, Events}, system::{Local, Res, SystemParam}, }; -/// Reads events of type `T` in order and tracks which events have already been read. +/// Reads [`BufferedEvent`]s of type `T` in order and tracks which events have already been read. /// /// # Concurrency /// @@ -14,13 +14,13 @@ use bevy_ecs::{ /// /// [`EventWriter`]: super::EventWriter #[derive(SystemParam, Debug)] -pub struct EventReader<'w, 's, E: Event> { +pub struct EventReader<'w, 's, E: BufferedEvent> { pub(super) reader: Local<'s, EventCursor>, - #[system_param(validation_message = "Event not initialized")] + #[system_param(validation_message = "BufferedEvent not initialized")] events: Res<'w, Events>, } -impl<'w, 's, E: Event> EventReader<'w, 's, E> { +impl<'w, 's, E: BufferedEvent> EventReader<'w, 's, E> { /// Iterates over the events this [`EventReader`] has not seen yet. This updates the /// [`EventReader`]'s event counter, which means subsequent event reads will not include events /// that happened before now. @@ -41,7 +41,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -88,7 +88,7 @@ impl<'w, 's, E: Event> EventReader<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct CollisionEvent; /// /// fn play_collision_sound(mut events: EventReader) { diff --git a/crates/bevy_ecs/src/event/registry.rs b/crates/bevy_ecs/src/event/registry.rs index 0beb41cd25..7889de62da 100644 --- a/crates/bevy_ecs/src/event/registry.rs +++ b/crates/bevy_ecs/src/event/registry.rs @@ -2,7 +2,7 @@ use alloc::vec::Vec; use bevy_ecs::{ change_detection::{DetectChangesMut, MutUntyped}, component::{ComponentId, Tick}, - event::{Event, Events}, + event::{BufferedEvent, Events}, resource::Resource, world::World, }; @@ -45,7 +45,7 @@ impl EventRegistry { /// /// If no instance of the [`EventRegistry`] exists in the world, this will add one - otherwise it will use /// the existing instance. - pub fn register_event(world: &mut World) { + pub fn register_event(world: &mut World) { // By initializing the resource here, we can be sure that it is present, // and receive the correct, up-to-date `ComponentId` even if it was previously removed. let component_id = world.init_resource::>(); @@ -82,7 +82,7 @@ impl EventRegistry { } /// Removes an event from the world and its associated [`EventRegistry`]. - pub fn deregister_events(world: &mut World) { + pub fn deregister_events(world: &mut World) { let component_id = world.init_resource::>(); let mut registry = world.get_resource_or_init::(); registry diff --git a/crates/bevy_ecs/src/event/writer.rs b/crates/bevy_ecs/src/event/writer.rs index 5854ab34fb..4c38401eb4 100644 --- a/crates/bevy_ecs/src/event/writer.rs +++ b/crates/bevy_ecs/src/event/writer.rs @@ -1,9 +1,9 @@ use bevy_ecs::{ - event::{Event, EventId, Events, SendBatchIds}, + event::{BufferedEvent, EventId, Events, SendBatchIds}, system::{ResMut, SystemParam}, }; -/// Sends events of type `T`. +/// Sends [`BufferedEvent`]s of type `T`. /// /// # Usage /// @@ -11,7 +11,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event)] +/// #[derive(Event, BufferedEvent)] /// pub struct MyEvent; // Custom event type. /// fn my_system(mut writer: EventWriter) { /// writer.write(MyEvent); @@ -21,8 +21,8 @@ use bevy_ecs::{ /// ``` /// # Observers /// -/// "Buffered" Events, such as those sent directly in [`Events`] or written using [`EventWriter`], do _not_ automatically -/// trigger any [`Observer`]s watching for that event, as each [`Event`] has different requirements regarding _if_ it will +/// "Buffered" events, such as those sent directly in [`Events`] or written using [`EventWriter`], do _not_ automatically +/// trigger any [`Observer`]s watching for that event, as each [`BufferedEvent`] has different requirements regarding _if_ it will /// be triggered, and if so, _when_ it will be triggered in the schedule. /// /// # Concurrency @@ -38,7 +38,7 @@ use bevy_ecs::{ /// /// ``` /// # use bevy_ecs::{prelude::*, event::Events}; -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # pub struct MyEvent; /// fn send_untyped(mut commands: Commands) { /// // Send an event of a specific type without having to declare that @@ -59,12 +59,12 @@ use bevy_ecs::{ /// /// [`Observer`]: crate::observer::Observer #[derive(SystemParam)] -pub struct EventWriter<'w, E: Event> { - #[system_param(validation_message = "Event not initialized")] +pub struct EventWriter<'w, E: BufferedEvent> { + #[system_param(validation_message = "BufferedEvent not initialized")] events: ResMut<'w, Events>, } -impl<'w, E: Event> EventWriter<'w, E> { +impl<'w, E: BufferedEvent> EventWriter<'w, E> { /// Writes an `event`, which can later be read by [`EventReader`](super::EventReader)s. /// This method returns the [ID](`EventId`) of the written `event`. /// diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index e138dfcac8..d99e89b355 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -10,8 +10,9 @@ use crate::reflect::{ReflectComponent, ReflectFromWorld}; use crate::{ bundle::Bundle, - component::{Component, HookContext}, + component::Component, entity::Entity, + lifecycle::HookContext, relationship::{RelatedSpawner, RelatedSpawnerCommands}, system::EntityCommands, world::{DeferredWorld, EntityWorldMut, FromWorld, World}, @@ -21,9 +22,9 @@ use alloc::{format, string::String, vec::Vec}; use bevy_reflect::std_traits::ReflectDefault; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; +use bevy_utils::prelude::DebugName; use core::ops::Deref; use core::slice; -use disqualified::ShortName; use log::warn; /// Stores the parent entity of this child entity with this component. @@ -293,6 +294,12 @@ impl<'w> EntityWorldMut<'w> { self.insert_related::(index, children) } + /// Insert child at specific index. + /// See also [`insert_related`](Self::insert_related). + pub fn insert_child(&mut self, index: usize, child: Entity) -> &mut Self { + self.insert_related::(index, &[child]) + } + /// Adds the given child to this entity /// See also [`add_related`](Self::add_related). pub fn add_child(&mut self, child: Entity) -> &mut Self { @@ -304,6 +311,11 @@ impl<'w> EntityWorldMut<'w> { self.remove_related::(children) } + /// Removes the relationship between this entity and the given entity. + pub fn remove_child(&mut self, child: Entity) -> &mut Self { + self.remove_related::(&[child]) + } + /// Replaces all the related children with a new set of children. pub fn replace_children(&mut self, children: &[Entity]) -> &mut Self { self.replace_related::(children) @@ -373,6 +385,12 @@ impl<'a> EntityCommands<'a> { self.insert_related::(index, children) } + /// Insert children at specific index. + /// See also [`insert_related`](Self::insert_related). + pub fn insert_child(&mut self, index: usize, child: Entity) -> &mut Self { + self.insert_related::(index, &[child]) + } + /// Adds the given child to this entity pub fn add_child(&mut self, child: Entity) -> &mut Self { self.add_related::(&[child]) @@ -383,6 +401,11 @@ impl<'a> EntityCommands<'a> { self.remove_related::(children) } + /// Removes the relationship between this entity and the given entity. + pub fn remove_child(&mut self, child: Entity) -> &mut Self { + self.remove_related::(&[child]) + } + /// Replaces the children on this entity with a new list of children. pub fn replace_children(&mut self, children: &[Entity]) -> &mut Self { self.replace_related::(children) @@ -438,11 +461,12 @@ pub fn validate_parent_has_component( { // TODO: print name here once Name lives in bevy_ecs let name: Option = None; + let debug_name = DebugName::type_name::(); warn!( "warning[B0004]: {}{name} with the {ty_name} component has a parent without {ty_name}.\n\ This will cause inconsistent behaviors! See: https://bevy.org/learn/errors/b0004", caller.map(|c| format!("{c}: ")).unwrap_or_default(), - ty_name = ShortName::of::(), + ty_name = debug_name.shortname(), name = name.map_or_else( || format!("Entity {entity}"), |s| format!("The {s} entity") @@ -640,6 +664,29 @@ mod tests { ); } + #[test] + fn insert_child() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut entity_world_mut = world.spawn_empty(); + + let first_children = entity_world_mut.add_children(&[child1, child2]); + + let root = first_children.insert_child(1, child3).id(); + + let hierarchy = get_hierarchy(&world, root); + assert_eq!( + hierarchy, + Node::new_with( + root, + vec![Node::new(child1), Node::new(child3), Node::new(child2)] + ) + ); + } + // regression test for https://github.com/bevyengine/bevy/pull/19134 #[test] fn insert_children_index_bound() { @@ -697,6 +744,25 @@ mod tests { ); } + #[test] + fn remove_child() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut root = world.spawn_empty(); + root.add_children(&[child1, child2, child3]); + root.remove_child(child2); + let root = root.id(); + + let hierarchy = get_hierarchy(&world, root); + assert_eq!( + hierarchy, + Node::new_with(root, vec![Node::new(child1), Node::new(child3)]) + ); + } + #[test] fn self_parenting_invalid() { let mut world = World::new(); diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 14802dfc8e..e5f0e908e5 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -41,6 +41,7 @@ pub mod event; pub mod hierarchy; pub mod intern; pub mod label; +pub mod lifecycle; pub mod name; pub mod never; pub mod observer; @@ -48,7 +49,6 @@ pub mod query; #[cfg(feature = "bevy_reflect")] pub mod reflect; pub mod relationship; -pub mod removal_detection; pub mod resource; pub mod schedule; pub mod spawn; @@ -60,13 +60,17 @@ pub mod world; pub use bevy_ptr as ptr; #[cfg(feature = "hotpatching")] -use event::Event; +use event::{BufferedEvent, Event}; /// The ECS prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { #[doc(hidden)] + #[expect( + deprecated, + reason = "`Trigger` was deprecated in favor of `On`, and `OnX` lifecycle events were deprecated in favor of `X` events." + )] pub use crate::{ bundle::Bundle, change_detection::{DetectChanges, DetectChangesMut, Mut, Ref}, @@ -74,14 +78,19 @@ pub mod prelude { component::Component, entity::{ContainsEntity, Entity, EntityMapper}, error::{BevyError, Result}, - event::{Event, EventMutator, EventReader, EventWriter, Events}, + event::{ + BufferedEvent, EntityEvent, Event, EventMutator, EventReader, EventWriter, Events, + }, hierarchy::{ChildOf, ChildSpawner, ChildSpawnerCommands, Children}, + lifecycle::{ + Add, Despawn, Insert, OnAdd, OnDespawn, OnInsert, OnRemove, OnReplace, Remove, + RemovedComponents, Replace, + }, name::{Name, NameOrEntity}, - observer::{Observer, Trigger}, + observer::{Observer, On, Trigger}, query::{Added, Allows, AnyOf, Changed, Has, Or, QueryBuilder, QueryState, With, Without}, related, relationship::RelationshipTarget, - removal_detection::RemovedComponents, resource::Resource, schedule::{ common_conditions::*, ApplyDeferred, IntoScheduleConfigs, IntoSystemSet, Schedule, @@ -96,7 +105,7 @@ pub mod prelude { }, world::{ EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, - FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, + FromWorld, World, }, }; @@ -130,7 +139,7 @@ pub mod __macro_exports { /// /// Systems should refresh their inner pointers. #[cfg(feature = "hotpatching")] -#[derive(Event, Default)] +#[derive(Event, BufferedEvent, Default)] pub struct HotPatched; #[cfg(test)] diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs new file mode 100644 index 0000000000..e92c6cc7f9 --- /dev/null +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -0,0 +1,643 @@ +//! This module contains various tools to allow you to react to component insertion or removal, +//! as well as entity spawning and despawning. +//! +//! There are four main ways to react to these lifecycle events: +//! +//! 1. Using component hooks, which act as inherent constructors and destructors for components. +//! 2. Using [observers], which are a user-extensible way to respond to events, including component lifecycle events. +//! 3. Using the [`RemovedComponents`] system parameter, which offers an event-style interface. +//! 4. Using the [`Added`] query filter, which checks each component to see if it has been added since the last time a system ran. +//! +//! [observers]: crate::observer +//! [`Added`]: crate::query::Added +//! +//! # Types of lifecycle events +//! +//! There are five types of lifecycle events, split into two categories. First, we have lifecycle events that are triggered +//! when a component is added to an entity: +//! +//! - [`Add`]: Triggered when a component is added to an entity that did not already have it. +//! - [`Insert`]: Triggered when a component is added to an entity, regardless of whether it already had it. +//! +//! When both events occur, [`Add`] hooks are evaluated before [`Insert`]. +//! +//! Next, we have lifecycle events that are triggered when a component is removed from an entity: +//! +//! - [`Replace`]: Triggered when a component is removed from an entity, regardless if it is then replaced with a new value. +//! - [`Remove`]: Triggered when a component is removed from an entity and not replaced, before the component is removed. +//! - [`Despawn`]: Triggered for each component on an entity when it is despawned. +//! +//! [`Replace`] hooks are evaluated before [`Remove`], then finally [`Despawn`] hooks are evaluated. +//! +//! [`Add`] and [`Remove`] are counterparts: they are only triggered when a component is added or removed +//! from an entity in such a way as to cause a change in the component's presence on that entity. +//! Similarly, [`Insert`] and [`Replace`] are counterparts: they are triggered when a component is added or replaced +//! on an entity, regardless of whether this results in a change in the component's presence on that entity. +//! +//! To reliably synchronize data structures using with component lifecycle events, +//! you can combine [`Insert`] and [`Replace`] to fully capture any changes to the data. +//! This is particularly useful in combination with immutable components, +//! to avoid any lifecycle-bypassing mutations. +//! +//! ## Lifecycle events and component types +//! +//! Despite the absence of generics, each lifecycle event is associated with a specific component. +//! When defining a component hook for a [`Component`] type, that component is used. +//! When listening to lifecycle events for observers, the `B: Bundle` generic is used. +//! +//! Each of these lifecycle events also corresponds to a fixed [`ComponentId`], +//! which are assigned during [`World`] initialization. +//! For example, [`Add`] corresponds to [`ADD`]. +//! This is used to skip [`TypeId`](core::any::TypeId) lookups in hot paths. +use crate::{ + change_detection::MaybeLocation, + component::{Component, ComponentId, ComponentIdFor, Tick}, + entity::Entity, + event::{ + BufferedEvent, EntityEvent, Event, EventCursor, EventId, EventIterator, + EventIteratorWithId, Events, + }, + query::FilteredAccessSet, + relationship::RelationshipHookMode, + storage::SparseSet, + system::{Local, ReadOnlySystemParam, SystemMeta, SystemParam}, + world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, +}; + +use derive_more::derive::Into; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; +use core::{ + fmt::Debug, + iter, + marker::PhantomData, + ops::{Deref, DerefMut}, + option, +}; + +/// The type used for [`Component`] lifecycle hooks such as `on_add`, `on_insert` or `on_remove`. +pub type ComponentHook = for<'w> fn(DeferredWorld<'w>, HookContext); + +/// Context provided to a [`ComponentHook`]. +#[derive(Clone, Copy, Debug)] +pub struct HookContext { + /// The [`Entity`] this hook was invoked for. + pub entity: Entity, + /// The [`ComponentId`] this hook was invoked for. + pub component_id: ComponentId, + /// The caller location is `Some` if the `track_caller` feature is enabled. + pub caller: MaybeLocation, + /// Configures how relationship hooks will run + pub relationship_hook_mode: RelationshipHookMode, +} + +/// [`World`]-mutating functions that run as part of lifecycle events of a [`Component`]. +/// +/// Hooks are functions that run when a component is added, overwritten, or removed from an entity. +/// These are intended to be used for structural side effects that need to happen when a component is added or removed, +/// and are not intended for general-purpose logic. +/// +/// For example, you might use a hook to update a cached index when a component is added, +/// to clean up resources when a component is removed, +/// or to keep hierarchical data structures across entities in sync. +/// +/// This information is stored in the [`ComponentInfo`](crate::component::ComponentInfo) of the associated component. +/// +/// There are two ways of configuring hooks for a component: +/// 1. Defining the relevant hooks on the [`Component`] implementation +/// 2. Using the [`World::register_component_hooks`] method +/// +/// # Example +/// +/// ``` +/// use bevy_ecs::prelude::*; +/// use bevy_platform::collections::HashSet; +/// +/// #[derive(Component)] +/// struct MyTrackedComponent; +/// +/// #[derive(Resource, Default)] +/// struct TrackedEntities(HashSet); +/// +/// let mut world = World::new(); +/// world.init_resource::(); +/// +/// // No entities with `MyTrackedComponent` have been added yet, so we can safely add component hooks +/// let mut tracked_component_query = world.query::<&MyTrackedComponent>(); +/// assert!(tracked_component_query.iter(&world).next().is_none()); +/// +/// world.register_component_hooks::().on_add(|mut world, context| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.insert(context.entity); +/// }); +/// +/// world.register_component_hooks::().on_remove(|mut world, context| { +/// let mut tracked_entities = world.resource_mut::(); +/// tracked_entities.0.remove(&context.entity); +/// }); +/// +/// let entity = world.spawn(MyTrackedComponent).id(); +/// let tracked_entities = world.resource::(); +/// assert!(tracked_entities.0.contains(&entity)); +/// +/// world.despawn(entity); +/// let tracked_entities = world.resource::(); +/// assert!(!tracked_entities.0.contains(&entity)); +/// ``` +#[derive(Debug, Clone, Default)] +pub struct ComponentHooks { + pub(crate) on_add: Option, + pub(crate) on_insert: Option, + pub(crate) on_replace: Option, + pub(crate) on_remove: Option, + pub(crate) on_despawn: Option, +} + +impl ComponentHooks { + pub(crate) fn update_from_component(&mut self) -> &mut Self { + if let Some(hook) = C::on_add() { + self.on_add(hook); + } + if let Some(hook) = C::on_insert() { + self.on_insert(hook); + } + if let Some(hook) = C::on_replace() { + self.on_replace(hook); + } + if let Some(hook) = C::on_remove() { + self.on_remove(hook); + } + if let Some(hook) = C::on_despawn() { + self.on_despawn(hook); + } + + self + } + + /// Register a [`ComponentHook`] that will be run when this component is added to an entity. + /// An `on_add` hook will always run before `on_insert` hooks. Spawning an entity counts as + /// adding all of its components. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_add` hook + pub fn on_add(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_add(hook) + .expect("Component already has an on_add hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is added (with `.insert`) + /// or replaced. + /// + /// An `on_insert` hook always runs after any `on_add` hooks (if the entity didn't already have the component). + /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this needs to be combined with immutable components to serve as a mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_insert` hook + pub fn on_insert(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_insert(hook) + .expect("Component already has an on_insert hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is about to be dropped, + /// such as being replaced (with `.insert`) or removed. + /// + /// If this component is inserted onto an entity that already has it, this hook will run before the value is replaced, + /// allowing access to the previous data just before it is dropped. + /// This hook does *not* run if the entity did not already have this component. + /// + /// An `on_replace` hook always runs before any `on_remove` hooks (if the component is being removed from the entity). + /// + /// # Warning + /// + /// The hook won't run if the component is already present and is only mutated, such as in a system via a query. + /// As a result, this needs to be combined with immutable components to serve as a mechanism for reliably updating indexes and other caches. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_replace` hook + pub fn on_replace(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_replace(hook) + .expect("Component already has an on_replace hook") + } + + /// Register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// Despawning an entity counts as removing all of its components. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_remove` hook + pub fn on_remove(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_remove(hook) + .expect("Component already has an on_remove hook") + } + + /// Register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. + /// + /// # Panics + /// + /// Will panic if the component already has an `on_despawn` hook + pub fn on_despawn(&mut self, hook: ComponentHook) -> &mut Self { + self.try_on_despawn(hook) + .expect("Component already has an on_despawn hook") + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is added to an entity. + /// + /// This is a fallible version of [`Self::on_add`]. + /// + /// Returns `None` if the component already has an `on_add` hook. + pub fn try_on_add(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_add.is_some() { + return None; + } + self.on_add = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is added (with `.insert`) + /// + /// This is a fallible version of [`Self::on_insert`]. + /// + /// Returns `None` if the component already has an `on_insert` hook. + pub fn try_on_insert(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_insert.is_some() { + return None; + } + self.on_insert = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is replaced (with `.insert`) or removed + /// + /// This is a fallible version of [`Self::on_replace`]. + /// + /// Returns `None` if the component already has an `on_replace` hook. + pub fn try_on_replace(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_replace.is_some() { + return None; + } + self.on_replace = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run when this component is removed from an entity. + /// + /// This is a fallible version of [`Self::on_remove`]. + /// + /// Returns `None` if the component already has an `on_remove` hook. + pub fn try_on_remove(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_remove.is_some() { + return None; + } + self.on_remove = Some(hook); + Some(self) + } + + /// Attempt to register a [`ComponentHook`] that will be run for each component on an entity when it is despawned. + /// + /// This is a fallible version of [`Self::on_despawn`]. + /// + /// Returns `None` if the component already has an `on_despawn` hook. + pub fn try_on_despawn(&mut self, hook: ComponentHook) -> Option<&mut Self> { + if self.on_despawn.is_some() { + return None; + } + self.on_despawn = Some(hook); + Some(self) + } +} + +/// [`ComponentId`] for [`Add`] +pub const ADD: ComponentId = ComponentId::new(0); +/// [`ComponentId`] for [`Insert`] +pub const INSERT: ComponentId = ComponentId::new(1); +/// [`ComponentId`] for [`Replace`] +pub const REPLACE: ComponentId = ComponentId::new(2); +/// [`ComponentId`] for [`Remove`] +pub const REMOVE: ComponentId = ComponentId::new(3); +/// [`ComponentId`] for [`Despawn`] +pub const DESPAWN: ComponentId = ComponentId::new(4); + +/// Trigger emitted when a component is inserted onto an entity that does not already have that +/// component. Runs before `Insert`. +/// See [`crate::lifecycle::ComponentHooks::on_add`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnAdd")] +pub struct Add; + +/// Trigger emitted when a component is inserted, regardless of whether or not the entity already +/// had that component. Runs after `Add`, if it ran. +/// See [`crate::lifecycle::ComponentHooks::on_insert`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnInsert")] +pub struct Insert; + +/// Trigger emitted when a component is removed from an entity, regardless +/// of whether or not it is later replaced. +/// +/// Runs before the value is replaced, so you can still access the original component data. +/// See [`crate::lifecycle::ComponentHooks::on_replace`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnReplace")] +pub struct Replace; + +/// Trigger emitted when a component is removed from an entity, and runs before the component is +/// removed, so you can still access the component data. +/// See [`crate::lifecycle::ComponentHooks::on_remove`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnRemove")] +pub struct Remove; + +/// Trigger emitted for each component on an entity when it is despawned. +/// See [`crate::lifecycle::ComponentHooks::on_despawn`] for more information. +#[derive(Event, EntityEvent, Debug, Clone)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] +#[doc(alias = "OnDespawn")] +pub struct Despawn; + +/// Deprecated in favor of [`Add`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Add`.")] +pub type OnAdd = Add; + +/// Deprecated in favor of [`Insert`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Insert`.")] +pub type OnInsert = Insert; + +/// Deprecated in favor of [`Replace`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Replace`.")] +pub type OnReplace = Replace; + +/// Deprecated in favor of [`Remove`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Remove`.")] +pub type OnRemove = Remove; + +/// Deprecated in favor of [`Despawn`]. +#[deprecated(since = "0.17.0", note = "Renamed to `Despawn`.")] +pub type OnDespawn = Despawn; + +/// Wrapper around [`Entity`] for [`RemovedComponents`]. +/// Internally, `RemovedComponents` uses these as an `Events`. +#[derive(Event, BufferedEvent, Debug, Clone, Into)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] +pub struct RemovedComponentEntity(Entity); + +/// Wrapper around a [`EventCursor`] so that we +/// can differentiate events between components. +#[derive(Debug)] +pub struct RemovedComponentReader +where + T: Component, +{ + reader: EventCursor, + marker: PhantomData, +} + +impl Default for RemovedComponentReader { + fn default() -> Self { + Self { + reader: Default::default(), + marker: PhantomData, + } + } +} + +impl Deref for RemovedComponentReader { + type Target = EventCursor; + fn deref(&self) -> &Self::Target { + &self.reader + } +} + +impl DerefMut for RemovedComponentReader { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.reader + } +} + +/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. +#[derive(Default, Debug)] +pub struct RemovedComponentEvents { + event_sets: SparseSet>, +} + +impl RemovedComponentEvents { + /// Creates an empty storage buffer for component removal events. + pub fn new() -> Self { + Self::default() + } + + /// For each type of component, swaps the event buffers and clears the oldest event buffer. + /// In general, this should be called once per frame/update. + pub fn update(&mut self) { + for (_component_id, events) in self.event_sets.iter_mut() { + events.update(); + } + } + + /// Returns an iterator over components and their entity events. + pub fn iter(&self) -> impl Iterator)> { + self.event_sets.iter() + } + + /// Gets the event storage for a given component. + pub fn get( + &self, + component_id: impl Into, + ) -> Option<&Events> { + self.event_sets.get(component_id.into()) + } + + /// Sends a removal event for the specified component. + pub fn send(&mut self, component_id: impl Into, entity: Entity) { + self.event_sets + .get_or_insert_with(component_id.into(), Default::default) + .send(RemovedComponentEntity(entity)); + } +} + +/// A [`SystemParam`] that yields entities that had their `T` [`Component`] +/// removed or have been despawned with it. +/// +/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). +/// +/// Unlike hooks or observers (see the [lifecycle](crate) module docs), +/// this does not allow you to see which data existed before removal. +/// +/// If you are using `bevy_ecs` as a standalone crate, +/// note that the [`RemovedComponents`] list will not be automatically cleared for you, +/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). +/// +/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is +/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. +/// For the main world, this is delayed until after all `SubApp`s have run. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ``` +/// # use bevy_ecs::component::Component; +/// # use bevy_ecs::system::IntoSystem; +/// # use bevy_ecs::lifecycle::RemovedComponents; +/// # +/// # #[derive(Component)] +/// # struct MyComponent; +/// fn react_on_removal(mut removed: RemovedComponents) { +/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); +/// } +/// # bevy_ecs::system::assert_is_system(react_on_removal); +/// ``` +#[derive(SystemParam)] +pub struct RemovedComponents<'w, 's, T: Component> { + component_id: ComponentIdFor<'s, T>, + reader: Local<'s, RemovedComponentReader>, + event_sets: &'w RemovedComponentEvents, +} + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIter<'a> = iter::Map< + iter::Flatten>>>, + fn(RemovedComponentEntity) -> Entity, +>; + +/// Iterator over entities that had a specific component removed. +/// +/// See [`RemovedComponents`]. +pub type RemovedIterWithId<'a> = iter::Map< + iter::Flatten>>, + fn( + (&RemovedComponentEntity, EventId), + ) -> (Entity, EventId), +>; + +fn map_id_events( + (entity, id): (&RemovedComponentEntity, EventId), +) -> (Entity, EventId) { + (entity.clone().into(), id) +} + +// For all practical purposes, the api surface of `RemovedComponents` +// should be similar to `EventReader` to reduce confusion. +impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { + /// Fetch underlying [`EventCursor`]. + pub fn reader(&self) -> &EventCursor { + &self.reader + } + + /// Fetch underlying [`EventCursor`] mutably. + pub fn reader_mut(&mut self) -> &mut EventCursor { + &mut self.reader + } + + /// Fetch underlying [`Events`]. + pub fn events(&self) -> Option<&Events> { + self.event_sets.get(self.component_id.get()) + } + + /// Destructures to get a mutable reference to the `EventCursor` + /// and a reference to `Events`. + /// + /// This is necessary since Rust can't detect destructuring through methods and most + /// usecases of the reader uses the `Events` as well. + pub fn reader_mut_with_events( + &mut self, + ) -> Option<( + &mut RemovedComponentReader, + &Events, + )> { + self.event_sets + .get(self.component_id.get()) + .map(|events| (&mut *self.reader, events)) + } + + /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the + /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events + /// that happened before now. + pub fn read(&mut self) -> RemovedIter<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read(events).cloned()) + .into_iter() + .flatten() + .map(RemovedComponentEntity::into) + } + + /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. + pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { + self.reader_mut_with_events() + .map(|(reader, events)| reader.read_with_id(events)) + .into_iter() + .flatten() + .map(map_id_events) + } + + /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. + pub fn len(&self) -> usize { + self.events() + .map(|events| self.reader.len(events)) + .unwrap_or(0) + } + + /// Returns `true` if there are no events available to read. + pub fn is_empty(&self) -> bool { + self.events() + .is_none_or(|events| self.reader.is_empty(events)) + } + + /// Consumes all available events. + /// + /// This means these events will not appear in calls to [`RemovedComponents::read()`] or + /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. + pub fn clear(&mut self) { + if let Some((reader, events)) = self.reader_mut_with_events() { + reader.clear(events); + } + } +} + +// SAFETY: Only reads World removed component events +unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} + +// SAFETY: no component value access. +unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { + type State = (); + type Item<'w, 's> = &'w RemovedComponentEvents; + + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } + + #[inline] + unsafe fn get_param<'w, 's>( + _state: &'s mut Self::State, + _system_meta: &SystemMeta, + world: UnsafeWorldCell<'w>, + _change_tick: Tick, + ) -> Self::Item<'w, 's> { + world.removed_components() + } +} diff --git a/crates/bevy_ecs/src/name.rs b/crates/bevy_ecs/src/name.rs index cd2e946678..67719ca18d 100644 --- a/crates/bevy_ecs/src/name.rs +++ b/crates/bevy_ecs/src/name.rs @@ -141,7 +141,7 @@ pub struct NameOrEntity { pub entity: Entity, } -impl<'a> core::fmt::Display for NameOrEntityItem<'a> { +impl<'w, 's> core::fmt::Display for NameOrEntityItem<'w, 's> { #[inline(always)] fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { match self.name { @@ -274,9 +274,9 @@ mod tests { let e2 = world.spawn(name.clone()).id(); let mut query = world.query::(); let d1 = query.get(&world, e1).unwrap(); - let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities without a Name should be {index}v{generation} assert_eq!(d1.to_string(), "0v0"); + let d2 = query.get(&world, e2).unwrap(); // NameOrEntity Display for entities with a Name should be the Name assert_eq!(d2.to_string(), "MyName"); } diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index 2c2d42b1c9..23be0e9672 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -1,18 +1,29 @@ use crate::{ - component::{ - Component, ComponentCloneBehavior, ComponentHook, HookContext, Mutable, StorageType, - }, + component::{Component, ComponentCloneBehavior, Mutable, StorageType}, entity::{ComponentCloneCtx, Entity, EntityClonerBuilder, EntityMapper, SourceComponent}, + lifecycle::{ComponentHook, HookContext}, world::World, }; use alloc::vec::Vec; +#[cfg(feature = "bevy_reflect")] +use crate::prelude::ReflectComponent; + use super::Observer; /// Tracks a list of entity observers for the [`Entity`] [`ObservedBy`] is added to. -#[derive(Default)] +#[derive(Default, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Component, Debug))] pub struct ObservedBy(pub(crate) Vec); +impl ObservedBy { + /// Provides a read-only reference to the list of entities observing this entity. + pub fn get(&self) -> &[Entity] { + &self.0 + } +} + impl Component for ObservedBy { const STORAGE_TYPE: StorageType = StorageType::SparseSet; type Mutability = Mutable; @@ -86,7 +97,7 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo let event_types = observer_state.descriptor.events.clone(); let components = observer_state.descriptor.components.clone(); for event_type in event_types { - let observers = world.observers.get_observers(event_type); + let observers = world.observers.get_observers_mut(event_type); if components.is_empty() { if let Some(map) = observers.entity_observers.get(&source).cloned() { observers.entity_observers.insert(target, map); @@ -97,8 +108,10 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo else { continue; }; - if let Some(map) = observers.entity_map.get(&source).cloned() { - observers.entity_map.insert(target, map); + if let Some(map) = + observers.entity_component_observers.get(&source).cloned() + { + observers.entity_component_observers.insert(target, map); } } } @@ -110,14 +123,18 @@ fn component_clone_observed_by(_source: &SourceComponent, ctx: &mut ComponentClo #[cfg(test)] mod tests { use crate::{ - entity::EntityCloner, event::Event, observer::Trigger, resource::Resource, system::ResMut, + entity::EntityCloner, + event::{EntityEvent, Event}, + observer::On, + resource::Resource, + system::ResMut, world::World, }; #[derive(Resource, Default)] struct Num(usize); - #[derive(Event)] + #[derive(Event, EntityEvent)] struct E; #[test] @@ -127,7 +144,7 @@ mod tests { let e = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .observe(|_: On, mut res: ResMut| res.0 += 1) .id(); world.flush(); diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 767dc7ec95..b3a8b3b5cd 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -1,4 +1,133 @@ -//! Types for creating and storing [`Observer`]s +//! Observers are a push-based tool for responding to [`Event`]s. +//! +//! ## Observer targeting +//! +//! Observers can be "global", listening for events that are both targeted at and not targeted at any specific entity, +//! or they can be "entity-specific", listening for events that are targeted at specific entities. +//! +//! They can also be further refined by listening to events targeted at specific components +//! (instead of using a generic event type), as is done with the [`Add`] family of lifecycle events. +//! +//! When entities are observed, they will receive an [`ObservedBy`] component, +//! which will be updated to track the observers that are currently observing them. +//! +//! Currently, [observers cannot be retargeted after spawning](https://github.com/bevyengine/bevy/issues/19587): +//! despawn and respawn an observer as a workaround. +//! +//! ## Writing observers +//! +//! Observers are systems which implement [`IntoObserverSystem`] that listen for [`Event`]s matching their +//! type and target(s). +//! To write observer systems, use [`On`] as the first parameter of your system. +//! This parameter provides access to the specific event that triggered the observer, +//! as well as the entity that the event was targeted at, if any. +//! +//! Observers can request other data from the world, such as via a [`Query`] or [`Res`]. +//! Commonly, you might want to verify that the entity that the observable event is targeting +//! has a specific component, or meets some other condition. [`Query::get`] or [`Query::contains`] +//! on the [`On::target`] entity is a good way to do this. +//! +//! [`Commands`] can also be used inside of observers. +//! This can be particularly useful for triggering other observers! +//! +//! ## Spawning observers +//! +//! Observers can be spawned via [`World::add_observer`], or the equivalent app method. +//! This will cause an entity with the [`Observer`] component to be created, +//! which will then run the observer system whenever the event it is watching is triggered. +//! +//! You can control the targets that an observer is watching by calling [`Observer::watch_entity`] +//! once the entity is spawned, or by manually spawning an entity with the [`Observer`] component +//! configured with the desired targets. +//! +//! Observers are fundamentally defined as "entities which have the [`Observer`] component" +//! allowing you to add it manually to existing entities. +//! At first, this seems convenient, but only one observer can be added to an entity at a time, +//! regardless of the event it responds to: like always, components are unique. +//! +//! Instead, a better way to achieve a similar aim is to +//! use the [`EntityWorldMut::observe`] / [`EntityCommands::observe`] method, +//! which spawns a new observer, and configures it to watch the entity it is called on. +//! Unfortunately, observers defined in this way +//! [currently cannot be spawned as part of bundles](https://github.com/bevyengine/bevy/issues/14204). +//! +//! ## Triggering observers +//! +//! Observers are most commonly triggered by [`Commands`], +//! via [`Commands::trigger`] (for untargeted [`Event`]s) or [`Commands::trigger_targets`] (for targeted [`EntityEvent`]s). +//! Like usual, equivalent methods are available on [`World`], allowing you to reduce overhead when working with exclusive world access. +//! +//! If your observer is configured to watch for a specific component or set of components instead, +//! you can pass in [`ComponentId`]s into [`Commands::trigger_targets`] by using the [`TriggerTargets`] trait. +//! As discussed in the [`On`] documentation, this use case is rare, and is currently only used +//! for [lifecycle](crate::lifecycle) events, which are automatically emitted. +//! +//! ## Observer bubbling +//! +//! When using an [`EntityEvent`] targeted at an entity, the event can optionally be propagated to other targets, +//! typically up to parents in an entity hierarchy. +//! +//! This behavior is controlled via [`EntityEvent::Traversal`] and [`EntityEvent::AUTO_PROPAGATE`], +//! with the details of the propagation path specified by the [`Traversal`](crate::traversal::Traversal) trait. +//! +//! When auto-propagation is enabled, propagation must be manually stopped to prevent the event from +//! continuing to other targets. This can be done using the [`On::propagate`] method inside of your observer. +//! +//! ## Observer timing +//! +//! Observers are triggered via [`Commands`], which imply that they are evaluated at the next sync point in the ECS schedule. +//! Accordingly, they have full access to the world, and are evaluated sequentially, in the order that the commands were sent. +//! +//! To control the relative ordering of observers sent from different systems, +//! order the systems in the schedule relative to each other. +//! +//! Currently, Bevy does not provide [a way to specify the ordering of observers](https://github.com/bevyengine/bevy/issues/14890) +//! listening to the same event relative to each other. +//! +//! Commands sent by observers are [currently not immediately applied](https://github.com/bevyengine/bevy/issues/19569). +//! Instead, all queued observers will run, and then all of the commands from those observers will be applied. +//! Careful use of [`Schedule::apply_deferred`] may help as a workaround. +//! +//! ## Lifecycle events and observers +//! +//! It is important to note that observers, just like [hooks](crate::lifecycle::ComponentHooks), +//! can listen to and respond to [lifecycle](crate::lifecycle) events. +//! Unlike hooks, observers are not treated as an "innate" part of component behavior: +//! they can be added or removed at runtime, and multiple observers +//! can be registered for the same lifecycle event for the same component. +//! +//! The ordering of hooks versus observers differs based on the lifecycle event in question: +//! +//! - when adding components, hooks are evaluated first, then observers +//! - when removing components, observers are evaluated first, then hooks +//! +//! This allows hooks to act as constructors and destructors for components, +//! as they always have the first and final say in the component's lifecycle. +//! +//! ## Cleaning up observers +//! +//! Currently, observer entities are never cleaned up, even if their target entity(s) are despawned. +//! This won't cause any runtime overhead, but is a waste of memory and can result in memory leaks. +//! +//! If you run into this problem, you could manually scan the world for observer entities and despawn them, +//! by checking if the entity in [`Observer::descriptor`] still exists. +//! +//! ## Observers vs buffered events +//! +//! By contrast, [`EventReader`] and [`EventWriter`] ("buffered events"), are pull-based. +//! They require periodically polling the world to check for new events, typically in a system that runs as part of a schedule. +//! +//! This imposes a small overhead, making observers a better choice for extremely rare events, +//! but buffered events can be more efficient for events that are expected to occur multiple times per frame, +//! as it allows for batch processing of events. +//! +//! The difference in timing is also an important consideration: +//! buffered events are evaluated at fixed points during schedules, +//! while observers are evaluated as soon as possible after the event is triggered. +//! +//! This provides more control over the timing of buffered event evaluation, +//! but allows for a more ad hoc approach with observers, +//! and enables indefinite chaining of observers triggering other observers (for both better and worse!). mod entity_observer; mod runner; @@ -28,16 +157,31 @@ use smallvec::SmallVec; /// Type containing triggered [`Event`] information for a given run of an [`Observer`]. This contains the /// [`Event`] data itself. If it was triggered for a specific [`Entity`], it includes that as well. It also -/// contains event propagation information. See [`Trigger::propagate`] for more information. -pub struct Trigger<'w, E, B: Bundle = ()> { +/// contains event propagation information. See [`On::propagate`] for more information. +/// +/// The generic `B: Bundle` is used to modify the further specialize the events that this observer is interested in. +/// The entity involved *does not* have to have these components, but the observer will only be +/// triggered if the event matches the components in `B`. +/// +/// This is used to to avoid providing a generic argument in your event, as is done for [`Add`] +/// and the other lifecycle events. +/// +/// Providing multiple components in this bundle will cause this event to be triggered by any +/// matching component in the bundle, +/// [rather than requiring all of them to be present](https://github.com/bevyengine/bevy/issues/15325). +pub struct On<'w, E, B: Bundle = ()> { event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger, _marker: PhantomData, } -impl<'w, E, B: Bundle> Trigger<'w, E, B> { - /// Creates a new trigger for the given event and observer information. +/// Deprecated in favor of [`On`]. +#[deprecated(since = "0.17.0", note = "Renamed to `On`.")] +pub type Trigger<'w, E, B = ()> = On<'w, E, B>; + +impl<'w, E, B: Bundle> On<'w, E, B> { + /// Creates a new instance of [`On`] for the given event and observer information. pub fn new(event: &'w mut E, propagate: &'w mut bool, trigger: ObserverTrigger) -> Self { Self { event, @@ -47,7 +191,7 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { } } - /// Returns the event type of this trigger. + /// Returns the event type of this [`On`] instance. pub fn event_type(&self) -> ComponentId { self.trigger.event_type } @@ -67,24 +211,6 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { Ptr::from(&self.event) } - /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. It may - /// be [`Entity::PLACEHOLDER`]. - /// - /// Observable events can target specific entities. When those events fire, they will trigger - /// any observers on the targeted entities. In this case, the `target()` and `observer()` are - /// the same, because the observer that was triggered is attached to the entity that was - /// targeted by the event. - /// - /// However, it is also possible for those events to bubble up the entity hierarchy and trigger - /// observers on *different* entities, or trigger a global observer. In these cases, the - /// observing entity is *different* from the entity being targeted by the event. - /// - /// This is an important distinction: the entity reacting to an event is not always the same as - /// the entity triggered by the event. - pub fn target(&self) -> Entity { - self.trigger.target - } - /// Returns the components that triggered the observer, out of the /// components defined in `B`. Does not necessarily include all of them as /// `B` acts like an `OR` filter rather than an `AND` filter. @@ -98,14 +224,14 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { /// # Examples /// /// ```rust - /// # use bevy_ecs::prelude::{Commands, Trigger}; + /// # use bevy_ecs::prelude::{Commands, On}; /// # /// # struct MyEvent { /// # done: bool, /// # } /// # /// /// Handle `MyEvent` and if it is done, stop observation. - /// fn my_observer(trigger: Trigger, mut commands: Commands) { + /// fn my_observer(trigger: On, mut commands: Commands) { /// if trigger.event().done { /// commands.entity(trigger.observer()).despawn(); /// return; @@ -118,14 +244,40 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { self.trigger.observer } + /// Returns the source code location that triggered this observer. + pub fn caller(&self) -> MaybeLocation { + self.trigger.caller + } +} + +impl<'w, E: EntityEvent, B: Bundle> On<'w, E, B> { + /// Returns the [`Entity`] that was targeted by the `event` that triggered this observer. + /// + /// Note that if event propagation is enabled, this may not be the same as the original target of the event, + /// which can be accessed via [`On::original_target`]. + /// + /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. + pub fn target(&self) -> Entity { + self.trigger.current_target.unwrap_or(Entity::PLACEHOLDER) + } + + /// Returns the original [`Entity`] that the `event` was targeted at when it was first triggered. + /// + /// If event propagation is not enabled, this will always return the same value as [`On::target`]. + /// + /// If the event was not targeted at a specific entity, this will return [`Entity::PLACEHOLDER`]. + pub fn original_target(&self) -> Entity { + self.trigger.original_target.unwrap_or(Entity::PLACEHOLDER) + } + /// Enables or disables event propagation, allowing the same event to trigger observers on a chain of different entities. /// /// The path an event will propagate along is specified by its associated [`Traversal`] component. By default, events /// use `()` which ends the path immediately and prevents propagation. /// /// To enable propagation, you must: - /// + Set [`Event::Traversal`] to the component you want to propagate along. - /// + Either call `propagate(true)` in the first observer or set [`Event::AUTO_PROPAGATE`] to `true`. + /// + Set [`EntityEvent::Traversal`] to the component you want to propagate along. + /// + Either call `propagate(true)` in the first observer or set [`EntityEvent::AUTO_PROPAGATE`] to `true`. /// /// You can prevent an event from propagating further using `propagate(false)`. /// @@ -136,20 +288,15 @@ impl<'w, E, B: Bundle> Trigger<'w, E, B> { /// Returns the value of the flag that controls event propagation. See [`propagate`] for more information. /// - /// [`propagate`]: Trigger::propagate + /// [`propagate`]: On::propagate pub fn get_propagate(&self) -> bool { *self.propagate } - - /// Returns the source code location that triggered this observer. - pub fn caller(&self) -> MaybeLocation { - self.trigger.caller - } } -impl<'w, E: Debug, B: Bundle> Debug for Trigger<'w, E, B> { +impl<'w, E: Debug, B: Bundle> Debug for On<'w, E, B> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("Trigger") + f.debug_struct("On") .field("event", &self.event) .field("propagate", &self.propagate) .field("trigger", &self.trigger) @@ -158,7 +305,7 @@ impl<'w, E: Debug, B: Bundle> Debug for Trigger<'w, E, B> { } } -impl<'w, E, B: Bundle> Deref for Trigger<'w, E, B> { +impl<'w, E, B: Bundle> Deref for On<'w, E, B> { type Target = E; fn deref(&self) -> &Self::Target { @@ -166,16 +313,20 @@ impl<'w, E, B: Bundle> Deref for Trigger<'w, E, B> { } } -impl<'w, E, B: Bundle> DerefMut for Trigger<'w, E, B> { +impl<'w, E, B: Bundle> DerefMut for On<'w, E, B> { fn deref_mut(&mut self) -> &mut Self::Target { self.event } } -/// Represents a collection of targets for a specific [`Trigger`] of an [`Event`]. Targets can be of type [`Entity`] or [`ComponentId`]. +/// Represents a collection of targets for a specific [`On`] instance of an [`Event`]. /// -/// When a trigger occurs for a given event and [`TriggerTargets`], any [`Observer`] that watches for that specific event-target combination -/// will run. +/// When an event is triggered with [`TriggerTargets`], any [`Observer`] that watches for that specific +/// event-target combination will run. +/// +/// This trait is implemented for both [`Entity`] and [`ComponentId`], allowing you to target specific entities or components. +/// It is also implemented for various collections of these types, such as [`Vec`], arrays, and tuples, +/// allowing you to trigger events for multiple targets at once. pub trait TriggerTargets { /// The components the trigger should target. fn components(&self) -> impl Iterator + Clone + '_; @@ -280,7 +431,9 @@ all_tuples!( T ); -/// A description of what an [`Observer`] observes. +/// Store information about what an [`Observer`] observes. +/// +/// This information is stored inside of the [`Observer`] component, #[derive(Default, Clone)] pub struct ObserverDescriptor { /// The events the observer is watching. @@ -331,7 +484,9 @@ impl ObserverDescriptor { } } -/// Event trigger metadata for a given [`Observer`], +/// Metadata about a specific [`Event`] that triggered an observer. +/// +/// This information is exposed via methods on [`On`]. #[derive(Debug)] pub struct ObserverTrigger { /// The [`Entity`] of the observer handling the trigger. @@ -340,8 +495,15 @@ pub struct ObserverTrigger { pub event_type: ComponentId, /// The [`ComponentId`]s the trigger targeted. components: SmallVec<[ComponentId; 2]>, - /// The entity the trigger targeted. - pub target: Entity, + /// The entity that the entity-event targeted, if any. + /// + /// Note that if event propagation is enabled, this may not be the same as [`ObserverTrigger::original_target`]. + pub current_target: Option, + /// The entity that the entity-event was originally targeted at, if any. + /// + /// If event propagation is enabled, this will be the first entity that the event was targeted at, + /// even if the event was propagated to other entities. + pub original_target: Option, /// The location of the source code that triggered the observer. pub caller: MaybeLocation, } @@ -353,61 +515,111 @@ impl ObserverTrigger { } } -// Map between an observer entity and its runner -type ObserverMap = EntityHashMap; +/// Map between an observer entity and its [`ObserverRunner`] +pub type ObserverMap = EntityHashMap; -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger targeted at a specific component. +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event targeted at a specific component. +/// +/// This is stored inside of [`CachedObservers`]. #[derive(Default, Debug)] pub struct CachedComponentObservers { - // Observers listening to triggers targeting this component - map: ObserverMap, - // Observers listening to triggers targeting this component on a specific entity - entity_map: EntityHashMap, + // Observers listening to events targeting this component, but not a specific entity + global_observers: ObserverMap, + // Observers listening to events targeting this component on a specific entity + entity_component_observers: EntityHashMap, } -/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular trigger. +impl CachedComponentObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting this component on a specific entity. + pub fn entity_component_observers(&self) -> &EntityHashMap { + &self.entity_component_observers + } +} + +/// Collection of [`ObserverRunner`] for [`Observer`] registered to a particular event. +/// +/// This is stored inside of [`Observers`], specialized for each kind of observer. #[derive(Default, Debug)] pub struct CachedObservers { - // Observers listening for any time this trigger is fired - map: ObserverMap, + // Observers listening for any time this event is fired, regardless of target + // This will also respond to events targeting specific components or entities + global_observers: ObserverMap, // Observers listening for this trigger fired at a specific component component_observers: HashMap, // Observers listening for this trigger fired at a specific entity entity_observers: EntityHashMap, } -/// Metadata for observers. Stores a cache mapping trigger ids to the registered observers. +impl CachedObservers { + /// Returns the observers listening for this trigger, regardless of target. + /// These observers will also respond to events targeting specific components or entities. + pub fn global_observers(&self) -> &ObserverMap { + &self.global_observers + } + + /// Returns the observers listening for this trigger targeting components. + pub fn get_component_observers(&self) -> &HashMap { + &self.component_observers + } + + /// Returns the observers listening for this trigger targeting entities. + pub fn entity_observers(&self) -> &HashMap { + &self.component_observers + } +} + +/// An internal lookup table tracking all of the observers in the world. +/// +/// Stores a cache mapping trigger ids to the registered observers. +/// Some observer kinds (like [lifecycle](crate::lifecycle) observers) have a dedicated field, +/// saving lookups for the most common triggers. +/// +/// This can be accessed via [`World::observers`]. #[derive(Default, Debug)] pub struct Observers { // Cached ECS observers to save a lookup most common triggers. - on_add: CachedObservers, - on_insert: CachedObservers, - on_replace: CachedObservers, - on_remove: CachedObservers, - on_despawn: CachedObservers, - // Map from trigger type to set of observers + add: CachedObservers, + insert: CachedObservers, + replace: CachedObservers, + remove: CachedObservers, + despawn: CachedObservers, + // Map from trigger type to set of observers listening to that trigger cache: HashMap, } impl Observers { - pub(crate) fn get_observers(&mut self, event_type: ComponentId) -> &mut CachedObservers { + pub(crate) fn get_observers_mut(&mut self, event_type: ComponentId) -> &mut CachedObservers { + use crate::lifecycle::*; + match event_type { - ON_ADD => &mut self.on_add, - ON_INSERT => &mut self.on_insert, - ON_REPLACE => &mut self.on_replace, - ON_REMOVE => &mut self.on_remove, - ON_DESPAWN => &mut self.on_despawn, + ADD => &mut self.add, + INSERT => &mut self.insert, + REPLACE => &mut self.replace, + REMOVE => &mut self.remove, + DESPAWN => &mut self.despawn, _ => self.cache.entry(event_type).or_default(), } } - pub(crate) fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { + /// Attempts to get the observers for the given `event_type`. + /// + /// When accessing the observers for lifecycle events, such as [`Add`], [`Insert`], [`Replace`], [`Remove`], and [`Despawn`], + /// use the [`ComponentId`] constants from the [`lifecycle`](crate::lifecycle) module. + pub fn try_get_observers(&self, event_type: ComponentId) -> Option<&CachedObservers> { + use crate::lifecycle::*; + match event_type { - ON_ADD => Some(&self.on_add), - ON_INSERT => Some(&self.on_insert), - ON_REPLACE => Some(&self.on_replace), - ON_REMOVE => Some(&self.on_remove), - ON_DESPAWN => Some(&self.on_despawn), + ADD => Some(&self.add), + INSERT => Some(&self.insert), + REPLACE => Some(&self.replace), + REMOVE => Some(&self.remove), + DESPAWN => Some(&self.despawn), _ => self.cache.get(&event_type), } } @@ -416,7 +628,8 @@ impl Observers { pub(crate) fn invoke( mut world: DeferredWorld, event_type: ComponentId, - target: Entity, + current_target: Option, + original_target: Option, components: impl Iterator + Clone, data: &mut T, propagate: &mut bool, @@ -444,7 +657,8 @@ impl Observers { observer, event_type, components: components.clone().collect(), - target, + current_target, + original_target, caller, }, data.into(), @@ -452,11 +666,14 @@ impl Observers { ); }; // Trigger observers listening for any kind of this trigger - observers.map.iter().for_each(&mut trigger_observer); + observers + .global_observers + .iter() + .for_each(&mut trigger_observer); // Trigger entity observers listening for this kind of trigger - if target != Entity::PLACEHOLDER { - if let Some(map) = observers.entity_observers.get(&target) { + if let Some(target_entity) = current_target { + if let Some(map) = observers.entity_observers.get(&target_entity) { map.iter().for_each(&mut trigger_observer); } } @@ -465,12 +682,15 @@ impl Observers { trigger_for_components.for_each(|id| { if let Some(component_observers) = observers.component_observers.get(&id) { component_observers - .map + .global_observers .iter() .for_each(&mut trigger_observer); - if target != Entity::PLACEHOLDER { - if let Some(map) = component_observers.entity_map.get(&target) { + if let Some(target_entity) = current_target { + if let Some(map) = component_observers + .entity_component_observers + .get(&target_entity) + { map.iter().for_each(&mut trigger_observer); } } @@ -479,12 +699,14 @@ impl Observers { } pub(crate) fn is_archetype_cached(event_type: ComponentId) -> Option { + use crate::lifecycle::*; + match event_type { - ON_ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), - ON_INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), - ON_REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), - ON_REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), - ON_DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), + ADD => Some(ArchetypeFlags::ON_ADD_OBSERVER), + INSERT => Some(ArchetypeFlags::ON_INSERT_OBSERVER), + REPLACE => Some(ArchetypeFlags::ON_REPLACE_OBSERVER), + REMOVE => Some(ArchetypeFlags::ON_REMOVE_OBSERVER), + DESPAWN => Some(ArchetypeFlags::ON_DESPAWN_OBSERVER), _ => None, } } @@ -494,39 +716,23 @@ impl Observers { component_id: ComponentId, flags: &mut ArchetypeFlags, ) { - if self.on_add.component_observers.contains_key(&component_id) { + if self.add.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_ADD_OBSERVER); } - if self - .on_insert - .component_observers - .contains_key(&component_id) - { + if self.insert.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_INSERT_OBSERVER); } - if self - .on_replace - .component_observers - .contains_key(&component_id) - { + if self.replace.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_REPLACE_OBSERVER); } - if self - .on_remove - .component_observers - .contains_key(&component_id) - { + if self.remove.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_REMOVE_OBSERVER); } - if self - .on_despawn - .component_observers - .contains_key(&component_id) - { + if self.despawn.component_observers.contains_key(&component_id) { flags.insert(ArchetypeFlags::ON_DESPAWN_OBSERVER); } } @@ -536,7 +742,7 @@ impl World { /// Spawns a "global" [`Observer`] which will watch for the given event. /// Returns its [`Entity`] as a [`EntityWorldMut`]. /// - /// `system` can be any system whose first parameter is a [`Trigger`]. + /// `system` can be any system whose first parameter is [`On`]. /// /// **Calling [`observe`](EntityWorldMut::observe) on the returned /// [`EntityWorldMut`] will observe the observer itself, which you very @@ -550,10 +756,10 @@ impl World { /// struct A; /// /// # let mut world = World::new(); - /// world.add_observer(|_: Trigger| { + /// world.add_observer(|_: On| { /// // ... /// }); - /// world.add_observer(|_: Trigger| { + /// world.add_observer(|_: On| { /// // ... /// }); /// ``` @@ -582,7 +788,7 @@ impl World { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` unsafe { - self.trigger_targets_dynamic_ref_with_caller(event_id, &mut event, (), caller); + self.trigger_dynamic_ref_with_caller(event_id, &mut event, caller); } } @@ -594,20 +800,41 @@ impl World { pub fn trigger_ref(&mut self, event: &mut E) { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` - unsafe { self.trigger_targets_dynamic_ref(event_id, event, ()) }; + unsafe { self.trigger_dynamic_ref_with_caller(event_id, event, MaybeLocation::caller()) }; } - /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. + unsafe fn trigger_dynamic_ref_with_caller( + &mut self, + event_id: ComponentId, + event_data: &mut E, + caller: MaybeLocation, + ) { + let mut world = DeferredWorld::from(self); + // SAFETY: `event_data` is accessible as the type represented by `event_id` + unsafe { + world.trigger_observers_with_data::<_, ()>( + event_id, + None, + None, + core::iter::empty::(), + event_data, + false, + caller, + ); + }; + } + + /// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it. /// /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. /// If you need to use the event after triggering it, use [`World::trigger_targets_ref`] instead. #[track_caller] - pub fn trigger_targets(&mut self, event: E, targets: impl TriggerTargets) { + pub fn trigger_targets(&mut self, event: E, targets: impl TriggerTargets) { self.trigger_targets_with_caller(event, targets, MaybeLocation::caller()); } - pub(crate) fn trigger_targets_with_caller( + pub(crate) fn trigger_targets_with_caller( &mut self, mut event: E, targets: impl TriggerTargets, @@ -620,19 +847,23 @@ impl World { } } - /// Triggers the given [`Event`] as a mutable reference for the given `targets`, + /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, /// which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger_targets`], this method is most useful when it's necessary to check /// or use the event after it has been modified by observers. #[track_caller] - pub fn trigger_targets_ref(&mut self, event: &mut E, targets: impl TriggerTargets) { + pub fn trigger_targets_ref( + &mut self, + event: &mut E, + targets: impl TriggerTargets, + ) { let event_id = E::register_component_id(self); // SAFETY: We just registered `event_id` with the type of `event` unsafe { self.trigger_targets_dynamic_ref(event_id, event, targets) }; } - /// Triggers the given [`Event`] for the given `targets`, which will run any [`Observer`]s watching for it. + /// Triggers the given [`EntityEvent`] for the given `targets`, which will run any [`Observer`]s watching for it. /// /// While event types commonly implement [`Copy`], /// those that don't will be consumed and will no longer be accessible. @@ -642,7 +873,7 @@ impl World { /// /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. #[track_caller] - pub unsafe fn trigger_targets_dynamic( + pub unsafe fn trigger_targets_dynamic( &mut self, event_id: ComponentId, mut event_data: E, @@ -654,7 +885,7 @@ impl World { }; } - /// Triggers the given [`Event`] as a mutable reference for the given `targets`, + /// Triggers the given [`EntityEvent`] as a mutable reference for the given `targets`, /// which will run any [`Observer`]s watching for it. /// /// Compared to [`World::trigger_targets_dynamic`], this method is most useful when it's necessary to check @@ -664,7 +895,7 @@ impl World { /// /// Caller must ensure that `event_data` is accessible as the type represented by `event_id`. #[track_caller] - pub unsafe fn trigger_targets_dynamic_ref( + pub unsafe fn trigger_targets_dynamic_ref( &mut self, event_id: ComponentId, event_data: &mut E, @@ -681,7 +912,7 @@ impl World { /// # Safety /// /// See `trigger_targets_dynamic_ref` - unsafe fn trigger_targets_dynamic_ref_with_caller( + unsafe fn trigger_targets_dynamic_ref_with_caller( &mut self, event_id: ComponentId, event_data: &mut E, @@ -695,7 +926,8 @@ impl World { unsafe { world.trigger_observers_with_data::<_, E::Traversal>( event_id, - Entity::PLACEHOLDER, + None, + None, targets.components(), event_data, false, @@ -708,7 +940,8 @@ impl World { unsafe { world.trigger_observers_with_data::<_, E::Traversal>( event_id, - target_entity, + Some(target_entity), + Some(target_entity), targets.components(), event_data, E::AUTO_PROPAGATE, @@ -735,10 +968,12 @@ impl World { let descriptor = &observer_state.descriptor; for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + let cache = observers.get_observers_mut(event_type); if descriptor.components.is_empty() && descriptor.entities.is_empty() { - cache.map.insert(observer_entity, observer_state.runner); + cache + .global_observers + .insert(observer_entity, observer_state.runner); } else if descriptor.components.is_empty() { // Observer is not targeting any components so register it as an entity observer for &watched_entity in &observer_state.descriptor.entities { @@ -760,11 +995,16 @@ impl World { }); if descriptor.entities.is_empty() { // Register for all triggers targeting the component - observers.map.insert(observer_entity, observer_state.runner); + observers + .global_observers + .insert(observer_entity, observer_state.runner); } else { // Register for each watched entity for &watched_entity in &descriptor.entities { - let map = observers.entity_map.entry(watched_entity).or_default(); + let map = observers + .entity_component_observers + .entry(watched_entity) + .or_default(); map.insert(observer_entity, observer_state.runner); } } @@ -779,9 +1019,9 @@ impl World { let observers = &mut self.observers; for &event_type in &descriptor.events { - let cache = observers.get_observers(event_type); + let cache = observers.get_observers_mut(event_type); if descriptor.components.is_empty() && descriptor.entities.is_empty() { - cache.map.remove(&entity); + cache.global_observers.remove(&entity); } else if descriptor.components.is_empty() { for watched_entity in &descriptor.entities { // This check should be unnecessary since this observer hasn't been unregistered yet @@ -799,20 +1039,24 @@ impl World { continue; }; if descriptor.entities.is_empty() { - observers.map.remove(&entity); + observers.global_observers.remove(&entity); } else { for watched_entity in &descriptor.entities { - let Some(map) = observers.entity_map.get_mut(watched_entity) else { + let Some(map) = + observers.entity_component_observers.get_mut(watched_entity) + else { continue; }; map.remove(&entity); if map.is_empty() { - observers.entity_map.remove(watched_entity); + observers.entity_component_observers.remove(watched_entity); } } } - if observers.map.is_empty() && observers.entity_map.is_empty() { + if observers.global_observers.is_empty() + && observers.entity_component_observers.is_empty() + { cache.component_observers.remove(component); if let Some(flag) = Observers::is_archetype_cached(event_type) { if let Some(by_component) = archetypes.by_component.get(component) { @@ -847,7 +1091,7 @@ mod tests { use crate::component::ComponentId; use crate::{ change_detection::MaybeLocation, - observer::{Observer, OnReplace}, + observer::{Observer, Replace}, prelude::*, traversal::Traversal, }; @@ -865,10 +1109,10 @@ mod tests { #[component(storage = "SparseSet")] struct S; - #[derive(Event)] + #[derive(Event, EntityEvent)] struct EventA; - #[derive(Event)] + #[derive(Event, EntityEvent)] struct EventWithData { counter: usize, } @@ -887,13 +1131,13 @@ mod tests { struct ChildOf(Entity); impl Traversal for &'_ ChildOf { - fn traverse(item: Self::Item<'_>, _: &D) -> Option { + fn traverse(item: Self::Item<'_, '_>, _: &D) -> Option { Some(item.0) } } - #[derive(Component, Event)] - #[event(traversal = &'static ChildOf, auto_propagate)] + #[derive(Component, Event, EntityEvent)] + #[entity_event(traversal = &'static ChildOf, auto_propagate)] struct EventPropagating; #[test] @@ -901,14 +1145,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let entity = world.spawn(A).id(); world.despawn(entity); @@ -923,14 +1165,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let mut entity = world.spawn_empty(); entity.insert(A); @@ -947,14 +1187,12 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); let mut entity = world.spawn_empty(); entity.insert(S); @@ -973,14 +1211,12 @@ mod tests { let entity = world.spawn(A).id(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add")); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("insert")); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| res.observed("add")); + world.add_observer(|_: On, mut res: ResMut| res.observed("insert")); + world.add_observer(|_: On, mut res: ResMut| { res.observed("replace"); }); - world - .add_observer(|_: Trigger, mut res: ResMut| res.observed("remove")); + world.add_observer(|_: On, mut res: ResMut| res.observed("remove")); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut // and therefore does not automatically flush. @@ -997,25 +1233,25 @@ mod tests { let mut world = World::new(); world.init_resource::(); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_a"); commands.entity(obs.target()).insert(B); }, ); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("remove_a"); commands.entity(obs.target()).remove::(); }, ); world.add_observer( - |obs: Trigger, mut res: ResMut, mut commands: Commands| { + |obs: On, mut res: ResMut, mut commands: Commands| { res.observed("add_b"); commands.entity(obs.target()).remove::(); }, ); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("remove_b"); }); @@ -1033,9 +1269,9 @@ mod tests { fn observer_trigger_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 1); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 2); - world.add_observer(|mut trigger: Trigger| trigger.event_mut().counter += 4); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 1); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 2); + world.add_observer(|mut trigger: On| trigger.event_mut().counter += 4); // This flush is required for the last observer to be called when triggering the event, // due to `World::add_observer` returning `WorldEntityMut`. world.flush(); @@ -1049,13 +1285,13 @@ mod tests { fn observer_trigger_targets_ref() { let mut world = World::new(); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 1; }); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 2; }); - world.add_observer(|mut trigger: Trigger| { + world.add_observer(|mut trigger: On| { trigger.event_mut().counter += 4; }); // This flush is required for the last observer to be called when triggering the event, @@ -1073,8 +1309,8 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add_1")); - world.add_observer(|_: Trigger, mut res: ResMut| res.observed("add_2")); + world.add_observer(|_: On, mut res: ResMut| res.observed("add_1")); + world.add_observer(|_: On, mut res: ResMut| res.observed("add_2")); world.spawn(A).flush(); assert_eq!(vec!["add_2", "add_1"], world.resource::().0); @@ -1086,11 +1322,11 @@ mod tests { fn observer_multiple_events() { let mut world = World::new(); world.init_resource::(); - let on_remove = OnRemove::register_component_id(&mut world); + let on_remove = Remove::register_component_id(&mut world); world.spawn( - // SAFETY: OnAdd and OnRemove are both unit types, so this is safe + // SAFETY: Add and Remove are both unit types, so this is safe unsafe { - Observer::new(|_: Trigger, mut res: ResMut| { + Observer::new(|_: On, mut res: ResMut| { res.observed("add/remove"); }) .with_event(on_remove) @@ -1112,7 +1348,7 @@ mod tests { world.register_component::(); world.register_component::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("add_ab"); }); @@ -1126,7 +1362,7 @@ mod tests { fn observer_despawn() { let mut world = World::new(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Observer triggered after being despawned."); }; let observer = world.add_observer(system).id(); @@ -1142,11 +1378,11 @@ mod tests { let entity = world.spawn((A, B)).flush(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("remove_a"); }); - let system: fn(Trigger) = |_: Trigger| { + let system: fn(On) = |_: On| { panic!("Observer triggered after being despawned."); }; @@ -1163,7 +1399,7 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("add_ab"); }); @@ -1176,11 +1412,11 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Trigger routed to non-targeted entity."); }; world.spawn_empty().observe(system); - world.add_observer(move |obs: Trigger, mut res: ResMut| { + world.add_observer(move |obs: On, mut res: ResMut| { assert_eq!(obs.target(), Entity::PLACEHOLDER); res.observed("event_a"); }); @@ -1198,16 +1434,16 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let system: fn(Trigger) = |_| { + let system: fn(On) = |_| { panic!("Trigger routed to non-targeted entity."); }; world.spawn_empty().observe(system); let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("a_1")) + .observe(|_: On, mut res: ResMut| res.observed("a_1")) .id(); - world.add_observer(move |obs: Trigger, mut res: ResMut| { + world.add_observer(move |obs: On, mut res: ResMut| { assert_eq!(obs.target(), entity); res.observed("a_2"); }); @@ -1233,26 +1469,26 @@ mod tests { // targets (entity_1, A) let entity_1 = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 1) + .observe(|_: On, mut res: ResMut| res.0 += 1) .id(); // targets (entity_2, B) let entity_2 = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.0 += 10) + .observe(|_: On, mut res: ResMut| res.0 += 10) .id(); // targets any entity or component - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 100); + world.add_observer(|_: On, mut res: ResMut| res.0 += 100); // targets any entity, and components A or B - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 1000); + world.add_observer(|_: On, mut res: ResMut| res.0 += 1000); // test all tuples - world.add_observer(|_: Trigger, mut res: ResMut| res.0 += 10000); + world.add_observer(|_: On, mut res: ResMut| res.0 += 10000); world.add_observer( - |_: Trigger, mut res: ResMut| { + |_: On, mut res: ResMut| { res.0 += 100000; }, ); world.add_observer( - |_: Trigger, + |_: On, mut res: ResMut| res.0 += 1000000, ); @@ -1340,7 +1576,7 @@ mod tests { let component_id = world.register_component::(); world.spawn( - Observer::new(|_: Trigger, mut res: ResMut| res.observed("event_a")) + Observer::new(|_: On, mut res: ResMut| res.observed("event_a")) .with_component(component_id), ); @@ -1360,7 +1596,7 @@ mod tests { fn observer_dynamic_trigger() { let mut world = World::new(); world.init_resource::(); - let event_a = OnRemove::register_component_id(&mut world); + let event_a = Remove::register_component_id(&mut world); // SAFETY: we registered `event_a` above and it matches the type of EventA let observe = unsafe { @@ -1384,21 +1620,27 @@ mod tests { let mut world = World::new(); world.init_resource::(); - let parent = world - .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + let parent = world.spawn_empty().id(); + let child = world.spawn(ChildOf(parent)).id(); + + world.entity_mut(parent).observe( + move |trigger: On, mut res: ResMut| { res.observed("parent"); - }) - .id(); - let child = world - .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + assert_eq!(trigger.target(), parent); + assert_eq!(trigger.original_target(), child); + }, + ); + + world.entity_mut(child).observe( + move |trigger: On, mut res: ResMut| { res.observed("child"); - }) - .id(); + assert_eq!(trigger.target(), child); + assert_eq!(trigger.original_target(), child); + }, + ); - // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // TODO: ideally this flush is not necessary, but right now observe() returns EntityWorldMut // and therefore does not automatically flush. world.flush(); world.trigger_targets(EventPropagating, child); @@ -1413,14 +1655,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child"); }) .id(); @@ -1443,14 +1685,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child"); }) .id(); @@ -1473,7 +1715,7 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); @@ -1481,7 +1723,7 @@ mod tests { let child = world .spawn(ChildOf(parent)) .observe( - |mut trigger: Trigger, mut res: ResMut| { + |mut trigger: On, mut res: ResMut| { res.observed("child"); trigger.propagate(false); }, @@ -1503,21 +1745,21 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent"); }) .id(); let child_a = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_a"); }) .id(); let child_b = world .spawn(ChildOf(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_b"); }) .id(); @@ -1540,7 +1782,7 @@ mod tests { let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("event"); }) .id(); @@ -1560,7 +1802,7 @@ mod tests { let parent_a = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent_a"); }) .id(); @@ -1568,7 +1810,7 @@ mod tests { let child_a = world .spawn(ChildOf(parent_a)) .observe( - |mut trigger: Trigger, mut res: ResMut| { + |mut trigger: On, mut res: ResMut| { res.observed("child_a"); trigger.propagate(false); }, @@ -1577,14 +1819,14 @@ mod tests { let parent_b = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("parent_b"); }) .id(); let child_b = world .spawn(ChildOf(parent_b)) - .observe(|_: Trigger, mut res: ResMut| { + .observe(|_: On, mut res: ResMut| { res.observed("child_b"); }) .id(); @@ -1605,7 +1847,7 @@ mod tests { let mut world = World::new(); world.init_resource::(); - world.add_observer(|_: Trigger, mut res: ResMut| { + world.add_observer(|_: On, mut res: ResMut| { res.observed("event"); }); @@ -1627,7 +1869,7 @@ mod tests { world.init_resource::(); world.add_observer( - |trigger: Trigger, query: Query<&A>, mut res: ResMut| { + |trigger: On, query: Query<&A>, mut res: ResMut| { if query.get(trigger.target()).is_ok() { res.observed("event"); } @@ -1649,7 +1891,7 @@ mod tests { // Originally for https://github.com/bevyengine/bevy/issues/18452 #[test] fn observer_modifies_relationship() { - fn on_add(trigger: Trigger, mut commands: Commands) { + fn on_add(trigger: On, mut commands: Commands) { commands .entity(trigger.target()) .with_related_entities::(|rsc| { @@ -1670,7 +1912,7 @@ mod tests { let mut world = World::new(); // Observe the removal of A - this will run during despawn - world.add_observer(|_: Trigger, mut cmd: Commands| { + world.add_observer(|_: On, mut cmd: Commands| { // Spawn a new entity - this reserves a new ID and requires a flush // afterward before Entities::free can be called. cmd.spawn_empty(); @@ -1678,7 +1920,7 @@ mod tests { let ent = world.spawn(A).id(); - // Despawn our entity, which runs the OnRemove observer and allocates a + // Despawn our entity, which runs the Remove observer and allocates a // new Entity. // Should not panic - if it does, then Entities was not flushed properly // after the observer's spawn_empty. @@ -1696,7 +1938,7 @@ mod tests { let mut world = World::new(); // This fails because `ResA` is not present in the world - world.add_observer(|_: Trigger, _: Res, mut commands: Commands| { + world.add_observer(|_: On, _: Res, mut commands: Commands| { commands.insert_resource(ResB); }); world.trigger(EventA); @@ -1709,7 +1951,7 @@ mod tests { let mut world = World::new(); world.add_observer( - |_: Trigger, mut params: ParamSet<(Query, Commands)>| { + |_: On, mut params: ParamSet<(Query, Commands)>| { params.p1().insert_resource(ResA); }, ); @@ -1730,7 +1972,7 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); world.trigger(EventA); @@ -1744,10 +1986,10 @@ mod tests { let caller = MaybeLocation::caller(); let mut world = World::new(); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); - world.add_observer(move |trigger: Trigger| { + world.add_observer(move |trigger: On| { assert_eq!(trigger.caller(), caller); }); world.commands().spawn(Component).clear(); @@ -1765,7 +2007,7 @@ mod tests { let b_id = world.register_component::(); world.add_observer( - |trigger: Trigger, mut counter: ResMut| { + |trigger: On, mut counter: ResMut| { for &component in trigger.components() { *counter.0.entry(component).or_default() += 1; } diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index 43ece18ff5..d6bffd8f22 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -1,9 +1,11 @@ use alloc::{boxed::Box, vec}; +use bevy_utils::prelude::DebugName; use core::any::Any; use crate::{ - component::{ComponentHook, ComponentId, HookContext, Mutable, StorageType}, + component::{ComponentId, Mutable, StorageType}, error::{ErrorContext, ErrorHandler}, + lifecycle::{ComponentHook, HookContext}, observer::{ObserverDescriptor, ObserverTrigger}, prelude::*, query::DebugCheckedUnwrap, @@ -20,15 +22,16 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// An [`Observer`] system. Add this [`Component`] to an [`Entity`] to turn it into an "observer". /// -/// Observers listen for a "trigger" of a specific [`Event`]. Events are triggered by calling [`World::trigger`] or [`World::trigger_targets`]. +/// Observers listen for a "trigger" of a specific [`Event`]. An event can be triggered on the [`World`] +/// by calling [`World::trigger`], or if the event is an [`EntityEvent`], it can also be triggered for specific +/// entity targets using [`World::trigger_targets`]. /// -/// Note that "buffered" events sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. They must be triggered at a specific -/// point in the schedule. +/// Note that [`BufferedEvent`]s sent using [`EventReader`] and [`EventWriter`] are _not_ automatically triggered. +/// They must be triggered at a specific point in the schedule. /// /// # Usage /// -/// The simplest usage -/// of the observer pattern looks like this: +/// The simplest usage of the observer pattern looks like this: /// /// ``` /// # use bevy_ecs::prelude::*; @@ -38,7 +41,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// message: String, /// } /// -/// world.add_observer(|trigger: Trigger| { +/// world.add_observer(|trigger: On| { /// println!("{}", trigger.event().message); /// }); /// @@ -59,8 +62,8 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # #[derive(Event)] /// # struct Speak; /// // These are functionally the same: -/// world.add_observer(|trigger: Trigger| {}); -/// world.spawn(Observer::new(|trigger: Trigger| {})); +/// world.add_observer(|trigger: On| {}); +/// world.spawn(Observer::new(|trigger: On| {})); /// ``` /// /// Observers are systems. They can access arbitrary [`World`] data by adding [`SystemParam`]s: @@ -72,14 +75,14 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # struct PrintNames; /// # #[derive(Component, Debug)] /// # struct Name; -/// world.add_observer(|trigger: Trigger, names: Query<&Name>| { +/// world.add_observer(|trigger: On, names: Query<&Name>| { /// for name in &names { /// println!("{name:?}"); /// } /// }); /// ``` /// -/// Note that [`Trigger`] must always be the first parameter. +/// Note that [`On`] must always be the first parameter. /// /// You can also add [`Commands`], which means you can spawn new entities, insert new components, etc: /// @@ -90,7 +93,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # struct SpawnThing; /// # #[derive(Component, Debug)] /// # struct Thing; -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { +/// world.add_observer(|trigger: On, mut commands: Commands| { /// commands.spawn(Thing); /// }); /// ``` @@ -104,7 +107,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # struct A; /// # #[derive(Event)] /// # struct B; -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { +/// world.add_observer(|trigger: On, mut commands: Commands| { /// commands.trigger(B); /// }); /// ``` @@ -113,16 +116,17 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// recursively evaluated until there are no commands left, meaning nested triggers all /// evaluate at the same time! /// -/// Events can be triggered for entities, which will be passed to the [`Observer`]: +/// If the event is an [`EntityEvent`], it can be triggered for specific entities, +/// which will be passed to the [`Observer`]: /// /// ``` /// # use bevy_ecs::prelude::*; /// # let mut world = World::default(); /// # let entity = world.spawn_empty().id(); -/// #[derive(Event)] +/// #[derive(Event, EntityEvent)] /// struct Explode; /// -/// world.add_observer(|trigger: Trigger, mut commands: Commands| { +/// world.add_observer(|trigger: On, mut commands: Commands| { /// println!("Entity {} goes BOOM!", trigger.target()); /// commands.entity(trigger.target()).despawn(); /// }); @@ -139,7 +143,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # let mut world = World::default(); /// # let e1 = world.spawn_empty().id(); /// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event)] +/// # #[derive(Event, EntityEvent)] /// # struct Explode; /// world.trigger_targets(Explode, [e1, e2]); /// ``` @@ -153,14 +157,14 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # let mut world = World::default(); /// # let e1 = world.spawn_empty().id(); /// # let e2 = world.spawn_empty().id(); -/// # #[derive(Event)] +/// # #[derive(Event, EntityEvent)] /// # struct Explode; -/// world.entity_mut(e1).observe(|trigger: Trigger, mut commands: Commands| { +/// world.entity_mut(e1).observe(|trigger: On, mut commands: Commands| { /// println!("Boom!"); /// commands.entity(trigger.target()).despawn(); /// }); /// -/// world.entity_mut(e2).observe(|trigger: Trigger, mut commands: Commands| { +/// world.entity_mut(e2).observe(|trigger: On, mut commands: Commands| { /// println!("The explosion fizzles! This entity is immune!"); /// }); /// ``` @@ -175,9 +179,9 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # use bevy_ecs::prelude::*; /// # let mut world = World::default(); /// # let entity = world.spawn_empty().id(); -/// # #[derive(Event)] +/// # #[derive(Event, EntityEvent)] /// # struct Explode; -/// let mut observer = Observer::new(|trigger: Trigger| {}); +/// let mut observer = Observer::new(|trigger: On| {}); /// observer.watch_entity(entity); /// world.spawn(observer); /// ``` @@ -191,7 +195,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: pub struct Observer { hook_on_add: ComponentHook, error_handler: Option, - system: Box, + system: Box, pub(crate) descriptor: ObserverDescriptor, pub(crate) last_trigger_id: u32, pub(crate) despawned_watched_entities: u32, @@ -229,7 +233,7 @@ impl Observer { /// Creates a new [`Observer`] with custom runner, this is mostly used for dynamic event observer pub fn with_dynamic_runner(runner: ObserverRunner) -> Self { Self { - system: Box::new(|| {}), + system: Box::new(IntoSystem::into_system(|| {})), descriptor: Default::default(), hook_on_add: |mut world, hook_context| { let default_error_handler = world.default_error_handler(); @@ -296,6 +300,11 @@ impl Observer { pub fn descriptor(&self) -> &ObserverDescriptor { &self.descriptor } + + /// Returns the name of the [`Observer`]'s system . + pub fn system_name(&self) -> DebugName { + self.system.system_name() + } } impl Component for Observer { @@ -350,7 +359,7 @@ fn observer_system_runner>( } state.last_trigger_id = last_trigger; - let trigger: Trigger = Trigger::new( + let trigger: On = On::new( // SAFETY: Caller ensures `ptr` is castable to `&mut T` unsafe { ptr.deref_mut() }, propagate, @@ -361,7 +370,8 @@ fn observer_system_runner>( // - observer was triggered so must have an `Observer` component. // - observer cannot be dropped or mutated until after the system pointer is already dropped. let system: *mut dyn ObserverSystem = unsafe { - let system = state.system.downcast_mut::().debug_checked_unwrap(); + let system: &mut dyn Any = state.system.as_mut(); + let system = system.downcast_mut::().debug_checked_unwrap(); &mut *system }; @@ -410,7 +420,17 @@ fn observer_system_runner>( } } -/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::component::ComponentHooks::on_add`). +trait AnyNamedSystem: Any + Send + Sync + 'static { + fn system_name(&self) -> DebugName; +} + +impl AnyNamedSystem for T { + fn system_name(&self) -> DebugName { + self.name() + } +} + +/// A [`ComponentHook`] used by [`Observer`] to handle its [`on-add`](`crate::lifecycle::ComponentHooks::on_add`). /// /// This function exists separate from [`Observer`] to allow [`Observer`] to have its type parameters /// erased. @@ -428,11 +448,12 @@ fn hook_on_add>( B::component_ids(&mut world.components_registrator(), &mut |id| { components.push(id); }); - if let Some(mut observe) = world.get_mut::(entity) { - observe.descriptor.events.push(event_id); - observe.descriptor.components.extend(components); + if let Some(mut observer) = world.get_mut::(entity) { + observer.descriptor.events.push(event_id); + observer.descriptor.components.extend(components); - let system: *mut dyn ObserverSystem = observe.system.downcast_mut::().unwrap(); + let system: &mut dyn Any = observer.system.as_mut(); + let system: *mut dyn ObserverSystem = system.downcast_mut::().unwrap(); // SAFETY: World reference is exclusive and initialize does not touch system, so references do not alias unsafe { (*system).initialize(world); @@ -447,7 +468,7 @@ mod tests { use crate::{ error::{ignore, DefaultErrorHandler}, event::Event, - observer::Trigger, + observer::On, }; #[derive(Event)] @@ -456,7 +477,7 @@ mod tests { #[test] #[should_panic(expected = "I failed!")] fn test_fallible_observer() { - fn system(_: Trigger) -> Result { + fn system(_: On) -> Result { Err("I failed!".into()) } @@ -471,7 +492,7 @@ mod tests { #[derive(Resource, Default)] struct Ran(bool); - fn system(_: Trigger, mut ran: ResMut) -> Result { + fn system(_: On, mut ran: ResMut) -> Result { ran.0 = true; Err("I failed!".into()) } @@ -499,7 +520,7 @@ mod tests { expected = "Exclusive system `bevy_ecs::observer::runner::tests::exclusive_system_cannot_be_observer::system` may not be used as observer.\nInstead of `&mut World`, use either `DeferredWorld` if you do not need structural changes, or `Commands` if you do." )] fn exclusive_system_cannot_be_observer() { - fn system(_: Trigger, _world: &mut World) {} + fn system(_: On, _world: &mut World) {} let mut world = World::default(); world.add_observer(system); } diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index 9c63cb5a74..0c5b29f715 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -4,7 +4,6 @@ use crate::world::World; use alloc::{format, string::String, vec, vec::Vec}; use core::{fmt, fmt::Debug, marker::PhantomData}; use derive_more::From; -use disqualified::ShortName; use fixedbitset::FixedBitSet; use thiserror::Error; @@ -999,12 +998,11 @@ impl AccessConflicts { .map(|index| { format!( "{}", - ShortName( - &world - .components - .get_name(ComponentId::get_sparse_set_index(index)) - .unwrap() - ) + world + .components + .get_name(ComponentId::get_sparse_set_index(index)) + .unwrap() + .shortname() ) }) .collect::>() diff --git a/crates/bevy_ecs/src/query/error.rs b/crates/bevy_ecs/src/query/error.rs index 6d0b149b86..fd431f4be1 100644 --- a/crates/bevy_ecs/src/query/error.rs +++ b/crates/bevy_ecs/src/query/error.rs @@ -1,3 +1,4 @@ +use bevy_utils::prelude::DebugName; use thiserror::Error; use crate::{ @@ -54,10 +55,10 @@ impl core::fmt::Display for QueryEntityError { pub enum QuerySingleError { /// No entity fits the query. #[error("No entities fit the query {0}")] - NoEntities(&'static str), + NoEntities(DebugName), /// Multiple entities fit the query. #[error("Multiple entities fit the query {0}")] - MultipleEntities(&'static str), + MultipleEntities(DebugName), } #[cfg(test)] diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index cffba8cda1..0a13b61819 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -12,6 +12,7 @@ use crate::{ }, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{cell::UnsafeCell, marker::PhantomData, panic::Location}; use smallvec::SmallVec; use variadics_please::all_tuples; @@ -47,6 +48,8 @@ use variadics_please::all_tuples; /// - **[`Ref`].** /// Similar to change detection filters but it is used as a query fetch parameter. /// It exposes methods to check for changes to the wrapped component. +/// - **[`Mut`].** +/// Mutable component access, with change detection data. /// - **[`Has`].** /// Returns a bool indicating whether the entity has the specified component. /// @@ -161,7 +164,7 @@ use variadics_please::all_tuples; /// } /// /// // `HealthQueryItem` is only available when accessing the query with mutable methods. -/// impl<'w> HealthQueryItem<'w> { +/// impl<'w, 's> HealthQueryItem<'w, 's> { /// fn damage(&mut self, value: f32) { /// self.health.0 -= value; /// } @@ -172,7 +175,7 @@ use variadics_please::all_tuples; /// } /// /// // `HealthQueryReadOnlyItem` is only available when accessing the query with immutable methods. -/// impl<'w> HealthQueryReadOnlyItem<'w> { +/// impl<'w, 's> HealthQueryReadOnlyItem<'w, 's> { /// fn total(&self) -> f32 { /// self.health.0 + self.buff.map_or(0.0, |Buff(buff)| *buff) /// } @@ -288,10 +291,12 @@ pub unsafe trait QueryData: WorldQuery { /// The item returned by this [`WorldQuery`] /// This will be the data retrieved by the query, /// and is visible to the end user when calling e.g. `Query::get`. - type Item<'a>; + type Item<'w, 's>; /// This function manually implements subtyping for the query items. - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort>; + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's>; /// Offers additional access above what we requested in `update_component_access`. /// Implementations may add additional access that is a subset of `available_access` @@ -320,11 +325,12 @@ pub unsafe trait QueryData: WorldQuery { /// - Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// `table_row` must be in the range of the current table and archetype. /// - There must not be simultaneous conflicting component access registered in `update_component_access`. - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w>; + ) -> Self::Item<'w, 's>; } /// A [`QueryData`] that is read only. @@ -335,9 +341,20 @@ pub unsafe trait QueryData: WorldQuery { pub unsafe trait ReadOnlyQueryData: QueryData {} /// The item type returned when a [`WorldQuery`] is iterated over -pub type QueryItem<'w, Q> = ::Item<'w>; +pub type QueryItem<'w, 's, Q> = ::Item<'w, 's>; /// The read-only variant of the item type returned when a [`QueryData`] is iterated over immutably -pub type ROQueryItem<'w, D> = QueryItem<'w, ::ReadOnly>; +pub type ROQueryItem<'w, 's, D> = QueryItem<'w, 's, ::ReadOnly>; + +/// A [`QueryData`] that does not borrow from its [`QueryState`](crate::query::QueryState). +/// +/// This is implemented by most `QueryData` types. +/// The main exceptions are [`FilteredEntityRef`], [`FilteredEntityMut`], [`EntityRefExcept`], and [`EntityMutExcept`], +/// which borrow an access list from their query state. +/// Consider using a full [`EntityRef`] or [`EntityMut`] if you would need those. +pub trait ReleaseStateQueryData: QueryData { + /// Releases the borrow from the query state by converting an item to have a `'static` state lifetime. + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static>; +} /// SAFETY: /// `update_component_access` does nothing. @@ -348,9 +365,9 @@ unsafe impl WorldQuery for Entity { fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -359,16 +376,20 @@ unsafe impl WorldQuery for Entity { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -392,18 +413,21 @@ unsafe impl QueryData for Entity { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { entity } } @@ -411,6 +435,12 @@ unsafe impl QueryData for Entity { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for Entity {} +impl ReleaseStateQueryData for Entity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. @@ -422,9 +452,9 @@ unsafe impl WorldQuery for EntityLocation { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -436,16 +466,20 @@ unsafe impl WorldQuery for EntityLocation { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -468,18 +502,21 @@ unsafe impl WorldQuery for EntityLocation { unsafe impl QueryData for EntityLocation { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityLocation; + type Item<'w, 's> = EntityLocation; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world unsafe { fetch.get(entity).debug_checked_unwrap() } } @@ -488,6 +525,12 @@ unsafe impl QueryData for EntityLocation { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityLocation {} +impl ReleaseStateQueryData for EntityLocation { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The `SpawnDetails` query parameter fetches the [`Tick`] the entity was spawned at. /// /// To evaluate whether the spawn happened since the last time the system ran, the system @@ -568,9 +611,9 @@ unsafe impl WorldQuery for SpawnDetails { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -584,16 +627,20 @@ unsafe impl WorldQuery for SpawnDetails { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -618,18 +665,21 @@ unsafe impl WorldQuery for SpawnDetails { unsafe impl QueryData for SpawnDetails { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Self; + type Item<'w, 's> = Self; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: only living entities are queried let (spawned_by, spawned_at) = unsafe { fetch @@ -648,6 +698,12 @@ unsafe impl QueryData for SpawnDetails { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for SpawnDetails {} +impl ReleaseStateQueryData for SpawnDetails { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for WorldQueries that can fetch multiple components from an entity /// ([`EntityRef`], [`EntityMut`], etc.) #[derive(Copy, Clone)] @@ -670,9 +726,9 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -686,16 +742,20 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { @@ -724,18 +784,21 @@ unsafe impl<'a> WorldQuery for EntityRef<'a> { unsafe impl<'a> QueryData for EntityRef<'a> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityRef<'w>; + type Item<'w, 's> = EntityRef<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -751,6 +814,12 @@ unsafe impl<'a> QueryData for EntityRef<'a> { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for EntityRef<'_> {} +impl ReleaseStateQueryData for EntityRef<'_> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for EntityMut<'a> { type Fetch<'w> = EntityFetch<'w>; @@ -760,9 +829,9 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -776,16 +845,20 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, access: &mut FilteredAccess) { @@ -814,18 +887,21 @@ unsafe impl<'a> WorldQuery for EntityMut<'a> { unsafe impl<'a> QueryData for EntityMut<'a> { const IS_READ_ONLY: bool = false; type ReadOnly = EntityRef<'a>; - type Item<'w> = EntityMut<'w>; + type Item<'w, 's> = EntityMut<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -838,6 +914,12 @@ unsafe impl<'a> QueryData for EntityMut<'a> { } } +impl ReleaseStateQueryData for EntityMut<'_> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// SAFETY: The accesses of `Self::ReadOnly` are a subset of the accesses of `Self` unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { type Fetch<'w> = (EntityFetch<'w>, Access); @@ -849,9 +931,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { const IS_DENSE: bool = false; - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -868,9 +950,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, _: &'w Archetype, _table: &Table, ) { @@ -878,7 +960,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Table) { fetch.1.clone_from(state); } @@ -913,9 +995,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityRef<'a> { unsafe impl<'a> QueryData for FilteredEntityRef<'a> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = FilteredEntityRef<'w>; + type Item<'w, 's> = FilteredEntityRef<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } @@ -939,11 +1023,12 @@ unsafe impl<'a> QueryData for FilteredEntityRef<'a> { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -970,9 +1055,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { const IS_DENSE: bool = false; - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -989,9 +1074,9 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, _: &'w Archetype, _table: &Table, ) { @@ -999,7 +1084,7 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, _: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, _: &'w Table) { fetch.1.clone_from(state); } @@ -1034,9 +1119,11 @@ unsafe impl<'a> WorldQuery for FilteredEntityMut<'a> { unsafe impl<'a> QueryData for FilteredEntityMut<'a> { const IS_READ_ONLY: bool = false; type ReadOnly = FilteredEntityRef<'a>; - type Item<'w> = FilteredEntityMut<'w>; + type Item<'w, 's> = FilteredEntityMut<'w>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } @@ -1058,11 +1145,12 @@ unsafe impl<'a> QueryData for FilteredEntityMut<'a> { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, (fetch, access): &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: `fetch` must be called with an entity that exists in the world let cell = unsafe { fetch @@ -1089,9 +1177,9 @@ where fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _: &Self::State, + _: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -1104,15 +1192,15 @@ where const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _: &mut Self::Fetch<'w>, - _: &Self::State, + _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w>(_: &mut Self::Fetch<'w>, _: &Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1128,7 +1216,7 @@ where assert!( access.is_compatible(&my_access), "`EntityRefExcept<{}>` conflicts with a previous access in this query.", - core::any::type_name::(), + DebugName::type_name::(), ); access.extend(&my_access); } @@ -1159,17 +1247,20 @@ where { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = EntityRefExcept<'w, B>; + type Item<'w, 's> = EntityRefExcept<'w, B>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let cell = fetch .world .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) @@ -1196,9 +1287,9 @@ where fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _: &Self::State, + _: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -1211,15 +1302,15 @@ where const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _: &mut Self::Fetch<'w>, - _: &Self::State, + _: &'s Self::State, _: &'w Archetype, _: &'w Table, ) { } - unsafe fn set_table<'w>(_: &mut Self::Fetch<'w>, _: &Self::State, _: &'w Table) {} + unsafe fn set_table<'w, 's>(_: &mut Self::Fetch<'w>, _: &'s Self::State, _: &'w Table) {} fn update_component_access( state: &Self::State, @@ -1235,7 +1326,7 @@ where assert!( access.is_compatible(&my_access), "`EntityMutExcept<{}>` conflicts with a previous access in this query.", - core::any::type_name::() + DebugName::type_name::() ); access.extend(&my_access); } @@ -1267,17 +1358,20 @@ where { const IS_READ_ONLY: bool = false; type ReadOnly = EntityRefExcept<'a, B>; - type Item<'w> = EntityMutExcept<'w, B>; + type Item<'w, 's> = EntityMutExcept<'w, B>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let cell = fetch .world .get_entity_with_ticks(entity, fetch.last_run, fetch.this_run) @@ -1297,9 +1391,9 @@ unsafe impl WorldQuery for &Archetype { fetch } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -1311,16 +1405,20 @@ unsafe impl WorldQuery for &Archetype { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -1343,18 +1441,21 @@ unsafe impl WorldQuery for &Archetype { unsafe impl QueryData for &Archetype { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = &'w Archetype; + type Item<'w, 's> = &'w Archetype; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let (entities, archetypes) = *fetch; // SAFETY: `fetch` must be called with an entity that exists in the world let location = unsafe { entities.get(entity).debug_checked_unwrap() }; @@ -1366,6 +1467,12 @@ unsafe impl QueryData for &Archetype { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for &Archetype {} +impl ReleaseStateQueryData for &Archetype { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for `& T`. pub struct ReadFetch<'w, T: Component> { components: StorageSwitch< @@ -1398,7 +1505,7 @@ unsafe impl WorldQuery for &T { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, _last_run: Tick, @@ -1463,7 +1570,7 @@ unsafe impl WorldQuery for &T { assert!( !access.access().has_component_write(component_id), "&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_read(component_id); } @@ -1488,18 +1595,21 @@ unsafe impl WorldQuery for &T { unsafe impl QueryData for &T { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = &'w T; + type Item<'w, 's> = &'w T; - fn shrink<'wlong: 'wshort, 'wshort>(item: &'wlong T) -> &'wshort T { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1525,6 +1635,12 @@ unsafe impl QueryData for &T { /// SAFETY: access is read only unsafe impl ReadOnlyQueryData for &T {} +impl ReleaseStateQueryData for &T { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + #[doc(hidden)] pub struct RefFetch<'w, T: Component> { components: StorageSwitch< @@ -1565,7 +1681,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, last_run: Tick, @@ -1639,7 +1755,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { assert!( !access.access().has_component_write(component_id), "&{} conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_read(component_id); } @@ -1664,18 +1780,21 @@ unsafe impl<'__w, T: Component> WorldQuery for Ref<'__w, T> { unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = Ref<'w, T>; + type Item<'w, 's> = Ref<'w, T>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Ref<'wlong, T>) -> Ref<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1724,6 +1843,12 @@ unsafe impl<'__w, T: Component> QueryData for Ref<'__w, T> { /// SAFETY: access is read only unsafe impl<'__w, T: Component> ReadOnlyQueryData for Ref<'__w, T> {} +impl ReleaseStateQueryData for Ref<'_, T> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The [`WorldQuery::Fetch`] type for `&mut T`. pub struct WriteFetch<'w, T: Component> { components: StorageSwitch< @@ -1764,7 +1889,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, &component_id: &ComponentId, last_run: Tick, @@ -1838,7 +1963,7 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { assert!( !access.access().has_component_read(component_id), "&mut {} conflicts with a previous access in this query. Mutable component access must be unique.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_write(component_id); } @@ -1863,18 +1988,21 @@ unsafe impl<'__w, T: Component> WorldQuery for &'__w mut T { unsafe impl<'__w, T: Component> QueryData for &'__w mut T { const IS_READ_ONLY: bool = false; type ReadOnly = &'__w T; - type Item<'w> = Mut<'w, T>; + type Item<'w, 's> = Mut<'w, T>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch.components.extract( |table| { // SAFETY: set_table was previously called @@ -1920,6 +2048,12 @@ unsafe impl<'__w, T: Component> QueryData for &'__w mut T } } +impl> ReleaseStateQueryData for &mut T { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// When `Mut` is used in a query, it will be converted to `Ref` when transformed into its read-only form, providing access to change detection methods. /// /// By contrast `&mut T` will result in a `Mut` item in mutable form to record mutations, but result in a bare `&T` in read-only form. @@ -1939,7 +2073,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { #[inline] // Forwarded to `&mut T` - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, state: &ComponentId, last_run: Tick, @@ -1978,7 +2112,7 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { assert!( !access.access().has_component_read(component_id), "Mut<{}> conflicts with a previous access in this query. Mutable component access mut be unique.", - core::any::type_name::(), + DebugName::type_name::(), ); access.add_component_write(component_id); } @@ -2006,23 +2140,32 @@ unsafe impl<'__w, T: Component> WorldQuery for Mut<'__w, T> { unsafe impl<'__w, T: Component> QueryData for Mut<'__w, T> { const IS_READ_ONLY: bool = false; type ReadOnly = Ref<'__w, T>; - type Item<'w> = Mut<'w, T>; + type Item<'w, 's> = Mut<'w, T>; // Forwarded to `&mut T` - fn shrink<'wlong: 'wshort, 'wshort>(item: Mut<'wlong, T>) -> Mut<'wshort, T> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { <&mut T as QueryData>::shrink(item) } #[inline(always)] // Forwarded to `&mut T` - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, // Rust complains about lifetime bounds not matching the trait if I directly use `WriteFetch<'w, T>` right here. // But it complains nowhere else in the entire trait implementation. fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Mut<'w, T> { - <&mut T as QueryData>::fetch(fetch, entity, table_row) + ) -> Self::Item<'w, 's> { + <&mut T as QueryData>::fetch(state, fetch, entity, table_row) + } +} + +impl> ReleaseStateQueryData for Mut<'_, T> { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item } } @@ -2057,9 +2200,9 @@ unsafe impl WorldQuery for Option { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &T::State, + state: &'s T::State, last_run: Tick, this_run: Tick, ) -> OptionFetch<'w, T> { @@ -2073,9 +2216,9 @@ unsafe impl WorldQuery for Option { const IS_DENSE: bool = T::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut OptionFetch<'w, T>, - state: &T::State, + state: &'s T::State, archetype: &'w Archetype, table: &'w Table, ) { @@ -2089,7 +2232,11 @@ unsafe impl WorldQuery for Option { } #[inline] - unsafe fn set_table<'w>(fetch: &mut OptionFetch<'w, T>, state: &T::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut OptionFetch<'w, T>, + state: &'s T::State, + table: &'w Table, + ) { fetch.matches = T::matches_component_set(state, &|id| table.has_column(id)); if fetch.matches { // SAFETY: The invariants are upheld by the caller. @@ -2134,28 +2281,37 @@ unsafe impl WorldQuery for Option { unsafe impl QueryData for Option { const IS_READ_ONLY: bool = T::IS_READ_ONLY; type ReadOnly = Option; - type Item<'w> = Option>; + type Item<'w, 's> = Option>; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item.map(T::shrink) } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { fetch .matches // SAFETY: The invariants are upheld by the caller. - .then(|| unsafe { T::fetch(&mut fetch.fetch, entity, table_row) }) + .then(|| unsafe { T::fetch(state, &mut fetch.fetch, entity, table_row) }) } } /// SAFETY: [`OptionFetch`] is read only because `T` is read only unsafe impl ReadOnlyQueryData for Option {} +impl ReleaseStateQueryData for Option { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item.map(T::release_state) + } +} + /// Returns a bool that describes if an entity has the component `T`. /// /// This can be used in a [`Query`](crate::system::Query) if you want to know whether or not entities @@ -2223,7 +2379,7 @@ pub struct Has(PhantomData); impl core::fmt::Debug for Has { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> Result<(), core::fmt::Error> { - write!(f, "Has<{}>", core::any::type_name::()) + write!(f, "Has<{}>", DebugName::type_name::()) } } @@ -2239,9 +2395,9 @@ unsafe impl WorldQuery for Has { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -2256,9 +2412,9 @@ unsafe impl WorldQuery for Has { }; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, _table: &Table, ) { @@ -2266,7 +2422,11 @@ unsafe impl WorldQuery for Has { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + state: &'s Self::State, + table: &'w Table, + ) { *fetch = table.has_column(*state); } @@ -2298,18 +2458,21 @@ unsafe impl WorldQuery for Has { unsafe impl QueryData for Has { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = bool; + type Item<'w, 's> = bool; - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { *fetch } } @@ -2317,6 +2480,12 @@ unsafe impl QueryData for Has { /// SAFETY: [`Has`] is read only unsafe impl ReadOnlyQueryData for Has {} +impl ReleaseStateQueryData for Has { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } +} + /// The `AnyOf` query parameter fetches entities with any of the component types included in T. /// /// `Query>` is equivalent to `Query<(Option<&A>, Option<&B>, Option<&mut C>), Or<(With, With, With)>>`. @@ -2325,7 +2494,7 @@ unsafe impl ReadOnlyQueryData for Has {} pub struct AnyOf(PhantomData); macro_rules! impl_tuple_query_data { - ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $item: ident, $state: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such the lints below may not always apply." @@ -2347,9 +2516,9 @@ macro_rules! impl_tuple_query_data { unsafe impl<$($name: QueryData),*> QueryData for ($($name,)*) { const IS_READ_ONLY: bool = true $(&& $name::IS_READ_ONLY)*; type ReadOnly = ($($name::ReadOnly,)*); - type Item<'w> = ($($name::Item<'w>,)*); + type Item<'w, 's> = ($($name::Item<'w, 's>,)*); - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>(item: Self::Item<'wlong, 's>) -> Self::Item<'wshort, 's> { let ($($name,)*) = item; ($( $name::shrink($name), @@ -2367,26 +2536,40 @@ macro_rules! impl_tuple_query_data { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { + let ($($state,)*) = state; let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - ($(unsafe { $name::fetch($name, entity, table_row) },)*) + ($(unsafe { $name::fetch($state, $name, entity, table_row) },)*) } } - $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for ($($name,)*) {} + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] + impl<$($name: ReleaseStateQueryData),*> ReleaseStateQueryData for ($($name,)*) { + fn release_state<'w>(($($item,)*): Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + ($($name::release_state($item),)*) + } + } }; } macro_rules! impl_anytuple_fetch { - ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident, $item: ident)),*) => { $(#[$meta])* #[expect( clippy::allow_attributes, @@ -2421,7 +2604,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - unsafe fn init_fetch<'w>(_world: UnsafeWorldCell<'w>, state: &Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(_world: UnsafeWorldCell<'w>, state: &'s Self::State, _last_run: Tick, _this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(( unsafe { $name::init_fetch(_world, $name, _last_run, _this_run) }, false),)*) @@ -2430,9 +2613,9 @@ macro_rules! impl_anytuple_fetch { const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table ) { @@ -2448,7 +2631,7 @@ macro_rules! impl_anytuple_fetch { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>(_fetch: &mut Self::Fetch<'w>, _state: &'s Self::State, _table: &'w Table) { let ($($name,)*) = _fetch; let ($($state,)*) = _state; $( @@ -2520,9 +2703,9 @@ macro_rules! impl_anytuple_fetch { unsafe impl<$($name: QueryData),*> QueryData for AnyOf<($($name,)*)> { const IS_READ_ONLY: bool = true $(&& $name::IS_READ_ONLY)*; type ReadOnly = AnyOf<($($name::ReadOnly,)*)>; - type Item<'w> = ($(Option<$name::Item<'w>>,)*); + type Item<'w, 's> = ($(Option<$name::Item<'w, 's>>,)*); - fn shrink<'wlong: 'wshort, 'wshort>(item: Self::Item<'wlong>) -> Self::Item<'wshort> { + fn shrink<'wlong: 'wshort, 'wshort, 's>(item: Self::Item<'wlong, 's>) -> Self::Item<'wshort, 's> { let ($($name,)*) = item; ($( $name.map($name::shrink), @@ -2530,15 +2713,17 @@ macro_rules! impl_anytuple_fetch { } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { let ($($name,)*) = _fetch; + let ($($state,)*) = _state; ($( // SAFETY: The invariants are required to be upheld by the caller. - $name.1.then(|| unsafe { $name::fetch(&mut $name.0, _entity, _table_row) }), + $name.1.then(|| unsafe { $name::fetch($state, &mut $name.0, _entity, _table_row) }), )*) } } @@ -2546,6 +2731,20 @@ macro_rules! impl_anytuple_fetch { $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for AnyOf<($($name,)*)> {} + + #[expect( + clippy::allow_attributes, + reason = "This is a tuple-related macro; as such the lints below may not always apply." + )] + #[allow( + clippy::unused_unit, + reason = "Zero-length tuples will generate some function bodies equivalent to `()`; however, this macro is meant for all applicable tuples, and as such it makes no sense to rewrite it just for that case." + )] + impl<$($name: ReleaseStateQueryData),*> ReleaseStateQueryData for AnyOf<($($name,)*)> { + fn release_state<'w>(($($item,)*): Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + ($($item.map(|$item| $name::release_state($item)),)*) + } + } }; } @@ -2555,7 +2754,8 @@ all_tuples!( 0, 15, F, - S + i, + s ); all_tuples!( #[doc(fake_variadic)] @@ -2563,7 +2763,8 @@ all_tuples!( 0, 15, F, - S + S, + i ); /// [`WorldQuery`] used to nullify queries by turning `Query` into `Query>` @@ -2578,7 +2779,8 @@ unsafe impl WorldQuery for NopWorldQuery { type Fetch<'w> = (); type State = D::State; - fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: ()) {} + fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { + } #[inline(always)] unsafe fn init_fetch( @@ -2625,36 +2827,44 @@ unsafe impl WorldQuery for NopWorldQuery { unsafe impl QueryData for NopWorldQuery { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_: ()) {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } /// SAFETY: `NopFetch` never accesses any data unsafe impl ReadOnlyQueryData for NopWorldQuery {} +impl ReleaseStateQueryData for NopWorldQuery { + fn release_state<'w>(_item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {} +} + /// SAFETY: /// `update_component_access` does nothing. /// This is sound because `fetch` does not access components. unsafe impl WorldQuery for PhantomData { - type Fetch<'a> = (); + type Fetch<'w> = (); type State = (); fn shrink_fetch<'wlong: 'wshort, 'wshort>(_fetch: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> { } - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -2664,15 +2874,19 @@ unsafe impl WorldQuery for PhantomData { // are stored in a Table (vacuous truth). const IS_DENSE: bool = true; - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &'w Table, ) { } - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &Self::State, _table: &'w Table) { + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { } fn update_component_access(_state: &Self::State, _access: &mut FilteredAccess) {} @@ -2695,21 +2909,29 @@ unsafe impl WorldQuery for PhantomData { unsafe impl QueryData for PhantomData { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'a> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_item: Self::Item<'wlong>) -> Self::Item<'wshort> {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } /// SAFETY: `PhantomData` never accesses any world data. unsafe impl ReadOnlyQueryData for PhantomData {} +impl ReleaseStateQueryData for PhantomData { + fn release_state<'w>(_item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> {} +} + /// A compile-time checked union of two different types that differs based on the /// [`StorageType`] of a given component. pub(super) union StorageSwitch { @@ -2826,6 +3048,124 @@ mod tests { assert_is_system(ignored_system); } + #[test] + fn derive_release_state() { + struct NonReleaseQueryData; + + /// SAFETY: + /// `update_component_access` and `update_archetype_component_access` do nothing. + /// This is sound because `fetch` does not access components. + unsafe impl WorldQuery for NonReleaseQueryData { + type Fetch<'w> = (); + type State = (); + + fn shrink_fetch<'wlong: 'wshort, 'wshort>( + _: Self::Fetch<'wlong>, + ) -> Self::Fetch<'wshort> { + } + + unsafe fn init_fetch<'w, 's>( + _world: UnsafeWorldCell<'w>, + _state: &'s Self::State, + _last_run: Tick, + _this_run: Tick, + ) -> Self::Fetch<'w> { + } + + const IS_DENSE: bool = true; + + #[inline] + unsafe fn set_archetype<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _archetype: &'w Archetype, + _table: &Table, + ) { + } + + #[inline] + unsafe fn set_table<'w, 's>( + _fetch: &mut Self::Fetch<'w>, + _state: &'s Self::State, + _table: &'w Table, + ) { + } + + fn update_component_access( + _state: &Self::State, + _access: &mut FilteredAccess, + ) { + } + + fn init_state(_world: &mut World) {} + + fn get_state(_components: &Components) -> Option<()> { + Some(()) + } + + fn matches_component_set( + _state: &Self::State, + _set_contains_id: &impl Fn(ComponentId) -> bool, + ) -> bool { + true + } + } + + /// SAFETY: `Self` is the same as `Self::ReadOnly` + unsafe impl QueryData for NonReleaseQueryData { + type ReadOnly = Self; + const IS_READ_ONLY: bool = true; + + type Item<'w, 's> = (); + + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } + + #[inline(always)] + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, + _fetch: &mut Self::Fetch<'w>, + _entity: Entity, + _table_row: TableRow, + ) -> Self::Item<'w, 's> { + } + } + + /// SAFETY: access is read only + unsafe impl ReadOnlyQueryData for NonReleaseQueryData {} + + #[derive(QueryData)] + pub struct DerivedNonReleaseRead { + non_release: NonReleaseQueryData, + a: &'static A, + } + + #[derive(QueryData)] + #[query_data(mutable)] + pub struct DerivedNonReleaseMutable { + non_release: NonReleaseQueryData, + a: &'static mut A, + } + + #[derive(QueryData)] + pub struct DerivedReleaseRead { + a: &'static A, + } + + #[derive(QueryData)] + #[query_data(mutable)] + pub struct DerivedReleaseMutable { + a: &'static mut A, + } + + fn assert_is_release_state() {} + + assert_is_release_state::(); + assert_is_release_state::(); + } + // Ensures that each field of a `WorldQuery` struct's read-only variant // has the same visibility as its corresponding mutable field. #[test] diff --git a/crates/bevy_ecs/src/query/filter.rs b/crates/bevy_ecs/src/query/filter.rs index eccc819ca7..312b330e04 100644 --- a/crates/bevy_ecs/src/query/filter.rs +++ b/crates/bevy_ecs/src/query/filter.rs @@ -7,6 +7,7 @@ use crate::{ world::{unsafe_world_cell::UnsafeWorldCell, World}, }; use bevy_ptr::{ThinSlicePtr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{cell::UnsafeCell, marker::PhantomData}; use variadics_please::all_tuples; @@ -103,6 +104,7 @@ pub unsafe trait QueryFilter: WorldQuery { /// Must always be called _after_ [`WorldQuery::set_table`] or [`WorldQuery::set_archetype`]. `entity` and /// `table_row` must be in the range of the current table and archetype. unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -204,6 +206,7 @@ unsafe impl QueryFilter for With { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, _fetch: &mut Self::Fetch<'_>, _entity: Entity, _table_row: TableRow, @@ -304,6 +307,7 @@ unsafe impl QueryFilter for Without { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, _fetch: &mut Self::Fetch<'_>, _entity: Entity, _table_row: TableRow, @@ -400,7 +404,7 @@ macro_rules! impl_or_query_filter { const IS_DENSE: bool = true $(&& $filter::IS_DENSE)*; #[inline] - unsafe fn init_fetch<'w>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($filter,)*) = state; ($(OrFetch { // SAFETY: The invariants are upheld by the caller. @@ -410,7 +414,7 @@ macro_rules! impl_or_query_filter { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table) { let ($($filter,)*) = fetch; let ($($state,)*) = state; $( @@ -423,9 +427,9 @@ macro_rules! impl_or_query_filter { } #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: & Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table ) { @@ -495,20 +499,22 @@ macro_rules! impl_or_query_filter { #[inline(always)] unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow ) -> bool { + let ($($state,)*) = state; let ($($filter,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - false $(|| ($filter.matches && unsafe { $filter::filter_fetch(&mut $filter.fetch, entity, table_row) }))* + false $(|| ($filter.matches && unsafe { $filter::filter_fetch($state, &mut $filter.fetch, entity, table_row) }))* } } }; } macro_rules! impl_tuple_query_filter { - ($(#[$meta:meta])* $($name: ident),*) => { + ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is a tuple-related macro; as such the lints below may not always apply." @@ -528,13 +534,15 @@ macro_rules! impl_tuple_query_filter { #[inline(always)] unsafe fn filter_fetch( + state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow ) -> bool { + let ($($state,)*) = state; let ($($name,)*) = fetch; // SAFETY: The invariants are upheld by the caller. - true $(&& unsafe { $name::filter_fetch($name, entity, table_row) })* + true $(&& unsafe { $name::filter_fetch($state, $name, entity, table_row) })* } } @@ -546,7 +554,8 @@ all_tuples!( impl_tuple_query_filter, 0, 15, - F + F, + S ); all_tuples!( #[doc(fake_variadic)] @@ -609,7 +618,12 @@ unsafe impl QueryFilter for Allows { const IS_ARCHETYPAL: bool = true; #[inline(always)] - unsafe fn filter_fetch(_: &mut Self::Fetch<'_>, _: Entity, _: TableRow) -> bool { + unsafe fn filter_fetch( + _: &Self::State, + _: &mut Self::Fetch<'_>, + _: Entity, + _: TableRow, + ) -> bool { true } } @@ -718,9 +732,9 @@ unsafe impl WorldQuery for Added { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - &id: &ComponentId, + &id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -748,9 +762,9 @@ unsafe impl WorldQuery for Added { }; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, ) { @@ -763,9 +777,9 @@ unsafe impl WorldQuery for Added { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { let table_ticks = Some( @@ -781,7 +795,7 @@ unsafe impl WorldQuery for Added { #[inline] fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { if access.access().has_component_write(id) { - panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",core::any::type_name::()); + panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", DebugName::type_name::()); } access.add_component_read(id); } @@ -807,6 +821,7 @@ unsafe impl QueryFilter for Added { const IS_ARCHETYPAL: bool = false; #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -944,9 +959,9 @@ unsafe impl WorldQuery for Changed { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - &id: &ComponentId, + &id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -974,9 +989,9 @@ unsafe impl WorldQuery for Changed { }; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, _archetype: &'w Archetype, table: &'w Table, ) { @@ -989,9 +1004,9 @@ unsafe impl WorldQuery for Changed { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { let table_ticks = Some( @@ -1007,7 +1022,7 @@ unsafe impl WorldQuery for Changed { #[inline] fn update_component_access(&id: &ComponentId, access: &mut FilteredAccess) { if access.access().has_component_write(id) { - panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.",core::any::type_name::()); + panic!("$state_name<{}> conflicts with a previous access in this query. Shared access cannot coincide with exclusive access.", DebugName::type_name::()); } access.add_component_read(id); } @@ -1034,6 +1049,7 @@ unsafe impl QueryFilter for Changed { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, table_row: TableRow, @@ -1141,9 +1157,9 @@ unsafe impl WorldQuery for Spawned { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - _state: &(), + _state: &'s (), last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -1157,16 +1173,16 @@ unsafe impl WorldQuery for Spawned { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &(), + _state: &'s (), _archetype: &'w Archetype, _table: &'w Table, ) { } #[inline] - unsafe fn set_table<'w>(_fetch: &mut Self::Fetch<'w>, _state: &(), _table: &'w Table) {} + unsafe fn set_table<'w, 's>(_fetch: &mut Self::Fetch<'w>, _state: &'s (), _table: &'w Table) {} #[inline] fn update_component_access(_state: &(), _access: &mut FilteredAccess) {} @@ -1188,6 +1204,7 @@ unsafe impl QueryFilter for Spawned { #[inline(always)] unsafe fn filter_fetch( + _state: &Self::State, fetch: &mut Self::Fetch<'_>, entity: Entity, _table_row: TableRow, diff --git a/crates/bevy_ecs/src/query/iter.rs b/crates/bevy_ecs/src/query/iter.rs index baf0d72697..eb49204434 100644 --- a/crates/bevy_ecs/src/query/iter.rs +++ b/crates/bevy_ecs/src/query/iter.rs @@ -140,7 +140,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { range: Option>, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if self.cursor.is_dense { // SAFETY: `self.cursor.is_dense` is true, so storage ids are guaranteed to be table ids. @@ -203,7 +203,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { rows: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if table.is_empty() { return accum; @@ -225,14 +225,26 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let fetched = unsafe { !F::filter_fetch(&mut self.cursor.filter, *entity, row) }; + let fetched = unsafe { + !F::filter_fetch( + &self.query_state.filter_state, + &mut self.cursor.filter, + *entity, + row, + ) + }; if fetched { continue; } // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let item = D::fetch(&mut self.cursor.fetch, *entity, row); + let item = D::fetch( + &self.query_state.fetch_state, + &mut self.cursor.fetch, + *entity, + row, + ); accum = func(accum, item); } @@ -255,7 +267,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { indices: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if archetype.is_empty() { return accum; @@ -283,6 +295,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // Caller assures `index` in range of the current archetype. let fetched = unsafe { !F::filter_fetch( + &self.query_state.filter_state, &mut self.cursor.filter, archetype_entity.id(), archetype_entity.table_row(), @@ -296,6 +309,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // Caller assures `index` in range of the current archetype. let item = unsafe { D::fetch( + &self.query_state.fetch_state, &mut self.cursor.fetch, archetype_entity.id(), archetype_entity.table_row(), @@ -324,7 +338,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { rows: Range, ) -> B where - Func: FnMut(B, D::Item<'w>) -> B, + Func: FnMut(B, D::Item<'w, 's>) -> B, { if archetype.is_empty() { return accum; @@ -356,14 +370,26 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let filter_matched = unsafe { F::filter_fetch(&mut self.cursor.filter, entity, row) }; + let filter_matched = unsafe { + F::filter_fetch( + &self.query_state.filter_state, + &mut self.cursor.filter, + entity, + row, + ) + }; if !filter_matched { continue; } // SAFETY: set_table was called prior. // Caller assures `row` in range of the current archetype. - let item = D::fetch(&mut self.cursor.fetch, entity, row); + let item = D::fetch( + &self.query_state.fetch_state, + &mut self.cursor.fetch, + entity, + row, + ); accum = func(accum, item); } @@ -492,7 +518,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort()) } @@ -549,7 +575,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort_unstable()) } @@ -605,7 +631,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// ``` pub fn sort_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedIter< 'w, 's, @@ -637,7 +663,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_unstable_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedIter< 'w, 's, @@ -729,7 +755,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// ``` pub fn sort_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -762,7 +788,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_unstable_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -797,7 +823,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. pub fn sort_by_cached_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedIter< 'w, 's, @@ -827,7 +853,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { /// This will panic if `next` has been called on `QueryIter` before, unless the underlying `Query` is empty. fn sort_impl( self, - f: impl FnOnce(&mut Vec<(L::Item<'_>, NeutralOrd)>), + f: impl FnOnce(&mut Vec<(L::Item<'_, '_>, NeutralOrd)>), ) -> QuerySortedIter< 'w, 's, @@ -856,7 +882,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { .map(|(key, entity)| (key, NeutralOrd(entity))) .collect(); f(&mut keyed_query); - let entity_iter = keyed_query.into_iter().map(|(.., entity)| entity.0); + let entity_iter = keyed_query + .into_iter() + .map(|(.., entity)| entity.0) + .collect::>() + .into_iter(); // SAFETY: // `self.world` has permission to access the required components. // Each lens query item is dropped before the respective actual query item is accessed. @@ -873,7 +903,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIter<'w, 's, D, F> { } impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for QueryIter<'w, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1010,7 +1040,7 @@ where /// # Safety /// `entity` must stem from `self.entity_iter`, and not have been passed before. #[inline(always)] - unsafe fn fetch_next(&mut self, entity: Entity) -> D::Item<'w> { + unsafe fn fetch_next(&mut self, entity: Entity) -> D::Item<'w, 's> { let (location, archetype, table); // SAFETY: // `tables` and `archetypes` belong to the same world that the [`QueryIter`] @@ -1039,7 +1069,14 @@ where // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - unsafe { D::fetch(&mut self.fetch, entity, location.table_row) } + unsafe { + D::fetch( + &self.query_state.fetch_state, + &mut self.fetch, + entity, + location.table_row, + ) + } } } @@ -1048,7 +1085,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> Iterator where I: Iterator, { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1170,7 +1207,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> fetch: &mut D::Fetch<'w>, filter: &mut F::Fetch<'w>, query_state: &'s QueryState, - ) -> Option> { + ) -> Option> { for entity_borrow in entity_iter { let entity = entity_borrow.entity(); let Some(location) = entities.get(entity) else { @@ -1200,11 +1237,20 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> // SAFETY: set_archetype was called prior. // `location.archetype_row` is an archetype index row in range of the current archetype, because if it was not, the match above would have `continue`d - if unsafe { F::filter_fetch(filter, entity, location.table_row) } { + if unsafe { + F::filter_fetch( + &query_state.filter_state, + filter, + entity, + location.table_row, + ) + } { // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - return Some(unsafe { D::fetch(fetch, entity, location.table_row) }); + return Some(unsafe { + D::fetch(&query_state.fetch_state, fetch, entity, location.table_row) + }); } } None @@ -1212,7 +1258,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// Get next result from the query #[inline(always)] - pub fn fetch_next(&mut self) -> Option> { + pub fn fetch_next(&mut self) -> Option> { // SAFETY: // All arguments stem from self. // We are limiting the returned reference to self, @@ -1336,7 +1382,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort()) } @@ -1394,7 +1440,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> impl ExactSizeIterator + DoubleEndedIterator + FusedIterator + 'w, > where - for<'lw> L::Item<'lw>: Ord, + for<'lw, 'ls> L::Item<'lw, 'ls>: Ord, { self.sort_impl::(|keyed_query| keyed_query.sort_unstable()) } @@ -1451,7 +1497,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// ``` pub fn sort_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedManyIter< 'w, 's, @@ -1482,7 +1528,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_unstable_by( self, - mut compare: impl FnMut(&L::Item<'_>, &L::Item<'_>) -> Ordering, + mut compare: impl FnMut(&L::Item<'_, '_>, &L::Item<'_, '_>) -> Ordering, ) -> QuerySortedManyIter< 'w, 's, @@ -1576,7 +1622,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// ``` pub fn sort_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1608,7 +1654,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_unstable_by_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1642,7 +1688,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. pub fn sort_by_cached_key( self, - mut f: impl FnMut(&L::Item<'_>) -> K, + mut f: impl FnMut(&L::Item<'_, '_>) -> K, ) -> QuerySortedManyIter< 'w, 's, @@ -1671,7 +1717,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// called on [`QueryManyIter`] before. fn sort_impl( self, - f: impl FnOnce(&mut Vec<(L::Item<'_>, NeutralOrd)>), + f: impl FnOnce(&mut Vec<(L::Item<'_, '_>, NeutralOrd)>), ) -> QuerySortedManyIter< 'w, 's, @@ -1721,7 +1767,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator Option> { + pub fn fetch_next_back(&mut self) -> Option> { // SAFETY: // All arguments stem from self. // We are limiting the returned reference to self, @@ -1745,7 +1791,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator> Iterator for QueryManyIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1861,7 +1907,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: EntitySetIterator> impl<'w, 's, D: QueryData, F: QueryFilter, I: EntitySetIterator> Iterator for QueryManyUniqueIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -1954,7 +2000,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> /// It is always safe for shared access. /// `entity` must stem from `self.entity_iter`, and not have been passed before. #[inline(always)] - unsafe fn fetch_next_aliased_unchecked(&mut self, entity: Entity) -> D::Item<'w> { + unsafe fn fetch_next_aliased_unchecked(&mut self, entity: Entity) -> D::Item<'w, 's> { let (location, archetype, table); // SAFETY: // `tables` and `archetypes` belong to the same world that the [`QueryIter`] @@ -1983,12 +2029,19 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: Iterator> // SAFETY: // - set_archetype was called prior, `location.archetype_row` is an archetype index in range of the current archetype // - fetch is only called once for each entity. - unsafe { D::fetch(&mut self.fetch, entity, location.table_row) } + unsafe { + D::fetch( + &self.query_state.fetch_state, + &mut self.fetch, + entity, + location.table_row, + ) + } } /// Get next result from the query #[inline(always)] - pub fn fetch_next(&mut self) -> Option> { + pub fn fetch_next(&mut self) -> Option> { let entity = self.entity_iter.next()?; // SAFETY: @@ -2007,7 +2060,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator { /// Get next result from the query #[inline(always)] - pub fn fetch_next_back(&mut self) -> Option> { + pub fn fetch_next_back(&mut self) -> Option> { let entity = self.entity_iter.next_back()?; // SAFETY: @@ -2024,7 +2077,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, I: DoubleEndedIterator impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, I: Iterator> Iterator for QuerySortedManyIter<'w, 's, D, F, I> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; #[inline(always)] fn next(&mut self) -> Option { @@ -2185,7 +2238,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< /// . /// It is always safe for shared access. #[inline] - unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option<[D::Item<'w>; K]> { + unsafe fn fetch_next_aliased_unchecked(&mut self) -> Option<[D::Item<'w, 's>; K]> { // PERF: can speed up the following code using `cursor.remaining()` instead of `next_item.is_none()` // when D::IS_ARCHETYPAL && F::IS_ARCHETYPAL // @@ -2211,11 +2264,12 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< } } - let mut values = MaybeUninit::<[D::Item<'w>; K]>::uninit(); + let mut values = MaybeUninit::<[D::Item<'w, 's>; K]>::uninit(); - let ptr = values.as_mut_ptr().cast::>(); + let ptr = values.as_mut_ptr().cast::>(); for (offset, cursor) in self.cursors.iter_mut().enumerate() { - ptr.add(offset).write(cursor.peek_last().unwrap()); + ptr.add(offset) + .write(cursor.peek_last(self.query_state).unwrap()); } Some(values.assume_init()) @@ -2223,7 +2277,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< /// Get next combination of queried components #[inline] - pub fn fetch_next(&mut self) -> Option<[D::Item<'_>; K]> { + pub fn fetch_next(&mut self) -> Option<[D::Item<'_, 's>; K]> { // SAFETY: we are limiting the returned reference to self, // making sure this method cannot be called multiple times without getting rid // of any previously returned unique references first, thus preventing aliasing. @@ -2240,7 +2294,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, const K: usize> QueryCombinationIter< impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, const K: usize> Iterator for QueryCombinationIter<'w, 's, D, F, K> { - type Item = [D::Item<'w>; K]; + type Item = [D::Item<'w, 's>; K]; #[inline] fn next(&mut self) -> Option { @@ -2390,7 +2444,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { /// The result of `next` and any previous calls to `peek_last` with this row must have been /// dropped to prevent aliasing mutable references. #[inline] - unsafe fn peek_last(&mut self) -> Option> { + unsafe fn peek_last(&mut self, query_state: &'s QueryState) -> Option> { if self.current_row > 0 { let index = self.current_row - 1; if self.is_dense { @@ -2401,6 +2455,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `*entity` and `index` are in the current table. unsafe { Some(D::fetch( + &query_state.fetch_state, &mut self.fetch, *entity, // SAFETY: This is from an exclusive range, so it can't be max. @@ -2416,6 +2471,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `archetype_entity.id()` and `archetype_entity.table_row()` are in the current archetype. unsafe { Some(D::fetch( + &query_state.fetch_state, &mut self.fetch, archetype_entity.id(), archetype_entity.table_row(), @@ -2457,7 +2513,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { tables: &'w Tables, archetypes: &'w Archetypes, query_state: &'s QueryState, - ) -> Option> { + ) -> Option> { if self.is_dense { loop { // we are on the beginning of the query, or finished processing a table, so skip to the next @@ -2484,7 +2540,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { unsafe { self.table_entities.get_unchecked(self.current_row as usize) }; // SAFETY: The row is less than the u32 len, so it must not be max. let row = unsafe { TableRow::new(NonMaxU32::new_unchecked(self.current_row)) }; - if !F::filter_fetch(&mut self.filter, *entity, row) { + if !F::filter_fetch(&query_state.filter_state, &mut self.filter, *entity, row) { self.current_row += 1; continue; } @@ -2494,7 +2550,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - `current_row` must be a table row in range of the current table, // because if it was not, then the above would have been executed. // - fetch is only called once for each `entity`. - let item = unsafe { D::fetch(&mut self.fetch, *entity, row) }; + let item = + unsafe { D::fetch(&query_state.fetch_state, &mut self.fetch, *entity, row) }; self.current_row += 1; return Some(item); @@ -2536,6 +2593,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { .get_unchecked(self.current_row as usize) }; if !F::filter_fetch( + &query_state.filter_state, &mut self.filter, archetype_entity.id(), archetype_entity.table_row(), @@ -2551,6 +2609,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryIterationCursor<'w, 's, D, F> { // - fetch is only called once for each `archetype_entity`. let item = unsafe { D::fetch( + &query_state.fetch_state, &mut self.fetch, archetype_entity.id(), archetype_entity.table_row(), diff --git a/crates/bevy_ecs/src/query/mod.rs b/crates/bevy_ecs/src/query/mod.rs index eb5e001e4d..7c1487fde4 100644 --- a/crates/bevy_ecs/src/query/mod.rs +++ b/crates/bevy_ecs/src/query/mod.rs @@ -824,9 +824,9 @@ mod tests { fn shrink_fetch<'wlong: 'wshort, 'wshort>(_: Self::Fetch<'wlong>) -> Self::Fetch<'wshort> {} - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( _world: UnsafeWorldCell<'w>, - _state: &Self::State, + _state: &'s Self::State, _last_run: Tick, _this_run: Tick, ) -> Self::Fetch<'w> { @@ -835,18 +835,18 @@ mod tests { const IS_DENSE: bool = true; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _archetype: &'w Archetype, _table: &Table, ) { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( _fetch: &mut Self::Fetch<'w>, - _state: &Self::State, + _state: &'s Self::State, _table: &'w Table, ) { } @@ -882,16 +882,20 @@ mod tests { unsafe impl QueryData for ReadsRData { const IS_READ_ONLY: bool = true; type ReadOnly = Self; - type Item<'w> = (); + type Item<'w, 's> = (); - fn shrink<'wlong: 'wshort, 'wshort>(_item: Self::Item<'wlong>) -> Self::Item<'wshort> {} + fn shrink<'wlong: 'wshort, 'wshort, 's>( + _item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { + } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + _state: &'s Self::State, _fetch: &mut Self::Fetch<'w>, _entity: Entity, _table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { } } diff --git a/crates/bevy_ecs/src/query/par_iter.rs b/crates/bevy_ecs/src/query/par_iter.rs index c685659260..b8d8618fa5 100644 --- a/crates/bevy_ecs/src/query/par_iter.rs +++ b/crates/bevy_ecs/src/query/par_iter.rs @@ -39,7 +39,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -76,7 +76,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> QueryParIter<'w, 's, D, F> { #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { @@ -190,7 +190,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -247,7 +247,7 @@ impl<'w, 's, D: ReadOnlyQueryData, F: QueryFilter, E: EntityEquivalent + Sync> #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { @@ -345,7 +345,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[inline] - pub fn for_each) + Send + Sync + Clone>(self, func: FN) { + pub fn for_each) + Send + Sync + Clone>(self, func: FN) { self.for_each_init(|| {}, |_, item| func(item)); } @@ -402,7 +402,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter, E: EntityEquivalent + Sync> #[inline] pub fn for_each_init(self, init: INIT, func: FN) where - FN: Fn(&mut T, QueryItem<'w, D>) + Send + Sync + Clone, + FN: Fn(&mut T, QueryItem<'w, 's, D>) + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { let func = |mut init, item| { diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 836fefbdfd..63ae1134a5 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -14,6 +14,7 @@ use crate::{ use crate::entity::UniqueEntityEquivalentSlice; use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use core::{fmt, ptr}; use fixedbitset::FixedBitSet; use log::warn; @@ -672,7 +673,7 @@ impl QueryState { assert!( component_access.is_subset(&self_access), "Transmuted state for {} attempts to access terms that are not allowed by original state {}.", - core::any::type_name::<(NewD, NewF)>(), core::any::type_name::<(D, F)>() + DebugName::type_name::<(NewD, NewF)>(), DebugName::type_name::<(D, F)>() ); QueryState { @@ -791,7 +792,7 @@ impl QueryState { assert!( component_access.is_subset(&joined_component_access), "Joined state for {} attempts to access terms that are not allowed by state {} joined with {}.", - core::any::type_name::<(NewD, NewF)>(), core::any::type_name::<(D, F)>(), core::any::type_name::<(OtherD, OtherF)>() + DebugName::type_name::<(NewD, NewF)>(), DebugName::type_name::<(D, F)>(), DebugName::type_name::<(OtherD, OtherF)>() ); if self.archetype_generation != other.archetype_generation { @@ -845,13 +846,17 @@ impl QueryState { /// /// This can only be called for read-only queries, see [`Self::get_mut`] for write-queries. /// + /// If you need to get multiple items at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::get_manual`] calls, + /// or making a single call with [`Self::get_many`] or [`Self::iter_many`]. + /// /// This is always guaranteed to run in `O(1)` time. #[inline] pub fn get<'w>( &mut self, world: &'w World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query(world).get_inner(entity) } @@ -892,7 +897,7 @@ impl QueryState { &mut self, world: &'w World, entities: [Entity; N], - ) -> Result<[ROQueryItem<'w, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'w, '_, D>; N], QueryEntityError> { self.query(world).get_many_inner(entities) } @@ -930,7 +935,7 @@ impl QueryState { &mut self, world: &'w World, entities: UniqueEntityArray, - ) -> Result<[ROQueryItem<'w, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'w, '_, D>; N], QueryEntityError> { self.query(world).get_many_unique_inner(entities) } @@ -942,7 +947,7 @@ impl QueryState { &mut self, world: &'w mut World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_mut(world).get_inner(entity) } @@ -989,7 +994,7 @@ impl QueryState { &mut self, world: &'w mut World, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, '_>; N], QueryEntityError> { self.query_mut(world).get_many_mut_inner(entities) } @@ -1034,7 +1039,7 @@ impl QueryState { &mut self, world: &'w mut World, entities: UniqueEntityArray, - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, '_>; N], QueryEntityError> { self.query_mut(world).get_many_unique_inner(entities) } @@ -1056,7 +1061,7 @@ impl QueryState { &self, world: &'w World, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_manual(world).get_inner(entity) } @@ -1073,13 +1078,16 @@ impl QueryState { &mut self, world: UnsafeWorldCell<'w>, entity: Entity, - ) -> Result, QueryEntityError> { + ) -> Result, QueryEntityError> { self.query_unchecked(world).get_inner(entity) } /// Returns an [`Iterator`] over the query results for the given [`World`]. /// /// This can only be called for read-only queries, see [`Self::iter_mut`] for write-queries. + /// + /// If you need to iterate multiple times at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::iter_manual`] calls. #[inline] pub fn iter<'w, 's>(&'s mut self, world: &'w World) -> QueryIter<'w, 's, D::ReadOnly, F> { self.query(world).into_iter() @@ -1168,6 +1176,9 @@ impl QueryState { /// Items are returned in the order of the list of entities. /// Entities that don't match the query are skipped. /// + /// If you need to iterate multiple times at once but get borrowing errors, + /// consider using [`Self::update_archetypes`] followed by multiple [`Self::iter_many_manual`] calls. + /// /// # See also /// /// - [`iter_many_mut`](Self::iter_many_mut) to get mutable query items. @@ -1387,8 +1398,8 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, T, FN, INIT>( - &self, + pub(crate) unsafe fn par_fold_init_unchecked_manual<'w, 's, T, FN, INIT>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, batch_size: u32, @@ -1396,7 +1407,7 @@ impl QueryState { last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, { // NOTE: If you are changing query iteration code, remember to update the following places, where relevant: @@ -1501,8 +1512,8 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_many_unique_fold_init_unchecked_manual<'w, T, FN, INIT, E>( - &self, + pub(crate) unsafe fn par_many_unique_fold_init_unchecked_manual<'w, 's, T, FN, INIT, E>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, entity_list: &UniqueEntityEquivalentSlice, @@ -1511,7 +1522,7 @@ impl QueryState { last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, E: EntityEquivalent + Sync, { @@ -1564,8 +1575,8 @@ impl QueryState { /// /// [`ComputeTaskPool`]: bevy_tasks::ComputeTaskPool #[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))] - pub(crate) unsafe fn par_many_fold_init_unchecked_manual<'w, T, FN, INIT, E>( - &self, + pub(crate) unsafe fn par_many_fold_init_unchecked_manual<'w, 's, T, FN, INIT, E>( + &'s self, init_accum: INIT, world: UnsafeWorldCell<'w>, entity_list: &[E], @@ -1574,7 +1585,7 @@ impl QueryState { last_run: Tick, this_run: Tick, ) where - FN: Fn(T, D::Item<'w>) -> T + Send + Sync + Clone, + FN: Fn(T, D::Item<'w, 's>) -> T + Send + Sync + Clone, INIT: Fn() -> T + Sync + Send + Clone, E: EntityEquivalent + Sync, { @@ -1686,7 +1697,10 @@ impl QueryState { /// /// Simply unwrapping the [`Result`] also works, but should generally be reserved for tests. #[inline] - pub fn single<'w>(&mut self, world: &'w World) -> Result, QuerySingleError> { + pub fn single<'w>( + &mut self, + world: &'w World, + ) -> Result, QuerySingleError> { self.query(world).single_inner() } @@ -1703,7 +1717,7 @@ impl QueryState { pub fn single_mut<'w>( &mut self, world: &'w mut World, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { self.query_mut(world).single_inner() } @@ -1720,7 +1734,7 @@ impl QueryState { pub unsafe fn single_unchecked<'w>( &mut self, world: UnsafeWorldCell<'w>, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { self.query_unchecked(world).single_inner() } @@ -1742,7 +1756,7 @@ impl QueryState { world: UnsafeWorldCell<'w>, last_run: Tick, this_run: Tick, - ) -> Result, QuerySingleError> { + ) -> Result, QuerySingleError> { // SAFETY: // - The caller ensured we have the correct access to the world. // - The caller ensured that the world matches. diff --git a/crates/bevy_ecs/src/query/world_query.rs b/crates/bevy_ecs/src/query/world_query.rs index a6bcbf58bd..1c739927ac 100644 --- a/crates/bevy_ecs/src/query/world_query.rs +++ b/crates/bevy_ecs/src/query/world_query.rs @@ -42,7 +42,7 @@ use variadics_please::all_tuples; /// [`QueryFilter`]: crate::query::QueryFilter pub unsafe trait WorldQuery { /// Per archetype/table state retrieved by this [`WorldQuery`] to compute [`Self::Item`](crate::query::QueryData::Item) for each entity. - type Fetch<'a>: Clone; + type Fetch<'w>: Clone; /// State used to construct a [`Self::Fetch`](WorldQuery::Fetch). This will be cached inside [`QueryState`](crate::query::QueryState), /// so it is best to move as much data / computation here as possible to reduce the cost of @@ -62,9 +62,9 @@ pub unsafe trait WorldQuery { /// in to this function. /// - `world` must have the **right** to access any access registered in `update_component_access`. /// - There must not be simultaneous resource access conflicting with readonly resource access registered in [`WorldQuery::update_component_access`]. - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - state: &Self::State, + state: &'s Self::State, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w>; @@ -87,9 +87,9 @@ pub unsafe trait WorldQuery { /// - `archetype` and `tables` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. /// - `table` must correspond to `archetype`. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table, ); @@ -101,7 +101,11 @@ pub unsafe trait WorldQuery { /// /// - `table` must be from the same [`World`] that [`WorldQuery::init_state`] was called on. /// - `state` must be the [`State`](Self::State) that `fetch` was initialized with. - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table); + unsafe fn set_table<'w, 's>( + fetch: &mut Self::Fetch<'w>, + state: &'s Self::State, + table: &'w Table, + ); /// Adds any component accesses used by this [`WorldQuery`] to `access`. /// @@ -166,7 +170,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - unsafe fn init_fetch<'w>(world: UnsafeWorldCell<'w>, state: &Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { + unsafe fn init_fetch<'w, 's>(world: UnsafeWorldCell<'w>, state: &'s Self::State, last_run: Tick, this_run: Tick) -> Self::Fetch<'w> { let ($($name,)*) = state; // SAFETY: The invariants are upheld by the caller. ($(unsafe { $name::init_fetch(world, $name, last_run, this_run) },)*) @@ -175,9 +179,9 @@ macro_rules! impl_tuple_world_query { const IS_DENSE: bool = true $(&& $name::IS_DENSE)*; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - state: &Self::State, + state: &'s Self::State, archetype: &'w Archetype, table: &'w Table ) { @@ -188,7 +192,7 @@ macro_rules! impl_tuple_world_query { } #[inline] - unsafe fn set_table<'w>(fetch: &mut Self::Fetch<'w>, state: &Self::State, table: &'w Table) { + unsafe fn set_table<'w, 's>(fetch: &mut Self::Fetch<'w>, state: &'s Self::State, table: &'w Table) { let ($($name,)*) = fetch; let ($($state,)*) = state; // SAFETY: The invariants are upheld by the caller. diff --git a/crates/bevy_ecs/src/reflect/bundle.rs b/crates/bevy_ecs/src/reflect/bundle.rs index ee02aff86e..133591c405 100644 --- a/crates/bevy_ecs/src/reflect/bundle.rs +++ b/crates/bevy_ecs/src/reflect/bundle.rs @@ -5,6 +5,7 @@ //! //! Same as [`super::component`], but for bundles. use alloc::boxed::Box; +use bevy_utils::prelude::DebugName; use core::any::{Any, TypeId}; use crate::{ @@ -172,7 +173,7 @@ impl FromType for Refl _ => panic!( "expected bundle `{}` to be named struct or tuple", // FIXME: once we have unique reflect, use `TypePath`. - core::any::type_name::(), + DebugName::type_name::(), ), } } @@ -215,7 +216,7 @@ impl FromType for Refl _ => panic!( "expected bundle `{}` to be a named struct or tuple", // FIXME: once we have unique reflect, use `TypePath`. - core::any::type_name::(), + DebugName::type_name::(), ), } } diff --git a/crates/bevy_ecs/src/reflect/component.rs b/crates/bevy_ecs/src/reflect/component.rs index 893e9b13fa..8a5c17179e 100644 --- a/crates/bevy_ecs/src/reflect/component.rs +++ b/crates/bevy_ecs/src/reflect/component.rs @@ -70,7 +70,7 @@ use crate::{ }, }; use bevy_reflect::{FromReflect, FromType, PartialReflect, Reflect, TypePath, TypeRegistry}; -use disqualified::ShortName; +use bevy_utils::prelude::DebugName; /// A struct used to operate on reflected [`Component`] trait of a type. /// @@ -308,7 +308,8 @@ impl FromType for ReflectComponent { }, apply: |mut entity, reflected_component| { if !C::Mutability::MUTABLE { - let name = ShortName::of::(); + let name = DebugName::type_name::(); + let name = name.shortname(); panic!("Cannot call `ReflectComponent::apply` on component {name}. It is immutable, and cannot modified through reflection"); } @@ -357,7 +358,8 @@ impl FromType for ReflectComponent { reflect: |entity| entity.get::().map(|c| c as &dyn Reflect), reflect_mut: |entity| { if !C::Mutability::MUTABLE { - let name = ShortName::of::(); + let name = DebugName::type_name::(); + let name = name.shortname(); panic!("Cannot call `ReflectComponent::reflect_mut` on component {name}. It is immutable, and cannot modified through reflection"); } @@ -370,7 +372,8 @@ impl FromType for ReflectComponent { }, reflect_unchecked_mut: |entity| { if !C::Mutability::MUTABLE { - let name = ShortName::of::(); + let name = DebugName::type_name::(); + let name = name.shortname(); panic!("Cannot call `ReflectComponent::reflect_unchecked_mut` on component {name}. It is immutable, and cannot modified through reflection"); } diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index 8ec2051b31..dce6237f1b 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -18,6 +18,7 @@ mod from_world; mod map_entities; mod resource; +use bevy_utils::prelude::DebugName; pub use bundle::{ReflectBundle, ReflectBundleFns}; pub use component::{ReflectComponent, ReflectComponentFns}; pub use entity_commands::ReflectCommandExt; @@ -148,7 +149,7 @@ pub fn from_reflect_with_fallback( `Default` or `FromWorld` traits. Are you perhaps missing a `#[reflect(Default)]` \ or `#[reflect(FromWorld)]`?", // FIXME: once we have unique reflect, use `TypePath`. - core::any::type_name::(), + DebugName::type_name::(), ); }; diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 9ec67ce36a..d570b9fabc 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -6,14 +6,16 @@ mod relationship_source_collection; use alloc::format; +use bevy_utils::prelude::DebugName; pub use related_methods::*; pub use relationship_query::*; pub use relationship_source_collection::*; use crate::{ - component::{Component, HookContext, Mutable}, + component::{Component, Mutable}, entity::{ComponentCloneCtx, Entity, SourceComponent}, error::{ignore, CommandWithEntity, HandleError}, + lifecycle::HookContext, world::{DeferredWorld, EntityWorldMut}, }; use log::warn; @@ -104,8 +106,8 @@ pub trait Relationship: Component + Sized { warn!( "{}The {}({target_entity:?}) relationship on entity {entity:?} points to itself. The invalid {} relationship has been removed.", caller.map(|location|format!("{location}: ")).unwrap_or_default(), - core::any::type_name::(), - core::any::type_name::() + DebugName::type_name::(), + DebugName::type_name::() ); world.commands().entity(entity).remove::(); return; @@ -124,8 +126,8 @@ pub trait Relationship: Component + Sized { warn!( "{}The {}({target_entity:?}) relationship on entity {entity:?} relates to an entity that does not exist. The invalid {} relationship has been removed.", caller.map(|location|format!("{location}: ")).unwrap_or_default(), - core::any::type_name::(), - core::any::type_name::() + DebugName::type_name::(), + DebugName::type_name::() ); world.commands().entity(entity).remove::(); } diff --git a/crates/bevy_ecs/src/relationship/related_methods.rs b/crates/bevy_ecs/src/relationship/related_methods.rs index 5a23214463..1983b6b37c 100644 --- a/crates/bevy_ecs/src/relationship/related_methods.rs +++ b/crates/bevy_ecs/src/relationship/related_methods.rs @@ -1,6 +1,7 @@ use crate::{ bundle::Bundle, entity::{hash_set::EntityHashSet, Entity}, + prelude::Children, relationship::{ Relationship, RelationshipHookMode, RelationshipSourceCollection, RelationshipTarget, }, @@ -138,22 +139,26 @@ impl<'w> EntityWorldMut<'w> { return self; } - let Some(mut existing_relations) = self.get_mut::() else { + let Some(existing_relations) = self.get_mut::() else { return self.add_related::(related); }; - // We take the collection here so we can modify it without taking the component itself (this would create archetype move). + // We replace the component here with a dummy value so we can modify it without taking it (this would create archetype move). // SAFETY: We eventually return the correctly initialized collection into the target. - let mut existing_relations = mem::replace( - existing_relations.collection_mut_risky(), - Collection::::with_capacity(0), + let mut relations = mem::replace( + existing_relations.into_inner(), + ::RelationshipTarget::from_collection_risky( + Collection::::with_capacity(0), + ), ); + let collection = relations.collection_mut_risky(); + let mut potential_relations = EntityHashSet::from_iter(related.iter().copied()); let id = self.id(); self.world_scope(|world| { - for related in existing_relations.iter() { + for related in collection.iter() { if !potential_relations.remove(related) { world.entity_mut(related).remove::(); } @@ -168,11 +173,9 @@ impl<'w> EntityWorldMut<'w> { }); // SAFETY: The entities we're inserting will be the entities that were either already there or entities that we've just inserted. - existing_relations.clear(); - existing_relations.extend_from_iter(related.iter().copied()); - self.insert(R::RelationshipTarget::from_collection_risky( - existing_relations, - )); + collection.clear(); + collection.extend_from_iter(related.iter().copied()); + self.insert(relations); self } @@ -238,11 +241,20 @@ impl<'w> EntityWorldMut<'w> { assert_eq!(newly_related_entities, entities_to_relate, "`entities_to_relate` ({entities_to_relate:?}) didn't contain all entities that would end up related"); }; - if !self.contains::() { - self.add_related::(entities_to_relate); + match self.get_mut::() { + None => { + self.add_related::(entities_to_relate); - return self; - }; + return self; + } + Some(mut target) => { + // SAFETY: The invariants expected by this function mean we'll only be inserting entities that are already related. + let collection = target.collection_mut_risky(); + collection.clear(); + + collection.extend_from_iter(entities_to_relate.iter().copied()); + } + } let this = self.id(); self.world_scope(|world| { @@ -251,32 +263,13 @@ impl<'w> EntityWorldMut<'w> { } for new_relation in newly_related_entities { - // We're changing the target collection manually so don't run the insert hook + // We changed the target collection manually so don't run the insert hook world .entity_mut(*new_relation) .insert_with_relationship_hook_mode(R::from(this), RelationshipHookMode::Skip); } }); - if !entities_to_relate.is_empty() { - if let Some(mut target) = self.get_mut::() { - // SAFETY: The invariants expected by this function mean we'll only be inserting entities that are already related. - let collection = target.collection_mut_risky(); - collection.clear(); - - collection.extend_from_iter(entities_to_relate.iter().copied()); - } else { - let mut empty = - ::Collection::with_capacity( - entities_to_relate.len(), - ); - empty.extend_from_iter(entities_to_relate.iter().copied()); - - // SAFETY: We've just initialized this collection and we know there's no `RelationshipTarget` on `self` - self.insert(R::RelationshipTarget::from_collection_risky(empty)); - } - } - self } @@ -302,6 +295,15 @@ impl<'w> EntityWorldMut<'w> { self } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityWorldMut::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::(); + self + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// @@ -467,6 +469,14 @@ impl<'a> EntityCommands<'a> { }) } + /// Despawns the children of this entity. + /// This entity will not be despawned. + /// + /// This is a specialization of [`despawn_related`](EntityCommands::despawn_related), a more general method for despawning via relationships. + pub fn despawn_children(&mut self) -> &mut Self { + self.despawn_related::() + } + /// Inserts a component or bundle of components into the entity and all related entities, /// traversing the relationship tracked in `S` in a breadth-first manner. /// @@ -650,4 +660,61 @@ mod tests { assert_eq!(world.entity(b).get::(), None); assert_eq!(world.entity(c).get::(), None); } + + #[test] + fn replace_related_works() { + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let child3 = world.spawn_empty().id(); + + let mut parent = world.spawn_empty(); + parent.add_children(&[child1, child2]); + let child_value = ChildOf(parent.id()); + let some_child = Some(&child_value); + + parent.replace_children(&[child2, child3]); + let children = parent.get::().unwrap().collection(); + assert_eq!(children, &[child2, child3]); + assert_eq!(parent.world().get::(child1), None); + assert_eq!(parent.world().get::(child2), some_child); + assert_eq!(parent.world().get::(child3), some_child); + + parent.replace_children_with_difference(&[child3], &[child1, child2], &[child1]); + let children = parent.get::().unwrap().collection(); + assert_eq!(children, &[child1, child2]); + assert_eq!(parent.world().get::(child1), some_child); + assert_eq!(parent.world().get::(child2), some_child); + assert_eq!(parent.world().get::(child3), None); + } + + #[test] + fn replace_related_keeps_data() { + #[derive(Component)] + #[relationship(relationship_target = Parent)] + pub struct Child(Entity); + + #[derive(Component)] + #[relationship_target(relationship = Child)] + pub struct Parent { + #[relationship] + children: Vec, + pub data: u8, + } + + let mut world = World::new(); + let child1 = world.spawn_empty().id(); + let child2 = world.spawn_empty().id(); + let mut parent = world.spawn_empty(); + parent.add_related::(&[child1]); + parent.get_mut::().unwrap().data = 42; + + parent.replace_related_with_difference::(&[child1], &[child2], &[child2]); + let data = parent.get::().unwrap().data; + assert_eq!(data, 42); + + parent.replace_related::(&[child1]); + let data = parent.get::().unwrap().data; + assert_eq!(data, 42); + } } diff --git a/crates/bevy_ecs/src/relationship/relationship_query.rs b/crates/bevy_ecs/src/relationship/relationship_query.rs index a2ec937c29..a7acea7de0 100644 --- a/crates/bevy_ecs/src/relationship/relationship_query.rs +++ b/crates/bevy_ecs/src/relationship/relationship_query.rs @@ -14,7 +14,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// target entity of that relationship. pub fn related(&'w self, entity: Entity) -> Option where - ::ReadOnly: QueryData = &'w R>, + ::ReadOnly: QueryData = &'w R>, { self.get(entity).map(R::get).ok() } @@ -26,7 +26,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> impl Iterator + 'w where - ::ReadOnly: QueryData = &'w S>, + ::ReadOnly: QueryData = &'w S>, { self.get(entity) .into_iter() @@ -42,7 +42,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// If your relationship is not a tree (like Bevy's hierarchy), be sure to stop if you encounter a duplicate entity. pub fn root_ancestor(&'w self, entity: Entity) -> Entity where - ::ReadOnly: QueryData = &'w R>, + ::ReadOnly: QueryData = &'w R>, { // Recursively search up the tree until we're out of parents match self.get(entity) { @@ -60,9 +60,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn iter_leaves( &'w self, entity: Entity, - ) -> impl Iterator + 'w + ) -> impl Iterator + use<'w, 's, S, D, F> where - ::ReadOnly: QueryData = &'w S>, + ::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { self.iter_descendants_depth_first(entity).filter(|entity| { @@ -80,7 +80,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> impl Iterator + 'w where - D::ReadOnly: QueryData = (Option<&'w R>, Option<&'w R::RelationshipTarget>)>, + D::ReadOnly: QueryData = (Option<&'w R>, Option<&'w R::RelationshipTarget>)>, { self.get(entity) .ok() @@ -103,7 +103,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { DescendantIter::new(self, entity) } @@ -120,7 +120,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { DescendantDepthFirstIter::new(self, entity) @@ -137,7 +137,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { entity: Entity, ) -> AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { AncestorIter::new(self, entity) } @@ -148,7 +148,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Traverses the hierarchy breadth-first. pub struct DescendantIter<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { children_query: &'w Query<'w, 's, D, F>, vecdeque: VecDeque, @@ -156,7 +156,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { /// Returns a new [`DescendantIter`]. pub fn new(children_query: &'w Query<'w, 's, D, F>, entity: Entity) -> Self { @@ -174,7 +174,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> Iterator for DescendantIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { type Item = Entity; @@ -194,7 +194,7 @@ where /// Traverses the hierarchy depth-first. pub struct DescendantDepthFirstIter<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, { children_query: &'w Query<'w, 's, D, F>, stack: SmallVec<[Entity; 8]>, @@ -203,7 +203,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { /// Returns a new [`DescendantDepthFirstIter`]. @@ -220,7 +220,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, S: RelationshipTarget> Iterator for DescendantDepthFirstIter<'w, 's, D, F, S> where - D::ReadOnly: QueryData = &'w S>, + D::ReadOnly: QueryData = &'w S>, SourceIter<'w, S>: DoubleEndedIterator, { type Item = Entity; @@ -239,7 +239,7 @@ where /// An [`Iterator`] of [`Entity`]s over the ancestors of an [`Entity`]. pub struct AncestorIter<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { parent_query: &'w Query<'w, 's, D, F>, next: Option, @@ -247,7 +247,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { /// Returns a new [`AncestorIter`]. pub fn new(parent_query: &'w Query<'w, 's, D, F>, entity: Entity) -> Self { @@ -261,7 +261,7 @@ where impl<'w, 's, D: QueryData, F: QueryFilter, R: Relationship> Iterator for AncestorIter<'w, 's, D, F, R> where - D::ReadOnly: QueryData = &'w R>, + D::ReadOnly: QueryData = &'w R>, { type Item = Entity; diff --git a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs index d4ea45f64f..01c9edf41b 100644 --- a/crates/bevy_ecs/src/relationship/relationship_source_collection.rs +++ b/crates/bevy_ecs/src/relationship/relationship_source_collection.rs @@ -86,13 +86,13 @@ pub trait OrderedRelationshipSourceCollection: RelationshipSourceCollection { /// Inserts the entity at a specific index. /// If the index is too large, the entity will be added to the end of the collection. fn insert(&mut self, index: usize, entity: Entity); - /// Removes the entity at the specified idnex if it exists. + /// Removes the entity at the specified index if it exists. fn remove_at(&mut self, index: usize) -> Option; /// Inserts the entity at a specific index. /// This will never reorder other entities. /// If the index is too large, the entity will be added to the end of the collection. fn insert_stable(&mut self, index: usize, entity: Entity); - /// Removes the entity at the specified idnex if it exists. + /// Removes the entity at the specified index if it exists. /// This will never reorder other entities. fn remove_at_stable(&mut self, index: usize) -> Option; /// Sorts the source collection. diff --git a/crates/bevy_ecs/src/removal_detection.rs b/crates/bevy_ecs/src/removal_detection.rs deleted file mode 100644 index 64cc63a7ce..0000000000 --- a/crates/bevy_ecs/src/removal_detection.rs +++ /dev/null @@ -1,268 +0,0 @@ -//! Alerting events when a component is removed from an entity. - -use crate::{ - component::{Component, ComponentId, ComponentIdFor, Tick}, - entity::Entity, - event::{Event, EventCursor, EventId, EventIterator, EventIteratorWithId, Events}, - prelude::Local, - storage::SparseSet, - system::{ReadOnlySystemParam, SystemMeta, SystemParam}, - world::{unsafe_world_cell::UnsafeWorldCell, World}, -}; - -use derive_more::derive::Into; - -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; -use core::{ - fmt::Debug, - iter, - marker::PhantomData, - ops::{Deref, DerefMut}, - option, -}; - -/// Wrapper around [`Entity`] for [`RemovedComponents`]. -/// Internally, `RemovedComponents` uses these as an `Events`. -#[derive(Event, Debug, Clone, Into)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] -pub struct RemovedComponentEntity(Entity); - -/// Wrapper around a [`EventCursor`] so that we -/// can differentiate events between components. -#[derive(Debug)] -pub struct RemovedComponentReader -where - T: Component, -{ - reader: EventCursor, - marker: PhantomData, -} - -impl Default for RemovedComponentReader { - fn default() -> Self { - Self { - reader: Default::default(), - marker: PhantomData, - } - } -} - -impl Deref for RemovedComponentReader { - type Target = EventCursor; - fn deref(&self) -> &Self::Target { - &self.reader - } -} - -impl DerefMut for RemovedComponentReader { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.reader - } -} - -/// Stores the [`RemovedComponents`] event buffers for all types of component in a given [`World`]. -#[derive(Default, Debug)] -pub struct RemovedComponentEvents { - event_sets: SparseSet>, -} - -impl RemovedComponentEvents { - /// Creates an empty storage buffer for component removal events. - pub fn new() -> Self { - Self::default() - } - - /// For each type of component, swaps the event buffers and clears the oldest event buffer. - /// In general, this should be called once per frame/update. - pub fn update(&mut self) { - for (_component_id, events) in self.event_sets.iter_mut() { - events.update(); - } - } - - /// Returns an iterator over components and their entity events. - pub fn iter(&self) -> impl Iterator)> { - self.event_sets.iter() - } - - /// Gets the event storage for a given component. - pub fn get( - &self, - component_id: impl Into, - ) -> Option<&Events> { - self.event_sets.get(component_id.into()) - } - - /// Sends a removal event for the specified component. - pub fn send(&mut self, component_id: impl Into, entity: Entity) { - self.event_sets - .get_or_insert_with(component_id.into(), Default::default) - .send(RemovedComponentEntity(entity)); - } -} - -/// A [`SystemParam`] that yields entities that had their `T` [`Component`] -/// removed or have been despawned with it. -/// -/// This acts effectively the same as an [`EventReader`](crate::event::EventReader). -/// -/// Note that this does not allow you to see which data existed before removal. -/// If you need this, you will need to track the component data value on your own, -/// using a regularly scheduled system that requests `Query<(Entity, &T), Changed>` -/// and stores the data somewhere safe to later cross-reference. -/// -/// If you are using `bevy_ecs` as a standalone crate, -/// note that the `RemovedComponents` list will not be automatically cleared for you, -/// and will need to be manually flushed using [`World::clear_trackers`](World::clear_trackers). -/// -/// For users of `bevy` and `bevy_app`, [`World::clear_trackers`](World::clear_trackers) is -/// automatically called by `bevy_app::App::update` and `bevy_app::SubApp::update`. -/// For the main world, this is delayed until after all `SubApp`s have run. -/// -/// # Examples -/// -/// Basic usage: -/// -/// ``` -/// # use bevy_ecs::component::Component; -/// # use bevy_ecs::system::IntoSystem; -/// # use bevy_ecs::removal_detection::RemovedComponents; -/// # -/// # #[derive(Component)] -/// # struct MyComponent; -/// fn react_on_removal(mut removed: RemovedComponents) { -/// removed.read().for_each(|removed_entity| println!("{}", removed_entity)); -/// } -/// # bevy_ecs::system::assert_is_system(react_on_removal); -/// ``` -#[derive(SystemParam)] -pub struct RemovedComponents<'w, 's, T: Component> { - component_id: ComponentIdFor<'s, T>, - reader: Local<'s, RemovedComponentReader>, - event_sets: &'w RemovedComponentEvents, -} - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIter<'a> = iter::Map< - iter::Flatten>>>, - fn(RemovedComponentEntity) -> Entity, ->; - -/// Iterator over entities that had a specific component removed. -/// -/// See [`RemovedComponents`]. -pub type RemovedIterWithId<'a> = iter::Map< - iter::Flatten>>, - fn( - (&RemovedComponentEntity, EventId), - ) -> (Entity, EventId), ->; - -fn map_id_events( - (entity, id): (&RemovedComponentEntity, EventId), -) -> (Entity, EventId) { - (entity.clone().into(), id) -} - -// For all practical purposes, the api surface of `RemovedComponents` -// should be similar to `EventReader` to reduce confusion. -impl<'w, 's, T: Component> RemovedComponents<'w, 's, T> { - /// Fetch underlying [`EventCursor`]. - pub fn reader(&self) -> &EventCursor { - &self.reader - } - - /// Fetch underlying [`EventCursor`] mutably. - pub fn reader_mut(&mut self) -> &mut EventCursor { - &mut self.reader - } - - /// Fetch underlying [`Events`]. - pub fn events(&self) -> Option<&Events> { - self.event_sets.get(self.component_id.get()) - } - - /// Destructures to get a mutable reference to the `EventCursor` - /// and a reference to `Events`. - /// - /// This is necessary since Rust can't detect destructuring through methods and most - /// usecases of the reader uses the `Events` as well. - pub fn reader_mut_with_events( - &mut self, - ) -> Option<( - &mut RemovedComponentReader, - &Events, - )> { - self.event_sets - .get(self.component_id.get()) - .map(|events| (&mut *self.reader, events)) - } - - /// Iterates over the events this [`RemovedComponents`] has not seen yet. This updates the - /// [`RemovedComponents`]'s event counter, which means subsequent event reads will not include events - /// that happened before now. - pub fn read(&mut self) -> RemovedIter<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read(events).cloned()) - .into_iter() - .flatten() - .map(RemovedComponentEntity::into) - } - - /// Like [`read`](Self::read), except also returning the [`EventId`] of the events. - pub fn read_with_id(&mut self) -> RemovedIterWithId<'_> { - self.reader_mut_with_events() - .map(|(reader, events)| reader.read_with_id(events)) - .into_iter() - .flatten() - .map(map_id_events) - } - - /// Determines the number of removal events available to be read from this [`RemovedComponents`] without consuming any. - pub fn len(&self) -> usize { - self.events() - .map(|events| self.reader.len(events)) - .unwrap_or(0) - } - - /// Returns `true` if there are no events available to read. - pub fn is_empty(&self) -> bool { - self.events() - .is_none_or(|events| self.reader.is_empty(events)) - } - - /// Consumes all available events. - /// - /// This means these events will not appear in calls to [`RemovedComponents::read()`] or - /// [`RemovedComponents::read_with_id()`] and [`RemovedComponents::is_empty()`] will return `true`. - pub fn clear(&mut self) { - if let Some((reader, events)) = self.reader_mut_with_events() { - reader.clear(events); - } - } -} - -// SAFETY: Only reads World removed component events -unsafe impl<'a> ReadOnlySystemParam for &'a RemovedComponentEvents {} - -// SAFETY: no component value access. -unsafe impl<'a> SystemParam for &'a RemovedComponentEvents { - type State = (); - type Item<'w, 's> = &'w RemovedComponentEvents; - - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} - - #[inline] - unsafe fn get_param<'w, 's>( - _state: &'s mut Self::State, - _system_meta: &SystemMeta, - world: UnsafeWorldCell<'w>, - _change_tick: Tick, - ) -> Self::Item<'w, 's> { - world.removed_components() - } -} diff --git a/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs b/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs index dda6d604a7..997259e104 100644 --- a/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs +++ b/crates/bevy_ecs/src/schedule/auto_insert_apply_deferred.rs @@ -102,7 +102,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { let mut system_has_conditions_cache = HashMap::::default(); let mut is_valid_explicit_sync_point = |system: NodeId| { let index = system.index(); - is_apply_deferred(graph.systems[index].get().unwrap()) + is_apply_deferred(&graph.systems[index].get().unwrap().system) && !*system_has_conditions_cache .entry(index) .or_insert_with(|| system_has_conditions(graph, system)) @@ -138,7 +138,11 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { } else if !node_needs_sync { // No previous node has postponed sync points to add so check if the system itself // has deferred params that require a sync point to apply them. - node_needs_sync = graph.systems[node.index()].get().unwrap().has_deferred(); + node_needs_sync = graph.systems[node.index()] + .get() + .unwrap() + .system + .has_deferred(); } for target in dependency_flattened.neighbors_directed(*node, Direction::Outgoing) { @@ -148,7 +152,11 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { let mut edge_needs_sync = node_needs_sync; if node_needs_sync - && !graph.systems[target.index()].get().unwrap().is_exclusive() + && !graph.systems[target.index()] + .get() + .unwrap() + .system + .is_exclusive() && self.no_sync_edges.contains(&(*node, target)) { // The node has deferred params to apply, but this edge is ignoring sync points. @@ -190,7 +198,7 @@ impl ScheduleBuildPass for AutoInsertApplyDeferredPass { continue; } - if is_apply_deferred(graph.systems[target.index()].get().unwrap()) { + if is_apply_deferred(&graph.systems[target.index()].get().unwrap().system) { // We don't need to insert a sync point since ApplyDeferred is a sync point // already! continue; diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 2b31ad50c7..9a7ce5d507 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -1,4 +1,5 @@ -use alloc::{borrow::Cow, boxed::Box, format}; +use alloc::{boxed::Box, format}; +use bevy_utils::prelude::DebugName; use core::ops::Not; use crate::system::{ @@ -11,8 +12,18 @@ pub type BoxedCondition = Box>; /// A system that determines if one or more scheduled systems should run. /// -/// Implemented for functions and closures that convert into [`System`](System) -/// with [read-only](crate::system::ReadOnlySystemParam) parameters. +/// `SystemCondition` is sealed and implemented for functions and closures with +/// [read-only](crate::system::ReadOnlySystemParam) parameters that convert into +/// [`System`](System), [`System>`](System) or +/// [`System>`](System). +/// +/// `SystemCondition` offers a private method +/// (called by [`run_if`](crate::schedule::IntoScheduleConfigs::run_if) and the provided methods) +/// that converts the implementing system into a condition (system) returning a bool. +/// Depending on the output type of the implementing system: +/// - `bool`: the implementing system is used as the condition; +/// - `Result<(), BevyError>`: the condition returns `true` if and only if the implementing system returns `Ok(())`; +/// - `Result`: the condition returns `true` if and only if the implementing system returns `Ok(true)`. /// /// # Marker type parameter /// @@ -31,7 +42,7 @@ pub type BoxedCondition = Box>; /// ``` /// /// # Examples -/// A condition that returns true every other time it's called. +/// A condition that returns `true` every other time it's called. /// ``` /// # use bevy_ecs::prelude::*; /// fn every_other_time() -> impl SystemCondition<()> { @@ -54,7 +65,7 @@ pub type BoxedCondition = Box>; /// # assert!(!world.resource::().0); /// ``` /// -/// A condition that takes a bool as an input and returns it unchanged. +/// A condition that takes a `bool` as an input and returns it unchanged. /// /// ``` /// # use bevy_ecs::prelude::*; @@ -71,8 +82,30 @@ pub type BoxedCondition = Box>; /// # world.insert_resource(DidRun(false)); /// # app.run(&mut world); /// # assert!(world.resource::().0); -pub trait SystemCondition: - sealed::SystemCondition +/// ``` +/// +/// A condition returning a `Result<(), BevyError>` +/// +/// ``` +/// # use bevy_ecs::prelude::*; +/// # #[derive(Component)] struct Player; +/// fn player_exists(q_player: Query<(), With>) -> Result { +/// Ok(q_player.single()?) +/// } +/// +/// # let mut app = Schedule::default(); +/// # #[derive(Resource)] struct DidRun(bool); +/// # fn my_system(mut did_run: ResMut) { did_run.0 = true; } +/// app.add_systems(my_system.run_if(player_exists)); +/// # let mut world = World::new(); +/// # world.insert_resource(DidRun(false)); +/// # app.run(&mut world); +/// # assert!(!world.resource::().0); +/// # world.spawn(Player); +/// # app.run(&mut world); +/// # assert!(world.resource::().0); +pub trait SystemCondition: + sealed::SystemCondition { /// Returns a new run condition that only returns `true` /// if both this one and the passed `and` return `true`. @@ -122,7 +155,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(and); let name = format!("{} && {}", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `false` @@ -174,7 +207,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(nand); let name = format!("!({} && {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `true` @@ -226,7 +259,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(nor); let name = format!("!({} || {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that returns `true` @@ -273,7 +306,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(or); let name = format!("{} || {}", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `true` @@ -325,7 +358,7 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(xnor); let name = format!("!({} ^ {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } /// Returns a new run condition that only returns `true` @@ -367,32 +400,65 @@ pub trait SystemCondition: let a = IntoSystem::into_system(self); let b = IntoSystem::into_system(xor); let name = format!("({} ^ {})", a.name(), b.name()); - CombinatorSystem::new(a, b, Cow::Owned(name)) + CombinatorSystem::new(a, b, DebugName::owned(name)) } } -impl SystemCondition for F where - F: sealed::SystemCondition +impl SystemCondition for F where + F: sealed::SystemCondition { } mod sealed { - use crate::system::{IntoSystem, ReadOnlySystem, SystemInput}; + use crate::{ + error::BevyError, + system::{IntoSystem, ReadOnlySystem, SystemInput}, + }; - pub trait SystemCondition: - IntoSystem + pub trait SystemCondition: + IntoSystem { // This associated type is necessary to let the compiler // know that `Self::System` is `ReadOnlySystem`. - type ReadOnlySystem: ReadOnlySystem; + type ReadOnlySystem: ReadOnlySystem; + + fn into_condition_system(self) -> impl ReadOnlySystem; } - impl SystemCondition for F + impl SystemCondition for F where F: IntoSystem, F::System: ReadOnlySystem, { type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self) + } + } + + impl SystemCondition> for F + where + F: IntoSystem, Marker>, + F::System: ReadOnlySystem, + { + type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self.map(|result| result.is_ok())) + } + } + + impl SystemCondition> for F + where + F: IntoSystem, Marker>, + F::System: ReadOnlySystem, + { + type ReadOnlySystem = F::System; + + fn into_condition_system(self) -> impl ReadOnlySystem { + IntoSystem::into_system(self.map(|result| matches!(result, Ok(true)))) + } } } @@ -401,10 +467,10 @@ pub mod common_conditions { use super::{NotSystem, SystemCondition}; use crate::{ change_detection::DetectChanges, - event::{Event, EventReader}, + event::{BufferedEvent, EventReader}, + lifecycle::RemovedComponents, prelude::{Component, Query, With}, query::QueryFilter, - removal_detection::RemovedComponents, resource::Resource, system::{In, IntoSystem, Local, Res, System, SystemInput}, }; @@ -863,7 +929,7 @@ pub mod common_conditions { /// my_system.run_if(on_event::), /// ); /// - /// #[derive(Event)] + /// #[derive(Event, BufferedEvent)] /// struct MyEvent; /// /// fn my_system(mut counter: ResMut) { @@ -880,7 +946,7 @@ pub mod common_conditions { /// app.run(&mut world); /// assert_eq!(world.resource::().0, 1); /// ``` - pub fn on_event(mut reader: EventReader) -> bool { + pub fn on_event(mut reader: EventReader) -> bool { // The events need to be consumed, so that there are no false positives on subsequent // calls of the run condition. Simply checking `is_empty` would not be enough. // PERF: note that `count` is efficient (not actually looping/iterating), @@ -1263,6 +1329,7 @@ where #[cfg(test)] mod tests { use super::{common_conditions::*, SystemCondition}; + use crate::event::{BufferedEvent, Event}; use crate::query::With; use crate::{ change_detection::ResMut, @@ -1271,7 +1338,7 @@ mod tests { system::Local, world::World, }; - use bevy_ecs_macros::{Event, Resource}; + use bevy_ecs_macros::Resource; #[derive(Resource, Default)] struct Counter(usize); @@ -1382,7 +1449,7 @@ mod tests { #[derive(Component)] struct TestComponent; - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct TestEvent; #[derive(Resource)] diff --git a/crates/bevy_ecs/src/schedule/config.rs b/crates/bevy_ecs/src/schedule/config.rs index f1a48e432b..4826d0a66d 100644 --- a/crates/bevy_ecs/src/schedule/config.rs +++ b/crates/bevy_ecs/src/schedule/config.rs @@ -14,8 +14,8 @@ use crate::{ system::{BoxedSystem, InfallibleSystemWrapper, IntoSystem, ScheduleSystem, System}, }; -fn new_condition(condition: impl SystemCondition) -> BoxedCondition { - let condition_system = IntoSystem::into_system(condition); +fn new_condition(condition: impl SystemCondition) -> BoxedCondition { + let condition_system = condition.into_condition_system(); assert!( condition_system.is_send(), "SystemCondition `{}` accesses `NonSend` resources. This is not currently supported.", @@ -447,7 +447,7 @@ pub trait IntoScheduleConfigs(self, condition: impl SystemCondition) -> ScheduleConfigs { + fn run_if(self, condition: impl SystemCondition) -> ScheduleConfigs { self.into_configs().run_if(condition) } @@ -535,7 +535,7 @@ impl> IntoScheduleCo self } - fn run_if(mut self, condition: impl SystemCondition) -> ScheduleConfigs { + fn run_if(mut self, condition: impl SystemCondition) -> ScheduleConfigs { self.run_if_dyn(new_condition(condition)); self } diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index cb6f3dcf8f..9156fc34a3 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -3,7 +3,8 @@ mod multi_threaded; mod simple; mod single_threaded; -use alloc::{borrow::Cow, vec, vec::Vec}; +use alloc::{vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::any::TypeId; #[expect(deprecated, reason = "We still need to support this.")] @@ -15,12 +16,12 @@ pub use self::multi_threaded::{MainThreadExecutor, MultiThreadedExecutor}; use fixedbitset::FixedBitSet; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::{BevyError, ErrorContext, Result}, prelude::{IntoSystemSet, SystemSet}, - query::{Access, FilteredAccessSet}, - schedule::{BoxedCondition, InternedSystemSet, NodeId, SystemTypeSet}, - system::{ScheduleSystem, System, SystemIn, SystemParamValidationError}, + query::FilteredAccessSet, + schedule::{ConditionWithAccess, InternedSystemSet, NodeId, SystemTypeSet, SystemWithAccess}, + system::{ScheduleSystem, System, SystemIn, SystemParamValidationError, SystemStateFlags}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; @@ -74,9 +75,9 @@ pub struct SystemSchedule { /// List of system node ids. pub(super) system_ids: Vec, /// Indexed by system node id. - pub(super) systems: Vec, + pub(super) systems: Vec, /// Indexed by system node id. - pub(super) system_conditions: Vec>, + pub(super) system_conditions: Vec>, /// Indexed by system node id. /// Number of systems that the system immediately depends on. #[cfg_attr( @@ -97,7 +98,7 @@ pub struct SystemSchedule { /// List of system set node ids. pub(super) set_ids: Vec, /// Indexed by system set node id. - pub(super) set_conditions: Vec>, + pub(super) set_conditions: Vec>, /// Indexed by system set node id. /// List of systems that are in sets that have conditions. /// @@ -158,39 +159,13 @@ impl System for ApplyDeferred { type In = (); type Out = Result<()>; - fn name(&self) -> Cow<'static, str> { - Cow::Borrowed("bevy_ecs::apply_deferred") + fn name(&self) -> DebugName { + DebugName::borrowed("bevy_ecs::apply_deferred") } - fn component_access(&self) -> &Access { - // This system accesses no components. - const { &Access::new() } - } - - fn component_access_set(&self) -> &FilteredAccessSet { - const { &FilteredAccessSet::new() } - } - - fn is_send(&self) -> bool { - // Although this system itself does nothing on its own, the system - // executor uses it to apply deferred commands. Commands must be allowed - // to access non-send resources, so this system must be non-send for - // scheduling purposes. - false - } - - fn is_exclusive(&self) -> bool { - // This system is labeled exclusive because it is used by the system - // executor to find places where deferred commands should be applied, - // and commands can only be applied with exclusive access to the world. - true - } - - fn has_deferred(&self) -> bool { - // This system itself doesn't have any commands to apply, but when it - // is pulled from the schedule to be ran, the executor will apply - // deferred commands from other systems. - false + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } unsafe fn run_unsafe( @@ -226,9 +201,11 @@ impl System for ApplyDeferred { Ok(()) } - fn initialize(&mut self, _world: &mut World) {} + fn initialize(&mut self, _world: &mut World) -> FilteredAccessSet { + FilteredAccessSet::new() + } - fn check_change_tick(&mut self, _change_tick: Tick) {} + fn check_change_tick(&mut self, _check: CheckChangeTicks) {} fn default_system_sets(&self) -> Vec { vec![SystemTypeSet::::new().intern()] diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 62a10298c9..b6036ee76b 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -15,7 +15,10 @@ use tracing::{info_span, Span}; use crate::{ error::{ErrorContext, ErrorHandler, Result}, prelude::Resource, - schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, + schedule::{ + is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, + SystemWithAccess, + }, system::ScheduleSystem, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -27,14 +30,14 @@ use super::__rust_begin_short_backtrace; /// Borrowed data used by the [`MultiThreadedExecutor`]. struct Environment<'env, 'sys> { executor: &'env MultiThreadedExecutor, - systems: &'sys [SyncUnsafeCell], + systems: &'sys [SyncUnsafeCell], conditions: SyncUnsafeCell>, world_cell: UnsafeWorldCell<'env>, } struct Conditions<'a> { - system_conditions: &'a mut [Vec], - set_conditions: &'a mut [Vec], + system_conditions: &'a mut [Vec], + set_conditions: &'a mut [Vec], sets_with_conditions_of_systems: &'a [FixedBitSet], systems_in_sets_with_conditions: &'a [FixedBitSet], } @@ -172,8 +175,8 @@ impl SystemExecutor for MultiThreadedExecutor { conflicting_systems: FixedBitSet::with_capacity(sys_count), condition_conflicting_systems: FixedBitSet::with_capacity(sys_count), dependents: schedule.system_dependents[index].clone(), - is_send: schedule.systems[index].is_send(), - is_exclusive: schedule.systems[index].is_exclusive(), + is_send: schedule.systems[index].system.is_send(), + is_exclusive: schedule.systems[index].system.is_exclusive(), }); if schedule.system_dependencies[index] == 0 { self.starting_systems.insert(index); @@ -187,10 +190,7 @@ impl SystemExecutor for MultiThreadedExecutor { let system1 = &schedule.systems[index1]; for index2 in 0..index1 { let system2 = &schedule.systems[index2]; - if !system2 - .component_access_set() - .is_compatible(system1.component_access_set()) - { + if !system2.access.is_compatible(&system1.access) { state.system_task_metadata[index1] .conflicting_systems .insert(index2); @@ -202,11 +202,10 @@ impl SystemExecutor for MultiThreadedExecutor { for index2 in 0..sys_count { let system2 = &schedule.systems[index2]; - if schedule.system_conditions[index1].iter().any(|condition| { - !system2 - .component_access_set() - .is_compatible(condition.component_access_set()) - }) { + if schedule.system_conditions[index1] + .iter() + .any(|condition| !system2.access.is_compatible(&condition.access)) + { state.system_task_metadata[index1] .condition_conflicting_systems .insert(index2); @@ -220,11 +219,10 @@ impl SystemExecutor for MultiThreadedExecutor { let mut conflicting_systems = FixedBitSet::with_capacity(sys_count); for sys_index in 0..sys_count { let system = &schedule.systems[sys_index]; - if schedule.set_conditions[set_idx].iter().any(|condition| { - !system - .component_access_set() - .is_compatible(condition.component_access_set()) - }) { + if schedule.set_conditions[set_idx] + .iter() + .any(|condition| !system.access.is_compatible(&condition.access)) + { conflicting_systems.insert(sys_index); } } @@ -344,7 +342,7 @@ impl<'scope, 'env: 'scope, 'sys> Context<'scope, 'env, 'sys> { #[cfg(feature = "std")] #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { - eprintln!("Encountered a panic in system `{}`!", &*system.name()); + eprintln!("Encountered a panic in system `{}`!", system.name()); } // set the payload to propagate the error { @@ -468,7 +466,8 @@ impl ExecutorState { debug_assert!(!self.running_systems.contains(system_index)); // SAFETY: Caller assured that these systems are not running. // Therefore, no other reference to this system exists and there is no aliasing. - let system = unsafe { &mut *context.environment.systems[system_index].get() }; + let system = + &mut unsafe { &mut *context.environment.systems[system_index].get() }.system; #[cfg(feature = "hotpatching")] if should_update_hotpatch { @@ -661,7 +660,7 @@ impl ExecutorState { /// used by the specified system. unsafe fn spawn_system_task(&mut self, context: &Context, system_index: usize) { // SAFETY: this system is not running, no other reference exists - let system = unsafe { &mut *context.environment.systems[system_index].get() }; + let system = &mut unsafe { &mut *context.environment.systems[system_index].get() }.system; // Move the full context object into the new future. let context = *context; @@ -703,7 +702,7 @@ impl ExecutorState { /// Caller must ensure no systems are currently borrowed. unsafe fn spawn_exclusive_system_task(&mut self, context: &Context, system_index: usize) { // SAFETY: this system is not running, no other reference exists - let system = unsafe { &mut *context.environment.systems[system_index].get() }; + let system = &mut unsafe { &mut *context.environment.systems[system_index].get() }.system; // Move the full context object into the new future. let context = *context; @@ -785,12 +784,12 @@ impl ExecutorState { fn apply_deferred( unapplied_systems: &FixedBitSet, - systems: &[SyncUnsafeCell], + systems: &[SyncUnsafeCell], world: &mut World, ) -> Result<(), Box> { for system_index in unapplied_systems.ones() { // SAFETY: none of these systems are running, no other references exist - let system = unsafe { &mut *systems[system_index].get() }; + let system = &mut unsafe { &mut *systems[system_index].get() }.system; let res = std::panic::catch_unwind(AssertUnwindSafe(|| { system.apply_deferred(world); })); @@ -800,7 +799,7 @@ fn apply_deferred( { eprintln!( "Encountered a panic when applying buffers for system `{}`!", - &*system.name() + system.name() ); } return Err(payload); @@ -813,7 +812,7 @@ fn apply_deferred( /// - `world` must have permission to read any world data /// required by `conditions`. unsafe fn evaluate_and_fold_conditions( - conditions: &mut [BoxedCondition], + conditions: &mut [ConditionWithAccess], world: UnsafeWorldCell, error_handler: ErrorHandler, ) -> bool { @@ -823,7 +822,7 @@ unsafe fn evaluate_and_fold_conditions( )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { // SAFETY: // - The caller ensures that `world` has permission to read any data // required by the condition. diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index d9069aa6e8..f0d655eab8 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -12,7 +12,8 @@ use std::eprintln; use crate::{ error::{ErrorContext, ErrorHandler}, schedule::{ - executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, + executor::is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, + SystemSchedule, }, world::World, }; @@ -70,9 +71,9 @@ impl SystemExecutor for SimpleExecutor { for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] - let name = schedule.systems[system_index].name(); + let name = schedule.systems[system_index].system.name(); #[cfg(feature = "trace")] - let should_run_span = info_span!("check_conditions", name = &*name).entered(); + let should_run_span = info_span!("check_conditions", name = name.as_string()).entered(); let mut should_run = !self.completed_systems.contains(system_index); for set_idx in schedule.sets_with_conditions_of_systems[system_index].ones() { @@ -105,7 +106,7 @@ impl SystemExecutor for SimpleExecutor { should_run &= system_conditions_met; - let system = &mut schedule.systems[system_index]; + let system = &mut schedule.systems[system_index].system; if should_run { let valid_params = match system.validate_param(world) { Ok(()) => true, @@ -160,7 +161,7 @@ impl SystemExecutor for SimpleExecutor { #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { if let Err(payload) = std::panic::catch_unwind(f) { - eprintln!("Encountered a panic in system `{}`!", &*system.name()); + eprintln!("Encountered a panic in system `{}`!", system.name()); std::panic::resume_unwind(payload); } } @@ -195,7 +196,7 @@ impl SimpleExecutor { note = "Use SingleThreadedExecutor instead. See https://github.com/bevyengine/bevy/issues/18453 for motivation." )] fn evaluate_and_fold_conditions( - conditions: &mut [BoxedCondition], + conditions: &mut [ConditionWithAccess], world: &mut World, error_handler: ErrorHandler, ) -> bool { @@ -211,7 +212,7 @@ fn evaluate_and_fold_conditions( )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { match condition.validate_param(world) { Ok(()) => (), Err(e) => { diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 68af623b40..21b8d2289d 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -9,7 +9,9 @@ use std::eprintln; use crate::{ error::{ErrorContext, ErrorHandler}, - schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, + schedule::{ + is_apply_deferred, ConditionWithAccess, ExecutorKind, SystemExecutor, SystemSchedule, + }, world::World, }; #[cfg(feature = "hotpatching")] @@ -70,9 +72,9 @@ impl SystemExecutor for SingleThreadedExecutor { for system_index in 0..schedule.systems.len() { #[cfg(feature = "trace")] - let name = schedule.systems[system_index].name(); + let name = schedule.systems[system_index].system.name(); #[cfg(feature = "trace")] - let should_run_span = info_span!("check_conditions", name = &*name).entered(); + let should_run_span = info_span!("check_conditions", name = name.as_string()).entered(); let mut should_run = !self.completed_systems.contains(system_index); for set_idx in schedule.sets_with_conditions_of_systems[system_index].ones() { @@ -105,7 +107,7 @@ impl SystemExecutor for SingleThreadedExecutor { should_run &= system_conditions_met; - let system = &mut schedule.systems[system_index]; + let system = &mut schedule.systems[system_index].system; if should_run { let valid_params = match system.validate_param(world) { Ok(()) => true, @@ -164,7 +166,7 @@ impl SystemExecutor for SingleThreadedExecutor { #[expect(clippy::print_stderr, reason = "Allowed behind `std` feature gate.")] { if let Err(payload) = std::panic::catch_unwind(f) { - eprintln!("Encountered a panic in system `{}`!", &*system.name()); + eprintln!("Encountered a panic in system `{}`!", system.name()); std::panic::resume_unwind(payload); } } @@ -204,7 +206,7 @@ impl SingleThreadedExecutor { fn apply_deferred(&mut self, schedule: &mut SystemSchedule, world: &mut World) { for system_index in self.unapplied_systems.ones() { - let system = &mut schedule.systems[system_index]; + let system = &mut schedule.systems[system_index].system; system.apply_deferred(world); } @@ -213,7 +215,7 @@ impl SingleThreadedExecutor { } fn evaluate_and_fold_conditions( - conditions: &mut [BoxedCondition], + conditions: &mut [ConditionWithAccess], world: &mut World, error_handler: ErrorHandler, ) -> bool { @@ -229,7 +231,7 @@ fn evaluate_and_fold_conditions( )] conditions .iter_mut() - .map(|condition| { + .map(|ConditionWithAccess { condition, .. }| { match condition.validate_param(world) { Ok(()) => (), Err(e) => { diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index 81912d2f72..91f1b41312 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -29,6 +29,7 @@ mod tests { use alloc::{string::ToString, vec, vec::Vec}; use core::sync::atomic::{AtomicU32, Ordering}; + use crate::error::BevyError; pub use crate::{ prelude::World, resource::Resource, @@ -49,10 +50,10 @@ mod tests { struct SystemOrder(Vec); #[derive(Resource, Default)] - struct RunConditionBool(pub bool); + struct RunConditionBool(bool); #[derive(Resource, Default)] - struct Counter(pub AtomicU32); + struct Counter(AtomicU32); fn make_exclusive_system(tag: u32) -> impl FnMut(&mut World) { move |world| world.resource_mut::().0.push(tag) @@ -252,12 +253,13 @@ mod tests { } mod conditions { + use crate::change_detection::DetectChanges; use super::*; #[test] - fn system_with_condition() { + fn system_with_condition_bool() { let mut world = World::default(); let mut schedule = Schedule::default(); @@ -276,6 +278,47 @@ mod tests { assert_eq!(world.resource::().0, vec![0]); } + #[test] + fn system_with_condition_result_unit() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_systems( + make_function_system(0).run_if(|| Err::<(), BevyError>(core::fmt::Error.into())), + ); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + schedule.add_systems(make_function_system(1).run_if(|| Ok(()))); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![1]); + } + + #[test] + fn system_with_condition_result_bool() { + let mut world = World::default(); + let mut schedule = Schedule::default(); + + world.init_resource::(); + + schedule.add_systems(( + make_function_system(0).run_if(|| Err::(core::fmt::Error.into())), + make_function_system(1).run_if(|| Ok(false)), + )); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![]); + + schedule.add_systems(make_function_system(2).run_if(|| Ok(true))); + + schedule.run(&mut world); + assert_eq!(world.resource::().0, vec![2]); + } + #[test] fn systems_with_distributive_condition() { let mut world = World::default(); @@ -741,8 +784,7 @@ mod tests { #[derive(Component)] struct B; - // An event type - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct E; #[derive(Resource, Component)] @@ -874,7 +916,6 @@ mod tests { } #[test] - #[ignore = "Known failing but fix is non-trivial: https://github.com/bevyengine/bevy/issues/4381"] fn filtered_components() { let mut world = World::new(); world.spawn(A); diff --git a/crates/bevy_ecs/src/schedule/schedule.rs b/crates/bevy_ecs/src/schedule/schedule.rs index 144ce6516c..3c67f1ea07 100644 --- a/crates/bevy_ecs/src/schedule/schedule.rs +++ b/crates/bevy_ecs/src/schedule/schedule.rs @@ -2,7 +2,6 @@ clippy::module_inception, reason = "This instance of module inception is being discussed; see #17344." )] -use alloc::borrow::Cow; use alloc::{ boxed::Box, collections::{BTreeMap, BTreeSet}, @@ -12,12 +11,11 @@ use alloc::{ vec::Vec, }; use bevy_platform::collections::{HashMap, HashSet}; -use bevy_utils::{default, TypeIdMap}; +use bevy_utils::{default, prelude::DebugName, TypeIdMap}; use core::{ any::{Any, TypeId}, fmt::{Debug, Write}, }; -use disqualified::ShortName; use fixedbitset::FixedBitSet; use log::{error, info, warn}; use pass::ScheduleBuildPassObj; @@ -25,9 +23,11 @@ use thiserror::Error; #[cfg(feature = "trace")] use tracing::info_span; +use crate::component::CheckChangeTicks; use crate::{ - component::{ComponentId, Components, Tick}, + component::{ComponentId, Components}, prelude::Component, + query::FilteredAccessSet, resource::Resource, schedule::*, system::ScheduleSystem, @@ -111,7 +111,7 @@ impl Schedules { /// Iterates the change ticks of all systems in all stored schedules and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { #[cfg(feature = "trace")] let _all_span = info_span!("check stored schedule ticks").entered(); #[cfg_attr( @@ -126,7 +126,7 @@ impl Schedules { let name = format!("{label:?}"); #[cfg(feature = "trace")] let _one_span = info_span!("check schedule ticks", name = &name).entered(); - schedule.check_change_ticks(change_tick); + schedule.check_change_ticks(check); } } @@ -166,7 +166,7 @@ impl Schedules { writeln!(message, "{}", components.get_name(*id).unwrap()).unwrap(); } - info!("{}", message); + info!("{message}"); } /// Adds one or more systems to the [`Schedule`] matching the provided [`ScheduleLabel`]. @@ -558,22 +558,22 @@ impl Schedule { /// Iterates the change ticks of all systems in the schedule and clamps any older than /// [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). /// This prevents overflow and thus prevents false positives. - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - for system in &mut self.executable.systems { + pub fn check_change_ticks(&mut self, check: CheckChangeTicks) { + for SystemWithAccess { system, .. } in &mut self.executable.systems { if !is_apply_deferred(system) { - system.check_change_tick(change_tick); + system.check_change_tick(check); } } for conditions in &mut self.executable.system_conditions { for system in conditions { - system.check_change_tick(change_tick); + system.condition.check_change_tick(check); } } for conditions in &mut self.executable.set_conditions { for system in conditions { - system.check_change_tick(change_tick); + system.condition.check_change_tick(check); } } } @@ -587,7 +587,7 @@ impl Schedule { /// This is used in rendering to extract data from the main world, storing the data in system buffers, /// before applying their buffers in a different world. pub fn apply_deferred(&mut self, world: &mut World) { - for system in &mut self.executable.systems { + for SystemWithAccess { system, .. } in &mut self.executable.systems { system.apply_deferred(world); } } @@ -609,7 +609,7 @@ impl Schedule { .system_ids .iter() .zip(&self.executable.systems) - .map(|(node_id, system)| (*node_id, system)); + .map(|(node_id, system)| (*node_id, &system.system)); Ok(iter) } @@ -677,26 +677,66 @@ impl SystemSetNode { } } -/// A [`ScheduleSystem`] stored in a [`ScheduleGraph`]. +/// A [`SystemWithAccess`] stored in a [`ScheduleGraph`]. pub struct SystemNode { - inner: Option, + inner: Option, +} + +/// A [`ScheduleSystem`] stored alongside the access returned from [`System::initialize`](crate::system::System::initialize). +pub struct SystemWithAccess { + /// The system itself. + pub system: ScheduleSystem, + /// The access returned by [`System::initialize`](crate::system::System::initialize). + /// This will be empty if the system has not been initialized yet. + pub access: FilteredAccessSet, +} + +impl SystemWithAccess { + /// Constructs a new [`SystemWithAccess`] from a [`ScheduleSystem`]. + /// The `access` will initially be empty. + pub fn new(system: ScheduleSystem) -> Self { + Self { + system, + access: FilteredAccessSet::new(), + } + } +} + +/// A [`BoxedCondition`] stored alongside the access returned from [`System::initialize`](crate::system::System::initialize). +pub struct ConditionWithAccess { + /// The condition itself. + pub condition: BoxedCondition, + /// The access returned by [`System::initialize`](crate::system::System::initialize). + /// This will be empty if the system has not been initialized yet. + pub access: FilteredAccessSet, +} + +impl ConditionWithAccess { + /// Constructs a new [`ConditionWithAccess`] from a [`BoxedCondition`]. + /// The `access` will initially be empty. + pub const fn new(condition: BoxedCondition) -> Self { + Self { + condition, + access: FilteredAccessSet::new(), + } + } } impl SystemNode { /// Create a new [`SystemNode`] pub fn new(system: ScheduleSystem) -> Self { Self { - inner: Some(system), + inner: Some(SystemWithAccess::new(system)), } } - /// Obtain a reference to the [`ScheduleSystem`] represented by this node. - pub fn get(&self) -> Option<&ScheduleSystem> { + /// Obtain a reference to the [`SystemWithAccess`] represented by this node. + pub fn get(&self) -> Option<&SystemWithAccess> { self.inner.as_ref() } - /// Obtain a mutable reference to the [`ScheduleSystem`] represented by this node. - pub fn get_mut(&mut self) -> Option<&mut ScheduleSystem> { + /// Obtain a mutable reference to the [`SystemWithAccess`] represented by this node. + pub fn get_mut(&mut self) -> Option<&mut SystemWithAccess> { self.inner.as_mut() } } @@ -710,11 +750,11 @@ pub struct ScheduleGraph { /// List of systems in the schedule pub systems: Vec, /// List of conditions for each system, in the same order as `systems` - pub system_conditions: Vec>, + pub system_conditions: Vec>, /// List of system sets in the schedule system_sets: Vec, /// List of conditions for each system set, in the same order as `system_sets` - system_set_conditions: Vec>, + system_set_conditions: Vec>, /// Map from system set to node id system_set_ids: HashMap, /// Systems that have not been initialized yet; for system sets, we store the index of the first uninitialized condition @@ -765,6 +805,7 @@ impl ScheduleGraph { self.systems .get(id.index()) .and_then(|system| system.inner.as_ref()) + .map(|system| &system.system) } /// Returns `true` if the given system set is part of the graph. Otherwise, returns `false`. @@ -801,7 +842,7 @@ impl ScheduleGraph { } /// Returns the conditions for the set at the given [`NodeId`], if it exists. - pub fn get_set_conditions_at(&self, id: NodeId) -> Option<&[BoxedCondition]> { + pub fn get_set_conditions_at(&self, id: NodeId) -> Option<&[ConditionWithAccess]> { if !id.is_set() { return None; } @@ -814,27 +855,31 @@ impl ScheduleGraph { /// /// Panics if it doesn't exist. #[track_caller] - pub fn set_conditions_at(&self, id: NodeId) -> &[BoxedCondition] { + pub fn set_conditions_at(&self, id: NodeId) -> &[ConditionWithAccess] { self.get_set_conditions_at(id) .ok_or_else(|| format!("set with id {id:?} does not exist in this Schedule")) .unwrap() } /// Returns an iterator over all systems in this schedule, along with the conditions for each system. - pub fn systems(&self) -> impl Iterator { + pub fn systems( + &self, + ) -> impl Iterator { self.systems .iter() .zip(self.system_conditions.iter()) .enumerate() .filter_map(|(i, (system_node, condition))| { - let system = system_node.inner.as_ref()?; + let system = &system_node.inner.as_ref()?.system; Some((NodeId::System(i), system, condition.as_slice())) }) } /// Returns an iterator over all system sets in this schedule, along with the conditions for each /// system set. - pub fn system_sets(&self) -> impl Iterator { + pub fn system_sets( + &self, + ) -> impl Iterator { self.system_set_ids.iter().map(|(_, &node_id)| { let set_node = &self.system_sets[node_id.index()]; let set = &*set_node.inner; @@ -1013,7 +1058,13 @@ impl ScheduleGraph { // system init has to be deferred (need `&mut World`) self.uninit.push((id, 0)); self.systems.push(SystemNode::new(config.node)); - self.system_conditions.push(config.conditions); + self.system_conditions.push( + config + .conditions + .into_iter() + .map(ConditionWithAccess::new) + .collect(), + ); Ok(id) } @@ -1031,7 +1082,7 @@ impl ScheduleGraph { let ScheduleConfig { node: set, metadata, - mut conditions, + conditions, } = set; let id = match self.system_set_ids.get(&set) { @@ -1045,7 +1096,7 @@ impl ScheduleGraph { // system init has to be deferred (need `&mut World`) let system_set_conditions = &mut self.system_set_conditions[id.index()]; self.uninit.push((id, system_set_conditions.len())); - system_set_conditions.append(&mut conditions); + system_set_conditions.extend(conditions.into_iter().map(ConditionWithAccess::new)); Ok(id) } @@ -1197,14 +1248,15 @@ impl ScheduleGraph { for (id, i) in self.uninit.drain(..) { match id { NodeId::System(index) => { - self.systems[index].get_mut().unwrap().initialize(world); + let system = self.systems[index].get_mut().unwrap(); + system.access = system.system.initialize(world); for condition in &mut self.system_conditions[index] { - condition.initialize(world); + condition.access = condition.condition.initialize(world); } } NodeId::Set(index) => { for condition in self.system_set_conditions[index].iter_mut().skip(i) { - condition.initialize(world); + condition.access = condition.condition.initialize(world); } } } @@ -1415,11 +1467,11 @@ impl ScheduleGraph { let system_a = self.systems[a.index()].get().unwrap(); let system_b = self.systems[b.index()].get().unwrap(); - if system_a.is_exclusive() || system_b.is_exclusive() { + if system_a.system.is_exclusive() || system_b.system.is_exclusive() { conflicting_systems.push((a, b, Vec::new())); } else { - let access_a = system_a.component_access(); - let access_b = system_b.component_access(); + let access_a = &system_a.access; + let access_b = &system_b.access; if !access_a.is_compatible(access_b) { match access_a.get_conflicts(access_b) { AccessConflicts::Individual(conflicts) => { @@ -1640,9 +1692,14 @@ impl ScheduleGraph { #[inline] fn get_node_name_inner(&self, id: &NodeId, report_sets: bool) -> String { - let name = match id { + match id { NodeId::System(_) => { - let name = self.systems[id.index()].get().unwrap().name().to_string(); + let name = self.systems[id.index()].get().unwrap().system.name(); + let name = if self.settings.use_shortnames { + name.shortname().to_string() + } else { + name.to_string() + }; if report_sets { let sets = self.names_of_sets_containing_node(id); if sets.is_empty() { @@ -1664,11 +1721,6 @@ impl ScheduleGraph { set.name() } } - }; - if self.settings.use_shortnames { - ShortName(&name).to_string() - } else { - name } } @@ -1707,10 +1759,7 @@ impl ScheduleGraph { match self.settings.hierarchy_detection { LogLevel::Ignore => unreachable!(), LogLevel::Warn => { - error!( - "Schedule {schedule_label:?} has redundant edges:\n {}", - message - ); + error!("Schedule {schedule_label:?} has redundant edges:\n {message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::HierarchyRedundancy(message)), @@ -1912,7 +1961,7 @@ impl ScheduleGraph { match self.settings.ambiguity_detection { LogLevel::Ignore => Ok(()), LogLevel::Warn => { - warn!("Schedule {schedule_label:?} has ambiguities.\n{}", message); + warn!("Schedule {schedule_label:?} has ambiguities.\n{message}"); Ok(()) } LogLevel::Error => Err(ScheduleBuildError::Ambiguity(message)), @@ -1951,7 +2000,7 @@ impl ScheduleGraph { &'a self, ambiguities: &'a [(NodeId, NodeId, Vec)], components: &'a Components, - ) -> impl Iterator>)> + 'a { + ) -> impl Iterator)> + 'a { ambiguities .iter() .map(move |(system_a, system_b, conflicts)| { diff --git a/crates/bevy_ecs/src/schedule/set.rs b/crates/bevy_ecs/src/schedule/set.rs index 4974be5d43..da91f616e9 100644 --- a/crates/bevy_ecs/src/schedule/set.rs +++ b/crates/bevy_ecs/src/schedule/set.rs @@ -1,4 +1,5 @@ use alloc::boxed::Box; +use bevy_utils::prelude::DebugName; use core::{ any::TypeId, fmt::Debug, @@ -60,7 +61,93 @@ define_label!( ); define_label!( - /// Types that identify logical groups of systems. + /// System sets are tag-like labels that can be used to group systems together. + /// + /// This allows you to share configuration (like run conditions) across multiple systems, + /// and order systems or system sets relative to conceptual groups of systems. + /// To control the behavior of a system set as a whole, use [`Schedule::configure_sets`](crate::prelude::Schedule::configure_sets), + /// or the method of the same name on `App`. + /// + /// Systems can belong to any number of system sets, reflecting multiple roles or facets that they might have. + /// For example, you may want to annotate a system as "consumes input" and "applies forces", + /// and ensure that your systems are ordered correctly for both of those sets. + /// + /// System sets can belong to any number of other system sets, + /// allowing you to create nested hierarchies of system sets to group systems together. + /// Configuration applied to system sets will flow down to their members (including other system sets), + /// allowing you to set and modify the configuration in a single place. + /// + /// Systems sets are also useful for exposing a consistent public API for dependencies + /// to hook into across versions of your crate, + /// allowing them to add systems to a specific set, or order relative to that set, + /// without leaking implementation details of the exact systems involved. + /// + /// ## Defining new system sets + /// + /// To create a new system set, use the `#[derive(SystemSet)]` macro. + /// Unit structs are a good choice for one-off sets. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// struct PhysicsSystems; + /// ``` + /// + /// When you want to define several related system sets, + /// consider creating an enum system set. + /// Each variant will be treated as a separate system set. + /// + /// ```rust + /// # use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// ``` + /// + /// By convention, the listed order of the system set in the enum + /// corresponds to the order in which the systems are run. + /// Ordering must be explicitly added to ensure that this is the case, + /// but following this convention will help avoid confusion. + /// + /// ### Adding systems to system sets + /// + /// To add systems to a system set, call [`in_set`](crate::prelude::IntoScheduleConfigs::in_set) on the system function + /// while adding it to your app or schedule. + /// + /// Like usual, these methods can be chained with other configuration methods like [`before`](crate::prelude::IntoScheduleConfigs::before), + /// or repeated to add systems to multiple sets. + /// + /// ```rust + /// use bevy_ecs::prelude::*; + /// + /// #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] + /// enum CombatSystems { + /// TargetSelection, + /// DamageCalculation, + /// Cleanup, + /// } + /// + /// fn target_selection() {} + /// + /// fn enemy_damage_calculation() {} + /// + /// fn player_damage_calculation() {} + /// + /// let mut schedule = Schedule::default(); + /// // Configuring the sets to run in order. + /// schedule.configure_sets((CombatSystems::TargetSelection, CombatSystems::DamageCalculation, CombatSystems::Cleanup).chain()); + /// + /// // Adding a single system to a set. + /// schedule.add_systems(target_selection.in_set(CombatSystems::TargetSelection)); + /// + /// // Adding multiple systems to a set. + /// schedule.add_systems((player_damage_calculation, enemy_damage_calculation).in_set(CombatSystems::DamageCalculation)); + /// ``` #[diagnostic::on_unimplemented( note = "consider annotating `{Self}` with `#[derive(SystemSet)]`" )] @@ -110,7 +197,7 @@ impl SystemTypeSet { impl Debug for SystemTypeSet { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.debug_tuple("SystemTypeSet") - .field(&format_args!("fn {}()", &core::any::type_name::())) + .field(&format_args!("fn {}()", DebugName::type_name::())) .finish() } } diff --git a/crates/bevy_ecs/src/schedule/stepping.rs b/crates/bevy_ecs/src/schedule/stepping.rs index 222dfdfcaf..b0d8b57fb0 100644 --- a/crates/bevy_ecs/src/schedule/stepping.rs +++ b/crates/bevy_ecs/src/schedule/stepping.rs @@ -475,9 +475,8 @@ impl Stepping { Some(state) => state.clear_behaviors(), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } }, @@ -486,9 +485,8 @@ impl Stepping { Some(state) => state.set_behavior(system, behavior), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } @@ -498,9 +496,8 @@ impl Stepping { Some(state) => state.clear_behavior(system), None => { warn!( - "stepping is not enabled for schedule {:?}; \ - use `.add_stepping({:?})` to enable stepping", - label, label + "stepping is not enabled for schedule {label:?}; \ + use `.add_stepping({label:?})` to enable stepping" ); } } @@ -898,9 +895,9 @@ mod tests { ($schedule:expr, $skipped_systems:expr, $($system:expr),*) => { // pull an ordered list of systems in the schedule, and save the // system TypeId, and name. - let systems: Vec<(TypeId, alloc::borrow::Cow<'static, str>)> = $schedule.systems().unwrap() + let systems: Vec<(TypeId, alloc::string::String)> = $schedule.systems().unwrap() .map(|(_, system)| { - (system.type_id(), system.name()) + (system.type_id(), system.name().as_string()) }) .collect(); diff --git a/crates/bevy_ecs/src/storage/resource.rs b/crates/bevy_ecs/src/storage/resource.rs index fa58610bdf..29752ae2e5 100644 --- a/crates/bevy_ecs/src/storage/resource.rs +++ b/crates/bevy_ecs/src/storage/resource.rs @@ -1,10 +1,10 @@ use crate::{ change_detection::{MaybeLocation, MutUntyped, TicksMut}, - component::{ComponentId, ComponentTicks, Components, Tick, TickCells}, + component::{CheckChangeTicks, ComponentId, ComponentTicks, Components, Tick, TickCells}, storage::{blob_vec::BlobVec, SparseSet}, }; -use alloc::string::String; use bevy_ptr::{OwningPtr, Ptr, UnsafeCellDeref}; +use bevy_utils::prelude::DebugName; use core::{cell::UnsafeCell, mem::ManuallyDrop, panic::Location}; #[cfg(feature = "std")] @@ -23,7 +23,7 @@ pub struct ResourceData { not(feature = "std"), expect(dead_code, reason = "currently only used with the std feature") )] - type_name: String, + type_name: DebugName, #[cfg(feature = "std")] origin_thread_id: Option, changed_by: MaybeLocation>>, @@ -298,9 +298,9 @@ impl ResourceData { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - self.added_ticks.get_mut().check_tick(change_tick); - self.changed_ticks.get_mut().check_tick(change_tick); + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { + self.added_ticks.get_mut().check_tick(check); + self.changed_ticks.get_mut().check_tick(check); } } @@ -385,7 +385,7 @@ impl Resources { data: ManuallyDrop::new(data), added_ticks: UnsafeCell::new(Tick::new(0)), changed_ticks: UnsafeCell::new(Tick::new(0)), - type_name: String::from(component_info.name()), + type_name: component_info.name(), #[cfg(feature = "std")] origin_thread_id: None, changed_by: MaybeLocation::caller().map(UnsafeCell::new), @@ -393,9 +393,9 @@ impl Resources { }) } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for info in self.resources.values_mut() { - info.check_change_ticks(change_tick); + info.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/storage/sparse_set.rs b/crates/bevy_ecs/src/storage/sparse_set.rs index 42adcd89dc..bb28f967af 100644 --- a/crates/bevy_ecs/src/storage/sparse_set.rs +++ b/crates/bevy_ecs/src/storage/sparse_set.rs @@ -1,6 +1,6 @@ use crate::{ change_detection::MaybeLocation, - component::{ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, + component::{CheckChangeTicks, ComponentId, ComponentInfo, ComponentTicks, Tick, TickCells}, entity::{Entity, EntityRow}, storage::{Column, TableRow}, }; @@ -360,8 +360,8 @@ impl ComponentSparseSet { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { - self.dense.check_change_ticks(change_tick); + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { + self.dense.check_change_ticks(check); } } @@ -650,9 +650,9 @@ impl SparseSets { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for set in self.sets.values_mut() { - set.check_change_ticks(change_tick); + set.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/storage/table/column.rs b/crates/bevy_ecs/src/storage/table/column.rs index 78fafe0a26..acf531d9b9 100644 --- a/crates/bevy_ecs/src/storage/table/column.rs +++ b/crates/bevy_ecs/src/storage/table/column.rs @@ -228,20 +228,20 @@ impl ThinColumn { /// # Safety /// `len` is the actual length of this column #[inline] - pub(crate) unsafe fn check_change_ticks(&mut self, len: usize, change_tick: Tick) { + pub(crate) unsafe fn check_change_ticks(&mut self, len: usize, check: CheckChangeTicks) { for i in 0..len { // SAFETY: // - `i` < `len` // we have a mutable reference to `self` unsafe { self.added_ticks.get_unchecked_mut(i) } .get_mut() - .check_tick(change_tick); + .check_tick(check); // SAFETY: // - `i` < `len` // we have a mutable reference to `self` unsafe { self.changed_ticks.get_unchecked_mut(i) } .get_mut() - .check_tick(change_tick); + .check_tick(check); } } @@ -646,12 +646,12 @@ impl Column { } #[inline] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for component_ticks in &mut self.added_ticks { - component_ticks.get_mut().check_tick(change_tick); + component_ticks.get_mut().check_tick(check); } for component_ticks in &mut self.changed_ticks { - component_ticks.get_mut().check_tick(change_tick); + component_ticks.get_mut().check_tick(check); } } diff --git a/crates/bevy_ecs/src/storage/table/mod.rs b/crates/bevy_ecs/src/storage/table/mod.rs index 5f09d4226f..be75c58f03 100644 --- a/crates/bevy_ecs/src/storage/table/mod.rs +++ b/crates/bevy_ecs/src/storage/table/mod.rs @@ -1,6 +1,6 @@ use crate::{ change_detection::MaybeLocation, - component::{ComponentId, ComponentInfo, ComponentTicks, Components, Tick}, + component::{CheckChangeTicks, ComponentId, ComponentInfo, ComponentTicks, Components, Tick}, entity::Entity, query::DebugCheckedUnwrap, storage::{blob_vec::BlobVec, ImmutableSparseSet, SparseSet}, @@ -629,11 +629,11 @@ impl Table { } /// Call [`Tick::check_tick`] on all of the ticks in the [`Table`] - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { let len = self.entity_count() as usize; for col in self.columns.values_mut() { // SAFETY: `len` is the actual length of the column - unsafe { col.check_change_ticks(len, change_tick) }; + unsafe { col.check_change_ticks(len, check) }; } } @@ -793,9 +793,9 @@ impl Tables { } } - pub(crate) fn check_change_ticks(&mut self, change_tick: Tick) { + pub(crate) fn check_change_ticks(&mut self, check: CheckChangeTicks) { for table in &mut self.tables { - table.check_change_ticks(change_tick); + table.check_change_ticks(check); } } } diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index f728a4e907..c655ed9407 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -1,4 +1,5 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use super::{IntoSystem, ReadOnlySystem, System, SystemParamValidationError}; use crate::{ @@ -101,7 +102,7 @@ where pub struct AdapterSystem { func: Func, system: S, - name: Cow<'static, str>, + name: DebugName, } impl AdapterSystem @@ -110,7 +111,7 @@ where S: System, { /// Creates a new [`System`] that uses `func` to adapt `system`, via the [`Adapt`] trait. - pub const fn new(func: Func, system: S, name: Cow<'static, str>) -> Self { + pub const fn new(func: Func, system: S, name: DebugName) -> Self { Self { func, system, name } } } @@ -123,30 +124,13 @@ where type In = Func::In; type Out = Func::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.name.clone() } - fn component_access(&self) -> &crate::query::Access { - self.system.component_access() - } - - fn component_access_set( - &self, - ) -> &crate::query::FilteredAccessSet { - self.system.component_access_set() - } - - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.system.flags() } #[inline] @@ -186,12 +170,15 @@ where unsafe { self.system.validate_param_unsafe(world) } } - fn initialize(&mut self, world: &mut crate::prelude::World) { - self.system.initialize(world); + fn initialize( + &mut self, + world: &mut crate::prelude::World, + ) -> crate::query::FilteredAccessSet { + self.system.initialize(world) } - fn check_change_tick(&mut self, change_tick: crate::component::Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: crate::component::CheckChangeTicks) { + self.system.check_change_tick(check); } fn default_system_sets(&self) -> Vec { diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index 6536c9cc1e..7aea731314 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -7,7 +7,7 @@ use crate::{ query::{QueryData, QueryFilter, QueryState}, resource::Resource, system::{ - DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemMeta, SystemParam, + DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemParam, SystemParamValidationError, When, }, world::{ @@ -17,7 +17,7 @@ use crate::{ }; use core::fmt::Debug; -use super::{init_query_param, Res, ResMut, SystemState}; +use super::{Res, ResMut, SystemState}; /// A builder that can create a [`SystemParam`]. /// @@ -104,19 +104,15 @@ use super::{init_query_param, Res, ResMut, SystemState}; /// /// # Safety /// -/// The implementor must ensure the following is true. -/// - [`SystemParamBuilder::build`] correctly registers all [`World`] accesses used -/// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). -/// - None of the world accesses may conflict with any prior accesses registered -/// on `system_meta`. -/// -/// Note that this depends on the implementation of [`SystemParam::get_param`], +/// The implementor must ensure that the state returned +/// from [`SystemParamBuilder::build`] is valid for `P`. +/// Note that the exact safety requiremensts depend on the implementation of [`SystemParam`], /// so if `Self` is not a local type then you must call [`SystemParam::init_state`] -/// or another [`SystemParamBuilder::build`] +/// or another [`SystemParamBuilder::build`]. pub unsafe trait SystemParamBuilder: Sized { /// Registers any [`World`] access used by this [`SystemParam`] /// and creates a new instance of this param's [`State`](SystemParam::State). - fn build(self, world: &mut World, meta: &mut SystemMeta) -> P::State; + fn build(self, world: &mut World) -> P::State; /// Create a [`SystemState`] from a [`SystemParamBuilder`]. /// To create a system, call [`SystemState::build_system`] on the result. @@ -169,8 +165,8 @@ pub struct ParamBuilder; // SAFETY: Calls `SystemParam::init_state` unsafe impl SystemParamBuilder

for ParamBuilder { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> P::State { - P::init_state(world, meta) + fn build(self, world: &mut World) -> P::State { + P::init_state(world) } } @@ -208,13 +204,13 @@ impl ParamBuilder { } } -// SAFETY: Calls `init_query_param`, just like `Query::init_state`. +// SAFETY: Any `QueryState` for the correct world is valid for `Query::State`, +// and we check the world during `build`. unsafe impl<'w, 's, D: QueryData + 'static, F: QueryFilter + 'static> SystemParamBuilder> for QueryState { - fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> QueryState { + fn build(self, world: &mut World) -> QueryState { self.validate_world(world.id()); - init_query_param(world, system_meta, &self); self } } @@ -282,7 +278,8 @@ impl<'a, D: QueryData, F: QueryFilter> } } -// SAFETY: Calls `init_query_param`, just like `Query::init_state`. +// SAFETY: Any `QueryState` for the correct world is valid for `Query::State`, +// and `QueryBuilder` produces one with the given `world`. unsafe impl< 'w, 's, @@ -291,12 +288,10 @@ unsafe impl< T: FnOnce(&mut QueryBuilder), > SystemParamBuilder> for QueryParamBuilder { - fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> QueryState { + fn build(self, world: &mut World) -> QueryState { let mut builder = QueryBuilder::new(world); (self.0)(&mut builder); - let state = builder.build(); - init_query_param(world, system_meta, &state); - state + builder.build() } } @@ -317,13 +312,13 @@ macro_rules! impl_system_param_builder_tuple { $(#[$meta])* // SAFETY: implementors of each `SystemParamBuilder` in the tuple have validated their impls unsafe impl<$($param: SystemParam,)* $($builder: SystemParamBuilder<$param>,)*> SystemParamBuilder<($($param,)*)> for ($($builder,)*) { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { + fn build(self, world: &mut World) -> <($($param,)*) as SystemParam>::State { let ($($builder,)*) = self; #[allow( clippy::unused_unit, reason = "Zero-length tuples won't generate any calls to the system parameter builders." )] - ($($builder.build(world, meta),)*) + ($($builder.build(world),)*) } } }; @@ -340,9 +335,9 @@ all_tuples!( // SAFETY: implementors of each `SystemParamBuilder` in the vec have validated their impls unsafe impl> SystemParamBuilder> for Vec { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { + fn build(self, world: &mut World) -> as SystemParam>::State { self.into_iter() - .map(|builder| builder.build(world, meta)) + .map(|builder| builder.build(world)) .collect() } } @@ -422,7 +417,7 @@ unsafe impl> SystemParamBuilder> pub struct ParamSetBuilder(pub T); macro_rules! impl_param_set_builder_tuple { - ($(($param: ident, $builder: ident, $meta: ident)),*) => { + ($(($param: ident, $builder: ident)),*) => { #[expect( clippy::allow_attributes, reason = "This is in a macro; as such, the below lints may not always apply." @@ -437,79 +432,38 @@ macro_rules! impl_param_set_builder_tuple { )] // SAFETY: implementors of each `SystemParamBuilder` in the tuple have validated their impls unsafe impl<'w, 's, $($param: SystemParam,)* $($builder: SystemParamBuilder<$param>,)*> SystemParamBuilder> for ParamSetBuilder<($($builder,)*)> { - fn build(self, world: &mut World, system_meta: &mut SystemMeta) -> <($($param,)*) as SystemParam>::State { + fn build(self, world: &mut World) -> <($($param,)*) as SystemParam>::State { let ParamSetBuilder(($($builder,)*)) = self; - // Note that this is slightly different from `init_state`, which calls `init_state` on each param twice. - // One call populates an empty `SystemMeta` with the new access, while the other runs against a cloned `SystemMeta` to check for conflicts. - // Builders can only be invoked once, so we do both in a single call here. - // That means that any `filtered_accesses` in the `component_access_set` will get copied to every `$meta` - // and will appear multiple times in the final `SystemMeta`. - $( - let mut $meta = system_meta.clone(); - let $param = $builder.build(world, &mut $meta); - )* - // Make the ParamSet non-send if any of its parameters are non-send. - if false $(|| !$meta.is_send())* { - system_meta.set_non_send(); - } - $( - system_meta - .component_access_set - .extend($meta.component_access_set); - )* - #[allow( - clippy::unused_unit, - reason = "Zero-length tuples won't generate any calls to the system parameter builders." - )] - ($($param,)*) + ($($builder.build(world),)*) } } }; } -all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B, meta); +all_tuples!(impl_param_set_builder_tuple, 1, 8, P, B); -// SAFETY: Relevant parameter ComponentId access is applied to SystemMeta. If any ParamState conflicts -// with any prior access, a panic will occur. +// SAFETY: implementors of each `SystemParamBuilder` in the vec have validated their impls unsafe impl<'w, 's, P: SystemParam, B: SystemParamBuilder

> SystemParamBuilder>> for ParamSetBuilder> { - fn build( - self, - world: &mut World, - system_meta: &mut SystemMeta, - ) -> as SystemParam>::State { - let mut states = Vec::with_capacity(self.0.len()); - let mut metas = Vec::with_capacity(self.0.len()); - for builder in self.0 { - let mut meta = system_meta.clone(); - states.push(builder.build(world, &mut meta)); - metas.push(meta); - } - if metas.iter().any(|m| !m.is_send()) { - system_meta.set_non_send(); - } - for meta in metas { - system_meta - .component_access_set - .extend(meta.component_access_set); - } - states + fn build(self, world: &mut World) -> as SystemParam>::State { + self.0 + .into_iter() + .map(|builder| builder.build(world)) + .collect() } } /// A [`SystemParamBuilder`] for a [`DynSystemParam`]. /// See the [`DynSystemParam`] docs for examples. -pub struct DynParamBuilder<'a>( - Box DynSystemParamState + 'a>, -); +pub struct DynParamBuilder<'a>(Box DynSystemParamState + 'a>); impl<'a> DynParamBuilder<'a> { /// Creates a new [`DynParamBuilder`] by wrapping a [`SystemParamBuilder`] of any type. /// The built [`DynSystemParam`] can be downcast to `T`. pub fn new(builder: impl SystemParamBuilder + 'a) -> Self { - Self(Box::new(|world, meta| { - DynSystemParamState::new::(builder.build(world, meta)) + Self(Box::new(|world| { + DynSystemParamState::new::(builder.build(world)) })) } } @@ -518,12 +472,8 @@ impl<'a> DynParamBuilder<'a> { // and the boxed builder was a valid implementation of `SystemParamBuilder` for that type. // The resulting `DynSystemParam` can only perform access by downcasting to that param type. unsafe impl<'a, 'w, 's> SystemParamBuilder> for DynParamBuilder<'a> { - fn build( - self, - world: &mut World, - meta: &mut SystemMeta, - ) -> as SystemParam>::State { - (self.0)(world, meta) + fn build(self, world: &mut World) -> as SystemParam>::State { + (self.0)(world) } } @@ -549,15 +499,11 @@ unsafe impl<'a, 'w, 's> SystemParamBuilder> for DynParamB #[derive(Default, Debug, Clone)] pub struct LocalBuilder(pub T); -// SAFETY: `Local` performs no world access. +// SAFETY: Any value of `T` is a valid state for `Local`. unsafe impl<'s, T: FromWorld + Send + 'static> SystemParamBuilder> for LocalBuilder { - fn build( - self, - _world: &mut World, - _meta: &mut SystemMeta, - ) -> as SystemParam>::State { + fn build(self, _world: &mut World) -> as SystemParam>::State { SyncCell::new(self.0) } } @@ -585,39 +531,14 @@ impl<'a> FilteredResourcesParamBuilder SystemParamBuilder> for FilteredResourcesParamBuilder { - fn build( - self, - world: &mut World, - meta: &mut SystemMeta, - ) -> as SystemParam>::State { + fn build(self, world: &mut World) -> as SystemParam>::State { let mut builder = FilteredResourcesBuilder::new(world); (self.0)(&mut builder); - let access = builder.build(); - - let combined_access = meta.component_access_set.combined_access(); - let conflicts = combined_access.get_conflicts(&access); - if !conflicts.is_empty() { - let accesses = conflicts.format_conflict_list(world); - let system_name = &meta.name; - panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); - } - - if access.has_read_all_resources() { - meta.component_access_set - .add_unfiltered_read_all_resources(); - } else { - for component_id in access.resource_reads_and_writes() { - meta.component_access_set - .add_unfiltered_resource_read(component_id); - } - } - - access + builder.build() } } @@ -644,49 +565,14 @@ impl<'a> FilteredResourcesMutParamBuilder SystemParamBuilder> for FilteredResourcesMutParamBuilder { - fn build( - self, - world: &mut World, - meta: &mut SystemMeta, - ) -> as SystemParam>::State { + fn build(self, world: &mut World) -> as SystemParam>::State { let mut builder = FilteredResourcesMutBuilder::new(world); (self.0)(&mut builder); - let access = builder.build(); - - let combined_access = meta.component_access_set.combined_access(); - let conflicts = combined_access.get_conflicts(&access); - if !conflicts.is_empty() { - let accesses = conflicts.format_conflict_list(world); - let system_name = &meta.name; - panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); - } - - if access.has_read_all_resources() { - meta.component_access_set - .add_unfiltered_read_all_resources(); - } else { - for component_id in access.resource_reads() { - meta.component_access_set - .add_unfiltered_resource_read(component_id); - } - } - - if access.has_write_all_resources() { - meta.component_access_set - .add_unfiltered_write_all_resources(); - } else { - for component_id in access.resource_writes() { - meta.component_access_set - .add_unfiltered_resource_write(component_id); - } - } - - access + builder.build() } } @@ -698,8 +584,8 @@ pub struct OptionBuilder(T); unsafe impl> SystemParamBuilder> for OptionBuilder { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { - self.0.build(world, meta) + fn build(self, world: &mut World) -> as SystemParam>::State { + self.0.build(world) } } @@ -714,9 +600,8 @@ unsafe impl> fn build( self, world: &mut World, - meta: &mut SystemMeta, ) -> as SystemParam>::State { - self.0.build(world, meta) + self.0.build(world) } } @@ -728,8 +613,8 @@ pub struct WhenBuilder(T); unsafe impl> SystemParamBuilder> for WhenBuilder { - fn build(self, world: &mut World, meta: &mut SystemMeta) -> as SystemParam>::State { - self.0.build(world, meta) + fn build(self, world: &mut World) -> as SystemParam>::State { + self.0.build(world) } } diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 29a87e93ce..d48c599f2d 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -1,10 +1,11 @@ -use alloc::{borrow::Cow, format, vec::Vec}; +use alloc::{format, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, prelude::World, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn, SystemParamValidationError}, world::unsafe_world_cell::UnsafeWorldCell, @@ -55,7 +56,7 @@ use super::{IntoSystem, ReadOnlySystem, System}; /// IntoSystem::into_system(resource_equals(A(1))), /// IntoSystem::into_system(resource_equals(B(1))), /// // The name of the combined system. -/// std::borrow::Cow::Borrowed("a ^ b"), +/// "a ^ b".into(), /// ))); /// # fn my_system(mut flag: ResMut) { flag.0 = true; } /// # @@ -112,21 +113,19 @@ pub struct CombinatorSystem { _marker: PhantomData Func>, a: A, b: B, - name: Cow<'static, str>, - component_access_set: FilteredAccessSet, + name: DebugName, } impl CombinatorSystem { /// Creates a new system that combines two inner systems. /// /// The returned system will only be usable if `Func` implements [`Combine`]. - pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self { + pub fn new(a: A, b: B, name: DebugName) -> Self { Self { _marker: PhantomData, a, b, name, - component_access_set: FilteredAccessSet::default(), } } } @@ -140,28 +139,13 @@ where type In = Func::In; type Out = Func::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.name.clone() } - fn component_access(&self) -> &Access { - self.component_access_set.combined_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( @@ -210,18 +194,16 @@ where unsafe { self.a.validate_param_unsafe(world) } } - fn initialize(&mut self, world: &mut World) { - self.a.initialize(world); - self.b.initialize(world); - self.component_access_set - .extend(self.a.component_access_set().clone()); - self.component_access_set - .extend(self.b.component_access_set().clone()); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + let mut a_access = self.a.initialize(world); + let b_access = self.b.initialize(world); + a_access.extend(b_access); + a_access } - fn check_change_tick(&mut self, change_tick: Tick) { - self.a.check_change_tick(change_tick); - self.b.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.a.check_change_tick(check); + self.b.check_change_tick(check); } fn default_system_sets(&self) -> Vec { @@ -290,7 +272,7 @@ where let system_a = IntoSystem::into_system(this.a); let system_b = IntoSystem::into_system(this.b); let name = format!("Pipe({}, {})", system_a.name(), system_b.name()); - PipeSystem::new(system_a, system_b, Cow::Owned(name)) + PipeSystem::new(system_a, system_b, DebugName::owned(name)) } } @@ -336,8 +318,7 @@ where pub struct PipeSystem { a: A, b: B, - name: Cow<'static, str>, - component_access_set: FilteredAccessSet, + name: DebugName, } impl PipeSystem @@ -347,13 +328,8 @@ where for<'a> B::In: SystemInput = A::Out>, { /// Creates a new system that pipes two inner systems. - pub fn new(a: A, b: B, name: Cow<'static, str>) -> Self { - Self { - a, - b, - name, - component_access_set: FilteredAccessSet::default(), - } + pub fn new(a: A, b: B, name: DebugName) -> Self { + Self { a, b, name } } } @@ -366,28 +342,13 @@ where type In = A::In; type Out = B::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.name.clone() } - fn component_access(&self) -> &Access { - self.component_access_set.combined_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - - fn is_send(&self) -> bool { - self.a.is_send() && self.b.is_send() - } - - fn is_exclusive(&self) -> bool { - self.a.is_exclusive() || self.b.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.a.has_deferred() || self.b.has_deferred() + #[inline] + fn flags(&self) -> super::SystemStateFlags { + self.a.flags() | self.b.flags() } unsafe fn run_unsafe( @@ -439,18 +400,16 @@ where Ok(()) } - fn initialize(&mut self, world: &mut World) { - self.a.initialize(world); - self.b.initialize(world); - self.component_access_set - .extend(self.a.component_access_set().clone()); - self.component_access_set - .extend(self.b.component_access_set().clone()); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + let mut a_access = self.a.initialize(world); + let b_access = self.b.initialize(world); + a_access.extend(b_access); + a_access } - fn check_change_tick(&mut self, change_tick: Tick) { - self.a.check_change_tick(change_tick); - self.b.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.a.check_change_tick(check); + self.b.check_change_tick(check); } fn default_system_sets(&self) -> Vec { diff --git a/crates/bevy_ecs/src/system/commands/command.rs b/crates/bevy_ecs/src/system/commands/command.rs index af7b88edfc..f3fc677e47 100644 --- a/crates/bevy_ecs/src/system/commands/command.rs +++ b/crates/bevy_ecs/src/system/commands/command.rs @@ -9,7 +9,7 @@ use crate::{ change_detection::MaybeLocation, entity::Entity, error::Result, - event::{Event, Events}, + event::{BufferedEvent, EntityEvent, Event, Events}, observer::TriggerTargets, resource::Resource, schedule::ScheduleLabel, @@ -144,10 +144,11 @@ where /// A [`Command`] that runs the given system, /// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. -pub fn run_system_cached(system: S) -> impl Command +pub fn run_system_cached(system: S) -> impl Command where + O: 'static, M: 'static, - S: IntoSystem<(), (), M> + Send + 'static, + S: IntoSystem<(), O, M> + Send + 'static, { move |world: &mut World| -> Result { world.run_system_cached(system)?; @@ -157,11 +158,15 @@ where /// A [`Command`] that runs the given system with the given input value, /// caching its [`SystemId`] in a [`CachedSystemId`](crate::system::CachedSystemId) resource. -pub fn run_system_cached_with(system: S, input: I::Inner<'static>) -> impl Command +pub fn run_system_cached_with( + system: S, + input: I::Inner<'static>, +) -> impl Command where I: SystemInput: Send> + Send + 'static, + O: 'static, M: 'static, - S: IntoSystem + Send + 'static, + S: IntoSystem + Send + 'static, { move |world: &mut World| -> Result { world.run_system_cached_with(system, input)?; @@ -175,7 +180,7 @@ where pub fn unregister_system(system_id: SystemId) -> impl Command where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { move |world: &mut World| -> Result { world.unregister_system(system_id)?; @@ -208,7 +213,7 @@ pub fn run_schedule(label: impl ScheduleLabel) -> impl Command { } } -/// A [`Command`] that sends a global [`Trigger`](crate::observer::Trigger) without any targets. +/// A [`Command`] that sends a global [`Event`] without any targets. #[track_caller] pub fn trigger(event: impl Event) -> impl Command { let caller = MaybeLocation::caller(); @@ -217,9 +222,9 @@ pub fn trigger(event: impl Event) -> impl Command { } } -/// A [`Command`] that sends a [`Trigger`](crate::observer::Trigger) for the given targets. +/// A [`Command`] that sends an [`EntityEvent`] for the given targets. pub fn trigger_targets( - event: impl Event, + event: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) -> impl Command { let caller = MaybeLocation::caller(); @@ -228,9 +233,9 @@ pub fn trigger_targets( } } -/// A [`Command`] that sends an arbitrary [`Event`]. +/// A [`Command`] that sends an arbitrary [`BufferedEvent`]. #[track_caller] -pub fn send_event(event: E) -> impl Command { +pub fn send_event(event: E) -> impl Command { let caller = MaybeLocation::caller(); move |world: &mut World| { let mut events = world.resource_mut::>(); diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 317ad8476a..87bd2d858b 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -12,7 +12,7 @@ use crate::{ change_detection::MaybeLocation, component::{Component, ComponentId, ComponentInfo}, entity::{Entity, EntityClonerBuilder}, - event::Event, + event::EntityEvent, relationship::RelationshipHookMode, system::IntoObserverSystem, world::{error::EntityMutableFetchError, EntityWorldMut, FromWorld}, @@ -218,7 +218,7 @@ pub fn despawn() -> impl EntityCommand { /// An [`EntityCommand`] that creates an [`Observer`](crate::observer::Observer) /// listening for events of type `E` targeting an entity #[track_caller] -pub fn observe( +pub fn observe( observer: impl IntoObserverSystem, ) -> impl EntityCommand { let caller = MaybeLocation::caller(); @@ -227,11 +227,11 @@ pub fn observe( } } -/// An [`EntityCommand`] that sends a [`Trigger`](crate::observer::Trigger) targeting an entity. +/// An [`EntityCommand`] that sends an [`EntityEvent`] targeting an entity. /// -/// This will run any [`Observer`](crate::observer::Observer) of the given [`Event`] watching the entity. +/// This will run any [`Observer`](crate::observer::Observer) of the given [`EntityEvent`] watching the entity. #[track_caller] -pub fn trigger(event: impl Event) -> impl EntityCommand { +pub fn trigger(event: impl EntityEvent) -> impl EntityCommand { let caller = MaybeLocation::caller(); move |mut entity: EntityWorldMut| { let id = entity.id(); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 3012a65458..84f9784228 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -20,7 +20,7 @@ use crate::{ component::{Component, ComponentId, Mutable}, entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError}, error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, - event::Event, + event::{BufferedEvent, EntityEvent, Event}, observer::{Observer, TriggerTargets}, resource::Resource, schedule::ScheduleLabel, @@ -120,18 +120,28 @@ const _: () = { type Item<'w, 's> = Commands<'w, 's>; - fn init_state( - world: &mut World, - system_meta: &mut bevy_ecs::system::SystemMeta, - ) -> Self::State { + fn init_state(world: &mut World) -> Self::State { FetchState { state: <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::init_state( world, - system_meta, ), } } + fn init_access( + state: &Self::State, + system_meta: &mut bevy_ecs::system::SystemMeta, + component_access_set: &mut bevy_ecs::query::FilteredAccessSet, + world: &mut World, + ) { + <__StructFieldsAlias<'_, '_> as bevy_ecs::system::SystemParam>::init_access( + &state.state, + system_meta, + component_access_set, + world, + ); + } + fn apply( state: &mut Self::State, system_meta: &bevy_ecs::system::SystemMeta, @@ -862,7 +872,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// It will internally return a [`RegisteredSystemError`](crate::system::system_registry::RegisteredSystemError), /// which will be handled by [logging the error at the `warn` level](warn). - pub fn run_system(&mut self, id: SystemId) { + pub fn run_system(&mut self, id: SystemId<(), O>) { self.queue(command::run_system(id).handle_error_with(warn)); } @@ -955,7 +965,7 @@ impl<'w, 's> Commands<'w, 's> { ) -> SystemId where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { let entity = self.spawn_empty().id(); let system = RegisteredSystem::::new(Box::new(IntoSystem::into_system(system))); @@ -980,7 +990,7 @@ impl<'w, 's> Commands<'w, 's> { pub fn unregister_system(&mut self, system_id: SystemId) where I: SystemInput + Send + 'static, - O: Send + 'static, + O: 'static, { self.queue(command::unregister_system(system_id).handle_error_with(warn)); } @@ -1029,10 +1039,11 @@ impl<'w, 's> Commands<'w, 's> { /// consider passing them in as inputs via [`Commands::run_system_cached_with`]. /// /// If that's not an option, consider [`Commands::register_system`] instead. - pub fn run_system_cached(&mut self, system: S) + pub fn run_system_cached(&mut self, system: S) where + O: 'static, M: 'static, - S: IntoSystem<(), (), M> + Send + 'static, + S: IntoSystem<(), O, M> + Send + 'static, { self.queue(command::run_system_cached(system).handle_error_with(warn)); } @@ -1059,16 +1070,17 @@ impl<'w, 's> Commands<'w, 's> { /// consider passing them in as inputs. /// /// If that's not an option, consider [`Commands::register_system`] instead. - pub fn run_system_cached_with(&mut self, system: S, input: I::Inner<'static>) + pub fn run_system_cached_with(&mut self, system: S, input: I::Inner<'static>) where I: SystemInput: Send> + Send + 'static, + O: 'static, M: 'static, - S: IntoSystem + Send + 'static, + S: IntoSystem + Send + 'static, { self.queue(command::run_system_cached_with(system, input).handle_error_with(warn)); } - /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. + /// Sends a global [`Event`] without any targets. /// /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. #[track_caller] @@ -1076,13 +1088,13 @@ impl<'w, 's> Commands<'w, 's> { self.queue(command::trigger(event)); } - /// Sends a [`Trigger`](crate::observer::Trigger) for the given targets. + /// Sends an [`EntityEvent`] for the given targets. /// - /// This will run any [`Observer`] of the given [`Event`] watching those targets. + /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. #[track_caller] pub fn trigger_targets( &mut self, - event: impl Event, + event: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) { self.queue(command::trigger_targets(event, targets)); @@ -1091,7 +1103,7 @@ impl<'w, 's> Commands<'w, 's> { /// Spawns an [`Observer`] and returns the [`EntityCommands`] associated /// with the entity that stores the observer. /// - /// `observer` can be any system whose first parameter is a [`Trigger`]. + /// `observer` can be any system whose first parameter is [`On`]. /// /// **Calling [`observe`](EntityCommands::observe) on the returned /// [`EntityCommands`] will observe the observer itself, which you very @@ -1101,7 +1113,7 @@ impl<'w, 's> Commands<'w, 's> { /// /// Panics if the given system is an exclusive system. /// - /// [`Trigger`]: crate::observer::Trigger + /// [`On`]: crate::observer::On pub fn add_observer( &mut self, observer: impl IntoObserverSystem, @@ -1109,7 +1121,7 @@ impl<'w, 's> Commands<'w, 's> { self.spawn(Observer::new(observer)) } - /// Sends an arbitrary [`Event`]. + /// Sends an arbitrary [`BufferedEvent`]. /// /// This is a convenience method for sending events /// without requiring an [`EventWriter`](crate::event::EventWriter). @@ -1122,7 +1134,7 @@ impl<'w, 's> Commands<'w, 's> { /// If these events are performance-critical or very frequently sent, /// consider using a typed [`EventWriter`](crate::event::EventWriter) instead. #[track_caller] - pub fn send_event(&mut self, event: E) -> &mut Self { + pub fn send_event(&mut self, event: E) -> &mut Self { self.queue(command::send_event(event)); self } @@ -1947,16 +1959,16 @@ impl<'a> EntityCommands<'a> { &mut self.commands } - /// Sends a [`Trigger`](crate::observer::Trigger) targeting the entity. + /// Sends an [`EntityEvent`] targeting the entity. /// - /// This will run any [`Observer`] of the given [`Event`] watching this entity. + /// This will run any [`Observer`] of the given [`EntityEvent`] watching this entity. #[track_caller] - pub fn trigger(&mut self, event: impl Event) -> &mut Self { + pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { self.queue(entity_command::trigger(event)) } /// Creates an [`Observer`] listening for events of type `E` targeting this entity. - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 0920fd1e1f..3a053f89d5 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -1,6 +1,6 @@ use crate::{ - component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + component::{CheckChangeTicks, ComponentId, Tick}, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ExclusiveSystemParam, ExclusiveSystemParamItem, IntoSystem, @@ -10,10 +10,11 @@ use crate::{ }; use alloc::{borrow::Cow, vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use variadics_please::all_tuples; -use super::SystemParamValidationError; +use super::{SystemParamValidationError, SystemStateFlags}; /// A function system that runs with exclusive [`World`] access. /// @@ -42,7 +43,7 @@ where /// /// Useful to give closure systems more readable and unique names for debugging and tracing. pub fn with_name(mut self, new_name: impl Into>) -> Self { - self.system_meta.set_name(new_name.into()); + self.system_meta.set_name(new_name); self } } @@ -83,37 +84,17 @@ where type Out = F::Out; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system_meta.name.clone() } #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - &self.system_meta.component_access_set - } - - #[inline] - fn is_send(&self) -> bool { - // exclusive systems should have access to non-send resources + fn flags(&self) -> SystemStateFlags { + // non-send , exclusive , no deferred // the executor runs exclusive systems on the main thread, so this // field reflects that constraint - false - } - - #[inline] - fn is_exclusive(&self) -> bool { - true - } - - #[inline] - fn has_deferred(&self) -> bool { // exclusive systems have no deferred system params - false + SystemStateFlags::NON_SEND | SystemStateFlags::EXCLUSIVE } #[inline] @@ -190,17 +171,18 @@ where } #[inline] - fn initialize(&mut self, world: &mut World) { + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX); self.param_state = Some(F::Param::init(world, &mut self.system_meta)); + FilteredAccessSet::new() } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { + fn check_change_tick(&mut self, check: CheckChangeTicks) { check_system_change_tick( &mut self.system_meta.last_run, - change_tick, - self.system_meta.name.as_ref(), + check, + self.system_meta.name.clone(), ); } diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index af26e81d2f..6009662809 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -1,7 +1,7 @@ use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, prelude::FromWorld, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, schedule::{InternedSystemSet, SystemSet}, system::{ check_system_change_tick, ReadOnlySystemParam, System, SystemIn, SystemInput, SystemParam, @@ -11,26 +11,24 @@ use crate::{ }; use alloc::{borrow::Cow, vec, vec::Vec}; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use variadics_please::all_tuples; #[cfg(feature = "trace")] use tracing::{info_span, Span}; -use super::{IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError}; +use super::{ + IntoSystem, ReadOnlySystem, SystemParamBuilder, SystemParamValidationError, SystemStateFlags, +}; /// The metadata of a [`System`]. #[derive(Clone)] pub struct SystemMeta { - pub(crate) name: Cow<'static, str>, - /// The set of component accesses for this system. This is used to determine - /// - soundness issues (e.g. multiple [`SystemParam`]s mutably accessing the same component) - /// - ambiguities in the schedule (e.g. two systems that have some sort of conflicting access) - pub(crate) component_access_set: FilteredAccessSet, + pub(crate) name: DebugName, // NOTE: this must be kept private. making a SystemMeta non-send is irreversible to prevent // SystemParams from overriding each other - is_send: bool, - has_deferred: bool, + flags: SystemStateFlags, pub(crate) last_run: Tick, #[cfg(feature = "trace")] pub(crate) system_span: Span, @@ -40,23 +38,21 @@ pub struct SystemMeta { impl SystemMeta { pub(crate) fn new() -> Self { - let name = core::any::type_name::(); + let name = DebugName::type_name::(); Self { - name: name.into(), - component_access_set: FilteredAccessSet::default(), - is_send: true, - has_deferred: false, + #[cfg(feature = "trace")] + system_span: info_span!("system", name = name.clone().as_string()), + #[cfg(feature = "trace")] + commands_span: info_span!("system_commands", name = name.clone().as_string()), + name, + flags: SystemStateFlags::empty(), last_run: Tick::new(0), - #[cfg(feature = "trace")] - system_span: info_span!("system", name = name), - #[cfg(feature = "trace")] - commands_span: info_span!("system_commands", name = name), } } /// Returns the system's name #[inline] - pub fn name(&self) -> &str { + pub fn name(&self) -> &DebugName { &self.name } @@ -72,13 +68,13 @@ impl SystemMeta { self.system_span = info_span!("system", name = name); self.commands_span = info_span!("system_commands", name = name); } - self.name = new_name; + self.name = new_name.into(); } /// Returns true if the system is [`Send`]. #[inline] pub fn is_send(&self) -> bool { - self.is_send + !self.flags.intersects(SystemStateFlags::NON_SEND) } /// Sets the system to be not [`Send`]. @@ -86,38 +82,20 @@ impl SystemMeta { /// This is irreversible. #[inline] pub fn set_non_send(&mut self) { - self.is_send = false; + self.flags |= SystemStateFlags::NON_SEND; } /// Returns true if the system has deferred [`SystemParam`]'s #[inline] pub fn has_deferred(&self) -> bool { - self.has_deferred + self.flags.intersects(SystemStateFlags::DEFERRED) } /// Marks the system as having deferred buffers like [`Commands`](`super::Commands`) /// This lets the scheduler insert [`ApplyDeferred`](`crate::prelude::ApplyDeferred`) systems automatically. #[inline] pub fn set_has_deferred(&mut self) { - self.has_deferred = true; - } - - /// Returns a reference to the [`FilteredAccessSet`] for [`ComponentId`]. - /// Used to check if systems and/or system params have conflicting access. - #[inline] - pub fn component_access_set(&self) -> &FilteredAccessSet { - &self.component_access_set - } - - /// Returns a mutable reference to the [`FilteredAccessSet`] for [`ComponentId`]. - /// Used internally to statically check if systems have conflicting access. - /// - /// # Safety - /// - /// No access can be removed from the returned [`FilteredAccessSet`]. - #[inline] - pub unsafe fn component_access_set_mut(&mut self) -> &mut FilteredAccessSet { - &mut self.component_access_set + self.flags |= SystemStateFlags::DEFERRED; } } @@ -154,7 +132,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # #[derive(Resource)] /// # struct MyResource(u32); @@ -187,7 +165,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// #[derive(Resource)] /// struct CachedSystemState { @@ -277,7 +255,11 @@ impl SystemState { pub fn new(world: &mut World) -> Self { let mut meta = SystemMeta::new::(); meta.last_run = world.change_tick().relative_to(Tick::MAX); - let param_state = Param::init_state(world, &mut meta); + let param_state = Param::init_state(world); + let mut component_access_set = FilteredAccessSet::new(); + // We need to call `init_access` to ensure there are no panics from conflicts within `Param`, + // even though we don't use the calculated access. + Param::init_access(¶m_state, &mut meta, &mut component_access_set, world); Self { meta, param_state, @@ -289,7 +271,11 @@ impl SystemState { pub(crate) fn from_builder(world: &mut World, builder: impl SystemParamBuilder) -> Self { let mut meta = SystemMeta::new::(); meta.last_run = world.change_tick().relative_to(Tick::MAX); - let param_state = builder.build(world, &mut meta); + let param_state = builder.build(world); + let mut component_access_set = FilteredAccessSet::new(); + // We need to call `init_access` to ensure there are no panics from conflicts within `Param`, + // even though we don't use the calculated access. + Param::init_access(¶m_state, &mut meta, &mut component_access_set, world); Self { meta, param_state, @@ -494,8 +480,7 @@ impl SystemState { /// Modifying the system param states may have unintended consequences. /// The param state is generally considered to be owned by the [`SystemParam`]. Modifications /// should respect any invariants as required by the [`SystemParam`]. - /// For example, modifying the system state of [`ResMut`](crate::system::ResMut) without also - /// updating [`SystemMeta::component_access_set`] will obviously create issues. + /// For example, modifying the system state of [`ResMut`](crate::system::ResMut) will obviously create issues. pub unsafe fn param_state_mut(&mut self) -> &mut Param::State { &mut self.param_state } @@ -616,33 +601,13 @@ where type Out = F::Out; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system_meta.name.clone() } #[inline] - fn component_access(&self) -> &Access { - self.system_meta.component_access_set.combined_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - &self.system_meta.component_access_set - } - - #[inline] - fn is_send(&self) -> bool { - self.system_meta.is_send - } - - #[inline] - fn is_exclusive(&self) -> bool { - false - } - - #[inline] - fn has_deferred(&self) -> bool { - self.system_meta.has_deferred + fn flags(&self) -> SystemStateFlags { + self.system_meta.flags } #[inline] @@ -720,28 +685,35 @@ where } #[inline] - fn initialize(&mut self, world: &mut World) { + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { if let Some(state) = &self.state { assert_eq!( state.world_id, world.id(), "System built with a different world than the one it was added to.", ); - } else { - self.state = Some(FunctionSystemState { - param: F::Param::init_state(world, &mut self.system_meta), - world_id: world.id(), - }); } + let state = self.state.get_or_insert_with(|| FunctionSystemState { + param: F::Param::init_state(world), + world_id: world.id(), + }); self.system_meta.last_run = world.change_tick().relative_to(Tick::MAX); + let mut component_access_set = FilteredAccessSet::new(); + F::Param::init_access( + &state.param, + &mut self.system_meta, + &mut component_access_set, + world, + ); + component_access_set } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { + fn check_change_tick(&mut self, check: CheckChangeTicks) { check_system_change_tick( &mut self.system_meta.last_run, - change_tick, - self.system_meta.name.as_ref(), + check, + self.system_meta.name.clone(), ); } diff --git a/crates/bevy_ecs/src/system/input.rs b/crates/bevy_ecs/src/system/input.rs index 12087fdf6a..cb75016ee9 100644 --- a/crates/bevy_ecs/src/system/input.rs +++ b/crates/bevy_ecs/src/system/input.rs @@ -2,7 +2,7 @@ use core::ops::{Deref, DerefMut}; use variadics_please::all_tuples; -use crate::{bundle::Bundle, prelude::Trigger, system::System}; +use crate::{bundle::Bundle, prelude::On, system::System}; /// Trait for types that can be used as input to [`System`]s. /// @@ -11,7 +11,7 @@ use crate::{bundle::Bundle, prelude::Trigger, system::System}; /// - [`In`]: For values /// - [`InRef`]: For read-only references to values /// - [`InMut`]: For mutable references to values -/// - [`Trigger`]: For [`ObserverSystem`]s +/// - [`On`]: For [`ObserverSystem`]s /// - [`StaticSystemInput`]: For arbitrary [`SystemInput`]s in generic contexts /// - Tuples of [`SystemInput`]s up to 8 elements /// @@ -222,9 +222,9 @@ impl<'i, T: ?Sized> DerefMut for InMut<'i, T> { /// Used for [`ObserverSystem`]s. /// /// [`ObserverSystem`]: crate::system::ObserverSystem -impl SystemInput for Trigger<'_, E, B> { - type Param<'i> = Trigger<'i, E, B>; - type Inner<'i> = Trigger<'i, E, B>; +impl SystemInput for On<'_, E, B> { + type Param<'i> = On<'i, E, B>; + type Inner<'i> = On<'i, E, B>; fn wrap(this: Self::Inner<'_>) -> Self::Param<'_> { this diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index 54e5a781ab..db4cd452c0 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -97,7 +97,7 @@ //! - [`EventWriter`](crate::event::EventWriter) //! - [`NonSend`] and `Option` //! - [`NonSendMut`] and `Option` -//! - [`RemovedComponents`](crate::removal_detection::RemovedComponents) +//! - [`RemovedComponents`](crate::lifecycle::RemovedComponents) //! - [`SystemName`] //! - [`SystemChangeTick`] //! - [`Archetypes`](crate::archetype::Archetypes) (Provides Archetype metadata) @@ -408,10 +408,10 @@ mod tests { component::{Component, Components}, entity::{Entities, Entity}, error::Result, + lifecycle::RemovedComponents, name::Name, - prelude::{AnyOf, EntityRef, Trigger}, + prelude::{Add, AnyOf, EntityRef, On}, query::{Added, Changed, Or, SpawnDetails, Spawned, With, Without}, - removal_detection::RemovedComponents, resource::Resource, schedule::{ common_conditions::resource_exists, ApplyDeferred, IntoScheduleConfigs, Schedule, @@ -421,7 +421,7 @@ mod tests { Commands, In, InMut, IntoSystem, Local, NonSend, NonSendMut, ParamSet, Query, Res, ResMut, Single, StaticSystemParam, System, SystemState, }, - world::{DeferredWorld, EntityMut, FromWorld, OnAdd, World}, + world::{DeferredWorld, EntityMut, FromWorld, World}, }; use super::ScheduleSystem; @@ -1163,10 +1163,10 @@ mod tests { let mut world = World::default(); let mut x = IntoSystem::into_system(sys_x); let mut y = IntoSystem::into_system(sys_y); - x.initialize(&mut world); - y.initialize(&mut world); + let x_access = x.initialize(&mut world); + let y_access = y.initialize(&mut world); - let conflicts = x.component_access().get_conflicts(y.component_access()); + let conflicts = x_access.get_conflicts(&y_access); let b_id = world .components() .get_resource_id(TypeId::of::()) @@ -1900,15 +1900,15 @@ mod tests { #[expect(clippy::unused_unit, reason = "this forces the () return type")] schedule.add_systems(|_query: Query<&Name>| -> () { todo!() }); - fn obs(_trigger: Trigger) { + fn obs(_trigger: On) { todo!() } world.add_observer(obs); - world.add_observer(|_trigger: Trigger| {}); - world.add_observer(|_trigger: Trigger| todo!()); + world.add_observer(|_trigger: On| {}); + world.add_observer(|_trigger: On| todo!()); #[expect(clippy::unused_unit, reason = "this forces the () return type")] - world.add_observer(|_trigger: Trigger| -> () { todo!() }); + world.add_observer(|_trigger: On| -> () { todo!() }); fn my_command(_world: &mut World) { todo!() diff --git a/crates/bevy_ecs/src/system/observer_system.rs b/crates/bevy_ecs/src/system/observer_system.rs index c0ac5e8de0..8e927d9529 100644 --- a/crates/bevy_ecs/src/system/observer_system.rs +++ b/crates/bevy_ecs/src/system/observer_system.rs @@ -1,12 +1,13 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use core::marker::PhantomData; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::Result, never::Never, - prelude::{Bundle, Trigger}, - query::{Access, FilteredAccessSet}, + prelude::{Bundle, On}, + query::FilteredAccessSet, schedule::{Fallible, Infallible}, system::{input::SystemIn, System}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, @@ -14,14 +15,14 @@ use crate::{ use super::{IntoSystem, SystemParamValidationError}; -/// Implemented for [`System`]s that have a [`Trigger`] as the first argument. +/// Implemented for [`System`]s that have [`On`] as the first argument. pub trait ObserverSystem: - System, Out = Out> + Send + 'static + System, Out = Out> + Send + 'static { } impl ObserverSystem for T where - T: System, Out = Out> + Send + 'static + T: System, Out = Out> + Send + 'static { } @@ -35,7 +36,7 @@ impl ObserverSystem for T where #[diagnostic::on_unimplemented( message = "`{Self}` cannot become an `ObserverSystem`", label = "the trait `IntoObserverSystem` is not implemented", - note = "for function `ObserverSystem`s, ensure the first argument is a `Trigger` and any subsequent ones are `SystemParam`" + note = "for function `ObserverSystem`s, ensure the first argument is `On` and any subsequent ones are `SystemParam`" )] pub trait IntoObserverSystem: Send + 'static { /// The type of [`System`] that this instance converts into. @@ -47,7 +48,7 @@ pub trait IntoObserverSystem: Send + 'st impl IntoObserverSystem for S where - S: IntoSystem, Out, M> + Send + 'static, + S: IntoSystem, Out, M> + Send + 'static, S::System: ObserverSystem, E: 'static, B: Bundle, @@ -61,7 +62,7 @@ where impl IntoObserverSystem for S where - S: IntoSystem, (), M> + Send + 'static, + S: IntoSystem, (), M> + Send + 'static, S::System: ObserverSystem, E: Send + Sync + 'static, B: Bundle, @@ -74,7 +75,7 @@ where } impl IntoObserverSystem for S where - S: IntoSystem, Never, M> + Send + 'static, + S: IntoSystem, Never, M> + Send + 'static, E: Send + Sync + 'static, B: Bundle, { @@ -108,37 +109,17 @@ where B: Bundle, Out: Send + Sync + 'static, { - type In = Trigger<'static, E, B>; + type In = On<'static, E, B>; type Out = Result; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.observer.name() } #[inline] - fn component_access(&self) -> &Access { - self.observer.component_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - self.observer.component_access_set() - } - - #[inline] - fn is_send(&self) -> bool { - self.observer.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.observer.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.observer.has_deferred() + fn flags(&self) -> super::SystemStateFlags { + self.observer.flags() } #[inline] @@ -176,13 +157,13 @@ where } #[inline] - fn initialize(&mut self, world: &mut World) { - self.observer.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + self.observer.initialize(world) } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.observer.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.observer.check_change_tick(check); } #[inline] @@ -204,7 +185,7 @@ where mod tests { use crate::{ event::Event, - observer::Trigger, + observer::On, system::{In, IntoSystem}, world::World, }; @@ -214,7 +195,7 @@ mod tests { #[test] fn test_piped_observer_systems_no_input() { - fn a(_: Trigger) {} + fn a(_: On) {} fn b() {} let mut world = World::new(); @@ -223,7 +204,7 @@ mod tests { #[test] fn test_piped_observer_systems_with_inputs() { - fn a(_: Trigger) -> u32 { + fn a(_: On) -> u32 { 3 } fn b(_: In) {} diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index f7e6de9803..6e44301b18 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1,3 +1,5 @@ +use bevy_utils::prelude::DebugName; + use crate::{ batching::BatchingStrategy, component::Tick, @@ -1185,7 +1187,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`par_iter_mut`]: Self::par_iter_mut /// [`World`]: crate::world::World #[inline] - pub fn par_iter(&self) -> QueryParIter<'_, '_, D::ReadOnly, F> { + pub fn par_iter(&self) -> QueryParIter<'_, 's, D::ReadOnly, F> { self.as_readonly().par_iter_inner() } @@ -1220,7 +1222,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// [`par_iter`]: Self::par_iter /// [`World`]: crate::world::World #[inline] - pub fn par_iter_mut(&mut self) -> QueryParIter<'_, '_, D, F> { + pub fn par_iter_mut(&mut self) -> QueryParIter<'_, 's, D, F> { self.reborrow().par_iter_inner() } @@ -1280,7 +1282,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many>( &self, entities: EntityList, - ) -> QueryParManyIter<'_, '_, D::ReadOnly, F, EntityList::Item> { + ) -> QueryParManyIter<'_, 's, D::ReadOnly, F, EntityList::Item> { QueryParManyIter { world: self.world, state: self.state.as_readonly(), @@ -1309,7 +1311,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many_unique>( &self, entities: EntityList, - ) -> QueryParManyUniqueIter<'_, '_, D::ReadOnly, F, EntityList::Item> { + ) -> QueryParManyUniqueIter<'_, 's, D::ReadOnly, F, EntityList::Item> { QueryParManyUniqueIter { world: self.world, state: self.state.as_readonly(), @@ -1338,7 +1340,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn par_iter_many_unique_mut>( &mut self, entities: EntityList, - ) -> QueryParManyUniqueIter<'_, '_, D, F, EntityList::Item> { + ) -> QueryParManyUniqueIter<'_, 's, D, F, EntityList::Item> { QueryParManyUniqueIter { world: self.world, state: self.state, @@ -1383,7 +1385,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) to get a mutable query item. #[inline] - pub fn get(&self, entity: Entity) -> Result, QueryEntityError> { + pub fn get(&self, entity: Entity) -> Result, QueryEntityError> { self.as_readonly().get_inner(entity) } @@ -1434,7 +1436,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many( &self, entities: [Entity; N], - ) -> Result<[ROQueryItem<'_, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'_, 's, D>; N], QueryEntityError> { // Note that we call a separate `*_inner` method from `get_many_mut` // because we don't need to check for duplicates. self.as_readonly().get_many_inner(entities) @@ -1485,7 +1487,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique( &self, entities: UniqueEntityArray, - ) -> Result<[ROQueryItem<'_, D>; N], QueryEntityError> { + ) -> Result<[ROQueryItem<'_, 's, D>; N], QueryEntityError> { self.as_readonly().get_many_unique_inner(entities) } @@ -1519,7 +1521,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get`](Self::get) to get a read-only query item. #[inline] - pub fn get_mut(&mut self, entity: Entity) -> Result, QueryEntityError> { + pub fn get_mut(&mut self, entity: Entity) -> Result, QueryEntityError> { self.reborrow().get_inner(entity) } @@ -1534,7 +1536,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) to get the item using a mutable borrow of the [`Query`]. #[inline] - pub fn get_inner(self, entity: Entity) -> Result, QueryEntityError> { + pub fn get_inner(self, entity: Entity) -> Result, QueryEntityError> { // SAFETY: system runs without conflicts with other systems. // same-system queries have runtime borrow checks when they conflict unsafe { @@ -1580,8 +1582,18 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { D::set_archetype(&mut fetch, &self.state.fetch_state, archetype, table); F::set_archetype(&mut filter, &self.state.filter_state, archetype, table); - if F::filter_fetch(&mut filter, entity, location.table_row) { - Ok(D::fetch(&mut fetch, entity, location.table_row)) + if F::filter_fetch( + &self.state.filter_state, + &mut filter, + entity, + location.table_row, + ) { + Ok(D::fetch( + &self.state.fetch_state, + &mut fetch, + entity, + location.table_row, + )) } else { Err(QueryEntityError::QueryDoesNotMatch( entity, @@ -1662,7 +1674,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_mut( &mut self, entities: [Entity; N], - ) -> Result<[D::Item<'_>; N], QueryEntityError> { + ) -> Result<[D::Item<'_, 's>; N], QueryEntityError> { self.reborrow().get_many_mut_inner(entities) } @@ -1730,7 +1742,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique_mut( &mut self, entities: UniqueEntityArray, - ) -> Result<[D::Item<'_>; N], QueryEntityError> { + ) -> Result<[D::Item<'_, 's>; N], QueryEntityError> { self.reborrow().get_many_unique_inner(entities) } @@ -1749,7 +1761,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_mut_inner( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { // Verify that all entities are unique for i in 0..N { for j in 0..i { @@ -1777,7 +1789,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_inner( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> where D: ReadOnlyQueryData, { @@ -1799,7 +1811,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { pub fn get_many_unique_inner( self, entities: UniqueEntityArray, - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { // SAFETY: All entities are unique, so the results don't alias. unsafe { self.get_many_impl(entities.into_inner()) } } @@ -1814,7 +1826,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { unsafe fn get_many_impl( self, entities: [Entity; N], - ) -> Result<[D::Item<'w>; N], QueryEntityError> { + ) -> Result<[D::Item<'w, 's>; N], QueryEntityError> { let mut values = [(); N].map(|_| MaybeUninit::uninit()); for (value, entity) in core::iter::zip(&mut values, entities) { @@ -1842,7 +1854,10 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`get_mut`](Self::get_mut) for the safe version. #[inline] - pub unsafe fn get_unchecked(&self, entity: Entity) -> Result, QueryEntityError> { + pub unsafe fn get_unchecked( + &self, + entity: Entity, + ) -> Result, QueryEntityError> { // SAFETY: The caller promises that this will not result in multiple mutable references. unsafe { self.reborrow_unsafe() }.get_inner(entity) } @@ -1878,7 +1893,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`single_mut`](Self::single_mut) to get the mutable query item. #[inline] - pub fn single(&self) -> Result, QuerySingleError> { + pub fn single(&self) -> Result, QuerySingleError> { self.as_readonly().single_inner() } @@ -1907,7 +1922,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// /// - [`single`](Self::single) to get the read-only query item. #[inline] - pub fn single_mut(&mut self) -> Result, QuerySingleError> { + pub fn single_mut(&mut self) -> Result, QuerySingleError> { self.reborrow().single_inner() } @@ -1939,15 +1954,15 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// - [`single_mut`](Self::single_mut) to get the mutable query item. /// - [`single_inner`](Self::single_inner) for the panicking version. #[inline] - pub fn single_inner(self) -> Result, QuerySingleError> { + pub fn single_inner(self) -> Result, QuerySingleError> { let mut query = self.into_iter(); let first = query.next(); let extra = query.next().is_some(); match (first, extra) { (Some(r), false) => Ok(r), - (None, _) => Err(QuerySingleError::NoEntities(core::any::type_name::())), - (Some(_), _) => Err(QuerySingleError::MultipleEntities(core::any::type_name::< + (None, _) => Err(QuerySingleError::NoEntities(DebugName::type_name::())), + (Some(_), _) => Err(QuerySingleError::MultipleEntities(DebugName::type_name::< Self, >())), } @@ -2016,17 +2031,67 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { self.as_nop().get(entity).is_ok() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new [`Query`] giving more + /// restrictive access to the entities matched by the current query. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// A transmute is valid only if `NewD` has a subset of the read, write, and required access + /// of the current query. A precise description of the access required by each parameter + /// type is given in the table below, but typical uses are to: + /// * Remove components, e.g. `Query<(&A, &B)>` to `Query<&A>`. + /// * Retrieve an existing component with reduced or equal access, e.g. `Query<&mut A>` to `Query<&A>` + /// or `Query<&T>` to `Query>`. + /// * Add parameters with no new access, for example adding an `Entity` parameter. + /// + /// Note that since filter terms are dropped, non-archetypal filters like + /// [`Added`], [`Changed`] and [`Spawned`] will not be respected. To maintain or change filter + /// terms see [`Self::transmute_lens_filtered`]. + /// + /// |`QueryData` parameter type|Access required| + /// |----|----| + /// |[`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], [`PhantomData`]|No access| + /// |[`EntityMut`]|Read and write access to all components, but no required access| + /// |[`EntityRef`]|Read access to all components, but no required access| + /// |`&T`, [`Ref`]|Read and required access to `T`| + /// |`&mut T`, [`Mut`]|Read, write and required access to `T`| + /// |[`Option`], [`AnyOf<(D, ...)>`]|Read and write access to `T`, but no required access| + /// |Tuples of query data and
`#[derive(QueryData)]` structs|The union of the access of their subqueries| + /// |[`FilteredEntityRef`], [`FilteredEntityMut`]|Determined by the [`QueryBuilder`] used to construct them. Any query can be transmuted to them, and they will receive the access of the source query. When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data| + /// + /// `transmute_lens` drops filter terms, but [`Self::transmute_lens_filtered`] supports returning a [`QueryLens`] with a new + /// filter type - the access required by filter parameters are as follows. + /// + /// |`QueryFilter` parameter type|Access required| + /// |----|----| + /// |[`Added`], [`Changed`]|Read and required access to `T`| + /// |[`With`], [`Without`]|No access| + /// |[`Or<(T, ...)>`]|Read access of the subqueries, but no required access| + /// |Tuples of query filters and `#[derive(QueryFilter)]` structs|The union of the access of their subqueries| + /// + /// [`Added`]: crate::query::Added + /// [`Added`]: crate::query::Added + /// [`AnyOf<(D, ...)>`]: crate::query::AnyOf + /// [`&Archetype`]: crate::archetype::Archetype + /// [`Changed`]: crate::query::Changed + /// [`Changed`]: crate::query::Changed + /// [`EntityMut`]: crate::world::EntityMut + /// [`EntityLocation`]: crate::entity::EntityLocation + /// [`EntityRef`]: crate::world::EntityRef + /// [`FilteredEntityRef`]: crate::world::FilteredEntityRef + /// [`FilteredEntityMut`]: crate::world::FilteredEntityMut + /// [`Has`]: crate::query::Has + /// [`Mut`]: crate::world::Mut + /// [`Or<(T, ...)>`]: crate::query::Or + /// [`QueryBuilder`]: crate::query::QueryBuilder + /// [`Ref`]: crate::world::Ref + /// [`SpawnDetails`]: crate::query::SpawnDetails + /// [`Spawned`]: crate::query::Spawned + /// [`With`]: crate::query::With + /// [`Without`]: crate::query::Without /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `D` + /// This will panic if the access required by `NewD` is not a subset of that required by + /// the original fetch `D`. /// /// ## Example /// @@ -2065,30 +2130,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, - /// you can also make limited changes to the types of parameters. - /// The new query must have a subset of the *read*, *write*, and *required* access of the original query. - /// - /// * `&mut T` and [`Mut`](crate::change_detection::Mut) have read, write, and required access to `T` - /// * `&T` and [`Ref`](crate::change_detection::Ref) have read and required access to `T` - /// * [`Option`] and [`AnyOf<(D, ...)>`](crate::query::AnyOf) have the read and write access of the subqueries, but no required access - /// * Tuples of query data and `#[derive(QueryData)]` structs have the union of the access of their subqueries - /// * [`EntityMut`](crate::world::EntityMut) has read and write access to all components, but no required access - /// * [`EntityRef`](crate::world::EntityRef) has read access to all components, but no required access - /// * [`Entity`], [`EntityLocation`], [`SpawnDetails`], [`&Archetype`], [`Has`], and [`PhantomData`] have no access at all, - /// so can be added to any query - /// * [`FilteredEntityRef`](crate::world::FilteredEntityRef) and [`FilteredEntityMut`](crate::world::FilteredEntityMut) - /// have access determined by the [`QueryBuilder`](crate::query::QueryBuilder) used to construct them. - /// Any query can be transmuted to them, and they will receive the access of the source query. - /// When combined with other `QueryData`, they will receive any access of the source query that does not conflict with the other data. - /// * [`Added`](crate::query::Added) and [`Changed`](crate::query::Changed) filters have read and required access to `T` - /// * [`With`](crate::query::With) and [`Without`](crate::query::Without) filters have no access at all, - /// so can be added to any query - /// * Tuples of query filters and `#[derive(QueryFilter)]` structs have the union of the access of their subqueries - /// * [`Or<(F, ...)>`](crate::query::Or) filters have the read access of the subqueries, but no required access - /// /// ### Examples of valid transmutes /// /// ```rust @@ -2165,28 +2206,21 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// // Nested inside of an `Or` filter, they have the same access as `Option<&T>`. /// assert_valid_transmute_filtered::, (), Entity, Or<(Changed, With)>>(); /// ``` - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`SpawnDetails`]: crate::query::SpawnDetails - /// [`&Archetype`]: crate::archetype::Archetype - /// [`Has`]: crate::query::Has #[track_caller] pub fn transmute_lens(&mut self) -> QueryLens<'_, NewD> { self.transmute_lens_filtered::() } - /// Returns a [`QueryLens`] that can be used to get a query with a more general fetch. + /// Returns a [`QueryLens`] that can be used to construct a new `Query` giving more restrictive + /// access to the entities matched by the current query. + /// /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// - /// For example, this can transform a `Query<(&A, &mut B)>` to a `Query<&B>`. - /// This can be useful for passing the query to another function. Note that since - /// filter terms are dropped, non-archetypal filters like [`Added`](crate::query::Added), - /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will not be - /// respected. To maintain or change filter terms see [`Self::transmute_lens_filtered`] + /// See [`Self::transmute_lens`] for a description of allowed transmutes. /// /// ## Panics /// - /// This will panic if `NewD` is not a subset of the original fetch `Q` + /// This will panic if `NewD` is not a subset of the original fetch `D` /// /// ## Example /// @@ -2225,22 +2259,6 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// # schedule.run(&mut world); /// ``` /// - /// ## Allowed Transmutes - /// - /// Besides removing parameters from the query, you can also - /// make limited changes to the types of parameters. - /// - /// * Can always add/remove [`Entity`] - /// * Can always add/remove [`EntityLocation`] - /// * Can always add/remove [`&Archetype`] - /// * `Ref` <-> `&T` - /// * `&mut T` -> `&T` - /// * `&mut T` -> `Ref` - /// * [`EntityMut`](crate::world::EntityMut) -> [`EntityRef`](crate::world::EntityRef) - /// - /// [`EntityLocation`]: crate::entity::EntityLocation - /// [`&Archetype`]: crate::archetype::Archetype - /// /// # See also /// /// - [`transmute_lens`](Self::transmute_lens) to convert to a lens using a mutable borrow of the [`Query`]. @@ -2251,6 +2269,8 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens`] but also includes a [`QueryFilter`] type. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), @@ -2266,10 +2286,13 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Equivalent to [`Self::transmute_lens_inner`] but also includes a [`QueryFilter`] type. /// This consumes the [`Query`] to return results with the actual "inner" world lifetime. /// + /// See [`Self::transmute_lens`] for a description of allowed transmutes. + /// /// Note that the lens will iterate the same tables and archetypes as the original query. This means that /// additional archetypal query terms like [`With`](crate::query::With) and [`Without`](crate::query::Without) /// will not necessarily be respected and non-archetypal terms like [`Added`](crate::query::Added), /// [`Changed`](crate::query::Changed) and [`Spawned`](crate::query::Spawned) will only be respected if they + /// are in the type signature. /// /// # See also /// @@ -2443,7 +2466,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Query<'w, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; type IntoIter = QueryIter<'w, 's, D, F>; fn into_iter(self) -> Self::IntoIter { @@ -2456,7 +2479,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for Query<'w, 's, D, F> } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, F> { - type Item = ROQueryItem<'w, D>; + type Item = ROQueryItem<'w, 's, D>; type IntoIter = QueryIter<'w, 's, D::ReadOnly, F>; fn into_iter(self) -> Self::IntoIter { @@ -2465,7 +2488,7 @@ impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w Query<'_, 's, D, } impl<'w, 's, D: QueryData, F: QueryFilter> IntoIterator for &'w mut Query<'_, 's, D, F> { - type Item = D::Item<'w>; + type Item = D::Item<'w, 's>; type IntoIter = QueryIter<'w, 's, D, F>; fn into_iter(self) -> Self::IntoIter { @@ -2565,28 +2588,43 @@ impl<'w, 'q, Q: QueryData, F: QueryFilter> From<&'q mut Query<'w, '_, Q, F>> /// See [`Query`] for more details. /// /// [System parameter]: crate::system::SystemParam -pub struct Single<'w, D: QueryData, F: QueryFilter = ()> { - pub(crate) item: D::Item<'w>, +/// +/// # Example +/// ``` +/// # use bevy_ecs::prelude::*; +/// #[derive(Component)] +/// struct Boss { +/// health: f32 +/// }; +/// +/// fn hurt_boss(mut boss: Single<&mut Boss>) { +/// boss.health -= 4.0; +/// } +/// ``` +/// Note that because [`Single`] implements [`Deref`] and [`DerefMut`], methods and fields like `health` can be accessed directly. +/// You can also access the underlying data manually, by calling `.deref`/`.deref_mut`, or by using the `*` operator. +pub struct Single<'w, 's, D: QueryData, F: QueryFilter = ()> { + pub(crate) item: D::Item<'w, 's>, pub(crate) _filter: PhantomData, } -impl<'w, D: QueryData, F: QueryFilter> Deref for Single<'w, D, F> { - type Target = D::Item<'w>; +impl<'w, 's, D: QueryData, F: QueryFilter> Deref for Single<'w, 's, D, F> { + type Target = D::Item<'w, 's>; fn deref(&self) -> &Self::Target { &self.item } } -impl<'w, D: QueryData, F: QueryFilter> DerefMut for Single<'w, D, F> { +impl<'w, 's, D: QueryData, F: QueryFilter> DerefMut for Single<'w, 's, D, F> { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.item } } -impl<'w, D: QueryData, F: QueryFilter> Single<'w, D, F> { +impl<'w, 's, D: QueryData, F: QueryFilter> Single<'w, 's, D, F> { /// Returns the inner item with ownership. - pub fn into_inner(self) -> D::Item<'w> { + pub fn into_inner(self) -> D::Item<'w, 's> { self.item } } diff --git a/crates/bevy_ecs/src/system/schedule_system.rs b/crates/bevy_ecs/src/system/schedule_system.rs index 26fdbdbe0f..ca4bdd4460 100644 --- a/crates/bevy_ecs/src/system/schedule_system.rs +++ b/crates/bevy_ecs/src/system/schedule_system.rs @@ -1,14 +1,15 @@ -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use crate::{ - component::{ComponentId, Tick}, + component::{CheckChangeTicks, ComponentId, Tick}, error::Result, - query::{Access, FilteredAccessSet}, + query::FilteredAccessSet, system::{input::SystemIn, BoxedSystem, System, SystemInput}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, }; -use super::{IntoSystem, SystemParamValidationError}; +use super::{IntoSystem, SystemParamValidationError, SystemStateFlags}; /// A wrapper system to change a system that returns `()` to return `Ok(())` to make it into a [`ScheduleSystem`] pub struct InfallibleSystemWrapper>(S); @@ -25,7 +26,7 @@ impl> System for InfallibleSystemWrapper { type Out = Result; #[inline] - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.0.name() } @@ -34,28 +35,8 @@ impl> System for InfallibleSystemWrapper { } #[inline] - fn component_access(&self) -> &Access { - self.0.component_access() - } - - #[inline] - fn component_access_set(&self) -> &FilteredAccessSet { - self.0.component_access_set() - } - - #[inline] - fn is_send(&self) -> bool { - self.0.is_send() - } - - #[inline] - fn is_exclusive(&self) -> bool { - self.0.is_exclusive() - } - - #[inline] - fn has_deferred(&self) -> bool { - self.0.has_deferred() + fn flags(&self) -> SystemStateFlags { + self.0.flags() } #[inline] @@ -93,13 +74,13 @@ impl> System for InfallibleSystemWrapper { } #[inline] - fn initialize(&mut self, world: &mut World) { - self.0.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + self.0.initialize(world) } #[inline] - fn check_change_tick(&mut self, change_tick: Tick) { - self.0.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.0.check_change_tick(check); } #[inline] @@ -157,31 +138,15 @@ where T: Send + Sync + 'static, { type In = (); - type Out = S::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system.name() } - fn component_access(&self) -> &Access { - self.system.component_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - self.system.component_access_set() - } - - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( @@ -213,12 +178,12 @@ where self.system.validate_param_unsafe(world) } - fn initialize(&mut self, world: &mut World) { - self.system.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { + self.system.initialize(world) } - fn check_change_tick(&mut self, change_tick: Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.system.check_change_tick(check); } fn get_last_run(&self) -> Tick { @@ -266,31 +231,15 @@ where T: FromWorld + Send + Sync + 'static, { type In = (); - type Out = S::Out; - fn name(&self) -> Cow<'static, str> { + fn name(&self) -> DebugName { self.system.name() } - fn component_access(&self) -> &Access { - self.system.component_access() - } - - fn component_access_set(&self) -> &FilteredAccessSet { - self.system.component_access_set() - } - - fn is_send(&self) -> bool { - self.system.is_send() - } - - fn is_exclusive(&self) -> bool { - self.system.is_exclusive() - } - - fn has_deferred(&self) -> bool { - self.system.has_deferred() + #[inline] + fn flags(&self) -> SystemStateFlags { + self.system.flags() } unsafe fn run_unsafe( @@ -326,15 +275,15 @@ where self.system.validate_param_unsafe(world) } - fn initialize(&mut self, world: &mut World) { - self.system.initialize(world); + fn initialize(&mut self, world: &mut World) -> FilteredAccessSet { if self.value.is_none() { self.value = Some(T::from_world(world)); } + self.system.initialize(world) } - fn check_change_tick(&mut self, change_tick: Tick) { - self.system.check_change_tick(change_tick); + fn check_change_tick(&mut self, check: CheckChangeTicks) { + self.system.check_change_tick(check); } fn get_last_run(&self) -> Tick { diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index edb2ead9cd..8527f3d3b8 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -2,23 +2,37 @@ clippy::module_inception, reason = "This instance of module inception is being discussed; see #17353." )] +use bevy_utils::prelude::DebugName; +use bitflags::bitflags; use core::fmt::Debug; use log::warn; use thiserror::Error; use crate::{ - component::{ComponentId, Tick}, - query::{Access, FilteredAccessSet}, + component::{CheckChangeTicks, ComponentId, Tick}, + query::FilteredAccessSet, schedule::InternedSystemSet, system::{input::SystemInput, SystemIn}, world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, World}, }; -use alloc::{borrow::Cow, boxed::Box, vec::Vec}; +use alloc::{boxed::Box, vec::Vec}; use core::any::TypeId; use super::{IntoSystem, SystemParamValidationError}; +bitflags! { + /// Bitflags representing system states and requirements. + #[derive(Clone, Copy, PartialEq, Eq, Hash)] + pub struct SystemStateFlags: u8 { + /// Set if system cannot be sent across threads + const NON_SEND = 1 << 0; + /// Set if system requires exclusive World access + const EXCLUSIVE = 1 << 1; + /// Set if system has deferred buffers. + const DEFERRED = 1 << 2; + } +} /// An ECS system that can be added to a [`Schedule`](crate::schedule::Schedule) /// /// Systems are functions with all arguments implementing @@ -36,28 +50,35 @@ pub trait System: Send + Sync + 'static { type In: SystemInput; /// The system's output. type Out; + /// Returns the system's name. - fn name(&self) -> Cow<'static, str>; + fn name(&self) -> DebugName; /// Returns the [`TypeId`] of the underlying system type. #[inline] fn type_id(&self) -> TypeId { TypeId::of::() } - /// Returns the system's component [`Access`]. - fn component_access(&self) -> &Access; - - /// Returns the system's component [`FilteredAccessSet`]. - fn component_access_set(&self) -> &FilteredAccessSet; + /// Returns the [`SystemStateFlags`] of the system. + fn flags(&self) -> SystemStateFlags; /// Returns true if the system is [`Send`]. - fn is_send(&self) -> bool; + #[inline] + fn is_send(&self) -> bool { + !self.flags().intersects(SystemStateFlags::NON_SEND) + } /// Returns true if the system must be run exclusively. - fn is_exclusive(&self) -> bool; + #[inline] + fn is_exclusive(&self) -> bool { + self.flags().intersects(SystemStateFlags::EXCLUSIVE) + } /// Returns true if system has deferred buffers. - fn has_deferred(&self) -> bool; + #[inline] + fn has_deferred(&self) -> bool { + self.flags().intersects(SystemStateFlags::DEFERRED) + } /// Runs the system with the given input in the world. Unlike [`System::run`], this function /// can be called in parallel with other systems and may break Rust's aliasing rules @@ -69,7 +90,7 @@ pub trait System: Send + Sync + 'static { /// # Safety /// /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data - /// registered in `component_access_set`. There must be no conflicting + /// registered in the access returned from [`System::initialize`]. There must be no conflicting /// simultaneous accesses while the system is running. /// - If [`System::is_exclusive`] returns `true`, then it must be valid to call /// [`UnsafeWorldCell::world_mut`] on `world`. @@ -130,7 +151,7 @@ pub trait System: Send + Sync + 'static { /// # Safety /// /// - The caller must ensure that [`world`](UnsafeWorldCell) has permission to access any world data - /// registered in `component_access_set`. There must be no conflicting + /// registered in the access returned from [`System::initialize`]. There must be no conflicting /// simultaneous accesses while the system is running. unsafe fn validate_param_unsafe( &mut self, @@ -147,13 +168,15 @@ pub trait System: Send + Sync + 'static { } /// Initialize the system. - fn initialize(&mut self, _world: &mut World); + /// + /// Returns a [`FilteredAccessSet`] with the access required to run the system. + fn initialize(&mut self, _world: &mut World) -> FilteredAccessSet; /// Checks any [`Tick`]s stored on this system and wraps their value if they get too old. /// /// This method must be called periodically to ensure that change detection behaves correctly. /// When using bevy's default configuration, this will be called for you as needed. - fn check_change_tick(&mut self, change_tick: Tick); + fn check_change_tick(&mut self, check: CheckChangeTicks); /// Returns the system's default [system sets](crate::schedule::SystemSet). /// @@ -203,9 +226,13 @@ pub unsafe trait ReadOnlySystem: System { /// A convenience type alias for a boxed [`System`] trait object. pub type BoxedSystem = Box>; -pub(crate) fn check_system_change_tick(last_run: &mut Tick, this_run: Tick, system_name: &str) { - if last_run.check_tick(this_run) { - let age = this_run.relative_to(*last_run).get(); +pub(crate) fn check_system_change_tick( + last_run: &mut Tick, + check: CheckChangeTicks, + system_name: DebugName, +) { + if last_run.check_tick(check) { + let age = check.present_tick().relative_to(*last_run).get(); warn!( "System '{system_name}' has not run for {age} ticks. \ Changes older than {} ticks will not be detected.", @@ -373,7 +400,7 @@ pub enum RunSystemError { #[error("System {system} did not run due to failed parameter validation: {err}")] InvalidParams { /// The identifier of the system that was run. - system: Cow<'static, str>, + system: DebugName, /// The returned parameter validation error. err: SystemParamValidationError, }, diff --git a/crates/bevy_ecs/src/system/system_name.rs b/crates/bevy_ecs/src/system/system_name.rs index b28ddd89f6..f38a5eb1aa 100644 --- a/crates/bevy_ecs/src/system/system_name.rs +++ b/crates/bevy_ecs/src/system/system_name.rs @@ -1,12 +1,12 @@ use crate::{ - component::Tick, + component::{ComponentId, Tick}, prelude::World, + query::FilteredAccessSet, system::{ExclusiveSystemParam, ReadOnlySystemParam, SystemMeta, SystemParam}, world::unsafe_world_cell::UnsafeWorldCell, }; -use alloc::borrow::Cow; -use core::ops::Deref; -use derive_more::derive::{AsRef, Display, Into}; +use bevy_utils::prelude::DebugName; +use derive_more::derive::{Display, Into}; /// [`SystemParam`] that returns the name of the system which it is used in. /// @@ -19,11 +19,11 @@ use derive_more::derive::{AsRef, Display, Into}; /// # use bevy_ecs::system::SystemParam; /// /// #[derive(SystemParam)] -/// struct Logger<'s> { -/// system_name: SystemName<'s>, +/// struct Logger { +/// system_name: SystemName, /// } /// -/// impl<'s> Logger<'s> { +/// impl Logger { /// fn log(&mut self, message: &str) { /// eprintln!("{}: {}", self.system_name, message); /// } @@ -34,57 +34,53 @@ use derive_more::derive::{AsRef, Display, Into}; /// logger.log("Hello"); /// } /// ``` -#[derive(Debug, Into, Display, AsRef)] -#[as_ref(str)] -pub struct SystemName<'s>(&'s str); +#[derive(Debug, Into, Display)] +pub struct SystemName(DebugName); -impl<'s> SystemName<'s> { +impl SystemName { /// Gets the name of the system. - pub fn name(&self) -> &str { - self.0 - } -} - -impl<'s> Deref for SystemName<'s> { - type Target = str; - fn deref(&self) -> &Self::Target { - self.name() + pub fn name(&self) -> DebugName { + self.0.clone() } } // SAFETY: no component value access -unsafe impl SystemParam for SystemName<'_> { - type State = Cow<'static, str>; - type Item<'w, 's> = SystemName<'s>; +unsafe impl SystemParam for SystemName { + type State = (); + type Item<'w, 's> = SystemName; - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.name.clone() + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { } #[inline] unsafe fn get_param<'w, 's>( - name: &'s mut Self::State, - _system_meta: &SystemMeta, + _state: &'s mut Self::State, + system_meta: &SystemMeta, _world: UnsafeWorldCell<'w>, _change_tick: Tick, ) -> Self::Item<'w, 's> { - SystemName(name) + SystemName(system_meta.name.clone()) } } // SAFETY: Only reads internal system state -unsafe impl<'s> ReadOnlySystemParam for SystemName<'s> {} +unsafe impl ReadOnlySystemParam for SystemName {} -impl ExclusiveSystemParam for SystemName<'_> { - type State = Cow<'static, str>; - type Item<'s> = SystemName<'s>; +impl ExclusiveSystemParam for SystemName { + type State = (); + type Item<'s> = SystemName; - fn init(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.name.clone() - } + fn init(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} - fn get_param<'s>(state: &'s mut Self::State, _system_meta: &SystemMeta) -> Self::Item<'s> { - SystemName(state) + fn get_param<'s>(_state: &'s mut Self::State, system_meta: &SystemMeta) -> Self::Item<'s> { + SystemName(system_meta.name.clone()) } } @@ -99,7 +95,7 @@ mod tests { #[test] fn test_system_name_regular_param() { fn testing(name: SystemName) -> String { - name.name().to_owned() + name.name().as_string() } let mut world = World::default(); @@ -111,7 +107,7 @@ mod tests { #[test] fn test_system_name_exclusive_param() { fn testing(_world: &mut World, name: SystemName) -> String { - name.name().to_owned() + name.name().as_string() } let mut world = World::default(); @@ -125,7 +121,7 @@ mod tests { let mut world = World::default(); let system = IntoSystem::into_system(|name: SystemName| name.name().to_owned()).with_name("testing"); - let name = world.run_system_once(system).unwrap(); + let name = world.run_system_once(system).unwrap().as_string(); assert_eq!(name, "testing"); } @@ -135,7 +131,7 @@ mod tests { let system = IntoSystem::into_system(|_world: &mut World, name: SystemName| name.name().to_owned()) .with_name("testing"); - let name = world.run_system_once(system).unwrap(); + let name = world.run_system_once(system).unwrap().as_string(); assert_eq!(name, "testing"); } } diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index fc795abf59..65fecaf564 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -25,6 +25,7 @@ use alloc::{ pub use bevy_ecs_macros::SystemParam; use bevy_platform::cell::SyncCell; use bevy_ptr::UnsafeCellDeref; +use bevy_utils::prelude::DebugName; use core::{ any::Any, fmt::{Debug, Display}, @@ -32,7 +33,6 @@ use core::{ ops::{Deref, DerefMut}, panic::Location, }; -use disqualified::ShortName; use thiserror::Error; use super::Populated; @@ -57,7 +57,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// # use bevy_ecs::prelude::*; /// # #[derive(Resource)] /// # struct SomeResource; -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct SomeEvent; /// # #[derive(Resource)] /// # struct SomeOtherResource; @@ -206,7 +206,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// # Safety /// /// The implementor must ensure the following is true. -/// - [`SystemParam::init_state`] correctly registers all [`World`] accesses used +/// - [`SystemParam::init_access`] correctly registers all [`World`] accesses used /// by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). /// - None of the world accesses may conflict with any prior accesses registered /// on `system_meta`. @@ -220,9 +220,16 @@ pub unsafe trait SystemParam: Sized { /// You could think of [`SystemParam::Item<'w, 's>`] as being an *operation* that changes the lifetimes bound to `Self`. type Item<'world, 'state>: SystemParam; + /// Creates a new instance of this param's [`State`](SystemParam::State). + fn init_state(world: &mut World) -> Self::State; + /// Registers any [`World`] access used by this [`SystemParam`] - /// and creates a new instance of this param's [`State`](SystemParam::State). - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State; + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ); /// Applies any deferred mutations stored in this [`SystemParam`]'s state. /// This is used to apply [`Commands`] during [`ApplyDeferred`](crate::prelude::ApplyDeferred). @@ -274,7 +281,7 @@ pub unsafe trait SystemParam: Sized { /// # Safety /// /// - The passed [`UnsafeWorldCell`] must have read-only access to world data - /// registered in [`init_state`](SystemParam::init_state). + /// registered in [`init_access`](SystemParam::init_access). /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). #[expect( unused_variables, @@ -293,7 +300,7 @@ pub unsafe trait SystemParam: Sized { /// # Safety /// /// - The passed [`UnsafeWorldCell`] must have access to any world data registered - /// in [`init_state`](SystemParam::init_state). + /// in [`init_access`](SystemParam::init_access). /// - `world` must be the same [`World`] that was used to initialize [`state`](SystemParam::init_state). unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, @@ -324,10 +331,25 @@ unsafe impl SystemParam for Qu type State = QueryState; type Item<'w, 's> = Query<'w, 's, D, F>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let state = QueryState::new(world); - init_query_param(world, system_meta, &state); - state + fn init_state(world: &mut World) -> Self::State { + QueryState::new(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + assert_component_access_compatibility( + &system_meta.name, + DebugName::type_name::(), + DebugName::type_name::(), + component_access_set, + &state.component_access, + world, + ); + component_access_set.add(state.component_access.clone()); } #[inline] @@ -345,28 +367,10 @@ unsafe impl SystemParam for Qu } } -pub(crate) fn init_query_param( - world: &mut World, - system_meta: &mut SystemMeta, - state: &QueryState, -) { - assert_component_access_compatibility( - &system_meta.name, - core::any::type_name::(), - core::any::type_name::(), - &system_meta.component_access_set, - &state.component_access, - world, - ); - system_meta - .component_access_set - .add(state.component_access.clone()); -} - fn assert_component_access_compatibility( - system_name: &str, - query_type: &'static str, - filter_type: &'static str, + system_name: &DebugName, + query_type: DebugName, + filter_type: DebugName, system_access: &FilteredAccessSet, current: &FilteredAccess, world: &World, @@ -380,17 +384,28 @@ fn assert_component_access_compatibility( if !accesses.is_empty() { accesses.push(' '); } - panic!("error[B0001]: Query<{}, {}> in system {system_name} accesses component(s) {accesses}in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001", ShortName(query_type), ShortName(filter_type)); + panic!("error[B0001]: Query<{}, {}> in system {system_name} accesses component(s) {accesses}in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevy.org/learn/errors/b0001", query_type.shortname(), filter_type.shortname()); } // SAFETY: Relevant query ComponentId access is applied to SystemMeta. If // this Query conflicts with any prior access, a panic will occur. -unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam for Single<'a, D, F> { +unsafe impl<'a, 'b, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam + for Single<'a, 'b, D, F> +{ type State = QueryState; - type Item<'w, 's> = Single<'w, D, F>; + type Item<'w, 's> = Single<'w, 's, D, F>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - Query::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + Query::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + Query::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -438,8 +453,8 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo } // SAFETY: QueryState is constrained to read-only fetches, so it only reads World. -unsafe impl<'a, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam - for Single<'a, D, F> +unsafe impl<'a, 'b, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> ReadOnlySystemParam + for Single<'a, 'b, D, F> { } @@ -451,8 +466,17 @@ unsafe impl SystemParam type State = QueryState; type Item<'w, 's> = Populated<'w, 's, D, F>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - Query::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + Query::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + Query::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -576,7 +600,7 @@ unsafe impl<'w, 's, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> Re /// ``` /// # use bevy_ecs::prelude::*; /// # -/// # #[derive(Event)] +/// # #[derive(Event, BufferedEvent)] /// # struct MyEvent; /// # impl MyEvent { /// # pub fn new() -> Self { Self } @@ -616,7 +640,7 @@ pub struct ParamSet<'w, 's, T: SystemParam> { } macro_rules! impl_param_set { - ($(($index: tt, $param: ident, $system_meta: ident, $fn_name: ident)),*) => { + ($(($index: tt, $param: ident, $fn_name: ident)),*) => { // SAFETY: All parameters are constrained to ReadOnlySystemParam, so World is only read unsafe impl<'w, 's, $($param,)*> ReadOnlySystemParam for ParamSet<'w, 's, ($($param,)*)> where $($param: ReadOnlySystemParam,)* @@ -637,25 +661,32 @@ macro_rules! impl_param_set { non_snake_case, reason = "Certain variable names are provided by the caller, not by us." )] - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { + ($($param::init_state(world),)*) + } + + #[expect( + clippy::allow_attributes, + reason = "This is inside a macro meant for tuples; as such, `non_snake_case` won't always lint." + )] + #[allow( + non_snake_case, + reason = "Certain variable names are provided by the caller, not by us." + )] + fn init_access(state: &Self::State, system_meta: &mut SystemMeta, component_access_set: &mut FilteredAccessSet, world: &mut World) { + let ($($param,)*) = state; $( - // Pretend to add each param to the system alone, see if it conflicts - let mut $system_meta = system_meta.clone(); - $system_meta.component_access_set.clear(); - $param::init_state(world, &mut $system_meta); - // The variable is being defined with non_snake_case here - let $param = $param::init_state(world, &mut system_meta.clone()); + // Call `init_access` on a clone of the original access set to check for conflicts + let component_access_set_clone = &mut component_access_set.clone(); + $param::init_access($param, system_meta, component_access_set_clone, world); )* - // Make the ParamSet non-send if any of its parameters are non-send. - if false $(|| !$system_meta.is_send())* { - system_meta.set_non_send(); - } $( - system_meta - .component_access_set - .extend($system_meta.component_access_set); + // Pretend to add the param to the system alone to gather the new access, + // then merge its access into the system. + let mut access_set = FilteredAccessSet::new(); + $param::init_access($param, system_meta, &mut access_set, world); + component_access_set.extend(access_set); )* - ($($param,)*) } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -711,7 +742,7 @@ macro_rules! impl_param_set { } } -all_tuples_enumerated!(impl_param_set, 1, 8, P, m, p); +all_tuples_enumerated!(impl_param_set, 1, 8, P, p); // SAFETY: Res only reads a single World resource unsafe impl<'a, T: Resource> ReadOnlySystemParam for Res<'a, T> {} @@ -722,21 +753,25 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { type State = ComponentId; type Item<'w, 's> = Res<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let component_id = world.components_registrator().register_resource::(); + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_resource::() + } - let combined_access = system_meta.component_access_set.combined_access(); + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); assert!( !combined_access.has_resource_write(component_id), "error[B0002]: Res<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), + DebugName::type_name::(), system_meta.name, ); - system_meta - .component_access_set - .add_unfiltered_resource_read(component_id); - component_id + component_access_set.add_unfiltered_resource_read(component_id); } #[inline] @@ -773,8 +808,8 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { panic!( "Resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() - ) + DebugName::type_name::() + ); }); Res { value: ptr.deref(), @@ -795,24 +830,27 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { type State = ComponentId; type Item<'w, 's> = ResMut<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - let component_id = world.components_registrator().register_resource::(); + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_resource::() + } - let combined_access = system_meta.component_access_set.combined_access(); + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); if combined_access.has_resource_write(component_id) { panic!( "error[B0002]: ResMut<{}> in system {} conflicts with a previous ResMut<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + DebugName::type_name::(), system_meta.name); } else if combined_access.has_resource_read(component_id) { panic!( "error[B0002]: ResMut<{}> in system {} conflicts with a previous Res<{0}> access. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + DebugName::type_name::(), system_meta.name); } - system_meta - .component_access_set - .add_unfiltered_resource_write(component_id); - - component_id + component_access_set.add_unfiltered_resource_write(component_id); } #[inline] @@ -848,8 +886,8 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { panic!( "Resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() - ) + DebugName::type_name::() + ); }); ResMut { value: value.value.deref_mut::(), @@ -872,18 +910,24 @@ unsafe impl SystemParam for &'_ World { type State = (); type Item<'w, 's> = &'w World; - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { let mut filtered_access = FilteredAccess::default(); filtered_access.read_all(); - if !system_meta - .component_access_set + if !component_access_set .get_conflicts_single(&filtered_access) .is_empty() { panic!("&World conflicts with a previous mutable system parameter. Allowing this would break Rust's mutability rules"); } - system_meta.component_access_set.add(filtered_access); + component_access_set.add(filtered_access); } #[inline] @@ -903,16 +947,20 @@ unsafe impl<'w> SystemParam for DeferredWorld<'w> { type State = (); type Item<'world, 'state> = DeferredWorld<'world>; - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { assert!( - !system_meta - .component_access_set - .combined_access() - .has_any_read(), + !component_access_set.combined_access().has_any_read(), "DeferredWorld in system {} conflicts with a previous access.", system_meta.name, ); - system_meta.component_access_set.write_all(); + component_access_set.write_all(); } unsafe fn get_param<'world, 'state>( @@ -1042,10 +1090,18 @@ unsafe impl<'a, T: FromWorld + Send + 'static> SystemParam for Local<'a, T> { type State = SyncCell; type Item<'w, 's> = Local<'s, T>; - fn init_state(world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { SyncCell::new(T::from_world(world)) } + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } + #[inline] unsafe fn get_param<'w, 's>( state: &'s mut Self::State, @@ -1222,11 +1278,19 @@ unsafe impl SystemParam for Deferred<'_, T> { type State = SyncCell; type Item<'w, 's> = Deferred<'s, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - system_meta.set_has_deferred(); + fn init_state(world: &mut World) -> Self::State { SyncCell::new(T::from_world(world)) } + fn init_access( + _state: &Self::State, + system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + system_meta.set_has_deferred(); + } + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { state.get().apply(system_meta, world); } @@ -1255,7 +1319,14 @@ unsafe impl SystemParam for NonSendMarker { type Item<'w, 's> = Self; #[inline] - fn init_state(_world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { system_meta.set_non_send(); } @@ -1349,23 +1420,26 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { type State = ComponentId; type Item<'w, 's> = NonSend<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_non_send::() + } + + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { system_meta.set_non_send(); - let component_id = world.components_registrator().register_non_send::(); - - let combined_access = system_meta.component_access_set.combined_access(); + let combined_access = component_access_set.combined_access(); assert!( !combined_access.has_resource_write(component_id), "error[B0002]: NonSend<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), + DebugName::type_name::(), system_meta.name, ); - system_meta - .component_access_set - .add_unfiltered_resource_read(component_id); - - component_id + component_access_set.add_unfiltered_resource_read(component_id); } #[inline] @@ -1402,7 +1476,7 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { panic!( "Non-send resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() + DebugName::type_name::() ) }); @@ -1422,26 +1496,29 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { type State = ComponentId; type Item<'w, 's> = NonSendMut<'w, T>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { + world.components_registrator().register_non_send::() + } + + fn init_access( + &component_id: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { system_meta.set_non_send(); - let component_id = world.components_registrator().register_non_send::(); - - let combined_access = system_meta.component_access_set.combined_access(); + let combined_access = component_access_set.combined_access(); if combined_access.has_component_write(component_id) { panic!( "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous mutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + DebugName::type_name::(), system_meta.name); } else if combined_access.has_component_read(component_id) { panic!( "error[B0002]: NonSendMut<{}> in system {} conflicts with a previous immutable resource access ({0}). Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002", - core::any::type_name::(), system_meta.name); + DebugName::type_name::(), system_meta.name); } - system_meta - .component_access_set - .add_unfiltered_resource_write(component_id); - - component_id + component_access_set.add_unfiltered_resource_write(component_id); } #[inline] @@ -1478,8 +1555,8 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { panic!( "Non-send resource requested by {} does not exist: {}", system_meta.name, - core::any::type_name::() - ) + DebugName::type_name::() + ); }); NonSendMut { value: ptr.assert_unique().deref_mut(), @@ -1497,7 +1574,15 @@ unsafe impl<'a> SystemParam for &'a Archetypes { type State = (); type Item<'w, 's> = &'w Archetypes; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1518,7 +1603,15 @@ unsafe impl<'a> SystemParam for &'a Components { type State = (); type Item<'w, 's> = &'w Components; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1539,7 +1632,15 @@ unsafe impl<'a> SystemParam for &'a Entities { type State = (); type Item<'w, 's> = &'w Entities; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1560,7 +1661,15 @@ unsafe impl<'a> SystemParam for &'a Bundles { type State = (); type Item<'w, 's> = &'w Bundles; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1610,7 +1719,15 @@ unsafe impl SystemParam for SystemChangeTick { type State = (); type Item<'w, 's> = SystemChangeTick; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'w, 's>( @@ -1632,8 +1749,17 @@ unsafe impl SystemParam for Option { type Item<'world, 'state> = Option>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - T::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + T::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -1666,8 +1792,17 @@ unsafe impl SystemParam for Result = Result, SystemParamValidationError>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - T::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + T::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -1752,8 +1887,17 @@ unsafe impl SystemParam for When { type Item<'world, 'state> = When>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - T::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + T::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(state, system_meta, component_access_set, world); } #[inline] @@ -1790,18 +1934,28 @@ unsafe impl SystemParam for When { // SAFETY: Delegates to `T`, which ensures the safety requirements are met unsafe impl ReadOnlySystemParam for When {} -// SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Registers access for each element of `state`. +// If any one conflicts, it will panic. unsafe impl SystemParam for Vec { type State = Vec; type Item<'world, 'state> = Vec>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Vec::new() } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + for state in state { + T::init_access(state, system_meta, component_access_set, world); + } + } + #[inline] unsafe fn validate_param( state: &mut Self::State, @@ -1824,7 +1978,7 @@ unsafe impl SystemParam for Vec { state .iter_mut() // SAFETY: - // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by each param. + // - We initialized the access for each parameter in `init_access`, so the caller ensures we have access to any world data needed by each param. // - The caller ensures this was the world used to initialize our state, and we used that world to initialize parameter states .map(|state| unsafe { T::get_param(state, system_meta, world, change_tick) }) .collect() @@ -1843,18 +1997,38 @@ unsafe impl SystemParam for Vec { } } -// SAFETY: When initialized with `init_state`, `get_param` returns an empty `Vec` and does no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty `Vec`s are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Registers access for each element of `state`. +// If any one conflicts with a previous parameter, +// the call passing a copy of the current access will panic. unsafe impl SystemParam for ParamSet<'_, '_, Vec> { type State = Vec; type Item<'world, 'state> = ParamSet<'world, 'state, Vec>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Vec::new() } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + for state in state { + // Call `init_access` on a clone of the original access set to check for conflicts + let component_access_set_clone = &mut component_access_set.clone(); + T::init_access(state, system_meta, component_access_set_clone, world); + } + for state in state { + // Pretend to add the param to the system alone to gather the new access, + // then merge its access into the system. + let mut access_set = FilteredAccessSet::new(); + T::init_access(state, system_meta, &mut access_set, world); + component_access_set.extend(access_set); + } + } + #[inline] unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, @@ -1888,7 +2062,7 @@ impl ParamSet<'_, '_, Vec> { /// No other parameters may be accessed while this one is active. pub fn get_mut(&mut self, index: usize) -> T::Item<'_, '_> { // SAFETY: - // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by any param. + // - We initialized the access for each parameter, so the caller ensures we have access to any world data needed by any param. // We have mutable access to the ParamSet, so no other params in the set are active. // - The caller of `get_param` ensured that this was the world used to initialize our state, and we used that world to initialize parameter states unsafe { @@ -1906,7 +2080,7 @@ impl ParamSet<'_, '_, Vec> { self.param_states.iter_mut().for_each(|state| { f( // SAFETY: - // - We initialized the state for each parameter in the builder, so the caller ensures we have access to any world data needed by any param. + // - We initialized the access for each parameter, so the caller ensures we have access to any world data needed by any param. // We have mutable access to the ParamSet, so no other params in the set are active. // - The caller of `get_param` ensured that this was the world used to initialize our state, and we used that world to initialize parameter states unsafe { T::get_param(state, &self.system_meta, self.world, self.change_tick) }, @@ -1940,8 +2114,13 @@ macro_rules! impl_system_param_tuple { type Item<'w, 's> = ($($param::Item::<'w, 's>,)*); #[inline] - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - (($($param::init_state(world, system_meta),)*)) + fn init_state(world: &mut World) -> Self::State { + (($($param::init_state(world),)*)) + } + + fn init_access(state: &Self::State, _system_meta: &mut SystemMeta, _component_access_set: &mut FilteredAccessSet, _world: &mut World) { + let ($($param,)*) = state; + $($param::init_access($param, _system_meta, _component_access_set, _world);)* } #[inline] @@ -2109,8 +2288,17 @@ unsafe impl SystemParam for StaticSystemParam<'_, '_, type State = P::State; type Item<'world, 'state> = StaticSystemParam<'world, 'state, P>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { - P::init_state(world, system_meta) + fn init_state(world: &mut World) -> Self::State { + P::init_state(world) + } + + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + P::init_access(state, system_meta, component_access_set, world); } fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { @@ -2147,7 +2335,15 @@ unsafe impl SystemParam for PhantomData { type State = (); type Item<'world, 'state> = Self; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State {} + fn init_state(_world: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'world, 'state>( @@ -2359,6 +2555,14 @@ trait DynParamState: Sync + Send + Any { /// Queues any deferred mutations to be applied at the next [`ApplyDeferred`](crate::prelude::ApplyDeferred). fn queue(&mut self, system_meta: &SystemMeta, world: DeferredWorld); + /// Registers any [`World`] access used by this [`SystemParam`] + fn init_access( + &self, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ); + /// Refer to [`SystemParam::validate_param`]. /// /// # Safety @@ -2382,6 +2586,15 @@ impl DynParamState for ParamState { T::queue(&mut self.0, system_meta, world); } + fn init_access( + &self, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + T::init_access(&self.0, system_meta, component_access_set, world); + } + unsafe fn validate_param( &mut self, system_meta: &SystemMeta, @@ -2391,16 +2604,27 @@ impl DynParamState for ParamState { } } -// SAFETY: `init_state` creates a state of (), which performs no access. The interesting safety checks are on the `SystemParamBuilder`. +// SAFETY: Delegates to the wrapped parameter, which ensures the safety requirements are met unsafe impl SystemParam for DynSystemParam<'_, '_> { type State = DynSystemParamState; type Item<'world, 'state> = DynSystemParam<'world, 'state>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { DynSystemParamState::new::<()>(()) } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + state + .0 + .init_access(system_meta, component_access_set, world); + } + #[inline] unsafe fn validate_param( state: &mut Self::State, @@ -2419,8 +2643,8 @@ unsafe impl SystemParam for DynSystemParam<'_, '_> { ) -> Self::Item<'world, 'state> { // SAFETY: // - `state.0` is a boxed `ParamState`. - // - The state was obtained from `SystemParamBuilder::build()`, which registers all [`World`] accesses used - // by [`SystemParam::get_param`] with the provided [`system_meta`](SystemMeta). + // - `init_access` calls `DynParamState::init_access`, which calls `init_access` on the inner parameter, + // so the caller ensures the world has the necessary access. // - The caller ensures that the provided world is the same and has the required access. unsafe { DynSystemParam::new(state.0.as_mut(), world, system_meta.clone(), change_tick) } } @@ -2434,26 +2658,48 @@ unsafe impl SystemParam for DynSystemParam<'_, '_> { } } -// SAFETY: When initialized with `init_state`, `get_param` returns a `FilteredResources` with no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty access are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Resource ComponentId access is applied to the access. If this FilteredResources +// conflicts with any prior access, a panic will occur. unsafe impl SystemParam for FilteredResources<'_, '_> { type State = Access; type Item<'world, 'state> = FilteredResources<'world, 'state>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Access::new() } + fn init_access( + access: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); + let conflicts = combined_access.get_conflicts(access); + if !conflicts.is_empty() { + let accesses = conflicts.format_conflict_list(world); + let system_name = &system_meta.name; + panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); + } + + if access.has_read_all_resources() { + component_access_set.add_unfiltered_read_all_resources(); + } else { + for component_id in access.resource_reads_and_writes() { + component_access_set.add_unfiltered_resource_read(component_id); + } + } + } + unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell<'world>, change_tick: Tick, ) -> Self::Item<'world, 'state> { - // SAFETY: The caller ensures that `world` has access to anything registered in `init_state` or `build`, - // and the builder registers `access` in `build`. + // SAFETY: The caller ensures that `world` has access to anything registered in `init_access`, + // and we registered all resource access in `state``. unsafe { FilteredResources::new(world, state, system_meta.last_run, change_tick) } } } @@ -2461,26 +2707,56 @@ unsafe impl SystemParam for FilteredResources<'_, '_> { // SAFETY: FilteredResources only reads resources. unsafe impl ReadOnlySystemParam for FilteredResources<'_, '_> {} -// SAFETY: When initialized with `init_state`, `get_param` returns a `FilteredResourcesMut` with no access. -// Therefore, `init_state` trivially registers all access, and no accesses can conflict. -// Note that the safety requirements for non-empty access are handled by the `SystemParamBuilder` impl that builds them. +// SAFETY: Resource ComponentId access is applied to the access. If this FilteredResourcesMut +// conflicts with any prior access, a panic will occur. unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { type State = Access; type Item<'world, 'state> = FilteredResourcesMut<'world, 'state>; - fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + fn init_state(_world: &mut World) -> Self::State { Access::new() } + fn init_access( + access: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + let combined_access = component_access_set.combined_access(); + let conflicts = combined_access.get_conflicts(access); + if !conflicts.is_empty() { + let accesses = conflicts.format_conflict_list(world); + let system_name = &system_meta.name; + panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevy.org/learn/errors/b0002"); + } + + if access.has_read_all_resources() { + component_access_set.add_unfiltered_read_all_resources(); + } else { + for component_id in access.resource_reads() { + component_access_set.add_unfiltered_resource_read(component_id); + } + } + + if access.has_write_all_resources() { + component_access_set.add_unfiltered_write_all_resources(); + } else { + for component_id in access.resource_writes() { + component_access_set.add_unfiltered_resource_write(component_id); + } + } + } + unsafe fn get_param<'world, 'state>( state: &'state mut Self::State, system_meta: &SystemMeta, world: UnsafeWorldCell<'world>, change_tick: Tick, ) -> Self::Item<'world, 'state> { - // SAFETY: The caller ensures that `world` has access to anything registered in `init_state` or `build`, - // and the builder registers `access` in `build`. + // SAFETY: The caller ensures that `world` has access to anything registered in `init_access`, + // and we registered all resource access in `state``. unsafe { FilteredResourcesMut::new(world, state, system_meta.last_run, change_tick) } } } @@ -2511,7 +2787,7 @@ pub struct SystemParamValidationError { /// A string identifying the invalid parameter. /// This is usually the type name of the parameter. - pub param: Cow<'static, str>, + pub param: DebugName, /// A string identifying the field within a parameter using `#[derive(SystemParam)]`. /// This will be an empty string for other parameters. @@ -2543,7 +2819,7 @@ impl SystemParamValidationError { Self { skipped, message: message.into(), - param: Cow::Borrowed(core::any::type_name::()), + param: DebugName::type_name::(), field: field.into(), } } @@ -2554,7 +2830,7 @@ impl Display for SystemParamValidationError { write!( fmt, "Parameter `{}{}` failed validation: {}", - ShortName(&self.param), + self.param.shortname(), self.field, self.message )?; @@ -2568,7 +2844,7 @@ impl Display for SystemParamValidationError { #[cfg(test)] mod tests { use super::*; - use crate::system::assert_is_system; + use crate::{event::Event, system::assert_is_system}; use core::cell::RefCell; // Compile test for https://github.com/bevyengine/bevy/pull/2838. @@ -2814,11 +3090,11 @@ mod tests { } #[test] - #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_event_error::event_system`: Parameter `EventReader::events` failed validation: Event not initialized"] + #[should_panic = "Encountered an error in system `bevy_ecs::system::system_param::tests::missing_event_error::event_system`: Parameter `EventReader::events` failed validation: BufferedEvent not initialized"] fn missing_event_error() { - use crate::prelude::{Event, EventReader}; + use crate::prelude::{BufferedEvent, EventReader}; - #[derive(Event)] + #[derive(Event, BufferedEvent)] pub struct MissingEvent; let mut schedule = crate::schedule::Schedule::default(); diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 272cc85d0d..6889a8f4cd 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -651,6 +651,19 @@ mod tests { assert_eq!(output, NonCopy(3)); } + #[test] + fn fallible_system() { + fn sys() -> Result<()> { + Err("error")?; + Ok(()) + } + + let mut world = World::new(); + let fallible_system_id = world.register_system(sys); + let output = world.run_system(fallible_system_id); + assert!(matches!(output, Ok(Err(_)))); + } + #[test] fn exclusive_system() { let mut world = World::new(); @@ -751,19 +764,54 @@ mod tests { assert!(matches!(output, Ok(x) if x == four())); } + #[test] + fn cached_fallible_system() { + fn sys() -> Result<()> { + Err("error")?; + Ok(()) + } + + let mut world = World::new(); + let fallible_system_id = world.register_system_cached(sys); + let output = world.run_system(fallible_system_id); + assert!(matches!(output, Ok(Err(_)))); + let output = world.run_system_cached(sys); + assert!(matches!(output, Ok(Err(_)))); + let output = world.run_system_cached_with(sys, ()); + assert!(matches!(output, Ok(Err(_)))); + } + #[test] fn cached_system_commands() { fn sys(mut counter: ResMut) { - counter.0 = 1; + counter.0 += 1; } let mut world = World::new(); world.insert_resource(Counter(0)); - world.commands().run_system_cached(sys); world.flush_commands(); - assert_eq!(world.resource::().0, 1); + world.commands().run_system_cached_with(sys, ()); + world.flush_commands(); + assert_eq!(world.resource::().0, 2); + } + + #[test] + fn cached_fallible_system_commands() { + fn sys(mut counter: ResMut) -> Result { + counter.0 += 1; + Ok(()) + } + + let mut world = World::new(); + world.insert_resource(Counter(0)); + world.commands().run_system_cached(sys); + world.flush_commands(); + assert_eq!(world.resource::().0, 1); + world.commands().run_system_cached_with(sys, ()); + world.flush_commands(); + assert_eq!(world.resource::().0, 2); } #[test] diff --git a/crates/bevy_ecs/src/traversal.rs b/crates/bevy_ecs/src/traversal.rs index 306ae7c92d..577720fd5d 100644 --- a/crates/bevy_ecs/src/traversal.rs +++ b/crates/bevy_ecs/src/traversal.rs @@ -1,6 +1,10 @@ //! A trait for components that let you traverse the ECS. -use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship}; +use crate::{ + entity::Entity, + query::{ReadOnlyQueryData, ReleaseStateQueryData}, + relationship::Relationship, +}; /// A component that can point to another entity, and which can be used to define a path through the ECS. /// @@ -14,19 +18,19 @@ use crate::{entity::Entity, query::ReadOnlyQueryData, relationship::Relationship /// avoiding infinite loops in their code. /// /// Traversals may be parameterized with additional data. For example, in observer event propagation, the -/// parameter `D` is the event type given in `Trigger`. This allows traversal to differ depending on event +/// parameter `D` is the event type given in `On`. This allows traversal to differ depending on event /// data. /// -/// [specify the direction]: crate::event::Event::Traversal -/// [event propagation]: crate::observer::Trigger::propagate +/// [specify the direction]: crate::event::EntityEvent::Traversal +/// [event propagation]: crate::observer::On::propagate /// [observers]: crate::observer::Observer -pub trait Traversal: ReadOnlyQueryData { +pub trait Traversal: ReadOnlyQueryData + ReleaseStateQueryData { /// Returns the next entity to visit. - fn traverse(item: Self::Item<'_>, data: &D) -> Option; + fn traverse(item: Self::Item<'_, '_>, data: &D) -> Option; } impl Traversal for () { - fn traverse(_: Self::Item<'_>, _data: &D) -> Option { + fn traverse(_: Self::Item<'_, '_>, _data: &D) -> Option { None } } @@ -37,9 +41,9 @@ impl Traversal for () { /// /// Traversing in a loop could result in infinite loops for relationship graphs with loops. /// -/// [event propagation]: crate::observer::Trigger::propagate +/// [event propagation]: crate::observer::On::propagate impl Traversal for &R { - fn traverse(item: Self::Item<'_>, _data: &D) -> Option { + fn traverse(item: Self::Item<'_, '_>, _data: &D) -> Option { Some(item.get()) } } diff --git a/crates/bevy_ecs/src/world/component_constants.rs b/crates/bevy_ecs/src/world/component_constants.rs deleted file mode 100644 index ea2899c5f9..0000000000 --- a/crates/bevy_ecs/src/world/component_constants.rs +++ /dev/null @@ -1,55 +0,0 @@ -//! Internal components used by bevy with a fixed component id. -//! Constants are used to skip [`TypeId`] lookups in hot paths. -use super::*; -#[cfg(feature = "bevy_reflect")] -use bevy_reflect::Reflect; - -/// [`ComponentId`] for [`OnAdd`] -pub const ON_ADD: ComponentId = ComponentId::new(0); -/// [`ComponentId`] for [`OnInsert`] -pub const ON_INSERT: ComponentId = ComponentId::new(1); -/// [`ComponentId`] for [`OnReplace`] -pub const ON_REPLACE: ComponentId = ComponentId::new(2); -/// [`ComponentId`] for [`OnRemove`] -pub const ON_REMOVE: ComponentId = ComponentId::new(3); -/// [`ComponentId`] for [`OnDespawn`] -pub const ON_DESPAWN: ComponentId = ComponentId::new(4); - -/// Trigger emitted when a component is inserted onto an entity that does not already have that -/// component. Runs before `OnInsert`. -/// See [`crate::component::ComponentHooks::on_add`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnAdd; - -/// Trigger emitted when a component is inserted, regardless of whether or not the entity already -/// had that component. Runs after `OnAdd`, if it ran. -/// See [`crate::component::ComponentHooks::on_insert`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnInsert; - -/// Trigger emitted when a component is inserted onto an entity that already has that component. -/// Runs before the value is replaced, so you can still access the original component data. -/// See [`crate::component::ComponentHooks::on_replace`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnReplace; - -/// Trigger emitted when a component is removed from an entity, and runs before the component is -/// removed, so you can still access the component data. -/// See [`crate::component::ComponentHooks::on_remove`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnRemove; - -/// Trigger emitted for each component on an entity when it is despawned. -/// See [`crate::component::ComponentHooks::on_despawn`] for more information. -#[derive(Event, Debug)] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -#[cfg_attr(feature = "bevy_reflect", reflect(Debug))] -pub struct OnDespawn; diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 02c12fe6a3..edf4b186a3 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -1,11 +1,14 @@ use core::ops::Deref; +use bevy_utils::prelude::DebugName; + use crate::{ archetype::Archetype, change_detection::{MaybeLocation, MutUntyped}, - component::{ComponentId, HookContext, Mutable}, + component::{ComponentId, Mutable}, entity::Entity, - event::{Event, EventId, Events, SendBatchIds}, + event::{BufferedEvent, EntityEvent, Event, EventId, Events, SendBatchIds}, + lifecycle::{HookContext, INSERT, REPLACE}, observer::{Observers, TriggerTargets}, prelude::{Component, QueryState}, query::{QueryData, QueryFilter}, @@ -16,14 +19,14 @@ use crate::{ world::{error::EntityMutableFetchError, EntityFetcher, WorldEntityFetch}, }; -use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World, ON_INSERT, ON_REPLACE}; +use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World}; /// A [`World`] reference that disallows structural ECS changes. /// This includes initializing resources, registering components or spawning entities. /// /// This means that in order to add entities, for example, you will need to use commands instead of the world directly. pub struct DeferredWorld<'w> { - // SAFETY: Implementors must not use this reference to make structural changes + // SAFETY: Implementers must not use this reference to make structural changes world: UnsafeWorldCell<'w>, } @@ -84,7 +87,7 @@ impl<'w> DeferredWorld<'w> { /// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and /// runs the provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -114,7 +117,7 @@ impl<'w> DeferredWorld<'w> { /// Temporarily removes a [`Component`] identified by the provided /// [`ComponentId`] from the provided [`Entity`] and runs the provided /// closure on it, returning the result if the component was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -144,7 +147,7 @@ impl<'w> DeferredWorld<'w> { // - DeferredWorld ensures archetype pointer will remain valid as no // relocations will occur. // - component_id exists on this world and this entity - // - ON_REPLACE is able to accept ZST events + // - REPLACE is able to accept ZST events unsafe { let archetype = &*archetype; self.trigger_on_replace( @@ -156,8 +159,8 @@ impl<'w> DeferredWorld<'w> { ); if archetype.has_replace_observer() { self.trigger_observers( - ON_REPLACE, - entity, + REPLACE, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -184,7 +187,7 @@ impl<'w> DeferredWorld<'w> { // - DeferredWorld ensures archetype pointer will remain valid as no // relocations will occur. // - component_id exists on this world and this entity - // - ON_REPLACE is able to accept ZST events + // - REPLACE is able to accept ZST events unsafe { let archetype = &*archetype; self.trigger_on_insert( @@ -196,8 +199,8 @@ impl<'w> DeferredWorld<'w> { ); if archetype.has_insert_observer() { self.trigger_observers( - ON_INSERT, - entity, + INSERT, + Some(entity), [component_id].into_iter(), MaybeLocation::caller(), ); @@ -450,7 +453,7 @@ impl<'w> DeferredWorld<'w> { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -479,7 +482,7 @@ impl<'w> DeferredWorld<'w> { "Requested non-send resource {} does not exist in the `World`. Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? Non-send resources can also be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -495,34 +498,34 @@ impl<'w> DeferredWorld<'w> { unsafe { self.world.get_non_send_resource_mut() } } - /// Sends an [`Event`]. + /// Sends a [`BufferedEvent`]. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { + pub fn send_event(&mut self, event: E) -> Option> { self.send_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. + /// Sends the default value of the [`BufferedEvent`] of type `E`. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_default(&mut self) -> Option> { + pub fn send_event_default(&mut self) -> Option> { self.send_event(E::default()) } - /// Sends a batch of [`Event`]s from an iterator. + /// Sends a batch of [`BufferedEvent`]s from an iterator. /// This method returns the [IDs](`EventId`) of the sent `events`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_batch( + pub fn send_event_batch( &mut self, events: impl IntoIterator, ) -> Option> { let Some(mut events_resource) = self.get_resource_mut::>() else { log::error!( "Unable to send event `{}`\n\tEvent must be added to the app with `add_event()`\n\thttps://docs.rs/bevy/*/bevy/app/struct.App.html#method.add_event ", - core::any::type_name::() + DebugName::type_name::() ); return None; }; @@ -738,7 +741,7 @@ impl<'w> DeferredWorld<'w> { pub(crate) unsafe fn trigger_observers( &mut self, event: ComponentId, - target: Entity, + target: Option, components: impl Iterator + Clone, caller: MaybeLocation, ) { @@ -746,6 +749,7 @@ impl<'w> DeferredWorld<'w> { self.reborrow(), event, target, + target, components, &mut (), &mut false, @@ -761,7 +765,8 @@ impl<'w> DeferredWorld<'w> { pub(crate) unsafe fn trigger_observers_with_data( &mut self, event: ComponentId, - mut target: Entity, + current_target: Option, + original_target: Option, components: impl Iterator + Clone, data: &mut E, mut propagate: bool, @@ -769,41 +774,64 @@ impl<'w> DeferredWorld<'w> { ) where T: Traversal, { + Observers::invoke::<_>( + self.reborrow(), + event, + current_target, + original_target, + components.clone(), + data, + &mut propagate, + caller, + ); + let Some(mut current_target) = current_target else { + return; + }; + loop { + if !propagate { + return; + } + if let Some(traverse_to) = self + .get_entity(current_target) + .ok() + .and_then(|entity| entity.get_components::()) + .and_then(|item| T::traverse(item, data)) + { + current_target = traverse_to; + } else { + break; + } Observers::invoke::<_>( self.reborrow(), event, - target, + Some(current_target), + original_target, components.clone(), data, &mut propagate, caller, ); - if !propagate { - break; - } - if let Some(traverse_to) = self - .get_entity(target) - .ok() - .and_then(|entity| entity.get_components::()) - .and_then(|item| T::traverse(item, data)) - { - target = traverse_to; - } else { - break; - } } } - /// Sends a "global" [`Trigger`](crate::observer::Trigger) without any targets. + /// Sends a global [`Event`] without any targets. + /// + /// This will run any [`Observer`] of the given [`Event`] that isn't scoped to specific targets. + /// + /// [`Observer`]: crate::observer::Observer pub fn trigger(&mut self, trigger: impl Event) { self.commands().trigger(trigger); } - /// Sends a [`Trigger`](crate::observer::Trigger) with the given `targets`. + /// Sends an [`EntityEvent`] with the given `targets` + /// + /// This will run any [`Observer`] of the given [`EntityEvent`] watching those targets. + /// + /// [`Observer`]: crate::observer::Observer pub fn trigger_targets( &mut self, - trigger: impl Event, + trigger: impl EntityEvent, targets: impl TriggerTargets + Send + Sync + 'static, ) { self.commands().trigger_targets(trigger, targets); diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 1f67865df5..d29c3db428 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -13,16 +13,14 @@ use crate::{ ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, EntityIdLocation, EntityLocation, }, - event::Event, + event::EntityEvent, + lifecycle::{DESPAWN, REMOVE, REPLACE}, observer::Observer, - query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData}, + query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, relationship::RelationshipHookMode, resource::Resource, system::IntoObserverSystem, - world::{ - error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World, - ON_DESPAWN, ON_REMOVE, ON_REPLACE, - }, + world::{error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World}, }; use alloc::vec::Vec; use bevy_platform::collections::{HashMap, HashSet}; @@ -281,14 +279,16 @@ impl<'w> EntityRef<'w> { /// # Panics /// /// If the entity does not have the components required by the query `Q`. - pub fn components(&self) -> Q::Item<'w> { + pub fn components(&self) -> Q::Item<'w, 'static> { self.get_components::() .expect("Query does not match the current entity") } /// Returns read-only components for the current entity that match the query `Q`, /// or `None` if the entity does not have the components required by the query `Q`. - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { // SAFETY: We have read-only access to all components of this entity. unsafe { self.cell.get_components::() } } @@ -548,13 +548,15 @@ impl<'w> EntityMut<'w> { /// # Panics /// /// If the entity does not have the components required by the query `Q`. - pub fn components(&self) -> Q::Item<'_> { + pub fn components(&self) -> Q::Item<'_, 'static> { self.as_readonly().components::() } /// Returns read-only components for the current entity that match the query `Q`, /// or `None` if the entity does not have the components required by the query `Q`. - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { self.as_readonly().get_components::() } @@ -1312,7 +1314,7 @@ impl<'w> EntityWorldMut<'w> { /// If the entity does not have the components required by the query `Q` or if the entity /// has been despawned while this `EntityWorldMut` is still alive. #[inline] - pub fn components(&self) -> Q::Item<'_> { + pub fn components(&self) -> Q::Item<'_, 'static> { self.as_readonly().components::() } @@ -1323,7 +1325,9 @@ impl<'w> EntityWorldMut<'w> { /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. #[inline] - pub fn get_components(&self) -> Option> { + pub fn get_components( + &self, + ) -> Option> { self.as_readonly().get_components::() } @@ -1379,7 +1383,7 @@ impl<'w> EntityWorldMut<'w> { /// Temporarily removes a [`Component`] `T` from this [`Entity`] and runs the /// provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1432,7 +1436,7 @@ impl<'w> EntityWorldMut<'w> { /// Temporarily removes a [`Component`] `T` from this [`Entity`] and runs the /// provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -2370,8 +2374,8 @@ impl<'w> EntityWorldMut<'w> { unsafe { if archetype.has_despawn_observer() { deferred_world.trigger_observers( - ON_DESPAWN, - self.entity, + DESPAWN, + Some(self.entity), archetype.components(), caller, ); @@ -2384,8 +2388,8 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_replace_observer() { deferred_world.trigger_observers( - ON_REPLACE, - self.entity, + REPLACE, + Some(self.entity), archetype.components(), caller, ); @@ -2399,8 +2403,8 @@ impl<'w> EntityWorldMut<'w> { ); if archetype.has_remove_observer() { deferred_world.trigger_observers( - ON_REMOVE, - self.entity, + REMOVE, + Some(self.entity), archetype.components(), caller, ); @@ -2609,14 +2613,14 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn entry<'a, T: Component>(&'a mut self) -> Entry<'w, 'a, T> { + pub fn entry<'a, T: Component>(&'a mut self) -> ComponentEntry<'w, 'a, T> { if self.contains::() { - Entry::Occupied(OccupiedEntry { + ComponentEntry::Occupied(OccupiedComponentEntry { entity_world: self, _marker: PhantomData, }) } else { - Entry::Vacant(VacantEntry { + ComponentEntry::Vacant(VacantComponentEntry { entity_world: self, _marker: PhantomData, }) @@ -2628,7 +2632,7 @@ impl<'w> EntityWorldMut<'w> { /// # Panics /// /// If the entity has been despawned while this `EntityWorldMut` is still alive. - pub fn trigger(&mut self, event: impl Event) -> &mut Self { + pub fn trigger(&mut self, event: impl EntityEvent) -> &mut Self { self.assert_not_despawned(); self.world.trigger_targets(event, self.entity); self.world.flush(); @@ -2645,14 +2649,14 @@ impl<'w> EntityWorldMut<'w> { /// /// Panics if the given system is an exclusive system. #[track_caller] - pub fn observe( + pub fn observe( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { self.observe_with_caller(observer, MaybeLocation::caller()) } - pub(crate) fn observe_with_caller( + pub(crate) fn observe_with_caller( &mut self, observer: impl IntoObserverSystem, caller: MaybeLocation, @@ -2859,14 +2863,14 @@ impl<'w> EntityWorldMut<'w> { /// This `enum` can only be constructed from the [`entry`] method on [`EntityWorldMut`]. /// /// [`entry`]: EntityWorldMut::entry -pub enum Entry<'w, 'a, T: Component> { +pub enum ComponentEntry<'w, 'a, T: Component> { /// An occupied entry. - Occupied(OccupiedEntry<'w, 'a, T>), + Occupied(OccupiedComponentEntry<'w, 'a, T>), /// A vacant entry. - Vacant(VacantEntry<'w, 'a, T>), + Vacant(VacantComponentEntry<'w, 'a, T>), } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { /// Provides in-place mutable access to an occupied entry. /// /// # Examples @@ -2885,17 +2889,17 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { #[inline] pub fn and_modify)>(self, f: F) -> Self { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { f(entry.get_mut()); - Entry::Occupied(entry) + ComponentEntry::Occupied(entry) } - Entry::Vacant(entry) => Entry::Vacant(entry), + ComponentEntry::Vacant(entry) => ComponentEntry::Vacant(entry), } } } -impl<'w, 'a, T: Component> Entry<'w, 'a, T> { - /// Replaces the component of the entry, and returns an [`OccupiedEntry`]. +impl<'w, 'a, T: Component> ComponentEntry<'w, 'a, T> { + /// Replaces the component of the entry, and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// @@ -2914,13 +2918,13 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(entry.get(), &Comp(2)); /// ``` #[inline] - pub fn insert_entry(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert_entry(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(mut entry) => { + ComponentEntry::Occupied(mut entry) => { entry.insert(component); entry } - Entry::Vacant(entry) => entry.insert(component), + ComponentEntry::Vacant(entry) => entry.insert(component), } } @@ -2946,10 +2950,10 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 8); /// ``` #[inline] - pub fn or_insert(self, default: T) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert(self, default: T) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default), } } @@ -2970,15 +2974,15 @@ impl<'w, 'a, T: Component> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 4); /// ``` #[inline] - pub fn or_insert_with T>(self, default: F) -> OccupiedEntry<'w, 'a, T> { + pub fn or_insert_with T>(self, default: F) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(default()), } } } -impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { +impl<'w, 'a, T: Component + Default> ComponentEntry<'w, 'a, T> { /// Ensures the entry has this component by inserting the default value if empty, and /// returns a mutable reference to this component in the entry. /// @@ -2996,42 +3000,42 @@ impl<'w, 'a, T: Component + Default> Entry<'w, 'a, T> { /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 0); /// ``` #[inline] - pub fn or_default(self) -> OccupiedEntry<'w, 'a, T> { + pub fn or_default(self) -> OccupiedComponentEntry<'w, 'a, T> { match self { - Entry::Occupied(entry) => entry, - Entry::Vacant(entry) => entry.insert(Default::default()), + ComponentEntry::Occupied(entry) => entry, + ComponentEntry::Vacant(entry) => entry.insert(Default::default()), } } } -/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. +/// A view into an occupied entry in a [`EntityWorldMut`]. It is part of the [`OccupiedComponentEntry`] enum. /// /// The contained entity must have the component type parameter if we have this struct. -pub struct OccupiedEntry<'w, 'a, T: Component> { +pub struct OccupiedComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a reference to the component in the entry. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.get().0, 5); /// } /// ``` #[inline] pub fn get(&self) -> &T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get::().unwrap() } @@ -3040,14 +3044,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.insert(Comp(10)); /// } /// @@ -3063,14 +3067,14 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// assert_eq!(o.take(), Comp(5)); /// } /// @@ -3078,30 +3082,30 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn take(self) -> T { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.take().unwrap() } } -impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { +impl<'w, 'a, T: Component> OccupiedComponentEntry<'w, 'a, T> { /// Gets a mutable reference to the component in the entry. /// - /// If you need a reference to the `OccupiedEntry` which may outlive the destruction of - /// the `Entry` value, see [`into_mut`]. + /// If you need a reference to the [`OccupiedComponentEntry`] which may outlive the destruction of + /// the [`OccupiedComponentEntry`] value, see [`into_mut`]. /// /// [`into_mut`]: Self::into_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(mut o) = entity.entry::() { + /// if let ComponentEntry::Occupied(mut o) = entity.entry::() { /// o.get_mut().0 += 10; /// assert_eq!(o.get().0, 15); /// @@ -3113,28 +3117,28 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn get_mut(&mut self) -> Mut<'_, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut::().unwrap() } - /// Converts the `OccupiedEntry` into a mutable reference to the value in the entry with + /// Converts the [`OccupiedComponentEntry`] into a mutable reference to the value in the entry with /// a lifetime bound to the `EntityWorldMut`. /// - /// If you need multiple references to the `OccupiedEntry`, see [`get_mut`]. + /// If you need multiple references to the [`OccupiedComponentEntry`], see [`get_mut`]. /// /// [`get_mut`]: Self::get_mut /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn(Comp(5)); /// - /// if let Entry::Occupied(o) = entity.entry::() { + /// if let ComponentEntry::Occupied(o) = entity.entry::() { /// o.into_mut().0 += 10; /// } /// @@ -3142,40 +3146,40 @@ impl<'w, 'a, T: Component> OccupiedEntry<'w, 'a, T> { /// ``` #[inline] pub fn into_mut(self) -> Mut<'a, T> { - // This shouldn't panic because if we have an OccupiedEntry the component must exist. + // This shouldn't panic because if we have an OccupiedComponentEntry the component must exist. self.entity_world.get_mut().unwrap() } } -/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`Entry`] enum. -pub struct VacantEntry<'w, 'a, T: Component> { +/// A view into a vacant entry in a [`EntityWorldMut`]. It is part of the [`ComponentEntry`] enum. +pub struct VacantComponentEntry<'w, 'a, T: Component> { entity_world: &'a mut EntityWorldMut<'w>, _marker: PhantomData, } -impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { - /// Inserts the component into the `VacantEntry` and returns an `OccupiedEntry`. +impl<'w, 'a, T: Component> VacantComponentEntry<'w, 'a, T> { + /// Inserts the component into the [`VacantComponentEntry`] and returns an [`OccupiedComponentEntry`]. /// /// # Examples /// /// ``` - /// # use bevy_ecs::{prelude::*, world::Entry}; + /// # use bevy_ecs::{prelude::*, world::ComponentEntry}; /// #[derive(Component, Default, Clone, Copy, Debug, PartialEq)] /// struct Comp(u32); /// /// # let mut world = World::new(); /// let mut entity = world.spawn_empty(); /// - /// if let Entry::Vacant(v) = entity.entry::() { + /// if let ComponentEntry::Vacant(v) = entity.entry::() { /// v.insert(Comp(10)); /// } /// /// assert_eq!(world.query::<&Comp>().single(&world).unwrap().0, 10); /// ``` #[inline] - pub fn insert(self, component: T) -> OccupiedEntry<'w, 'a, T> { + pub fn insert(self, component: T) -> OccupiedComponentEntry<'w, 'a, T> { self.entity_world.insert(component); - OccupiedEntry { + OccupiedComponentEntry { entity_world: self.entity_world, _marker: PhantomData, } @@ -3186,7 +3190,7 @@ impl<'w, 'a, T: Component> VacantEntry<'w, 'a, T> { /// /// To define the access when used as a [`QueryData`](crate::query::QueryData), /// use a [`QueryBuilder`](crate::query::QueryBuilder) or [`QueryParamBuilder`](crate::system::QueryParamBuilder). -/// The `FilteredEntityRef` must be the entire `QueryData`, and not nested inside a tuple with other data. +/// The [`FilteredEntityRef`] must be the entire [`QueryData`](crate::query::QueryData), and not nested inside a tuple with other data. /// /// ``` /// # use bevy_ecs::{prelude::*, world::FilteredEntityRef}; @@ -4756,7 +4760,8 @@ mod tests { use core::panic::AssertUnwindSafe; use std::sync::OnceLock; - use crate::component::{HookContext, Tick}; + use crate::component::Tick; + use crate::lifecycle::HookContext; use crate::{ change_detection::{MaybeLocation, MutUntyped}, component::ComponentId, @@ -5740,7 +5745,7 @@ mod tests { assert_eq!((&mut X(8), &mut Y(9)), (x_component, y_component)); } - #[derive(Event)] + #[derive(Event, EntityEvent)] struct TestEvent; #[test] @@ -5748,7 +5753,7 @@ mod tests { let mut world = World::new(); let entity = world .spawn_empty() - .observe(|trigger: Trigger, mut commands: Commands| { + .observe(|trigger: On, mut commands: Commands| { commands.entity(trigger.target()).insert(TestComponent(0)); }) .id(); @@ -5758,7 +5763,7 @@ mod tests { let mut a = world.entity_mut(entity); a.trigger(TestEvent); // this adds command to change entity archetype - a.observe(|_: Trigger| {}); // this flushes commands implicitly by spawning + a.observe(|_: On| {}); // this flushes commands implicitly by spawning let location = a.location(); assert_eq!(world.entities().get(entity), Some(location)); } @@ -5767,11 +5772,9 @@ mod tests { #[should_panic] fn location_on_despawned_entity_panics() { let mut world = World::new(); - world.add_observer( - |trigger: Trigger, mut commands: Commands| { - commands.entity(trigger.target()).despawn(); - }, - ); + world.add_observer(|trigger: On, mut commands: Commands| { + commands.entity(trigger.target()).despawn(); + }); let entity = world.spawn_empty().id(); let mut a = world.entity_mut(entity); a.insert(TestComponent(0)); @@ -5789,14 +5792,12 @@ mod tests { fn archetype_modifications_trigger_flush() { let mut world = World::new(); world.insert_resource(TestFlush(0)); - world.add_observer(|_: Trigger, mut commands: Commands| { + world.add_observer(|_: On, mut commands: Commands| { + commands.queue(count_flush); + }); + world.add_observer(|_: On, mut commands: Commands| { commands.queue(count_flush); }); - world.add_observer( - |_: Trigger, mut commands: Commands| { - commands.queue(count_flush); - }, - ); world.commands().queue(count_flush); let entity = world.spawn_empty().id(); assert_eq!(world.resource::().0, 1); @@ -5861,19 +5862,19 @@ mod tests { .push("OrdA hook on_remove"); } - fn ord_a_observer_on_add(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_add(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_add"); } - fn ord_a_observer_on_insert(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_insert(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_insert"); } - fn ord_a_observer_on_replace(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_replace(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_replace"); } - fn ord_a_observer_on_remove(_trigger: Trigger, mut res: ResMut) { + fn ord_a_observer_on_remove(_trigger: On, mut res: ResMut) { res.0.push("OrdA observer on_remove"); } @@ -5912,19 +5913,19 @@ mod tests { .push("OrdB hook on_remove"); } - fn ord_b_observer_on_add(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_add(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_add"); } - fn ord_b_observer_on_insert(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_insert(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_insert"); } - fn ord_b_observer_on_replace(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_replace(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_replace"); } - fn ord_b_observer_on_remove(_trigger: Trigger, mut res: ResMut) { + fn ord_b_observer_on_remove(_trigger: On, mut res: ResMut) { res.0.push("OrdB observer on_remove"); } @@ -6200,10 +6201,10 @@ mod tests { world.insert_resource(Tracker { a: false, b: false }); let entity = world.spawn(A).id(); - world.add_observer(|_: Trigger, mut tracker: ResMut| { + world.add_observer(|_: On, mut tracker: ResMut| { tracker.a = true; }); - world.add_observer(|_: Trigger, mut tracker: ResMut| { + world.add_observer(|_: On, mut tracker: ResMut| { tracker.b = true; }); diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index 3527967942..03574331f2 100644 --- a/crates/bevy_ecs/src/world/error.rs +++ b/crates/bevy_ecs/src/world/error.rs @@ -1,6 +1,7 @@ //! Contains error types returned by bevy's schedule. use alloc::vec::Vec; +use bevy_utils::prelude::DebugName; use crate::{ component::ComponentId, @@ -24,7 +25,7 @@ pub struct TryRunScheduleError(pub InternedScheduleLabel); #[error("Could not insert bundles of type {bundle_type} into the entities with the following IDs because they do not exist: {entities:?}")] pub struct TryInsertBatchError { /// The bundles' type name. - pub bundle_type: &'static str, + pub bundle_type: DebugName, /// The IDs of the provided entities that do not exist. pub entities: Vec, } diff --git a/crates/bevy_ecs/src/world/identifier.rs b/crates/bevy_ecs/src/world/identifier.rs index 6b1c803e75..51f9a0ee2c 100644 --- a/crates/bevy_ecs/src/world/identifier.rs +++ b/crates/bevy_ecs/src/world/identifier.rs @@ -1,5 +1,6 @@ use crate::{ - component::Tick, + component::{ComponentId, Tick}, + query::FilteredAccessSet, storage::SparseSetIndex, system::{ExclusiveSystemParam, ReadOnlySystemParam, SystemMeta, SystemParam}, world::{FromWorld, World}, @@ -53,7 +54,15 @@ unsafe impl SystemParam for WorldId { type Item<'world, 'state> = WorldId; - fn init_state(_: &mut World, _: &mut SystemMeta) -> Self::State {} + fn init_state(_: &mut World) -> Self::State {} + + fn init_access( + _state: &Self::State, + _system_meta: &mut SystemMeta, + _component_access_set: &mut FilteredAccessSet, + _world: &mut World, + ) { + } #[inline] unsafe fn get_param<'world, 'state>( diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 4172f0b31d..fca9091dd5 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1,7 +1,6 @@ //! Defines the [`World`] and APIs for accessing it directly. pub(crate) mod command_queue; -mod component_constants; mod deferred_world; mod entity_fetch; mod entity_ref; @@ -14,18 +13,24 @@ pub mod unsafe_world_cell; #[cfg(feature = "bevy_reflect")] pub mod reflect; -use crate::error::{DefaultErrorHandler, ErrorHandler}; pub use crate::{ change_detection::{Mut, Ref, CHECK_TICK_THRESHOLD}, world::command_queue::CommandQueue, }; +use crate::{ + error::{DefaultErrorHandler, ErrorHandler}, + event::BufferedEvent, + lifecycle::{ComponentHooks, ADD, DESPAWN, INSERT, REMOVE, REPLACE}, + prelude::{Add, Despawn, Insert, Remove, Replace}, +}; pub use bevy_ecs_macros::FromWorld; -pub use component_constants::*; +use bevy_utils::prelude::DebugName; pub use deferred_world::DeferredWorld; pub use entity_fetch::{EntityFetcher, WorldEntityFetch}; pub use entity_ref::{ - DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, EntityWorldMut, - Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, TryFromFilteredError, VacantEntry, + ComponentEntry, DynamicComponentFetch, EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, + EntityWorldMut, FilteredEntityMut, FilteredEntityRef, OccupiedComponentEntry, + TryFromFilteredError, VacantComponentEntry, }; pub use filtered_resource::*; pub use identifier::WorldId; @@ -39,17 +44,17 @@ use crate::{ }, change_detection::{MaybeLocation, MutUntyped, TicksMut}, component::{ - Component, ComponentDescriptor, ComponentHooks, ComponentId, ComponentIds, ComponentInfo, + CheckChangeTicks, Component, ComponentDescriptor, ComponentId, ComponentIds, ComponentInfo, ComponentTicks, Components, ComponentsQueuedRegistrator, ComponentsRegistrator, Mutable, RequiredComponents, RequiredComponentsError, Tick, }, entity::{Entities, Entity, EntityDoesNotExistError}, entity_disabling::DefaultQueryFilters, event::{Event, EventId, Events, SendBatchIds}, + lifecycle::RemovedComponentEvents, observer::Observers, query::{DebugCheckedUnwrap, QueryData, QueryFilter, QueryState}, relationship::RelationshipHookMode, - removal_detection::RemovedComponentEvents, resource::Resource, schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, @@ -147,20 +152,20 @@ impl World { #[inline] fn bootstrap(&mut self) { // The order that we register these events is vital to ensure that the constants are correct! - let on_add = OnAdd::register_component_id(self); - assert_eq!(ON_ADD, on_add); + let on_add = Add::register_component_id(self); + assert_eq!(ADD, on_add); - let on_insert = OnInsert::register_component_id(self); - assert_eq!(ON_INSERT, on_insert); + let on_insert = Insert::register_component_id(self); + assert_eq!(INSERT, on_insert); - let on_replace = OnReplace::register_component_id(self); - assert_eq!(ON_REPLACE, on_replace); + let on_replace = Replace::register_component_id(self); + assert_eq!(REPLACE, on_replace); - let on_remove = OnRemove::register_component_id(self); - assert_eq!(ON_REMOVE, on_remove); + let on_remove = Remove::register_component_id(self); + assert_eq!(REMOVE, on_remove); - let on_despawn = OnDespawn::register_component_id(self); - assert_eq!(ON_DESPAWN, on_despawn); + let on_despawn = Despawn::register_component_id(self); + assert_eq!(DESPAWN, on_despawn); // This sets up `Disabled` as a disabling component, via the FromWorld impl self.init_resource::(); @@ -257,6 +262,12 @@ impl World { &self.removed_components } + /// Retrieves this world's [`Observers`] list + #[inline] + pub fn observers(&self) -> &Observers { + &self.observers + } + /// Creates a new [`Commands`] instance that writes to the world's command queue /// Use [`World::flush`] to apply all queued commands #[inline] @@ -1270,7 +1281,7 @@ impl World { /// Temporarily removes a [`Component`] `T` from the provided [`Entity`] and /// runs the provided closure on it, returning the result if `T` was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1316,7 +1327,7 @@ impl World { /// Temporarily removes a [`Component`] identified by the provided /// [`ComponentId`] from the provided [`Entity`] and runs the provided /// closure on it, returning the result if the component was available. - /// This will trigger the `OnRemove` and `OnReplace` component hooks without + /// This will trigger the `Remove` and `Replace` component hooks without /// causing an archetype move. /// /// This is most useful with immutable components, where removal and reinsertion @@ -1444,7 +1455,7 @@ impl World { /// assert!(!transform.is_changed()); /// ``` /// - /// [`RemovedComponents`]: crate::removal_detection::RemovedComponents + /// [`RemovedComponents`]: crate::lifecycle::RemovedComponents pub fn clear_trackers(&mut self) { self.removed_components.update(); self.last_change_tick = self.increment_change_tick(); @@ -1938,7 +1949,7 @@ impl World { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -1962,7 +1973,7 @@ impl World { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -1986,7 +1997,7 @@ impl World { Did you forget to add it using `app.insert_resource` / `app.init_resource`? Resources are also implicitly added via `app.add_event`, and can be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2150,7 +2161,7 @@ impl World { "Requested non-send resource {} does not exist in the `World`. Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? Non-send resources can also be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2172,7 +2183,7 @@ impl World { "Requested non-send resource {} does not exist in the `World`. Did you forget to add it using `app.insert_non_send_resource` / `app.init_non_send_resource`? Non-send resources can also be added by plugins.", - core::any::type_name::() + DebugName::type_name::() ), } } @@ -2339,11 +2350,11 @@ impl World { ) }; } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {entity}, which {}. See: https://bevy.org/learn/errors/b0003", core::any::type_name::(), self.entities.entity_does_not_exist_error_details(entity)); + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {entity}, which {}. See: https://bevy.org/learn/errors/b0003", DebugName::type_name::(), self.entities.entity_does_not_exist_error_details(entity)); } } } else { - panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {first_entity}, which {}. See: https://bevy.org/learn/errors/b0003", core::any::type_name::(), self.entities.entity_does_not_exist_error_details(first_entity)); + panic!("error[B0003]: Could not insert a bundle (of type `{}`) for entity {first_entity}, which {}. See: https://bevy.org/learn/errors/b0003", DebugName::type_name::(), self.entities.entity_does_not_exist_error_details(first_entity)); } } } @@ -2507,7 +2518,7 @@ impl World { Ok(()) } else { Err(TryInsertBatchError { - bundle_type: core::any::type_name::(), + bundle_type: DebugName::type_name::(), entities: invalid_entities, }) } @@ -2541,7 +2552,7 @@ impl World { #[track_caller] pub fn resource_scope(&mut self, f: impl FnOnce(&mut World, Mut) -> U) -> U { self.try_resource_scope(f) - .unwrap_or_else(|| panic!("resource does not exist: {}", core::any::type_name::())) + .unwrap_or_else(|| panic!("resource does not exist: {}", DebugName::type_name::())) } /// Temporarily removes the requested resource from this [`World`] if it exists, runs custom user code, @@ -2581,7 +2592,7 @@ impl World { assert!(!self.contains_resource::(), "Resource `{}` was inserted during a call to World::resource_scope.\n\ This is not allowed as the original resource is reinserted to the world after the closure is invoked.", - core::any::type_name::()); + DebugName::type_name::()); OwningPtr::make(value, |ptr| { // SAFETY: pointer is of type R @@ -2595,34 +2606,34 @@ impl World { Some(result) } - /// Sends an [`Event`]. + /// Sends a [`BufferedEvent`]. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event(&mut self, event: E) -> Option> { + pub fn send_event(&mut self, event: E) -> Option> { self.send_event_batch(core::iter::once(event))?.next() } - /// Sends the default value of the [`Event`] of type `E`. + /// Sends the default value of the [`BufferedEvent`] of type `E`. /// This method returns the [ID](`EventId`) of the sent `event`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_default(&mut self) -> Option> { + pub fn send_event_default(&mut self) -> Option> { self.send_event(E::default()) } - /// Sends a batch of [`Event`]s from an iterator. + /// Sends a batch of [`BufferedEvent`]s from an iterator. /// This method returns the [IDs](`EventId`) of the sent `events`, /// or [`None`] if the `event` could not be sent. #[inline] - pub fn send_event_batch( + pub fn send_event_batch( &mut self, events: impl IntoIterator, ) -> Option> { let Some(mut events_resource) = self.get_resource_mut::>() else { log::error!( "Unable to send event `{}`\n\tEvent must be added to the app with `add_event()`\n\thttps://docs.rs/bevy/*/bevy/app/struct.App.html#method.add_event ", - core::any::type_name::() + DebugName::type_name::() ); return None; }; @@ -2934,17 +2945,21 @@ impl World { } /// Iterates all component change ticks and clamps any older than [`MAX_CHANGE_AGE`](crate::change_detection::MAX_CHANGE_AGE). - /// This prevents overflow and thus prevents false positives. + /// This also triggers [`CheckChangeTicks`] observers and returns the same event here. /// - /// **Note:** Does nothing if the [`World`] counter has not been incremented at least [`CHECK_TICK_THRESHOLD`] + /// Calling this method prevents [`Tick`]s overflowing and thus prevents false positives when comparing them. + /// + /// **Note:** Does nothing and returns `None` if the [`World`] counter has not been incremented at least [`CHECK_TICK_THRESHOLD`] /// times since the previous pass. // TODO: benchmark and optimize - pub fn check_change_ticks(&mut self) { + pub fn check_change_ticks(&mut self) -> Option { let change_tick = self.change_tick(); if change_tick.relative_to(self.last_check_tick).get() < CHECK_TICK_THRESHOLD { - return; + return None; } + let check = CheckChangeTicks(change_tick); + let Storages { ref mut tables, ref mut sparse_sets, @@ -2954,17 +2969,22 @@ impl World { #[cfg(feature = "trace")] let _span = tracing::info_span!("check component ticks").entered(); - tables.check_change_ticks(change_tick); - sparse_sets.check_change_ticks(change_tick); - resources.check_change_ticks(change_tick); - non_send_resources.check_change_ticks(change_tick); - self.entities.check_change_ticks(change_tick); + tables.check_change_ticks(check); + sparse_sets.check_change_ticks(check); + resources.check_change_ticks(check); + non_send_resources.check_change_ticks(check); + self.entities.check_change_ticks(check); if let Some(mut schedules) = self.get_resource_mut::() { - schedules.check_change_ticks(change_tick); + schedules.check_change_ticks(check); } + self.trigger(check); + self.flush(); + self.last_check_tick = change_tick; + + Some(check) } /// Runs both [`clear_entities`](Self::clear_entities) and [`clear_resources`](Self::clear_resources), @@ -3614,6 +3634,7 @@ mod tests { }; use bevy_ecs_macros::Component; use bevy_platform::collections::{HashMap, HashSet}; + use bevy_utils::prelude::DebugName; use core::{ any::TypeId, panic, @@ -3797,12 +3818,12 @@ mod tests { let mut iter = world.iter_resources(); let (info, ptr) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource` assert_eq!(unsafe { ptr.deref::().0 }, 42); let (info, ptr) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); assert_eq!( // SAFETY: We know that the resource is of type `TestResource2` unsafe { &ptr.deref::().0 }, @@ -3825,14 +3846,14 @@ mod tests { let mut iter = world.iter_resources_mut(); let (info, mut mut_untyped) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource` unsafe { mut_untyped.as_mut().deref_mut::().0 = 43; }; let (info, mut mut_untyped) = iter.next().unwrap(); - assert_eq!(info.name(), core::any::type_name::()); + assert_eq!(info.name(), DebugName::type_name::()); // SAFETY: We know that the resource is of type `TestResource2` unsafe { mut_untyped.as_mut().deref_mut::().0 = "Hello, world?".to_string(); diff --git a/crates/bevy_ecs/src/world/reflect.rs b/crates/bevy_ecs/src/world/reflect.rs index 5ecdf88156..aada63bf61 100644 --- a/crates/bevy_ecs/src/world/reflect.rs +++ b/crates/bevy_ecs/src/world/reflect.rs @@ -4,8 +4,8 @@ use core::any::TypeId; use thiserror::Error; -use alloc::string::{String, ToString}; use bevy_reflect::{Reflect, ReflectFromPtr}; +use bevy_utils::prelude::DebugName; use crate::{prelude::*, world::ComponentId}; @@ -77,10 +77,7 @@ impl World { }; let Some(comp_ptr) = self.get_by_id(entity, component_id) else { - let component_name = self - .components() - .get_name(component_id) - .map(|name| name.to_string()); + let component_name = self.components().get_name(component_id); return Err(GetComponentReflectError::EntityDoesNotHaveComponent { entity, @@ -166,10 +163,7 @@ impl World { // HACK: Only required for the `None`-case/`else`-branch, but it borrows `self`, which will // already be mutably borrowed by `self.get_mut_by_id()`, and I didn't find a way around it. - let component_name = self - .components() - .get_name(component_id) - .map(|name| name.to_string()); + let component_name = self.components().get_name(component_id).clone(); let Some(comp_mut_untyped) = self.get_mut_by_id(entity, component_id) else { return Err(GetComponentReflectError::EntityDoesNotHaveComponent { @@ -223,7 +217,7 @@ pub enum GetComponentReflectError { component_id: ComponentId, /// The name corresponding to the [`Component`] with the given [`TypeId`], or `None` /// if not available. - component_name: Option, + component_name: Option, }, /// The [`World`] was missing the [`AppTypeRegistry`] resource. diff --git a/crates/bevy_ecs/src/world/unsafe_world_cell.rs b/crates/bevy_ecs/src/world/unsafe_world_cell.rs index 1959f3e5f3..38d4333843 100644 --- a/crates/bevy_ecs/src/world/unsafe_world_cell.rs +++ b/crates/bevy_ecs/src/world/unsafe_world_cell.rs @@ -8,10 +8,10 @@ use crate::{ component::{ComponentId, ComponentTicks, Components, Mutable, StorageType, Tick, TickCells}, entity::{ContainsEntity, Entities, Entity, EntityDoesNotExistError, EntityLocation}, error::{DefaultErrorHandler, ErrorHandler}, + lifecycle::RemovedComponentEvents, observer::Observers, prelude::Component, - query::{DebugCheckedUnwrap, ReadOnlyQueryData}, - removal_detection::RemovedComponentEvents, + query::{DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, resource::Resource, storage::{ComponentSparseSet, Storages, Table}, world::RawCommandQueue, @@ -36,7 +36,7 @@ use thiserror::Error; /// /// This alone is not enough to implement bevy systems where multiple systems can access *disjoint* parts of the world concurrently. For this, bevy stores all values of /// resources and components (and [`ComponentTicks`]) in [`UnsafeCell`]s, and carefully validates disjoint access patterns using -/// APIs like [`System::component_access`](crate::system::System::component_access). +/// APIs like [`System::initialize`](crate::system::System::initialize). /// /// A system then can be executed using [`System::run_unsafe`](crate::system::System::run_unsafe) with a `&World` and use methods with interior mutability to access resource values. /// @@ -998,7 +998,9 @@ impl<'w> UnsafeEntityCell<'w> { /// It is the caller's responsibility to ensure that /// - the [`UnsafeEntityCell`] has permission to access the queried data immutably /// - no mutable references to the queried data exist at the same time - pub(crate) unsafe fn get_components(&self) -> Option> { + pub(crate) unsafe fn get_components( + &self, + ) -> Option> { // SAFETY: World is only used to access query data and initialize query state let state = unsafe { let world = self.world().world(); @@ -1028,7 +1030,8 @@ impl<'w> UnsafeEntityCell<'w> { // Table corresponds to archetype. State is the same state used to init fetch above. unsafe { Q::set_archetype(&mut fetch, &state, archetype, table) } // SAFETY: Called after set_archetype above. Entity and location are guaranteed to exist. - unsafe { Some(Q::fetch(&mut fetch, self.id(), location.table_row)) } + let item = unsafe { Q::fetch(&state, &mut fetch, self.id(), location.table_row) }; + Some(Q::release_state(item)) } else { None } diff --git a/crates/bevy_gilrs/src/lib.rs b/crates/bevy_gilrs/src/lib.rs index db1b404abc..9b86edf03a 100644 --- a/crates/bevy_gilrs/src/lib.rs +++ b/crates/bevy_gilrs/src/lib.rs @@ -39,7 +39,7 @@ thread_local! { /// `NonSendMut` parameter, which told Bevy that the system was `!Send`, but now with the removal of `!Send` /// resource/system parameter usage, there is no internal guarantee that the system will run in only one thread, so /// we need to rely on the platform to make such a guarantee. - static GILRS: RefCell> = const { RefCell::new(None) }; + pub static GILRS: RefCell> = const { RefCell::new(None) }; } #[derive(Resource)] diff --git a/crates/bevy_gizmos/src/gizmos.rs b/crates/bevy_gizmos/src/gizmos.rs index 87af7c4925..e0f139a1a8 100644 --- a/crates/bevy_gizmos/src/gizmos.rs +++ b/crates/bevy_gizmos/src/gizmos.rs @@ -9,7 +9,8 @@ use core::{ use bevy_color::{Color, LinearRgba}; use bevy_ecs::{ - component::Tick, + component::{ComponentId, Tick}, + query::FilteredAccessSet, resource::Resource, system::{ Deferred, ReadOnlySystemParam, Res, SystemBuffer, SystemMeta, SystemParam, @@ -199,12 +200,26 @@ where type State = GizmosFetchState; type Item<'w, 's> = Gizmos<'w, 's, Config, Clear>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { GizmosFetchState { - state: GizmosState::::init_state(world, system_meta), + state: GizmosState::::init_state(world), } } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + GizmosState::::init_access( + &state.state, + system_meta, + component_access_set, + world, + ); + } + fn apply(state: &mut Self::State, system_meta: &SystemMeta, world: &mut World) { GizmosState::::apply(&mut state.state, system_meta, world); } diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index de48d94e42..5804729e06 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -631,8 +631,8 @@ impl RenderCommand

for SetLineGizmoBindGroup #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - uniform_index: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + uniform_index: Option>, bind_group: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -662,8 +662,8 @@ impl RenderCommand

for DrawLineGizmo #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - config: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + config: Option>, line_gizmos: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -725,8 +725,8 @@ impl RenderCommand

for DrawLineJointGizmo { #[inline] fn render<'w>( _item: &P, - _view: ROQueryItem<'w, Self::ViewQuery>, - config: Option>, + _view: ROQueryItem<'w, '_, Self::ViewQuery>, + config: Option>, line_gizmos: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { diff --git a/crates/bevy_gltf/src/convert_coordinates.rs b/crates/bevy_gltf/src/convert_coordinates.rs new file mode 100644 index 0000000000..4148cecd9a --- /dev/null +++ b/crates/bevy_gltf/src/convert_coordinates.rs @@ -0,0 +1,80 @@ +use core::f32::consts::PI; + +use bevy_math::{Mat4, Quat, Vec3}; +use bevy_transform::components::Transform; + +pub(crate) trait ConvertCoordinates { + /// Converts the glTF coordinates to Bevy's coordinate system. + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + /// + /// See + fn convert_coordinates(self) -> Self; +} + +pub(crate) trait ConvertCameraCoordinates { + /// Like `convert_coordinates`, but uses the following for the lens rotation: + /// - forward: -Z + /// - up: Y + /// - right: X + /// + /// See + fn convert_camera_coordinates(self) -> Self; +} + +impl ConvertCoordinates for Vec3 { + fn convert_coordinates(self) -> Self { + Vec3::new(-self.x, self.y, -self.z) + } +} + +impl ConvertCoordinates for [f32; 3] { + fn convert_coordinates(self) -> Self { + [-self[0], self[1], -self[2]] + } +} + +impl ConvertCoordinates for [f32; 4] { + fn convert_coordinates(self) -> Self { + // Solution of q' = r q r* + [-self[0], self[1], -self[2], self[3]] + } +} + +impl ConvertCoordinates for Quat { + fn convert_coordinates(self) -> Self { + // Solution of q' = r q r* + Quat::from_array([-self.x, self.y, -self.z, self.w]) + } +} + +impl ConvertCoordinates for Mat4 { + fn convert_coordinates(self) -> Self { + let m: Mat4 = Mat4::from_scale(Vec3::new(-1.0, 1.0, -1.0)); + // Same as the original matrix + let m_inv = m; + m_inv * self * m + } +} + +impl ConvertCoordinates for Transform { + fn convert_coordinates(mut self) -> Self { + self.translation = self.translation.convert_coordinates(); + self.rotation = self.rotation.convert_coordinates(); + self + } +} + +impl ConvertCameraCoordinates for Transform { + fn convert_camera_coordinates(mut self) -> Self { + self.translation = self.translation.convert_coordinates(); + self.rotate_y(PI); + self + } +} diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 97600f3ed0..4262d43eb7 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -91,6 +91,7 @@ //! You can use [`GltfAssetLabel`] to ensure you are using the correct label. mod assets; +mod convert_coordinates; mod label; mod loader; mod vertex_attributes; diff --git a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs index 83e6778b99..3fce51d527 100644 --- a/crates/bevy_gltf/src/loader/gltf_ext/scene.rs +++ b/crates/bevy_gltf/src/loader/gltf_ext/scene.rs @@ -10,7 +10,10 @@ use itertools::Itertools; #[cfg(feature = "bevy_animation")] use bevy_platform::collections::{HashMap, HashSet}; -use crate::GltfError; +use crate::{ + convert_coordinates::{ConvertCameraCoordinates as _, ConvertCoordinates as _}, + GltfError, +}; pub(crate) fn node_name(node: &Node) -> Name { let name = node @@ -26,8 +29,8 @@ pub(crate) fn node_name(node: &Node) -> Name { /// on [`Node::transform()`](gltf::Node::transform) directly because it uses optimized glam types and /// if `libm` feature of `bevy_math` crate is enabled also handles cross /// platform determinism properly. -pub(crate) fn node_transform(node: &Node) -> Transform { - match node.transform() { +pub(crate) fn node_transform(node: &Node, convert_coordinates: bool) -> Transform { + let transform = match node.transform() { gltf::scene::Transform::Matrix { matrix } => { Transform::from_matrix(Mat4::from_cols_array_2d(&matrix)) } @@ -40,6 +43,15 @@ pub(crate) fn node_transform(node: &Node) -> Transform { rotation: bevy_math::Quat::from_array(rotation), scale: Vec3::from(scale), }, + }; + if convert_coordinates { + if node.camera().is_some() { + transform.convert_camera_coordinates() + } else { + transform.convert_coordinates() + } + } else { + transform } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index abc555002a..5e0f752e50 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -84,6 +84,7 @@ use self::{ texture::{texture_handle, texture_sampler, texture_transform_to_affine2}, }, }; +use crate::convert_coordinates::ConvertCoordinates as _; /// An error that occurs when loading a glTF file. #[derive(Error, Debug)] @@ -191,6 +192,16 @@ pub struct GltfLoaderSettings { pub default_sampler: Option, /// If true, the loader will ignore sampler data from gltf and use the default sampler. pub override_sampler: bool, + /// If true, the loader will convert glTF coordinates to Bevy's coordinate system. + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + pub convert_coordinates: bool, } impl Default for GltfLoaderSettings { @@ -203,6 +214,7 @@ impl Default for GltfLoaderSettings { include_source: false, default_sampler: None, override_sampler: false, + convert_coordinates: false, } } } @@ -303,7 +315,16 @@ async fn load_gltf<'a, 'b, 'c>( match outputs { ReadOutputs::Translations(tr) => { let translation_property = animated_field!(Transform::translation); - let translations: Vec = tr.map(Vec3::from).collect(); + let translations: Vec = tr + .map(Vec3::from) + .map(|verts| { + if settings.convert_coordinates { + Vec3::convert_coordinates(verts) + } else { + verts + } + }) + .collect(); if keyframe_timestamps.len() == 1 { Some(VariableCurve::new(AnimatableCurve::new( translation_property, @@ -350,8 +371,17 @@ async fn load_gltf<'a, 'b, 'c>( } ReadOutputs::Rotations(rots) => { let rotation_property = animated_field!(Transform::rotation); - let rotations: Vec = - rots.into_f32().map(Quat::from_array).collect(); + let rotations: Vec = rots + .into_f32() + .map(Quat::from_array) + .map(|quat| { + if settings.convert_coordinates { + Quat::convert_coordinates(quat) + } else { + quat + } + }) + .collect(); if keyframe_timestamps.len() == 1 { Some(VariableCurve::new(AnimatableCurve::new( rotation_property, @@ -633,6 +663,7 @@ async fn load_gltf<'a, 'b, 'c>( accessor, &buffer_data, &loader.custom_vertex_attributes, + settings.convert_coordinates, ) { Ok((attribute, values)) => mesh.insert_attribute(attribute, values), Err(err) => warn!("{}", err), @@ -752,7 +783,17 @@ async fn load_gltf<'a, 'b, 'c>( let reader = gltf_skin.reader(|buffer| Some(&buffer_data[buffer.index()])); let local_to_bone_bind_matrices: Vec = reader .read_inverse_bind_matrices() - .map(|mats| mats.map(|mat| Mat4::from_cols_array_2d(&mat)).collect()) + .map(|mats| { + mats.map(|mat| Mat4::from_cols_array_2d(&mat)) + .map(|mat| { + if settings.convert_coordinates { + mat.convert_coordinates() + } else { + mat + } + }) + .collect() + }) .unwrap_or_else(|| { core::iter::repeat_n(Mat4::IDENTITY, gltf_skin.joints().len()).collect() }); @@ -834,7 +875,7 @@ async fn load_gltf<'a, 'b, 'c>( &node, children, mesh, - node_transform(&node), + node_transform(&node, settings.convert_coordinates), skin, node.extras().as_deref().map(GltfExtras::from), ); @@ -1306,7 +1347,7 @@ fn load_node( document: &Document, ) -> Result<(), GltfError> { let mut gltf_error = None; - let transform = node_transform(gltf_node); + let transform = node_transform(gltf_node, settings.convert_coordinates); let world_transform = *parent_transform * transform; // according to https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#instantiation, // if the determinant of the transform is negative we must invert the winding order of @@ -1359,7 +1400,6 @@ fn load_node( }, ..OrthographicProjection::default_3d() }; - Projection::Orthographic(orthographic_projection) } gltf::camera::Projection::Perspective(perspective) => { @@ -1377,6 +1417,7 @@ fn load_node( Projection::Perspective(perspective_projection) } }; + node.insert(( Camera3d::default(), projection, diff --git a/crates/bevy_gltf/src/vertex_attributes.rs b/crates/bevy_gltf/src/vertex_attributes.rs index d4ae811c90..2620d608a0 100644 --- a/crates/bevy_gltf/src/vertex_attributes.rs +++ b/crates/bevy_gltf/src/vertex_attributes.rs @@ -6,6 +6,8 @@ use gltf::{ }; use thiserror::Error; +use crate::convert_coordinates::ConvertCoordinates; + /// Represents whether integer data requires normalization #[derive(Copy, Clone)] struct Normalization(bool); @@ -132,15 +134,23 @@ impl<'a> VertexAttributeIter<'a> { } /// Materializes values for any supported format of vertex attribute - fn into_any_values(self) -> Result { + fn into_any_values(self, convert_coordinates: bool) -> Result { match self { VertexAttributeIter::F32(it) => Ok(Values::Float32(it.collect())), VertexAttributeIter::U32(it) => Ok(Values::Uint32(it.collect())), VertexAttributeIter::F32x2(it) => Ok(Values::Float32x2(it.collect())), VertexAttributeIter::U32x2(it) => Ok(Values::Uint32x2(it.collect())), - VertexAttributeIter::F32x3(it) => Ok(Values::Float32x3(it.collect())), + VertexAttributeIter::F32x3(it) => Ok(if convert_coordinates { + Values::Float32x3(it.map(ConvertCoordinates::convert_coordinates).collect()) + } else { + Values::Float32x3(it.collect()) + }), VertexAttributeIter::U32x3(it) => Ok(Values::Uint32x3(it.collect())), - VertexAttributeIter::F32x4(it) => Ok(Values::Float32x4(it.collect())), + VertexAttributeIter::F32x4(it) => Ok(if convert_coordinates { + Values::Float32x4(it.map(ConvertCoordinates::convert_coordinates).collect()) + } else { + Values::Float32x4(it.collect()) + }), VertexAttributeIter::U32x4(it) => Ok(Values::Uint32x4(it.collect())), VertexAttributeIter::S16x2(it, n) => { Ok(n.apply_either(it.collect(), Values::Snorm16x2, Values::Sint16x2)) @@ -188,7 +198,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x4(it, Normalization(true)) => Ok(Values::Float32x4( ReadColors::RgbaU16(it).into_rgba_f32().collect(), )), - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -198,7 +208,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U8x4(it, Normalization(false)) => { Ok(Values::Uint16x4(ReadJoints::U8(it).into_u16().collect())) } - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -211,7 +221,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x4(it, Normalization(true)) => { Ok(Values::Float32x4(ReadWeights::U16(it).into_f32().collect())) } - s => s.into_any_values(), + s => s.into_any_values(false), } } @@ -224,7 +234,7 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::U16x2(it, Normalization(true)) => Ok(Values::Float32x2( ReadTexCoords::U16(it).into_f32().collect(), )), - s => s.into_any_values(), + s => s.into_any_values(false), } } } @@ -252,28 +262,49 @@ pub(crate) fn convert_attribute( accessor: gltf::Accessor, buffer_data: &Vec>, custom_vertex_attributes: &HashMap, MeshVertexAttribute>, + convert_coordinates: bool, ) -> Result<(MeshVertexAttribute, Values), ConvertAttributeError> { - if let Some((attribute, conversion)) = match &semantic { - gltf::Semantic::Positions => Some((Mesh::ATTRIBUTE_POSITION, ConversionMode::Any)), - gltf::Semantic::Normals => Some((Mesh::ATTRIBUTE_NORMAL, ConversionMode::Any)), - gltf::Semantic::Tangents => Some((Mesh::ATTRIBUTE_TANGENT, ConversionMode::Any)), - gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba)), - gltf::Semantic::TexCoords(0) => Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord)), - gltf::Semantic::TexCoords(1) => Some((Mesh::ATTRIBUTE_UV_1, ConversionMode::TexCoord)), - gltf::Semantic::Joints(0) => { - Some((Mesh::ATTRIBUTE_JOINT_INDEX, ConversionMode::JointIndex)) + if let Some((attribute, conversion, convert_coordinates)) = match &semantic { + gltf::Semantic::Positions => Some(( + Mesh::ATTRIBUTE_POSITION, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Normals => Some(( + Mesh::ATTRIBUTE_NORMAL, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Tangents => Some(( + Mesh::ATTRIBUTE_TANGENT, + ConversionMode::Any, + convert_coordinates, + )), + gltf::Semantic::Colors(0) => Some((Mesh::ATTRIBUTE_COLOR, ConversionMode::Rgba, false)), + gltf::Semantic::TexCoords(0) => { + Some((Mesh::ATTRIBUTE_UV_0, ConversionMode::TexCoord, false)) } - gltf::Semantic::Weights(0) => { - Some((Mesh::ATTRIBUTE_JOINT_WEIGHT, ConversionMode::JointWeight)) + gltf::Semantic::TexCoords(1) => { + Some((Mesh::ATTRIBUTE_UV_1, ConversionMode::TexCoord, false)) } + gltf::Semantic::Joints(0) => Some(( + Mesh::ATTRIBUTE_JOINT_INDEX, + ConversionMode::JointIndex, + false, + )), + gltf::Semantic::Weights(0) => Some(( + Mesh::ATTRIBUTE_JOINT_WEIGHT, + ConversionMode::JointWeight, + false, + )), gltf::Semantic::Extras(name) => custom_vertex_attributes .get(name.as_str()) - .map(|attr| (*attr, ConversionMode::Any)), + .map(|attr| (*attr, ConversionMode::Any, false)), _ => None, } { let raw_iter = VertexAttributeIter::from_accessor(accessor.clone(), buffer_data); let converted_values = raw_iter.and_then(|iter| match conversion { - ConversionMode::Any => iter.into_any_values(), + ConversionMode::Any => iter.into_any_values(convert_coordinates), ConversionMode::Rgba => iter.into_rgba_values(), ConversionMode::TexCoord => iter.into_tex_coord_values(), ConversionMode::JointIndex => iter.into_joint_index_values(), diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index 0ef1213b46..fe086db674 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -81,19 +81,35 @@ impl ImageLoader { } } +/// How to determine an image's format when loading. #[derive(Serialize, Deserialize, Default, Debug, Clone)] pub enum ImageFormatSetting { + /// Determine the image format from its file extension. + /// + /// This is the default. #[default] FromExtension, + /// Declare the image format explicitly. Format(ImageFormat), + /// Guess the image format by looking for magic bytes at the + /// beginning of its data. Guess, } +/// Settings for loading an [`Image`] using an [`ImageLoader`]. #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ImageLoaderSettings { + /// How to determine the image's format. pub format: ImageFormatSetting, + /// Specifies whether image data is linear + /// or in sRGB space when this is not determined by + /// the image format. pub is_srgb: bool, + /// [`ImageSampler`] to use when rendering - this does + /// not affect the loading of the image data. pub sampler: ImageSampler, + /// Where the asset will be used - see the docs on + /// [`RenderAssetUsages`] for details. pub asset_usage: RenderAssetUsages, } @@ -108,11 +124,14 @@ impl Default for ImageLoaderSettings { } } +/// An error when loading an image using [`ImageLoader`]. #[non_exhaustive] #[derive(Debug, Error)] pub enum ImageLoaderError { - #[error("Could load shader: {0}")] + /// An error occurred while trying to load the image bytes. + #[error("Failed to load image bytes: {0}")] Io(#[from] std::io::Error), + /// An error occurred while trying to decode the image bytes. #[error("Could not load texture file: {0}")] FileTexture(#[from] FileTextureError), } @@ -170,7 +189,7 @@ impl AssetLoader for ImageLoader { /// An error that occurs when loading a texture from a file. #[derive(Error, Debug)] -#[error("Error reading image file {path}: {error}, this is an error in `bevy_render`.")] +#[error("Error reading image file {path}: {error}.")] pub struct FileTextureError { error: TextureError, path: String, diff --git a/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index 58ab62aefa..e4ff47f470 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -122,7 +122,7 @@ use { /// [`DetectChangesMut::bypass_change_detection`]: bevy_ecs::change_detection::DetectChangesMut::bypass_change_detection #[derive(Debug, Clone, Resource)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default, Resource))] -pub struct ButtonInput { +pub struct ButtonInput { /// A collection of every button that is currently being pressed. pressed: HashSet, /// A collection of every button that has just been pressed. @@ -131,7 +131,7 @@ pub struct ButtonInput { just_released: HashSet, } -impl Default for ButtonInput { +impl Default for ButtonInput { fn default() -> Self { Self { pressed: Default::default(), @@ -143,12 +143,12 @@ impl Default for ButtonInput { impl ButtonInput where - T: Copy + Eq + Hash + Send + Sync + 'static, + T: Clone + Eq + Hash + Send + Sync + 'static, { /// Registers a press for the given `input`. pub fn press(&mut self, input: T) { // Returns `true` if the `input` wasn't pressed. - if self.pressed.insert(input) { + if self.pressed.insert(input.clone()) { self.just_pressed.insert(input); } } diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index 2b0148909c..7d2f551201 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -10,7 +10,7 @@ use bevy_ecs::{ change_detection::DetectChangesMut, component::Component, entity::Entity, - event::{Event, EventReader, EventWriter}, + event::{BufferedEvent, Event, EventReader, EventWriter}, name::Name, system::{Commands, Query}, }; @@ -32,7 +32,7 @@ use thiserror::Error; /// the in-frame relative ordering of events is important. /// /// This event is produced by `bevy_input`. -#[derive(Event, Debug, Clone, PartialEq, From)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, From)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -59,7 +59,7 @@ pub enum GamepadEvent { /// the in-frame relative ordering of events is important. /// /// This event type is used by `bevy_input` to feed its components. -#[derive(Event, Debug, Clone, PartialEq, From)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, From)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -80,7 +80,7 @@ pub enum RawGamepadEvent { } /// [`GamepadButton`] changed event unfiltered by [`GamepadSettings`]. -#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Copy, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -112,7 +112,7 @@ impl RawGamepadButtonChangedEvent { } /// [`GamepadAxis`] changed event unfiltered by [`GamepadSettings`]. -#[derive(Event, Debug, Copy, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Copy, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -143,9 +143,9 @@ impl RawGamepadAxisChangedEvent { } } -/// A Gamepad connection event. Created when a connection to a gamepad +/// A [`Gamepad`] connection event. Created when a connection to a gamepad /// is established and when a gamepad is disconnected. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -184,7 +184,7 @@ impl GamepadConnectionEvent { } /// [`GamepadButton`] event triggered by a digital state change. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -216,7 +216,7 @@ impl GamepadButtonStateChangedEvent { } /// [`GamepadButton`] event triggered by an analog state change. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -251,7 +251,7 @@ impl GamepadButtonChangedEvent { } /// [`GamepadAxis`] event triggered by an analog state change. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( feature = "bevy_reflect", @@ -1774,7 +1774,7 @@ impl GamepadRumbleIntensity { #[doc(alias = "force feedback")] #[doc(alias = "vibration")] #[doc(alias = "vibrate")] -#[derive(Event, Clone)] +#[derive(Event, BufferedEvent, Clone)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Clone))] pub enum GamepadRumbleRequest { /// Add a rumble to the given gamepad. diff --git a/crates/bevy_input/src/gestures.rs b/crates/bevy_input/src/gestures.rs index 5cd14d4634..9daa21d525 100644 --- a/crates/bevy_input/src/gestures.rs +++ b/crates/bevy_input/src/gestures.rs @@ -1,6 +1,6 @@ //! Gestures functionality, from touchscreens and touchpads. -use bevy_ecs::event::Event; +use bevy_ecs::event::{BufferedEvent, Event}; use bevy_math::Vec2; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; @@ -17,7 +17,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -39,7 +39,7 @@ pub struct PinchGesture(pub f32); /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -58,7 +58,7 @@ pub struct RotationGesture(pub f32); /// /// - Only available on **`macOS`** and **`iOS`**. /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -76,7 +76,7 @@ pub struct DoubleTapGesture; /// ## Platform-specific /// /// - On **`iOS`**, must be enabled first -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index ea5452fb53..70efe18a84 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -69,7 +69,7 @@ use crate::{ButtonInput, ButtonState}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, system::ResMut, }; @@ -92,9 +92,10 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// ## Usage /// -/// The event is consumed inside of the [`keyboard_input_system`] -/// to update the [`ButtonInput`](ButtonInput) resource. -#[derive(Event, Debug, Clone, PartialEq, Eq, Hash)] +/// The event is consumed inside of the [`keyboard_input_system`] to update the +/// [`ButtonInput`](ButtonInput) and +/// [`ButtonInput`](ButtonInput) resources. +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -107,8 +108,12 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; )] pub struct KeyboardInput { /// The physical key code of the key. + /// + /// This corresponds to the location of the key independent of the keyboard layout. pub key_code: KeyCode, - /// The logical key of the input + /// The logical key of the input. + /// + /// This corresponds to the actual key taking keyboard layout into account. pub logical_key: Key, /// The press state of the key. pub state: ButtonState, @@ -139,7 +144,7 @@ pub struct KeyboardInput { /// when, for example, switching between windows with 'Alt-Tab' or using any other /// OS specific key combination that leads to Bevy window losing focus and not receiving any /// input events -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Clone, PartialEq))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -148,32 +153,46 @@ pub struct KeyboardInput { )] pub struct KeyboardFocusLost; -/// Updates the [`ButtonInput`] resource with the latest [`KeyboardInput`] events. +/// Updates the [`ButtonInput`] and [`ButtonInput`] resources with the latest [`KeyboardInput`] events. /// /// ## Differences /// -/// The main difference between the [`KeyboardInput`] event and the [`ButtonInput`] resources is that +/// The main difference between the [`KeyboardInput`] event and the [`ButtonInput`] resources are that /// the latter has convenient functions such as [`ButtonInput::pressed`], [`ButtonInput::just_pressed`] and [`ButtonInput::just_released`] and is window id agnostic. +/// +/// There is a [`ButtonInput`] for both [`KeyCode`] and [`Key`] as they are both useful in different situations, see their documentation for the details. pub fn keyboard_input_system( - mut key_input: ResMut>, + mut keycode_input: ResMut>, + mut key_input: ResMut>, mut keyboard_input_events: EventReader, mut focus_events: EventReader, ) { - // Avoid clearing if it's not empty to ensure change detection is not triggered. + // Avoid clearing if not empty to ensure change detection is not triggered. + keycode_input.bypass_change_detection().clear(); key_input.bypass_change_detection().clear(); + for event in keyboard_input_events.read() { let KeyboardInput { - key_code, state, .. + key_code, + logical_key, + state, + .. } = event; match state { - ButtonState::Pressed => key_input.press(*key_code), - ButtonState::Released => key_input.release(*key_code), + ButtonState::Pressed => { + keycode_input.press(*key_code); + key_input.press(logical_key.clone()); + } + ButtonState::Released => { + keycode_input.release(*key_code); + key_input.release(logical_key.clone()); + } } } // Release all cached input to avoid having stuck input when switching between windows in os if !focus_events.is_empty() { - key_input.release_all(); + keycode_input.release_all(); focus_events.clear(); } } @@ -220,13 +239,13 @@ pub enum NativeKeyCode { /// It is used as the generic `T` value of an [`ButtonInput`] to create a `Res>`. /// /// Code representing the location of a physical key -/// This mostly conforms to the UI Events Specification's [`KeyboardEvent.code`] with a few +/// This mostly conforms to the [`UI Events Specification's KeyboardEvent.code`] with a few /// exceptions: /// - The keys that the specification calls `MetaLeft` and `MetaRight` are named `SuperLeft` and /// `SuperRight` here. /// - The key that the specification calls "Super" is reported as `Unidentified` here. /// -/// [`KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables +/// [`UI Events Specification's KeyboardEvent.code`]: https://w3c.github.io/uievents-code/#code-value-tables /// /// ## Updating /// @@ -756,6 +775,19 @@ pub enum NativeKey { /// The logical key code of a [`KeyboardInput`]. /// +/// This contains the actual value that is produced by pressing the key. This is +/// useful when you need the actual letters, and for symbols like `+` and `-` +/// when implementing zoom, as they can be in different locations depending on +/// the keyboard layout. +/// +/// In many cases you want the key location instead, for example when +/// implementing WASD controls so the keys are located the same place on QWERTY +/// and other layouts. In that case use [`KeyCode`] instead. +/// +/// ## Usage +/// +/// It is used as the generic `T` value of an [`ButtonInput`] to create a `Res>`. +/// /// ## Technical /// /// Its values map 1 to 1 to winit's Key. diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index 653af4c991..5a6a177223 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -49,7 +49,7 @@ use bevy_ecs::prelude::*; #[cfg(feature = "bevy_reflect")] use bevy_reflect::Reflect; use gestures::*; -use keyboard::{keyboard_input_system, KeyCode, KeyboardFocusLost, KeyboardInput}; +use keyboard::{keyboard_input_system, Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use mouse::{ accumulate_mouse_motion_system, accumulate_mouse_scroll_system, mouse_button_input_system, AccumulatedMouseMotion, AccumulatedMouseScroll, MouseButton, MouseButtonInput, MouseMotion, @@ -89,6 +89,7 @@ impl Plugin for InputPlugin { .add_event::() .add_event::() .init_resource::>() + .init_resource::>() .add_systems(PreUpdate, keyboard_input_system.in_set(InputSystems)) // mouse .add_event::() diff --git a/crates/bevy_input/src/mouse.rs b/crates/bevy_input/src/mouse.rs index 3a377d9329..e6b52bf51d 100644 --- a/crates/bevy_input/src/mouse.rs +++ b/crates/bevy_input/src/mouse.rs @@ -4,7 +4,7 @@ use crate::{ButtonInput, ButtonState}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, resource::Resource, system::ResMut, }; @@ -26,7 +26,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// The event is read inside of the [`mouse_button_input_system`] /// to update the [`ButtonInput`] resource. -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -91,7 +91,7 @@ pub enum MouseButton { /// However, the event data does not make it possible to distinguish which device it is referring to. /// /// [`DeviceEvent::MouseMotion`]: https://docs.rs/winit/latest/winit/event/enum.DeviceEvent.html#variant.MouseMotion -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -140,7 +140,7 @@ pub enum MouseScrollUnit { /// A mouse wheel event. /// /// This event is the translated version of the `WindowEvent::MouseWheel` from the `winit` crate. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input/src/touch.rs b/crates/bevy_input/src/touch.rs index 28f3159d53..df1cf3764f 100644 --- a/crates/bevy_input/src/touch.rs +++ b/crates/bevy_input/src/touch.rs @@ -2,7 +2,7 @@ use bevy_ecs::{ entity::Entity, - event::{Event, EventReader}, + event::{BufferedEvent, Event, EventReader}, resource::Resource, system::ResMut, }; @@ -37,7 +37,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// /// This event is the translated version of the `WindowEvent::Touch` from the `winit` crate. /// It is available to the end user and can be used for game logic. -#[derive(Event, Debug, Clone, Copy, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index e7ff3f6fe8..49eea8dcea 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -73,9 +73,6 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ thiserror = { version = "2", default-features = false } log = { version = "0.4", default-features = false } -[dev-dependencies] -smol_str = "0.2" - [lints] workspace = true diff --git a/crates/bevy_input_focus/src/autofocus.rs b/crates/bevy_input_focus/src/autofocus.rs index 72024418d2..f6a862db88 100644 --- a/crates/bevy_input_focus/src/autofocus.rs +++ b/crates/bevy_input_focus/src/autofocus.rs @@ -1,6 +1,6 @@ //! Contains the [`AutoFocus`] component and related machinery. -use bevy_ecs::{component::HookContext, prelude::*, world::DeferredWorld}; +use bevy_ecs::{lifecycle::HookContext, prelude::*, world::DeferredWorld}; use crate::InputFocus; diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index da3a7bc8e9..cbf88740fd 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -30,7 +30,7 @@ pub mod tab_navigation; mod autofocus; pub use autofocus::*; -use bevy_app::{App, Plugin, PreUpdate, Startup}; +use bevy_app::{App, Plugin, PostStartup, PreUpdate}; use bevy_ecs::{prelude::*, query::QueryData, system::SystemParam, traversal::Traversal}; use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel}; use bevy_window::{PrimaryWindow, Window}; @@ -137,21 +137,16 @@ pub struct InputFocusVisible(pub bool); /// /// To set up your own bubbling input event, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, /// in the [`InputFocusSystems::Dispatch`] system set during [`PreUpdate`]. -#[derive(Clone, Debug, Component)] +#[derive(Event, EntityEvent, Clone, Debug, Component)] +#[entity_event(traversal = WindowTraversal, auto_propagate)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))] -pub struct FocusedInput { +pub struct FocusedInput { /// The underlying input event. pub input: E, /// The primary window entity. window: Entity, } -impl Event for FocusedInput { - type Traversal = WindowTraversal; - - const AUTO_PROPAGATE: bool = true; -} - #[derive(QueryData)] /// These are for accessing components defined on the targeted entity pub struct WindowTraversal { @@ -159,8 +154,8 @@ pub struct WindowTraversal { window: Option<&'static Window>, } -impl Traversal> for WindowTraversal { - fn traverse(item: Self::Item<'_>, event: &FocusedInput) -> Option { +impl Traversal> for WindowTraversal { + fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput) -> Option { let WindowTraversalItem { child_of, window } = item; // Send event to parent, if it has one. @@ -185,7 +180,7 @@ pub struct InputDispatchPlugin; impl Plugin for InputDispatchPlugin { fn build(&self, app: &mut App) { - app.add_systems(Startup, set_initial_focus) + app.add_systems(PostStartup, set_initial_focus) .init_resource::() .init_resource::() .add_systems( @@ -218,17 +213,19 @@ pub enum InputFocusSystems { #[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")] pub type InputFocusSet = InputFocusSystems; -/// Sets the initial focus to the primary window, if any. +/// If no entity is focused, sets the focus to the primary window, if any. pub fn set_initial_focus( mut input_focus: ResMut, window: Single>, ) { - input_focus.0 = Some(*window); + if input_focus.0.is_none() { + input_focus.0 = Some(*window); + } } /// System which dispatches bubbled input events to the focused entity, or to the primary window /// if no entity has focus. -pub fn dispatch_focused_input( +pub fn dispatch_focused_input( mut key_events: EventReader, focus: Res, windows: Query>, @@ -368,30 +365,18 @@ mod tests { use super::*; use alloc::string::String; - use bevy_ecs::{ - component::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld, - }; + use bevy_app::Startup; + use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld}; use bevy_input::{ keyboard::{Key, KeyCode}, ButtonState, InputPlugin, }; - use bevy_window::WindowResolution; - use smol_str::SmolStr; - - #[derive(Component)] - #[component(on_add = set_focus_on_add)] - struct SetFocusOnAdd; - - fn set_focus_on_add(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { - let mut input_focus = world.resource_mut::(); - input_focus.set(entity); - } #[derive(Component, Default)] struct GatherKeyboardEvents(String); fn gather_keyboard_events( - trigger: Trigger>, + trigger: On>, mut query: Query<&mut GatherKeyboardEvents>, ) { if let Ok(mut gather) = query.get_mut(trigger.target()) { @@ -401,14 +386,16 @@ mod tests { } } - const KEY_A_EVENT: KeyboardInput = KeyboardInput { - key_code: KeyCode::KeyA, - logical_key: Key::Character(SmolStr::new_static("A")), - state: ButtonState::Pressed, - text: Some(SmolStr::new_static("A")), - repeat: false, - window: Entity::PLACEHOLDER, - }; + fn key_a_event() -> KeyboardInput { + KeyboardInput { + key_code: KeyCode::KeyA, + logical_key: Key::Character("A".into()), + state: ButtonState::Pressed, + text: Some("A".into()), + repeat: false, + window: Entity::PLACEHOLDER, + } + } #[test] fn test_no_panics_if_resource_missing() { @@ -438,6 +425,55 @@ mod tests { .unwrap(); } + #[test] + fn initial_focus_unset_if_no_primary_window() { + let mut app = App::new(); + app.add_plugins((InputPlugin, InputDispatchPlugin)); + + app.update(); + + assert_eq!(app.world().resource::().0, None); + } + + #[test] + fn initial_focus_set_to_primary_window() { + let mut app = App::new(); + app.add_plugins((InputPlugin, InputDispatchPlugin)); + + let entity_window = app + .world_mut() + .spawn((Window::default(), PrimaryWindow)) + .id(); + app.update(); + + assert_eq!(app.world().resource::().0, Some(entity_window)); + } + + #[test] + fn initial_focus_not_overridden() { + let mut app = App::new(); + app.add_plugins((InputPlugin, InputDispatchPlugin)); + + app.world_mut().spawn((Window::default(), PrimaryWindow)); + + app.add_systems(Startup, |mut commands: Commands| { + commands.spawn(AutoFocus); + }); + + app.update(); + + let autofocus_entity = app + .world_mut() + .query_filtered::>() + .single(app.world()) + .unwrap(); + + assert_eq!( + app.world().resource::().0, + Some(autofocus_entity) + ); + } + #[test] fn test_keyboard_events() { fn get_gathered(app: &App, entity: Entity) -> &str { @@ -454,18 +490,14 @@ mod tests { app.add_plugins((InputPlugin, InputDispatchPlugin)) .add_observer(gather_keyboard_events); - let window = Window { - resolution: WindowResolution::new(800., 600.), - ..Default::default() - }; - app.world_mut().spawn((window, PrimaryWindow)); + app.world_mut().spawn((Window::default(), PrimaryWindow)); // Run the world for a single frame to set up the initial focus app.update(); let entity_a = app .world_mut() - .spawn((GatherKeyboardEvents::default(), SetFocusOnAdd)) + .spawn((GatherKeyboardEvents::default(), AutoFocus)) .id(); let child_of_b = app @@ -487,7 +519,7 @@ mod tests { assert!(!app.world().is_focus_visible(child_of_b)); // entity_a should receive this event - app.world_mut().send_event(KEY_A_EVENT); + app.world_mut().send_event(key_a_event()); app.update(); assert_eq!(get_gathered(&app, entity_a), "A"); @@ -500,7 +532,7 @@ mod tests { assert!(!app.world().is_focus_visible(entity_a)); // This event should be lost - app.world_mut().send_event(KEY_A_EVENT); + app.world_mut().send_event(key_a_event()); app.update(); assert_eq!(get_gathered(&app, entity_a), "A"); @@ -520,7 +552,8 @@ mod tests { assert!(app.world().is_focus_within(entity_b)); // These events should be received by entity_b and child_of_b - app.world_mut().send_event_batch([KEY_A_EVENT; 4]); + app.world_mut() + .send_event_batch(core::iter::repeat_n(key_a_event(), 4)); app.update(); assert_eq!(get_gathered(&app, entity_a), "A"); diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 60df130ae0..39c6e4ebcf 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -30,7 +30,7 @@ use bevy_ecs::{ component::Component, entity::Entity, hierarchy::{ChildOf, Children}, - observer::Trigger, + observer::On, query::{With, Without}, system::{Commands, Query, Res, ResMut, SystemParam}, }; @@ -337,7 +337,7 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + mut trigger: On>, nav: TabNavigation, mut focus: ResMut, mut visible: ResMut, diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index ced31b63ac..e45e9582bf 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -360,6 +360,8 @@ web = [ hotpatching = ["bevy_app/hotpatching", "bevy_ecs/hotpatching"] +debug = ["bevy_utils/debug"] + [dependencies] # bevy (no_std) bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = false, features = [ @@ -409,6 +411,7 @@ bevy_color = { path = "../bevy_color", optional = true, version = "0.16.0-dev", "bevy_reflect", ] } bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.16.0-dev" } +bevy_core_widgets = { path = "../bevy_core_widgets", optional = true, version = "0.16.0-dev" } bevy_anti_aliasing = { path = "../bevy_anti_aliasing", optional = true, version = "0.16.0-dev" } bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.16.0-dev" } bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.16.0-dev" } @@ -423,6 +426,7 @@ bevy_picking = { path = "../bevy_picking", optional = true, version = "0.16.0-de bevy_remote = { path = "../bevy_remote", optional = true, version = "0.16.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.16.0-dev" } bevy_scene = { path = "../bevy_scene", optional = true, version = "0.16.0-dev" } +bevy_solari = { path = "../bevy_solari", optional = true, version = "0.16.0-dev" } bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.16.0-dev" } bevy_state = { path = "../bevy_state", optional = true, version = "0.16.0-dev", default-features = false, features = [ "bevy_app", diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 274364882e..b9934088f1 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -29,6 +29,8 @@ pub use bevy_audio as audio; pub use bevy_color as color; #[cfg(feature = "bevy_core_pipeline")] pub use bevy_core_pipeline as core_pipeline; +#[cfg(feature = "bevy_core_widgets")] +pub use bevy_core_widgets as core_widgets; #[cfg(feature = "bevy_dev_tools")] pub use bevy_dev_tools as dev_tools; pub use bevy_diagnostic as diagnostic; @@ -60,6 +62,8 @@ pub use bevy_remote as remote; pub use bevy_render as render; #[cfg(feature = "bevy_scene")] pub use bevy_scene as scene; +#[cfg(feature = "bevy_solari")] +pub use bevy_solari as solari; #[cfg(feature = "bevy_sprite")] pub use bevy_sprite as sprite; #[cfg(feature = "bevy_state")] diff --git a/crates/bevy_log/Cargo.toml b/crates/bevy_log/Cargo.toml index e6d8899d63..fce602f7ad 100644 --- a/crates/bevy_log/Cargo.toml +++ b/crates/bevy_log/Cargo.toml @@ -45,7 +45,7 @@ bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = fa ] } [target.'cfg(target_os = "ios")'.dependencies] -tracing-oslog = "0.2" +tracing-oslog = "0.3" [lints] workspace = true diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index e4868dbf69..893c84ecc5 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -119,6 +119,21 @@ pub struct Mesh { morph_targets: Option>, morph_target_names: Option>, pub asset_usage: RenderAssetUsages, + /// Whether or not to build a BLAS for use with `bevy_solari` raytracing. + /// + /// Note that this is _not_ whether the mesh is _compatible_ with `bevy_solari` raytracing. + /// This field just controls whether or not a BLAS gets built for this mesh, assuming that + /// the mesh is compatible. + /// + /// The use case for this field is using lower-resolution proxy meshes for raytracing (to save on BLAS memory usage), + /// while using higher-resolution meshes for raster. You can set this field to true for the lower-resolution proxy mesh, + /// and to false for the high-resolution raster mesh. + /// + /// Alternatively, you can use the same mesh for both raster and raytracing, with this field set to true. + /// + /// Does nothing if not used with `bevy_solari`, or if the mesh is not compatible + /// with `bevy_solari` (see `bevy_solari`'s docs). + pub enable_raytracing: bool, } impl Mesh { @@ -203,6 +218,7 @@ impl Mesh { morph_targets: None, morph_target_names: None, asset_usage, + enable_raytracing: true, } } diff --git a/crates/bevy_mikktspace/src/lib.rs b/crates/bevy_mikktspace/src/lib.rs index f74e05098b..12efbf5d62 100644 --- a/crates/bevy_mikktspace/src/lib.rs +++ b/crates/bevy_mikktspace/src/lib.rs @@ -7,9 +7,7 @@ unsafe_op_in_unsafe_fn, clippy::all, clippy::undocumented_unsafe_blocks, - clippy::ptr_cast_constness, - // FIXME(15321): solve CI failures, then replace with `#![expect()]`. - missing_docs + clippy::ptr_cast_constness )] #![cfg_attr(docsrs, feature(doc_auto_cfg))] #![doc( @@ -18,6 +16,10 @@ )] #![no_std] +//! An implementation of [Mikkelsen's algorithm] for tangent space generation. +//! +//! [Mikkelsen's algorithm]: http://www.mikktspace.com + #[cfg(feature = "std")] extern crate std; diff --git a/crates/bevy_pbr/src/atmosphere/mod.rs b/crates/bevy_pbr/src/atmosphere/mod.rs index 8b08751428..a55403630a 100644 --- a/crates/bevy_pbr/src/atmosphere/mod.rs +++ b/crates/bevy_pbr/src/atmosphere/mod.rs @@ -309,7 +309,7 @@ impl ExtractComponent for Atmosphere { type Out = Atmosphere; - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } @@ -405,7 +405,7 @@ impl ExtractComponent for AtmosphereSettings { type Out = AtmosphereSettings; - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } diff --git a/crates/bevy_pbr/src/atmosphere/node.rs b/crates/bevy_pbr/src/atmosphere/node.rs index 851447d760..e09b27c590 100644 --- a/crates/bevy_pbr/src/atmosphere/node.rs +++ b/crates/bevy_pbr/src/atmosphere/node.rs @@ -181,7 +181,7 @@ impl ViewNode for RenderSkyNode { view_uniforms_offset, lights_uniforms_offset, render_sky_pipeline_id, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs index 8c57fb8eb9..d7c93c4418 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -560,7 +560,7 @@ pub(super) fn prepare_atmosphere_transforms( }; for (entity, view) in &views { - let world_from_view = view.world_from_view.compute_matrix(); + let world_from_view = view.world_from_view.to_matrix(); let camera_z = world_from_view.z_axis.truncate(); let camera_y = world_from_view.y_axis.truncate(); let atmo_z = camera_z diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index 1b7b3563d7..b0c0fb6347 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -353,7 +353,7 @@ pub(crate) fn assign_objects_to_clusters( let mut requested_cluster_dimensions = config.dimensions_for_screen_size(screen_size); - let world_from_view = camera_transform.compute_matrix(); + let world_from_view = camera_transform.to_matrix(); let view_from_world_scale = camera_transform.compute_transform().scale.recip(); let view_from_world_scale_max = view_from_world_scale.abs().max_element(); let view_from_world = world_from_view.inverse(); diff --git a/crates/bevy_pbr/src/decal/clustered.rs b/crates/bevy_pbr/src/decal/clustered.rs index dd77d2088d..5618b31831 100644 --- a/crates/bevy_pbr/src/decal/clustered.rs +++ b/crates/bevy_pbr/src/decal/clustered.rs @@ -69,7 +69,7 @@ pub struct ClusteredDecalPlugin; /// An object that projects a decal onto surfaces within its bounds. /// /// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It -/// projects the given [`Self::image`] onto surfaces in the +Z direction (thus +/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus /// you may find [`Transform::looking_at`] useful). /// /// Clustered decals are the highest-quality types of decals that Bevy supports, diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index 65be474e65..28edd38c52 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -449,6 +449,7 @@ pub fn prepare_deferred_lighting_pipelines( ), Has>, Has>, + Has, )>, ) { for ( @@ -461,12 +462,13 @@ pub fn prepare_deferred_lighting_pipelines( (normal_prepass, depth_prepass, motion_vector_prepass, deferred_prepass), has_environment_maps, has_irradiance_volumes, + skip_deferred_lighting, ) in &views { - // If there is no deferred prepass, remove the old pipeline if there was - // one. This handles the case in which a view using deferred stops using - // it. - if !deferred_prepass { + // If there is no deferred prepass or we want to skip the deferred lighting pass, + // remove the old pipeline if there was one. This handles the case in which a + // view using deferred stops using it. + if !deferred_prepass || skip_deferred_lighting { commands.entity(entity).remove::(); continue; } @@ -552,3 +554,14 @@ pub fn prepare_deferred_lighting_pipelines( .insert(DeferredLightingPipeline { pipeline_id }); } } + +/// Component to skip running the deferred lighting pass in [`DeferredOpaquePass3dPbrLightingNode`] for a specific view. +/// +/// This works like [`crate::PbrPlugin::add_default_deferred_lighting_plugin`], but is per-view instead of global. +/// +/// Useful for cases where you want to generate a gbuffer, but skip the built-in deferred lighting pass +/// to run your own custom lighting pass instead. +/// +/// Insert this component in the render world only. +#[derive(Component, Clone, Copy, Default)] +pub struct SkipDeferredLighting; diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 4ff4662bb2..8273ae4b6d 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -341,7 +341,7 @@ pub fn build_directional_light_cascades( .iter() .filter_map(|(entity, transform, projection, camera)| { if camera.is_active { - Some((entity, projection, transform.compute_matrix())) + Some((entity, projection, transform.to_matrix())) } else { None } @@ -357,7 +357,7 @@ pub fn build_directional_light_cascades( // light_to_world has orthogonal upper-left 3x3 and zero translation. // Even though only the direction (i.e. rotation) of the light matters, we don't constrain // users to not change any other aspects of the transform - there's no guarantee - // `transform.compute_matrix()` will give us a matrix with our desired properties. + // `transform.to_matrix()` will give us a matrix with our desired properties. // Instead, we directly create a good matrix from just the rotation. let world_from_light = Mat4::from_quat(transform.compute_transform().rotation); let light_to_world_inverse = world_from_light.inverse(); @@ -628,7 +628,7 @@ pub fn update_point_light_frusta( for (view_rotation, frustum) in view_rotations.iter().zip(cubemap_frusta.iter_mut()) { let world_from_view = view_translation * *view_rotation; - let clip_from_world = clip_from_view * world_from_view.compute_matrix().inverse(); + let clip_from_world = clip_from_view * world_from_view.to_matrix().inverse(); *frustum = Frustum::from_clip_from_world_custom_far( &clip_from_world, diff --git a/crates/bevy_pbr/src/light_probe/environment_map.rs b/crates/bevy_pbr/src/light_probe/environment_map.rs index b31801f757..fa55b4d94a 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.rs +++ b/crates/bevy_pbr/src/light_probe/environment_map.rs @@ -192,7 +192,7 @@ impl ExtractInstance for EnvironmentMapIds { type QueryFilter = (); - fn extract(item: QueryItem<'_, Self::QueryData>) -> Option { + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option { Some(EnvironmentMapIds { diffuse: item.diffuse_map.id(), specular: item.specular_map.id(), diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs index 431d1245a2..e2dea463f2 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.rs +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.rs @@ -17,11 +17,12 @@ //! documentation in the `bevy-baked-gi` project for more details on this //! workflow. //! -//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes that can -//! be arbitrarily scaled, rotated, and positioned in a scene with the -//! [`bevy_transform::components::Transform`] component. The 3D voxel grid will -//! be stretched to fill the interior of the cube, and the illumination from the -//! irradiance volume will apply to all fragments within that bounding region. +//! Like all light probes in Bevy, irradiance volumes are 1×1×1 cubes, centered +//! on the origin, that can be arbitrarily scaled, rotated, and positioned in a +//! scene with the [`bevy_transform::components::Transform`] component. The 3D +//! voxel grid will be stretched to fill the interior of the cube, with linear +//! interpolation, and the illumination from the irradiance volume will apply to +//! all fragments within that bounding region. //! //! Bevy's irradiance volumes are based on Valve's [*ambient cubes*] as used in //! *Half-Life 2* ([Mitchell 2006, slide 27]). These encode a single color of @@ -154,7 +155,7 @@ use crate::{ MAX_VIEW_LIGHT_PROBES, }; -use super::LightProbeComponent; +use super::{LightProbe, LightProbeComponent}; /// On WebGL and WebGPU, we must disable irradiance volumes, as otherwise we can /// overflow the number of texture bindings when deferred rendering is in use @@ -164,8 +165,12 @@ pub(crate) const IRRADIANCE_VOLUMES_ARE_USABLE: bool = cfg!(not(target_arch = "w /// The component that defines an irradiance volume. /// /// See [`crate::irradiance_volume`] for detailed information. +/// +/// This component requires the [`LightProbe`] component, and is typically used with +/// [`bevy_transform::components::Transform`] to place the volume appropriately. #[derive(Clone, Reflect, Component, Debug)] #[reflect(Component, Default, Debug, Clone)] +#[require(LightProbe)] pub struct IrradianceVolume { /// The 3D texture that represents the ambient cubes, encoded in the format /// described in [`crate::irradiance_volume`]. diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index 74710ce1d5..bfce2f1e26 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -378,7 +378,7 @@ fn gather_environment_map_uniform( let environment_map_uniform = if let Some(environment_map_light) = environment_map_light { EnvironmentMapUniform { transform: Transform::from_rotation(environment_map_light.rotation) - .compute_matrix() + .to_matrix() .inverse(), } } else { @@ -595,7 +595,7 @@ where ) -> Option> { environment_map.id(image_assets).map(|id| LightProbeInfo { world_from_light: light_probe_transform.affine(), - light_from_world: light_probe_transform.compute_matrix().inverse(), + light_from_world: light_probe_transform.to_matrix().inverse(), asset_id: id, intensity: environment_map.intensity(), affects_lightmapped_mesh_diffuse: environment_map.affects_lightmapped_mesh_diffuse(), diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index cb7aecc2cb..567bbce674 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -37,9 +37,9 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, + lifecycle::RemovedComponents, query::{Changed, Or}, reflect::ReflectComponent, - removal_detection::RemovedComponents, resource::Resource, schedule::IntoScheduleConfigs, system::{Query, Res, ResMut}, diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 35182910a9..aef2b74177 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -216,11 +216,16 @@ pub fn update_previous_view_data( query: Query<(Entity, &Camera, &GlobalTransform), Or<(With, With)>>, ) { for (entity, camera, camera_transform) in &query { - let view_from_world = camera_transform.compute_matrix().inverse(); + let world_from_view = camera_transform.to_matrix(); + let view_from_world = world_from_view.inverse(); + let view_from_clip = camera.clip_from_view().inverse(); + commands.entity(entity).try_insert(PreviousViewData { view_from_world, clip_from_world: camera.clip_from_view() * view_from_world, clip_from_view: camera.clip_from_view(), + world_from_clip: world_from_view * view_from_clip, + view_from_clip, }); } } @@ -698,11 +703,16 @@ pub fn prepare_previous_view_uniforms( let prev_view_data = match maybe_previous_view_uniforms { Some(previous_view) => previous_view.clone(), None => { - let view_from_world = camera.world_from_view.compute_matrix().inverse(); + let world_from_view = camera.world_from_view.to_matrix(); + let view_from_world = world_from_view.inverse(); + let view_from_clip = camera.clip_from_view.inverse(); + PreviousViewData { view_from_world, clip_from_world: camera.clip_from_view * view_from_world, clip_from_view: camera.clip_from_view, + world_from_clip: world_from_view * view_from_clip, + view_from_clip, } } }; diff --git a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl index 3bd27b2e03..141f7d7b0d 100644 --- a/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl +++ b/crates/bevy_pbr/src/prepass/prepass_bindings.wgsl @@ -4,6 +4,8 @@ struct PreviousViewUniforms { view_from_world: mat4x4, clip_from_world: mat4x4, clip_from_view: mat4x4, + world_from_clip: mat4x4, + view_from_clip: mat4x4, } @group(0) @binding(2) var previous_view_uniforms: PreviousViewUniforms; diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index e779dbc841..83d28a7da7 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -548,7 +548,7 @@ pub struct LightViewEntities(EntityHashMap>); // TODO: using required component pub(crate) fn add_light_view_entities( - trigger: Trigger, + trigger: On, mut commands: Commands, ) { if let Ok(mut v) = commands.get_entity(trigger.target()) { @@ -558,7 +558,7 @@ pub(crate) fn add_light_view_entities( /// Removes [`LightViewEntities`] when light is removed. See [`add_light_view_entities`]. pub(crate) fn extracted_light_removed( - trigger: Trigger, + trigger: On, mut commands: Commands, ) { if let Ok(mut v) = commands.get_entity(trigger.target()) { @@ -567,7 +567,7 @@ pub(crate) fn extracted_light_removed( } pub(crate) fn remove_light_view_entities( - trigger: Trigger, + trigger: On, query: Query<&LightViewEntities>, mut commands: Commands, ) { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 8c408233e1..d8a3256b13 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1540,7 +1540,7 @@ fn extract_mesh_for_gpu_building( not_shadow_caster, no_automatic_batching, visibility_range, - ): ::Item<'_>, + ): ::Item<'_, '_>, render_visibility_ranges: &RenderVisibilityRanges, render_mesh_instances: &RenderMeshInstancesGpu, queue: &mut RenderMeshInstanceGpuQueue, @@ -2874,7 +2874,7 @@ impl RenderCommand

for SetMeshViewBindGroup view_environment_map, mesh_view_bind_group, maybe_oit_layers_count_offset, - ): ROQueryItem<'w, Self::ViewQuery>, + ): ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, _: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 9f7dbb2f76..22d5acd1e9 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -280,7 +280,7 @@ impl ViewNode for ScreenSpaceReflectionsNode { view_environment_map_offset, view_bind_group, ssr_pipeline_id, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { // Grab the render pipeline. @@ -498,7 +498,7 @@ impl ExtractComponent for ScreenSpaceReflections { type Out = ScreenSpaceReflectionsUniform; - fn extract_component(settings: QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(settings: QueryItem<'_, '_, Self::QueryData>) -> Option { if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { once!(info!( "Disabling screen-space reflections on this platform because depth textures \ diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 45f694e546..cf2989a980 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -347,7 +347,7 @@ impl ViewNode for VolumetricFogNode { view_ssr_offset, msaa, view_environment_map_offset, - ): QueryItem<'w, Self::ViewQuery>, + ): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let pipeline_cache = world.resource::(); @@ -700,7 +700,7 @@ pub fn prepare_volumetric_fog_uniforms( // Do this up front to avoid O(n^2) matrix inversion. local_from_world_matrices.clear(); for (_, _, fog_transform) in fog_volumes.iter() { - local_from_world_matrices.push(fog_transform.compute_matrix().inverse()); + local_from_world_matrices.push(fog_transform.to_matrix().inverse()); } let uniform_count = view_targets.iter().len() * local_from_world_matrices.len(); @@ -712,7 +712,7 @@ pub fn prepare_volumetric_fog_uniforms( }; for (view_entity, extracted_view, volumetric_fog) in view_targets.iter() { - let world_from_view = extracted_view.world_from_view.compute_matrix(); + let world_from_view = extracted_view.world_from_view.to_matrix(); let mut view_fog_volumes = vec![]; diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index cc64ad2e4f..e42e1309ec 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -374,7 +374,7 @@ impl ViewNode for Wireframe3dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(wireframe_phase) = world.get_resource::>() diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 9e28cc6d7c..28693314d9 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -55,7 +55,7 @@ pub mod prelude { /// Note that systems reading these events in [`PreUpdate`](bevy_app::PreUpdate) will not report ordering /// ambiguities with picking backends. Take care to ensure such systems are explicitly ordered /// against [`PickingSystems::Backend`](crate::PickingSystems::Backend), or better, avoid reading `PointerHits` in `PreUpdate`. -#[derive(Event, Debug, Clone, Reflect)] +#[derive(Event, BufferedEvent, Debug, Clone, Reflect)] #[reflect(Debug, Clone)] pub struct PointerHits { /// The pointer associated with this hit test. diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index 72c0f06c46..a7a3979c59 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -11,7 +11,7 @@ //! # use bevy_picking::prelude::*; //! # let mut world = World::default(); //! world.spawn_empty() -//! .observe(|trigger: Trigger>| { +//! .observe(|trigger: On>| { //! println!("I am being hovered over"); //! }); //! ``` @@ -31,7 +31,7 @@ //! //! The events this module defines fall into a few broad categories: //! + Hovering and movement: [`Over`], [`Move`], and [`Out`]. -//! + Clicking and pressing: [`Pressed`], [`Released`], and [`Click`]. +//! + Clicking and pressing: [`Press`], [`Release`], and [`Click`]. //! + Dragging and dropping: [`DragStart`], [`Drag`], [`DragEnd`], [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`]. //! //! When received by an observer, these events will always be wrapped by the [`Pointer`] type, which contains @@ -59,11 +59,10 @@ use crate::{ /// /// The documentation for the [`pointer_events`] explains the events this module exposes and /// the order in which they fire. -#[derive(Clone, PartialEq, Debug, Reflect, Component)] +#[derive(Event, BufferedEvent, EntityEvent, Clone, PartialEq, Debug, Reflect, Component)] +#[entity_event(traversal = PointerTraversal, auto_propagate)] #[reflect(Component, Debug, Clone)] pub struct Pointer { - /// The original target of this picking event, before bubbling - pub target: Entity, /// The pointer that triggered this event pub pointer_id: PointerId, /// The location of the pointer during this event @@ -87,7 +86,7 @@ impl Traversal> for PointerTraversal where E: Debug + Clone + Reflect, { - fn traverse(item: Self::Item<'_>, pointer: &Pointer) -> Option { + fn traverse(item: Self::Item<'_, '_>, pointer: &Pointer) -> Option { let PointerTraversalItem { child_of, window } = item; // Send event to parent, if it has one. @@ -106,15 +105,6 @@ where } } -impl Event for Pointer -where - E: Debug + Clone + Reflect, -{ - type Traversal = PointerTraversal; - - const AUTO_PROPAGATE: bool = true; -} - impl core::fmt::Display for Pointer { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { f.write_fmt(format_args!( @@ -134,9 +124,8 @@ impl core::ops::Deref for Pointer { impl Pointer { /// Construct a new `Pointer` event. - pub fn new(id: PointerId, location: Location, target: Entity, event: E) -> Self { + pub fn new(id: PointerId, location: Location, event: E) -> Self { Self { - target, pointer_id: id, pointer_location: location, event, @@ -171,7 +160,7 @@ pub struct Out { /// Fires when a pointer button is pressed over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Pressed { +pub struct Press { /// Pointer button pressed to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -181,7 +170,7 @@ pub struct Pressed { /// Fires when a pointer button is released over the `target` entity. #[derive(Clone, PartialEq, Debug, Reflect)] #[reflect(Clone, PartialEq)] -pub struct Released { +pub struct Release { /// Pointer button lifted to trigger this event. pub button: PointerButton, /// Information about the picking intersection. @@ -400,7 +389,7 @@ impl PointerState { pub struct PickingEventWriters<'w> { cancel_events: EventWriter<'w, Pointer>, click_events: EventWriter<'w, Pointer>, - pressed_events: EventWriter<'w, Pointer>, + pressed_events: EventWriter<'w, Pointer>, drag_drop_events: EventWriter<'w, Pointer>, drag_end_events: EventWriter<'w, Pointer>, drag_enter_events: EventWriter<'w, Pointer>, @@ -412,7 +401,7 @@ pub struct PickingEventWriters<'w> { move_events: EventWriter<'w, Pointer>, out_events: EventWriter<'w, Pointer>, over_events: EventWriter<'w, Pointer>, - released_events: EventWriter<'w, Pointer>, + released_events: EventWriter<'w, Pointer>, } /// Dispatches interaction events to the target entities. @@ -422,7 +411,7 @@ pub struct PickingEventWriters<'w> { /// + [`DragEnter`] → [`Over`]. /// + Any number of any of the following: /// + For each movement: [`DragStart`] → [`Drag`] → [`DragOver`] → [`Move`]. -/// + For each button press: [`Pressed`] or [`Click`] → [`Released`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. +/// + For each button press: [`Press`] or [`Click`] → [`Release`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`]. /// + For each pointer cancellation: [`Cancel`]. /// /// Additionally, across multiple frames, the following are also strictly @@ -430,7 +419,7 @@ pub struct PickingEventWriters<'w> { /// + When a pointer moves over the target: /// [`Over`], [`Move`], [`Out`]. /// + When a pointer presses buttons on the target: -/// [`Pressed`], [`Click`], [`Released`]. +/// [`Press`], [`Click`], [`Release`]. /// + When a pointer drags the target: /// [`DragStart`], [`Drag`], [`DragEnd`]. /// + When a pointer drags something over the target: @@ -452,7 +441,7 @@ pub struct PickingEventWriters<'w> { /// In the context of UI, this is especially problematic. Additional hierarchy-aware /// events will be added in a future release. /// -/// Both [`Click`] and [`Released`] target the entity hovered in the *previous frame*, +/// Both [`Click`] and [`Release`] target the entity hovered in the *previous frame*, /// rather than the current frame. This is because touch pointers hover nothing /// on the frame they are released. The end effect is that these two events can /// be received sequentially after an [`Out`] event (but always on the same frame @@ -505,12 +494,7 @@ pub fn pointer_events( }; // Always send Out events - let out_event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - Out { hit: hit.clone() }, - ); + let out_event = Pointer::new(pointer_id, location.clone(), Out { hit: hit.clone() }); commands.trigger_targets(out_event.clone(), hovered_entity); event_writers.out_events.write(out_event); @@ -522,7 +506,6 @@ pub fn pointer_events( let drag_leave_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragLeave { button, dragged: *drag_target, @@ -564,7 +547,6 @@ pub fn pointer_events( let drag_enter_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragEnter { button, dragged: *drag_target, @@ -577,12 +559,7 @@ pub fn pointer_events( } // Always send Over events - let over_event = Pointer::new( - pointer_id, - location.clone(), - hovered_entity, - Over { hit: hit.clone() }, - ); + let over_event = Pointer::new(pointer_id, location.clone(), Over { hit: hit.clone() }); commands.trigger_targets(over_event.clone(), hovered_entity); event_writers.over_events.write(over_event); } @@ -608,8 +585,7 @@ pub fn pointer_events( let pressed_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, - Pressed { + Press { button, hit: hit.clone(), }, @@ -636,7 +612,6 @@ pub fn pointer_events( let click_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Click { button, hit: hit.clone(), @@ -646,12 +621,11 @@ pub fn pointer_events( commands.trigger_targets(click_event.clone(), hovered_entity); event_writers.click_events.write(click_event); } - // Always send the Released event + // Always send the Release event let released_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, - Released { + Release { button, hit: hit.clone(), }, @@ -667,7 +641,6 @@ pub fn pointer_events( let drag_drop_event = Pointer::new( pointer_id, location.clone(), - *dragged_over, DragDrop { button, dropped: drag_target, @@ -681,7 +654,6 @@ pub fn pointer_events( let drag_end_event = Pointer::new( pointer_id, location.clone(), - drag_target, DragEnd { button, distance: drag.latest_pos - drag.start_pos, @@ -694,7 +666,6 @@ pub fn pointer_events( let drag_leave_event = Pointer::new( pointer_id, location.clone(), - *dragged_over, DragLeave { button, dragged: drag_target, @@ -735,7 +706,6 @@ pub fn pointer_events( let drag_start_event = Pointer::new( pointer_id, location.clone(), - *press_target, DragStart { button, hit: hit.clone(), @@ -754,7 +724,6 @@ pub fn pointer_events( let drag_event = Pointer::new( pointer_id, location.clone(), - *drag_target, Drag { button, distance: location.position - drag.start_pos, @@ -777,7 +746,6 @@ pub fn pointer_events( let drag_over_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, DragOver { button, dragged: *drag_target, @@ -799,7 +767,6 @@ pub fn pointer_events( let move_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Move { hit: hit.clone(), delta, @@ -819,7 +786,6 @@ pub fn pointer_events( let scroll_event = Pointer::new( pointer_id, location.clone(), - hovered_entity, Scroll { unit, x, @@ -839,8 +805,7 @@ pub fn pointer_events( .iter() .flat_map(|h| h.iter().map(|(entity, data)| (*entity, data.to_owned()))) { - let cancel_event = - Pointer::new(pointer_id, location.clone(), hovered_entity, Cancel { hit }); + let cancel_event = Pointer::new(pointer_id, location.clone(), Cancel { hit }); commands.trigger_targets(cancel_event.clone(), hovered_entity); event_writers.cancel_events.write(cancel_event); } diff --git a/crates/bevy_picking/src/hover.rs b/crates/bevy_picking/src/hover.rs index 6347568c02..dbb6ee942e 100644 --- a/crates/bevy_picking/src/hover.rs +++ b/crates/bevy_picking/src/hover.rs @@ -14,7 +14,7 @@ use crate::{ }; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::prelude::*; +use bevy_ecs::{entity::EntityHashSet, prelude::*}; use bevy_math::FloatOrd; use bevy_platform::collections::HashMap; use bevy_reflect::prelude::*; @@ -208,18 +208,6 @@ pub fn update_interactions( mut pointers: Query<(&PointerId, &PointerPress, &mut PointerInteraction)>, mut interact: Query<&mut PickingInteraction>, ) { - // Clear all previous hover data from pointers and entities - for (pointer, _, mut pointer_interaction) in &mut pointers { - pointer_interaction.sorted_entities.clear(); - if let Some(previously_hovered_entities) = previous_hover_map.get(pointer) { - for entity in previously_hovered_entities.keys() { - if let Ok(mut interaction) = interact.get_mut(*entity) { - *interaction = PickingInteraction::None; - } - } - } - } - // Create a map to hold the aggregated interaction for each entity. This is needed because we // need to be able to insert the interaction component on entities if they do not exist. To do // so we need to know the final aggregated interaction state to avoid the scenario where we set @@ -239,13 +227,29 @@ pub fn update_interactions( } // Take the aggregated entity states and update or insert the component if missing. - for (hovered_entity, new_interaction) in new_interaction_state.drain() { + for (&hovered_entity, &new_interaction) in new_interaction_state.iter() { if let Ok(mut interaction) = interact.get_mut(hovered_entity) { - *interaction = new_interaction; + interaction.set_if_neq(new_interaction); } else if let Ok(mut entity_commands) = commands.get_entity(hovered_entity) { entity_commands.try_insert(new_interaction); } } + + // Clear all previous hover data from pointers that are no longer hovering any entities. + // We do this last to preserve change detection for picking interactions. + for (pointer, _, _) in &mut pointers { + let Some(previously_hovered_entities) = previous_hover_map.get(pointer) else { + continue; + }; + + for entity in previously_hovered_entities.keys() { + if !new_interaction_state.contains_key(entity) { + if let Ok(mut interaction) = interact.get_mut(*entity) { + interaction.set_if_neq(PickingInteraction::None); + } + } + } + } } /// Merge the interaction state of this entity into the aggregated map. @@ -275,3 +279,285 @@ fn merge_interaction_states( new_interaction_state.insert(*hovered_entity, new_interaction); } } + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// enters or leaves an entity. Users should insert this component on an entity to indicate interest +/// in knowing about hover state changes. +/// +/// The component's boolean value will be `true` whenever the pointer is currently directly hovering +/// over the entity, or any of the entity's descendants (as defined by the [`ChildOf`] +/// relationship). This is consistent with the behavior of the CSS `:hover` pseudo-class, which +/// applies to the element and all of its descendants. +/// +/// The contained boolean value is guaranteed to only be mutated when the pointer enters or leaves +/// the entity, allowing Bevy change detection to be used efficiently. This is in contrast to the +/// [`HoverMap`] resource, which is updated every frame. +/// +/// Typically, a simple hoverable entity or widget will have this component added to it. More +/// complex widgets can have this component added to each hoverable part. +/// +/// The computational cost of keeping the `Hovered` components up to date is relatively cheap, and +/// linear in the number of entities that have the [`Hovered`] component inserted. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct Hovered(pub bool); + +impl Hovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// A component that allows users to use regular Bevy change detection to determine when the pointer +/// is directly hovering over an entity. Users should insert this component on an entity to indicate +/// interest in knowing about hover state changes. +/// +/// This is similar to [`Hovered`] component, except that it does not include descendants in the +/// hover state. +#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[component(immutable)] +pub struct DirectlyHovered(pub bool); + +impl DirectlyHovered { + /// Get whether the entity is currently hovered. + pub fn get(&self) -> bool { + self.0 + } +} + +/// Uses [`HoverMap`] changes to update [`Hovered`] components. +pub fn update_is_hovered( + hover_map: Option>, + mut hovers: Query<(Entity, &Hovered)>, + parent_query: Query<&ChildOf>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + // Algorithm: for each entity having a `Hovered` component, we want to know if the current + // entry in the hover map is "within" (that is, in the set of descenants of) that entity. Rather + // than doing an expensive breadth-first traversal of children, instead start with the hovermap + // entry and search upwards. We can make this even cheaper by building a set of ancestors for + // the hovermap entry, and then testing each `Hovered` entity against that set. + + // A set which contains the hovered for the current pointer entity and its ancestors. The + // capacity is based on the likely tree depth of the hierarchy, which is typically greater for + // UI (because of layout issues) than for 3D scenes. A depth of 32 is a reasonable upper bound + // for most use cases. + let mut hover_ancestors = EntityHashSet::with_capacity(32); + if let Some(map) = hover_map.get(&PointerId::Mouse) { + for hovered_entity in map.keys() { + hover_ancestors.insert(*hovered_entity); + hover_ancestors.extend(parent_query.iter_ancestors(*hovered_entity)); + } + } + + // For each hovered entity, it is considered "hovering" if it's in the set of hovered ancestors. + for (entity, hoverable) in hovers.iter_mut() { + let is_hovering = hover_ancestors.contains(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(Hovered(is_hovering)); + } + } +} + +/// Uses [`HoverMap`] changes to update [`DirectlyHovered`] components. +pub fn update_is_directly_hovered( + hover_map: Option>, + hovers: Query<(Entity, &DirectlyHovered)>, + mut commands: Commands, +) { + // Don't do any work if there's no hover map. + let Some(hover_map) = hover_map else { return }; + + // Don't bother collecting ancestors if there are no hovers. + if hovers.is_empty() { + return; + } + + if let Some(map) = hover_map.get(&PointerId::Mouse) { + // It's hovering if it's in the HoverMap. + for (entity, hoverable) in hovers.iter() { + let is_hovering = map.contains_key(&entity); + if hoverable.0 != is_hovering { + commands.entity(entity).insert(DirectlyHovered(is_hovering)); + } + } + } else { + // No hovered entity, reset all hovers. + for (entity, hoverable) in hovers.iter() { + if hoverable.0 { + commands.entity(entity).insert(DirectlyHovered(false)); + } + } + } +} + +#[cfg(test)] +mod tests { + use bevy_render::camera::Camera; + + use super::*; + + #[test] + fn update_is_hovered_memoized() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world.spawn(Hovered(false)).add_child(hovered_child).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_hovered).is_ok()); + + // Check to insure that the hovered entity has the Hovered component set to true + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_hovered).is_ok()); + let hover = world.entity(hovered_entity).get_ref::().unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_self() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_entity = world.spawn(DirectlyHovered(false)).id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_entity, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the hovered entity has the DirectlyHovered component set to true + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + assert!(hover.is_changed()); + + // Now do it again, but don't change the hover map. + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(hover.get()); + + // Should not be changed + // NOTE: Test doesn't work - thinks it is always changed + // assert!(!hover.is_changed()); + + // Clear the hover map and run again. + world.insert_resource(HoverMap::default()); + world.increment_change_tick(); + + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } + + #[test] + fn update_is_hovered_direct_child() { + let mut world = World::default(); + let camera = world.spawn(Camera::default()).id(); + + // Setup entities + let hovered_child = world.spawn_empty().id(); + let hovered_entity = world + .spawn(DirectlyHovered(false)) + .add_child(hovered_child) + .id(); + + // Setup hover map with hovered_entity hovered by mouse + let mut hover_map = HoverMap::default(); + let mut entity_map = HashMap::new(); + entity_map.insert( + hovered_child, + HitData { + depth: 0.0, + camera, + position: None, + normal: None, + }, + ); + hover_map.insert(PointerId::Mouse, entity_map); + world.insert_resource(hover_map); + + // Run the system + assert!(world.run_system_cached(update_is_directly_hovered).is_ok()); + + // Check to insure that the DirectlyHovered component is still false + let hover = world + .entity(hovered_entity) + .get_ref::() + .unwrap(); + assert!(!hover.get()); + assert!(hover.is_changed()); + } +} diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 53387e84c8..74a765fbcd 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -17,7 +17,7 @@ //! # struct MyComponent; //! # let mut world = World::new(); //! world.spawn(MyComponent) -//! .observe(|mut trigger: Trigger>| { +//! .observe(|mut trigger: On>| { //! println!("I was just clicked!"); //! // Get the underlying pointer event data //! let click_event: &Pointer = trigger.event(); @@ -39,7 +39,7 @@ //! //! When events are generated, they bubble up the entity hierarchy starting from their target, until //! they reach the root or bubbling is halted with a call to -//! [`Trigger::propagate`](bevy_ecs::observer::Trigger::propagate). See [`Observer`] for details. +//! [`On::propagate`](bevy_ecs::observer::On::propagate). See [`Observer`] for details. //! //! This allows you to run callbacks when any children of an entity are interacted with, and leads //! to succinct, expressive code: @@ -48,22 +48,22 @@ //! # use bevy_ecs::prelude::*; //! # use bevy_transform::prelude::*; //! # use bevy_picking::prelude::*; -//! # #[derive(Event)] +//! # #[derive(Event, BufferedEvent)] //! # struct Greeting; //! fn setup(mut commands: Commands) { //! commands.spawn(Transform::default()) -//! // Spawn your entity here, e.g. a Mesh. +//! // Spawn your entity here, e.g. a `Mesh3d`. //! // When dragged, mutate the `Transform` component on the dragged target entity: -//! .observe(|trigger: Trigger>, mut transforms: Query<&mut Transform>| { +//! .observe(|trigger: On>, mut transforms: Query<&mut Transform>| { //! let mut transform = transforms.get_mut(trigger.target()).unwrap(); //! let drag = trigger.event(); //! transform.rotate_local_y(drag.delta.x / 50.0); //! }) -//! .observe(|trigger: Trigger>, mut commands: Commands| { +//! .observe(|trigger: On>, mut commands: Commands| { //! println!("Entity {} goes BOOM!", trigger.target()); //! commands.entity(trigger.target()).despawn(); //! }) -//! .observe(|trigger: Trigger>, mut events: EventWriter| { +//! .observe(|trigger: On>, mut events: EventWriter| { //! events.write(Greeting); //! }); //! } @@ -170,6 +170,7 @@ pub mod window; use bevy_app::{prelude::*, PluginGroupBuilder}; use bevy_ecs::prelude::*; use bevy_reflect::prelude::*; +use hover::{update_is_directly_hovered, update_is_hovered}; /// The picking prelude. /// @@ -285,7 +286,7 @@ pub type PickSet = PickingSystems; /// /// Note: for any of these plugins to work, they require a picking backend to be active, /// The picking backend is responsible to turn an input, into a [`crate::backend::PointerHits`] -/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::Trigger`]s. +/// that [`PickingPlugin`] and [`InteractionPlugin`] will refine into [`bevy_ecs::observer::On`]s. #[derive(Default)] pub struct DefaultPickingPlugins; @@ -392,6 +393,7 @@ impl Plugin for PickingPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -414,7 +416,7 @@ impl Plugin for InteractionPlugin { .init_resource::() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_event::>() .add_event::>() @@ -425,11 +427,16 @@ impl Plugin for InteractionPlugin { .add_event::>() .add_event::>() .add_event::>() - .add_event::>() + .add_event::>() .add_event::>() .add_systems( PreUpdate, - (generate_hovermap, update_interactions, pointer_events) + ( + generate_hovermap, + update_interactions, + (update_is_hovered, update_is_directly_hovered), + pointer_events, + ) .chain() .in_set(PickingSystems::Hover), ); diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs index 9988a96e19..5ac2d89887 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -317,7 +317,7 @@ mod tests { #[test] fn ray_mesh_intersection_simple() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = None; @@ -338,7 +338,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -359,7 +359,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -381,7 +381,7 @@ mod tests { #[test] fn ray_mesh_intersection_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[[-1., 0., 0.], [-1., 0., 0.], [-1., 0., 0.]]); @@ -403,7 +403,7 @@ mod tests { #[test] fn ray_mesh_intersection_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = None; @@ -424,7 +424,7 @@ mod tests { #[test] fn ray_mesh_intersection_indices_missing_vertex_normals() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals: Option<&[[f32; 3]]> = Some(&[]); let indices: Option<&[u16]> = Some(&[0, 1, 2]); @@ -445,7 +445,7 @@ mod tests { #[test] fn ray_mesh_intersection_not_enough_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0]); @@ -466,7 +466,7 @@ mod tests { #[test] fn ray_mesh_intersection_bad_indices() { let ray = Ray3d::new(Vec3::ZERO, Dir3::X); - let mesh_transform = GlobalTransform::IDENTITY.compute_matrix(); + let mesh_transform = GlobalTransform::IDENTITY.to_matrix(); let positions = &[V0, V1, V2]; let vertex_normals = None; let indices: Option<&[u16]> = Some(&[0, 1, 3]); diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs index c1f465b96a..e42dc160e2 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/mod.rs @@ -233,7 +233,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { if let Some(distance) = ray_aabb_intersection_3d( ray, &Aabb3d::new(aabb.center, aabb.half_extents), - &transform.compute_matrix(), + &transform.to_matrix(), ) { aabb_hits_tx.send((FloatOrd(distance), entity)).ok(); } @@ -287,7 +287,7 @@ impl<'w, 's> MeshRayCast<'w, 's> { // Perform the actual ray cast. let _ray_cast_guard = ray_cast_guard.enter(); - let transform = transform.compute_matrix(); + let transform = transform.to_matrix(); let intersection = ray_intersection_over_mesh(mesh, &transform, ray, backfaces); if let Some(intersection) = intersection { diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index faba90cbb9..0406cb61f5 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -269,7 +269,7 @@ pub enum PointerAction { } /// An input event effecting a pointer. -#[derive(Event, Debug, Clone, Reflect)] +#[derive(Event, BufferedEvent, Debug, Clone, Reflect)] #[reflect(Clone)] pub struct PointerInput { /// The id of the pointer. diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index d981619f8c..80387bc31b 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -133,7 +133,7 @@ wgpu-types = { version = "24", features = [ inventory = { version = "0.3", optional = true } [dev-dependencies] -ron = "0.8.0" +ron = "0.10" rmp-serde = "1.1" bincode = { version = "2.0", features = ["serde"] } serde_json = "1.0.140" diff --git a/crates/bevy_reflect/src/array.rs b/crates/bevy_reflect/src/array.rs index 55f62b34c8..df9580b820 100644 --- a/crates/bevy_reflect/src/array.rs +++ b/crates/bevy_reflect/src/array.rs @@ -167,6 +167,7 @@ pub struct DynamicArray { } impl DynamicArray { + /// Creates a new [`DynamicArray`]. #[inline] pub fn new(values: Box<[Box]>) -> Self { Self { diff --git a/crates/bevy_reflect/src/attributes.rs b/crates/bevy_reflect/src/attributes.rs index 728102c4b0..4d8154fad3 100644 --- a/crates/bevy_reflect/src/attributes.rs +++ b/crates/bevy_reflect/src/attributes.rs @@ -1,3 +1,5 @@ +//! Types and functions for creating, manipulating and querying [`CustomAttributes`]. + use crate::Reflect; use alloc::boxed::Box; use bevy_utils::TypeIdMap; @@ -98,16 +100,19 @@ struct CustomAttribute { } impl CustomAttribute { + /// Creates a new [`CustomAttribute`] containing `value`. pub fn new(value: T) -> Self { Self { value: Box::new(value), } } + /// Returns a reference to the attribute's value if it is of type `T`, or [`None`] if not. pub fn value(&self) -> Option<&T> { self.value.downcast_ref() } + /// Returns a reference to the attribute's value as a [`Reflect`] trait object. pub fn reflect_value(&self) -> &dyn Reflect { &*self.value } diff --git a/crates/bevy_reflect/src/enums/dynamic_enum.rs b/crates/bevy_reflect/src/enums/dynamic_enum.rs index 3f0b275519..2835306b22 100644 --- a/crates/bevy_reflect/src/enums/dynamic_enum.rs +++ b/crates/bevy_reflect/src/enums/dynamic_enum.rs @@ -13,9 +13,12 @@ use derive_more::derive::From; /// A dynamic representation of an enum variant. #[derive(Debug, Default, From)] pub enum DynamicVariant { + /// A unit variant. #[default] Unit, + /// A tuple variant. Tuple(DynamicTuple), + /// A struct variant. Struct(DynamicStruct), } diff --git a/crates/bevy_reflect/src/enums/enum_trait.rs b/crates/bevy_reflect/src/enums/enum_trait.rs index 126c407f23..32e4b96124 100644 --- a/crates/bevy_reflect/src/enums/enum_trait.rs +++ b/crates/bevy_reflect/src/enums/enum_trait.rs @@ -263,6 +263,7 @@ pub struct VariantFieldIter<'a> { } impl<'a> VariantFieldIter<'a> { + /// Creates a new [`VariantFieldIter`]. pub fn new(container: &'a dyn Enum) -> Self { Self { container, @@ -295,12 +296,16 @@ impl<'a> Iterator for VariantFieldIter<'a> { impl<'a> ExactSizeIterator for VariantFieldIter<'a> {} +/// A field in the current enum variant. pub enum VariantField<'a> { + /// The name and value of a field in a struct variant. Struct(&'a str, &'a dyn PartialReflect), + /// The value of a field in a tuple variant. Tuple(&'a dyn PartialReflect), } impl<'a> VariantField<'a> { + /// Returns the name of a struct variant field, or [`None`] for a tuple variant field. pub fn name(&self) -> Option<&'a str> { if let Self::Struct(name, ..) = self { Some(*name) @@ -309,6 +314,7 @@ impl<'a> VariantField<'a> { } } + /// Gets a reference to the value of this field. pub fn value(&self) -> &'a dyn PartialReflect { match *self { Self::Struct(_, value) | Self::Tuple(value) => value, diff --git a/crates/bevy_reflect/src/enums/variants.rs b/crates/bevy_reflect/src/enums/variants.rs index 55ccb8efb1..d4fcc2845a 100644 --- a/crates/bevy_reflect/src/enums/variants.rs +++ b/crates/bevy_reflect/src/enums/variants.rs @@ -47,7 +47,9 @@ pub enum VariantInfoError { /// [type]: VariantType #[error("variant type mismatch: expected {expected:?}, received {received:?}")] TypeMismatch { + /// Expected variant type. expected: VariantType, + /// Received variant type. received: VariantType, }, } @@ -84,6 +86,7 @@ pub enum VariantInfo { } impl VariantInfo { + /// The name of the enum variant. pub fn name(&self) -> &'static str { match self { Self::Struct(info) => info.name(), diff --git a/crates/bevy_reflect/src/error.rs b/crates/bevy_reflect/src/error.rs index a13b55cdc0..d8bb8a9e14 100644 --- a/crates/bevy_reflect/src/error.rs +++ b/crates/bevy_reflect/src/error.rs @@ -11,14 +11,20 @@ pub enum ReflectCloneError { /// /// [`PartialReflect::reflect_clone`]: crate::PartialReflect::reflect_clone #[error("`PartialReflect::reflect_clone` not implemented for `{type_path}`")] - NotImplemented { type_path: Cow<'static, str> }, + NotImplemented { + /// The fully qualified path of the type that [`PartialReflect::reflect_clone`](crate::PartialReflect::reflect_clone) is not implemented for. + type_path: Cow<'static, str>, + }, /// The type cannot be cloned via [`PartialReflect::reflect_clone`]. /// /// This type should be returned when a type is intentionally opting out of reflection cloning. /// /// [`PartialReflect::reflect_clone`]: crate::PartialReflect::reflect_clone #[error("`{type_path}` cannot be made cloneable for `PartialReflect::reflect_clone`")] - NotCloneable { type_path: Cow<'static, str> }, + NotCloneable { + /// The fully qualified path of the type that cannot be cloned via [`PartialReflect::reflect_clone`](crate::PartialReflect::reflect_clone). + type_path: Cow<'static, str>, + }, /// The field cannot be cloned via [`PartialReflect::reflect_clone`]. /// /// When [deriving `Reflect`], this usually means that a field marked with `#[reflect(ignore)]` @@ -33,8 +39,11 @@ pub enum ReflectCloneError { full_path(.field, .variant.as_deref(), .container_type_path) )] FieldNotCloneable { + /// Struct field or enum variant field which cannot be cloned. field: FieldId, + /// Variant this field is part of if the container is an enum, otherwise [`None`]. variant: Option>, + /// Fully qualified path of the type containing this field. container_type_path: Cow<'static, str>, }, /// Could not downcast to the expected type. @@ -44,7 +53,9 @@ pub enum ReflectCloneError { /// [`Reflect`]: crate::Reflect #[error("expected downcast to `{expected}`, but received `{received}`")] FailedDowncast { + /// The fully qualified path of the type that was expected. expected: Cow<'static, str>, + /// The fully qualified path of the type that was received. received: Cow<'static, str>, }, } diff --git a/crates/bevy_reflect/src/fields.rs b/crates/bevy_reflect/src/fields.rs index 21d4ccd98a..53223835b3 100644 --- a/crates/bevy_reflect/src/fields.rs +++ b/crates/bevy_reflect/src/fields.rs @@ -82,6 +82,7 @@ pub struct UnnamedField { } impl UnnamedField { + /// Create a new [`UnnamedField`]. pub fn new(index: usize) -> Self { Self { index, @@ -135,7 +136,9 @@ impl UnnamedField { /// A representation of a field's accessor. #[derive(Clone, Debug, PartialEq, Eq)] pub enum FieldId { + /// Access a field by name. Named(Cow<'static, str>), + /// Access a field by index. Unnamed(usize), } diff --git a/crates/bevy_reflect/src/func/args/arg.rs b/crates/bevy_reflect/src/func/args/arg.rs index 8ca03aafd3..1c157a6b2f 100644 --- a/crates/bevy_reflect/src/func/args/arg.rs +++ b/crates/bevy_reflect/src/func/args/arg.rs @@ -196,8 +196,11 @@ impl<'a> Arg<'a> { /// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug)] pub enum ArgValue<'a> { + /// An owned argument. Owned(Box), + /// An immutable reference argument. Ref(&'a dyn PartialReflect), + /// A mutable reference argument. Mut(&'a mut dyn PartialReflect), } diff --git a/crates/bevy_reflect/src/func/args/error.rs b/crates/bevy_reflect/src/func/args/error.rs index bd32bd5e5a..20b6cd6220 100644 --- a/crates/bevy_reflect/src/func/args/error.rs +++ b/crates/bevy_reflect/src/func/args/error.rs @@ -12,15 +12,21 @@ pub enum ArgError { /// The argument is not the expected type. #[error("expected `{expected}` but received `{received}` (@ argument index {index})")] UnexpectedType { + /// Argument index. index: usize, + /// Expected argument type path. expected: Cow<'static, str>, + /// Received argument type path. received: Cow<'static, str>, }, /// The argument has the wrong ownership. #[error("expected {expected} value but received {received} value (@ argument index {index})")] InvalidOwnership { + /// Argument index. index: usize, + /// Expected ownership. expected: Ownership, + /// Received ownership. received: Ownership, }, /// Occurs when attempting to access an argument from an empty [`ArgList`]. diff --git a/crates/bevy_reflect/src/func/error.rs b/crates/bevy_reflect/src/func/error.rs index d9d105db1b..dc442e9da8 100644 --- a/crates/bevy_reflect/src/func/error.rs +++ b/crates/bevy_reflect/src/func/error.rs @@ -18,11 +18,18 @@ pub enum FunctionError { ArgError(#[from] ArgError), /// The number of arguments provided does not match the expected number. #[error("received {received} arguments but expected one of {expected:?}")] - ArgCountMismatch { expected: ArgCount, received: usize }, + ArgCountMismatch { + /// Expected argument count. [`ArgCount`] for overloaded functions will contain multiple possible counts. + expected: ArgCount, + /// Number of arguments received. + received: usize, + }, /// No overload was found for the given set of arguments. #[error("no overload found for arguments with signature `{received:?}`, expected one of `{expected:?}`")] NoOverload { + /// The set of available argument signatures. expected: HashSet, + /// The received argument signature. received: ArgumentSignature, }, } @@ -47,6 +54,9 @@ pub enum FunctionOverloadError { /// An error that occurs when attempting to add a function overload with a duplicate signature. #[error("could not add function overload: duplicate found for signature `{0:?}`")] DuplicateSignature(ArgumentSignature), + /// An attempt was made to add an overload with more than [`ArgCount::MAX_COUNT`] arguments. + /// + /// [`ArgCount::MAX_COUNT`]: crate::func::args::ArgCount #[error( "argument signature `{:?}` has too many arguments (max {})", 0, diff --git a/crates/bevy_reflect/src/func/info.rs b/crates/bevy_reflect/src/func/info.rs index 4b130e5772..2f5f82fbf5 100644 --- a/crates/bevy_reflect/src/func/info.rs +++ b/crates/bevy_reflect/src/func/info.rs @@ -235,6 +235,12 @@ impl TryFrom<[SignatureInfo; N]> for FunctionInfo { } } +/// Type information for the signature of a [`DynamicFunction`] or [`DynamicFunctionMut`]. +/// +/// Every [`FunctionInfo`] contains one or more [`SignatureInfo`]s. +/// +/// [`DynamicFunction`]: crate::func::DynamicFunction +/// [`DynamicFunctionMut`]: crate::func::DynamicFunctionMut #[derive(Debug, Clone)] pub struct SignatureInfo { name: Option>, diff --git a/crates/bevy_reflect/src/func/mod.rs b/crates/bevy_reflect/src/func/mod.rs index 74a89282c6..237bc9eafc 100644 --- a/crates/bevy_reflect/src/func/mod.rs +++ b/crates/bevy_reflect/src/func/mod.rs @@ -42,7 +42,7 @@ //! //! A "function" is a callable that does not capture its environment. //! These are typically defined with the `fn` keyword, which are referred to as _named_ functions. -//! But they are also _anonymous_ functions, which are unnamed and defined with anonymous function syntax. +//! But there are also _anonymous_ functions, which are unnamed and defined with anonymous function syntax. //! //! ```rust //! // This is a named function: diff --git a/crates/bevy_reflect/src/func/registry.rs b/crates/bevy_reflect/src/func/registry.rs index e476353b8b..08ed7bd7f1 100644 --- a/crates/bevy_reflect/src/func/registry.rs +++ b/crates/bevy_reflect/src/func/registry.rs @@ -336,6 +336,7 @@ impl Debug for FunctionRegistry { /// A synchronized wrapper around a [`FunctionRegistry`]. #[derive(Clone, Default, Debug)] pub struct FunctionRegistryArc { + /// The wrapped [`FunctionRegistry`]. pub internal: Arc>, } diff --git a/crates/bevy_reflect/src/impls/core/primitives.rs b/crates/bevy_reflect/src/impls/core/primitives.rs index 8bee33b4c8..3600f2ece5 100644 --- a/crates/bevy_reflect/src/impls/core/primitives.rs +++ b/crates/bevy_reflect/src/impls/core/primitives.rs @@ -282,6 +282,7 @@ impl GetTypeRegistration for &'static str { let mut registration = TypeRegistration::of::(); registration.insert::(FromType::::from_type()); registration.insert::(FromType::::from_type()); + registration.insert::(FromType::::from_type()); registration } } diff --git a/crates/bevy_reflect/src/kind.rs b/crates/bevy_reflect/src/kind.rs index 3eef10d0e5..3764e863ba 100644 --- a/crates/bevy_reflect/src/kind.rs +++ b/crates/bevy_reflect/src/kind.rs @@ -134,7 +134,9 @@ macro_rules! impl_reflect_kind_conversions { #[derive(Debug, Error)] #[error("kind mismatch: expected {expected:?}, received {received:?}")] pub struct ReflectKindMismatchError { + /// Expected kind. pub expected: ReflectKind, + /// Received kind. pub received: ReflectKind, } @@ -176,16 +178,46 @@ macro_rules! impl_cast_method { /// /// ["kinds"]: ReflectKind pub enum ReflectRef<'a> { + /// An immutable reference to a [struct-like] type. + /// + /// [struct-like]: Struct Struct(&'a dyn Struct), + /// An immutable reference to a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(&'a dyn TupleStruct), + /// An immutable reference to a [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(&'a dyn Tuple), + /// An immutable reference to a [list-like] type. + /// + /// [list-like]: List List(&'a dyn List), + /// An immutable reference to an [array-like] type. + /// + /// [array-like]: Array Array(&'a dyn Array), + /// An immutable reference to a [map-like] type. + /// + /// [map-like]: Map Map(&'a dyn Map), + /// An immutable reference to a [set-like] type. + /// + /// [set-like]: Set Set(&'a dyn Set), + /// An immutable reference to an [enum-like] type. + /// + /// [enum-like]: Enum Enum(&'a dyn Enum), + /// An immutable reference to a [function-like] type. + /// + /// [function-like]: Function #[cfg(feature = "functions")] Function(&'a dyn Function), + /// An immutable refeence to an [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(&'a dyn PartialReflect), } impl_reflect_kind_conversions!(ReflectRef<'_>); @@ -211,16 +243,46 @@ impl<'a> ReflectRef<'a> { /// /// ["kinds"]: ReflectKind pub enum ReflectMut<'a> { + /// A mutable reference to a [struct-like] type. + /// + /// [struct-like]: Struct Struct(&'a mut dyn Struct), + /// A mutable reference to a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(&'a mut dyn TupleStruct), + /// A mutable reference to a [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(&'a mut dyn Tuple), + /// A mutable reference to a [list-like] type. + /// + /// [list-like]: List List(&'a mut dyn List), + /// A mutable reference to an [array-like] type. + /// + /// [array-like]: Array Array(&'a mut dyn Array), + /// A mutable reference to a [map-like] type. + /// + /// [map-like]: Map Map(&'a mut dyn Map), + /// A mutable reference to a [set-like] type. + /// + /// [set-like]: Set Set(&'a mut dyn Set), + /// A mutable reference to an [enum-like] type. + /// + /// [enum-like]: Enum Enum(&'a mut dyn Enum), #[cfg(feature = "functions")] + /// A mutable reference to a [function-like] type. + /// + /// [function-like]: Function Function(&'a mut dyn Function), + /// A mutable refeence to an [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(&'a mut dyn PartialReflect), } impl_reflect_kind_conversions!(ReflectMut<'_>); @@ -246,16 +308,46 @@ impl<'a> ReflectMut<'a> { /// /// ["kinds"]: ReflectKind pub enum ReflectOwned { + /// An owned [struct-like] type. + /// + /// [struct-like]: Struct Struct(Box), + /// An owned [tuple-struct-like] type. + /// + /// [tuple-struct-like]: TupleStruct TupleStruct(Box), + /// An owned [tuple-like] type. + /// + /// [tuple-like]: Tuple Tuple(Box), + /// An owned [list-like] type. + /// + /// [list-like]: List List(Box), + /// An owned [array-like] type. + /// + /// [array-like]: Array Array(Box), + /// An owned [map-like] type. + /// + /// [map-like]: Map Map(Box), + /// An owned [set-like] type. + /// + /// [set-like]: Set Set(Box), + /// An owned [enum-like] type. + /// + /// [enum-like]: Enum Enum(Box), + /// An owned [function-like] type. + /// + /// [function-like]: Function #[cfg(feature = "functions")] Function(Box), + /// An owned [opaque] type. + /// + /// [opaque]: ReflectKind::Opaque Opaque(Box), } impl_reflect_kind_conversions!(ReflectOwned); diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index a4029946b1..e2503a05fb 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -1,4 +1,3 @@ -#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] #![cfg_attr( any(docsrs, docsrs_dep), expect( diff --git a/crates/bevy_reflect/src/map.rs b/crates/bevy_reflect/src/map.rs index 1a1fcefb63..20531569e8 100644 --- a/crates/bevy_reflect/src/map.rs +++ b/crates/bevy_reflect/src/map.rs @@ -192,6 +192,8 @@ impl MapInfo { impl_generic_info_methods!(generics); } +/// Used to produce an error message when an attempt is made to hash +/// a [`PartialReflect`] value that does not support hashing. #[macro_export] macro_rules! hash_error { ( $key:expr ) => {{ diff --git a/crates/bevy_reflect/src/reflect.rs b/crates/bevy_reflect/src/reflect.rs index 04e4a2a4b0..c1e283a5f4 100644 --- a/crates/bevy_reflect/src/reflect.rs +++ b/crates/bevy_reflect/src/reflect.rs @@ -21,32 +21,47 @@ pub enum ApplyError { #[error("attempted to apply `{from_kind}` to `{to_kind}`")] /// Attempted to apply the wrong [kind](ReflectKind) to a type, e.g. a struct to an enum. MismatchedKinds { + /// Kind of the value we attempted to apply. from_kind: ReflectKind, + /// Kind of the type we attempted to apply the value to. to_kind: ReflectKind, }, #[error("enum variant `{variant_name}` doesn't have a field named `{field_name}`")] /// Enum variant that we tried to apply to was missing a field. MissingEnumField { + /// Name of the enum variant. variant_name: Box, + /// Name of the missing field. field_name: Box, }, #[error("`{from_type}` is not `{to_type}`")] /// Tried to apply incompatible types. MismatchedTypes { + /// Type of the value we attempted to apply. from_type: Box, + /// Type we attempted to apply the value to. to_type: Box, }, #[error("attempted to apply type with {from_size} size to a type with {to_size} size")] - /// Attempted to apply to types with mismatched sizes, e.g. a [u8; 4] to [u8; 3]. - DifferentSize { from_size: usize, to_size: usize }, + /// Attempted to apply an [array-like] type to another of different size, e.g. a [u8; 4] to [u8; 3]. + /// + /// [array-like]: crate::Array + DifferentSize { + /// Size of the value we attempted to apply, in elements. + from_size: usize, + /// Size of the type we attempted to apply the value to, in elements. + to_size: usize, + }, #[error("variant with name `{variant_name}` does not exist on enum `{enum_name}`")] /// The enum we tried to apply to didn't contain a variant with the give name. UnknownVariant { + /// Name of the enum. enum_name: Box, + /// Name of the missing variant. variant_name: Box, }, } diff --git a/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs index f92a8e68e2..8a216f87b9 100644 --- a/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs +++ b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs @@ -42,6 +42,9 @@ use serde::Deserializer; /// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer /// [via the registry]: TypeRegistry::register_type_data pub trait DeserializeWithRegistry<'de>: Sized { + /// Deserialize this value using the given [Deserializer] and [`TypeRegistry`]. + /// + /// [`Deserializer`]: ::serde::Deserializer fn deserialize(deserializer: D, registry: &TypeRegistry) -> Result where D: Deserializer<'de>; diff --git a/crates/bevy_reflect/src/serde/de/registrations.rs b/crates/bevy_reflect/src/serde/de/registrations.rs index adc0025c54..768b8ed32f 100644 --- a/crates/bevy_reflect/src/serde/de/registrations.rs +++ b/crates/bevy_reflect/src/serde/de/registrations.rs @@ -15,6 +15,7 @@ pub struct TypeRegistrationDeserializer<'a> { } impl<'a> TypeRegistrationDeserializer<'a> { + /// Creates a new [`TypeRegistrationDeserializer`]. pub fn new(registry: &'a TypeRegistry) -> Self { Self { registry } } diff --git a/crates/bevy_reflect/src/serde/mod.rs b/crates/bevy_reflect/src/serde/mod.rs index 032590e0c7..2ee47d4a7f 100644 --- a/crates/bevy_reflect/src/serde/mod.rs +++ b/crates/bevy_reflect/src/serde/mod.rs @@ -1,3 +1,5 @@ +//! Serde integration for reflected types. + mod de; mod ser; mod type_data; diff --git a/crates/bevy_reflect/src/serde/ser/processor.rs b/crates/bevy_reflect/src/serde/ser/processor.rs index cf31ab7566..fc35ff883a 100644 --- a/crates/bevy_reflect/src/serde/ser/processor.rs +++ b/crates/bevy_reflect/src/serde/ser/processor.rs @@ -112,15 +112,15 @@ use crate::{PartialReflect, TypeRegistry}; /// } /// } /// -/// fn save(type_registry: &TypeRegistry, asset: &MyAsset) -> Result, AssetError> { -/// let mut asset_bytes = Vec::new(); +/// fn save(type_registry: &TypeRegistry, asset: &MyAsset) -> Result { +/// let mut asset_string = String::new(); /// /// let processor = HandleProcessor; /// let serializer = ReflectSerializer::with_processor(asset, type_registry, &processor); -/// let mut ron_serializer = ron::Serializer::new(&mut asset_bytes, None)?; +/// let mut ron_serializer = ron::Serializer::new(&mut asset_string, None)?; /// /// serializer.serialize(&mut ron_serializer)?; -/// Ok(asset_bytes) +/// Ok(asset_string) /// } /// ``` /// diff --git a/crates/bevy_reflect/src/serde/ser/serializable.rs b/crates/bevy_reflect/src/serde/ser/serializable.rs index 6a8a4c978f..c83737c842 100644 --- a/crates/bevy_reflect/src/serde/ser/serializable.rs +++ b/crates/bevy_reflect/src/serde/ser/serializable.rs @@ -3,7 +3,9 @@ use core::ops::Deref; /// A type-erased serializable value. pub enum Serializable<'a> { + /// An owned serializable value. Owned(Box), + /// An immutable reference to a serializable value. Borrowed(&'a dyn erased_serde::Serialize), } diff --git a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs index 9c5bfb06f1..f9e6370799 100644 --- a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs +++ b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs @@ -40,6 +40,9 @@ use serde::{Serialize, Serializer}; /// [`ReflectSerializer`]: crate::serde::ReflectSerializer /// [via the registry]: TypeRegistry::register_type_data pub trait SerializeWithRegistry { + /// Serialize this value using the given [Serializer] and [`TypeRegistry`]. + /// + /// [`Serializer`]: ::serde::Serializer fn serialize(&self, serializer: S, registry: &TypeRegistry) -> Result where S: Serializer; diff --git a/crates/bevy_reflect/src/std_traits.rs b/crates/bevy_reflect/src/std_traits.rs index cad001132b..9b7f46c300 100644 --- a/crates/bevy_reflect/src/std_traits.rs +++ b/crates/bevy_reflect/src/std_traits.rs @@ -1,3 +1,5 @@ +//! Module containing the [`ReflectDefault`] type. + use crate::{FromType, Reflect}; use alloc::boxed::Box; @@ -10,6 +12,7 @@ pub struct ReflectDefault { } impl ReflectDefault { + /// Returns the default value for a type. pub fn default(&self) -> Box { (self.default)() } diff --git a/crates/bevy_reflect/src/struct_trait.rs b/crates/bevy_reflect/src/struct_trait.rs index 4346f55e27..e419947b3a 100644 --- a/crates/bevy_reflect/src/struct_trait.rs +++ b/crates/bevy_reflect/src/struct_trait.rs @@ -71,6 +71,7 @@ pub trait Struct: PartialReflect { /// Returns an iterator over the values of the reflectable fields for this struct. fn iter_fields(&self) -> FieldIter; + /// Creates a new [`DynamicStruct`] from this struct. fn to_dynamic_struct(&self) -> DynamicStruct { let mut dynamic_struct = DynamicStruct::default(); dynamic_struct.set_represented_type(self.get_represented_type_info()); @@ -192,6 +193,7 @@ pub struct FieldIter<'a> { } impl<'a> FieldIter<'a> { + /// Creates a new [`FieldIter`]. pub fn new(value: &'a dyn Struct) -> Self { FieldIter { struct_val: value, diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index 8bdd08099b..51f402c698 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -76,6 +76,7 @@ pub struct TupleFieldIter<'a> { } impl<'a> TupleFieldIter<'a> { + /// Creates a new [`TupleFieldIter`]. pub fn new(value: &'a dyn Tuple) -> Self { TupleFieldIter { tuple: value, diff --git a/crates/bevy_reflect/src/tuple_struct.rs b/crates/bevy_reflect/src/tuple_struct.rs index ab5b99a96b..cceab9904e 100644 --- a/crates/bevy_reflect/src/tuple_struct.rs +++ b/crates/bevy_reflect/src/tuple_struct.rs @@ -146,6 +146,7 @@ pub struct TupleStructFieldIter<'a> { } impl<'a> TupleStructFieldIter<'a> { + /// Creates a new [`TupleStructFieldIter`]. pub fn new(value: &'a dyn TupleStruct) -> Self { TupleStructFieldIter { tuple_struct: value, diff --git a/crates/bevy_reflect/src/type_info.rs b/crates/bevy_reflect/src/type_info.rs index 1a3be15c36..122ace0293 100644 --- a/crates/bevy_reflect/src/type_info.rs +++ b/crates/bevy_reflect/src/type_info.rs @@ -169,7 +169,9 @@ pub enum TypeInfoError { /// [kind]: ReflectKind #[error("kind mismatch: expected {expected:?}, received {received:?}")] KindMismatch { + /// Expected kind. expected: ReflectKind, + /// Received kind. received: ReflectKind, }, } @@ -183,7 +185,7 @@ pub enum TypeInfoError { /// 3. [`PartialReflect::get_represented_type_info`] /// 4. [`TypeRegistry::get_type_info`] /// -/// Each return a static reference to [`TypeInfo`], but they all have their own use cases. +/// Each returns a static reference to [`TypeInfo`], but they all have their own use cases. /// For example, if you know the type at compile time, [`Typed::type_info`] is probably /// the simplest. If you have a `dyn Reflect` you can use [`DynamicTyped::reflect_type_info`]. /// If all you have is a `dyn PartialReflect`, you'll probably want [`PartialReflect::get_represented_type_info`]. @@ -199,14 +201,40 @@ pub enum TypeInfoError { /// [type path]: TypePath::type_path #[derive(Debug, Clone)] pub enum TypeInfo { + /// Type information for a [struct-like] type. + /// + /// [struct-like]: crate::Struct Struct(StructInfo), + /// Type information for a [tuple-struct-like] type. + /// + /// [tuple-struct-like]: crate::TupleStruct TupleStruct(TupleStructInfo), + /// Type information for a [tuple-like] type. + /// + /// [tuple-like]: crate::Tuple Tuple(TupleInfo), + /// Type information for a [list-like] type. + /// + /// [list-like]: crate::List List(ListInfo), + /// Type information for an [array-like] type. + /// + /// [array-like]: crate::Array Array(ArrayInfo), + /// Type information for a [map-like] type. + /// + /// [map-like]: crate::Map Map(MapInfo), + /// Type information for a [set-like] type. + /// + /// [set-like]: crate::Set Set(SetInfo), + /// Type information for an [enum-like] type. + /// + /// [enum-like]: crate::Enum Enum(EnumInfo), + /// Type information for an opaque type - see the [`OpaqueInfo`] docs for + /// a discussion of opaque types. Opaque(OpaqueInfo), } @@ -557,6 +585,7 @@ pub struct OpaqueInfo { } impl OpaqueInfo { + /// Creates a new [`OpaqueInfo`]. pub fn new() -> Self { Self { ty: Type::of::(), diff --git a/crates/bevy_reflect/src/type_registry.rs b/crates/bevy_reflect/src/type_registry.rs index 7b661f9ef2..22db84d580 100644 --- a/crates/bevy_reflect/src/type_registry.rs +++ b/crates/bevy_reflect/src/type_registry.rs @@ -38,6 +38,7 @@ pub struct TypeRegistry { /// A synchronized wrapper around a [`TypeRegistry`]. #[derive(Clone, Default)] pub struct TypeRegistryArc { + /// The wrapped [`TypeRegistry`]. pub internal: Arc>, } @@ -351,6 +352,7 @@ impl TypeRegistry { data.insert(D::from_type()); } + /// Whether the type with given [`TypeId`] has been registered in this registry. pub fn contains(&self, type_id: TypeId) -> bool { self.registrations.contains_key(&type_id) } @@ -722,6 +724,7 @@ impl Clone for TypeRegistration { /// /// [crate-level documentation]: crate pub trait TypeData: Downcast + Send + Sync { + /// Creates a type-erased clone of this value. fn clone_type_data(&self) -> Box; } impl_downcast!(TypeData); @@ -740,6 +743,7 @@ where /// This is used by the `#[derive(Reflect)]` macro to generate an implementation /// of [`TypeData`] to pass to [`TypeRegistration::insert`]. pub trait FromType { + /// Creates an instance of `Self` for type `T`. fn from_type() -> Self; } @@ -784,6 +788,8 @@ impl ReflectSerialize { /// [`FromType::from_type`]. #[derive(Clone)] pub struct ReflectDeserialize { + /// Function used by [`ReflectDeserialize::deserialize`] to + /// perform deserialization. pub func: fn( deserializer: &mut dyn erased_serde::Deserializer, ) -> Result, erased_serde::Error>, diff --git a/crates/bevy_reflect/src/utility.rs b/crates/bevy_reflect/src/utility.rs index 5735a29dbe..db8416bd6c 100644 --- a/crates/bevy_reflect/src/utility.rs +++ b/crates/bevy_reflect/src/utility.rs @@ -16,6 +16,7 @@ use core::{ /// /// [`Non`]: NonGenericTypeCell pub trait TypedProperty: sealed::Sealed { + /// The type of the value stored in [`GenericTypeCell`]. type Stored: 'static; } @@ -201,7 +202,7 @@ impl Default for NonGenericTypeCell { /// static CELL: GenericTypePathCell = GenericTypePathCell::new(); /// CELL.get_or_insert::(|| format!("my_crate::foo::Foo<{}>", T::type_path())) /// } -/// +/// /// fn short_type_path() -> &'static str { /// static CELL: GenericTypePathCell = GenericTypePathCell::new(); /// CELL.get_or_insert::(|| format!("Foo<{}>", T::short_type_path())) diff --git a/crates/bevy_remote/Cargo.toml b/crates/bevy_remote/Cargo.toml index ca84c2916e..28e0fc6f2b 100644 --- a/crates/bevy_remote/Cargo.toml +++ b/crates/bevy_remote/Cargo.toml @@ -9,8 +9,9 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["http"] +default = ["http", "bevy_asset"] http = ["dep:async-io", "dep:smol-hyper"] +bevy_asset = ["dep:bevy_asset"] [dependencies] # bevy @@ -21,11 +22,14 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", features = [ ] } bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.16.0-dev" } -bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev" } +bevy_utils = { path = "../bevy_utils", version = "0.16.0-dev", features = [ + "debug", +] } bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ "std", "serialize", ] } +bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev", optional = true } # other anyhow = "1" diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 2f7b912a3d..0bf3f9773b 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -8,9 +8,9 @@ use bevy_ecs::{ entity::Entity, event::EventCursor, hierarchy::ChildOf, + lifecycle::RemovedComponentEntity, query::QueryBuilder, reflect::{AppTypeRegistry, ReflectComponent, ReflectResource}, - removal_detection::RemovedComponentEntity, system::{In, Local}, world::{EntityRef, EntityWorldMut, FilteredEntityRef, World}, }; @@ -24,7 +24,10 @@ use serde_json::{Map, Value}; use crate::{ error_codes, - schemas::{json_schema::JsonSchemaBevyType, open_rpc::OpenRpcDocument}, + schemas::{ + json_schema::{export_type, JsonSchemaBevyType}, + open_rpc::OpenRpcDocument, + }, BrpError, BrpResult, }; @@ -1130,7 +1133,7 @@ pub fn process_remote_list_request(In(params): In>, world: &World) let Some(component_info) = world.components().get_info(component_id) else { continue; }; - response.push(component_info.name().to_owned()); + response.push(component_info.name().to_string()); } } // If `None`, list all registered components. @@ -1189,7 +1192,7 @@ pub fn process_remote_list_watching_request( let Some(component_info) = world.components().get_info(component_id) else { continue; }; - response.added.push(component_info.name().to_owned()); + response.added.push(component_info.name().to_string()); } } @@ -1202,7 +1205,7 @@ pub fn process_remote_list_watching_request( let Some(component_info) = world.components().get_info(*component_id) else { continue; }; - response.removed.push(component_info.name().to_owned()); + response.removed.push(component_info.name().to_string()); } } } @@ -1223,24 +1226,27 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br Some(params) => parse(params)?, }; + let extra_info = world.resource::(); let types = world.resource::(); let types = types.read(); let schemas = types .iter() - .map(crate::schemas::json_schema::export_type) - .filter(|(_, schema)| { - if let Some(crate_name) = &schema.crate_name { + .filter_map(|type_reg| { + let path_table = type_reg.type_info().type_path_table(); + if let Some(crate_name) = &path_table.crate_name() { if !filter.with_crates.is_empty() && !filter.with_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } if !filter.without_crates.is_empty() && filter.without_crates.iter().any(|c| crate_name.eq(c)) { - return false; + return None; } } + let (id, schema) = export_type(type_reg, extra_info); + if !filter.type_limit.with.is_empty() && !filter .type_limit @@ -1248,7 +1254,7 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } if !filter.type_limit.without.is_empty() && filter @@ -1257,10 +1263,9 @@ pub fn export_registry_types(In(params): In>, world: &World) -> Br .iter() .any(|c| schema.reflect_types.iter().any(|cc| c.eq(cc))) { - return false; + return None; } - - true + Some((id.to_string(), schema)) }) .collect::>(); diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 97b2e453e7..348be8089d 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -364,6 +364,8 @@ //! [fully-qualified type names]: bevy_reflect::TypePath::type_path //! [fully-qualified type name]: bevy_reflect::TypePath::type_path +extern crate alloc; + use async_channel::{Receiver, Sender}; use bevy_app::{prelude::*, MainScheduleOrder}; use bevy_derive::{Deref, DerefMut}; @@ -539,6 +541,7 @@ impl Plugin for RemotePlugin { .insert_after(Last, RemoteLast); app.insert_resource(remote_methods) + .init_resource::() .init_resource::() .add_systems(PreStartup, setup_mailbox_channel) .configure_sets( diff --git a/crates/bevy_remote/src/schemas/json_schema.rs b/crates/bevy_remote/src/schemas/json_schema.rs index 3fcc588f92..4e56625bc8 100644 --- a/crates/bevy_remote/src/schemas/json_schema.rs +++ b/crates/bevy_remote/src/schemas/json_schema.rs @@ -1,47 +1,63 @@ //! Module with JSON Schema type for Bevy Registry Types. //! It tries to follow this standard: -use bevy_ecs::reflect::{ReflectComponent, ReflectResource}; +use alloc::borrow::Cow; use bevy_platform::collections::HashMap; use bevy_reflect::{ - prelude::ReflectDefault, NamedField, OpaqueInfo, ReflectDeserialize, ReflectSerialize, - TypeInfo, TypeRegistration, VariantInfo, + GetTypeRegistration, NamedField, OpaqueInfo, TypeInfo, TypeRegistration, TypeRegistry, + VariantInfo, }; use core::any::TypeId; use serde::{Deserialize, Serialize}; use serde_json::{json, Map, Value}; -/// Exports schema info for a given type -pub fn export_type(reg: &TypeRegistration) -> (String, JsonSchemaBevyType) { - (reg.type_info().type_path().to_owned(), reg.into()) -} +use crate::schemas::SchemaTypesMetadata; -fn get_registered_reflect_types(reg: &TypeRegistration) -> Vec { - // Vec could be moved to allow registering more types by game maker. - let registered_reflect_types: [(TypeId, &str); 5] = [ - { (TypeId::of::(), "Component") }, - { (TypeId::of::(), "Resource") }, - { (TypeId::of::(), "Default") }, - { (TypeId::of::(), "Serialize") }, - { (TypeId::of::(), "Deserialize") }, - ]; - let mut result = Vec::new(); - for (id, name) in registered_reflect_types { - if reg.data_by_id(id).is_some() { - result.push(name.to_owned()); - } +/// Helper trait for converting `TypeRegistration` to `JsonSchemaBevyType` +pub trait TypeRegistrySchemaReader { + /// Export type JSON Schema. + fn export_type_json_schema( + &self, + extra_info: &SchemaTypesMetadata, + ) -> Option { + self.export_type_json_schema_for_id(extra_info, TypeId::of::()) } - result + /// Export type JSON Schema. + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option; } -impl From<&TypeRegistration> for JsonSchemaBevyType { - fn from(reg: &TypeRegistration) -> Self { +impl TypeRegistrySchemaReader for TypeRegistry { + fn export_type_json_schema_for_id( + &self, + extra_info: &SchemaTypesMetadata, + type_id: TypeId, + ) -> Option { + let type_reg = self.get(type_id)?; + Some((type_reg, extra_info).into()) + } +} + +/// Exports schema info for a given type +pub fn export_type( + reg: &TypeRegistration, + metadata: &SchemaTypesMetadata, +) -> (Cow<'static, str>, JsonSchemaBevyType) { + (reg.type_info().type_path().into(), (reg, metadata).into()) +} + +impl From<(&TypeRegistration, &SchemaTypesMetadata)> for JsonSchemaBevyType { + fn from(value: (&TypeRegistration, &SchemaTypesMetadata)) -> Self { + let (reg, metadata) = value; let t = reg.type_info(); let binding = t.type_path_table(); let short_path = binding.short_path(); let type_path = binding.path(); let mut typed_schema = JsonSchemaBevyType { - reflect_types: get_registered_reflect_types(reg), + reflect_types: metadata.get_registered_reflect_types(reg), short_path: short_path.to_owned(), type_path: type_path.to_owned(), crate_name: binding.crate_name().map(str::to_owned), @@ -351,8 +367,12 @@ impl SchemaJsonReference for &NamedField { #[cfg(test)] mod tests { use super::*; + use bevy_ecs::prelude::ReflectComponent; + use bevy_ecs::prelude::ReflectResource; + use bevy_ecs::{component::Component, reflect::AppTypeRegistry, resource::Resource}; - use bevy_reflect::Reflect; + use bevy_reflect::prelude::ReflectDefault; + use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; #[test] fn reflect_export_struct() { @@ -373,7 +393,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), @@ -418,7 +438,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -453,7 +473,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( !schema.reflect_types.contains(&"Component".to_owned()), "Should not be a component" @@ -466,6 +486,62 @@ mod tests { assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); } + #[test] + fn reflect_struct_with_custom_type_data() { + #[derive(Reflect, Default, Deserialize, Serialize)] + #[reflect(Default)] + enum EnumComponent { + ValueOne(i32), + ValueTwo { + test: i32, + }, + #[default] + NoValue, + } + + #[derive(Clone)] + pub struct ReflectCustomData; + + impl bevy_reflect::FromType for ReflectCustomData { + fn from_type() -> Self { + ReflectCustomData + } + } + + let atr = AppTypeRegistry::default(); + { + let mut register = atr.write(); + register.register::(); + register.register_type_data::(); + } + let mut metadata = SchemaTypesMetadata::default(); + metadata.map_type_data::("CustomData"); + let type_registry = atr.read(); + let foo_registration = type_registry + .get(TypeId::of::()) + .expect("SHOULD BE REGISTERED") + .clone(); + let (_, schema) = export_type(&foo_registration, &metadata); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a component" + ); + assert!( + !metadata.has_type_data::(&schema.reflect_types), + "Should not be a resource" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have default" + ); + assert!( + metadata.has_type_data::(&schema.reflect_types), + "Should have CustomData" + ); + assert!(schema.properties.is_empty(), "Should not have any field"); + assert!(schema.one_of.len() == 3, "Should have 3 possible schemas"); + } + #[test] fn reflect_export_tuple_struct() { #[derive(Reflect, Component, Default, Deserialize, Serialize)] @@ -482,7 +558,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); assert!( schema.reflect_types.contains(&"Component".to_owned()), "Should be a component" @@ -513,7 +589,7 @@ mod tests { .get(TypeId::of::()) .expect("SHOULD BE REGISTERED") .clone(); - let (_, schema) = export_type(&foo_registration); + let (_, schema) = export_type(&foo_registration, &SchemaTypesMetadata::default()); let schema_as_value = serde_json::to_value(&schema).expect("Should serialize"); let value = json!({ "shortPath": "Foo", @@ -538,6 +614,31 @@ mod tests { "a" ] }); - assert_eq!(schema_as_value, value); + assert_normalized_values(schema_as_value, value); + } + + /// This function exist to avoid false failures due to ordering differences between `serde_json` values. + fn assert_normalized_values(mut one: Value, mut two: Value) { + normalize_json(&mut one); + normalize_json(&mut two); + assert_eq!(one, two); + + /// Recursively sorts arrays in a `serde_json::Value` + fn normalize_json(value: &mut Value) { + match value { + Value::Array(arr) => { + for v in arr.iter_mut() { + normalize_json(v); + } + arr.sort_by_key(ToString::to_string); // Sort by stringified version + } + Value::Object(map) => { + for (_k, v) in map.iter_mut() { + normalize_json(v); + } + } + _ => {} + } + } } } diff --git a/crates/bevy_remote/src/schemas/mod.rs b/crates/bevy_remote/src/schemas/mod.rs index 7104fd5547..10cb2e9421 100644 --- a/crates/bevy_remote/src/schemas/mod.rs +++ b/crates/bevy_remote/src/schemas/mod.rs @@ -1,4 +1,68 @@ //! Module with schemas used for various BRP endpoints +use bevy_ecs::{ + reflect::{ReflectComponent, ReflectResource}, + resource::Resource, +}; +use bevy_platform::collections::HashMap; +use bevy_reflect::{ + prelude::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize, TypeData, + TypeRegistration, +}; +use core::any::TypeId; pub mod json_schema; pub mod open_rpc; + +/// Holds mapping of reflect [type data](TypeData) to strings, +/// later on used in Bevy Json Schema. +#[derive(Debug, Resource, Reflect)] +#[reflect(Resource)] +pub struct SchemaTypesMetadata { + /// Type Data id mapping to strings. + pub type_data_map: HashMap, +} + +impl Default for SchemaTypesMetadata { + fn default() -> Self { + let mut data_types = Self { + type_data_map: Default::default(), + }; + data_types.map_type_data::("Component"); + data_types.map_type_data::("Resource"); + data_types.map_type_data::("Default"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("Asset"); + #[cfg(feature = "bevy_asset")] + data_types.map_type_data::("AssetHandle"); + data_types.map_type_data::("Serialize"); + data_types.map_type_data::("Deserialize"); + data_types + } +} + +impl SchemaTypesMetadata { + /// Map `TypeId` of `TypeData` to string + pub fn map_type_data(&mut self, name: impl Into) { + self.type_data_map.insert(TypeId::of::(), name.into()); + } + + /// Build reflect types list for a given type registration + pub fn get_registered_reflect_types(&self, reg: &TypeRegistration) -> Vec { + self.type_data_map + .iter() + .filter_map(|(id, name)| reg.data_by_id(*id).and(Some(name.clone()))) + .collect() + } + + /// Checks if slice contains string value that matches checked `TypeData` + pub fn has_type_data(&self, types_string_slice: &[String]) -> bool { + self.has_type_data_by_id(TypeId::of::(), types_string_slice) + } + + /// Checks if slice contains string value that matches checked `TypeData` by id. + pub fn has_type_data_by_id(&self, id: TypeId, types_string_slice: &[String]) -> bool { + self.type_data_map + .get(&id) + .is_some_and(|data_s| types_string_slice.iter().any(|e| e.eq(data_s))) + } +} diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index e844d950be..d9775e9c8f 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -119,7 +119,7 @@ wesl = { version = "0.1.2", optional = true } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # Omit the `glsl` feature in non-WebAssembly by default. -naga_oil = { version = "0.17", default-features = false, features = [ +naga_oil = { version = "0.17.1", default-features = false, features = [ "test_shader", ] } @@ -127,7 +127,7 @@ naga_oil = { version = "0.17", default-features = false, features = [ proptest = "1" [target.'cfg(target_arch = "wasm32")'.dependencies] -naga_oil = "0.17" +naga_oil = "0.17.1" js-sys = "0.3" web-sys = { version = "0.3.67", features = [ 'Blob', diff --git a/crates/bevy_render/macros/src/extract_component.rs b/crates/bevy_render/macros/src/extract_component.rs index 2bfd0e0e11..8526f7b889 100644 --- a/crates/bevy_render/macros/src/extract_component.rs +++ b/crates/bevy_render/macros/src/extract_component.rs @@ -43,7 +43,7 @@ pub fn derive_extract_component(input: TokenStream) -> TokenStream { type QueryFilter = #filter; type Out = Self; - fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, Self::QueryData>) -> Option { + fn extract_component(item: #bevy_ecs_path::query::QueryItem<'_, '_, Self::QueryData>) -> Option { Some(item.clone()) } } diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index f19f57fa5f..2bddcd0d05 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -22,9 +22,10 @@ use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, - component::{Component, HookContext}, + component::Component, entity::{ContainsEntity, Entity}, event::EventReader, + lifecycle::HookContext, prelude::With, query::Has, reflect::ReflectComponent, @@ -600,8 +601,7 @@ impl Camera { rect_relative.y = 1.0 - rect_relative.y; let ndc = rect_relative * 2. - Vec2::ONE; - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.clip_from_view.inverse(); + let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); let world_near_plane = ndc_to_world.project_point3(ndc.extend(1.)); // Using EPSILON because an ndc with Z = 0 returns NaNs. let world_far_plane = ndc_to_world.project_point3(ndc.extend(f32::EPSILON)); @@ -667,7 +667,7 @@ impl Camera { ) -> Option { // Build a transformation matrix to convert from world space to NDC using camera data let clip_from_world: Mat4 = - self.computed.clip_from_view * camera_transform.compute_matrix().inverse(); + self.computed.clip_from_view * camera_transform.to_matrix().inverse(); let ndc_space_coords: Vec3 = clip_from_world.project_point3(world_position); (!ndc_space_coords.is_nan()).then_some(ndc_space_coords) @@ -688,8 +688,7 @@ impl Camera { /// Will panic if the projection matrix is invalid (has a determinant of 0) and `glam_assert` is enabled. pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option { // Build a transformation matrix to convert from NDC to world space using camera data - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.clip_from_view.inverse(); + let ndc_to_world = camera_transform.to_matrix() * self.computed.clip_from_view.inverse(); let world_space_coords = ndc_to_world.project_point3(ndc); @@ -715,12 +714,13 @@ impl Camera { } } -/// Control how this camera outputs once rendering is completed. +/// Control how this [`Camera`] outputs once rendering is completed. #[derive(Debug, Clone, Copy)] pub enum CameraOutputMode { /// Writes the camera output to configured render target. Write { /// The blend state that will be used by the pipeline that writes the intermediate render textures to the final render target texture. + /// If not set, the output will be written as-is, ignoring `clear_color` and the existing data in the final render target texture. blend_state: Option, /// The clear color operation to perform on the final render target texture. clear_color: ClearColorConfig, @@ -1060,6 +1060,7 @@ pub fn camera_system( #[reflect(opaque)] #[reflect(Component, Default, Clone)] pub struct CameraMainTextureUsages(pub TextureUsages); + impl Default for CameraMainTextureUsages { fn default() -> Self { Self( @@ -1070,6 +1071,13 @@ impl Default for CameraMainTextureUsages { } } +impl CameraMainTextureUsages { + pub fn with(mut self, usages: TextureUsages) -> Self { + self.0 |= usages; + self + } +} + #[derive(Component, Debug)] pub struct ExtractedCamera { pub target: Option, diff --git a/crates/bevy_render/src/camera/clear_color.rs b/crates/bevy_render/src/camera/clear_color.rs index 157bcf8998..6183a1d4de 100644 --- a/crates/bevy_render/src/camera/clear_color.rs +++ b/crates/bevy_render/src/camera/clear_color.rs @@ -6,7 +6,9 @@ use bevy_reflect::prelude::*; use derive_more::derive::From; use serde::{Deserialize, Serialize}; -/// For a camera, specifies the color used to clear the viewport before rendering. +/// For a camera, specifies the color used to clear the viewport +/// [before rendering](crate::camera::Camera::clear_color) +/// or when [writing to the final render target texture](crate::camera::Camera::output_mode). #[derive(Reflect, Serialize, Deserialize, Copy, Clone, Debug, Default, From)] #[reflect(Serialize, Deserialize, Default, Clone)] pub enum ClearColorConfig { @@ -21,10 +23,15 @@ pub enum ClearColorConfig { None, } -/// A [`Resource`] that stores the color that is used to clear the screen between frames. +/// A [`Resource`] that stores the default color that cameras use to clear the screen between frames. /// /// This color appears as the "background" color for simple apps, /// when there are portions of the screen with nothing rendered. +/// +/// Individual cameras may use [`Camera.clear_color`] to specify a different +/// clear color or opt out of clearing their viewport. +/// +/// [`Camera.clear_color`]: crate::camera::Camera::clear_color #[derive(Resource, Clone, Debug, Deref, DerefMut, ExtractResource, Reflect)] #[reflect(Resource, Default, Debug, Clone)] pub struct ClearColor(pub Color); diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index a7796a1d1a..ee2a5080d2 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -93,8 +93,7 @@ pub trait CameraProjection { /// This code is called by [`update_frusta`](crate::view::visibility::update_frusta) system /// for each camera to update its frustum. fn compute_frustum(&self, camera_transform: &GlobalTransform) -> Frustum { - let clip_from_world = - self.get_clip_from_view() * camera_transform.compute_matrix().inverse(); + let clip_from_world = self.get_clip_from_view() * camera_transform.to_matrix().inverse(); Frustum::from_clip_from_world_custom_far( &clip_from_world, &camera_transform.translation(), diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index e1f528d6ab..b7bb05e425 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -60,7 +60,7 @@ pub trait ExtractComponent: Component { // type Out: Component = Self; /// Defines how the component is transferred into the "render world". - fn extract_component(item: QueryItem<'_, Self::QueryData>) -> Option; + fn extract_component(item: QueryItem<'_, '_, Self::QueryData>) -> Option; } /// This plugin prepares the components of the corresponding type for the GPU diff --git a/crates/bevy_render/src/extract_instances.rs b/crates/bevy_render/src/extract_instances.rs index a8e5a9ecbd..cd194b7c40 100644 --- a/crates/bevy_render/src/extract_instances.rs +++ b/crates/bevy_render/src/extract_instances.rs @@ -34,7 +34,7 @@ pub trait ExtractInstance: Send + Sync + Sized + 'static { type QueryFilter: QueryFilter; /// Defines how the component is transferred into the "render world". - fn extract(item: QueryItem<'_, Self::QueryData>) -> Option; + fn extract(item: QueryItem<'_, '_, Self::QueryData>) -> Option; } /// This plugin extracts one or more components into the "render world" as diff --git a/crates/bevy_render/src/extract_param.rs b/crates/bevy_render/src/extract_param.rs index ac6b04ff0f..e97c758260 100644 --- a/crates/bevy_render/src/extract_param.rs +++ b/crates/bevy_render/src/extract_param.rs @@ -1,7 +1,8 @@ use crate::MainWorld; use bevy_ecs::{ - component::Tick, + component::{ComponentId, Tick}, prelude::*, + query::FilteredAccessSet, system::{ ReadOnlySystemParam, SystemMeta, SystemParam, SystemParamItem, SystemParamValidationError, SystemState, @@ -71,14 +72,28 @@ where type State = ExtractState

; type Item<'w, 's> = Extract<'w, 's, P>; - fn init_state(world: &mut World, system_meta: &mut SystemMeta) -> Self::State { + fn init_state(world: &mut World) -> Self::State { let mut main_world = world.resource_mut::(); ExtractState { state: SystemState::new(&mut main_world), - main_world_state: Res::::init_state(world, system_meta), + main_world_state: Res::::init_state(world), } } + fn init_access( + state: &Self::State, + system_meta: &mut SystemMeta, + component_access_set: &mut FilteredAccessSet, + world: &mut World, + ) { + Res::::init_access( + &state.main_world_state, + system_meta, + component_access_set, + world, + ); + } + #[inline] unsafe fn validate_param( state: &mut Self::State, diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index c05861f3da..adcb883559 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -15,14 +15,14 @@ use async_channel::{Receiver, Sender}; use bevy_app::{App, Plugin}; use bevy_asset::Handle; use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::schedule::IntoScheduleConfigs; use bevy_ecs::{ change_detection::ResMut, entity::Entity, - event::Event, + event::EntityEvent, prelude::{Component, Resource, World}, system::{Query, Res}, }; +use bevy_ecs::{event::Event, schedule::IntoScheduleConfigs}; use bevy_image::{Image, TextureFormatPixelInfo}; use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; @@ -96,7 +96,7 @@ impl Readback { /// /// The event contains the data as a `Vec`, which can be interpreted as the raw bytes of the /// requested buffer or texture. -#[derive(Event, Deref, DerefMut, Reflect, Debug)] +#[derive(Event, EntityEvent, Deref, DerefMut, Reflect, Debug)] #[reflect(Debug)] pub struct ReadbackComplete(pub Vec); diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index eb2d4de626..c171cf3957 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -78,6 +78,9 @@ pub struct MeshAllocator { /// WebGL 2. On this platform, we must give each vertex array its own /// buffer, because we can't adjust the first vertex when we perform a draw. general_vertex_slabs_supported: bool, + + /// Additional buffer usages to add to any vertex or index buffers created. + pub extra_buffer_usages: BufferUsages, } /// Tunable parameters that customize the behavior of the allocator. @@ -348,6 +351,7 @@ impl FromWorld for MeshAllocator { mesh_id_to_index_slab: HashMap::default(), next_slab_id: default(), general_vertex_slabs_supported, + extra_buffer_usages: BufferUsages::empty(), } } } @@ -598,7 +602,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: len as u64, - usage: buffer_usages | BufferUsages::COPY_DST, + usage: buffer_usages | BufferUsages::COPY_DST | self.extra_buffer_usages, mapped_at_creation: true, }); { @@ -835,7 +839,7 @@ impl MeshAllocator { buffer_usages_to_str(buffer_usages) )), size: slab.current_slot_capacity as u64 * slab.element_layout.slot_size(), - usage: buffer_usages, + usage: buffer_usages | self.extra_buffer_usages, mapped_at_creation: false, }); diff --git a/crates/bevy_render/src/render_graph/node.rs b/crates/bevy_render/src/render_graph/node.rs index 0a634c2598..4355892487 100644 --- a/crates/bevy_render/src/render_graph/node.rs +++ b/crates/bevy_render/src/render_graph/node.rs @@ -366,7 +366,7 @@ pub trait ViewNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - view_query: QueryItem<'w, Self::ViewQuery>, + view_query: QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError>; } diff --git a/crates/bevy_render/src/render_phase/draw.rs b/crates/bevy_render/src/render_phase/draw.rs index 374dc6b330..42a6725c8c 100644 --- a/crates/bevy_render/src/render_phase/draw.rs +++ b/crates/bevy_render/src/render_phase/draw.rs @@ -213,8 +213,8 @@ pub trait RenderCommand { /// issuing draw calls, etc.) via the [`TrackedRenderPass`]. fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, - entity: Option>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, + entity: Option>, param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult; @@ -246,8 +246,8 @@ macro_rules! render_command_tuple_impl { )] fn render<'w>( _item: &P, - ($($view,)*): ROQueryItem<'w, Self::ViewQuery>, - maybe_entities: Option>, + ($($view,)*): ROQueryItem<'w, '_, Self::ViewQuery>, + maybe_entities: Option>, ($($name,)*): SystemParamItem<'w, '_, Self::Param>, _pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { diff --git a/crates/bevy_render/src/render_resource/bind_group_entries.rs b/crates/bevy_render/src/render_resource/bind_group_entries.rs index 3aaf46183f..cc8eb188de 100644 --- a/crates/bevy_render/src/render_resource/bind_group_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_entries.rs @@ -147,6 +147,13 @@ impl<'a> IntoBinding<'a> for &'a TextureView { } } +impl<'a> IntoBinding<'a> for &'a wgpu::TextureView { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::TextureView(self) + } +} + impl<'a> IntoBinding<'a> for &'a [&'a wgpu::TextureView] { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -161,6 +168,13 @@ impl<'a> IntoBinding<'a> for &'a Sampler { } } +impl<'a> IntoBinding<'a> for &'a [&'a wgpu::Sampler] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::SamplerArray(self) + } +} + impl<'a> IntoBinding<'a> for BindingResource<'a> { #[inline] fn into_binding(self) -> BindingResource<'a> { @@ -175,6 +189,13 @@ impl<'a> IntoBinding<'a> for wgpu::BufferBinding<'a> { } } +impl<'a> IntoBinding<'a> for &'a [wgpu::BufferBinding<'a>] { + #[inline] + fn into_binding(self) -> BindingResource<'a> { + BindingResource::BufferArray(self) + } +} + pub trait IntoBindingArray<'b, const N: usize> { fn into_array(self) -> [BindingResource<'b>; N]; } diff --git a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs index bc4a7d306d..41affa4349 100644 --- a/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs +++ b/crates/bevy_render/src/render_resource/bind_group_layout_entries.rs @@ -568,4 +568,8 @@ pub mod binding_types { } .into_bind_group_layout_entry_builder() } + + pub fn acceleration_structure() -> BindGroupLayoutEntryBuilder { + BindingType::AccelerationStructure.into_bind_group_layout_entry_builder() + } } diff --git a/crates/bevy_render/src/render_resource/mod.rs b/crates/bevy_render/src/render_resource/mod.rs index b777d96290..aecf27173d 100644 --- a/crates/bevy_render/src/render_resource/mod.rs +++ b/crates/bevy_render/src/render_resource/mod.rs @@ -38,18 +38,21 @@ pub use wgpu::{ BufferInitDescriptor, DispatchIndirectArgs, DrawIndexedIndirectArgs, DrawIndirectArgs, TextureDataOrder, }, - AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, AstcChannel, BindGroupDescriptor, - BindGroupEntry, BindGroupLayoutDescriptor, BindGroupLayoutEntry, BindingResource, BindingType, + AccelerationStructureFlags, AccelerationStructureGeometryFlags, + AccelerationStructureUpdateMode, AdapterInfo as WgpuAdapterInfo, AddressMode, AstcBlock, + AstcChannel, BindGroupDescriptor, BindGroupEntry, BindGroupLayoutDescriptor, + BindGroupLayoutEntry, BindingResource, BindingType, Blas, BlasBuildEntry, BlasGeometries, + BlasGeometrySizeDescriptors, BlasTriangleGeometry, BlasTriangleGeometrySizeDescriptor, BlendComponent, BlendFactor, BlendOperation, BlendState, BufferAddress, BufferAsyncError, BufferBinding, BufferBindingType, BufferDescriptor, BufferSize, BufferUsages, ColorTargetState, ColorWrites, CommandEncoder, CommandEncoderDescriptor, CompareFunction, ComputePass, ComputePassDescriptor, ComputePipelineDescriptor as RawComputePipelineDescriptor, - DepthBiasState, DepthStencilState, DownlevelFlags, Extent3d, Face, Features as WgpuFeatures, - FilterMode, FragmentState as RawFragmentState, FrontFace, ImageSubresourceRange, IndexFormat, - Limits as WgpuLimits, LoadOp, Maintain, MapMode, MultisampleState, Operations, Origin3d, - PipelineCompilationOptions, PipelineLayout, PipelineLayoutDescriptor, PolygonMode, - PrimitiveState, PrimitiveTopology, PushConstantRange, RenderPassColorAttachment, - RenderPassDepthStencilAttachment, RenderPassDescriptor, + CreateBlasDescriptor, CreateTlasDescriptor, DepthBiasState, DepthStencilState, DownlevelFlags, + Extent3d, Face, Features as WgpuFeatures, FilterMode, FragmentState as RawFragmentState, + FrontFace, ImageSubresourceRange, IndexFormat, Limits as WgpuLimits, LoadOp, Maintain, MapMode, + MultisampleState, Operations, Origin3d, PipelineCompilationOptions, PipelineLayout, + PipelineLayoutDescriptor, PolygonMode, PrimitiveState, PrimitiveTopology, PushConstantRange, + RenderPassColorAttachment, RenderPassDepthStencilAttachment, RenderPassDescriptor, RenderPipelineDescriptor as RawRenderPipelineDescriptor, Sampler as WgpuSampler, SamplerBindingType, SamplerBindingType as WgpuSamplerBindingType, SamplerDescriptor, ShaderModule, ShaderModuleDescriptor, ShaderSource, ShaderStages, StencilFaceState, @@ -57,8 +60,9 @@ pub use wgpu::{ TexelCopyBufferLayout, TexelCopyTextureInfo, TextureAspect, TextureDescriptor, TextureDimension, TextureFormat, TextureFormatFeatureFlags, TextureFormatFeatures, TextureSampleType, TextureUsages, TextureView as WgpuTextureView, TextureViewDescriptor, - TextureViewDimension, VertexAttribute, VertexBufferLayout as RawVertexBufferLayout, - VertexFormat, VertexState as RawVertexState, VertexStepMode, COPY_BUFFER_ALIGNMENT, + TextureViewDimension, Tlas, TlasInstance, TlasPackage, VertexAttribute, + VertexBufferLayout as RawVertexBufferLayout, VertexFormat, VertexState as RawVertexState, + VertexStepMode, COPY_BUFFER_ALIGNMENT, }; pub use crate::mesh::VertexBufferLayout; diff --git a/crates/bevy_render/src/render_resource/pipeline_cache.rs b/crates/bevy_render/src/render_resource/pipeline_cache.rs index 416a83cd65..9d658cf361 100644 --- a/crates/bevy_render/src/render_resource/pipeline_cache.rs +++ b/crates/bevy_render/src/render_resource/pipeline_cache.rs @@ -1103,10 +1103,6 @@ fn create_pipeline_task( target_os = "macos", not(feature = "multi_threaded") ))] -#[expect( - clippy::large_enum_variant, - reason = "See https://github.com/bevyengine/bevy/issues/19220" -)] fn create_pipeline_task( task: impl Future> + Send + 'static, _sync: bool, @@ -1197,6 +1193,10 @@ fn get_capabilities(features: Features, downlevel: DownlevelFlags) -> Capabiliti Capabilities::MULTISAMPLED_SHADING, downlevel.contains(DownlevelFlags::MULTISAMPLED_SHADING), ); + capabilities.set( + Capabilities::RAY_QUERY, + features.contains(Features::EXPERIMENTAL_RAY_QUERY), + ); capabilities.set( Capabilities::DUAL_SOURCE_BLENDING, features.contains(Features::DUAL_SOURCE_BLENDING), diff --git a/crates/bevy_render/src/renderer/mod.rs b/crates/bevy_render/src/renderer/mod.rs index f2cfbcd9d0..bec30200a8 100644 --- a/crates/bevy_render/src/renderer/mod.rs +++ b/crates/bevy_render/src/renderer/mod.rs @@ -180,12 +180,6 @@ pub async fn initialize_renderer( features -= wgpu::Features::MAPPABLE_PRIMARY_BUFFERS; } - // RAY_QUERY and RAY_TRACING_ACCELERATION STRUCTURE will sometimes cause DeviceLost failures on platforms - // that report them as supported: - // - features -= wgpu::Features::EXPERIMENTAL_RAY_QUERY; - features -= wgpu::Features::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE; - limits = adapter.limits(); } diff --git a/crates/bevy_render/src/sync_world.rs b/crates/bevy_render/src/sync_world.rs index f6c2f87593..b216b5fd32 100644 --- a/crates/bevy_render/src/sync_world.rs +++ b/crates/bevy_render/src/sync_world.rs @@ -1,15 +1,16 @@ use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::entity::EntityHash; +use bevy_ecs::lifecycle::{Add, Remove}; use bevy_ecs::{ component::Component, entity::{ContainsEntity, Entity, EntityEquivalent}, - observer::Trigger, + observer::On, query::With, reflect::ReflectComponent, resource::Resource, system::{Local, Query, ResMut, SystemState}, - world::{Mut, OnAdd, OnRemove, World}, + world::{Mut, World}, }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; @@ -93,12 +94,12 @@ impl Plugin for SyncWorldPlugin { fn build(&self, app: &mut bevy_app::App) { app.init_resource::(); app.add_observer( - |trigger: Trigger, mut pending: ResMut| { + |trigger: On, mut pending: ResMut| { pending.push(EntityRecord::Added(trigger.target())); }, ); app.add_observer( - |trigger: Trigger, + |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { if let Ok(e) = query.get(trigger.target()) { @@ -219,10 +220,10 @@ pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut Worl EntityRecord::Added(e) => { if let Ok(mut main_entity) = world.get_entity_mut(e) { match main_entity.entry::() { - bevy_ecs::world::Entry::Occupied(_) => { + bevy_ecs::world::ComponentEntry::Occupied(_) => { panic!("Attempting to synchronize an entity that has already been synchronized!"); } - bevy_ecs::world::Entry::Vacant(entry) => { + bevy_ecs::world::ComponentEntry::Vacant(entry) => { let id = render_world.spawn(MainEntity(e)).id(); entry.insert(RenderEntity(id)); @@ -280,7 +281,7 @@ mod render_entities_world_query_impls { archetype::Archetype, component::{ComponentId, Components, Tick}, entity::Entity, - query::{FilteredAccess, QueryData, ReadOnlyQueryData, WorldQuery}, + query::{FilteredAccess, QueryData, ReadOnlyQueryData, ReleaseStateQueryData, WorldQuery}, storage::{Table, TableRow}, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -298,9 +299,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -313,9 +314,9 @@ mod render_entities_world_query_impls { const IS_DENSE: bool = <&'static RenderEntity as WorldQuery>::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, archetype: &'w Archetype, table: &'w Table, ) { @@ -326,9 +327,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. @@ -363,21 +364,24 @@ mod render_entities_world_query_impls { unsafe impl QueryData for RenderEntity { const IS_READ_ONLY: bool = true; type ReadOnly = RenderEntity; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Entity) -> Entity { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `RenderEntity`. let component = - unsafe { <&RenderEntity as QueryData>::fetch(fetch, entity, table_row) }; + unsafe { <&RenderEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.id() } } @@ -385,6 +389,12 @@ mod render_entities_world_query_impls { // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. unsafe impl ReadOnlyQueryData for RenderEntity {} + impl ReleaseStateQueryData for RenderEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } + /// SAFETY: defers completely to `&RenderEntity` implementation, /// and then only modifies the output safely. unsafe impl WorldQuery for MainEntity { @@ -398,9 +408,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn init_fetch<'w>( + unsafe fn init_fetch<'w, 's>( world: UnsafeWorldCell<'w>, - component_id: &ComponentId, + component_id: &'s ComponentId, last_run: Tick, this_run: Tick, ) -> Self::Fetch<'w> { @@ -413,7 +423,7 @@ mod render_entities_world_query_impls { const IS_DENSE: bool = <&'static MainEntity as WorldQuery>::IS_DENSE; #[inline] - unsafe fn set_archetype<'w>( + unsafe fn set_archetype<'w, 's>( fetch: &mut Self::Fetch<'w>, component_id: &ComponentId, archetype: &'w Archetype, @@ -426,9 +436,9 @@ mod render_entities_world_query_impls { } #[inline] - unsafe fn set_table<'w>( + unsafe fn set_table<'w, 's>( fetch: &mut Self::Fetch<'w>, - &component_id: &ComponentId, + &component_id: &'s ComponentId, table: &'w Table, ) { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. @@ -463,26 +473,36 @@ mod render_entities_world_query_impls { unsafe impl QueryData for MainEntity { const IS_READ_ONLY: bool = true; type ReadOnly = MainEntity; - type Item<'w> = Entity; + type Item<'w, 's> = Entity; - fn shrink<'wlong: 'wshort, 'wshort>(item: Entity) -> Entity { + fn shrink<'wlong: 'wshort, 'wshort, 's>( + item: Self::Item<'wlong, 's>, + ) -> Self::Item<'wshort, 's> { item } #[inline(always)] - unsafe fn fetch<'w>( + unsafe fn fetch<'w, 's>( + state: &'s Self::State, fetch: &mut Self::Fetch<'w>, entity: Entity, table_row: TableRow, - ) -> Self::Item<'w> { + ) -> Self::Item<'w, 's> { // SAFETY: defers to the `&T` implementation, with T set to `MainEntity`. - let component = unsafe { <&MainEntity as QueryData>::fetch(fetch, entity, table_row) }; + let component = + unsafe { <&MainEntity as QueryData>::fetch(state, fetch, entity, table_row) }; component.id() } } // SAFETY: the underlying `Entity` is copied, and no mutable access is provided. unsafe impl ReadOnlyQueryData for MainEntity {} + + impl ReleaseStateQueryData for MainEntity { + fn release_state<'w>(item: Self::Item<'w, '_>) -> Self::Item<'w, 'static> { + item + } + } } #[cfg(test)] @@ -490,10 +510,11 @@ mod tests { use bevy_ecs::{ component::Component, entity::Entity, - observer::Trigger, + lifecycle::{Add, Remove}, + observer::On, query::With, system::{Query, ResMut}, - world::{OnAdd, OnRemove, World}, + world::World, }; use super::{ @@ -511,12 +532,12 @@ mod tests { main_world.init_resource::(); main_world.add_observer( - |trigger: Trigger, mut pending: ResMut| { + |trigger: On, mut pending: ResMut| { pending.push(EntityRecord::Added(trigger.target())); }, ); main_world.add_observer( - |trigger: Trigger, + |trigger: On, mut pending: ResMut, query: Query<&RenderEntity>| { if let Ok(e) = query.get(trigger.target()) { diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index d25e8e7f49..b2b90d0b24 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -316,7 +316,7 @@ pub struct ExtractedView { impl ExtractedView { /// Creates a 3D rangefinder for a view pub fn rangefinder3d(&self) -> ViewRangefinder3d { - ViewRangefinder3d::from_world_from_view(&self.world_from_view.compute_matrix()) + ViewRangefinder3d::from_world_from_view(&self.world_from_view.to_matrix()) } } @@ -934,7 +934,7 @@ pub fn prepare_view_uniforms( } let view_from_clip = clip_from_view.inverse(); - let world_from_view = extracted_view.world_from_view.compute_matrix(); + let world_from_view = extracted_view.world_from_view.to_matrix(); let view_from_world = world_from_view.inverse(); let clip_from_world = if temporal_jitter.is_some() { diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 43533b5354..13b8ac74d4 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -3,8 +3,8 @@ mod render_layers; use core::any::TypeId; -use bevy_ecs::component::HookContext; use bevy_ecs::entity::EntityHashSet; +use bevy_ecs::lifecycle::HookContext; use bevy_ecs::world::DeferredWorld; use derive_more::derive::{Deref, DerefMut}; pub use range::*; diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index 2559d3b8d2..80f89ce936 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -10,9 +10,9 @@ use bevy_app::{App, Plugin, PostUpdate}; use bevy_ecs::{ component::Component, entity::{Entity, EntityHashMap}, + lifecycle::RemovedComponents, query::{Changed, With}, reflect::ReflectComponent, - removal_detection::RemovedComponents, resource::Resource, schedule::IntoScheduleConfigs as _, system::{Local, Query, Res, ResMut}, diff --git a/crates/bevy_render/src/view/window/screenshot.rs b/crates/bevy_render/src/view/window/screenshot.rs index 56d591a365..33b76d269d 100644 --- a/crates/bevy_render/src/view/window/screenshot.rs +++ b/crates/bevy_render/src/view/window/screenshot.rs @@ -39,7 +39,7 @@ use std::{ use tracing::{error, info, warn}; use wgpu::{CommandEncoder, Extent3d, TextureFormat}; -#[derive(Event, Deref, DerefMut, Reflect, Debug)] +#[derive(Event, EntityEvent, Deref, DerefMut, Reflect, Debug)] #[reflect(Debug)] pub struct ScreenshotCaptured(pub Image); @@ -122,7 +122,7 @@ struct RenderScreenshotsPrepared(EntityHashMap); struct RenderScreenshotsSender(Sender<(Entity, Image)>); /// Saves the captured screenshot to disk at the provided path. -pub fn save_to_disk(path: impl AsRef) -> impl FnMut(Trigger) { +pub fn save_to_disk(path: impl AsRef) -> impl FnMut(On) { let path = path.as_ref().to_owned(); move |trigger| { let img = trigger.event().deref().clone(); diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index 1d684c9dac..2293beef1e 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -93,7 +93,7 @@ impl Scene { type_registry .get(type_id) .ok_or_else(|| SceneSpawnError::UnregisteredType { - std_type_name: component_info.name().to_string(), + std_type_name: component_info.name(), })?; let reflect_resource = registration.data::().ok_or_else(|| { SceneSpawnError::UnregisteredResource { @@ -133,7 +133,7 @@ impl Scene { let registration = type_registry .get(component_info.type_id().unwrap()) .ok_or_else(|| SceneSpawnError::UnregisteredType { - std_type_name: component_info.name().to_string(), + std_type_name: component_info.name(), })?; let reflect_component = registration.data::().ok_or_else(|| { diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index 1daa0158b3..71cd848751 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -2,7 +2,7 @@ use crate::{DynamicScene, Scene}; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_ecs::{ entity::{Entity, EntityHashMap}, - event::{Event, EventCursor, Events}, + event::{EntityEvent, Event, EventCursor, Events}, hierarchy::ChildOf, reflect::AppTypeRegistry, resource::Resource, @@ -10,6 +10,7 @@ use bevy_ecs::{ }; use bevy_platform::collections::{HashMap, HashSet}; use bevy_reflect::Reflect; +use bevy_utils::prelude::DebugName; use thiserror::Error; use uuid::Uuid; @@ -22,10 +23,10 @@ use bevy_ecs::{ }; /// Triggered on a scene's parent entity when [`crate::SceneInstance`] becomes ready to use. /// -/// See also [`Trigger`], [`SceneSpawner::instance_is_ready`]. +/// See also [`On`], [`SceneSpawner::instance_is_ready`]. /// -/// [`Trigger`]: bevy_ecs::observer::Trigger -#[derive(Clone, Copy, Debug, Eq, PartialEq, Event, Reflect)] +/// [`On`]: bevy_ecs::observer::On +#[derive(Clone, Copy, Debug, Eq, PartialEq, Event, EntityEvent, Reflect)] #[reflect(Debug, PartialEq, Clone)] pub struct SceneInstanceReady { /// Instance which has been spawned. @@ -105,7 +106,7 @@ pub enum SceneSpawnError { )] UnregisteredType { /// The [type name](std::any::type_name) for the unregistered type. - std_type_name: String, + std_type_name: DebugName, }, /// Scene contains an unregistered type which has a `TypePath`. #[error( @@ -542,7 +543,7 @@ mod tests { use bevy_ecs::{ component::Component, hierarchy::Children, - observer::Trigger, + observer::On, prelude::ReflectComponent, query::With, system::{Commands, Query, Res, ResMut, RunSystemOnce}, @@ -721,10 +722,10 @@ mod tests { .expect("Failed to run dynamic scene builder system.") } - fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Entity) { + fn observe_trigger(app: &mut App, scene_id: InstanceId, scene_entity: Option) { // Add observer app.world_mut().add_observer( - move |trigger: Trigger, + move |trigger: On, scene_spawner: Res, mut trigger_count: ResMut| { assert_eq!( @@ -734,7 +735,7 @@ mod tests { ); assert_eq!( trigger.target(), - scene_entity, + scene_entity.unwrap_or(Entity::PLACEHOLDER), "`SceneInstanceReady` triggered on the wrong parent entity" ); assert!( @@ -773,7 +774,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -792,7 +793,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, Entity::PLACEHOLDER); + observe_trigger(&mut app, scene_id, None); } #[test] @@ -816,7 +817,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] @@ -840,7 +841,7 @@ mod tests { .unwrap(); // Check trigger. - observe_trigger(&mut app, scene_id, scene_entity); + observe_trigger(&mut app, scene_id, Some(scene_entity)); } #[test] diff --git a/crates/bevy_solari/Cargo.toml b/crates/bevy_solari/Cargo.toml new file mode 100644 index 0000000000..85cf85bd49 --- /dev/null +++ b/crates/bevy_solari/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "bevy_solari" +version = "0.16.0-dev" +edition = "2024" +description = "Provides raytraced lighting for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.16.0-dev" } +bevy_asset = { path = "../bevy_asset", version = "0.16.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.16.0-dev" } +bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.16.0-dev" } +bevy_derive = { path = "../bevy_derive", version = "0.16.0-dev" } +bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } +bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } +bevy_mesh = { path = "../bevy_mesh", version = "0.16.0-dev" } +bevy_pbr = { path = "../bevy_pbr", version = "0.16.0-dev" } +bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false, features = [ + "std", +] } +bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev" } +bevy_render = { path = "../bevy_render", version = "0.16.0-dev" } +bevy_transform = { path = "../bevy_transform", version = "0.16.0-dev" } + +# other +tracing = { version = "0.1", default-features = false, features = ["std"] } +derive_more = { version = "1", default-features = false, features = ["from"] } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_solari/LICENSE-APACHE b/crates/bevy_solari/LICENSE-APACHE new file mode 100644 index 0000000000..d9a10c0d8e --- /dev/null +++ b/crates/bevy_solari/LICENSE-APACHE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/crates/bevy_solari/LICENSE-MIT b/crates/bevy_solari/LICENSE-MIT new file mode 100644 index 0000000000..9cf106272a --- /dev/null +++ b/crates/bevy_solari/LICENSE-MIT @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/crates/bevy_solari/README.md b/crates/bevy_solari/README.md new file mode 100644 index 0000000000..089418e60d --- /dev/null +++ b/crates/bevy_solari/README.md @@ -0,0 +1,9 @@ +# Bevy Solari + +[![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) +[![Crates.io](https://img.shields.io/crates/v/bevy_solari.svg)](https://crates.io/crates/bevy_solari) +[![Downloads](https://img.shields.io/crates/d/bevy_solari.svg)](https://crates.io/crates/bevy_solari) +[![Docs](https://docs.rs/bevy_solari/badge.svg)](https://docs.rs/bevy_solari/latest/bevy_solari/) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) + +![Logo](../../assets/branding/bevy_solari.svg) diff --git a/crates/bevy_solari/src/lib.rs b/crates/bevy_solari/src/lib.rs new file mode 100644 index 0000000000..022f44b5c5 --- /dev/null +++ b/crates/bevy_solari/src/lib.rs @@ -0,0 +1,52 @@ +#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] + +//! Provides raytraced lighting. +//! +//! See [`SolariPlugin`] for more info. +//! +//! ![`bevy_solari` logo](https://raw.githubusercontent.com/bevyengine/bevy/assets/branding/bevy_solari.svg) +pub mod pathtracer; +pub mod scene; + +/// The solari prelude. +/// +/// This includes the most common types in this crate, re-exported for your convenience. +pub mod prelude { + pub use super::SolariPlugin; + pub use crate::pathtracer::Pathtracer; + pub use crate::scene::RaytracingMesh3d; +} + +use bevy_app::{App, Plugin}; +use bevy_render::settings::WgpuFeatures; +use pathtracer::PathtracingPlugin; +use scene::RaytracingScenePlugin; + +/// An experimental plugin for raytraced lighting. +/// +/// This plugin provides: +/// * (Coming soon) - Raytraced direct and indirect lighting. +/// * [`RaytracingScenePlugin`] - BLAS building, resource and lighting binding. +/// * [`PathtracingPlugin`] - A non-realtime pathtracer for validation purposes. +/// +/// To get started, add `RaytracingMesh3d` and `MeshMaterial3d::` to your entities. +pub struct SolariPlugin; + +impl Plugin for SolariPlugin { + fn build(&self, app: &mut App) { + app.add_plugins((RaytracingScenePlugin, PathtracingPlugin)); + } +} + +impl SolariPlugin { + /// [`WgpuFeatures`] required for this plugin to function. + pub fn required_wgpu_features() -> WgpuFeatures { + WgpuFeatures::EXPERIMENTAL_RAY_TRACING_ACCELERATION_STRUCTURE + | WgpuFeatures::EXPERIMENTAL_RAY_QUERY + | WgpuFeatures::BUFFER_BINDING_ARRAY + | WgpuFeatures::TEXTURE_BINDING_ARRAY + | WgpuFeatures::UNIFORM_BUFFER_AND_STORAGE_TEXTURE_ARRAY_NON_UNIFORM_INDEXING + | WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING + | WgpuFeatures::PARTIALLY_BOUND_BINDING_ARRAY + } +} diff --git a/crates/bevy_solari/src/pathtracer/extract.rs b/crates/bevy_solari/src/pathtracer/extract.rs new file mode 100644 index 0000000000..38f27968a6 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/extract.rs @@ -0,0 +1,33 @@ +use super::{prepare::PathtracerAccumulationTexture, Pathtracer}; +use bevy_ecs::{ + change_detection::DetectChanges, + system::{Commands, Query}, + world::Ref, +}; +use bevy_render::{camera::Camera, sync_world::RenderEntity, Extract}; +use bevy_transform::components::GlobalTransform; + +pub fn extract_pathtracer( + cameras_3d: Extract< + Query<( + RenderEntity, + &Camera, + Ref, + Option<&Pathtracer>, + )>, + >, + mut commands: Commands, +) { + for (entity, camera, global_transform, pathtracer) in &cameras_3d { + let mut entity_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); + if pathtracer.is_some() && camera.is_active { + let mut pathtracer = pathtracer.unwrap().clone(); + pathtracer.reset |= global_transform.is_changed(); + entity_commands.insert(pathtracer); + } else { + entity_commands.remove::<(Pathtracer, PathtracerAccumulationTexture)>(); + } + } +} diff --git a/crates/bevy_solari/src/pathtracer/mod.rs b/crates/bevy_solari/src/pathtracer/mod.rs new file mode 100644 index 0000000000..1e2cd95ed8 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/mod.rs @@ -0,0 +1,67 @@ +mod extract; +mod node; +mod prepare; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_asset::embedded_asset; +use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d}; +use bevy_ecs::{component::Component, reflect::ReflectComponent, schedule::IntoScheduleConfigs}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::{ + render_graph::{RenderGraphApp, ViewNodeRunner}, + renderer::RenderDevice, + view::Hdr, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use extract::extract_pathtracer; +use node::PathtracerNode; +use prepare::prepare_pathtracer_accumulation_texture; +use tracing::warn; + +/// Non-realtime pathtracing. +/// +/// This plugin is meant to generate reference screenshots to compare against, +/// and is not intended to be used by games. +pub struct PathtracingPlugin; + +impl Plugin for PathtracingPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "pathtracer.wgsl"); + + app.register_type::(); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "PathtracingPlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + + render_app + .add_systems(ExtractSchedule, extract_pathtracer) + .add_systems( + Render, + prepare_pathtracer_accumulation_texture.in_set(RenderSystems::PrepareResources), + ) + .add_render_graph_node::>( + Core3d, + node::graph::PathtracerNode, + ) + .add_render_graph_edges(Core3d, (Node3d::EndMainPass, node::graph::PathtracerNode)); + } +} + +#[derive(Component, Reflect, Default, Clone)] +#[reflect(Component, Default, Clone)] +#[require(Hdr)] +pub struct Pathtracer { + pub reset: bool, +} diff --git a/crates/bevy_solari/src/pathtracer/node.rs b/crates/bevy_solari/src/pathtracer/node.rs new file mode 100644 index 0000000000..30031c1d51 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/node.rs @@ -0,0 +1,134 @@ +use super::{prepare::PathtracerAccumulationTexture, Pathtracer}; +use crate::scene::RaytracingSceneBindings; +use bevy_asset::load_embedded_asset; +use bevy_ecs::{ + query::QueryItem, + world::{FromWorld, World}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_graph::{NodeRunError, RenderGraphContext, ViewNode}, + render_resource::{ + binding_types::{texture_storage_2d, uniform_buffer}, + BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries, CachedComputePipelineId, + ComputePassDescriptor, ComputePipelineDescriptor, ImageSubresourceRange, PipelineCache, + ShaderStages, StorageTextureAccess, TextureFormat, + }, + renderer::{RenderContext, RenderDevice}, + view::{ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, +}; + +pub mod graph { + use bevy_render::render_graph::RenderLabel; + + #[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)] + pub struct PathtracerNode; +} + +pub struct PathtracerNode { + bind_group_layout: BindGroupLayout, + pipeline: CachedComputePipelineId, +} + +impl ViewNode for PathtracerNode { + type ViewQuery = ( + &'static Pathtracer, + &'static PathtracerAccumulationTexture, + &'static ExtractedCamera, + &'static ViewTarget, + &'static ViewUniformOffset, + ); + + fn run( + &self, + _graph: &mut RenderGraphContext, + render_context: &mut RenderContext, + (pathtracer, accumulation_texture, camera, view_target, view_uniform_offset): QueryItem< + Self::ViewQuery, + >, + world: &World, + ) -> Result<(), NodeRunError> { + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + let view_uniforms = world.resource::(); + let (Some(pipeline), Some(scene_bindings), Some(viewport), Some(view_uniforms)) = ( + pipeline_cache.get_compute_pipeline(self.pipeline), + &scene_bindings.bind_group, + camera.physical_viewport_size, + view_uniforms.uniforms.binding(), + ) else { + return Ok(()); + }; + + let bind_group = render_context.render_device().create_bind_group( + "pathtracer_bind_group", + &self.bind_group_layout, + &BindGroupEntries::sequential(( + &accumulation_texture.0.default_view, + view_target.get_unsampled_color_attachment().view, + view_uniforms, + )), + ); + + let command_encoder = render_context.command_encoder(); + + if pathtracer.reset { + command_encoder.clear_texture( + &accumulation_texture.0.texture, + &ImageSubresourceRange::default(), + ); + } + + let mut pass = command_encoder.begin_compute_pass(&ComputePassDescriptor { + label: Some("pathtracer"), + timestamp_writes: None, + }); + pass.set_pipeline(pipeline); + pass.set_bind_group(0, scene_bindings, &[]); + pass.set_bind_group(1, &bind_group, &[view_uniform_offset.offset]); + pass.dispatch_workgroups(viewport.x.div_ceil(8), viewport.y.div_ceil(8), 1); + + Ok(()) + } +} + +impl FromWorld for PathtracerNode { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + let pipeline_cache = world.resource::(); + let scene_bindings = world.resource::(); + + let bind_group_layout = render_device.create_bind_group_layout( + "pathtracer_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_storage_2d(TextureFormat::Rgba32Float, StorageTextureAccess::ReadWrite), + texture_storage_2d( + ViewTarget::TEXTURE_FORMAT_HDR, + StorageTextureAccess::WriteOnly, + ), + uniform_buffer::(true), + ), + ), + ); + + let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("pathtracer_pipeline".into()), + layout: vec![ + scene_bindings.bind_group_layout.clone(), + bind_group_layout.clone(), + ], + push_constant_ranges: vec![], + shader: load_embedded_asset!(world, "pathtracer.wgsl"), + shader_defs: vec![], + entry_point: "pathtrace".into(), + zero_initialize_workgroup_memory: false, + }); + + Self { + bind_group_layout, + pipeline, + } + } +} diff --git a/crates/bevy_solari/src/pathtracer/pathtracer.wgsl b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl new file mode 100644 index 0000000000..c67b53e58e --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/pathtracer.wgsl @@ -0,0 +1,78 @@ +#import bevy_core_pipeline::tonemapping::tonemapping_luminance as luminance +#import bevy_pbr::utils::{rand_f, rand_vec2f} +#import bevy_render::maths::PI +#import bevy_render::view::View +#import bevy_solari::sampling::{sample_random_light, sample_cosine_hemisphere} +#import bevy_solari::scene_bindings::{trace_ray, resolve_ray_hit_full, RAY_T_MIN, RAY_T_MAX} + +@group(1) @binding(0) var accumulation_texture: texture_storage_2d; +@group(1) @binding(1) var view_output: texture_storage_2d; +@group(1) @binding(2) var view: View; + +@compute @workgroup_size(8, 8, 1) +fn pathtrace(@builtin(global_invocation_id) global_id: vec3) { + if any(global_id.xy >= vec2u(view.viewport.zw)) { + return; + } + + let old_color = textureLoad(accumulation_texture, global_id.xy); + + // Setup RNG + let pixel_index = global_id.x + global_id.y * u32(view.viewport.z); + let frame_index = u32(old_color.a) * 5782582u; + var rng = pixel_index + frame_index; + + // Shoot the first ray from the camera + let pixel_center = vec2(global_id.xy) + 0.5; + let jitter = rand_vec2f(&rng) - 0.5; + let pixel_uv = (pixel_center + jitter) / view.viewport.zw; + let pixel_ndc = (pixel_uv * 2.0) - 1.0; + let primary_ray_target = view.world_from_clip * vec4(pixel_ndc.x, -pixel_ndc.y, 1.0, 1.0); + var ray_origin = view.world_position; + var ray_direction = normalize((primary_ray_target.xyz / primary_ray_target.w) - ray_origin); + var ray_t_min = 0.0; + + // Path trace + var radiance = vec3(0.0); + var throughput = vec3(1.0); + loop { + let ray_hit = trace_ray(ray_origin, ray_direction, ray_t_min, RAY_T_MAX, RAY_FLAG_NONE); + if ray_hit.kind != RAY_QUERY_INTERSECTION_NONE { + let ray_hit = resolve_ray_hit_full(ray_hit); + + // Evaluate material BRDF + let diffuse_brdf = ray_hit.material.base_color / PI; + + // Use emissive only on the first ray (coming from the camera) + if ray_t_min == 0.0 { radiance = ray_hit.material.emissive; } + + // Sample direct lighting + radiance += throughput * diffuse_brdf * sample_random_light(ray_hit.world_position, ray_hit.world_normal, &rng); + + // Sample new ray direction from the material BRDF for next bounce + ray_direction = sample_cosine_hemisphere(ray_hit.world_normal, &rng); + + // Update other variables for next bounce + ray_origin = ray_hit.world_position; + ray_t_min = RAY_T_MIN; + + // Update throughput for next bounce + let cos_theta = dot(-ray_direction, ray_hit.world_normal); + let cosine_hemisphere_pdf = cos_theta / PI; // Weight for the next bounce because we importance sampled the diffuse BRDF for the next ray direction + throughput *= (diffuse_brdf * cos_theta) / cosine_hemisphere_pdf; + + // Russian roulette for early termination + let p = luminance(throughput); + if rand_f(&rng) > p { break; } + throughput /= p; + } else { break; } + } + + // Camera exposure + radiance *= view.exposure; + + // Accumulation over time via running average + let new_color = mix(old_color.rgb, radiance, 1.0 / (old_color.a + 1.0)); + textureStore(accumulation_texture, global_id.xy, vec4(new_color, old_color.a + 1.0)); + textureStore(view_output, global_id.xy, vec4(new_color, 1.0)); +} diff --git a/crates/bevy_solari/src/pathtracer/prepare.rs b/crates/bevy_solari/src/pathtracer/prepare.rs new file mode 100644 index 0000000000..7ef4733124 --- /dev/null +++ b/crates/bevy_solari/src/pathtracer/prepare.rs @@ -0,0 +1,52 @@ +use super::Pathtracer; +use bevy_ecs::{ + component::Component, + entity::Entity, + query::With, + system::{Commands, Query, Res, ResMut}, +}; +use bevy_render::{ + camera::ExtractedCamera, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, + renderer::RenderDevice, + texture::{CachedTexture, TextureCache}, +}; + +#[derive(Component)] +pub struct PathtracerAccumulationTexture(pub CachedTexture); + +pub fn prepare_pathtracer_accumulation_texture( + query: Query<(Entity, &ExtractedCamera), With>, + mut texture_cache: ResMut, + render_device: Res, + mut commands: Commands, +) { + for (entity, camera) in &query { + let Some(viewport) = camera.physical_viewport_size else { + continue; + }; + + let descriptor = TextureDescriptor { + label: Some("pathtracer_accumulation_texture"), + size: Extent3d { + width: viewport.x, + height: viewport.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: 1, + dimension: TextureDimension::D2, + format: TextureFormat::Rgba32Float, + usage: TextureUsages::STORAGE_BINDING, + view_formats: &[], + }; + + commands + .entity(entity) + .insert(PathtracerAccumulationTexture( + texture_cache.get(&render_device, descriptor), + )); + } +} diff --git a/crates/bevy_solari/src/scene/binder.rs b/crates/bevy_solari/src/scene/binder.rs new file mode 100644 index 0000000000..889efb538e --- /dev/null +++ b/crates/bevy_solari/src/scene/binder.rs @@ -0,0 +1,366 @@ +use super::{blas::BlasManager, extract::StandardMaterialAssets, RaytracingMesh3d}; +use bevy_asset::{AssetId, Handle}; +use bevy_color::{ColorToComponents, LinearRgba}; +use bevy_ecs::{ + resource::Resource, + system::{Query, Res, ResMut}, + world::{FromWorld, World}, +}; +use bevy_math::{ops::cos, Mat4, Vec3}; +use bevy_pbr::{ExtractedDirectionalLight, MeshMaterial3d, StandardMaterial}; +use bevy_platform::{collections::HashMap, hash::FixedHasher}; +use bevy_render::{ + mesh::allocator::MeshAllocator, + render_asset::RenderAssets, + render_resource::{binding_types::*, *}, + renderer::{RenderDevice, RenderQueue}, + texture::{FallbackImage, GpuImage}, +}; +use bevy_transform::components::GlobalTransform; +use core::{f32::consts::TAU, hash::Hash, num::NonZeroU32, ops::Deref}; + +const MAX_MESH_SLAB_COUNT: NonZeroU32 = NonZeroU32::new(500).unwrap(); +const MAX_TEXTURE_COUNT: NonZeroU32 = NonZeroU32::new(5_000).unwrap(); + +/// Average angular diameter of the sun as seen from earth. +/// +const SUN_ANGULAR_DIAMETER_RADIANS: f32 = 0.00930842; + +#[derive(Resource)] +pub struct RaytracingSceneBindings { + pub bind_group: Option, + pub bind_group_layout: BindGroupLayout, +} + +pub fn prepare_raytracing_scene_bindings( + instances_query: Query<( + &RaytracingMesh3d, + &MeshMaterial3d, + &GlobalTransform, + )>, + directional_lights_query: Query<&ExtractedDirectionalLight>, + mesh_allocator: Res, + blas_manager: Res, + material_assets: Res, + texture_assets: Res>, + fallback_texture: Res, + render_device: Res, + render_queue: Res, + mut raytracing_scene_bindings: ResMut, +) { + raytracing_scene_bindings.bind_group = None; + + if instances_query.iter().len() == 0 { + return; + } + + let mut vertex_buffers = CachedBindingArray::new(); + let mut index_buffers = CachedBindingArray::new(); + let mut textures = CachedBindingArray::new(); + let mut samplers = Vec::new(); + let mut materials = StorageBufferList::::default(); + let mut tlas = TlasPackage::new(render_device.wgpu_device().create_tlas( + &CreateTlasDescriptor { + label: Some("tlas"), + flags: AccelerationStructureFlags::PREFER_FAST_TRACE, + update_mode: AccelerationStructureUpdateMode::Build, + max_instances: instances_query.iter().len() as u32, + }, + )); + let mut transforms = StorageBufferList::::default(); + let mut geometry_ids = StorageBufferList::::default(); + let mut material_ids = StorageBufferList::::default(); + let mut light_sources = StorageBufferList::::default(); + let mut directional_lights = StorageBufferList::::default(); + + let mut material_id_map: HashMap, u32, FixedHasher> = + HashMap::default(); + let mut material_id = 0; + let mut process_texture = |texture_handle: &Option>| -> Option { + match texture_handle { + Some(texture_handle) => match texture_assets.get(texture_handle.id()) { + Some(texture) => { + let (texture_id, is_new) = + textures.push_if_absent(texture.texture_view.deref(), texture_handle.id()); + if is_new { + samplers.push(texture.sampler.deref()); + } + Some(texture_id) + } + None => None, + }, + None => Some(u32::MAX), + } + }; + for (asset_id, material) in material_assets.iter() { + let Some(base_color_texture_id) = process_texture(&material.base_color_texture) else { + continue; + }; + let Some(normal_map_texture_id) = process_texture(&material.normal_map_texture) else { + continue; + }; + let Some(emissive_texture_id) = process_texture(&material.emissive_texture) else { + continue; + }; + + materials.get_mut().push(GpuMaterial { + base_color: material.base_color.to_linear(), + emissive: material.emissive, + base_color_texture_id, + normal_map_texture_id, + emissive_texture_id, + _padding: Default::default(), + }); + + material_id_map.insert(*asset_id, material_id); + material_id += 1; + } + + if material_id == 0 { + return; + } + + if textures.is_empty() { + textures.vec.push(fallback_texture.d2.texture_view.deref()); + samplers.push(fallback_texture.d2.sampler.deref()); + } + + let mut instance_id = 0; + for (mesh, material, transform) in &instances_query { + let Some(blas) = blas_manager.get(&mesh.id()) else { + continue; + }; + let Some(vertex_slice) = mesh_allocator.mesh_vertex_slice(&mesh.id()) else { + continue; + }; + let Some(index_slice) = mesh_allocator.mesh_index_slice(&mesh.id()) else { + continue; + }; + let Some(material_id) = material_id_map.get(&material.id()).copied() else { + continue; + }; + let Some(material) = materials.get().get(material_id as usize) else { + continue; + }; + + let transform = transform.to_matrix(); + *tlas.get_mut_single(instance_id).unwrap() = Some(TlasInstance::new( + blas, + tlas_transform(&transform), + Default::default(), + 0xFF, + )); + + transforms.get_mut().push(transform); + + let (vertex_buffer_id, _) = vertex_buffers.push_if_absent( + vertex_slice.buffer.as_entire_buffer_binding(), + vertex_slice.buffer.id(), + ); + let (index_buffer_id, _) = index_buffers.push_if_absent( + index_slice.buffer.as_entire_buffer_binding(), + index_slice.buffer.id(), + ); + + geometry_ids.get_mut().push(GpuInstanceGeometryIds { + vertex_buffer_id, + vertex_buffer_offset: vertex_slice.range.start, + index_buffer_id, + index_buffer_offset: index_slice.range.start, + }); + + material_ids.get_mut().push(material_id); + + if material.emissive != LinearRgba::BLACK { + light_sources + .get_mut() + .push(GpuLightSource::new_emissive_mesh_light( + instance_id as u32, + (index_slice.range.len() / 3) as u32, + )); + } + + instance_id += 1; + } + + if instance_id == 0 { + return; + } + + for directional_light in &directional_lights_query { + let directional_lights = directional_lights.get_mut(); + let directional_light_id = directional_lights.len() as u32; + + directional_lights.push(GpuDirectionalLight::new(directional_light)); + + light_sources + .get_mut() + .push(GpuLightSource::new_directional_light(directional_light_id)); + } + + materials.write_buffer(&render_device, &render_queue); + transforms.write_buffer(&render_device, &render_queue); + geometry_ids.write_buffer(&render_device, &render_queue); + material_ids.write_buffer(&render_device, &render_queue); + light_sources.write_buffer(&render_device, &render_queue); + directional_lights.write_buffer(&render_device, &render_queue); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("build_tlas_command_encoder"), + }); + command_encoder.build_acceleration_structures(&[], [&tlas]); + render_queue.submit([command_encoder.finish()]); + + raytracing_scene_bindings.bind_group = Some(render_device.create_bind_group( + "raytracing_scene_bind_group", + &raytracing_scene_bindings.bind_group_layout, + &BindGroupEntries::sequential(( + vertex_buffers.as_slice(), + index_buffers.as_slice(), + textures.as_slice(), + samplers.as_slice(), + materials.binding().unwrap(), + tlas.as_binding(), + transforms.binding().unwrap(), + geometry_ids.binding().unwrap(), + material_ids.binding().unwrap(), + light_sources.binding().unwrap(), + directional_lights.binding().unwrap(), + )), + )); +} + +impl FromWorld for RaytracingSceneBindings { + fn from_world(world: &mut World) -> Self { + let render_device = world.resource::(); + + Self { + bind_group: None, + bind_group_layout: render_device.create_bind_group_layout( + "raytracing_scene_bind_group_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT), + storage_buffer_read_only_sized(false, None).count(MAX_MESH_SLAB_COUNT), + texture_2d(TextureSampleType::Float { filterable: true }) + .count(MAX_TEXTURE_COUNT), + sampler(SamplerBindingType::Filtering).count(MAX_TEXTURE_COUNT), + storage_buffer_read_only_sized(false, None), + acceleration_structure(), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + storage_buffer_read_only_sized(false, None), + ), + ), + ), + } + } +} + +struct CachedBindingArray { + map: HashMap, + vec: Vec, +} + +impl CachedBindingArray { + fn new() -> Self { + Self { + map: HashMap::default(), + vec: Vec::default(), + } + } + + fn push_if_absent(&mut self, item: T, item_id: I) -> (u32, bool) { + let mut is_new = false; + let i = *self.map.entry(item_id).or_insert_with(|| { + is_new = true; + let i = self.vec.len() as u32; + self.vec.push(item); + i + }); + (i, is_new) + } + + fn is_empty(&self) -> bool { + self.vec.is_empty() + } + + fn as_slice(&self) -> &[T] { + self.vec.as_slice() + } +} + +type StorageBufferList = StorageBuffer>; + +#[derive(ShaderType)] +struct GpuInstanceGeometryIds { + vertex_buffer_id: u32, + vertex_buffer_offset: u32, + index_buffer_id: u32, + index_buffer_offset: u32, +} + +#[derive(ShaderType)] +struct GpuMaterial { + base_color: LinearRgba, + emissive: LinearRgba, + base_color_texture_id: u32, + normal_map_texture_id: u32, + emissive_texture_id: u32, + _padding: u32, +} + +#[derive(ShaderType)] +struct GpuLightSource { + kind: u32, + id: u32, +} + +impl GpuLightSource { + fn new_emissive_mesh_light(instance_id: u32, triangle_count: u32) -> GpuLightSource { + Self { + kind: triangle_count << 1, + id: instance_id, + } + } + + fn new_directional_light(directional_light_id: u32) -> GpuLightSource { + Self { + kind: 1, + id: directional_light_id, + } + } +} + +#[derive(ShaderType, Default)] +struct GpuDirectionalLight { + direction_to_light: Vec3, + cos_theta_max: f32, + luminance: Vec3, + inverse_pdf: f32, +} + +impl GpuDirectionalLight { + fn new(directional_light: &ExtractedDirectionalLight) -> Self { + let cos_theta_max = cos(SUN_ANGULAR_DIAMETER_RADIANS / 2.0); + let solid_angle = TAU * (1.0 - cos_theta_max); + let luminance = + (directional_light.color.to_vec3() * directional_light.illuminance) / solid_angle; + + Self { + direction_to_light: directional_light.transform.back().into(), + cos_theta_max, + luminance, + inverse_pdf: solid_angle, + } + } +} + +fn tlas_transform(transform: &Mat4) -> [f32; 12] { + transform.transpose().to_cols_array()[..12] + .try_into() + .unwrap() +} diff --git a/crates/bevy_solari/src/scene/blas.rs b/crates/bevy_solari/src/scene/blas.rs new file mode 100644 index 0000000000..5beaa3b57c --- /dev/null +++ b/crates/bevy_solari/src/scene/blas.rs @@ -0,0 +1,133 @@ +use bevy_asset::AssetId; +use bevy_ecs::{ + resource::Resource, + system::{Res, ResMut}, +}; +use bevy_mesh::{Indices, Mesh}; +use bevy_platform::collections::HashMap; +use bevy_render::{ + mesh::{ + allocator::{MeshAllocator, MeshBufferSlice}, + RenderMesh, + }, + render_asset::ExtractedAssets, + render_resource::*, + renderer::{RenderDevice, RenderQueue}, +}; + +#[derive(Resource, Default)] +pub struct BlasManager(HashMap, Blas>); + +impl BlasManager { + pub fn get(&self, mesh: &AssetId) -> Option<&Blas> { + self.0.get(mesh) + } +} + +pub fn prepare_raytracing_blas( + mut blas_manager: ResMut, + extracted_meshes: Res>, + mesh_allocator: Res, + render_device: Res, + render_queue: Res, +) { + let blas_manager = &mut blas_manager.0; + + // Delete BLAS for deleted or modified meshes + for asset_id in extracted_meshes + .removed + .iter() + .chain(extracted_meshes.modified.iter()) + { + blas_manager.remove(asset_id); + } + + if extracted_meshes.extracted.is_empty() { + return; + } + + // Create new BLAS for added or changed meshes + let blas_resources = extracted_meshes + .extracted + .iter() + .filter(|(_, mesh)| is_mesh_raytracing_compatible(mesh)) + .map(|(asset_id, _)| { + let vertex_slice = mesh_allocator.mesh_vertex_slice(asset_id).unwrap(); + let index_slice = mesh_allocator.mesh_index_slice(asset_id).unwrap(); + + let (blas, blas_size) = + allocate_blas(&vertex_slice, &index_slice, asset_id, &render_device); + + blas_manager.insert(*asset_id, blas); + + (*asset_id, vertex_slice, index_slice, blas_size) + }) + .collect::>(); + + // Build geometry into each BLAS + let build_entries = blas_resources + .iter() + .map(|(asset_id, vertex_slice, index_slice, blas_size)| { + let geometry = BlasTriangleGeometry { + size: blas_size, + vertex_buffer: vertex_slice.buffer, + first_vertex: vertex_slice.range.start, + vertex_stride: 48, + index_buffer: Some(index_slice.buffer), + first_index: Some(index_slice.range.start), + transform_buffer: None, + transform_buffer_offset: None, + }; + BlasBuildEntry { + blas: &blas_manager[asset_id], + geometry: BlasGeometries::TriangleGeometries(vec![geometry]), + } + }) + .collect::>(); + + let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor { + label: Some("build_blas_command_encoder"), + }); + command_encoder.build_acceleration_structures(&build_entries, &[]); + render_queue.submit([command_encoder.finish()]); +} + +fn allocate_blas( + vertex_slice: &MeshBufferSlice, + index_slice: &MeshBufferSlice, + asset_id: &AssetId, + render_device: &RenderDevice, +) -> (Blas, BlasTriangleGeometrySizeDescriptor) { + let blas_size = BlasTriangleGeometrySizeDescriptor { + vertex_format: Mesh::ATTRIBUTE_POSITION.format, + vertex_count: vertex_slice.range.len() as u32, + index_format: Some(IndexFormat::Uint32), + index_count: Some(index_slice.range.len() as u32), + flags: AccelerationStructureGeometryFlags::OPAQUE, + }; + + let blas = render_device.wgpu_device().create_blas( + &CreateBlasDescriptor { + label: Some(&asset_id.to_string()), + flags: AccelerationStructureFlags::PREFER_FAST_TRACE, + update_mode: AccelerationStructureUpdateMode::Build, + }, + BlasGeometrySizeDescriptors::Triangles { + descriptors: vec![blas_size.clone()], + }, + ); + + (blas, blas_size) +} + +fn is_mesh_raytracing_compatible(mesh: &Mesh) -> bool { + let triangle_list = mesh.primitive_topology() == PrimitiveTopology::TriangleList; + let vertex_attributes = mesh.attributes().map(|(attribute, _)| attribute.id).eq([ + Mesh::ATTRIBUTE_POSITION.id, + Mesh::ATTRIBUTE_NORMAL.id, + Mesh::ATTRIBUTE_UV_0.id, + Mesh::ATTRIBUTE_TANGENT.id, + ]); + let indexed_32 = matches!(mesh.indices(), Some(Indices::U32(..))); + mesh.enable_raytracing && triangle_list && vertex_attributes && indexed_32 +} diff --git a/crates/bevy_solari/src/scene/extract.rs b/crates/bevy_solari/src/scene/extract.rs new file mode 100644 index 0000000000..46b11ba2b4 --- /dev/null +++ b/crates/bevy_solari/src/scene/extract.rs @@ -0,0 +1,45 @@ +use super::RaytracingMesh3d; +use bevy_asset::{AssetId, Assets}; +use bevy_derive::Deref; +use bevy_ecs::{ + resource::Resource, + system::{Commands, Query}, +}; +use bevy_pbr::{MeshMaterial3d, StandardMaterial}; +use bevy_platform::collections::HashMap; +use bevy_render::{extract_resource::ExtractResource, sync_world::RenderEntity, Extract}; +use bevy_transform::components::GlobalTransform; + +pub fn extract_raytracing_scene( + instances: Extract< + Query<( + RenderEntity, + &RaytracingMesh3d, + &MeshMaterial3d, + &GlobalTransform, + )>, + >, + mut commands: Commands, +) { + for (render_entity, mesh, material, transform) in &instances { + commands + .entity(render_entity) + .insert((mesh.clone(), material.clone(), *transform)); + } +} + +#[derive(Resource, Deref, Default)] +pub struct StandardMaterialAssets(HashMap, StandardMaterial>); + +impl ExtractResource for StandardMaterialAssets { + type Source = Assets; + + fn extract_resource(source: &Self::Source) -> Self { + Self( + source + .iter() + .map(|(asset_id, material)| (asset_id, material.clone())) + .collect(), + ) + } +} diff --git a/crates/bevy_solari/src/scene/mod.rs b/crates/bevy_solari/src/scene/mod.rs new file mode 100644 index 0000000000..a68e126480 --- /dev/null +++ b/crates/bevy_solari/src/scene/mod.rs @@ -0,0 +1,77 @@ +mod binder; +mod blas; +mod extract; +mod types; + +pub use binder::RaytracingSceneBindings; +pub use types::RaytracingMesh3d; + +use crate::SolariPlugin; +use bevy_app::{App, Plugin}; +use bevy_ecs::schedule::IntoScheduleConfigs; +use bevy_render::{ + extract_resource::ExtractResourcePlugin, + load_shader_library, + mesh::{ + allocator::{allocate_and_free_meshes, MeshAllocator}, + RenderMesh, + }, + render_asset::prepare_assets, + render_resource::BufferUsages, + renderer::RenderDevice, + ExtractSchedule, Render, RenderApp, RenderSystems, +}; +use binder::prepare_raytracing_scene_bindings; +use blas::{prepare_raytracing_blas, BlasManager}; +use extract::{extract_raytracing_scene, StandardMaterialAssets}; +use tracing::warn; + +/// Creates acceleration structures and binding arrays of resources for raytracing. +pub struct RaytracingScenePlugin; + +impl Plugin for RaytracingScenePlugin { + fn build(&self, app: &mut App) { + load_shader_library!(app, "raytracing_scene_bindings.wgsl"); + load_shader_library!(app, "sampling.wgsl"); + + app.register_type::(); + } + + fn finish(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + let render_device = render_app.world().resource::(); + let features = render_device.features(); + if !features.contains(SolariPlugin::required_wgpu_features()) { + warn!( + "RaytracingScenePlugin not loaded. GPU lacks support for required features: {:?}.", + SolariPlugin::required_wgpu_features().difference(features) + ); + return; + } + + app.add_plugins(ExtractResourcePlugin::::default()); + + let render_app = app.sub_app_mut(RenderApp); + + render_app + .world_mut() + .resource_mut::() + .extra_buffer_usages |= BufferUsages::BLAS_INPUT | BufferUsages::STORAGE; + + render_app + .init_resource::() + .init_resource::() + .init_resource::() + .add_systems(ExtractSchedule, extract_raytracing_scene) + .add_systems( + Render, + ( + prepare_raytracing_blas + .in_set(RenderSystems::PrepareAssets) + .before(prepare_assets::) + .after(allocate_and_free_meshes), + prepare_raytracing_scene_bindings.in_set(RenderSystems::PrepareBindGroups), + ), + ); + } +} diff --git a/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl new file mode 100644 index 0000000000..99dbff9d89 --- /dev/null +++ b/crates/bevy_solari/src/scene/raytracing_scene_bindings.wgsl @@ -0,0 +1,164 @@ +#define_import_path bevy_solari::scene_bindings + +struct InstanceGeometryIds { + vertex_buffer_id: u32, + vertex_buffer_offset: u32, + index_buffer_id: u32, + index_buffer_offset: u32, +} + +struct VertexBuffer { vertices: array } + +struct IndexBuffer { indices: array } + +struct PackedVertex { + a: vec4, + b: vec4, + tangent: vec4, +} + +struct Vertex { + position: vec3, + normal: vec3, + uv: vec2, + tangent: vec4, +} + +fn unpack_vertex(packed: PackedVertex) -> Vertex { + var vertex: Vertex; + vertex.position = packed.a.xyz; + vertex.normal = vec3(packed.a.w, packed.b.xy); + vertex.uv = packed.b.zw; + vertex.tangent = packed.tangent; + return vertex; +} + +struct Material { + base_color: vec4, + emissive: vec4, + base_color_texture_id: u32, + normal_map_texture_id: u32, + emissive_texture_id: u32, + _padding: u32, +} + +const TEXTURE_MAP_NONE = 0xFFFFFFFFu; + +struct LightSource { + kind: u32, // 1 bit for kind, 31 bits for extra data + id: u32, +} + +const LIGHT_SOURCE_KIND_EMISSIVE_MESH = 0u; +const LIGHT_SOURCE_KIND_DIRECTIONAL = 1u; + +struct DirectionalLight { + direction_to_light: vec3, + cos_theta_max: f32, + luminance: vec3, + inverse_pdf: f32, +} + +@group(0) @binding(0) var vertex_buffers: binding_array; +@group(0) @binding(1) var index_buffers: binding_array; +@group(0) @binding(2) var textures: binding_array>; +@group(0) @binding(3) var samplers: binding_array; +@group(0) @binding(4) var materials: array; +@group(0) @binding(5) var tlas: acceleration_structure; +@group(0) @binding(6) var transforms: array>; +@group(0) @binding(7) var geometry_ids: array; +@group(0) @binding(8) var material_ids: array; // TODO: Store material_id in instance_custom_index instead? +@group(0) @binding(9) var light_sources: array; +@group(0) @binding(10) var directional_lights: array; + +const RAY_T_MIN = 0.0001; +const RAY_T_MAX = 100000.0; + +const RAY_NO_CULL = 0xFFu; + +fn trace_ray(ray_origin: vec3, ray_direction: vec3, ray_t_min: f32, ray_t_max: f32, ray_flag: u32) -> RayIntersection { + let ray = RayDesc(ray_flag, RAY_NO_CULL, ray_t_min, ray_t_max, ray_origin, ray_direction); + var rq: ray_query; + rayQueryInitialize(&rq, tlas, ray); + rayQueryProceed(&rq); + return rayQueryGetCommittedIntersection(&rq); +} + +fn sample_texture(id: u32, uv: vec2) -> vec3 { + return textureSampleLevel(textures[id], samplers[id], uv, 0.0).rgb; // TODO: Mipmap +} + +struct ResolvedMaterial { + base_color: vec3, + emissive: vec3, +} + +struct ResolvedRayHitFull { + world_position: vec3, + world_normal: vec3, + geometric_world_normal: vec3, + uv: vec2, + triangle_area: f32, + material: ResolvedMaterial, +} + +fn resolve_material(material: Material, uv: vec2) -> ResolvedMaterial { + var m: ResolvedMaterial; + + m.base_color = material.base_color.rgb; + if material.base_color_texture_id != TEXTURE_MAP_NONE { + m.base_color *= sample_texture(material.base_color_texture_id, uv); + } + + m.emissive = material.emissive.rgb; + if material.emissive_texture_id != TEXTURE_MAP_NONE { + m.emissive *= sample_texture(material.emissive_texture_id, uv); + } + + return m; +} + +fn resolve_ray_hit_full(ray_hit: RayIntersection) -> ResolvedRayHitFull { + let barycentrics = vec3(1.0 - ray_hit.barycentrics.x - ray_hit.barycentrics.y, ray_hit.barycentrics); + return resolve_triangle_data_full(ray_hit.instance_id, ray_hit.primitive_index, barycentrics); +} + +fn resolve_triangle_data_full(instance_id: u32, triangle_id: u32, barycentrics: vec3) -> ResolvedRayHitFull { + let instance_geometry_ids = geometry_ids[instance_id]; + let material_id = material_ids[instance_id]; + + let index_buffer = &index_buffers[instance_geometry_ids.index_buffer_id].indices; + let vertex_buffer = &vertex_buffers[instance_geometry_ids.vertex_buffer_id].vertices; + let material = materials[material_id]; + + let indices_i = (triangle_id * 3u) + vec3(0u, 1u, 2u) + instance_geometry_ids.index_buffer_offset; + let indices = vec3((*index_buffer)[indices_i.x], (*index_buffer)[indices_i.y], (*index_buffer)[indices_i.z]) + instance_geometry_ids.vertex_buffer_offset; + let vertices = array(unpack_vertex((*vertex_buffer)[indices.x]), unpack_vertex((*vertex_buffer)[indices.y]), unpack_vertex((*vertex_buffer)[indices.z])); + + let transform = transforms[instance_id]; + let local_position = mat3x3(vertices[0].position, vertices[1].position, vertices[2].position) * barycentrics; + let world_position = (transform * vec4(local_position, 1.0)).xyz; + + let uv = mat3x2(vertices[0].uv, vertices[1].uv, vertices[2].uv) * barycentrics; + + let local_normal = mat3x3(vertices[0].normal, vertices[1].normal, vertices[2].normal) * barycentrics; // TODO: Use barycentric lerp, ray_hit.object_to_world, cross product geo normal + var world_normal = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_normal); + let geometric_world_normal = world_normal; + if material.normal_map_texture_id != TEXTURE_MAP_NONE { + let local_tangent = mat3x3(vertices[0].tangent.xyz, vertices[1].tangent.xyz, vertices[2].tangent.xyz) * barycentrics; + let world_tangent = normalize(mat3x3(transform[0].xyz, transform[1].xyz, transform[2].xyz) * local_tangent); + let N = world_normal; + let T = world_tangent; + let B = vertices[0].tangent.w * cross(N, T); + let Nt = sample_texture(material.normal_map_texture_id, uv); + world_normal = normalize(Nt.x * T + Nt.y * B + Nt.z * N); + } + + let triangle_edge0 = vertices[0].position - vertices[1].position; + let triangle_edge1 = vertices[0].position - vertices[2].position; + let triangle_area = length(cross(triangle_edge0, triangle_edge1)) / 2.0; + + let resolved_material = resolve_material(material, uv); + + return ResolvedRayHitFull(world_position, world_normal, geometric_world_normal, uv, triangle_area, resolved_material); +} diff --git a/crates/bevy_solari/src/scene/sampling.wgsl b/crates/bevy_solari/src/scene/sampling.wgsl new file mode 100644 index 0000000000..4e2c8db33a --- /dev/null +++ b/crates/bevy_solari/src/scene/sampling.wgsl @@ -0,0 +1,169 @@ +#define_import_path bevy_solari::sampling + +#import bevy_pbr::utils::{rand_f, rand_vec2f, rand_range_u} +#import bevy_render::maths::PI_2 +#import bevy_solari::scene_bindings::{trace_ray, RAY_T_MIN, RAY_T_MAX, light_sources, directional_lights, LIGHT_SOURCE_KIND_DIRECTIONAL, resolve_triangle_data_full} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec28%3A303 +fn sample_cosine_hemisphere(normal: vec3, rng: ptr) -> vec3 { + let cos_theta = 1.0 - 2.0 * rand_f(rng); + let phi = PI_2 * rand_f(rng); + let sin_theta = sqrt(max(1.0 - cos_theta * cos_theta, 0.0)); + let x = normal.x + sin_theta * cos(phi); + let y = normal.y + sin_theta * sin(phi); + let z = normal.z + cos_theta; + return vec3(x, y, z); +} + +fn sample_random_light(ray_origin: vec3, origin_world_normal: vec3, rng: ptr) -> vec3 { + let light_sample = generate_random_light_sample(rng); + let light_contribution = calculate_light_contribution(light_sample, ray_origin, origin_world_normal); + let visibility = trace_light_visibility(light_sample, ray_origin); + return light_contribution.radiance * visibility * light_contribution.inverse_pdf; +} + +struct LightSample { + light_id: vec2, + random: vec2, +} + +struct LightContribution { + radiance: vec3, + inverse_pdf: f32, +} + +fn generate_random_light_sample(rng: ptr) -> LightSample { + let light_count = arrayLength(&light_sources); + let light_id = rand_range_u(light_count, rng); + let random = rand_vec2f(rng); + + let light_source = light_sources[light_id]; + var triangle_id = 0u; + + if light_source.kind != LIGHT_SOURCE_KIND_DIRECTIONAL { + let triangle_count = light_source.kind >> 1u; + triangle_id = rand_range_u(triangle_count, rng); + } + + return LightSample(vec2(light_id, triangle_id), random); +} + +fn calculate_light_contribution(light_sample: LightSample, ray_origin: vec3, origin_world_normal: vec3) -> LightContribution { + let light_id = light_sample.light_id.x; + let light_source = light_sources[light_id]; + + var light_contribution: LightContribution; + if light_source.kind == LIGHT_SOURCE_KIND_DIRECTIONAL { + light_contribution = calculate_directional_light_contribution(light_sample, light_source.id, origin_world_normal); + } else { + let triangle_count = light_source.kind >> 1u; + light_contribution = calculate_emissive_mesh_contribution(light_sample, light_source.id, triangle_count, ray_origin, origin_world_normal); + } + + let light_count = arrayLength(&light_sources); + light_contribution.inverse_pdf *= f32(light_count); + + return light_contribution; +} + +fn calculate_directional_light_contribution(light_sample: LightSample, directional_light_id: u32, origin_world_normal: vec3) -> LightContribution { + let directional_light = directional_lights[directional_light_id]; + + // Sample a random direction within a cone whose base is the sun approximated as a disk + // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305 + let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max; + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + let phi = light_sample.random.y * PI_2; + let x = cos(phi) * sin_theta; + let y = sin(phi) * sin_theta; + var ray_direction = vec3(x, y, cos_theta); + + // Rotate the ray so that the cone it was sampled from is aligned with the light direction + ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction; + + let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal)); + let radiance = directional_light.luminance * cos_theta_origin; + + return LightContribution(radiance, directional_light.inverse_pdf); +} + +fn calculate_emissive_mesh_contribution(light_sample: LightSample, instance_id: u32, triangle_count: u32, ray_origin: vec3, origin_world_normal: vec3) -> LightContribution { + let barycentrics = triangle_barycentrics(light_sample.random); + let triangle_id = light_sample.light_id.y; + + let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics); + + let light_distance = distance(ray_origin, triangle_data.world_position); + let ray_direction = (triangle_data.world_position - ray_origin) / light_distance; + let cos_theta_origin = saturate(dot(ray_direction, origin_world_normal)); + let cos_theta_light = saturate(dot(-ray_direction, triangle_data.world_normal)); + let light_distance_squared = light_distance * light_distance; + + let radiance = triangle_data.material.emissive.rgb * cos_theta_origin * (cos_theta_light / light_distance_squared); + let inverse_pdf = f32(triangle_count) * triangle_data.triangle_area; + + return LightContribution(radiance, inverse_pdf); +} + +fn trace_light_visibility(light_sample: LightSample, ray_origin: vec3) -> f32 { + let light_id = light_sample.light_id.x; + let light_source = light_sources[light_id]; + + if light_source.kind == LIGHT_SOURCE_KIND_DIRECTIONAL { + return trace_directional_light_visibility(light_sample, light_source.id, ray_origin); + } else { + return trace_emissive_mesh_visibility(light_sample, light_source.id, ray_origin); + } +} + +fn trace_directional_light_visibility(light_sample: LightSample, directional_light_id: u32, ray_origin: vec3) -> f32 { + let directional_light = directional_lights[directional_light_id]; + + // Sample a random direction within a cone whose base is the sun approximated as a disk + // https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec30%3A305 + let cos_theta = (1.0 - light_sample.random.x) + light_sample.random.x * directional_light.cos_theta_max; + let sin_theta = sqrt(1.0 - cos_theta * cos_theta); + let phi = light_sample.random.y * PI_2; + let x = cos(phi) * sin_theta; + let y = sin(phi) * sin_theta; + var ray_direction = vec3(x, y, cos_theta); + + // Rotate the ray so that the cone it was sampled from is aligned with the light direction + ray_direction = build_orthonormal_basis(directional_light.direction_to_light) * ray_direction; + + let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, RAY_T_MAX, RAY_FLAG_TERMINATE_ON_FIRST_HIT); + return f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE); +} + +fn trace_emissive_mesh_visibility(light_sample: LightSample, instance_id: u32, ray_origin: vec3) -> f32 { + let barycentrics = triangle_barycentrics(light_sample.random); + let triangle_id = light_sample.light_id.y; + + let triangle_data = resolve_triangle_data_full(instance_id, triangle_id, barycentrics); + + let light_distance = distance(ray_origin, triangle_data.world_position); + let ray_direction = (triangle_data.world_position - ray_origin) / light_distance; + + let ray_t_max = light_distance - RAY_T_MIN - RAY_T_MIN; + if ray_t_max < RAY_T_MIN { return 0.0; } + + let ray_hit = trace_ray(ray_origin, ray_direction, RAY_T_MIN, ray_t_max, RAY_FLAG_TERMINATE_ON_FIRST_HIT); + return f32(ray_hit.kind == RAY_QUERY_INTERSECTION_NONE); +} + +// https://www.realtimerendering.com/raytracinggems/unofficial_RayTracingGems_v1.9.pdf#0004286901.INDD%3ASec22%3A297 +fn triangle_barycentrics(random: vec2) -> vec3 { + var barycentrics = random; + if barycentrics.x + barycentrics.y > 1.0 { barycentrics = 1.0 - barycentrics; } + return vec3(1.0 - barycentrics.x - barycentrics.y, barycentrics); +} + +// https://jcgt.org/published/0006/01/01/paper.pdf +fn build_orthonormal_basis(normal: vec3) -> mat3x3 { + let sign = select(-1.0, 1.0, normal.z >= 0.0); + let a = -1.0 / (sign + normal.z); + let b = normal.x * normal.y * a; + let tangent = vec3(1.0 + sign * normal.x * normal.x * a, sign * b, -sign * normal.x); + let bitangent = vec3(b, sign + normal.y * normal.y * a, -normal.y); + return mat3x3(tangent, bitangent, normal); +} diff --git a/crates/bevy_solari/src/scene/types.rs b/crates/bevy_solari/src/scene/types.rs new file mode 100644 index 0000000000..8ee33b31fc --- /dev/null +++ b/crates/bevy_solari/src/scene/types.rs @@ -0,0 +1,21 @@ +use bevy_asset::Handle; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, prelude::ReflectComponent}; +use bevy_mesh::Mesh; +use bevy_pbr::{MeshMaterial3d, StandardMaterial}; +use bevy_reflect::{prelude::ReflectDefault, Reflect}; +use bevy_render::sync_world::SyncToRenderWorld; +use bevy_transform::components::Transform; +use derive_more::derive::From; + +/// A mesh component used for raytracing. +/// +/// The mesh used in this component must have [`bevy_render::mesh::Mesh::enable_raytracing`] set to true, +/// use the following set of vertex attributes: `{POSITION, NORMAL, UV_0, TANGENT}`, use [`bevy_render::render_resource::PrimitiveTopology::TriangleList`], +/// and use [`bevy_mesh::Indices::U32`]. +/// +/// The material used for this entity must be [`MeshMaterial3d`]. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq, From)] +#[reflect(Component, Default, Clone, PartialEq)] +#[require(MeshMaterial3d, Transform, SyncToRenderWorld)] +pub struct RaytracingMesh3d(pub Handle); diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index a3d9ee3eb2..204dd85e24 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -778,7 +778,7 @@ impl RenderCommand

for SetMesh2dViewBindGroup( _item: &P, - (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, + (view_uniform, mesh2d_view_bind_group): ROQueryItem<'w, '_, Self::ViewQuery>, _view: Option<()>, _param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 8ffb12a582..03de94be1c 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -372,7 +372,7 @@ impl ViewNode for Wireframe2dNode { &self, graph: &mut RenderGraphContext, render_context: &mut RenderContext<'w>, - (camera, view, target, depth): QueryItem<'w, Self::ViewQuery>, + (camera, view, target, depth): QueryItem<'w, '_, Self::ViewQuery>, world: &'w World, ) -> Result<(), NodeRunError> { let Some(wireframe_phase) = diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 7602addc0b..761e2c628a 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -908,7 +908,7 @@ impl RenderCommand

for SetSpriteViewBindGroup( _item: &P, - (view_uniform, sprite_view_bind_group): ROQueryItem<'w, Self::ViewQuery>, + (view_uniform, sprite_view_bind_group): ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, _param: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -925,7 +925,7 @@ impl RenderCommand

for SetSpriteTextureBindGrou fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, (image_bind_groups, batches): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, @@ -955,7 +955,7 @@ impl RenderCommand

for DrawSpriteBatch { fn render<'w>( item: &P, - view: ROQueryItem<'w, Self::ViewQuery>, + view: ROQueryItem<'w, '_, Self::ViewQuery>, _entity: Option<()>, (sprite_meta, batches): SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, diff --git a/crates/bevy_state/src/state/mod.rs b/crates/bevy_state/src/state/mod.rs index 9267478281..61ee0627a3 100644 --- a/crates/bevy_state/src/state/mod.rs +++ b/crates/bevy_state/src/state/mod.rs @@ -642,6 +642,203 @@ mod tests { } } + #[derive(PartialEq, Eq, Debug, Hash, Clone)] + enum MultiSourceComputedState { + FromSimpleBTrue, + FromSimple2B2, + FromBoth, + } + + impl ComputedStates for MultiSourceComputedState { + type SourceStates = (SimpleState, SimpleState2); + + fn compute((simple_state, simple_state2): (SimpleState, SimpleState2)) -> Option { + match (simple_state, simple_state2) { + // If both are in their special states, prioritize the "both" variant. + (SimpleState::B(true), SimpleState2::B2) => Some(Self::FromBoth), + // If only SimpleState is B(true). + (SimpleState::B(true), _) => Some(Self::FromSimpleBTrue), + // If only SimpleState2 is B2. + (_, SimpleState2::B2) => Some(Self::FromSimple2B2), + // Otherwise, no computed state. + _ => None, + } + } + } + + /// This test ensures that [`ComputedStates`] with multiple source states + /// react when any source changes. + #[test] + fn computed_state_with_multiple_sources_should_react_to_any_source_change() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SimpleState::register_state(&mut apply_changes); + SimpleState2::register_state(&mut apply_changes); + MultiSourceComputedState::register_computed_state_systems(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + setup_state_transitions_in_world(&mut world); + + // Initial state: SimpleState::A, SimpleState2::A1 and + // MultiSourceComputedState should not exist yet. + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + // Change only SimpleState to B(true) - this should trigger + // MultiSourceComputedState. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SimpleState2::A1); + // The computed state should exist because SimpleState changed to + // B(true). + assert!(world.contains_resource::>()); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimpleBTrue + ); + + // Reset SimpleState to A - computed state should be removed. + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + // Now change only SimpleState2 to B2 - this should also trigger + // MultiSourceComputedState. + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::B2); + // The computed state should exist because SimpleState2 changed to B2. + assert!(world.contains_resource::>()); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimple2B2 + ); + + // Test that changes to both states work. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + MultiSourceComputedState::FromSimpleBTrue + ); + } + + // Test SubState that depends on multiple source states. + #[derive(PartialEq, Eq, Debug, Default, Hash, Clone)] + enum MultiSourceSubState { + #[default] + Active, + } + + impl SubStates for MultiSourceSubState { + type SourceStates = (SimpleState, SimpleState2); + + fn should_exist( + (simple_state, simple_state2): (SimpleState, SimpleState2), + ) -> Option { + // SubState should exist when: + // - SimpleState is B(true), OR + // - SimpleState2 is B2 + match (simple_state, simple_state2) { + (SimpleState::B(true), _) | (_, SimpleState2::B2) => Some(Self::Active), + _ => None, + } + } + } + + impl States for MultiSourceSubState { + const DEPENDENCY_DEPTH: usize = ::SourceStates::SET_DEPENDENCY_DEPTH + 1; + } + + impl FreelyMutableState for MultiSourceSubState {} + + /// This test ensures that [`SubStates`] with multiple source states react + /// when any source changes. + #[test] + fn sub_state_with_multiple_sources_should_react_to_any_source_change() { + let mut world = World::new(); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + EventRegistry::register_event::>(&mut world); + + world.init_resource::>(); + world.init_resource::>(); + + let mut schedules = Schedules::new(); + let mut apply_changes = Schedule::new(StateTransition); + SimpleState::register_state(&mut apply_changes); + SimpleState2::register_state(&mut apply_changes); + MultiSourceSubState::register_sub_state_systems(&mut apply_changes); + schedules.insert(apply_changes); + + world.insert_resource(schedules); + setup_state_transitions_in_world(&mut world); + + // Initial state: SimpleState::A, SimpleState2::A1 and + // MultiSourceSubState should not exist yet. + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::A1); + assert!(!world.contains_resource::>()); + + // Change only SimpleState to B(true) - this should trigger + // MultiSourceSubState. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.run_schedule(StateTransition); + assert_eq!( + world.resource::>().0, + SimpleState::B(true) + ); + assert_eq!(world.resource::>().0, SimpleState2::A1); + // The sub state should exist because SimpleState changed to B(true). + assert!(world.contains_resource::>()); + + // Reset to initial state. + world.insert_resource(NextState::Pending(SimpleState::A)); + world.run_schedule(StateTransition); + assert!(!world.contains_resource::>()); + + // Now change only SimpleState2 to B2 - this should also trigger + // MultiSourceSubState creation. + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert_eq!(world.resource::>().0, SimpleState::A); + assert_eq!(world.resource::>().0, SimpleState2::B2); + // The sub state should exist because SimpleState2 changed to B2. + assert!(world.contains_resource::>()); + + // Finally, test that it works when both change simultaneously. + world.insert_resource(NextState::Pending(SimpleState::B(false))); + world.insert_resource(NextState::Pending(SimpleState2::A1)); + world.run_schedule(StateTransition); + // After this transition, the state should not exist since SimpleState + // is B(false). + assert!(!world.contains_resource::>()); + + // Change both at the same time. + world.insert_resource(NextState::Pending(SimpleState::B(true))); + world.insert_resource(NextState::Pending(SimpleState2::B2)); + world.run_schedule(StateTransition); + assert!(world.contains_resource::>()); + } + #[test] fn check_transition_orders() { let mut world = World::new(); diff --git a/crates/bevy_state/src/state/state_set.rs b/crates/bevy_state/src/state/state_set.rs index 69a6c41b3d..3cf1e1d260 100644 --- a/crates/bevy_state/src/state/state_set.rs +++ b/crates/bevy_state/src/state/state_set.rs @@ -293,7 +293,7 @@ macro_rules! impl_state_set_sealed_tuples { current_state_res: Option>>, next_state_res: Option>>, ($($val),*,): ($(Option>>),*,)| { - let parent_changed = ($($evt.read().last().is_some())&&*); + let parent_changed = ($($evt.read().last().is_some())||*); let next_state = take_next_state(next_state_res); if !parent_changed && next_state.is_none() { diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index dfe711f245..1ee21826c3 100644 --- a/crates/bevy_state/src/state/transitions.rs +++ b/crates/bevy_state/src/state/transitions.rs @@ -1,7 +1,7 @@ use core::{marker::PhantomData, mem}; use bevy_ecs::{ - event::{Event, EventReader, EventWriter}, + event::{BufferedEvent, Event, EventReader, EventWriter}, schedule::{IntoScheduleConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, system::{Commands, In, ResMut}, world::World, @@ -55,11 +55,11 @@ pub struct OnTransition { #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)] pub struct StateTransition; -/// Event sent when any state transition of `S` happens. +/// A [`BufferedEvent`] sent when any state transition of `S` happens. /// This includes identity transitions, where `exited` and `entered` have the same value. /// /// If you know exactly what state you want to respond to ahead of time, consider [`OnEnter`], [`OnTransition`], or [`OnExit`] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Event)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Event, BufferedEvent)] pub struct StateTransitionEvent { /// The state being exited. pub exited: Option, diff --git a/crates/bevy_state/src/state_scoped_events.rs b/crates/bevy_state/src/state_scoped_events.rs index b11d8e79df..adba1ca6b6 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -3,7 +3,7 @@ use core::marker::PhantomData; use bevy_app::{App, SubApp}; use bevy_ecs::{ - event::{Event, EventReader, Events}, + event::{BufferedEvent, EventReader, Events}, resource::Resource, system::Commands, world::World, @@ -12,7 +12,7 @@ use bevy_platform::collections::HashMap; use crate::state::{OnEnter, OnExit, StateTransitionEvent, States}; -fn clear_event_queue(w: &mut World) { +fn clear_event_queue(w: &mut World) { if let Some(mut queue) = w.get_resource_mut::>() { queue.clear(); } @@ -33,7 +33,7 @@ struct StateScopedEvents { } impl StateScopedEvents { - fn add_event(&mut self, state: S, transition_type: TransitionType) { + fn add_event(&mut self, state: S, transition_type: TransitionType) { let map = match transition_type { TransitionType::OnExit => &mut self.on_exit, TransitionType::OnEnter => &mut self.on_enter, @@ -106,7 +106,7 @@ fn clear_events_on_enter_state( }); } -fn clear_events_on_state_transition( +fn clear_events_on_state_transition( app: &mut SubApp, _p: PhantomData, state: S, @@ -128,7 +128,7 @@ fn clear_events_on_state_transition( /// Extension trait for [`App`] adding methods for registering state scoped events. pub trait StateScopedEventsAppExt { - /// Clears an [`Event`] when exiting the specified `state`. + /// Clears an [`BufferedEvent`] when exiting the specified `state`. /// /// Note that event cleanup is ambiguously ordered relative to /// [`DespawnOnExitState`](crate::prelude::DespawnOnExitState) entity cleanup, @@ -136,9 +136,9 @@ pub trait StateScopedEventsAppExt { /// All of these (state scoped entities and events cleanup, and `OnExit`) /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::ExitSchedules`. - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self; - /// Clears an [`Event`] when entering the specified `state`. + /// Clears an [`BufferedEvent`] when entering the specified `state`. /// /// Note that event cleanup is ambiguously ordered relative to /// [`DespawnOnEnterState`](crate::prelude::DespawnOnEnterState) entity cleanup, @@ -146,11 +146,11 @@ pub trait StateScopedEventsAppExt { /// All of these (state scoped entities and events cleanup, and `OnEnter`) /// occur within schedule [`StateTransition`](crate::prelude::StateTransition) /// and system set `StateTransitionSystems::EnterSchedules`. - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self; + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self; } impl StateScopedEventsAppExt for App { - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition( self.main_mut(), PhantomData::, @@ -160,7 +160,7 @@ impl StateScopedEventsAppExt for App { self } - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition( self.main_mut(), PhantomData::, @@ -172,12 +172,12 @@ impl StateScopedEventsAppExt for App { } impl StateScopedEventsAppExt for SubApp { - fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_exit_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnExit); self } - fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { + fn clear_events_on_enter_state(&mut self, state: impl States) -> &mut Self { clear_events_on_state_transition(self, PhantomData::, state, TransitionType::OnEnter); self } @@ -187,6 +187,7 @@ impl StateScopedEventsAppExt for SubApp { mod tests { use super::*; use crate::app::StatesPlugin; + use bevy_ecs::event::{BufferedEvent, Event}; use bevy_state::prelude::*; #[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)] @@ -196,10 +197,10 @@ mod tests { B, } - #[derive(Event, Debug)] + #[derive(Event, BufferedEvent, Debug)] struct StandardEvent; - #[derive(Event, Debug)] + #[derive(Event, BufferedEvent, Debug)] struct StateScopedEvent; #[test] diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 70b3aaf2e8..ad162a7ef7 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -15,7 +15,12 @@ default = ["std", "async_executor"] ## Enables multi-threading support. ## Without this feature, all tasks will be run on a single thread. -multi_threaded = ["std", "dep:async-channel", "dep:concurrent-queue"] +multi_threaded = [ + "std", + "dep:async-channel", + "dep:concurrent-queue", + "async_executor", +] ## Uses `async-executor` as a task execution backend. ## This backend is incompatible with `no_std` targets. diff --git a/crates/bevy_text/src/bounds.rs b/crates/bevy_text/src/bounds.rs index db2ceb0b28..1c0833b443 100644 --- a/crates/bevy_text/src/bounds.rs +++ b/crates/bevy_text/src/bounds.rs @@ -5,7 +5,7 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; /// The maximum width and height of text. The text will wrap according to the specified size. /// /// Characters out of the bounds after wrapping will be truncated. Text is aligned according to the -/// specified [`JustifyText`](crate::text::JustifyText). +/// specified [`Justify`](crate::text::Justify). /// /// Note: only characters that are completely out of the bounds will be truncated, so this is not a /// reliable limit if it is necessary to contain the text strictly in the bounds. Currently this diff --git a/crates/bevy_text/src/lib.rs b/crates/bevy_text/src/lib.rs index 2bc74a1aa7..b36f5fa2bb 100644 --- a/crates/bevy_text/src/lib.rs +++ b/crates/bevy_text/src/lib.rs @@ -61,7 +61,7 @@ pub use text_access::*; pub mod prelude { #[doc(hidden)] pub use crate::{ - Font, JustifyText, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, + Font, Justify, LineBreak, Text2d, Text2dReader, Text2dWriter, TextColor, TextError, TextFont, TextLayout, TextSpan, }; } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index 50b983f929..dd6ca77246 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -16,8 +16,8 @@ use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, JustifyText, - LineBreak, PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, + error::TextError, ComputedTextBlock, Font, FontAtlasSets, FontSmoothing, Justify, LineBreak, + PositionedGlyph, TextBounds, TextEntity, TextFont, TextLayout, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -88,7 +88,7 @@ impl TextPipeline { fonts: &Assets, text_spans: impl Iterator, linebreak: LineBreak, - justify: JustifyText, + justify: Justify, bounds: TextBounds, scale_factor: f64, computed: &mut ComputedTextBlock, @@ -201,7 +201,7 @@ impl TextPipeline { // Workaround for alignment not working for unbounded text. // See https://github.com/pop-os/cosmic-text/issues/343 - if bounds.width.is_none() && justify != JustifyText::Left { + if bounds.width.is_none() && justify != Justify::Left { let dimensions = buffer_dimensions(buffer); // `set_size` causes a re-layout to occur. buffer.set_size(font_system, Some(dimensions.x), bounds.height); diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index debf9cc375..ccfdb2a372 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -116,19 +116,19 @@ impl Default for ComputedTextBlock { pub struct TextLayout { /// The text's internal alignment. /// Should not affect its position within a container. - pub justify: JustifyText, + pub justify: Justify, /// How the text should linebreak when running out of the bounds determined by `max_size`. pub linebreak: LineBreak, } impl TextLayout { /// Makes a new [`TextLayout`]. - pub const fn new(justify: JustifyText, linebreak: LineBreak) -> Self { + pub const fn new(justify: Justify, linebreak: LineBreak) -> Self { Self { justify, linebreak } } - /// Makes a new [`TextLayout`] with the specified [`JustifyText`]. - pub fn new_with_justify(justify: JustifyText) -> Self { + /// Makes a new [`TextLayout`] with the specified [`Justify`]. + pub fn new_with_justify(justify: Justify) -> Self { Self::default().with_justify(justify) } @@ -143,8 +143,8 @@ impl TextLayout { Self::default().with_no_wrap() } - /// Returns this [`TextLayout`] with the specified [`JustifyText`]. - pub const fn with_justify(mut self, justify: JustifyText) -> Self { + /// Returns this [`TextLayout`] with the specified [`Justify`]. + pub const fn with_justify(mut self, justify: Justify) -> Self { self.justify = justify; self } @@ -246,7 +246,7 @@ impl From for TextSpan { /// [`TextBounds`](super::bounds::TextBounds) component with an explicit `width` value. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize, Clone, PartialEq, Hash)] -pub enum JustifyText { +pub enum Justify { /// Leftmost character is immediately to the right of the render position. /// Bounds start from the render position and advance rightwards. #[default] @@ -263,13 +263,13 @@ pub enum JustifyText { Justified, } -impl From for cosmic_text::Align { - fn from(justify: JustifyText) -> Self { +impl From for cosmic_text::Align { + fn from(justify: Justify) -> Self { match justify { - JustifyText::Left => cosmic_text::Align::Left, - JustifyText::Center => cosmic_text::Align::Center, - JustifyText::Right => cosmic_text::Align::Right, - JustifyText::Justified => cosmic_text::Align::Justified, + Justify::Left => cosmic_text::Align::Left, + Justify::Center => cosmic_text::Align::Center, + Justify::Right => cosmic_text::Align::Right, + Justify::Justified => cosmic_text::Align::Justified, } } } @@ -354,8 +354,8 @@ impl Default for TextFont { /// Specifies the height of each line of text for `Text` and `Text2d` /// /// Default is 1.2x the font size -#[derive(Debug, Clone, Copy, Reflect)] -#[reflect(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Reflect)] +#[reflect(Debug, Clone, PartialEq)] pub enum LineHeight { /// Set line height to a specific number of pixels Px(f32), diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 5069804df8..8d4a926e1b 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -51,7 +51,7 @@ use bevy_window::{PrimaryWindow, Window}; /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, JustifyText, Text2d, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, Text2d, TextLayout, TextFont, TextColor, TextSpan}; /// # /// # let font_handle: Handle = Default::default(); /// # let mut world = World::default(); @@ -73,7 +73,7 @@ use bevy_window::{PrimaryWindow, Window}; /// // With text justification. /// world.spawn(( /// Text2d::new("hello world\nand bevy!"), -/// TextLayout::new_with_justify(JustifyText::Center) +/// TextLayout::new_with_justify(Justify::Center) /// )); /// /// // With spans diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs index 6e7b3b3991..c4118b876c 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -185,7 +185,10 @@ mod tests { use crate::{Fixed, Time, TimePlugin, TimeUpdateStrategy, Virtual}; use bevy_app::{App, FixedUpdate, Startup, Update}; use bevy_ecs::{ - event::{Event, EventReader, EventRegistry, EventWriter, Events, ShouldUpdateEvents}, + event::{ + BufferedEvent, Event, EventReader, EventRegistry, EventWriter, Events, + ShouldUpdateEvents, + }, resource::Resource, system::{Local, Res, ResMut}, }; @@ -193,7 +196,7 @@ mod tests { use core::time::Duration; use std::println; - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct TestEvent { sender: std::sync::mpsc::Sender, } @@ -206,7 +209,7 @@ mod tests { } } - #[derive(Event)] + #[derive(Event, BufferedEvent)] struct DummyEvent; #[derive(Resource, Default)] diff --git a/crates/bevy_time/src/time.rs b/crates/bevy_time/src/time.rs index d0845e22d3..8c4456f0e3 100644 --- a/crates/bevy_time/src/time.rs +++ b/crates/bevy_time/src/time.rs @@ -119,7 +119,7 @@ use { /// # use bevy_ecs::prelude::*; /// # use bevy_time::prelude::*; /// # -/// #[derive(Event)] +/// #[derive(Event, BufferedEvent)] /// struct PauseEvent(bool); /// /// fn pause_system(mut time: ResMut>, mut events: EventReader) { diff --git a/crates/bevy_transform/src/components/global_transform.rs b/crates/bevy_transform/src/components/global_transform.rs index b10d5a9d1a..cd7db6ef71 100644 --- a/crates/bevy_transform/src/components/global_transform.rs +++ b/crates/bevy_transform/src/components/global_transform.rs @@ -115,7 +115,7 @@ impl GlobalTransform { /// Returns the 3d affine transformation matrix as a [`Mat4`]. #[inline] - pub fn compute_matrix(&self) -> Mat4 { + pub fn to_matrix(&self) -> Mat4 { Mat4::from(self.0) } @@ -139,8 +139,9 @@ impl GlobalTransform { } } - /// Returns the isometric part of the transformation as an [isometry]. Any scaling done by the - /// transformation will be ignored. + /// Computes a Scale-Rotation-Translation decomposition of the transformation and returns + /// the isometric part as an [isometry]. Any scaling done by the transformation will be ignored. + /// Note: this is a somewhat costly and lossy conversion. /// /// The transform is expected to be non-degenerate and without shearing, or the output /// will be invalid. diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index 7873ae743c..bc161e9e8c 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -256,10 +256,10 @@ impl Transform { self } - /// Returns the 3d affine transformation matrix from this transforms translation, + /// Computes the 3d affine transformation matrix from this transform's translation, /// rotation, and scale. #[inline] - pub fn compute_matrix(&self) -> Mat4 { + pub fn to_matrix(&self) -> Mat4 { Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation) } diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 637ed943f2..78e80717a2 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -1,6 +1,7 @@ use crate::{ experimental::UiChildren, prelude::{Button, Label}, + ui_transform::UiGlobalTransform, widget::{ImageNode, TextUiReader}, ComputedNode, }; @@ -13,11 +14,9 @@ use bevy_ecs::{ system::{Commands, Query}, world::Ref, }; -use bevy_math::Vec3Swizzles; -use bevy_render::camera::CameraUpdateSystems; -use bevy_transform::prelude::GlobalTransform; use accesskit::{Node, Rect, Role}; +use bevy_render::camera::CameraUpdateSystems; fn calc_label( text_reader: &mut TextUiReader, @@ -40,12 +39,12 @@ fn calc_bounds( mut nodes: Query<( &mut AccessibilityNode, Ref, - Ref, + Ref, )>, ) { for (mut accessible, node, transform) in &mut nodes { if node.is_changed() || transform.is_changed() { - let center = transform.translation().xy(); + let center = transform.translation; let half_size = 0.5 * node.size; let min = center - half_size; let max = center + half_size; diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index 9242bf1380..f55cbb92b8 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,18 +1,21 @@ -use crate::{CalculatedClip, ComputedNode, ComputedNodeTarget, ResolvedBorderRadius, UiStack}; +use crate::{ + picking_backend::clip_check_recursive, ui_transform::UiGlobalTransform, ComputedNode, + ComputedNodeTarget, Node, UiStack, +}; use bevy_ecs::{ change_detection::DetectChangesMut, entity::{ContainsEntity, Entity}, + hierarchy::ChildOf, prelude::{Component, With}, query::QueryData, reflect::ReflectComponent, system::{Local, Query, Res}, }; use bevy_input::{mouse::MouseButton, touch::Touches, ButtonInput}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{camera::NormalizedRenderTarget, prelude::Camera, view::InheritedVisibility}; -use bevy_transform::components::GlobalTransform; use bevy_window::{PrimaryWindow, Window}; use smallvec::SmallVec; @@ -67,12 +70,12 @@ impl Default for Interaction { } } -/// A component storing the position of the mouse relative to the node, (0., 0.) being the top-left corner and (1., 1.) being the bottom-right -/// If the mouse is not over the node, the value will go beyond the range of (0., 0.) to (1., 1.) +/// A component storing the position of the mouse relative to the node, (0., 0.) being the center and (0.5, 0.5) being the bottom-right +/// If the mouse is not over the node, the value will go beyond the range of (-0.5, -0.5) to (0.5, 0.5) /// /// It can be used alongside [`Interaction`] to get the position of the press. /// -/// The component is updated when it is in the same entity with [`Node`](crate::Node). +/// The component is updated when it is in the same entity with [`Node`]. #[derive(Component, Copy, Clone, Default, PartialEq, Debug, Reflect)] #[reflect(Component, Default, PartialEq, Debug, Clone)] #[cfg_attr( @@ -81,8 +84,8 @@ impl Default for Interaction { reflect(Serialize, Deserialize) )] pub struct RelativeCursorPosition { - /// Visible area of the Node relative to the size of the entire Node. - pub normalized_visible_node_rect: Rect, + /// True if the cursor position is over an unclipped area of the Node. + pub cursor_over: bool, /// Cursor position relative to the size and position of the Node. /// A None value indicates that the cursor position is unknown. pub normalized: Option, @@ -90,9 +93,8 @@ pub struct RelativeCursorPosition { impl RelativeCursorPosition { /// A helper function to check if the mouse is over the node - pub fn mouse_over(&self) -> bool { - self.normalized - .is_some_and(|position| self.normalized_visible_node_rect.contains(position)) + pub fn cursor_over(&self) -> bool { + self.cursor_over } } @@ -133,11 +135,10 @@ pub struct State { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, + transform: &'static UiGlobalTransform, interaction: Option<&'static mut Interaction>, relative_cursor_position: Option<&'static mut RelativeCursorPosition>, focus_policy: Option<&'static FocusPolicy>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -154,6 +155,8 @@ pub fn ui_focus_system( touches_input: Res, ui_stack: Res, mut node_query: Query, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: Query<&ChildOf>, ) { let primary_window = primary_window.iter().next(); @@ -234,46 +237,30 @@ pub fn ui_focus_system( } let camera_entity = node.target_camera.camera()?; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); - - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); - let cursor_position = camera_cursor_positions.get(&camera_entity); + let contains_cursor = cursor_position.is_some_and(|point| { + node.node.contains_point(*node.transform, *point) + && clip_check_recursive(*point, *entity, &clipping_query, &child_of_query) + }); + // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // (-0.5, -0.5) is the top-left corner, (0.5, 0.5) is the bottom-right corner // Coordinates are relative to the entire node, not just the visible region. - let relative_cursor_position = cursor_position.and_then(|cursor_position| { + let normalized_cursor_position = cursor_position.and_then(|cursor_position| { // ensure node size is non-zero in all dimensions, otherwise relative position will be // +/-inf. if the node is hidden, the visible rect min/max will also be -inf leading to // false positives for mouse_over (#12395) - (node_rect.size().cmpgt(Vec2::ZERO).all()) - .then_some((*cursor_position - node_rect.min) / node_rect.size()) + node.node.normalize_point(*node.transform, *cursor_position) }); // If the current cursor position is within the bounds of the node's visible area, consider it for // clicking let relative_cursor_position_component = RelativeCursorPosition { - normalized_visible_node_rect: visible_rect.normalize(node_rect), - normalized: relative_cursor_position, + cursor_over: contains_cursor, + normalized: normalized_cursor_position, }; - let contains_cursor = relative_cursor_position_component.mouse_over() - && cursor_position.is_some_and(|point| { - pick_rounded_rect( - *point - node_rect.center(), - node_rect.size(), - node.node.border_radius, - ) - }); - // Save the relative cursor position to the correct component if let Some(mut node_relative_cursor_position_component) = node.relative_cursor_position { @@ -284,7 +271,8 @@ pub fn ui_focus_system( Some(*entity) } else { if let Some(mut interaction) = node.interaction { - if *interaction == Interaction::Hovered || (relative_cursor_position.is_none()) + if *interaction == Interaction::Hovered + || (normalized_cursor_position.is_none()) { interaction.set_if_neq(Interaction::None); } @@ -334,26 +322,3 @@ pub fn ui_focus_system( } } } - -// Returns true if `point` (relative to the rectangle's center) is within the bounds of a rounded rectangle with -// the given size and border radius. -// -// Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. -pub(crate) fn pick_rounded_rect( - point: Vec2, - size: Vec2, - border_radius: ResolvedBorderRadius, -) -> bool { - let [top, bottom] = if point.x < 0. { - [border_radius.top_left, border_radius.bottom_left] - } else { - [border_radius.top_right, border_radius.bottom_right] - }; - let r = if point.y < 0. { top } else { bottom }; - - let corner_to_point = point.abs() - 0.5 * size; - let q = corner_to_point + r; - let l = q.max(Vec2::ZERO).length(); - let m = q.max_element().min(0.); - l + m - r < 0. -} diff --git a/crates/bevy_ui/src/interaction_states.rs b/crates/bevy_ui/src/interaction_states.rs new file mode 100644 index 0000000000..b50f4cc245 --- /dev/null +++ b/crates/bevy_ui/src/interaction_states.rs @@ -0,0 +1,82 @@ +/// This module contains components that are used to track the interaction state of UI widgets. +use bevy_a11y::AccessibilityNode; +use bevy_ecs::{ + component::Component, + lifecycle::{Add, Remove}, + observer::On, + world::DeferredWorld, +}; + +/// A component indicating that a widget is disabled and should be "grayed out". +/// This is used to prevent user interaction with the widget. It should not, however, prevent +/// the widget from being updated or rendered, or from acquiring keyboard focus. +/// +/// For apps which support a11y: if a widget (such as a slider) contains multiple entities, +/// the `InteractionDisabled` component should be added to the root entity of the widget - the +/// same entity that contains the `AccessibilityNode` component. This will ensure that +/// the a11y tree is updated correctly. +#[derive(Component, Debug, Clone, Copy, Default)] +pub struct InteractionDisabled; + +pub(crate) fn on_add_disabled(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_disabled(); + } +} + +pub(crate) fn on_remove_disabled( + trigger: On, + mut world: DeferredWorld, +) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.clear_disabled(); + } +} + +/// Component that indicates whether a button or widget is currently in a pressed or "held down" +/// state. +#[derive(Component, Default, Debug)] +pub struct Pressed; + +/// Component that indicates that a widget can be checked. +#[derive(Component, Default, Debug)] +pub struct Checkable; + +/// Component that indicates whether a checkbox or radio button is in a checked state. +#[derive(Component, Default, Debug)] +pub struct Checked; + +pub(crate) fn on_add_checkable(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + let checked = entity.get::().is_some(); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(match checked { + true => accesskit::Toggled::True, + false => accesskit::Toggled::False, + }); + } +} + +pub(crate) fn on_remove_checkable(trigger: On, mut world: DeferredWorld) { + // Remove the 'toggled' attribute entirely. + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.clear_toggled(); + } +} + +pub(crate) fn on_add_checked(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(accesskit::Toggled::True); + } +} + +pub(crate) fn on_remove_checked(trigger: On, mut world: DeferredWorld) { + let mut entity = world.entity_mut(trigger.target()); + if let Some(mut accessibility) = entity.get_mut::() { + accessibility.set_toggled(accesskit::Toggled::False); + } +} diff --git a/crates/bevy_ui/src/layout/convert.rs b/crates/bevy_ui/src/layout/convert.rs index 079e73bb49..53c03113b9 100644 --- a/crates/bevy_ui/src/layout/convert.rs +++ b/crates/bevy_ui/src/layout/convert.rs @@ -448,6 +448,8 @@ impl RepeatedGridTrack { #[cfg(test)] mod tests { + use bevy_math::Vec2; + use super::*; #[test] @@ -523,7 +525,7 @@ mod tests { grid_column: GridPlacement::start(4), grid_row: GridPlacement::span(3), }; - let viewport_values = LayoutContext::new(1.0, bevy_math::Vec2::new(800., 600.)); + let viewport_values = LayoutContext::new(1.0, Vec2::new(800., 600.)); let taffy_style = from_node(&node, &viewport_values, false); assert_eq!(taffy_style.display, taffy::style::Display::Flex); assert_eq!(taffy_style.box_sizing, taffy::style::BoxSizing::ContentBox); @@ -661,7 +663,7 @@ mod tests { #[test] fn test_into_length_percentage() { use taffy::style::LengthPercentage; - let context = LayoutContext::new(2.0, bevy_math::Vec2::new(800., 600.)); + let context = LayoutContext::new(2.0, Vec2::new(800., 600.)); let cases = [ (Val::Auto, LengthPercentage::Length(0.)), (Val::Percent(1.), LengthPercentage::Percent(0.01)), diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index b38241a95a..484acbd4af 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -1,5 +1,6 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::{UiGlobalTransform, UiTransform}, BorderRadius, ComputedNode, ComputedNodeTarget, ContentSize, Display, LayoutConfig, Node, Outline, OverflowAxis, ScrollPosition, }; @@ -7,14 +8,14 @@ use bevy_ecs::{ change_detection::{DetectChanges, DetectChangesMut}, entity::Entity, hierarchy::{ChildOf, Children}, + lifecycle::RemovedComponents, query::With, - removal_detection::RemovedComponents, system::{Commands, Query, ResMut}, world::Ref, }; -use bevy_math::Vec2; + +use bevy_math::{Affine2, Vec2}; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; use thiserror::Error; use tracing::warn; use ui_surface::UiSurface; @@ -81,9 +82,10 @@ pub fn ui_layout_system( )>, computed_node_query: Query<(Entity, Option>), With>, ui_children: UiChildren, - mut node_transform_query: Query<( + mut node_update_query: Query<( &mut ComputedNode, - &mut Transform, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -175,7 +177,8 @@ with UI components as a child of an entity without UI components, your UI layout &mut ui_surface, true, computed_target.physical_size().as_vec2(), - &mut node_transform_query, + Affine2::IDENTITY, + &mut node_update_query, &ui_children, computed_target.scale_factor.recip(), Vec2::ZERO, @@ -190,9 +193,11 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface: &mut UiSurface, inherited_use_rounding: bool, target_size: Vec2, - node_transform_query: &mut Query<( + mut inherited_transform: Affine2, + node_update_query: &mut Query<( &mut ComputedNode, - &mut Transform, + &UiTransform, + &mut UiGlobalTransform, &Node, Option<&LayoutConfig>, Option<&BorderRadius>, @@ -206,13 +211,14 @@ with UI components as a child of an entity without UI components, your UI layout ) { if let Ok(( mut node, - mut transform, + transform, + mut global_transform, style, maybe_layout_config, maybe_border_radius, maybe_outline, maybe_scroll_position, - )) = node_transform_query.get_mut(entity) + )) = node_update_query.get_mut(entity) { let use_rounding = maybe_layout_config .map(|layout_config| layout_config.use_rounding) @@ -224,10 +230,11 @@ with UI components as a child of an entity without UI components, your UI layout let layout_size = Vec2::new(layout.size.width, layout.size.height); + // Taffy layout position of the top-left corner of the node, relative to its parent. let layout_location = Vec2::new(layout.location.x, layout.location.y); - // The position of the center of the node, stored in the node's transform - let node_center = + // The position of the center of the node relative to its top-left corner. + let local_center = layout_location - parent_scroll_position + 0.5 * (layout_size - parent_size); // only trigger change detection when the new values are different @@ -253,6 +260,16 @@ with UI components as a child of an entity without UI components, your UI layout node.bypass_change_detection().border = taffy_rect_to_border_rect(layout.border); node.bypass_change_detection().padding = taffy_rect_to_border_rect(layout.padding); + // Computer the node's new global transform + let mut local_transform = + transform.compute_affine(inverse_target_scale_factor, layout_size, target_size); + local_transform.translation += local_center; + inherited_transform *= local_transform; + + if inherited_transform != **global_transform { + *global_transform = inherited_transform.into(); + } + if let Some(border_radius) = maybe_border_radius { // We don't trigger change detection for changes to border radius node.bypass_change_detection().border_radius = border_radius.resolve( @@ -290,10 +307,6 @@ with UI components as a child of an entity without UI components, your UI layout .max(0.); } - if transform.translation.truncate() != node_center { - transform.translation = node_center.extend(0.); - } - let scroll_position: Vec2 = maybe_scroll_position .map(|scroll_pos| { Vec2::new( @@ -333,7 +346,8 @@ with UI components as a child of an entity without UI components, your UI layout ui_surface, use_rounding, target_size, - node_transform_query, + inherited_transform, + node_update_query, ui_children, inverse_target_scale_factor, layout_size, @@ -356,10 +370,7 @@ mod tests { use bevy_platform::collections::HashMap; use bevy_render::{camera::ManualTextureViews, prelude::Camera}; use bevy_transform::systems::mark_dirty_trees; - use bevy_transform::{ - prelude::GlobalTransform, - systems::{propagate_parent_transforms, sync_simple_transforms}, - }; + use bevy_transform::systems::{propagate_parent_transforms, sync_simple_transforms}; use bevy_utils::prelude::default; use bevy_window::{ PrimaryWindow, Window, WindowCreated, WindowResized, WindowResolution, @@ -684,23 +695,20 @@ mod tests { ui_schedule.run(&mut world); let overlap_check = world - .query_filtered::<(Entity, &ComputedNode, &GlobalTransform), Without>() + .query_filtered::<(Entity, &ComputedNode, &UiGlobalTransform), Without>() .iter(&world) .fold( Option::<(Rect, bool)>::None, - |option_rect, (entity, node, global_transform)| { - let current_rect = Rect::from_center_size( - global_transform.translation().truncate(), - node.size(), - ); + |option_rect, (entity, node, transform)| { + let current_rect = Rect::from_center_size(transform.translation, node.size()); assert!( current_rect.height().abs() + current_rect.width().abs() > 0., "root ui node {entity} doesn't have a logical size" ); assert_ne!( - global_transform.affine(), - GlobalTransform::default().affine(), - "root ui node {entity} global transform is not populated" + *transform, + UiGlobalTransform::default(), + "root ui node {entity} transform is not populated" ); let Some((rect, is_overlapping)) = option_rect else { return Some((current_rect, false)); diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 4ce6359206..47d396b201 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -10,6 +10,7 @@ //! Spawn UI elements with [`widget::Button`], [`ImageNode`], [`Text`](prelude::Text) and [`Node`] //! This UI is laid out with the Flexbox and CSS Grid layout models (see ) +pub mod interaction_states; pub mod measurement; pub mod ui_material; pub mod update; @@ -18,6 +19,7 @@ pub mod widget; pub mod gradients; #[cfg(feature = "bevy_ui_picking_backend")] pub mod picking_backend; +pub mod ui_transform; use bevy_derive::{Deref, DerefMut}; #[cfg(feature = "bevy_ui_picking_backend")] @@ -37,11 +39,13 @@ mod ui_node; pub use focus::*; pub use geometry::*; pub use gradients::*; +pub use interaction_states::{Checkable, Checked, InteractionDisabled, Pressed}; pub use layout::*; pub use measurement::*; pub use render::*; pub use ui_material::*; pub use ui_node::*; +pub use ui_transform::*; use widget::{ImageNode, ImageNodeSize, ViewportNode}; @@ -64,6 +68,7 @@ pub mod prelude { gradients::*, ui_material::*, ui_node::*, + ui_transform::*, widget::{Button, ImageNode, Label, NodeImageMode, ViewportNode}, Interaction, MaterialNode, UiMaterialPlugin, UiScale, }, @@ -316,6 +321,13 @@ fn build_text_interop(app: &mut App) { app.add_plugins(accessibility::AccessibilityPlugin); + app.add_observer(interaction_states::on_add_disabled) + .add_observer(interaction_states::on_remove_disabled) + .add_observer(interaction_states::on_add_checkable) + .add_observer(interaction_states::on_remove_checkable) + .add_observer(interaction_states::on_add_checked) + .add_observer(interaction_states::on_remove_checked); + app.configure_sets( PostUpdate, AmbiguousWithText.ambiguous_with(widget::text_system), diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index 26b84c6005..5647baee12 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -24,14 +24,13 @@ #![deny(missing_docs)] -use crate::{focus::pick_rounded_rect, prelude::*, UiStack}; +use crate::{prelude::*, ui_transform::UiGlobalTransform, UiStack}; use bevy_app::prelude::*; use bevy_ecs::{prelude::*, query::QueryData}; -use bevy_math::{Rect, Vec2}; +use bevy_math::Vec2; use bevy_platform::collections::HashMap; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::prelude::*; -use bevy_transform::prelude::*; use bevy_window::PrimaryWindow; use bevy_picking::backend::prelude::*; @@ -91,9 +90,8 @@ impl Plugin for UiPickingPlugin { pub struct NodeQuery { entity: Entity, node: &'static ComputedNode, - global_transform: &'static GlobalTransform, + transform: &'static UiGlobalTransform, pickable: Option<&'static Pickable>, - calculated_clip: Option<&'static CalculatedClip>, inherited_visibility: Option<&'static InheritedVisibility>, target_camera: &'static ComputedNodeTarget, } @@ -110,6 +108,8 @@ pub fn ui_picking( ui_stack: Res, node_query: Query, mut output: EventWriter, + clipping_query: Query<(&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: Query<&ChildOf>, ) { // For each camera, the pointer and its position let mut pointer_pos_by_camera = HashMap::>::default(); @@ -181,43 +181,33 @@ pub fn ui_picking( continue; }; - let node_rect = Rect::from_center_size( - node.global_transform.translation().truncate(), - node.node.size(), - ); - // Nodes with Display::None have a (0., 0.) logical rect and can be ignored - if node_rect.size() == Vec2::ZERO { + if node.node.size() == Vec2::ZERO { continue; } - // Intersect with the calculated clip rect to find the bounds of the visible region of the node - let visible_rect = node - .calculated_clip - .map(|clip| node_rect.intersect(clip.clip)) - .unwrap_or(node_rect); - let pointers_on_this_cam = pointer_pos_by_camera.get(&camera_entity); - // The mouse position relative to the node - // (0., 0.) is the top-left corner, (1., 1.) is the bottom-right corner + // Find the normalized cursor position relative to the node. + // (±0., 0.) is the center with the corners at points (±0.5, ±0.5). // Coordinates are relative to the entire node, not just the visible region. for (pointer_id, cursor_position) in pointers_on_this_cam.iter().flat_map(|h| h.iter()) { - let relative_cursor_position = (*cursor_position - node_rect.min) / node_rect.size(); - - if visible_rect - .normalize(node_rect) - .contains(relative_cursor_position) - && pick_rounded_rect( - *cursor_position - node_rect.center(), - node_rect.size(), - node.node.border_radius, + if node.node.contains_point(*node.transform, *cursor_position) + && clip_check_recursive( + *cursor_position, + *node_entity, + &clipping_query, + &child_of_query, ) { hit_nodes .entry((camera_entity, *pointer_id)) .or_default() - .push((*node_entity, relative_cursor_position)); + .push(( + *node_entity, + node.transform.inverse().transform_point2(*cursor_position) + / node.node.size(), + )); } } } @@ -262,3 +252,27 @@ pub fn ui_picking( output.write(PointerHits::new(*pointer, picks, order)); } } + +/// Walk up the tree child-to-parent checking that `point` is not clipped by any ancestor node. +pub fn clip_check_recursive( + point: Vec2, + entity: Entity, + clipping_query: &Query<'_, '_, (&ComputedNode, &UiGlobalTransform, &Node)>, + child_of_query: &Query<&ChildOf>, +) -> bool { + if let Ok(child_of) = child_of_query.get(entity) { + let parent = child_of.0; + if let Ok((computed_node, transform, node)) = clipping_query.get(parent) { + if !computed_node + .resolve_clip_rect(node.overflow, node.overflow_clip_margin) + .contains(transform.inverse().transform_point2(point)) + { + // The point is clipped and should be ignored by picking + return false; + } + } + return clip_check_recursive(point, parent, clipping_query, child_of_query); + } + // Reached root, point unclipped by all ancestors + true +} diff --git a/crates/bevy_ui/src/render/box_shadow.rs b/crates/bevy_ui/src/render/box_shadow.rs index 1c2b2c7d0a..b6f3f3501e 100644 --- a/crates/bevy_ui/src/render/box_shadow.rs +++ b/crates/bevy_ui/src/render/box_shadow.rs @@ -2,6 +2,7 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::{ BoxShadow, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, RenderUiSystems, ResolvedBorderRadius, TransparentUi, Val, @@ -18,7 +19,7 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{vec2, FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{vec2, Affine2, FloatOrd, Rect, Vec2}; use bevy_render::sync_world::MainEntity; use bevy_render::RenderApp; use bevy_render::{ @@ -29,7 +30,6 @@ use bevy_render::{ view::*, Extract, ExtractSchedule, Render, RenderSystems, }; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; use super::{stack_z_offsets, UiCameraMap, UiCameraView, QUAD_INDICES, QUAD_VERTEX_POSITIONS}; @@ -211,7 +211,7 @@ impl SpecializedRenderPipeline for BoxShadowPipeline { /// Description of a shadow to be sorted and queued for rendering pub struct ExtractedBoxShadow { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub bounds: Vec2, pub clip: Option, pub extracted_camera_entity: Entity, @@ -236,7 +236,7 @@ pub fn extract_shadows( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, &BoxShadow, Option<&CalculatedClip>, @@ -302,7 +302,7 @@ pub fn extract_shadows( extracted_box_shadows.box_shadows.push(ExtractedBoxShadow { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix() * Mat4::from_translation(offset.extend(0.)), + transform: Affine2::from(transform) * Affine2::from_translation(offset), color: drop_shadow.color.into(), bounds: shadow_size + 6. * blur_radius, clip: clip.map(|clip| clip.clip), @@ -405,11 +405,15 @@ pub fn prepare_shadows( .get(item.index) .filter(|n| item.entity() == n.render_entity) { - let rect_size = box_shadow.bounds.extend(1.0); + let rect_size = box_shadow.bounds; // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (box_shadow.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + box_shadow + .transform + .transform_point2(pos * rect_size) + .extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -443,7 +447,7 @@ pub fn prepare_shadows( positions[3] + positions_diff[3].extend(0.), ]; - let transformed_rect_size = box_shadow.transform.transform_vector3(rect_size); + let transformed_rect_size = box_shadow.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -492,7 +496,7 @@ pub fn prepare_shadows( size: box_shadow.size.into(), radius, blur: box_shadow.blur_radius, - bounds: rect_size.xy().into(), + bounds: rect_size.into(), }); } diff --git a/crates/bevy_ui/src/render/debug_overlay.rs b/crates/bevy_ui/src/render/debug_overlay.rs index aa9440b8d8..c3ada22c2e 100644 --- a/crates/bevy_ui/src/render/debug_overlay.rs +++ b/crates/bevy_ui/src/render/debug_overlay.rs @@ -1,5 +1,6 @@ use crate::shader_flags; use crate::ui_node::ComputedNodeTarget; +use crate::ui_transform::UiGlobalTransform; use crate::CalculatedClip; use crate::ComputedNode; use bevy_asset::AssetId; @@ -16,7 +17,6 @@ use bevy_render::sync_world::TemporaryRenderEntity; use bevy_render::view::InheritedVisibility; use bevy_render::Extract; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; use super::ExtractedUiItem; use super::ExtractedUiNode; @@ -62,9 +62,9 @@ pub fn extract_debug_overlay( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, - &GlobalTransform, &ComputedNodeTarget, )>, >, @@ -76,7 +76,7 @@ pub fn extract_debug_overlay( let mut camera_mapper = camera_map.get_mapper(); - for (entity, uinode, visibility, maybe_clip, transform, computed_target) in &uinode_query { + for (entity, uinode, transform, visibility, maybe_clip, computed_target) in &uinode_query { if !debug_options.show_hidden && !visibility.get() { continue; } @@ -102,7 +102,7 @@ pub fn extract_debug_overlay( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: BorderRect::all(debug_options.line_width / uinode.inverse_scale_factor()), diff --git a/crates/bevy_ui/src/render/gradient.rs b/crates/bevy_ui/src/render/gradient.rs index 31369899f7..bd818c7d5b 100644 --- a/crates/bevy_ui/src/render/gradient.rs +++ b/crates/bevy_ui/src/render/gradient.rs @@ -17,8 +17,9 @@ use bevy_ecs::{ use bevy_image::prelude::*; use bevy_math::{ ops::{cos, sin}, - FloatOrd, Mat4, Rect, Vec2, Vec3Swizzles, Vec4Swizzles, + FloatOrd, Rect, Vec2, }; +use bevy_math::{Affine2, Vec2Swizzles}; use bevy_render::sync_world::MainEntity; use bevy_render::{ render_phase::*, @@ -29,7 +30,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; use super::shader_flags::BORDER_ALL; @@ -238,7 +238,7 @@ pub enum ResolvedGradient { pub struct ExtractedGradient { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub clip: Option, pub extracted_camera_entity: Entity, @@ -354,7 +354,7 @@ pub fn extract_gradients( Entity, &ComputedNode, &ComputedNodeTarget, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, AnyOf<(&BackgroundGradient, &BorderGradient)>, @@ -414,7 +414,7 @@ pub fn extract_gradients( border_radius: uinode.border_radius, border: uinode.border, node_type, - transform: transform.compute_matrix(), + transform: transform.into(), }, main_entity: entity.into(), render_entity: commands.spawn(TemporaryRenderEntity).id(), @@ -439,7 +439,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -487,7 +487,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -541,7 +541,7 @@ pub fn extract_gradients( extracted_gradients.items.push(ExtractedGradient { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), stops_range: range_start..extracted_color_stops.0.len(), rect: Rect { min: Vec2::ZERO, @@ -675,12 +675,16 @@ pub fn prepare_gradient( *item.batch_range_mut() = item_index as u32..item_index as u32 + 1; let uinode_rect = gradient.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (gradient.transform * (pos * rect_size).extend(1.)).xyz()); - let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + gradient + .transform + .transform_point2(pos * rect_size) + .extend(0.) + }); + let corner_points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -721,7 +725,7 @@ pub fn prepare_gradient( corner_points[3] + positions_diff[3], ]; - let transformed_rect_size = gradient.transform.transform_vector3(rect_size); + let transformed_rect_size = gradient.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 83140d0f3b..61319eda9b 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -8,7 +8,9 @@ pub mod ui_texture_slice_pipeline; mod debug_overlay; mod gradient; +use crate::prelude::UiGlobalTransform; use crate::widget::{ImageNode, ViewportNode}; + use crate::{ BackgroundColor, BorderColor, BoxShadowSamples, CalculatedClip, ComputedNode, ComputedNodeTarget, Outline, ResolvedBorderRadius, TextShadow, UiAntiAlias, @@ -22,7 +24,7 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_ecs::prelude::*; use bevy_ecs::system::SystemParam; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Mat4, Rect, UVec4, Vec2}; use bevy_render::load_shader_library; use bevy_render::render_graph::{NodeRunError, RenderGraphContext}; use bevy_render::render_phase::ViewSortedRenderPhases; @@ -243,7 +245,7 @@ pub enum ExtractedUiItem { /// Ordering: left, top, right, bottom. border: BorderRect, node_type: NodeType, - transform: Mat4, + transform: Affine2, }, /// A contiguous sequence of text glyphs from the same section Glyphs { @@ -253,7 +255,7 @@ pub enum ExtractedUiItem { } pub struct ExtractedGlyph { - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, } @@ -344,7 +346,7 @@ pub fn extract_uinode_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -383,7 +385,7 @@ pub fn extract_uinode_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -403,7 +405,7 @@ pub fn extract_uinode_images( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -467,7 +469,7 @@ pub fn extract_uinode_images( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: image.flip_x, flip_y: image.flip_y, border: uinode.border, @@ -487,7 +489,7 @@ pub fn extract_uinode_borders( Entity, &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -503,7 +505,7 @@ pub fn extract_uinode_borders( entity, node, computed_node, - global_transform, + transform, inherited_visibility, maybe_clip, camera, @@ -567,7 +569,7 @@ pub fn extract_uinode_borders( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: global_transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: computed_node.border(), @@ -600,7 +602,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), extracted_camera_entity, item: ExtractedUiItem::Node { - transform: global_transform.compute_matrix(), + transform: transform.into(), atlas_scaling: None, flip_x: false, flip_y: false, @@ -749,7 +751,7 @@ pub fn extract_viewport_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -792,7 +794,7 @@ pub fn extract_viewport_nodes( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform.compute_matrix(), + transform: transform.into(), flip_x: false, flip_y: false, border: uinode.border(), @@ -812,7 +814,7 @@ pub fn extract_text_sections( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -830,7 +832,7 @@ pub fn extract_text_sections( for ( entity, uinode, - global_transform, + transform, inherited_visibility, clip, camera, @@ -847,8 +849,7 @@ pub fn extract_text_sections( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation((-0.5 * uinode.size()).extend(0.)); + let transform = Affine2::from(*transform) * Affine2::from_translation(-0.5 * uinode.size()); for ( i, @@ -866,7 +867,7 @@ pub fn extract_text_sections( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: transform * Affine2::from_translation(*position), rect, }); @@ -910,8 +911,8 @@ pub fn extract_text_shadows( Query<( Entity, &ComputedNode, + &UiGlobalTransform, &ComputedNodeTarget, - &GlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &TextLayoutInfo, @@ -924,16 +925,8 @@ pub fn extract_text_shadows( let mut end = start + 1; let mut camera_mapper = camera_map.get_mapper(); - for ( - entity, - uinode, - target, - global_transform, - inherited_visibility, - clip, - text_layout_info, - shadow, - ) in &uinode_query + for (entity, uinode, transform, target, inherited_visibility, clip, text_layout_info, shadow) in + &uinode_query { // Skip if not visible or if size is set to zero (e.g. when a parent is set to `Display::None`) if !inherited_visibility.get() || uinode.is_empty() { @@ -944,9 +937,9 @@ pub fn extract_text_shadows( continue; }; - let transform = global_transform.affine() - * Mat4::from_translation( - (-0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor()).extend(0.), + let node_transform = Affine2::from(*transform) + * Affine2::from_translation( + -0.5 * uinode.size() + shadow.offset / uinode.inverse_scale_factor(), ); for ( @@ -965,7 +958,7 @@ pub fn extract_text_shadows( .textures[atlas_info.location.glyph_index] .as_rect(); extracted_uinodes.glyphs.push(ExtractedGlyph { - transform: transform * Mat4::from_translation(position.extend(0.)), + transform: node_transform * Affine2::from_translation(*position), rect, }); @@ -998,7 +991,7 @@ pub fn extract_text_background_colors( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -1021,8 +1014,8 @@ pub fn extract_text_background_colors( continue; }; - let transform = global_transform.affine() - * bevy_math::Affine3A::from_translation(-0.5 * uinode.size().extend(0.)); + let transform = + Affine2::from(global_transform) * Affine2::from_translation(-0.5 * uinode.size()); for &(section_entity, rect) in text_layout_info.section_rects.iter() { let Ok(text_background_color) = text_background_colors_query.get(section_entity) else { @@ -1042,7 +1035,7 @@ pub fn extract_text_background_colors( extracted_camera_entity, item: ExtractedUiItem::Node { atlas_scaling: None, - transform: transform * Mat4::from_translation(rect.center().extend(0.)), + transform: transform * Affine2::from_translation(rect.center()), flip_x: false, flip_y: false, border: uinode.border(), @@ -1093,11 +1086,11 @@ impl Default for UiMeta { } } -pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ - Vec3::new(-0.5, -0.5, 0.0), - Vec3::new(0.5, -0.5, 0.0), - Vec3::new(0.5, 0.5, 0.0), - Vec3::new(-0.5, 0.5, 0.0), +pub(crate) const QUAD_VERTEX_POSITIONS: [Vec2; 4] = [ + Vec2::new(-0.5, -0.5), + Vec2::new(0.5, -0.5), + Vec2::new(0.5, 0.5), + Vec2::new(-0.5, 0.5), ]; pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; @@ -1321,12 +1314,12 @@ pub fn prepare_uinodes( let mut uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); - let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy()); + .map(|pos| transform.transform_point2(pos * rect_size).extend(0.)); + let points = QUAD_VERTEX_POSITIONS.map(|pos| pos * rect_size); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -1367,7 +1360,7 @@ pub fn prepare_uinodes( points[3] + positions_diff[3], ]; - let transformed_rect_size = transform.transform_vector3(rect_size); + let transformed_rect_size = transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π @@ -1448,7 +1441,7 @@ pub fn prepare_uinodes( border_radius.bottom_left, ], border: [border.left, border.top, border.right, border.bottom], - size: rect_size.xy().into(), + size: rect_size.into(), point: points[i].into(), }); } @@ -1470,13 +1463,14 @@ pub fn prepare_uinodes( let color = extracted_uinode.color.to_f32_array(); for glyph in &extracted_uinodes.glyphs[range.clone()] { let glyph_rect = glyph.rect; - let size = glyph.rect.size(); - - let rect_size = glyph_rect.size().extend(1.0); + let rect_size = glyph_rect.size(); // Specify the corners of the glyph let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (glyph.transform * (pos * rect_size).extend(1.)).xyz() + glyph + .transform + .transform_point2(pos * glyph_rect.size()) + .extend(0.) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -1511,7 +1505,7 @@ pub fn prepare_uinodes( // cull nodes that are completely clipped let transformed_rect_size = - glyph.transform.transform_vector3(rect_size); + glyph.transform.transform_vector2(rect_size); if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x.abs() || positions_diff[1].y - positions_diff[2].y @@ -1548,7 +1542,7 @@ pub fn prepare_uinodes( flags: shader_flags::TEXTURED | shader_flags::CORNERS[i], radius: [0.0; 4], border: [0.0; 4], - size: size.into(), + size: rect_size.into(), point: [0.0; 2], }); } diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 02fab4fdee..3ad4f4ea6a 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -1,9 +1,7 @@ -use core::{hash::Hash, marker::PhantomData, ops::Range}; - use crate::*; use bevy_asset::*; use bevy_ecs::{ - prelude::Component, + prelude::{Component, With}, query::ROQueryItem, system::{ lifetimeless::{Read, SRes}, @@ -11,24 +9,22 @@ use bevy_ecs::{ }, }; use bevy_image::BevyDefault as _; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_render::{ extract_component::ExtractComponentPlugin, globals::{GlobalsBuffer, GlobalsUniform}, + load_shader_library, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::{binding_types::uniform_buffer, *}, renderer::{RenderDevice, RenderQueue}, + sync_world::{MainEntity, TemporaryRenderEntity}, view::*, Extract, ExtractSchedule, Render, RenderSystems, }; -use bevy_render::{ - load_shader_library, - sync_world::{MainEntity, TemporaryRenderEntity}, -}; use bevy_sprite::BorderRect; -use bevy_transform::prelude::GlobalTransform; use bytemuck::{Pod, Zeroable}; +use core::{hash::Hash, marker::PhantomData, ops::Range}; /// Adds the necessary ECS resources and render logic to enable rendering entities using the given /// [`UiMaterial`] asset type (which includes [`UiMaterial`] types). @@ -280,7 +276,7 @@ impl RenderCommand

fn render<'w>( _item: &P, _view: (), - material_handle: Option>, + material_handle: Option>, materials: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { @@ -321,7 +317,7 @@ impl RenderCommand

for DrawUiMaterialNode { pub struct ExtractedUiMaterialNode { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub border: BorderRect, pub border_radius: ResolvedBorderRadius, @@ -356,7 +352,7 @@ pub fn extract_ui_material_nodes( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &MaterialNode, &InheritedVisibility, Option<&CalculatedClip>, @@ -387,7 +383,7 @@ pub fn extract_ui_material_nodes( extracted_uinodes.uinodes.push(ExtractedUiMaterialNode { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: computed_node.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), material: handle.id(), rect: Rect { min: Vec2::ZERO, @@ -459,10 +455,13 @@ pub fn prepare_uimaterial_nodes( let uinode_rect = extracted_uinode.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); let positions = QUAD_VERTEX_POSITIONS.map(|pos| { - (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz() + extracted_uinode + .transform + .transform_point2(pos * rect_size) + .extend(1.0) }); let positions_diff = if let Some(clip) = extracted_uinode.clip { @@ -496,7 +495,7 @@ pub fn prepare_uimaterial_nodes( ]; let transformed_rect_size = - extracted_uinode.transform.transform_vector3(rect_size); + extracted_uinode.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 80a55bbcd4..0e232ab1cc 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -1,5 +1,6 @@ use core::{hash::Hash, ops::Range}; +use crate::prelude::UiGlobalTransform; use crate::*; use bevy_asset::*; use bevy_color::{Alpha, ColorToComponents, LinearRgba}; @@ -11,7 +12,7 @@ use bevy_ecs::{ }, }; use bevy_image::prelude::*; -use bevy_math::{FloatOrd, Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_math::{Affine2, FloatOrd, Rect, Vec2}; use bevy_platform::collections::HashMap; use bevy_render::sync_world::MainEntity; use bevy_render::{ @@ -25,7 +26,6 @@ use bevy_render::{ Extract, ExtractSchedule, Render, RenderSystems, }; use bevy_sprite::{SliceScaleMode, SpriteAssetEvents, SpriteImageMode, TextureSlicer}; -use bevy_transform::prelude::GlobalTransform; use binding_types::{sampler, texture_2d}; use bytemuck::{Pod, Zeroable}; use widget::ImageNode; @@ -218,7 +218,7 @@ impl SpecializedRenderPipeline for UiTextureSlicePipeline { pub struct ExtractedUiTextureSlice { pub stack_index: u32, - pub transform: Mat4, + pub transform: Affine2, pub rect: Rect, pub atlas_rect: Option, pub image: AssetId, @@ -246,7 +246,7 @@ pub fn extract_ui_texture_slices( Query<( Entity, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, &InheritedVisibility, Option<&CalculatedClip>, &ComputedNodeTarget, @@ -306,7 +306,7 @@ pub fn extract_ui_texture_slices( extracted_ui_slicers.slices.push(ExtractedUiTextureSlice { render_entity: commands.spawn(TemporaryRenderEntity).id(), stack_index: uinode.stack_index, - transform: transform.compute_matrix(), + transform: transform.into(), color: image.color.into(), rect: Rect { min: Vec2::ZERO, @@ -497,11 +497,12 @@ pub fn prepare_ui_slices( let uinode_rect = texture_slices.rect; - let rect_size = uinode_rect.size().extend(1.0); + let rect_size = uinode_rect.size(); // Specify the corners of the node - let positions = QUAD_VERTEX_POSITIONS - .map(|pos| (texture_slices.transform * (pos * rect_size).extend(1.)).xyz()); + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (texture_slices.transform.transform_point2(pos * rect_size)).extend(0.) + }); // Calculate the effect of clipping // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) @@ -536,7 +537,7 @@ pub fn prepare_ui_slices( ]; let transformed_rect_size = - texture_slices.transform.transform_vector3(rect_size); + texture_slices.transform.transform_vector2(rect_size); // Don't try to cull nodes that have a rotation // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index f5f914bdc0..d81f80f8b9 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1,4 +1,7 @@ -use crate::{FocusPolicy, UiRect, Val}; +use crate::{ + ui_transform::{UiGlobalTransform, UiTransform}, + FocusPolicy, UiRect, Val, +}; use bevy_color::{Alpha, Color}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{prelude::*, system::SystemParam}; @@ -9,7 +12,6 @@ use bevy_render::{ view::Visibility, }; use bevy_sprite::BorderRect; -use bevy_transform::components::Transform; use bevy_utils::once; use bevy_window::{PrimaryWindow, WindowRef}; use core::{f32, num::NonZero}; @@ -229,6 +231,73 @@ impl ComputedNode { pub const fn inverse_scale_factor(&self) -> f32 { self.inverse_scale_factor } + + // Returns true if `point` within the node. + // + // Matches the sdf function in `ui.wgsl` that is used by the UI renderer to draw rounded rectangles. + pub fn contains_point(&self, transform: UiGlobalTransform, point: Vec2) -> bool { + let Some(local_point) = transform + .try_inverse() + .map(|transform| transform.transform_point2(point)) + else { + return false; + }; + let [top, bottom] = if local_point.x < 0. { + [self.border_radius.top_left, self.border_radius.bottom_left] + } else { + [ + self.border_radius.top_right, + self.border_radius.bottom_right, + ] + }; + let r = if local_point.y < 0. { top } else { bottom }; + let corner_to_point = local_point.abs() - 0.5 * self.size; + let q = corner_to_point + r; + let l = q.max(Vec2::ZERO).length(); + let m = q.max_element().min(0.); + l + m - r < 0. + } + + /// Transform a point to normalized node space with the center of the node at the origin and the corners at [+/-0.5, +/-0.5] + pub fn normalize_point(&self, transform: UiGlobalTransform, point: Vec2) -> Option { + self.size + .cmpgt(Vec2::ZERO) + .all() + .then(|| transform.try_inverse()) + .flatten() + .map(|transform| transform.transform_point2(point) / self.size) + } + + /// Resolve the node's clipping rect in local space + pub fn resolve_clip_rect( + &self, + overflow: Overflow, + overflow_clip_margin: OverflowClipMargin, + ) -> Rect { + let mut clip_rect = Rect::from_center_size(Vec2::ZERO, self.size); + + let clip_inset = match overflow_clip_margin.visual_box { + OverflowClipBox::BorderBox => BorderRect::ZERO, + OverflowClipBox::ContentBox => self.content_inset(), + OverflowClipBox::PaddingBox => self.border(), + }; + + clip_rect.min.x += clip_inset.left; + clip_rect.min.y += clip_inset.top; + clip_rect.max.x -= clip_inset.right; + clip_rect.max.y -= clip_inset.bottom; + + if overflow.x == OverflowAxis::Visible { + clip_rect.min.x = -f32::INFINITY; + clip_rect.max.x = f32::INFINITY; + } + if overflow.y == OverflowAxis::Visible { + clip_rect.min.y = -f32::INFINITY; + clip_rect.max.y = f32::INFINITY; + } + + clip_rect + } } impl ComputedNode { @@ -323,12 +392,12 @@ impl From for ScrollPosition { #[require( ComputedNode, ComputedNodeTarget, + UiTransform, BackgroundColor, BorderColor, BorderRadius, FocusPolicy, ScrollPosition, - Transform, Visibility, ZIndex )] @@ -2061,6 +2130,16 @@ impl BorderColor { } } + /// Helper to set all border colors to a given color. + pub fn set_all(&mut self, color: impl Into) -> &mut Self { + let color: Color = color.into(); + self.top = color; + self.bottom = color; + self.left = color; + self.right = color; + self + } + /// Check if all contained border colors are transparent pub fn is_fully_transparent(&self) -> bool { self.top.is_fully_transparent() @@ -2796,8 +2875,10 @@ impl ComputedNodeTarget { } /// Adds a shadow behind text -#[derive(Component, Copy, Clone, Debug, Reflect)] -#[reflect(Component, Default, Debug, Clone)] +/// +/// Not supported by `Text2d` +#[derive(Component, Copy, Clone, Debug, PartialEq, Reflect)] +#[reflect(Component, Default, Debug, Clone, PartialEq)] pub struct TextShadow { /// Shadow displacement in logical pixels /// With a value of zero the shadow will be hidden directly behind the text diff --git a/crates/bevy_ui/src/ui_transform.rs b/crates/bevy_ui/src/ui_transform.rs new file mode 100644 index 0000000000..47f8484e54 --- /dev/null +++ b/crates/bevy_ui/src/ui_transform.rs @@ -0,0 +1,191 @@ +use crate::Val; +use bevy_derive::Deref; +use bevy_ecs::component::Component; +use bevy_ecs::prelude::ReflectComponent; +use bevy_math::Affine2; +use bevy_math::Rot2; +use bevy_math::Vec2; +use bevy_reflect::prelude::*; + +/// A pair of [`Val`]s used to represent a 2-dimensional size or offset. +#[derive(Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct Val2 { + /// Translate the node along the x-axis. + /// `Val::Percent` values are resolved based on the computed width of the Ui Node. + /// `Val::Auto` is resolved to `0.`. + pub x: Val, + /// Translate the node along the y-axis. + /// `Val::Percent` values are resolved based on the computed height of the UI Node. + /// `Val::Auto` is resolved to `0.`. + pub y: Val, +} + +impl Val2 { + pub const ZERO: Self = Self { + x: Val::ZERO, + y: Val::ZERO, + }; + + /// Creates a new [`Val2`] where both components are in logical pixels + pub const fn px(x: f32, y: f32) -> Self { + Self { + x: Val::Px(x), + y: Val::Px(y), + } + } + + /// Creates a new [`Val2`] where both components are percentage values + pub const fn percent(x: f32, y: f32) -> Self { + Self { + x: Val::Percent(x), + y: Val::Percent(y), + } + } + + /// Creates a new [`Val2`] + pub const fn new(x: Val, y: Val) -> Self { + Self { x, y } + } + + /// Resolves this [`Val2`] from the given `scale_factor`, `parent_size`, + /// and `viewport_size`. + /// + /// Component values of [`Val::Auto`] are resolved to 0. + pub fn resolve(&self, scale_factor: f32, base_size: Vec2, viewport_size: Vec2) -> Vec2 { + Vec2::new( + self.x + .resolve(scale_factor, base_size.x, viewport_size) + .unwrap_or(0.), + self.y + .resolve(scale_factor, base_size.y, viewport_size) + .unwrap_or(0.), + ) + } +} + +impl Default for Val2 { + fn default() -> Self { + Self::ZERO + } +} + +/// Relative 2D transform for UI nodes +/// +/// [`UiGlobalTransform`] is automatically inserted whenever [`UiTransform`] is inserted. +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +#[require(UiGlobalTransform)] +pub struct UiTransform { + /// Translate the node. + pub translation: Val2, + /// Scale the node. A negative value reflects the node in that axis. + pub scale: Vec2, + /// Rotate the node clockwise. + pub rotation: Rot2, +} + +impl UiTransform { + pub const IDENTITY: Self = Self { + translation: Val2::ZERO, + scale: Vec2::ONE, + rotation: Rot2::IDENTITY, + }; + + /// Creates a UI transform representing a rotation. + pub fn from_rotation(rotation: Rot2) -> Self { + Self { + rotation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a responsive translation. + pub fn from_translation(translation: Val2) -> Self { + Self { + translation, + ..Self::IDENTITY + } + } + + /// Creates a UI transform representing a scaling. + pub fn from_scale(scale: Vec2) -> Self { + Self { + scale, + ..Self::IDENTITY + } + } + + /// Resolves the translation from the given `scale_factor`, `base_value`, and `target_size` + /// and returns a 2d affine transform from the resolved translation, and the `UiTransform`'s rotation, and scale. + pub fn compute_affine(&self, scale_factor: f32, base_size: Vec2, target_size: Vec2) -> Affine2 { + Affine2::from_scale_angle_translation( + self.scale, + self.rotation.as_radians(), + self.translation + .resolve(scale_factor, base_size, target_size), + ) + } +} + +impl Default for UiTransform { + fn default() -> Self { + Self::IDENTITY + } +} + +/// Absolute 2D transform for UI nodes +/// +/// [`UiGlobalTransform`]s are updated from [`UiTransform`] and [`Node`](crate::ui_node::Node) +/// in [`ui_layout_system`](crate::layout::ui_layout_system) +#[derive(Component, Debug, PartialEq, Clone, Copy, Reflect, Deref)] +#[reflect(Component, Default, PartialEq, Debug, Clone)] +#[cfg_attr( + feature = "serialize", + derive(serde::Serialize, serde::Deserialize), + reflect(Serialize, Deserialize) +)] +pub struct UiGlobalTransform(Affine2); + +impl Default for UiGlobalTransform { + fn default() -> Self { + Self(Affine2::IDENTITY) + } +} + +impl UiGlobalTransform { + /// If the transform is invertible returns its inverse. + /// Otherwise returns `None`. + #[inline] + pub fn try_inverse(&self) -> Option { + (self.matrix2.determinant() != 0.).then_some(self.inverse()) + } +} + +impl From for UiGlobalTransform { + fn from(value: Affine2) -> Self { + Self(value) + } +} + +impl From for Affine2 { + fn from(value: UiGlobalTransform) -> Self { + value.0 + } +} + +impl From<&UiGlobalTransform> for Affine2 { + fn from(value: &UiGlobalTransform) -> Self { + value.0 + } +} diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 7e27c4abdd..c0e9d09d7b 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -2,6 +2,7 @@ use crate::{ experimental::{UiChildren, UiRootNodes}, + ui_transform::UiGlobalTransform, CalculatedClip, ComputedNodeTarget, DefaultUiCamera, Display, Node, OverflowAxis, UiScale, UiTargetCamera, }; @@ -17,7 +18,6 @@ use bevy_ecs::{ use bevy_math::{Rect, UVec2}; use bevy_render::camera::Camera; use bevy_sprite::BorderRect; -use bevy_transform::components::GlobalTransform; /// Updates clipping for all nodes pub fn update_clipping_system( @@ -26,7 +26,7 @@ pub fn update_clipping_system( mut node_query: Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, ui_children: UiChildren, @@ -48,14 +48,13 @@ fn update_clipping( node_query: &mut Query<( &Node, &ComputedNode, - &GlobalTransform, + &UiGlobalTransform, Option<&mut CalculatedClip>, )>, entity: Entity, mut maybe_inherited_clip: Option, ) { - let Ok((node, computed_node, global_transform, maybe_calculated_clip)) = - node_query.get_mut(entity) + let Ok((node, computed_node, transform, maybe_calculated_clip)) = node_query.get_mut(entity) else { return; }; @@ -91,10 +90,7 @@ fn update_clipping( maybe_inherited_clip } else { // Find the current node's clipping rect and intersect it with the inherited clipping rect, if one exists - let mut clip_rect = Rect::from_center_size( - global_transform.translation().truncate(), - computed_node.size(), - ); + let mut clip_rect = Rect::from_center_size(transform.translation, computed_node.size()); // Content isn't clipped at the edges of the node but at the edges of the region specified by [`Node::overflow_clip_margin`]. // diff --git a/crates/bevy_ui/src/widget/image.rs b/crates/bevy_ui/src/widget/image.rs index c65c4df354..9a743595b8 100644 --- a/crates/bevy_ui/src/widget/image.rs +++ b/crates/bevy_ui/src/widget/image.rs @@ -138,8 +138,8 @@ impl From> for ImageNode { } /// Controls how the image is altered to fit within the layout and how the layout algorithm determines the space in the layout for the image -#[derive(Default, Debug, Clone, Reflect)] -#[reflect(Clone, Default)] +#[derive(Default, Debug, Clone, PartialEq, Reflect)] +#[reflect(Clone, Default, PartialEq)] pub enum NodeImageMode { /// The image will be sized automatically by taking the size of the source image and applying any layout constraints. #[default] diff --git a/crates/bevy_ui/src/widget/text.rs b/crates/bevy_ui/src/widget/text.rs index 785040c1e9..d7f8e243a4 100644 --- a/crates/bevy_ui/src/widget/text.rs +++ b/crates/bevy_ui/src/widget/text.rs @@ -61,7 +61,7 @@ impl Default for TextNodeFlags { /// # use bevy_color::Color; /// # use bevy_color::palettes::basic::BLUE; /// # use bevy_ecs::world::World; -/// # use bevy_text::{Font, JustifyText, TextLayout, TextFont, TextColor, TextSpan}; +/// # use bevy_text::{Font, Justify, TextLayout, TextFont, TextColor, TextSpan}; /// # use bevy_ui::prelude::Text; /// # /// # let font_handle: Handle = Default::default(); @@ -84,7 +84,7 @@ impl Default for TextNodeFlags { /// // With text justification. /// world.spawn(( /// Text::new("hello world\nand bevy!"), -/// TextLayout::new_with_justify(JustifyText::Center) +/// TextLayout::new_with_justify(Justify::Center) /// )); /// /// // With spans diff --git a/crates/bevy_ui/src/widget/viewport.rs b/crates/bevy_ui/src/widget/viewport.rs index f68033ea7f..9cdc348da5 100644 --- a/crates/bevy_ui/src/widget/viewport.rs +++ b/crates/bevy_ui/src/widget/viewport.rs @@ -171,11 +171,6 @@ pub fn update_viewport_render_target_size( height: u32::max(1, size.y as u32), ..default() }; - let image = images.get_mut(image_handle).unwrap(); - if image.data.is_some() { - image.resize(size); - } else { - image.texture_descriptor.size = size; - } + images.get_mut(image_handle).unwrap().resize(size); } } diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index 53eda0d358..aaf6a0834e 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -16,9 +16,14 @@ wgpu_wrapper = ["dep:send_wrapper"] # Provides access to the `Parallel` type. parallel = ["bevy_platform/std", "dep:thread_local"] +std = ["disqualified/alloc"] + +debug = [] + [dependencies] bevy_platform = { path = "../bevy_platform", version = "0.16.0-dev", default-features = false } +disqualified = { version = "1.0", default-features = false } thread_local = { version = "1.0", optional = true } [target.'cfg(all(target_arch = "wasm32", target_feature = "atomics"))'.dependencies] diff --git a/crates/bevy_utils/src/debug_info.rs b/crates/bevy_utils/src/debug_info.rs new file mode 100644 index 0000000000..c79c5ebe60 --- /dev/null +++ b/crates/bevy_utils/src/debug_info.rs @@ -0,0 +1,102 @@ +use alloc::{borrow::Cow, fmt, string::String}; +#[cfg(feature = "debug")] +use core::any::type_name; +use disqualified::ShortName; + +#[cfg(not(feature = "debug"))] +const FEATURE_DISABLED: &'static str = "Enable the debug feature to see the name"; + +/// Wrapper to help debugging ECS issues. This is used to display the names of systems, components, ... +/// +/// * If the `debug` feature is enabled, the actual name will be used +/// * If it is disabled, a string mentioning the disabled feature will be used +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct DebugName { + #[cfg(feature = "debug")] + name: Cow<'static, str>, +} + +impl fmt::Display for DebugName { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + #[cfg(feature = "debug")] + f.write_str(self.name.as_ref())?; + #[cfg(not(feature = "debug"))] + f.write_str(FEATURE_DISABLED)?; + + Ok(()) + } +} + +impl DebugName { + /// Create a new `DebugName` from a `&str` + /// + /// The value will be ignored if the `debug` feature is not enabled + #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + pub fn borrowed(value: &'static str) -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Borrowed(value), + } + } + + /// Create a new `DebugName` from a `String` + /// + /// The value will be ignored if the `debug` feature is not enabled + #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + pub fn owned(value: String) -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Owned(value), + } + } + + /// Create a new `DebugName` from a type by using its [`core::any::type_name`] + /// + /// The value will be ignored if the `debug` feature is not enabled + pub fn type_name() -> Self { + DebugName { + #[cfg(feature = "debug")] + name: Cow::Borrowed(type_name::()), + } + } + + /// Get the [`ShortName`] corresping to this debug name + /// + /// The value will be a static string if the `debug` feature is not enabled + pub fn shortname(&self) -> ShortName { + #[cfg(feature = "debug")] + return ShortName(self.name.as_ref()); + #[cfg(not(feature = "debug"))] + return ShortName(FEATURE_DISABLED); + } + + /// Return the string hold by this `DebugName` + /// + /// This is intended for debugging purpose, and only available if the `debug` feature is enabled + #[cfg(feature = "debug")] + pub fn as_string(&self) -> String { + self.name.clone().into_owned() + } +} + +impl From> for DebugName { + #[cfg_attr(not(feature = "debug"), expect(unused_variables))] + fn from(value: Cow<'static, str>) -> Self { + Self { + #[cfg(feature = "debug")] + name: value, + } + } +} + +impl From for DebugName { + fn from(value: String) -> Self { + Self::owned(value) + } +} + +impl From<&'static str> for DebugName { + fn from(value: &'static str) -> Self { + Self::borrowed(value) + } +} diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index e3bb07a512..58979139bb 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -43,12 +43,14 @@ cfg::parallel! { /// /// This includes the most common types in this crate, re-exported for your convenience. pub mod prelude { + pub use crate::debug_info::DebugName; pub use crate::default; } #[cfg(feature = "wgpu_wrapper")] mod wgpu_wrapper; +mod debug_info; mod default; mod once; diff --git a/crates/bevy_utils/src/map.rs b/crates/bevy_utils/src/map.rs index ca74e34dbb..3b54a357aa 100644 --- a/crates/bevy_utils/src/map.rs +++ b/crates/bevy_utils/src/map.rs @@ -1,7 +1,7 @@ use core::{any::TypeId, hash::Hash}; use bevy_platform::{ - collections::HashMap, + collections::{hash_map::Entry, HashMap}, hash::{Hashed, NoOpHash, PassHash}, }; @@ -38,6 +38,78 @@ impl PreHashMapExt for PreHashMap = HashMap; +/// Extension trait to make use of [`TypeIdMap`] more ergonomic. +/// +/// Each function on this trait is a trivial wrapper for a function +/// on [`HashMap`], replacing a `TypeId` key with a +/// generic parameter `T`. +/// +/// # Examples +/// +/// ```rust +/// # use std::any::TypeId; +/// # use bevy_utils::TypeIdMap; +/// use bevy_utils::TypeIdMapExt; +/// +/// struct MyType; +/// +/// // Using the built-in `HashMap` functions requires manually looking up `TypeId`s. +/// let mut map = TypeIdMap::default(); +/// map.insert(TypeId::of::(), 7); +/// assert_eq!(map.get(&TypeId::of::()), Some(&7)); +/// +/// // Using `TypeIdMapExt` functions does the lookup for you. +/// map.insert_type::(7); +/// assert_eq!(map.get_type::(), Some(&7)); +/// ``` +pub trait TypeIdMapExt { + /// Inserts a value for the type `T`. + /// + /// If the map did not previously contain this key then [`None`] is returned, + /// otherwise the value for this key is updated and the old value returned. + fn insert_type(&mut self, v: V) -> Option; + + /// Returns a reference to the value for type `T`, if one exists. + fn get_type(&self) -> Option<&V>; + + /// Returns a mutable reference to the value for type `T`, if one exists. + fn get_type_mut(&mut self) -> Option<&mut V>; + + /// Removes type `T` from the map, returning the value for this + /// key if it was previously present. + fn remove_type(&mut self) -> Option; + + /// Gets the type `T`'s entry in the map for in-place manipulation. + fn entry_type(&mut self) -> Entry<'_, TypeId, V, NoOpHash>; +} + +impl TypeIdMapExt for TypeIdMap { + #[inline] + fn insert_type(&mut self, v: V) -> Option { + self.insert(TypeId::of::(), v) + } + + #[inline] + fn get_type(&self) -> Option<&V> { + self.get(&TypeId::of::()) + } + + #[inline] + fn get_type_mut(&mut self) -> Option<&mut V> { + self.get_mut(&TypeId::of::()) + } + + #[inline] + fn remove_type(&mut self) -> Option { + self.remove(&TypeId::of::()) + } + + #[inline] + fn entry_type(&mut self) -> Entry<'_, TypeId, V, NoOpHash> { + self.entry(TypeId::of::()) + } +} + #[cfg(test)] mod tests { use super::*; @@ -67,7 +139,7 @@ mod tests { #[test] fn stable_hash_within_same_program_execution() { use alloc::vec::Vec; - + let mut map_1 = >::default(); let mut map_2 = >::default(); for i in 1..10 { diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 026b85dc32..81360ef9c4 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -1,5 +1,8 @@ use alloc::string::String; -use bevy_ecs::{entity::Entity, event::Event}; +use bevy_ecs::{ + entity::Entity, + event::{BufferedEvent, Event}, +}; use bevy_input::{ gestures::*, keyboard::{KeyboardFocusLost, KeyboardInput}, @@ -23,7 +26,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; use crate::WindowTheme; /// A window event that is sent whenever a window's logical size has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -45,7 +48,7 @@ pub struct WindowResized { /// An event that indicates all of the application's windows should be redrawn, /// even if their control flow is set to `Wait` and there have been no window events. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -61,7 +64,7 @@ pub struct RequestRedraw; /// An event that is sent whenever a new window is created. /// /// To create a new window, spawn an entity with a [`crate::Window`] on it. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -87,7 +90,7 @@ pub struct WindowCreated { /// /// [`WindowPlugin`]: crate::WindowPlugin /// [`Window`]: crate::Window -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -105,7 +108,7 @@ pub struct WindowCloseRequested { /// An event that is sent whenever a window is closed. This will be sent when /// the window entity loses its [`Window`](crate::window::Window) component or is despawned. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -126,7 +129,7 @@ pub struct WindowClosed { /// An event that is sent whenever a window is closing. This will be sent when /// after a [`WindowCloseRequested`] event is received and the window is in the process of closing. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -146,7 +149,7 @@ pub struct WindowClosing { /// /// Note that if your application only has a single window, this event may be your last chance to /// persist state before the application terminates. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -176,7 +179,7 @@ pub struct WindowDestroyed { /// you should not use it for non-cursor-like behavior such as 3D camera control. Please see `MouseMotion` instead. /// /// [`WindowEvent::CursorMoved`]: https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.CursorMoved -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -201,7 +204,7 @@ pub struct CursorMoved { } /// An event that is sent whenever the user's cursor enters a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -218,7 +221,7 @@ pub struct CursorEntered { } /// An event that is sent whenever the user's cursor leaves a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -239,7 +242,7 @@ pub struct CursorLeft { /// This event is the translated version of the `WindowEvent::Ime` from the `winit` crate. /// /// It is only sent if IME was enabled on the window with [`Window::ime_enabled`](crate::window::Window::ime_enabled). -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -284,7 +287,7 @@ pub enum Ime { } /// An event that indicates a window has received or lost focus. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -311,7 +314,7 @@ pub struct WindowFocused { /// It is the translated version of [`WindowEvent::Occluded`] from the `winit` crate. /// /// [`WindowEvent::Occluded`]: https://docs.rs/winit/latest/winit/event/enum.WindowEvent.html#variant.Occluded -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -330,7 +333,7 @@ pub struct WindowOccluded { } /// An event that indicates a window's scale factor has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -349,7 +352,7 @@ pub struct WindowScaleFactorChanged { } /// An event that indicates a window's OS-reported scale factor has changed. -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -368,7 +371,7 @@ pub struct WindowBackendScaleFactorChanged { } /// Events related to files being dragged and dropped on a window. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -404,7 +407,7 @@ pub enum FileDragAndDrop { } /// An event that is sent when a window is repositioned in physical pixels. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -426,7 +429,7 @@ pub struct WindowMoved { /// /// This event is only sent when the window is relying on the system theme to control its appearance. /// i.e. It is only sent when [`Window::window_theme`](crate::window::Window::window_theme) is `None` and the system theme changes. -#[derive(Event, Debug, Clone, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -445,7 +448,7 @@ pub struct WindowThemeChanged { } /// Application lifetime events -#[derive(Event, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -488,7 +491,7 @@ impl AppLifecycle { /// access window events in the order they were received from the /// operating system. Otherwise, the event types are individually /// readable with `EventReader` (e.g. `EventReader`). -#[derive(Event, Debug, Clone, PartialEq)] +#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -499,38 +502,66 @@ impl AppLifecycle { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[expect(missing_docs, reason = "Not all docs are written yet, see #3492.")] pub enum WindowEvent { + /// An application lifecycle event. AppLifecycle(AppLifecycle), + /// The user's cursor has entered a window. CursorEntered(CursorEntered), + ///The user's cursor has left a window. CursorLeft(CursorLeft), + /// The user's cursor has moved inside a window. CursorMoved(CursorMoved), + /// A file drag and drop event. FileDragAndDrop(FileDragAndDrop), + /// An Input Method Editor event. Ime(Ime), + /// A redraw of all of the application's windows has been requested. RequestRedraw(RequestRedraw), + /// The window's OS-reported scale factor has changed. WindowBackendScaleFactorChanged(WindowBackendScaleFactorChanged), + /// The OS has requested that a window be closed. WindowCloseRequested(WindowCloseRequested), + /// A new window has been created. WindowCreated(WindowCreated), + /// A window has been destroyed by the underlying windowing system. WindowDestroyed(WindowDestroyed), + /// A window has received or lost focus. WindowFocused(WindowFocused), + /// A window has been moved. WindowMoved(WindowMoved), + /// A window has started or stopped being occluded. WindowOccluded(WindowOccluded), + /// A window's logical size has changed. WindowResized(WindowResized), + /// A window's scale factor has changed. WindowScaleFactorChanged(WindowScaleFactorChanged), + /// Sent for windows that are using the system theme when the system theme changes. WindowThemeChanged(WindowThemeChanged), + /// The state of a mouse button has changed. MouseButtonInput(MouseButtonInput), + /// The physical position of a pointing device has changed. MouseMotion(MouseMotion), + /// The mouse wheel has moved. MouseWheel(MouseWheel), + /// A two finger pinch gesture. PinchGesture(PinchGesture), + /// A two finger rotation gesture. RotationGesture(RotationGesture), + /// A double tap gesture. DoubleTapGesture(DoubleTapGesture), + /// A pan gesture. PanGesture(PanGesture), + /// A touch input state change. TouchInput(TouchInput), + /// A keyboard input. KeyboardInput(KeyboardInput), + /// Sent when focus has been lost for all Bevy windows. + /// + /// Used to clear pressed key state. KeyboardFocusLost(KeyboardFocusLost), } diff --git a/crates/bevy_window/src/lib.rs b/crates/bevy_window/src/lib.rs index fb8f1fb18f..22e657cf03 100644 --- a/crates/bevy_window/src/lib.rs +++ b/crates/bevy_window/src/lib.rs @@ -57,6 +57,7 @@ impl Default for WindowPlugin { fn default() -> Self { WindowPlugin { primary_window: Some(Window::default()), + primary_cursor_options: Some(CursorOptions::default()), exit_condition: ExitCondition::OnAllClosed, close_when_requested: true, } @@ -76,6 +77,13 @@ pub struct WindowPlugin { /// [`exit_on_all_closed`]. pub primary_window: Option, + /// Settings for the cursor on the primary window. + /// + /// Defaults to `Some(CursorOptions::default())`. + /// + /// Has no effect if [`WindowPlugin::primary_window`] is `None`. + pub primary_cursor_options: Option, + /// Whether to exit the app when there are no open windows. /// /// If disabling this, ensure that you send the [`bevy_app::AppExit`] @@ -122,10 +130,14 @@ impl Plugin for WindowPlugin { .add_event::(); if let Some(primary_window) = &self.primary_window { - app.world_mut().spawn(primary_window.clone()).insert(( + let mut entity_commands = app.world_mut().spawn(primary_window.clone()); + entity_commands.insert(( PrimaryWindow, RawHandleWrapperHolder(Arc::new(Mutex::new(None))), )); + if let Some(primary_cursor_options) = &self.primary_cursor_options { + entity_commands.insert(primary_cursor_options.clone()); + } } match self.exit_condition { @@ -168,7 +180,8 @@ impl Plugin for WindowPlugin { // Register window descriptor and related types #[cfg(feature = "bevy_reflect")] app.register_type::() - .register_type::(); + .register_type::() + .register_type::(); } } diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 4f7d7b2f7b..403801e9d0 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -158,10 +158,8 @@ impl ContainsEntity for NormalizedWindowRef { all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] +#[require(CursorOptions)] pub struct Window { - /// The cursor options of this window. Cursor icons are set with the `Cursor` component on the - /// window entity. - pub cursor_options: CursorOptions, /// What presentation mode to give the window. pub present_mode: PresentMode, /// Which fullscreen or windowing mode should be used. @@ -470,7 +468,6 @@ impl Default for Window { Self { title: DEFAULT_WINDOW_TITLE.to_owned(), name: None, - cursor_options: Default::default(), present_mode: Default::default(), mode: Default::default(), position: Default::default(), @@ -728,11 +725,11 @@ impl WindowResizeConstraints { } /// Cursor data for a [`Window`]. -#[derive(Debug, Clone)] +#[derive(Component, Debug, Clone)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), - reflect(Debug, Default, Clone) + reflect(Component, Debug, Default, Clone) )] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr( @@ -1171,13 +1168,17 @@ pub enum MonitorSelection { /// References an exclusive fullscreen video mode. /// /// Used when setting [`WindowMode::Fullscreen`] on a window. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Reflect)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( - feature = "serialize", - derive(serde::Serialize, serde::Deserialize), + feature = "bevy_reflect", + derive(Reflect), + reflect(Debug, PartialEq, Clone) +)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), reflect(Serialize, Deserialize) )] -#[reflect(Debug, PartialEq, Clone)] pub enum VideoModeSelection { /// Uses the video mode that the monitor is already in. Current, diff --git a/crates/bevy_winit/src/cursor.rs b/crates/bevy_winit/src/cursor.rs index bdca3f8585..c5c5e489a6 100644 --- a/crates/bevy_winit/src/cursor.rs +++ b/crates/bevy_winit/src/cursor.rs @@ -22,11 +22,12 @@ use bevy_ecs::{ change_detection::DetectChanges, component::Component, entity::Entity, - observer::Trigger, + lifecycle::Remove, + observer::On, query::With, reflect::ReflectComponent, system::{Commands, Local, Query}, - world::{OnRemove, Ref}, + world::Ref, }; #[cfg(feature = "custom_cursor")] use bevy_image::{Image, TextureAtlasLayout}; @@ -191,7 +192,7 @@ fn update_cursors( } /// Resets the cursor to the default icon when `CursorIcon` is removed. -fn on_remove_cursor_icon(trigger: Trigger, mut commands: Commands) { +fn on_remove_cursor_icon(trigger: On, mut commands: Commands) { // Use `try_insert` to avoid panic if the window is being destroyed. commands .entity(trigger.target()) diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 968386bc02..8926095dc0 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -25,8 +25,8 @@ use winit::{event_loop::EventLoop, window::WindowId}; use bevy_a11y::AccessibilityRequested; use bevy_app::{App, Last, Plugin}; use bevy_ecs::prelude::*; -use bevy_window::{exit_on_all_closed, Window, WindowCreated}; -use system::{changed_windows, check_keyboard_focus_lost, despawn_windows}; +use bevy_window::{exit_on_all_closed, CursorOptions, Window, WindowCreated}; +use system::{changed_cursor_options, changed_windows, check_keyboard_focus_lost, despawn_windows}; pub use system::{create_monitors, create_windows}; #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub use winit::platform::web::CustomCursorExtWebSys; @@ -55,7 +55,9 @@ mod winit_monitors; mod winit_windows; thread_local! { - static WINIT_WINDOWS: RefCell = const { RefCell::new(WinitWindows::new()) }; + /// Temporary storage of WinitWindows data to replace usage of `!Send` resources. This will be replaced with proper + /// storage of `!Send` data after issue #17667 is complete. + pub static WINIT_WINDOWS: RefCell = const { RefCell::new(WinitWindows::new()) }; } /// A [`Plugin`] that uses `winit` to create and manage windows, and receive window and input @@ -69,9 +71,9 @@ thread_local! { /// in systems. /// /// When using eg. `MinimalPlugins` you can add this using `WinitPlugin::::default()`, where -/// `WakeUp` is the default `Event` that bevy uses. +/// `WakeUp` is the default event that bevy uses. #[derive(Default)] -pub struct WinitPlugin { +pub struct WinitPlugin { /// Allows the window (and the event loop) to be created on any thread /// instead of only the main thread. /// @@ -85,7 +87,7 @@ pub struct WinitPlugin { marker: PhantomData, } -impl Plugin for WinitPlugin { +impl Plugin for WinitPlugin { fn name(&self) -> &str { "bevy_winit::WinitPlugin" } @@ -140,6 +142,7 @@ impl Plugin for WinitPlugin { // `exit_on_all_closed` only checks if windows exist but doesn't access data, // so we don't need to care about its ordering relative to `changed_windows` changed_windows.ambiguous_with(exit_on_all_closed), + changed_cursor_options, despawn_windows, check_keyboard_focus_lost, ) @@ -153,7 +156,7 @@ impl Plugin for WinitPlugin { /// The default event that can be used to wake the window loop /// Wakes up the loop if in wait state -#[derive(Debug, Default, Clone, Copy, Event, Reflect)] +#[derive(Debug, Default, Clone, Copy, Event, BufferedEvent, Reflect)] #[reflect(Debug, Default, Clone)] pub struct WakeUp; @@ -164,7 +167,7 @@ pub struct WakeUp; /// /// When you receive this event it has already been handled by Bevy's main loop. /// Sending these events will NOT cause them to be processed by Bevy. -#[derive(Debug, Clone, Event)] +#[derive(Debug, Clone, Event, BufferedEvent)] pub struct RawWinitWindowEvent { /// The window for which the event was fired. pub window_id: WindowId, @@ -209,6 +212,7 @@ pub type CreateWindowParams<'w, 's, F = ()> = ( ( Entity, &'static mut Window, + &'static CursorOptions, Option<&'static RawHandleWrapperHolder>, ), F, diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 083341fd2b..5b873d4620 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -46,7 +46,7 @@ use bevy_window::{ WindowScaleFactorChanged, WindowThemeChanged, }; #[cfg(target_os = "android")] -use bevy_window::{PrimaryWindow, RawHandleWrapper}; +use bevy_window::{CursorOptions, PrimaryWindow, RawHandleWrapper}; use crate::{ accessibility::ACCESS_KIT_ADAPTERS, @@ -58,7 +58,7 @@ use crate::{ /// Persistent state that is used to run the [`App`] according to the current /// [`UpdateMode`]. -struct WinitAppRunnerState { +struct WinitAppRunnerState { /// The running app. app: App, /// Exit value once the loop is finished. @@ -106,7 +106,7 @@ struct WinitAppRunnerState { )>, } -impl WinitAppRunnerState { +impl WinitAppRunnerState { fn new(mut app: App) -> Self { app.add_event::(); #[cfg(feature = "custom_cursor")] @@ -198,7 +198,7 @@ pub enum CursorSource { #[derive(Component, Debug)] pub struct PendingCursor(pub Option); -impl ApplicationHandler for WinitAppRunnerState { +impl ApplicationHandler for WinitAppRunnerState { fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { if event_loop.exiting() { return; @@ -474,7 +474,7 @@ impl ApplicationHandler for WinitAppRunnerState { if let Ok((window_component, mut cache)) = windows.get_mut(self.world_mut(), window) { if window_component.is_changed() { - cache.window = window_component.clone(); + **cache = window_component.clone(); } } }); @@ -549,7 +549,7 @@ impl ApplicationHandler for WinitAppRunnerState { } } -impl WinitAppRunnerState { +impl WinitAppRunnerState { fn redraw_requested(&mut self, event_loop: &ActiveEventLoop) { let mut redraw_event_reader = EventCursor::::default(); @@ -605,10 +605,12 @@ impl WinitAppRunnerState { { // Get windows that are cached but without raw handles. Those window were already created, but got their // handle wrapper removed when the app was suspended. + let mut query = self.world_mut() - .query_filtered::<(Entity, &Window), (With, Without)>(); - if let Ok((entity, window)) = query.single(&self.world()) { + .query_filtered::<(Entity, &Window, &CursorOptions), (With, Without)>(); + if let Ok((entity, window, cursor_options)) = query.single(&self.world()) { let window = window.clone(); + let cursor_options = cursor_options.clone(); WINIT_WINDOWS.with_borrow_mut(|winit_windows| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { @@ -622,6 +624,7 @@ impl WinitAppRunnerState { event_loop, entity, &window, + &cursor_options, adapters, &mut handlers, &accessibility_requested, @@ -934,7 +937,7 @@ impl WinitAppRunnerState { /// /// Overriding the app's [runner](bevy_app::App::runner) while using `WinitPlugin` will bypass the /// `EventLoop`. -pub fn winit_runner(mut app: App, event_loop: EventLoop) -> AppExit { +pub fn winit_runner(mut app: App, event_loop: EventLoop) -> AppExit { if app.plugins_state() == PluginsState::Ready { app.finish(); app.cleanup(); diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 97483c7358..3cc7c5a5f6 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -1,18 +1,20 @@ use std::collections::HashMap; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ + change_detection::DetectChangesMut, entity::Entity, event::EventWriter, + lifecycle::RemovedComponents, prelude::{Changed, Component}, query::QueryFilter, - removal_detection::RemovedComponents, system::{Local, NonSendMarker, Query, SystemParamItem}, }; use bevy_input::keyboard::{Key, KeyCode, KeyboardFocusLost, KeyboardInput}; use bevy_window::{ - ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, - WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, WindowResized, - WindowWrapper, + ClosingWindow, CursorOptions, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, + WindowClosed, WindowClosing, WindowCreated, WindowEvent, WindowFocused, WindowMode, + WindowResized, WindowWrapper, }; use tracing::{error, info, warn}; @@ -59,7 +61,7 @@ pub fn create_windows( ) { WINIT_WINDOWS.with_borrow_mut(|winit_windows| { ACCESS_KIT_ADAPTERS.with_borrow_mut(|adapters| { - for (entity, mut window, handle_holder) in &mut created_windows { + for (entity, mut window, cursor_options, handle_holder) in &mut created_windows { if winit_windows.get_window(entity).is_some() { continue; } @@ -70,6 +72,7 @@ pub fn create_windows( event_loop, entity, &window, + cursor_options, adapters, &mut handlers, &accessibility_requested, @@ -85,9 +88,8 @@ pub fn create_windows( .set_scale_factor_and_apply_to_physical_size(winit_window.scale_factor() as f32); commands.entity(entity).insert(( - CachedWindow { - window: window.clone(), - }, + CachedWindow(window.clone()), + CachedCursorOptions(cursor_options.clone()), WinitWindowPressedKeys::default(), )); @@ -281,10 +283,12 @@ pub(crate) fn despawn_windows( } /// The cached state of the window so we can check which properties were changed from within the app. -#[derive(Debug, Clone, Component)] -pub struct CachedWindow { - pub window: Window, -} +#[derive(Debug, Clone, Component, Deref, DerefMut)] +pub(crate) struct CachedWindow(Window); + +/// The cached state of the window so we can check which properties were changed from within the app. +#[derive(Debug, Clone, Component, Deref, DerefMut)] +pub(crate) struct CachedCursorOptions(CursorOptions); /// Propagates changes from [`Window`] entities to the [`winit`] backend. /// @@ -306,11 +310,11 @@ pub(crate) fn changed_windows( continue; }; - if window.title != cache.window.title { + if window.title != cache.title { winit_window.set_title(window.title.as_str()); } - if window.mode != cache.window.mode { + if window.mode != cache.mode { let new_mode = match window.mode { WindowMode::BorderlessFullscreen(monitor_selection) => { Some(Some(winit::window::Fullscreen::Borderless(select_monitor( @@ -352,15 +356,15 @@ pub(crate) fn changed_windows( } } - if window.resolution != cache.window.resolution { + if window.resolution != cache.resolution { let mut physical_size = PhysicalSize::new( window.resolution.physical_width(), window.resolution.physical_height(), ); let cached_physical_size = PhysicalSize::new( - cache.window.physical_width(), - cache.window.physical_height(), + cache.physical_width(), + cache.physical_height(), ); let base_scale_factor = window.resolution.base_scale_factor(); @@ -368,12 +372,12 @@ pub(crate) fn changed_windows( // Note: this may be different from `winit`'s base scale factor if // `scale_factor_override` is set to Some(f32) let scale_factor = window.scale_factor(); - let cached_scale_factor = cache.window.scale_factor(); + let cached_scale_factor = cache.scale_factor(); // Check and update `winit`'s physical size only if the window is not maximized if scale_factor != cached_scale_factor && !winit_window.is_maximized() { let logical_size = - if let Some(cached_factor) = cache.window.resolution.scale_factor_override() { + if let Some(cached_factor) = cache.resolution.scale_factor_override() { physical_size.to_logical::(cached_factor as f64) } else { physical_size.to_logical::(base_scale_factor as f64) @@ -397,7 +401,7 @@ pub(crate) fn changed_windows( } } - if window.physical_cursor_position() != cache.window.physical_cursor_position() { + if window.physical_cursor_position() != cache.physical_cursor_position() { if let Some(physical_position) = window.physical_cursor_position() { let position = PhysicalPosition::new(physical_position.x, physical_position.y); @@ -407,44 +411,23 @@ pub(crate) fn changed_windows( } } - if window.cursor_options.grab_mode != cache.window.cursor_options.grab_mode - && crate::winit_windows::attempt_grab(winit_window, window.cursor_options.grab_mode) - .is_err() - { - window.cursor_options.grab_mode = cache.window.cursor_options.grab_mode; - } - - if window.cursor_options.visible != cache.window.cursor_options.visible { - winit_window.set_cursor_visible(window.cursor_options.visible); - } - - if window.cursor_options.hit_test != cache.window.cursor_options.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { - window.cursor_options.hit_test = cache.window.cursor_options.hit_test; - warn!( - "Could not set cursor hit test for window {}: {}", - window.title, err - ); - } - } - - if window.decorations != cache.window.decorations + if window.decorations != cache.decorations && window.decorations != winit_window.is_decorated() { winit_window.set_decorations(window.decorations); } - if window.resizable != cache.window.resizable + if window.resizable != cache.resizable && window.resizable != winit_window.is_resizable() { winit_window.set_resizable(window.resizable); } - if window.enabled_buttons != cache.window.enabled_buttons { + if window.enabled_buttons != cache.enabled_buttons { winit_window.set_enabled_buttons(convert_enabled_buttons(window.enabled_buttons)); } - if window.resize_constraints != cache.window.resize_constraints { + if window.resize_constraints != cache.resize_constraints { let constraints = window.resize_constraints.check_constraints(); let min_inner_size = LogicalSize { width: constraints.min_width, @@ -461,7 +444,7 @@ pub(crate) fn changed_windows( } } - if window.position != cache.window.position { + if window.position != cache.position { if let Some(position) = crate::winit_window_position( &window.position, &window.resolution, @@ -502,62 +485,62 @@ pub(crate) fn changed_windows( } } - if window.focused != cache.window.focused && window.focused { + if window.focused != cache.focused && window.focused { winit_window.focus_window(); } - if window.window_level != cache.window.window_level { + if window.window_level != cache.window_level { winit_window.set_window_level(convert_window_level(window.window_level)); } // Currently unsupported changes - if window.transparent != cache.window.transparent { - window.transparent = cache.window.transparent; + if window.transparent != cache.transparent { + window.transparent = cache.transparent; warn!("Winit does not currently support updating transparency after window creation."); } #[cfg(target_arch = "wasm32")] - if window.canvas != cache.window.canvas { - window.canvas.clone_from(&cache.window.canvas); + if window.canvas != cache.canvas { + window.canvas.clone_from(&cache.canvas); warn!( "Bevy currently doesn't support modifying the window canvas after initialization." ); } - if window.ime_enabled != cache.window.ime_enabled { + if window.ime_enabled != cache.ime_enabled { winit_window.set_ime_allowed(window.ime_enabled); } - if window.ime_position != cache.window.ime_position { + if window.ime_position != cache.ime_position { winit_window.set_ime_cursor_area( LogicalPosition::new(window.ime_position.x, window.ime_position.y), PhysicalSize::new(10, 10), ); } - if window.window_theme != cache.window.window_theme { + if window.window_theme != cache.window_theme { winit_window.set_theme(window.window_theme.map(convert_window_theme)); } - if window.visible != cache.window.visible { + if window.visible != cache.visible { winit_window.set_visible(window.visible); } #[cfg(target_os = "ios")] { - if window.recognize_pinch_gesture != cache.window.recognize_pinch_gesture { + if window.recognize_pinch_gesture != cache.recognize_pinch_gesture { winit_window.recognize_pinch_gesture(window.recognize_pinch_gesture); } - if window.recognize_rotation_gesture != cache.window.recognize_rotation_gesture { + if window.recognize_rotation_gesture != cache.recognize_rotation_gesture { winit_window.recognize_rotation_gesture(window.recognize_rotation_gesture); } - if window.recognize_doubletap_gesture != cache.window.recognize_doubletap_gesture { + if window.recognize_doubletap_gesture != cache.recognize_doubletap_gesture { winit_window.recognize_doubletap_gesture(window.recognize_doubletap_gesture); } - if window.recognize_pan_gesture != cache.window.recognize_pan_gesture { + if window.recognize_pan_gesture != cache.recognize_pan_gesture { match ( window.recognize_pan_gesture, - cache.window.recognize_pan_gesture, + cache.recognize_pan_gesture, ) { (Some(_), Some(_)) => { warn!("Bevy currently doesn't support modifying PanGesture number of fingers recognition. Please disable it before re-enabling it with the new number of fingers"); @@ -567,16 +550,15 @@ pub(crate) fn changed_windows( } } - if window.prefers_home_indicator_hidden != cache.window.prefers_home_indicator_hidden { + if window.prefers_home_indicator_hidden != cache.prefers_home_indicator_hidden { winit_window .set_prefers_home_indicator_hidden(window.prefers_home_indicator_hidden); } - if window.prefers_status_bar_hidden != cache.window.prefers_status_bar_hidden { + if window.prefers_status_bar_hidden != cache.prefers_status_bar_hidden { winit_window.set_prefers_status_bar_hidden(window.prefers_status_bar_hidden); } if window.preferred_screen_edges_deferring_system_gestures != cache - .window .preferred_screen_edges_deferring_system_gestures { use crate::converters::convert_screen_edge; @@ -585,7 +567,59 @@ pub(crate) fn changed_windows( winit_window.set_preferred_screen_edges_deferring_system_gestures(preferred_edge); } } - cache.window = window.clone(); + **cache = window.clone(); + } + }); +} + +pub(crate) fn changed_cursor_options( + mut changed_windows: Query< + ( + Entity, + &Window, + &mut CursorOptions, + &mut CachedCursorOptions, + ), + Changed, + >, + _non_send_marker: NonSendMarker, +) { + WINIT_WINDOWS.with_borrow(|winit_windows| { + for (entity, window, mut cursor_options, mut cache) in &mut changed_windows { + // This system already only runs when the cursor options change, so we need to bypass change detection or the next frame will also run this system + let cursor_options = cursor_options.bypass_change_detection(); + let Some(winit_window) = winit_windows.get_window(entity) else { + continue; + }; + // Don't check the cache for the grab mode. It can change through external means, leaving the cache outdated. + if let Err(err) = + crate::winit_windows::attempt_grab(winit_window, cursor_options.grab_mode) + { + warn!( + "Could not set cursor grab mode for window {}: {}", + window.title, err + ); + cursor_options.grab_mode = cache.grab_mode; + } else { + cache.grab_mode = cursor_options.grab_mode; + } + + if cursor_options.visible != cache.visible { + winit_window.set_cursor_visible(cursor_options.visible); + cache.visible = cursor_options.visible; + } + + if cursor_options.hit_test != cache.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) { + warn!( + "Could not set cursor hit test for window {}: {}", + window.title, err + ); + cursor_options.hit_test = cache.hit_test; + } else { + cache.hit_test = cursor_options.hit_test; + } + } } }); } diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 8bf326f453..5110e670c2 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -4,8 +4,8 @@ use bevy_ecs::entity::Entity; use bevy_ecs::entity::EntityHashMap; use bevy_platform::collections::HashMap; use bevy_window::{ - CursorGrabMode, MonitorSelection, VideoModeSelection, Window, WindowMode, WindowPosition, - WindowResolution, WindowWrapper, + CursorGrabMode, CursorOptions, MonitorSelection, VideoModeSelection, Window, WindowMode, + WindowPosition, WindowResolution, WindowWrapper, }; use tracing::warn; @@ -58,6 +58,7 @@ impl WinitWindows { event_loop: &ActiveEventLoop, entity: Entity, window: &Window, + cursor_options: &CursorOptions, adapters: &mut AccessKitAdapters, handlers: &mut WinitActionRequestHandlers, accessibility_requested: &AccessibilityRequested, @@ -310,16 +311,16 @@ impl WinitWindows { winit_window.set_visible(window.visible); // Do not set the grab mode on window creation if it's none. It can fail on mobile. - if window.cursor_options.grab_mode != CursorGrabMode::None { - let _ = attempt_grab(&winit_window, window.cursor_options.grab_mode); + if cursor_options.grab_mode != CursorGrabMode::None { + let _ = attempt_grab(&winit_window, cursor_options.grab_mode); } - winit_window.set_cursor_visible(window.cursor_options.visible); + winit_window.set_cursor_visible(cursor_options.visible); // Do not set the cursor hittest on window creation if it's false, as it will always fail on // some platforms and log an unfixable warning. - if !window.cursor_options.hit_test { - if let Err(err) = winit_window.set_cursor_hittest(window.cursor_options.hit_test) { + if !cursor_options.hit_test { + if let Err(err) = winit_window.set_cursor_hittest(cursor_options.hit_test) { warn!( "Could not set cursor hit test for window {}: {}", window.title, err diff --git a/docs/cargo_features.md b/docs/cargo_features.md index f3ea7f9770..e5b75e36c4 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -21,6 +21,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_audio|Provides audio functionality| |bevy_color|Provides shared color types and operations| |bevy_core_pipeline|Provides cameras and other basic render pipeline features| +|bevy_core_widgets|Headless widget collection for Bevy UI.| |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support| @@ -40,6 +41,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_window|Windowing layer| |bevy_winit|winit window and input backend| |custom_cursor|Enable winit custom cursor support| +|debug|Enable collecting debug information about systems and components to help with diagnostics| |default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase| |hdr|HDR image format support| |ktx2|KTX2 compressed texture support| @@ -69,6 +71,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_dev_tools|Provides a collection of developer tools| |bevy_image|Load and access image data. Usually added by an image format| |bevy_remote|Enable the Bevy Remote Protocol| +|bevy_solari|Provides raytraced lighting (experimental)| |bevy_ui_debug|Provides a debug overlay for bevy UI| |bmp|BMP image format support| |critical-section|`critical-section` provides the building blocks for synchronization primitives on all platforms, including `no_std`.| diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs index c549134419..9cffb8e00c 100644 --- a/examples/2d/sprite_scale.rs +++ b/examples/2d/sprite_scale.rs @@ -129,7 +129,7 @@ fn setup_sprites(mut commands: Commands, asset_server: Res) { cmd.with_children(|builder| { builder.spawn(( Text2d::new(rect.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, @@ -275,7 +275,7 @@ fn setup_texture_atlas( cmd.with_children(|builder| { builder.spawn(( Text2d::new(sprite_sheet.text), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * sprite_sheet.size.y - 10., 0.), bevy::sprite::Anchor::TOP_CENTER, diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index 94f4fe809f..91918b1d66 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -94,7 +94,7 @@ fn spawn_sprites( children![( Text2d::new(label), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_xyz(0., -0.5 * size.y - 10., 0.0), bevy::sprite::Anchor::TOP_CENTER, )], diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index 7b1abfd8da..3123e8d891 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { font_size: 50.0, ..default() }; - let text_justification = JustifyText::Center; + let text_justification = Justify::Center; commands.spawn(Camera2d); // Demonstrate changing translation commands.spawn(( @@ -78,7 +78,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(Unicode linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::WordBoundary), + TextLayout::new(Justify::Left, LineBreak::WordBoundary), // Wrap text in the rectangle TextBounds::from(box_size), // Ensure the text is drawn on top of the box @@ -94,7 +94,7 @@ fn setup(mut commands: Commands, asset_server: Res) { children![( Text2d::new("this text wraps in the box\n(AnyCharacter linebreaks)"), slightly_smaller_text_font.clone(), - TextLayout::new(JustifyText::Left, LineBreak::AnyCharacter), + TextLayout::new(Justify::Left, LineBreak::AnyCharacter), // Wrap text in the rectangle TextBounds::from(other_box_size), // Ensure the text is drawn on top of the box @@ -104,11 +104,11 @@ fn setup(mut commands: Commands, asset_server: Res) { // Demonstrate font smoothing off commands.spawn(( - Text2d::new("This text has\nFontSmoothing::None\nAnd JustifyText::Center"), + Text2d::new("This text has\nFontSmoothing::None\nAnd Justify::Center"), slightly_smaller_text_font .clone() .with_font_smoothing(FontSmoothing::None), - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform::from_translation(Vec3::new(-400.0, -250.0, 0.0)), )); diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 7510afbcee..25106adcfb 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -279,7 +279,7 @@ fn create_label( commands.spawn(( Text2d::new(text), text_style, - TextLayout::new_with_justify(JustifyText::Center), + TextLayout::new_with_justify(Justify::Center), Transform { translation: Vec3::new(translation.0, translation.1, translation.2), ..default() diff --git a/examples/3d/edit_material_on_gltf.rs b/examples/3d/edit_material_on_gltf.rs index f9de5842a9..97c3c48296 100644 --- a/examples/3d/edit_material_on_gltf.rs +++ b/examples/3d/edit_material_on_gltf.rs @@ -8,7 +8,7 @@ use bevy::{ gltf::GltfAssetLabel, math::{Dir3, Vec3}, pbr::{DirectionalLight, MeshMaterial3d, StandardMaterial}, - prelude::{Camera3d, Children, Commands, Component, Query, Res, ResMut, Transform, Trigger}, + prelude::{Camera3d, Children, Commands, Component, On, Query, Res, ResMut, Transform}, scene::{SceneInstanceReady, SceneRoot}, DefaultPlugins, }; @@ -57,7 +57,7 @@ fn setup_scene(mut commands: Commands, asset_server: Res) { } fn change_material( - trigger: Trigger, + trigger: On, mut commands: Commands, children: Query<&Children>, color_override: Query<&ColorOverride>, diff --git a/examples/3d/irradiance_volumes.rs b/examples/3d/irradiance_volumes.rs index 31529c4219..80373512db 100644 --- a/examples/3d/irradiance_volumes.rs +++ b/examples/3d/irradiance_volumes.rs @@ -251,7 +251,6 @@ fn spawn_irradiance_volume(commands: &mut Commands, assets: &ExampleAssets) { intensity: IRRADIANCE_VOLUME_INTENSITY, ..default() }, - LightProbe, )); } diff --git a/examples/3d/lighting.rs b/examples/3d/lighting.rs index b8d7883763..100816feb6 100644 --- a/examples/3d/lighting.rs +++ b/examples/3d/lighting.rs @@ -20,7 +20,15 @@ fn main() { sensor_height: 0.01866, })) .add_systems(Startup, setup) - .add_systems(Update, (update_exposure, movement, animate_light_direction)) + .add_systems( + Update, + ( + update_exposure, + toggle_ambient_light, + movement, + animate_light_direction, + ), + ) .run(); } @@ -111,9 +119,10 @@ fn setup( )); // ambient light + // ambient lights' brightnesses are measured in candela per meter square, calculable as (color * brightness) commands.insert_resource(AmbientLight { color: ORANGE_RED.into(), - brightness: 0.02, + brightness: 200.0, ..default() }); @@ -211,6 +220,7 @@ fn setup( ..default() }, children![ + TextSpan::new("Ambient light is on\n"), TextSpan(format!("Aperture: f/{:.0}\n", parameters.aperture_f_stops,)), TextSpan(format!( "Shutter speed: 1/{:.0}s\n", @@ -224,6 +234,7 @@ fn setup( TextSpan::new("Controls\n"), TextSpan::new("---------------\n"), TextSpan::new("Arrow keys - Move objects\n"), + TextSpan::new("Space - Toggle ambient light\n"), TextSpan::new("1/2 - Decrease/Increase aperture\n"), TextSpan::new("3/4 - Decrease/Increase shutter speed\n"), TextSpan::new("5/6 - Decrease/Increase sensitivity\n"), @@ -267,16 +278,38 @@ fn update_exposure( *parameters = Parameters::default(); } - *writer.text(entity, 1) = format!("Aperture: f/{:.0}\n", parameters.aperture_f_stops); - *writer.text(entity, 2) = format!( + *writer.text(entity, 2) = format!("Aperture: f/{:.0}\n", parameters.aperture_f_stops); + *writer.text(entity, 3) = format!( "Shutter speed: 1/{:.0}s\n", 1.0 / parameters.shutter_speed_s ); - *writer.text(entity, 3) = format!("Sensitivity: ISO {:.0}\n", parameters.sensitivity_iso); + *writer.text(entity, 4) = format!("Sensitivity: ISO {:.0}\n", parameters.sensitivity_iso); **exposure = Exposure::from_physical_camera(**parameters); } +fn toggle_ambient_light( + key_input: Res>, + mut ambient_light: ResMut, + text: Single>, + mut writer: TextUiWriter, +) { + if key_input.just_pressed(KeyCode::Space) { + if ambient_light.brightness > 1. { + ambient_light.brightness = 0.; + } else { + ambient_light.brightness = 200.; + } + + let entity = *text; + let ambient_light_state_text: &str = match ambient_light.brightness { + 0. => "off", + _ => "on", + }; + *writer.text(entity, 1) = format!("Ambient light is {}\n", ambient_light_state_text); + } +} + fn animate_light_direction( time: Res, mut b: EventReade } /// A dummy event type. -#[derive(Debug, Clone, Event)] +#[derive(Debug, Clone, Event, BufferedEvent)] struct DebugEvent { resend_from_param_set: bool, resend_from_local_event_reader: bool, diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index 0a0b04f7a2..b8d02e8159 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -89,7 +89,7 @@ struct Ball; #[derive(Component, Deref, DerefMut)] struct Velocity(Vec2); -#[derive(Event, Default)] +#[derive(Event, BufferedEvent, Default)] struct CollisionEvent; #[derive(Component)] diff --git a/examples/games/desk_toy.rs b/examples/games/desk_toy.rs index c25286dd9c..b5c638348e 100644 --- a/examples/games/desk_toy.rs +++ b/examples/games/desk_toy.rs @@ -10,7 +10,7 @@ use bevy::{ app::AppExit, input::common_conditions::{input_just_pressed, input_just_released}, prelude::*, - window::{PrimaryWindow, WindowLevel}, + window::{CursorOptions, PrimaryWindow, WindowLevel}, }; #[cfg(target_os = "macos")] @@ -219,12 +219,13 @@ fn get_cursor_world_pos( /// Update whether the window is clickable or not fn update_cursor_hit_test( cursor_world_pos: Res, - mut primary_window: Single<&mut Window, With>, + primary_window: Single<(&Window, &mut CursorOptions), With>, bevy_logo_transform: Single<&Transform, With>, ) { + let (window, mut cursor_options) = primary_window.into_inner(); // If the window has decorations (e.g. a border) then it should be clickable - if primary_window.decorations { - primary_window.cursor_options.hit_test = true; + if window.decorations { + cursor_options.hit_test = true; return; } @@ -234,7 +235,7 @@ fn update_cursor_hit_test( }; // If the cursor is within the radius of the Bevy logo make the window clickable otherwise the window is not clickable - primary_window.cursor_options.hit_test = bevy_logo_transform + cursor_options.hit_test = bevy_logo_transform .translation .truncate() .distance(cursor_world_pos) diff --git a/examples/games/stepping.rs b/examples/games/stepping.rs index 653b7a12ff..dce37d0842 100644 --- a/examples/games/stepping.rs +++ b/examples/games/stepping.rs @@ -104,7 +104,10 @@ fn build_ui( mut state: ResMut, ) { let mut text_spans = Vec::new(); - let mut always_run = Vec::new(); + let mut always_run: Vec<( + bevy_ecs::intern::Interned, + NodeId, + )> = Vec::new(); let Ok(schedule_order) = stepping.schedules() else { return; @@ -131,7 +134,8 @@ fn build_ui( for (node_id, system) in systems { // skip bevy default systems; we don't want to step those - if system.name().starts_with("bevy") { + #[cfg(feature = "debug")] + if system.name().as_string().starts_with("bevy") { always_run.push((*label, node_id)); continue; } diff --git a/examples/helpers/camera_controller.rs b/examples/helpers/camera_controller.rs index 07f0f31b11..3f6e4ed477 100644 --- a/examples/helpers/camera_controller.rs +++ b/examples/helpers/camera_controller.rs @@ -8,7 +8,7 @@ use bevy::{ input::mouse::{AccumulatedMouseMotion, AccumulatedMouseScroll, MouseScrollUnit}, prelude::*, - window::CursorGrabMode, + window::{CursorGrabMode, CursorOptions}, }; use std::{f32::consts::*, fmt}; @@ -126,7 +126,7 @@ Freecam Controls: fn run_camera_controller( time: Res