Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Immutable Component Support #16372

Merged
merged 34 commits into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
e822fe0
Add Immutable Component Support
bushrat011899 Nov 13, 2024
9d00974
Fix Examples
bushrat011899 Nov 13, 2024
fdd3b35
Fixed benchmarks and documentation
bushrat011899 Nov 13, 2024
d895c9a
Formatting
bushrat011899 Nov 13, 2024
24619f9
CI fixes, replace #[immutable] with #[component(immutable)]
bushrat011899 Nov 13, 2024
3ab5e97
Fix docs
bushrat011899 Nov 13, 2024
ac328b1
Fix Docs
bushrat011899 Nov 13, 2024
cae92e5
Updated `immutable_components` Example
bushrat011899 Nov 13, 2024
ef1802c
Added `ComponentImmutable` trait
bushrat011899 Nov 13, 2024
d71fae0
Simplify reflection story
bushrat011899 Nov 13, 2024
4050f62
Added immutability support to dynamic components
bushrat011899 Nov 13, 2024
9511f8c
Fixed overly restrictive traits bounds in component reflection
bushrat011899 Nov 13, 2024
d837843
Revert change to `bevy_scene`
bushrat011899 Nov 13, 2024
c353a74
Response to Feedback
bushrat011899 Nov 14, 2024
462bbe8
Added `immutable_dynamic_components` Example
bushrat011899 Nov 14, 2024
e49dd27
Update crates/bevy_ecs/src/component.rs
bushrat011899 Nov 14, 2024
2ac50cf
Update crates/bevy_ecs/src/world/deferred_world.rs
bushrat011899 Nov 14, 2024
07f0892
Update crates/bevy_ecs/src/world/deferred_world.rs
bushrat011899 Nov 14, 2024
42c531b
Adjust `ComponentInfo` and `ComponentDescriptor` to store `mutable`
bushrat011899 Nov 14, 2024
72c660d
Update crates/bevy_ecs/src/world/unsafe_world_cell.rs
bushrat011899 Nov 15, 2024
1ff87eb
Typo in comment
alice-i-cecile Nov 17, 2024
0699a9b
Remove Redundant `tale`/`insert`
bushrat011899 Nov 17, 2024
c38c227
Improve `ReflectComponent` `panic` messages
bushrat011899 Nov 17, 2024
83f5b0f
Update `immutable_components` Example `unreachable` messages
bushrat011899 Nov 17, 2024
7794cfb
Updated documentation for newly panicking `ReflectComponent` methods
bushrat011899 Nov 17, 2024
edb27c8
Not being a hypocrite
bushrat011899 Nov 17, 2024
2dad7a5
Merge remote-tracking branch 'upstream/main' into ImmutableComponents
bushrat011899 Dec 3, 2024
60b8d55
Moved immutable component documentation
bushrat011899 Dec 3, 2024
e8ec322
Removed `new_immutable_with_layout`
bushrat011899 Dec 3, 2024
ded6561
Remove `Mutable` and `Immutable` from prelude
bushrat011899 Dec 3, 2024
1326909
Merge `immutable_components_dynamic` into `immutable_components`
bushrat011899 Dec 3, 2024
045f764
Merge remote-tracking branch 'upstream/main' into ImmutableComponents
bushrat011899 Dec 3, 2024
611386b
Merge remote-tracking branch 'upstream/main' into ImmutableComponents
bushrat011899 Dec 3, 2024
ab82afb
Fix documentation
bushrat011899 Dec 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1954,6 +1954,17 @@ description = "Creates a hierarchy of parents and children entities"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "immutable_components"
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
path = "examples/ecs/immutable_components.rs"
doc-scrape-examples = true

[package.metadata.example.immutable_components]
name = "Immutable Components"
description = "Demonstrates the creation and utility of immutable components"
category = "ECS (Entity Component System)"
wasm = false

[[example]]
name = "iter_combinations"
path = "examples/ecs/iter_combinations.rs"
Expand Down
12 changes: 6 additions & 6 deletions benches/benches/bevy_ecs/change_detection.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use bevy_ecs::{
component::Component,
component::{Component, Mutable},
entity::Entity,
prelude::{Added, Changed, EntityWorldMut, QueryState},
query::QueryFilter,
Expand Down Expand Up @@ -124,7 +124,7 @@ fn all_added_detection(criterion: &mut Criterion) {
}
}

