From 6a60732c98d848f416195fe2ab2cdf1a7023ffbf Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 27 Jul 2026 15:53:23 +0200 Subject: [PATCH] build: FPSemantics = Precise + clear the IWYU debt that blocked it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Jahni: cross-platform play (Linux and Windows, either side hosting) is a product requirement, so fix C9 at the cause instead of measuring the symptom. Verified in UE 5.7 source rather than assumed: VCToolChain.cs:1264 Default -> /fp:fast Precise -> /fp:precise ClangToolChain.cs:712 Default -> -ffp-contract=off Precise -> -ffp-contract=off Default really did mean opposite float models per platform; Precise collapses them onto the same one, so a Windows host and a Linux client agree by construction. Losing the shared PCH is what the IWYU debt hid behind. Every use turned out to be a pointer, TWeakObjectPtr or TSubclassOf parameter, so forward declarations suffice; only the templates and macros needed real includes. Seven headers fixed. VoxelDensityVolume.h was the one worth catching: it tests ENABLE_DRAW_DEBUG in an #if, and an undefined macro there is silently 0 — the debug block would have vanished without a warning rather than failing the build. Include paths verified against the engine tree, not guessed. Expect a residual tail; the shared PCH hid these for years and only a build enumerates them all. Build.cs now says so, and says the fix is to add the include rather than revert FPSemantics. Also adds VoxelForge.Determinism.CrossPlatformDigest: SHAPE digest (sign of density = the world) and FIELD digest (bit-for-bit) over a fixed integer grid, plus NearIso to bound how many samples could flip sign at all. Reports rather than asserts until pinned. The cross-platform comparison itself is deferred per Jahni. Expect a perf regression from losing reassociation and contraction on a noise-heavy hot path — measure against ARCHITECTURE 8.10. UNVERIFIED: not compiled. Co-Authored-By: Claude Opus 5 --- OPSTACK-PROGRESS.md | 71 +++++ .../Tests/VoxelForgeCrossPlatformTest.cpp | 251 ++++++++++++++++++ .../Public/VoxelAtmosphereManager.h | 1 + .../VoxelForge/Public/VoxelBiomeDefinition.h | 5 + .../VoxelForge/Public/VoxelContentManager.h | 2 + Source/VoxelForge/Public/VoxelDensityVolume.h | 8 + Source/VoxelForge/Public/VoxelSettings.h | 3 + .../VoxelForge/Public/VoxelStrateDefinition.h | 7 + Source/VoxelForge/Public/VoxelStrateTypes.h | 2 + Source/VoxelForge/VoxelForge.Build.cs | 58 ++-- 10 files changed, 392 insertions(+), 16 deletions(-) create mode 100644 Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp diff --git a/OPSTACK-PROGRESS.md b/OPSTACK-PROGRESS.md index 779550d..ad9f586 100644 --- a/OPSTACK-PROGRESS.md +++ b/OPSTACK-PROGRESS.md @@ -1119,3 +1119,74 @@ needs Jahni's go-ahead on scope (~40 sites, re-rolls every world, one build). - **`SurfaceWorld`** (`OPSTACK-DECOMPOSITION §5`): the biggest op-stack payoff, most care. --- + +## 2026-07-27 — C9 FIXED AT THE CAUSE: FPSemantics = Precise, IWYU debt cleared. + +**Jahni's call, and the right one:** *"I believe we could put FPSemantics to precise? Clearing up +anything that makes us not use it, then skip on the cross-platform same result for now and continue +on the rework of the opstack."* Fix the cause rather than measure the symptom — if both toolchains +compile IEEE-754 with no contraction, they agree **by construction** and there is nothing left to +compare. + +**Verified in the UE 5.7 source rather than assumed** (`E:\Program Files\Epic Games\UE_5.7`): + +| | `Default` | `Precise` | +|---|---|---| +| `VCToolChain.cs:1264` (Windows/MSVC) | `/fp:fast` | **`/fp:precise`** | +| `ClangToolChain.cs:712` (Linux/Mac) | `-ffp-contract=off` | **`-ffp-contract=off`** | + +So `Default` really did mean **opposite** float models per platform, and `Precise` collapses them +onto the same one. `FPSemantics` is a `ModuleRules` property (`ModuleRules.cs:777`), so per-module +is the right granularity. + +### The IWYU debt, cleared + +Losing the shared PCH is what the debt was hiding behind. All uses turned out to be pointers, +`TWeakObjectPtr` or `TSubclassOf` parameters, so **forward declarations suffice** — only the +templates and macros needed real includes: + +| Header | Added | +|---|---| +| `VoxelBiomeDefinition.h` | `class UMaterialInterface;` | +| `VoxelSettings.h` | `class UMaterialInterface;` | +| `VoxelStrateDefinition.h` | `Templates/SubclassOf.h` + `UMaterialInterface`, `USoundBase`, `AActor` | +| `VoxelStrateTypes.h` | `Templates/SubclassOf.h` + `AActor` | +| `VoxelContentManager.h` | `Templates/SubclassOf.h` + `AActor` | +| `VoxelAtmosphereManager.h` | `class AActor;` | +| `VoxelDensityVolume.h` | **`DrawDebugHelpers.h`** + `class AActor;` | + +**`VoxelDensityVolume.h` was the one worth catching.** It uses `ENABLE_DRAW_DEBUG` in an `#if`, and +an **undefined macro in an `#if` is silently 0** — so without the include the debug block would have +vanished without a single warning, rather than failing the build. Include paths verified against the +engine tree (`Engine/Public/DrawDebugHelpers.h`, `CoreUObject/Public/Templates/SubclassOf.h`), not +guessed. + +**Expect a residual tail.** The shared PCH hid these for years and only a real build enumerates them +all. Any further "undefined type" error from this build is IWYU, **not** the float setting — the fix +is to add the include, never to revert `FPSemantics`. That instruction is now written into +`VoxelForge.Build.cs` where the next person will hit it. + +### Also landed: the cross-platform instrument (kept, not blocking) + +`VoxelForge.Determinism.CrossPlatformDigest` — two FNV-1a digests over a fixed integer grid (no RNG, +so the sample set cannot itself diverge): +- **SHAPE** — the sign of density only. This is all the mesher reads, so it *is* the world: same + shape digest ⇒ same cavities, same walls, same collision. Jahni's "99.99% reproducible", literally. +- **FIELD** — every float bit. Differing while SHAPE matches ⇒ sub-voxel vertex wobble, harmless. + +Plus `NearIso`: how many samples sit within 1e-4 of the isosurface, i.e. how many could *possibly* +flip sign under a float-model change. That **bounds** the risk instead of assuming it. Digests are +reported, not asserted, until pinned — pinning before the platforms agree would just carve the +divergence into the test. Per Jahni, the cross-platform comparison is deferred; the test costs +nothing to leave in and becomes a permanent regression guard the day someone runs it on Linux. + +**UNVERIFIED:** none of this is compiled. **Expect a perf regression** — `/fp:precise` forbids the +reassociation and FMA contraction `/fp:fast` allowed, on a noise-heavy hot path. Worth measuring +against `ARCHITECTURE §8.10` rather than assuming it is small. + +**Next single action:** build. Then, per Jahni, back to the opstack — `SurfaceWorld` +(`OPSTACK-DECOMPOSITION §5`), the biggest payoff and the most care: the T1.a column cache and the +exact-lattice `ClassifyTile` bound must both survive the port. `§C1` (bounded seed offsets) stays +open and still wants doing before too many more archetypes copy it. + +--- diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp new file mode 100644 index 0000000..f67f532 --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeCrossPlatformTest.cpp @@ -0,0 +1,251 @@ +// VoxelForgeCrossPlatformTest.cpp +// LA QUESTION MULTIJOUEUR, RENDUE MESURABLE / THE MULTIPLAYER QUESTION, MADE MEASURABLE +// +// Jahni, 2026-07-27 : *« je voudrais que le jeu soit jouable sur les deux plateformes, Linux et +// Windows, donc un hôte Windows avec un client Linux pourrait arriver, et l'inverse. »* +// Et la barre d'acceptation : *« 99.99% au pire reproductible si deux personnes partagent la même +// seed, puisque tout le monde le reconstruit en multijoueur. »* +// +// ⚠️ LE PROBLÈME (AUDIT §C9) : le MÊME `FPSemanticsMode.Default` d'UBT ne veut pas dire la même +// chose selon la toolchain — `VCToolChain` (Windows/MSVC) le résout en **`/fp:fast`**, +// `ClangToolChain` (Linux/Mac/Windows-Clang) le résout en **précis + `-ffp-contract=off`**. Deux +// builds de la MÊME source sont donc compilés sous des règles flottantes OPPOSÉES. Un hôte Windows +// et un client Linux ne sont pas seulement *autorisés* à diverger : ils sont compilés pour. +// +// ───────────────────────────────────────────────────────────────────────────────────────── +// CE TEST NE CORRIGE RIEN — IL MESURE, et c'est ce qui manque +// ───────────────────────────────────────────────────────────────────────────────────────── +// Aucune quantité de raisonnement ne dit à quel point les deux plateformes divergent : il faut le +// LIRE. Ce test produit deux empreintes du même monde, à la même seed, et les affiche. On le lance +// sur Windows, on le lance sur Linux, on compare les deux lignes. +// +// • **EMPREINTE DE FORME** — le SIGNE de la densité seulement (solide / air). C'est la SEULE +// chose que le mesher lit (`D >= IsoLevel ⇒ air`). Si cette empreinte est identique, les deux +// plateformes ont **le même monde** : mêmes cavités, mêmes murs, même navigabilité, même +// collision au voxel près. C'est littéralement le critère « 99.99% reproductible » de Jahni. +// +// • **EMPREINTE DE CHAMP** — tous les bits de tous les floats. Identique ⇒ reproductibilité +// BIT à BIT. Différente alors que la forme est identique ⇒ la divergence est un frémissement +// sous-voxel de la position des sommets, sans conséquence de jeu. +// +// C'est le bon découpage parce qu'il sépare les deux échecs possibles, qui n'ont pas du tout la +// même gravité : +// +// forme == && champ == ⇒ parfait, rien à faire. +// forme == && champ != ⇒ ACCEPTABLE. Les sommets bougent de ~1e-5 voxel. Personne ne le voit, +// rien ne s'y accroche — SAUF si un jour on compare des hashs de +// géométrie entre pairs. À ne pas faire, donc. +// forme != ⇒ **INACCEPTABLE**. Un voxel solide chez l'un est de l'air chez +// l'autre : un joueur traverse un mur que l'autre voit plein. +// +// ⚠️ POURQUOI LA FORME A DE BONNES CHANCES DE TENIR MÊME AUJOURD'HUI — et pourquoi il faut quand +// même la mesurer : toutes les décisions STRUCTURELLES du plugin (quelle arête de treillis est +// ouverte, quelle cellule porte une colonne, où sont les salles et les passages) passent par +// `VoxelHash::*`, c.-à-d. de l'ARITHMÉTIQUE ENTIÈRE, identique sur toute plateforme. Le flottant +// ne décide que la POSITION de la surface. Un signe ne bascule donc que si un échantillon tombe à +// ~1e-5 de l'isosurface — d'où le troisième chiffre affiché, `NearIso`, qui BORNE le risque au +// lieu de le supposer. +// +// The structural decisions all go through integer hashing, so only the surface POSITION is +// float-decided. A sign flips only where a sample sits within ~1e-5 of the isosurface, which is why +// NearIso is reported: it bounds the risk instead of assuming it. +// +// ───────────────────────────────────────────────────────────────────────────────────────── +// COMMENT S'EN SERVIR / HOW TO USE THIS +// ───────────────────────────────────────────────────────────────────────────────────────── +// 1. Lancer sur Windows, noter les deux empreintes. +// 2. Lancer sur Linux, comparer. +// 3. Une fois `FPSemantics = Precise` posé sur le module (AUDIT §C9, bloqué par la dette IWYU) +// et les deux plateformes d'accord : **épingler** les valeurs dans `PinnedShapeDigest` / +// `PinnedFieldDigest` ci-dessous. Le test devient alors un garde-fou permanent — toute +// régression de déterminisme échoue bruyamment, sur la plateforme qui a dérivé. +// +// Tant que les constantes valent 0, le test ne peut pas échouer sur les empreintes : il RAPPORTE. +// C'est délibéré — épingler une valeur avant que les plateformes soient d'accord ne ferait que +// graver la divergence dans le test. + +#if WITH_DEV_AUTOMATION_TESTS + +#include "Misc/AutomationTest.h" + +#include "VoxelForgeTestFixture.h" +#include "VoxelGenerator.h" + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeCrossPlatformTest, + "VoxelForge.Determinism.CrossPlatformDigest", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + +namespace +{ + //========================================================================= + // ÉPINGLES / PINS — 0 = « pas encore d'accord de référence », le test rapporte sans juger. + //========================================================================= + // À remplir UNIQUEMENT quand Windows et Linux rendent la même valeur. Voir l'en-tête. + constexpr uint64 PinnedShapeDigest = 0; + constexpr uint64 PinnedFieldDigest = 0; + + // FNV-1a 64 bits, octet par octet, **entier pur**. Volontairement écrit à la main plutôt que + // pris dans le moteur : une empreinte de déterminisme ne doit dépendre d'aucune implémentation + // qui pourrait, elle, varier. Ici il n'y a que des `^` et des `*` sur uint64. + // Hand-rolled on purpose: a determinism digest must not depend on an implementation that could + // itself vary. Nothing here but XOR and multiply on uint64. + constexpr uint64 FnvOffsetBasis = 0xcbf29ce484222325ull; + constexpr uint64 FnvPrime = 0x00000100000001b3ull; + + FORCEINLINE void FnvAccumByte(uint64& H, uint8 B) + { + H ^= (uint64)B; + H *= FnvPrime; + } + + FORCEINLINE void FnvAccumU32(uint64& H, uint32 V) + { + FnvAccumByte(H, (uint8)( V & 0xFFu)); + FnvAccumByte(H, (uint8)((V >> 8) & 0xFFu)); + FnvAccumByte(H, (uint8)((V >> 16) & 0xFFu)); + FnvAccumByte(H, (uint8)((V >> 24) & 0xFFu)); + } + + /** Bits d'un float, avec les NaN NORMALISÉS : un NaN a plusieurs représentations et rien ne + * garantit que deux plateformes produisent la même. On les compte à part. */ + FORCEINLINE uint32 FloatBitsNormalised(float V, bool& bOutWasNaN) + { + bOutWasNaN = FMath::IsNaN(V); + if (bOutWasNaN) { return 0x7FC00000u; } + return *reinterpret_cast(&V); + } +} + +bool FVoxelForgeCrossPlatformTest::RunTest(const FString& Parameters) +{ + using namespace VoxelForgeTest; + + FTestWorld World; + World.Build(); + if (!World.IsValid()) + { + AddError(World.WhyInvalid()); + return false; + } + + const UVoxelGenerator* Gen = World.Generator.Get(); + + //========================================================================= + // LA GRILLE — entièrement déterministe, SANS RNG + //========================================================================= + // Pas de `FRandomStream` ici, contrairement aux autres tests : l'ensemble des points doit être + // identique sur les deux plateformes SANS dépendre d'une seule ligne de code partagé. Une + // boucle entière sur des bornes entières ne peut pas diverger. + // No RNG: the point set must be identical across platforms without depending on any shared + // code at all. An integer loop over integer bounds cannot diverge. + const int32 TopVoxelZ = World.TopChunkZ * CHUNK_SIZE + CHUNK_SIZE - 1; + const int32 BottomVoxelZ = World.BottomChunkZ * CHUNK_SIZE; + + constexpr int32 XYStep = 8; + constexpr int32 ZStep = 8; + const int32 XYExtent = 3 * CHUNK_SIZE; // couvre la spine (0,0), les passages et le rocher + + uint64 ShapeDigest = FnvOffsetBasis; + uint64 FieldDigest = FnvOffsetBasis; + + int32 NumSamples = 0, NumSolid = 0, NumNaN = 0, NumNearIso = 0; + + // `NearIso` : à quelle distance de zéro un échantillon doit-il être pour qu'une différence + // d'ULP puisse faire basculer son SIGNE ? Les écarts mesurés entre le `switch` et la pile + // valent ~1e-5 au pire (AUDIT §C10) ; un écart entre modèles flottants est du même ordre. + // Tout échantillon plus loin que ça de l'isosurface ne peut PAS changer de côté. + constexpr float NearIsoBand = 1.0e-4f; // 10× la pire divergence observée : marge délibérée + + for (int32 Z = BottomVoxelZ; Z <= TopVoxelZ; Z += ZStep) + { + for (int32 Y = -XYExtent; Y <= XYExtent; Y += XYStep) + { + for (int32 X = -XYExtent; X <= XYExtent; X += XYStep) + { + const float D = Gen->GetDensityAt((float)X, (float)Y, (float)Z); + + bool bWasNaN = false; + const uint32 Bits = FloatBitsNormalised(D, bWasNaN); + if (bWasNaN) { ++NumNaN; } + + // FORME : un seul bit par échantillon — le côté de l'isosurface, ce que lit le + // mesher. C'est l'empreinte qui doit tenir entre plateformes. + const bool bAir = (D >= 0.0f); + if (!bAir) { ++NumSolid; } + FnvAccumByte(ShapeDigest, bAir ? 1u : 0u); + + // CHAMP : tous les bits. + FnvAccumU32(FieldDigest, Bits); + + if (!bWasNaN && FMath::Abs(D) < NearIsoBand) { ++NumNearIso; } + ++NumSamples; + } + } + } + + //========================================================================= + // LE RAPPORT — c'est le produit de ce test + //========================================================================= + AddInfo(FString::Printf( + TEXT("CROSS-PLATFORM DIGEST (seed %d, %d samples, step %d/%d)\n") + TEXT(" SHAPE digest : 0x%016llX <- must match across Windows/Linux. This is the world.\n") + TEXT(" FIELD digest : 0x%016llX <- bit-for-bit. May differ; see NearIso below.\n") + TEXT(" solid %d / air %d / NaN %d"), + World.Settings->Seed, NumSamples, XYStep, ZStep, + ShapeDigest, FieldDigest, NumSolid, NumSamples - NumSolid, NumNaN)); + + // LE chiffre qui borne le risque, plutôt que de le supposer. + if (NumNearIso == 0) + { + AddInfo(FString::Printf( + TEXT("NearIso: 0 of %d samples sit within %.1e of the isosurface. No sample is close ") + TEXT("enough for a float-model difference (~1e-5 worst observed) to flip its SIGN, so ") + TEXT("the SHAPE digest is robust to the /fp:fast-vs-precise split by a 10x margin at ") + TEXT("these sample points. That is evidence, not proof -- it covers this grid, not ") + TEXT("every voxel in a world."), + NumSamples, NearIsoBand)); + } + else + { + AddWarning(FString::Printf( + TEXT("NearIso: %d of %d samples sit within %.1e of the isosurface -- close enough that ") + TEXT("a float-model difference could flip their SIGN, which is a solid-vs-air ") + TEXT("disagreement between a Windows host and a Linux client. This is the concrete ") + TEXT("mechanism behind AUDIT C9, and the count is roughly how many voxels per %d are at ") + TEXT("risk. It does not mean they DO differ -- run this on both platforms and compare ") + TEXT("the SHAPE digest to find out."), + NumNearIso, NumSamples, NearIsoBand, NumSamples)); + } + + TestEqual(TEXT("no sample produced NaN"), NumNaN, 0); + + // Un monde entièrement solide ou entièrement vide rendrait les empreintes vraies mais vides de + // sens. Garde-fou minimal contre un test qui se félicite de ne rien mesurer. + TestTrue(TEXT("the sampled world contains both solid and air (the digest is meaningful)"), + NumSolid > 0 && NumSolid < NumSamples); + + //========================================================================= + // LES ÉPINGLES — inertes tant que personne ne les a posées + //========================================================================= + if (PinnedShapeDigest != 0) + { + TestEqual(TEXT("SHAPE digest matches the pinned cross-platform reference"), + ShapeDigest, PinnedShapeDigest); + } + else + { + AddInfo(TEXT("SHAPE digest is not pinned yet. Pin it only once Windows and Linux agree -- ") + TEXT("pinning first would just carve the divergence into the test.")); + } + + if (PinnedFieldDigest != 0) + { + TestEqual(TEXT("FIELD digest matches the pinned cross-platform reference"), + FieldDigest, PinnedFieldDigest); + } + + return true; +} + +#endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Public/VoxelAtmosphereManager.h b/Source/VoxelForge/Public/VoxelAtmosphereManager.h index c862cc9..97914aa 100644 --- a/Source/VoxelForge/Public/VoxelAtmosphereManager.h +++ b/Source/VoxelForge/Public/VoxelAtmosphereManager.h @@ -12,6 +12,7 @@ #include "CoreMinimal.h" #include "VoxelAtmosphereManager.generated.h" +class AActor; // IWYU : pointeur / TWeakObjectPtr seulement / pointer-only class UVoxelStrateManager; class UVoxelStrateDefinition; class UVoxelBiomeDefinition; diff --git a/Source/VoxelForge/Public/VoxelBiomeDefinition.h b/Source/VoxelForge/Public/VoxelBiomeDefinition.h index 61e70dc..92ab3ff 100644 --- a/Source/VoxelForge/Public/VoxelBiomeDefinition.h +++ b/Source/VoxelForge/Public/VoxelBiomeDefinition.h @@ -22,6 +22,11 @@ #include "VoxelStrateTypes.h" // FStrateDecoration / FStrateAmbientActor #include "VoxelBiomeDefinition.generated.h" +// IWYU : utilisé en pointeur seulement ⇒ déclaration avant. Fournie gratuitement par le PCH +// partagé jusqu'ici ; `FPSemantics = Precise` nous en sort. / Pointer-only use, so a forward +// declaration is enough. The shared PCH used to provide this for free. +class UMaterialInterface; + /** * UVoxelBiomeDefinition — one biome's identity, placement, terrain modulation and content. */ diff --git a/Source/VoxelForge/Public/VoxelContentManager.h b/Source/VoxelForge/Public/VoxelContentManager.h index 1a260cc..a5498fb 100644 --- a/Source/VoxelForge/Public/VoxelContentManager.h +++ b/Source/VoxelForge/Public/VoxelContentManager.h @@ -45,9 +45,11 @@ #include "VoxelTypes.h" #include "VoxelStrateTypes.h" // FStrateDecoration (resolved per dominant biome) #include "VoxelBiomeTypes.h" // FBiomeContext (per-column biome resolve on the worker) +#include "Templates/SubclassOf.h" // IWYU : TSubclassOf (FRegionActorBucket & co) #include #include "VoxelContentManager.generated.h" +class AActor; // IWYU : pointeur / TWeakObjectPtr / TSubclassOf seulement class UVoxelStrateManager; class UVoxelStrateDefinition; class UVoxelGenerator; diff --git a/Source/VoxelForge/Public/VoxelDensityVolume.h b/Source/VoxelForge/Public/VoxelDensityVolume.h index 8d873df..6167c79 100644 --- a/Source/VoxelForge/Public/VoxelDensityVolume.h +++ b/Source/VoxelForge/Public/VoxelDensityVolume.h @@ -37,9 +37,17 @@ #include "CoreMinimal.h" #include "Containers/Queue.h" #include "VoxelTypes.h" +// ⚠️ IWYU, ET CELUI-CI EST PIÉGEUX : `ENABLE_DRAW_DEBUG` est utilisé en `#if` plus bas. Un macro +// NON DÉFINI vaut 0 dans un `#if` — donc sans cet include le bloc de debug disparaît EN SILENCE au +// lieu de provoquer une erreur de compilation. Il venait du PCH partagé ; `FPSemantics = Precise` +// (AUDIT §C9) nous en prive. Défini par DrawDebugHelpers.h (vérifié dans UE 5.7). +// An UNDEFINED macro evaluates to 0 in an #if, so without this include the debug block vanishes +// SILENTLY instead of failing the build. Defined by DrawDebugHelpers.h (verified in UE 5.7). +#include "DrawDebugHelpers.h" #include #include "VoxelDensityVolume.generated.h" +class AActor; // IWYU : pointeur / TWeakObjectPtr seulement / pointer-only class UVoxelGenerator; class UVoxelSettings; class UVolumeTexture; diff --git a/Source/VoxelForge/Public/VoxelSettings.h b/Source/VoxelForge/Public/VoxelSettings.h index 3221208..231ab16 100644 --- a/Source/VoxelForge/Public/VoxelSettings.h +++ b/Source/VoxelForge/Public/VoxelSettings.h @@ -11,6 +11,9 @@ #include "VoxelStrateDefinition.h" #include "VoxelSettings.generated.h" +// IWYU : pointeur seulement (VoxelMaterial). / Pointer-only use. +class UMaterialInterface; + UCLASS(BlueprintType) class UVoxelSettings : public UPrimaryDataAsset { diff --git a/Source/VoxelForge/Public/VoxelStrateDefinition.h b/Source/VoxelForge/Public/VoxelStrateDefinition.h index 7a01210..68fb512 100644 --- a/Source/VoxelForge/Public/VoxelStrateDefinition.h +++ b/Source/VoxelForge/Public/VoxelStrateDefinition.h @@ -18,9 +18,16 @@ #include "GameplayTagContainer.h" #include "VoxelStrateTypes.h" #include "VoxelBiomeTypes.h" +#include "Templates/SubclassOf.h" // IWYU : TSubclassOf (Atmosphere/Ceiling/FloorLayerActor) #include "VoxelStrateDefinition.generated.h" class UVoxelBiomeDefinition; +// IWYU : tous en pointeur ou en paramètre de TSubclassOf ⇒ déclarations avant suffisantes. +// Le PCH partagé les fournissait ; `FPSemantics = Precise` (AUDIT §C9) nous en prive. +// All pointer-only or TSubclassOf parameters, so forward declarations suffice. +class UMaterialInterface; +class USoundBase; +class AActor; /** * UVoxelStrateDefinition — The content bag for a strate type. diff --git a/Source/VoxelForge/Public/VoxelStrateTypes.h b/Source/VoxelForge/Public/VoxelStrateTypes.h index 1bbb7aa..c43ee8b 100644 --- a/Source/VoxelForge/Public/VoxelStrateTypes.h +++ b/Source/VoxelForge/Public/VoxelStrateTypes.h @@ -14,9 +14,11 @@ #include "CoreMinimal.h" #include "GameplayTagContainer.h" +#include "Templates/SubclassOf.h" // IWYU : TSubclassOf (FPlacementProfile & co) #include "VoxelStrateTypes.generated.h" class UVoxelBiomeDefinition; // FPlacementProfile::RequiredBiome (optional per-entry biome filter) +class AActor; // IWYU : paramètre de TSubclassOf seulement / TSubclassOf param only //============================================================================= // ENUMS diff --git a/Source/VoxelForge/VoxelForge.Build.cs b/Source/VoxelForge/VoxelForge.Build.cs index 6a89050..e1c802d 100644 --- a/Source/VoxelForge/VoxelForge.Build.cs +++ b/Source/VoxelForge/VoxelForge.Build.cs @@ -12,25 +12,51 @@ public class VoxelForge : ModuleRules PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs; // ============================================================================ - // ⚠️ DO NOT SET `FPSemantics` HERE — tried 2026-07-27, it does not build. + // FLOAT MODEL — pinned to Precise so Windows and Linux compute the SAME WORLD. // ============================================================================ - // 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. + // Jahni, 2026-07-27: the game must be playable on both Linux and Windows, either side + // hosting. The MP design replicates the SEED and has every peer regenerate the terrain, so + // a Windows host and a Linux client must agree on the density field. // - // 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. + // They did not, by construction. UBT resolves FPSemanticsMode.Default differently per + // toolchain (verified in UE 5.7 source, not assumed): + // VCToolChain.cs:1264 Default/Imprecise -> "/fp:fast" (Windows/MSVC) + // ClangToolChain.cs:712 Default/Precise -> "-ffp-contract=off" (Linux/Mac/Clang) + // So the same source was compiled under OPPOSITE float rules depending on who built it. // - // 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. + // Precise resolves to "/fp:precise" on MSVC and "-ffp-contract=off" on Clang — both + // IEEE-754 compliant with no FMA contraction, so the two toolchains agree BY CONSTRUCTION + // rather than by luck. That is the fix for AUDIT-2026-07.md C9. + // + // COST: /fp:precise forbids the reassociation and contraction /fp:fast allowed, on a + // noise-heavy hot path. Expect a measurable perf regression and check it against + // ARCHITECTURE 8.10 — determinism across platforms is worth paying for, but the price + // should be known, not assumed. + // + // VERIFY: run VoxelForge.Determinism.CrossPlatformDigest on both platforms and compare the + // SHAPE digest (sign of density = the world) and the FIELD digest (bit-for-bit). Pin the + // values in that test once they agree, and it guards this forever after. + FPSemantics = FPSemanticsMode.Precise; + + // ============================================================================ + // ⚠️ HISTORY — why this took a second attempt (kept: it explains the includes below) + // ============================================================================ + // Setting FPSemantics (or any 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 first attempt + // (2026-07-27) was therefore reverted: it failed with ~30 "undefined type" errors that + // were not FP-related at all — UMaterialInterface, USoundBase, TSubclassOf, + // ENABLE_DRAW_DEBUG — i.e. includes this plugin had always taken from the shared PCH for + // free. That is a latent IWYU debt, not an FP problem. + // + // ⚠️ SO IF YOU SEE "undefined type" ERRORS HERE, THEY ARE IWYU, NOT FLOAT SETTINGS. + // The fix is to add the missing include or forward declaration to the header that needs + // it — never to revert FPSemantics, which is now load-bearing for cross-platform play. + // Headers fixed on 2026-07-27: VoxelBiomeDefinition, VoxelSettings, VoxelStrateDefinition, + // VoxelStrateTypes, VoxelContentManager, VoxelAtmosphereManager, VoxelDensityVolume. + // Expect a residual tail: the shared PCH hid these for years and only a build enumerates + // them all. VoxelDensityVolume's was the nasty one — ENABLE_DRAW_DEBUG is used in an #if, + // and an undefined macro there is silently 0 rather than an error. // Modules we depend on: // - Core: Basic types (TArray, FString, etc.)