Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add GlobalRngSeed resource for tracking initial state/seed #19

Merged
merged 2 commits into from
May 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 <[email protected]>"]
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
Loading