Skip to content

Commit

Permalink
Add no_std support to bevy_ecs (#16758)
Browse files Browse the repository at this point in the history
# Objective

- Contributes to #15460

## Solution

- Added the following features:
  - `std` (default)
  - `async_executor` (default)
  - `edge_executor`
  - `critical-section`
  - `portable-atomic`
- Gated `tracing` in `bevy_utils` to allow compilation on certain
platforms
- Switched from `tracing` to `log` for simple message logging within
`bevy_ecs`. Note that `tracing` supports capturing from `log` so this
should be an uncontroversial change.
- Fixed imports and added feature gates as required 
- Made `bevy_tasks` optional within `bevy_ecs`. Turns out it's only
needed for parallel operations which are already gated behind
`multi_threaded` anyway.

## Testing

- Added to `compile-check-no-std` CI command
- `cargo check -p bevy_ecs --no-default-features --features
edge_executor,critical-section,portable-atomic --target
thumbv6m-none-eabi`
- `cargo check -p bevy_ecs --no-default-features --features
edge_executor,critical-section`
- `cargo check -p bevy_ecs --no-default-features`

## Draft Release Notes

Bevy's core ECS now supports `no_std` platforms.

In prior versions of Bevy, it was not possible to work with embedded or
niche platforms due to our reliance on the standard library, `std`. This
has blocked a number of novel use-cases for Bevy, such as an embedded
database for IoT devices, or for creating games on retro consoles.

With this release, `bevy_ecs` no longer requires `std`. To use Bevy on a
`no_std` platform, you must disable default features and enable the new
`edge_executor` and `critical-section` features. You may also need to
enable `portable-atomic` and `critical-section` if your platform does
not natively support all atomic types and operations used by Bevy.

```toml
[dependencies]
bevy_ecs = { version = "0.16", default-features = false, features = [
  # Required for platforms with incomplete atomics (e.g., Raspberry Pi Pico)
  "portable-atomic",
  "critical-section",

  # Optional
  "bevy_reflect",
  "serialize",
  "bevy_debug_stepping",
  "edge_executor"
] }
```

Currently, this has been tested on bare-metal x86 and the Raspberry Pi
Pico. If you have trouble using `bevy_ecs` on a particular platform,
please reach out either through a GitHub issue or in the `no_std`
working group on the Bevy Discord server.

Keep an eye out for future `no_std` updates as we continue to improve
the parity between `std` and `no_std`. We look forward to seeing what
kinds of applications are now possible with Bevy!

## Notes

- Creating PR in draft to ensure CI is passing before requesting
reviews.
- This implementation has no support for multithreading in `no_std`,
especially due to `NonSend` being unsound if allowed in multithreading.
The reason is we cannot check the `ThreadId` in `no_std`, so we have no
mechanism to at-runtime determine if access is sound.

---------

Co-authored-by: Alice Cecile <[email protected]>
Co-authored-by: Vic <[email protected]>
  • Loading branch information
3 people authored Dec 17, 2024
1 parent 6fd6ce1 commit 1f2d0e6
Show file tree
Hide file tree
Showing 63 changed files with 521 additions and 181 deletions.
126 changes: 108 additions & 18 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,129 @@ categories = ["game-engines", "data-structures"]
rust-version = "1.81.0"

[features]
default = ["bevy_reflect"]
trace = []
multi_threaded = ["bevy_tasks/multi_threaded", "arrayvec"]
default = ["std", "bevy_reflect", "async_executor"]

# Functionality

## Enables multithreading support. Schedules will attempt to run systems on
## multiple threads whenever possible.
multi_threaded = ["bevy_tasks/multi_threaded", "dep:arrayvec"]

## Adds serialization support through `serde`.
serialize = ["dep:serde", "bevy_utils/serde"]

## Adds runtime reflection support using `bevy_reflect`.
bevy_reflect = ["dep:bevy_reflect"]

## Extends reflection support to functions.
reflect_functions = ["bevy_reflect", "bevy_reflect/functions"]

# Debugging Features

## Enables `tracing` integration, allowing spans and other metrics to be reported
## through that framework.
trace = ["std", "dep:tracing", "bevy_utils/tracing"]

## Enables a more detailed set of traces which may be noisy if left on by default.
detailed_trace = ["trace"]

## Provides system stepping support, allowing them to be paused, stepped, and
## other debug operations which can help with diagnosing certain behaviors.
bevy_debug_stepping = []
serialize = ["dep:serde"]

## Provides more detailed tracking of the cause of various effects within the ECS.
## This will often provide more detailed error messages.
track_change_detection = []
reflect_functions = ["bevy_reflect", "bevy_reflect/functions"]
detailed_trace = []

# Executor Backend

## Uses `async-executor` as a task execution backend.
## This backend is incompatible with `no_std` targets.
async_executor = ["dep:bevy_tasks", "std", "bevy_tasks/async_executor"]

## Uses `edge-executor` as a task execution backend.
## Use this instead of `async-executor` if working on a `no_std` target.
edge_executor = ["dep:bevy_tasks", "bevy_tasks/edge_executor"]

# Platform Compatibility

## Allows access to the `std` crate. Enabling this feature will prevent compilation
## on `no_std` targets, but provides access to certain additional features on
## supported platforms.
std = [
"bevy_reflect?/std",
"bevy_tasks/std",
"bevy_utils/std",
"bitflags/std",
"concurrent-queue/std",
"disqualified/alloc",
"fixedbitset/std",
"indexmap/std",
"serde?/std",
"nonmax/std",
"arrayvec?/std",
"log/std",
]

## `critical-section` provides the building blocks for synchronization primitives
## on all platforms, including `no_std`.
critical-section = [
"dep:critical-section",
"bevy_tasks/critical-section",
"portable-atomic?/critical-section",
]

## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = [
"dep:portable-atomic",
"dep:portable-atomic-util",
"bevy_tasks/portable-atomic",
"concurrent-queue/portable-atomic",
]

