From 26b34765d54d2cc0c853b68223c53da1c718ea23 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Mon, 14 Sep 2026 04:29:11 +0200 Subject: [PATCH] perf(generation): exact worm block skip with a proven 3D Perlin bound -- measured NEUTRAL The sampler put the worm carve's first noise call (VoxelGenerator.cpp:3985, FusedWorm) at 19.4% of LOD0. The worm only carves where N1 + N2 < T with N2 >= 0, so where N1 >= T it contributes nothing, bit for bit. A block proven to have N1 >= T everywhere can skip both noise calls exactly. - The Lipschitz bound for this Perlin3D implementation: L = (15/4) * sqrt(3) * 1.25 = 8.118988160, in scaled-N1 units, with a 1e-3 float-rounding margin. Adversarial sampling found a worst ratio of 3.187 (0.39 L). The existing 2D floor-relief bound (8.5) is a component bound of a different field and was confirmed not to transfer. - A soundness test that can fail (VoxelForgeWormBlockSkipTest): 6,476 proved blocks, 355,945 exact lattice samples, 0 violations. A deliberately halved L proves a bad block; the full L rejects it. - voxel.WormBlockSkip, default 1. Field exact: export OBJ byte-identical (b3e5f4c3..9b377), 841/841 keyed tiles identical to NextHeadStaticTrace_20260913, capability 20,830 / 10,909. RESULT, stated plainly: no measurable speed gain. The skip rate is 4.07% of worm-eligible LOD0 samples and 0% at LOD1-4, because the bound is ~2.5x looser than the worst observed ratio at the block size used. Paired on-vs-off, two interleaved runs each: static worker +2.15%, generation p50 +1.8%; moving worker -1.42%, request p50 -2.5%. Within noise. The next round turns the default OFF. The proven bound and its test are kept for a tighter skip later (smaller or hierarchical blocks, or a per-cell bound). Run ledger (from Codex's report): the first build failed on an ambiguous test overload and was fixed; two timing runs are void because the orchestrator stopped the session so the owner could use the PC; the eight valid timing runs, the sampler run and the diagnostics runs completed. No new crash folders. PERF-SAMPLED.md now lists all eight earlier-round UECC crash records. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y --- ARCHITECTURE.md | 10 + CODEMAP.md | 8 +- PERF-SAMPLED.md | 172 +++++++++ .../Tests/VoxelForgeWormBlockSkipTest.cpp | 350 ++++++++++++++++++ .../Private/VoxelDensityProfile.cpp | 3 + Source/VoxelForge/Private/VoxelGenerator.cpp | 346 ++++++++++++++++- Source/VoxelForge/Private/VoxelWorld.cpp | 20 +- .../VoxelForge/Public/VoxelDensityProfile.h | 3 + Source/VoxelForge/Public/VoxelNoise.h | 99 +++++ 9 files changed, 987 insertions(+), 24 deletions(-) create mode 100644 Source/VoxelForge/Private/Tests/VoxelForgeWormBlockSkipTest.cpp diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index b951f07..a0dfc9b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -580,6 +580,16 @@ driven by `EditorBrush*` props. no loads AND no LOD mismatches outstanding. - **SDF cache** (`GetDensityWithParams`): search-BOX validity, not chunk-key — gradient ±1 sampling must not thrash the (expensive) rebuild. +- **Worm block skip** (`VF_TryGetWormBlockSkip`): inside the unchanged `WormNetworkRange` mask, + a worker-local direct-indexed cache covers the mesher's 4×4×4 lattice blocks (including the + normal halo). It samples N1 once at the transformed block centre and proves the entire block + clears the threshold with the actual 3D hash-gradient field bound + `L = (15/4) * sqrt(3) * VOXEL_NOISE_SCALE = 8.118988160`, plus a `1e-3` scaled-output + rounding margin. The radius is measured after the exact `WormFrequency`, `VerticalScale`, and + `WormHorizontalBias` transform, so anisotropic Z is included. The cache is thread-local and + keyed by generator owner, seed, params/layout fingerprint, tile origin, step, lattice size, and + worm parameters; malformed/non-lattice/failed proofs fall through. A proof only suppresses N1 + and N2 because N2 is nonnegative; it never changes `NetworkMask` or density arithmetic. - **Operator channel metadata** (`FVoxelOpStack`): `ChannelReads`, `ChannelWrites`, and `IsAdditive` are called only when an operator is added to a stack and their values are cached in the stack entry. `ValidateChannelOrder` is assembly/diagnostic-only; no declaration virtual may diff --git a/CODEMAP.md b/CODEMAP.md index c855151..dff9a56 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -341,7 +341,7 @@ This is **where terrain shape lives.** | `ApplyPassageCarving` (static) | 197 | Punches passages/elevator through the seal. | | `InitializeSettings` | 211 | Copies the effective seed, spine radius, and global XY world-bound settings from `UVoxelSettings` (the season owns all three when assigned); radius 0 preserves the unbounded field. | | **`GetDensityAt`** | 218 | **Entry point.** Picks strate + generator type, materialises a cooked-season recipe on the existing per-chunk refetch when present, dispatches, then adds disturbances/diff. Its `CP_*` state is keyed by `(DensityCacheOwnerId, ChunkCoord, LayoutVersion)`; workers copy immutable recipe/vector data and never read editor composer state. | -| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. | +| **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. The optional `voxel.WormBlockSkip` path uses `VF_TryGetWormBlockSkip` + a worker-local 4³ lattice-block cache to prove `|N1| >= WormThreshold` and remove both worm noise calls only; it never changes `NetworkMask` or the fallback field path. | | **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. | | `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. | | `ComputeSurfaceTerrainZ` / `GetSurfaceDensity` | — | SurfaceWorld heightfield → terrain Z, then density; biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/sheets/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in `SurfaceDensityFromColumn`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. The field-preserving LOD0 pass removes only needless temporaries/default construction; every output remains bit-identical. §8.14. | @@ -359,7 +359,11 @@ This is **where terrain shape lives.** > memoizes the strate index per (chunkZ, `GetLayoutVersion()`); `thread_local` per-cell lattice > bakes cover slab columns (`GetSlabDensity` step 4), maze open edges, vertical shafts + > cross-connectors, floating-island constants, and the disturbance chasms/bridges/ridges; worm -> tunnels short-circuit the 2nd Perlin when N1 ≥ WormThreshold (N2 ≥ 0 ⇒ can't carve). Room +> tunnels short-circuit the 2nd Perlin when N1 ≥ WormThreshold (N2 ≥ 0 ⇒ can't carve). With +> `voxel.WormBlockSkip=1`, a worker-local cache proves whole 4³ lattice blocks only when the +> actual 3D Perlin bound (`L = 8.118988160`, including `VOXEL_NOISE_SCALE`) plus a `1e-3` +> output-rounding margin clears the threshold; failed proofs use the original per-sample path. +> Room > shapes are pre-baked in `FCachedRoom` (§3.7). **T2.b:** per-voxel fractal call sites take their > octave count through `VoxelGenLOD::Eff(N)` (VoxelGenerator.h) — a `thread_local` bias set per > tile by the mesher drops tail octaves on coarse tiles (opt-in `LODOctaveDrop`, default 0 = off); diff --git a/PERF-SAMPLED.md b/PERF-SAMPLED.md index 6e6898e..3c52d11 100644 --- a/PERF-SAMPLED.md +++ b/PERF-SAMPLED.md @@ -928,3 +928,175 @@ Smaller unresolved groups map to `UnrealEditor-Engine.dll + RVA 0x500947` (gener This closes the actionable part of candidate 5: the previous plugin constructor was removed, while the remaining unknown is an engine-symbolisation limitation. No commit, push, or stash was performed. + +## Worm block skip round (2026-09-14) + +### Proof and gates + +The worm skip is enabled by default through `voxel.WormBlockSkip=1`. It is fail-closed and only +removes the two worm noise calls after the unchanged `WormNetworkRange` mask has passed. The +worker-local cache is keyed by generator owner, seed, params/layout fingerprint, tile origin, +step, lattice size, and worm parameters; it is never shared across tiles or workers. + +For this implementation's actual `GradDot` set, the unique gradients are +`(±1,±1,0)`, `(±1,0,±1)`, and `(0,±1,±1)`. The exact finite-support partial-derivative relaxation +with `Fade'(t)=30t²(1-t)²` has supremum `15/4`, attained at `(0.5,0.5,0.5)`. Therefore the +noise-space vector bound is + +`L = (15/4) * sqrt(3) * VOXEL_NOISE_SCALE = 8.118988160` (`VOXEL_NOISE_SCALE=1.25`). + +The existing 2D floor constant was proven differently and does not transfer: its comment uses +the loose component bound `[-1,1]`, four-edge interpolation difference bound, and +`max SmoothCurve' = 1.875`, giving `1 + 4*1.875 = 8.5` per partial. The worm derives its +bound from the actual 3D gradient support. The block radius is measured after the exact X/Y/Z +noise transform, including `WormFrequency`, `VerticalScale`, and `WormHorizontalBias`; `Abs` is +1-Lipschitz and the positive noise scale is included in `L`. A `1e-3` scaled-output margin is +subtracted before the threshold comparison. It is deliberately much larger than the accumulated +float rounding at the center/radius operations and is covered by the soundness test. + +The adversarial attack used dense corner/face sampling plus random pairs: 17,365,280 pairs, +worst scaled ratio `3.187330411`, or `0.392577295*L`. The fail-capable automation test also +audited 6,476 proved blocks over 355,945 exact lattice samples with zero violations. A deliberately +halved `L` proves the negative-control block while the exact point has `N1=0.011259466 < 0.015` +and the full `L` rejects it. + +Default-on export is byte-identical to the baseline: geometry hash +`B3E5F4C398DD0547C6C55F415872058246D4A292C2F0D636EE1CD0829DE9B377`, 82,273 vertices, +153,346 triangles. The static trace is 841/841 keyed tiles with zero field/empty/triangle +differences. Capability remains 20,830 / 10,909. + +### Clean game A/B + +Sampler off, diagnostics off, `voxel.OuterClassifierMode=0`, interleaved static and moving runs. +Static runs had 343 LOD0 ready samples; moving runs had 1,274. All valid runs had identical +applied geometry totals, zero validation calls, zero obsolete worker tasks, and verified the loaded +plugin module. The clean timings below used the staged runtime before the final counter-only +diagnostic-accounting rebuild (`81F52389A78543E41EEC8E3E40DEF8F2634A79EB83E2A933FF35EE42CD611E12`). +That patch is behind diagnostics, so it does not change this clean path. The final staged runtime +used by the last diagnostic run is `E72E140100B3D258F28E9DBA19CD95D379D98794DA49D24C708FDD01B5B5FE08`. + +| run | switch | request p50/p95 (s) | generation p50/p95 (s) | worker s | +|---|---|---:|---:|---:| +| WormSkipGameOffStaticA_20260914 | off | 0.087151 / 0.150116 | 0.063115 / 0.104426 | 33.308914 | +| WormSkipGameOnStaticA_Resume_20260914 | on | 0.092174 / 0.154458 | 0.065730 / 0.110507 | 34.980771 | +| WormSkipGameOffMovingA_20260914 | off | 0.066421 / 0.138179 | 0.059918 / 0.108181 | 99.193222 | +| WormSkipGameOnMovingA_20260914 | on | 0.064163 / 0.135519 | 0.059273 / 0.106991 | 97.624292 | +| WormSkipGameOffStaticB_20260914 | off | 0.090372 / 0.160169 | 0.064016 / 0.109362 | 34.116813 | +| WormSkipGameOnStaticB_Resume_20260914 | on | 0.091922 / 0.154718 | 0.063731 / 0.112871 | 33.892686 | +| WormSkipGameOffMovingB_Resume_20260914 | off | 0.066659 / 0.142168 | 0.061916 / 0.110043 | 100.995082 | +| WormSkipGameOnMovingB_Resume_20260914 | on | 0.065557 / 0.141592 | 0.060515 / 0.110629 | 99.716139 | + +Paired means, on relative to off: + +| mode | request p50/p95 | generation p50/p95 | worker s | on vs off worker | +|---|---:|---:|---:|---:| +| static | off 0.088762 / 0.155143; on 0.092048 / 0.154588 | off 0.063566 / 0.106894; on 0.064731 / 0.111689 | 33.712864 → 34.436729 | +2.1471% | +| moving | off 0.066540 / 0.140174; on 0.064860 / 0.138556 | off 0.060917 / 0.109112; on 0.059894 / 0.108810 | 100.094152 → 98.670216 | −1.4226% | + +The two-run result is mixed: moving improves modestly, while static is within ordinary run noise +and is slightly worse on worker sum. The switch remains default-on because the field proof is exact; +the timing result is not presented as a universal speed claim. + +### Skip rate + +The final diagnostics-enabled moving run used the final DLL and emitted 2,360 tile profiles. The +rate is `worm_block_skipped / worm_eligible`, not a fraction of all density samples: + +| LOD | worm-eligible samples | successful block proofs | skipped eligible samples | skip rate | +|---:|---:|---:|---:|---:| +| 0 | 12,381,885 | 9,541 | 503,696 | 4.068007% | +| 1 | 4,821,000 | 0 | 0 | 0.000000% | +| 2 | 188,063 | 0 | 0 | 0.000000% | +| 3 | 58,276 | 0 | 0 | 0.000000% | +| 4 | 25,253 | 0 | 0 | 0.000000% | + +### Sampler-on moving profile + +`WormSkipGameOnMovingSampler_20260914` retained 96,649 samples with zero drops and zero capture +failures: 61,869 LOD0 and 34,780 LOD1+. It ran for 24.972305 s; resolved-frame fraction was +52.9174%, unknown-frame fraction 47.0826%, wait-like fraction 0.1011%, and resolved-leaf +fraction 95.3388%. + +LOD0 exclusive top 20: + +| # | samples | % | leaf | +|---:|---:|---:|---| +| 1 | 12472 | 20.1587 | `GetDensityWithParams` — `VoxelGenerator.cpp:3128` | +| 2 | 3137 | 5.0704 | `GetDensityAt` — `VoxelGenerator.cpp:2994` | +| 3 | 2853 | 4.6114 | unknown | +| 4 | 2540 | 4.1054 | `Sqrt` | +| 5 | 2238 | 3.6173 | `VoxelSDF::SmoothMin` — `VoxelCaveMorphology.h:170` | +| 6 | 1736 | 2.8059 | `VF_ProjectNativePassageFloorUncached` — `VoxelStrateManager.cpp:561` | +| 7 | 1704 | 2.7542 | `FChunkSDFSpatialIndex::GetRange` — `VoxelCaveMorphology.cpp:1356` | +| 8 | 1434 | 2.3178 | `VF_ProjectTunnelSegmentXY` — `VoxelCaveMorphology.cpp:970` | +| 9 | 1244 | 2.0107 | `ProjectWalkableTunnelFloor` — `VoxelPassageGeometry.h:338` | +| 10 | 1229 | 1.9865 | `VF_EvaluateSweptTunnelChain` — `VoxelCaveMorphology.cpp:1128` | +| 11 | 1151 | 1.8604 | `EvaluateTunnelCoreWorld` lambda — `VoxelCaveMorphology.cpp:7635` | +| 12 | 1009 | 1.6309 | swept-chain lambda — `VoxelCaveMorphology.cpp:1074` | +| 13 | 938 | 1.5161 | `EvaluateSDFCached` lambda — `VoxelCaveMorphology.cpp:7526` | +| 14 | 819 | 1.3238 | `IsFiniteFast` — `VoxelTypes.h:30` | +| 15 | 769 | 1.2429 | `IsFinite` — `VoxelTypes.h:47` | +| 16 | 764 | 1.2349 | `EvaluateModifierSDF` — `VoxelStrateManager.cpp:2451` | +| 17 | 748 | 1.2090 | `VF_EvaluatePassageLandingSDF` — `VoxelCaveMorphology.cpp:5673` | +| 18 | 739 | 1.1945 | `GenerateMesh` — `VoxelMarchingCubesMesher.cpp:578` | +| 19 | 724 | 1.1702 | `FScopedTimer::End` — `VoxelDensityProfile.h:435` | +| 20 | 700 | 1.1314 | `VF_DistanceSquaredToAabb` — `VoxelCaveMorphology.cpp:74` | + +LOD0 inclusive top 20: + +| # | samples | % | inclusive function | +|---:|---:|---:|---| +| 1 | 61869 | 100.0000 | `Invoke` | +| 2 | 61869 | 100.0000 | `LowLevelTasks::FTask::Init` lambda | +| 3 | 61869 | 100.0000 | `LowLevelTasks::TTaskDelegate::Call` | +| 4 | 61869 | 100.0000 | `UE::Tasks::Private::FTaskBase::Init` lambda | +| 5 | 61869 | 100.0000 | `LowLevelTasks::TTaskDelegate::CallAndMove` | +| 6 | 61869 | 100.0000 | `UE::Tasks::Private::FTaskBase::TryExecuteTask` | +| 7 | 61869 | 100.0000 | `TExecutableTaskBase::ExecuteTask` | +| 8 | 61869 | 100.0000 | `AVoxelWorld::LoadTile` lambda — `VoxelWorld.cpp:3782` | +| 9 | 61869 | 100.0000 | `AVoxelWorld::GenerateTileResult` — `VoxelWorld.cpp:4142` | +| 10 | 61774 | 99.8464 | `GenerateMesh` — `VoxelMarchingCubesMesher.cpp:578` | +| 11 | 60310 | 97.4802 | `GetDensityAt` — `VoxelGenerator.cpp:2606` | +| 12 | 38073 | 61.5381 | `GetDensityWithParams` — `VoxelGenerator.cpp:3426` | +| 13 | 15666 | 25.3212 | `VF_ForEachSpatialCandidate` — `VoxelCaveMorphology.cpp:1570` | +| 14 | 9775 | 15.7995 | `EvaluateSDFCached` — `VoxelCaveMorphology.cpp:7484` | +| 15 | 7846 | 12.6816 | `VF_EvaluateSweptTunnel` — `VoxelCaveMorphology.cpp:1167` | +| 16 | 7460 | 12.0577 | `VF_EvaluateSweptTunnelChain` — `VoxelCaveMorphology.cpp:1128` | +| 17 | 7276 | 11.7603 | `ApplyPassageCarvingOnly` — `VoxelStrateManager.cpp:2596` | +| 18 | 7209 | 11.6520 | `EvaluateTunnelCoreWorld` — `VoxelCaveMorphology.cpp:7732` | +| 19 | 7128 | 11.5211 | `VF_ApplyPassageCarving` — `VoxelDensityPrimitives.h:75` | +| 20 | 7106 | 11.4856 | `EvaluateModifierSDF` — `VoxelStrateManager.cpp:2538` | + +### Run ledger + +Every worm-round launch is listed here, including invalidated launches and the compile retry: + +| launch | outcome | +|---|---| +| Initial staged build | Failed compiling `VoxelForgeWormBlockSkipTest.cpp` because of an ambiguous `FMath::IsNearlyEqual` overload; fixed by an explicit cast. No Unreal process was launched. | +| Rebuilt staged plugin | Succeeded; final build log reports `Result: Succeeded`. | +| `VoxelForge.Determinism.WormBlockSkipSoundness` | Passed: 6,476 proofs / 355,945 lattice samples / 0 violations; halved-L control fails as intended. | +| `WormSkipFieldOn_20260914` | Passed; export byte-identical and loaded module verified. | +| `WormSkipStaticTrace_20260914` | Data passed 841/841; harness intentionally stopped the process after `.done` (`ExitCode=-1`), not a crash. | +| `WormSkipCapability_20260914` | Passed; capability 20,830 / 10,909. | +| `WormSkipGameOffStaticA_20260914` | Completed; valid off A. | +| `WormSkipGameOnStaticA_20260914` | Launched before the stop; void by owner instruction and excluded, even though a complete artifact exists. | +| `WormSkipGameOffMovingA_20260914` | Completed; valid on/off interleave partner. | +| `WormSkipGameOnMovingA_20260914` | Completed; valid on/off interleave partner. | +| `WormSkipGameOffStaticB_20260914` | Completed; valid off B. | +| `WormSkipGameOnStaticB_20260914` | Interrupted at the stop; partial module/RSS artifacts only, void and excluded. | +| `WormSkipGameOnStaticA_Resume_20260914` | Completed; valid on A replacement. | +| `WormSkipGameOffMovingB_Resume_20260914` | Completed; valid off B replacement. | +| `WormSkipGameOnMovingB_Resume_20260914` | Completed; valid on B replacement. | +| `WormSkipGameOnStaticB_Resume_20260914` | Completed; valid on B replacement. | +| `WormSkipGameOnMovingSampler_20260914` | Completed; sampler-on moving profile, zero drops/failures. | +| `WormSkipDiagnosticsMoving_20260914` | Completed; superseded by the final counter-semantics diagnostic run, not used for reported skip rates. | +| `WormSkipDiagnosticsMovingProofs_20260914` | Completed on final DLL; per-LOD skip rates above, module verified. | + +The Saved crash inventory also contains earlier-round UECC artifacts, which are kept visible here: +three failed `Task4CurrentTracePIE` attempts on 2026-09-09 (plugin-manager/ShaderCore/DDC launch +failures), `GameMovingOn_20260913` (fatal shutdown), `MemoGameStaticOffA_20260913` (no writable +DDC graph; retried successfully), `TakeoverFinalMovingSamplerOnHost_20260913` (crash artifact; +`...Host2` retry succeeded), `Final3MovingSamplerOn_20260913` (fatal shutdown), and +`IsFiniteExact_20260913` (no writable DDC graph). No CrashReportClient crash occurred in the +worm-round launches above. diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeWormBlockSkipTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeWormBlockSkipTest.cpp new file mode 100644 index 0000000..64e85d4 --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeWormBlockSkipTest.cpp @@ -0,0 +1,350 @@ +// Soundness and empirical coverage for the worker-local worm N1 block proof. + +#if WITH_DEV_AUTOMATION_TESTS + +#include "Misc/AutomationTest.h" + +#include "VoxelCaveMorphology.h" +#include "VoxelNoise.h" + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeWormBlockSkipTest, + "VoxelForge.Determinism.WormBlockSkipSoundness", + EAutomationTestFlags_ApplicationContextMask | EAutomationTestFlags::EngineFilter) + +namespace +{ + constexpr float WormFrequency = 0.015f; + constexpr float WormHorizontalBias = 3.0f; + constexpr float WormThreshold = 0.06f; + constexpr float WormVerticalScale = 1.0f; + constexpr int32 TestCellsPerAxis = 32; + constexpr int32 TestTileCount = 192; + constexpr int32 TestRandomPairCount = 500000; + + FORCEINLINE uint32 NextTestBits(uint32& State) + { + State += 0x9E3779B9u; + uint32 Z = State; + Z = (Z ^ (Z >> 16)) * 0x7FEB352Du; + Z = (Z ^ (Z >> 15)) * 0x846CA68Bu; + return Z ^ (Z >> 16); + } + + FORCEINLINE float NextTestUnit(uint32& State) + { + return static_cast(NextTestBits(State)) / 4294967296.0f; + } + + FORCEINLINE float ScaledN1(const FVector3f& Position) + { + return FMath::Abs(VoxelNoise::Perlin3D(Position) * VOXEL_NOISE_SCALE); + } + + FORCEINLINE FVector3f MakeWormNoisePosition( + int32 WorldX, int32 WorldY, int32 WorldZ, uint32 Seed) + { + float EffectiveZ = static_cast(WorldZ); + if (WormVerticalScale != 1.0f && WormVerticalScale > 0.0f) + { + EffectiveZ /= WormVerticalScale; + } + const float WormZFreq = WormFrequency * WormHorizontalBias; + return FVector3f( + static_cast(WorldX) * WormFrequency + + VoxelHash::SeedOffset(Seed, 1.0f), + static_cast(WorldY) * WormFrequency + + VoxelHash::SeedOffset(Seed, 1.7f), + EffectiveZ * WormZFreq + VoxelHash::SeedOffset(Seed, 2.3f)); + } + + FORCEINLINE double FadePrime(double T) + { + return 30.0 * T * T * (1.0 - T) * (1.0 - T); + } + + FORCEINLINE double GradientSupport(double X, double Y, double Z) + { + const double AX = FMath::Abs(X); + const double AY = FMath::Abs(Y); + const double AZ = FMath::Abs(Z); + return FMath::Max(FMath::Max(AX + AY, AX + AZ), AY + AZ); + } + + // Supremum of the exact finite support relaxation used in the header derivation. This is + // intentionally independent of the hash: all eight corners may choose any member of the + // actual GradDot set, so this is the field-wide proof envelope rather than an average sample. + double SupportRelaxedXDerivative(double X, double Y, double Z) + { + const double D = FadePrime(X); + const double SY = Y * Y * Y * (Y * (Y * 6.0 - 15.0) + 10.0); + const double SZ = Z * Z * Z * (Z * (Z * 6.0 - 15.0) + 10.0); + double Result = 0.0; + for (int32 J = 0; J < 2; ++J) + { + for (int32 K = 0; K < 2; ++K) + { + const double Weight = (J != 0 ? SY : 1.0 - SY) + * (K != 0 ? SZ : 1.0 - SZ); + const double YLocal = Y - static_cast(J); + const double ZLocal = Z - static_cast(K); + Result += Weight * ( + GradientSupport(1.0 - X - D * X, -D * YLocal, -D * ZLocal) + + GradientSupport(X - D * (1.0 - X), D * YLocal, D * ZLocal)); + } + } + return Result; + } +} + +bool FVoxelForgeWormBlockSkipTest::RunTest(const FString& Parameters) +{ + (void)Parameters; + + const double FullL = VoxelNoise::WormN1NoiseSpaceLipschitzBound; + const double HalfL = FullL * 0.5; + TestTrue(TEXT("the production N1 Lipschitz bound is finite and positive"), + FMath::IsFinite(FullL) && FullL > 0.0); + TestTrue(TEXT("the actual 3D partial bound is 15/4"), + FMath::IsNearlyEqual(VoxelNoise::Perlin3DPartialAbsBound, 3.75, 1.0e-12)); + TestTrue(TEXT("VOXEL_NOISE_SCALE is included in the N1 bound"), + FMath::IsNearlyEqual( + FullL, + static_cast(VOXEL_NOISE_SCALE) + * VoxelNoise::Perlin3DPartialAbsBound + * static_cast(1.732050807568877293527446341505872L), + 1.0e-12)); + + // Check the finite support maximization densely, including corners and every cell face. The + // closed-form case split is recorded beside the production constant; this grid is a regression + // tripwire for changing GradDot/Fade/Lerp without updating that proof. + double MaxRelaxedSupport = 0.0; + FVector3f MaxRelaxedPoint = FVector3f::ZeroVector; + constexpr int32 SupportGrid = 128; + for (int32 IX = 0; IX <= SupportGrid; ++IX) + { + const double X = static_cast(IX) / SupportGrid; + for (int32 IY = 0; IY <= SupportGrid; ++IY) + { + const double Y = static_cast(IY) / SupportGrid; + for (int32 IZ = 0; IZ <= SupportGrid; ++IZ) + { + const double Z = static_cast(IZ) / SupportGrid; + const double Value = SupportRelaxedXDerivative(X, Y, Z); + if (Value > MaxRelaxedSupport) + { + MaxRelaxedSupport = Value; + MaxRelaxedPoint = FVector3f( + static_cast(X), static_cast(Y), static_cast(Z)); + } + } + } + } + AddInfo(FString::Printf( + TEXT("3D support relaxation max=%.12f at (%.6f,%.6f,%.6f), bound=%.12f; " + "the exact case split gives 15/4 at (0.5,0.5,0.5)."), + MaxRelaxedSupport, MaxRelaxedPoint.X, MaxRelaxedPoint.Y, MaxRelaxedPoint.Z, + VoxelNoise::Perlin3DPartialAbsBound)); + TestTrue(TEXT("the dense support relaxation stays below the proven partial bound"), + MaxRelaxedSupport <= VoxelNoise::Perlin3DPartialAbsBound + 1.0e-10); + TestTrue(TEXT("the support relaxation reaches the stated supremum"), + MaxRelaxedSupport >= VoxelNoise::Perlin3DPartialAbsBound - 1.0e-10); + + // Empirical attack: samples straddle lattice faces/corners at several scales, then a large + // deterministic random-pair population probes ordinary cells and arbitrary directions. + double MaxEmpiricalRatio = 0.0; + FVector3f WorstA = FVector3f::ZeroVector; + FVector3f WorstB = FVector3f::ZeroVector; + uint64 PairCount = 0; + auto ObservePair = [&](const FVector3f& A, const FVector3f& B) + { + const double DX = static_cast(A.X) - static_cast(B.X); + const double DY = static_cast(A.Y) - static_cast(B.Y); + const double DZ = static_cast(A.Z) - static_cast(B.Z); + const double Distance = FMath::Sqrt(DX * DX + DY * DY + DZ * DZ); + if (!(Distance > 0.0)) + { + return; + } + ++PairCount; + const double Ratio = FMath::Abs( + static_cast(ScaledN1(A)) - static_cast(ScaledN1(B))) / Distance; + if (Ratio > MaxEmpiricalRatio) + { + MaxEmpiricalRatio = Ratio; + WorstA = A; + WorstB = B; + } + }; + + constexpr float FaceEpsilons[] = { 1.0e-5f, 1.0e-4f, 1.0e-3f, 1.0e-2f, 5.0e-2f }; + constexpr float FaceCoordinates[] = { + 1.0e-3f, 1.0e-2f, 0.1f, 0.25f, 0.5f, 0.75f, 0.9f, 0.99f, 0.999f }; + for (int32 X = -12; X < 12; ++X) + { + for (int32 Y = -12; Y < 12; ++Y) + { + for (int32 Z = -12; Z < 12; ++Z) + { + for (const float Epsilon : FaceEpsilons) + { + for (const float V : FaceCoordinates) + { + for (const float W : FaceCoordinates) + { + ObservePair( + FVector3f(X + 1.0f - Epsilon, Y + V, Z + W), + FVector3f(X + 1.0f + Epsilon, Y + V, Z + W)); + ObservePair( + FVector3f(X + V, Y + 1.0f - Epsilon, Z + W), + FVector3f(X + V, Y + 1.0f + Epsilon, Z + W)); + ObservePair( + FVector3f(X + V, Y + W, Z + 1.0f - Epsilon), + FVector3f(X + V, Y + W, Z + 1.0f + Epsilon)); + } + } + ObservePair( + FVector3f(X + 1.0f - Epsilon, Y + 1.0f - Epsilon, Z + 1.0f - Epsilon), + FVector3f(X + 1.0f + Epsilon, Y + 1.0f + Epsilon, Z + 1.0f + Epsilon)); + } + } + } + } + + uint32 RandomState = 0xA5B3571Du; + for (int32 Index = 0; Index < TestRandomPairCount; ++Index) + { + const FVector3f A( + NextTestUnit(RandomState) * 128.0f - 64.0f, + NextTestUnit(RandomState) * 128.0f - 64.0f, + NextTestUnit(RandomState) * 128.0f - 64.0f); + const FVector3f B( + A.X + NextTestUnit(RandomState) * 0.5f - 0.25f, + A.Y + NextTestUnit(RandomState) * 0.5f - 0.25f, + A.Z + NextTestUnit(RandomState) * 0.5f - 0.25f); + ObservePair(A, B); + } + AddInfo(FString::Printf( + TEXT("Empirical scaled-N1 Lipschitz attack: pairs=%llu worst=%.9f, worst/L=%.9f, " + "L=%.9f; worst A=(%.6f,%.6f,%.6f) B=(%.6f,%.6f,%.6f)."), + static_cast(PairCount), MaxEmpiricalRatio, + MaxEmpiricalRatio / FullL, FullL, + WorstA.X, WorstA.Y, WorstA.Z, WorstB.X, WorstB.Y, WorstB.Z)); + TestTrue(TEXT("empirical near-face/corner/random ratios stay below L"), + MaxEmpiricalRatio <= FullL + 0.01); + + // Brute-force the exact mesher lattice for a large deterministic set of tile contexts. Every + // block admitted by the production proof is checked at every lattice point it covers. + uint64 BlocksScanned = 0; + uint64 ProductionProofs = 0; + uint64 ProductionLatticeSamples = 0; + uint64 ProductionViolations = 0; + for (int32 TileIndex = 0; TileIndex < TestTileCount; ++TileIndex) + { + const FIntVector TileOrigin( + (TileIndex % 17 - 8) * 64, + ((TileIndex / 17) % 13 - 6) * 64, + ((TileIndex / 221) % 9 - 4) * 64); + const uint32 Seed = 1337u + static_cast(TileIndex) * 0x9E3779B9u; + + for (int32 BlockZ = 0; BlockZ < 9; ++BlockZ) + { + for (int32 BlockY = 0; BlockY < 9; ++BlockY) + { + for (int32 BlockX = 0; BlockX < 9; ++BlockX) + { + ++BlocksScanned; + const int32 MinX = BlockX * VoxelNoise::WormBlockSampleSide - 1; + const int32 MinY = BlockY * VoxelNoise::WormBlockSampleSide - 1; + const int32 MinZ = BlockZ * VoxelNoise::WormBlockSampleSide - 1; + const int32 MaxX = FMath::Min( + TestCellsPerAxis + 1, + MinX + VoxelNoise::WormBlockSampleSide - 1); + const int32 MaxY = FMath::Min( + TestCellsPerAxis + 1, + MinY + VoxelNoise::WormBlockSampleSide - 1); + const int32 MaxZ = FMath::Min( + TestCellsPerAxis + 1, + MinZ + VoxelNoise::WormBlockSampleSide - 1); + const FVector3f Q0 = MakeWormNoisePosition( + TileOrigin.X + MinX, TileOrigin.Y + MinY, TileOrigin.Z + MinZ, Seed); + const FVector3f Q1 = MakeWormNoisePosition( + TileOrigin.X + MaxX, TileOrigin.Y + MaxY, TileOrigin.Z + MaxZ, Seed); + const FVector3f QMin( + FMath::Min(Q0.X, Q1.X), FMath::Min(Q0.Y, Q1.Y), FMath::Min(Q0.Z, Q1.Z)); + const FVector3f QMax( + FMath::Max(Q0.X, Q1.X), FMath::Max(Q0.Y, Q1.Y), FMath::Max(Q0.Z, Q1.Z)); + const FVector3f QCenter( + static_cast((static_cast(QMin.X) + QMax.X) * 0.5), + static_cast((static_cast(QMin.Y) + QMax.Y) * 0.5), + static_cast((static_cast(QMin.Z) + QMax.Z) * 0.5)); + + if (!VoxelNoise::ProvesScaledAbsPerlin3DAbove( + QCenter, QMin, QMax, WormThreshold, FullL)) + { + continue; + } + ++ProductionProofs; + for (int32 Z = MinZ; Z <= MaxZ; ++Z) + { + for (int32 Y = MinY; Y <= MaxY; ++Y) + { + for (int32 X = MinX; X <= MaxX; ++X) + { + ++ProductionLatticeSamples; + const float ExactN1 = ScaledN1(MakeWormNoisePosition( + TileOrigin.X + X, TileOrigin.Y + Y, TileOrigin.Z + Z, Seed)); + if (ExactN1 < WormThreshold) + { + ++ProductionViolations; + } + } + } + } + } + } + } + } + AddInfo(FString::Printf( + TEXT("Production block audit: tiles=%d blocks=%llu proofs=%llu lattice_samples=%llu " + "violations=%llu, side=%d, threshold=%.6f."), + TestTileCount, static_cast(BlocksScanned), + static_cast(ProductionProofs), + static_cast(ProductionLatticeSamples), + static_cast(ProductionViolations), + VoxelNoise::WormBlockSampleSide, WormThreshold)); + TestTrue(TEXT("the production-bound block audit found at least one proof"), + ProductionProofs > 0); + TestEqual(TEXT("every production-bound proof is sound on its exact lattice"), + ProductionViolations, static_cast(0)); + + // Fail-capable negative control. This pair is from a real low-coordinate hash cell. Its exact + // scaled-N1 slope is above L/2; the threshold is selected between the two exact values, so a + // deliberately halved bound admits a proof that the second lattice point violates. + const FVector3f WitnessCenter(1510.5478515625f, 2191.43701171875f, 7279.404296875f); + const FVector3f WitnessPoint(1510.54931640625f, 2191.437255859375f, 7279.50390625f); + const FVector3f WitnessMin( + 2.0f * WitnessCenter.X - WitnessPoint.X, + 2.0f * WitnessCenter.Y - WitnessPoint.Y, + 2.0f * WitnessCenter.Z - WitnessPoint.Z); + const FVector3f WitnessMax = WitnessPoint; + constexpr float WitnessThreshold = 0.015f; + const float WitnessCenterN1 = ScaledN1(WitnessCenter); + const float WitnessPointN1 = ScaledN1(WitnessPoint); + const bool bHalfProof = VoxelNoise::ProvesScaledAbsPerlin3DAbove( + WitnessCenter, WitnessMin, WitnessMax, WitnessThreshold, HalfL); + const bool bFullProof = VoxelNoise::ProvesScaledAbsPerlin3DAbove( + WitnessCenter, WitnessMin, WitnessMax, WitnessThreshold, FullL); + AddInfo(FString::Printf( + TEXT("Halved-L negative control: center_N1=%.9f point_N1=%.9f threshold=%.6f " + "half_proof=%d full_proof=%d."), + WitnessCenterN1, WitnessPointN1, WitnessThreshold, + bHalfProof ? 1 : 0, bFullProof ? 1 : 0)); + TestTrue(TEXT("the deliberately halved L admits the negative-control block"), bHalfProof); + TestFalse(TEXT("the full L rejects the negative-control block"), bFullProof); + TestTrue(TEXT("the negative-control lattice point really violates N1 >= threshold"), + WitnessPointN1 < WitnessThreshold); + return true; +} + +#endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Private/VoxelDensityProfile.cpp b/Source/VoxelForge/Private/VoxelDensityProfile.cpp index c93217b..f523e2f 100644 --- a/Source/VoxelForge/Private/VoxelDensityProfile.cpp +++ b/Source/VoxelForge/Private/VoxelDensityProfile.cpp @@ -688,6 +688,9 @@ namespace VoxelDensityProfile case ECounter::FusedTunnelSamples: return TEXT("FusedTunnelSamples"); case ECounter::FusedTunnelDetailSamples: return TEXT("FusedTunnelDetailSamples"); case ECounter::FusedTunnelDetailSkipped: return TEXT("FusedTunnelDetailSkipped"); + case ECounter::WormEligibleSamples: return TEXT("WormEligibleSamples"); + case ECounter::WormBlockProofs: return TEXT("WormBlockProofs"); + case ECounter::WormBlockSkippedSamples: return TEXT("WormBlockSkippedSamples"); case ECounter::OpBlockBuilds: return TEXT("OpBlockBuilds"); case ECounter::OpBlockSamples: return TEXT("OpBlockSamples"); case ECounter::OpBlockOperators: return TEXT("OpBlockOperators"); diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index a7ff304..8ddfb2e 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -85,6 +85,32 @@ namespace } } + // A proved N1-above-threshold block skips both worm noise calls. The switch is parsed once + // on the same scalar entry point as the other generation A/B switches; 0 is the exact legacy + // path, and 1 is the default after the proof/soundness test passes. + int32 GVoxelForgeWormBlockSkip = 1; + FAutoConsoleVariableRef CVarVoxelForgeWormBlockSkip( + TEXT("voxel.WormBlockSkip"), + GVoxelForgeWormBlockSkip, + TEXT("Skip worm N1/N2 when a worker-local noise-space block proof says N1 >= threshold.")); + bool GVoxelForgeWormBlockSkipSwitchParsed = false; + + void VF_ParseWormBlockSkipSwitch() + { + if (GVoxelForgeWormBlockSkipSwitchParsed) + { + return; + } + GVoxelForgeWormBlockSkipSwitchParsed = true; + int32 CommandLineValue = GVoxelForgeWormBlockSkip; + if (FParse::Value( + FCommandLine::Get(), TEXT("voxel.WormBlockSkip="), CommandLineValue)) + { + GVoxelForgeWormBlockSkip = CommandLineValue; + } + GVoxelForgeWormBlockSkip = GVoxelForgeWormBlockSkip != 0 ? 1 : 0; + } + // A tile-sized cache window is the production default on the coarse paths where it wins. // Keep the switches in the generator module so native and op-stack sources make the same A/B // decision, including when a headless harness applies command-line CVars after module startup. @@ -309,6 +335,257 @@ struct FSurfaceColumnCache // donc un ClassifyTile qui rend Mixed laisse ses colonnes chaudes pour le GenerateMesh qui suit. static thread_local FSurfaceColumnCache GSurfColCache; +//============================================================================= +// WORM N1 BLOCK PROOF (worker-local, scalar GetDensityAt hand-off) +//============================================================================= +// GenerateMesh installs TileOriginVoxels/SampleStep/TileCellsPerAxis around every density-grid +// fill. Keep the verdicts here, rather than in the mesher, because the scalar GetDensityAt path +// owns the actual worm call and is also used by standalone/classifier queries. The cache is +// thread_local, direct-indexed by the block coordinate, and invalidated by every field identity +// that can affect N1. A miss or an invalid context always returns false and therefore executes +// the old N1/N2 code. +namespace +{ + constexpr int32 VF_WormBlockCacheAxis = + (CHUNK_SIZE + 3 + VoxelNoise::WormBlockSampleSide - 1) + / VoxelNoise::WormBlockSampleSide; + constexpr int32 VF_WormBlockCacheEntryCount = + VF_WormBlockCacheAxis * VF_WormBlockCacheAxis * VF_WormBlockCacheAxis; + + static_assert(VF_WormBlockCacheAxis == 9, + "The fixed worm block table must cover the 32-cell mesher lattice plus its halo."); + + FORCEINLINE uint32 VF_FloatBits(float Value) + { + uint32 Bits = 0; + FMemory::Memcpy(&Bits, &Value, sizeof(Bits)); + return Bits; + } + + struct FWormBlockSkipEntry + { + bool bValid = false; + bool bSkip = false; + }; + + struct FWormBlockSkipCache + { + bool bContextValid = false; + uint64 OwnerId = 0; + uint32 Seed = 0; + uint32 ParamsFingerprint = 0xFFFFFFFFu; + uint32 LayoutVersion = 0xFFFFFFFFu; + FIntVector TileOrigin = FIntVector::ZeroValue; + int32 Step = 0; + int32 Cells = 0; + uint32 WormFrequencyBits = 0; + uint32 WormHorizontalBiasBits = 0; + uint32 WormThresholdBits = 0; + uint32 VerticalScaleBits = 0; + FWormBlockSkipEntry Entries[VF_WormBlockCacheEntryCount]; + + void SetContext( + uint64 InOwnerId, uint32 InSeed, uint32 InParamsFingerprint, + uint32 InLayoutVersion, const FIntVector& InTileOrigin, + int32 InStep, int32 InCells, const FStrateGenerationParams& Params) + { + const uint32 InWormFrequencyBits = VF_FloatBits(Params.WormFrequency); + const uint32 InWormHorizontalBiasBits = VF_FloatBits(Params.WormHorizontalBias); + const uint32 InWormThresholdBits = VF_FloatBits(Params.WormThreshold); + const uint32 InVerticalScaleBits = VF_FloatBits(Params.VerticalScale); + const bool bChanged = + !bContextValid + || OwnerId != InOwnerId + || Seed != InSeed + || ParamsFingerprint != InParamsFingerprint + || LayoutVersion != InLayoutVersion + || TileOrigin != InTileOrigin + || Step != InStep + || Cells != InCells + || WormFrequencyBits != InWormFrequencyBits + || WormHorizontalBiasBits != InWormHorizontalBiasBits + || WormThresholdBits != InWormThresholdBits + || VerticalScaleBits != InVerticalScaleBits; + if (!bChanged) + { + return; + } + + bContextValid = true; + OwnerId = InOwnerId; + Seed = InSeed; + ParamsFingerprint = InParamsFingerprint; + LayoutVersion = InLayoutVersion; + TileOrigin = InTileOrigin; + Step = InStep; + Cells = InCells; + WormFrequencyBits = InWormFrequencyBits; + WormHorizontalBiasBits = InWormHorizontalBiasBits; + WormThresholdBits = InWormThresholdBits; + VerticalScaleBits = InVerticalScaleBits; + FMemory::Memzero(Entries, sizeof(Entries)); + } + }; + + static thread_local FWormBlockSkipCache GWormBlockSkipCache; + + FORCEINLINE FVector3f VF_WormN1NoisePosition( + float WorldX, float WorldY, float WorldZ, + const FStrateGenerationParams& Params, uint32 SeedU) + { + float EffectiveZ = WorldZ; + if (Params.VerticalScale != 1.0f && Params.VerticalScale > 0.0f) + { + EffectiveZ = WorldZ / Params.VerticalScale; + } + const float WormZFreq = Params.WormFrequency * Params.WormHorizontalBias; + return FVector3f( + WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f), + WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f), + EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f)); + } + + FORCEINLINE bool VF_TryGetWormBlockSkip( + uint64 OwnerId, uint32 SeedU, uint32 ParamsFingerprint, uint32 LayoutVersion, + float WorldX, float WorldY, float WorldZ, + const FStrateGenerationParams& Params, bool& bOutProofComputed) + { + bOutProofComputed = false; + + // Point queries and malformed authored parameters stay on the exact original path. In + // particular, VerticalScale <= 0 has legacy "no division" semantics; declining the proof + // here avoids making a new promise for malformed data. + if (!FMath::IsFinite(WorldX) || !FMath::IsFinite(WorldY) || !FMath::IsFinite(WorldZ) + || WorldX != FMath::FloorToFloat(WorldX) + || WorldY != FMath::FloorToFloat(WorldY) + || WorldZ != FMath::FloorToFloat(WorldZ) + || !FMath::IsFinite(Params.WormFrequency) + || !FMath::IsFinite(Params.WormHorizontalBias) + || !FMath::IsFinite(Params.WormThreshold) + || !FMath::IsFinite(Params.VerticalScale) + || !(Params.WormThreshold > 0.0f) + || !(Params.VerticalScale > 0.0f) + || !FMath::IsFinite(Params.WormFrequency * Params.WormHorizontalBias)) + { + return false; + } + + const int32 Step = VoxelGenLOD::SampleStep; + const int32 Cells = VoxelGenLOD::TileCellsPerAxis; + if (Step <= 0 || Cells <= 0 || Cells > CHUNK_SIZE) + { + return false; + } + + // FMath::RoundToInt is only reached after the finite/integer check and a range guard. The + // tile context itself is int32, so coordinates outside this range cannot belong to it. + constexpr float SafeInt32Min = -2147483008.0f; + constexpr float SafeInt32Max = 2147483008.0f; + if (WorldX < SafeInt32Min || WorldX > SafeInt32Max + || WorldY < SafeInt32Min || WorldY > SafeInt32Max + || WorldZ < SafeInt32Min || WorldZ > SafeInt32Max) + { + return false; + } + const int32 IX = FMath::RoundToInt(WorldX); + const int32 IY = FMath::RoundToInt(WorldY); + const int32 IZ = FMath::RoundToInt(WorldZ); + const FIntVector TileOrigin = VoxelGenLOD::TileOriginVoxels; + const int64 DX = static_cast(IX) - static_cast(TileOrigin.X); + const int64 DY = static_cast(IY) - static_cast(TileOrigin.Y); + const int64 DZ = static_cast(IZ) - static_cast(TileOrigin.Z); + if (DX % Step != 0 || DY % Step != 0 || DZ % Step != 0) + { + return false; + } + + const int32 LocalX = static_cast(DX / Step); + const int32 LocalY = static_cast(DY / Step); + const int32 LocalZ = static_cast(DZ / Step); + const int32 MinLocalLattice = -1; + const int32 MaxLocalLattice = Cells + 1; + if (LocalX < MinLocalLattice || LocalX > MaxLocalLattice + || LocalY < MinLocalLattice || LocalY > MaxLocalLattice + || LocalZ < MinLocalLattice || LocalZ > MaxLocalLattice) + { + return false; + } + + const int32 BlockX = (LocalX + 1) / VoxelNoise::WormBlockSampleSide; + const int32 BlockY = (LocalY + 1) / VoxelNoise::WormBlockSampleSide; + const int32 BlockZ = (LocalZ + 1) / VoxelNoise::WormBlockSampleSide; + if (BlockX < 0 || BlockX >= VF_WormBlockCacheAxis + || BlockY < 0 || BlockY >= VF_WormBlockCacheAxis + || BlockZ < 0 || BlockZ >= VF_WormBlockCacheAxis) + { + return false; + } + + GWormBlockSkipCache.SetContext( + OwnerId, SeedU, ParamsFingerprint, LayoutVersion, TileOrigin, Step, Cells, Params); + FWormBlockSkipEntry& Entry = GWormBlockSkipCache.Entries[ + (BlockZ * VF_WormBlockCacheAxis + BlockY) * VF_WormBlockCacheAxis + BlockX]; + if (Entry.bValid) + { + return Entry.bSkip; + } + + const int32 BlockMinX = BlockX * VoxelNoise::WormBlockSampleSide - 1; + const int32 BlockMinY = BlockY * VoxelNoise::WormBlockSampleSide - 1; + const int32 BlockMinZ = BlockZ * VoxelNoise::WormBlockSampleSide - 1; + const int32 BlockMaxX = FMath::Min(MaxLocalLattice, + BlockMinX + VoxelNoise::WormBlockSampleSide - 1); + const int32 BlockMaxY = FMath::Min(MaxLocalLattice, + BlockMinY + VoxelNoise::WormBlockSampleSide - 1); + const int32 BlockMaxZ = FMath::Min(MaxLocalLattice, + BlockMinZ + VoxelNoise::WormBlockSampleSide - 1); + + const auto WorldAt = [Step, &TileOrigin](int32 Local, int32 Origin) -> int64 + { + return static_cast(Origin) + static_cast(Local) * Step; + }; + const int64 MinWorldX64 = WorldAt(BlockMinX, TileOrigin.X); + const int64 MinWorldY64 = WorldAt(BlockMinY, TileOrigin.Y); + const int64 MinWorldZ64 = WorldAt(BlockMinZ, TileOrigin.Z); + const int64 MaxWorldX64 = WorldAt(BlockMaxX, TileOrigin.X); + const int64 MaxWorldY64 = WorldAt(BlockMaxY, TileOrigin.Y); + const int64 MaxWorldZ64 = WorldAt(BlockMaxZ, TileOrigin.Z); + if (MinWorldX64 < MIN_int32 || MinWorldX64 > MAX_int32 + || MinWorldY64 < MIN_int32 || MinWorldY64 > MAX_int32 + || MinWorldZ64 < MIN_int32 || MinWorldZ64 > MAX_int32 + || MaxWorldX64 < MIN_int32 || MaxWorldX64 > MAX_int32 + || MaxWorldY64 < MIN_int32 || MaxWorldY64 > MAX_int32 + || MaxWorldZ64 < MIN_int32 || MaxWorldZ64 > MAX_int32) + { + return false; + } + + const FVector3f Q0 = VF_WormN1NoisePosition( + static_cast(static_cast(MinWorldX64)), + static_cast(static_cast(MinWorldY64)), + static_cast(static_cast(MinWorldZ64)), Params, SeedU); + const FVector3f Q1 = VF_WormN1NoisePosition( + static_cast(static_cast(MaxWorldX64)), + static_cast(static_cast(MaxWorldY64)), + static_cast(static_cast(MaxWorldZ64)), Params, SeedU); + const FVector3f QMin( + FMath::Min(Q0.X, Q1.X), FMath::Min(Q0.Y, Q1.Y), FMath::Min(Q0.Z, Q1.Z)); + const FVector3f QMax( + FMath::Max(Q0.X, Q1.X), FMath::Max(Q0.Y, Q1.Y), FMath::Max(Q0.Z, Q1.Z)); + const FVector3f QCenter( + static_cast((static_cast(QMin.X) + static_cast(QMax.X)) * 0.5), + static_cast((static_cast(QMin.Y) + static_cast(QMax.Y)) * 0.5), + static_cast((static_cast(QMin.Z) + static_cast(QMax.Z)) * 0.5)); + + Entry.bSkip = VoxelNoise::ProvesScaledAbsPerlin3DAbove( + QCenter, QMin, QMax, Params.WormThreshold, + VoxelNoise::WormN1NoiseSpaceLipschitzBound); + Entry.bValid = true; + bOutProofComputed = true; + return Entry.bSkip; + } +} + //============================================================================= // FRACTAL NOISE (fBm — fractional Brownian motion) //============================================================================= @@ -1812,6 +2089,7 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_GetDensityAt); VF_ParseFastIsFiniteSwitch(); VF_ParseFusedEvaluatorSwitch(); + VF_ParseWormBlockSkipSwitch(); VoxelDensityProfile::FScopedTimer DensityProfileTimer( VoxelDensityProfile::EBucket::GetDensityAt); VoxelDensityProfile::FScopedTimer DensityPrologueTimer( @@ -3980,30 +4258,58 @@ float UVoxelGenerator::GetDensityWithParams(float WorldX, float WorldY, float Wo if (NetworkMask > 0.0f) { - float WormZFreq = Params.WormFrequency * Params.WormHorizontalBias; - - float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f( - WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f), - WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f), - EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) - )) * VOXEL_NOISE_SCALE); - - // N2 >= 0, so if N1 alone already clears the threshold the sum can't carve — - // skip the second Perlin entirely (most voxels; bit-identical output). - if (N1 < Params.WormThreshold) + const bool bCollectWormDiagnostics = + bCollectFusedDiagnostics && VoxelDensityProfile::AreCountersEnabled(); + if (bCollectWormDiagnostics) { - float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f( - WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f) + 137.0f, - WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f) + 259.0f, - EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) + 431.0f + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::WormEligibleSamples); + } + + bool bWormBlockProofComputed = false; + const bool bSkipWormBlock = GVoxelForgeWormBlockSkip != 0 + && VF_TryGetWormBlockSkip( + DensityCacheOwnerId, SeedU, ParamsFingerprint, LayoutVersion, + WorldX, WorldY, WorldZ, Params, bWormBlockProofComputed); + if (bWormBlockProofComputed && bSkipWormBlock && bCollectWormDiagnostics) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::WormBlockProofs); + } + if (bSkipWormBlock) + { + if (bCollectWormDiagnostics) + { + VoxelDensityProfile::AddCounter( + VoxelDensityProfile::ECounter::WormBlockSkippedSamples); + } + } + else + { + const float WormZFreq = Params.WormFrequency * Params.WormHorizontalBias; + const float N1 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f( + WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f), + WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f), + EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) )) * VOXEL_NOISE_SCALE); - float WormValue = N1 + N2; - - if (WormValue < Params.WormThreshold) + // N2 >= 0, so if N1 alone already clears the threshold the sum can't carve — + // skip the second Perlin entirely (most voxels; bit-identical output). + if (N1 < Params.WormThreshold) { - float t = 1.0f - (WormValue / Params.WormThreshold); - Density -= t * Params.WormStrength * NetworkMask; + const float N2 = FMath::Abs(VoxelNoise::Perlin3D(FVector3f( + WorldX * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.0f) + 137.0f, + WorldY * Params.WormFrequency + VoxelHash::SeedOffset(SeedU, 1.7f) + 259.0f, + EffectiveZ * WormZFreq + VoxelHash::SeedOffset(SeedU, 2.3f) + 431.0f + )) * VOXEL_NOISE_SCALE); + + const float WormValue = N1 + N2; + + if (WormValue < Params.WormThreshold) + { + const float t = 1.0f - (WormValue / Params.WormThreshold); + Density -= t * Params.WormStrength * NetworkMask; + } } } } diff --git a/Source/VoxelForge/Private/VoxelWorld.cpp b/Source/VoxelForge/Private/VoxelWorld.cpp index 420bb9a..94134e4 100644 --- a/Source/VoxelForge/Private/VoxelWorld.cpp +++ b/Source/VoxelForge/Private/VoxelWorld.cpp @@ -3860,6 +3860,16 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector CycleDelta(VoxelDensityProfile::EBucket::RoomGraphBuild)); const double EvaluationSeconds = FMath::Max( 0.0, TileSeconds - CacheBuildSeconds); + const uint64 WormEligibleSamples = CounterDelta( + VoxelDensityProfile::ECounter::WormEligibleSamples); + const uint64 WormBlockProofs = CounterDelta( + VoxelDensityProfile::ECounter::WormBlockProofs); + const uint64 WormBlockSkippedSamples = CounterDelta( + VoxelDensityProfile::ECounter::WormBlockSkippedSamples); + const double WormBlockSkipRate = WormEligibleSamples > 0 + ? static_cast(WormBlockSkippedSamples) + / static_cast(WormEligibleSamples) + : 0.0; UE_LOG(LogTemp, Display, TEXT("[VoxelForgeTileProfile] tile=(%d,%d,%d) level=%d step=%d cells=%d sheet=%d aborted=%d empty=%d " "verdict=%d classify=%.6f mesh=%.6f streams=%.6f seconds=%.6f " @@ -3876,7 +3886,9 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector "cache_op_local=%llu cache_classifier_shared=%llu cache_classifier_local=%llu " "cache_unknown=%llu room_candidates=%llu room_evaluated=%llu " "tunnel_candidates=%llu tunnel_evaluated=%llu tunnel_core_candidates=%llu " - "tunnel_core_evaluated=%llu support_column_candidates=%llu"), + "tunnel_core_evaluated=%llu support_column_candidates=%llu " + "worm_eligible=%llu worm_block_proofs=%llu worm_block_skipped=%llu " + "worm_skip_rate=%.6f"), Tile.Coord.X, Tile.Coord.Y, Tile.Coord.Z, Tile.Level, Step, Cells, bSheetTile ? 1 : 0, Result.bAborted ? 1 : 0, Result.bEmpty ? 1 : 0, ClassifyVerdict, ClassifySeconds, MeshSeconds, StreamSeconds, @@ -3918,7 +3930,11 @@ void AVoxelWorld::GenerateTileResult(const FVoxelTileKey& Tile, const FIntVector static_cast(CounterDelta(VoxelDensityProfile::ECounter::CaveTunnelEvaluated)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::TunnelCoreCandidates)), static_cast(CounterDelta(VoxelDensityProfile::ECounter::TunnelCoreEvaluated)), - static_cast(CounterDelta(VoxelDensityProfile::ECounter::TunnelSupportColumnCandidates))); + static_cast(CounterDelta(VoxelDensityProfile::ECounter::TunnelSupportColumnCandidates)), + static_cast(WormEligibleSamples), + static_cast(WormBlockProofs), + static_cast(WormBlockSkippedSamples), + WormBlockSkipRate); if (bProfileOps) { diff --git a/Source/VoxelForge/Public/VoxelDensityProfile.h b/Source/VoxelForge/Public/VoxelDensityProfile.h index 36351e9..723252e 100644 --- a/Source/VoxelForge/Public/VoxelDensityProfile.h +++ b/Source/VoxelForge/Public/VoxelDensityProfile.h @@ -150,6 +150,9 @@ namespace VoxelDensityProfile FusedTunnelSamples, FusedTunnelDetailSamples, FusedTunnelDetailSkipped, + WormEligibleSamples, + WormBlockProofs, + WormBlockSkippedSamples, OpBlockBuilds, OpBlockSamples, OpBlockOperators, diff --git a/Source/VoxelForge/Public/VoxelNoise.h b/Source/VoxelForge/Public/VoxelNoise.h index 78e9cfd..ecaaa3a 100644 --- a/Source/VoxelForge/Public/VoxelNoise.h +++ b/Source/VoxelForge/Public/VoxelNoise.h @@ -30,6 +30,7 @@ #pragma once #include "CoreMinimal.h" +#include "VoxelTypes.h" #if !defined(VF_NOISE_USE_SIMD) #if PLATFORM_ENABLE_VECTORINTRINSICS && PLATFORM_CPU_X86_FAMILY @@ -141,6 +142,104 @@ FORCEINLINE float Perlin3D(const FVector3f& P) return Perlin3D(P.X, P.Y, P.Z); } +//============================================================================= +// LIPSCHITZ BOUND FOR THE ACTUAL 3D FIELD +//============================================================================= +// This is deliberately derived from the implementation above, rather than borrowed from a +// textbook Perlin constant. GradDot's h&15 table has the twelve UNNORMALIZED gradients +// +// (±1,±1,0), (±1,0,±1), (0,±1,±1) +// +// (the four duplicate selectors do not add any vectors). For one partial derivative, write the +// four x-edges of the trilinear interpolation as independent gradient pairs. With +// d = Fade'(x) = 30 x²(1-x)², each edge contributes the support of the actual gradient set at +// +// c0 = (1-x-dx, -dy, -dz), c1 = (x-d(1-x), dy, dz). +// +// The support is max(|a|+|b|, |a|+|c|, |b|+|c|), because every allowed gradient has exactly two +// non-zero ±1 components. Taking the four y/z edge weights and the exact fade derivative, a +// finite case split over those three support branches has supremum 15/4 on [0,1]^3 (attained at +// x=y=z=1/2). This maximization permits every corner hash to choose any member of the actual +// set, so it is an upper bound for every hash cell; it is also the supremum of that finite support +// relaxation, not the looser 1 + 4*max(Fade') = 8.5 bound used by the unrelated 2D floor relief. +// +// The vector bound below follows the same per-axis-bound × sqrt(dimension) method used by the +// floor-relief proof. Abs is 1-Lipschitz and VOXEL_NOISE_SCALE is positive, so the scaled N1 +// field has this bound in noise-space coordinates. WormBlockSkip computes its radius after the +// anisotropic world->noise transform, so the HBias and VerticalScale factors are included there. +static constexpr double Perlin3DPartialAbsBound = 15.0 / 4.0; +static constexpr double Perlin3DGradientBound = + Perlin3DPartialAbsBound * 1.732050807568877293527446341505872L; +static constexpr double WormN1NoiseSpaceLipschitzBound = + static_cast(VOXEL_NOISE_SCALE) * Perlin3DGradientBound; + +// A four-point side gives the natural small worker-local blocks for the mesher's 35-point +// (32 cells + one-sample normal halo) LOD0 lattice. The cache is only an optimization; a failed +// proof falls through to the original per-sample N1/N2 code. +static constexpr int32 WormBlockSampleSide = 4; + +// The center value is evaluated with the same float Perlin expression as N1. The endpoint noise +// coordinates are already float results of monotone positive/negative affine transforms, so their +// min/max box contains every float lattice sample exactly. The 1e-3 scaled-N1 margin covers the +// remaining center-evaluation/interpolation rounding with substantial headroom; the proof remains +// strict because both the Lipschitz term and this margin are subtracted before comparing with T. +static constexpr double WormBlockOutputRoundMargin = 1.0e-3; + +FORCEINLINE bool ProvesScaledAbsPerlin3DAbove( + const FVector3f& CenterNoisePosition, + const FVector3f& MinNoisePosition, + const FVector3f& MaxNoisePosition, + float Threshold, + double ScaledFieldLipschitzBound) +{ + if (!FMath::IsFinite(CenterNoisePosition.X) + || !FMath::IsFinite(CenterNoisePosition.Y) + || !FMath::IsFinite(CenterNoisePosition.Z) + || !FMath::IsFinite(MinNoisePosition.X) + || !FMath::IsFinite(MinNoisePosition.Y) + || !FMath::IsFinite(MinNoisePosition.Z) + || !FMath::IsFinite(MaxNoisePosition.X) + || !FMath::IsFinite(MaxNoisePosition.Y) + || !FMath::IsFinite(MaxNoisePosition.Z) + || !FMath::IsFinite(Threshold) + || !(Threshold > 0.0f) + || !FMath::IsFinite(ScaledFieldLipschitzBound) + || !(ScaledFieldLipschitzBound > 0.0)) + { + return false; + } + + const double Dx = FMath::Max( + FMath::Abs(static_cast(MinNoisePosition.X) + - static_cast(CenterNoisePosition.X)), + FMath::Abs(static_cast(MaxNoisePosition.X) + - static_cast(CenterNoisePosition.X))); + const double Dy = FMath::Max( + FMath::Abs(static_cast(MinNoisePosition.Y) + - static_cast(CenterNoisePosition.Y)), + FMath::Abs(static_cast(MaxNoisePosition.Y) + - static_cast(CenterNoisePosition.Y))); + const double Dz = FMath::Max( + FMath::Abs(static_cast(MinNoisePosition.Z) + - static_cast(CenterNoisePosition.Z)), + FMath::Abs(static_cast(MaxNoisePosition.Z) + - static_cast(CenterNoisePosition.Z))); + const double Radius = FMath::Sqrt(Dx * Dx + Dy * Dy + Dz * Dz); + + // Keep this operation in float and in the same order as the production N1 expression. The + // double conversion is only for the conservative comparison below. + const float CenterN1 = FMath::Abs( + Perlin3D(CenterNoisePosition) * VOXEL_NOISE_SCALE); + if (!FMath::IsFinite(CenterN1)) + { + return false; + } + + return static_cast(CenterN1) + - ScaledFieldLipschitzBound * Radius + >= static_cast(Threshold) + WormBlockOutputRoundMargin; +} + //============================================================================= // 4-WIDE BATCH — the SIMD multiplier. Computes 4 independent Perlin samples. // Inputs are 4-element arrays; unused lanes must be zero-filled by the caller