Fix MoistureGen/TemperatureGen non-determinism (FastNoiseLite race)
FastNoiseLite lazily populates its internal _perm[512] table on the first GetNoise call via EnsurePerm(). When called concurrently from a Parallel.For loop, threads race on this initialization and may read a partially-populated table, producing different moisture/temperature values per row across runs. Empirical: a 10-run worldgen-hash sweep on seed 12345 produced 4+ distinct moisture hashes and 3+ distinct temperature hashes. All other channels (elevation, biomes, settlements, polylines) remained stable; biomes only because their bucket thresholds happened to absorb the upstream float noise. The fix is the same one ElevationGenStage:125-130 and BorderDistortionGenStage: 102-104 already apply: call GetNoise once on the main thread before the Parallel.For so _perm is fully initialized when worker threads start reading. MoistureGenStage and TemperatureGenStage were missing this; now they have it. WorldgenDeterminismTests didn't catch this because xUnit's WorldCache fixture runs both pipeline variants in the same process, where consecutive runs hit the same JIT/thread-pool state and produce the same corrupted output. The Godot port surfaced it by invoking Core from a fresh process with different threading. Verified: post-fix 10-run sweep produces stable hashes on all six channels (0xA8F99BB9795D8CF8 moisture, 0xAA05F3FB1523F6C3 temperature, seed 12345). 708/708 tests still pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,10 @@ public sealed class MoistureGenStage : IWorldGenStage
|
||||
int W = C.WORLD_WIDTH_TILES;
|
||||
int H = C.WORLD_HEIGHT_TILES;
|
||||
|
||||
// FastNoiseLite.EnsurePerm() is not thread-safe; calling GetNoise once here
|
||||
// populates _perm in the main thread before the parallel loop reads it.
|
||||
_ = noise.GetNoise01(0f, 0f);
|
||||
|
||||
Parallel.For(0, H, ty =>
|
||||
{
|
||||
for (int tx = 0; tx < W; tx++)
|
||||
|
||||
@@ -29,6 +29,10 @@ public sealed class TemperatureGenStage : IWorldGenStage
|
||||
int W = C.WORLD_WIDTH_TILES;
|
||||
int H = C.WORLD_HEIGHT_TILES;
|
||||
|
||||
// FastNoiseLite.EnsurePerm() is not thread-safe; calling GetNoise once here
|
||||
// populates _perm in the main thread before the parallel loop reads it.
|
||||
_ = noise.GetNoise(0f, 0f);
|
||||
|
||||
Parallel.For(0, H, ty =>
|
||||
{
|
||||
// Latitude: 0 at north (cold), 1 at south (warm)
|
||||
|
||||
Reference in New Issue
Block a user