From b026fcdf03b07e626a258d34c8ceba4a99de2728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 2 Mar 2023 15:37:16 +0100 Subject: [PATCH 01/28] initial bevy_entropy implementation --- Cargo.toml | 4 + crates/bevy_entropy/Cargo.toml | 14 ++ crates/bevy_entropy/README.md | 7 + crates/bevy_entropy/src/component.rs | 212 ++++++++++++++++++++++ crates/bevy_entropy/src/entropy_source.rs | 50 +++++ crates/bevy_entropy/src/lib.rs | 110 +++++++++++ crates/bevy_entropy/src/prelude.rs | 3 + crates/bevy_entropy/src/resource.rs | 155 ++++++++++++++++ crates/bevy_internal/Cargo.toml | 1 + crates/bevy_internal/src/lib.rs | 6 + crates/bevy_internal/src/prelude.rs | 4 + 11 files changed, 566 insertions(+) create mode 100644 crates/bevy_entropy/Cargo.toml create mode 100644 crates/bevy_entropy/README.md create mode 100644 crates/bevy_entropy/src/component.rs create mode 100644 crates/bevy_entropy/src/entropy_source.rs create mode 100644 crates/bevy_entropy/src/lib.rs create mode 100644 crates/bevy_entropy/src/prelude.rs create mode 100644 crates/bevy_entropy/src/resource.rs 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..5700eb80e7212 --- /dev/null +++ b/crates/bevy_entropy/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bevy_entropy" +version = "0.9.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bevy_app = { path = "../bevy_app", version = "0.9.0" } +bevy_ecs = { path = "../bevy_ecs", version = "0.9.0" } +bevy_reflect = { path = "../bevy_reflect", version = "0.9.0" } +serde = { version = "1.0", features = ["derive"] } +rand_core = { version = "0.6", features = ["serde1"] } +rand_chacha = { version = "0.3" } diff --git a/crates/bevy_entropy/README.md b/crates/bevy_entropy/README.md new file mode 100644 index 0000000000000..be40db48f7641 --- /dev/null +++ b/crates/bevy_entropy/README.md @@ -0,0 +1,7 @@ +# 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) + +Docs TBA diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs new file mode 100644 index 0000000000000..ce304f3217328 --- /dev/null +++ b/crates/bevy_entropy/src/component.rs @@ -0,0 +1,212 @@ +use std::fmt::Debug; + +use bevy_ecs::{prelude::Component, world::Mut, system::ResMut}; +use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; +use rand_core::{RngCore, SeedableRng}; +use serde::{Deserialize, Serialize}; + +use crate::{resource::GlobalEntropy, entropy_source::EntropySource}; + +#[derive(Debug, Clone, PartialEq, Eq, Component, Reflect, FromReflect, Serialize, Deserialize)] +#[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] +#[reflect_value(Debug, PartialEq, Serialize, Deserialize)] +pub struct EntropyComponent< + R: RngCore + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, +>(R); + +impl< + R: RngCore + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > EntropyComponent +{ + #[inline] + #[must_use] + pub fn new(rng: R) -> Self { + Self(rng) + } +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > EntropyComponent +{ + #[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(R::from_rng(EntropySource).unwrap()) + } + + #[inline] + pub fn reseed(&mut self, seed: R::Seed) { + self.0 = R::from_seed(seed); + } +} + +impl< + R: RngCore + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > From for EntropyComponent +{ + fn from(value: R) -> Self { + Self::new(value) + } +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > Default for EntropyComponent +{ + fn default() -> Self { + Self::from_entropy() + } +} + +impl< + R: RngCore + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > 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< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > SeedableRng for EntropyComponent +{ + type Seed = R::Seed; + + fn from_seed(seed: Self::Seed) -> Self { + Self::new(R::from_seed(seed)) + } +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > From<&mut R> for EntropyComponent +{ + fn from(rng: &mut R) -> Self { + Self::from_rng(rng).unwrap() + } +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > From<&mut Mut<'_, R>> for EntropyComponent +{ + fn from(rng: &mut Mut<'_, R>) -> Self { + Self::from_rng(rng.as_mut()).unwrap() + } +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > From<&mut ResMut<'_, GlobalEntropy>> for EntropyComponent +{ + fn from(rng: &mut ResMut<'_, GlobalEntropy>) -> Self { + Self::from_rng(rng.as_mut()).unwrap() + } +} diff --git a/crates/bevy_entropy/src/entropy_source.rs b/crates/bevy_entropy/src/entropy_source.rs new file mode 100644 index 0000000000000..e988c21843b7d --- /dev/null +++ b/crates/bevy_entropy/src/entropy_source.rs @@ -0,0 +1,50 @@ +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 `EntropySource` is used within thread-local destructors. + pub(crate) static SOURCE: Rc> = Rc::new(UnsafeCell::new(ChaCha12Rng::from_entropy())) +} + +pub(crate) struct EntropySource; + +impl EntropySource { + /// 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. + fn get_rng(&mut self) -> &'static 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()); + + // SAFETY: We must make sure to stop using `rng` before anyone else creates another + // mutable reference + unsafe { &mut *rng } + } +} + +impl RngCore for EntropySource { + #[inline] + fn next_u32(&mut self) -> u32 { + self.get_rng().next_u32() + } + + #[inline] + fn next_u64(&mut self) -> u64 { + self.get_rng().next_u64() + } + + #[inline] + fn fill_bytes(&mut self, dest: &mut [u8]) { + self.get_rng().fill_bytes(dest) + } + + #[inline] + fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> { + self.get_rng().try_fill_bytes(dest) + } +} diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs new file mode 100644 index 0000000000000..f636d5d55fc94 --- /dev/null +++ b/crates/bevy_entropy/src/lib.rs @@ -0,0 +1,110 @@ +#![warn(clippy::undocumented_unsafe_blocks)] +#![doc = include_str!("../README.md")] + +pub mod component; +mod entropy_source; +pub mod prelude; +pub mod resource; + +use std::{fmt::Debug, marker::PhantomData}; + +use bevy_app::{App, Plugin}; +use component::EntropyComponent; +use rand_core::{RngCore, SeedableRng}; +use resource::GlobalEntropy; +use serde::{Deserialize, Serialize}; + +pub struct EntropyPlugin< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, +> { + seed: Option, + _marker: PhantomData<&'static mut R>, +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > EntropyPlugin +where + R::Seed: Send + Sync + Copy, +{ + pub fn new() -> Self { + Self { + seed: None, + _marker: PhantomData, + } + } + + pub fn with_seed(mut self, seed: R::Seed) -> Self { + self.seed = Some(seed); + self + } +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > Default for EntropyPlugin +where + R::Seed: Send + Sync + Copy, +{ + fn default() -> Self { + Self::new() + } +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > Plugin for EntropyPlugin +where + R::Seed: Send + Sync + Copy, +{ + fn build(&self, app: &mut App) { + app.register_type::>() + .register_type::>(); + + if let Some(seed) = self.seed { + app.insert_resource(GlobalEntropy::::from_seed(seed)); + } else { + app.init_resource::>(); + } + } + + fn is_unique(&self) -> bool { + false + } +} diff --git a/crates/bevy_entropy/src/prelude.rs b/crates/bevy_entropy/src/prelude.rs new file mode 100644 index 0000000000000..0eb8355893e98 --- /dev/null +++ b/crates/bevy_entropy/src/prelude.rs @@ -0,0 +1,3 @@ +pub use crate::component::EntropyComponent; +pub use crate::resource::GlobalEntropy; +pub use crate::EntropyPlugin; diff --git a/crates/bevy_entropy/src/resource.rs b/crates/bevy_entropy/src/resource.rs new file mode 100644 index 0000000000000..6bf18e9d6d134 --- /dev/null +++ b/crates/bevy_entropy/src/resource.rs @@ -0,0 +1,155 @@ +use std::fmt::Debug; + +use bevy_ecs::prelude::Resource; +use bevy_reflect::{Reflect, FromReflect, ReflectSerialize, ReflectDeserialize}; +use serde::{Deserialize, Serialize}; +use rand_core::{RngCore, SeedableRng}; + +#[derive(Debug, Clone, PartialEq, Eq, Resource, Reflect, FromReflect, Serialize, Deserialize)] +#[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] +#[reflect_value(Debug, PartialEq, Serialize, Deserialize)] +pub struct GlobalEntropy< + R: RngCore + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, +>(R); + +impl< + R: RngCore + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > GlobalEntropy +{ + #[inline] + #[must_use] + pub fn new(rng: R) -> Self { + Self(rng) + } +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > GlobalEntropy +{ + #[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()) + } + + #[inline] + pub fn reseed(&mut self, seed: R::Seed) { + self.0 = R::from_seed(seed); + } +} + +impl< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > Default for GlobalEntropy +{ + fn default() -> Self { + Self::from_entropy() + } +} + +impl< + R: RngCore + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > From for GlobalEntropy +{ + fn from(value: R) -> Self { + Self::new(value) + } +} + +impl< + R: RngCore + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > 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< + R: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + 'static, + > SeedableRng for GlobalEntropy +{ + type Seed = R::Seed; + + fn from_seed(seed: Self::Seed) -> Self { + Self::new(R::from_seed(seed)) + } +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index fea2205f39a18..d75a0ce4ac1ce 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -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::*; From af2acc32f80247cfc40dadf21e1190c9f19f8751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 2 Mar 2023 18:02:02 +0100 Subject: [PATCH 02/28] Add cargo.toml info and license --- crates/bevy_entropy/Cargo.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/bevy_entropy/Cargo.toml b/crates/bevy_entropy/Cargo.toml index 5700eb80e7212..dc60f0568b51b 100644 --- a/crates/bevy_entropy/Cargo.toml +++ b/crates/bevy_entropy/Cargo.toml @@ -2,6 +2,12 @@ name = "bevy_entropy" version = "0.9.0" 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"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html From 56a1153b89700003ea9517396f21664a24055c9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 2 Mar 2023 18:33:28 +0100 Subject: [PATCH 03/28] Add plugin doctest as initial smoke test --- crates/bevy_entropy/Cargo.toml | 2 +- crates/bevy_entropy/src/lib.rs | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/bevy_entropy/Cargo.toml b/crates/bevy_entropy/Cargo.toml index dc60f0568b51b..b05168b5df8b8 100644 --- a/crates/bevy_entropy/Cargo.toml +++ b/crates/bevy_entropy/Cargo.toml @@ -17,4 +17,4 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.9.0" } bevy_reflect = { path = "../bevy_reflect", version = "0.9.0" } serde = { version = "1.0", features = ["derive"] } rand_core = { version = "0.6", features = ["serde1"] } -rand_chacha = { version = "0.3" } +rand_chacha = { version = "0.3", features = ["serde1"] } diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs index f636d5d55fc94..1b0ec8f14125a 100644 --- a/crates/bevy_entropy/src/lib.rs +++ b/crates/bevy_entropy/src/lib.rs @@ -14,6 +14,28 @@ use rand_core::{RngCore, SeedableRng}; use resource::GlobalEntropy; use serde::{Deserialize, Serialize}; +/// 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; +/// use bevy_entropy::prelude::*; +/// use rand_core::RngCore; +/// use rand_chacha::ChaCha8Rng; +/// +/// fn main() { +/// App::new() +/// .add_plugin(EntropyPlugin::::default()) +/// .add_system(print_random_value) +/// .run(); +/// } +/// +/// fn print_random_value(mut rng: ResMut>) { +/// println!("Random value: {}", rng.next_u32()); +/// } +/// ``` pub struct EntropyPlugin< R: RngCore + SeedableRng From d9a5d3a394ee971e7c3b72643f309141f0a2540c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 2 Mar 2023 18:38:57 +0100 Subject: [PATCH 04/28] Apply cargo fmt --- crates/bevy_entropy/src/component.rs | 4 ++-- crates/bevy_entropy/src/lib.rs | 2 +- crates/bevy_entropy/src/resource.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index ce304f3217328..9a686f16cff7f 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -1,11 +1,11 @@ use std::fmt::Debug; -use bevy_ecs::{prelude::Component, world::Mut, system::ResMut}; +use bevy_ecs::{prelude::Component, system::ResMut, world::Mut}; use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; use rand_core::{RngCore, SeedableRng}; use serde::{Deserialize, Serialize}; -use crate::{resource::GlobalEntropy, entropy_source::EntropySource}; +use crate::{entropy_source::EntropySource, resource::GlobalEntropy}; #[derive(Debug, Clone, PartialEq, Eq, Component, Reflect, FromReflect, Serialize, Deserialize)] #[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs index 1b0ec8f14125a..1e324ad48a6f9 100644 --- a/crates/bevy_entropy/src/lib.rs +++ b/crates/bevy_entropy/src/lib.rs @@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize}; /// 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; diff --git a/crates/bevy_entropy/src/resource.rs b/crates/bevy_entropy/src/resource.rs index 6bf18e9d6d134..634b6df389dec 100644 --- a/crates/bevy_entropy/src/resource.rs +++ b/crates/bevy_entropy/src/resource.rs @@ -1,9 +1,9 @@ use std::fmt::Debug; use bevy_ecs::prelude::Resource; -use bevy_reflect::{Reflect, FromReflect, ReflectSerialize, ReflectDeserialize}; -use serde::{Deserialize, Serialize}; +use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; use rand_core::{RngCore, SeedableRng}; +use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Resource, Reflect, FromReflect, Serialize, Deserialize)] #[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] From 4c02a47549ca31a319c1f58a7ad7b26d49d9eb75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 2 Mar 2023 18:43:34 +0100 Subject: [PATCH 05/28] Add missing features doc --- docs/cargo_features.md | 1 + 1 file changed, 1 insertion(+) 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| From 38177e2470360bf5934ef98ce8c48df152d626f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 2 Mar 2023 18:54:11 +0100 Subject: [PATCH 06/28] Add missing semi-colon --- crates/bevy_entropy/src/entropy_source.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_entropy/src/entropy_source.rs b/crates/bevy_entropy/src/entropy_source.rs index e988c21843b7d..b493d234091a5 100644 --- a/crates/bevy_entropy/src/entropy_source.rs +++ b/crates/bevy_entropy/src/entropy_source.rs @@ -40,7 +40,7 @@ impl RngCore for EntropySource { #[inline] fn fill_bytes(&mut self, dest: &mut [u8]) { - self.get_rng().fill_bytes(dest) + self.get_rng().fill_bytes(dest); } #[inline] From 25d0da59fb247033e5600f85f98b1c8925521bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Fri, 3 Mar 2023 09:21:23 +0100 Subject: [PATCH 07/28] fix feature enablement and organise code better --- crates/bevy_entropy/Cargo.toml | 5 ++++- crates/bevy_entropy/src/component.rs | 8 ++++---- crates/bevy_entropy/src/entropy_source.rs | 5 +++-- crates/bevy_entropy/src/lib.rs | 3 +++ crates/bevy_entropy/src/resource.rs | 3 +-- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/bevy_entropy/Cargo.toml b/crates/bevy_entropy/Cargo.toml index b05168b5df8b8..02c8c4c52cd17 100644 --- a/crates/bevy_entropy/Cargo.toml +++ b/crates/bevy_entropy/Cargo.toml @@ -16,5 +16,8 @@ bevy_app = { path = "../bevy_app", version = "0.9.0" } bevy_ecs = { path = "../bevy_ecs", version = "0.9.0" } bevy_reflect = { path = "../bevy_reflect", version = "0.9.0" } serde = { version = "1.0", features = ["derive"] } -rand_core = { version = "0.6", features = ["serde1"] } +rand_core = { version = "0.6", features = ["std", "serde1"] } rand_chacha = { version = "0.3", features = ["serde1"] } + +[target.'cfg(all(any(target_arch = "wasm32", target_arch = "wasm64"), target_os = "unknown"))'.dependencies] +getrandom = { version = "0.2", features = ["js"] } diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index 9a686f16cff7f..9b5387efacb40 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -1,11 +1,11 @@ use std::fmt::Debug; +use crate::{ + entropy_source::EntropySource, resource::GlobalEntropy, Deserialize, RngCore, SeedableRng, + Serialize, +}; use bevy_ecs::{prelude::Component, system::ResMut, world::Mut}; use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; -use rand_core::{RngCore, SeedableRng}; -use serde::{Deserialize, Serialize}; - -use crate::{entropy_source::EntropySource, resource::GlobalEntropy}; #[derive(Debug, Clone, PartialEq, Eq, Component, Reflect, FromReflect, Serialize, Deserialize)] #[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] diff --git a/crates/bevy_entropy/src/entropy_source.rs b/crates/bevy_entropy/src/entropy_source.rs index b493d234091a5..53d5bec88db5a 100644 --- a/crates/bevy_entropy/src/entropy_source.rs +++ b/crates/bevy_entropy/src/entropy_source.rs @@ -1,11 +1,11 @@ use std::{cell::UnsafeCell, rc::Rc}; +use crate::{RngCore, SeedableRng}; use rand_chacha::ChaCha12Rng; -use rand_core::{RngCore, SeedableRng}; thread_local! { // We require `Rc` to avoid premature freeing when `EntropySource` is used within thread-local destructors. - pub(crate) static SOURCE: Rc> = Rc::new(UnsafeCell::new(ChaCha12Rng::from_entropy())) + static SOURCE: Rc> = Rc::new(UnsafeCell::new(ChaCha12Rng::from_entropy())); } pub(crate) struct EntropySource; @@ -16,6 +16,7 @@ impl EntropySource { /// 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. + #[inline] fn get_rng(&mut self) -> &'static mut ChaCha12Rng { // Obtain pointer to thread local instance of PRNG which with Rc, should be !Send & !Sync as well // as 'static. diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs index 1e324ad48a6f9..4b8ba283662fa 100644 --- a/crates/bevy_entropy/src/lib.rs +++ b/crates/bevy_entropy/src/lib.rs @@ -67,6 +67,8 @@ impl< where R::Seed: Send + Sync + Copy, { + #[inline] + #[must_use] pub fn new() -> Self { Self { seed: None, @@ -74,6 +76,7 @@ where } } + #[inline] pub fn with_seed(mut self, seed: R::Seed) -> Self { self.seed = Some(seed); self diff --git a/crates/bevy_entropy/src/resource.rs b/crates/bevy_entropy/src/resource.rs index 634b6df389dec..bfd2b40ccd69f 100644 --- a/crates/bevy_entropy/src/resource.rs +++ b/crates/bevy_entropy/src/resource.rs @@ -1,9 +1,8 @@ use std::fmt::Debug; +use crate::{Deserialize, RngCore, SeedableRng, Serialize}; use bevy_ecs::prelude::Resource; use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; -use rand_core::{RngCore, SeedableRng}; -use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, PartialEq, Eq, Resource, Reflect, FromReflect, Serialize, Deserialize)] #[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] From 608dec1f55d661b4aeb2d13d570cc902813f1927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Sat, 4 Mar 2023 17:58:59 +0100 Subject: [PATCH 08/28] provide examples on deterministic RNG output --- crates/bevy_entropy/Cargo.toml | 11 ++ crates/bevy_entropy/examples/determinism.rs | 199 ++++++++++++++++++++ crates/bevy_entropy/examples/parallelism.rs | 74 ++++++++ 3 files changed, 284 insertions(+) create mode 100644 crates/bevy_entropy/examples/determinism.rs create mode 100644 crates/bevy_entropy/examples/parallelism.rs diff --git a/crates/bevy_entropy/Cargo.toml b/crates/bevy_entropy/Cargo.toml index 02c8c4c52cd17..01c65279cc978 100644 --- a/crates/bevy_entropy/Cargo.toml +++ b/crates/bevy_entropy/Cargo.toml @@ -19,5 +19,16 @@ serde = { version = "1.0", features = ["derive"] } rand_core = { version = "0.6", features = ["std", "serde1"] } rand_chacha = { version = "0.3", features = ["serde1"] } +[dev-dependencies] +rand = "0.8" + [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/examples/determinism.rs b/crates/bevy_entropy/examples/determinism.rs new file mode 100644 index 0000000000000..d560d6d86615f --- /dev/null +++ b/crates/bevy_entropy/examples/determinism.rs @@ -0,0 +1,199 @@ +#![allow(clippy::type_complexity)] + +use bevy_app::App; +use bevy_ecs::{ + prelude::{Component, Entity, ResMut}, + query::With, + schedule::IntoSystemConfig, + 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_startup_systems((setup_player, setup_enemies.after(setup_player))) + .add_system(determine_attack_order.pipe(attack_turn)) + .add_system(buff_entities.after(attack_turn)) + .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..62c7cc2d17689 --- /dev/null +++ b/crates/bevy_entropy/examples/parallelism.rs @@ -0,0 +1,74 @@ +#![allow(clippy::type_complexity)] + +use bevy_app::App; +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 just the RNG output, as long as the entities are +/// seeded deterministically and any systems that query a specific entity or group of entities are +/// assured to be in order. +fn main() { + App::new() + .add_plugin(EntropyPlugin::::new().with_seed([2; 32])) + .add_startup_system(setup_sources) + .add_system(random_output_a) + .add_system(random_output_b) + .add_system(random_output_c) + .add_system(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))); +} From b10205c418d6721e907fb78961545990767fae6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Mon, 6 Mar 2023 09:35:54 +0100 Subject: [PATCH 09/28] reorganise trait soup into wrapper traits --- crates/bevy_entropy/src/component.rs | 155 +++--------------- crates/bevy_entropy/src/lib.rs | 58 +------ crates/bevy_entropy/src/resource.rs | 114 +++---------- ...ropy_source.rs => thread_local_entropy.rs} | 8 +- crates/bevy_entropy/src/traits.rs | 39 +++++ 5 files changed, 94 insertions(+), 280 deletions(-) rename crates/bevy_entropy/src/{entropy_source.rs => thread_local_entropy.rs} (87%) create mode 100644 crates/bevy_entropy/src/traits.rs diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index 9b5387efacb40..09cfdaa7ce147 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -1,8 +1,10 @@ use std::fmt::Debug; use crate::{ - entropy_source::EntropySource, resource::GlobalEntropy, Deserialize, RngCore, SeedableRng, - Serialize, + thread_local_entropy::ThreadLocalEntropy, + resource::GlobalEntropy, + traits::{EntropySource, SeedableEntropySource}, + Deserialize, RngCore, SeedableRng, Serialize, }; use bevy_ecs::{prelude::Component, system::ResMut, world::Mut}; use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; @@ -10,30 +12,9 @@ use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; #[derive(Debug, Clone, PartialEq, Eq, Component, Reflect, FromReflect, Serialize, Deserialize)] #[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] #[reflect_value(Debug, PartialEq, Serialize, Deserialize)] -pub struct EntropyComponent< - R: RngCore - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, ->(R); - -impl< - R: RngCore - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > EntropyComponent -{ +pub struct EntropyComponent(R); + +impl EntropyComponent { #[inline] #[must_use] pub fn new(rng: R) -> Self { @@ -41,26 +22,14 @@ impl< } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > EntropyComponent -{ +impl EntropyComponent { #[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(R::from_rng(EntropySource).unwrap()) + Self(R::from_rng(ThreadLocalEntropy).unwrap()) } #[inline] @@ -69,53 +38,13 @@ impl< } } -impl< - R: RngCore - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > From for EntropyComponent -{ - fn from(value: R) -> Self { - Self::new(value) - } -} - -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > Default for EntropyComponent -{ +impl Default for EntropyComponent { fn default() -> Self { Self::from_entropy() } } -impl< - R: RngCore - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > RngCore for EntropyComponent -{ +impl RngCore for EntropyComponent { #[inline] fn next_u32(&mut self) -> u32 { self.0.next_u32() @@ -137,19 +66,7 @@ impl< } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > SeedableRng for EntropyComponent -{ +impl SeedableRng for EntropyComponent { type Seed = R::Seed; fn from_seed(seed: Self::Seed) -> Self { @@ -157,55 +74,25 @@ impl< } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > From<&mut R> for EntropyComponent -{ +impl From for EntropyComponent { + fn from(value: R) -> Self { + Self::new(value) + } +} + +impl From<&mut R> for EntropyComponent { fn from(rng: &mut R) -> Self { Self::from_rng(rng).unwrap() } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > From<&mut Mut<'_, R>> for EntropyComponent -{ +impl From<&mut Mut<'_, R>> for EntropyComponent { fn from(rng: &mut Mut<'_, R>) -> Self { Self::from_rng(rng.as_mut()).unwrap() } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > From<&mut ResMut<'_, GlobalEntropy>> for EntropyComponent -{ +impl From<&mut ResMut<'_, GlobalEntropy>> for EntropyComponent { fn from(rng: &mut ResMut<'_, GlobalEntropy>) -> Self { Self::from_rng(rng.as_mut()).unwrap() } diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs index 4b8ba283662fa..ba777a7b1ce4b 100644 --- a/crates/bevy_entropy/src/lib.rs +++ b/crates/bevy_entropy/src/lib.rs @@ -2,17 +2,19 @@ #![doc = include_str!("../README.md")] pub mod component; -mod entropy_source; pub mod prelude; pub mod resource; +mod thread_local_entropy; +mod traits; -use std::{fmt::Debug, marker::PhantomData}; +use std::marker::PhantomData; use bevy_app::{App, Plugin}; use component::EntropyComponent; use rand_core::{RngCore, SeedableRng}; use resource::GlobalEntropy; use serde::{Deserialize, Serialize}; +use traits::SeedableEntropySource; /// Plugin for integrating a PRNG that implements `RngCore` into /// the bevy engine, registering types for a global resource and @@ -36,34 +38,12 @@ use serde::{Deserialize, Serialize}; /// println!("Random value: {}", rng.next_u32()); /// } /// ``` -pub struct EntropyPlugin< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, -> { +pub struct EntropyPlugin { seed: Option, _marker: PhantomData<&'static mut R>, } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > EntropyPlugin +impl EntropyPlugin where R::Seed: Send + Sync + Copy, { @@ -83,18 +63,7 @@ where } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > Default for EntropyPlugin +impl Default for EntropyPlugin where R::Seed: Send + Sync + Copy, { @@ -103,18 +72,7 @@ where } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > Plugin for EntropyPlugin +impl Plugin for EntropyPlugin where R::Seed: Send + Sync + Copy, { diff --git a/crates/bevy_entropy/src/resource.rs b/crates/bevy_entropy/src/resource.rs index bfd2b40ccd69f..fbe233de36a0a 100644 --- a/crates/bevy_entropy/src/resource.rs +++ b/crates/bevy_entropy/src/resource.rs @@ -1,36 +1,18 @@ use std::fmt::Debug; -use crate::{Deserialize, RngCore, SeedableRng, Serialize}; +use crate::{ + traits::{EntropySource, SeedableEntropySource}, + Deserialize, RngCore, SeedableRng, Serialize, +}; use bevy_ecs::prelude::Resource; use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; #[derive(Debug, Clone, PartialEq, Eq, Resource, Reflect, FromReflect, Serialize, Deserialize)] #[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] #[reflect_value(Debug, PartialEq, Serialize, Deserialize)] -pub struct GlobalEntropy< - R: RngCore - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, ->(R); +pub struct GlobalEntropy(R); -impl< - R: RngCore - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > GlobalEntropy -{ +impl GlobalEntropy { #[inline] #[must_use] pub fn new(rng: R) -> Self { @@ -38,19 +20,7 @@ impl< } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > GlobalEntropy -{ +impl GlobalEntropy { #[inline] #[must_use] pub fn from_entropy() -> Self { @@ -65,53 +35,13 @@ impl< } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > Default for GlobalEntropy -{ +impl Default for GlobalEntropy { fn default() -> Self { Self::from_entropy() } } -impl< - R: RngCore - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > From for GlobalEntropy -{ - fn from(value: R) -> Self { - Self::new(value) - } -} - -impl< - R: RngCore - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > RngCore for GlobalEntropy -{ +impl RngCore for GlobalEntropy { #[inline] fn next_u32(&mut self) -> u32 { self.0.next_u32() @@ -133,22 +63,22 @@ impl< } } -impl< - R: RngCore - + SeedableRng - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + 'static, - > SeedableRng for GlobalEntropy -{ +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() + } +} diff --git a/crates/bevy_entropy/src/entropy_source.rs b/crates/bevy_entropy/src/thread_local_entropy.rs similarity index 87% rename from crates/bevy_entropy/src/entropy_source.rs rename to crates/bevy_entropy/src/thread_local_entropy.rs index 53d5bec88db5a..aba52764958a3 100644 --- a/crates/bevy_entropy/src/entropy_source.rs +++ b/crates/bevy_entropy/src/thread_local_entropy.rs @@ -4,13 +4,13 @@ use crate::{RngCore, SeedableRng}; use rand_chacha::ChaCha12Rng; thread_local! { - // We require `Rc` to avoid premature freeing when `EntropySource` is used within thread-local destructors. + // 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 EntropySource; +pub(crate) struct ThreadLocalEntropy; -impl EntropySource { +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 @@ -28,7 +28,7 @@ impl EntropySource { } } -impl RngCore for EntropySource { +impl RngCore for ThreadLocalEntropy { #[inline] fn next_u32(&mut self) -> u32 { self.get_rng().next_u32() diff --git a/crates/bevy_entropy/src/traits.rs b/crates/bevy_entropy/src/traits.rs new file mode 100644 index 0000000000000..cb64b9cbe015b --- /dev/null +++ b/crates/bevy_entropy/src/traits.rs @@ -0,0 +1,39 @@ +use std::fmt::Debug; + +use crate::{Deserialize, RngCore, SeedableRng, Serialize}; + +pub trait EntropySource: + RngCore + Clone + Debug + PartialEq + Sync + Send + Serialize + for<'a> Deserialize<'a> +{ +} + +impl EntropySource for T where + T: RngCore + Clone + Debug + PartialEq + Sync + Send + Serialize + for<'a> Deserialize<'a> +{ +} + +pub trait SeedableEntropySource: + RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> +{ +} + +impl SeedableEntropySource for T where + T: RngCore + + SeedableRng + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> +{ +} From bc97d7a3414f718f0b20ad944b27e16fe519dbcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 7 Mar 2023 09:09:18 +0100 Subject: [PATCH 10/28] first-pass on splitting features, --- crates/bevy_entropy/Cargo.toml | 22 +++-- crates/bevy_entropy/src/component.rs | 27 ++++-- crates/bevy_entropy/src/lib.rs | 86 +------------------ crates/bevy_entropy/src/plugin.rs | 85 ++++++++++++++++++ crates/bevy_entropy/src/prelude.rs | 2 +- crates/bevy_entropy/src/resource.rs | 25 ++++-- .../bevy_entropy/src/thread_local_entropy.rs | 2 +- crates/bevy_entropy/src/traits.rs | 49 ++++++++++- crates/bevy_internal/Cargo.toml | 2 +- 9 files changed, 189 insertions(+), 111 deletions(-) create mode 100644 crates/bevy_entropy/src/plugin.rs diff --git a/crates/bevy_entropy/Cargo.toml b/crates/bevy_entropy/Cargo.toml index 01c65279cc978..108dd89d58319 100644 --- a/crates/bevy_entropy/Cargo.toml +++ b/crates/bevy_entropy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bevy_entropy" -version = "0.9.0" +version = "0.11.0-dev" edition = "2021" description = "Bevy Engine's RNG integration" homepage = "https://bevyengine.org" @@ -9,15 +9,21 @@ license = "MIT OR Apache-2.0" keywords = ["game", "bevy", "rand", "rng"] categories = ["game-engines", "algorithms"] -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[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_app = { path = "../bevy_app", version = "0.9.0" } -bevy_ecs = { path = "../bevy_ecs", version = "0.9.0" } -bevy_reflect = { path = "../bevy_reflect", version = "0.9.0" } -serde = { version = "1.0", features = ["derive"] } -rand_core = { version = "0.6", features = ["std", "serde1"] } -rand_chacha = { version = "0.3", features = ["serde1"] } +# bevy +bevy_app = { path = "../bevy_app", version = "0.9.0", default-features = false } +bevy_ecs = { path = "../bevy_ecs", version = "0.9.0", default-features = false } +bevy_reflect = { path = "../bevy_reflect", version = "0.9.0", 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" diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index 09cfdaa7ce147..5a06c1869fccd 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -1,17 +1,30 @@ use std::fmt::Debug; use crate::{ - thread_local_entropy::ThreadLocalEntropy, resource::GlobalEntropy, + thread_local_entropy::ThreadLocalEntropy, traits::{EntropySource, SeedableEntropySource}, - Deserialize, RngCore, SeedableRng, Serialize, }; use bevy_ecs::{prelude::Component, 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, ReflectSerialize}; -#[derive(Debug, Clone, PartialEq, Eq, Component, Reflect, FromReflect, Serialize, Deserialize)] -#[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] -#[reflect_value(Debug, PartialEq, Serialize, Deserialize)] +#[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, Serialize, Deserialize) +)] pub struct EntropyComponent(R); impl EntropyComponent { @@ -92,7 +105,9 @@ impl From<&mut Mut<'_, R>> for EntropyCompon } } -impl From<&mut ResMut<'_, GlobalEntropy>> for EntropyComponent { +impl From<&mut ResMut<'_, GlobalEntropy>> + for EntropyComponent +{ fn from(rng: &mut ResMut<'_, GlobalEntropy>) -> Self { Self::from_rng(rng.as_mut()).unwrap() } diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs index ba777a7b1ce4b..13880a1d0bc96 100644 --- a/crates/bevy_entropy/src/lib.rs +++ b/crates/bevy_entropy/src/lib.rs @@ -4,90 +4,6 @@ pub mod component; pub mod prelude; pub mod resource; +pub mod plugin; mod thread_local_entropy; mod traits; - -use std::marker::PhantomData; - -use bevy_app::{App, Plugin}; -use component::EntropyComponent; -use rand_core::{RngCore, SeedableRng}; -use resource::GlobalEntropy; -use serde::{Deserialize, Serialize}; -use traits::SeedableEntropySource; - -/// 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; -/// use bevy_entropy::prelude::*; -/// use rand_core::RngCore; -/// use rand_chacha::ChaCha8Rng; -/// -/// fn main() { -/// App::new() -/// .add_plugin(EntropyPlugin::::default()) -/// .add_system(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, -{ - #[inline] - #[must_use] - pub fn new() -> Self { - Self { - seed: None, - _marker: PhantomData, - } - } - - #[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) { - app.register_type::>() - .register_type::>(); - - if let Some(seed) = self.seed { - app.insert_resource(GlobalEntropy::::from_seed(seed)); - } else { - app.init_resource::>(); - } - } - - fn is_unique(&self) -> bool { - false - } -} diff --git a/crates/bevy_entropy/src/plugin.rs b/crates/bevy_entropy/src/plugin.rs new file mode 100644 index 0000000000000..9d7ae724be2fe --- /dev/null +++ b/crates/bevy_entropy/src/plugin.rs @@ -0,0 +1,85 @@ +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; +/// use bevy_entropy::prelude::*; +/// use rand_core::RngCore; +/// use rand_chacha::ChaCha8Rng; +/// +/// fn main() { +/// App::new() +/// .add_plugin(EntropyPlugin::::default()) +/// .add_system(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, +{ + #[inline] + #[must_use] + pub fn new() -> Self { + Self { + seed: None, + _marker: PhantomData, + } + } + + #[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::>(); + } + } + + fn is_unique(&self) -> bool { + false + } +} diff --git a/crates/bevy_entropy/src/prelude.rs b/crates/bevy_entropy/src/prelude.rs index 0eb8355893e98..d8c777605bf6c 100644 --- a/crates/bevy_entropy/src/prelude.rs +++ b/crates/bevy_entropy/src/prelude.rs @@ -1,3 +1,3 @@ pub use crate::component::EntropyComponent; pub use crate::resource::GlobalEntropy; -pub use crate::EntropyPlugin; +pub use crate::plugin::EntropyPlugin; diff --git a/crates/bevy_entropy/src/resource.rs b/crates/bevy_entropy/src/resource.rs index fbe233de36a0a..a269d296904c9 100644 --- a/crates/bevy_entropy/src/resource.rs +++ b/crates/bevy_entropy/src/resource.rs @@ -1,15 +1,26 @@ use std::fmt::Debug; -use crate::{ - traits::{EntropySource, SeedableEntropySource}, - Deserialize, RngCore, SeedableRng, Serialize, -}; +use crate::traits::{EntropySource, SeedableEntropySource}; use bevy_ecs::prelude::Resource; +use rand_core::{RngCore, SeedableRng}; + +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "bevy_reflect")] use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; -#[derive(Debug, Clone, PartialEq, Eq, Resource, Reflect, FromReflect, Serialize, Deserialize)] -#[serde(bound(deserialize = "R: for<'a> Deserialize<'a>"))] -#[reflect_value(Debug, PartialEq, Serialize, Deserialize)] +#[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, Serialize, Deserialize) +)] pub struct GlobalEntropy(R); impl GlobalEntropy { diff --git a/crates/bevy_entropy/src/thread_local_entropy.rs b/crates/bevy_entropy/src/thread_local_entropy.rs index aba52764958a3..b5c4de0e0a0e7 100644 --- a/crates/bevy_entropy/src/thread_local_entropy.rs +++ b/crates/bevy_entropy/src/thread_local_entropy.rs @@ -1,7 +1,7 @@ use std::{cell::UnsafeCell, rc::Rc}; -use crate::{RngCore, SeedableRng}; 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. diff --git a/crates/bevy_entropy/src/traits.rs b/crates/bevy_entropy/src/traits.rs index cb64b9cbe015b..cd58e608927a5 100644 --- a/crates/bevy_entropy/src/traits.rs +++ b/crates/bevy_entropy/src/traits.rs @@ -1,17 +1,40 @@ use std::fmt::Debug; -use crate::{Deserialize, RngCore, SeedableRng, Serialize}; +use rand_core::{RngCore, SeedableRng}; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "serialize")] pub trait EntropySource: - RngCore + Clone + Debug + PartialEq + Sync + Send + Serialize + for<'a> Deserialize<'a> + RngCore + + Clone + + Debug + + PartialEq + + Sync + + Send + + Serialize + + for<'a> Deserialize<'a> + + private::SealedEntropy { } +#[cfg(feature = "serialize")] impl EntropySource for T where T: RngCore + Clone + Debug + PartialEq + Sync + Send + Serialize + for<'a> Deserialize<'a> { } +#[cfg(not(feature = "serialize"))] +pub trait EntropySource: + RngCore + Clone + Debug + PartialEq + Sync + Send + private::SealedEntropy +{ +} + +#[cfg(not(feature = "serialize"))] +impl EntropySource for T where T: RngCore + Clone + Debug + PartialEq + Sync + Send {} + +#[cfg(feature = "serialize")] pub trait SeedableEntropySource: RngCore + SeedableRng @@ -22,9 +45,11 @@ pub trait SeedableEntropySource: + Send + Serialize + for<'a> Deserialize<'a> + + private::SealedSeedable { } +#[cfg(feature = "serialize")] impl SeedableEntropySource for T where T: RngCore + SeedableRng @@ -37,3 +62,23 @@ impl SeedableEntropySource for T where + for<'a> Deserialize<'a> { } + +#[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 SealedEntropy {} + pub trait SealedSeedable {} + + impl SealedEntropy for T {} + impl SealedSeedable for T {} +} diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index d75a0ce4ac1ce..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"] From 058b5405cac6fed496c1bc0945da2466f04e5d3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 7 Mar 2023 09:15:22 +0100 Subject: [PATCH 11/28] fix dependency versions post rebase --- crates/bevy_entropy/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_entropy/Cargo.toml b/crates/bevy_entropy/Cargo.toml index 108dd89d58319..971ee2dea9e12 100644 --- a/crates/bevy_entropy/Cargo.toml +++ b/crates/bevy_entropy/Cargo.toml @@ -16,9 +16,9 @@ serialize = ["dep:serde", "rand_core/serde1", "rand_chacha/serde1"] [dependencies] # bevy -bevy_app = { path = "../bevy_app", version = "0.9.0", default-features = false } -bevy_ecs = { path = "../bevy_ecs", version = "0.9.0", default-features = false } -bevy_reflect = { path = "../bevy_reflect", version = "0.9.0", optional = true } +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 } From 3dded389346f1cc7547aafec35bfe83214b97320 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 7 Mar 2023 09:21:18 +0100 Subject: [PATCH 12/28] cargo fmt fix --- crates/bevy_entropy/src/lib.rs | 2 +- crates/bevy_entropy/src/prelude.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs index 13880a1d0bc96..563277e90b65d 100644 --- a/crates/bevy_entropy/src/lib.rs +++ b/crates/bevy_entropy/src/lib.rs @@ -2,8 +2,8 @@ #![doc = include_str!("../README.md")] pub mod component; +pub mod plugin; pub mod prelude; pub mod resource; -pub mod plugin; mod thread_local_entropy; mod traits; diff --git a/crates/bevy_entropy/src/prelude.rs b/crates/bevy_entropy/src/prelude.rs index d8c777605bf6c..264d2ac3cb80d 100644 --- a/crates/bevy_entropy/src/prelude.rs +++ b/crates/bevy_entropy/src/prelude.rs @@ -1,3 +1,3 @@ pub use crate::component::EntropyComponent; -pub use crate::resource::GlobalEntropy; pub use crate::plugin::EntropyPlugin; +pub use crate::resource::GlobalEntropy; From e221dea10eb7b87f1ddd9661a71579c770af11a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 7 Mar 2023 09:45:37 +0100 Subject: [PATCH 13/28] make use of new system config features in examples --- crates/bevy_entropy/examples/determinism.rs | 7 +++---- crates/bevy_entropy/examples/parallelism.rs | 10 ++++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/crates/bevy_entropy/examples/determinism.rs b/crates/bevy_entropy/examples/determinism.rs index d560d6d86615f..edd121961102e 100644 --- a/crates/bevy_entropy/examples/determinism.rs +++ b/crates/bevy_entropy/examples/determinism.rs @@ -4,7 +4,7 @@ use bevy_app::App; use bevy_ecs::{ prelude::{Component, Entity, ResMut}, query::With, - schedule::IntoSystemConfig, + schedule::IntoSystemConfigs, system::{Commands, In, IntoPipeSystem, Query}, }; use bevy_entropy::prelude::*; @@ -52,9 +52,8 @@ struct Health { fn main() { App::new() .add_plugin(EntropyPlugin::::new().with_seed([1; 32])) - .add_startup_systems((setup_player, setup_enemies.after(setup_player))) - .add_system(determine_attack_order.pipe(attack_turn)) - .add_system(buff_entities.after(attack_turn)) + .add_startup_systems((setup_player, setup_enemies).chain()) + .add_systems((determine_attack_order.pipe(attack_turn), buff_entities).chain()) .run(); } diff --git a/crates/bevy_entropy/examples/parallelism.rs b/crates/bevy_entropy/examples/parallelism.rs index 62c7cc2d17689..58b2bbf95dfd1 100644 --- a/crates/bevy_entropy/examples/parallelism.rs +++ b/crates/bevy_entropy/examples/parallelism.rs @@ -32,10 +32,12 @@ fn main() { App::new() .add_plugin(EntropyPlugin::::new().with_seed([2; 32])) .add_startup_system(setup_sources) - .add_system(random_output_a) - .add_system(random_output_b) - .add_system(random_output_c) - .add_system(random_output_d) + .add_systems(( + random_output_a, + random_output_b, + random_output_c, + random_output_d, + )) .run(); } From 0ce4535e24b8eea231ed5cd06203ddc20e38d51f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 7 Mar 2023 10:11:12 +0100 Subject: [PATCH 14/28] enforce lifetime tracking on thread local mut rng ref --- crates/bevy_entropy/examples/parallelism.rs | 6 +++--- crates/bevy_entropy/src/thread_local_entropy.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_entropy/examples/parallelism.rs b/crates/bevy_entropy/examples/parallelism.rs index 58b2bbf95dfd1..b8dae7d0b6369 100644 --- a/crates/bevy_entropy/examples/parallelism.rs +++ b/crates/bevy_entropy/examples/parallelism.rs @@ -25,9 +25,9 @@ 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 just the RNG output, as long as the entities are -/// seeded deterministically and any systems that query a specific entity or group of entities are -/// assured to be in order. +/// 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])) diff --git a/crates/bevy_entropy/src/thread_local_entropy.rs b/crates/bevy_entropy/src/thread_local_entropy.rs index b5c4de0e0a0e7..fa7aada397529 100644 --- a/crates/bevy_entropy/src/thread_local_entropy.rs +++ b/crates/bevy_entropy/src/thread_local_entropy.rs @@ -17,7 +17,7 @@ impl ThreadLocalEntropy { /// `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. #[inline] - fn get_rng(&mut self) -> &'static mut ChaCha12Rng { + 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()); From 1efa1a822ab63ed85c961bf1fda5aae113f04e10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 7 Mar 2023 12:01:34 +0100 Subject: [PATCH 15/28] fix from implementation with Mut reference --- crates/bevy_entropy/src/component.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index 5a06c1869fccd..051a213ea8da4 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -99,8 +99,10 @@ impl From<&mut R> for EntropyComponent { } } -impl From<&mut Mut<'_, R>> for EntropyComponent { - fn from(rng: &mut Mut<'_, R>) -> Self { +impl From<&mut Mut<'_, EntropyComponent>> + for EntropyComponent +{ + fn from(rng: &mut Mut<'_, EntropyComponent>) -> Self { Self::from_rng(rng.as_mut()).unwrap() } } From 68c0fb7448e77573f184a8090cfcbcc7712468fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Tue, 7 Mar 2023 18:34:53 +0100 Subject: [PATCH 16/28] fix from implementation, add forking test --- crates/bevy_entropy/src/component.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index 051a213ea8da4..f94535434d578 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -93,8 +93,8 @@ impl From for EntropyComponent { } } -impl From<&mut R> for EntropyComponent { - fn from(rng: &mut R) -> Self { +impl From<&mut EntropyComponent> for EntropyComponent { + fn from(rng: &mut EntropyComponent) -> Self { Self::from_rng(rng).unwrap() } } @@ -103,7 +103,7 @@ impl From<&mut Mut<'_, EntropyComponent>> for EntropyComponent { fn from(rng: &mut Mut<'_, EntropyComponent>) -> Self { - Self::from_rng(rng.as_mut()).unwrap() + Self::from(rng.as_mut()) } } @@ -114,3 +114,19 @@ impl From<&mut ResMut<'_, GlobalEntropy>> 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"); + } +} From 683db77ad36b5a1ef452d119eeef1e2468974b3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Wed, 8 Mar 2023 19:09:00 +0100 Subject: [PATCH 17/28] first-pass on documentation --- crates/bevy_entropy/README.md | 108 ++++++++++++++++++++++++++- crates/bevy_entropy/src/component.rs | 94 ++++++++++++++++++++++- crates/bevy_entropy/src/lib.rs | 5 ++ crates/bevy_entropy/src/plugin.rs | 4 + crates/bevy_entropy/src/resource.rs | 21 ++++++ crates/bevy_entropy/src/traits.rs | 12 +++ 6 files changed, 241 insertions(+), 3 deletions(-) diff --git a/crates/bevy_entropy/README.md b/crates/bevy_entropy/README.md index be40db48f7641..953ea02381d89 100644 --- a/crates/bevy_entropy/README.md +++ b/crates/bevy_entropy/README.md @@ -4,4 +4,110 @@ [![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) -Docs TBA +## 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. + +## 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 random yet deterministic states, 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. + +### 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. diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index f94535434d578..672b1bced1ac0 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -14,6 +14,88 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, 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))] @@ -28,6 +110,7 @@ use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; pub struct EntropyComponent(R); impl EntropyComponent { + /// Create a new component from an `RngCore` instance. #[inline] #[must_use] pub fn new(rng: R) -> Self { @@ -36,15 +119,19 @@ impl EntropyComponent { } 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(R::from_rng(ThreadLocalEntropy).unwrap()) + 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); @@ -127,6 +214,9 @@ mod tests { let rng2 = EntropyComponent::from(&mut rng1); - assert_ne!(rng1, rng2, "forked EntropyComponents should not match each other"); + assert_ne!( + rng1, rng2, + "forked EntropyComponents should not match each other" + ); } } diff --git a/crates/bevy_entropy/src/lib.rs b/crates/bevy_entropy/src/lib.rs index 563277e90b65d..5ed75a92b8c17 100644 --- a/crates/bevy_entropy/src/lib.rs +++ b/crates/bevy_entropy/src/lib.rs @@ -1,9 +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 index 9d7ae724be2fe..3b4e781c69e01 100644 --- a/crates/bevy_entropy/src/plugin.rs +++ b/crates/bevy_entropy/src/plugin.rs @@ -38,6 +38,8 @@ 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 { @@ -47,6 +49,8 @@ where } } + /// 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); diff --git a/crates/bevy_entropy/src/resource.rs b/crates/bevy_entropy/src/resource.rs index a269d296904c9..b75dd10a6291f 100644 --- a/crates/bevy_entropy/src/resource.rs +++ b/crates/bevy_entropy/src/resource.rs @@ -10,6 +10,22 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, 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))] @@ -24,6 +40,7 @@ use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; pub struct GlobalEntropy(R); impl GlobalEntropy { + /// Create a new resource from a `RngCore` instance. #[inline] #[must_use] pub fn new(rng: R) -> Self { @@ -32,6 +49,9 @@ impl GlobalEntropy { } 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 { @@ -40,6 +60,7 @@ impl GlobalEntropy { 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); diff --git a/crates/bevy_entropy/src/traits.rs b/crates/bevy_entropy/src/traits.rs index cd58e608927a5..df5c95814d05d 100644 --- a/crates/bevy_entropy/src/traits.rs +++ b/crates/bevy_entropy/src/traits.rs @@ -5,6 +5,9 @@ use rand_core::{RngCore, SeedableRng}; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; +/// A wrapper trait to encapsulate the required trait bounds for a PRNG to +/// integrate into [`crate::component::EntropyComponent`] or +/// [`crate::resource::GlobalEntropy`]. This is a sealed trait. #[cfg(feature = "serialize")] pub trait EntropySource: RngCore @@ -25,6 +28,9 @@ impl EntropySource for T where { } +/// A wrapper trait to encapsulate the required trait bounds for a PRNG to +/// integrate into [`crate::component::EntropyComponent`] or +/// [`crate::resource::GlobalEntropy`]. This is a sealed trait. #[cfg(not(feature = "serialize"))] pub trait EntropySource: RngCore + Clone + Debug + PartialEq + Sync + Send + private::SealedEntropy @@ -34,6 +40,9 @@ pub trait EntropySource: #[cfg(not(feature = "serialize"))] impl EntropySource for T where T: RngCore + Clone + Debug + PartialEq + Sync + Send {} +/// 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 @@ -63,6 +72,9 @@ impl SeedableEntropySource for T where { } +/// 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 From d797a32acf9287cc89ddb22d9db72772310118e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Wed, 8 Mar 2023 20:38:25 +0100 Subject: [PATCH 18/28] Document registering a PRNG --- crates/bevy_entropy/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/bevy_entropy/README.md b/crates/bevy_entropy/README.md index 953ea02381d89..7c149d3a14d3f 100644 --- a/crates/bevy_entropy/README.md +++ b/crates/bevy_entropy/README.md @@ -36,6 +36,23 @@ Bevy Entropy approaches forking via `From` implementations of the various compon 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. From 541c496f84e47df87601faddd8b7558251abfb79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 9 Mar 2023 12:41:46 +0100 Subject: [PATCH 19/28] add reflection/serialization tests --- crates/bevy_entropy/Cargo.toml | 1 + crates/bevy_entropy/src/component.rs | 40 +++++++++++++++++++++++ crates/bevy_entropy/src/resource.rs | 47 ++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/crates/bevy_entropy/Cargo.toml b/crates/bevy_entropy/Cargo.toml index 971ee2dea9e12..3f2adc963cfe6 100644 --- a/crates/bevy_entropy/Cargo.toml +++ b/crates/bevy_entropy/Cargo.toml @@ -27,6 +27,7 @@ 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"] } diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index 672b1bced1ac0..8e8b20fcd8544 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -219,4 +219,44 @@ mod tests { "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/resource.rs b/crates/bevy_entropy/src/resource.rs index b75dd10a6291f..434cc4ca20182 100644 --- a/crates/bevy_entropy/src/resource.rs +++ b/crates/bevy_entropy/src/resource.rs @@ -114,3 +114,50 @@ impl From<&mut R> for GlobalEntropy { 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"); + } +} From ebeec6714e1a54c85fa03766a0111b7847ece82b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 9 Mar 2023 12:49:11 +0100 Subject: [PATCH 20/28] cargo fmt the reflection tests --- crates/bevy_entropy/src/component.rs | 11 +++++++++-- crates/bevy_entropy/src/resource.rs | 11 +++++++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index 8e8b20fcd8544..cbac5579bd49c 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -255,8 +255,15 @@ mod tests { let mut dynamic = value.take::>().unwrap(); // The two instances should be the same - assert_eq!(val, dynamic, "The deserialized EntropyComponent should equal the original"); + 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"); + 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/resource.rs b/crates/bevy_entropy/src/resource.rs index 434cc4ca20182..427d2af1107fc 100644 --- a/crates/bevy_entropy/src/resource.rs +++ b/crates/bevy_entropy/src/resource.rs @@ -156,8 +156,15 @@ mod tests { let mut dynamic = value.take::>().unwrap(); // The two instances should be the same - assert_eq!(val, dynamic, "The deserialized GlobalEntropy should equal the original"); + 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"); + assert_eq!( + val.next_u32(), + dynamic.next_u32(), + "The deserialized GlobalEntropy should have the same output as original" + ); } } From da038e99394773ccf2b892121cd0b9803501e861 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 9 Mar 2023 16:17:10 +0100 Subject: [PATCH 21/28] revise safety of internal get_rng method for ThreadLocalEntropy --- .../bevy_entropy/src/thread_local_entropy.rs | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/bevy_entropy/src/thread_local_entropy.rs b/crates/bevy_entropy/src/thread_local_entropy.rs index fa7aada397529..eeec7d9346ab1 100644 --- a/crates/bevy_entropy/src/thread_local_entropy.rs +++ b/crates/bevy_entropy/src/thread_local_entropy.rs @@ -16,36 +16,48 @@ impl ThreadLocalEntropy { /// 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] - fn get_rng(&'_ mut self) -> &'_ mut ChaCha12Rng { + 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()); - // SAFETY: We must make sure to stop using `rng` before anyone else creates another - // mutable reference - unsafe { &mut *rng } + &mut *rng } } impl RngCore for ThreadLocalEntropy { #[inline] fn next_u32(&mut self) -> u32 { - self.get_rng().next_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 { - self.get_rng().next_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]) { - self.get_rng().fill_bytes(dest); + // 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> { - self.get_rng().try_fill_bytes(dest) + // SAFETY: We must ensure to drop the `&mut rng` ref before creating another + // mutable reference + unsafe { self.get_rng().try_fill_bytes(dest) } } } From 06165fe2f7dbbc43cf1efbae0c68a6921ff7ec65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Fri, 10 Mar 2023 11:45:58 +0100 Subject: [PATCH 22/28] Ensure unique initialisation per generic plugin --- crates/bevy_entropy/src/plugin.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/bevy_entropy/src/plugin.rs b/crates/bevy_entropy/src/plugin.rs index 3b4e781c69e01..28daee9356d5c 100644 --- a/crates/bevy_entropy/src/plugin.rs +++ b/crates/bevy_entropy/src/plugin.rs @@ -16,11 +16,12 @@ use crate::component::EntropyComponent; /// use bevy_app::App; /// use bevy_entropy::prelude::*; /// use rand_core::RngCore; -/// use rand_chacha::ChaCha8Rng; +/// use rand_chacha::{ChaCha8Rng, ChaCha12Rng}; /// /// fn main() { /// App::new() /// .add_plugin(EntropyPlugin::::default()) +/// .add_plugin(EntropyPlugin::::default()) /// .add_system(print_random_value) /// .run(); /// } @@ -82,8 +83,4 @@ where app.init_resource::>(); } } - - fn is_unique(&self) -> bool { - false - } } From 60ac6ddcd984648707b39a05a566893bc808f6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Mon, 13 Mar 2023 12:44:28 +0100 Subject: [PATCH 23/28] use rfc text for overview on bevy_entropy --- crates/bevy_entropy/README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/bevy_entropy/README.md b/crates/bevy_entropy/README.md index 7c149d3a14d3f..08f001e3d4b4e 100644 --- a/crates/bevy_entropy/README.md +++ b/crates/bevy_entropy/README.md @@ -12,6 +12,29 @@ Bevy Entropy is a plugin to provide integration of `rand` ecosystem PRNGs in an 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. @@ -28,7 +51,7 @@ Bevy Entropy operates around a global entropy source provided as a resource, and ### 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 random yet deterministic states, 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. +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. From c1a4bb4e88743f6951e61387ad8a4106241e5fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 16 Mar 2023 18:21:21 +0100 Subject: [PATCH 24/28] Note about PRNG algorithms and how to select them --- crates/bevy_entropy/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/bevy_entropy/README.md b/crates/bevy_entropy/README.md index 08f001e3d4b4e..4fb32cc799ab9 100644 --- a/crates/bevy_entropy/README.md +++ b/crates/bevy_entropy/README.md @@ -151,3 +151,11 @@ fn setup_npc_from_source( 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. From 526f48696f0d5a67d6e4cebc8f6df59cd97d1a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Mon, 27 Mar 2023 13:16:28 +0200 Subject: [PATCH 25/28] update examples to use new add_systems API --- crates/bevy_entropy/examples/determinism.rs | 9 ++++++--- crates/bevy_entropy/examples/parallelism.rs | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/crates/bevy_entropy/examples/determinism.rs b/crates/bevy_entropy/examples/determinism.rs index edd121961102e..6722386a7ecf0 100644 --- a/crates/bevy_entropy/examples/determinism.rs +++ b/crates/bevy_entropy/examples/determinism.rs @@ -1,6 +1,6 @@ #![allow(clippy::type_complexity)] -use bevy_app::App; +use bevy_app::{App, Startup, Update}; use bevy_ecs::{ prelude::{Component, Entity, ResMut}, query::With, @@ -52,8 +52,11 @@ struct Health { fn main() { App::new() .add_plugin(EntropyPlugin::::new().with_seed([1; 32])) - .add_startup_systems((setup_player, setup_enemies).chain()) - .add_systems((determine_attack_order.pipe(attack_turn), buff_entities).chain()) + .add_systems(Startup, (setup_player, setup_enemies).chain()) + .add_systems( + Update, + (determine_attack_order.pipe(attack_turn), buff_entities).chain(), + ) .run(); } diff --git a/crates/bevy_entropy/examples/parallelism.rs b/crates/bevy_entropy/examples/parallelism.rs index b8dae7d0b6369..da90c8d7afd87 100644 --- a/crates/bevy_entropy/examples/parallelism.rs +++ b/crates/bevy_entropy/examples/parallelism.rs @@ -1,6 +1,6 @@ #![allow(clippy::type_complexity)] -use bevy_app::App; +use bevy_app::{App, Startup, Update}; use bevy_ecs::{ prelude::{Component, ResMut}, query::With, @@ -31,13 +31,16 @@ struct SourceD; fn main() { App::new() .add_plugin(EntropyPlugin::::new().with_seed([2; 32])) - .add_startup_system(setup_sources) - .add_systems(( - random_output_a, - random_output_b, - random_output_c, - random_output_d, - )) + .add_systems(Startup, setup_sources) + .add_systems( + Update, + ( + random_output_a, + random_output_b, + random_output_c, + random_output_d, + ), + ) .run(); } From 2cd98ebe6f8a734004deccd4ea02423448c28660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Mon, 27 Mar 2023 13:54:59 +0200 Subject: [PATCH 26/28] Fix markdown lint error --- crates/bevy_entropy/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_entropy/README.md b/crates/bevy_entropy/README.md index 4fb32cc799ab9..f980092541b95 100644 --- a/crates/bevy_entropy/README.md +++ b/crates/bevy_entropy/README.md @@ -156,6 +156,6 @@ The examples provided in this repo demonstrate the two different concepts of par `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. +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. From 700ef360580c586350b9531e5adc9f78dd24771e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Mon, 27 Mar 2023 19:12:30 +0200 Subject: [PATCH 27/28] update doc examples with add_systems --- crates/bevy_entropy/src/plugin.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_entropy/src/plugin.rs b/crates/bevy_entropy/src/plugin.rs index 28daee9356d5c..1f19f4d56a1cf 100644 --- a/crates/bevy_entropy/src/plugin.rs +++ b/crates/bevy_entropy/src/plugin.rs @@ -13,7 +13,7 @@ use crate::component::EntropyComponent; /// /// ``` /// use bevy_ecs::prelude::ResMut; -/// use bevy_app::App; +/// use bevy_app::{App, Update}; /// use bevy_entropy::prelude::*; /// use rand_core::RngCore; /// use rand_chacha::{ChaCha8Rng, ChaCha12Rng}; @@ -22,7 +22,7 @@ use crate::component::EntropyComponent; /// App::new() /// .add_plugin(EntropyPlugin::::default()) /// .add_plugin(EntropyPlugin::::default()) -/// .add_system(print_random_value) +/// .add_systems(Update, print_random_value) /// .run(); /// } /// From 80bb082d19f006ef4f628e3e4615f0e29ef03a96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gon=C3=A7alo=20Rica=20Pais=20da=20Silva?= Date: Thu, 13 Apr 2023 19:36:39 +0200 Subject: [PATCH 28/28] More reflection derives, converge to single trait --- crates/bevy_entropy/src/component.rs | 29 ++++++++++++++-------- crates/bevy_entropy/src/prelude.rs | 1 + crates/bevy_entropy/src/resource.rs | 22 +++++++++++------ crates/bevy_entropy/src/traits.rs | 37 ---------------------------- 4 files changed, 34 insertions(+), 55 deletions(-) diff --git a/crates/bevy_entropy/src/component.rs b/crates/bevy_entropy/src/component.rs index cbac5579bd49c..f7f32b6782bd1 100644 --- a/crates/bevy_entropy/src/component.rs +++ b/crates/bevy_entropy/src/component.rs @@ -1,18 +1,23 @@ use std::fmt::Debug; use crate::{ - resource::GlobalEntropy, - thread_local_entropy::ThreadLocalEntropy, - traits::{EntropySource, SeedableEntropySource}, + resource::GlobalEntropy, thread_local_entropy::ThreadLocalEntropy, + traits::SeedableEntropySource, +}; +use bevy_ecs::{ + prelude::{Component, ReflectComponent}, + system::ResMut, + world::Mut, }; -use bevy_ecs::{prelude::Component, 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, ReflectSerialize}; +use bevy_reflect::{ + FromReflect, Reflect, ReflectDeserialize, ReflectFromReflect, ReflectSerialize, +}; /// An [`EntropyComponent`] that wraps a random number generator that implements /// [`RngCore`] & [`SeedableRng`]. @@ -105,11 +110,15 @@ use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; )] #[cfg_attr( all(feature = "serialize", feature = "bevy_reflect"), - reflect_value(Debug, PartialEq, Serialize, Deserialize) + reflect_value(Debug, PartialEq, Component, FromReflect, Serialize, Deserialize) )] -pub struct EntropyComponent(R); +#[cfg_attr( + all(not(feature = "serialize"), feature = "bevy_reflect"), + reflect_value(Debug, PartialEq, Component, FromReflect) +)] +pub struct EntropyComponent(R); -impl EntropyComponent { +impl EntropyComponent { /// Create a new component from an `RngCore` instance. #[inline] #[must_use] @@ -144,7 +153,7 @@ impl Default for EntropyComponent { } } -impl RngCore for EntropyComponent { +impl RngCore for EntropyComponent { #[inline] fn next_u32(&mut self) -> u32 { self.0.next_u32() @@ -174,7 +183,7 @@ impl SeedableRng for EntropyComponent { } } -impl From for EntropyComponent { +impl From for EntropyComponent { fn from(value: R) -> Self { Self::new(value) } diff --git a/crates/bevy_entropy/src/prelude.rs b/crates/bevy_entropy/src/prelude.rs index 264d2ac3cb80d..564a6b27c9a5d 100644 --- a/crates/bevy_entropy/src/prelude.rs +++ b/crates/bevy_entropy/src/prelude.rs @@ -1,3 +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 index 427d2af1107fc..7b57a33e5f850 100644 --- a/crates/bevy_entropy/src/resource.rs +++ b/crates/bevy_entropy/src/resource.rs @@ -1,14 +1,16 @@ use std::fmt::Debug; -use crate::traits::{EntropySource, SeedableEntropySource}; -use bevy_ecs::prelude::Resource; +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, ReflectSerialize}; +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 @@ -35,11 +37,15 @@ use bevy_reflect::{FromReflect, Reflect, ReflectDeserialize, ReflectSerialize}; )] #[cfg_attr( all(feature = "serialize", feature = "bevy_reflect"), - reflect_value(Debug, PartialEq, Serialize, Deserialize) + reflect_value(Debug, PartialEq, Resource, FromReflect, Serialize, Deserialize) )] -pub struct GlobalEntropy(R); +#[cfg_attr( + all(not(feature = "serialize"), feature = "bevy_reflect"), + reflect_value(Debug, PartialEq, Resource, FromReflect) +)] +pub struct GlobalEntropy(R); -impl GlobalEntropy { +impl GlobalEntropy { /// Create a new resource from a `RngCore` instance. #[inline] #[must_use] @@ -73,7 +79,7 @@ impl Default for GlobalEntropy { } } -impl RngCore for GlobalEntropy { +impl RngCore for GlobalEntropy { #[inline] fn next_u32(&mut self) -> u32 { self.0.next_u32() @@ -103,7 +109,7 @@ impl SeedableRng for GlobalEntropy { } } -impl From for GlobalEntropy { +impl From for GlobalEntropy { fn from(value: R) -> Self { Self::new(value) } diff --git a/crates/bevy_entropy/src/traits.rs b/crates/bevy_entropy/src/traits.rs index df5c95814d05d..9215cdd615743 100644 --- a/crates/bevy_entropy/src/traits.rs +++ b/crates/bevy_entropy/src/traits.rs @@ -5,41 +5,6 @@ use rand_core::{RngCore, SeedableRng}; #[cfg(feature = "serialize")] use serde::{Deserialize, Serialize}; -/// A wrapper trait to encapsulate the required trait bounds for a PRNG to -/// integrate into [`crate::component::EntropyComponent`] or -/// [`crate::resource::GlobalEntropy`]. This is a sealed trait. -#[cfg(feature = "serialize")] -pub trait EntropySource: - RngCore - + Clone - + Debug - + PartialEq - + Sync - + Send - + Serialize - + for<'a> Deserialize<'a> - + private::SealedEntropy -{ -} - -#[cfg(feature = "serialize")] -impl EntropySource for T where - T: RngCore + Clone + Debug + PartialEq + Sync + Send + Serialize + for<'a> Deserialize<'a> -{ -} - -/// A wrapper trait to encapsulate the required trait bounds for a PRNG to -/// integrate into [`crate::component::EntropyComponent`] or -/// [`crate::resource::GlobalEntropy`]. This is a sealed trait. -#[cfg(not(feature = "serialize"))] -pub trait EntropySource: - RngCore + Clone + Debug + PartialEq + Sync + Send + private::SealedEntropy -{ -} - -#[cfg(not(feature = "serialize"))] -impl EntropySource for T where T: RngCore + Clone + Debug + PartialEq + Sync + Send {} - /// 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. @@ -88,9 +53,7 @@ impl SeedableEntropySource for T where } mod private { - pub trait SealedEntropy {} pub trait SealedSeedable {} - impl SealedEntropy for T {} impl SealedSeedable for T {} }