[dependencies]
bevy_ptr = { path = "../bevy_ptr", version = "0.15.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", optional = true }
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", default-features = false, optional = true }
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev", default-features = false, optional = true }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false, features = [
"alloc",
] }
bevy_ecs_macros = { path = "macros", version = "0.15.0-dev" }

bitflags = "2.3"
concurrent-queue = "2.5.0"
disqualified = "1.0"
fixedbitset = "0.5"
serde = { version = "1", optional = true, default-features = false }
bitflags = { version = "2.3", default-features = false }
concurrent-queue = { version = "2.5.0", default-features = false }
disqualified = { version = "1.0", default-features = false }
fixedbitset = { version = "0.5", default-features = false }
serde = { version = "1", default-features = false, features = [
"alloc",
], optional = true }
thiserror = { version = "2", default-features = false }
derive_more = { version = "1", default-features = false, features = [
"from",
"display",
"into",
"as_ref",
] }
nonmax = "0.5"
arrayvec = { version = "0.7.4", optional = true }
nonmax = { version = "0.5", default-features = false }
arrayvec = { version = "0.7.4", default-features = false, optional = true }
smallvec = { version = "1", features = ["union"] }
indexmap = { version = "2.5.0", default-features = false, features = ["std"] }
variadics_please = "1.0"
indexmap = { version = "2.5.0", default-features = false }
variadics_please = { version = "1.0", default-features = false }
critical-section = { version = "1.2.0", optional = true }
portable-atomic = { version = "1", default-features = false, features = [
"fallback",
], optional = true }
portable-atomic-util = { version = "0.2.4", features = [
"alloc",
], optional = true }
spin = { version = "0.9.8", default-features = false, features = [
"spin_mutex",
"rwlock",
"once",
] }
tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false }

