-
-
Notifications
You must be signed in to change notification settings - Fork 4k
Add no_std
support to bevy_ecs
#16758
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 no_std
support to bevy_ecs
#16758
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's some comments to assist reviewers with evaluating this PR. It is fairly lengthy so I'd recommend skimming through these first.
crates/bevy_ecs/Cargo.toml
Outdated
critical-section = [ | ||
"dep:critical-section", | ||
"bevy_tasks/critical-section", | ||
"portable-atomic?/critical-section", | ||
] | ||
portable-atomic = [ | ||
"dep:portable-atomic", | ||
"dep:portable-atomic-util", | ||
"bevy_tasks/portable-atomic", | ||
"concurrent-queue/portable-atomic", | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These features allow compiling on platforms without full support for atomics. Currently, they just enable features in our other dependencies and provide a couple of shims for things like Arc
. In the future, we may be able to better leverage critical-section
for other operations synchronisation areas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Useful, but would be more useful as a comment in the code ;)
tracing = { version = "0.1", default-features = false, optional = true } | ||
log = { version = "0.4", default-features = false } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Previously, tracing
was being brought in via a re-export from bevy_utils
. I have opted to flatten this dependency, as in general flatter dependency graphs yield faster compilation times. Additionally, I'm bringing in log
to allow the basic logging (warn!
, trace!
, etc.) to work on platforms tracing
does not support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should swap to a workspace dependency IMO: synchronizing these manually is silly. But that probably warrants its own independent discussion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It might be worth bringing in tracing_log
to allow tracing
users to still consume these log records as tracing
events.
@@ -26,6 +26,7 @@ use crate::{ | |||
observer::Observers, | |||
storage::{ImmutableSparseSet, SparseArray, SparseSet, SparseSetIndex, TableId, TableRow}, | |||
}; | |||
use alloc::{boxed::Box, vec::Vec}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most files have changed that look like this; just including items that were previously implicitly available.
#[cfg(feature = "portable-atomic")] | ||
use portable_atomic_util::Arc; | ||
|
||
#[cfg(not(feature = "portable-atomic"))] | ||
use alloc::sync::Arc; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For a handful of atomic types, this kind of feature gating is required to support platforms without full native atomic support. In general, portable-atomic
is a drop-in replacement for alloc::sync
.
// `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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This area is a little frustrating. In short, std::sync::Arc
has certain privileges that portable_atomic_util::Arc
doesn't, the key one being unsized coercion. To work around this, we first create a Box
(which can do the coercion), and then create an Arc
from that box. This code is a little verbose to handle the 2×2 feature matrix of tracking changes and portable atomics while also ensuring the Box
workaround is only used when portable-atomic
is enabled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be easier to combine the code if the closure inside RequiredComponentConstructor
took a MaybeLocation
? The comments on the alias say "Please use this type sparingly", but I'm not sure what actual cost there is to using it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I do think this should be used in more places. I don't like the verbosity that the cfg(...)
introduces, and I don't believe that it's more performant (a ZST should be removable from a function argument at compile time, since it has zero size)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be good to note that the feature that resolves this, [derive(CoercePointee)]
is currently in FCP for stabilization, so it will likely hit stable in about 2 months.
#[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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tried it, but I believe using portable-atomic
may allow access to an AtomicI64
on all platforms, removing the possible panic. This should be investigated in a follow-up PR.
#[cfg(feature = "std")] | ||
use std::sync::{OnceLock, PoisonError, RwLock}; | ||
|
||
#[cfg(not(feature = "std"))] | ||
use spin::{once::Once as OnceLock, rwlock::RwLock}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A handful of sync primitives are conditionally replaced with alternatives from spin
. I would like to revisit our reliance on spin
at a later date. Spinning is considered a last-resort sync option, and designing it out may improve std
performance too.
CI is passing, so I consider this PR ready for review! |
/// Exports used by macros. | ||
/// | ||
/// These are not meant to be used directly and are subject to breaking changes. | ||
#[doc(hidden)] | ||
pub mod __macro_exports { | ||
// Cannot directly use `alloc::vec::Vec` in macros, as a crate may not have | ||
// included `extern crate alloc;`. This re-export ensures we have access | ||
// to `Vec` in `no_std` and `std` contexts. | ||
pub use alloc::vec::Vec; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new required components recursion check uses a Vec
, which isn't in the global namespace with no_std
. This technique of adding a pub mod __macro_exports { ... }
is how bevy_reflect
gets around this issue, so this is at least consistent, even if it isn't ideal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could define a feature-flagged type alias here instead right? But yeah, consistent is fine for now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once the features are more clear this LGTM. Spin-locking on no-std platforms is a fine solution for now.
Co-Authored-By: Alice Cecile <alice.i.cecile@gmail.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some small code style changes, then it'd LGTM!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(didn't catch the review comments)
@bushrat011899 once those comments are cleaned up I'll merge this in for you :) |
Co-Authored-By: Vic <59878206+Victoronz@users.noreply.github.com>
Previously grouped operations to obfuscate `cfg(...)` gating. Co-Authored-By: Vic <59878206+Victoronz@users.noreply.github.com>
This reverts commit ba413b6.
@@ -423,6 +420,7 @@ impl Stepping { | |||
// transitions, and add debugging messages for permitted | |||
// transitions. Any action transition that falls through | |||
// this match block will be performed. | |||
#[expect(clippy::match_same_arms)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be part of this change? It seems unrelated.
(And should we fix it instead of ignoring it? It seems like clippy wants (_, Action::RunAll) => info!("disabled stepping"),
, which seems fairly reasonable.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'm not sure why, but locally I was hitting this match
statement in CI clippy
. I contemplated resolving the issue, but I think the original intent with this match statement was to list out all these situations explicitly as they are.
let access = READ_ALL_RESOURCES.get_or_init(|| { | ||
let mut access = Access::new(); | ||
access.read_all_resources(); | ||
let access = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We might be able to get rid of these OnceLock
s completely now that const
is more powerful. I think we could make read_all_resources()
a const fn
and then do let access = &const { <what init does now> }
.
That isn't actually related to this PR, though, so it should probably be a follow-up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd love this. Even ignoring no_std
, removing these sync primitives is just a good thing for code quality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just tested:
const READ_ALL_RESOURCES: &'static Access<ComponentId> = {
const ACCESS: Access<ComponentId> = {
let mut access = Access::new();
access.read_all_resources();
access
};
&ACCESS
};
You need the nested const
s to create a static reference while guaranteeing that Access
wont ever be dropped (compiler refuses to compile if it's possible to drop an Access
as it's a "non-trivial drop"). But this does remove the use of OnceLock
from READ_ALL_RESOURCES
and WRITE_ALL_RESOURCES
, which is very nice!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too late now, but you can do it with one const
if you put the &
around the whole block like
const READ_ALL_RESOURCES: &'static Access<ComponentId> = &{
let mut access = Access::new();
access.read_all_resources();
access
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahhh that's how you'd do it! I was placing it on the inside of the block.
// `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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be easier to combine the code if the closure inside RequiredComponentConstructor
took a MaybeLocation
? The comments on the alias say "Please use this type sparingly", but I'm not sure what actual cost there is to using it.
That CI issue around building |
Another question, I just realized that |
Thankfully, |
Turns out `bevy_tasks` can just be optional
Ok I've made |
Excellent! That definitely feels like the right design, and something we would have wanted to do anyways. |
# Objective - Contributes to bevyengine#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 <alice.i.cecile@gmail.com> Co-authored-by: Vic <59878206+Victoronz@users.noreply.github.com>
# Objective - Contributes to bevyengine#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 <alice.i.cecile@gmail.com> Co-authored-by: Vic <59878206+Victoronz@users.noreply.github.com>
Thank you to everyone involved with the authoring or reviewing of this PR! This work is relatively important and needs release notes! Head over to bevyengine/bevy-website#1976 if you'd like to help out. |
Objective
no_std
Bevy #15460Solution
std
(default)async_executor
(default)edge_executor
critical-section
portable-atomic
tracing
inbevy_utils
to allow compilation on certain platformstracing
tolog
for simple message logging withinbevy_ecs
. Note thattracing
supports capturing fromlog
so this should be an uncontroversial change.bevy_tasks
optional withinbevy_ecs
. Turns out it's only needed for parallel operations which are already gated behindmulti_threaded
anyway.Testing
compile-check-no-std
CI commandcargo 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 requiresstd
. To use Bevy on ano_std
platform, you must disable default features and enable the newedge_executor
andcritical-section
features. You may also need to enableportable-atomic
andcritical-section
if your platform does not natively support all atomic types and operations used by Bevy.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 theno_std
working group on the Bevy Discord server.Keep an eye out for future
no_std
updates as we continue to improve the parity betweenstd
andno_std
. We look forward to seeing what kinds of applications are now possible with Bevy!Notes
no_std
, especially due toNonSend
being unsound if allowed in multithreading. The reason is we cannot check theThreadId
inno_std
, so we have no mechanism to at-runtime determine if access is sound.