Skip to content

Commit

Permalink
Merge pull request #11 from Bluefinger/forking-refactor
Browse files Browse the repository at this point in the history
refactor: Forking via traits
  • Loading branch information
Bluefinger authored Nov 4, 2023
2 parents 70cff97 + 113ce38 commit 965313b
Show file tree
Hide file tree
Showing 16 changed files with 432 additions and 165 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
rustup override set nightly
cargo miri setup
- name: Test with Miri
run: cargo miri test --all-features
run: cargo miri test
env:
# -Zrandomize-layout makes sure we dont rely on the layout of anything that might change
RUSTFLAGS: -Zrandomize-layout
Expand Down Expand Up @@ -102,4 +102,4 @@ jobs:
run: rustup update ${{ env.MSRV }} --no-self-update && rustup default ${{ env.MSRV }}
- name: Run cargo check
id: check
run: cargo check --all-features
run: cargo check
23 changes: 20 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bevy_rand"
version = "0.3.0"
version = "0.4.0"
edition = "2021"
authors = ["Gonçalo Rica Pais da Silva <[email protected]>"]
description = "A plugin to integrate rand for ECS optimised RNG for the Bevy game engine."
Expand All @@ -16,21 +16,33 @@ rust-version = "1.70.0"
default = ["serialize", "thread_local_entropy"]
thread_local_entropy = ["dep:rand_chacha"]
serialize = ["dep:serde", "rand_core/serde1"]
rand_chacha = ["bevy_prng/rand_chacha"]
rand_pcg = ["bevy_prng/rand_pcg"]
rand_xoshiro = ["bevy_prng/rand_xoshiro"]
wyrand = ["bevy_prng/wyrand"]

[workspace]
members = ["bevy_prng"]

[dependencies]
# bevy
bevy = { version = "0.11", default-features = false }
bevy = { version = "0.12.0", default-features = false }
bevy_prng = { path = "bevy_prng", version = "0.2" }

# others
serde = { version = "1.0", features = ["derive"], optional = true }
rand_core = { version = "0.6", features = ["std"] }
rand_chacha = { version = "0.3", optional = true }

# This cfg cannot be enabled, but it forces Cargo to keep bevy_prng's
# version in lockstep with bevy_rand, so that even minor versions
# cannot be out of step with bevy_rand due to dependencies on traits
# and implementations between the two crates.
[target.'cfg(any())'.dependencies]
bevy_prng = { path = "bevy_prng", version = "=0.2" }

[dev-dependencies]
bevy_prng = { path = "bevy_prng", version = "0.1", features = ["rand_chacha"] }
bevy_prng = { path = "bevy_prng", version = "0.2", features = ["rand_chacha", "wyrand"] }
rand = "0.8"
ron = { version = "0.8.0", features = ["integer128"] }

Expand All @@ -43,3 +55,8 @@ getrandom = { version = "0.2", features = ["js"] }
[[example]]
name = "turn_based_game"
path = "examples/turn_based_game.rs"

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
rustc-args = ["--cfg", "docsrs"]
21 changes: 21 additions & 0 deletions MIGRATIONS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Migration Notes

## Migrating from v0.2 to v0.3

As v0.3 is a breaking change to v0.2, the process to migrate over is fairly simple. The rand algorithm crates can no longer be used directly, but they can be swapped wholesale with `bevy_prng` instead. So the following `Cargo.toml` changes:

```diff
- rand_chacha = { version = "0.3", features = ["serde1"] }
+ bevy_prng = { version = "0.1", features = ["rand_chacha"] }
```

allows then you to swap your import like so, which should then plug straight into existing `bevy_rand` usage seamlessly:

```diff
use bevy::prelude::*;
use bevy_rand::prelude::*;
- use rand_chacha::ChaCha8Rng;
+ use bevy_prng::ChaCha8Rng;
```

This **will** change the type path and the serialization format for the PRNGs, but currently, moving between different bevy versions has this problem as well as there's currently no means to migrate serialized formats from one version to another yet. The rationale for this change is to enable stable `TypePath` that is being imposed by bevy's reflection system, so that future compiler changes won't break things unexpectedly as `std::any::type_name` has no stability guarantees. Going forward, this should resolve any stability problems `bevy_rand` might have and be able to hook into any migration tool `bevy` might offer for when scene formats change/update.
74 changes: 40 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,23 @@ Bevy Rand operates around a global entropy source provided as a resource, and th

If cloning creates a second instance that shares the same state as the original, forking derives a new state from the original, leaving the original 'changed' and the new instance with a randomised seed. Forking RNG instances from a global source is a way to ensure that one seed produces many deterministic states, while making it difficult to predict outputs from many sources and also ensuring no one source shares the same state either with the original or with each other.

