Skip to content

[Engine] Overhaul simulation correctness, world generation, and chunk sleeping#13

Open
Kakapio wants to merge 1 commit into
mainfrom
core-sim-overhaul
Open

[Engine] Overhaul simulation correctness, world generation, and chunk sleeping#13
Kakapio wants to merge 1 commit into
mainfrom
core-sim-overhaul

Conversation

@Kakapio

@Kakapio Kakapio commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

A deep-dive review of the core engine found several correctness bugs (including one genuine data race), constant GPU re-upload churn, and a move pipeline that every future particle type would have to re-implement. This PR fixes all of it in three pieces, with a regression test suite to keep it fixed.

1. Move pipeline: one pure decision path, one application path

Simulator implementations are now pure: FluidSimulator::calculate_step reads a StepContext and returns a StepAction (Stay, MoveTo, ReplaceTarget, ConvertTarget) — the engine owns all writes via a single apply_local_step. In-chunk empty-cell moves apply immediately; cross-chunk moves and all interactions become DeferredSteps, applied serially in deterministic order with both ends re-validated. A step that lost its race simply doesn't happen and the particle stays put.

This one mechanism fixes four bugs:

  • Cross-chunk Replace interactions placed the result at the source cell (water turning into obsidian in place, lava untouched)
  • Cross-chunk Preserve interactions silently did nothing
  • Inter-chunk move conflicts destroyed the losing particle (slow mass leak wherever streams converged near chunk borders)
  • In-chunk interactions only fired when the target happened to be earlier in iteration order

Simulation is now fully deterministic: per-(tick, chunk) seeded SmallRng plus the ordered apply phase make thread scheduling irrelevant. The dashmap dependency is gone.

2. World generation: no more unsafe, seeded, layout-correct

The old generator shared a Vec<Chunk> across manually-spawned threads through UnsafeCell + unsafe impl Sync, partitioned by column ranges — but ore veins write at ±1 offsets across partition boundaries, so this was a real data race (UB). It's replaced by rayon per-chunk generation with vein spill-over collected and applied serially. Zero unsafe remains in the codebase.

  • Map::generate_with_seed makes worlds reproducible (generate still picks a random seed)
  • Chunks now live in a flat row-major Vec behind one shared coords::chunk_index convention, fixing a latent bug where the chunk layout was only correct for square maps
  • roll_special_particle no longer allocates a Vec and sorts per cell
  • Generation of the full 640×640 map runs in ~0.9 ms

3. Chunk sleeping and conditional versions

  • Chunk::version only bumps when a pass actually changes something, so the renderer stops re-uploading 4 KB materials at 80 Hz for still water
  • A chunk whose pass changes nothing goes to sleep; it wakes on any mutation (set_particle_at, including facing neighbors for border cells) or when a neighbor's border changes (BorderActivity). Simulation cost now scales with moving liquid, not total liquid
  • The old dirty/trigger_refresh/update_dirty_chunks machinery became redundant and was deleted — painting now activates simulation without depending on Update-schedule ordering

Tests

Restructured as lib + bin (src/lib.rs) so integration tests use cavernborn:: directly instead of the previous #[path] include hack. 24 tests:

  • Mass conservation: particle count asserted on every one of 300 ticks while a blob falls and spreads across chunk boundaries
  • Cross-chunk and in-chunk interaction regressions (obsidian forms at the lava's position; acid conversion preserves the water)
  • Sleep/wake: settled chunks stop simulating and their versions freeze; erasing a particle wakes them
  • Determinism: identical worlds after 150 ticks of chaotic spreading
  • Generator: seed determinism, no floating gaps, plus insta snapshots of generated terrain and a settled pool (64 particles → exactly two flat rows)

Deliberately preserved quirks (follow-up candidates)

  • Liquids check only the target cell, not the path, so they can "tunnel" through thin floors at high viscosity (pre-existing)
  • The .max(0) edge clamp lets lone droplets wander sideways along the floor row (pre-existing)
  • Preserve interactions are directional despite commutative rule keys — part of the planned material/property-table redesign

🤖 Generated with Claude Code

… sleeping

Restructure as lib + bin so integration tests can use the crate directly.

Simulation: make simulators pure deciders (calculate_step -> StepAction)
with the engine owning all writes. Cross-chunk moves and all interactions
become DeferredSteps, applied serially with re-validation, fixing:
- cross-chunk Replace placing the result at the source cell
- cross-chunk Preserve interactions silently doing nothing
- inter-chunk move conflicts destroying the losing particle
- iteration-order-dependent in-chunk interactions
Per-(tick, chunk) seeded RNG + ordered apply make simulation fully
deterministic; DashMap dependency removed.

Generation: replace UnsafeCell + manual threads (a real data race via
vein spill-over across thread boundaries) with rayon per-chunk generation
and serial spill application. Zero unsafe remains. Seeded generation via
Map::generate_with_seed. Flat row-major chunk storage with one shared
index convention fixes the square-map-only chunk layout.

Performance: chunk versions only bump on real change (no more 80 Hz GPU
re-uploads of still water) and settled chunks sleep, woken by mutations
and neighbor border activity. Dirty-flag refresh machinery deleted.

Tests: 24 passing - mass conservation, cross-chunk interaction
regressions, sleep/wake, determinism, generator invariants, and insta
snapshots of terrain and a settled pool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant