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

[Merged by Bors] - Refactor ECS to reduce the dependency on a 1-to-1 mapping between components and real rust types #2490

Closed
wants to merge 8 commits into from
20 changes: 11 additions & 9 deletions crates/bevy_ecs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,15 +110,15 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
.map(|field| &field.ty)
.collect::<Vec<_>>();

let mut field_type_infos = Vec::new();
let mut field_component_ids = Vec::new();
let mut field_get_components = Vec::new();
let mut field_from_components = Vec::new();
for ((field_type, is_bundle), field) in
field_type.iter().zip(is_bundle.iter()).zip(field.iter())
{
if *is_bundle {
field_type_infos.push(quote! {
type_info.extend(<#field_type as #ecs_path::bundle::Bundle>::type_info());
field_component_ids.push(quote! {
component_ids.extend(<#field_type as #ecs_path::bundle::Bundle>::component_ids(components));
});
field_get_components.push(quote! {
self.#field.get_components(&mut func);
Expand All @@ -127,8 +127,8 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
#field: <#field_type as #ecs_path::bundle::Bundle>::from_components(&mut func),
});
} else {
field_type_infos.push(quote! {
type_info.push(#ecs_path::component::TypeInfo::of::<#field_type>());
field_component_ids.push(quote! {
component_ids.push(components.get_or_insert_id::<#field_type>());
});
field_get_components.push(quote! {
func((&mut self.#field as *mut #field_type).cast::<u8>());
Expand All @@ -147,10 +147,12 @@ pub fn derive_bundle(input: TokenStream) -> TokenStream {
TokenStream::from(quote! {
/// SAFE: TypeInfo is returned in field-definition-order. [from_components] and [get_components] use field-definition-order
unsafe impl #impl_generics #ecs_path::bundle::Bundle for #struct_name#ty_generics #where_clause {
fn type_info() -> Vec<#ecs_path::component::TypeInfo> {
let mut type_info = Vec::with_capacity(#field_len);
#(#field_type_infos)*
type_info
fn component_ids(
components: &mut #ecs_path::component::Components,
) -> Vec<#ecs_path::component::ComponentId> {
let mut component_ids = Vec::with_capacity(#field_len);
#(#field_component_ids)*
component_ids
}

#[allow(unused_variables, unused_mut, non_snake_case)]
Expand Down
43 changes: 23 additions & 20 deletions crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ pub use bevy_ecs_macros::Bundle;

use crate::{
archetype::ComponentStatus,
component::{Component, ComponentId, ComponentTicks, Components, StorageType, TypeInfo},
component::{Component, ComponentId, ComponentTicks, Components, StorageType},
entity::Entity,
storage::{SparseSetIndex, SparseSets, Table},
};
Expand Down Expand Up @@ -38,13 +38,13 @@ use std::{any::TypeId, collections::HashMap};
/// ```
///
/// # Safety
/// [Bundle::type_info] must return the TypeInfo for each component type in the bundle, in the
/// [Bundle::component_id] must return the ComponentId for each component type in the bundle, in the
/// _exact_ order that [Bundle::get_components] is called.
/// [Bundle::from_components] must call `func` exactly once for each [TypeInfo] returned by
/// [Bundle::type_info]
/// [Bundle::from_components] must call `func` exactly once for each [ComponentId] returned by
/// [Bundle::component_id]
pub unsafe trait Bundle: Send + Sync + 'static {
/// Gets this [Bundle]'s components type info, in the order of this bundle's Components
fn type_info() -> Vec<TypeInfo>;
/// Gets this [Bundle]'s component ids, in the order of this bundle's Components
fn component_ids(components: &mut Components) -> Vec<ComponentId>;

/// Calls `func`, which should return data for each component in the bundle, in the order of
/// this bundle's Components
Expand All @@ -66,8 +66,9 @@ macro_rules! tuple_impl {
($($name: ident),*) => {
/// SAFE: TypeInfo is returned in tuple-order. [Bundle::from_components] and [Bundle::get_components] use tuple-order
unsafe impl<$($name: Component),*> Bundle for ($($name,)*) {
fn type_info() -> Vec<TypeInfo> {
vec![$(TypeInfo::of::<$name>()),*]
#[allow(unused_variables)]
fn component_ids(components: &mut Components) -> Vec<ComponentId> {
vec![$(components.get_or_insert_id::<$name>()),*]
}

#[allow(unused_variables, unused_mut)]
Expand Down Expand Up @@ -205,10 +206,12 @@ impl Bundles {
) -> &'a BundleInfo {
let bundle_infos = &mut self.bundle_infos;
let id = self.bundle_ids.entry(TypeId::of::<T>()).or_insert_with(|| {
let type_info = T::type_info();
let component_ids = T::component_ids(components);
let id = BundleId(bundle_infos.len());
let bundle_info =
initialize_bundle(std::any::type_name::<T>(), &type_info, id, components);
// SAFE: T::component_id ensures info was created
let bundle_info = unsafe {
initialize_bundle(std::any::type_name::<T>(), component_ids, id, components)
};
bundle_infos.push(bundle_info);
id
});
Expand All @@ -217,21 +220,21 @@ impl Bundles {
}
}

fn initialize_bundle(
/// # Safety
///
/// `component_id` must be valid [ComponentId]'s
unsafe fn initialize_bundle(
bundle_type_name: &'static str,
type_info: &[TypeInfo],
component_ids: Vec<ComponentId>,
id: BundleId,
components: &mut Components,
) -> BundleInfo {
let mut component_ids = Vec::new();
let mut storage_types = Vec::new();

for type_info in type_info {
let component_id = components.get_or_insert_with(type_info.type_id(), || type_info.clone());
// SAFE: get_with_type_info ensures info was created
let info = unsafe { components.get_info_unchecked(component_id) };
component_ids.push(component_id);
storage_types.push(info.storage_type());
for &component_id in &component_ids {
// SAFE: component_id exists and is therefore valid
let component_info = components.get_info_unchecked(component_id);
storage_types.push(component_info.storage_type());
}

let mut deduped = component_ids.clone();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
mod type_info;

pub use type_info::*;

use crate::storage::SparseSetIndex;
use std::{
alloc::Layout,
Expand Down Expand Up @@ -56,15 +52,8 @@ impl Default for StorageType {

#[derive(Debug)]
pub struct ComponentInfo {
name: String,
id: ComponentId,
type_id: Option<TypeId>,
// SAFETY: This must remain private. It must only be set to "true" if this component is
// actually Send + Sync
is_send_and_sync: bool,
layout: Layout,
drop: unsafe fn(*mut u8),
storage_type: StorageType,
descriptor: ComponentDescriptor,
}

impl ComponentInfo {
Expand All @@ -75,44 +64,36 @@ impl ComponentInfo {

#[inline]
pub fn name(&self) -> &str {
&self.name
&self.descriptor.name
}

#[inline]
pub fn type_id(&self) -> Option<TypeId> {
self.type_id
self.descriptor.type_id
}

#[inline]
pub fn layout(&self) -> Layout {
self.layout
self.descriptor.layout
}

#[inline]
pub fn drop(&self) -> unsafe fn(*mut u8) {
self.drop
self.descriptor.drop
}

#[inline]
pub fn storage_type(&self) -> StorageType {
self.storage_type
self.descriptor.storage_type
}

#[inline]
pub fn is_send_and_sync(&self) -> bool {
self.is_send_and_sync
self.descriptor.is_send_and_sync
}

fn new(id: ComponentId, descriptor: ComponentDescriptor) -> Self {
ComponentInfo {
id,
name: descriptor.name,
storage_type: descriptor.storage_type,
type_id: descriptor.type_id,
is_send_and_sync: descriptor.is_send_and_sync,
drop: descriptor.drop,
layout: descriptor.layout,
}
ComponentInfo { id, descriptor }
}
}

Expand Down Expand Up @@ -142,6 +123,7 @@ impl SparseSetIndex for ComponentId {
}
}

#[derive(Debug)]
pub struct ComponentDescriptor {
name: String,
storage_type: StorageType,
Expand All @@ -154,14 +136,30 @@ pub struct ComponentDescriptor {
}

impl ComponentDescriptor {
// SAFETY: The pointer points to a valid value of type `T` and it is safe to drop this value.
unsafe fn drop_ptr<T>(x: *mut u8) {
x.cast::<T>().drop_in_place()
}

pub fn new<T: Component>(storage_type: StorageType) -> Self {
Self {
name: std::any::type_name::<T>().to_string(),
storage_type,
is_send_and_sync: true,
type_id: Some(TypeId::of::<T>()),
layout: Layout::new::<T>(),
drop: TypeInfo::drop_ptr::<T>,
drop: Self::drop_ptr::<T>,
}
}

fn new_non_send<T: Any>(storage_type: StorageType) -> Self {
Self {
name: std::any::type_name::<T>().to_string(),
storage_type,
is_send_and_sync: false,
type_id: Some(TypeId::of::<T>()),
layout: Layout::new::<T>(),
drop: Self::drop_ptr::<T>,
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Making this public may help with #2486, but there are probably other places that need to be changed too for it to work.

}

Expand All @@ -181,19 +179,6 @@ impl ComponentDescriptor {
}
}

impl From<TypeInfo> for ComponentDescriptor {
fn from(type_info: TypeInfo) -> Self {
Self {
name: type_info.type_name().to_string(),
storage_type: StorageType::default(),
is_send_and_sync: type_info.is_send_and_sync(),
type_id: Some(type_info.type_id()),
drop: type_info.drop(),
layout: type_info.layout(),
}
}
}

#[derive(Debug, Default)]
pub struct Components {
components: Vec<ComponentInfo>,
Expand Down Expand Up @@ -231,7 +216,12 @@ impl Components {

#[inline]
pub fn get_or_insert_id<T: Component>(&mut self) -> ComponentId {
self.get_or_insert_with(TypeId::of::<T>(), TypeInfo::of::<T>)
// SAFE: The [`ComponentDescriptor`] matches the [`TypeId`]
unsafe {
self.get_or_insert_with(TypeId::of::<T>(), || {
ComponentDescriptor::new::<T>(StorageType::default())
})
}
}

#[inline]
Expand Down Expand Up @@ -279,42 +269,58 @@ impl Components {

#[inline]
pub fn get_or_insert_resource_id<T: Component>(&mut self) -> ComponentId {
self.get_or_insert_resource_with(TypeId::of::<T>(), TypeInfo::of::<T>)
// SAFE: The [`ComponentDescriptor`] matches the [`TypeId`]
unsafe {
self.get_or_insert_resource_with(TypeId::of::<T>(), || {
ComponentDescriptor::new::<T>(StorageType::default())
})
}
}

#[inline]
pub fn get_or_insert_non_send_resource_id<T: Any>(&mut self) -> ComponentId {
self.get_or_insert_resource_with(TypeId::of::<T>(), TypeInfo::of_non_send_and_sync::<T>)
// SAFE: The [`ComponentDescriptor`] matches the [`TypeId`]
unsafe {
self.get_or_insert_resource_with(TypeId::of::<T>(), || {
ComponentDescriptor::new_non_send::<T>(StorageType::default())
})
}
}

/// # Safety
///
/// The [`ComponentDescriptor`] must match the [`TypeId`]
#[inline]
fn get_or_insert_resource_with(
unsafe fn get_or_insert_resource_with(
&mut self,
type_id: TypeId,
func: impl FnOnce() -> TypeInfo,
func: impl FnOnce() -> ComponentDescriptor,
Copy link
Member

Choose a reason for hiding this comment

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

do we know if perf is actually worse without this func param? can we just take a ComponentDescriptor always and drop the type_id arg?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ComponentDescriptor allocates a String for the type name. That is not something you want in the hot path.

Copy link
Member

Choose a reason for hiding this comment

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

ah right, the string... rip

Copy link
Member

Choose a reason for hiding this comment

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

type_name returns a &'static str though, seems like we could atleast use Cow<&'static str> here. Wouldn't be the best for custom names though :frown:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cow<'static, str> would work. The name is not frequently used, so the extra branch shouldn't hurt. I will leave that for another PR though I think.

Copy link
Member

Choose a reason for hiding this comment

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

I think personally I'd rather drop the unsafe, use a Cow and then always take a ComponentDescriptor

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ComponentDescriptor is 72 bytes with String and 80 bytes with Cow, which is relatively large. https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=93f494d4a982196b786f0f7d7f69c895

Copy link
Member

@BoxyUwU BoxyUwU Jul 20, 2021

Choose a reason for hiding this comment

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

I dont know if that actually tells us anything about the performance here though? Can we run benches or whatever and see

) -> ComponentId {
let components = &mut self.components;
let index = self.resource_indices.entry(type_id).or_insert_with(|| {
let type_info = func();
let descriptor = func();
let index = components.len();
components.push(ComponentInfo::new(ComponentId(index), type_info.into()));
components.push(ComponentInfo::new(ComponentId(index), descriptor));
index
});

ComponentId(*index)
}

/// # Safety
///
/// The [`ComponentDescriptor`] must match the [`TypeId`]
#[inline]
pub(crate) fn get_or_insert_with(
pub(crate) unsafe fn get_or_insert_with(
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 method was already de-facto unsafe. Nobody checked that the TypeId in the TypeInfo matched the type_id argument.

&mut self,
type_id: TypeId,
func: impl FnOnce() -> TypeInfo,
func: impl FnOnce() -> ComponentDescriptor,
) -> ComponentId {
let components = &mut self.components;
let index = self.indices.entry(type_id).or_insert_with(|| {
let type_info = func();
let descriptor = func();
let index = components.len();
components.push(ComponentInfo::new(ComponentId(index), type_info.into()));
components.push(ComponentInfo::new(ComponentId(index), descriptor));
index
});

Expand Down
Loading