fix: AUDIT C1 — bounded, site-salted seed offsets across all 85 noise sites

The op stack had already inherited this three times and every remaining port would
copy it again, so fixing it now is cheaper than after.

The audit's documented fix was wrong: bounding SeedF while keeping the * 97.7f
multiplier still reaches 1.6e6, where the ULP is 0.19 — 9.5x the per-voxel step.
Less spectacular, still broken, ticket closed.

VoxelHash::SeedOffset(Seed, SiteKey) inverts the roles: the multiplier no longer
decorrelates by amplifying, it IDENTIFIES the site, and the hash decorrelates.
Output is in final units, bounded to [0, 16383], so the ULP is 10% of a voxel step.
Site-salted, so two seeds must collide at all ~50 sites rather than sharing one
global bucket.

Safe to apply without compiling because the transformation is a pure regex and the
literal stays visible at the call site, so each line remains eye-checkable against
the original. Applied to all three files in one pass so the archetype switch and the
ported ops changed identically — had they not, the three equivalence tests would say
so. 62 + 7 + 16 sites, none left, plus two bare `+ SeedF` worm sites by hand.

New test VoxelForge.Determinism.LargeSeedSurvives (seeds up to 2e9) because the
equivalence tests are structurally blind to this: they compare the stack against the
switch, both read the same faulty expression, so at a large seed both collapse
identically — bit-identical, green, and both flat. An oracle that shares the bug
cannot see it. This test asserts a property instead of a comparison.

EXPECT EVERY WORLD TO LOOK DIFFERENT: this re-rolls every noise offset in the plugin.
Intended, and covered by OPSTACK-PLAN 2.6.1.

UNVERIFIED: not compiled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-07-27 16:33:36 +02:00
1 parent f7ed9407bf
commit cd4cf216f5
7 files changed
+346 -109

No files matched your search

@@ -254,7 +254,7 @@ namespace
{
public:
FSlabVoidSource(const FSlabGenerationParams& P, int32 Seed)
: SeedF((float)Seed)
: SeedU((uint32)Seed)
, FloorRoughness(P.FloorRoughness)
, FloorFrequency(P.FloorRoughnessFrequency)
, CeilRoughness(P.CeilingRoughness)
@@ -364,8 +364,8 @@ namespace
{
if (FloorRoughness <= 0.0f) { return FloorZ; }
const float FF = FloorFrequency;
const FVector NoisePos(WorldX * FF + SeedF * 7.3f,
WorldY * FF + SeedF * 11.1f,
const FVector NoisePos(WorldX * FF + VoxelHash::SeedOffset(SeedU, 7.3f),
WorldY * FF + VoxelHash::SeedOffset(SeedU, 11.1f),
0.0f);
const float N = VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z,
VoxelGenLOD::Eff(3), 2.0f, 0.5f)
@@ -379,8 +379,8 @@ namespace
if (CeilRoughness > 0.0f)
{
const float CF = CeilFrequency;
const FVector NoisePos(WorldX * CF + SeedF * 17.3f + 1000.0f,
WorldY * CF + SeedF * 19.7f + 2000.0f,
const FVector NoisePos(WorldX * CF + VoxelHash::SeedOffset(SeedU, 17.3f) + 1000.0f,
WorldY * CF + VoxelHash::SeedOffset(SeedU, 19.7f) + 2000.0f,
3000.0f);
const float Raw = VoxelNoise::FBM((float)NoisePos.X, (float)NoisePos.Y, (float)NoisePos.Z,
VoxelGenLOD::Eff(3), 2.0f, 0.5f)
@@ -391,7 +391,7 @@ namespace
return FMath::Max(CeilZ - CeilNoise, FloorSurface + 2.0f);
}
float SeedF;
uint32 SeedU;
float FloorZ = 0.0f, CeilZ = 0.0f;
float FloorRoughness, FloorFrequency;
float CeilRoughness, CeilFrequency;
@@ -573,7 +573,7 @@ namespace
public:
FOverhangShelfMod(const FSurfaceGenerationParams& InP, int32 Seed,
const FSurfaceColumnSource* InColumn)
: P(InP), SeedF((float)Seed), Column(InColumn) {}
: P(InP), SeedU((uint32)Seed), Column(InColumn) {}
EVoxelOpRole GetRole() const override { return EVoxelOpRole::DetailModifier; }
void PrepareChunk(const FVoxelOpContext&) override {}
@@ -594,9 +594,9 @@ namespace
// Bruit de forme d'étagère [0,1] ; le terme en Z fait onduler la portée avec la hauteur
// (déchiqueté, pas une lèvre lisse).
const float Ns = HFractal3D(FVector(
WorldX * f + SeedF * 17.3f,
WorldY * f + SeedF * 23.9f,
WorldZ * f * P.OverhangZScale + SeedF * 5.1f), 3) * 0.5f + 0.5f; // [0,1]
WorldX * f + VoxelHash::SeedOffset(SeedU, 17.3f),
WorldY * f + VoxelHash::SeedOffset(SeedU, 23.9f),
WorldZ * f * P.OverhangZScale + VoxelHash::SeedOffset(SeedU, 5.1f)), 3) * 0.5f + 0.5f; // [0,1]
// LA CLÉ : la portée amont CROÎT avec la hauteur dans la fenêtre (Frac : 0 au sol → 1
// au plafond de la fenêtre). En bas le décalage est minuscule ⇒ on emprunte de la roche
@@ -626,7 +626,7 @@ namespace
private:
FSurfaceGenerationParams P;
float SeedF;
uint32 SeedU;
const FSurfaceColumnSource* Column; // NON possédant : la pile possède la source
};