build: revert FPSemantics (drops the shared PCH); answer the FP question in the test instead

Setting FPSemantics on VoxelForge broke the build with ~30 "undefined type"
errors -- UMaterialInterface, USoundBase, TSubclassOf<AActor>, APawn,
ENABLE_DRAW_DEBUG -- none of them FP-related. UBT can only share a precompiled
header between modules whose compile environments match, so changing FPSemantics
cost the module the engine's shared PCH and with it ~30 includes the plugin has
always relied on getting for free.

That is a genuine latent IWYU debt in seven files, and worth fixing on its own
terms one day, but not inside an unrelated diagnostic. Reverted, with the reason
recorded in Build.cs so nobody retries it blind.

The question it was meant to settle is now answered without touching any build
setting: MazeEquivalence compiles a verbatim copy of the Maze core into the
TEST's translation unit and compares three implementations of identical source --
the generator's TU, the op stack's TU, and the test's own.

  A != C          -> same source, different TU, different result: the compiler.
                     Nothing to fix in the port.
  A == C, B != C  -> source is TU-stable, so the op stack differs for a LOGIC
                     reason, and it is in FLatticeCorridorSource or FSdfCarveOp.

Duplicating code is normally a fault. Here it is the only instrument that can
answer the question, because three careful readings all concluded "identical" and
the test keeps disagreeing. Marked diagnostic-only; it comes out once answered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 14:39:09 +02:00
parent a49d440bf6
commit d5c71d6b68
3 changed files with 203 additions and 22 deletions
+46
View File
@@ -491,3 +491,49 @@ marked as a temporary experiment with removal instructions and a read-the-result
paused. paused.
--- ---
## 2026-07-27 — FPSemantics on VoxelForge does not build. Reverted; measuring a safer way.
**What happened:** setting `FPSemantics` on the VoxelForge module broke the build with ~30 errors —
`UMaterialInterface`, `USoundBase`, `TSubclassOf<AActor>`, `APawn`, `ENABLE_DRAW_DEBUG` all
"undefined type". **None of them are FP-related.**
**Why:** UBT can only share a precompiled header between modules whose **compile environments
match**. Changing `FPSemantics` changed VoxelForge's environment, so it lost eligibility for the
engine's shared PCH — and with it ~30 includes the plugin has always been getting for free.
**Genuine latent finding, worth its own item some day:** several public headers use engine types they
never include (`VoxelBiomeDefinition.h`, `VoxelStrateDefinition.h`, `VoxelSettings.h`,
`VoxelStrateTypes.h`, `VoxelContentManager.h`, `VoxelDensityVolume.h`, and `VoxelWorld.cpp`). The
plugin compiles today only because the shared PCH supplies them. UE has been moving away from
implicit shared-PCH includes for years, so this will need doing eventually — **but not inside an
unrelated diagnostic**, which is why it was reverted rather than chased.
**Reverted**, with the reason written into `VoxelForge.Build.cs` so nobody retries it blind.
### The FP question, answered without touching build settings
`MazeEquivalence` now compiles a **verbatim copy of the Maze core into the TEST's translation unit**
and compares three implementations of the same source:
```
A = GetMazeDensity (VoxelGenerator.cpp TU)
B = the operator stack (VoxelDensityOpStack.cpp TU)
C = MazeCoreVerbatim (the test's own TU)
```
- **A != C** ⇒ identical source, different TU, different result ⇒ the compiler, not the port.
Nothing to fix; record it and move on.
- **A == C, B != C** ⇒ the source IS stable across TUs ⇒ the operator stack differs for a **logic**
reason, and it is in `FLatticeCorridorSource` or `FSdfCarveOp`.
Duplicating code is normally a fault; here it is the only instrument that answers the question,
because three careful readings all concluded "identical" and the test disagrees. It is marked
diagnostic-only and comes out once the answer is in.
**UNVERIFIED:** everything in this entry.
**Next single action:** rebuild (normal incremental now — the Build.cs change is reverted) and read
the THREE-WAY block. Phase 1 step 3 still paused.
---
@@ -33,6 +33,7 @@
#include "VoxelForgeTestFixture.h" #include "VoxelForgeTestFixture.h"
#include "VoxelDensityOpStack.h" #include "VoxelDensityOpStack.h"
#include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash — for the verbatim copy below
#include <atomic> #include <atomic>
@@ -45,6 +46,88 @@ namespace
{ {
constexpr int32 NumMazeSamples = 20000; constexpr int32 NumMazeSamples = 20000;
/**
* COPIE VERBATIM du cœur de `GetMazeDensity` (VoxelGenerator.cpp), compilée dans CETTE unité
* de compilation. Diagnostic uniquement — à supprimer une fois la question tranchée.
*
* POURQUOI DUPLIQUER DU CODE, ce qui est normalement une faute :
* la question ouverte est « du code SOURCE IDENTIQUE donne-t-il un résultat différent selon
* l'unité de compilation ? ». On ne peut pas y répondre en relisant le code — trois lectures
* ont conclu « identique » et le test dit le contraire. Il faut un TROISIÈME point de mesure.
*
* A = GetMazeDensity (unité VoxelGenerator.cpp)
* B = la pile d'opérateurs (unité VoxelDensityOpStack.cpp)
* C = cette copie (unité du test)
*
* A != C ⇒ même source, unités différentes, résultats différents ⇒ c'est le COMPILATEUR,
* et cela explique entièrement A != B. Rien à corriger dans le portage.
* A == C ⇒ la source est stable d'une unité à l'autre ⇒ B diffère pour une raison de
* LOGIQUE, et il faut la trouver dans les opérateurs.
*
* Reproduit la variante « corridors + carve ONLY » du bisect (rugosité / seal / spine /
* passages omis), parce que c'est là que le bisect a montré l'écart survivre.
*/
float MazeCoreVerbatim(float WorldX, float WorldY, float WorldZ,
const FMazeGenerationParams& Params, int32 Seed)
{
const float CS = FMath::Max(Params.CellSize, 1.0f);
const FVector Pos(WorldX, WorldY, WorldZ);
const uint32 S = (uint32)Seed ^ 0x4D617A65u; // 'Maze'
float Density = Params.BaseDensity;
const int32 CX = FMath::FloorToInt(WorldX / CS);
const int32 CY = FMath::FloorToInt(WorldY / CS);
const int32 CZ = FMath::FloorToInt(WorldZ / CS);
struct FMazeEdge { FVector A, B; };
TArray<FMazeEdge, TInlineAllocator<24>> Edges;
auto NodeCenter = [CS](int32 X, int32 Y, int32 Z)
{
return FVector((X + 0.5f) * CS, (Y + 0.5f) * CS, (Z + 0.5f) * CS);
};
auto EdgeOpen = [S](int32 X, int32 Y, int32 Z, uint32 AxisSalt, float Threshold) -> bool
{
uint32 H = VoxelHash::Cell(X, Y, S ^ AxisSalt);
H ^= VoxelHash::Mix((uint32)(Z * 73856093) ^ AxisSalt);
return VoxelHash::ToFloat01(VoxelHash::Mix(H)) < Threshold;
};
for (int32 dz = -1; dz <= 0; dz++)
for (int32 dy = -1; dy <= 0; dy++)
for (int32 dx = -1; dx <= 0; dx++)
{
const int32 nx = CX + dx, ny = CY + dy, nz = CZ + dz;
const FVector A = NodeCenter(nx, ny, nz);
if (EdgeOpen(nx, ny, nz, 0xA1u, Params.BranchProbability))
Edges.Add({ A, NodeCenter(nx + 1, ny, nz) });
if (EdgeOpen(nx, ny, nz, 0xB2u, Params.BranchProbability))
Edges.Add({ A, NodeCenter(nx, ny + 1, nz) });
if (EdgeOpen(nx, ny, nz, 0xC3u, Params.Verticality))
Edges.Add({ A, NodeCenter(nx, ny, nz + 1) });
}
const float R = FMath::Max(Params.CorridorRadius, 0.5f);
float MazeSDF = FLT_MAX;
for (const FMazeEdge& E : Edges)
{
MazeSDF = FMath::Min(MazeSDF, VoxelSDF::Capsule(Pos, E.A, E.B, R));
}
// Rugosité omise volontairement (variante du bisect).
const float Blend = 2.0f;
if (MazeSDF < Blend)
{
float Carve = FMath::Clamp((Blend - MazeSDF) / (Blend * 2.0f), 0.0f, 1.0f);
Carve = SmoothStep01(Carve);
Density -= Carve * Params.BaseDensity * 2.0f;
}
return -Density; // convention MC
}
/** Les params Maze de la strate Maze de la fixture, bornes Z de runtime comprises. */ /** Les params Maze de la strate Maze de la fixture, bornes Z de runtime comprises. */
bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out, bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out,
int32& OutTopVoxelZ, int32& OutBottomVoxelZ) int32& OutTopVoxelZ, int32& OutBottomVoxelZ)
@@ -231,6 +314,64 @@ bool FVoxelForgeOpStackMazeTest::RunTest(const FString& Parameters)
Old, Bits(Old), New, Bits(New), S.Sdf, Bits(S.Sdf), Base, CarveOld, CarveNew)); Old, Bits(Old), New, Bits(New), S.Sdf, Bits(S.Sdf), Base, CarveOld, CarveNew));
} }
//=========================================================================
// LE TEST À TROIS VOIES — la mesure qui tranche
//=========================================================================
// A = GetMazeDensity (unité VoxelGenerator.cpp) · B = la pile (unité VoxelDensityOpStack.cpp)
// C = MazeCoreVerbatim (unité DE CE TEST). Voir le commentaire de MazeCoreVerbatim.
if (NumDiff > 0)
{
FMazeGenerationParams Core = MazeParams;
Core.SurfaceRoughness = 0.0f; // variante « corridors + carve ONLY » du bisect
Core.BoundarySealThickness = 0.0f;
UVoxelGenerator* MutableGen = World.Generator.Get();
const float SavedSpine = MutableGen->OriginSpineRadius;
const UVoxelStrateManager* SavedMgr = MutableGen->StrateManager;
MutableGen->OriginSpineRadius = 0.0f;
MutableGen->SetStrateManager(nullptr);
FVoxelOpStack CoreStack;
VoxelDensityOps::BuildMazeStack(CoreStack, Core, World.Settings->Seed, 0.0f, nullptr);
const int32 N = FMath::Min(NumMazeSamples, 5000);
int32 DiffAB = 0, DiffAC = 0, DiffBC = 0;
for (int32 i = 0; i < N; ++i)
{
const float X = (float)Points[i].X, Y = (float)Points[i].Y, Z = (float)Points[i].Z;
const float A = MutableGen->GetMazeDensity(X, Y, Z, Core);
const float B = CoreStack.EvalMC(X, Y, Z);
const float C = MazeCoreVerbatim(X, Y, Z, Core, World.Settings->Seed);
if (!BitEqual(A, B)) { ++DiffAB; }
if (!BitEqual(A, C)) { ++DiffAC; }
if (!BitEqual(B, C)) { ++DiffBC; }
}
MutableGen->OriginSpineRadius = SavedSpine;
MutableGen->SetStrateManager(SavedMgr);
const TCHAR* Verdict =
(DiffAC > 0)
? TEXT("A != C: IDENTICAL SOURCE, DIFFERENT TRANSLATION UNIT, DIFFERENT RESULT. The "
"cause is the compiler, not the port. Nothing to fix in the operator stack -- "
"record it and move on.")
: ((DiffBC > 0)
? TEXT("A == C but B != C: the source IS stable across translation units, so the "
"operator stack differs for a LOGIC reason. Hunt it in the ops -- start "
"with FLatticeCorridorSource's edge sweep and FSdfCarveOp.")
: TEXT("All three agree here, so whatever causes the full-stack difference lives "
"in a stage this core variant switched off (roughness / seal / spine / "
"passages). Re-run the bisect with that in mind."));
AddInfo(FString::Printf(
TEXT("THREE-WAY (corridors + carve only, %d samples):\n")
TEXT(" A generator TU vs B opstack TU : %d differ\n")
TEXT(" A generator TU vs C test TU : %d differ\n")
TEXT(" B opstack TU vs C test TU : %d differ\n")
TEXT(" VERDICT: %s"),
N, DiffAB, DiffAC, DiffBC, Verdict));
}
if (NumDiff == 0) if (NumDiff == 0)
{ {
AddInfo(FString::Printf( AddInfo(FString::Printf(
+16 -22
View File
@@ -12,31 +12,25 @@ public class VoxelForge : ModuleRules
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
// ============================================================================ // ============================================================================
// ⚠️ TEMPORARY EXPERIMENT — 2026-07-27. REMOVE THIS LINE WHEN THE ANSWER IS IN. // ⚠️ DO NOT SET `FPSemantics` HERE — tried 2026-07-27, it does not build.
// ============================================================================ // ============================================================================
// Testing whether the ~1 ULP residue between GetMazeDensity and its operator-stack // Setting FPSemantics (or any other property that alters this module's compile
// port (VoxelForge.OpStack.MazeEquivalence: 454 of 20000 samples, 0 crossing the // environment) makes VoxelForge ineligible for the ENGINE'S SHARED PCH: UBT can only
// isosurface) is caused by the compiler being allowed to reassociate identical // share a precompiled header between modules whose compile environments match. The
// source differently per translation unit. // build then fails with ~30 "undefined type" errors — UMaterialInterface, USoundBase,
// TSubclassOf<AActor>, APawn, ENABLE_DRAW_DEBUG — none of which are FP-related. They
// are includes this plugin has always relied on the shared PCH to provide for free.
// //
// FPSemantics is a PER-MODULE property. Setting it on the VoxelM game module does // So the plugin has a latent IWYU (include-what-you-use) debt: several public headers
// NOT affect this one — every line of density code lives in VoxelForge, so the // use engine types they never include. That is worth fixing on its own terms one day
// switch has to be here to mean anything. (That mistake already cost one build and // (UE has been moving away from implicit shared-PCH includes for years), but it is a
// one wrong conclusion.) // real chunk of work and must not be attempted inside an unrelated diagnostic.
// //
// UnrealBuildTool's Windows default is /fp:fast ("Default is imprecise FP // The FP question it was meant to settle — whether identical source reassociates
// semantics", VCToolChain.cs); every Clang target defaults to precise instead. // differently per translation unit under /fp:fast — is now answered inside
// // VoxelForge.OpStack.MazeEquivalence instead, by compiling a verbatim copy of the
// READ THE RESULT LIKE THIS: // Maze core into the TEST's translation unit and comparing all three. No build
// 454 -> 0 : the FP model WAS the cause. Then decide separately whether to keep // settings involved, and it cannot break anything.
// precise (it costs vectorisation on the density hot path — the thing
// T2.a's SIMD noise work was buying — for an unmeasured amount).
// 454 -> 454 : the FP model is NOT the cause and the difference is real logic.
// Read the WORST-POINT DUMP the test now prints.
//
// EITHER WAY THIS LINE COMES BACK OUT once measured. Keeping /fp:precise on the hot
// path is a decision that needs a profile, not a leftover from a diagnostic.
FPSemantics = FPSemanticsMode.Precise;
// Modules we depend on: // Modules we depend on:
// - Core: Basic types (TArray, FString, etc.) // - Core: Basic types (TArray, FString, etc.)