diff --git a/benches/benches/bevy_ecs/events/iter.rs b/benches/benches/bevy_ecs/events/iter.rs index 5f85633312..daf79091c9 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, BufferedEvent)] +#[derive(BufferedEvent)] struct BenchEvent([u8; SIZE]); pub struct Benchmark(Events>); diff --git a/benches/benches/bevy_ecs/events/write.rs b/benches/benches/bevy_ecs/events/write.rs index 8095aee738..530935bc43 100644 --- a/benches/benches/bevy_ecs/events/write.rs +++ b/benches/benches/bevy_ecs/events/write.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct BenchEvent([u8; SIZE]); impl Default for BenchEvent { diff --git a/crates/bevy_a11y/src/lib.rs b/crates/bevy_a11y/src/lib.rs index 22b2f71f07..291173fb2b 100644 --- a/crates/bevy_a11y/src/lib.rs +++ b/crates/bevy_a11y/src/lib.rs @@ -26,10 +26,7 @@ use accesskit::Node; use bevy_app::Plugin; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ - component::Component, - event::{BufferedEvent, Event}, - resource::Resource, - schedule::SystemSet, + component::Component, event::BufferedEvent, resource::Resource, schedule::SystemSet, }; #[cfg(feature = "bevy_reflect")] @@ -45,7 +42,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, BufferedEvent, Deref, DerefMut)] +#[derive(BufferedEvent, Deref, DerefMut)] #[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct ActionRequest(pub accesskit::ActionRequest); diff --git a/crates/bevy_app/src/app.rs b/crates/bevy_app/src/app.rs index 7363a97f77..b933f5cfd7 100644 --- a/crates/bevy_app/src/app.rs +++ b/crates/bevy_app/src/app.rs @@ -356,7 +356,7 @@ impl App { /// # use bevy_app::prelude::*; /// # use bevy_ecs::prelude::*; /// # - /// # #[derive(Event, BufferedEvent)] + /// # #[derive(BufferedEvent)] /// # struct MyEvent; /// # let mut app = App::new(); /// # @@ -1418,7 +1418,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, BufferedEvent, Debug, Clone, Default, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, Default, PartialEq, Eq)] pub enum AppExit { /// [`App`] exited without any problems. #[default] @@ -1486,7 +1486,7 @@ mod tests { change_detection::{DetectChanges, ResMut}, component::Component, entity::Entity, - event::{BufferedEvent, Event, EventWriter, Events}, + event::{BufferedEvent, EventWriter, Events}, lifecycle::RemovedComponents, query::With, resource::Resource, @@ -1852,7 +1852,7 @@ mod tests { } #[test] fn events_should_be_updated_once_per_update() { - #[derive(Event, BufferedEvent, Clone)] + #[derive(BufferedEvent, Clone)] struct TestEvent; let mut app = App::new(); diff --git a/crates/bevy_asset/src/event.rs b/crates/bevy_asset/src/event.rs index 42de19fe44..b3e70aec49 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::{BufferedEvent, Event}; +use bevy_ecs::event::BufferedEvent; use bevy_reflect::Reflect; use core::fmt::Debug; /// A [`BufferedEvent`] emitted when a specific [`Asset`] fails to load. /// /// For an untyped equivalent, see [`UntypedAssetLoadFailedEvent`]. -#[derive(Event, BufferedEvent, Clone, Debug)] +#[derive(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, BufferedEvent, Clone, Debug)] +#[derive(BufferedEvent, Clone, Debug)] pub struct UntypedAssetLoadFailedEvent { /// The stable identifier of the asset that failed to load. pub id: UntypedAssetId, @@ -46,7 +46,7 @@ impl From<&AssetLoadFailedEvent> for UntypedAssetLoadFailedEvent { /// [`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, BufferedEvent, Reflect)] +#[derive(BufferedEvent, Reflect)] pub enum AssetEvent { /// Emitted whenever an [`Asset`] is added. Added { id: AssetId }, diff --git a/crates/bevy_asset/src/loader_builders.rs b/crates/bevy_asset/src/loader_builders.rs index 13bea2b71d..8e94b742ec 100644 --- a/crates/bevy_asset/src/loader_builders.rs +++ b/crates/bevy_asset/src/loader_builders.rs @@ -314,7 +314,7 @@ impl NestedLoader<'_, '_, StaticTyped, Deferred> { } else { self.load_context .asset_server - .get_or_create_path_handle(path, None) + .get_or_create_path_handle(path, self.meta_transform) }; self.load_context.dependencies.insert(handle.id().untyped()); handle diff --git a/crates/bevy_camera/src/camera.rs b/crates/bevy_camera/src/camera.rs index a70cbeb39e..1eea963077 100644 --- a/crates/bevy_camera/src/camera.rs +++ b/crates/bevy_camera/src/camera.rs @@ -567,6 +567,30 @@ impl Camera { /// To get the world space coordinates with Normalized Device Coordinates, you should use /// [`ndc_to_world`](Self::ndc_to_world). /// + /// # Example + /// ```no_run + /// # use bevy_window::Window; + /// # use bevy_ecs::prelude::{Single, IntoScheduleConfigs}; + /// # use bevy_transform::prelude::{GlobalTransform, TransformSystems}; + /// # use bevy_camera::Camera; + /// # use bevy_app::{App, PostUpdate}; + /// # + /// fn system(camera_query: Single<(&Camera, &GlobalTransform)>, window: Single<&Window>) { + /// let (camera, camera_transform) = *camera_query; + /// + /// if let Some(cursor_position) = window.cursor_position() + /// // Calculate a ray pointing from the camera into the world based on the cursor's position. + /// && let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_position) + /// { + /// println!("{ray:?}"); + /// } + /// } + /// + /// # let mut app = App::new(); + /// // Run the system after transform propagation so the camera's global transform is up-to-date. + /// app.add_systems(PostUpdate, system.after(TransformSystems::Propagate)); + /// ``` + /// /// # Panics /// /// Will panic if the camera's projection matrix is invalid (has a determinant of 0) and @@ -605,6 +629,30 @@ impl Camera { /// To get the world space coordinates with Normalized Device Coordinates, you should use /// [`ndc_to_world`](Self::ndc_to_world). /// + /// # Example + /// ```no_run + /// # use bevy_window::Window; + /// # use bevy_ecs::prelude::*; + /// # use bevy_transform::prelude::{GlobalTransform, TransformSystems}; + /// # use bevy_camera::Camera; + /// # use bevy_app::{App, PostUpdate}; + /// # + /// fn system(camera_query: Single<(&Camera, &GlobalTransform)>, window: Single<&Window>) { + /// let (camera, camera_transform) = *camera_query; + /// + /// if let Some(cursor_position) = window.cursor_position() + /// // Calculate a world position based on the cursor's position. + /// && let Ok(world_pos) = camera.viewport_to_world_2d(camera_transform, cursor_position) + /// { + /// println!("World position: {world_pos:.2}"); + /// } + /// } + /// + /// # let mut app = App::new(); + /// // Run the system after transform propagation so the camera's global transform is up-to-date. + /// app.add_systems(PostUpdate, system.after(TransformSystems::Propagate)); + /// ``` + /// /// # Panics /// /// Will panic if the camera's projection matrix is invalid (has a determinant of 0) and diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index 5acd98dd30..f515cdc58c 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -36,7 +36,7 @@ impl Plugin for BlitPlugin { #[derive(Resource)] pub struct BlitPipeline { - pub texture_bind_group: BindGroupLayout, + pub layout: BindGroupLayout, pub sampler: Sampler, pub fullscreen_shader: FullscreenShader, pub fragment_shader: Handle, @@ -46,7 +46,7 @@ impl FromWorld for BlitPipeline { fn from_world(render_world: &mut World) -> Self { let render_device = render_world.resource::(); - let texture_bind_group = render_device.create_bind_group_layout( + let layout = render_device.create_bind_group_layout( "blit_bind_group_layout", &BindGroupLayoutEntries::sequential( ShaderStages::FRAGMENT, @@ -60,7 +60,7 @@ impl FromWorld for BlitPipeline { let sampler = render_device.create_sampler(&SamplerDescriptor::default()); BlitPipeline { - texture_bind_group, + layout, sampler, fullscreen_shader: render_world.resource::().clone(), fragment_shader: load_embedded_asset!(render_world, "blit.wgsl"), @@ -68,6 +68,20 @@ impl FromWorld for BlitPipeline { } } +impl BlitPipeline { + pub fn create_bind_group( + &self, + render_device: &RenderDevice, + src_texture: &TextureView, + ) -> BindGroup { + render_device.create_bind_group( + None, + &self.layout, + &BindGroupEntries::sequential((src_texture, &self.sampler)), + ) + } +} + #[derive(PartialEq, Eq, Hash, Clone, Copy)] pub struct BlitPipelineKey { pub texture_format: TextureFormat, @@ -81,7 +95,7 @@ impl SpecializedRenderPipeline for BlitPipeline { fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { RenderPipelineDescriptor { label: Some("blit pipeline".into()), - layout: vec![self.texture_bind_group.clone()], + layout: vec![self.layout.clone()], vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { shader: self.fragment_shader.clone(), diff --git a/crates/bevy_core_pipeline/src/msaa_writeback.rs b/crates/bevy_core_pipeline/src/msaa_writeback.rs index 93116dc9fd..151660876a 100644 --- a/crates/bevy_core_pipeline/src/msaa_writeback.rs +++ b/crates/bevy_core_pipeline/src/msaa_writeback.rs @@ -98,11 +98,8 @@ impl ViewNode for MsaaWritebackNode { occlusion_query_set: None, }; - let bind_group = render_context.render_device().create_bind_group( - None, - &blit_pipeline.texture_bind_group, - &BindGroupEntries::sequential((post_process.source, &blit_pipeline.sampler)), - ); + let bind_group = + blit_pipeline.create_bind_group(render_context.render_device(), post_process.source); let mut render_pass = render_context .command_encoder() diff --git a/crates/bevy_core_pipeline/src/upscaling/node.rs b/crates/bevy_core_pipeline/src/upscaling/node.rs index ece71c1947..493a1484c6 100644 --- a/crates/bevy_core_pipeline/src/upscaling/node.rs +++ b/crates/bevy_core_pipeline/src/upscaling/node.rs @@ -3,9 +3,7 @@ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ camera::{CameraOutputMode, ClearColor, ClearColorConfig, ExtractedCamera}, render_graph::{NodeRunError, RenderGraphContext, ViewNode}, - render_resource::{ - BindGroup, BindGroupEntries, PipelineCache, RenderPassDescriptor, TextureViewId, - }, + render_resource::{BindGroup, PipelineCache, RenderPassDescriptor, TextureViewId}, renderer::RenderContext, view::ViewTarget, }; @@ -30,9 +28,9 @@ impl ViewNode for UpscalingNode { (target, upscaling_target, camera): QueryItem, world: &World, ) -> Result<(), NodeRunError> { - let pipeline_cache = world.get_resource::().unwrap(); - let blit_pipeline = world.get_resource::().unwrap(); - let clear_color_global = world.get_resource::().unwrap(); + let pipeline_cache = world.resource::(); + let blit_pipeline = world.resource::(); + let clear_color_global = world.resource::(); let clear_color = if let Some(camera) = camera { match camera.output_mode { @@ -48,19 +46,18 @@ impl ViewNode for UpscalingNode { ClearColorConfig::None => None, }; let converted_clear_color = clear_color.map(Into::into); - let upscaled_texture = target.main_texture_view(); + // texture to be upscaled to the output texture + let main_texture_view = target.main_texture_view(); let mut cached_bind_group = self.cached_texture_bind_group.lock().unwrap(); let bind_group = match &mut *cached_bind_group { - Some((id, bind_group)) if upscaled_texture.id() == *id => bind_group, + Some((id, bind_group)) if main_texture_view.id() == *id => bind_group, cached_bind_group => { - let bind_group = render_context.render_device().create_bind_group( - None, - &blit_pipeline.texture_bind_group, - &BindGroupEntries::sequential((upscaled_texture, &blit_pipeline.sampler)), - ); + let bind_group = blit_pipeline + .create_bind_group(render_context.render_device(), main_texture_view); - let (_, bind_group) = cached_bind_group.insert((upscaled_texture.id(), bind_group)); + let (_, bind_group) = + cached_bind_group.insert((main_texture_view.id(), bind_group)); bind_group } }; diff --git a/crates/bevy_core_widgets/Cargo.toml b/crates/bevy_core_widgets/Cargo.toml index 186b2ec820..01da19ae7c 100644 --- a/crates/bevy_core_widgets/Cargo.toml +++ b/crates/bevy_core_widgets/Cargo.toml @@ -18,9 +18,7 @@ bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" } bevy_log = { path = "../bevy_log", version = "0.17.0-dev" } bevy_math = { path = "../bevy_math", version = "0.17.0-dev" } bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" } -bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [ - "bevy_ui_picking_backend", -] } +bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev" } # other accesskit = "0.19" diff --git a/crates/bevy_dev_tools/src/ci_testing/config.rs b/crates/bevy_dev_tools/src/ci_testing/config.rs index 01ab4f26cd..dd369da3e3 100644 --- a/crates/bevy_dev_tools/src/ci_testing/config.rs +++ b/crates/bevy_dev_tools/src/ci_testing/config.rs @@ -52,7 +52,7 @@ pub enum CiTestingEvent { } /// A custom event that can be configured from a configuration file for CI testing. -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] pub struct CiTestingCustomEvent(pub String); #[cfg(test)] diff --git a/crates/bevy_ecs/README.md b/crates/bevy_ecs/README.md index b085a79c21..0cb6a4b37b 100644 --- a/crates/bevy_ecs/README.md +++ b/crates/bevy_ecs/README.md @@ -285,7 +285,7 @@ They can be sent using the `EventWriter` system parameter and received with `Eve ```rust use bevy_ecs::prelude::*; -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct Message(String); fn writer(mut writer: EventWriter) { diff --git a/crates/bevy_ecs/examples/events.rs b/crates/bevy_ecs/examples/events.rs index ecdcb31a33..2a458964a9 100644 --- a/crates/bevy_ecs/examples/events.rs +++ b/crates/bevy_ecs/examples/events.rs @@ -37,7 +37,7 @@ fn main() { } // This is our event that we will send and receive in systems -#[derive(Event, BufferedEvent)] +#[derive(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 1322022581..e707c540ad 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -293,8 +293,14 @@ pub fn derive_component(input: TokenStream) -> TokenStream { .then_some(quote! { #bevy_ecs_path::component::Immutable }) .unwrap_or(quote! { #bevy_ecs_path::component::Mutable }); - let clone_behavior = if relationship_target.is_some() { - quote!(#bevy_ecs_path::component::ComponentCloneBehavior::Custom(#bevy_ecs_path::relationship::clone_relationship_target::)) + let clone_behavior = if relationship_target.is_some() || relationship.is_some() { + quote!( + use #bevy_ecs_path::relationship::{ + RelationshipCloneBehaviorBase, RelationshipCloneBehaviorViaClone, RelationshipCloneBehaviorViaReflect, + RelationshipTargetCloneBehaviorViaClone, RelationshipTargetCloneBehaviorViaReflect, RelationshipTargetCloneBehaviorHierarchy + }; + (&&&&&&&#bevy_ecs_path::relationship::RelationshipCloneBehaviorSpecialization::::default()).default_clone_behavior() + ) } else if let Some(behavior) = attrs.clone_behavior { quote!(#bevy_ecs_path::component::ComponentCloneBehavior::#behavior) } else { diff --git a/crates/bevy_ecs/src/bundle/remove.rs b/crates/bevy_ecs/src/bundle/remove.rs index 236ad7616f..66cc06eac6 100644 --- a/crates/bevy_ecs/src/bundle/remove.rs +++ b/crates/bevy_ecs/src/bundle/remove.rs @@ -22,6 +22,7 @@ pub(crate) struct BundleRemover<'w> { old_and_new_table: Option<(NonNull, NonNull
)>, old_archetype: NonNull, new_archetype: NonNull, + pub(crate) relationship_hook_mode: RelationshipHookMode, } impl<'w> BundleRemover<'w> { @@ -97,6 +98,7 @@ impl<'w> BundleRemover<'w> { old_archetype: old_archetype.into(), old_and_new_table: tables, world: world.as_unsafe_world_cell(), + relationship_hook_mode: RelationshipHookMode::Run, }; if is_new_created { remover @@ -160,7 +162,7 @@ impl<'w> BundleRemover<'w> { entity, bundle_components_in_archetype(), caller, - RelationshipHookMode::Run, + self.relationship_hook_mode, ); if self.old_archetype.as_ref().has_remove_observer() { deferred_world.trigger_observers( diff --git a/crates/bevy_ecs/src/change_detection.rs b/crates/bevy_ecs/src/change_detection.rs index 05b76207b7..1ba4f23db2 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, BufferedEvent, PartialEq, Eq)] + /// #[derive(BufferedEvent, PartialEq, Eq)] /// pub struct ScoreChanged { /// current: u32, /// previous: u32, diff --git a/crates/bevy_ecs/src/component/clone.rs b/crates/bevy_ecs/src/component/clone.rs index ecbc6e60e5..ff90255e2e 100644 --- a/crates/bevy_ecs/src/component/clone.rs +++ b/crates/bevy_ecs/src/component/clone.rs @@ -3,16 +3,16 @@ use core::marker::PhantomData; use crate::component::Component; use crate::entity::{ComponentCloneCtx, SourceComponent}; -/// Function type that can be used to clone an entity. +/// Function type that can be used to clone a component of an entity. pub type ComponentCloneFn = fn(&SourceComponent, &mut ComponentCloneCtx); -/// The clone behavior to use when cloning a [`Component`]. +/// The clone behavior to use when cloning or moving a [`Component`]. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub enum ComponentCloneBehavior { /// Uses the default behavior (which is passed to [`ComponentCloneBehavior::resolve`]) #[default] Default, - /// Do not clone this component. + /// Do not clone/move this component. Ignore, /// Uses a custom [`ComponentCloneFn`]. Custom(ComponentCloneFn), diff --git a/crates/bevy_ecs/src/component/info.rs b/crates/bevy_ecs/src/component/info.rs index 5a1bf96e16..3c3059f6be 100644 --- a/crates/bevy_ecs/src/component/info.rs +++ b/crates/bevy_ecs/src/component/info.rs @@ -641,6 +641,7 @@ impl Components { /// /// # See also /// + /// * [`ComponentIdFor`](super::ComponentIdFor) /// * [`Components::get_id()`] /// * [`Components::resource_id()`] /// * [`World::component_id()`](crate::world::World::component_id) diff --git a/crates/bevy_ecs/src/component/register.rs b/crates/bevy_ecs/src/component/register.rs index bf1720b005..cb3ca7f544 100644 --- a/crates/bevy_ecs/src/component/register.rs +++ b/crates/bevy_ecs/src/component/register.rs @@ -472,58 +472,58 @@ impl<'w> ComponentsQueuedRegistrator<'w> { Self { components, ids } } - /// Queues this function to run as a component registrator. + /// Queues this function to run as a component registrator if the given + /// type is not already queued as a component. /// /// # Safety /// - /// The [`TypeId`] must not already be registered or queued as a component. - unsafe fn force_register_arbitrary_component( + /// The [`TypeId`] must not already be registered as a component. + unsafe fn register_arbitrary_component( &self, type_id: TypeId, descriptor: ComponentDescriptor, func: impl FnOnce(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor) + 'static, ) -> ComponentId { - let id = self.ids.next(); self.components .queued .write() .unwrap_or_else(PoisonError::into_inner) .components - .insert( - type_id, + .entry(type_id) + .or_insert_with(|| { // SAFETY: The id was just generated. - unsafe { QueuedRegistration::new(id, descriptor, func) }, - ); - id + unsafe { QueuedRegistration::new(self.ids.next(), descriptor, func) } + }) + .id } - /// Queues this function to run as a resource registrator. + /// Queues this function to run as a resource registrator if the given + /// type is not already queued as a resource. /// /// # Safety /// - /// The [`TypeId`] must not already be registered or queued as a resource. - unsafe fn force_register_arbitrary_resource( + /// The [`TypeId`] must not already be registered as a resource. + unsafe fn register_arbitrary_resource( &self, type_id: TypeId, descriptor: ComponentDescriptor, func: impl FnOnce(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor) + 'static, ) -> ComponentId { - let id = self.ids.next(); self.components .queued .write() .unwrap_or_else(PoisonError::into_inner) .resources - .insert( - type_id, + .entry(type_id) + .or_insert_with(|| { // SAFETY: The id was just generated. - unsafe { QueuedRegistration::new(id, descriptor, func) }, - ); - id + unsafe { QueuedRegistration::new(self.ids.next(), descriptor, func) } + }) + .id } /// Queues this function to run as a dynamic registrator. - fn force_register_arbitrary_dynamic( + fn register_arbitrary_dynamic( &self, descriptor: ComponentDescriptor, func: impl FnOnce(&mut ComponentsRegistrator, ComponentId, ComponentDescriptor) + 'static, @@ -554,9 +554,9 @@ impl<'w> ComponentsQueuedRegistrator<'w> { #[inline] pub fn queue_register_component(&self) -> ComponentId { self.component_id::().unwrap_or_else(|| { - // SAFETY: We just checked that this type was not in the queue. + // SAFETY: We just checked that this type was not already registered. unsafe { - self.force_register_arbitrary_component( + self.register_arbitrary_component( TypeId::of::(), ComponentDescriptor::new::(), |registrator, id, _descriptor| { @@ -584,7 +584,7 @@ impl<'w> ComponentsQueuedRegistrator<'w> { &self, descriptor: ComponentDescriptor, ) -> ComponentId { - self.force_register_arbitrary_dynamic(descriptor, |registrator, id, descriptor| { + self.register_arbitrary_dynamic(descriptor, |registrator, id, descriptor| { // SAFETY: Id uniqueness handled by caller. unsafe { registrator.register_component_inner(id, descriptor); @@ -606,9 +606,9 @@ impl<'w> ComponentsQueuedRegistrator<'w> { pub fn queue_register_resource(&self) -> ComponentId { let type_id = TypeId::of::(); self.get_resource_id(type_id).unwrap_or_else(|| { - // SAFETY: We just checked that this type was not in the queue. + // SAFETY: We just checked that this type was not already registered. unsafe { - self.force_register_arbitrary_resource( + self.register_arbitrary_resource( type_id, ComponentDescriptor::new_resource::(), move |registrator, id, descriptor| { @@ -638,9 +638,9 @@ impl<'w> ComponentsQueuedRegistrator<'w> { pub fn queue_register_non_send(&self) -> ComponentId { let type_id = TypeId::of::(); self.get_resource_id(type_id).unwrap_or_else(|| { - // SAFETY: We just checked that this type was not in the queue. + // SAFETY: We just checked that this type was not already registered. unsafe { - self.force_register_arbitrary_resource( + self.register_arbitrary_resource( type_id, ComponentDescriptor::new_non_send::(StorageType::default()), move |registrator, id, descriptor| { @@ -669,7 +669,7 @@ impl<'w> ComponentsQueuedRegistrator<'w> { &self, descriptor: ComponentDescriptor, ) -> ComponentId { - self.force_register_arbitrary_dynamic(descriptor, |registrator, id, descriptor| { + self.register_arbitrary_dynamic(descriptor, |registrator, id, descriptor| { // SAFETY: Id uniqueness handled by caller. unsafe { registrator.register_component_inner(id, descriptor); diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index f9179b924d..c840a41290 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -8,7 +8,8 @@ use derive_more::derive::From; use crate::{ archetype::Archetype, - bundle::{BundleId, InsertMode, StaticBundle}, + bundle::{BundleId, BundleRemover, InsertMode, StaticBundle}, + change_detection::MaybeLocation, component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo}, entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper}, query::DebugCheckedUnwrap, @@ -76,6 +77,7 @@ impl<'a> SourceComponent<'a> { pub struct ComponentCloneCtx<'a, 'b> { component_id: ComponentId, target_component_written: bool, + target_component_moved: bool, bundle_scratch: &'a mut BundleScratch<'b>, bundle_scratch_allocator: &'b Bump, entities: &'a Entities, @@ -117,6 +119,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { target, bundle_scratch, target_component_written: false, + target_component_moved: false, bundle_scratch_allocator, entities, mapper, @@ -131,6 +134,11 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { self.target_component_written } + /// Returns `true` if used in moving context + pub fn moving(&self) -> bool { + self.state.move_components + } + /// Returns the current source entity. pub fn source(&self) -> Entity { self.source @@ -284,6 +292,12 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { ) { self.state.deferred_commands.push_back(Box::new(deferred)); } + + /// Marks component as moved and it's `drop` won't run. + fn move_component(&mut self) { + self.target_component_moved = true; + self.target_component_written = true; + } } /// A configuration determining how to clone entities. This can be built using [`EntityCloner::build_opt_out`]/ @@ -340,6 +354,19 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// 2. component-defined handler using [`Component::clone_behavior`] /// 3. default handler override using [`EntityClonerBuilder::with_default_clone_fn`]. /// 4. reflect-based or noop default clone handler depending on if `bevy_reflect` feature is enabled or not. +/// +/// # Moving components +/// [`EntityCloner`] can be configured to move components instead of cloning them by using [`EntityClonerBuilder::move_components`]. +/// In this mode components will be moved - removed from source entity and added to the target entity. +/// +/// Components with [`ComponentCloneBehavior::Ignore`] clone behavior will not be moved, while components that +/// have a [`ComponentCloneBehavior::Custom`] clone behavior will be cloned using it and then removed from the source entity. +/// All other components will be bitwise copied from the source entity onto the target entity and then removed without dropping. +/// +/// Choosing to move components instead of cloning makes [`EntityClonerBuilder::with_default_clone_fn`] ineffective since it's replaced by +/// move handler for components that have [`ComponentCloneBehavior::Default`] clone behavior. +/// +/// Note that moving components still triggers `on_remove` hooks/observers on source entity and `on_insert`/`on_add` hooks/observers on the target entity. #[derive(Default)] pub struct EntityCloner { filter: EntityClonerFilter, @@ -391,6 +418,7 @@ impl<'a> BundleScratch<'a> { /// /// # Safety /// All [`ComponentId`] values in this instance must come from `world`. + #[track_caller] pub(crate) unsafe fn write( self, world: &mut World, @@ -527,6 +555,7 @@ impl EntityCloner { } /// Clones and inserts components from the `source` entity into the entity mapped by `mapper` from `source` using the stored configuration. + #[track_caller] fn clone_entity_internal( state: &mut EntityClonerState, filter: &mut impl CloneByFilter, @@ -539,6 +568,8 @@ impl EntityCloner { // PERF: reusing allocated space across clones would be more efficient. Consider an allocation model similar to `Commands`. let bundle_scratch_allocator = Bump::new(); let mut bundle_scratch: BundleScratch; + let mut moved_components: Vec = Vec::new(); + let mut deferred_cloned_component_ids: Vec = Vec::new(); { let world = world.as_unsafe_world_cell(); let source_entity = world.get_entity(source).expect("Source entity must exist"); @@ -564,14 +595,26 @@ impl EntityCloner { .archetype() }); + if state.move_components { + moved_components.reserve(source_archetype.component_count()); + // Replace default handler with special handler which would track if component was moved instead of cloned. + // This is later used to determine whether we need to run component's drop function when removing it from the source entity or not. + state.default_clone_fn = |_, ctx| ctx.move_component(); + } + filter.clone_components(source_archetype, target_archetype, |component| { - let handler = match state.clone_behavior_overrides.get(&component) { - Some(clone_behavior) => clone_behavior.resolve(state.default_clone_fn), - None => world + let handler = match state.clone_behavior_overrides.get(&component).or_else(|| { + world .components() .get_info(component) - .map(|info| info.clone_behavior().resolve(state.default_clone_fn)) - .unwrap_or(state.default_clone_fn), + .map(ComponentInfo::clone_behavior) + }) { + Some(behavior) => match behavior { + ComponentCloneBehavior::Default => state.default_clone_fn, + ComponentCloneBehavior::Ignore => return, + ComponentCloneBehavior::Custom(custom) => *custom, + }, + None => state.default_clone_fn, }; // SAFETY: This component exists because it is present on the archetype. @@ -607,6 +650,19 @@ impl EntityCloner { }; (handler)(&source_component, &mut ctx); + + if ctx.state.move_components { + if ctx.target_component_moved { + moved_components.push(component); + } + // Component wasn't written by the clone handler, so assume it's going to be + // cloned/processed using deferred_commands instead. + // This means that it's ComponentId won't be present in BundleScratch's component_ids, + // but it should still be removed when move_components is true. + else if !ctx.target_component_written() { + deferred_cloned_component_ids.push(component); + } + } }); } @@ -621,9 +677,67 @@ impl EntityCloner { } if state.move_components { - world - .entity_mut(source) - .remove_by_ids(&bundle_scratch.component_ids); + let mut source_entity = world.entity_mut(source); + + let cloned_components = if deferred_cloned_component_ids.is_empty() { + &bundle_scratch.component_ids + } else { + // Remove all cloned components with drop by concatenating both vectors + deferred_cloned_component_ids.extend(&bundle_scratch.component_ids); + &deferred_cloned_component_ids + }; + source_entity.remove_by_ids_with_caller( + cloned_components, + MaybeLocation::caller(), + RelationshipHookMode::RunIfNotLinked, + BundleRemover::empty_pre_remove, + ); + + let table_row = source_entity.location().table_row; + + // Copy moved components and then forget them without calling drop + source_entity.remove_by_ids_with_caller( + &moved_components, + MaybeLocation::caller(), + RelationshipHookMode::RunIfNotLinked, + |sparse_sets, mut table, components, bundle| { + for &component_id in bundle { + let Some(component_ptr) = sparse_sets + .get(component_id) + .and_then(|component| component.get(source)) + .or_else(|| { + // SAFETY: table_row is within this table because we just got it from entity's current location + table.as_mut().and_then(|table| unsafe { + table.get_component(component_id, table_row) + }) + }) + else { + // Component was removed by some other component's clone side effect before we got to it. + continue; + }; + + // SAFETY: component_id is valid because remove_by_ids_with_caller checked it before calling this closure + let info = unsafe { components.get_info_unchecked(component_id) }; + let layout = info.layout(); + let target_ptr = bundle_scratch_allocator.alloc_layout(layout); + // SAFETY: + // - component_ptr points to data with component layout + // - target_ptr was just allocated with component layout + // - component_ptr and target_ptr don't overlap + // - component_ptr matches component_id + unsafe { + core::ptr::copy_nonoverlapping( + component_ptr.as_ptr(), + target_ptr.as_ptr(), + layout.size(), + ); + bundle_scratch.push_ptr(component_id, PtrMut::new(target_ptr)); + } + } + + (/* should drop? */ false, ()) + }, + ); } // SAFETY: @@ -688,6 +802,8 @@ impl<'w, Filter: CloneByFilter> EntityClonerBuilder<'w, Filter> { } /// Sets the default clone function to use. + /// + /// Will be overridden if [`EntityClonerBuilder::move_components`] is enabled. pub fn with_default_clone_fn(&mut self, clone_fn: ComponentCloneFn) -> &mut Self { self.state.default_clone_fn = clone_fn; self @@ -700,6 +816,8 @@ impl<'w, Filter: CloneByFilter> EntityClonerBuilder<'w, Filter> { /// /// The setting only applies to components that are allowed through the filter /// at the time [`EntityClonerBuilder::clone_entity`] is called. + /// + /// Enabling this overrides any custom function set with [`EntityClonerBuilder::with_default_clone_fn`]. pub fn move_components(&mut self, enable: bool) -> &mut Self { self.state.move_components = enable; self @@ -1333,8 +1451,9 @@ mod tests { use super::*; use crate::{ component::{ComponentDescriptor, StorageType}, + lifecycle::HookContext, prelude::{ChildOf, Children, Resource}, - world::{FromWorld, World}, + world::{DeferredWorld, FromWorld, World}, }; use bevy_ptr::OwningPtr; use core::marker::PhantomData; @@ -2030,10 +2149,21 @@ mod tests { assert!(root_children.iter().all(|e| *e != child1 && *e != child2)); assert_eq!(root_children.len(), 2); + assert_eq!( + ( + world.get::(root_children[0]), + world.get::(root_children[1]) + ), + (Some(&ChildOf(clone_root)), Some(&ChildOf(clone_root))) + ); let child1_children = world.entity(root_children[0]).get::().unwrap(); assert_eq!(child1_children.len(), 1); assert_ne!(child1_children[0], grandchild); assert!(world.entity(root_children[1]).get::().is_none()); + assert_eq!( + world.get::(child1_children[0]), + Some(&ChildOf(root_children[0])) + ); assert_eq!( world.entity(root).get::().unwrap().deref(), @@ -2062,4 +2192,403 @@ mod tests { assert_eq!(world.entity(e_clone).get::(), Some(&A)); assert_eq!(world.entity(e_clone).get::(), Some(&B(1))); } + + #[test] + fn move_without_clone() { + #[derive(Component, PartialEq, Debug)] + #[component(storage = "SparseSet")] + struct A; + + #[derive(Component, PartialEq, Debug)] + struct B(Vec); + + let mut world = World::default(); + let e = world.spawn((A, B(alloc::vec![1, 2, 3]))).id(); + let e_clone = world.spawn_empty().id(); + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.move_components(true); + let mut cloner = builder.finish(); + + cloner.clone_entity(&mut world, e, e_clone); + + assert_eq!(world.get::(e), None); + assert_eq!(world.get::(e), None); + + assert_eq!(world.get::(e_clone), Some(&A)); + assert_eq!(world.get::(e_clone), Some(&B(alloc::vec![1, 2, 3]))); + } + + #[test] + fn move_with_remove_hook() { + #[derive(Component, PartialEq, Debug)] + #[component(on_remove=remove_hook)] + struct B(Option>); + + fn remove_hook(mut world: DeferredWorld, ctx: HookContext) { + world.get_mut::(ctx.entity).unwrap().0.take(); + } + + let mut world = World::default(); + let e = world.spawn(B(Some(alloc::vec![1, 2, 3]))).id(); + let e_clone = world.spawn_empty().id(); + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.move_components(true); + let mut cloner = builder.finish(); + + cloner.clone_entity(&mut world, e, e_clone); + + assert_eq!(world.get::(e), None); + assert_eq!(world.get::(e_clone), Some(&B(None))); + } + + #[test] + fn move_with_deferred() { + #[derive(Component, PartialEq, Debug)] + #[component(clone_behavior=Custom(custom))] + struct A(u32); + + #[derive(Component, PartialEq, Debug)] + struct B(u32); + + fn custom(_src: &SourceComponent, ctx: &mut ComponentCloneCtx) { + // Clone using deferred + let source = ctx.source(); + ctx.queue_deferred(move |world, mapper| { + let target = mapper.get_mapped(source); + world.entity_mut(target).insert(A(10)); + }); + } + + let mut world = World::default(); + let e = world.spawn((A(0), B(1))).id(); + let e_clone = world.spawn_empty().id(); + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.move_components(true); + let mut cloner = builder.finish(); + + cloner.clone_entity(&mut world, e, e_clone); + + assert_eq!(world.get::(e), None); + assert_eq!(world.get::(e_clone), Some(&A(10))); + assert_eq!(world.get::(e), None); + assert_eq!(world.get::(e_clone), Some(&B(1))); + } + + #[test] + fn move_relationship() { + #[derive(Component, Clone, PartialEq, Eq, Debug)] + #[relationship(relationship_target=Target)] + struct Source(Entity); + + #[derive(Component, Clone, PartialEq, Eq, Debug)] + #[relationship_target(relationship=Source)] + struct Target(Vec); + + #[derive(Component, PartialEq, Debug)] + struct A(u32); + + let mut world = World::default(); + let e_target = world.spawn(A(1)).id(); + let e_source = world.spawn((A(2), Source(e_target))).id(); + + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.move_components(true); + let mut cloner = builder.finish(); + + let e_source_moved = world.spawn_empty().id(); + + cloner.clone_entity(&mut world, e_source, e_source_moved); + + assert_eq!(world.get::(e_source), None); + assert_eq!(world.get::(e_source_moved), Some(&A(2))); + assert_eq!(world.get::(e_source), None); + assert_eq!(world.get::(e_source_moved), Some(&Source(e_target))); + assert_eq!( + world.get::(e_target), + Some(&Target(alloc::vec![e_source_moved])) + ); + + let e_target_moved = world.spawn_empty().id(); + + cloner.clone_entity(&mut world, e_target, e_target_moved); + + assert_eq!(world.get::(e_target), None); + assert_eq!(world.get::(e_target_moved), Some(&A(1))); + assert_eq!(world.get::(e_target), None); + assert_eq!( + world.get::(e_target_moved), + Some(&Target(alloc::vec![e_source_moved])) + ); + assert_eq!( + world.get::(e_source_moved), + Some(&Source(e_target_moved)) + ); + } + + #[test] + fn move_hierarchy() { + #[derive(Component, PartialEq, Debug)] + struct A(u32); + + let mut world = World::default(); + let e_parent = world.spawn(A(1)).id(); + let e_child1 = world.spawn((A(2), ChildOf(e_parent))).id(); + let e_child2 = world.spawn((A(3), ChildOf(e_parent))).id(); + let e_child1_1 = world.spawn((A(4), ChildOf(e_child1))).id(); + + let e_parent_clone = world.spawn_empty().id(); + + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.move_components(true).linked_cloning(true); + let mut cloner = builder.finish(); + + cloner.clone_entity(&mut world, e_parent, e_parent_clone); + + assert_eq!(world.get::(e_parent), None); + assert_eq!(world.get::(e_child1), None); + assert_eq!(world.get::(e_child2), None); + assert_eq!(world.get::(e_child1_1), None); + + let mut children = world.get::(e_parent_clone).unwrap().iter(); + let e_child1_clone = *children.next().unwrap(); + let e_child2_clone = *children.next().unwrap(); + let mut children = world.get::(e_child1_clone).unwrap().iter(); + let e_child1_1_clone = *children.next().unwrap(); + + assert_eq!(world.get::(e_parent_clone), Some(&A(1))); + assert_eq!(world.get::(e_child1_clone), Some(&A(2))); + assert_eq!( + world.get::(e_child1_clone), + Some(&ChildOf(e_parent_clone)) + ); + assert_eq!(world.get::(e_child2_clone), Some(&A(3))); + assert_eq!( + world.get::(e_child2_clone), + Some(&ChildOf(e_parent_clone)) + ); + assert_eq!(world.get::(e_child1_1_clone), Some(&A(4))); + assert_eq!( + world.get::(e_child1_1_clone), + Some(&ChildOf(e_child1_clone)) + ); + } + + // Original: E1 Target{target: [E2], data: [4,5,6]} + // | E2 Source{target: E1, data: [1,2,3]} + // + // Cloned: E3 Target{target: [], data: [4,5,6]} + #[test] + fn clone_relationship_with_data() { + #[derive(Component, Clone)] + #[relationship(relationship_target=Target)] + struct Source { + #[relationship] + target: Entity, + data: Vec, + } + + #[derive(Component, Clone)] + #[relationship_target(relationship=Source)] + struct Target { + #[relationship] + target: Vec, + data: Vec, + } + + let mut world = World::default(); + let e_target = world.spawn_empty().id(); + let e_source = world + .spawn(Source { + target: e_target, + data: alloc::vec![1, 2, 3], + }) + .id(); + world.get_mut::(e_target).unwrap().data = alloc::vec![4, 5, 6]; + + let builder = EntityCloner::build_opt_out(&mut world); + let mut cloner = builder.finish(); + + let e_target_clone = world.spawn_empty().id(); + cloner.clone_entity(&mut world, e_target, e_target_clone); + + let target = world.get::(e_target).unwrap(); + let cloned_target = world.get::(e_target_clone).unwrap(); + + assert_eq!(cloned_target.data, target.data); + assert_eq!(target.target, alloc::vec![e_source]); + assert_eq!(cloned_target.target.len(), 0); + + let source = world.get::(e_source).unwrap(); + + assert_eq!(source.data, alloc::vec![1, 2, 3]); + } + + // Original: E1 Target{target: [E2], data: [4,5,6]} + // | E2 Source{target: E1, data: [1,2,3]} + // + // Cloned: E3 Target{target: [E4], data: [4,5,6]} + // | E4 Source{target: E3, data: [1,2,3]} + #[test] + fn clone_linked_relationship_with_data() { + #[derive(Component, Clone)] + #[relationship(relationship_target=Target)] + struct Source { + #[relationship] + target: Entity, + data: Vec, + } + + #[derive(Component, Clone)] + #[relationship_target(relationship=Source, linked_spawn)] + struct Target { + #[relationship] + target: Vec, + data: Vec, + } + + let mut world = World::default(); + let e_target = world.spawn_empty().id(); + let e_source = world + .spawn(Source { + target: e_target, + data: alloc::vec![1, 2, 3], + }) + .id(); + world.get_mut::(e_target).unwrap().data = alloc::vec![4, 5, 6]; + + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.linked_cloning(true); + let mut cloner = builder.finish(); + + let e_target_clone = world.spawn_empty().id(); + cloner.clone_entity(&mut world, e_target, e_target_clone); + + let target = world.get::(e_target).unwrap(); + let cloned_target = world.get::(e_target_clone).unwrap(); + + assert_eq!(cloned_target.data, target.data); + assert_eq!(target.target, alloc::vec![e_source]); + assert_eq!(cloned_target.target.len(), 1); + + let source = world.get::(e_source).unwrap(); + let cloned_source = world.get::(cloned_target.target[0]).unwrap(); + + assert_eq!(cloned_source.data, source.data); + assert_eq!(source.target, e_target); + assert_eq!(cloned_source.target, e_target_clone); + } + + // Original: E1 + // E2 + // + // Moved: E3 Target{target: [], data: [4,5,6]} + #[test] + fn move_relationship_with_data() { + #[derive(Component, Clone, PartialEq, Eq, Debug)] + #[relationship(relationship_target=Target)] + struct Source { + #[relationship] + target: Entity, + data: Vec, + } + + #[derive(Component, Clone, PartialEq, Eq, Debug)] + #[relationship_target(relationship=Source)] + struct Target { + #[relationship] + target: Vec, + data: Vec, + } + + let source_data = alloc::vec![1, 2, 3]; + let target_data = alloc::vec![4, 5, 6]; + + let mut world = World::default(); + let e_target = world.spawn_empty().id(); + let e_source = world + .spawn(Source { + target: e_target, + data: source_data.clone(), + }) + .id(); + world.get_mut::(e_target).unwrap().data = target_data.clone(); + + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.move_components(true); + let mut cloner = builder.finish(); + + let e_target_moved = world.spawn_empty().id(); + cloner.clone_entity(&mut world, e_target, e_target_moved); + + assert_eq!(world.get::(e_target), None); + assert_eq!( + world.get::(e_source), + Some(&Source { + data: source_data, + target: e_target_moved, + }) + ); + assert_eq!( + world.get::(e_target_moved), + Some(&Target { + target: alloc::vec![e_source], + data: target_data + }) + ); + } + + // Original: E1 + // E2 + // + // Moved: E3 Target{target: [E4], data: [4,5,6]} + // | E4 Source{target: E3, data: [1,2,3]} + #[test] + fn move_linked_relationship_with_data() { + #[derive(Component, Clone, PartialEq, Eq, Debug)] + #[relationship(relationship_target=Target)] + struct Source { + #[relationship] + target: Entity, + data: Vec, + } + + #[derive(Component, Clone, PartialEq, Eq, Debug)] + #[relationship_target(relationship=Source, linked_spawn)] + struct Target { + #[relationship] + target: Vec, + data: Vec, + } + + let source_data = alloc::vec![1, 2, 3]; + let target_data = alloc::vec![4, 5, 6]; + + let mut world = World::default(); + let e_target = world.spawn_empty().id(); + let e_source = world + .spawn(Source { + target: e_target, + data: source_data.clone(), + }) + .id(); + world.get_mut::(e_target).unwrap().data = target_data.clone(); + + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.move_components(true).linked_cloning(true); + let mut cloner = builder.finish(); + + let e_target_moved = world.spawn_empty().id(); + cloner.clone_entity(&mut world, e_target, e_target_moved); + + assert_eq!(world.get::(e_target), None); + assert_eq!(world.get::(e_source), None); + + let moved_target = world.get::(e_target_moved).unwrap(); + assert_eq!(moved_target.data, target_data); + assert_eq!(moved_target.target.len(), 1); + + let moved_source = world.get::(moved_target.target[0]).unwrap(); + assert_eq!(moved_source.data, source_data); + assert_eq!(moved_source.target, e_target_moved); + } } diff --git a/crates/bevy_ecs/src/event/base.rs b/crates/bevy_ecs/src/event/base.rs index 89a3e10acd..60a214c0f0 100644 --- a/crates/bevy_ecs/src/event/base.rs +++ b/crates/bevy_ecs/src/event/base.rs @@ -259,7 +259,7 @@ pub trait EntityEvent: Event { const AUTO_PROPAGATE: bool = false; } -/// A buffered [`Event`] for pull-based event handling. +/// 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, @@ -286,7 +286,7 @@ pub trait EntityEvent: Event { /// ``` /// # use bevy_ecs::prelude::*; /// # -/// #[derive(Event, BufferedEvent)] +/// #[derive(BufferedEvent)] /// struct Message(String); /// ``` /// @@ -295,7 +295,7 @@ pub trait EntityEvent: Event { /// ``` /// # use bevy_ecs::prelude::*; /// # -/// # #[derive(Event, BufferedEvent)] +/// # #[derive(BufferedEvent)] /// # struct Message(String); /// # /// fn write_hello(mut writer: EventWriter) { @@ -308,7 +308,7 @@ pub trait EntityEvent: Event { /// ``` /// # use bevy_ecs::prelude::*; /// # -/// # #[derive(Event, BufferedEvent)] +/// # #[derive(BufferedEvent)] /// # struct Message(String); /// # /// fn read_messages(mut reader: EventReader) { @@ -327,9 +327,9 @@ pub trait EntityEvent: Event { #[diagnostic::on_unimplemented( message = "`{Self}` is not an `BufferedEvent`", label = "invalid `BufferedEvent`", - note = "consider annotating `{Self}` with `#[derive(Event, BufferedEvent)]`" + note = "consider annotating `{Self}` with `#[derive(BufferedEvent)]`" )] -pub trait BufferedEvent: Event {} +pub trait BufferedEvent: Send + Sync + 'static {} /// An internal type that implements [`Component`] for a given [`Event`] type. /// diff --git a/crates/bevy_ecs/src/event/collections.rs b/crates/bevy_ecs/src/event/collections.rs index 5175efb03a..1fc020eceb 100644 --- a/crates/bevy_ecs/src/event/collections.rs +++ b/crates/bevy_ecs/src/event/collections.rs @@ -40,9 +40,9 @@ use { /// # Example /// /// ``` -/// use bevy_ecs::event::{BufferedEvent, Event, Events}; +/// use bevy_ecs::event::{BufferedEvent, Events}; /// -/// #[derive(Event, BufferedEvent)] +/// #[derive(BufferedEvent)] /// struct MyEvent { /// value: usize /// } @@ -419,11 +419,11 @@ impl ExactSizeIterator for WriteBatchIds { #[cfg(test)] mod tests { - use crate::event::{BufferedEvent, Event, Events}; + use crate::event::{BufferedEvent, Events}; #[test] fn iter_current_update_events_iterates_over_current_events() { - #[derive(Event, BufferedEvent, Clone)] + #[derive(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 f0460a9424..12163caa90 100644 --- a/crates/bevy_ecs/src/event/event_cursor.rs +++ b/crates/bevy_ecs/src/event/event_cursor.rs @@ -22,7 +22,7 @@ use core::marker::PhantomData; /// use bevy_ecs::prelude::*; /// use bevy_ecs::event::{BufferedEvent, Events, EventCursor}; /// -/// #[derive(Event, BufferedEvent, Clone, Debug)] +/// #[derive(BufferedEvent, Clone, Debug)] /// struct MyEvent; /// /// /// A system that both sends and receives events using a [`Local`] [`EventCursor`]. diff --git a/crates/bevy_ecs/src/event/mod.rs b/crates/bevy_ecs/src/event/mod.rs index 77741a47ab..020b258557 100644 --- a/crates/bevy_ecs/src/event/mod.rs +++ b/crates/bevy_ecs/src/event/mod.rs @@ -41,12 +41,12 @@ mod tests { use bevy_ecs::{event::*, system::assert_is_read_only_system}; use bevy_ecs_macros::BufferedEvent; - #[derive(Event, BufferedEvent, Copy, Clone, PartialEq, Eq, Debug)] + #[derive(BufferedEvent, Copy, Clone, PartialEq, Eq, Debug)] struct TestEvent { i: usize, } - #[derive(Event, BufferedEvent, Clone, PartialEq, Debug, Default)] + #[derive(BufferedEvent, Clone, PartialEq, Debug, Default)] struct EmptyTestEvent; fn get_events( diff --git a/crates/bevy_ecs/src/event/mutator.rs b/crates/bevy_ecs/src/event/mutator.rs index 847cbf3e16..b2631a6565 100644 --- a/crates/bevy_ecs/src/event/mutator.rs +++ b/crates/bevy_ecs/src/event/mutator.rs @@ -15,7 +15,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event, BufferedEvent, Debug)] +/// #[derive(BufferedEvent, Debug)] /// pub struct MyEvent(pub u32); // Custom event type. /// fn my_system(mut reader: EventMutator) { /// for event in reader.read() { @@ -69,7 +69,7 @@ impl<'w, 's, E: BufferedEvent> EventMutator<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event, BufferedEvent)] + /// #[derive(BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -116,7 +116,7 @@ impl<'w, 's, E: BufferedEvent> EventMutator<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event, BufferedEvent)] + /// #[derive(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 2e135eab2f..7aadd72d60 100644 --- a/crates/bevy_ecs/src/event/reader.rs +++ b/crates/bevy_ecs/src/event/reader.rs @@ -41,7 +41,7 @@ impl<'w, 's, E: BufferedEvent> EventReader<'w, 's, E> { /// # use bevy_ecs::prelude::*; /// # use std::sync::atomic::{AtomicUsize, Ordering}; /// - /// #[derive(Event, BufferedEvent)] + /// #[derive(BufferedEvent)] /// struct MyEvent { /// value: usize, /// } @@ -88,7 +88,7 @@ impl<'w, 's, E: BufferedEvent> EventReader<'w, 's, E> { /// ``` /// # use bevy_ecs::prelude::*; /// # - /// #[derive(Event, BufferedEvent)] + /// #[derive(BufferedEvent)] /// struct CollisionEvent; /// /// fn play_collision_sound(mut events: EventReader) { diff --git a/crates/bevy_ecs/src/event/writer.rs b/crates/bevy_ecs/src/event/writer.rs index 015e59891c..4f87039f37 100644 --- a/crates/bevy_ecs/src/event/writer.rs +++ b/crates/bevy_ecs/src/event/writer.rs @@ -11,7 +11,7 @@ use bevy_ecs::{ /// ``` /// # use bevy_ecs::prelude::*; /// -/// #[derive(Event, BufferedEvent)] +/// #[derive(BufferedEvent)] /// pub struct MyEvent; // Custom event type. /// fn my_system(mut writer: EventWriter) { /// writer.write(MyEvent); @@ -38,7 +38,7 @@ use bevy_ecs::{ /// /// ``` /// # use bevy_ecs::{prelude::*, event::Events}; -/// # #[derive(Event, BufferedEvent)] +/// # #[derive(BufferedEvent)] /// # pub struct MyEvent; /// fn write_untyped(mut commands: Commands) { /// // Write an event of a specific type without having to declare that diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 7b411a6cfa..e57bb5afb4 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -60,7 +60,7 @@ pub mod world; pub use bevy_ptr as ptr; #[cfg(feature = "hotpatching")] -use event::{BufferedEvent, Event}; +use event::BufferedEvent; /// The ECS prelude. /// @@ -140,7 +140,7 @@ pub mod __macro_exports { /// /// Systems should refresh their inner pointers. #[cfg(feature = "hotpatching")] -#[derive(Event, BufferedEvent, Default)] +#[derive(BufferedEvent, Default)] pub struct HotPatched; #[cfg(test)] @@ -2837,4 +2837,17 @@ mod tests { fn custom_clone(_source: &SourceComponent, _ctx: &mut ComponentCloneCtx) {} } + + #[test] + fn queue_register_component_toctou() { + for _ in 0..1000 { + let w = World::new(); + + std::thread::scope(|s| { + let c1 = s.spawn(|| w.components_queue().queue_register_component::()); + let c2 = s.spawn(|| w.components_queue().queue_register_component::()); + assert_eq!(c1.join().unwrap(), c2.join().unwrap()); + }); + } + } } diff --git a/crates/bevy_ecs/src/lifecycle.rs b/crates/bevy_ecs/src/lifecycle.rs index 7c3f1ffa4a..8fa61b8297 100644 --- a/crates/bevy_ecs/src/lifecycle.rs +++ b/crates/bevy_ecs/src/lifecycle.rs @@ -393,7 +393,7 @@ pub type OnDespawn = Despawn; /// Wrapper around [`Entity`] for [`RemovedComponents`]. /// Internally, `RemovedComponents` uses these as an `Events`. -#[derive(Event, BufferedEvent, Debug, Clone, Into)] +#[derive(BufferedEvent, Debug, Clone, Into)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr(feature = "bevy_reflect", reflect(Debug, Clone))] pub struct RemovedComponentEntity(Entity); diff --git a/crates/bevy_ecs/src/relationship/mod.rs b/crates/bevy_ecs/src/relationship/mod.rs index 82b39e04e5..943268ee17 100644 --- a/crates/bevy_ecs/src/relationship/mod.rs +++ b/crates/bevy_ecs/src/relationship/mod.rs @@ -4,6 +4,8 @@ mod related_methods; mod relationship_query; mod relationship_source_collection; +use core::marker::PhantomData; + use alloc::format; use bevy_utils::prelude::DebugName; @@ -12,8 +14,8 @@ pub use relationship_query::*; pub use relationship_source_collection::*; use crate::{ - component::{Component, Mutable}, - entity::{ComponentCloneCtx, Entity, SourceComponent}, + component::{Component, ComponentCloneBehavior, Mutable}, + entity::{ComponentCloneCtx, Entity}, error::CommandWithEntity, lifecycle::HookContext, world::{DeferredWorld, EntityWorldMut}, @@ -240,7 +242,19 @@ pub trait RelationshipTarget: Component + Sized { /// The `on_replace` component hook that maintains the [`Relationship`] / [`RelationshipTarget`] connection. // note: think of this as "on_drop" - fn on_replace(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { + fn on_replace( + mut world: DeferredWorld, + HookContext { + entity, + relationship_hook_mode, + .. + }: HookContext, + ) { + match relationship_hook_mode { + RelationshipHookMode::Run => {} + // For RelationshipTarget we don't want to run this hook even if it isn't linked, but for Relationship we do. + RelationshipHookMode::Skip | RelationshipHookMode::RunIfNotLinked => return, + } let (entities, mut commands) = world.entities_and_commands(); let relationship_target = entities.get(entity).unwrap().get::().unwrap(); for source_entity in relationship_target.iter() { @@ -287,28 +301,38 @@ pub trait RelationshipTarget: Component + Sized { } } -/// The "clone behavior" for [`RelationshipTarget`]. This actually creates an empty -/// [`RelationshipTarget`] instance with space reserved for the number of targets in the -/// original instance. The [`RelationshipTarget`] will then be populated with the proper components +/// The "clone behavior" for [`RelationshipTarget`]. The [`RelationshipTarget`] will be populated with the proper components /// when the corresponding [`Relationship`] sources of truth are inserted. Cloning the actual entities /// in the original [`RelationshipTarget`] would result in duplicates, so we don't do that! /// /// This will also queue up clones of the relationship sources if the [`EntityCloner`](crate::entity::EntityCloner) is configured /// to spawn recursively. pub fn clone_relationship_target( - source: &SourceComponent, + component: &T, + cloned: &mut T, context: &mut ComponentCloneCtx, ) { - if let Some(component) = source.read::() { - let mut cloned = T::with_capacity(component.len()); - if context.linked_cloning() && T::LINKED_SPAWN { - let collection = cloned.collection_mut_risky(); - for entity in component.iter() { - collection.add(entity); - context.queue_entity_clone(entity); - } + if context.linked_cloning() && T::LINKED_SPAWN { + let collection = cloned.collection_mut_risky(); + for entity in component.iter() { + collection.add(entity); + context.queue_entity_clone(entity); + } + } else if context.moving() { + let target = context.target(); + let collection = cloned.collection_mut_risky(); + for entity in component.iter() { + collection.add(entity); + context.queue_deferred(move |world, _mapper| { + // We don't want relationships hooks to run because we are manually constructing the collection here + _ = DeferredWorld::from(world) + .modify_component_with_relationship_hook_mode::( + entity, + RelationshipHookMode::Skip, + |r| r.set_risky(target), + ); + }); } - context.write_target_component(cloned); } } @@ -323,6 +347,122 @@ pub enum RelationshipHookMode { Skip, } +/// Wrapper for components clone specialization using autoderef. +#[doc(hidden)] +pub struct RelationshipCloneBehaviorSpecialization(PhantomData); + +impl Default for RelationshipCloneBehaviorSpecialization { + fn default() -> Self { + Self(PhantomData) + } +} + +/// Base trait for relationship clone specialization using autoderef. +#[doc(hidden)] +pub trait RelationshipCloneBehaviorBase { + fn default_clone_behavior(&self) -> ComponentCloneBehavior; +} + +impl RelationshipCloneBehaviorBase for RelationshipCloneBehaviorSpecialization { + fn default_clone_behavior(&self) -> ComponentCloneBehavior { + // Relationships currently must have `Clone`/`Reflect`-based handler for cloning/moving logic to properly work. + ComponentCloneBehavior::Ignore + } +} + +/// Specialized trait for relationship clone specialization using autoderef. +#[doc(hidden)] +pub trait RelationshipCloneBehaviorViaReflect { + fn default_clone_behavior(&self) -> ComponentCloneBehavior; +} + +#[cfg(feature = "bevy_reflect")] +impl RelationshipCloneBehaviorViaReflect + for &RelationshipCloneBehaviorSpecialization +{ + fn default_clone_behavior(&self) -> ComponentCloneBehavior { + ComponentCloneBehavior::reflect() + } +} + +/// Specialized trait for relationship clone specialization using autoderef. +#[doc(hidden)] +pub trait RelationshipCloneBehaviorViaClone { + fn default_clone_behavior(&self) -> ComponentCloneBehavior; +} + +impl RelationshipCloneBehaviorViaClone + for &&RelationshipCloneBehaviorSpecialization +{ + fn default_clone_behavior(&self) -> ComponentCloneBehavior { + ComponentCloneBehavior::clone::() + } +} + +/// Specialized trait for relationship target clone specialization using autoderef. +#[doc(hidden)] +pub trait RelationshipTargetCloneBehaviorViaReflect { + fn default_clone_behavior(&self) -> ComponentCloneBehavior; +} + +#[cfg(feature = "bevy_reflect")] +impl + RelationshipTargetCloneBehaviorViaReflect for &&&RelationshipCloneBehaviorSpecialization +{ + fn default_clone_behavior(&self) -> ComponentCloneBehavior { + ComponentCloneBehavior::Custom(|source, context| { + if let Some(component) = source.read::() + && let Ok(mut cloned) = component.reflect_clone_and_take::() + { + cloned.collection_mut_risky().clear(); + clone_relationship_target(component, &mut cloned, context); + context.write_target_component(cloned); + } + }) + } +} + +/// Specialized trait for relationship target clone specialization using autoderef. +#[doc(hidden)] +pub trait RelationshipTargetCloneBehaviorViaClone { + fn default_clone_behavior(&self) -> ComponentCloneBehavior; +} + +impl RelationshipTargetCloneBehaviorViaClone + for &&&&RelationshipCloneBehaviorSpecialization +{ + fn default_clone_behavior(&self) -> ComponentCloneBehavior { + ComponentCloneBehavior::Custom(|source, context| { + if let Some(component) = source.read::() { + let mut cloned = component.clone(); + cloned.collection_mut_risky().clear(); + clone_relationship_target(component, &mut cloned, context); + context.write_target_component(cloned); + } + }) + } +} + +/// We know there's no additional data on Children, so this handler is an optimization to avoid cloning the entire Collection. +#[doc(hidden)] +pub trait RelationshipTargetCloneBehaviorHierarchy { + fn default_clone_behavior(&self) -> ComponentCloneBehavior; +} + +impl RelationshipTargetCloneBehaviorHierarchy + for &&&&&RelationshipCloneBehaviorSpecialization +{ + fn default_clone_behavior(&self) -> ComponentCloneBehavior { + ComponentCloneBehavior::Custom(|source, context| { + if let Some(component) = source.read::() { + let mut cloned = crate::hierarchy::Children::with_capacity(component.len()); + clone_relationship_target(component, &mut cloned, context); + context.write_target_component(cloned); + } + }) + } +} + #[cfg(test)] mod tests { use crate::prelude::{ChildOf, Children}; diff --git a/crates/bevy_ecs/src/schedule/condition.rs b/crates/bevy_ecs/src/schedule/condition.rs index 2cd71db9b4..fb971f1aa7 100644 --- a/crates/bevy_ecs/src/schedule/condition.rs +++ b/crates/bevy_ecs/src/schedule/condition.rs @@ -864,7 +864,7 @@ pub mod common_conditions { /// my_system.run_if(on_event::), /// ); /// - /// #[derive(Event, BufferedEvent)] + /// #[derive(BufferedEvent)] /// struct MyEvent; /// /// fn my_system(mut counter: ResMut) { @@ -1264,7 +1264,7 @@ where #[cfg(test)] mod tests { use super::{common_conditions::*, SystemCondition}; - use crate::event::{BufferedEvent, Event}; + use crate::event::BufferedEvent; use crate::query::With; use crate::{ change_detection::ResMut, @@ -1384,7 +1384,7 @@ mod tests { #[derive(Component)] struct TestComponent; - #[derive(Event, BufferedEvent)] + #[derive(BufferedEvent)] struct TestEvent; #[derive(Resource)] diff --git a/crates/bevy_ecs/src/schedule/mod.rs b/crates/bevy_ecs/src/schedule/mod.rs index d368e6b5e7..8b559b33c3 100644 --- a/crates/bevy_ecs/src/schedule/mod.rs +++ b/crates/bevy_ecs/src/schedule/mod.rs @@ -783,7 +783,7 @@ mod tests { #[derive(Component)] struct B; - #[derive(Event, BufferedEvent)] + #[derive(BufferedEvent)] struct E; #[derive(Resource, Component)] diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index 1fc69d1c46..2d037eef16 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -252,6 +252,7 @@ where } /// An [`IntoSystem`] creating an instance of [`PipeSystem`]. +#[derive(Clone)] pub struct IntoPipeSystem { a: A, b: B, diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 599e9be50e..316ba0b2e4 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -310,8 +310,20 @@ pub fn clone_components(target: Entity) -> impl EntityCommand { } } -/// An [`EntityCommand`] that clones the specified components of an entity -/// and inserts them into another entity, then removes them from the original entity. +/// An [`EntityCommand`] moves the specified components of this entity into another entity. +/// +/// Components with [`Ignore`] clone behavior will not be moved, while components that +/// have a [`Custom`] clone behavior will be cloned using it and then removed from the source entity. +/// All other components will be moved without any other special handling. +/// +/// Note that this will trigger `on_remove` hooks/observers on this entity and `on_insert`/`on_add` hooks/observers on the target entity. +/// +/// # Panics +/// +/// The command will panic when applied if the target entity does not exist. +/// +/// [`Ignore`]: crate::component::ComponentCloneBehavior::Ignore +/// [`Custom`]: crate::component::ComponentCloneBehavior::Custom pub fn move_components(target: Entity) -> impl EntityCommand { move |mut entity: EntityWorldMut| { entity.move_components::(target); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 59f4b51ff6..7c92d65f5e 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -2237,15 +2237,20 @@ impl<'a> EntityCommands<'a> { self.queue(entity_command::clone_components::(target)) } - /// Clones the specified components of this entity and inserts them into another entity, - /// then removes the components from this entity. + /// Moves the specified components of this entity into another entity. /// - /// Components can only be cloned if they implement - /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). + /// Components with [`Ignore`] clone behavior will not be moved, while components that + /// have a [`Custom`] clone behavior will be cloned using it and then removed from the source entity. + /// All other components will be moved without any other special handling. + /// + /// Note that this will trigger `on_remove` hooks/observers on this entity and `on_insert`/`on_add` hooks/observers on the target entity. /// /// # Panics /// /// The command will panic when applied if the target entity does not exist. + /// + /// [`Ignore`]: crate::component::ComponentCloneBehavior::Ignore + /// [`Custom`]: crate::component::ComponentCloneBehavior::Custom pub fn move_components(&mut self, target: Entity) -> &mut Self { self.queue(entity_command::move_components::(target)) } diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 35d7e709e9..89740a9dce 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -135,7 +135,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event, BufferedEvent)] +/// # #[derive(BufferedEvent)] /// # struct MyEvent; /// # #[derive(Resource)] /// # struct MyResource(u32); @@ -168,7 +168,7 @@ impl SystemMeta { /// # use bevy_ecs::system::SystemState; /// # use bevy_ecs::event::Events; /// # -/// # #[derive(Event, BufferedEvent)] +/// # #[derive(BufferedEvent)] /// # struct MyEvent; /// #[derive(Resource)] /// struct CachedSystemState { diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index aad37c09d0..fb28ea081a 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -221,6 +221,10 @@ pub trait System: Send + Sync + 'static { /// /// This must only be implemented for system types which do not mutate the `World` /// when [`System::run_unsafe`] is called. +#[diagnostic::on_unimplemented( + message = "`{Self}` is not a read-only system", + label = "invalid read-only system" +)] pub unsafe trait ReadOnlySystem: System { /// Runs this system with the given input in the world. /// @@ -245,6 +249,9 @@ pub unsafe trait ReadOnlySystem: System { /// A convenience type alias for a boxed [`System`] trait object. pub type BoxedSystem = Box>; +/// A convenience type alias for a boxed [`ReadOnlySystem`] trait object. +pub type BoxedReadOnlySystem = Box>; + pub(crate) fn check_system_change_tick( last_run: &mut Tick, check: CheckChangeTicks, diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index d552bf1f1d..bd9ccc1976 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -57,7 +57,7 @@ use variadics_please::{all_tuples, all_tuples_enumerated}; /// # use bevy_ecs::prelude::*; /// # #[derive(Resource)] /// # struct SomeResource; -/// # #[derive(Event, BufferedEvent)] +/// # #[derive(BufferedEvent)] /// # struct SomeEvent; /// # #[derive(Resource)] /// # struct SomeOtherResource; @@ -601,7 +601,7 @@ unsafe impl<'w, 's, D: ReadOnlyQueryData + 'static, F: QueryFilter + 'static> Re /// ``` /// # use bevy_ecs::prelude::*; /// # -/// # #[derive(Event, BufferedEvent)] +/// # #[derive(BufferedEvent)] /// # struct MyEvent; /// # impl MyEvent { /// # pub fn new() -> Self { Self } @@ -2853,7 +2853,7 @@ impl Display for SystemParamValidationError { #[cfg(test)] mod tests { use super::*; - use crate::{event::Event, system::assert_is_system}; + use crate::system::assert_is_system; use core::cell::RefCell; // Compile test for https://github.com/bevyengine/bevy/pull/2838. @@ -3103,7 +3103,7 @@ mod tests { fn missing_event_error() { use crate::prelude::{BufferedEvent, EventReader}; - #[derive(Event, BufferedEvent)] + #[derive(BufferedEvent)] pub struct MissingEvent; let mut schedule = crate::schedule::Schedule::default(); diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 182c03e7fb..ac95018f89 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -19,6 +19,7 @@ use crate::{ query::{Access, DebugCheckedUnwrap, ReadOnlyQueryData, ReleaseStateQueryData}, relationship::RelationshipHookMode, resource::Resource, + storage::{SparseSets, Table}, system::IntoObserverSystem, world::{error::EntityComponentError, unsafe_world_cell::UnsafeEntityCell, Mut, Ref, World}, }; @@ -2270,6 +2271,27 @@ impl<'w> EntityWorldMut<'w> { /// entity has been despawned while this `EntityWorldMut` is still alive. #[track_caller] pub fn remove_by_ids(&mut self, component_ids: &[ComponentId]) -> &mut Self { + self.remove_by_ids_with_caller( + component_ids, + MaybeLocation::caller(), + RelationshipHookMode::Run, + BundleRemover::empty_pre_remove, + ) + } + + #[inline] + pub(crate) fn remove_by_ids_with_caller( + &mut self, + component_ids: &[ComponentId], + caller: MaybeLocation, + relationship_hook_mode: RelationshipHookMode, + pre_remove: impl FnOnce( + &mut SparseSets, + Option<&mut Table>, + &Components, + &[ComponentId], + ) -> (bool, T), + ) -> &mut Self { let location = self.location(); let components = &mut self.world.components; @@ -2285,16 +2307,9 @@ impl<'w> EntityWorldMut<'w> { }) else { return self; }; + remover.relationship_hook_mode = relationship_hook_mode; // SAFETY: The remover archetype came from the passed location and the removal can not fail. - let new_location = unsafe { - remover.remove( - self.entity, - location, - MaybeLocation::caller(), - BundleRemover::empty_pre_remove, - ) - } - .0; + let new_location = unsafe { remover.remove(self.entity, location, caller, pre_remove) }.0; self.location = Some(new_location); self.world.flush(); diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 4aca71d64b..0dc6b6b12d 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -602,6 +602,7 @@ impl World { /// /// # See also /// + /// * [`ComponentIdFor`](crate::component::ComponentIdFor) /// * [`Components::component_id()`] /// * [`Components::get_id()`] #[inline] @@ -704,7 +705,7 @@ impl World { /// } /// ``` /// - /// ## [`EntityHashSet`](crate::entity::EntityHashMap) + /// ## [`EntityHashSet`](crate::entity::EntityHashSet) /// /// ``` /// # use bevy_ecs::{prelude::*, entity::EntityHashSet}; @@ -838,7 +839,7 @@ impl World { /// } /// ``` /// - /// ## [`EntityHashSet`](crate::entity::EntityHashMap) + /// ## [`EntityHashSet`](crate::entity::EntityHashSet) /// /// ``` /// # use bevy_ecs::{prelude::*, entity::EntityHashSet}; diff --git a/crates/bevy_feathers/src/controls/slider.rs b/crates/bevy_feathers/src/controls/slider.rs index 228801b85c..632d0f2cb6 100644 --- a/crates/bevy_feathers/src/controls/slider.rs +++ b/crates/bevy_feathers/src/controls/slider.rs @@ -99,7 +99,7 @@ pub fn slider(props: SliderProps, overrides: B) -> impl Bundle { ColorStop::new(Color::NONE, Val::Percent(50.)), ColorStop::new(Color::NONE, Val::Percent(100.)), ], - color_space: InterpolationColorSpace::Srgb, + color_space: InterpolationColorSpace::Srgba, })]), overrides, children![( diff --git a/crates/bevy_image/src/ktx2.rs b/crates/bevy_image/src/ktx2.rs index 61304c2145..be9f8ecc55 100644 --- a/crates/bevy_image/src/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -58,7 +58,7 @@ pub fn ktx2_buffer_to_image( })?; levels.push(decompressed); } - #[cfg(feature = "zstd_rust")] + #[cfg(all(feature = "zstd_rust", not(feature = "zstd_c")))] SupercompressionScheme::Zstandard => { let mut cursor = std::io::Cursor::new(level.data); let mut decoder = ruzstd::decoding::StreamingDecoder::new(&mut cursor) @@ -71,7 +71,7 @@ pub fn ktx2_buffer_to_image( })?; levels.push(decompressed); } - #[cfg(all(feature = "zstd_c", not(feature = "zstd_rust")))] + #[cfg(feature = "zstd_c")] SupercompressionScheme::Zstandard => { levels.push(zstd::decode_all(level.data).map_err(|err| { TextureError::SuperDecompressionError(format!( diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index aaef46b3e8..88ae462284 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::{BufferedEvent, Event, EventReader, EventWriter}, + event::{BufferedEvent, 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, BufferedEvent, Debug, Clone, PartialEq, From)] +#[derive(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, BufferedEvent, Debug, Clone, PartialEq, From)] +#[derive(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, BufferedEvent, Debug, Copy, Clone, PartialEq)] +#[derive(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, BufferedEvent, Debug, Copy, Clone, PartialEq)] +#[derive(BufferedEvent, Debug, Copy, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -145,7 +145,7 @@ impl RawGamepadAxisChangedEvent { /// A [`Gamepad`] connection event. Created when a connection to a gamepad /// is established and when a gamepad is disconnected. -#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] +#[derive(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, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(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, BufferedEvent, Debug, Clone, Copy, PartialEq)] +#[derive(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, BufferedEvent, Debug, Clone, Copy, PartialEq)] +#[derive(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, BufferedEvent, Clone)] +#[derive(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 9daa21d525..8b4074c858 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::{BufferedEvent, Event}; +use bevy_ecs::event::BufferedEvent; 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, BufferedEvent, Debug, Clone, Copy, PartialEq)] +#[derive(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, BufferedEvent, Debug, Clone, Copy, PartialEq)] +#[derive(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, BufferedEvent, Debug, Clone, Copy, PartialEq)] +#[derive(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, BufferedEvent, Debug, Clone, Copy, PartialEq)] +#[derive(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 70efe18a84..4b7c2edc42 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::{BufferedEvent, Event, EventReader}, + event::{BufferedEvent, EventReader}, system::ResMut, }; @@ -95,7 +95,7 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize}; /// 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)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq, Hash)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -144,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, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(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( diff --git a/crates/bevy_input/src/mouse.rs b/crates/bevy_input/src/mouse.rs index e6b52bf51d..129a837046 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::{BufferedEvent, Event, EventReader}, + event::{BufferedEvent, 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, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(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, BufferedEvent, Debug, Clone, Copy, PartialEq)] +#[derive(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, BufferedEvent, Debug, Clone, Copy, PartialEq)] +#[derive(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 df1cf3764f..d9d39d87a5 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::{BufferedEvent, Event, EventReader}, + event::{BufferedEvent, 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, BufferedEvent, Debug, Clone, Copy, PartialEq)] +#[derive(BufferedEvent, Debug, Clone, Copy, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_light/src/lib.rs b/crates/bevy_light/src/lib.rs index 655171da63..8a8fe5ce2c 100644 --- a/crates/bevy_light/src/lib.rs +++ b/crates/bevy_light/src/lib.rs @@ -201,8 +201,8 @@ impl Plugin for LightPlugin { pub type WithLight = Or<(With, With, With)>; /// Add this component to make a [`Mesh3d`] not cast shadows. -#[derive(Debug, Component, Reflect, Default)] -#[reflect(Component, Default, Debug)] +#[derive(Debug, Component, Reflect, Default, Clone, PartialEq)] +#[reflect(Component, Default, Debug, Clone, PartialEq)] pub struct NotShadowCaster; /// Add this component to make a [`Mesh3d`] not receive shadows. /// diff --git a/crates/bevy_math/src/sampling/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs index c17bc6fa76..4b245eb8ff 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_math/src/sampling/shape_sampling.rs @@ -224,6 +224,26 @@ impl ShapeSample for Annulus { } } +impl ShapeSample for Rhombus { + type Output = Vec2; + + fn sample_interior(&self, rng: &mut R) -> Vec2 { + let x: f32 = rng.gen_range(0.0..=1.0); + let y: f32 = rng.gen_range(0.0..=1.0); + + let unit_p = Vec2::NEG_X + x * Vec2::ONE + Vec2::new(y, -y); + unit_p * self.half_diagonals + } + + fn sample_boundary(&self, rng: &mut R) -> Vec2 { + let x: f32 = rng.gen_range(-1.0..=1.0); + let y_sign = if rng.r#gen() { -1.0 } else { 1.0 }; + + let y = (1.0 - ops::abs(x)) * y_sign; + Vec2::new(x, y) * self.half_diagonals + } +} + impl ShapeSample for Rectangle { type Output = Vec2; diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index 96569b2861..3c303331ff 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -7,7 +7,7 @@ use crate::{ }; use crate::{DistanceFog, MeshPipelineKey, ViewFogUniformOffset, ViewLightsUniformOffset}; use bevy_app::prelude::*; -use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, deferred::{ @@ -19,6 +19,7 @@ use bevy_core_pipeline::{ use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_image::BevyDefault as _; use bevy_light::{EnvironmentMapLight, ShadowFilteringMethod}; +use bevy_render::RenderStartup; use bevy_render::{ extract_component::{ ComponentUniforms, ExtractComponent, ExtractComponentPlugin, UniformComponentPlugin, @@ -104,6 +105,7 @@ impl Plugin for DeferredPbrLightingPlugin { render_app .init_resource::>() + .add_systems(RenderStartup, init_deferred_lighting_layout) .add_systems( Render, (prepare_deferred_lighting_pipelines.in_set(RenderSystems::Prepare),), @@ -121,14 +123,6 @@ impl Plugin for DeferredPbrLightingPlugin { ), ); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - render_app.init_resource::(); - } } #[derive(Default)] @@ -394,22 +388,27 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { } } -impl FromWorld for DeferredLightingLayout { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - let layout = render_device.create_bind_group_layout( - "deferred_lighting_layout", - &BindGroupLayoutEntries::single( - ShaderStages::VERTEX_FRAGMENT, - uniform_buffer::(false), - ), - ); - Self { - mesh_pipeline: world.resource::().clone(), - bind_group_layout_2: layout, - deferred_lighting_shader: load_embedded_asset!(world, "deferred_lighting.wgsl"), - } - } +pub fn init_deferred_lighting_layout( + mut commands: Commands, + render_device: Res, + mesh_pipeline: Res, + asset_server: Res, +) { + let layout = render_device.create_bind_group_layout( + "deferred_lighting_layout", + &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + uniform_buffer::(false), + ), + ); + commands.insert_resource(DeferredLightingLayout { + mesh_pipeline: mesh_pipeline.clone(), + bind_group_layout_2: layout, + deferred_lighting_shader: load_embedded_asset!( + asset_server.as_ref(), + "deferred_lighting.wgsl" + ), + }); } pub fn insert_deferred_lighting_pass_id_component( diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index bce844bb21..3c2da1f521 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -289,9 +289,7 @@ impl Plugin for LightProbePlugin { load_shader_library!(app, "irradiance_volume.wgsl"); app.add_plugins(ExtractInstancesPlugin::::new()); - } - fn finish(&self, app: &mut App) { let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index 567bbce674..682fac09c0 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -42,8 +42,7 @@ use bevy_ecs::{ reflect::ReflectComponent, resource::Resource, schedule::IntoScheduleConfigs, - system::{Query, Res, ResMut}, - world::{FromWorld, World}, + system::{Commands, Query, Res, ResMut}, }; use bevy_image::Image; use bevy_math::{uvec2, vec4, Rect, UVec2}; @@ -57,7 +56,7 @@ use bevy_render::{ sync_world::MainEntity, texture::{FallbackImage, GpuImage}, view::ViewVisibility, - Extract, ExtractSchedule, RenderApp, + Extract, ExtractSchedule, RenderApp, RenderStartup, }; use bevy_render::{renderer::RenderDevice, sync_world::MainEntityHashMap}; use bevy_utils::default; @@ -186,17 +185,16 @@ pub struct LightmapSlotIndex(pub(crate) NonMaxU16); impl Plugin for LightmapPlugin { fn build(&self, app: &mut App) { load_shader_library!(app, "lightmap.wgsl"); - } - fn finish(&self, app: &mut App) { let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - - render_app.init_resource::().add_systems( - ExtractSchedule, - extract_lightmaps.after(MeshExtractionSystems), - ); + render_app + .add_systems(RenderStartup, init_render_lightmaps) + .add_systems( + ExtractSchedule, + extract_lightmaps.after(MeshExtractionSystems), + ); } } @@ -334,21 +332,20 @@ impl Default for Lightmap { } } -impl FromWorld for RenderLightmaps { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - let render_adapter = world.resource::(); +pub fn init_render_lightmaps( + mut commands: Commands, + render_device: Res, + render_adapter: Res, +) { + let bindless_supported = binding_arrays_are_usable(&render_device, &render_adapter); - let bindless_supported = binding_arrays_are_usable(render_device, render_adapter); - - RenderLightmaps { - render_lightmaps: default(), - slabs: vec![], - free_slabs: FixedBitSet::new(), - pending_lightmaps: default(), - bindless_supported, - } - } + commands.insert_resource(RenderLightmaps { + render_lightmaps: default(), + slabs: vec![], + free_slabs: FixedBitSet::new(), + pending_lightmaps: default(), + bindless_supported, + }); } impl RenderLightmaps { diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index 2e642dba92..3ebc89bccd 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -267,6 +267,15 @@ impl Plugin for MaterialsPlugin { .init_resource::() .init_resource::() .init_resource::() + .init_resource::>() + .init_resource::() + .init_resource::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_render_command::() + .add_systems(RenderStartup, init_material_pipeline) .add_systems( Render, ( @@ -301,21 +310,6 @@ impl Plugin for MaterialsPlugin { ); } } - - fn finish(&self, app: &mut App) { - if let Some(render_app) = app.get_sub_app_mut(RenderApp) { - render_app - .init_resource::>() - .init_resource::() - .init_resource::() - .init_resource::() - .add_render_command::() - .add_render_command::() - .add_render_command::() - .add_render_command::() - .add_render_command::(); - } - } } /// Adds the necessary ECS resources and render logic to enable rendering entities using the given [`Material`] @@ -485,12 +479,10 @@ impl SpecializedMeshPipeline for MaterialPipelineSpecializer { } } -impl FromWorld for MaterialPipeline { - fn from_world(world: &mut World) -> Self { - MaterialPipeline { - mesh_pipeline: world.resource::().clone(), - } - } +pub fn init_material_pipeline(mut commands: Commands, mesh_pipeline: Res) { + commands.insert_resource(MaterialPipeline { + mesh_pipeline: mesh_pipeline.clone(), + }); } pub type DrawMaterial = ( diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 0dda6127f0..66de219a90 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -2,13 +2,13 @@ mod prepass_bindings; use crate::{ alpha_mode_pipeline_key, binding_arrays_are_usable, buffer_layout, - collect_meshes_for_gpu_building, set_mesh_motion_vector_flags, setup_morph_and_skinning_defs, - skin, DeferredDrawFunction, DeferredFragmentShader, DeferredVertexShader, DrawMesh, - EntitySpecializationTicks, ErasedMaterialPipelineKey, Material, MaterialPipeline, - MaterialProperties, MeshLayouts, MeshPipeline, MeshPipelineKey, OpaqueRendererMethod, - PreparedMaterial, PrepassDrawFunction, PrepassFragmentShader, PrepassVertexShader, - RenderLightmaps, RenderMaterialInstances, RenderMeshInstanceFlags, RenderMeshInstances, - RenderPhaseType, SetMaterialBindGroup, SetMeshBindGroup, ShadowView, + collect_meshes_for_gpu_building, init_material_pipeline, set_mesh_motion_vector_flags, + setup_morph_and_skinning_defs, skin, DeferredDrawFunction, DeferredFragmentShader, + DeferredVertexShader, DrawMesh, EntitySpecializationTicks, ErasedMaterialPipelineKey, Material, + MaterialPipeline, MaterialProperties, MeshLayouts, MeshPipeline, MeshPipelineKey, + OpaqueRendererMethod, PreparedMaterial, PrepassDrawFunction, PrepassFragmentShader, + PrepassVertexShader, RenderLightmaps, RenderMaterialInstances, RenderMeshInstanceFlags, + RenderMeshInstances, RenderPhaseType, SetMaterialBindGroup, SetMeshBindGroup, ShadowView, }; use bevy_app::{App, Plugin, PreUpdate}; use bevy_render::{ @@ -21,11 +21,11 @@ use bevy_render::{ renderer::RenderAdapter, sync_world::RenderEntity, view::{RenderVisibilityRanges, RetainedViewEntity, VISIBILITY_RANGES_STORAGE_BUFFER_COUNT}, - ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderSystems, + ExtractSchedule, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, }; pub use prepass_bindings::*; -use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; +use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer, Handle}; use bevy_core_pipeline::{ core_3d::CORE_3D_DEPTH_FORMAT, deferred::*, prelude::Camera3d, prepass::*, }; @@ -87,22 +87,20 @@ impl Plugin for PrepassPipelinePlugin { }; render_app + .add_systems( + RenderStartup, + ( + init_prepass_pipeline.after(init_material_pipeline), + init_prepass_view_bind_group, + ) + .chain(), + ) .add_systems( Render, prepare_prepass_view_bind_group.in_set(RenderSystems::PrepareBindGroups), ) .init_resource::>(); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - render_app - .init_resource::() - .init_resource::(); - } } /// Sets up the prepasses for a material. @@ -273,78 +271,79 @@ pub struct PrepassPipeline { pub material_pipeline: MaterialPipeline, } -impl FromWorld for PrepassPipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - let render_adapter = world.resource::(); - let visibility_ranges_buffer_binding_type = render_device - .get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); +pub fn init_prepass_pipeline( + mut commands: Commands, + render_device: Res, + render_adapter: Res, + mesh_pipeline: Res, + material_pipeline: Res, + asset_server: Res, +) { + let visibility_ranges_buffer_binding_type = + render_device.get_supported_read_only_binding_type(VISIBILITY_RANGES_STORAGE_BUFFER_COUNT); - let view_layout_motion_vectors = render_device.create_bind_group_layout( - "prepass_view_layout_motion_vectors", - &BindGroupLayoutEntries::with_indices( - ShaderStages::VERTEX_FRAGMENT, + let view_layout_motion_vectors = render_device.create_bind_group_layout( + "prepass_view_layout_motion_vectors", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX_FRAGMENT, + ( + // View + (0, uniform_buffer::(true)), + // Globals + (1, uniform_buffer::(false)), + // PreviousViewUniforms + (2, uniform_buffer::(true)), + // VisibilityRanges ( - // View - (0, uniform_buffer::(true)), - // Globals - (1, uniform_buffer::(false)), - // PreviousViewUniforms - (2, uniform_buffer::(true)), - // VisibilityRanges - ( - 14, - buffer_layout( - visibility_ranges_buffer_binding_type, - false, - Some(Vec4::min_size()), - ) - .visibility(ShaderStages::VERTEX), - ), + 14, + buffer_layout( + visibility_ranges_buffer_binding_type, + false, + Some(Vec4::min_size()), + ) + .visibility(ShaderStages::VERTEX), ), ), - ); + ), + ); - let view_layout_no_motion_vectors = render_device.create_bind_group_layout( - "prepass_view_layout_no_motion_vectors", - &BindGroupLayoutEntries::with_indices( - ShaderStages::VERTEX_FRAGMENT, + let view_layout_no_motion_vectors = render_device.create_bind_group_layout( + "prepass_view_layout_no_motion_vectors", + &BindGroupLayoutEntries::with_indices( + ShaderStages::VERTEX_FRAGMENT, + ( + // View + (0, uniform_buffer::(true)), + // Globals + (1, uniform_buffer::(false)), + // VisibilityRanges ( - // View - (0, uniform_buffer::(true)), - // Globals - (1, uniform_buffer::(false)), - // VisibilityRanges - ( - 14, - buffer_layout( - visibility_ranges_buffer_binding_type, - false, - Some(Vec4::min_size()), - ) - .visibility(ShaderStages::VERTEX), - ), + 14, + buffer_layout( + visibility_ranges_buffer_binding_type, + false, + Some(Vec4::min_size()), + ) + .visibility(ShaderStages::VERTEX), ), ), - ); + ), + ); - let mesh_pipeline = world.resource::(); - - let depth_clip_control_supported = render_device - .features() - .contains(WgpuFeatures::DEPTH_CLIP_CONTROL); - PrepassPipeline { - view_layout_motion_vectors, - view_layout_no_motion_vectors, - mesh_layouts: mesh_pipeline.mesh_layouts.clone(), - default_prepass_shader: load_embedded_asset!(world, "prepass.wgsl"), - skins_use_uniform_buffers: skin::skins_use_uniform_buffers(render_device), - depth_clip_control_supported, - binding_arrays_are_usable: binding_arrays_are_usable(render_device, render_adapter), - empty_layout: render_device.create_bind_group_layout("prepass_empty_layout", &[]), - material_pipeline: world.resource::().clone(), - } - } + let depth_clip_control_supported = render_device + .features() + .contains(WgpuFeatures::DEPTH_CLIP_CONTROL); + commands.insert_resource(PrepassPipeline { + view_layout_motion_vectors, + view_layout_no_motion_vectors, + mesh_layouts: mesh_pipeline.mesh_layouts.clone(), + default_prepass_shader: load_embedded_asset!(asset_server.as_ref(), "prepass.wgsl"), + skins_use_uniform_buffers: skin::skins_use_uniform_buffers(&render_device), + depth_clip_control_supported, + binding_arrays_are_usable: binding_arrays_are_usable(&render_device, &render_adapter), + empty_layout: render_device.create_bind_group_layout("prepass_empty_layout", &[]), + material_pipeline: material_pipeline.clone(), + }); } pub struct PrepassPipelineSpecializer { @@ -702,22 +701,21 @@ pub struct PrepassViewBindGroup { pub empty_bind_group: BindGroup, } -impl FromWorld for PrepassViewBindGroup { - fn from_world(world: &mut World) -> Self { - let pipeline = world.resource::(); - - let render_device = world.resource::(); - let empty_bind_group = render_device.create_bind_group( - "prepass_view_empty_bind_group", - &pipeline.empty_layout, - &[], - ); - PrepassViewBindGroup { - motion_vectors: None, - no_motion_vectors: None, - empty_bind_group, - } - } +pub fn init_prepass_view_bind_group( + mut commands: Commands, + render_device: Res, + pipeline: Res, +) { + let empty_bind_group = render_device.create_bind_group( + "prepass_view_empty_bind_group", + &pipeline.empty_layout, + &[], + ); + commands.insert_resource(PrepassViewBindGroup { + motion_vectors: None, + no_motion_vectors: None, + empty_bind_group, + }); } pub fn prepare_prepass_view_bind_group( diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index 3ba62f1414..aaf9d0ef7d 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -66,8 +66,7 @@ struct Lights { cluster_factors: vec4, n_directional_lights: u32, spot_light_shadowmap_offset: i32, - environment_map_smallest_specular_mip_level: u32, - environment_map_intensity: f32, + ambient_light_affects_lightmapped_meshes: u32 }; struct Fog { diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index 84f7b95661..2c862958dd 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -604,21 +604,6 @@ fn apply_pbr_lighting( // Environment map light (indirect) #ifdef ENVIRONMENT_MAP - -#ifdef STANDARD_MATERIAL_ANISOTROPY - var bent_normal_lighting_input = lighting_input; - bend_normal_for_anisotropy(&bent_normal_lighting_input); - let environment_map_lighting_input = &bent_normal_lighting_input; -#else // STANDARD_MATERIAL_ANISOTROPY - let environment_map_lighting_input = &lighting_input; -#endif // STANDARD_MATERIAL_ANISOTROPY - - let environment_light = environment_map::environment_map_light( - environment_map_lighting_input, - &clusterable_object_index_ranges, - found_diffuse_indirect, - ); - // If screen space reflections are going to be used for this material, don't // accumulate environment map light yet. The SSR shader will do it. #ifdef SCREEN_SPACE_REFLECTIONS @@ -627,22 +612,38 @@ fn apply_pbr_lighting( #else // SCREEN_SPACE_REFLECTIONS let use_ssr = false; #endif // SCREEN_SPACE_REFLECTIONS - + if (!use_ssr) { +#ifdef STANDARD_MATERIAL_ANISOTROPY + var bent_normal_lighting_input = lighting_input; + bend_normal_for_anisotropy(&bent_normal_lighting_input); + let environment_map_lighting_input = &bent_normal_lighting_input; +#else // STANDARD_MATERIAL_ANISOTROPY + let environment_map_lighting_input = &lighting_input; +#endif // STANDARD_MATERIAL_ANISOTROPY + let environment_light = environment_map::environment_map_light( - &lighting_input, + environment_map_lighting_input, &clusterable_object_index_ranges, - found_diffuse_indirect + found_diffuse_indirect, ); indirect_light += environment_light.diffuse * diffuse_occlusion + environment_light.specular * specular_occlusion; } - #endif // ENVIRONMENT_MAP // Ambient light (indirect) - indirect_light += ambient::ambient_light(in.world_position, in.N, in.V, NdotV, diffuse_color, F0, perceptual_roughness, diffuse_occlusion); + // If we are lightmapped, disable the ambient contribution if requested. + // This is to avoid double-counting ambient light. (It might be part of the lightmap) +#ifdef LIGHTMAP + let enable_ambient = view_bindings::lights.ambient_light_affects_lightmapped_meshes != 0u; +#else // LIGHTMAP + let enable_ambient = true; +#endif // LIGHTMAP + if (enable_ambient) { + indirect_light += ambient::ambient_light(in.world_position, in.N, in.V, NdotV, diffuse_color, F0, perceptual_roughness, diffuse_occlusion); + } // we'll use the specular component of the transmitted environment // light in the call to `specular_transmissive_light()` below diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index e28412d7bd..0b1b21020e 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -46,11 +46,11 @@ use bevy_render::{ render_graph::{RenderGraphExt, ViewNodeRunner}, render_resource::SpecializedRenderPipelines, sync_component::SyncComponentPlugin, - ExtractSchedule, Render, RenderApp, RenderSystems, + ExtractSchedule, Render, RenderApp, RenderStartup, RenderSystems, }; use render::{VolumetricFogNode, VolumetricFogPipeline, VolumetricFogUniformBuffer}; -use crate::graph::NodePbr; +use crate::{graph::NodePbr, volumetric_fog::render::init_volumetric_fog_pipeline}; pub mod render; @@ -84,6 +84,7 @@ impl Plugin for VolumetricFogPlugin { }) .init_resource::>() .init_resource::() + .add_systems(RenderStartup, init_volumetric_fog_pipeline) .add_systems(ExtractSchedule, render::extract_volumetric_fog) .add_systems( Render, @@ -94,16 +95,7 @@ impl Plugin for VolumetricFogPlugin { .in_set(RenderSystems::Prepare) .before(prepare_core_3d_depth_textures), ), - ); - } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - - render_app - .init_resource::() + ) .add_render_graph_node::>( Core3d, NodePbr::VolumetricFog, diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index f24550a456..a49e9b62a4 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -2,7 +2,7 @@ use core::array; -use bevy_asset::{load_embedded_asset, AssetId, Handle}; +use bevy_asset::{load_embedded_asset, AssetId, AssetServer, Handle}; use bevy_color::ColorToComponents as _; use bevy_core_pipeline::{ core_3d::Camera3d, @@ -15,7 +15,7 @@ use bevy_ecs::{ query::{Has, QueryItem, With}, resource::Resource, system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut}, - world::{FromWorld, World}, + world::World, }; use bevy_image::{BevyDefault, Image}; use bevy_math::{vec4, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles as _}; @@ -201,61 +201,61 @@ pub struct ViewFogVolume { #[derive(Resource, Default, Deref, DerefMut)] pub struct VolumetricFogUniformBuffer(pub DynamicUniformBuffer); -impl FromWorld for VolumetricFogPipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - let mesh_view_layouts = world.resource::(); +pub fn init_volumetric_fog_pipeline( + mut commands: Commands, + render_device: Res, + mesh_view_layouts: Res, + asset_server: Res, +) { + // Create the bind group layout entries common to all bind group + // layouts. + let base_bind_group_layout_entries = &BindGroupLayoutEntries::single( + ShaderStages::VERTEX_FRAGMENT, + // `volumetric_fog` + uniform_buffer::(true), + ); - // Create the bind group layout entries common to all bind group - // layouts. - let base_bind_group_layout_entries = &BindGroupLayoutEntries::single( - ShaderStages::VERTEX_FRAGMENT, - // `volumetric_fog` - uniform_buffer::(true), - ); + // For every combination of `VolumetricFogBindGroupLayoutKey` bits, + // create a bind group layout. + let bind_group_layouts = array::from_fn(|bits| { + let flags = VolumetricFogBindGroupLayoutKey::from_bits_retain(bits as u8); - // For every combination of `VolumetricFogBindGroupLayoutKey` bits, - // create a bind group layout. - let bind_group_layouts = array::from_fn(|bits| { - let flags = VolumetricFogBindGroupLayoutKey::from_bits_retain(bits as u8); + let mut bind_group_layout_entries = base_bind_group_layout_entries.to_vec(); - let mut bind_group_layout_entries = base_bind_group_layout_entries.to_vec(); + // `depth_texture` + bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices( + ShaderStages::FRAGMENT, + (( + 1, + if flags.contains(VolumetricFogBindGroupLayoutKey::MULTISAMPLED) { + texture_depth_2d_multisampled() + } else { + texture_depth_2d() + }, + ),), + )); - // `depth_texture` + // `density_texture` and `density_sampler` + if flags.contains(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE) { bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices( ShaderStages::FRAGMENT, - (( - 1, - if flags.contains(VolumetricFogBindGroupLayoutKey::MULTISAMPLED) { - texture_depth_2d_multisampled() - } else { - texture_depth_2d() - }, - ),), + ( + (2, texture_3d(TextureSampleType::Float { filterable: true })), + (3, sampler(SamplerBindingType::Filtering)), + ), )); - - // `density_texture` and `density_sampler` - if flags.contains(VolumetricFogBindGroupLayoutKey::DENSITY_TEXTURE) { - bind_group_layout_entries.extend_from_slice(&BindGroupLayoutEntries::with_indices( - ShaderStages::FRAGMENT, - ( - (2, texture_3d(TextureSampleType::Float { filterable: true })), - (3, sampler(SamplerBindingType::Filtering)), - ), - )); - } - - // Create the bind group layout. - let description = flags.bind_group_layout_description(); - render_device.create_bind_group_layout(&*description, &bind_group_layout_entries) - }); - - VolumetricFogPipeline { - mesh_view_layouts: mesh_view_layouts.clone(), - volumetric_view_bind_group_layouts: bind_group_layouts, - shader: load_embedded_asset!(world, "volumetric_fog.wgsl"), } - } + + // Create the bind group layout. + let description = flags.bind_group_layout_description(); + render_device.create_bind_group_layout(&*description, &bind_group_layout_entries) + }); + + commands.insert_resource(VolumetricFogPipeline { + mesh_view_layouts: mesh_view_layouts.clone(), + volumetric_view_bind_group_layouts: bind_group_layouts, + shader: load_embedded_asset!(asset_server.as_ref(), "volumetric_fog.wgsl"), + }); } /// Extracts [`VolumetricFog`], [`FogVolume`], and [`VolumetricLight`]s diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 9cf3bc08dd..ad280e054f 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -6,7 +6,7 @@ use crate::{ use bevy_app::{App, Plugin, PostUpdate, Startup, Update}; use bevy_asset::{ embedded_asset, load_embedded_asset, prelude::AssetChanged, AsAssetId, Asset, AssetApp, - AssetEventSystems, AssetId, Assets, Handle, UntypedAssetId, + AssetEventSystems, AssetId, AssetServer, Assets, Handle, UntypedAssetId, }; use bevy_color::{Color, ColorToComponents}; use bevy_core_pipeline::core_3d::{ @@ -25,7 +25,6 @@ use bevy_platform::{ hash::FixedHasher, }; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -use bevy_render::camera::extract_cameras; use bevy_render::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, camera::ExtractedCamera, @@ -54,6 +53,7 @@ use bevy_render::{ }, Extract, Render, RenderApp, RenderDebugFlags, RenderSystems, }; +use bevy_render::{camera::extract_cameras, RenderStartup}; use core::{hash::Hash, ops::Range}; use tracing::error; @@ -132,6 +132,7 @@ impl Plugin for WireframePlugin { Node3d::PostProcessing, ), ) + .add_systems(RenderStartup, init_wireframe_3d_pipeline) .add_systems( ExtractSchedule, ( @@ -153,13 +154,6 @@ impl Plugin for WireframePlugin { ), ); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - render_app.init_resource::(); - } } /// Enables wireframe rendering for any entity it is attached to. @@ -331,13 +325,15 @@ pub struct Wireframe3dPipeline { shader: Handle, } -impl FromWorld for Wireframe3dPipeline { - fn from_world(render_world: &mut World) -> Self { - Wireframe3dPipeline { - mesh_pipeline: render_world.resource::().clone(), - shader: load_embedded_asset!(render_world, "render/wireframe.wgsl"), - } - } +pub fn init_wireframe_3d_pipeline( + mut commands: Commands, + mesh_pipeline: Res, + asset_server: Res, +) { + commands.insert_resource(Wireframe3dPipeline { + mesh_pipeline: mesh_pipeline.clone(), + shader: load_embedded_asset!(asset_server.as_ref(), "render/wireframe.wgsl"), + }); } impl SpecializedMeshPipeline for Wireframe3dPipeline { diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 28693314d9..e715d5fab2 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, BufferedEvent, Debug, Clone, Reflect)] +#[derive(BufferedEvent, Debug, Clone, Reflect)] #[reflect(Debug, Clone)] pub struct PointerHits { /// The pointer associated with this hit test. diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 026d2a1953..05171914f9 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -48,7 +48,7 @@ //! # use bevy_ecs::prelude::*; //! # use bevy_transform::prelude::*; //! # use bevy_picking::prelude::*; -//! # #[derive(Event, BufferedEvent)] +//! # #[derive(BufferedEvent)] //! # struct Greeting; //! fn setup(mut commands: Commands) { //! commands.spawn(Transform::default()) diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index 95a44ab3ec..46aef8dc3c 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, BufferedEvent, Debug, Clone, Reflect)] +#[derive(BufferedEvent, Debug, Clone, Reflect)] #[reflect(Clone)] pub struct PointerInput { /// The id of the pointer. diff --git a/crates/bevy_reflect/derive/src/derive_data.rs b/crates/bevy_reflect/derive/src/derive_data.rs index 3f6532a408..9e3e169bc2 100644 --- a/crates/bevy_reflect/derive/src/derive_data.rs +++ b/crates/bevy_reflect/derive/src/derive_data.rs @@ -605,13 +605,11 @@ impl<'a> ReflectStruct<'a> { } /// Get a collection of types which are exposed to the reflection API - pub fn active_types(&self) -> Vec { - // Collect via `IndexSet` to eliminate duplicate types. + pub fn active_types(&self) -> IndexSet { + // Collect into an `IndexSet` to eliminate duplicate types. self.active_fields() .map(|field| field.reflected_type().clone()) .collect::>() - .into_iter() - .collect::>() } /// Get an iterator of fields which are exposed to the reflection API. @@ -634,7 +632,7 @@ impl<'a> ReflectStruct<'a> { } pub fn where_clause_options(&self) -> WhereClauseOptions { - WhereClauseOptions::new_with_fields(self.meta(), self.active_types().into_boxed_slice()) + WhereClauseOptions::new_with_types(self.meta(), self.active_types()) } /// Generates a `TokenStream` for `TypeInfo::Struct` or `TypeInfo::TupleStruct` construction. @@ -841,13 +839,11 @@ impl<'a> ReflectEnum<'a> { } /// Get a collection of types which are exposed to the reflection API - pub fn active_types(&self) -> Vec { - // Collect via `IndexSet` to eliminate duplicate types. + pub fn active_types(&self) -> IndexSet { + // Collect into an `IndexSet` to eliminate duplicate types. self.active_fields() .map(|field| field.reflected_type().clone()) .collect::>() - .into_iter() - .collect::>() } /// Get an iterator of fields which are exposed to the reflection API @@ -856,7 +852,7 @@ impl<'a> ReflectEnum<'a> { } pub fn where_clause_options(&self) -> WhereClauseOptions { - WhereClauseOptions::new_with_fields(self.meta(), self.active_types().into_boxed_slice()) + WhereClauseOptions::new_with_types(self.meta(), self.active_types()) } /// Returns the `GetTypeRegistration` impl as a `TokenStream`. @@ -869,7 +865,7 @@ impl<'a> ReflectEnum<'a> { crate::registration::impl_get_type_registration( where_clause_options, None, - Some(self.active_fields().map(StructField::reflected_type)), + Some(self.active_types().iter()), ) } diff --git a/crates/bevy_reflect/derive/src/impls/func/from_arg.rs b/crates/bevy_reflect/derive/src/impls/func/from_arg.rs index 2220a704ea..1e92900fc7 100644 --- a/crates/bevy_reflect/derive/src/impls/func/from_arg.rs +++ b/crates/bevy_reflect/derive/src/impls/func/from_arg.rs @@ -13,23 +13,11 @@ pub(crate) fn impl_from_arg(where_clause_options: &WhereClauseOptions) -> proc_m quote! { impl #impl_generics #bevy_reflect::func::args::FromArg for #type_path #ty_generics #where_reflect_clause { type This<'from_arg> = #type_path #ty_generics; - fn from_arg(arg: #bevy_reflect::func::args::Arg) -> #FQResult, #bevy_reflect::func::args::ArgError> { + fn from_arg(arg: #bevy_reflect::func::args::Arg) -> + #FQResult, #bevy_reflect::func::args::ArgError> + { arg.take_owned() } } - - impl #impl_generics #bevy_reflect::func::args::FromArg for &'static #type_path #ty_generics #where_reflect_clause { - type This<'from_arg> = &'from_arg #type_path #ty_generics; - fn from_arg(arg: #bevy_reflect::func::args::Arg) -> #FQResult, #bevy_reflect::func::args::ArgError> { - arg.take_ref() - } - } - - impl #impl_generics #bevy_reflect::func::args::FromArg for &'static mut #type_path #ty_generics #where_reflect_clause { - type This<'from_arg> = &'from_arg mut #type_path #ty_generics; - fn from_arg(arg: #bevy_reflect::func::args::Arg) -> #FQResult, #bevy_reflect::func::args::ArgError> { - arg.take_mut() - } - } } } diff --git a/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs b/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs index abdfb803ed..bcf4ee2b2f 100644 --- a/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs +++ b/crates/bevy_reflect/derive/src/impls/func/get_ownership.rs @@ -17,17 +17,5 @@ pub(crate) fn impl_get_ownership( #bevy_reflect::func::args::Ownership::Owned } } - - impl #impl_generics #bevy_reflect::func::args::GetOwnership for &'_ #type_path #ty_generics #where_reflect_clause { - fn ownership() -> #bevy_reflect::func::args::Ownership { - #bevy_reflect::func::args::Ownership::Ref - } - } - - impl #impl_generics #bevy_reflect::func::args::GetOwnership for &'_ mut #type_path #ty_generics #where_reflect_clause { - fn ownership() -> #bevy_reflect::func::args::Ownership { - #bevy_reflect::func::args::Ownership::Mut - } - } } } diff --git a/crates/bevy_reflect/derive/src/impls/func/into_return.rs b/crates/bevy_reflect/derive/src/impls/func/into_return.rs index 221028a99e..73386f9f8a 100644 --- a/crates/bevy_reflect/derive/src/impls/func/into_return.rs +++ b/crates/bevy_reflect/derive/src/impls/func/into_return.rs @@ -13,21 +13,11 @@ pub(crate) fn impl_into_return( quote! { impl #impl_generics #bevy_reflect::func::IntoReturn for #type_path #ty_generics #where_reflect_clause { - fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return { + fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> + where Self: 'into_return + { #bevy_reflect::func::Return::Owned(#bevy_reflect::__macro_exports::alloc_utils::Box::new(self)) } } - - impl #impl_generics #bevy_reflect::func::IntoReturn for &#type_path #ty_generics #where_reflect_clause { - fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return { - #bevy_reflect::func::Return::Ref(self) - } - } - - impl #impl_generics #bevy_reflect::func::IntoReturn for &mut #type_path #ty_generics #where_reflect_clause { - fn into_return<'into_return>(self) -> #bevy_reflect::func::Return<'into_return> where Self: 'into_return { - #bevy_reflect::func::Return::Mut(self) - } - } } } diff --git a/crates/bevy_reflect/derive/src/where_clause_options.rs b/crates/bevy_reflect/derive/src/where_clause_options.rs index d2d3b15a44..ea56679eb9 100644 --- a/crates/bevy_reflect/derive/src/where_clause_options.rs +++ b/crates/bevy_reflect/derive/src/where_clause_options.rs @@ -1,5 +1,6 @@ use crate::derive_data::ReflectMeta; use bevy_macro_utils::fq_std::{FQAny, FQSend, FQSync}; +use indexmap::IndexSet; use proc_macro2::{TokenStream, TokenTree}; use quote::{quote, ToTokens}; use syn::{punctuated::Punctuated, Ident, Token, Type, WhereClause}; @@ -7,22 +8,19 @@ use syn::{punctuated::Punctuated, Ident, Token, Type, WhereClause}; /// Options defining how to extend the `where` clause for reflection. pub(crate) struct WhereClauseOptions<'a, 'b> { meta: &'a ReflectMeta<'b>, - active_fields: Box<[Type]>, + active_types: IndexSet, } impl<'a, 'b> WhereClauseOptions<'a, 'b> { pub fn new(meta: &'a ReflectMeta<'b>) -> Self { Self { meta, - active_fields: Box::new([]), + active_types: IndexSet::new(), } } - pub fn new_with_fields(meta: &'a ReflectMeta<'b>, active_fields: Box<[Type]>) -> Self { - Self { - meta, - active_fields, - } + pub fn new_with_types(meta: &'a ReflectMeta<'b>, active_types: IndexSet) -> Self { + Self { meta, active_types } } pub fn meta(&self) -> &'a ReflectMeta<'b> { @@ -207,7 +205,7 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> { false } - Some(self.active_fields.iter().filter_map(move |ty| { + Some(self.active_types.iter().filter_map(move |ty| { // Field type bounds are only required if `ty` is generic. How to determine that? // Search `ty`s token stream for identifiers that match the identifiers from the // function's type params. E.g. if `T` and `U` are the type param identifiers and diff --git a/crates/bevy_reflect/src/func/args/from_arg.rs b/crates/bevy_reflect/src/func/args/from_arg.rs index 88d04aefe7..c49d065169 100644 --- a/crates/bevy_reflect/src/func/args/from_arg.rs +++ b/crates/bevy_reflect/src/func/args/from_arg.rs @@ -1,14 +1,17 @@ use crate::func::args::{Arg, ArgError}; +use crate::{Reflect, TypePath}; /// A trait for types that can be created from an [`Arg`]. /// /// This trait exists so that types can be automatically converted into an [`Arg`] -/// so they can be put into an [`ArgList`] and passed to a [`DynamicFunction`] or [`DynamicFunctionMut`]. +/// so they can be put into an [`ArgList`] and passed to a [`DynamicFunction`] or +/// [`DynamicFunctionMut`]. /// /// This trait is used instead of a blanket [`From`] implementation due to coherence issues: /// we can't implement `From` for both `T` and `&T`/`&mut T`. /// -/// This trait is automatically implemented when using the `Reflect` [derive macro]. +/// This trait is automatically implemented for non-reference types when using the `Reflect` +/// [derive macro]. Blanket impls cover `&T` and `&mut T`. /// /// [`ArgList`]: crate::func::args::ArgList /// [`DynamicFunction`]: crate::func::DynamicFunction @@ -29,6 +32,22 @@ pub trait FromArg { fn from_arg(arg: Arg) -> Result, ArgError>; } +// Blanket impl. +impl FromArg for &'static T { + type This<'a> = &'a T; + fn from_arg(arg: Arg) -> Result, ArgError> { + arg.take_ref() + } +} + +// Blanket impl. +impl FromArg for &'static mut T { + type This<'a> = &'a mut T; + fn from_arg(arg: Arg) -> Result, ArgError> { + arg.take_mut() + } +} + /// Implements the [`FromArg`] trait for the given type. /// /// This will implement it for `$ty`, `&$ty`, and `&mut $ty`. @@ -40,18 +59,13 @@ macro_rules! impl_from_arg { ( $ty: ty $(; - < - $($T: ident $(: $T1: tt $(+ $T2: tt)*)?),* - > + < $($T: ident $(: $T1: tt $(+ $T2: tt)*)?),* > )? $( - [ - $(const $N: ident : $size: ident),* - ] + [ $(const $N: ident : $size: ident),* ] )? $( - where - $($U: ty $(: $U1: tt $(+ $U2: tt)*)?),* + where $($U: ty $(: $U1: tt $(+ $U2: tt)*)?),* )? ) => { impl < @@ -59,45 +73,16 @@ macro_rules! impl_from_arg { $(, $(const $N : $size),*)? > $crate::func::args::FromArg for $ty $( - where - $($U $(: $U1 $(+ $U2)*)?),* + where $($U $(: $U1 $(+ $U2)*)?),* )? { type This<'from_arg> = $ty; - fn from_arg(arg: $crate::func::args::Arg) -> Result, $crate::func::args::ArgError> { + fn from_arg(arg: $crate::func::args::Arg) -> + Result, $crate::func::args::ArgError> + { arg.take_owned() } } - - impl < - $($($T $(: $T1 $(+ $T2)*)?),*)? - $(, $(const $N : $size),*)? - > $crate::func::args::FromArg for &'static $ty - $( - where - $($U $(: $U1 $(+ $U2)*)?),* - )? - { - type This<'from_arg> = &'from_arg $ty; - fn from_arg(arg: $crate::func::args::Arg) -> Result, $crate::func::args::ArgError> { - arg.take_ref() - } - } - - impl < - $($($T $(: $T1 $(+ $T2)*)?),*)? - $(, $(const $N : $size),*)? - > $crate::func::args::FromArg for &'static mut $ty - $( - where - $($U $(: $U1 $(+ $U2)*)?),* - )? - { - type This<'from_arg> = &'from_arg mut $ty; - fn from_arg(arg: $crate::func::args::Arg) -> Result, $crate::func::args::ArgError> { - arg.take_mut() - } - } }; } diff --git a/crates/bevy_reflect/src/func/args/ownership.rs b/crates/bevy_reflect/src/func/args/ownership.rs index b9395c742f..449cc67cd8 100644 --- a/crates/bevy_reflect/src/func/args/ownership.rs +++ b/crates/bevy_reflect/src/func/args/ownership.rs @@ -1,21 +1,5 @@ use core::fmt::{Display, Formatter}; -/// A trait for getting the ownership of a type. -/// -/// This trait exists so that [`TypedFunction`] can automatically generate -/// [`FunctionInfo`] containing the proper [`Ownership`] for its [argument] types. -/// -/// This trait is automatically implemented when using the `Reflect` [derive macro]. -/// -/// [`TypedFunction`]: crate::func::TypedFunction -/// [`FunctionInfo`]: crate::func::FunctionInfo -/// [argument]: crate::func::args::Arg -/// [derive macro]: derive@crate::Reflect -pub trait GetOwnership { - /// Returns the ownership of [`Self`]. - fn ownership() -> Ownership; -} - /// The ownership of a type. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Ownership { @@ -37,6 +21,39 @@ impl Display for Ownership { } } +/// A trait for getting the ownership of a type. +/// +/// This trait exists so that [`TypedFunction`] can automatically generate +/// [`FunctionInfo`] containing the proper [`Ownership`] for its [argument] types. +/// +/// This trait is automatically implemented for non-reference types when using the `Reflect` +/// [derive macro]. Blanket impls cover `&T` and `&mut T`. +/// +/// [`TypedFunction`]: crate::func::TypedFunction +/// [`FunctionInfo`]: crate::func::FunctionInfo +/// [argument]: crate::func::args::Arg +/// [derive macro]: derive@crate::Reflect +pub trait GetOwnership { + /// Returns the ownership of [`Self`]. + fn ownership() -> Ownership { + Ownership::Owned + } +} + +// Blanket impl. +impl GetOwnership for &'_ T { + fn ownership() -> Ownership { + Ownership::Ref + } +} + +// Blanket impl. +impl GetOwnership for &'_ mut T { + fn ownership() -> Ownership { + Ownership::Mut + } +} + /// Implements the [`GetOwnership`] trait for the given type. /// /// This will implement it for `$ty`, `&$ty`, and `&mut $ty`. @@ -48,18 +65,13 @@ macro_rules! impl_get_ownership { ( $ty: ty $(; - < - $($T: ident $(: $T1: tt $(+ $T2: tt)*)?),* - > + < $($T: ident $(: $T1: tt $(+ $T2: tt)*)?),* > )? $( - [ - $(const $N: ident : $size: ident),* - ] + [ $(const $N: ident : $size: ident),* ] )? $( - where - $($U: ty $(: $U1: tt $(+ $U2: tt)*)?),* + where $($U: ty $(: $U1: tt $(+ $U2: tt)*)?),* )? ) => { impl < @@ -67,42 +79,9 @@ macro_rules! impl_get_ownership { $(, $(const $N : $size),*)? > $crate::func::args::GetOwnership for $ty $( - where - $($U $(: $U1 $(+ $U2)*)?),* + where $($U $(: $U1 $(+ $U2)*)?),* )? - { - fn ownership() -> $crate::func::args::Ownership { - $crate::func::args::Ownership::Owned - } - } - - impl < - $($($T $(: $T1 $(+ $T2)*)?),*)? - $(, $(const $N : $size),*)? - > $crate::func::args::GetOwnership for &'_ $ty - $( - where - $($U $(: $U1 $(+ $U2)*)?),* - )? - { - fn ownership() -> $crate::func::args::Ownership { - $crate::func::args::Ownership::Ref - } - } - - impl < - $($($T $(: $T1 $(+ $T2)*)?),*)? - $(, $(const $N : $size),*)? - > $crate::func::args::GetOwnership for &'_ mut $ty - $( - where - $($U $(: $U1 $(+ $U2)*)?),* - )? - { - fn ownership() -> $crate::func::args::Ownership { - $crate::func::args::Ownership::Mut - } - } + {} }; } diff --git a/crates/bevy_reflect/src/func/macros.rs b/crates/bevy_reflect/src/func/macros.rs index 3fb93a2230..5ceb2e270e 100644 --- a/crates/bevy_reflect/src/func/macros.rs +++ b/crates/bevy_reflect/src/func/macros.rs @@ -28,69 +28,49 @@ macro_rules! impl_function_traits { ( $ty: ty $(; - < - $($T: ident $(: $T1: tt $(+ $T2: tt)*)?),* - > + < $($T: ident $(: $T1: tt $(+ $T2: tt)*)?),* > )? $( - [ - $(const $N: ident : $size: ident),* - ] + [ $(const $N: ident : $size: ident),* ] )? $( - where - $($U: ty $(: $U1: tt $(+ $U2: tt)*)?),* + where $($U: ty $(: $U1: tt $(+ $U2: tt)*)?),* )? ) => { $crate::func::args::impl_get_ownership!( $ty $(; - < - $($T $(: $T1 $(+ $T2)*)?),* - > + < $($T $(: $T1 $(+ $T2)*)?),* > )? $( - [ - $(const $N : $size),* - ] + [ $(const $N : $size),* ] )? $( - where - $($U $(: $U1 $(+ $U2)*)?),* + where $($U $(: $U1 $(+ $U2)*)?),* )? ); $crate::func::args::impl_from_arg!( $ty $(; - < - $($T $(: $T1 $(+ $T2)*)?),* - > + < $($T $(: $T1 $(+ $T2)*)?),* > )? $( - [ - $(const $N : $size),* - ] + [ $(const $N : $size),* ] )? $( - where - $($U $(: $U1 $(+ $U2)*)?),* + where $($U $(: $U1 $(+ $U2)*)?),* )? ); $crate::func::impl_into_return!( $ty $(; - < - $($T $(: $T1 $(+ $T2)*)?),* - > + < $($T $(: $T1 $(+ $T2)*)?),* > )? $( - [ - $(const $N : $size),* - ] + [ $(const $N : $size),* ] )? $( - where - $($U $(: $U1 $(+ $U2)*)?),* + where $($U $(: $U1 $(+ $U2)*)?),* )? ); }; diff --git a/crates/bevy_reflect/src/func/return_type.rs b/crates/bevy_reflect/src/func/return_type.rs index 9abe0ef32c..06c308b6b2 100644 --- a/crates/bevy_reflect/src/func/return_type.rs +++ b/crates/bevy_reflect/src/func/return_type.rs @@ -76,7 +76,8 @@ impl<'a> Return<'a> { /// This trait is used instead of a blanket [`Into`] implementation due to coherence issues: /// we can't implement `Into` for both `T` and `&T`/`&mut T`. /// -/// This trait is automatically implemented when using the `Reflect` [derive macro]. +/// This trait is automatically implemented for non-reference types when using the `Reflect` +/// [derive macro]. Blanket impls cover `&T` and `&mut T`. /// /// [`ReflectFn`]: crate::func::ReflectFn /// [`ReflectFnMut`]: crate::func::ReflectFnMut @@ -88,6 +89,26 @@ pub trait IntoReturn { Self: 'a; } +// Blanket impl. +impl IntoReturn for &'_ T { + fn into_return<'a>(self) -> Return<'a> + where + Self: 'a, + { + Return::Ref(self) + } +} + +// Blanket impl. +impl IntoReturn for &'_ mut T { + fn into_return<'a>(self) -> Return<'a> + where + Self: 'a, + { + Return::Mut(self) + } +} + impl IntoReturn for () { fn into_return<'a>(self) -> Return<'a> { Return::unit() @@ -105,18 +126,13 @@ macro_rules! impl_into_return { ( $ty: ty $(; - < - $($T: ident $(: $T1: tt $(+ $T2: tt)*)?),* - > + < $($T: ident $(: $T1: tt $(+ $T2: tt)*)?),* > )? $( - [ - $(const $N: ident : $size: ident),* - ] + [ $(const $N: ident : $size: ident),* ] )? $( - where - $($U: ty $(: $U1: tt $(+ $U2: tt)*)?),* + where $($U: ty $(: $U1: tt $(+ $U2: tt)*)?),* )? ) => { impl < @@ -124,42 +140,15 @@ macro_rules! impl_into_return { $(, $(const $N : $size),*)? > $crate::func::IntoReturn for $ty $( - where - $($U $(: $U1 $(+ $U2)*)?),* + where $($U $(: $U1 $(+ $U2)*)?),* )? { - fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> where Self: 'into_return { + fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> + where Self: 'into_return + { $crate::func::Return::Owned(bevy_platform::prelude::Box::new(self)) } } - - impl < - $($($T $(: $T1 $(+ $T2)*)?),*)? - $(, $(const $N : $size),*)? - > $crate::func::IntoReturn for &'static $ty - $( - where - $($U $(: $U1 $(+ $U2)*)?),* - )? - { - fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> where Self: 'into_return { - $crate::func::Return::Ref(self) - } - } - - impl < - $($($T $(: $T1 $(+ $T2)*)?),*)? - $(, $(const $N : $size),*)? - > $crate::func::IntoReturn for &'static mut $ty - $( - where - $($U $(: $U1 $(+ $U2)*)?),* - )? - { - fn into_return<'into_return>(self) -> $crate::func::Return<'into_return> where Self: 'into_return { - $crate::func::Return::Mut(self) - } - } }; } diff --git a/crates/bevy_reflect/src/impls/core/panic.rs b/crates/bevy_reflect/src/impls/core/panic.rs index 75bf365422..3d1cebe53e 100644 --- a/crates/bevy_reflect/src/impls/core/panic.rs +++ b/crates/bevy_reflect/src/impls/core/panic.rs @@ -153,6 +153,3 @@ impl FromReflect for &'static Location<'static> { reflect.try_downcast_ref::().copied() } } - -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(&'static Location<'static>); diff --git a/crates/bevy_reflect/src/impls/core/primitives.rs b/crates/bevy_reflect/src/impls/core/primitives.rs index 3600f2ece5..7582559823 100644 --- a/crates/bevy_reflect/src/impls/core/primitives.rs +++ b/crates/bevy_reflect/src/impls/core/primitives.rs @@ -293,9 +293,6 @@ impl FromReflect for &'static str { } } -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(&'static str); - impl Array for [T; N] { #[inline] fn get(&self, index: usize) -> Option<&dyn PartialReflect> { diff --git a/crates/bevy_reflect/src/impls/std/path.rs b/crates/bevy_reflect/src/impls/std/path.rs index a73ee44141..a669068ae3 100644 --- a/crates/bevy_reflect/src/impls/std/path.rs +++ b/crates/bevy_reflect/src/impls/std/path.rs @@ -160,9 +160,6 @@ impl FromReflect for &'static Path { } } -#[cfg(feature = "functions")] -crate::func::macros::impl_function_traits!(&'static Path); - impl PartialReflect for Cow<'static, Path> { fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { Some(::type_info()) diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index 346762aecc..03564a80fe 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -433,6 +433,17 @@ pub fn extract_cameras( mapper: Extract>, ) { let primary_window = primary_window.iter().next(); + type ExtractedCameraComponents = ( + ExtractedCamera, + ExtractedView, + RenderVisibleEntities, + TemporalJitter, + MipBias, + RenderLayers, + Projection, + NoIndirectDrawing, + ViewUniformOffset, + ); for ( main_entity, render_entity, @@ -452,17 +463,9 @@ pub fn extract_cameras( ) in query.iter() { if !camera.is_active { - commands.entity(render_entity).remove::<( - ExtractedCamera, - ExtractedView, - RenderVisibleEntities, - TemporalJitter, - MipBias, - RenderLayers, - Projection, - NoIndirectDrawing, - ViewUniformOffset, - )>(); + commands + .entity(render_entity) + .remove::(); continue; } @@ -481,6 +484,9 @@ pub fn extract_cameras( camera.physical_target_size(), ) { if target_size.x == 0 || target_size.y == 0 { + commands + .entity(render_entity) + .remove::(); continue; } diff --git a/crates/bevy_render/src/gpu_readback.rs b/crates/bevy_render/src/gpu_readback.rs index adcb883559..a9949c908d 100644 --- a/crates/bevy_render/src/gpu_readback.rs +++ b/crates/bevy_render/src/gpu_readback.rs @@ -77,7 +77,10 @@ impl Plugin for GpuReadbackPlugin { #[derive(Component, ExtractComponent, Clone, Debug)] pub enum Readback { Texture(Handle), - Buffer(Handle), + Buffer { + buffer: Handle, + start_offset_and_size: Option<(u64, u64)>, + }, } impl Readback { @@ -86,9 +89,21 @@ impl Readback { Self::Texture(image) } - /// Create a readback component for a buffer using the given handle. + /// Create a readback component for a full buffer using the given handle. pub fn buffer(buffer: Handle) -> Self { - Self::Buffer(buffer) + Self::Buffer { + buffer, + start_offset_and_size: None, + } + } + + /// Create a readback component for a buffer range using the given handle, a start offset in bytes + /// and a number of bytes to read. + pub fn buffer_range(buffer: Handle, start_offset: u64, size: u64) -> Self { + Self::Buffer { + buffer, + start_offset_and_size: Some((start_offset, size)), + } } } @@ -193,9 +208,8 @@ enum ReadbackSource { size: Extent3d, }, Buffer { - src_start: u64, - dst_start: u64, buffer: Buffer, + start_offset_and_size: Option<(u64, u64)>, }, } @@ -266,16 +280,30 @@ fn prepare_buffers( }); } } - Readback::Buffer(buffer) => { + Readback::Buffer { + buffer, + start_offset_and_size, + } => { if let Some(ssbo) = ssbos.get(buffer) { - let size = ssbo.buffer.size(); + let full_size = ssbo.buffer.size(); + let size = start_offset_and_size + .map(|(start, size)| { + let end = start + size; + if end > full_size { + panic!( + "Tried to read past the end of the buffer (start: {start}, \ + size: {size}, buffer size: {full_size})." + ); + } + size + }) + .unwrap_or(full_size); let buffer = buffer_pool.get(&render_device, size); let (tx, rx) = async_channel::bounded(1); readbacks.requested.push(GpuReadback { entity: entity.id(), src: ReadbackSource::Buffer { - src_start: 0, - dst_start: 0, + start_offset_and_size: *start_offset_and_size, buffer: ssbo.buffer.clone(), }, buffer, @@ -307,17 +335,11 @@ pub(crate) fn submit_readback_commands(world: &World, command_encoder: &mut Comm ); } ReadbackSource::Buffer { - src_start, - dst_start, buffer, + start_offset_and_size, } => { - command_encoder.copy_buffer_to_buffer( - buffer, - *src_start, - &readback.buffer, - *dst_start, - buffer.size(), - ); + let (src_start, size) = start_offset_and_size.unwrap_or((0, buffer.size())); + command_encoder.copy_buffer_to_buffer(buffer, src_start, &readback.buffer, 0, size); } } } diff --git a/crates/bevy_solari/src/lib.rs b/crates/bevy_solari/src/lib.rs index 3847bc074a..8f07a004a6 100644 --- a/crates/bevy_solari/src/lib.rs +++ b/crates/bevy_solari/src/lib.rs @@ -4,7 +4,7 @@ //! //! See [`SolariPlugins`] for more info. //! -//! ![`bevy_solari` logo](https://raw.githubusercontent.com/bevyengine/bevy/assets/branding/bevy_solari.svg) +//! ![`bevy_solari` logo](https://raw.githubusercontent.com/bevyengine/bevy/refs/heads/main/assets/branding/bevy_solari.svg) pub mod pathtracer; pub mod realtime; pub mod scene; diff --git a/crates/bevy_state/src/state/transitions.rs b/crates/bevy_state/src/state/transitions.rs index 90b033b7fd..878d342e1d 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::{BufferedEvent, Event, EventReader, EventWriter}, + event::{BufferedEvent, EventReader, EventWriter}, schedule::{IntoScheduleConfigs, Schedule, ScheduleLabel, Schedules, SystemSet}, system::{Commands, In, ResMut}, world::World, @@ -61,7 +61,7 @@ pub struct StateTransition; /// 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, BufferedEvent)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, 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 a84490f64c..8321e8cb32 100644 --- a/crates/bevy_state/src/state_scoped_events.rs +++ b/crates/bevy_state/src/state_scoped_events.rs @@ -187,7 +187,7 @@ impl StateScopedEventsAppExt for SubApp { mod tests { use super::*; use crate::app::StatesPlugin; - use bevy_ecs::event::{BufferedEvent, Event}; + use bevy_ecs::event::BufferedEvent; use bevy_state::prelude::*; #[derive(States, Default, Clone, Hash, Eq, PartialEq, Debug)] @@ -197,10 +197,10 @@ mod tests { B, } - #[derive(Event, BufferedEvent, Debug)] + #[derive(BufferedEvent, Debug)] struct StandardEvent; - #[derive(Event, BufferedEvent, Debug)] + #[derive(BufferedEvent, Debug)] struct StateScopedEvent; #[test] diff --git a/crates/bevy_time/src/lib.rs b/crates/bevy_time/src/lib.rs index ff2e70b1db..8173b7c91b 100644 --- a/crates/bevy_time/src/lib.rs +++ b/crates/bevy_time/src/lib.rs @@ -186,8 +186,7 @@ mod tests { use bevy_app::{App, FixedUpdate, Startup, Update}; use bevy_ecs::{ event::{ - BufferedEvent, Event, EventReader, EventRegistry, EventWriter, Events, - ShouldUpdateEvents, + BufferedEvent, EventReader, EventRegistry, EventWriter, Events, ShouldUpdateEvents, }, resource::Resource, system::{Local, Res, ResMut}, @@ -196,7 +195,7 @@ mod tests { use core::time::Duration; use std::println; - #[derive(Event, BufferedEvent)] + #[derive(BufferedEvent)] struct TestEvent { sender: std::sync::mpsc::Sender, } @@ -209,7 +208,7 @@ mod tests { } } - #[derive(Event, BufferedEvent)] + #[derive(BufferedEvent)] struct DummyEvent; #[derive(Resource, Default)] diff --git a/crates/bevy_time/src/time.rs b/crates/bevy_time/src/time.rs index 8c4456f0e3..054706c040 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, BufferedEvent)] +/// #[derive(BufferedEvent)] /// struct PauseEvent(bool); /// /// fn pause_system(mut time: ResMut>, mut events: EventReader) { diff --git a/crates/bevy_time/src/timer.rs b/crates/bevy_time/src/timer.rs index 4b746c58e6..a283511c20 100644 --- a/crates/bevy_time/src/timer.rs +++ b/crates/bevy_time/src/timer.rs @@ -5,13 +5,25 @@ use core::time::Duration; /// Tracks elapsed time. Enters the finished state once `duration` is reached. /// -/// Non repeating timers will stop tracking and stay in the finished state until reset. -/// Repeating timers will only be in the finished state on each tick `duration` is reached or -/// exceeded, and can still be reset at any given point. -/// -/// Paused timers will not have elapsed time increased. -/// /// Note that in order to advance the timer [`tick`](Timer::tick) **MUST** be called. +/// +/// # Timer modes +/// +/// There are two timer modes ([`TimerMode`]): +/// +/// - Non repeating timers will stop tracking and stay in the finished state until reset. +/// - Repeating timers will only be in the finished state on each tick `duration` is reached or +/// exceeded, and can still be reset at any given point. +/// +/// # Pausing timers +/// +/// You can pause a timer using [`Timer::pause`]. Paused timers will not have elapsed time increased. +/// +/// # Elapsing multiple times a frame +/// +/// Repeating timers might elapse multiple times per frame if the time is advanced by more than the timer duration. +/// You can check how many times a timer elapsed each tick with [`Timer::times_finished_this_tick`]. +/// For non-repeating timers, this will always be 0 or 1. #[derive(Clone, Debug, Default, PartialEq, Eq)] #[cfg_attr(feature = "serialize", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr( diff --git a/crates/bevy_ui/src/gradients.rs b/crates/bevy_ui/src/gradients.rs index 87b7afc581..c6357ae302 100644 --- a/crates/bevy_ui/src/gradients.rs +++ b/crates/bevy_ui/src/gradients.rs @@ -63,7 +63,7 @@ impl ColorStop { } // Set the interpolation midpoint between this and the following stop - pub fn with_hint(mut self, hint: f32) -> Self { + pub const fn with_hint(mut self, hint: f32) -> Self { self.hint = hint; self } @@ -175,7 +175,7 @@ impl AngularColorStop { } // Set the interpolation midpoint between this and the following stop - pub fn with_hint(mut self, hint: f32) -> Self { + pub const fn with_hint(mut self, hint: f32) -> Self { self.hint = hint; self } @@ -387,7 +387,7 @@ impl RadialGradient { } } - pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + pub const fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { self.color_space = color_space; self } @@ -437,18 +437,18 @@ impl ConicGradient { } /// Sets the starting angle of the gradient in radians - pub fn with_start(mut self, start: f32) -> Self { + pub const fn with_start(mut self, start: f32) -> Self { self.start = start; self } /// Sets the position of the gradient - pub fn with_position(mut self, position: UiPosition) -> Self { + pub const fn with_position(mut self, position: UiPosition) -> Self { self.position = position; self } - pub fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { + pub const fn in_color_space(mut self, color_space: InterpolationColorSpace) -> Self { self.color_space = color_space; self } @@ -478,7 +478,7 @@ pub enum Gradient { impl Gradient { /// Returns true if the gradient has no stops. - pub fn is_empty(&self) -> bool { + pub const fn is_empty(&self) -> bool { match self { Gradient::Linear(gradient) => gradient.stops.is_empty(), Gradient::Radial(gradient) => gradient.stops.is_empty(), @@ -578,19 +578,19 @@ pub enum RadialGradientShape { Ellipse(Val, Val), } -fn close_side(p: f32, h: f32) -> f32 { +const fn close_side(p: f32, h: f32) -> f32 { (-h - p).abs().min((h - p).abs()) } -fn far_side(p: f32, h: f32) -> f32 { +const fn far_side(p: f32, h: f32) -> f32 { (-h - p).abs().max((h - p).abs()) } -fn close_side2(p: Vec2, h: Vec2) -> f32 { +const fn close_side2(p: Vec2, h: Vec2) -> f32 { close_side(p.x, h.x).min(close_side(p.y, h.y)) } -fn far_side2(p: Vec2, h: Vec2) -> f32 { +const fn far_side2(p: Vec2, h: Vec2) -> f32 { far_side(p.x, h.x).max(far_side(p.y, h.y)) } @@ -638,25 +638,25 @@ impl RadialGradientShape { reflect(Serialize, Deserialize) )] pub enum InterpolationColorSpace { - /// Interpolates in `OKLab` space. + /// Interpolates in OKLABA space. #[default] - OkLab, - /// Interpolates in OKLCH space, taking the shortest hue path. - OkLch, - /// Interpolates in OKLCH space, taking the longest hue path. - OkLchLong, - /// Interpolates in sRGB space. - Srgb, - /// Interpolates in linear sRGB space. - LinearRgb, - /// Interpolates in HSL space, taking the shortest hue path. - Hsl, - /// Interpolates in HSL space, taking the longest hue path. - HslLong, - /// Interpolates in HSV space, taking the shortest hue path. - Hsv, - /// Interpolates in HSV space, taking the longest hue path. - HsvLong, + Oklaba, + /// Interpolates in OKLCHA space, taking the shortest hue path. + Oklcha, + /// Interpolates in OKLCHA space, taking the longest hue path. + OklchaLong, + /// Interpolates in sRGBA space. + Srgba, + /// Interpolates in linear sRGBA space. + LinearRgba, + /// Interpolates in HSLA space, taking the shortest hue path. + Hsla, + /// Interpolates in HSLA space, taking the longest hue path. + HslaLong, + /// Interpolates in HSVA space, taking the shortest hue path. + Hsva, + /// Interpolates in HSVA space, taking the longest hue path. + HsvaLong, } /// Set the color space used for interpolation. @@ -665,28 +665,28 @@ pub trait InColorSpace: Sized { fn in_color_space(self, color_space: InterpolationColorSpace) -> Self; /// Interpolate in `OKLab` space. - fn in_oklab(self) -> Self { - self.in_color_space(InterpolationColorSpace::OkLab) + fn in_oklaba(self) -> Self { + self.in_color_space(InterpolationColorSpace::Oklaba) } /// Interpolate in OKLCH space (short hue path). fn in_oklch(self) -> Self { - self.in_color_space(InterpolationColorSpace::OkLch) + self.in_color_space(InterpolationColorSpace::Oklcha) } /// Interpolate in OKLCH space (long hue path). fn in_oklch_long(self) -> Self { - self.in_color_space(InterpolationColorSpace::OkLchLong) + self.in_color_space(InterpolationColorSpace::OklchaLong) } /// Interpolate in sRGB space. fn in_srgb(self) -> Self { - self.in_color_space(InterpolationColorSpace::Srgb) + self.in_color_space(InterpolationColorSpace::Srgba) } /// Interpolate in linear sRGB space. fn in_linear_rgb(self) -> Self { - self.in_color_space(InterpolationColorSpace::LinearRgb) + self.in_color_space(InterpolationColorSpace::LinearRgba) } } diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index 12cfcbeb68..a2ff3a0dee 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -181,15 +181,15 @@ impl SpecializedRenderPipeline for GradientPipeline { ], ); let color_space = match key.color_space { - InterpolationColorSpace::OkLab => "IN_OKLAB", - InterpolationColorSpace::OkLch => "IN_OKLCH", - InterpolationColorSpace::OkLchLong => "IN_OKLCH_LONG", - InterpolationColorSpace::Srgb => "IN_SRGB", - InterpolationColorSpace::LinearRgb => "IN_LINEAR_RGB", - InterpolationColorSpace::Hsl => "IN_HSL", - InterpolationColorSpace::HslLong => "IN_HSL_LONG", - InterpolationColorSpace::Hsv => "IN_HSV", - InterpolationColorSpace::HsvLong => "IN_HSV_LONG", + InterpolationColorSpace::Oklaba => "IN_OKLAB", + InterpolationColorSpace::Oklcha => "IN_OKLCH", + InterpolationColorSpace::OklchaLong => "IN_OKLCH_LONG", + InterpolationColorSpace::Srgba => "IN_SRGB", + InterpolationColorSpace::LinearRgba => "IN_LINEAR_RGB", + InterpolationColorSpace::Hsla => "IN_HSL", + InterpolationColorSpace::HslaLong => "IN_HSL_LONG", + InterpolationColorSpace::Hsva => "IN_HSV", + InterpolationColorSpace::HsvaLong => "IN_HSV_LONG", }; let shader_defs = if key.anti_alias { diff --git a/crates/bevy_window/src/event.rs b/crates/bevy_window/src/event.rs index 89f219d269..7bb7f72f19 100644 --- a/crates/bevy_window/src/event.rs +++ b/crates/bevy_window/src/event.rs @@ -1,8 +1,5 @@ use alloc::string::String; -use bevy_ecs::{ - entity::Entity, - event::{BufferedEvent, Event}, -}; +use bevy_ecs::{entity::Entity, event::BufferedEvent}; use bevy_input::{ gestures::*, keyboard::{KeyboardFocusLost, KeyboardInput}, @@ -26,7 +23,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, BufferedEvent, Debug, Clone, PartialEq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -48,7 +45,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, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -64,7 +61,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, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -90,7 +87,7 @@ pub struct WindowCreated { /// /// [`WindowPlugin`]: crate::WindowPlugin /// [`Window`]: crate::Window -#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -108,7 +105,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, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -129,7 +126,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, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -149,7 +146,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, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -179,7 +176,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, BufferedEvent, Debug, Clone, PartialEq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -204,7 +201,7 @@ pub struct CursorMoved { } /// An event that is sent whenever the user's cursor enters a window. -#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -221,7 +218,7 @@ pub struct CursorEntered { } /// An event that is sent whenever the user's cursor leaves a window. -#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -242,7 +239,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, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -287,7 +284,7 @@ pub enum Ime { } /// An event that indicates a window has received or lost focus. -#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -314,7 +311,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, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -333,7 +330,7 @@ pub struct WindowOccluded { } /// An event that indicates a window's scale factor has changed. -#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -352,7 +349,7 @@ pub struct WindowScaleFactorChanged { } /// An event that indicates a window's OS-reported scale factor has changed. -#[derive(Event, BufferedEvent, Debug, Clone, PartialEq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -371,7 +368,7 @@ pub struct WindowBackendScaleFactorChanged { } /// Events related to files being dragged and dropped on a window. -#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -407,7 +404,7 @@ pub enum FileDragAndDrop { } /// An event that is sent when a window is repositioned in physical pixels. -#[derive(Event, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -429,7 +426,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, BufferedEvent, Debug, Clone, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -448,7 +445,7 @@ pub struct WindowThemeChanged { } /// Application lifetime events -#[derive(Event, BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] +#[derive(BufferedEvent, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), @@ -491,7 +488,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, BufferedEvent, Debug, Clone, PartialEq)] +#[derive(BufferedEvent, Debug, Clone, PartialEq)] #[cfg_attr( feature = "bevy_reflect", derive(Reflect), diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 8926095dc0..65b7b6c04a 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -156,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, BufferedEvent, Reflect)] +#[derive(Debug, Default, Clone, Copy, BufferedEvent, Reflect)] #[reflect(Debug, Default, Clone)] pub struct WakeUp; @@ -167,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, BufferedEvent)] +#[derive(Debug, Clone, BufferedEvent)] pub struct RawWinitWindowEvent { /// The window for which the event was fired. pub window_id: WindowId, diff --git a/docs/profiling.md b/docs/profiling.md index fe55b048af..2780853b43 100644 --- a/docs/profiling.md +++ b/docs/profiling.md @@ -67,9 +67,14 @@ The [Tracy profiling tool](https://github.com/wolfpld/tracy) is: There are binaries available for Windows, and installation / build instructions for other operating systems can be found in the [Tracy documentation PDF](https://github.com/wolfpld/tracy/releases/latest/download/tracy.pdf). +To determine which Tracy version to install + +1. Run `cargo tree --features bevy/trace_tracy | grep tracy` in your Bevy workspace root to see which tracy dep versions are used +2. Cross reference the tracy dep versions with the [Version Support Table](https://github.com/nagisa/rust_tracy_client?tab=readme-ov-file#version-support-table) + It has a command line capture tool that can record the execution of graphical applications, saving it as a profile file. Tracy has a GUI to inspect these profile files. The GUI app also supports live capture, showing you in real time the trace of your app. The version of tracy must be matched to the version of tracing-tracy used in bevy. A compatibility table can be found on [crates.io](https://crates.io/crates/tracing-tracy) and the version used can be found [here](https://github.com/bevyengine/bevy/blob/latest/crates/bevy_log/Cargo.toml). -On macOS, Tracy can be installed through Homebrew by running `brew install tracy`, and the GUI client can be launched by running `tracy`. +On macOS, Tracy can be installed through Homebrew by running `brew install tracy`, and the GUI client can be launched by running `tracy`. Note that `brew` does not always have the latest version of Tracy available, in which cases you may be required to build from source. In one terminal, run: `./capture-release -o my_capture.tracy` @@ -154,20 +159,20 @@ Follow the steps below to start GPU debugging on macOS. There is no need to crea 1. In the menu bar click on Debug > Debug Executable… - ![Xcode's menu bar open to Debug > Debug Executable...](https://github.com/user-attachments/assets/efdc5037-0957-4227-b29d-9a789ba17a0a) + ![Xcode's menu bar open to Debug > Debug Executable...](https://github.com/user-attachments/assets/efdc5037-0957-4227-b29d-9a789ba17a0a) 2. Select your executable from your project’s target folder. 3. The Scheme Editor will open. If your assets are not located next to your executable, you can go to the Arguments tab and set `BEVY_ASSET_ROOT` to the absolute path for your project (the parent of your assets folder). The rest of the defaults should be fine. - ![Xcode's Schema Editor opened to an environment variable configuration](https://github.com/user-attachments/assets/29cafb05-0c49-4777-8d41-8643812e8f6a) + ![Xcode's Schema Editor opened to an environment variable configuration](https://github.com/user-attachments/assets/29cafb05-0c49-4777-8d41-8643812e8f6a) 4. Click the play button in the top left and this should start your bevy app. - ![A cursor hovering over the play button in XCode](https://github.com/user-attachments/assets/859580e2-779b-4db8-8ea6-73cf4ef696c9) + ![A cursor hovering over the play button in XCode](https://github.com/user-attachments/assets/859580e2-779b-4db8-8ea6-73cf4ef696c9) 5. Go back to Xcode and click on the Metal icon in the bottom drawer and then Capture in the following the popup menu. - ![A cursor hovering over the Capture button in the Metal debugging popup menu](https://github.com/user-attachments/assets/c0ce1591-0a53-499b-bd1b-4d89538ea248) + ![A cursor hovering over the Capture button in the Metal debugging popup menu](https://github.com/user-attachments/assets/c0ce1591-0a53-499b-bd1b-4d89538ea248) 6. Start debugging and profiling! @@ -183,6 +188,7 @@ When you compile with Bevy's `trace_tracy` feature, GPU spans will show up in a > [!NOTE] > Due to dynamic clock speeds, GPU timings will have large frame-to-frame variance, unless you use an external tool to lock your GPU clocks to base speeds. When measuring GPU performance via Tracy, only look at the MTPC column of Tracy's statistics panel, or the span distribution/median, and not at any individual frame data. + > [!NOTE] diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index ce611bd4e9..3227244713 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -21,49 +21,32 @@ fn main() { fn draw_cursor( camera_query: Single<(&Camera, &GlobalTransform)>, - window: Query<&Window>, + window: Single<&Window>, mut gizmos: Gizmos, ) { let (camera, camera_transform) = *camera_query; - let Ok(window) = window.single() else { - return; - }; - let Some(cursor_position) = window.cursor_position() else { - return; - }; - - // Calculate a world position based on the cursor's position. - let Ok(world_pos) = camera.viewport_to_world_2d(camera_transform, cursor_position) else { - return; - }; - - // To test Camera::world_to_viewport, convert result back to viewport space and then back to world space. - let Ok(viewport_check) = camera.world_to_viewport(camera_transform, world_pos.extend(0.0)) - else { - return; - }; - let Ok(world_check) = camera.viewport_to_world_2d(camera_transform, viewport_check.xy()) else { - return; - }; - - gizmos.circle_2d(world_pos, 10., WHITE); - // Should be the same as world_pos - gizmos.circle_2d(world_check, 8., RED); + if let Some(cursor_position) = window.cursor_position() + // Calculate a world position based on the cursor's position. + && let Ok(world_pos) = camera.viewport_to_world_2d(camera_transform, cursor_position) + // To test Camera::world_to_viewport, convert result back to viewport space and then back to world space. + && let Ok(viewport_check) = camera.world_to_viewport(camera_transform, world_pos.extend(0.0)) + && let Ok(world_check) = camera.viewport_to_world_2d(camera_transform, viewport_check.xy()) + { + gizmos.circle_2d(world_pos, 10., WHITE); + // Should be the same as world_pos + gizmos.circle_2d(world_check, 8., RED); + } } fn controls( - mut camera_query: Query<(&mut Camera, &mut Transform, &mut Projection)>, - window: Query<&Window>, + camera_query: Single<(&mut Camera, &mut Transform, &mut Projection)>, + window: Single<&Window>, input: Res>, time: Res>, ) { - let Ok(window) = window.single() else { - return; - }; - let Ok((mut camera, mut transform, mut projection)) = camera_query.single_mut() else { - return; - }; + let (mut camera, mut transform, mut projection) = camera_query.into_inner(); + let fspeed = 600.0 * time.delta_secs(); let uspeed = fspeed as u32; let window_size = window.resolution.physical_size(); diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 58ec9b5ceb..f61f540ef3 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -27,7 +27,7 @@ use bevy::{ sync_component::SyncComponentPlugin, sync_world::{MainEntityHashMap, RenderEntity}, view::{ExtractedView, RenderVisibleEntities, ViewTarget}, - Extract, Render, RenderApp, RenderSystems, + Extract, Render, RenderApp, RenderStartup, RenderSystems, }, sprite::{ extract_mesh2d, DrawMesh2d, Material2dBindGroupId, Mesh2dPipeline, Mesh2dPipelineKey, @@ -132,14 +132,16 @@ pub struct ColoredMesh2dPipeline { shader: Handle, } -impl FromWorld for ColoredMesh2dPipeline { - fn from_world(world: &mut World) -> Self { - Self { - mesh2d_pipeline: Mesh2dPipeline::from_world(world), - // Get the shader from the shader resource we inserted in the plugin. - shader: world.resource::().0.clone(), - } - } +fn init_colored_mesh_2d_pipeline( + mut commands: Commands, + mesh2d_pipeline: Res, + colored_mesh2d_shader: Res, +) { + commands.insert_resource(ColoredMesh2dPipeline { + mesh2d_pipeline: mesh2d_pipeline.clone(), + // Clone the shader from the shader resource we inserted in the plugin. + shader: colored_mesh2d_shader.0.clone(), + }); } // We implement `SpecializedPipeline` to customize the default rendering from `Mesh2dPipeline` @@ -307,6 +309,7 @@ impl Plugin for ColoredMesh2dPlugin { .add_render_command::() .init_resource::>() .init_resource::() + .add_systems(RenderStartup, init_colored_mesh_2d_pipeline) .add_systems( ExtractSchedule, extract_colored_mesh2d.after(extract_mesh2d), @@ -316,13 +319,6 @@ impl Plugin for ColoredMesh2dPlugin { queue_colored_mesh2d.in_set(RenderSystems::QueueMeshes), ); } - - fn finish(&self, app: &mut App) { - // Register our custom pipeline - app.get_sub_app_mut(RenderApp) - .unwrap() - .init_resource::(); - } } /// Extract the [`ColoredMesh2d`] marker component into the render app diff --git a/examples/3d/manual_material.rs b/examples/3d/manual_material.rs index f93265a50a..8d90fe9ad7 100644 --- a/examples/3d/manual_material.rs +++ b/examples/3d/manual_material.rs @@ -29,7 +29,7 @@ use bevy::{ sync_world::MainEntity, texture::GpuImage, view::ExtractedView, - Extract, RenderApp, + Extract, RenderApp, RenderStartup, }, utils::Parallel, }; @@ -55,54 +55,48 @@ impl Plugin for ImageMaterialPlugin { check_entities_needing_specialization.after(AssetEventSystems), ) .init_resource::>(); - } - fn finish(&self, app: &mut App) { let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - render_app.add_systems( - ExtractSchedule, - ( - extract_image_materials, - extract_image_materials_needing_specialization, - ), - ); - - render_app.world_mut().resource_scope( - |world: &mut World, mut bind_group_allocators: Mut| { - world.resource_scope(|world: &mut World, render_device: Mut| { - let bind_group_layout = render_device.create_bind_group_layout( - "image_material_layout", - &BindGroupLayoutEntries::sequential( - ShaderStages::FRAGMENT, - ( - texture_2d(TextureSampleType::Float { filterable: false }), - sampler(SamplerBindingType::NonFiltering), - ), - ), - ); - let sampler = render_device.create_sampler(&SamplerDescriptor::default()); - world.insert_resource(ImageMaterialBindGroupLayout(bind_group_layout.clone())); - world.insert_resource(ImageMaterialBindGroupSampler(sampler)); - - bind_group_allocators.insert( - TypeId::of::(), - MaterialBindGroupAllocator::new( - &render_device, - None, - None, - bind_group_layout, - None, - ), - ); - }); - }, - ); + render_app + .add_systems(RenderStartup, init_image_material_resources) + .add_systems( + ExtractSchedule, + ( + extract_image_materials, + extract_image_materials_needing_specialization, + ), + ); } } +fn init_image_material_resources( + mut commands: Commands, + render_device: Res, + mut bind_group_allocators: ResMut, +) { + let bind_group_layout = render_device.create_bind_group_layout( + "image_material_layout", + &BindGroupLayoutEntries::sequential( + ShaderStages::FRAGMENT, + ( + texture_2d(TextureSampleType::Float { filterable: false }), + sampler(SamplerBindingType::NonFiltering), + ), + ), + ); + let sampler = render_device.create_sampler(&SamplerDescriptor::default()); + commands.insert_resource(ImageMaterialBindGroupLayout(bind_group_layout.clone())); + commands.insert_resource(ImageMaterialBindGroupSampler(sampler)); + + bind_group_allocators.insert( + TypeId::of::(), + MaterialBindGroupAllocator::new(&render_device, None, None, bind_group_layout, None), + ); +} + #[derive(Resource)] struct ImageMaterialBindGroupLayout(BindGroupLayout); diff --git a/examples/3d/mixed_lighting.rs b/examples/3d/mixed_lighting.rs index ffff8652b4..e2453a6132 100644 --- a/examples/3d/mixed_lighting.rs +++ b/examples/3d/mixed_lighting.rs @@ -72,7 +72,7 @@ enum LightingMode { /// An event that's fired whenever the user changes the lighting mode. /// /// This is also fired when the scene loads for the first time. -#[derive(Clone, Copy, Default, Event, BufferedEvent)] +#[derive(Clone, Copy, Default, BufferedEvent)] struct LightingModeChanged; #[derive(Clone, Copy, Component, Debug)] diff --git a/examples/app/log_layers_ecs.rs b/examples/app/log_layers_ecs.rs index bcf5e5571e..75083a22cb 100644 --- a/examples/app/log_layers_ecs.rs +++ b/examples/app/log_layers_ecs.rs @@ -38,7 +38,7 @@ fn main() { } /// A basic message. This is what we will be sending from the [`CaptureLayer`] to [`CapturedLogEvents`] non-send resource. -#[derive(Debug, Event, BufferedEvent)] +#[derive(Debug, BufferedEvent)] struct LogEvent { message: String, level: Level, diff --git a/examples/async_tasks/external_source_external_thread.rs b/examples/async_tasks/external_source_external_thread.rs index 9cd7a57ae4..3ff31ab128 100644 --- a/examples/async_tasks/external_source_external_thread.rs +++ b/examples/async_tasks/external_source_external_thread.rs @@ -20,7 +20,7 @@ fn main() { #[derive(Resource, Deref)] struct StreamReceiver(Receiver); -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct StreamEvent(u32); fn setup(mut commands: Commands) { diff --git a/examples/audio/pitch.rs b/examples/audio/pitch.rs index 6f4108025a..24293f2a60 100644 --- a/examples/audio/pitch.rs +++ b/examples/audio/pitch.rs @@ -12,7 +12,7 @@ fn main() { .run(); } -#[derive(Event, BufferedEvent, Default)] +#[derive(BufferedEvent, Default)] struct PlayPitch; #[derive(Resource)] diff --git a/examples/ecs/component_hooks.rs b/examples/ecs/component_hooks.rs index a50452a11e..94091dffe8 100644 --- a/examples/ecs/component_hooks.rs +++ b/examples/ecs/component_hooks.rs @@ -45,7 +45,7 @@ impl Component for MyComponent { #[derive(Resource, Default, Debug, Deref, DerefMut)] struct MyComponentIndex(HashMap); -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct MyEvent; fn main() { diff --git a/examples/ecs/event.rs b/examples/ecs/event.rs index 33ff7ad60a..b9a084a50d 100644 --- a/examples/ecs/event.rs +++ b/examples/ecs/event.rs @@ -6,17 +6,17 @@ use bevy::prelude::*; // In order to send or receive events first you must define them // This event should be sent when something attempts to deal damage to another entity. -#[derive(Event, BufferedEvent, Debug)] +#[derive(BufferedEvent, Debug)] struct DealDamage { pub amount: i32, } // This event should be sent when an entity receives damage. -#[derive(Event, BufferedEvent, Debug, Default)] +#[derive(BufferedEvent, Debug, Default)] struct DamageReceived; // This event should be sent when an entity blocks damage with armor. -#[derive(Event, BufferedEvent, Debug, Default)] +#[derive(BufferedEvent, Debug, Default)] struct ArmorBlockedDamage; // This resource represents a timer used to determine when to deal damage diff --git a/examples/ecs/send_and_receive_events.rs b/examples/ecs/send_and_receive_events.rs index 7a376529f5..e54e98e896 100644 --- a/examples/ecs/send_and_receive_events.rs +++ b/examples/ecs/send_and_receive_events.rs @@ -46,10 +46,10 @@ fn main() { app.update(); } -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct A; -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct B; // This works fine, because the types are different, @@ -62,7 +62,7 @@ fn read_and_write_different_event_types(mut a: EventWriter, mut b: EventReade } /// A dummy event type. -#[derive(Debug, Clone, Event, BufferedEvent)] +#[derive(Debug, Clone, 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 b8d02e8159..f890d90acf 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, BufferedEvent, Default)] +#[derive(BufferedEvent, Default)] struct CollisionEvent; #[derive(Component)] diff --git a/examples/helpers/widgets.rs b/examples/helpers/widgets.rs index 02039fed49..55c6f3f459 100644 --- a/examples/helpers/widgets.rs +++ b/examples/helpers/widgets.rs @@ -6,7 +6,7 @@ use bevy::{ecs::system::EntityCommands, prelude::*}; /// An event that's sent whenever the user changes one of the settings by /// clicking a radio button. -#[derive(Clone, Event, BufferedEvent, Deref, DerefMut)] +#[derive(Clone, BufferedEvent, Deref, DerefMut)] pub struct WidgetClickEvent(T); /// A marker component that we place on all widgets that send diff --git a/examples/movement/physics_in_fixed_timestep.rs b/examples/movement/physics_in_fixed_timestep.rs index d10f7af785..c2e9735c2a 100644 --- a/examples/movement/physics_in_fixed_timestep.rs +++ b/examples/movement/physics_in_fixed_timestep.rs @@ -51,6 +51,16 @@ //! See the [documentation of the lightyear crate](https://cbournhonesque.github.io/lightyear/book/concepts/advanced_replication/visual_interpolation.html) //! for a nice overview of the different methods and their respective tradeoffs. //! +//! If we decide to use a fixed timestep, our game logic should mostly go in the `FixedUpdate` schedule. +//! One notable exception is the camera. Cameras should update as often as possible, or the player will very quickly +//! notice choppy movement if it's only updated at the same rate as the physics simulation. So, we use a variable timestep for the camera, +//! updating its transform every frame. The question now is which schedule to use. That depends on whether the camera data is required +//! for the physics simulation to run or not. +//! For example, in 3D games, the camera rotation often determines which direction the player moves when pressing "W", +//! so we need to rotate the camera *before* the fixed timestep. In contrast, the translation of the camera depends on what the physics simulation +//! has calculated for the player's position. Therefore, we need to update the camera's translation *after* the fixed timestep. Fortunately, +//! we can get smooth movement by simply using the interpolated player translation for the camera as well. +//! //! ## Implementation //! //! - The player's inputs since the last physics update are stored in the `AccumulatedInput` component. @@ -62,6 +72,7 @@ //! - Accumulate the player's input and set the current speed in the `handle_input` system. //! This is run in the `RunFixedMainLoop` schedule, ordered in `RunFixedMainLoopSystems::BeforeFixedMainLoop`, //! which runs before the fixed timestep loop. This is run every frame. +//! - Rotate the camera based on the player's input. This is also run in `RunFixedMainLoopSystems::BeforeFixedMainLoop`. //! - Advance the physics simulation by one fixed timestep in the `advance_physics` system. //! Accumulated input is consumed here. //! This is run in the `FixedUpdate` schedule, which runs zero or multiple times per frame. @@ -69,6 +80,7 @@ //! This interpolates between the player's previous and current position in the physics simulation. //! It is run in the `RunFixedMainLoop` schedule, ordered in `RunFixedMainLoopSystems::AfterFixedMainLoop`, //! which runs after the fixed timestep loop. This is run every frame. +//! - Update the camera's translation to the player's interpolated translation. This is also run in `RunFixedMainLoopSystems::AfterFixedMainLoop`. //! //! //! ## Controls @@ -79,27 +91,51 @@ //! | `S` | Move down | //! | `A` | Move left | //! | `D` | Move right | +//! | Mouse | Rotate camera | -use bevy::prelude::*; +use std::f32::consts::FRAC_PI_2; + +use bevy::{color::palettes::tailwind, input::mouse::AccumulatedMouseMotion, prelude::*}; fn main() { App::new() .add_plugins(DefaultPlugins) - .add_systems(Startup, (spawn_text, spawn_player)) + .init_resource::() + .add_systems(Startup, (spawn_text, spawn_player, spawn_environment)) + // At the beginning of each frame, clear the flag that indicates whether the fixed timestep has run this frame. + .add_systems(PreUpdate, clear_fixed_timestep_flag) + // At the beginning of each fixed timestep, set the flag that indicates whether the fixed timestep has run this frame. + .add_systems(FixedPreUpdate, set_fixed_time_step_flag) // Advance the physics simulation using a fixed timestep. .add_systems(FixedUpdate, advance_physics) .add_systems( // The `RunFixedMainLoop` schedule allows us to schedule systems to run before and after the fixed timestep loop. RunFixedMainLoop, ( - // The physics simulation needs to know the player's input, so we run this before the fixed timestep loop. - // Note that if we ran it in `Update`, it would be too late, as the physics simulation would already have been advanced. - // If we ran this in `FixedUpdate`, it would sometimes not register player input, as that schedule may run zero times per frame. - handle_input.in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop), - // The player's visual representation needs to be updated after the physics simulation has been advanced. - // This could be run in `Update`, but if we run it here instead, the systems in `Update` - // will be working with the `Transform` that will actually be shown on screen. - interpolate_rendered_transform.in_set(RunFixedMainLoopSystems::AfterFixedMainLoop), + ( + // The camera needs to be rotated before the physics simulation is advanced in before the fixed timestep loop, + // so that the physics simulation can use the current rotation. + // Note that if we ran it in `Update`, it would be too late, as the physics simulation would already have been advanced. + // If we ran this in `FixedUpdate`, it would sometimes not register player input, as that schedule may run zero times per frame. + rotate_camera, + // Accumulate our input before the fixed timestep loop to tell the physics simulation what it should do during the fixed timestep. + accumulate_input, + ) + .chain() + .in_set(RunFixedMainLoopSystems::BeforeFixedMainLoop), + ( + // Clear our accumulated input after it was processed during the fixed timestep. + // By clearing the input *after* the fixed timestep, we can still use `AccumulatedInput` inside `FixedUpdate` if we need it. + clear_input.run_if(did_fixed_timestep_run_this_frame), + // The player's visual representation needs to be updated after the physics simulation has been advanced. + // This could be run in `Update`, but if we run it here instead, the systems in `Update` + // will be working with the `Transform` that will actually be shown on screen. + interpolate_rendered_transform, + // The camera can then use the interpolated transform to position itself correctly. + translate_camera, + ) + .chain() + .in_set(RunFixedMainLoopSystems::AfterFixedMainLoop), ), ) .run(); @@ -108,7 +144,12 @@ fn main() { /// A vector representing the player's input, accumulated over all frames that ran /// since the last time the physics simulation was advanced. #[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)] -struct AccumulatedInput(Vec2); +struct AccumulatedInput { + // The player's movement input (WASD). + movement: Vec2, + // Other input that could make sense would be e.g. + // boost: bool +} /// A vector representing the player's velocity in the physics simulation. #[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)] @@ -128,12 +169,13 @@ struct PhysicalTranslation(Vec3); #[derive(Debug, Component, Clone, Copy, PartialEq, Default, Deref, DerefMut)] struct PreviousPhysicalTranslation(Vec3); -/// Spawn the player sprite and a 2D camera. -fn spawn_player(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2d); +/// Spawn the player and a 3D camera. We could also spawn the camera as a child of the player, +/// but in practice, they are usually spawned separately so that the player's rotation does not +/// influence the camera's rotation. +fn spawn_player(mut commands: Commands) { + commands.spawn((Camera3d::default(), CameraSensitivity::default())); commands.spawn(( Name::new("Player"), - Sprite::from_image(asset_server.load("branding/icon.png")), Transform::from_scale(Vec3::splat(0.3)), AccumulatedInput::default(), Velocity::default(), @@ -142,55 +184,187 @@ fn spawn_player(mut commands: Commands, asset_server: Res) { )); } +/// Spawn a field of floating spheres to fly around in +fn spawn_environment( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + let sphere_material = materials.add(Color::from(tailwind::SKY_200)); + let sphere_mesh = meshes.add(Sphere::new(0.3)); + let spheres_in_x = 6; + let spheres_in_y = 4; + let spheres_in_z = 10; + let distance = 3.0; + for x in 0..spheres_in_x { + for y in 0..spheres_in_y { + for z in 0..spheres_in_z { + let translation = Vec3::new( + x as f32 * distance - (spheres_in_x as f32 - 1.0) * distance / 2.0, + y as f32 * distance - (spheres_in_y as f32 - 1.0) * distance / 2.0, + z as f32 * distance - (spheres_in_z as f32 - 1.0) * distance / 2.0, + ); + commands.spawn(( + Name::new("Sphere"), + Transform::from_translation(translation), + Mesh3d(sphere_mesh.clone()), + MeshMaterial3d(sphere_material.clone()), + )); + } + } + } + + commands.spawn(( + DirectionalLight::default(), + Transform::default().looking_to(Vec3::new(-1.0, -3.0, 0.5), Vec3::Y), + )); +} + /// Spawn a bit of UI text to explain how to move the player. fn spawn_text(mut commands: Commands) { - commands - .spawn(Node { + let font = TextFont { + font_size: 25.0, + ..default() + }; + commands.spawn(( + Node { position_type: PositionType::Absolute, bottom: Val::Px(12.0), left: Val::Px(12.0), + flex_direction: FlexDirection::Column, ..default() - }) - .with_child(( - Text::new("Move the player with WASD"), - TextFont { - font_size: 25.0, - ..default() - }, - )); + }, + children![ + (Text::new("Move the player with WASD"), font.clone()), + (Text::new("Rotate the camera with the mouse"), font) + ], + )); +} + +fn rotate_camera( + accumulated_mouse_motion: Res, + player: Single<(&mut Transform, &CameraSensitivity), With>, +) { + let (mut transform, camera_sensitivity) = player.into_inner(); + + let delta = accumulated_mouse_motion.delta; + + if delta != Vec2::ZERO { + // Note that we are not multiplying by delta time here. + // The reason is that for mouse movement, we already get the full movement that happened since the last frame. + // This means that if we multiply by delta time, we will get a smaller rotation than intended by the user. + let delta_yaw = -delta.x * camera_sensitivity.x; + let delta_pitch = -delta.y * camera_sensitivity.y; + + let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ); + let yaw = yaw + delta_yaw; + + // If the pitch was ±¹⁄₂ π, the camera would look straight up or down. + // When the user wants to move the camera back to the horizon, which way should the camera face? + // The camera has no way of knowing what direction was "forward" before landing in that extreme position, + // so the direction picked will for all intents and purposes be arbitrary. + // Another issue is that for mathematical reasons, the yaw will effectively be flipped when the pitch is at the extremes. + // To not run into these issues, we clamp the pitch to a safe range. + const PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01; + let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT); + + transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll); + } +} + +#[derive(Debug, Component, Deref, DerefMut)] +struct CameraSensitivity(Vec2); + +impl Default for CameraSensitivity { + fn default() -> Self { + Self( + // These factors are just arbitrary mouse sensitivity values. + // It's often nicer to have a faster horizontal sensitivity than vertical. + // We use a component for them so that we can make them user-configurable at runtime + // for accessibility reasons. + // It also allows you to inspect them in an editor if you `Reflect` the component. + Vec2::new(0.003, 0.002), + ) + } } /// Handle keyboard input and accumulate it in the `AccumulatedInput` component. /// /// There are many strategies for how to handle all the input that happened since the last fixed timestep. -/// This is a very simple one: we just accumulate the input and average it out by normalizing it. -fn handle_input( +/// This is a very simple one: we just use the last available input. +/// That strategy works fine for us since the user continuously presses the input keys in this example. +/// If we had some kind of instantaneous action like activating a boost ability, we would need to remember that that input +/// was pressed at some point since the last fixed timestep. +fn accumulate_input( keyboard_input: Res>, - mut query: Query<(&mut AccumulatedInput, &mut Velocity)>, + player: Single<(&mut AccumulatedInput, &mut Velocity)>, + camera: Single<&Transform, With>, ) { - /// Since Bevy's default 2D camera setup is scaled such that - /// one unit is one pixel, you can think of this as - /// "How many pixels per second should the player move?" - const SPEED: f32 = 210.0; - for (mut input, mut velocity) in query.iter_mut() { - if keyboard_input.pressed(KeyCode::KeyW) { - input.y += 1.0; - } - if keyboard_input.pressed(KeyCode::KeyS) { - input.y -= 1.0; - } - if keyboard_input.pressed(KeyCode::KeyA) { - input.x -= 1.0; - } - if keyboard_input.pressed(KeyCode::KeyD) { - input.x += 1.0; - } - - // Need to normalize and scale because otherwise - // diagonal movement would be faster than horizontal or vertical movement. - // This effectively averages the accumulated input. - velocity.0 = input.extend(0.0).normalize_or_zero() * SPEED; + /// Since Bevy's 3D renderer assumes SI units, this has the unit of meters per second. + /// Note that about 1.5 is the average walking speed of a human. + const SPEED: f32 = 4.0; + let (mut input, mut velocity) = player.into_inner(); + // Reset the input to zero before reading the new input. As mentioned above, we can only do this + // because this is continuously pressed by the user. Do not reset e.g. whether the user wants to boost. + input.movement = Vec2::ZERO; + if keyboard_input.pressed(KeyCode::KeyW) { + input.movement.y += 1.0; } + if keyboard_input.pressed(KeyCode::KeyS) { + input.movement.y -= 1.0; + } + if keyboard_input.pressed(KeyCode::KeyA) { + input.movement.x -= 1.0; + } + if keyboard_input.pressed(KeyCode::KeyD) { + input.movement.x += 1.0; + } + + // Remap the 2D input to Bevy's 3D coordinate system. + // Pressing W makes `input.y` go up. Since Bevy assumes that -Z is forward, we make our new Z equal to -input.y + let input_3d = Vec3 { + x: input.movement.x, + y: 0.0, + z: -input.movement.y, + }; + + // Rotate the input so that forward is aligned with the camera's forward direction. + let rotated_input = camera.rotation * input_3d; + + // We need to normalize and scale because otherwise + // diagonal movement would be faster than horizontal or vertical movement. + // We use `clamp_length_max` instead of `.normalize_or_zero()` because gamepad input + // may be smaller than 1.0 when the player is pushing the stick just a little bit. + velocity.0 = rotated_input.clamp_length_max(1.0) * SPEED; +} + +/// A simple resource that tells us whether the fixed timestep ran this frame. +#[derive(Resource, Debug, Deref, DerefMut, Default)] +pub struct DidFixedTimestepRunThisFrame(bool); + +/// Reset the flag at the start of every frame. +fn clear_fixed_timestep_flag( + mut did_fixed_timestep_run_this_frame: ResMut, +) { + did_fixed_timestep_run_this_frame.0 = false; +} + +/// Set the flag during each fixed timestep. +fn set_fixed_time_step_flag( + mut did_fixed_timestep_run_this_frame: ResMut, +) { + did_fixed_timestep_run_this_frame.0 = true; +} + +fn did_fixed_timestep_run_this_frame( + did_fixed_timestep_run_this_frame: Res, +) -> bool { + did_fixed_timestep_run_this_frame.0 +} + +// Clear the input after it was processed in the fixed timestep. +fn clear_input(mut input: Single<&mut AccumulatedInput>) { + **input = AccumulatedInput::default(); } /// Advance the physics simulation by one fixed timestep. This may run zero or multiple times per frame. @@ -202,22 +376,14 @@ fn advance_physics( mut query: Query<( &mut PhysicalTranslation, &mut PreviousPhysicalTranslation, - &mut AccumulatedInput, &Velocity, )>, ) { - for ( - mut current_physical_translation, - mut previous_physical_translation, - mut input, - velocity, - ) in query.iter_mut() + for (mut current_physical_translation, mut previous_physical_translation, velocity) in + query.iter_mut() { previous_physical_translation.0 = current_physical_translation.0; current_physical_translation.0 += velocity.0 * fixed_time.delta_secs(); - - // Reset the input accumulator, as we are currently consuming all input that happened since the last fixed timestep. - input.0 = Vec2::ZERO; } } @@ -241,3 +407,11 @@ fn interpolate_rendered_transform( transform.translation = rendered_translation; } } + +// Sync the camera's position with the player's interpolated position +fn translate_camera( + mut camera: Single<&mut Transform, With>, + player: Single<&Transform, (With, Without)>, +) { + camera.translation = player.translation; +} diff --git a/examples/shader/compute_shader_game_of_life.rs b/examples/shader/compute_shader_game_of_life.rs index aa10ccf4bf..575a09f1d1 100644 --- a/examples/shader/compute_shader_game_of_life.rs +++ b/examples/shader/compute_shader_game_of_life.rs @@ -12,7 +12,7 @@ use bevy::{ render_resource::{binding_types::texture_storage_2d, *}, renderer::{RenderContext, RenderDevice}, texture::GpuImage, - Render, RenderApp, RenderSystems, + Render, RenderApp, RenderStartup, RenderSystems, }, }; use std::borrow::Cow; @@ -103,20 +103,17 @@ impl Plugin for GameOfLifeComputePlugin { // for operation on by the compute shader and display on the sprite. app.add_plugins(ExtractResourcePlugin::::default()); let render_app = app.sub_app_mut(RenderApp); - render_app.add_systems( - Render, - prepare_bind_group.in_set(RenderSystems::PrepareBindGroups), - ); + render_app + .add_systems(RenderStartup, init_game_of_life_pipeline) + .add_systems( + Render, + prepare_bind_group.in_set(RenderSystems::PrepareBindGroups), + ); let mut render_graph = render_app.world_mut().resource_mut::(); render_graph.add_node(GameOfLifeLabel, GameOfLifeNode::default()); render_graph.add_node_edge(GameOfLifeLabel, bevy::render::graph::CameraDriverLabel); } - - fn finish(&self, app: &mut App) { - let render_app = app.sub_app_mut(RenderApp); - render_app.init_resource::(); - } } #[derive(Resource, Clone, ExtractResource)] @@ -157,40 +154,41 @@ struct GameOfLifePipeline { update_pipeline: CachedComputePipelineId, } -impl FromWorld for GameOfLifePipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - let texture_bind_group_layout = render_device.create_bind_group_layout( - "GameOfLifeImages", - &BindGroupLayoutEntries::sequential( - ShaderStages::COMPUTE, - ( - texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::ReadOnly), - texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), - ), +fn init_game_of_life_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, + pipeline_cache: Res, +) { + let texture_bind_group_layout = render_device.create_bind_group_layout( + "GameOfLifeImages", + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::ReadOnly), + texture_storage_2d(TextureFormat::R32Float, StorageTextureAccess::WriteOnly), ), - ); - let shader = world.load_asset(SHADER_ASSET_PATH); - let pipeline_cache = world.resource::(); - let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - layout: vec![texture_bind_group_layout.clone()], - shader: shader.clone(), - entry_point: Some(Cow::from("init")), - ..default() - }); - let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - layout: vec![texture_bind_group_layout.clone()], - shader, - entry_point: Some(Cow::from("update")), - ..default() - }); + ), + ); + let shader = asset_server.load(SHADER_ASSET_PATH); + let init_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + layout: vec![texture_bind_group_layout.clone()], + shader: shader.clone(), + entry_point: Some(Cow::from("init")), + ..default() + }); + let update_pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + layout: vec![texture_bind_group_layout.clone()], + shader, + entry_point: Some(Cow::from("update")), + ..default() + }); - GameOfLifePipeline { - texture_bind_group_layout, - init_pipeline, - update_pipeline, - } - } + commands.insert_resource(GameOfLifePipeline { + texture_bind_group_layout, + init_pipeline, + update_pipeline, + }); } enum GameOfLifeState { diff --git a/examples/shader/custom_render_phase.rs b/examples/shader/custom_render_phase.rs index 89f5ce6d67..9993de341c 100644 --- a/examples/shader/custom_render_phase.rs +++ b/examples/shader/custom_render_phase.rs @@ -55,7 +55,7 @@ use bevy::{ renderer::RenderContext, sync_world::MainEntity, view::{ExtractedView, RenderVisibleEntities, RetainedViewEntity, ViewTarget}, - Extract, Render, RenderApp, RenderDebugFlags, RenderSystems, + Extract, Render, RenderApp, RenderDebugFlags, RenderStartup, RenderSystems, }, }; use nonmax::NonMaxU32; @@ -127,6 +127,7 @@ impl Plugin for MeshStencilPhasePlugin { .init_resource::>() .add_render_command::() .init_resource::>() + .add_systems(RenderStartup, init_stencil_pipeline) .add_systems(ExtractSchedule, extract_camera_phases) .add_systems( Render, @@ -143,16 +144,6 @@ impl Plugin for MeshStencilPhasePlugin { // Tell the node to run after the main pass .add_render_graph_edges(Core3d, (Node3d::MainOpaquePass, CustomDrawPassLabel)); } - - fn finish(&self, app: &mut App) { - // We need to get the render app from the main app - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - // The pipeline needs the RenderDevice to be created and it's only available once plugins - // are initialized - render_app.init_resource::(); - } } #[derive(Resource)] @@ -167,13 +158,15 @@ struct StencilPipeline { shader_handle: Handle, } -impl FromWorld for StencilPipeline { - fn from_world(world: &mut World) -> Self { - Self { - mesh_pipeline: MeshPipeline::from_world(world), - shader_handle: world.resource::().load(SHADER_ASSET_PATH), - } - } +fn init_stencil_pipeline( + mut commands: Commands, + mesh_pipeline: Res, + asset_server: Res, +) { + commands.insert_resource(StencilPipeline { + mesh_pipeline: mesh_pipeline.clone(), + shader_handle: asset_server.load(SHADER_ASSET_PATH), + }); } // For more information on how SpecializedMeshPipeline work, please look at the diff --git a/examples/shader/custom_shader_instancing.rs b/examples/shader/custom_shader_instancing.rs index 358a2beac7..4a578dc5c6 100644 --- a/examples/shader/custom_shader_instancing.rs +++ b/examples/shader/custom_shader_instancing.rs @@ -32,7 +32,7 @@ use bevy::{ renderer::RenderDevice, sync_world::MainEntity, view::{ExtractedView, NoFrustumCulling, NoIndirectDrawing}, - Render, RenderApp, RenderSystems, + Render, RenderApp, RenderStartup, RenderSystems, }, }; use bytemuck::{Pod, Zeroable}; @@ -102,6 +102,7 @@ impl Plugin for CustomMaterialPlugin { app.sub_app_mut(RenderApp) .add_render_command::() .init_resource::>() + .add_systems(RenderStartup, init_custom_pipeline) .add_systems( Render, ( @@ -110,10 +111,6 @@ impl Plugin for CustomMaterialPlugin { ), ); } - - fn finish(&self, app: &mut App) { - app.sub_app_mut(RenderApp).init_resource::(); - } } #[derive(Clone, Copy, Pod, Zeroable)] @@ -203,15 +200,15 @@ struct CustomPipeline { mesh_pipeline: MeshPipeline, } -impl FromWorld for CustomPipeline { - fn from_world(world: &mut World) -> Self { - let mesh_pipeline = world.resource::(); - - CustomPipeline { - shader: world.load_asset(SHADER_ASSET_PATH), - mesh_pipeline: mesh_pipeline.clone(), - } - } +fn init_custom_pipeline( + mut commands: Commands, + asset_server: Res, + mesh_pipeline: Res, +) { + commands.insert_resource(CustomPipeline { + shader: asset_server.load(SHADER_ASSET_PATH), + mesh_pipeline: mesh_pipeline.clone(), + }); } impl SpecializedMeshPipeline for CustomPipeline { diff --git a/examples/shader/gpu_readback.rs b/examples/shader/gpu_readback.rs index 964776291e..44bb43d5d9 100644 --- a/examples/shader/gpu_readback.rs +++ b/examples/shader/gpu_readback.rs @@ -15,7 +15,7 @@ use bevy::{ renderer::{RenderContext, RenderDevice}, storage::{GpuShaderStorageBuffer, ShaderStorageBuffer}, texture::GpuImage, - Render, RenderApp, RenderSystems, + Render, RenderApp, RenderStartup, RenderSystems, }, }; @@ -41,24 +41,22 @@ fn main() { // We need a plugin to organize all the systems and render node required for this example struct GpuReadbackPlugin; impl Plugin for GpuReadbackPlugin { - fn build(&self, _app: &mut App) {} - - fn finish(&self, app: &mut App) { - let render_app = app.sub_app_mut(RenderApp); - render_app.init_resource::().add_systems( - Render, - prepare_bind_group - .in_set(RenderSystems::PrepareBindGroups) - // We don't need to recreate the bind group every frame - .run_if(not(resource_exists::)), - ); - - // Add the compute node as a top level node to the render graph - // This means it will only execute once per frame + fn build(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; render_app - .world_mut() - .resource_mut::() - .add_node(ComputeNodeLabel, ComputeNode::default()); + .add_systems( + RenderStartup, + (init_compute_pipeline, add_compute_render_graph_node), + ) + .add_systems( + Render, + prepare_bind_group + .in_set(RenderSystems::PrepareBindGroups) + // We don't need to recreate the bind group every frame + .run_if(not(resource_exists::)), + ); } } @@ -74,7 +72,7 @@ fn setup( mut buffers: ResMut>, ) { // Create a storage buffer with some data - let buffer = vec![0u32; BUFFER_LEN]; + let buffer: Vec = (0..BUFFER_LEN as u32).collect(); let mut buffer = ShaderStorageBuffer::from(buffer); // We need to enable the COPY_SRC usage so we can copy the buffer to the cpu buffer.buffer_description.usage |= BufferUsages::COPY_SRC; @@ -110,6 +108,19 @@ fn setup( let data: Vec = trigger.event().to_shader_type(); info!("Buffer {:?}", data); }); + + // It is also possible to read only a range of the buffer. + commands + .spawn(Readback::buffer_range( + buffer.clone(), + 4 * u32::SHADER_SIZE.get(), // skip the first four elements + 8 * u32::SHADER_SIZE.get(), // read eight elements + )) + .observe(|trigger: On| { + let data: Vec = trigger.event().to_shader_type(); + info!("Buffer range {:?}", data); + }); + // This is just a simple way to pass the buffer handle to the render app for our compute node commands.insert_resource(ReadbackBuffer(buffer)); @@ -127,6 +138,13 @@ fn setup( commands.insert_resource(ReadbackImage(image)); } +fn add_compute_render_graph_node(mut render_graph: ResMut) { + // Add the compute node as a top-level node to the render graph. This means it will only execute + // once per frame. Normally, adding a node would use the `RenderGraphApp::add_render_graph_node` + // method, but it does not allow adding as a top-level node. + render_graph.add_node(ComputeNodeLabel, ComputeNode::default()); +} + #[derive(Resource)] struct GpuBufferBindGroup(BindGroup); @@ -158,29 +176,30 @@ struct ComputePipeline { pipeline: CachedComputePipelineId, } -impl FromWorld for ComputePipeline { - fn from_world(world: &mut World) -> Self { - let render_device = world.resource::(); - let layout = render_device.create_bind_group_layout( - None, - &BindGroupLayoutEntries::sequential( - ShaderStages::COMPUTE, - ( - storage_buffer::>(false), - texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), - ), +fn init_compute_pipeline( + mut commands: Commands, + render_device: Res, + asset_server: Res, + pipeline_cache: Res, +) { + let layout = render_device.create_bind_group_layout( + None, + &BindGroupLayoutEntries::sequential( + ShaderStages::COMPUTE, + ( + storage_buffer::>(false), + texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), ), - ); - let shader = world.load_asset(SHADER_ASSET_PATH); - let pipeline_cache = world.resource::(); - let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { - label: Some("GPU readback compute shader".into()), - layout: vec![layout.clone()], - shader: shader.clone(), - ..default() - }); - ComputePipeline { layout, pipeline } - } + ), + ); + let shader = asset_server.load(SHADER_ASSET_PATH); + let pipeline = pipeline_cache.queue_compute_pipeline(ComputePipelineDescriptor { + label: Some("GPU readback compute shader".into()), + layout: vec![layout.clone()], + shader: shader.clone(), + ..default() + }); + commands.insert_resource(ComputePipeline { layout, pipeline }); } /// Label to identify the node in the render graph diff --git a/examples/shader/specialized_mesh_pipeline.rs b/examples/shader/specialized_mesh_pipeline.rs index f9aebcda7e..3b5d95f0b5 100644 --- a/examples/shader/specialized_mesh_pipeline.rs +++ b/examples/shader/specialized_mesh_pipeline.rs @@ -39,7 +39,7 @@ use bevy::{ }, view::NoIndirectDrawing, view::{self, ExtractedView, RenderVisibleEntities, ViewTarget, VisibilityClass}, - Render, RenderApp, RenderSystems, + Render, RenderApp, RenderStartup, RenderSystems, }, }; @@ -118,20 +118,12 @@ impl Plugin for CustomRenderedMeshPipelinePlugin { .init_resource::>() // We need to use a custom draw command so we need to register it .add_render_command::() + .add_systems(RenderStartup, init_custom_mesh_pipeline) .add_systems( Render, queue_custom_mesh_pipeline.in_set(RenderSystems::Queue), ); } - - fn finish(&self, app: &mut App) { - let Some(render_app) = app.get_sub_app_mut(RenderApp) else { - return; - }; - // Creating this pipeline needs the RenderDevice and RenderQueue - // which are only available once rendering plugins are initialized. - render_app.init_resource::(); - } } /// A marker component that represents an entity that is to be rendered using @@ -174,15 +166,17 @@ struct CustomMeshPipeline { shader_handle: Handle, } -impl FromWorld for CustomMeshPipeline { - fn from_world(world: &mut World) -> Self { - // Load the shader - let shader_handle: Handle = world.resource::().load(SHADER_ASSET_PATH); - Self { - mesh_pipeline: MeshPipeline::from_world(world), - shader_handle, - } - } +fn init_custom_mesh_pipeline( + mut commands: Commands, + asset_server: Res, + mesh_pipeline: Res, +) { + // Load the shader + let shader_handle: Handle = asset_server.load(SHADER_ASSET_PATH); + commands.insert_resource(CustomMeshPipeline { + mesh_pipeline: mesh_pipeline.clone(), + shader_handle, + }); } impl SpecializedMeshPipeline for CustomMeshPipeline { diff --git a/examples/shader/texture_binding_array.rs b/examples/shader/texture_binding_array.rs index bfb439d81f..f18d08e8d3 100644 --- a/examples/shader/texture_binding_array.rs +++ b/examples/shader/texture_binding_array.rs @@ -13,7 +13,7 @@ use bevy::{ }, renderer::RenderDevice, texture::{FallbackImage, GpuImage}, - RenderApp, + RenderApp, RenderStartup, }, }; use std::{num::NonZero, process::exit}; @@ -40,28 +40,12 @@ const TILE_ID: [usize; 16] = [ struct GpuFeatureSupportChecker; impl Plugin for GpuFeatureSupportChecker { - fn build(&self, _app: &mut App) {} - - fn finish(&self, app: &mut App) { + fn build(&self, app: &mut App) { let Some(render_app) = app.get_sub_app_mut(RenderApp) else { return; }; - let render_device = render_app.world().resource::(); - - // Check if the device support the required feature. If not, exit the example. - // In a real application, you should setup a fallback for the missing feature - if !render_device - .features() - .contains(WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING) - { - error!( - "Render device doesn't support feature \ -SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, \ -which is required for texture binding arrays" - ); - exit(1); - } + render_app.add_systems(RenderStartup, verify_required_features); } } @@ -89,6 +73,22 @@ fn setup( )); } +fn verify_required_features(render_device: Res) { + // Check if the device support the required feature. If not, exit the example. In a real + // application, you should setup a fallback for the missing feature + if !render_device + .features() + .contains(WgpuFeatures::SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING) + { + error!( + "Render device doesn't support feature \ +SAMPLED_TEXTURE_AND_STORAGE_BUFFER_ARRAY_NON_UNIFORM_INDEXING, \ +which is required for texture binding arrays" + ); + exit(1); + } +} + #[derive(Asset, TypePath, Debug, Clone)] struct BindlessMaterial { textures: Vec>, diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 4bbc8e5d77..f7757d42ba 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -597,15 +597,15 @@ mod linear_gradient { ], ] { for color_space in [ - InterpolationColorSpace::LinearRgb, - InterpolationColorSpace::Srgb, - InterpolationColorSpace::OkLab, - InterpolationColorSpace::OkLch, - InterpolationColorSpace::OkLchLong, - InterpolationColorSpace::Hsl, - InterpolationColorSpace::HslLong, - InterpolationColorSpace::Hsv, - InterpolationColorSpace::HsvLong, + InterpolationColorSpace::LinearRgba, + InterpolationColorSpace::Srgba, + InterpolationColorSpace::Oklaba, + InterpolationColorSpace::Oklcha, + InterpolationColorSpace::OklchaLong, + InterpolationColorSpace::Hsla, + InterpolationColorSpace::HslaLong, + InterpolationColorSpace::Hsva, + InterpolationColorSpace::HsvaLong, ] { commands.spawn(( Node { diff --git a/examples/ui/gradients.rs b/examples/ui/gradients.rs index 82c69d4f8e..4167cbc1a1 100644 --- a/examples/ui/gradients.rs +++ b/examples/ui/gradients.rs @@ -232,32 +232,32 @@ fn setup(mut commands: Commands) { } }; *space = match *space { - InterpolationColorSpace::OkLab => { - InterpolationColorSpace::OkLch + InterpolationColorSpace::Oklaba => { + InterpolationColorSpace::Oklcha } - InterpolationColorSpace::OkLch => { - InterpolationColorSpace::OkLchLong + InterpolationColorSpace::Oklcha => { + InterpolationColorSpace::OklchaLong } - InterpolationColorSpace::OkLchLong => { - InterpolationColorSpace::Srgb + InterpolationColorSpace::OklchaLong => { + InterpolationColorSpace::Srgba } - InterpolationColorSpace::Srgb => { - InterpolationColorSpace::LinearRgb + InterpolationColorSpace::Srgba => { + InterpolationColorSpace::LinearRgba } - InterpolationColorSpace::LinearRgb => { - InterpolationColorSpace::Hsl + InterpolationColorSpace::LinearRgba => { + InterpolationColorSpace::Hsla } - InterpolationColorSpace::Hsl => { - InterpolationColorSpace::HslLong + InterpolationColorSpace::Hsla => { + InterpolationColorSpace::HslaLong } - InterpolationColorSpace::HslLong => { - InterpolationColorSpace::Hsv + InterpolationColorSpace::HslaLong => { + InterpolationColorSpace::Hsva } - InterpolationColorSpace::Hsv => { - InterpolationColorSpace::HsvLong + InterpolationColorSpace::Hsva => { + InterpolationColorSpace::HsvaLong } - InterpolationColorSpace::HsvLong => { - InterpolationColorSpace::OkLab + InterpolationColorSpace::HsvaLong => { + InterpolationColorSpace::Oklaba } }; current_space = *space; diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index 9bedb719d2..0ec6a0d748 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -15,14 +15,99 @@ fn main() { app.add_plugins(DefaultPlugins) .insert_resource(WinitSettings::desktop_app()) .add_systems(Startup, setup) - .add_systems(Update, update_scroll_position); + .add_systems(Update, send_scroll_events) + .add_observer(on_scroll_handler); app.run(); } -const FONT_SIZE: f32 = 20.; const LINE_HEIGHT: f32 = 21.; +/// Injects scroll events into the UI hierarchy. +fn send_scroll_events( + mut mouse_wheel_events: EventReader, + hover_map: Res, + keyboard_input: Res>, + mut commands: Commands, +) { + for event in mouse_wheel_events.read() { + let mut delta = -Vec2::new(event.x, event.y); + + if event.unit == MouseScrollUnit::Line { + delta *= LINE_HEIGHT; + } + + if keyboard_input.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight]) { + std::mem::swap(&mut delta.x, &mut delta.y); + } + + for pointer_map in hover_map.values() { + for entity in pointer_map.keys() { + commands.trigger_targets(Scroll { delta }, *entity); + } + } + } +} + +/// UI scrolling event. +#[derive(Event, EntityEvent, Debug)] +#[entity_event(auto_propagate, traversal = &'static ChildOf)] +struct Scroll { + /// Scroll delta in logical coordinates. + delta: Vec2, +} + +fn on_scroll_handler( + mut trigger: On, + mut query: Query<(&mut ScrollPosition, &Node, &ComputedNode)>, +) { + let target = trigger.target(); + let delta = &mut trigger.event_mut().delta; + + let Ok((mut scroll_position, node, computed)) = query.get_mut(target) else { + return; + }; + + let max_offset = (computed.content_size() - computed.size()) * computed.inverse_scale_factor(); + + if node.overflow.x == OverflowAxis::Scroll && delta.x != 0. { + // Is this node already scrolled all the way in the direction of the scroll? + let max = if delta.x > 0. { + scroll_position.x >= max_offset.x + } else { + scroll_position.x <= 0. + }; + + if !max { + scroll_position.x += delta.x; + // Consume the X portion of the scroll delta. + delta.x = 0.; + } + } + + if node.overflow.y == OverflowAxis::Scroll && delta.y != 0. { + // Is this node already scrolled all the way in the direction of the scroll? + let max = if delta.y > 0. { + scroll_position.y >= max_offset.y + } else { + scroll_position.y <= 0. + }; + + if !max { + scroll_position.y += delta.y; + // Consume the Y portion of the scroll delta. + delta.y = 0.; + } + } + + // Stop propagating when the delta is fully consumed. + if *delta == Vec2::ZERO { + trigger.propagate(false); + } +} + +const FONT_SIZE: f32 = 20.; + fn setup(mut commands: Commands, asset_server: Res) { // Camera commands.spawn((Camera2d, IsDefaultUiCamera)); @@ -39,7 +124,6 @@ fn setup(mut commands: Commands, asset_server: Res) { flex_direction: FlexDirection::Column, ..default() }) - .insert(Pickable::IGNORE) .with_children(|parent| { // horizontal scroll example parent @@ -83,16 +167,12 @@ fn setup(mut commands: Commands, asset_server: Res) { }, Label, AccessibilityNode(Accessible::new(Role::ListItem)), + Node { + min_width: Val::Px(200.), + align_content: AlignContent::Center, + ..default() + }, )) - .insert(Node { - min_width: Val::Px(200.), - align_content: AlignContent::Center, - ..default() - }) - .insert(Pickable { - should_block_lower: false, - ..default() - }) .observe( |trigger: On>, mut commands: Commands| { if trigger.event().button == PointerButton::Primary { @@ -159,10 +239,6 @@ fn vertically_scrolling_list(font_handle: Handle) -> impl Bundle { max_height: Val::Px(LINE_HEIGHT), ..default() }, - Pickable { - should_block_lower: false, - ..default() - }, children![( Text(format!("Item {i}")), TextFont { @@ -171,10 +247,6 @@ fn vertically_scrolling_list(font_handle: Handle) -> impl Bundle { }, Label, AccessibilityNode(Accessible::new(Role::ListItem)), - Pickable { - should_block_lower: false, - ..default() - } )], ) }))) @@ -217,7 +289,6 @@ fn bidirectional_scrolling_list(font_handle: Handle) -> impl Bundle { flex_direction: FlexDirection::Row, ..default() }, - Pickable::IGNORE, Children::spawn(SpawnIter((0..10).map({ let value = font_handle.clone(); move |i| { @@ -229,10 +300,6 @@ fn bidirectional_scrolling_list(font_handle: Handle) -> impl Bundle { }, Label, AccessibilityNode(Accessible::new(Role::ListItem)), - Pickable { - should_block_lower: false, - ..default() - }, ) } }))), @@ -264,45 +331,38 @@ fn nested_scrolling_list(font_handle: Handle) -> impl Bundle { Label, ), ( - // Outer, horizontal scrolling container + // Outer, bi-directional scrolling container Node { column_gap: Val::Px(20.), flex_direction: FlexDirection::Row, align_self: AlignSelf::Stretch, height: Val::Percent(50.), - overflow: Overflow::scroll_x(), // n.b. + overflow: Overflow::scroll(), ..default() }, BackgroundColor(Color::srgb(0.10, 0.10, 0.10)), // Inner, scrolling columns - Children::spawn(SpawnIter((0..30).map(move |oi| { + Children::spawn(SpawnIter((0..5).map(move |oi| { ( Node { flex_direction: FlexDirection::Column, align_self: AlignSelf::Stretch, + height: Val::Percent(200. / 5. * (oi as f32 + 1.)), overflow: Overflow::scroll_y(), ..default() }, BackgroundColor(Color::srgb(0.05, 0.05, 0.05)), - Pickable { - should_block_lower: false, - ..default() - }, - Children::spawn(SpawnIter((0..30).map({ + Children::spawn(SpawnIter((0..20).map({ let value = font_handle.clone(); move |i| { ( - Text(format!("Item {}", (oi * 25) + i)), + Text(format!("Item {}", (oi * 20) + i)), TextFont { font: value.clone(), ..default() }, Label, AccessibilityNode(Accessible::new(Role::ListItem)), - Pickable { - should_block_lower: false, - ..default() - }, ) } }))), @@ -312,36 +372,3 @@ fn nested_scrolling_list(font_handle: Handle) -> impl Bundle { ], ) } - -/// Updates the scroll position of scrollable nodes in response to mouse input -pub fn update_scroll_position( - mut mouse_wheel_events: EventReader, - hover_map: Res, - mut scrolled_node_query: Query<&mut ScrollPosition>, - keyboard_input: Res>, -) { - for mouse_wheel_event in mouse_wheel_events.read() { - let (mut dx, mut dy) = match mouse_wheel_event.unit { - MouseScrollUnit::Line => ( - mouse_wheel_event.x * LINE_HEIGHT, - mouse_wheel_event.y * LINE_HEIGHT, - ), - MouseScrollUnit::Pixel => (mouse_wheel_event.x, mouse_wheel_event.y), - }; - - if keyboard_input.pressed(KeyCode::ControlLeft) - || keyboard_input.pressed(KeyCode::ControlRight) - { - std::mem::swap(&mut dx, &mut dy); - } - - for (_pointer, pointer_map) in hover_map.iter() { - for (entity, _hit) in pointer_map.iter() { - if let Ok(mut scroll_position) = scrolled_node_query.get_mut(*entity) { - scroll_position.x -= dx; - scroll_position.y -= dy; - } - } - } - } -} diff --git a/examples/ui/size_constraints.rs b/examples/ui/size_constraints.rs index b67ece4ab0..8b376d329e 100644 --- a/examples/ui/size_constraints.rs +++ b/examples/ui/size_constraints.rs @@ -35,7 +35,7 @@ enum Constraint { #[derive(Copy, Clone, Component)] struct ButtonValue(Val); -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct ButtonActivatedEvent(Entity); fn setup(mut commands: Commands, asset_server: Res) { diff --git a/examples/window/custom_user_event.rs b/examples/window/custom_user_event.rs index d9e1895b98..85cff62eab 100644 --- a/examples/window/custom_user_event.rs +++ b/examples/window/custom_user_event.rs @@ -6,7 +6,7 @@ use bevy::{ }; use std::fmt::Formatter; -#[derive(Default, Debug, Event, BufferedEvent)] +#[derive(Default, Debug, BufferedEvent)] enum CustomEvent { #[default] WakeUp, diff --git a/release-content/migration-guides/pointer_target.md b/release-content/migration-guides/pointer_target.md index 2a5fc427ba..9122961647 100644 --- a/release-content/migration-guides/pointer_target.md +++ b/release-content/migration-guides/pointer_target.md @@ -13,7 +13,7 @@ As a workaround, you can transform any entity-event into a buffered event that c an observer than emits events. ```rust -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct TransformedEntityEvent { entity: Entity, event: E, diff --git a/release-content/migration-guides/render_startup.md b/release-content/migration-guides/render_startup.md index c1f20f78b3..d1b9b76052 100644 --- a/release-content/migration-guides/render_startup.md +++ b/release-content/migration-guides/render_startup.md @@ -20,6 +20,13 @@ The following are the (public) resources that are now initialized in `RenderStar - `UiPipeline` - `UiMaterialPipeline` - `UiTextureSlicePipeline` +- `VolumetricFogPipeline` +- `DeferredLightingLayout` +- `RenderLightmaps` +- `PrepassPipeline` +- `PrepassViewBindGroup` +- `Wireframe3dPipeline` +- `MaterialPipeline` The vast majority of cases for initializing render resources look like so (in Bevy 0.16): diff --git a/release-content/release-notes/event_split.md b/release-content/release-notes/event_split.md index e285d143f2..4ef947f368 100644 --- a/release-content/release-notes/event_split.md +++ b/release-content/release-notes/event_split.md @@ -1,7 +1,7 @@ --- title: Event Split authors: ["@Jondolf"] -pull_requests: [19647] +pull_requests: [19647, 20101] --- In past releases, all event types were defined by simply deriving the `Event` trait: @@ -28,9 +28,9 @@ or an observer event with `EventReader`, leaving the user wondering why the even **Bevy 0.17** aims to solve this ambiguity by splitting the event traits into `Event`, `EntityEvent`, and `BufferedEvent`. -- `Event`: A shared trait for all events. +- `Event`: A shared trait for observer events. - `EntityEvent`: An `Event` that additionally supports targeting specific entities and propagating the event from one entity to another. -- `BufferedEvent`: An `Event` that additionally supports usage with `EventReader` and `EventWriter` for pull-based event handling. +- `BufferedEvent`: An event that supports usage with `EventReader` and `EventWriter` for pull-based event handling. ## Using Events @@ -92,10 +92,10 @@ let armor_piece = commands commands.trigger_targets(Damage { amount: 10.0 }, armor_piece); ``` -To allow an event to be used with the buffered API, you can also derive `BufferedEvent`: +To allow an event to be used with the buffered API, you can instead derive `BufferedEvent`: ```rust -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct Message(String); ``` @@ -117,5 +117,5 @@ fn read_messages(mut reader: EventReader) { In summary: - Need a basic event you can trigger and observe? Derive `Event`! -- Need the event to be targeted at an entity? Derive `EntityEvent`! +- Need the observer event to be targeted at an entity? Derive `EntityEvent`! - Need the event to be buffered and support the `EventReader`/`EventWriter` API? Derive `BufferedEvent`! diff --git a/release-content/release-notes/faster-zstd-option.md b/release-content/release-notes/faster-zstd-option.md new file mode 100644 index 0000000000..01c4bc98c3 --- /dev/null +++ b/release-content/release-notes/faster-zstd-option.md @@ -0,0 +1,27 @@ +--- +title: Faster Zstd decompression option +authors: ["@atlv24", "@brianreavis"] +pull_requests: [19793] +--- + +There is now an option to use the [zstd](https://crates.io/crates/zstd) c-bindings instead of [ruzstd](https://crates.io/crates/ruzstd). +This is less safe and portable, but can be around 44% faster. + +The two features that control which one is used are `zstd_rust` and `zstd_c`. +`zstd_rust` is enabled by default, but `zstd_c` takes precedence if both are enabled. + +To enable it, add the feature to the `bevy` entry of your Cargo.toml: + +```toml +bevy = { version = "0.17.0", features = ["zstd_c"] } +``` + +Note: this will still include a dependency on `ruzstd`, because mutually exclusive features are not supported by Cargo. +To remove this dependency, disable default-features, and manually enable any default features you need: + +```toml +bevy = { version = "0.17.0", default-features = false, features = [ + "zstd_c", + "bevy_render", # etc.. +] } +``` diff --git a/release-content/release-notes/light-textures.md b/release-content/release-notes/light-textures.md new file mode 100644 index 0000000000..b586b04732 --- /dev/null +++ b/release-content/release-notes/light-textures.md @@ -0,0 +1,10 @@ +--- +title: Light Textures +authors: ["@robtfm"] +pull_requests: [18031] +--- + +New components `PointLightTexture`, `SpotLightTexture`, and `DirectionalLightTexture` allow specifying light textures for lights, also commonly known as light cookies. +These modulate the intensity of light cast upon surfaces for various artistic effects. See the light_textures example for usage. + +(TODO: Embed light_textures example screenshot here) diff --git a/tests/how_to_test_systems.rs b/tests/how_to_test_systems.rs index d17753b346..d9c5127511 100644 --- a/tests/how_to_test_systems.rs +++ b/tests/how_to_test_systems.rs @@ -7,7 +7,7 @@ struct Enemy { score_value: u32, } -#[derive(Event, BufferedEvent)] +#[derive(BufferedEvent)] struct EnemyDied(u32); #[derive(Resource)]