Skip to content

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

Merged
merged 19 commits into from
Dec 17, 2024

Conversation

bushrat011899
Copy link
Contributor

@bushrat011899 bushrat011899 commented Dec 10, 2024

Objective

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.

[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.

@bushrat011899 bushrat011899 added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events X-Contentious There are nontrivial implications that should be thought through D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Dec 10, 2024
@alice-i-cecile alice-i-cecile added the M-Needs-Release-Note Work that should be called out in the blog due to impact label Dec 10, 2024
Copy link
Contributor Author

@bushrat011899 bushrat011899 left a 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.

Comment on lines 39 to 49
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",
]
Copy link
Contributor Author

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.

Copy link
Member

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 ;)

Comment on lines +95 to +96
tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false }
Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor

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};
Copy link
Contributor Author

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.

Comment on lines +32 to +36
#[cfg(feature = "portable-atomic")]
use portable_atomic_util::Arc;

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

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.

Comment on lines +1993 to +1997
// `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
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 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.

Copy link
Contributor

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.

Copy link
Contributor Author

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)

Copy link
Contributor

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.

Comment on lines -76 to +88
#[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;
Copy link
Contributor Author

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.

Comment on lines +10 to +14
#[cfg(feature = "std")]
use std::sync::{OnceLock, PoisonError, RwLock};

#[cfg(not(feature = "std"))]
use spin::{once::Once as OnceLock, rwlock::RwLock};
Copy link
Contributor Author

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.

@bushrat011899 bushrat011899 marked this pull request as ready for review December 11, 2024 00:54
@bushrat011899
Copy link
Contributor Author

CI is passing, so I consider this PR ready for review!

Comment on lines +92 to +101
/// 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;
}
Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Member

@alice-i-cecile alice-i-cecile left a 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.

Copy link
Contributor

@Victoronz Victoronz left a 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!

Copy link
Contributor

@Victoronz Victoronz left a 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)

@alice-i-cecile
Copy link
Member

@bushrat011899 once those comments are cleaned up I'll merge this in for you :)

bushrat011899 and others added 5 commits December 18, 2024 07:46
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>
@@ -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)]
Copy link
Contributor

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.)

Copy link
Contributor Author

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 = {
Copy link
Contributor

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 OnceLocks 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.

Copy link
Contributor Author

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.

Copy link
Contributor Author

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 consts 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!

Copy link
Contributor

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
};

Copy link
Contributor Author

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.

Comment on lines +1993 to +1997
// `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
Copy link
Contributor

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.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Dec 17, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Dec 17, 2024
@bushrat011899 bushrat011899 added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged and removed S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Dec 17, 2024
@bushrat011899
Copy link
Contributor Author

That CI issue around building bevy_ecs without default features will need to be fixed! I'll try and get that resolved shortly...

@Victoronz
Copy link
Contributor

Another question, I just realized that HashSet/HashMap are neither in core nor alloc. Yet EntityHashSet/EntityHashMap doesn't come up in the diff with a feature flag here, has that just not come up yet?

@bushrat011899
Copy link
Contributor Author

Another question, I just realized that HashSet/HashMap are neither in core nor alloc. Yet EntityHashSet/EntityHashMap doesn't come up in the diff with a feature flag here, has that just not come up yet?

Thankfully, hashbrown is no_std compatible, and bevy_utils has provided hashbrown as an alternative to the std HashMap and HashSet for a while now!

Turns out `bevy_tasks` can just be optional
@bushrat011899 bushrat011899 added S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it and removed S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged labels Dec 17, 2024
@bushrat011899
Copy link
Contributor Author

Ok I've made bevy_ecs work without default features. As a nice bonus, the fix involved making bevy_tasks optional, which was shockingly simple (only needed for things that were already gated behind multi_threaded). This reduces the number of dependencies required to build bevy_ecs to 40 on my machine.

@alice-i-cecile alice-i-cecile added this pull request to the merge queue Dec 17, 2024
@alice-i-cecile
Copy link
Member

Excellent! That definitely feels like the right design, and something we would have wanted to do anyways.

Merged via the queue into bevyengine:main with commit 1f2d0e6 Dec 17, 2024
33 checks passed
@TimJentzsch TimJentzsch added the O-Embedded Weird hardware and no_std platforms label Dec 31, 2024
ecoskey pushed a commit to ecoskey/bevy that referenced this pull request Jan 6, 2025
# 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>
mrchantey pushed a commit to mrchantey/bevy that referenced this pull request Feb 4, 2025
# 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>
@alice-i-cecile
Copy link
Member

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Modest A "normal" level of difficulty; suitable for simple features or challenging fixes M-Needs-Release-Note Work that should be called out in the blog due to impact O-Embedded Weird hardware and no_std platforms S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it X-Contentious There are nontrivial implications that should be thought through
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants