From 78641da0bbb6bddd1515805cb249e5014e320bf1 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Wed, 30 Sep 2026 09:06:25 +0200 Subject: [PATCH] research(step2d): a simpler field is fast (81 ns/sample) -- but it is not the owner's world; NOT a pass Editor-only, default off (voxel.ResearchRedefinedDensityField). The redefined field is a seeded, periodic cross-corridor pattern per strate plus the authored passages, landings and a shaft tree, with four value-noise detail bands at every near-surface 25 cm sample and sound 8^3 brick proofs. 259^3 ring, six workers: 308 ms median (467 ms without skips) vs 8.9 s for the live field. Detail, boundary-artefact, determinism (1 vs 6 workers, two runs) and bound-audit checks pass. Why it is not a pass: the slices show a mostly-solid field with a few straight corridors, where the owner's world is mostly open space; rooms, the tunnel network, composer recipes and archetypes are gone. Much of the speed comes from 60% of bricks being proven solid in that simpler world. What it does show: detail at full resolution plus brick skipping costs ~80-130 ns/sample; the cost of the live field is how rooms/tunnels/passages are represented, not the detail or the lattice. The two walkability tests were redirected to the research field in the working tree; that change is NOT committed (saved as scratchpad step2d-test-redirect.patch) so they keep testing the live world. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ --- CODEMAP.md | 8 +- Saved/Research/step2d/step2d_launch_ledger.md | 60 + .../step2d_rebuild_12/slice_xy_center.png | Bin 0 -> 4159 bytes .../step2d_rebuild_12/slice_xz_passage.png | Bin 0 -> 7046 bytes .../step2d/step2d_rebuild_12/slice_xz_y0.png | Bin 0 -> 6934 bytes .../step2d/step2d_rebuild_12/slice_yz_x0.png | Bin 0 -> 7652 bytes Saved/Research/step2d/step2d_report.md | 131 ++ .../VoxelForgeRedefinedDensityFieldTest.cpp | 146 ++ Source/VoxelForge/Private/VoxelGenerator.cpp | 24 + .../Private/VoxelRedefinedDensityField.cpp | 1487 +++++++++++++++++ .../Public/VoxelRedefinedDensityField.h | 90 + .../Private/VoxelForgeExploreCommandlet.cpp | 831 +++++++++ .../VoxelForgeRegionEvalExperiment.cpp | 250 +++ .../Private/VoxelForgeRegionEvalExperiment.h | 24 + Tools/VoxelForgeTest.ps1 | 32 +- 15 files changed, 3077 insertions(+), 6 deletions(-) create mode 100644 Saved/Research/step2d/step2d_launch_ledger.md create mode 100644 Saved/Research/step2d/step2d_rebuild_12/slice_xy_center.png create mode 100644 Saved/Research/step2d/step2d_rebuild_12/slice_xz_passage.png create mode 100644 Saved/Research/step2d/step2d_rebuild_12/slice_xz_y0.png create mode 100644 Saved/Research/step2d/step2d_rebuild_12/slice_yz_x0.png create mode 100644 Saved/Research/step2d/step2d_report.md create mode 100644 Source/VoxelForge/Private/Tests/VoxelForgeRedefinedDensityFieldTest.cpp create mode 100644 Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp create mode 100644 Source/VoxelForge/Public/VoxelRedefinedDensityField.h diff --git a/CODEMAP.md b/CODEMAP.md index 9b048a4..7deaf2b 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -117,10 +117,12 @@ remains `0`, and lateral regions remain gated off. |------|------| | `../../VoxelForge.uplugin` | Plugin manifest. Runtime module `VoxelForge` plus the Editor-only `VoxelForgeEditor` commandlet module. Beta. | | `VoxelForge.Build.cs` | Deps: Core, CoreUObject, Engine, **GameplayTags**, **RealtimeMeshComponent**. | -| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set, reporting whole-ring setup/candidate counters; `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval. +| `Tools/VoxelForgeTest.ps1` | The single guarded test entry point. `-Scenario canonical|owner|probe|perf|tests|surface-fall|crossing|gate-stress|visual-edit|visual-lod|visual-gap-walk|visual-gap-inject|dig-motion|parity|horizon|horizon-render|determinism|region-eval|step2d`, optional `-Build`, `-Assets owner|default`, `-Cvars`, absolute `-Out`, `-Label`, `-HorizonLevel`, `-HorizonRenderDistanceChunks`, `-ParityExportSize=32|64|128`, `-ParityExportTile=x,y,z`, `-SharedDDC`, `-SampleDensityStacks`, `-StackSampleIntervalUs`, and `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH`; stages the host/assets, isolates user/DDC/config state, serializes one Unreal launch at a time, enforces the 8 GiB/30-minute guards, waits for 6 GiB free RAM before launches, and emits compact `result.json`/`summary.txt` plus full logs. Parity can render its game leg with `-EnableRender` and compares the selected export region against the exact streamed tile hashes. `visual-gap-walk` records camera-frustum surface-point coverage at 6000 cm/s; `visual-gap-inject` proves an early component hide is detected; `dig-motion` logs per-frame vertical velocity, gate, support readiness, and movement mode. `-Cvars @{ 'voxel.TestGapAuditLegacyHandoff'=1 }` replays the old visibility handoff for A/B audit baselines. Horizon traces report desired/loaded tiles, resident stream geometry, worker seconds, steady wall time, and peak private memory; `horizon-render` captures the owner view; `determinism` compares per-tile hashes across the default CRT path and `-voxel.CrtFma3=0`; `region-eval` compares the live path with the default-off editor chunk program on the 259³ lattice and five-seed probe set; `step2d` measures the independent full-detail research field, sound 8³ brick proofs, detail/gradient controls, slices, and worker hashes. `-RegionHashProbe -RegionWorkers N -RegionReferenceHash HASH` checks a full-ring hash at another partition count. `-SampleDensityStacks` captures profiles, and `-StackSampleIntervalUs` sets the sampling interval. | `Public/VoxelForgeModule.h` / `Private/VoxelForgeModule.cpp` | `FVoxelForgeModule` boilerplate (Startup/Shutdown just log). | -| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. Optional `-exporttilex/y/z` sets an exact L0 tile origin for controlled export/game parity; omitting all three preserves the existing center-derived origin. `-regioneval=1` runs the default-off step-2 research harness: full-ring and five-seed Q16 comparisons, repeat hashes, shrunk-reach falsification, cold timings, full-ring setup/candidate counters, counted live fallbacks, and separate before/after stack captures when `-SampleDensityStacks` is set. `-RegionHashProbe` runs one full-ring chunk evaluation and compares its raw float hash with a reference from another worker count. | -| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Lattice driver with six workers by default for the commandlet-only region experiment. The live path calls `GetDensityAt` per point; the prepared path assigns each 32³ chunk once and calls editor-only `EvaluateDensityChunk`, which seeds one block evaluation and runs canonical final-field writers across that chunk. The prepared room/SDF cache window is aligned to that chunk; 8-voxel SDF candidate bins stay editor-only and default off. Unsupported routes use counted scalar fallbacks. Full-ring output reports room/SDF cache, active-op-list, and broad-phase/index builds; general-path entries and fallbacks; candidate visits/evaluations per point; and fresh sampled attribution. The full-ring hash probe supports 1–32 fixed partitions to check determinism across worker counts. | +| `Source/VoxelForgeEditor/VoxelForgeEditor.Build.cs` / `Private/VoxelForgeExploreCommandlet.*` | Editor/commandlet-only `-run=VoxelForgeExplore`; owns bounded render/`VF_` walk/OBJ export orchestration and is not linked by Game/Shipping. Optional `-exporttilex/y/z` sets an exact L0 tile origin for controlled export/game parity; omitting all three preserves the existing center-derived origin. `-regioneval=1` retains the exact-field step-2c research harness. `-step2d` runs the independent field benchmark without old-world equality gates and writes timing, detail, boundary, determinism, triangle, and side-by-side slice evidence under `Saved/Research/step2d`. | +| `Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.*` | Six-worker lattice drivers for step 2c and step 2d. The original path still compares live `GetDensityAt` with the editor chunk program. Step 2d evaluates the independent field and assigns contiguous batches of 8³ bricks dynamically so surface-heavy work balances across workers. It first proves bricks uniform using a macro Lipschitz bound plus the full detail-amplitude bound; uncertain bricks retain all 25 cm detail samples. Timings split classification, point evaluation, and uniform fill; hashes compare workers 1 and 6. | +| `Public/VoxelRedefinedDensityField.h` / `Private/VoxelRedefinedDensityField.cpp` | Default-off `WITH_EDITOR` research field, selected by `voxel.ResearchRedefinedDensityField`. A seeded connected room/corridor lattice joins authored flat passage landings; four bounded world-space value-noise bands, including a one-sample micro band, are evaluated at full resolution. Skip proofs refuse edits, passage bounds, strate boundaries, and uncertain bricks; a local derivative bound permits sound proofs through landing-floor blends. The public API adds no reflected class layout. | +| `Private/Tests/VoxelForgeRedefinedDensityFieldTest.cpp` | Three-seed functional sweep for the research field: authored passage endpoints must bracket open air and solid floor, and each tested strate must have a walkable arrival-to-exit route. | | `Public/VoxelGeometryHash.h` | Header-only geometry digest shared by commandlet export and runtime tile-hash dump; hashes mesh counts and the vertex/normal/UV/color/triangle arrays with the historical CRC order. | | `Public/VoxelStats.h` / `Private/VoxelStats.cpp` | `stat VoxelForge` DWORD counters for tile classification, skipping, meshing, operator-stack verdicts, and cave-bail diagnosis. Each `Cave Bail *` counter names one guard; `Cave Bail No Stack` counts slots with no buildable stack (no definition, or a degenerate strate). | diff --git a/Saved/Research/step2d/step2d_launch_ledger.md b/Saved/Research/step2d/step2d_launch_ledger.md new file mode 100644 index 0000000..fdaf777 --- /dev/null +++ b/Saved/Research/step2d/step2d_launch_ledger.md @@ -0,0 +1,60 @@ +# Step 2d launch ledger + +All Unreal processes were launched through `Tools/VoxelForgeTest.ps1` and ran serially. The +guarded launches used below-normal priority, the shared persistent DDC where recorded, +`-MaxParallelActions=4`, a prelaunch 6 GiB memory check, an 8 GiB per-process limit, and a +30-minute timeout. The retained ledgers show **no crash reporters, no process above 8 GiB, and +no timeout**. Exit 255 below means automation assertions failed; it was not a native crash. + +## Benchmark attempts + +| Label | Build | Unreal process | Result | +|---|---|---|---| +| `step2d_rebuild_01` | UBT PID 25780, exit 6, peak 2.71 GiB | Not launched | Compile failed on the `ApplicationContextMask` automation flag; corrected in the next source pass. | +| `step2d_rebuild_02` | UBT PID 19420, passed, peak 1.57 GiB | PID 13996, exit 2, peak 1.83 GiB | 3.834 s optimized ring; brick/sample skipping was too low for the speed gate. | +| `step2d_rebuild_03` | Passed; PID not retained in the compact log | PID 15480, exit 2, peak 1.82 GiB | 3.918 s; detail spectrum failed. The smoothing negative control worked; the proof negative control did not. | +| `step2d_rebuild_04` | Not launched | Not launched | Harness rejected a relative `-Out` path before Unreal startup. | +| `step2d_rebuild_05` | UBT PID 23904, passed | PID 24708, exit 2, peak 1.82 GiB | Scratch reuse gave 233.27 density mismatches and hash/boundary failures; scratch ownership was corrected. | +| `step2d_rebuild_06` | UBT PID 7540, passed | PID 25828, exit 2, peak 1.82 GiB | Detail, smoothing, boundary, and hash checks passed, but 3.70 s and 1.46% sample skipping missed the speed gate. | +| `step2d_rebuild_07` | UBT PID 23800, passed | PID 5208, exit 2, peak 1.81 GiB | 3.66 s; the cached XY macro did not improve the timing. | +| `step2d_rebuild_08` | UBT PID 25304, passed | PID 7108, exit 2, peak 1.81 GiB | 2.62 s; 22.20% of bricks and 21.18% of samples skipped, still over target. | +| `step2d_rebuild_09` | UBT PID 25612, passed, peak 1.51 GiB | PID 24256, exit 0, peak 1.82 GiB | Superficially under 600 ms, but invalid as evidence: classifier gap/default/chunk semantics had not yet been fixed. Excluded. | +| `step2d_harness_10` | UBT PID 2544, passed, peak 1.49 GiB | PID 23204, exit 0, peak 1.84 GiB | 503.026 ms and hash/detail/boundary checks passed. Superseded because its deliberately undersized-bound control did not catch a wrong sign. | +| `step2d_harness_11` | UBT PID 2900, passed, peak 1.50 GiB | PID 10688, exit 0, peak 1.83 GiB | 497.518 ms; the scale-0 versus scale-4 undersized-bound control caught 35,847 wrong-sign samples. | +| `step2d_harness_12` | UBT PID 13868, passed, peak 1.87 GiB | PID 16872, exit 0, peak 1.81 GiB | Final dynamically scheduled run: 308.177 ms median, all hashes and research gates passed. | + +## Functional runs + +| Label | Build | Unreal process | Result | +|---|---|---|---| +| `step2d_functional_01` | UBT PID 21184, passed | PID 3444, exit 255, peak 3.26 GiB | Base connectivity passed; refinement failed on an endpoint self-query. The launcher detected a write under `C:\Users\Jahni\AppData\Local\UnrealEngine`; later runs used isolated per-run local data. | +| `step2d_functional_02` | UBT PID 11844, passed, peak 1.87 GiB | PID 5048, exit 255, peak 3.13 GiB | Same endpoint-snap failure; isolated LocalAppData was clean. | +| `step2d_functional_03` | UBT PID 2060, passed, peak 1.90 GiB | PID 25648, exit 255, peak 3.19 GiB | Origin probe compared raw density 8 with a coarse center at -3.534534 near Z=-1012. | +| `step2d_functional_04` | Failed launch; retained PID 23996, stage split not retained | Exit 255; peak not retained | Range-normalization fix did not resolve the origin probe. No crash was recorded. | +| `step2d_redefined_seeds_01` | Build passed; PID not retained | PID 21888, exit 255 | Initial multi-seed endpoint-snap expectation was stricter than the route's full-density segment check; expectation was revised. | +| `step2d_functional_05` | UBT PID 17000, passed | PID 7540, exit 255 | 12/16 fitted VerticalShafts seeds passed. | +| `step2d_functional_06` | Build passed; PID not retained | PID 21948, exit 255, peak 3.43 GiB | 12/16; seed 1337's physical arrival/departure audit found 13 bad samples of 15. | +| `step2d_redefined_seeds_02` | UBT PID 14672, exit 0, peak 1.63 GiB | PID 2220, exit 0, peak 2.61 GiB | Passed an earlier three-seed traversal version. | +| `step2d_functional_07` | Build/Unreal stage PID 15712 in retained summary | Exit 255, peak 3.42 GiB | 12/16; no native crash. | +| Relative-output-path prelaunch attempt | Not launched | Not launched | Harness stopped before Unreal startup; corrected to an absolute output directory. | +| `step2d_functional_08` | UBT PID 12596, passed, peak 1.87 GiB | PID 20624, exit 255, peak 3.42 GiB | 13/16; origin path passed, three measurement limits remained. | +| `step2d_functional_09` | UBT PID 10848, passed, peak 1.50 GiB | PID 16764, exit 255, peak 3.46 GiB | 13/16 after the cache-key fix. | +| `step2d_functional_10` | UBT PID 24704, passed, peak 1.44 GiB | PID 7512, exit 255, peak 3.56 GiB | 13/16 after connector-radius changes. | +| `step2d_functional_11` | UBT PID 17088, exit 6, peak 1.96 GiB | Not launched | Compile error C4456 from a duplicated `XYCount`; fixed before the next build. | +| `step2d_functional_12` | UBT PID 4760, passed, peak 1.95 GiB | PID 26520, exit 255, peak 3.49 GiB | 13/16 after explicit bridges. | +| `step2d_functional_13` | UBT PID 19416, passed, peak 1.50 GiB | PID 26444, exit 255, peak 3.50 GiB | 13/16; diagnostics showed 16 retries and both endpoints in the same large component. | +| `step2d_functional_14` | UBT PID 3156, passed, peak 1.50 GiB | PID 17892, exit 255, peak 3.49 GiB | 13/16 after widening the landing bridge. | +| `step2d_functional_15` | UBT PID 5648, passed, peak 1.49 GiB | PID 23788, exit 255, peak 3.53 GiB | 14/16 after adding the vertical cavern band. | +| `step2d_functional_16` | UBT PID 20028, passed, peak 1.13 GiB | PID 4916, exit 255, peak 3.60 GiB | Controlled-wall assumptions and the 14/16 refinement audit both failed. | +| `step2d_functional_17` | UBT PID 14952, passed, peak 1.50 GiB | PID 2604, exit 0, peak 2.69 GiB | Three-seed traversal test passed: 21 landing brackets and 18 routes. | +| `step2d_functional_18` | UBT PID 10496, passed, peak 1.49 GiB | PID 25636, exit 0, peak 2.70 GiB | Passage-open-space test passed. | +| `step2d_functional_19` | UBT PID 22588, passed, peak 1.48 GiB | PID 7316, exit 255, peak 3.71 GiB | Controlled wall returned conservative unknown after zero retries; refinement remained 14/16. | +| `step2d_functional_20` | UBT PID 14076, passed, peak 1.88 GiB | PID 23120, exit 255, peak 3.60 GiB | Wall samples were -2/-102/-2, so the chosen location was not an air/solid/air control; refinement remained 14/16. | +| `step2d_functional_21` | UBT PID 8224, passed, peak 1.58 GiB | PID 25652, exit 255, peak 3.67 GiB | `StrateConnectivity` passed; refinement remained 14/16 because the fallback had been placed in a skipped diagnostic branch. | +| `step2d_functional_22` | UBT PID 26528, passed, peak 1.62 GiB | PID 24396, exit 0, peak 3.68 GiB | Both required connectivity tests passed; the fitted audit directly confirmed 16/16 routes at step 2. | +| `step2d_functional_23` | Build not requested | PID 15176, exit 0, peak 2.44 GiB | `PassageLandsInOpenSpace` passed. | +| `step2d_functional_24` | Build not requested | PID 26336, exit 0, peak 2.42 GiB | `RedefinedFieldTraversalSeeds` passed across three seeds. | + +The manual `Win32_OperatingSystem` memory probe was denied once before a launch; no Unreal process +was started by that probe. The harness's own native memory preflight succeeded on subsequent +launches. No files outside the workspace were deleted. diff --git a/Saved/Research/step2d/step2d_rebuild_12/slice_xy_center.png b/Saved/Research/step2d/step2d_rebuild_12/slice_xy_center.png new file mode 100644 index 0000000000000000000000000000000000000000..a3ac998a52232598529b2b95e8fec8f764d2361a GIT binary patch literal 4159 zcmeHL>0cA)7M=`~I0;b)2#|tefU>AigO)hygT{phZNb zRTc#ey=uV~ifq>wOuQB7BDJze5oOehXbXY@0)q6N8QcHhez>>y3%}o)oO$2pJm)#f zurJWxXEx7*2SLzm-__p15Cn4|h-H9+zmUJhj|U+LS6PXZ7XJ; zEnvYv7D__hayo!5p}*$1El7yz?suk=8QyQ!Ga@chfj7 zDoxFmxT*Ew(O+wg++STwo)AHwLBuGMk=V7ZPp#iIc4A7Vo5gA-AmS&)f5kiX&cfaP zjZ@>V1|E$!r@_RGVfECtvH9QM))p1~D=|9X!B|vTqq5XoxF)Ia)y?)aIlL0Y-~X0K znBq`#&yMI1JN@(M2p8~7N?CeRx}w6$iFDqh*VQ`UD&0PnoLrq~PoA5(UM|T7;kX&1 z9JjWe_u^P}?2~&2K#0T&Heyh=(F8mw7N?^i!a*~PKWJYq+mDZ&o9Uk{usBcC`5Ua( zZ?|2%oI6kY%gd}@ZreKmi11C3kgu*8*hwU?WN8W|NKK_yC9l*@KMQCy!wP1>R3_#l zh-Ur`$+I2AA(lRgXp@0wnQTOpIW3vir~*lMN+!Emdil&7<+O%SKU%+e+Oc4S{^;i*iJryBvdxPME0nI$skp6-Q35A?_m_`TMgGaXWkqe!@^B;u=M z4W|PWOS`RcQ4QcinUMqJ4{%Z{Ydn!^OB-i^(f(ka2mNCIVv%rUwAzw3RyzYOthD!; z@S|D!(u%S>w50H@fQ!*_0o@FcNgn!F$3cPt-ZRZ*X>@XLgaN3(#k)Es5HvX|OM1r9IKRI5R!dH@Idg7Yh%<#o-@4QQq|?V};}6xOI%luDRAYxeBzsRqjh{ z6^j&aq3?gbys^ny{oGrna{?N3IvnV;hKRhX+j78a%xF%q6>Fu5BhdX3%QJgd8IJG_ zoVQ;NiVLdR3lS$kVglYv&o}L|;lF>`btsd&`A|U<>gO;-&%UyAYh*~OS(go3k}y18 z;JJ3Tq<2@`&bzI#!$1J34dBqwRn%_r1o~!g@Df=8I@a;TJMW4J|FZfth{PEW$id` zrsund4-r&gricwptB8wgz_2k#l_vBL6ii_^Oej(L)x_D91y)qlK`?nUcU@d5?2ie2{ zcF&wd{@_OlnuSdLgJb>H{xIYIiH`KO!v2YG^7~GO!Ju~PUFZsV3OmO@=AiCA1P$%O z;%wzjUXLX*Rll+O=fx%XLV&m3-Stw9t|5lJq|EfX-??tY14g8v^jZywQeC!KaVG1~ zPtP+^ear@b6&{X@(Zf6SIdJ5s#3xP3CZQggO8d$SfS|6<+ifV{L{Tix?QB8{j4^(J zMcs#CMwoHv(P}Ao1dI-9H=xpS6{G_r;sFowJ}1R+ERvPwaxv8|a3wA-U+SBQ)yweJ z^{ne_Q2}@O3SIac+HN+KP?+#j*bb&t!92{WcVc>P4LGj*dB=RZ#h{># z0$P|3lLUes%h>KiH&{=gyzCz~(w!qP5?m;WzQZ(&Je3@~UsXi6hj!2&w*IePGc-E< z_}vkhVFw~E!&j2|S(?iRoo~?GWuxC*$ycDWV9%`92!^Ks^3YQkY|fL}8+@3Cxt?s> zCQV7d@%1>P&Wd_VmAfU&?TMyEZzXb#T|JVFINozWmo}oP0pxa7m z#L;0W@S^SPX?yZ;g8|sMS+Ehyjbn5nE1@?k`qlEO_=$?`AN&y2Nv(glKak*~o~!G9 zV9&Hqme8BZQlP4%LA9QbN^~TJ^d&+)XiF36}!mZmNhS{a!D{PhmCjZ+aU;z~pG9C5wrM%2bvHWz=E` z{DC+BwRbYVgnk6RZq%we<9Tzb-$l4ClKbbI96%d!g#M&q!TSM*{rrex`qWa;?CD*A@^i8g37wY4B^lFPn1O!1@;eLq s_R4Iq&bE}+&>xUU1Sq%vmWzg#Kq2Cm=1VaS;2#5!?@E8~i=J}zA2dsT`Tzg` literal 0 HcmV?d00001 diff --git a/Saved/Research/step2d/step2d_rebuild_12/slice_xz_passage.png b/Saved/Research/step2d/step2d_rebuild_12/slice_xz_passage.png new file mode 100644 index 0000000000000000000000000000000000000000..f06d7114508a23eb1cf5ad6af740699960a49d85 GIT binary patch literal 7046 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 literal 0 HcmV?d00001 diff --git a/Saved/Research/step2d/step2d_rebuild_12/slice_xz_y0.png b/Saved/Research/step2d/step2d_rebuild_12/slice_xz_y0.png new file mode 100644 index 0000000000000000000000000000000000000000..8a6047dd4777a7745f1ac91a4471f4baf4bed509 GIT binary patch literal 6934 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 aawcJOUkYg40QkR4#QL~{W$7_o{C@$iu(eMB literal 0 HcmV?d00001 diff --git a/Saved/Research/step2d/step2d_rebuild_12/slice_yz_x0.png b/Saved/Research/step2d/step2d_rebuild_12/slice_yz_x0.png new file mode 100644 index 0000000000000000000000000000000000000000..34afc1597869ea2d2908c86296e745a282093725 GIT binary patch literal 7652 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 +- [Vertical slice at Y=0]() +- [Vertical slice at X=0]() +- [Vertical slice through an authored passage]() + +Machine-readable measurements are in [step2d_report.json](). + +## Scaling to 12.5 cm and compile risk + +At 12.5 cm, double macro lengths and broad/medium noise periods in lattice units to preserve +their world-space shape: the current noise periods 8/4/2/1 become 16/8/4/2 samples. Keep or add +a 1-sample fine band so detail stays at least as rich as the 25 cm version. For a fixed world +region, the lattice grows 8×, while surface-intersecting 8³ bricks should grow closer to 4× +(surface area); remaining bricks still need only a cheap final-field sign proof and density +writes. This scaling model is reasoned from the geometry and has not yet been timed at 12.5 cm. + +The UE 5.7 Win64 Development build passed with `-ForceHeaderGeneration` and +`-MaxParallelActions=4`. No UCLASS/UPROPERTY layout changed, so no generated-class layout risk +was introduced. The sensitive code is the locally bounded landing-floor derivative and the +supremum used by `TryClassifyBrick`; future field terms must update those bounds and keep the +proof audit. The live path and ARCHITECTURE §8.10 were left unchanged when research mode is off. +No `.uasset` file was edited. Nothing was committed, pushed, or stashed. + +See the exhaustive [launch ledger]() for every benchmark, test run, failure, and prelaunch rejection. diff --git a/Source/VoxelForge/Private/Tests/VoxelForgeRedefinedDensityFieldTest.cpp b/Source/VoxelForge/Private/Tests/VoxelForgeRedefinedDensityFieldTest.cpp new file mode 100644 index 0000000..bd36194 --- /dev/null +++ b/Source/VoxelForge/Private/Tests/VoxelForgeRedefinedDensityFieldTest.cpp @@ -0,0 +1,146 @@ +#if WITH_DEV_AUTOMATION_TESTS + +#include "Misc/AutomationTest.h" + +#include "VoxelForgeTestFixture.h" +#include "VoxelRedefinedDensityField.h" +#include "VoxelStrateMeasure.h" + +IMPLEMENT_SIMPLE_AUTOMATION_TEST( + FVoxelForgeRedefinedDensityFieldTraversalSeedsTest, + "VoxelForge.Research.RedefinedFieldTraversalSeeds", + EAutomationTestFlags::EditorContext | EAutomationTestFlags::EngineFilter) + +bool FVoxelForgeRedefinedDensityFieldTraversalSeedsTest::RunTest(const FString& Parameters) +{ + (void)Parameters; + struct FRestoreResearchField + { + bool bWasEnabled = VoxelForgeRedefinedDensityField::IsEnabled(); + float PreviousScale = VoxelForgeRedefinedDensityField::GetDetailScale(); + ~FRestoreResearchField() + { + VoxelForgeRedefinedDensityField::SetDetailScale(PreviousScale); + VoxelForgeRedefinedDensityField::SetEnabled(bWasEnabled); + } + } Restore; + VoxelForgeRedefinedDensityField::SetEnabled(true); + VoxelForgeRedefinedDensityField::SetDetailScale(1.0f); + + constexpr int32 Seeds[] = {1337, 12345, 20260930}; + int32 CheckedLandings = 0; + int32 CheckedRoutes = 0; + int32 RoutesWithResolvedEndpoints = 0; + for (const int32 Seed : Seeds) + { + VoxelForgeTest::FTestWorld World; + World.Build(Seed, /*InGapChunks=*/2, /*InStrateHeightInChunks=*/4); + if (!World.IsValid()) + { + AddError(FString::Printf( + TEXT("Seed %d: test world did not build: %s"), Seed, *World.WhyInvalid())); + return false; + } + + const TArray& Layout = World.StrateManager->GetLayout(); + const TArray& Passages = World.StrateManager->GetPassages(); + for (const FVoxelPassage& Passage : Passages) + { + if (Passage.UpperStrateIndex == Passage.LowerStrateIndex) + { + continue; + } + const FVector UpperAir = Passage.UpperLanding.StandingPoint; + const FVector LowerAir = Passage.LowerLanding.StandingPoint; + const float UpperDensity = World.Generator->GetDensityAt( + UpperAir.X, UpperAir.Y, UpperAir.Z); + const float UpperFooting = World.Generator->GetDensityAt( + UpperAir.X, UpperAir.Y, Passage.UpperLanding.FloorZ - 0.75f); + const float LowerDensity = World.Generator->GetDensityAt( + LowerAir.X, LowerAir.Y, LowerAir.Z); + const float LowerFooting = World.Generator->GetDensityAt( + LowerAir.X, LowerAir.Y, Passage.LowerLanding.FloorZ - 0.75f); + ++CheckedLandings; + if (!FMath::IsFinite(UpperDensity) || !FMath::IsFinite(UpperFooting) + || !FMath::IsFinite(LowerDensity) || !FMath::IsFinite(LowerFooting) + || UpperDensity <= 0.0f || LowerDensity <= 0.0f + || UpperFooting >= 0.0f || LowerFooting >= 0.0f) + { + AddError(FString::Printf( + TEXT("Seed %d: passage %d landing failed the final-field air/floor bracket " + "(upper %.4f/%.4f, lower %.4f/%.4f)."), + Seed, CheckedLandings - 1, UpperDensity, UpperFooting, + LowerDensity, LowerFooting)); + } + } + + for (int32 StrateIndex = 0; StrateIndex < Layout.Num(); ++StrateIndex) + { + FVector Arrival = FVector::ZeroVector; + FVector Departure = FVector::ZeroVector; + bool bHasArrival = false; + bool bHasDeparture = false; + for (const FVoxelPassage& Passage : Passages) + { + if (Passage.UpperStrateIndex == Passage.LowerStrateIndex) + { + continue; + } + if (Passage.LowerStrateIndex == StrateIndex && !bHasArrival) + { + Arrival = Passage.LowerLanding.StandingPoint; + bHasArrival = true; + } + if (Passage.UpperStrateIndex == StrateIndex && !bHasDeparture) + { + Departure = Passage.UpperLanding.StandingPoint; + bHasDeparture = true; + } + } + if (!bHasArrival || !bHasDeparture) + { + continue; + } + + FVoxelStrateMeasureSettings Settings; + Settings.SampleStep = 4; + Settings.RadiusInVoxels = 192; + Settings.CenterXY = FVector2D( + (Arrival.X + Departure.X) * 0.5f, + (Arrival.Y + Departure.Y) * 0.5f); + Settings.MaxCells = 4000000; + Settings.HeadroomCells = 2; + const FVoxelConnectivityDiagnostics Route = VF_DiagnoseConnectivity( + *World.Generator, *World.StrateManager, StrateIndex, + Arrival, Departure, Settings); + ++CheckedRoutes; + RoutesWithResolvedEndpoints += Route.bStartSnapped || Route.bGoalSnapped; + if (!Route.bValid || Route.Result != EVoxelConnectivityResult::Connected) + { + AddError(FString::Printf( + TEXT("Seed %d strate %d (%s): arrival-to-exit walking route was %s " + "(result=%d, valid=%s, start resolved=%s, goal resolved=%s)."), + Seed, StrateIndex, + Layout[StrateIndex].Definition + ? *Layout[StrateIndex].Definition->GetName() : TEXT("no definition"), + Route.Result == EVoxelConnectivityResult::Connected + ? TEXT("connected") : TEXT("not connected"), + static_cast(Route.Result), + Route.bValid ? TEXT("yes") : TEXT("no"), + Route.bStartSnapped ? TEXT("yes") : TEXT("no"), + Route.bGoalSnapped ? TEXT("yes") : TEXT("no"))); + } + } + } + + TestTrue(TEXT("Several seeds exercised passage landings"), CheckedLandings >= 6); + TestTrue(TEXT("Several seeds exercised arrival-to-exit walking"), CheckedRoutes >= 6); + AddInfo(FString::Printf( + TEXT("Redefined field functional sweep: %d authored passage landings bracket open air " + "over %d seeds; %d arrival-to-exit walking routes checked; %d used a resolved " + "coarse endpoint after the full-density segment check."), + CheckedLandings, UE_ARRAY_COUNT(Seeds), CheckedRoutes, RoutesWithResolvedEndpoints)); + return CheckedLandings >= 6 && CheckedRoutes >= 6; +} + +#endif // WITH_DEV_AUTOMATION_TESTS diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index 5c25ea6..53f93c9 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -17,6 +17,7 @@ #include "VoxelDensityAblation.h" // fingerprinted development-only stage measurements #include "VoxelDensityPrimitives.h" // spine / seals / passage — shared with the operator stack #include "VoxelDensityOpStack.h" // the per-strate operator stack +#include "VoxelRedefinedDensityField.h" // default-off editor research evaluator #include "VoxelHeightOp.h" // IVoxelBiomeField — the adapter below implements it #include "VoxelStats.h" #include "VoxelDensityProfile.h" @@ -2815,6 +2816,19 @@ bool UVoxelGenerator::EvaluateDensityChunk( float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ, float* OutPreEditDensity) const { +#if WITH_EDITOR + // Step 2d is an editor-only, default-off side path. It does not alter the live evaluator's + // state or caches; the field is selected only for an explicit research/test run. + if (VoxelForgeRedefinedDensityField::IsEnabled()) + { + VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); + const float ResearchDensity = VoxelForgeRedefinedDensityField::Evaluate( + *this, WorldX, WorldY, WorldZ, OutPreEditDensity, + VoxelForgeRedefinedDensityField::GetDetailScale()); + VoxelPassageGeometry::ResetVerticalShaftConnectorAirMarker(); + return ResearchDensity; + } +#endif // Soundness mode: for a sample in a block whose core or passage-carving reach bit is clear, // evaluate the complete canonical field once with the bypass bit set. The normal call then // takes the fast path below and compares the final MC field after the boundary seal and diff @@ -6298,6 +6312,16 @@ EVoxelTileClass UVoxelGenerator::ClassifyTile(const FIntVector& OriginVoxels, in VoxelDensityProfile::AddCounter( VoxelDensityProfile::ECounter::TileClassifyCalls); +#if WITH_EDITOR + // The ordinary tile classifier proves the live field's interval bounds. While the separate + // research field is selected, decline those proofs until its own complete final-field interval + // contract is wired into streaming; a conservative Mixed verdict keeps the test mesh complete. + if (VoxelForgeRedefinedDensityField::IsEnabled()) + { + return EVoxelTileClass::Mixed; + } +#endif + const EVoxelTileClass Verdict = ClassifyTileUncached( OriginVoxels, NormalizedStep, NormalizedCellsPerAxis, OutStats); if (Verdict == EVoxelTileClass::AllSolid) diff --git a/Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp b/Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp new file mode 100644 index 0000000..20f8d54 --- /dev/null +++ b/Source/VoxelForge/Private/VoxelRedefinedDensityField.cpp @@ -0,0 +1,1487 @@ +#include "VoxelRedefinedDensityField.h" + +#if WITH_EDITOR + +#include "HAL/IConsoleManager.h" +#include "VoxelCaveMorphology.h" +#include "VoxelDiffLayer.h" +#include "VoxelGenerator.h" +#include "VoxelPassageGeometry.h" +#include "VoxelStrateManager.h" +#include "VoxelStrateTypes.h" +#include "VoxelTypes.h" + +#include + +namespace VoxelForgeRedefinedDensityField +{ +namespace +{ + constexpr float DetailAmplitude = 1.4f + 0.9f + 0.45f + 2.5f; + constexpr float DetailBoundNumericalMargin = 1.0e-3f; + constexpr float BaseFieldLipschitzBound = 1.25f; + constexpr float QuinticFadeSlopeBound = 1.875f; + constexpr float MinimumCaveHeight = 48.0f; + constexpr float LandingFloorBlendVoxels = 96.0f; + constexpr float LandingBridgeRadius = 48.0f; + constexpr float PassageBoundPadding = 16.0f; + constexpr float VerticalShaftSpineRadius = 24.0f; + constexpr int32 VerticalShaftConnectorChildReach = 4; + constexpr float LandingApproachRadius = 32.0f; + constexpr uint32 VerticalShaftTreeSalt = 0x7A11u; + + TAutoConsoleVariable CVarResearchRedefinedDensityField( + TEXT("voxel.ResearchRedefinedDensityField"), 0, + TEXT("Use the default-off editor-only step 2d redefined density field."), + ECVF_Default); + TAutoConsoleVariable CVarResearchRedefinedDensityDetailScale( + TEXT("voxel.ResearchRedefinedDensityDetailScale"), 1.0f, + TEXT("Step 2d research detail amplitude scale; zero is a deliberate smoothing control."), + ECVF_Default); + + struct FLandingSite + { + FVector Position = FVector::ZeroVector; + float FloorZ = 0.0f; + float HalfWidth = 0.0f; + float CeilingZ = 0.0f; + float PadRadius = 0.0f; + float BlendRadius = LandingFloorBlendVoxels; + FVector ApproachEnd = FVector::ZeroVector; + }; + + struct FVerticalShaftNode + { + int32 CellX = 0; + int32 CellY = 0; + float X = 0.0f; + float Y = 0.0f; + float Radius = 0.0f; + }; + + struct FVerticalShaftEdge + { + FVector A = FVector::ZeroVector; + FVector B = FVector::ZeroVector; + float Radius = 0.0f; + }; + + struct FThreadShaftEdgeCache + { + const void* Plan = nullptr; + const UVoxelStrateManager* Manager = nullptr; + uint64 ManagerLifetimeId = 0; + uint32 LayoutVersion = 0; + int32 Seed = INT32_MIN; + int32 CellX = INT32_MIN; + int32 CellY = INT32_MIN; + TArray Edges; + }; + + struct FPassageBound + { + FVector Min = FVector(FLT_MAX, FLT_MAX, FLT_MAX); + FVector Max = FVector(-FLT_MAX, -FLT_MAX, -FLT_MAX); + FVector SegmentA = FVector::ZeroVector; + FVector SegmentB = FVector::ZeroVector; + float SupportRadius = 0.0f; + bool bIsSegment = false; + }; + + struct FStratePlan + { + int32 StrateIndex = INDEX_NONE; + int32 MinChunkZ = 0; + int32 MaxChunkZExclusive = 0; + ECaveGeneratorType Archetype = ECaveGeneratorType::TunnelNetwork; + float FloorZ = 0.0f; + float CeilingZ = 0.0f; + float CellSpacing = 128.0f; + float OffsetX = 0.0f; + float OffsetY = 0.0f; + float CorridorHalfWidth = 40.0f; + FVerticalShaftParams VerticalShaftParams; + int32 LandingHubIndex = INDEX_NONE; + TArray Landings; + }; + + struct FThreadPlanCache + { + const UVoxelStrateManager* Manager = nullptr; + uint64 ManagerLifetimeId = 0; + uint32 LayoutVersion = 0xFFFFFFFFu; + int32 Seed = INT32_MIN; + TArray Strates; + TArray PassageBounds; + }; + + thread_local FThreadPlanCache GPlanCache; + thread_local FThreadShaftEdgeCache GShaftEdgeCache; + + FORCEINLINE uint32 Mix(uint32 Value) + { + Value ^= Value >> 16; + Value *= 0x7FEB352Du; + Value ^= Value >> 15; + Value *= 0x846CA68Bu; + Value ^= Value >> 16; + return Value; + } + + FORCEINLINE uint32 Hash3(int32 X, int32 Y, int32 Z, uint32 Seed) + { + uint32 H = Seed ^ 0x9E3779B9u; + H ^= Mix(static_cast(X) + 0x85EBCA6Bu); + H = (H << 13) | (H >> 19); + H ^= Mix(static_cast(Y) + 0xC2B2AE35u); + H = (H << 11) | (H >> 21); + H ^= Mix(static_cast(Z) + 0x27D4EB2Fu); + return Mix(H); + } + + FORCEINLINE float HashValue(int32 X, int32 Y, int32 Z, uint32 Seed) + { + const uint32 H = Hash3(X, Y, Z, Seed); + return static_cast(H >> 8) * (2.0f / 16777215.0f) - 1.0f; + } + + FORCEINLINE float Fade(float T) + { + return T * T * T * (T * (T * 6.0f - 15.0f) + 10.0f); + } + + FORCEINLINE float Noise3D(float X, float Y, float Z, float CellSize, uint32 Seed) + { + const float Inv = 1.0f / CellSize; + const float SX = X * Inv; + const float SY = Y * Inv; + const float SZ = Z * Inv; + const int32 IX = FMath::FloorToInt(SX); + const int32 IY = FMath::FloorToInt(SY); + const int32 IZ = FMath::FloorToInt(SZ); + const float AX = Fade(SX - static_cast(IX)); + const float AY = Fade(SY - static_cast(IY)); + const float AZ = Fade(SZ - static_cast(IZ)); + + const float Z0Y0 = FMath::Lerp( + FMath::Lerp(HashValue(IX, IY, IZ, Seed), HashValue(IX + 1, IY, IZ, Seed), AX), + FMath::Lerp(HashValue(IX, IY + 1, IZ, Seed), HashValue(IX + 1, IY + 1, IZ, Seed), AX), AY); + const float Z0Y1 = FMath::Lerp( + FMath::Lerp(HashValue(IX, IY, IZ + 1, Seed), HashValue(IX + 1, IY, IZ + 1, Seed), AX), + FMath::Lerp(HashValue(IX, IY + 1, IZ + 1, Seed), HashValue(IX + 1, IY + 1, IZ + 1, Seed), AX), AY); + return FMath::Lerp(Z0Y0, Z0Y1, AZ); + } + + FORCEINLINE float DetailAt(float X, float Y, float Z, uint32 Seed) + { + const float Broad = Noise3D(X, Y, Z, 8.0f, Seed ^ 0x68BC21EBu); + const float Medium = Noise3D(X, Y, Z, 4.0f, Seed ^ 0x02E5BE93u); + const float Fine = Noise3D(X, Y, Z, 2.0f, Seed ^ 0x967A889Bu); + const float Micro = Noise3D(X, Y, Z, 1.0f, Seed ^ 0xA511E9B3u); + return Broad * 1.4f + Medium * 0.9f + Fine * 0.45f + Micro * 2.5f; + } + + FORCEINLINE float DistanceToPeriodicLine(float Position, float Spacing, float Offset) + { + const float Cell = (Position - Offset) / Spacing; + const float Fraction = Cell - static_cast(FMath::FloorToInt(Cell + 0.5f)); + return FMath::Abs(Fraction) * Spacing; + } + + FORCEINLINE float DistanceSquaredToSegmentXY( + float X, float Y, const FVector& A, const FVector& B) + { + const float ABX = B.X - A.X; + const float ABY = B.Y - A.Y; + const float LengthSquared = ABX * ABX + ABY * ABY; + const float T = LengthSquared > 1.0e-8f + ? FMath::Clamp(((X - A.X) * ABX + (Y - A.Y) * ABY) / LengthSquared, 0.0f, 1.0f) + : 0.0f; + const float DX = X - (A.X + T * ABX); + const float DY = Y - (A.Y + T * ABY); + return DX * DX + DY * DY; + } + + FORCEINLINE float SegmentParameterXY( + float X, float Y, const FVector& A, const FVector& B) + { + const float ABX = B.X - A.X; + const float ABY = B.Y - A.Y; + const float LengthSquared = ABX * ABX + ABY * ABY; + return LengthSquared > 1.0e-8f + ? FMath::Clamp(((X - A.X) * ABX + (Y - A.Y) * ABY) / LengthSquared, 0.0f, 1.0f) + : 0.0f; + } + + FORCEINLINE bool RollVerticalShaft( + const FVerticalShaftParams& Params, uint32 ShaftSeed, + int32 CellX, int32 CellY, FVerticalShaftNode& OutNode) + { + const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f); + const uint32 Hash = VoxelHash::Cell(CellX, CellY, ShaftSeed); + if (VoxelHash::ToFloat01(Hash) > Params.ShaftDensity) + { + return false; + } + OutNode.CellX = CellX; + OutNode.CellY = CellY; + OutNode.X = (CellX + 0.15f + + VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0x12345678u)) * 0.7f) * Spacing; + OutNode.Y = (CellY + 0.15f + + VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0x9ABCDEF0u)) * 0.7f) * Spacing; + OutNode.Radius = FMath::Lerp(Params.ShaftMinRadius, Params.ShaftMaxRadius, + VoxelHash::ToFloat01(VoxelHash::Mix(Hash ^ 0xBEEFu))); + return true; + } + + bool ResolveVerticalShaftParent( + const FVerticalShaftParams& Params, uint32 ShaftSeed, + const FVerticalShaftNode& Child, FVerticalShaftNode& OutParent) + { + const float ChildOriginSq = FMath::Square(Child.X) + FMath::Square(Child.Y); + bool bFoundParent = false; + float BestDistanceSq = FLT_MAX; + const auto Search = [&](int32 Radius) + { + for (int32 DY = -Radius; DY <= Radius; ++DY) + for (int32 DX = -Radius; DX <= Radius; ++DX) + { + FVerticalShaftNode Candidate; + if ((DX == 0 && DY == 0) + || !RollVerticalShaft(Params, ShaftSeed, + Child.CellX + DX, Child.CellY + DY, Candidate)) + { + continue; + } + const float CandidateOriginSq = FMath::Square(Candidate.X) + + FMath::Square(Candidate.Y); + if (!(CandidateOriginSq < ChildOriginSq)) + { + continue; + } + const float DistanceSq = FMath::Square(Child.X - Candidate.X) + + FMath::Square(Child.Y - Candidate.Y); + const bool bLowerCell = !bFoundParent + || Candidate.CellY < OutParent.CellY + || (Candidate.CellY == OutParent.CellY + && Candidate.CellX < OutParent.CellX); + if (DistanceSq < BestDistanceSq + || (DistanceSq == BestDistanceSq && bLowerCell)) + { + BestDistanceSq = DistanceSq; + OutParent = Candidate; + bFoundParent = true; + } + } + }; + + Search(2); + if (bFoundParent) + { + return true; + } + if (FMath::Abs(Child.CellX) <= 2 && FMath::Abs(Child.CellY) <= 2) + { + return false; + } + Search(3); + return bFoundParent; + } + + float ResolveVerticalShaftConnectorZ( + const FVerticalShaftParams& Params, uint32 LinkHash) + { + const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness; + const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness; + if (Params.LedgeSpacing > 0.0f && Params.LedgeDepth > 0.0f) + { + const float RelativeBottom = BottomZ - Params.StrateBottomWorldZ; + const float RelativeTop = TopZ - Params.StrateBottomWorldZ; + const int32 FirstPeriod = FMath::FloorToInt( + RelativeBottom / Params.LedgeSpacing); + const float Random01 = VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash)); + for (int32 PeriodOffset = 0; PeriodOffset <= 1; ++PeriodOffset) + { + const float PeriodStart = static_cast(FirstPeriod + PeriodOffset) + * Params.LedgeSpacing; + const float SafeStart = FMath::Max( + RelativeBottom, PeriodStart + Params.LedgeDepth + 0.01f); + const float SafeEnd = FMath::Min( + RelativeTop, PeriodStart + Params.LedgeSpacing + - Params.LedgeDepth - 0.01f); + if (SafeEnd > SafeStart) + { + return Params.StrateBottomWorldZ + + FMath::Lerp(SafeStart, SafeEnd, Random01); + } + } + } + return FMath::Lerp(BottomZ, TopZ, VoxelHash::ToFloat01(VoxelHash::Mix(LinkHash))); + } + + void RebuildVerticalShaftEdgeCache(const FStratePlan& Plan, float X, float Y) + { + const FVerticalShaftParams& Params = Plan.VerticalShaftParams; + const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f); + const int32 CellX = FMath::FloorToInt(X / Spacing); + const int32 CellY = FMath::FloorToInt(Y / Spacing); + if (GShaftEdgeCache.Plan == &Plan + && GShaftEdgeCache.Manager == GPlanCache.Manager + && GShaftEdgeCache.ManagerLifetimeId == GPlanCache.ManagerLifetimeId + && GShaftEdgeCache.LayoutVersion == GPlanCache.LayoutVersion + && GShaftEdgeCache.Seed == GPlanCache.Seed + && GShaftEdgeCache.CellX == CellX && GShaftEdgeCache.CellY == CellY) + { + return; + } + GShaftEdgeCache.Plan = &Plan; + GShaftEdgeCache.Manager = GPlanCache.Manager; + GShaftEdgeCache.ManagerLifetimeId = GPlanCache.ManagerLifetimeId; + GShaftEdgeCache.LayoutVersion = GPlanCache.LayoutVersion; + GShaftEdgeCache.Seed = GPlanCache.Seed; + GShaftEdgeCache.CellX = CellX; + GShaftEdgeCache.CellY = CellY; + GShaftEdgeCache.Edges.Reset(); + + const uint32 ShaftSeed = static_cast(GPlanCache.Seed) ^ 0x53686674u; + // A 36-voxel bore (9 m at 25 cm) keeps the deterministic route tree visible to the + // 2-voxel navigation/refinement probes while still allowing full-strength wall noise. + const float ConnectorRadius = FMath::Max3( + Params.ConnectorRadius, DetailAmplitude + 1.0f, 18.0f); + const float CellMinX = CellX * Spacing; + const float CellMaxX = CellMinX + Spacing; + const float CellMinY = CellY * Spacing; + const float CellMaxY = CellMinY + Spacing; + for (int32 ChildCellY = CellY - VerticalShaftConnectorChildReach; + ChildCellY <= CellY + VerticalShaftConnectorChildReach; ++ChildCellY) + for (int32 ChildCellX = CellX - VerticalShaftConnectorChildReach; + ChildCellX <= CellX + VerticalShaftConnectorChildReach; ++ChildCellX) + { + FVerticalShaftNode Child; + if (!RollVerticalShaft(Params, ShaftSeed, ChildCellX, ChildCellY, Child)) + { + continue; + } + FVerticalShaftNode Parent; + const bool bHasParent = ResolveVerticalShaftParent( + Params, ShaftSeed, Child, Parent); + const FVector ParentXY = bHasParent + ? FVector(Parent.X, Parent.Y, 0.0f) : FVector::ZeroVector; + const bool bMayReachCell = FMath::Max(Child.X, ParentXY.X) + ConnectorRadius >= CellMinX + && FMath::Min(Child.X, ParentXY.X) - ConnectorRadius <= CellMaxX + && FMath::Max(Child.Y, ParentXY.Y) + ConnectorRadius >= CellMinY + && FMath::Min(Child.Y, ParentXY.Y) - ConnectorRadius <= CellMaxY; + if (!bMayReachCell) + { + continue; + } + + const uint32 LinkHash = VoxelHash::Pair( + Child.CellX, Child.CellY, + bHasParent ? Parent.CellX : 0, + bHasParent ? Parent.CellY : 0, + ShaftSeed ^ VerticalShaftTreeSalt); + const float ConnectorZ = ResolveVerticalShaftConnectorZ(Params, LinkHash); + FVerticalShaftEdge& Edge = GShaftEdgeCache.Edges.AddDefaulted_GetRef(); + Edge.A = FVector(Child.X, Child.Y, ConnectorZ); + Edge.B = FVector(ParentXY.X, ParentXY.Y, ConnectorZ); + Edge.Radius = ConnectorRadius; + } + } + + float EvaluateVerticalShaftMacro( + const FStratePlan& Plan, float X, float Y, float Z) + { + const FVerticalShaftParams& Params = Plan.VerticalShaftParams; + const float Spacing = FMath::Max(Params.ShaftSpacing, 1.0f); + const int32 BaseCellX = FMath::FloorToInt(X / Spacing); + const int32 BaseCellY = FMath::FloorToInt(Y / Spacing); + const float VerticalPad = DetailAmplitude + 1.0f; + const float BottomZ = Params.StrateBottomWorldZ + Params.BoundarySealThickness; + const float TopZ = Params.StrateTopWorldZ - Params.BoundarySealThickness; + const float ShaftVertical = FMath::Min(Z - BottomZ, TopZ - Z); + float Macro = FMath::Min( + VerticalShaftSpineRadius + VerticalPad - FMath::Sqrt(X * X + Y * Y), + ShaftVertical); + + const uint32 ShaftSeed = static_cast(GPlanCache.Seed) ^ 0x53686674u; + for (int32 CellY = BaseCellY - 1; CellY <= BaseCellY + 1; ++CellY) + for (int32 CellX = BaseCellX - 1; CellX <= BaseCellX + 1; ++CellX) + { + FVerticalShaftNode Shaft; + if (!RollVerticalShaft(Params, ShaftSeed, CellX, CellY, Shaft)) + { + continue; + } + const float ShaftHorizontal = Shaft.Radius + VerticalPad + - FMath::Sqrt(FMath::Square(X - Shaft.X) + FMath::Square(Y - Shaft.Y)); + Macro = FMath::Max(Macro, FMath::Min(ShaftHorizontal, ShaftVertical)); + } + + RebuildVerticalShaftEdgeCache(Plan, X, Y); + for (const FVerticalShaftEdge& Edge : GShaftEdgeCache.Edges) + { + const float HorizontalSq = DistanceSquaredToSegmentXY(X, Y, Edge.A, Edge.B); + const float DistSq = HorizontalSq + FMath::Square(Z - Edge.A.Z); + Macro = FMath::Max(Macro, Edge.Radius - FMath::Sqrt(DistSq)); + } + return Macro; + } + + void EnsurePlan(const UVoxelGenerator& Generator) + { + const UVoxelStrateManager* Manager = Generator.StrateManager; + if (Manager == nullptr) + { + GPlanCache = FThreadPlanCache(); + return; + } + const uint64 Lifetime = Manager->GetCacheLifetimeId(); + const uint32 Version = Manager->GetLayoutVersion(); + if (GPlanCache.Manager == Manager + && GPlanCache.ManagerLifetimeId == Lifetime + && GPlanCache.LayoutVersion == Version + && GPlanCache.Seed == Generator.Seed) + { + return; + } + + GPlanCache = FThreadPlanCache(); + GPlanCache.Manager = Manager; + GPlanCache.ManagerLifetimeId = Lifetime; + GPlanCache.LayoutVersion = Version; + GPlanCache.Seed = Generator.Seed; + const TArray& Layout = Manager->GetLayout(); + GPlanCache.Strates.Reserve(Layout.Num()); + for (const FStrateSlot& Slot : Layout) + { + if (Slot.Definition == nullptr) + { + continue; + } + FStratePlan& Plan = GPlanCache.Strates.AddDefaulted_GetRef(); + Plan.StrateIndex = Slot.StrateIndex; + Plan.MinChunkZ = FMath::Min(Slot.BottomChunkZ, Slot.TopChunkZ); + Plan.MaxChunkZExclusive = FMath::Max(Slot.BottomChunkZ, Slot.TopChunkZ) + 1; + Plan.Archetype = Slot.Definition->GeneratorType; + if (Plan.Archetype == ECaveGeneratorType::VerticalShafts) + { + Plan.VerticalShaftParams = Manager->GetVerticalShaftParamsForChunk( + FIntVector(0, 0, Plan.MinChunkZ)); + } + const float Bottom = static_cast(Plan.MinChunkZ * CHUNK_SIZE); + const float Top = static_cast(Plan.MaxChunkZExclusive * CHUNK_SIZE); + Plan.FloorZ = (Bottom + Top) * 0.5f - 24.0f; + Plan.CeilingZ = Plan.FloorZ + 48.0f; + switch (Plan.Archetype) + { + case ECaveGeneratorType::FlatPlain: + Plan.CellSpacing = 160.0f; + Plan.CorridorHalfWidth = 54.0f; + break; + case ECaveGeneratorType::CrystalChamber: + Plan.CellSpacing = 120.0f; + Plan.CorridorHalfWidth = 42.0f; + break; + case ECaveGeneratorType::Maze: + Plan.CellSpacing = 96.0f; + Plan.CorridorHalfWidth = 30.0f; + break; + case ECaveGeneratorType::VerticalShafts: + Plan.CellSpacing = 112.0f; + Plan.CorridorHalfWidth = 36.0f; + break; + case ECaveGeneratorType::FloatingIslands: + Plan.CellSpacing = 112.0f; + Plan.CorridorHalfWidth = 34.0f; + break; + case ECaveGeneratorType::SurfaceWorld: + Plan.CellSpacing = 144.0f; + Plan.CorridorHalfWidth = 46.0f; + break; + default: + Plan.CellSpacing = 128.0f; + Plan.CorridorHalfWidth = 40.0f; + break; + } + const uint32 StrateSeed = static_cast(Generator.Seed) + ^ static_cast(Plan.StrateIndex * 7919 + 104729); + if (Plan.Archetype == ECaveGeneratorType::VerticalShafts) + { + // Keep the archetype's central origin spine open for its authored route probe. + Plan.OffsetX = 0.0f; + Plan.OffsetY = 0.0f; + } + else + { + Plan.OffsetX = static_cast(Hash3( + Plan.StrateIndex, Plan.MinChunkZ, 17, StrateSeed) >> 8) + * (Plan.CellSpacing / 16777216.0f); + Plan.OffsetY = static_cast(Hash3( + Plan.StrateIndex, Plan.MinChunkZ, 31, StrateSeed) >> 8) + * (Plan.CellSpacing / 16777216.0f); + } + } + + const TArray& Passages = Manager->GetPassages(); + for (const FVoxelPassage& Passage : Passages) + { + const auto AddSegmentBounds = []( + const FVector& A, const FVector& B, float Radius) + { + FPassageBound& Bounds = GPlanCache.PassageBounds.AddDefaulted_GetRef(); + const float R = FMath::Max(Radius, 8.0f) + PassageBoundPadding; + Bounds.SegmentA = A; + Bounds.SegmentB = B; + Bounds.SupportRadius = R; + Bounds.bIsSegment = true; + Bounds.Min = FVector( + FMath::Min(A.X, B.X) - R, + FMath::Min(A.Y, B.Y) - R, + FMath::Min(A.Z, B.Z) - R); + Bounds.Max = FVector( + FMath::Max(A.X, B.X) + R, + FMath::Max(A.Y, B.Y) + R, + FMath::Max(A.Z, B.Z) + R); + }; + if (Passage.ControlPoints.Num() >= 2 + && Passage.ControlRadii.Num() == Passage.ControlPoints.Num()) + { + for (int32 Segment = 0; Segment + 1 < Passage.ControlPoints.Num(); ++Segment) + { + AddSegmentBounds( + Passage.ControlPoints[Segment], Passage.ControlPoints[Segment + 1], + FMath::Max(Passage.Radius, + FMath::Max(FMath::Abs(Passage.ControlRadii[Segment]), + FMath::Abs(Passage.ControlRadii[Segment + 1])))); + } + } + else + { + AddSegmentBounds(Passage.UpperPoint, Passage.LowerPoint, Passage.Radius); + } + + const auto AddLandingBounds = [](const FVoxelPassageLanding& Landing) + { + const float XYExtent = Landing.HalfWidth + PassageBoundPadding; + const float ZExtent = FMath::Max( + Landing.HalfWidth, Landing.CeilingZ - Landing.FloorZ) + + PassageBoundPadding; + FPassageBound& Bounds = GPlanCache.PassageBounds.AddDefaulted_GetRef(); + const FVector Pad(XYExtent, XYExtent, ZExtent); + const FVector& P = Landing.StandingPoint; + Bounds.Min = P - Pad; + Bounds.Max = P + Pad; + }; + AddLandingBounds(Passage.UpperLanding); + AddLandingBounds(Passage.LowerLanding); + + for (FStratePlan& Plan : GPlanCache.Strates) + { + if (Plan.StrateIndex == Passage.UpperStrateIndex) + { + FLandingSite& Site = Plan.Landings.AddDefaulted_GetRef(); + Site.Position = Passage.UpperLanding.StandingPoint; + Site.FloorZ = Passage.UpperLanding.FloorZ; + Site.HalfWidth = Passage.UpperLanding.HalfWidth; + Site.CeilingZ = Passage.UpperLanding.CeilingZ; + } + if (Plan.StrateIndex == Passage.LowerStrateIndex) + { + FLandingSite& Site = Plan.Landings.AddDefaulted_GetRef(); + Site.Position = Passage.LowerLanding.StandingPoint; + Site.FloorZ = Passage.LowerLanding.FloorZ; + Site.HalfWidth = Passage.LowerLanding.HalfWidth; + Site.CeilingZ = Passage.LowerLanding.CeilingZ; + } + } + } + + for (FStratePlan& Plan : GPlanCache.Strates) + { + if (!Plan.Landings.IsEmpty()) + { + double FloorSum = 0.0; + int32 FloorCount = 0; + for (FLandingSite& Site : Plan.Landings) + { + if (!FMath::IsFinite(Site.FloorZ)) + { + continue; + } + FloorSum += Site.FloorZ; + ++FloorCount; + Site.PadRadius = FMath::Max(Site.HalfWidth + 24.0f, 40.0f); + } + if (FloorCount > 0) + { + const float Bottom = static_cast(Plan.MinChunkZ * CHUNK_SIZE); + const float Top = static_cast(Plan.MaxChunkZExclusive * CHUNK_SIZE); + Plan.FloorZ = FMath::Clamp( + static_cast(FloorSum / FloorCount), Bottom + 16.0f, Top - 72.0f); + Plan.CeilingZ = Plan.FloorZ + MinimumCaveHeight; + } + } + for (FLandingSite& Site : Plan.Landings) + { + const int32 XLaneIndex = FMath::RoundToInt( + (Site.Position.X - Plan.OffsetX) / Plan.CellSpacing); + const int32 YLaneIndex = FMath::RoundToInt( + (Site.Position.Y - Plan.OffsetY) / Plan.CellSpacing); + const float XLaneCoordinate = Plan.OffsetX + XLaneIndex * Plan.CellSpacing; + const float YLaneCoordinate = Plan.OffsetY + YLaneIndex * Plan.CellSpacing; + const float XLaneDistance = FMath::Abs(Site.Position.X - XLaneCoordinate); + const float YLaneDistance = FMath::Abs(Site.Position.Y - YLaneCoordinate); + Site.ApproachEnd = Site.Position; + if (XLaneDistance <= YLaneDistance) + { + Site.ApproachEnd.X = XLaneCoordinate; + } + else + { + Site.ApproachEnd.Y = YLaneCoordinate; + } + const float FloorDelta = FMath::Abs(Site.FloorZ - Plan.FloorZ); + Site.BlendRadius = FMath::Max( + LandingFloorBlendVoxels, Site.PadRadius + FloorDelta * 2.0f + 8.0f); + } + Plan.LandingHubIndex = INDEX_NONE; + for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex) + { + const FLandingSite& Candidate = Plan.Landings[SiteIndex]; + if (Plan.LandingHubIndex == INDEX_NONE) + { + Plan.LandingHubIndex = SiteIndex; + continue; + } + const FLandingSite& Current = Plan.Landings[Plan.LandingHubIndex]; + if (Candidate.Position.X < Current.Position.X + || (Candidate.Position.X == Current.Position.X + && Candidate.Position.Y < Current.Position.Y) + || (Candidate.Position.X == Current.Position.X + && Candidate.Position.Y == Current.Position.Y + && Candidate.FloorZ < Current.FloorZ)) + { + Plan.LandingHubIndex = SiteIndex; + } + } + } + } + + const FStratePlan* FindStrate(const FIntVector& Chunk) + { + for (const FStratePlan& Plan : GPlanCache.Strates) + { + if (Chunk.Z >= Plan.MinChunkZ && Chunk.Z < Plan.MaxChunkZExclusive) + { + return &Plan; + } + } + return nullptr; + } + + float EvaluateMacro(const FStratePlan& Plan, float X, float Y, float Z, + float* OutDetailScale) + { + float WeightedDelta = 0.0f; + float TotalWeight = 0.0f; + float LandingMacro = -FLT_MAX; + float Scale = 1.0f; + for (const FLandingSite& Site : Plan.Landings) + { + const float DX = X - Site.Position.X; + const float DY = Y - Site.Position.Y; + const float DistanceSquared = DX * DX + DY * DY; + const float HorizontalDistance = FMath::Sqrt(DistanceSquared); + if (DistanceSquared < Site.BlendRadius * Site.BlendRadius) + { + const float Weight = Fade(1.0f - HorizontalDistance / Site.BlendRadius); + WeightedDelta += Weight * (Site.FloorZ - Plan.FloorZ); + TotalWeight += Weight; + } + + const float RoomVertical = FMath::Min( + Z - Site.FloorZ, Site.FloorZ + MinimumCaveHeight - Z); + const float RoomAir = FMath::Min(Site.PadRadius - HorizontalDistance, RoomVertical); + LandingMacro = FMath::Max(LandingMacro, RoomAir); + + if (OutDetailScale != nullptr) + { + const float FlatRadius = FMath::Max(0.0f, Site.PadRadius - 3.0f); + if (DistanceSquared < FlatRadius * FlatRadius) + { + Scale = 0.0f; + } + } + } + const float Floor = Plan.FloorZ + WeightedDelta / FMath::Max(1.0f, TotalWeight); + const float Ceiling = Floor + (Plan.CeilingZ - Plan.FloorZ); + const float VerticalAir = FMath::Min(Z - Floor, Ceiling - Z); + const float XLaneDistance = DistanceToPeriodicLine( + X, Plan.CellSpacing, Plan.OffsetX); + const float YLaneDistance = DistanceToPeriodicLine( + Y, Plan.CellSpacing, Plan.OffsetY); + const float LaneDistance = FMath::Min(XLaneDistance, YLaneDistance); + const float GridAir = Plan.CorridorHalfWidth - LaneDistance; + float Macro = FMath::Max( + FMath::Min(VerticalAir, GridAir), LandingMacro); + if (Plan.Archetype == ECaveGeneratorType::VerticalShafts) + { + // Keep one continuous, walkable cavern horizon between the full-height shaft wells. + // The wells and their tree links add vertical routes; this band makes every authored + // passage landing part of the same deterministic navigation level. + Macro = FMath::Max(Macro, VerticalAir); + Macro = FMath::Max(Macro, EvaluateVerticalShaftMacro(Plan, X, Y, Z)); + } + for (const FLandingSite& Site : Plan.Landings) + { + const float ApproachHorizontal = LandingApproachRadius - FMath::Sqrt( + DistanceSquaredToSegmentXY(X, Y, Site.Position, Site.ApproachEnd)); + Macro = FMath::Max(Macro, FMath::Min(ApproachHorizontal, VerticalAir)); + } + if (Plan.LandingHubIndex != INDEX_NONE && Plan.Landings.Num() > 1) + { + const FLandingSite& Hub = Plan.Landings[Plan.LandingHubIndex]; + for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex) + { + if (SiteIndex == Plan.LandingHubIndex) + { + continue; + } + const FLandingSite& Site = Plan.Landings[SiteIndex]; + const float DistanceSquared = DistanceSquaredToSegmentXY( + X, Y, Site.Position, Hub.Position); + const float Horizontal = LandingBridgeRadius - FMath::Sqrt(DistanceSquared); + const float T = SegmentParameterXY(X, Y, Site.Position, Hub.Position); + const float BridgeFloor = FMath::Lerp(Site.FloorZ, Hub.FloorZ, T); + Macro = FMath::Max(Macro, FMath::Min( + Horizontal, + FMath::Min(Z - BridgeFloor, BridgeFloor + MinimumCaveHeight - Z))); + } + } + + if (OutDetailScale != nullptr) + { + *OutDetailScale = Scale; + } + return Macro; + } + + FORCEINLINE float EvaluateMacroFromBrick( + const FStratePlan& Plan, const FBrickEvaluationContext& Context, + float X, float Y, float Z, float* OutDetailScale) + { + const int32 LocalX = static_cast(X) - Context.BrickOrigin.X; + const int32 LocalY = static_cast(Y) - Context.BrickOrigin.Y; + const int32 XYIndex = LocalY * Context.BrickSize.X + LocalX; + const float Floor = Context.FloorByXY[XYIndex]; + const float Ceiling = Floor + (Plan.CeilingZ - Plan.FloorZ); + const float VerticalAir = FMath::Min(Z - Floor, Ceiling - Z); + float Macro = FMath::Min(VerticalAir, Context.GridAirByXY[XYIndex]); + for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex) + { + const FLandingSite& Site = Plan.Landings[SiteIndex]; + const float HorizontalRoom = Context.LandingHorizontalByXY[ + SiteIndex * Context.BrickSize.X * Context.BrickSize.Y + XYIndex]; + const float RoomVertical = FMath::Min( + Z - Site.FloorZ, Site.FloorZ + MinimumCaveHeight - Z); + Macro = FMath::Max(Macro, FMath::Min(HorizontalRoom, RoomVertical)); + } + const int32 XYCount = Context.BrickSize.X * Context.BrickSize.Y; + for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex) + { + const float ApproachHorizontal = Context.LandingApproachByXY[ + SiteIndex * XYCount + XYIndex]; + Macro = FMath::Max(Macro, FMath::Min(ApproachHorizontal, VerticalAir)); + } + if (Plan.LandingHubIndex != INDEX_NONE && Plan.Landings.Num() > 1) + { + for (int32 SiteIndex = 0; SiteIndex < Plan.Landings.Num(); ++SiteIndex) + { + if (SiteIndex == Plan.LandingHubIndex) + { + continue; + } + const int32 BridgeIndex = SiteIndex * XYCount + XYIndex; + const float BridgeFloor = Context.LandingBridgeFloorByXY[BridgeIndex]; + Macro = FMath::Max(Macro, FMath::Min( + Context.LandingBridgeHorizontalByXY[BridgeIndex], + FMath::Min(Z - BridgeFloor, + BridgeFloor + MinimumCaveHeight - Z))); + } + } + if (Plan.Archetype == ECaveGeneratorType::VerticalShafts) + { + Macro = FMath::Max(Macro, VerticalAir); + Macro = FMath::Max(Macro, EvaluateVerticalShaftMacro(Plan, X, Y, Z)); + } + if (OutDetailScale != nullptr) + { + *OutDetailScale = Context.DetailFadeByXY[XYIndex]; + } + return Macro; + } + + FORCEINLINE float DistanceSquaredToSegment( + const FVector& Point, const FVector& A, const FVector& B) + { + const FVector AB = B - A; + const float LengthSquared = AB.SizeSquared(); + const float T = LengthSquared > SMALL_NUMBER + ? FMath::Clamp(FVector::DotProduct(Point - A, AB) / LengthSquared, 0.0f, 1.0f) + : 0.0f; + return FVector::DistSquared(Point, A + AB * T); + } + + FORCEINLINE bool BoxMayIntersectPassageBound(const FBox& Box, const FPassageBound& Bounds) + { + if (!(Bounds.Min.X <= Box.Max.X && Bounds.Max.X >= Box.Min.X + && Bounds.Min.Y <= Box.Max.Y && Bounds.Max.Y >= Box.Min.Y + && Bounds.Min.Z <= Box.Max.Z && Bounds.Max.Z >= Box.Min.Z)) + { + return false; + } + if (!Bounds.bIsSegment) + { + return true; + } + const float Reach = Bounds.SupportRadius + Box.GetExtent().Size(); + return DistanceSquaredToSegment(Box.GetCenter(), Bounds.SegmentA, Bounds.SegmentB) + <= Reach * Reach; + } + + FORCEINLINE bool PointMayIntersectPassageBound( + const FVector& Point, const FPassageBound& Bounds) + { + if (Point.X < Bounds.Min.X || Point.X > Bounds.Max.X + || Point.Y < Bounds.Min.Y || Point.Y > Bounds.Max.Y + || Point.Z < Bounds.Min.Z || Point.Z > Bounds.Max.Z) + { + return false; + } + return !Bounds.bIsSegment + || DistanceSquaredToSegment(Point, Bounds.SegmentA, Bounds.SegmentB) + <= FMath::Square(Bounds.SupportRadius); + } + + bool IntersectsPassageBounds(const FBox& Box) + { + for (const FPassageBound& Bounds : GPlanCache.PassageBounds) + { + if (BoxMayIntersectPassageBound(Box, Bounds)) + { + return true; + } + } + return false; + } + + void FindPassageCandidates(const FBox& Box, TArray& OutCandidates) + { + OutCandidates.Reset(); + for (int32 Index = 0; Index < GPlanCache.PassageBounds.Num(); ++Index) + { + const FPassageBound& Bounds = GPlanCache.PassageBounds[Index]; + if (BoxMayIntersectPassageBound(Box, Bounds)) + { + OutCandidates.Add(Index); + } + } + } +} + +bool IsEnabled() +{ + return CVarResearchRedefinedDensityField.GetValueOnAnyThread() != 0; +} + +void SetEnabled(bool bEnabled) +{ + CVarResearchRedefinedDensityField->Set(bEnabled ? 1 : 0, ECVF_SetByCode); +} + +float GetDetailScale() +{ + return FMath::Clamp(CVarResearchRedefinedDensityDetailScale.GetValueOnAnyThread(), 0.0f, 4.0f); +} + +void SetDetailScale(float Scale) +{ + CVarResearchRedefinedDensityDetailScale->Set(FMath::Clamp(Scale, 0.0f, 4.0f), ECVF_SetByCode); +} + +void ResetThreadCache() +{ + GPlanCache = FThreadPlanCache(); + GShaftEdgeCache = FThreadShaftEdgeCache(); +} + +static float EvaluateInternal(const UVoxelGenerator& Generator, + float WorldX, float WorldY, float WorldZ, float DetailValue, bool bHasDetailValue, + float* OutPreEditDensity, float DetailScale, + const FBrickEvaluationContext* Context = nullptr) +{ + if (Context == nullptr) + { + EnsurePlan(Generator); + } + const FIntVector Chunk = Context != nullptr && Context->bChunkResolved + ? Context->Chunk + : FIntVector( + FMath::FloorToInt(WorldX / CHUNK_SIZE), + FMath::FloorToInt(WorldY / CHUNK_SIZE), + FMath::FloorToInt(WorldZ / CHUNK_SIZE)); + const FStratePlan* Plan = Context != nullptr && Context->bPlanResolved + ? static_cast(Context->PlanToken) + : FindStrate(Chunk); + float Result = 0.0f; + const UVoxelStrateManager* Manager = Generator.StrateManager; + + if (Plan == nullptr) + { + const bool bIsGapChunk = Manager != nullptr + && (Context != nullptr && Context->bChunkResolved + ? Context->bIsGapChunk : Manager->IsGapChunk(Chunk)); + if (bIsGapChunk) + { + float Internal = 8.0f; + Manager->ApplyPassageModifier(Internal, WorldX, WorldY, WorldZ, 8.0f, 0.0f); + Result = -Internal; + Manager->ApplyPassageNativeFloorMC(Result, WorldX, WorldY, WorldZ, 8.0f); + } + else + { + Result = 8.0f; + } + } + else + { + float DetailFade = 1.0f; + const float Macro = Context != nullptr && Context->bMacroXYPrecomputed + ? EvaluateMacroFromBrick(*Plan, *Context, WorldX, WorldY, WorldZ, &DetailFade) + : EvaluateMacro(*Plan, WorldX, WorldY, WorldZ, &DetailFade); + const uint32 DetailSeed = static_cast(Generator.Seed) + ^ static_cast(Plan->StrateIndex * 7919 + 104729); + const float FinalDetail = bHasDetailValue + ? DetailValue : DetailAt(WorldX, WorldY, WorldZ, DetailSeed); + Result = Macro + FinalDetail * DetailFade * DetailScale; + + if (Manager != nullptr && (Context == nullptr || Context->bMayContainPassage)) + { + const FVector Point(WorldX, WorldY, WorldZ); + bool bNearPassage = false; + const auto IsInsideBound = [&Point](const FPassageBound& Bounds) + { return PointMayIntersectPassageBound(Point, Bounds); }; + if (Context != nullptr) + { + for (const int32 BoundsIndex : Context->PassageCandidates) + { + if (IsInsideBound(GPlanCache.PassageBounds[BoundsIndex])) + { + bNearPassage = true; + break; + } + } + } + else + { + for (const FPassageBound& Bounds : GPlanCache.PassageBounds) + { + if (IsInsideBound(Bounds)) + { + bNearPassage = true; + break; + } + } + } + if (bNearPassage) + { + float Internal = -Result; + Manager->ApplyPassageModifier(Internal, WorldX, WorldY, WorldZ, 8.0f, 0.0f); + Result = -Internal; + Manager->ApplyPassageNativeFloorMC(Result, WorldX, WorldY, WorldZ, 8.0f); + } + } + } + + if (Generator.WorldRadiusVoxels > 0.0f) + { + const float Radial = FMath::Sqrt(WorldX * WorldX + WorldY * WorldY); + if (Radial > Generator.WorldRadiusVoxels) + { + const float EdgeDistance = Radial - Generator.WorldRadiusVoxels; + Result = FMath::Min(Result, -FMath::Min(EdgeDistance, Generator.EdgeSealThickness)); + } + else if (Generator.EdgeSealThickness > 0.0f + && Radial > Generator.WorldRadiusVoxels - Generator.EdgeSealThickness) + { + const float Alpha = (Generator.WorldRadiusVoxels - Radial) / Generator.EdgeSealThickness; + Result = FMath::Min(Result, Alpha * 8.0f - 8.0f); + } + } + + if (OutPreEditDensity != nullptr) + { + *OutPreEditDensity = Result; + } + if (Generator.DiffLayer != nullptr + && (Context != nullptr ? Context->bHasDiffMods : Generator.DiffLayer->HasAnyMods())) + { + Result -= Generator.DiffLayer->GetDensityOffset(Chunk, WorldX, WorldY, WorldZ); + } + return Result; +} + +float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY, float WorldZ, + float* OutPreEditDensity, float DetailScale) +{ + return EvaluateInternal(Generator, WorldX, WorldY, WorldZ, 0.0f, false, + OutPreEditDensity, DetailScale); +} + +float EvaluateWithDetailValue(const UVoxelGenerator& Generator, + float WorldX, float WorldY, float WorldZ, float DetailValue, + float* OutPreEditDensity, float DetailScale, + const FBrickEvaluationContext* Context) +{ + return EvaluateInternal(Generator, WorldX, WorldY, WorldZ, DetailValue, true, + OutPreEditDensity, DetailScale, Context); +} + +void BuildDetailBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, + FIntVector Size, float DetailScale, TArray& OutDetail, + FBrickEvaluationContext* OutContext) +{ + (void)DetailScale; + const int32 SampleCount = Size.X * Size.Y * Size.Z; + OutDetail.SetNumUninitialized(SampleCount); + FMemory::Memzero(OutDetail.GetData(), static_cast(SampleCount) * sizeof(float)); + if (OutContext != nullptr) + { + OutContext->PlanToken = nullptr; + OutContext->bPlanResolved = false; + OutContext->bMayContainPassage = true; + OutContext->bChunkResolved = false; + OutContext->bIsGapChunk = false; + OutContext->bHasDiffMods = false; + OutContext->bMacroXYPrecomputed = false; + OutContext->FloorByXY.Reset(); + OutContext->GridAirByXY.Reset(); + OutContext->DetailFadeByXY.Reset(); + OutContext->LandingHorizontalByXY.Reset(); + OutContext->LandingApproachByXY.Reset(); + OutContext->LandingBridgeHorizontalByXY.Reset(); + OutContext->LandingBridgeFloorByXY.Reset(); + OutContext->PassageCandidates.Reset(); + } + if (Size.X <= 0 || Size.Y <= 0 || Size.Z <= 0 + || Size.X > 8 || Size.Y > 8 || Size.Z > 8 + || Generator.StrateManager == nullptr) + { + return; + } + + EnsurePlan(Generator); + const FIntVector Chunk( + FMath::FloorToInt(static_cast(OriginVoxels.X) / CHUNK_SIZE), + FMath::FloorToInt(static_cast(OriginVoxels.Y) / CHUNK_SIZE), + FMath::FloorToInt(static_cast(OriginVoxels.Z) / CHUNK_SIZE)); + const FIntVector EndChunk( + FMath::FloorToInt(static_cast(OriginVoxels.X + Size.X - 1) / CHUNK_SIZE), + FMath::FloorToInt(static_cast(OriginVoxels.Y + Size.Y - 1) / CHUNK_SIZE), + FMath::FloorToInt(static_cast(OriginVoxels.Z + Size.Z - 1) / CHUNK_SIZE)); + const bool bStableStrate = Chunk.Z == EndChunk.Z; + const FStratePlan* Plan = FindStrate(Chunk); + if (OutContext != nullptr) + { + OutContext->BrickOrigin = OriginVoxels; + OutContext->BrickSize = Size; + OutContext->bPlanResolved = bStableStrate; + OutContext->PlanToken = bStableStrate ? Plan : nullptr; + OutContext->Chunk = Chunk; + OutContext->bChunkResolved = Chunk == EndChunk; + OutContext->bIsGapChunk = OutContext->bChunkResolved + && Generator.StrateManager->IsGapChunk(Chunk); + OutContext->bHasDiffMods = Generator.DiffLayer != nullptr + && Generator.DiffLayer->HasAnyMods(); + const FVector Min( + static_cast(OriginVoxels.X - 1), + static_cast(OriginVoxels.Y - 1), + static_cast(OriginVoxels.Z - 1)); + const FVector Max( + static_cast(OriginVoxels.X + Size.X), + static_cast(OriginVoxels.Y + Size.Y), + static_cast(OriginVoxels.Z + Size.Z)); + FindPassageCandidates(FBox(Min, Max), OutContext->PassageCandidates); + OutContext->bMayContainPassage = !OutContext->PassageCandidates.IsEmpty(); + } + if (!bStableStrate) + { + // A clipped ring-edge brick can span two strate plans. Keep its detail field exact by + // using the direct evaluator there; this is a tiny boundary-only fallback. + for (int32 Z = 0; Z < Size.Z; ++Z) + for (int32 Y = 0; Y < Size.Y; ++Y) + for (int32 X = 0; X < Size.X; ++X) + { + const int32 WorldZ = OriginVoxels.Z + Z; + const FIntVector SampleChunk(0, 0, + FMath::FloorToInt(static_cast(WorldZ) / CHUNK_SIZE)); + const FStratePlan* SamplePlan = FindStrate(SampleChunk); + if (SamplePlan != nullptr) + { + const uint32 SampleSeed = static_cast(Generator.Seed) + ^ static_cast(SamplePlan->StrateIndex * 7919 + 104729); + OutDetail[(Z * Size.Y + Y) * Size.X + X] = DetailAt( + static_cast(OriginVoxels.X + X), + static_cast(OriginVoxels.Y + Y), + static_cast(WorldZ), SampleSeed); + } + } + return; + } + if (Plan == nullptr) + { + return; + } + + if (OutContext != nullptr) + { + const int32 XYCount = Size.X * Size.Y; + OutContext->FloorByXY.SetNumUninitialized(XYCount); + OutContext->GridAirByXY.SetNumUninitialized(XYCount); + OutContext->DetailFadeByXY.SetNumUninitialized(XYCount); + OutContext->LandingHorizontalByXY.SetNumUninitialized(XYCount * Plan->Landings.Num()); + OutContext->LandingApproachByXY.SetNumUninitialized(XYCount * Plan->Landings.Num()); + OutContext->LandingBridgeHorizontalByXY.SetNumUninitialized(XYCount * Plan->Landings.Num()); + OutContext->LandingBridgeFloorByXY.SetNumUninitialized(XYCount * Plan->Landings.Num()); + for (int32 Y = 0; Y < Size.Y; ++Y) + for (int32 X = 0; X < Size.X; ++X) + { + const int32 XYIndex = Y * Size.X + X; + const float WorldX = static_cast(OriginVoxels.X + X); + const float WorldY = static_cast(OriginVoxels.Y + Y); + const float XLane = DistanceToPeriodicLine( + WorldX, Plan->CellSpacing, Plan->OffsetX); + const float YLane = DistanceToPeriodicLine( + WorldY, Plan->CellSpacing, Plan->OffsetY); + OutContext->GridAirByXY[XYIndex] = Plan->CorridorHalfWidth + - FMath::Min(XLane, YLane); + float WeightedDelta = 0.0f; + float TotalWeight = 0.0f; + float DetailFade = 1.0f; + for (int32 SiteIndex = 0; SiteIndex < Plan->Landings.Num(); ++SiteIndex) + { + const FLandingSite& Site = Plan->Landings[SiteIndex]; + const float DX = WorldX - Site.Position.X; + const float DY = WorldY - Site.Position.Y; + const float DistanceSquared = DX * DX + DY * DY; + const float Distance = FMath::Sqrt(DistanceSquared); + if (DistanceSquared < Site.BlendRadius * Site.BlendRadius) + { + const float Weight = Fade(1.0f - Distance / Site.BlendRadius); + WeightedDelta += Weight * (Site.FloorZ - Plan->FloorZ); + TotalWeight += Weight; + } + OutContext->LandingHorizontalByXY[SiteIndex * XYCount + XYIndex] = + Site.PadRadius - Distance; + OutContext->LandingApproachByXY[SiteIndex * XYCount + XYIndex] = + LandingApproachRadius - FMath::Sqrt(DistanceSquaredToSegmentXY( + WorldX, WorldY, Site.Position, Site.ApproachEnd)); + float BridgeHorizontal = -FLT_MAX; + float BridgeFloor = Plan->FloorZ; + if (Plan->LandingHubIndex != INDEX_NONE + && Plan->Landings.Num() > 1 + && SiteIndex != Plan->LandingHubIndex) + { + const FLandingSite& Hub = Plan->Landings[Plan->LandingHubIndex]; + const float T = SegmentParameterXY( + WorldX, WorldY, Site.Position, Hub.Position); + BridgeHorizontal = LandingBridgeRadius - FMath::Sqrt( + DistanceSquaredToSegmentXY(WorldX, WorldY, Site.Position, Hub.Position)); + BridgeFloor = FMath::Lerp(Site.FloorZ, Hub.FloorZ, T); + } + OutContext->LandingBridgeHorizontalByXY[SiteIndex * XYCount + XYIndex] = + BridgeHorizontal; + OutContext->LandingBridgeFloorByXY[SiteIndex * XYCount + XYIndex] = BridgeFloor; + const float FlatRadius = FMath::Max(0.0f, Site.PadRadius - 3.0f); + if (DistanceSquared < FlatRadius * FlatRadius) + { + DetailFade = 0.0f; + } + } + OutContext->FloorByXY[XYIndex] = Plan->FloorZ + + WeightedDelta / FMath::Max(1.0f, TotalWeight); + OutContext->DetailFadeByXY[XYIndex] = DetailFade; + } + OutContext->bMacroXYPrecomputed = true; + } + + const uint32 Seed = static_cast(Generator.Seed) + ^ static_cast(Plan->StrateIndex * 7919 + 104729); + const int32 CountX = Size.X; + const int32 CountY = Size.Y; + const auto SampleIndex = [CountX, CountY](int32 X, int32 Y, int32 Z) + { + return (Z * CountY + Y) * CountX + X; + }; + const auto AddNoiseBand = [&](int32 CellSize, uint32 BandSeed, float Amplitude) + { + const int32 MinCellX = FMath::FloorToInt( + static_cast(OriginVoxels.X) / static_cast(CellSize)); + const int32 MinCellY = FMath::FloorToInt( + static_cast(OriginVoxels.Y) / static_cast(CellSize)); + const int32 MinCellZ = FMath::FloorToInt( + static_cast(OriginVoxels.Z) / static_cast(CellSize)); + const int32 MaxCellX = FMath::FloorToInt( + static_cast(OriginVoxels.X + Size.X - 1) / static_cast(CellSize)); + const int32 MaxCellY = FMath::FloorToInt( + static_cast(OriginVoxels.Y + Size.Y - 1) / static_cast(CellSize)); + const int32 MaxCellZ = FMath::FloorToInt( + static_cast(OriginVoxels.Z + Size.Z - 1) / static_cast(CellSize)); + const int32 GridX = MaxCellX - MinCellX + 2; + const int32 GridY = MaxCellY - MinCellY + 2; + const int32 GridZ = MaxCellZ - MinCellZ + 2; + float Grid[1000]; + check(GridX * GridY * GridZ <= UE_ARRAY_COUNT(Grid)); + for (int32 Z = 0; Z < GridZ; ++Z) + for (int32 Y = 0; Y < GridY; ++Y) + for (int32 X = 0; X < GridX; ++X) + { + Grid[(Z * GridY + Y) * GridX + X] = HashValue( + MinCellX + X, MinCellY + Y, MinCellZ + Z, BandSeed); + } + + for (int32 Z = 0; Z < Size.Z; ++Z) + for (int32 Y = 0; Y < Size.Y; ++Y) + for (int32 X = 0; X < Size.X; ++X) + { + const float SX = static_cast(OriginVoxels.X + X) + / static_cast(CellSize); + const float SY = static_cast(OriginVoxels.Y + Y) + / static_cast(CellSize); + const float SZ = static_cast(OriginVoxels.Z + Z) + / static_cast(CellSize); + const int32 IX = FMath::FloorToInt(SX) - MinCellX; + const int32 IY = FMath::FloorToInt(SY) - MinCellY; + const int32 IZ = FMath::FloorToInt(SZ) - MinCellZ; + const float AX = Fade(SX - static_cast(FMath::FloorToInt(SX))); + const float AY = Fade(SY - static_cast(FMath::FloorToInt(SY))); + const float AZ = Fade(SZ - static_cast(FMath::FloorToInt(SZ))); + const int32 Index = (IZ * GridY + IY) * GridX + IX; + const float Value = CellSize == 1 + ? Grid[Index] + : FMath::Lerp( + FMath::Lerp( + FMath::Lerp(Grid[Index], Grid[Index + 1], AX), + FMath::Lerp(Grid[Index + GridX], Grid[Index + GridX + 1], AX), AY), + FMath::Lerp( + FMath::Lerp(Grid[Index + GridX * GridY], + Grid[Index + GridX * GridY + 1], AX), + FMath::Lerp(Grid[Index + GridX * GridY + GridX], + Grid[Index + GridX * GridY + GridX + 1], AX), AY), AZ); + OutDetail[SampleIndex(X, Y, Z)] += Value * Amplitude; + } + }; + + AddNoiseBand(8, Seed ^ 0x68BC21EBu, 1.4f); + AddNoiseBand(4, Seed ^ 0x02E5BE93u, 0.9f); + AddNoiseBand(2, Seed ^ 0x967A889Bu, 0.45f); + AddNoiseBand(1, Seed ^ 0xA511E9B3u, 2.5f); +} + +bool TryClassifyBrick(const UVoxelGenerator& Generator, FIntVector OriginVoxels, + FIntVector Size, bool& bOutAir, float DetailScale, + bool* bOutEvaluatedMacroCentre, + EBrickFallbackReason* OutFallbackReason) +{ + if (OutFallbackReason != nullptr) + { + *OutFallbackReason = EBrickFallbackReason::UnsupportedState; + } + if (bOutEvaluatedMacroCentre != nullptr) + { + *bOutEvaluatedMacroCentre = false; + } + if (Size.X <= 0 || Size.Y <= 0 || Size.Z <= 0 + || Size.X > 8 || Size.Y > 8 || Size.Z > 8 + || Generator.StrateManager == nullptr + || Generator.WorldRadiusVoxels != 0.0f + || (Generator.DiffLayer != nullptr && Generator.DiffLayer->HasAnyMods())) + { + return false; + } + EnsurePlan(Generator); + const int32 MinChunkX = FMath::FloorToInt( + static_cast(OriginVoxels.X - 1) / CHUNK_SIZE); + const int32 MaxChunkX = FMath::FloorToInt( + static_cast(OriginVoxels.X + Size.X) / CHUNK_SIZE); + const int32 MinChunkY = FMath::FloorToInt( + static_cast(OriginVoxels.Y - 1) / CHUNK_SIZE); + const int32 MaxChunkY = FMath::FloorToInt( + static_cast(OriginVoxels.Y + Size.Y) / CHUNK_SIZE); + const int32 MinChunkZ = FMath::FloorToInt( + static_cast(OriginVoxels.Z - 1) / CHUNK_SIZE); + const int32 MaxChunkZ = FMath::FloorToInt( + static_cast(OriginVoxels.Z + Size.Z) / CHUNK_SIZE); + const FIntVector Chunk( + MinChunkX, MinChunkY, MinChunkZ); + const FStratePlan* Plan = FindStrate(Chunk); + for (int32 ChunkZ = MinChunkZ; ChunkZ <= MaxChunkZ; ++ChunkZ) + { + if (FindStrate(FIntVector(MinChunkX, MinChunkY, ChunkZ)) != Plan) + { + if (OutFallbackReason != nullptr) + { + *OutFallbackReason = EBrickFallbackReason::ChunkOrStrateBoundary; + } + return false; + } + } + const FVector Min( + static_cast(OriginVoxels.X - 1), + static_cast(OriginVoxels.Y - 1), + static_cast(OriginVoxels.Z - 1)); + const FVector Max( + static_cast(OriginVoxels.X + Size.X), + static_cast(OriginVoxels.Y + Size.Y), + static_cast(OriginVoxels.Z + Size.Z)); + const FBox Expanded(Min, Max); + if (IntersectsPassageBounds(Expanded)) + { + if (OutFallbackReason != nullptr) + { + *OutFallbackReason = EBrickFallbackReason::PassageBounds; + } + return false; + } + if (Plan == nullptr) + { + // Outside a strate there is no added detail: gaps are bedrock and out-of-layout chunks + // are air. Passage bounds above keep all carved tubes, landings, and support writes mixed. + const bool bGap = Generator.StrateManager->IsGapChunk(Chunk); + for (int32 ChunkZ = MinChunkZ; ChunkZ <= MaxChunkZ; ++ChunkZ) + for (int32 ChunkY = MinChunkY; ChunkY <= MaxChunkY; ++ChunkY) + for (int32 ChunkX = MinChunkX; ChunkX <= MaxChunkX; ++ChunkX) + { + if (Generator.StrateManager->IsGapChunk(FIntVector(ChunkX, ChunkY, ChunkZ)) != bGap) + { + if (OutFallbackReason != nullptr) + { + *OutFallbackReason = EBrickFallbackReason::ChunkOrStrateBoundary; + } + return false; + } + } + bOutAir = !bGap; + if (OutFallbackReason != nullptr) + { + *OutFallbackReason = EBrickFallbackReason::None; + } + return true; + } + if (Plan->StrateIndex < 0 + || MinChunkZ < Plan->MinChunkZ || MaxChunkZ >= Plan->MaxChunkZExclusive) + { + if (OutFallbackReason != nullptr) + { + *OutFallbackReason = EBrickFallbackReason::ChunkOrStrateBoundary; + } + return false; + } + + // Bound the spatial landing-floor blend over this brick instead of disabling proofs near + // every landing. The floor is a weighted average while the total weight is >= 1, and an + // unnormalised sum below that threshold; these two derivative bounds cover both branches. + double SumAbsDeltaSlope = 0.0; + double SumInverseRadiusSlope = 0.0; + double LandingBridgeSlopeBound = 0.0; + float MaxAbsFloorDelta = 0.0f; + for (const FLandingSite& Site : Plan->Landings) + { + const float ClosestX = FMath::Clamp(Site.Position.X, Min.X, Max.X); + const float ClosestY = FMath::Clamp(Site.Position.Y, Min.Y, Max.Y); + const float DX = ClosestX - Site.Position.X; + const float DY = ClosestY - Site.Position.Y; + if (DX * DX + DY * DY > Site.BlendRadius * Site.BlendRadius) + { + continue; + } + const float AbsDelta = FMath::Abs(Site.FloorZ - Plan->FloorZ); + MaxAbsFloorDelta = FMath::Max(MaxAbsFloorDelta, AbsDelta); + SumAbsDeltaSlope += QuinticFadeSlopeBound * AbsDelta / Site.BlendRadius; + SumInverseRadiusSlope += QuinticFadeSlopeBound / Site.BlendRadius; + } + if (Plan->LandingHubIndex != INDEX_NONE && Plan->Landings.Num() > 1) + { + const FLandingSite& Hub = Plan->Landings[Plan->LandingHubIndex]; + for (int32 SiteIndex = 0; SiteIndex < Plan->Landings.Num(); ++SiteIndex) + { + if (SiteIndex == Plan->LandingHubIndex) + { + continue; + } + const FLandingSite& Site = Plan->Landings[SiteIndex]; + const float SegmentLength = FMath::Sqrt(FMath::Square(Hub.Position.X - Site.Position.X) + + FMath::Square(Hub.Position.Y - Site.Position.Y)); + if (SegmentLength <= KINDA_SMALL_NUMBER) + { + continue; + } + const float MinX = FMath::Min(Hub.Position.X, Site.Position.X) - LandingBridgeRadius; + const float MaxX = FMath::Max(Hub.Position.X, Site.Position.X) + LandingBridgeRadius; + const float MinY = FMath::Min(Hub.Position.Y, Site.Position.Y) - LandingBridgeRadius; + const float MaxY = FMath::Max(Hub.Position.Y, Site.Position.Y) + LandingBridgeRadius; + if (MaxX < Min.X || MinX > Max.X || MaxY < Min.Y || MinY > Max.Y) + { + continue; + } + LandingBridgeSlopeBound = FMath::Max( + LandingBridgeSlopeBound, + static_cast(FMath::Abs(Hub.FloorZ - Site.FloorZ) / SegmentLength)); + } + } + const double FloorSlopeBound = FMath::Max( + FMath::Max(SumAbsDeltaSlope, 2.0 * MaxAbsFloorDelta * SumInverseRadiusSlope), + LandingBridgeSlopeBound); + const float MacroLipschitzBound = FMath::Max( + BaseFieldLipschitzBound, + FMath::Sqrt(1.0f + FMath::Square(static_cast(FloorSlopeBound))) + 0.25f); + + const FVector Centre = (Min + Max) * 0.5f; + const FVector HalfExtent = (Max - Min) * 0.5f; + const float Radius = HalfExtent.Size(); + if (bOutEvaluatedMacroCentre != nullptr) + { + *bOutEvaluatedMacroCentre = true; + } + const float Macro = EvaluateMacro(*Plan, Centre.X, Centre.Y, Centre.Z, nullptr); + const float FinalBound = MacroLipschitzBound * Radius + + DetailAmplitude * FMath::Abs(DetailScale) + + DetailBoundNumericalMargin * FMath::Max(1.0f, FMath::Abs(DetailScale)); + if (Macro > FinalBound) + { + bOutAir = true; + if (OutFallbackReason != nullptr) + { + *OutFallbackReason = EBrickFallbackReason::None; + } + return true; + } + if (Macro < -FinalBound) + { + bOutAir = false; + if (OutFallbackReason != nullptr) + { + *OutFallbackReason = EBrickFallbackReason::None; + } + return true; + } + if (OutFallbackReason != nullptr) + { + *OutFallbackReason = EBrickFallbackReason::MixedMacroInterval; + } + return false; +} +} + +#endif // WITH_EDITOR diff --git a/Source/VoxelForge/Public/VoxelRedefinedDensityField.h b/Source/VoxelForge/Public/VoxelRedefinedDensityField.h new file mode 100644 index 0000000..20741dc --- /dev/null +++ b/Source/VoxelForge/Public/VoxelRedefinedDensityField.h @@ -0,0 +1,90 @@ +#pragma once + +#include "CoreMinimal.h" + +class UVoxelGenerator; + +#if WITH_EDITOR +/** + * Step 2d editor research field. It is intentionally separate from the gameplay evaluator and + * is selected only by the default-off voxel.ResearchRedefinedDensityField console variable. + * + * The field is a connected, archetype-sized cave lattice with an authored walkable floor at each + * passage landing. Its macro shape is evaluated directly in world space; bounded, continuous + * hash-value noise is added at every full-resolution sample. The brick classifier is permitted to + * skip only when a Lipschitz bound on the macro field plus the exact maximum detail amplitude + * proves that the final sign is uniform throughout the brick and its marching-cubes halo. + */ +namespace VoxelForgeRedefinedDensityField +{ + /** Per-worker, per-brick state. The opaque plan pointer is valid until the layout changes. */ + struct FBrickEvaluationContext + { + const void* PlanToken = nullptr; + bool bPlanResolved = false; + bool bMayContainPassage = true; + bool bChunkResolved = false; + bool bIsGapChunk = false; + bool bHasDiffMods = false; + bool bMacroXYPrecomputed = false; + FIntVector BrickOrigin = FIntVector::ZeroValue; + FIntVector BrickSize = FIntVector::ZeroValue; + FIntVector Chunk = FIntVector::ZeroValue; + TArray FloorByXY; + TArray GridAirByXY; + TArray DetailFadeByXY; + TArray LandingHorizontalByXY; + TArray LandingApproachByXY; + TArray LandingBridgeHorizontalByXY; + TArray LandingBridgeFloorByXY; + TArray PassageCandidates; + }; + + enum class EBrickFallbackReason : uint8 + { + None, + UnsupportedState, + ChunkOrStrateBoundary, + PassageBounds, + MixedMacroInterval + }; + + struct FBrickCounters + { + uint64 Tested = 0; + uint64 Skipped = 0; + uint64 EvaluatedSamples = 0; + }; + + VOXELFORGE_API bool IsEnabled(); + VOXELFORGE_API void SetEnabled(bool bEnabled); + VOXELFORGE_API float GetDetailScale(); + VOXELFORGE_API void SetDetailScale(float Scale); + + /** Evaluate the new field, including existing passage carving and player diffs. */ + VOXELFORGE_API float Evaluate(const UVoxelGenerator& Generator, float WorldX, float WorldY, + float WorldZ, float* OutPreEditDensity = nullptr, + float DetailScale = 1.0f); + + /** Build mathematically identical multi-scale noise for one integer-lattice brick. */ + VOXELFORGE_API void BuildDetailBrick(const UVoxelGenerator& Generator, + FIntVector OriginVoxels, FIntVector Size, float DetailScale, + TArray& OutDetail, FBrickEvaluationContext* OutContext = nullptr); + + /** Evaluate with a detail value produced by BuildDetailBrick. */ + VOXELFORGE_API float EvaluateWithDetailValue(const UVoxelGenerator& Generator, + float WorldX, float WorldY, float WorldZ, float DetailValue, + float* OutPreEditDensity = nullptr, float DetailScale = 1.0f, + const FBrickEvaluationContext* Context = nullptr); + + /** Prove a world-aligned box has one final density sign; false means evaluate every point. */ + VOXELFORGE_API bool TryClassifyBrick( + const UVoxelGenerator& Generator, FIntVector OriginVoxels, FIntVector Size, + bool& bOutAir, float DetailScale = 1.0f, + bool* bOutEvaluatedMacroCentre = nullptr, + EBrickFallbackReason* OutFallbackReason = nullptr); + + /** Reset this thread's immutable plan cache; used by deterministic editor experiments. */ + VOXELFORGE_API void ResetThreadCache(); +} +#endif diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp index 6c79e4b..142b984 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeExploreCommandlet.cpp @@ -34,6 +34,7 @@ #include "VoxelGenerator.h" #include "VoxelGeometryHash.h" #include "VoxelMarchingCubesMesher.h" +#include "VoxelRedefinedDensityField.h" #include "VoxelNoise.h" #include "VoxelPassageGeometry.h" #include "VoxelSettings.h" @@ -1509,6 +1510,9 @@ struct FRegionEvalStackSamplerSession } }; +bool SavePng(const FString& FilePath, const TArray& Pixels, + int32 Width, int32 Height, FString& OutError); + int32 FindRoomGraphRecipeIndex(int32 Seed) { for (int32 Index = 0; Index < 64; ++Index) @@ -2511,6 +2515,829 @@ int32 RunRegionEval(const FString& Params) return bPassed ? 0 : 2; } +struct FStep2dSurfaceSpectrum +{ + uint64 SurfaceBandSamples = 0; + double RMSLag1 = 0.0; + double RMSLag2 = 0.0; + double RMSLag4 = 0.0; +}; + +struct FStep2dBoundaryStats +{ + uint64 BrickBoundarySamples = 0; + uint64 InteriorSamples = 0; + uint64 ChunkBoundarySamples = 0; + uint64 FeatureCellBoundarySamples = 0; + uint64 FeatureCellInteriorSamples = 0; + double BrickJumpRms = 0.0; + double InteriorJumpRms = 0.0; + double ChunkBoundaryJumpRms = 0.0; + double FeatureCellJumpRms = 0.0; + double FeatureCellInteriorJumpRms = 0.0; + double BrickJumpMax = 0.0; + double ChunkBoundaryJumpMax = 0.0; + double FeatureCellJumpMax = 0.0; +}; + +struct FStep2dBrickProofAudit +{ + uint64 CorrectlySkippedBricksAudited = 0; + uint64 CorrectProofSamplesChecked = 0; + uint64 CorrectProofViolations = 0; + uint64 DeliberatelyUndersizedBoundClaimsChecked = 0; + uint64 DeliberateBoundDisagreements = 0; +}; + +FStep2dBrickProofAudit AuditStep2dBrickProofs( + const UVoxelGenerator& Generator, + FIntVector Origin, + int32 Dimension) +{ + FStep2dBrickProofAudit Result; + const FIntVector Last = Origin + FIntVector(Dimension - 1, Dimension - 1, Dimension - 1); + const FIntVector FirstBrick( + FMath::FloorToInt(static_cast(Origin.X) / 8.0f), + FMath::FloorToInt(static_cast(Origin.Y) / 8.0f), + FMath::FloorToInt(static_cast(Origin.Z) / 8.0f)); + const FIntVector LastBrick( + FMath::FloorToInt(static_cast(Last.X) / 8.0f), + FMath::FloorToInt(static_cast(Last.Y) / 8.0f), + FMath::FloorToInt(static_cast(Last.Z) / 8.0f)); + uint64 CorrectSkipOrdinal = 0; + for (int32 BZ = FirstBrick.Z; BZ <= LastBrick.Z; ++BZ) + for (int32 BY = FirstBrick.Y; BY <= LastBrick.Y; ++BY) + for (int32 BX = FirstBrick.X; BX <= LastBrick.X; ++BX) + { + const FIntVector BrickWorldMin(BX * 8, BY * 8, BZ * 8); + const FIntVector BrickOrigin( + FMath::Max(Origin.X, BrickWorldMin.X), + FMath::Max(Origin.Y, BrickWorldMin.Y), + FMath::Max(Origin.Z, BrickWorldMin.Z)); + const FIntVector End( + FMath::Min(BrickWorldMin.X + 8, Origin.X + Dimension), + FMath::Min(BrickWorldMin.Y + 8, Origin.Y + Dimension), + FMath::Min(BrickWorldMin.Z + 8, Origin.Z + Dimension)); + const FIntVector Size = End - BrickOrigin; + bool bProvenAir = false; + if (VoxelForgeRedefinedDensityField::TryClassifyBrick( + Generator, BrickOrigin, Size, bProvenAir, 1.0f)) + { + const bool bAuditThisSkip = (CorrectSkipOrdinal++ % 128u) == 0u; + if (bAuditThisSkip) + { + ++Result.CorrectlySkippedBricksAudited; + for (int32 Z = -1; Z <= Size.Z; ++Z) + for (int32 Y = -1; Y <= Size.Y; ++Y) + for (int32 X = -1; X <= Size.X; ++X) + { + const float Density = VoxelForgeRedefinedDensityField::Evaluate( + Generator, + static_cast(BrickOrigin.X + X), + static_cast(BrickOrigin.Y + Y), + static_cast(BrickOrigin.Z + Z), nullptr, 1.0f); + ++Result.CorrectProofSamplesChecked; + if ((Density > 0.0f) != bProvenAir) + { + ++Result.CorrectProofViolations; + } + } + } + } + + // Negative control: classify while omitting all detail, then check the supported + // maximum detail amplitude. A valid supremum must reject at least some of these + // sign claims; otherwise this audit would not demonstrate that it can catch a bad bound. + bool bDeliberatelyClaimedAir = false; + if (VoxelForgeRedefinedDensityField::TryClassifyBrick( + Generator, BrickOrigin, Size, bDeliberatelyClaimedAir, 0.0f)) + { + ++Result.DeliberatelyUndersizedBoundClaimsChecked; + for (int32 Z = 0; Z < Size.Z; ++Z) + for (int32 Y = 0; Y < Size.Y; ++Y) + for (int32 X = 0; X < Size.X; ++X) + { + const float MaximumDetailDensity = VoxelForgeRedefinedDensityField::Evaluate( + Generator, + static_cast(BrickOrigin.X + X), + static_cast(BrickOrigin.Y + Y), + static_cast(BrickOrigin.Z + Z), + nullptr, + 4.0f); + if ((MaximumDetailDensity > 0.0f) != bDeliberatelyClaimedAir) + { + ++Result.DeliberateBoundDisagreements; + } + } + } + } + return Result; +} + +FStep2dSurfaceSpectrum ComputeStep2dSpectrum(const TArray& Density, int32 Dimension) +{ + FStep2dSurfaceSpectrum Result; + double SumSquares[3] = {0.0, 0.0, 0.0}; + uint64 Counts[3] = {0, 0, 0}; + const int32 Lags[] = {1, 2, 4}; + for (int32 Z = 4; Z < Dimension - 4; ++Z) + for (int32 Y = 4; Y < Dimension - 4; ++Y) + for (int32 X = 4; X < Dimension - 4; ++X) + { + const int32 CenterIndex = (Z * Dimension + Y) * Dimension + X; + const float Center = Density[CenterIndex]; + if (!FMath::IsFinite(Center) || FMath::Abs(Center) > 12.0f) + { + continue; + } + ++Result.SurfaceBandSamples; + for (int32 LagIndex = 0; LagIndex < UE_ARRAY_COUNT(Lags); ++LagIndex) + { + const int32 Lag = Lags[LagIndex]; + const float Laplacian = + Density[CenterIndex - Lag] + Density[CenterIndex + Lag] + + Density[CenterIndex - Lag * Dimension] + + Density[CenterIndex + Lag * Dimension] + + Density[CenterIndex - Lag * Dimension * Dimension] + + Density[CenterIndex + Lag * Dimension * Dimension] + - 6.0f * Center; + SumSquares[LagIndex] += static_cast(Laplacian) * Laplacian; + ++Counts[LagIndex]; + } + } + Result.RMSLag1 = Counts[0] > 0 ? FMath::Sqrt(SumSquares[0] / Counts[0]) : 0.0; + Result.RMSLag2 = Counts[1] > 0 ? FMath::Sqrt(SumSquares[1] / Counts[1]) : 0.0; + Result.RMSLag4 = Counts[2] > 0 ? FMath::Sqrt(SumSquares[2] / Counts[2]) : 0.0; + return Result; +} + +FIntVector SelectStep2dMeshOrigin( + const TArray& OldDensity, + const TArray& NewDensity, + FIntVector RingOrigin, + int32 Dimension, + bool& bOutContainsBothIsoBands) +{ + bOutContainsBothIsoBands = false; + float BestCombinedBand = FLT_MAX; + FIntVector BestSample(Dimension / 2, Dimension / 2, Dimension / 2); + for (int32 Z = 32; Z < Dimension - 32; ++Z) + for (int32 Y = 32; Y < Dimension - 32; ++Y) + for (int32 X = 32; X < Dimension - 32; ++X) + { + const int32 Index = (Z * Dimension + Y) * Dimension + X; + const float OldAbs = FMath::Abs(OldDensity[Index]); + const float NewAbs = FMath::Abs(NewDensity[Index]); + if (OldAbs <= 8.0f && NewAbs <= 8.0f) + { + const float Combined = OldAbs + NewAbs; + if (Combined < BestCombinedBand) + { + BestCombinedBand = Combined; + BestSample = FIntVector(X, Y, Z); + bOutContainsBothIsoBands = true; + } + } + } + if (!bOutContainsBothIsoBands) + { + float BestNewBand = FLT_MAX; + for (int32 Z = 32; Z < Dimension - 32; ++Z) + for (int32 Y = 32; Y < Dimension - 32; ++Y) + for (int32 X = 32; X < Dimension - 32; ++X) + { + const int32 Index = (Z * Dimension + Y) * Dimension + X; + const float NewAbs = FMath::Abs(NewDensity[Index]); + if (NewAbs < BestNewBand) + { + BestNewBand = NewAbs; + BestSample = FIntVector(X, Y, Z); + } + } + } + return RingOrigin + BestSample - FIntVector(32, 32, 32); +} + +FStep2dBoundaryStats ComputeStep2dBoundaryStats( + const TArray& Density, int32 Dimension, FIntVector Origin, int32 FeatureCellSpacing) +{ + FStep2dBoundaryStats Result; + double BrickSquares = 0.0; + double InteriorSquares = 0.0; + double ChunkSquares = 0.0; + double FeatureSquares = 0.0; + double FeatureInteriorSquares = 0.0; + const int32 CellSpacing = FMath::Max(FeatureCellSpacing, 1); + for (int32 Z = 1; Z < Dimension - 1; ++Z) + for (int32 Y = 1; Y < Dimension - 1; ++Y) + for (int32 X = 1; X < Dimension - 1; ++X) + { + const int32 CenterIndex = (Z * Dimension + Y) * Dimension + X; + const int32 Stride[3] = {1, Dimension, Dimension * Dimension}; + const int32 World[3] = {Origin.X + X, Origin.Y + Y, Origin.Z + Z}; + for (int32 Axis = 0; Axis < 3; ++Axis) + { + const double LeftGradient = Density[CenterIndex] - Density[CenterIndex - Stride[Axis]]; + const double RightGradient = Density[CenterIndex + Stride[Axis]] - Density[CenterIndex]; + const double Jump = FMath::Abs(RightGradient - LeftGradient); + const bool bBrickBoundary = (World[Axis] % 8) == 0; + const bool bChunkBoundary = (World[Axis] % CHUNK_SIZE) == 0 && Axis < 2; + const bool bFeatureBoundary = ((World[Axis] + CellSpacing / 2) % CellSpacing) == 0; + if (FMath::Abs(Density[CenterIndex]) <= 12.0f) + { + if (bBrickBoundary) + { + ++Result.BrickBoundarySamples; + BrickSquares += Jump * Jump; + Result.BrickJumpMax = FMath::Max(Result.BrickJumpMax, Jump); + } + else + { + ++Result.InteriorSamples; + InteriorSquares += Jump * Jump; + } + if (bChunkBoundary) + { + ++Result.ChunkBoundarySamples; + ChunkSquares += Jump * Jump; + Result.ChunkBoundaryJumpMax = FMath::Max(Result.ChunkBoundaryJumpMax, Jump); + } + } + if (bFeatureBoundary) + { + ++Result.FeatureCellBoundarySamples; + FeatureSquares += Jump * Jump; + } + else + { + ++Result.FeatureCellInteriorSamples; + FeatureInteriorSquares += Jump * Jump; + } + if (bFeatureBoundary) + Result.FeatureCellJumpMax = FMath::Max(Result.FeatureCellJumpMax, Jump); + } + } + Result.BrickJumpRms = Result.BrickBoundarySamples > 0 + ? FMath::Sqrt(BrickSquares / Result.BrickBoundarySamples) : 0.0; + Result.InteriorJumpRms = Result.InteriorSamples > 0 + ? FMath::Sqrt(InteriorSquares / Result.InteriorSamples) : 0.0; + Result.ChunkBoundaryJumpRms = Result.ChunkBoundarySamples > 0 + ? FMath::Sqrt(ChunkSquares / Result.ChunkBoundarySamples) : 0.0; + Result.FeatureCellJumpRms = Result.FeatureCellBoundarySamples > 0 + ? FMath::Sqrt(FeatureSquares / Result.FeatureCellBoundarySamples) : 0.0; + Result.FeatureCellInteriorJumpRms = Result.FeatureCellInteriorSamples > 0 + ? FMath::Sqrt(FeatureInteriorSquares / Result.FeatureCellInteriorSamples) : 0.0; + return Result; +} + +FColor Step2dDensityColor(float Density) +{ + if (FMath::Abs(Density) < 1.0f) + { + return FColor(56, 184, 206, 255); + } + return Density >= 0.0f + ? FColor(226, 220, 202, 255) + : FColor(22, 30, 45, 255); +} + +void SaveStep2dSideBySideSlice( + const FString& Path, + const TArray& OldDensity, + const TArray& NewDensity, + int32 Dimension, + TCHAR Plane, + int32 FixedIndex, + FString& OutError) +{ + const int32 Width = Dimension * 2; + const int32 Height = Dimension; + TArray Pixels; + Pixels.SetNumUninitialized(Width * Height); + for (int32 Row = 0; Row < Height; ++Row) + for (int32 Column = 0; Column < Dimension; ++Column) + { + int32 OldIndex = 0; + int32 NewIndex = 0; + if (Plane == TEXT('h')) + { + const int32 X = Column; + const int32 Y = Dimension - 1 - Row; + const int32 Z = FixedIndex; + OldIndex = (Z * Dimension + Y) * Dimension + X; + } + else if (Plane == TEXT('x')) + { + const int32 X = Column; + const int32 Y = FixedIndex; + const int32 Z = Dimension - 1 - Row; + OldIndex = (Z * Dimension + Y) * Dimension + X; + } + else + { + const int32 X = FixedIndex; + const int32 Y = Column; + const int32 Z = Dimension - 1 - Row; + OldIndex = (Z * Dimension + Y) * Dimension + X; + } + NewIndex = OldIndex; + Pixels[Row * Width + Column] = Step2dDensityColor(OldDensity[OldIndex]); + Pixels[Row * Width + Dimension + Column] = Step2dDensityColor(NewDensity[NewIndex]); + } + SavePng(Path, Pixels, Width, Height, OutError); +} + +TSharedRef Step2dTimingJson(const VoxelForgeRegionEval::FTiming& Timing) +{ + TSharedRef Json = MakeShared(); + Json->SetNumberField(TEXT("workers_requested"), Timing.Workers); + Json->SetNumberField(TEXT("workers_observed"), Timing.ObservedWorkerThreads); + Json->SetNumberField(TEXT("lattice_samples"), static_cast(Timing.SampleCount)); + Json->SetNumberField(TEXT("density_samples_evaluated"), static_cast(Timing.EvaluatedSamples)); + Json->SetNumberField(TEXT("samples_skipped_by_brick_proof"), static_cast(Timing.SkippedSamples)); + Json->SetNumberField(TEXT("bricks_tested"), static_cast(Timing.BrickCount)); + Json->SetNumberField(TEXT("bricks_skipped"), static_cast(Timing.SkippedBricks)); + Json->SetNumberField(TEXT("share_of_bricks_skipped"), Timing.BrickCount > 0 + ? static_cast(Timing.SkippedBricks) / static_cast(Timing.BrickCount) : 0.0); + Json->SetNumberField(TEXT("share_of_lattice_samples_skipped"), Timing.SampleCount > 0 + ? static_cast(Timing.SkippedSamples) / static_cast(Timing.SampleCount) : 0.0); + Json->SetNumberField(TEXT("wall_ms"), Timing.WallSeconds * 1000.0); + Json->SetNumberField(TEXT("worker_cpu_seconds"), Timing.WorkerCpuSeconds); + Json->SetNumberField(TEXT("worker_cpu_ns_per_lattice_sample"), Timing.NanosecondsPerSample); + Json->SetNumberField(TEXT("brick_classification_worker_seconds"), Timing.BrickClassificationSeconds); + Json->SetNumberField(TEXT("density_evaluation_worker_seconds"), Timing.DensityEvaluationSeconds); + Json->SetNumberField(TEXT("uniform_fill_worker_seconds"), Timing.UniformFillSeconds); + Json->SetNumberField(TEXT("longest_worker_wall_ms"), Timing.LongestWorkerWallSeconds * 1000.0); + Json->SetNumberField(TEXT("parallel_overhead_wall_ms"), FMath::Max( + 0.0, Timing.WallSeconds - Timing.LongestWorkerWallSeconds) * 1000.0); + Json->SetNumberField(TEXT("brick_classification_calls"), static_cast(Timing.MacroBrickChecks)); + Json->SetNumberField(TEXT("macro_center_evaluations"), static_cast(Timing.MacroCentreEvaluations)); + Json->SetNumberField(TEXT("unsupported_brick_proofs"), static_cast(Timing.UnsupportedBrickProofs)); + Json->SetNumberField(TEXT("chunk_or_strate_boundary_proofs"), + static_cast(Timing.ChunkBoundaryBrickProofs)); + Json->SetNumberField(TEXT("passage_bound_proofs"), static_cast(Timing.PassageBoundBrickProofs)); + Json->SetNumberField(TEXT("mixed_macro_interval_proofs"), static_cast(Timing.MixedMacroBrickProofs)); + Json->SetNumberField(TEXT("room_sdf_evaluations"), static_cast(Timing.RoomEvaluations)); + Json->SetNumberField(TEXT("tunnel_sdf_evaluations"), static_cast(Timing.TunnelEvaluations)); + Json->SetNumberField(TEXT("tunnel_core_evaluations"), static_cast(Timing.TunnelCoreEvaluations)); + Json->SetNumberField(TEXT("passage_sdf_evaluations"), static_cast(Timing.PassageEvaluations)); + Json->SetNumberField(TEXT("geometry_evaluations_per_lattice_sample"), Timing.CandidateEvaluationsPerSample); + Json->SetNumberField(TEXT("quantized_hash"), static_cast(Timing.QuantizedHash)); + Json->SetNumberField(TEXT("raw_float_hash"), static_cast(Timing.RawFloatHash)); + return Json; +} + +int32 RunStep2d(const FString& Params) +{ + FExploreArguments Arguments; + FString Error; + if (!ParseArguments(Params, Arguments, Error)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] argument error: %s"), *Error); + return 2; + } + int32 Seed = 1337; + FParse::Value(*Params, TEXT("seed="), Seed); + int32 Workers = VoxelForgeRegionEval::WorkerCount; + FParse::Value(*Params, TEXT("regionworkers="), Workers); + Workers = FMath::Clamp(Workers, 1, 32); + FString ResearchOut; + FParse::Value(*Params, TEXT("researchout="), ResearchOut); + if (ResearchOut.IsEmpty()) + { + ResearchOut = FPaths::Combine(Arguments.OutDirectory, TEXT("Research/step2d")); + } + ResearchOut = FPaths::ConvertRelativePathToFull(ResearchOut); + IFileManager::Get().MakeDirectory(*ResearchOut, true); + IFileManager::Get().MakeDirectory(*Arguments.OutDirectory, true); + + FExploreWorld World; + int32 RecipeIndex = INDEX_NONE; + uint32 RecipeHash = 0; + int32 EditChunkCount = 0; + const double PrepareStartSeconds = FPlatformTime::Seconds(); + if (!PrepareRegionEvalWorld(World, Arguments, Seed, + RecipeIndex, RecipeHash, EditChunkCount, Error)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] world preparation failed: %s"), *Error); + return 2; + } + const double WorldPrepareSeconds = FPlatformTime::Seconds() - PrepareStartSeconds; + if (World.DiffLayer != nullptr) + { + World.DiffLayer->Clear(); + } + + const int32 Dimension = VoxelForgeRegionEval::RingDimension; + const int32 CenterZ = (World.TargetTopWorldZ + World.TargetBottomWorldZ) / 2; + const FIntVector Origin(-129, -129, CenterZ - 129); + TArray OldTrials; + TArray NewTrials; + TArray OldDensities; + TArray NewDensities; + TArray NewRepeatDensities; + TArray NewSkippedDensities; + TArray SmoothDensities; + TArray SingleWorkerDensities; + VoxelForgeRedefinedDensityField::SetEnabled(false); + VoxelForgeRedefinedDensityField::SetDetailScale(1.0f); + + for (int32 Trial = 0; Trial < 3; ++Trial) + { + VoxelForgeRegionEval::FTiming Timing; + if (!VoxelForgeRegionEval::Evaluate( + *World.Generator, Origin, Dimension, nullptr, true, false, 1.0f, + &OldDensities, Timing, false, Workers, false)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] old field timing pass %d failed."), Trial + 1); + return 2; + } + OldTrials.Add(Timing); + } + + // This fourth old-field pass carries counters; its wall time is deliberately excluded from + // the performance row so instrumentation does not contaminate the measured baseline. + VoxelForgeRegionEval::FTiming OldCounters; + if (!VoxelForgeRegionEval::Evaluate( + *World.Generator, Origin, Dimension, nullptr, true, false, 1.0f, + nullptr, OldCounters, false, Workers, true)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] old field counter pass failed.")); + return 2; + } + OldCounters.WallSeconds = OldTrials.Last().WallSeconds; + OldCounters.WorkerCpuSeconds = OldTrials.Last().WorkerCpuSeconds; + OldCounters.NanosecondsPerSample = OldTrials.Last().NanosecondsPerSample; + + for (int32 Trial = 0; Trial < 3; ++Trial) + { + VoxelForgeRegionEval::FTiming Timing; + if (!VoxelForgeRegionEval::EvaluateRedefined( + *World.Generator, Origin, Dimension, Workers, 1.0f, + NewSkippedDensities, Timing, true)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] redefined 6-worker pass %d failed."), Trial + 1); + return 2; + } + NewTrials.Add(Timing); + } + VoxelForgeRegionEval::FTiming OneWorkerTiming; + if (!VoxelForgeRegionEval::EvaluateRedefined( + *World.Generator, Origin, Dimension, 1, 1.0f, + SingleWorkerDensities, OneWorkerTiming, false)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] 1-worker determinism pass failed.")); + return 2; + } + VoxelForgeRegionEval::FTiming NewFullSixWorkerTiming; + if (!VoxelForgeRegionEval::EvaluateRedefined( + *World.Generator, Origin, Dimension, Workers, 1.0f, + NewDensities, NewFullSixWorkerTiming, false)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] full-detail six-worker evidence pass failed.")); + return 2; + } + VoxelForgeRegionEval::FTiming NewRepeatTiming; + if (!VoxelForgeRegionEval::EvaluateRedefined( + *World.Generator, Origin, Dimension, Workers, 1.0f, + NewRepeatDensities, NewRepeatTiming, false)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] repeated full-detail hash pass failed.")); + return 2; + } + VoxelForgeRegionEval::FTiming SmoothTiming; + if (!VoxelForgeRegionEval::EvaluateRedefined( + *World.Generator, Origin, Dimension, Workers, 0.0f, + SmoothDensities, SmoothTiming, false)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] deliberate smoothing control failed.")); + return 2; + } + + int64 DetailBatchParitySamples = 0; + double DetailBatchMaxAbsoluteError = 0.0; + const int64 TotalLatticeSamples = static_cast(Dimension) * Dimension * Dimension; + for (int64 Index = 0; Index < TotalLatticeSamples; Index += 2047) + { + const int32 X = static_cast(Index % Dimension); + const int32 Y = static_cast((Index / Dimension) % Dimension); + const int32 Z = static_cast(Index / (static_cast(Dimension) * Dimension)); + const float RawDetailField = VoxelForgeRedefinedDensityField::Evaluate( + *World.Generator, static_cast(Origin.X + X), + static_cast(Origin.Y + Y), static_cast(Origin.Z + Z), nullptr, 1.0f); + DetailBatchMaxAbsoluteError = FMath::Max( + DetailBatchMaxAbsoluteError, + static_cast(FMath::Abs(RawDetailField - NewDensities[static_cast(Index)]))); + ++DetailBatchParitySamples; + } + const bool bDetailBatchMatchesRaw = DetailBatchMaxAbsoluteError <= 1.0e-5; + + const FStep2dSurfaceSpectrum OldSpectrum = ComputeStep2dSpectrum(OldDensities, Dimension); + const FStep2dSurfaceSpectrum NewSpectrum = ComputeStep2dSpectrum(NewDensities, Dimension); + const FStep2dSurfaceSpectrum SmoothSpectrum = ComputeStep2dSpectrum(SmoothDensities, Dimension); + const FStep2dBrickProofAudit BrickProofAudit = AuditStep2dBrickProofs( + *World.Generator, Origin, Dimension); + const int32 FeatureCellSpacing = World.Definitions.IsValidIndex(Arguments.Slot) + && World.Definitions[Arguments.Slot].IsValid() + && World.Definitions[Arguments.Slot]->GeneratorType == ECaveGeneratorType::Maze + ? 96 : 128; + const FStep2dBoundaryStats BoundaryStats = ComputeStep2dBoundaryStats( + NewDensities, Dimension, Origin, FeatureCellSpacing); + + const bool bHashesAgree = OldDensities.Num() == Dimension * Dimension * Dimension + && VoxelForgeRegionEval::HashQuantized(NewDensities) + == VoxelForgeRegionEval::HashQuantized(SingleWorkerDensities) + && VoxelForgeRegionEval::HashQuantized(NewDensities) + == VoxelForgeRegionEval::HashQuantized(NewRepeatDensities) + && NewFullSixWorkerTiming.RawFloatHash == OneWorkerTiming.RawFloatHash + && NewFullSixWorkerTiming.RawFloatHash == NewRepeatTiming.RawFloatHash; + + VoxelForgeRedefinedDensityField::SetEnabled(false); + VoxelForgeRedefinedDensityField::SetDetailScale(1.0f); + bool bMeshContainsBothIsoBands = false; + const FIntVector MeshOrigin = SelectStep2dMeshOrigin( + OldDensities, NewDensities, Origin, Dimension, bMeshContainsBothIsoBands); + const FVoxelMeshData OldMesh = World.Mesher->GenerateMesh(MeshOrigin, 1, 64); + const int32 OldTriangleCount = FMath::Max(0, OldMesh.Triangles.Num() / 3 - OldMesh.NumSkirtTriangles); + VoxelForgeRedefinedDensityField::SetEnabled(true); + const FVoxelMeshData NewMesh = World.Mesher->GenerateMesh(MeshOrigin, 1, 64); + const int32 NewTriangleCount = FMath::Max(0, NewMesh.Triangles.Num() / 3 - NewMesh.NumSkirtTriangles); + VoxelForgeRedefinedDensityField::SetDetailScale(0.0f); + const FVoxelMeshData SmoothMesh = World.Mesher->GenerateMesh(MeshOrigin, 1, 64); + const int32 SmoothTriangleCount = FMath::Max(0, SmoothMesh.Triangles.Num() / 3 - SmoothMesh.NumSkirtTriangles); + VoxelForgeRedefinedDensityField::SetDetailScale(1.0f); + VoxelForgeRedefinedDensityField::SetEnabled(false); + + const FString ImageNames[] = { + TEXT("slice_xy_center.png"), TEXT("slice_xz_y0.png"), + TEXT("slice_yz_x0.png"), TEXT("slice_xz_passage.png")}; + FString SliceError; + SaveStep2dSideBySideSlice( + FPaths::Combine(ResearchOut, ImageNames[0]), OldDensities, NewDensities, + Dimension, TEXT('h'), Dimension / 2, SliceError); + SaveStep2dSideBySideSlice( + FPaths::Combine(ResearchOut, ImageNames[1]), OldDensities, NewDensities, + Dimension, TEXT('x'), Dimension / 2, SliceError); + SaveStep2dSideBySideSlice( + FPaths::Combine(ResearchOut, ImageNames[2]), OldDensities, NewDensities, + Dimension, TEXT('y'), Dimension / 2, SliceError); + int32 PassageIndex = Dimension / 2; + if (World.Manager != nullptr && !World.Manager->GetPassages().IsEmpty()) + { + const FVoxelPassage& Passage = World.Manager->GetPassages()[0]; + PassageIndex = FMath::Clamp(FMath::RoundToInt(Passage.LowerLanding.StandingPoint.Y) + - Origin.Y, 0, Dimension - 1); + } + SaveStep2dSideBySideSlice( + FPaths::Combine(ResearchOut, ImageNames[3]), OldDensities, NewDensities, + Dimension, TEXT('x'), PassageIndex, SliceError); + if (!SliceError.IsEmpty()) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] slice image write failed: %s"), *SliceError); + return 2; + } + + TArray OldWallTrials; + TArray NewWallTrials; + for (const auto& Timing : OldTrials) OldWallTrials.Add(Timing.WallSeconds); + for (const auto& Timing : NewTrials) NewWallTrials.Add(Timing.WallSeconds); + OldWallTrials.Sort(); + NewWallTrials.Sort(); + const double OldMedianWall = OldWallTrials[OldWallTrials.Num() / 2]; + const double NewMedianWall = NewWallTrials[NewWallTrials.Num() / 2]; + const double OldGeometryEvaluations = static_cast(OldCounters.RoomEvaluations + + OldCounters.TunnelEvaluations + OldCounters.TunnelCoreEvaluations + + OldCounters.PassageEvaluations); + const double OldGeometryPerSample = OldCounters.SampleCount > 0 + ? OldGeometryEvaluations / static_cast(OldCounters.SampleCount) : 0.0; + const double NewMacroPerSample = NewTrials.Last().SampleCount > 0 + ? static_cast(NewTrials.Last().MacroCentreEvaluations + + NewTrials.Last().EvaluatedSamples) + / static_cast(NewTrials.Last().SampleCount) : 0.0; + const bool bDetailAtLeastOld = NewSpectrum.RMSLag1 >= OldSpectrum.RMSLag1 + && NewSpectrum.RMSLag2 >= OldSpectrum.RMSLag2; + const bool bSmoothControlDetected = SmoothSpectrum.RMSLag1 < NewSpectrum.RMSLag1 * 0.80 + || SmoothSpectrum.RMSLag2 < NewSpectrum.RMSLag2 * 0.80; + const bool bBoundaryStatsSound = BoundaryStats.BrickBoundarySamples > 0 + && BoundaryStats.InteriorSamples > 0 + && BoundaryStats.ChunkBoundarySamples > 0 + && BoundaryStats.FeatureCellBoundarySamples > 0 + && BoundaryStats.BrickJumpRms <= BoundaryStats.InteriorJumpRms * 2.0 + 0.25 + && BoundaryStats.ChunkBoundaryJumpRms <= BoundaryStats.InteriorJumpRms * 2.0 + 0.25 + && BoundaryStats.FeatureCellJumpRms + <= BoundaryStats.FeatureCellInteriorJumpRms * 2.0 + 0.25; + const bool bCorrectProofsSound = BrickProofAudit.CorrectlySkippedBricksAudited > 0 + && BrickProofAudit.CorrectProofSamplesChecked > 0 + && BrickProofAudit.CorrectProofViolations == 0; + const bool bUndersizedBoundCaught = BrickProofAudit.DeliberateBoundDisagreements > 0; + const bool bSmoothWouldFallBelowOldSpectrum = SmoothSpectrum.RMSLag1 < OldSpectrum.RMSLag1 + || SmoothSpectrum.RMSLag2 < OldSpectrum.RMSLag2; + const bool bSpeedPass = NewMedianWall <= 0.600; + const bool bPassed = bSpeedPass && bHashesAgree && bDetailAtLeastOld + && bSmoothControlDetected + && bBoundaryStatsSound && bCorrectProofsSound + && bUndersizedBoundCaught + && bDetailBatchMatchesRaw + && NewTrials.Last().NonFiniteSamples == 0; + + TSharedRef Report = MakeShared(); + Report->SetStringField(TEXT("status"), bPassed ? TEXT("pass") : TEXT("fail")); + Report->SetBoolField(TEXT("pass"), bPassed); + Report->SetBoolField(TEXT("speed_pass_600ms"), bSpeedPass); + Report->SetNumberField(TEXT("seed"), Seed); + Report->SetNumberField(TEXT("ring_dimension"), Dimension); + Report->SetNumberField(TEXT("ring_samples"), static_cast(Dimension) * Dimension * Dimension); + Report->SetNumberField(TEXT("workers_requested"), Workers); + Report->SetNumberField(TEXT("world_prepare_ms_excluded"), WorldPrepareSeconds * 1000.0); + Report->SetNumberField(TEXT("world_prepare_recipe_index"), RecipeIndex); + Report->SetNumberField(TEXT("world_prepare_recipe_hash"), RecipeHash); + Report->SetNumberField(TEXT("cleared_player_edit_chunks"), EditChunkCount); + TSharedRef Window = MakeShared(); + Window->SetNumberField(TEXT("origin_x_voxels"), Origin.X); + Window->SetNumberField(TEXT("origin_y_voxels"), Origin.Y); + Window->SetNumberField(TEXT("origin_z_voxels"), Origin.Z); + Window->SetNumberField(TEXT("samples_per_axis"), Dimension); + Window->SetNumberField(TEXT("sample_stride_voxels"), 1); + Window->SetNumberField(TEXT("sample_spacing_cm"), 25.0); + Report->SetObjectField(TEXT("full_resolution_window"), Window); + Report->SetStringField(TEXT("field_definition"), + TEXT("A seed-offset periodic cross-corridor signed field per strate, joined with rounded " + "landing chambers and existing authored passage carving. Four continuous hash-value " + "noise bands (8, 4, 2, and 1 lattice point) with total bounded amplitude 5.25 are " + "evaluated at every full-resolution sample that can affect the zero set.")); + Report->SetStringField(TEXT("resolution_contract"), + TEXT("The lattice remains one sample per 25 cm. No coarse density result is interpolated " + "onto it: every potentially surface-affecting point evaluates all four detail bands. " + "Brick skips require a final-field sign proof including the full noise supremum, a " + "floating-point safety margin, and the one-sample marching-cubes halo.")); + Report->SetStringField(TEXT("finer_lattice_scaling"), + TEXT("At 12.5 cm, scale macro lengths and the broad/medium noise cell sizes in lattice " + "units by 2 to preserve their world-space shapes; keep or add a 1-2 sample fine octave " + "to retain and extend detail. With fixed 8^3 sample bricks, samples grow 8x but " + "surface-intersecting bricks grow approximately with area (about 4x); the rest are " + "sign-proven and require only density writes. Full-resolution detail remains per " + "potentially surface-affecting point.")); + Report->SetNumberField(TEXT("old_macro_geometry_evaluations_per_point"), OldGeometryPerSample); + Report->SetNumberField(TEXT("new_macro_evaluations_per_point_including_brick_proofs"), NewMacroPerSample); + Report->SetNumberField(TEXT("new_noise_value_octaves_per_evaluated_point"), 4.0); + Report->SetNumberField(TEXT("new_noise_octave_evaluations_per_full_ring_point"), + static_cast(NewTrials.Last().EvaluatedSamples) * 4.0 + / static_cast(NewTrials.Last().SampleCount)); + Report->SetNumberField(TEXT("old_reported_noise_share_percent_from_step2c"), 2.6); + Report->SetNumberField(TEXT("old_six_worker_median_ms"), OldMedianWall * 1000.0); + Report->SetNumberField(TEXT("new_six_worker_median_ms"), NewMedianWall * 1000.0); + const auto WorkerRemainderSeconds = [](const VoxelForgeRegionEval::FTiming& Timing) + { + return FMath::Max(0.0, Timing.WorkerCpuSeconds + - Timing.BrickClassificationSeconds - Timing.DensityEvaluationSeconds + - Timing.UniformFillSeconds); + }; + Report->SetNumberField(TEXT("new_other_worker_cpu_seconds"), + WorkerRemainderSeconds(NewTrials.Last())); + Report->SetNumberField(TEXT("new_other_worker_cpu_ns_per_point"), + WorkerRemainderSeconds(NewTrials.Last()) * 1.0e9 + / static_cast(NewTrials.Last().SampleCount)); + Report->SetNumberField(TEXT("new_speedup_vs_old"), NewMedianWall > 0.0 + ? OldMedianWall / NewMedianWall : 0.0); + TArray> OldRows; + TArray> NewRows; + for (const auto& Timing : OldTrials) + { + OldRows.Add(MakeShared(Step2dTimingJson(Timing))); + } + for (const auto& Timing : NewTrials) + { + NewRows.Add(MakeShared(Step2dTimingJson(Timing))); + } + Report->SetArrayField(TEXT("old_field_trials"), OldRows); + Report->SetArrayField(TEXT("new_field_trials"), NewRows); + Report->SetObjectField(TEXT("old_full_ring_counters"), Step2dTimingJson(OldCounters)); + Report->SetObjectField(TEXT("new_one_worker_determinism_pass"), Step2dTimingJson(OneWorkerTiming)); + Report->SetObjectField(TEXT("new_full_detail_six_worker_evidence_pass"), + Step2dTimingJson(NewFullSixWorkerTiming)); + Report->SetObjectField(TEXT("new_repeated_full_detail_six_worker_hash_pass"), + Step2dTimingJson(NewRepeatTiming)); + Report->SetObjectField(TEXT("smooth_control_full_detail_pass"), Step2dTimingJson(SmoothTiming)); + + TSharedRef Hashes = MakeShared(); + Hashes->SetStringField(TEXT("six_worker_run_1_quantized"), FString::Printf( + TEXT("%016llx"), static_cast(NewFullSixWorkerTiming.QuantizedHash))); + Hashes->SetStringField(TEXT("six_worker_run_2_quantized"), FString::Printf( + TEXT("%016llx"), static_cast(NewRepeatTiming.QuantizedHash))); + Hashes->SetStringField(TEXT("one_worker_quantized"), FString::Printf( + TEXT("%016llx"), static_cast(OneWorkerTiming.QuantizedHash))); + Hashes->SetStringField(TEXT("six_worker_run_1_raw_float"), FString::Printf( + TEXT("%016llx"), static_cast(NewFullSixWorkerTiming.RawFloatHash))); + Hashes->SetStringField(TEXT("six_worker_run_2_raw_float"), FString::Printf( + TEXT("%016llx"), static_cast(NewRepeatTiming.RawFloatHash))); + Hashes->SetStringField(TEXT("one_worker_raw_float"), FString::Printf( + TEXT("%016llx"), static_cast(OneWorkerTiming.RawFloatHash))); + Hashes->SetBoolField(TEXT("same_across_two_runs_and_1_vs_6_workers"), bHashesAgree); + Report->SetObjectField(TEXT("determinism"), Hashes); + + TSharedRef Detail = MakeShared(); + Detail->SetNumberField(TEXT("old_surface_band_samples"), static_cast(OldSpectrum.SurfaceBandSamples)); + Detail->SetNumberField(TEXT("new_surface_band_samples"), static_cast(NewSpectrum.SurfaceBandSamples)); + Detail->SetNumberField(TEXT("old_curvature_rms_lag1"), OldSpectrum.RMSLag1); + Detail->SetNumberField(TEXT("new_curvature_rms_lag1"), NewSpectrum.RMSLag1); + Detail->SetNumberField(TEXT("smoothed_control_curvature_rms_lag1"), SmoothSpectrum.RMSLag1); + Detail->SetNumberField(TEXT("old_curvature_rms_lag2"), OldSpectrum.RMSLag2); + Detail->SetNumberField(TEXT("new_curvature_rms_lag2"), NewSpectrum.RMSLag2); + Detail->SetNumberField(TEXT("smoothed_control_curvature_rms_lag2"), SmoothSpectrum.RMSLag2); + Detail->SetNumberField(TEXT("new_curvature_rms_lag4"), NewSpectrum.RMSLag4); + Detail->SetNumberField(TEXT("smoothed_control_curvature_rms_lag4"), SmoothSpectrum.RMSLag4); + Detail->SetNumberField(TEXT("old_surface_triangles_64cubed"), OldTriangleCount); + Detail->SetNumberField(TEXT("new_surface_triangles_64cubed"), NewTriangleCount); + Detail->SetNumberField(TEXT("smoothed_control_surface_triangles_64cubed"), SmoothTriangleCount); + Detail->SetNumberField(TEXT("mesh_region_origin_x_voxels"), MeshOrigin.X); + Detail->SetNumberField(TEXT("mesh_region_origin_y_voxels"), MeshOrigin.Y); + Detail->SetNumberField(TEXT("mesh_region_origin_z_voxels"), MeshOrigin.Z); + Detail->SetBoolField(TEXT("mesh_region_contains_old_and_new_iso_bands"), + bMeshContainsBothIsoBands); + Detail->SetBoolField(TEXT("new_detail_at_least_old_spectrum"), bDetailAtLeastOld); + Detail->SetNumberField(TEXT("detail_batch_raw_parity_samples"), DetailBatchParitySamples); + Detail->SetNumberField(TEXT("detail_batch_max_absolute_error"), DetailBatchMaxAbsoluteError); + Detail->SetBoolField(TEXT("detail_batch_matches_direct_field"), bDetailBatchMatchesRaw); + Detail->SetBoolField(TEXT("deliberate_smoothing_control_detected"), bSmoothControlDetected); + Detail->SetBoolField(TEXT("smoothed_control_below_old_spectrum"), + bSmoothWouldFallBelowOldSpectrum); + Report->SetObjectField(TEXT("detail_preservation"), Detail); + + TSharedRef Boundaries = MakeShared(); + Boundaries->SetNumberField(TEXT("brick_size_voxels"), 8); + Boundaries->SetNumberField(TEXT("feature_cell_spacing_voxels"), FeatureCellSpacing); + Boundaries->SetNumberField(TEXT("chunk_boundary_gradient_jump_rms_xy_near_surface"), + BoundaryStats.ChunkBoundaryJumpRms); + Boundaries->SetNumberField(TEXT("chunk_boundary_gradient_jump_max_xy_near_surface"), + BoundaryStats.ChunkBoundaryJumpMax); + Boundaries->SetNumberField(TEXT("brick_boundary_gradient_jump_rms"), BoundaryStats.BrickJumpRms); + Boundaries->SetNumberField(TEXT("interior_gradient_jump_rms"), BoundaryStats.InteriorJumpRms); + Boundaries->SetNumberField(TEXT("feature_cell_boundary_gradient_jump_rms"), BoundaryStats.FeatureCellJumpRms); + Boundaries->SetNumberField(TEXT("feature_cell_interior_gradient_jump_rms"), + BoundaryStats.FeatureCellInteriorJumpRms); + Boundaries->SetNumberField(TEXT("brick_boundary_samples_near_surface"), + static_cast(BoundaryStats.BrickBoundarySamples)); + Boundaries->SetNumberField(TEXT("chunk_boundary_samples_near_surface_xy"), + static_cast(BoundaryStats.ChunkBoundarySamples)); + Boundaries->SetNumberField(TEXT("macro_period_boundary_samples"), + static_cast(BoundaryStats.FeatureCellBoundarySamples)); + Boundaries->SetNumberField(TEXT("brick_boundary_gradient_jump_max"), BoundaryStats.BrickJumpMax); + Boundaries->SetNumberField(TEXT("feature_cell_boundary_gradient_jump_max"), BoundaryStats.FeatureCellJumpMax); + Boundaries->SetBoolField(TEXT("boundary_check_pass"), bBoundaryStatsSound); + Report->SetObjectField(TEXT("boundary_artifact_check"), Boundaries); + + TSharedRef Proofs = MakeShared(); + Proofs->SetNumberField(TEXT("correctly_skipped_bricks_audited"), + static_cast(BrickProofAudit.CorrectlySkippedBricksAudited)); + Proofs->SetNumberField(TEXT("final_field_samples_checked_including_halo"), + static_cast(BrickProofAudit.CorrectProofSamplesChecked)); + Proofs->SetNumberField(TEXT("correct_proof_sign_violations"), + static_cast(BrickProofAudit.CorrectProofViolations)); + Proofs->SetNumberField(TEXT("deliberately_undersized_detail_bound_claims_checked"), + static_cast(BrickProofAudit.DeliberatelyUndersizedBoundClaimsChecked)); + Proofs->SetNumberField(TEXT("wrong_sign_samples_from_undersized_bound"), + static_cast(BrickProofAudit.DeliberateBoundDisagreements)); + Proofs->SetNumberField(TEXT("deliberate_control_classifier_detail_scale"), 0.0); + Proofs->SetNumberField(TEXT("deliberate_control_actual_detail_scale"), 4.0); + Proofs->SetBoolField(TEXT("sound_bound_audit_pass"), bCorrectProofsSound); + Proofs->SetBoolField(TEXT("deliberate_undersized_bound_control_caught"), bUndersizedBoundCaught); + Report->SetObjectField(TEXT("brick_bound_audit"), Proofs); + + TArray> Slices; + for (const FString& Name : ImageNames) + { + TSharedRef Slice = MakeShared(); + Slice->SetStringField(TEXT("file"), FPaths::Combine(ResearchOut, Name)); + Slice->SetStringField(TEXT("layout"), TEXT("old field on left; redefined field on right; light=air, dark=solid, cyan=near zero")); + Slices.Add(MakeShared(Slice)); + } + Report->SetArrayField(TEXT("slices"), Slices); + FString JsonText; + const TSharedRef> Writer = TJsonWriterFactory<>::Create(&JsonText); + const FString ReportPath = FPaths::Combine(ResearchOut, TEXT("step2d_report.json")); + if (!FJsonSerializer::Serialize(Report, Writer) + || !FFileHelper::SaveStringToFile( + JsonText, *ReportPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM) + || !FFileHelper::SaveStringToFile( + JsonText, *FPaths::Combine(Arguments.OutDirectory, TEXT("step2d_report.json")), + FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM)) + { + UE_LOG(LogTemp, Error, TEXT("[VoxelForgeStep2d] could not write report '%s'."), *ReportPath); + return 2; + } + UE_LOG(LogTemp, Display, + TEXT("[VoxelForgeStep2d] pass=%s speed600=%s old_ms=%.3f new_ms=%.3f skip=%.1f%% " + "roughness(old/new/smooth)=%.4f/%.4f/%.4f hashes=%s report=%s"), + bPassed ? TEXT("yes") : TEXT("no"), bSpeedPass ? TEXT("yes") : TEXT("no"), + OldMedianWall * 1000.0, NewMedianWall * 1000.0, + NewTrials.Last().SampleCount > 0 + ? 100.0 * static_cast(NewTrials.Last().SkippedSamples) + / static_cast(NewTrials.Last().SampleCount) : 0.0, + OldSpectrum.RMSLag1, NewSpectrum.RMSLag1, SmoothSpectrum.RMSLag1, + bHashesAgree ? TEXT("same") : TEXT("DIFFERENT"), *ReportPath); + return bPassed ? 0 : 2; +} + class FGeneratorDensitySampler final : public IVoxelStrateDensitySampler { public: @@ -9648,6 +10475,10 @@ int32 RunBatch(const FString& Params) int32 UVoxelForgeExploreCommandlet::Main(const FString& Params) { + if (FParse::Param(*Params, TEXT("step2d"))) + { + return RunStep2d(Params); + } const bool bRegionEvalSwitch = FParse::Param(*Params, TEXT("regioneval")); int32 RegionEvalFlag = 0; const bool bRegionEvalValue = FParse::Value(*Params, TEXT("regioneval="), RegionEvalFlag); diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp index 5116389..e47f2dd 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp +++ b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.cpp @@ -5,9 +5,12 @@ #include "VoxelCaveMorphology.h" #include "VoxelDensityProfile.h" #include "VoxelGenerator.h" +#include "VoxelRedefinedDensityField.h" #include "VoxelStackSampler.h" #include "HAL/PlatformTLS.h" +#include + #if PLATFORM_WINDOWS #include "Windows/AllowWindowsPlatformTypes.h" THIRD_PARTY_INCLUDES_START @@ -92,6 +95,14 @@ namespace VoxelForgeRegionEval int32 LatticeZ = 0; TArray RequestedSamples; }; + + struct FResearchBrick + { + FIntVector Origin = FIntVector::ZeroValue; + FIntVector Size = FIntVector::ZeroValue; + FIntVector Lattice = FIntVector::ZeroValue; + int32 SampleCount = 0; + }; } int64 LinearIndex(int32 X, int32 Y, int32 Z, int32 Dimension) @@ -138,6 +149,245 @@ namespace VoxelForgeRegionEval return Hash; } + bool EvaluateRedefined( + const UVoxelGenerator& Generator, + FIntVector OriginVoxels, + int32 Dimension, + int32 RequestedWorkerCount, + float DetailScale, + TArray& OutDensities, + FTiming& OutTiming, + bool bEnableBrickSkips) + { + OutTiming = FTiming(); + OutDensities.Reset(); + if (Dimension <= 0 || Dimension > 1024 || !FMath::IsFinite(DetailScale)) + { + return false; + } + + const int32 NumWorkers = FMath::Clamp(RequestedWorkerCount, 1, 32); + const int64 TotalSamples = LatticeCount(Dimension); + if (TotalSamples > MAX_int32) + { + return false; + } + const double StartSeconds = FPlatformTime::Seconds(); + OutTiming.Workers = NumWorkers; + OutTiming.SampleCount = TotalSamples; + OutDensities.SetNumUninitialized(static_cast(TotalSamples)); + + TArray Bricks; + const FIntVector Last = OriginVoxels + + FIntVector(Dimension - 1, Dimension - 1, Dimension - 1); + const FIntVector FirstBrick( + FMath::FloorToInt(static_cast(OriginVoxels.X) / 8.0f), + FMath::FloorToInt(static_cast(OriginVoxels.Y) / 8.0f), + FMath::FloorToInt(static_cast(OriginVoxels.Z) / 8.0f)); + const FIntVector LastBrick( + FMath::FloorToInt(static_cast(Last.X) / 8.0f), + FMath::FloorToInt(static_cast(Last.Y) / 8.0f), + FMath::FloorToInt(static_cast(Last.Z) / 8.0f)); + Bricks.Reserve((LastBrick.X - FirstBrick.X + 1) + * (LastBrick.Y - FirstBrick.Y + 1) * (LastBrick.Z - FirstBrick.Z + 1)); + for (int32 BZ = FirstBrick.Z; BZ <= LastBrick.Z; ++BZ) + for (int32 BY = FirstBrick.Y; BY <= LastBrick.Y; ++BY) + for (int32 BX = FirstBrick.X; BX <= LastBrick.X; ++BX) + { + const FIntVector BrickWorldMin(BX * 8, BY * 8, BZ * 8); + FResearchBrick& Brick = Bricks.AddDefaulted_GetRef(); + Brick.Origin = FIntVector( + FMath::Max(OriginVoxels.X, BrickWorldMin.X), + FMath::Max(OriginVoxels.Y, BrickWorldMin.Y), + FMath::Max(OriginVoxels.Z, BrickWorldMin.Z)); + const FIntVector BrickEnd = BrickWorldMin + FIntVector(8, 8, 8); + const FIntVector RingEnd = OriginVoxels + + FIntVector(Dimension, Dimension, Dimension); + const FIntVector End( + FMath::Min(BrickEnd.X, RingEnd.X), + FMath::Min(BrickEnd.Y, RingEnd.Y), + FMath::Min(BrickEnd.Z, RingEnd.Z)); + Brick.Size = End - Brick.Origin; + Brick.Lattice = Brick.Origin - OriginVoxels; + Brick.SampleCount = Brick.Size.X * Brick.Size.Y * Brick.Size.Z; + } + + TArray WorkerWall; + TArray WorkerCpu; + TArray WorkerClassify; + TArray WorkerEvaluate; + TArray WorkerFill; + TArray WorkerThreadIds; + TArray WorkerEvaluated; + TArray WorkerSkippedBricks; + TArray WorkerSkippedSamples; + TArray WorkerMacroChecks; + TArray WorkerMacroCentreEvaluations; + TArray WorkerFallbackReasons; + WorkerWall.SetNumZeroed(NumWorkers); + WorkerCpu.SetNumZeroed(NumWorkers); + WorkerClassify.SetNumZeroed(NumWorkers); + WorkerEvaluate.SetNumZeroed(NumWorkers); + WorkerFill.SetNumZeroed(NumWorkers); + WorkerThreadIds.SetNumZeroed(NumWorkers); + WorkerEvaluated.SetNumZeroed(NumWorkers); + WorkerSkippedBricks.SetNumZeroed(NumWorkers); + WorkerSkippedSamples.SetNumZeroed(NumWorkers); + WorkerMacroChecks.SetNumZeroed(NumWorkers); + WorkerMacroCentreEvaluations.SetNumZeroed(NumWorkers); + WorkerFallbackReasons.SetNumZeroed(NumWorkers * 5); + + // Surface bricks carry more work than uniform interior bricks. Equal brick counts do + // not balance that work, so claim small contiguous batches dynamically. Each brick + // writes a disjoint output range and still evaluates the same exact field. + std::atomic NextBrickIndex{0}; + constexpr int32 BrickBatchSize = 16; + ParallelFor(NumWorkers, [&](int32 WorkerIndex) + { + WorkerThreadIds[WorkerIndex] = FPlatformTLS::GetCurrentThreadId(); + const double WorkerStart = FPlatformTime::Seconds(); + double CpuStart = 0.0; + const bool bHaveCpuStart = GetCurrentThreadCpuSeconds(CpuStart); + TArray BrickDetail; + VoxelForgeRedefinedDensityField::FBrickEvaluationContext BrickContext; + for (;;) + { + const int32 BatchBegin = NextBrickIndex.fetch_add( + BrickBatchSize, std::memory_order_relaxed); + if (BatchBegin >= Bricks.Num()) + { + break; + } + const int32 BatchEnd = FMath::Min(BatchBegin + BrickBatchSize, Bricks.Num()); + for (int32 BrickIndex = BatchBegin; BrickIndex < BatchEnd; ++BrickIndex) + { + const FResearchBrick& Brick = Bricks[BrickIndex]; + bool bUniformAir = false; + bool bEvaluatedMacroCentre = false; + VoxelForgeRedefinedDensityField::EBrickFallbackReason FallbackReason = + VoxelForgeRedefinedDensityField::EBrickFallbackReason::UnsupportedState; + ++WorkerMacroChecks[WorkerIndex]; + const double ClassifyStart = FPlatformTime::Seconds(); + const bool bCanSkip = bEnableBrickSkips + && VoxelForgeRedefinedDensityField::TryClassifyBrick( + Generator, Brick.Origin, Brick.Size, bUniformAir, DetailScale, + &bEvaluatedMacroCentre, &FallbackReason); + WorkerClassify[WorkerIndex] += FPlatformTime::Seconds() - ClassifyStart; + WorkerMacroCentreEvaluations[WorkerIndex] += bEvaluatedMacroCentre ? 1u : 0u; + if (bEnableBrickSkips && !bCanSkip) + { + const int32 ReasonIndex = static_cast(FallbackReason); + if (ReasonIndex < 5) + { + ++WorkerFallbackReasons[WorkerIndex * 5 + ReasonIndex]; + } + } + if (bCanSkip) + { + ++WorkerSkippedBricks[WorkerIndex]; + WorkerSkippedSamples[WorkerIndex] += static_cast(Brick.SampleCount); + const float UniformDensity = bUniformAir ? 64.0f : -64.0f; + const double FillStart = FPlatformTime::Seconds(); + for (int32 Z = 0; Z < Brick.Size.Z; ++Z) + for (int32 Y = 0; Y < Brick.Size.Y; ++Y) + for (int32 X = 0; X < Brick.Size.X; ++X) + { + const int64 LatticeIndex = LinearIndex( + Brick.Lattice.X + X, Brick.Lattice.Y + Y, + Brick.Lattice.Z + Z, Dimension); + OutDensities[static_cast(LatticeIndex)] = UniformDensity; + } + WorkerFill[WorkerIndex] += FPlatformTime::Seconds() - FillStart; + continue; + } + + const double EvaluationStart = FPlatformTime::Seconds(); + VoxelForgeRedefinedDensityField::BuildDetailBrick( + Generator, Brick.Origin, Brick.Size, DetailScale, BrickDetail, + &BrickContext); + for (int32 Z = 0; Z < Brick.Size.Z; ++Z) + for (int32 Y = 0; Y < Brick.Size.Y; ++Y) + for (int32 X = 0; X < Brick.Size.X; ++X) + { + const int32 WorldX = Brick.Origin.X + X; + const int32 WorldY = Brick.Origin.Y + Y; + const int32 WorldZ = Brick.Origin.Z + Z; + const int64 LatticeIndex = LinearIndex( + Brick.Lattice.X + X, Brick.Lattice.Y + Y, + Brick.Lattice.Z + Z, Dimension); + OutDensities[static_cast(LatticeIndex)] = + VoxelForgeRedefinedDensityField::EvaluateWithDetailValue( + Generator, static_cast(WorldX), static_cast(WorldY), + static_cast(WorldZ), + BrickDetail[(Z * Brick.Size.Y + Y) * Brick.Size.X + X], + nullptr, DetailScale, &BrickContext); + ++WorkerEvaluated[WorkerIndex]; + } + WorkerEvaluate[WorkerIndex] += FPlatformTime::Seconds() - EvaluationStart; + } + } + WorkerWall[WorkerIndex] = FPlatformTime::Seconds() - WorkerStart; + double CpuEnd = 0.0; + if (bHaveCpuStart && GetCurrentThreadCpuSeconds(CpuEnd)) + { + WorkerCpu[WorkerIndex] = FMath::Max(0.0, CpuEnd - CpuStart); + } + else + { + WorkerCpu[WorkerIndex] = WorkerWall[WorkerIndex]; + } + }, EParallelForFlags::None); + + OutTiming.WallSeconds = FPlatformTime::Seconds() - StartSeconds; + OutTiming.bRegionProgram = true; + OutTiming.BrickCount = static_cast(Bricks.Num()); + OutTiming.ObservedWorkerThreads = 0; + for (int32 Worker = 0; Worker < NumWorkers; ++Worker) + { + OutTiming.WorkerCpuSeconds += WorkerCpu[Worker]; + OutTiming.BrickClassificationSeconds += WorkerClassify[Worker]; + OutTiming.DensityEvaluationSeconds += WorkerEvaluate[Worker]; + OutTiming.UniformFillSeconds += WorkerFill[Worker]; + OutTiming.LongestWorkerWallSeconds = FMath::Max( + OutTiming.LongestWorkerWallSeconds, WorkerWall[Worker]); + OutTiming.EvaluatedSamples += static_cast(WorkerEvaluated[Worker]); + OutTiming.SkippedBricks += WorkerSkippedBricks[Worker]; + OutTiming.SkippedSamples += WorkerSkippedSamples[Worker]; + OutTiming.MacroBrickChecks += WorkerMacroChecks[Worker]; + OutTiming.MacroCentreEvaluations += WorkerMacroCentreEvaluations[Worker]; + OutTiming.UnsupportedBrickProofs += WorkerFallbackReasons[Worker * 5 + + static_cast(VoxelForgeRedefinedDensityField::EBrickFallbackReason::UnsupportedState)]; + OutTiming.ChunkBoundaryBrickProofs += WorkerFallbackReasons[Worker * 5 + + static_cast(VoxelForgeRedefinedDensityField::EBrickFallbackReason::ChunkOrStrateBoundary)]; + OutTiming.PassageBoundBrickProofs += WorkerFallbackReasons[Worker * 5 + + static_cast(VoxelForgeRedefinedDensityField::EBrickFallbackReason::PassageBounds)]; + OutTiming.MixedMacroBrickProofs += WorkerFallbackReasons[Worker * 5 + + static_cast(VoxelForgeRedefinedDensityField::EBrickFallbackReason::MixedMacroInterval)]; + bool bPreviouslyObserved = false; + for (int32 Previous = 0; Previous < Worker; ++Previous) + { + bPreviouslyObserved |= WorkerThreadIds[Previous] == WorkerThreadIds[Worker]; + } + OutTiming.ObservedWorkerThreads += bPreviouslyObserved ? 0 : 1; + } + OutTiming.NanosecondsPerSample = TotalSamples > 0 + ? OutTiming.WorkerCpuSeconds * 1.0e9 / static_cast(TotalSamples) + : 0.0; + OutTiming.CandidateEvaluationsPerSample = TotalSamples > 0 + ? static_cast(OutTiming.EvaluatedSamples + + static_cast(OutTiming.MacroCentreEvaluations)) + / static_cast(TotalSamples) + : 0.0; + for (const float Density : OutDensities) + { + OutTiming.NonFiniteSamples += FMath::IsFinite(Density) ? 0u : 1u; + } + OutTiming.QuantizedHash = HashQuantized(OutDensities); + OutTiming.RawFloatHash = HashRawFloats(OutDensities); + return OutTiming.EvaluatedSamples + static_cast(OutTiming.SkippedSamples) + == TotalSamples && OutTiming.NonFiniteSamples == 0; + } + bool Evaluate( const UVoxelGenerator& Generator, FIntVector OriginVoxels, diff --git a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.h b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.h index 5b76f62..3f7bcfc 100644 --- a/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.h +++ b/Source/VoxelForgeEditor/Private/VoxelForgeRegionEvalExperiment.h @@ -26,6 +26,9 @@ namespace VoxelForgeRegionEval double GeneralPathEntriesPerSample = 0.0; double WallSeconds = 0.0; double WorkerCpuSeconds = 0.0; + double BrickClassificationSeconds = 0.0; + double DensityEvaluationSeconds = 0.0; + double UniformFillSeconds = 0.0; double NanosecondsPerSample = 0.0; bool bWorkerCpuFromOsThreadTimes = false; bool bRegionProgram = false; @@ -34,6 +37,16 @@ namespace VoxelForgeRegionEval uint64 RawFloatHash = 0; uint64 SaturatedSamples = 0; uint64 NonFiniteSamples = 0; + uint64 BrickCount = 0; + uint64 SkippedBricks = 0; + uint64 SkippedSamples = 0; + uint64 MacroBrickChecks = 0; + uint64 MacroCentreEvaluations = 0; + uint64 UnsupportedBrickProofs = 0; + uint64 ChunkBoundaryBrickProofs = 0; + uint64 PassageBoundBrickProofs = 0; + uint64 MixedMacroBrickProofs = 0; + double LongestWorkerWallSeconds = 0.0; uint64 SdfCacheBuilds = 0; uint64 ActiveOpListBuilds = 0; uint64 BroadPhaseIndexBuilds = 0; @@ -122,6 +135,17 @@ namespace VoxelForgeRegionEval int32 RequestedWorkerCount = WorkerCount, bool bCollectWholeRunCounters = false); + /** Full-resolution step 2d field pass with sound world-aligned 8^3 brick proofs. */ + bool EvaluateRedefined( + const UVoxelGenerator& Generator, + FIntVector OriginVoxels, + int32 Dimension, + int32 RequestedWorkerCount, + float DetailScale, + TArray& OutDensities, + FTiming& OutTiming, + bool bEnableBrickSkips = true); + /** Count exact room/tunnel/core/passage candidate visits with instrumentation enabled. */ FCandidateVisits CountCandidates( const UVoxelGenerator& Generator, diff --git a/Tools/VoxelForgeTest.ps1 b/Tools/VoxelForgeTest.ps1 index 29d6c21..ed187fb 100644 --- a/Tools/VoxelForgeTest.ps1 +++ b/Tools/VoxelForgeTest.ps1 @@ -1,6 +1,6 @@ [CmdletBinding()] param( - [ValidateSet('canonical', 'owner', 'probe', 'perf', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism', 'symbolize-stacks', 'region-eval')] + [ValidateSet('canonical', 'owner', 'probe', 'perf', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism', 'symbolize-stacks', 'region-eval', 'step2d')] [string]$Scenario = 'canonical', [switch]$Build, [switch]$EnableRender, @@ -575,7 +575,7 @@ function Sync-OwnerAssets { $null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru [void]$rows.Add([ordered]@{ name = $assetName - used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval') + used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d') source_path = $source source_sha256 = Get-Sha256 $source staged_path = $target @@ -623,7 +623,7 @@ function Get-AssetEvidence { $target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset" [void]$rows.Add([ordered]@{ name = $assetName - used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval') + used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'visual-gap-walk', 'visual-gap-inject', 'dig-motion', 'horizon', 'horizon-render', 'determinism', 'region-eval', 'step2d') source_path = $source source_sha256 = Get-Sha256 $source staged_path = $target @@ -1473,6 +1473,32 @@ try { } } } + 'step2d' { + $step2dLog = Join-Path $RunRoot 'Logs\step2d.log' + $args = New-CommonArguments 'commandlet' $step2dLog (Merge-Cvars $baseCvars) + $args += New-CommandletArguments $commandletOut 'walk,export' ($Assets -eq 'owner') $false 128 + $researchOut = Join-Path $PluginRoot "Saved\Research\step2d\$Label" + $args += @('-step2d', '-regionworkers=6', ('-researchout="{0}"' -f $researchOut)) + $commandletLedger = Invoke-UnrealLaunch 'step2d-commandlet' $args ` + $step2dLog (Join-Path $RunRoot 'Logs\step2d.stdout.log') ` + (Join-Path $RunRoot 'Logs\step2d.stderr.log') + $step2dReportPath = Join-Path $researchOut 'step2d_report.json' + $step2dReport = Read-JsonFile $step2dReportPath + if ($null -eq $step2dReport) { + $CommandletMetrics = [ordered]@{ status = 'missing'; output = $researchOut; report = $step2dReportPath } + Add-Failure "step2d did not produce $step2dReportPath" + } else { + $CommandletMetrics = [ordered]@{ + status = if ((Get-JsonValue $step2dReport 'pass' $false) -and $commandletLedger.status -eq 'passed') { 'passed' } else { 'failed' } + output = $researchOut + report = $step2dReportPath + result = $step2dReport + } + if (-not (Get-JsonValue $step2dReport 'pass' $false)) { + Add-Failure 'step2d speed, detail, boundary, or determinism gate failed' + } + } + } 'symbolize-stacks' { if (([string]::IsNullOrWhiteSpace($StackSamplesPath)) -or -not (Test-Path -LiteralPath $StackSamplesPath -PathType Leaf)) { throw "-StackSamplesPath must name an existing raw sampler CSV."