Bevy Rand approaches forking via `From` implementations of the various component/resource types, making it straightforward to use.
Bevy Rand provides forking via `ForkableRng`/`ForkableAsRng`/`ForkableInnerRng` traits, allowing one to easily fork with just a simple `.fork_rng()` method call, making it straightforward to use. There's also `From` implementations, **but from v0.4 onwards, these are considered deprecated and will likely be removed/changed in a future version**.

## Using Bevy Rand

Usage of Bevy Rand can range from very simple to quite complex use-cases, all depending on whether one cares about deterministic output or not. First, add `bevy_rand`,`bevy_prng`, and either `rand_core` or `rand` to your `Cargo.toml` to bring in both the components and the PRNGs you want to use, along with the various traits needed to use the RNGs. To select a given algorithm type with `bevy_prng`, enable the feature representing the newtypes from the `rand_*` crate you want to use.
Usage of Bevy Rand can range from very simple to quite complex use-cases, all depending on whether one cares about deterministic output or not. First, add `bevy_rand`, and either `rand_core` or `rand` to your `Cargo.toml` to bring in both the components and the PRNGs you want to use, along with the various traits needed to use the RNGs. To select a given algorithm type with `bevy_rand`, enable the feature representing the algorithm `rand_*` crate you want to use. This will then give you access to the PRNG structs via the prelude. Alternatively, you can use `bevy_prng` directly to get the newtyped structs with the same feature flags. However, using the algorithm crates like `rand_chacha` directly will not work as these don't implement the necessary traits to support bevy's reflection. The examples below use `bevy_prng` directly for purposes of clarity.

#### `bevy_rand` feature activation
```toml
rand_core = "0.6"
bevy_rand = "0.3"
bevy_prng = { version = "0.1", features = ["rand_chacha"] }
bevy_rand = { version = "0.4", features = ["rand_chacha"] }
```

#### `bevy_prng` feature activation
```toml
rand_core = "0.6"
bevy_rand = "0.4"
bevy_prng = { version = "0.2", features = ["rand_chacha"] }
```

### Registering a PRNG for use with Bevy Rand
Expand Down Expand Up @@ -96,7 +103,7 @@ fn print_random_value(mut rng: ResMut<GlobalEntropy<ChaCha8Rng>>) {

### Forking RNGs

For seeding `EntropyComponent`s from a global source, it is best to make use of forking instead of generating the seed value directly.
For seeding `EntropyComponent`s from a global source, it is best to make use of forking instead of generating the seed value directly. `GlobalEntropy` can only exist as a singular instance, so when forking normally, it will always fork as `EntropyComponent` instances.

```rust
use bevy::prelude::*;
Expand All @@ -110,7 +117,7 @@ fn setup_source(mut commands: Commands, mut global: ResMut<GlobalEntropy<ChaCha8
commands
.spawn((
Source,
EntropyComponent::from(&mut global),
global.fork_rng(),
));
}
```
Expand All @@ -137,12 +144,14 @@ fn setup_npc_from_source(
commands
.spawn((
Npc,
EntropyComponent::from(&mut source)
source.fork_rng()
));
}
}
```

For both `GlobalEntropy` and `EntropyComponent`s, one can fork the inner PRNG instance to use directly or pass into methods via `fork_inner()`.

### Enabling Determinism

Determinism relies on not just how RNGs are seeded, but also how systems are grouped and ordered relative to each other. Systems accessing the same source/entities will run serially to each other, but if you can separate entities into different groups that do not overlap with each other, systems can then run in parallel as well. Overall, care must be taken with regards to system ordering and scheduling, as well as unstable query iteration meaning the order of entities a query iterates through is not the same per run. This can affect the outcome/state of the PRNGs, producing different results.
Expand All @@ -151,12 +160,20 @@ The examples provided as integration tests in this repo demonstrate the two diff

## Selecting and using PRNG Algorithms

All supported PRNGs and compatible structs are provided by `bevy_prng`, so the easiest way to work with `bevy_rand` is to import the necessary algorithm from `bevy_prng`. Simply activate the relevant features in `bevy_prng` to pull in the PRNG algorithm you want to use, and then import them like so:
All supported PRNGs and compatible structs are provided by the `bevy_prng` crate. Simply activate the relevant features in `bevy_rand`/`bevy_prng` to pull in the PRNG algorithm you want to use, and then import them like so:

```toml
bevy_prng = { version = "0.1", features = ["rand_chacha", "wyrand"] }
bevy_rand = { version = "0.4", features = ["rand_chacha", "wyrand"] }
```
```rust ignore
use bevy::prelude::*;
use bevy_rand::prelude::{ChaCha8Rng, WyRand};
```
or
```toml
bevy_rand = "0.4"
bevy_prng = { version = "0.2", features = ["rand_chacha", "wyrand"] }
```

```rust ignore
use bevy::prelude::*;
use bevy_rand::prelude::*;
Expand All @@ -167,41 +184,30 @@ Using PRNGs directly from the `rand_*` crates is not possible without newtyping,

