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:
2026-04-11 00:08:41 +02:00
co-authored by Claude Opus 4.6
parent 309c05d441
commit 2175e9b31c
5 changed files with 128 additions and 19 deletions
+42 -2
View File
@@ -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::*;