diff --git a/Cargo.toml b/Cargo.toml index 764add1..5592fa0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ authors = ["Gonçalo Rica Pais da Silva "] edition = "2021" repository = "https://github.com/Bluefinger/bevy_rand" license = "MIT OR Apache-2.0" -version = "0.6.0" +version = "0.7.0" rust-version = "1.76.0" [workspace.dependencies] @@ -45,9 +45,10 @@ wyrand = ["bevy_prng/wyrand"] [dependencies] # bevy bevy.workspace = true -bevy_prng = { path = "bevy_prng", version = "0.6" } +bevy_prng = { path = "bevy_prng", version = "0.7" } # others +getrandom = "0.2" rand_core.workspace = true rand_chacha = { workspace = true, optional = true } serde = { workspace = true, optional = true } @@ -58,10 +59,10 @@ serde_derive = { workspace = true, optional = true } # cannot be out of step with bevy_rand due to dependencies on traits # and implementations between the two crates. [target.'cfg(any())'.dependencies] -bevy_prng = { path = "bevy_prng", version = "=0.6" } +bevy_prng = { path = "bevy_prng", version = "=0.7" } [dev-dependencies] -bevy_prng = { path = "bevy_prng", version = "0.6", features = ["rand_chacha", "wyrand"] } +bevy_prng = { path = "bevy_prng", version = "0.7", features = ["rand_chacha", "wyrand"] } rand = "0.8" ron = { version = "0.8.0", features = ["integer128"] } diff --git a/src/lib.rs b/src/lib.rs index 506e629..ade1557 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,6 +12,8 @@ pub mod plugin; pub mod prelude; /// Resource for integrating [`RngCore`] PRNGs into bevy. Must be newtyped to support [`Reflect`]. pub mod resource; +/// Seed Resource for seeding [`crate::resource::GlobalEntropy`]. +pub mod seed; #[cfg(feature = "thread_local_entropy")] mod thread_local_entropy; /// Traits for enabling utility methods for [`crate::component::EntropyComponent`] and [`crate::resource::GlobalEntropy`]. diff --git a/src/plugin.rs b/src/plugin.rs index 61752e4..76e7c5a 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,7 +1,9 @@ -use crate::{component::EntropyComponent, resource::GlobalEntropy}; -use bevy::prelude::{App, Plugin}; +use crate::{component::EntropyComponent, resource::GlobalEntropy, seed::GlobalRngSeed}; +use bevy::{ + prelude::{App, Plugin}, + reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}, +}; use bevy_prng::SeedableEntropySource; -use rand_core::SeedableRng; /// Plugin for integrating a PRNG that implements `RngCore` into /// the bevy engine, registering types for a global resource and @@ -33,7 +35,7 @@ pub struct EntropyPlugin { impl EntropyPlugin where - R::Seed: Send + Sync + Copy, + R::Seed: Send + Sync + Clone, { /// Creates a new plugin instance configured for randomised, /// non-deterministic seeding of the global entropy resource. @@ -53,7 +55,7 @@ where impl Default for EntropyPlugin where - R::Seed: Send + Sync + Copy, + R::Seed: Send + Sync + Clone, { fn default() -> Self { Self::new() @@ -62,16 +64,20 @@ where impl Plugin for EntropyPlugin where - R::Seed: Send + Sync + Copy, + R::Seed: Send + Sync + Clone + Reflect + FromReflect + GetTypeRegistration + TypePath, { fn build(&self, app: &mut App) { app.register_type::>() .register_type::>(); - if let Some(seed) = self.seed { - app.insert_resource(GlobalEntropy::::from_seed(seed)); + GlobalRngSeed::::register_type(app); + + if let Some(seed) = self.seed.as_ref() { + app.insert_resource(GlobalRngSeed::::new(seed.clone())); } else { - app.init_resource::>(); + app.init_resource::>(); } + + app.init_resource::>(); } } diff --git a/src/prelude.rs b/src/prelude.rs index 58a9c7f..8deaccf 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,6 +1,7 @@ pub use crate::component::EntropyComponent; pub use crate::plugin::EntropyPlugin; pub use crate::resource::GlobalEntropy; +pub use crate::seed::GlobalRngSeed; pub use crate::traits::{ForkableAsRng, ForkableInnerRng, ForkableRng}; #[cfg(feature = "wyrand")] #[cfg_attr(docsrs, doc(cfg(feature = "wyrand")))] diff --git a/src/resource.rs b/src/resource.rs index e1f6e51..d28b394 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -2,9 +2,13 @@ use std::fmt::Debug; use crate::{ component::EntropyComponent, + seed::GlobalRngSeed, traits::{EcsEntropySource, ForkableAsRng, ForkableInnerRng, ForkableRng}, }; -use bevy::prelude::{Reflect, ReflectFromReflect, ReflectResource, Resource}; +use bevy::{ + ecs::world::{FromWorld, World}, + prelude::{Reflect, ReflectFromReflect, ReflectFromWorld, ReflectResource, Resource}, +}; use bevy_prng::SeedableEntropySource; use rand_core::{RngCore, SeedableRng}; @@ -44,12 +48,21 @@ use serde::Deserialize; )] #[cfg_attr( feature = "serialize", - reflect(Debug, PartialEq, Resource, FromReflect, Serialize, Deserialize) + reflect( + Debug, + PartialEq, + Resource, + FromReflect, + Serialize, + Deserialize, + FromWorld + ) )] #[cfg_attr( not(feature = "serialize"), - reflect(Debug, PartialEq, Resource, FromReflect) + reflect(Debug, PartialEq, Resource, FromReflect, FromWorld) )] +#[reflect(where R::Seed: Sync + Send + Clone)] pub struct GlobalEntropy(R); impl GlobalEntropy { @@ -69,9 +82,16 @@ impl GlobalEntropy { } } -impl Default for GlobalEntropy { - fn default() -> Self { - Self::from_entropy() +impl FromWorld for GlobalEntropy +where + R::Seed: Send + Sync + Clone, +{ + fn from_world(world: &mut World) -> Self { + if let Some(seed) = world.get_resource::>() { + Self::new(R::from_seed(seed.get_seed())) + } else { + Self::from_entropy() + } } } @@ -197,7 +217,7 @@ mod tests { #[test] fn forking_as() { - let mut rng1 = GlobalEntropy::::default(); + let mut rng1 = GlobalEntropy::::from_entropy(); let rng2 = rng1.fork_as::(); @@ -212,7 +232,7 @@ mod tests { #[test] fn forking_inner() { - let mut rng1 = GlobalEntropy::::default(); + let mut rng1 = GlobalEntropy::::from_entropy(); let rng2 = rng1.fork_inner(); diff --git a/src/seed.rs b/src/seed.rs new file mode 100644 index 0000000..aa5131a --- /dev/null +++ b/src/seed.rs @@ -0,0 +1,150 @@ +use std::marker::PhantomData; + +use bevy::{ + app::App, + ecs::system::Resource, + reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath}, +}; +use bevy_prng::SeedableEntropySource; +use rand_core::RngCore; + +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Resource, Reflect)] +#[cfg_attr( + feature = "serialize", + derive(serde_derive::Serialize, serde_derive::Deserialize) +)] +#[cfg_attr( + feature = "serialize", + serde(bound(deserialize = "R::Seed: Serialize + for<'a> Deserialize<'a>")) +)] +/// Resource for storing the initial seed used to initialize a [`crate::resource::GlobalEntropy`]. +/// Useful for tracking the starting seed or for forcing [`crate::resource::GlobalEntropy`] to reseed. +pub struct GlobalRngSeed { + seed: R::Seed, + #[reflect(ignore)] + rng: PhantomData, +} + +impl GlobalRngSeed +where + R::Seed: Sync + Send + Clone + Reflect + GetTypeRegistration + FromReflect + TypePath, +{ + /// Helper method to register the necessary types for [`Reflect`] purposes. Ensures + /// that not only the main type is registered, but also the correct seed type for the + /// PRNG. + pub fn register_type(app: &mut App) { + app.register_type::(); + app.register_type::(); + } +} + +impl GlobalRngSeed +where + R::Seed: Sync + Send + Clone, +{ + /// Create a new instance of [`GlobalRngSeed`]. + #[inline] + #[must_use] + pub fn new(seed: R::Seed) -> Self { + Self { + seed, + rng: PhantomData, + } + } + + /// Returns a cloned instance of the seed value. + #[inline] + pub fn get_seed(&self) -> R::Seed { + self.seed.clone() + } + + /// Initializes an instance of [`GlobalRngSeed`] with a randomised seed + /// value, drawn from thread-local or OS sources. + #[inline] + pub fn from_entropy() -> Self { + let mut seed = Self::new(R::Seed::default()); + + #[cfg(feature = "thread_local_entropy")] + { + use crate::thread_local_entropy::ThreadLocalEntropy; + + ThreadLocalEntropy::new().fill_bytes(seed.as_mut()); + } + #[cfg(not(feature = "thread_local_entropy"))] + { + use getrandom::getrandom; + + getrandom(seed.as_mut()).expect("Unable to source entropy for seeding"); + } + + seed + } +} + +impl Default for GlobalRngSeed +where + R::Seed: Sync + Send + Clone, +{ + #[inline] + fn default() -> Self { + Self::from_entropy() + } +} + +impl AsMut<[u8]> for GlobalRngSeed +where + R::Seed: Sync + Send + Clone, +{ + #[inline] + fn as_mut(&mut self) -> &mut [u8] { + self.seed.as_mut() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(feature = "serialize")] + #[test] + fn reflection_serialization_round_trip_works() { + use bevy::reflect::{ + serde::{TypedReflectDeserializer, TypedReflectSerializer}, + GetTypeRegistration, TypeRegistry, + }; + use bevy_prng::WyRand; + use ron::to_string; + use serde::de::DeserializeSeed; + + let mut registry = TypeRegistry::default(); + registry.register::>(); + registry.register::<[u8; 8]>(); + + let registered_type = GlobalRngSeed::::get_type_registration(); + + let val = GlobalRngSeed::::new(u64::MAX.to_ne_bytes()); + + let ser = TypedReflectSerializer::new(&val, ®istry); + + let serialized = to_string(&ser).unwrap(); + + assert_eq!(&serialized, "(seed:(255,255,255,255,255,255,255,255))"); + + let mut deserializer = ron::Deserializer::from_str(&serialized).unwrap(); + + let de = TypedReflectDeserializer::new(®istered_type, ®istry); + + let value = de.deserialize(&mut deserializer).unwrap(); + + assert!(value.is_dynamic()); + assert!(value.represents::>()); + assert!(!value.is::>()); + + let recreated = GlobalRngSeed::::from_reflect(value.as_reflect()).unwrap(); + + assert_eq!(val.get_seed(), recreated.get_seed()); + } +} diff --git a/tests/determinism.rs b/tests/determinism.rs index a33146c..cb4584a 100644 --- a/tests/determinism.rs +++ b/tests/determinism.rs @@ -3,7 +3,7 @@ use bevy::prelude::*; use bevy_prng::{ChaCha12Rng, ChaCha8Rng, WyRand}; use bevy_rand::prelude::{ - EntropyComponent, EntropyPlugin, ForkableAsRng, ForkableRng, GlobalEntropy, + EntropyComponent, EntropyPlugin, ForkableAsRng, ForkableRng, GlobalEntropy, GlobalRngSeed, }; use rand::prelude::Rng; @@ -91,6 +91,10 @@ fn setup_sources(mut commands: Commands, mut rng: ResMut())); } +fn read_global_seed(seed: Res>) { + assert_eq!(seed.get_seed(), [2; 32]); +} + /// 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 @@ -103,8 +107,17 @@ fn setup_sources(mut commands: Commands, mut rng: ResMut::with_seed([2; 32])) + let mut app = App::new(); + + #[cfg(not(target_arch = "wasm32"))] + app.edit_schedule(Update, |schedule| { + use bevy::ecs::schedule::ExecutorKind; + + // Ensure the Update schedule is Multithreaded on non-WASM platforms + schedule.set_executor_kind(ExecutorKind::MultiThreaded); + }); + + app.add_plugins(EntropyPlugin::::with_seed([2; 32])) .add_systems(Startup, setup_sources) .add_systems( Update, @@ -114,6 +127,7 @@ fn test_parallel_determinism() { random_output_c, random_output_d, random_output_e, + read_global_seed, ), ) .run();