[dev-dependencies]
rand = "0.8"
Expand Down
2 changes: 1 addition & 1 deletion crates/bevy_ecs/macros/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
storages: &mut #bevy_ecs_path::storage::Storages,
required_components: &mut #bevy_ecs_path::component::RequiredComponents,
inheritance_depth: u16,
recursion_check_stack: &mut Vec<#bevy_ecs_path::component::ComponentId>
recursion_check_stack: &mut #bevy_ecs_path::__macro_exports::Vec<#bevy_ecs_path::component::ComponentId>
) {
#bevy_ecs_path::component::enforce_no_required_components_recursion(components, recursion_check_stack);
let self_id = components.register_component::<Self>(storages);
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/archetype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::{
observer::Observers,
storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow},
};
use alloc::{boxed::Box, vec::Vec};
use bevy_utils::HashMap;
use core::{
hash::Hash,
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::{
storage::{SparseSetIndex, SparseSets, Storages, Table, TableRow},
world::{unsafe_world_cell::UnsafeWorldCell, ON_ADD, ON_INSERT, ON_REPLACE},
};
use alloc::{boxed::Box, vec, vec::Vec};
use bevy_ptr::{ConstNonNull, OwningPtr};
use bevy_utils::{HashMap, HashSet, TypeIdMap};
#[cfg(feature = "track_change_detection")]
Expand Down
100 changes: 70 additions & 30 deletions crates/bevy_ecs/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
system::{Local, Resource, SystemParam},
world::{DeferredWorld, FromWorld, World},
};
use alloc::{borrow::Cow, sync::Arc};
use alloc::{borrow::Cow, format, vec::Vec};
pub use bevy_ecs_macros::Component;
use bevy_ptr::{OwningPtr, UnsafeCellDeref};
#[cfg(feature = "bevy_reflect")]
Expand All @@ -30,6 +30,12 @@ use core::{
use disqualified::ShortName;
use thiserror::Error;

#[cfg(feature = "portable-atomic")]
use portable_atomic_util::Arc;

#[cfg(not(feature = "portable-atomic"))]
use alloc::sync::Arc;

pub use bevy_ecs_macros::require;

/// A data type that can be used to store data for an [entity].
Expand Down Expand Up @@ -2005,35 +2011,69 @@ impl RequiredComponents {
constructor: fn() -> C,
inheritance_depth: u16,
) {
let erased: RequiredComponentConstructor = RequiredComponentConstructor(Arc::new(
move |table,
sparse_sets,
change_tick,
table_row,
entity,
#[cfg(feature = "track_change_detection")] caller| {
OwningPtr::make(constructor(), |ptr| {
// SAFETY: This will only be called in the context of `BundleInfo::write_components`, which will
// pass in a valid table_row and entity requiring a C constructor
// C::STORAGE_TYPE is the storage type associated with `component_id` / `C`
// `ptr` points to valid `C` data, which matches the type associated with `component_id`
unsafe {
BundleInfo::initialize_required_component(
table,
sparse_sets,
change_tick,
table_row,
entity,
component_id,
C::STORAGE_TYPE,
ptr,
#[cfg(feature = "track_change_detection")]
caller,
);
}
});
},
));
let erased: RequiredComponentConstructor = RequiredComponentConstructor({
// `portable-atomic-util` `Arc` is not able to coerce an unsized
// type like `std::sync::Arc` can. Creating a `Box` first does the
// coercion.
//
// This would be resolved by https://github.com/rust-lang/rust/issues/123430

#[cfg(feature = "portable-atomic")]
use alloc::boxed::Box;

#[cfg(feature = "track_change_detection")]
type Constructor = dyn for<'a, 'b> Fn(
&'a mut Table,
&'b mut SparseSets,
Tick,
TableRow,
Entity,
&'static Location<'static>,
);

#[cfg(not(feature = "track_change_detection"))]
type Constructor =
dyn for<'a, 'b> Fn(&'a mut Table, &'b mut SparseSets, Tick, TableRow, Entity);

#[cfg(feature = "portable-atomic")]
type Intermediate<T> = Box<T>;

#[cfg(not(feature = "portable-atomic"))]
type Intermediate<T> = Arc<T>;

let boxed: Intermediate<Constructor> = Intermediate::new(
move |table,
sparse_sets,
change_tick,
table_row,
entity,
#[cfg(feature = "track_change_detection")] caller| {
OwningPtr::make(constructor(), |ptr| {
// SAFETY: This will only be called in the context of `BundleInfo::write_components`, which will
// pass in a valid table_row and entity requiring a C constructor
// C::STORAGE_TYPE is the storage type associated with `component_id` / `C`
// `ptr` points to valid `C` data, which matches the type associated with `component_id`
unsafe {
BundleInfo::initialize_required_component(
table,
sparse_sets,
change_tick,
table_row,
entity,
component_id,
C::STORAGE_TYPE,
ptr,
#[cfg(feature = "track_change_detection")]
caller,
);
}
});
},
);

Arc::from(boxed)
});

