Skip to content

Fix thread-safety crashes in city generation under parallel worldgen - #788

Open
Arilas wants to merge 1 commit into
McJtyMods:1.21.11_neofrom
Arilas:fix/chunkdriver-thread-safety
Open

Arilas wants to merge 1 commit into
McJtyMods:1.21.11_neofrom
Arilas:fix/chunkdriver-thread-safety

Conversation

@Arilas

@Arilas Arilas commented Jul 24, 2026

Copy link
Copy Markdown

Problem

Each dimension caches a single LostCityTerrainFeature, which owns a single shared ChunkDriver, and IDimensionInfo.setWorld() swaps the shared region reference right before every generation. The feature step of chunk generation can run concurrently for different chunks on worker threads, so there is a race: one thread's driver teardown (the setPrimer(oldRegion, oldChunk) restore at the end of generate()) or region swap can land in the middle of another thread's generation.

Crashes we captured during world creation ("Preparing spawn area"):

  • java.lang.NullPointerException: Cannot invoke "ChunkAccess.getPos()" because "this.primer" is null at ChunkDriver.current (via Spheres.fillSphere ← LostCitySphereFeature.place)
  • NullPointerException: "this.region" is null at ChunkDriver.getBlock
  • IllegalStateException: Requested chunk unavailable during world generation at WorldGenRegion.getChunk via getBlockSafe → updateAdjacent → correct → block
  • generation running against another thread's WorldGenRegion (visible as reads/writes 3–8 chunks outside the current region)

This reproduces near-instantly with parallel-worldgen mods (C2ME on the Fabric side is how we hit it), but the race itself exists under vanilla parallel chunk generation too.

Fix

  • LostCityFeature.place and LostCitySphereFeature.place now synchronize the setWorld + generate sequence on the shared per-dimension terrain feature instance (both features use the same monitor). Only Lost Cities' own generation is serialized per dimension; the rest of worldgen stays parallel.
  • ChunkDriver.getBlockSafe/updateAdjacent treat neighbouring chunks that aren't available in the current WorldGenRegion as air / skip the update instead of throwing — these reads only drive cosmetic connection states (fences, walls, stairs), so a neighbour read can never kill chunk generation anymore.

Verification

  • ./gradlew compileJava passes on this branch (1.21.11_neo).
  • The identical fix, ported to a Fabric/26.2 build of this codebase, was smoke-tested with dedicated-server world creation on fresh worlds using both the default and biosphere profiles: no exceptions, cities and sphere shells generate. I have not run the NeoForge build in-game.

🤖 Generated with Claude Code

The per-dimension LostCityTerrainFeature holds a single shared ChunkDriver,
and IDimensionInfo.setWorld() swaps the shared region reference right before
each generation, with no synchronization at the feature entry points. The
vanilla feature step can run concurrently for different chunks on worker
threads, so one thread's driver teardown (setPrimer restore) or region swap
can hit in the middle of another thread's generation. Observed crashes:

- NullPointerException: "this.primer" is null at ChunkDriver.current
  (via Spheres.fillSphere / LostCitySphereFeature.place)
- NullPointerException: "this.region" is null at ChunkDriver.getBlock
- IllegalStateException: Requested chunk unavailable during world generation
  at WorldGenRegion.getChunk via getBlockSafe -> updateAdjacent -> correct
- Generation running against another thread's WorldGenRegion, visible as
  reads/writes 3-8 chunks outside the current region

This reproduces near-instantly with parallel worldgen mods like C2ME but the
race also exists under vanilla parallel chunk generation.

Fixes:
- LostCityFeature.place and LostCitySphereFeature.place now synchronize the
  setWorld + generate sequence on the shared per-dimension terrain feature
  instance (both features use the same monitor), so Lost Cities' own
  generation is serialized per dimension while the rest of worldgen stays
  parallel
- ChunkDriver.getBlockSafe/updateAdjacent treat neighbouring chunks that are
  not available in the current WorldGenRegion as air / skip the update
  instead of crashing; these paths only drive cosmetic connection states
  (fences, walls, stairs)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@McJty

McJty commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

I'm actually working on a big reword of Lost Cities in 1.20.1 branch which I will later port over to 1.21. These changes include work around thread-safety and such so I would wait a bit with this.

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.

2 participants