As a whole, which algorithm should be used/selected is dependent on a range of factors. Cryptographically Secure PRNGs (CSPRNGs) produce very hard to predict output (very high quality entropy), but in general are slow. The ChaCha algorithm can be sped up by using versions with less rounds (iterations of the algorithm), but this in turn reduces the quality of the output (making it easier to predict). However, `ChaCha8Rng` is still far stronger than what is feasible to be attacked, and is considerably faster as a source of entropy than the full `ChaCha20Rng`. `rand` uses `ChaCha12Rng` as a balance between security/quality of output and speed for its `StdRng`. CSPRNGs are important for cases when you _really_ don't want your output to be predictable and you need that extra level of assurance, such as doing any cryptography/authentication/security tasks.

If that extra level of security is not necessary, but there is still need for extra speed while maintaining good enough randomness, other PRNG algorithms exist for this purpose. These algorithms still try to output as high quality entropy as possible, but the level of entropy is not enough for cryptographic purposes. These algorithms should **never be used in situations that demand security**. Algorithms like `WyRand` and `Xoshiro256StarStar` are tuned for maximum throughput, while still possessing _good enough_ entropy for use as a source of randomness for non-security purposes. It still matters that the output is not predictable, but not to the same extent as CSPRNGs are required to be.
If that extra level of security is not necessary, but there is still need for extra speed while maintaining good enough randomness, other PRNG algorithms exist for this purpose. These algorithms still try to output as high quality entropy as possible, but the level of entropy is not enough for cryptographic purposes. These algorithms should **never be used in situations that demand security**. Algorithms like `WyRand` and `Xoshiro256StarStar` are tuned for maximum throughput, while still possessing _good enough_ entropy for use as a source of randomness for non-security purposes. It still matters that the output is not predictable, but not to the same extent as CSPRNGs are required to be. PRNGs like `WyRand` also have small state sizes, which makes them take less memory per instance compared to CSPRNGs like `ChaCha8Rng`.

## Features

- **`thread_local_entropy`** - Enables `ThreadLocalEntropy`, overriding `SeedableRng::from_entropy` implementations to make use of thread local entropy sources for faster PRNG initialisation. Enabled by default.
- **`serialize`** - Enables [`Serialize`] and [`Deserialize`] derives. Enabled by default.
- **`serialize`** - Enables `Serialize` and `Deserialize` derives. Enabled by default.
- **`rand_chacha`** - This enables the exporting of newtyped `ChaCha*Rng` structs, for those that want/need to use a CSPRNG level 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 reexports `Seed512` so to allow setting up `Xoshiro512StarStar` and so forth without the need to pull in `rand_xoshiro` explicitly.
- **`wyrand`** - This enables the exporting of newtyped `WyRand` from `wyrand`, the same algorithm in use within `fastrand`/`turborand`.

## Supported Versions & MSRV

`bevy_rand` uses the same MSRV as `bevy`.

| `bevy` | `bevy_rand` |
| -------- | ----------- |
| v0.11 | v0.2, v0.3 |
| v0.10 | v0.1 |

## Migrating from v0.2 to v0.3

As v0.3 is a breaking change to v0.2, the process to migrate over is fairly simple. The rand algorithm crates can no longer be used directly, but they can be swapped wholesale with `bevy_prng` instead. So the following `Cargo.toml` changes:

```diff
- rand_chacha = { version = "0.3", features = ["serde1"] }
+ bevy_prng = { version = "0.1", features = ["rand_chacha"] }
```
| `bevy` | `bevy_rand` |
| ------ | ----------- |
| v0.12 | v0.4 |
| v0.11 | v0.2, v0.3 |
| v0.10 | v0.1 |

allows then you to swap your import like so, which should then plug straight into existing `bevy_rand` usage seamlessly:

```diff
use bevy::prelude::*;
use bevy_rand::prelude::*;
- use rand_chacha::ChaCha8Rng;
+ use bevy_prng::ChaCha8Rng;
```
## Migrations

This **will** change the type path and the serialization format for the PRNGs, but currently, moving between different bevy versions has this problem as well as there's currently no means to migrate serialized formats from one version to another yet. The rationale for this change is to enable stable `TypePath` that is being imposed by bevy's reflection system, so that future compiler changes won't break things unexpectedly as `std::any::type_name` has no stability guarantees. Going forward, this should resolve any stability problems `bevy_rand` might have and be able to hook into any migration tool `bevy` might offer for when scene formats change/update.
Notes on migrating between versions can be found [here](MIGRATIONS.md).

## License

