diff --git a/Cargo.toml b/Cargo.toml index a35c8ee752b86..8a453c60cdbd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ default = [ "animation", "bevy_asset", "bevy_audio", + "bevy_entropy", "bevy_gilrs", "bevy_scene", "bevy_winit", @@ -69,6 +70,9 @@ bevy_audio = ["bevy_internal/bevy_audio"] # Provides cameras and other basic render pipeline features bevy_core_pipeline = ["bevy_internal/bevy_core_pipeline", "bevy_asset", "bevy_render"] +# Plugin for providing PRNG integration into bevy +bevy_entropy = ["bevy_internal/bevy_entropy"] + # Plugin for dynamic loading (using [libloading](https://crates.io/crates/libloading)) bevy_dynamic_plugin = ["bevy_internal/bevy_dynamic_plugin"] diff --git a/crates/bevy_entropy/Cargo.toml b/crates/bevy_entropy/Cargo.toml new file mode 100644 index 0000000000000..3f2adc963cfe6 --- /dev/null +++ b/crates/bevy_entropy/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "bevy_entropy" +version = "0.11.0-dev" +edition = "2021" +description = "Bevy Engine's RNG integration" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["game", "bevy", "rand", "rng"] +categories = ["game-engines", "algorithms"] + +[features] +bevy_reflect = ["dep:bevy_reflect", "bevy_app/bevy_reflect", "serialize"] +default = ["bevy_reflect"] +serialize = ["dep:serde", "rand_core/serde1", "rand_chacha/serde1"] + +[dependencies] +# bevy +bevy_app = { path = "../bevy_app", version = "0.11.0-dev", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.11.0-dev", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.11.0-dev", optional = true } + +# others +serde = { version = "1.0", features = ["derive"], optional = true } +rand_core = { version = "0.6", features = ["std"] } +rand_chacha = "0.3" + +[dev-dependencies] +rand = "0.8" +ron = { version = "0.8.0", features = ["integer128"] } + +[target.'cfg(all(any(target_arch = "wasm32", target_arch = "wasm64"), target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", features = ["js"] } + +[[example]] +name = "determinism" +path = "examples/determinism.rs" + +[[example]] +name = "parallelism" +path = "examples/parallelism.rs" diff --git a/crates/bevy_entropy/README.md b/crates/bevy_entropy/README.md new file mode 100644 index 0000000000000..f980092541b95 --- /dev/null +++ b/crates/bevy_entropy/README.md @@ -0,0 +1,161 @@ +# Bevy Entropy + +[![Crates.io](https://img.shields.io/crates/v/bevy_entropy.svg)](https://crates.io/crates/bevy_entropy) +[![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/bevyengine/bevy/blob/HEAD/LICENSE) +[![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) + +## What is Bevy Entropy? + +Bevy Entropy is a plugin to provide integration of `rand` ecosystem PRNGs in an ECS friendly way. It provides a set of wrapper component and resource types that allow for safe access to a PRNG for generating random numbers, giving features like reflection, serialization for free. And with these types, it becomes possible to have determinism with the usage of these integrated PRNGs in ways that work with multi-threading and also avoid pitfalls such as unstable query iteration order. + +## Prerequisites + +For a PRNG crate to be usable with Bevy Entropy, at its minimum, it must implement `RngCore` and `SeedableRng` traits from `rand_core`, as well as `PartialEq`, `Clone`, and `Debug` traits. For reflection/serialization support, it should also implement `Serialize`/`Deserialize` traits from `serde`, though this can be disabled if one is not making use of reflection/serialization. As long as these traits are implemented, the PRNG can just be plugged in without an issue. + +## Overview + +Games often use randomness as a core mechanic. For example, card games generate a random deck for each game and killing monsters in an RPG often rewards players with a random item. While randomness makes games more interesting and increases replayability, it also makes games harder to test and prevents advanced techniques such as [deterministic lockstep](https://gafferongames.com/post/deterministic_lockstep/). + +Let's pretend you are creating a poker game where a human player can play against the computer. The computer's poker logic is very simple: when the computer has a good hand, it bets all of its money. To make sure the behavior works, you write a test to first check the computer's hand and if it is good confirm that all its money is bet. If the test passes does it ensure the computer behaves as intended? Sadly, no. + +Because the deck is randomly shuffled for each game (without doing so the player would already know the card order from the previous game), it is not guaranteed that the computer player gets a good hand and thus the betting logic goes unchecked. +While there are ways around this (a fake deck that is not shuffled, running the test many times to increase confidence, breaking the logic into units and testing those) it would be very helpful to have randomness as well as a way to make it _less_ random. + +Luckily, when a computer needs a random number it doesn't use real randomness and instead uses a [pseudorandom number generator](https://en.wikipedia.org/wiki/Pseudorandom_number_generator). Popular Rust libraries containing pseudorandom number generators are [`rand`](https://crates.io/crates/rand) and [`fastrand`](https://crates.io/crates/fastrand). + +Pseudorandom number generators require a source of [entropy](https://en.wikipedia.org/wiki/Entropy) called a [random seed](https://en.wikipedia.org/wiki/Random_seed). The random seed is used as input to generate numbers that _appear_ random but are instead in a specific and deterministic order. For the same random seed, a pseudorandom number generator always returns the same numbers in the same order. + +For example, let's say you seed a pseudorandom number generator with `1234`. +You then ask for a random number between `10` and `99` and the pseudorandom number generator returns `12`. +If you run the program again with the same seed (`1234`) and ask for another random number between `1` and `99`, you will again get `12`. +If you then change the seed to `4567` and run the program, more than likely the result will not be `12` and will instead be a different number. +If you run the program again with the `4567` seed, you should see the same number from the previous `4567`-seeded run. + +There are many types of pseudorandom number generators each with their own strengths and weaknesses. Because of this, Bevy does not include a pseudorandom number generator. Instead, the `bevy_entropy` plugin includes a source of entropy to use as a random seed for your chosen pseudorandom number generator. + +Note that Bevy currently has [other sources of non-determinism](https://github.com/bevyengine/bevy/discussions/2480) unrelated to pseudorandom number generators. + +## Concepts + +Bevy Entropy operates around a global entropy source provided as a resource, and then entropy components that can then be attached to entities. The use of resources/components allow the ECS to schedule systems appropriately so to make it easier to achieve determinism. + +### GlobalEntropy + +`GlobalEntropy` is the main resource for providing a global entropy source. It can only be accessed via a `ResMut` if looking to generate random numbers from it, as `RngCore` only exposes `&mut self` methods. As a result, working with `ResMut>` means any systems that access it will not be able to run in parallel to each other, as the `mut` access requires the scheduler to ensure that only one system at a time is accessing it. Therefore, if one intends on parallelising RNG workloads, limiting use/access of `GlobalEntropy` is vital. However, if one intends on having a single seed to deterministic control/derive many RNGs, `GlobalEntropy` is the best source for this purpose. + +### EntropyComponent + +`EntropyComponent` is a wrapper component that allows for entities to have their own RNG source. In order to generate random numbers from it, the `EntropyComponent` must be accessed with a `&mut` reference. Doing so will limit systems accessing the same source, but to increase parallelism, one can create many different sources instead. For ensuring determinism, query iteration must be accounted for as well as it isn't stable. Therefore, entities that need to perform some randomised task should 'own' their own `EntropyComponent`. + +`EntropyComponent` can be seeded directly, or be created from a `GlobalEntropy` source or other `EntropyComponent`s. + +### Forking + +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 Entropy approaches forking via `From` implementations of the various component/resource types, making it straightforward to use. + +## Using Bevy Entropy + +Usage of Bevy Entropy can range from very simple to quite complex use-cases, all depending on whether one cares about deterministic output or not. + +### Registering a PRNG for use with Bevy Entropy + +Before a PRNG can be used via `GlobalEntropy` or `EntropyComponent`, it must be registered via the plugin. + +```rust +use bevy_app::App; +use bevy_entropy::prelude::*; +use rand_core::RngCore; +use rand_chacha::ChaCha8Rng; + +fn main() { + App::new() + .add_plugin(EntropyPlugin::::default()) + .run(); +} +``` + +### Basic Usage + +At the simplest case, using `GlobalEntropy` directly for all random number generation, though this does limit how well systems using `GlobalEntropy` can be parallelised. All systems that access `GlobalEntropy` will run serially to each other. + +```rust +use bevy_ecs::prelude::ResMut; +use bevy_entropy::prelude::*; +use rand_core::RngCore; +use rand_chacha::ChaCha8Rng; + +fn print_random_value(mut rng: ResMut>) { + println!("Random value: {}", rng.next_u32()); +} +``` + +### 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. + +```rust +use bevy_ecs::{ + prelude::{Component, ResMut}, + system::Commands, +}; +use bevy_entropy::prelude::*; +use rand_chacha::ChaCha8Rng; + +#[derive(Component)] +struct Source; + +fn setup_source(mut commands: Commands, mut global: ResMut>) { + commands + .spawn(( + Source, + EntropyComponent::from(&mut global), + )); +} +``` + +`EntropyComponent`s can be seeded/forked from other `EntropyComponent`s as well. + +```rust +use bevy_ecs::{ + prelude::{Component, Query, With, Without}, + system::Commands, +}; +use bevy_entropy::prelude::*; +use rand_chacha::ChaCha8Rng; + +#[derive(Component)] +struct Npc; + +#[derive(Component)] +struct Source; + +fn setup_npc_from_source( + mut commands: Commands, + mut q_source: Query<&mut EntropyComponent, (With, Without)>, +) { + let mut source = q_source.single_mut(); + for _ in 0..2 { + commands + .spawn(( + Npc, + EntropyComponent::from(&mut source) + )); + } +} +``` + +### 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. + +The examples provided in this repo demonstrate the two different concepts of parallelisation and deterministic outputs, so check them out to see how one might achieve determinism. + +### Selecting PRNG algorithms + +`rand` provides a number of PRNGs, but under types such as `StdRng` and `SmallRng`. These are **not** intended to be stable/deterministic across different versions of `rand`. `rand` might change the underlying implementations of `StdRng` and `SmallRng` at any point, yielding different output. If the lack of stability is fine, then plugging these into `bevy_entropy` is fine. Else, the recommendation (made by `rand` crate as well) is that if determinism of output and stability of the algorithm used is important, then to use the algorithm crates directly. So instead of using `StdRng`, use `ChaCha12Rng` from the `rand_chacha` crate. + +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 `Xoshiro256++` 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. diff --git a/crates/bevy_entropy/examples/determinism.rs b/crates/bevy_entropy/examples/determinism.rs new file mode 100644 index 0000000000000..6722386a7ecf0 --- /dev/null +++ b/crates/bevy_entropy/examples/determinism.rs @@ -0,0 +1,201 @@ +#![allow(clippy::type_complexity)] + +use bevy_app::{App, Startup, Update}; +use bevy_ecs::{ + prelude::{Component, Entity, ResMut}, + query::With, + schedule::IntoSystemConfigs, + system::{Commands, In, IntoPipeSystem, Query}, +}; +use bevy_entropy::prelude::*; +use rand::prelude::{IteratorRandom, Rng}; +use rand_chacha::ChaCha8Rng; + +#[derive(Component)] +struct Player; + +#[derive(Component)] +struct Enemy; + +#[derive(Component, PartialEq, Eq)] +enum Kind { + Player, + Enemy, +} + +#[derive(Component)] +struct Name(pub String); + +#[derive(Component)] +struct Attack { + max: f32, + min: f32, +} + +#[derive(Component)] +struct Defense { + dodge: f64, + armor: f32, +} + +#[derive(Component)] +struct Buff { + effect: f32, + chance: f64, +} + +#[derive(Component)] +struct Health { + amount: f32, +} + +fn main() { + App::new() + .add_plugin(EntropyPlugin::::new().with_seed([1; 32])) + .add_systems(Startup, (setup_player, setup_enemies).chain()) + .add_systems( + Update, + (determine_attack_order.pipe(attack_turn), buff_entities).chain(), + ) + .run(); +} + +fn setup_player(mut commands: Commands, mut rng: ResMut>) { + commands.spawn(( + Kind::Player, + Name("Player".into()), + Attack { + max: 10.0, + min: 2.0, + }, + Defense { + dodge: 0.25, + armor: 3.0, + }, + Buff { + effect: 5.0, + chance: 0.5, + }, + Health { amount: 50.0 }, + // 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), + )); +} + +fn setup_enemies(mut commands: Commands, mut rng: ResMut>) { + for i in 1..=2 { + commands.spawn(( + Kind::Enemy, + Name(format!("Goblin {i}")), + Attack { max: 8.0, min: 1.0 }, + Defense { + dodge: 0.2, + armor: 2.5, + }, + Buff { + effect: 5.0, + chance: 0.25, + }, + Health { amount: 20.0 }, + // 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), + )); + } +} + +fn determine_attack_order( + mut q_entities: Query<(Entity, &mut EntropyComponent), With>, +) -> Vec { + // No matter the order of entities in the query, because they have their own RNG instance, + // it will always result in a deterministic output due to being seeded from a single global + // RNG instance with a chosen seed. + let mut entities: Vec<_> = q_entities + .iter_mut() + .map(|mut entity| (entity.1.gen::(), entity)) + .collect(); + + entities.sort_by_key(|k| k.0); + + entities.iter_mut().map(|(_, entity)| entity.0).collect() +} + +fn attack_turn( + In(attack_order): In>, + mut q_entities: Query<( + Entity, + &Kind, + &Attack, + &Defense, + &Name, + &mut Health, + &mut EntropyComponent, + )>, +) { + // Establish list of enemy entities for player to attack + let enemies: Vec<_> = q_entities + .iter() + .filter_map(|entity| entity.1.eq(&Kind::Enemy).then_some(entity.0)) + .collect(); + + // Get the Player entity for the enemies to target + let player = q_entities + .iter() + .find_map(|entity| entity.1.eq(&Kind::Player).then_some(entity.0)) + .unwrap(); + + // We've created a sorted attack order from another system, so this should always be deterministic. + for entity in attack_order { + // Calculate the target and the amount of damage to attempt to apply to the target. + 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 target = if attacker == &Kind::Player { + enemies.iter().choose(a_rng.as_mut()).copied().unwrap() + } else { + player + }; + + (target, attack_damage, name.0.clone()) + }; + + // Calculate the defense of the target for mitigating the damage. + 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) { + println!("{} dodged {}'s attack!", defender.0, attacker); + } else { + let damage_taken = (attack_damage - defense.armor).clamp(0.0, f32::MAX); + + hp.amount = (hp.amount - damage_taken).clamp(0.0, f32::MAX); + + println!( + "{} took {} damage from {}", + defender.0, damage_taken, attacker + ); + } + } +} + +fn buff_entities( + mut q_entities: Query< + (&Name, &Buff, &mut Health, &mut EntropyComponent), + With, + >, +) { + // 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) { + hp.amount += buff.effect; + + println!("{} buffed their health by {} points!", name.0, buff.effect); + } + } +} diff --git a/crates/bevy_entropy/examples/parallelism.rs b/crates/bevy_entropy/examples/parallelism.rs new file mode 100644 index 0000000000000..da90c8d7afd87 --- /dev/null +++ b/crates/bevy_entropy/examples/parallelism.rs @@ -0,0 +1,79 @@ +#![allow(clippy::type_complexity)] + +use bevy_app::{App, Startup, Update}; +use bevy_ecs::{ + prelude::{Component, ResMut}, + query::With, + system::{Commands, Query}, +}; +use bevy_entropy::prelude::*; +use rand::prelude::Rng; +use rand_chacha::ChaCha8Rng; + +#[derive(Component)] +struct SourceA; + +#[derive(Component)] +struct SourceB; + +#[derive(Component)] +struct SourceC; + +#[derive(Component)] +struct SourceD; + +/// Entities having their own sources side-steps issues with parallel execution and scheduling +/// not ensuring that certain systems run before others. With an entity having its own RNG source, +/// no matter when the systems that query that entity run, it will always result in a deterministic +/// output. The order of execution will not affect the RNG output, as long as the entities are +/// seeded deterministically and any systems that query a specific entity or group of entities that +/// share the same RNG source are assured to be in order. +fn main() { + App::new() + .add_plugin(EntropyPlugin::::new().with_seed([2; 32])) + .add_systems(Startup, setup_sources) + .add_systems( + Update, + ( + random_output_a, + random_output_b, + random_output_c, + random_output_d, + ), + ) + .run(); +} + +fn random_output_a(mut q_source: Query<&mut EntropyComponent, With>) { + let mut rng = q_source.single_mut(); + + println!("SourceA result: {}", rng.gen::()); +} + +fn random_output_b(mut q_source: Query<&mut EntropyComponent, With>) { + let mut rng = q_source.single_mut(); + + println!("SourceB result: {}", rng.gen_bool(0.5)); +} + +fn random_output_c(mut q_source: Query<&mut EntropyComponent, With>) { + let mut rng = q_source.single_mut(); + + println!("SourceC result: {}", rng.gen_range(0u32..=20u32)); +} + +fn random_output_d(mut q_source: Query<&mut EntropyComponent, With>) { + let mut rng = q_source.single_mut(); + + println!("SourceD result: {:?}", rng.gen::<(u16, u16)>()); +} + +fn setup_sources(mut commands: Commands, mut rng: ResMut>) { + commands.spawn((SourceA, EntropyComponent::from(&mut rng))); + + commands.spawn((SourceB, EntropyComponent::from(&mut rng))); + + commands.spawn((SourceC, EntropyComponent::from(&mut rng))); + + commands.spawn((SourceD, EntropyComponent::from(&mut rng))); +} diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs new file mode 100644 index 0000000000000..f7f32b6782bd1 --- /dev/null +++ b/crates/bevy_entropy/src/component.rs @@ -0,0 +1,278 @@ +use std::fmt::Debug; + +use crate::{ + resource::GlobalEntropy, thread_local_entropy::ThreadLocalEntropy, + traits::SeedableEntropySource, +}; +use bevy_ecs::{ + prelude::{Component, ReflectComponent}, + system::ResMut, + world::Mut, +}; +use rand_core::{RngCore, SeedableRng}; + +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{ + FromReflect, Reflect, ReflectDeserialize, ReflectFromReflect, ReflectSerialize, +}; + +/// An [`EntropyComponent`] that wraps a random number generator that implements +/// [`RngCore`] & [`SeedableRng`]. +/// +/// ## Creating new [`EntropyComponent`]s. +/// +/// You can creates a new [`EntropyComponent`] directly from anything that implements +/// [`RngCore`] or provides a mut reference to [`RngCore`], such as [`ResMut`] or a +/// [`Component`], or from a [`RngCore`] source directly. +/// +/// ## Examples +/// +/// Randomised Component: +/// ``` +/// use bevy_ecs::{ +/// prelude::Component, +/// system::Commands, +/// }; +/// use bevy_entropy::prelude::*; +/// use rand_chacha::ChaCha8Rng; +/// +/// #[derive(Component)] +/// struct Source; +/// +/// fn setup_source(mut commands: Commands) { +/// commands +/// .spawn(( +/// Source, +/// EntropyComponent::::default(), +/// )); +/// } +/// ``` +/// +/// Seeded from a resource: +/// ``` +/// use bevy_ecs::{ +/// prelude::{Component, ResMut}, +/// system::Commands, +/// }; +/// use bevy_entropy::prelude::*; +/// use rand_chacha::ChaCha8Rng; +/// +/// #[derive(Component)] +/// struct Source; +/// +/// fn setup_source(mut commands: Commands, mut global: ResMut>) { +/// commands +/// .spawn(( +/// Source, +/// EntropyComponent::from(&mut global), +/// )); +/// } +/// ``` +/// +/// Seeded from a component: +/// ``` +/// use bevy_ecs::{ +/// prelude::{Component, Query, With, Without}, +/// system::Commands, +/// }; +/// use bevy_entropy::prelude::*; +/// use rand_chacha::ChaCha8Rng; +/// +/// #[derive(Component)] +/// struct Npc; +/// #[derive(Component)] +/// struct Source; +/// +/// fn setup_npc_from_source( +/// mut commands: Commands, +/// mut q_source: Query<&mut EntropyComponent, (With, Without)>, +/// ) { +/// let mut source = q_source.single_mut(); +/// +/// for _ in 0..2 { +/// commands +/// .spawn(( +/// Npc, +/// EntropyComponent::from(&mut source) +/// )); +/// } +/// } +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Component)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect, FromReflect))] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serialize", + serde(bound(deserialize = "R: for<'a> Deserialize<'a>")) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect_value(Debug, PartialEq, Component, FromReflect, Serialize, Deserialize) +)] +#[cfg_attr( + all(not(feature = "serialize"), feature = "bevy_reflect"), + reflect_value(Debug, PartialEq, Component, FromReflect) +)] +pub struct EntropyComponent(R); + +impl EntropyComponent { + /// Create a new component from an `RngCore` instance. + #[inline] + #[must_use] + pub fn new(rng: R) -> Self { + Self(rng) + } +} + +impl EntropyComponent { + /// Create a new component with an `RngCore` instance seeded + /// from a local entropy source. Generates a randomised, + /// non-deterministic seed for the component. + #[inline] + #[must_use] + pub fn from_entropy() -> Self { + // Source entropy from thread local user-space RNG instead of + // system entropy source to reduce overhead when creating many + // rng instances for many entities at once. + Self::new(R::from_rng(ThreadLocalEntropy).unwrap()) + } + + /// Reseeds the internal `RngCore` instance with a new seed. + #[inline] + pub fn reseed(&mut self, seed: R::Seed) { + self.0 = R::from_seed(seed); + } +} + +impl Default for EntropyComponent { + fn default() -> Self { + Self::from_entropy() + } +} + +impl RngCore for EntropyComponent { + #[inline] + fn next_u32(&mut self) -> u32 { + self.0.next_u32() + } + + #[inline] + fn next_u64(&mut self) -> u64 { + self.0.next_u64() + } + + #[inline] + 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 EntropyComponent { + type Seed = R::Seed; + + fn from_seed(seed: Self::Seed) -> Self { + Self::new(R::from_seed(seed)) + } +} + +impl From for EntropyComponent { + fn from(value: R) -> Self { + Self::new(value) + } +} + +impl From<&mut EntropyComponent> for EntropyComponent { + fn from(rng: &mut EntropyComponent) -> Self { + Self::from_rng(rng).unwrap() + } +} + +impl From<&mut Mut<'_, EntropyComponent>> + for EntropyComponent +{ + fn from(rng: &mut Mut<'_, EntropyComponent>) -> Self { + Self::from(rng.as_mut()) + } +} + +impl From<&mut ResMut<'_, GlobalEntropy>> + for EntropyComponent +{ + fn from(rng: &mut ResMut<'_, GlobalEntropy>) -> Self { + Self::from_rng(rng.as_mut()).unwrap() + } +} + +#[cfg(test)] +mod tests { + use rand_chacha::ChaCha8Rng; + + use super::*; + + #[test] + fn forking() { + let mut rng1 = EntropyComponent::::default(); + + let rng2 = EntropyComponent::from(&mut rng1); + + assert_ne!( + rng1, rng2, + "forked EntropyComponents should not match each other" + ); + } + + #[test] + fn rng_reflection() { + use bevy_reflect::{ + serde::{ReflectSerializer, UntypedReflectDeserializer}, + TypeRegistry, + }; + use ron::ser::to_string; + use serde::de::DeserializeSeed; + + let mut registry = TypeRegistry::default(); + registry.register::>(); + + let mut val = EntropyComponent::::from_seed([7; 32]); + + // Modify the state of the RNG instance + val.next_u32(); + + let ser = ReflectSerializer::new(&val, ®istry); + + let serialized = to_string(&ser).unwrap(); + + assert_eq!( + &serialized, + "{\"bevy_entropy::component::EntropyComponent\":((seed:(7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7),stream:0,word_pos:1))}" + ); + + let mut deserializer = ron::Deserializer::from_str(&serialized).unwrap(); + + let de = UntypedReflectDeserializer::new(®istry); + + let value = de.deserialize(&mut deserializer).unwrap(); + + let mut dynamic = value.take::>().unwrap(); + + // The two instances should be the same + assert_eq!( + val, dynamic, + "The deserialized EntropyComponent should equal the original" + ); + // They should output the same numbers, as no state is lost between serialization and deserialization. + assert_eq!( + val.next_u32(), + dynamic.next_u32(), + "The deserialized EntropyComponent should have the same output as original" + ); + } +} diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs new file mode 100644 index 0000000000000..5ed75a92b8c17 --- /dev/null +++ b/crates/bevy_entropy/src/lib.rs @@ -0,0 +1,14 @@ +#![warn(clippy::undocumented_unsafe_blocks)] +#![deny(missing_docs)] +#![doc = include_str!("../README.md")] + +/// Components for integrating `RngCore` PRNGs into bevy. +pub mod component; +/// Plugin for integrating `RngCore` PRNGs into bevy. +pub mod plugin; +/// Prelude for providing all necessary types for easy use. +pub mod prelude; +/// Resource for integrating `RngCore` PRNGs into bevy. +pub mod resource; +mod thread_local_entropy; +mod traits; diff --git a/crates/bevy_entropy/src/plugin.rs b/crates/bevy_entropy/src/plugin.rs new file mode 100644 index 0000000000000..1f19f4d56a1cf --- /dev/null +++ b/crates/bevy_entropy/src/plugin.rs @@ -0,0 +1,86 @@ +use std::marker::PhantomData; + +use crate::{resource::GlobalEntropy, traits::SeedableEntropySource}; +use bevy_app::{App, Plugin}; +use rand_core::SeedableRng; + +#[cfg(feature = "bevy_reflect")] +use crate::component::EntropyComponent; + +/// Plugin for integrating a PRNG that implements `RngCore` into +/// the bevy engine, registering types for a global resource and +/// entropy components. +/// +/// ``` +/// use bevy_ecs::prelude::ResMut; +/// use bevy_app::{App, Update}; +/// use bevy_entropy::prelude::*; +/// use rand_core::RngCore; +/// use rand_chacha::{ChaCha8Rng, ChaCha12Rng}; +/// +/// fn main() { +/// App::new() +/// .add_plugin(EntropyPlugin::::default()) +/// .add_plugin(EntropyPlugin::::default()) +/// .add_systems(Update, print_random_value) +/// .run(); +/// } +/// +/// fn print_random_value(mut rng: ResMut>) { +/// println!("Random value: {}", rng.next_u32()); +/// } +/// ``` +pub struct EntropyPlugin { + seed: Option, + _marker: PhantomData<&'static mut R>, +} + +impl EntropyPlugin +where + R::Seed: Send + Sync + Copy, +{ + /// Creates a new plugin instance configured for randomised, + /// non-deterministic seeding of the global entropy resource. + #[inline] + #[must_use] + pub fn new() -> Self { + Self { + seed: None, + _marker: PhantomData, + } + } + + /// Configures the plugin instance to have a set seed for the + /// global entropy resource. + #[inline] + pub fn with_seed(mut self, seed: R::Seed) -> Self { + self.seed = Some(seed); + self + } +} + +impl Default for EntropyPlugin +where + R::Seed: Send + Sync + Copy, +{ + fn default() -> Self { + Self::new() + } +} + +impl Plugin for EntropyPlugin +where + R::Seed: Send + Sync + Copy, +{ + fn build(&self, app: &mut App) { + #[cfg(feature = "bevy_reflect")] + app.register_type::>() + .register_type::>(); + + if let Some(seed) = self.seed { + app.insert_resource(GlobalEntropy::::from_seed(seed)); + } else { + app.init_resource::>(); + } + } +} diff --git a/crates/bevy_entropy/src/prelude.rs b/crates/bevy_entropy/src/prelude.rs new file mode 100644 index 0000000000000..564a6b27c9a5d --- /dev/null +++ b/crates/bevy_entropy/src/prelude.rs @@ -0,0 +1,4 @@ +pub use crate::component::EntropyComponent; +pub use crate::plugin::EntropyPlugin; +pub use crate::resource::GlobalEntropy; +pub use crate::traits::SeedableEntropySource; diff --git a/crates/bevy_entropy/src/resource.rs b/crates/bevy_entropy/src/resource.rs new file mode 100644 index 0000000000000..7b57a33e5f850 --- /dev/null +++ b/crates/bevy_entropy/src/resource.rs @@ -0,0 +1,176 @@ +use std::fmt::Debug; + +use crate::traits::SeedableEntropySource; +use bevy_ecs::prelude::{ReflectResource, Resource}; +use rand_core::{RngCore, SeedableRng}; + +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{ + FromReflect, Reflect, ReflectDeserialize, ReflectFromReflect, ReflectSerialize, +}; + +/// A Global [`RngCore`] instance, meant for use as a Resource. Gets +/// created automatically with [`crate::plugin::EntropyPlugin`], or +/// can be created and added manually. +/// +/// # Example +/// +/// ``` +/// use bevy_ecs::prelude::ResMut; +/// use bevy_entropy::prelude::*; +/// use rand_core::RngCore; +/// use rand_chacha::ChaCha8Rng; +/// +/// fn print_random_value(mut rng: ResMut>) { +/// println!("Random value: {}", rng.next_u32()); +/// } +/// ``` +#[derive(Debug, Clone, PartialEq, Eq, Resource)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect, FromReflect))] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] +#[cfg_attr( + feature = "serialize", + serde(bound(deserialize = "R: for<'a> Deserialize<'a>")) +)] +#[cfg_attr( + all(feature = "serialize", feature = "bevy_reflect"), + reflect_value(Debug, PartialEq, Resource, FromReflect, Serialize, Deserialize) +)] +#[cfg_attr( + all(not(feature = "serialize"), feature = "bevy_reflect"), + reflect_value(Debug, PartialEq, Resource, FromReflect) +)] +pub struct GlobalEntropy(R); + +impl GlobalEntropy { + /// Create a new resource from a `RngCore` instance. + #[inline] + #[must_use] + pub fn new(rng: R) -> Self { + Self(rng) + } +} + +impl GlobalEntropy { + /// Create a new resource with an `RngCore` instance seeded + /// from a local entropy source. Generates a randomised, + /// non-deterministic seed for the resource. + #[inline] + #[must_use] + pub fn from_entropy() -> Self { + // Source entropy from system as there's only one Resource instance + // globally, so the overhead of a single operation is neglible. + Self(R::from_entropy()) + } + + /// Reseeds the internal `RngCore` instance with a new seed. + #[inline] + pub fn reseed(&mut self, seed: R::Seed) { + self.0 = R::from_seed(seed); + } +} + +impl Default for GlobalEntropy { + fn default() -> Self { + Self::from_entropy() + } +} + +impl RngCore for GlobalEntropy { + #[inline] + fn next_u32(&mut self) -> u32 { + self.0.next_u32() + } + + #[inline] + fn next_u64(&mut self) -> u64 { + self.0.next_u64() + } + + #[inline] + 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 GlobalEntropy { + type Seed = R::Seed; + + fn from_seed(seed: Self::Seed) -> Self { + Self::new(R::from_seed(seed)) + } +} + +impl From for GlobalEntropy { + fn from(value: R) -> Self { + Self::new(value) + } +} + +impl From<&mut R> for GlobalEntropy { + fn from(value: &mut R) -> Self { + Self::from_rng(value).unwrap() + } +} + +#[cfg(test)] +mod tests { + use rand_chacha::ChaCha8Rng; + + use super::*; + + #[test] + fn rng_reflection() { + use bevy_reflect::{ + serde::{ReflectSerializer, UntypedReflectDeserializer}, + TypeRegistry, + }; + use ron::ser::to_string; + use serde::de::DeserializeSeed; + + let mut registry = TypeRegistry::default(); + registry.register::>(); + + let mut val = GlobalEntropy::::from_seed([7; 32]); + + // Modify the state of the RNG instance + val.next_u32(); + + let ser = ReflectSerializer::new(&val, ®istry); + + let serialized = to_string(&ser).unwrap(); + + assert_eq!( + &serialized, + "{\"bevy_entropy::resource::GlobalEntropy\":((seed:(7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7),stream:0,word_pos:1))}" + ); + + let mut deserializer = ron::Deserializer::from_str(&serialized).unwrap(); + + let de = UntypedReflectDeserializer::new(®istry); + + let value = de.deserialize(&mut deserializer).unwrap(); + + let mut dynamic = value.take::>().unwrap(); + + // The two instances should be the same + assert_eq!( + val, dynamic, + "The deserialized GlobalEntropy should equal the original" + ); + // They should output the same numbers, as no state is lost between serialization and deserialization. + assert_eq!( + val.next_u32(), + dynamic.next_u32(), + "The deserialized GlobalEntropy should have the same output as original" + ); + } +} diff --git a/crates/bevy_entropy/src/thread_local_entropy.rs b/crates/bevy_entropy/src/thread_local_entropy.rs new file mode 100644 index 0000000000000..eeec7d9346ab1 --- /dev/null +++ b/crates/bevy_entropy/src/thread_local_entropy.rs @@ -0,0 +1,63 @@ +use std::{cell::UnsafeCell, rc::Rc}; + +use rand_chacha::ChaCha12Rng; +use rand_core::{RngCore, SeedableRng}; + +thread_local! { + // We require `Rc` to avoid premature freeing when `ThreadLocalEntropy` is used within thread-local destructors. + static SOURCE: Rc> = Rc::new(UnsafeCell::new(ChaCha12Rng::from_entropy())); +} + +pub(crate) struct ThreadLocalEntropy; + +impl ThreadLocalEntropy { + /// Inspired by `rand`'s approach to `ThreadRng` as well as `turborand`'s instantiation methods. The `Rc` + /// prevents the Rng instance from being cleaned up, giving it a `'static` lifetime. However, it does not + /// allow mutable access without a cell, so using `UnsafeCell` to bypass overheads associated with + /// `RefCell`. There's no direct access to the pointer or mutable reference, so we control how long it + /// lives and can ensure no multiple mutable references exist. + /// + /// # Safety + /// + /// Caller must ensure only one `mut` reference exists at a time. + #[inline] + unsafe fn get_rng(&'_ mut self) -> &'_ mut ChaCha12Rng { + // Obtain pointer to thread local instance of PRNG which with Rc, should be !Send & !Sync as well + // as 'static. + let rng = SOURCE.with(|source| source.get()); + + &mut *rng + } +} + +impl RngCore for ThreadLocalEntropy { + #[inline] + fn next_u32(&mut self) -> u32 { + // SAFETY: We must ensure to drop the `&mut rng` ref before creating another + // mutable reference + unsafe { self.get_rng().next_u32() } + } + + #[inline] + fn next_u64(&mut self) -> u64 { + // SAFETY: We must ensure to drop the `&mut rng` ref before creating another + // mutable reference + unsafe { self.get_rng().next_u64() } + } + + #[inline] + fn fill_bytes(&mut self, dest: &mut [u8]) { + // SAFETY: We must ensure to drop the `&mut rng` ref before creating another + // mutable reference + unsafe { + self.get_rng().fill_bytes(dest); + } + } + + #[inline] + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + // SAFETY: We must ensure to drop the `&mut rng` ref before creating another + // mutable reference + unsafe { self.get_rng().try_fill_bytes(dest) } + } +} diff --git a/crates/bevy_entropy/src/traits.rs b/crates/bevy_entropy/src/traits.rs new file mode 100644 index 0000000000000..9215cdd615743 --- /dev/null +++ b/crates/bevy_entropy/src/traits.rs @@ -0,0 +1,59 @@ +use std::fmt::Debug; + +use rand_core::{RngCore, SeedableRng}; + +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; + +/// A wrapper trait to encapsulate the required trait bounds for a seedable PRNG to +/// integrate into [`crate::component::EntropyComponent`] or +/// [`crate::resource::GlobalEntropy`]. This is a sealed trait. +#[cfg(feature = "serialize")] +pub trait SeedableEntropySource: + RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + private::SealedSeedable +{ +} + +#[cfg(feature = "serialize")] +impl SeedableEntropySource for T where + T: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> +{ +} + +/// A wrapper trait to encapsulate the required trait bounds for a seedable PRNG to +/// integrate into [`crate::component::EntropyComponent`] or +/// [`crate::resource::GlobalEntropy`]. This is a sealed trait. +#[cfg(not(feature = "serialize"))] +pub trait SeedableEntropySource: + RngCore + SeedableRng + Clone + Debug + PartialEq + Sync + Send + private::SealedSeedable +{ +} + +#[cfg(not(feature = "serialize"))] +impl SeedableEntropySource for T where + T: RngCore + SeedableRng + Clone + Debug + PartialEq + Sync + Send +{ +} + +mod private { + pub trait SealedSeedable {} + + impl SealedSeedable for T {} +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index fea2205f39a18..e784485b5f08e 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -59,7 +59,7 @@ symphonia-wav = ["bevy_audio/symphonia-wav"] # Enable watching file system for asset hot reload filesystem_watcher = ["bevy_asset/filesystem_watcher"] -serialize = ["bevy_core/serialize", "bevy_input/serialize", "bevy_time/serialize", "bevy_window/serialize", "bevy_transform/serialize", "bevy_math/serialize", "bevy_scene/serialize"] +serialize = ["bevy_core/serialize", "bevy_input/serialize", "bevy_time/serialize", "bevy_window/serialize", "bevy_transform/serialize", "bevy_math/serialize", "bevy_scene/serialize", "bevy_entropy?/serialize"] # Display server protocol support (X11 is enabled by default) wayland = ["bevy_winit/wayland"] @@ -120,6 +120,7 @@ bevy_animation = { path = "../bevy_animation", optional = true, version = "0.11. bevy_asset = { path = "../bevy_asset", optional = true, version = "0.11.0-dev" } bevy_audio = { path = "../bevy_audio", optional = true, version = "0.11.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", optional = true, version = "0.11.0-dev" } +bevy_entropy = { path = "../bevy_entropy", optional = true, version = "0.11.0-dev" } bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.11.0-dev" } bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.11.0-dev" } bevy_render = { path = "../bevy_render", optional = true, version = "0.11.0-dev" } diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs index 723e65afd9c8f..414da9a05ed57 100644 --- a/crates/bevy_internal/src/lib.rs +++ b/crates/bevy_internal/src/lib.rs @@ -121,6 +121,12 @@ pub mod core_pipeline { pub use bevy_core_pipeline::*; } +#[cfg(feature = "bevy_entropy")] +pub mod entropy { + //! Provides types and plugins for integrating PRNGs into bevy. + pub use bevy_entropy::*; +} + #[cfg(feature = "bevy_gilrs")] pub mod gilrs { //! Bevy interface with `GilRs` - "Game Input Library for Rust" - to handle gamepad inputs. diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index f9243382a11ef..8ad027c20df40 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -23,6 +23,10 @@ pub use crate::animation::prelude::*; #[cfg(feature = "bevy_core_pipeline")] pub use crate::core_pipeline::prelude::*; +#[doc(hidden)] +#[cfg(feature = "bevy_entropy")] +pub use crate::entropy::prelude::*; + #[doc(hidden)] #[cfg(feature = "bevy_pbr")] pub use crate::pbr::prelude::*; diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 1ba7c2ebb0d12..39a6f0a3efde3 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -17,6 +17,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_asset|Provides asset functionality| |bevy_audio|Provides audio functionality| |bevy_core_pipeline|Provides cameras and other basic render pipeline features| +|bevy_entropy|Plugin for providing PRNG integration into bevy| |bevy_gilrs|Adds gamepad support| |bevy_gizmos|Adds support for rendering gizmos| |bevy_gltf|[glTF](https://www.khronos.org/gltf/) support|