fn all_changed_detection_generic<T: Component + Default + BenchModify>(
fn all_changed_detection_generic<T: Component<Mutability = Mutable> + Default + BenchModify>(
group: &mut BenchGroup,
entity_count: u32,
) {
Expand Down Expand Up @@ -172,7 +172,7 @@ fn all_changed_detection(criterion: &mut Criterion) {
}
}

fn few_changed_detection_generic<T: Component + Default + BenchModify>(
fn few_changed_detection_generic<T: Component<Mutability = Mutable> + Default + BenchModify>(
group: &mut BenchGroup,
entity_count: u32,
) {
Expand Down Expand Up @@ -222,7 +222,7 @@ fn few_changed_detection(criterion: &mut Criterion) {
}
}

fn none_changed_detection_generic<T: Component + Default>(
fn none_changed_detection_generic<T: Component<Mutability = Mutable> + Default>(
group: &mut BenchGroup,
entity_count: u32,
) {
Expand Down Expand Up @@ -271,7 +271,7 @@ fn insert_if_bit_enabled<const B: u16>(entity: &mut EntityWorldMut, i: u16) {
}
}

fn add_archetypes_entities<T: Component + Default>(
fn add_archetypes_entities<T: Component<Mutability = Mutable> + Default>(
world: &mut World,
archetype_count: u16,
entity_count: u32,
Expand All @@ -298,7 +298,7 @@ fn add_archetypes_entities<T: Component + Default>(
}
}
}
fn multiple_archetype_none_changed_detection_generic<T: Component + Default + BenchModify>(
fn multiple_archetype_none_changed_detection_generic<T: Component<Mutability = Mutable> + Default + BenchModify>(
group: &mut BenchGroup,
archetype_count: u16,
entity_count: u32,
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_animation/src/animation_curves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ use core::{
marker::PhantomData,
};

use bevy_ecs::component::Component;
use bevy_ecs::component::{Component, Mutable};
use bevy_math::curve::{
cores::{UnevenCore, UnevenCoreError},
iterable::IterableCurve,
Expand Down Expand Up @@ -221,7 +221,7 @@ pub struct AnimatedField<C, A, F: Fn(&mut C) -> &mut A> {

impl<C, A, F> AnimatableProperty for AnimatedField<C, A, F>
where
C: Component,
C: Component<Mutability = Mutable>,
A: Animatable + Clone + Sync + Debug,
F: Fn(&mut C) -> &mut A + Send + Sync + 'static,
{
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_core_pipeline/src/oit/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ impl Default for OrderIndependentTransparencySettings {
// we can hook on_add to issue a warning in case `layer_count` is seemingly too high.
impl Component for OrderIndependentTransparencySettings {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;

fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_add(|world, entity, _| {
Expand Down
14 changes: 14 additions & 0 deletions crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ pub fn derive_event(input: TokenStream) -> TokenStream {

impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #bevy_ecs_path::component::StorageType::SparseSet;
type Mutability = #bevy_ecs_path::component::Mutable;
}
})
}
Expand Down Expand Up @@ -127,11 +128,17 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
let struct_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();

let mutable_type = attrs
.immutable
.then_some(quote! { #bevy_ecs_path::component::Immutable })
.unwrap_or(quote! { #bevy_ecs_path::component::Mutable });

// This puts `register_required` before `register_recursive_requires` to ensure that the constructors of _all_ top
// level components are initialized first, giving them precedence over recursively defined constructors for the same component type
TokenStream::from(quote! {
impl #impl_generics #bevy_ecs_path::component::Component for #struct_name #type_generics #where_clause {
const STORAGE_TYPE: #bevy_ecs_path::component::StorageType = #storage;
type Mutability = #mutable_type;
fn register_required_components(
requiree: #bevy_ecs_path::component::ComponentId,
components: &mut #bevy_ecs_path::component::Components,
Expand Down Expand Up @@ -191,13 +198,16 @@ pub const ON_INSERT: &str = "on_insert";
pub const ON_REPLACE: &str = "on_replace";
pub const ON_REMOVE: &str = "on_remove";

pub const IMMUTABLE: &str = "immutable";

struct Attrs {
storage: StorageTy,
requires: Option<Punctuated<Require, Comma>>,
on_add: Option<ExprPath>,
on_insert: Option<ExprPath>,
on_replace: Option<ExprPath>,
on_remove: Option<ExprPath>,
immutable: bool,
}

#[derive(Clone, Copy)]
Expand Down Expand Up @@ -228,6 +238,7 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
on_replace: None,
on_remove: None,
requires: None,
immutable: false,
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
};

let mut require_paths = HashSet::new();
Expand Down Expand Up @@ -257,6 +268,9 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
} else if nested.path.is_ident(ON_REMOVE) {
attrs.on_remove = Some(nested.value()?.parse::<ExprPath>()?);
Ok(())
} else if nested.path.is_ident(IMMUTABLE) {
attrs.immutable = true;
Ok(())
} else {
Err(nested.error("Unsupported attribute"))
}
Expand Down
88 changes: 88 additions & 0 deletions crates/bevy_ecs/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,18 @@ pub use bevy_ecs_macros::require;
///
/// # Component and data access
///
/// Components can be marked as immutable by adding the `#[component(immutable)]`
/// attribute when using the derive macro.
/// See the documentation for [`ComponentMutability`] for more details around this
/// feature.
///
/// See the [`entity`] module level documentation to learn how to add or remove components from an entity.
///
/// See the documentation for [`Query`] to learn how to access component data from a system.
///
/// [`entity`]: crate::entity#usage
/// [`Query`]: crate::system::Query
/// [`ComponentMutability`]: crate::component::ComponentMutability
///
/// # Choosing a storage type
///
Expand Down Expand Up @@ -380,6 +386,14 @@ pub trait Component: Send + Sync + 'static {
/// A constant indicating the storage type used for this component.
const STORAGE_TYPE: StorageType;

/// A marker type to assist Bevy with determining if this component is
/// mutable, or immutable. Mutable components will have [`Component<Mutability = Mutable>`],
/// while immutable components will instead have [`Component<Mutability = Immutable>`].
///
/// * For a component to be mutable, this type must be [`Mutable`].
/// * For a component to be immutable, this type must be [`Immutable`].
type Mutability: ComponentMutability;

/// Called when registering this component, allowing mutable access to its [`ComponentHooks`].
fn register_component_hooks(_hooks: &mut ComponentHooks) {}

Expand All @@ -401,6 +415,61 @@ pub trait Component: Send + Sync + 'static {
}
}

mod private {
pub trait Seal {}
}

/// The mutability option for a [`Component`]. This can either be:
bushrat011899 marked this conversation as resolved.
Show resolved Hide resolved
/// * [`Mutable`]
/// * [`Immutable`]
///
/// This is controlled through either [`Component::Mutability`] or `#[component(immutable)]`
/// when using the derive macro.
///
/// Immutable components are guaranteed to never have an exclusive reference,
/// `&mut ...`, created while inserted onto an entity.
/// In all other ways, they are identical to mutable components.
/// This restriction allows hooks to observe all changes made to an immutable
/// component, effectively turning the `OnInsert` and `OnReplace` hooks into a
/// `OnMutate` hook.
/// This is not practical for mutable components, as the runtime cost of invoking
/// a hook for every exclusive reference created would be far too high.
///
/// # Examples
///
/// ```rust
/// # use bevy_ecs::component::Component;
/// #
/// #[derive(Component)]
/// #[component(immutable)]
/// struct ImmutableFoo;
/// ```
pub trait ComponentMutability: private::Seal + 'static {
/// Boolean to indicate if this mutability setting implies a mutable or immutable
/// component.
const MUTABLE: bool;
}

/// Parameter indicating a [`Component`] is immutable.
///
/// See [`ComponentMutability`] for details.
pub struct Immutable;

impl private::Seal for Immutable {}
impl ComponentMutability for Immutable {
const MUTABLE: bool = false;
}

/// Parameter indicating a [`Component`] is mutable.
///
/// See [`ComponentMutability`] for details.
pub struct Mutable;

impl private::Seal for Mutable {}
impl ComponentMutability for Mutable {
const MUTABLE: bool = true;
}

/// The storage used for a specific component type.
///
/// # Examples
Expand Down Expand Up @@ -626,6 +695,12 @@ impl ComponentInfo {
&self.descriptor.name
}

/// Returns `true` if the current component is mutable.
#[inline]
pub fn mutable(&self) -> bool {
self.descriptor.mutable
}

/// Returns the [`TypeId`] of the underlying component type.
/// Returns `None` if the component does not correspond to a Rust type.
#[inline]
Expand Down Expand Up @@ -778,6 +853,7 @@ pub struct ComponentDescriptor {
// this descriptor describes.
// None if the underlying type doesn't need to be dropped
drop: Option<for<'a> unsafe fn(OwningPtr<'a>)>,
mutable: bool,
}

// We need to ignore the `drop` field in our `Debug` impl
Expand All @@ -789,6 +865,7 @@ impl Debug for ComponentDescriptor {
.field("is_send_and_sync", &self.is_send_and_sync)
.field("type_id", &self.type_id)
.field("layout", &self.layout)
.field("mutable", &self.mutable)
.finish()
}
}
Expand All @@ -813,6 +890,7 @@ impl ComponentDescriptor {
type_id: Some(TypeId::of::<T>()),
layout: Layout::new::<T>(),
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: T::Mutability::MUTABLE,
}
}

Expand All @@ -826,6 +904,7 @@ impl ComponentDescriptor {
storage_type: StorageType,
layout: Layout,
drop: Option<for<'a> unsafe fn(OwningPtr<'a>)>,
mutable: bool,
) -> Self {
Self {
name: name.into(),
Expand All @@ -834,6 +913,7 @@ impl ComponentDescriptor {
type_id: None,
layout,
drop,
mutable,
}
}

Expand All @@ -850,6 +930,7 @@ impl ComponentDescriptor {
type_id: Some(TypeId::of::<T>()),
layout: Layout::new::<T>(),
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: true,
}
}

Expand All @@ -861,6 +942,7 @@ impl ComponentDescriptor {
type_id: Some(TypeId::of::<T>()),
layout: Layout::new::<T>(),
drop: needs_drop::<T>().then_some(Self::drop_ptr::<T> as _),
mutable: true,
}
}

Expand All @@ -882,6 +964,12 @@ impl ComponentDescriptor {
pub fn name(&self) -> &str {
self.name.as_ref()
}

/// Returns whether this component is mutable.
#[inline]
pub fn mutable(&self) -> bool {
self.mutable
}
}

/// Function type that can be used to clone an entity.
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_ecs/src/entity/clone_entities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,13 @@ impl EntityCloner {
/// Here's an example of how to do it using [`get_component_clone_handler`](Component::get_component_clone_handler):
/// ```
/// # use bevy_ecs::prelude::*;
/// # use bevy_ecs::component::{StorageType, component_clone_via_clone, ComponentCloneHandler};
/// # use bevy_ecs::component::{StorageType, component_clone_via_clone, ComponentCloneHandler, Mutable};
/// #[derive(Clone)]
/// struct SomeComponent;
///
/// impl Component for SomeComponent {
/// const STORAGE_TYPE: StorageType = StorageType::Table;
/// type Mutability = Mutable;
/// fn get_component_clone_handler() -> ComponentCloneHandler {
/// ComponentCloneHandler::Custom(component_clone_via_clone::<Self>)
/// }
Expand Down
3 changes: 2 additions & 1 deletion crates/bevy_ecs/src/observer/entity_observer.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
component::{Component, ComponentCloneHandler, ComponentHooks, StorageType},
component::{Component, ComponentCloneHandler, ComponentHooks, Mutable, StorageType},
entity::{Entity, EntityCloneBuilder, EntityCloner},
observer::ObserverState,
world::{DeferredWorld, World},
Expand All @@ -11,6 +11,7 @@ pub(crate) struct ObservedBy(pub(crate) Vec<Entity>);

impl Component for ObservedBy {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;

fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_remove(|mut world, entity, _| {
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_ecs/src/observer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,7 +456,7 @@ impl World {
// Populate ObservedBy for each observed entity.
for watched_entity in &(*observer_state).descriptor.entities {
let mut entity_mut = self.entity_mut(*watched_entity);
let mut observed_by = entity_mut.entry::<ObservedBy>().or_default();
let mut observed_by = entity_mut.entry::<ObservedBy>().or_default().into_mut();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the consequence of changes made to the entry API for components. Ideally, we would return Mut<T> when calling or_default() on a mutable component, and &T on an immutable component (and likewise for other entry methods). The problem is that would require specialisation, since the Rust compiler has no way of knowing that Component<Mutability = Mutable> and Component<Mutability = Immutable> are mutually exclusive traits.

Since we already have the OccupiedEntry type, I decided to return that for all relevant operations instead, since it already has methods to either get a im/mutable reference to the underlying component. The alternatives would either be to not allow the entry API for immutable components (bad), or duplicate all the methods (or_default_immutable(), etc.)

observed_by.0.push(observer_entity);
}
(&*observer_state, &mut self.archetypes, &mut self.observers)
Expand Down
4 changes: 3 additions & 1 deletion crates/bevy_ecs/src/observer/runner.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use core::any::Any;

use crate::{
component::{ComponentHook, ComponentHooks, ComponentId, StorageType},
component::{ComponentHook, ComponentHooks, ComponentId, Mutable, StorageType},
observer::{ObserverDescriptor, ObserverTrigger},
prelude::*,
query::DebugCheckedUnwrap,
Expand Down Expand Up @@ -62,6 +62,7 @@ impl ObserverState {

impl Component for ObserverState {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;

fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_add(|mut world, entity, _| {
Expand Down Expand Up @@ -314,6 +315,7 @@ impl Observer {

impl Component for Observer {
const STORAGE_TYPE: StorageType = StorageType::SparseSet;
type Mutability = Mutable;
fn register_component_hooks(hooks: &mut ComponentHooks) {
hooks.on_add(|world, entity, _id| {
let Some(observe) = world.get::<Self>(entity) else {
Expand Down
Loading
Loading