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

feat: Upgrade to rand v0.9 #43

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,13 @@ jobs:
- name: Test with all features enabled
run: cargo test --all-features
- name: Test wasm
env:
RUSTFLAGS: --cfg getrandom_backend="wasm_js"
run: wasm-pack test --headless --chrome --firefox -- --all-features
if: startsWith(matrix.os, 'ubuntu')
- name: Test wasm (no default features)
env:
RUSTFLAGS: --cfg getrandom_backend="wasm_js"
run: wasm-pack test --headless --chrome --firefox -- --no-default-features
if: startsWith(matrix.os, 'ubuntu')
miri:
Expand Down
19 changes: 10 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ bevy_ecs = { git = "https://github.com/bevyengine/bevy", package = "bevy_ecs", d
] }
bevy_reflect = { git = "https://github.com/bevyengine/bevy", package = "bevy_reflect", default-features = false }
serde = { version = "1", default-features = false, features = ["derive"] }
rand_core = { version = "0.6", features = ["getrandom"] }
rand_chacha = { version = "0.3", default-features = false }
wyrand = "0.2"
rand_pcg = "0.3"
rand_xoshiro = "0.6"
rand_core = { version = "0.9", features = ["os_rng"] }
rand_chacha = { version = "0.9", default-features = false }
wyrand = "0.3"
rand_pcg = "0.9"
rand_xoshiro = "0.7"

[package]
name = "bevy_rand"
Expand All @@ -43,11 +43,12 @@ default = ["serialize", "thread_local_entropy", "std"]
std = ["bevy_prng/std"]
experimental = []
thread_local_entropy = ["dep:rand_chacha", "std"]
serialize = ["dep:serde", "rand_core/serde1", "bevy_prng/serialize"]
serialize = ["dep:serde", "rand_core/serde", "bevy_prng/serialize"]
rand_chacha = ["bevy_prng/rand_chacha"]
rand_pcg = ["bevy_prng/rand_pcg"]
rand_xoshiro = ["bevy_prng/rand_xoshiro"]
wyrand = ["bevy_prng/wyrand"]
wasm_js = ["getrandom/wasm_js"]

[dependencies]
bevy_app.workspace = true
Expand All @@ -56,7 +57,7 @@ bevy_reflect.workspace = true
bevy_prng = { path = "bevy_prng", version = "0.10" }

# others
getrandom = "0.2"
getrandom = "0.3"
rand_core.workspace = true
rand_chacha = { workspace = true, optional = true }
serde = { workspace = true, optional = true }
Expand All @@ -79,12 +80,12 @@ bevy_ecs = { git = "https://github.com/bevyengine/bevy", package = "bevy_ecs", d
"async_executor",
] }
bevy_prng = { path = "bevy_prng", version = "0.10", features = ["rand_chacha", "wyrand"] }
rand = "0.8"
rand = "0.9"
ron = { version = "0.8.0", features = ["integer128"] }

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
wasm-bindgen-test = "0.3"
getrandom = { version = "0.2", features = ["js"] }
getrandom = { version = "0.3", features = ["wasm_js"] }
bevy_ecs = { git = "https://github.com/bevyengine/bevy", package = "bevy_ecs", default-features = false, features = [
"bevy_reflect",
] }
Expand Down
2 changes: 1 addition & 1 deletion MIGRATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ struct Source;

## Migrating from v0.9 to v0.10

