feat(simulation): multi-threaded executor + EntityRng (#843 Part B)
Enable Bevy multi-threaded executor via bevy_tasks multi_threaded feature. Systems within the same TickPhase that don't share mutable resources now run in parallel automatically. Add EntityRng component — per-entity ChaCha20Rng seeded from world_seed + StableId via splitmix64 mixing. More deterministic than shared SimRng (order-independent). Migrate all monologue systems (4 of 13 SimRng consumers) to EntityRng, removing contention that serialized them against conversation/dialogue systems. Add rayon dependency (infrastructure only, no par_iter calls yet). SimRng retained for world-level randomness: conversation pairing, knowledge transfer, dialogue, ticker, storyteller. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,8 +6,11 @@ use bevy_ecs::prelude::*;
|
||||
use rand::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
/// Simulation RNG resource
|
||||
/// ChaCha20 RNG with stored seed for deterministic replay
|
||||
/// Simulation RNG resource — shared world-level randomness.
|
||||
///
|
||||
/// Used for non-entity randomness: storyteller dice, world events, ticker rotation.
|
||||
/// For entity-level randomness, use [`EntityRng`] instead — it enables parallelism
|
||||
/// because each entity has its own independent, deterministic RNG stream.
|
||||
#[derive(Resource)]
|
||||
pub struct SimRng {
|
||||
pub rng: ChaCha20Rng,
|
||||
@@ -29,6 +32,43 @@ impl SimRng {
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-entity RNG component — deterministic, parallel-safe (#843).
|
||||
///
|
||||
/// Each entity gets its own ChaCha20 stream seeded from `world_seed + StableId`.
|
||||
/// Systems that need randomness for a specific entity query `&mut EntityRng`
|
||||
/// instead of `ResMut<SimRng>`. This unlocks parallelism: Bevy can run systems
|
||||
/// on disjoint entity sets concurrently.
|
||||
///
|
||||
/// Determinism: same `world_seed` + same `StableId` = same RNG sequence,
|
||||
/// regardless of thread execution order.
|
||||
#[derive(Component)]
|
||||
pub struct EntityRng {
|
||||
pub rng: ChaCha20Rng,
|
||||
}
|
||||
|
||||
impl EntityRng {
|
||||
/// Create a new EntityRng from a world seed and entity stable ID.
|
||||
///
|
||||
/// Uses splitmix64 mixing to combine the two seeds with good avalanche
|
||||
/// properties — avoids correlated sequences for entities with similar IDs.
|
||||
pub fn from_seed_and_id(world_seed: u64, stable_id: u64) -> Self {
|
||||
let mixed = splitmix64(world_seed.wrapping_add(stable_id));
|
||||
Self {
|
||||
rng: ChaCha20Rng::seed_from_u64(mixed),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Splitmix64 mixing function — good avalanche properties for seed derivation.
|
||||
/// Ensures that similar inputs (e.g., consecutive StableIds) produce
|
||||
/// uncorrelated output seeds.
|
||||
fn splitmix64(mut x: u64) -> u64 {
|
||||
x = x.wrapping_add(0x9e3779b97f4a7c15);
|
||||
x = (x ^ (x >> 30)).wrapping_mul(0xbf58476d1ce4e5b9);
|
||||
x = (x ^ (x >> 27)).wrapping_mul(0x94d049bb133111eb);
|
||||
x ^ (x >> 31)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user