Skip to content

Commit

Permalink
Merge pull request #19 from Bluefinger/rngseed
Browse files Browse the repository at this point in the history
feat: add GlobalRngSeed resource for tracking initial state/seed
  • Loading branch information
Bluefinger committed May 6, 2024
2 parents 89474b9 + bd311d4 commit 4191410
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 24 deletions.
9 changes: 5 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ authors = ["Gonçalo Rica Pais da Silva <bluefinger@gmail.com>"]
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]
Expand Down Expand Up @@ -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 }
Expand All @@ -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"] }

Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
Expand Down
24 changes: 15 additions & 9 deletions src/plugin.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -33,7 +35,7 @@ pub struct EntropyPlugin<R: SeedableEntropySource + 'static> {

impl<R: SeedableEntropySource + 'static> EntropyPlugin<R>
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.
Expand All @@ -53,7 +55,7 @@ where

impl<R: SeedableEntropySource + 'static> Default for EntropyPlugin<R>
where
R::Seed: Send + Sync + Copy,
R::Seed: Send + Sync + Clone,
{
fn default() -> Self {
Self::new()
Expand All @@ -62,16 +64,20 @@ where

impl<R: SeedableEntropySource + 'static> Plugin for EntropyPlugin<R>
where
R::Seed: Send + Sync + Copy,
R::Seed: Send + Sync + Clone + Reflect + FromReflect + GetTypeRegistration + TypePath,
{
fn build(&self, app: &mut App) {
app.register_type::<GlobalEntropy<R>>()
.register_type::<EntropyComponent<R>>();

if let Some(seed) = self.seed {
app.insert_resource(GlobalEntropy::<R>::from_seed(seed));
GlobalRngSeed::<R>::register_type(app);

if let Some(seed) = self.seed.as_ref() {
app.insert_resource(GlobalRngSeed::<R>::new(seed.clone()));
} else {
app.init_resource::<GlobalEntropy<R>>();
app.init_resource::<GlobalRngSeed<R>>();
}

app.init_resource::<GlobalEntropy<R>>();
}
}
1 change: 1 addition & 0 deletions src/prelude.rs
Original file line number Diff line number Diff line change
@@ -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")))]
Expand Down
36 changes: 28 additions & 8 deletions src/resource.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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: SeedableEntropySource + 'static>(R);

impl<R: SeedableEntropySource + 'static> GlobalEntropy<R> {
Expand All @@ -69,9 +82,16 @@ impl<R: SeedableEntropySource + 'static> GlobalEntropy<R> {
}
}

impl<R: SeedableEntropySource + 'static> Default for GlobalEntropy<R> {
fn default() -> Self {
Self::from_entropy()
impl<R: SeedableEntropySource + 'static> FromWorld for GlobalEntropy<R>
where
R::Seed: Send + Sync + Clone,
{
fn from_world(world: &mut World) -> Self {
if let Some(seed) = world.get_resource::<GlobalRngSeed<R>>() {
Self::new(R::from_seed(seed.get_seed()))
} else {
Self::from_entropy()
}
}
}

Expand Down Expand Up @@ -197,7 +217,7 @@ mod tests {

#[test]
fn forking_as() {
let mut rng1 = GlobalEntropy::<ChaCha12Rng>::default();
let mut rng1 = GlobalEntropy::<ChaCha12Rng>::from_entropy();

let rng2 = rng1.fork_as::<WyRand>();

Expand All @@ -212,7 +232,7 @@ mod tests {

#[test]
fn forking_inner() {
let mut rng1 = GlobalEntropy::<ChaCha8Rng>::default();
let mut rng1 = GlobalEntropy::<ChaCha8Rng>::from_entropy();

let rng2 = rng1.fork_inner();

Expand Down
150 changes: 150 additions & 0 deletions src/seed.rs
Original file line number Diff line number Diff line change
@@ -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<R: SeedableEntropySource> {
seed: R::Seed,
#[reflect(ignore)]
rng: PhantomData<R>,
}

impl<R: SeedableEntropySource> GlobalRngSeed<R>
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::<Self>();
app.register_type::<R::Seed>();
}
}

impl<R: SeedableEntropySource> GlobalRngSeed<R>
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<R: SeedableEntropySource> Default for GlobalRngSeed<R>
where
R::Seed: Sync + Send + Clone,
{
#[inline]
fn default() -> Self {
Self::from_entropy()
}
}

impl<R: SeedableEntropySource> AsMut<[u8]> for GlobalRngSeed<R>
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::<GlobalRngSeed<WyRand>>();
registry.register::<[u8; 8]>();

let registered_type = GlobalRngSeed::<WyRand>::get_type_registration();

let val = GlobalRngSeed::<WyRand>::new(u64::MAX.to_ne_bytes());

let ser = TypedReflectSerializer::new(&val, &registry);

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(&registered_type, &registry);

let value = de.deserialize(&mut deserializer).unwrap();

assert!(value.is_dynamic());
assert!(value.represents::<GlobalRngSeed<WyRand>>());
assert!(!value.is::<GlobalRngSeed<WyRand>>());

let recreated = GlobalRngSeed::<WyRand>::from_reflect(value.as_reflect()).unwrap();

assert_eq!(val.get_seed(), recreated.get_seed());
}
}
20 changes: 17 additions & 3 deletions tests/determinism.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -91,6 +91,10 @@ fn setup_sources(mut commands: Commands, mut rng: ResMut<GlobalEntropy<ChaCha8Rn
commands.spawn((SourceE, rng.fork_as::<WyRand>()));
}

fn read_global_seed(seed: Res<GlobalRngSeed<ChaCha8Rng>>) {
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
Expand All @@ -103,8 +107,17 @@ fn setup_sources(mut commands: Commands, mut rng: ResMut<GlobalEntropy<ChaCha8Rn
#[test]
#[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
fn test_parallel_determinism() {
App::new()
.add_plugins(EntropyPlugin::<ChaCha8Rng>::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::<ChaCha8Rng>::with_seed([2; 32]))
.add_systems(Startup, setup_sources)
.add_systems(
Update,
Expand All @@ -114,6 +127,7 @@ fn test_parallel_determinism() {
random_output_c,
random_output_d,
random_output_e,
read_global_seed,
),
)
.run();
Expand Down

0 comments on commit 4191410

Please sign in to comment.