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:
@@ -33,6 +33,7 @@
|
||||
|
||||
#include "VoxelForgeTestFixture.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
#include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash — for the verbatim copy below
|
||||
|
||||
#include <atomic>
|
||||
|
||||
@@ -45,6 +46,88 @@ namespace
|
||||
{
|
||||
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. */
|
||||
bool ResolveMazeParams(const VoxelForgeTest::FTestWorld& World, FMazeGenerationParams& Out,
|
||||
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));
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// 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)
|
||||
{
|
||||
AddInfo(FString::Printf(
|
||||
|
||||
@@ -12,31 +12,25 @@ public class VoxelForge : ModuleRules
|
||||
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
|
||||
// port (VoxelForge.OpStack.MazeEquivalence: 454 of 20000 samples, 0 crossing the
|
||||
// isosurface) is caused by the compiler being allowed to reassociate identical
|
||||
// source differently per translation unit.
|
||||
// Setting FPSemantics (or any other property that alters this module's compile
|
||||
// environment) makes VoxelForge ineligible for the ENGINE'S SHARED PCH: UBT can only
|
||||
// share a precompiled header between modules whose compile environments match. The
|
||||
// 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
|
||||
// NOT affect this one — every line of density code lives in VoxelForge, so the
|
||||
// switch has to be here to mean anything. (That mistake already cost one build and
|
||||
// one wrong conclusion.)
|
||||
// So the plugin has a latent IWYU (include-what-you-use) debt: several public headers
|
||||
// use engine types they never include. That is worth fixing on its own terms one day
|
||||
// (UE has been moving away from implicit shared-PCH includes for years), but it is a
|
||||
// real chunk of work and must not be attempted inside an unrelated diagnostic.
|
||||
//
|
||||
// UnrealBuildTool's Windows default is /fp:fast ("Default is imprecise FP
|
||||
// semantics", VCToolChain.cs); every Clang target defaults to precise instead.
|
||||
//
|
||||
// READ THE RESULT LIKE THIS:
|
||||
// 454 -> 0 : the FP model WAS the cause. Then decide separately whether to keep
|
||||
// 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;
|
||||
// The FP question it was meant to settle — whether identical source reassociates
|
||||
// differently per translation unit under /fp:fast — is now answered inside
|
||||
// VoxelForge.OpStack.MazeEquivalence instead, by compiling a verbatim copy of the
|
||||
// Maze core into the TEST's translation unit and comparing all three. No build
|
||||
// settings involved, and it cannot break anything.
|
||||
|
||||
// Modules we depend on:
|
||||
// - Core: Basic types (TArray, FString, etc.)
|
||||
|
||||
Reference in New Issue
Block a user