To begin with, the `experimental` feature no longer does anything, as the observers/commands API is now exposed by default. The feature hasn't been removed, as it may be used for future experimental APIs.
To begin with, the `experimental` feature no longer does anything, as the observers/commands API is now exposed by default. The feature hasn't been removed, as it may be used for future experimental APIs. There is now a `wasm_js` feature to help configure `getrandom` for WASM, though there's additional steps needed to build for WASM [outlined here](README#usage-within-web-wasm-environments).

`GlobalSource` and `GlobalSeed` have been removed and now is represented by a `GlobalRngEntity` SystemParam. All uses of `GlobalSource` & `GlobalSeed` can be replaced by `GlobalRngEntity`.

Expand Down
24 changes: 17 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,18 +42,26 @@ DO **NOT** use `bevy_rand` for actual security purposes, as this requires much m
bevy_rand = { version = "0.10", default-features = false, features = ["rand_chacha", "wyrand"] }
```

All PRNG backends should support `no_std` environments. Furthermore, `getrandom` needs to be configured to support the platform, so in the case of a `no_std` environment such as an embedded board or console, you'll need to implement the [custom backend for `getrandom` to compile](https://docs.rs/getrandom/0.2.15/getrandom/index.html#custom-implementations).
All PRNG backends should support `no_std` environments. Furthermore, `getrandom` needs to be configured to support the platform, so in the case of a `no_std` environment such as an embedded board or console, you'll need to implement the [custom backend for `getrandom` to compile](https://docs.rs/getrandom/latest/getrandom/#custom-backend).

#### Usage within Web WASM environments

From `v0.9`, `bevy_rand` will no longer assume that `bevy` will be run in a web environment when compiled for WASM. To enable that, just paste the following into your `Cargo.toml` for your binary crate:

```toml
[target.'cfg(all(any(target_arch = "wasm32", target_arch = "wasm64"), target_os = "unknown"))'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
bevy_rand = { version = "0.10", features = ["wasm_js"] }
```

This is in preparation for the newer versions of `getrandom`, which will force users to select the correct entropy backend for their application, something that can no longer be done by library crates.
This enables the `wasm_js` backend to be made available for `getrandom`, but it doesn't actually build. The next step is to either edit your `.cargo/config.toml` with the below configuration:

```toml
# It's recommended to set the flag on a per-target basis:
[target.wasm32-unknown-unknown]
rustflags = ['--cfg', 'getrandom_backend="wasm_js"']
```

Or pass an environment variable: `RUSTFLAGS='--cfg getrandom_backend="wasm_js"'`. This then enables the `getrandom` WASM backend to get built correctly.

### Registering a PRNG for use with Bevy Rand

Expand Down Expand Up @@ -145,7 +153,8 @@ fn setup_npc_from_source(
- **`rand_pcg`** - This enables the exporting of newtyped `Pcg*` structs from `rand_pcg`.
- **`rand_xoshiro`** - This enables the exporting of newtyped `Xoshiro*` structs from `rand_xoshiro`. It also exports a remote-reflected version of `Seed512` so to allow setting up `Xoshiro512StarStar` and so forth.
- **`wyrand`** - This enables the exporting of newtyped `WyRand` from `wyrand`, the same algorithm in use within `fastrand`/`turborand`.
- **`experimental`** - This enables any unstable/experimental features for `bevy_rand`. Currently, this will expose utilities for making use of observers for reseeding sources.
- **`experimental`** - This enables any unstable/experimental features for `bevy_rand`. Currently, this does nothing at the moment.
- **`wasm_js`** - This enables the `getrandom` WASM backend, though doesn't make `getrandom` use it. That requires extra steps outlined [here](#usage-within-web-wasm-environments).

## Supported Versions & MSRV

Expand All @@ -163,9 +172,10 @@ fn setup_npc_from_source(

The versions of `rand_core`/`rand` that `bevy_rand` is compatible with is as follows:

| `bevy_rand` | `rand_core` | `rand` |
| ------------- | ----------- | ------ |
| v0.1 -> v0.10 | v0.6 | v0.8 |
| `bevy_rand` | `rand_core` | `rand` | `getrandom` |
| ------------- | ----------- | ------ | ----------- |
| v0.10 | v0.9 | 0.9 | v0.3 |
| v0.1 -> v0.9 | v0.6 | v0.8 | v0.2 |

## Migrations

Expand Down
8 changes: 4 additions & 4 deletions bevy_prng/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ default = []
std = ["rand_chacha?/std"]
serialize = [
"dep:serde",
"rand_core/serde1",
"rand_chacha?/serde1",
"rand_pcg?/serde1",
"rand_xoshiro?/serde1",
"rand_core/serde",
"rand_chacha?/serde",
"rand_pcg?/serde",
"rand_xoshiro?/serde",
"wyrand?/serde1",
]
rand_chacha = ["dep:rand_chacha"]
Expand Down
18 changes: 4 additions & 14 deletions bevy_prng/src/newtype.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,6 @@ macro_rules! newtype_prng {
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest)
}

#[inline]
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), ::rand_core::Error> {
self.0.try_fill_bytes(dest)
}
}

impl SeedableRng for $newtype {
Expand All @@ -60,8 +55,8 @@ macro_rules! newtype_prng {
}

#[inline]
fn from_rng<R: RngCore>(source: R) -> Result<Self, ::rand_core::Error> {
Ok(Self::new(<$rng>::from_rng(source)?))
fn from_rng(source: &mut impl ::rand_core::RngCore) -> Self {
Self::new(<$rng>::from_rng(source))
}
}

Expand Down Expand Up @@ -122,11 +117,6 @@ macro_rules! newtype_prng_remote {
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest)
}

#[inline]
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), ::rand_core::Error> {
self.0.try_fill_bytes(dest)
}
}

impl SeedableRng for $newtype {
Expand All @@ -138,8 +128,8 @@ macro_rules! newtype_prng_remote {
}

#[inline]
fn from_rng<R: RngCore>(source: R) -> Result<Self, ::rand_core::Error> {
Ok(Self::new(<$rng>::from_rng(source)?))
fn from_rng(source: &mut impl ::rand_core::RngCore) -> Self {
Self::new(<$rng>::from_rng(source))
}
}

Expand Down
6 changes: 6 additions & 0 deletions bevy_prng/src/xoshiro.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ use bevy_reflect::{ReflectDeserialize, ReflectSerialize};
#[reflect(Debug, Default)]
pub struct Seed512(pub [u8; 64]);

impl AsRef<[u8]> for Seed512 {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}

impl AsMut<[u8]> for Seed512 {
fn as_mut(&mut self) -> &mut [u8] {
self.0.as_mut()
Expand Down
8 changes: 4 additions & 4 deletions examples/turn_based_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ fn determine_attack_order(
// RNG instance with a chosen seed.
let mut entities: Vec<_> = q_entities
.iter_mut()
.map(|mut entity| (entity.1.r#gen::<u32>(), entity))
.map(|mut entity| (entity.1.random::<u32>(), entity))
.collect();

entities.sort_by_key(|k| k.0);
Expand Down Expand Up @@ -142,7 +142,7 @@ fn attack_turn(
let (target, attack_damage, attacker) = {
let (_, attacker, attack, _, name, _, mut a_rng) = q_entities.get_mut(entity).unwrap();

let attack_damage = a_rng.gen_range(attack.min..=attack.max);
let attack_damage = a_rng.random_range(attack.min..=attack.max);

let target = if attacker == &Kind::Player {
enemies.iter().choose(a_rng.as_mut()).copied().unwrap()
Expand All @@ -157,7 +157,7 @@ fn attack_turn(
let (_, _, _, defense, defender, mut hp, mut d_rng) = q_entities.get_mut(target).unwrap();

// Will they dodge the attack?
if d_rng.gen_bool(defense.dodge) {
if d_rng.random_bool(defense.dodge) {
println!("{} dodged {}'s attack!", defender.0, attacker);
} else {
let damage_taken = (attack_damage - defense.armor).clamp(0.0, f32::MAX);
Expand All @@ -178,7 +178,7 @@ fn buff_entities(
// Query iteration order is not stable, but entities having their own RNG source side-steps this
// completely, so the result is always deterministic.
for (name, buff, mut hp, mut rng) in q_entities.iter_mut() {
if rng.gen_bool(buff.chance) {
if rng.random_bool(buff.chance) {
hp.amount += buff.effect;

println!("{} buffed their health by {} points!", name.0, buff.effect);
Expand Down
18 changes: 7 additions & 11 deletions src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ impl<R: EntropySource + 'static> Entropy<R> {
impl<R: EntropySource + 'static> Default for Entropy<R> {
#[inline]
fn default() -> Self {
Self::from_entropy()
Self::from_os_rng()
}
}

Expand All @@ -148,11 +148,6 @@ impl<R: EntropySource + 'static> RngCore for Entropy<R> {
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.0.fill_bytes(dest);
}

#[inline]
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
self.0.try_fill_bytes(dest)
}
}

impl<R: EntropySource + 'static> SeedableRng for Entropy<R> {
Expand All @@ -164,23 +159,24 @@ impl<R: EntropySource + 'static> SeedableRng for Entropy<R> {
}

#[inline]
fn from_rng<S: RngCore>(rng: S) -> Result<Self, rand_core::Error> {
R::from_rng(rng).map(Self::new)
fn from_rng(rng: &mut impl RngCore) -> Self {
Self::new(R::from_rng(rng))
}

/// Creates a new instance of the RNG seeded via [`ThreadLocalEntropy`]. This method is the recommended way
/// to construct non-deterministic PRNGs since it is convenient and secure. It overrides the standard
/// [`SeedableRng::from_entropy`] method while the `thread_local_entropy` feature is enabled.
/// [`SeedableRng::from_os_rng`] method while the `thread_local_entropy` feature is enabled.
///
/// # Panics
///
/// If [`ThreadLocalEntropy`] cannot get initialised because `getrandom` is unable to provide secure entropy,
/// this method will panic.
#[cfg(feature = "thread_local_entropy")]
#[cfg_attr(docsrs, doc(cfg(feature = "thread_local_entropy")))]
fn from_entropy() -> Self {
fn from_os_rng() -> Self {
let mut rng = ThreadLocalEntropy::new();
// This operation should never yield Err on any supported PRNGs
Self::from_rng(ThreadLocalEntropy::new()).unwrap()
Self::from_rng(&mut rng)
}
}

Expand Down
7 changes: 1 addition & 6 deletions src/thread_local_entropy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use rand_core::{CryptoRng, RngCore, SeedableRng};

thread_local! {
// We require `Rc` to avoid premature freeing when `ThreadLocalEntropy` is used within thread-local destructors.
static SOURCE: Rc<UnsafeCell<ChaCha8Rng>> = Rc::new(UnsafeCell::new(ChaCha8Rng::from_entropy()));
static SOURCE: Rc<UnsafeCell<ChaCha8Rng>> = Rc::new(UnsafeCell::new(ChaCha8Rng::from_os_rng()));
}

/// [`ThreadLocalEntropy`] uses thread local [`ChaCha8Rng`] instances to provide faster alternative for
Expand Down Expand Up @@ -72,11 +72,6 @@ impl RngCore for ThreadLocalEntropy {
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.access_local_source(|rng| rng.fill_bytes(dest));
}

#[inline]
fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
self.access_local_source(|rng| rng.try_fill_bytes(dest))
}
}

impl CryptoRng for ThreadLocalEntropy {}
Expand Down
10 changes: 5 additions & 5 deletions src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ pub trait ForkableRng: EcsEntropy {
/// }
/// ```
fn fork_rng(&mut self) -> Self::Output {
Self::Output::from_rng(self).unwrap()
Self::Output::from_rng(self)
}
}

Expand Down Expand Up @@ -59,7 +59,7 @@ pub trait ForkableAsRng: EcsEntropy {
/// }
/// ```
fn fork_as<T: EntropySource>(&mut self) -> Self::Output<T> {
Self::Output::<_>::from_rng(self).unwrap()
Self::Output::<_>::from_rng(self)
}
}

Expand Down Expand Up @@ -92,7 +92,7 @@ pub trait ForkableInnerRng: EcsEntropy {
/// }
/// ```
fn fork_inner(&mut self) -> Self::Output {
Self::Output::from_rng(self).unwrap()
Self::Output::from_rng(self)
}
}

Expand Down Expand Up @@ -242,9 +242,9 @@ where
}
#[cfg(not(feature = "thread_local_entropy"))]
{
use getrandom::getrandom;
use getrandom::fill;

getrandom(dest.as_mut()).expect("Unable to source entropy for seeding");
fill(dest.as_mut()).expect("Unable to source entropy for seeding");
}

