diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 4c0fd46..f58ad6d 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -491,3 +491,49 @@ marked as a temporary experiment with removal instructions and a read-the-result 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`, `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. + +--- diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp index 9f0f266..924024b 100644 --- a/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp +++ b/Source/VoxelForge/Private/Tests/VoxelForgeOpStackMazeTest.cpp @@ -33,6 +33,7 @@ #include "VoxelForgeTestFixture.h" #include "VoxelDensityOpStack.h" +#include "VoxelCaveMorphology.h" // VoxelSDF::Capsule, VoxelHash — for the verbatim copy below #include @@ -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> 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( diff --git a/Source/VoxelForge/VoxelForge.Build.cs b/Source/VoxelForge/VoxelForge.Build.cs index cd58d59..6a89050 100644 --- a/Source/VoxelForge/VoxelForge.Build.cs +++ b/Source/VoxelForge/VoxelForge.Build.cs @@ -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, 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.)