fix(simulation): address #843 review — seeding, atomics, API consistency

Review fixes from Hoshe + Tyre:
- EntityRng seeding: splitmix64(seed) ^ splitmix64(id) instead of
  splitmix64(seed + id) — eliminates collision class where adjacent
  seeds produce identical streams
- AtomicBool ordering: Relaxed → SeqCst for shutdown flag (correct
  on weakly-ordered architectures)
- Worker Drop: join handles instead of detaching threads
- Normalize stub API: remove ChunkGenWorker convenience wrappers,
  use .pool consistently across all 3 workers
- trigger_monologue: downgrade &mut to shared refs (no-op anchor
  was blocking parallel systems)
- Remove dead SimRng inserts from migrated monologue tests
- Document determinism gap on poll_worker_results
- Document bevy_tasks/rayon dep rationale in Cargo.toml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 00:22:44 +02:00
co-authored by Claude Opus 4.6
parent 0ae3542550
commit 5f678dac48
6 changed files with 65 additions and 28 deletions
+6 -5
View File
@@ -588,11 +588,12 @@ fn has_hear_sound_event(
pub fn trigger_monologue(
_time: Res<SimulationTime>,
_query: Query<
(&TilePosition, &mut MonologueState, &mut MonologueBuffer),
(&TilePosition, &MonologueState, &MonologueBuffer),
With<PlayerCharacter>,
>,
) {
// Intentional no-op — kept as schedule ordering anchor. See doc comment.
// Uses shared refs (not &mut) to avoid blocking parallel systems.
}
// ---------------------------------------------------------------------------
@@ -790,7 +791,7 @@ mod tests {
fn setup_anomaly_world() -> World {
let mut world = World::new();
world.init_resource::<SimulationTime>();
world.insert_resource(SimRng::new(42));
// SimRng no longer needed — migrated systems use EntityRng
world
}
@@ -991,7 +992,7 @@ mod tests {
fn setup_recognition_world() -> World {
let mut world = World::new();
world.init_resource::<SimulationTime>();
world.insert_resource(SimRng::new(42));
// SimRng no longer needed — migrated systems use EntityRng
world
}
@@ -1353,7 +1354,7 @@ mod tests {
fn setup_event_world() -> World {
let mut world = World::new();
world.init_resource::<SimulationTime>();
world.insert_resource(SimRng::new(42));
// SimRng no longer needed — migrated systems use EntityRng
world.init_resource::<ObservationEventQueue>();
world.init_resource::<SoundEventQueue>();
world.init_resource::<PostConversationQueue>();
@@ -1923,7 +1924,7 @@ mod tests {
fn setup_contradiction_world() -> bevy_ecs::world::World {
let mut world = bevy_ecs::world::World::new();
world.init_resource::<SimulationTime>();
world.insert_resource(SimRng::new(42));
// SimRng no longer needed — migrated systems use EntityRng
world.insert_resource(ContradictionDetectedQueue::default());
world.init_resource::<EntityRegistry>();
world
+32 -1
View File
@@ -52,7 +52,9 @@ impl EntityRng {
/// 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));
// Mix each input independently then XOR — avoids the collision class
// where (seed=0, id=N) == (seed=1, id=N-1) that wrapping_add creates.
let mixed = splitmix64(world_seed) ^ splitmix64(stable_id);
Self {
rng: ChaCha20Rng::seed_from_u64(mixed),
}
@@ -91,4 +93,33 @@ mod tests {
let val2: u32 = rng2.rng.random();
assert_ne!(val1, val2);
}
#[test]
fn entity_rng_same_seed_same_id_same_sequence() {
let mut a = EntityRng::from_seed_and_id(42, 7);
let mut b = EntityRng::from_seed_and_id(42, 7);
let va: Vec<u32> = (0..100).map(|_| a.rng.random()).collect();
let vb: Vec<u32> = (0..100).map(|_| b.rng.random()).collect();
assert_eq!(va, vb);
}
#[test]
fn entity_rng_same_seed_different_id_different_sequence() {
let mut a = EntityRng::from_seed_and_id(42, 0);
let mut b = EntityRng::from_seed_and_id(42, 1);
let va: u32 = a.rng.random();
let vb: u32 = b.rng.random();
assert_ne!(va, vb);
}
#[test]
fn entity_rng_adjacent_seeds_no_collision() {
// Verify (seed=0, id=N) != (seed=1, id=N-1) — the collision class
// that wrapping_add would create.
let mut a = EntityRng::from_seed_and_id(0, 5);
let mut b = EntityRng::from_seed_and_id(1, 4);
let va: u32 = a.rng.random();
let vb: u32 = b.rng.random();
assert_ne!(va, vb);
}
}