// SAFETY:
// `component_id` matches the type initialized by the `erased` constructor above.
// `erased` initializes a component for `component_id` in such a way that
Expand Down
8 changes: 7 additions & 1 deletion crates/bevy_ecs/src/entity/clone_entities.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
use alloc::sync::Arc;
use alloc::{borrow::ToOwned, vec::Vec};
use core::any::TypeId;

use bevy_utils::{HashMap, HashSet};

#[cfg(feature = "portable-atomic")]
use portable_atomic_util::Arc;

#[cfg(not(feature = "portable-atomic"))]
use alloc::sync::Arc;

use crate::{
bundle::Bundle,
component::{component_clone_ignore, Component, ComponentCloneHandler, ComponentId},
Expand Down
22 changes: 17 additions & 5 deletions crates/bevy_ecs/src/entity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,6 @@ pub use visit_entities::*;
mod hash;
pub use hash::*;

use bevy_utils::tracing::warn;

use crate::{
archetype::{ArchetypeId, ArchetypeRow},
identifier::{
Expand All @@ -61,22 +59,36 @@ use crate::{
},
storage::{SparseSetIndex, TableId, TableRow},
};
use alloc::{borrow::ToOwned, string::String, vec::Vec};
use core::{fmt, hash::Hash, mem, num::NonZero};
use log::warn;

#[cfg(feature = "track_change_detection")]
use core::panic::Location;
use core::{fmt, hash::Hash, mem, num::NonZero, sync::atomic::Ordering};

#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};

#[cfg(target_has_atomic = "64")]
#[cfg(not(feature = "portable-atomic"))]
use core::sync::atomic::Ordering;

#[cfg(feature = "portable-atomic")]
use portable_atomic::Ordering;

#[cfg(all(target_has_atomic = "64", not(feature = "portable-atomic")))]
use core::sync::atomic::AtomicI64 as AtomicIdCursor;
#[cfg(all(target_has_atomic = "64", feature = "portable-atomic"))]
use portable_atomic::AtomicI64 as AtomicIdCursor;
#[cfg(target_has_atomic = "64")]
type IdCursor = i64;

/// Most modern platforms support 64-bit atomics, but some less-common platforms
/// do not. This fallback allows compilation using a 32-bit cursor instead, with
/// the caveat that some conversions may fail (and panic) at runtime.
#[cfg(not(target_has_atomic = "64"))]
#[cfg(all(not(target_has_atomic = "64"), not(feature = "portable-atomic")))]
use core::sync::atomic::AtomicIsize as AtomicIdCursor;
#[cfg(all(not(target_has_atomic = "64"), feature = "portable-atomic"))]
use portable_atomic::AtomicIsize as AtomicIdCursor;
#[cfg(not(target_has_atomic = "64"))]
type IdCursor = isize;

Expand Down
8 changes: 5 additions & 3 deletions crates/bevy_ecs/src/event/collections.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate as bevy_ecs;
use alloc::vec::Vec;
use bevy_ecs::{
event::{Event, EventCursor, EventId, EventInstance},
system::Resource,
};
use bevy_utils::detailed_trace;
#[cfg(feature = "track_change_detection")]
use core::panic::Location;
use core::{
Expand Down Expand Up @@ -142,7 +142,8 @@ impl<E: Event> Events<E> {
caller,
_marker: PhantomData,
};
detailed_trace!("Events::send() -> id: {}", event_id);
#[cfg(feature = "detailed_trace")]
tracing::trace!("Events::send() -> id: {}", event_id);

let event_instance = EventInstance { event_id, event };

Expand Down Expand Up @@ -318,7 +319,8 @@ impl<E: Event> Extend<E> for Events<E> {
self.events_b.extend(events);

if old_count != event_count {
detailed_trace!(
#[cfg(feature = "detailed_trace")]
tracing::trace!(
"Events::extend() -> ids: ({}..{})",
self.event_count,
event_count
Expand Down
Loading

0 comments on commit 1f2d0e6

Please sign in to comment.