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>
Captures the pre-Godot-port state of the codebase. This is the rollback
anchor for the Godot port (M0 of theriapolis-rpg-implementation-plan-godot-port.md).
All Phase 0 through Phase 6.5 work is included; Phase 7 is in flight.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>