Expand Down
9 changes: 7 additions & 2 deletions bevy_prng/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "bevy_prng"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
authors = ["Gonçalo Rica Pais da Silva <[email protected]>"]
description = "A crate providing newtyped RNGs for integration into Bevy."
Expand All @@ -24,10 +24,15 @@ serialize = [
]

[dependencies]
bevy = { version = "0.11", default-features = false }
bevy = { version = "0.12.0", default-features = false }
rand_core = { version = "0.6", features = ["std"] }
serde = { version = "1.0", features = ["derive"], optional = true }
rand_chacha = { version = "0.3", optional = true }
wyrand = { version = "0.1", optional = true }
rand_pcg = { version = "0.3", optional = true }
rand_xoshiro = { version = "0.6", optional = true }

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
rustc-args = ["--cfg", "docsrs"]
7 changes: 4 additions & 3 deletions bevy_prng/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ All the below crates implement the necessary traits to be compatible with `bevy_

`bevy_prng` uses the same MSRV as `bevy`.

| `bevy` | `bevy_prng` |
| -------- | ----------- |
| v0.11 | v0.1 |
| `bevy` | `bevy_prng` |
| ------ | ----------- |
| v0.12 | v0.2 |
| v0.11 | v0.1 |

## License

Expand Down
76 changes: 59 additions & 17 deletions bevy_prng/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(docsrs, allow(unused_attributes))]

use std::fmt::Debug;

use bevy::{
prelude::{FromReflect, Reflect},
reflect::{GetTypeRegistration, TypePath},
};
use rand_core::{RngCore, SeedableRng};

#[cfg(any(
feature = "wyrand",
feature = "rand_chacha",
feature = "rand_pcg",
feature = "rand_xoshiro"
))]
use bevy::prelude::{Reflect, ReflectFromReflect};
#[cfg(any(
feature = "wyrand",
feature = "rand_chacha",
feature = "rand_pcg",
feature = "rand_xoshiro"
))]
use rand_core::{RngCore, SeedableRng};
use bevy::prelude::ReflectFromReflect;

#[cfg(all(
any(
Expand All @@ -28,17 +30,55 @@ use rand_core::{RngCore, SeedableRng};
))]
use bevy::prelude::{ReflectDeserialize, ReflectSerialize};

#[cfg(all(
any(
feature = "wyrand",
feature = "rand_chacha",
feature = "rand_pcg",
feature = "rand_xoshiro"
),
feature = "serialize"
))]
#[cfg(feature = "serialize")]
use serde::{Deserialize, Serialize};

/// A marker trait to define the required trait bounds for a seedable PRNG to
/// integrate into `EntropyComponent` or `GlobalEntropy`. This is a sealed trait.
#[cfg(feature = "serialize")]
pub trait SeedableEntropySource:
RngCore
+ SeedableRng
+ Clone
+ Debug
+ PartialEq
+ Sync
+ Send
+ Reflect
+ TypePath
+ FromReflect
+ GetTypeRegistration
+ Serialize
+ for<'a> Deserialize<'a>
+ private::SealedSeedable
{
}

/// A marker trait to define the required trait bounds for a seedable PRNG to
/// integrate into `EntropyComponent` or `GlobalEntropy`. This is a sealed trait.
#[cfg(not(feature = "serialize"))]
pub trait SeedableEntropySource:
RngCore
+ SeedableRng
+ Clone
+ Debug
+ PartialEq
+ Reflect
+ TypePath
+ FromReflect
+ GetTypeRegistration
+ Sync
+ Send
+ private::SealedSeedable
{
}

mod private {
pub trait SealedSeedable {}

impl<T: super::SeedableEntropySource> SealedSeedable for T {}
}

#[cfg(any(
feature = "wyrand",
feature = "rand_chacha",
Expand Down Expand Up @@ -108,6 +148,8 @@ macro_rules! newtype_prng {
Self::new(value)
}
}

impl SeedableEntropySource for $newtype {}
};
}

Expand Down
4 changes: 2 additions & 2 deletions examples/turn_based_game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ fn setup_player(mut commands: Commands, mut rng: ResMut<GlobalEntropy<ChaCha8Rng
// Forking from the global instance creates a random, but deterministic
// seed for the component, making it hard to guess yet still have a
// deterministic output
EntropyComponent::from(&mut rng),
rng.fork_rng(),
));
}

Expand All @@ -96,7 +96,7 @@ fn setup_enemies(mut commands: Commands, mut rng: ResMut<GlobalEntropy<ChaCha8Rn
// Forking from the global instance creates a random, but deterministic
// seed for the component, making it hard to guess yet still have a
// deterministic output
EntropyComponent::from(&mut rng),
rng.fork_rng(),
));
}
}
Expand Down
Loading

0 comments on commit 965313b

Please sign in to comment.