Self::from_seed(dest)
Expand Down
8 changes: 4 additions & 4 deletions tests/integration/determinism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn random_output_a(mut q_source: Query<&mut Entropy<ChaCha8Rng>, With<SourceA>>)
let mut rng = q_source.single_mut();

assert_eq!(
rng.r#gen::<u32>(),
rng.random::<u32>(),
3315785188,
"SourceA does not match expected output"
);
Expand All @@ -40,14 +40,14 @@ fn random_output_a(mut q_source: Query<&mut Entropy<ChaCha8Rng>, With<SourceA>>)
fn random_output_b(mut q_source: Query<&mut Entropy<ChaCha8Rng>, With<SourceB>>) {
let mut rng = q_source.single_mut();

assert!(rng.gen_bool(0.5), "SourceB does not match expected output");
assert!(rng.random_bool(0.5), "SourceB does not match expected output");
}

fn random_output_c(mut q_source: Query<&mut Entropy<ChaCha8Rng>, With<SourceC>>) {
let mut rng = q_source.single_mut();

assert_eq!(
rng.gen_range(0u32..=20u32),
rng.random_range(0u32..=20u32),
4,
"SourceC does not match expected output"
);
Expand All @@ -57,7 +57,7 @@ fn random_output_d(mut q_source: Query<&mut Entropy<ChaCha12Rng>, With<SourceD>>
let mut rng = q_source.single_mut();

assert_eq!(
rng.r#gen::<(u16, u16)>(),
rng.random::<(u16, u16)>(),
(41421, 7891),
"SourceD does not match expected output"
);
Expand Down