standalone: speed round closed -- full-payload whole view 2.16 s @8 workers; UV/colour/groups cost 0.62 s

Takeover after the PC reset. Verified at 1 and 8 workers: density, geometry, full payload and
topology hashes equal for all nine outputs; routes 5642/5642; crater transaction 17 ms; player
neighbourhood mesh-ready 23 ms after streaming starts.
- The plugin's materials consume UV, the packed biome/slope/blend colours and the ceiling/ground
  sections, so the viewer-only skips stay off by default (diagnostic only: 1.53 s without them).
- UVs staged as FVector2f (matches the game stream; -20 MB, no speed change). Far-bin reserve 24x.
- LOD6 is the long pole: 32.7 M density samples, 1.83 M vertices, ~1.1 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5.5 committed 2026-10-04 13:27:03 +02:00
1 parent ee5b05b357
commit 6ec4a8f856
10 files changed
+417 -166

No files matched your search

+5 -5
View File
@@ -3,8 +3,8 @@ param([string]$OutputName = 'VoxelForgeStandalone.exe')
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
$BuildHostProcess = Get-Process -Id $PID $BuildHostProcess = Get-Process -Id $PID
$BuildHostProcess.PriorityClass = 'Normal' $BuildHostProcess.PriorityClass = 'BelowNormal'
$BuildHostProcess.ProcessorAffinity = [IntPtr]0xFFFF $BuildHostProcess.ProcessorAffinity = [IntPtr]0xFF00
$Root = [IO.Path]::GetFullPath($PSScriptRoot) $Root = [IO.Path]::GetFullPath($PSScriptRoot)
$BuildDir = Join-Path $Root 'Build' $BuildDir = Join-Path $Root 'Build'
$BinDir = Join-Path $Root 'bin' $BinDir = Join-Path $Root 'bin'
@@ -43,7 +43,7 @@ foreach ($Source in $Sources) {
$ResponsePath = Join-Path $BuildDir 'standalone.rsp' $ResponsePath = Join-Path $BuildDir 'standalone.rsp'
$RspLines = @( $RspLines = @(
'/nologo', '/std:c++20', '/O2', '/arch:AVX2', '/fp:precise', '/EHsc', '/MP16', '/nologo', '/std:c++20', '/O2', '/arch:AVX2', '/fp:precise', '/EHsc', '/MP8',
'/D_CRT_SECURE_NO_WARNINGS', '/D_CRT_SECURE_NO_WARNINGS',
('/I"{0}"' -f (Join-Path $Root 'Compat')), ('/I"{0}"' -f (Join-Path $Root 'Compat')),
('/I"{0}"' -f (Join-Path $Root 'Source')), ('/I"{0}"' -f (Join-Path $Root 'Source')),
@@ -51,14 +51,14 @@ $RspLines = @(
('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Private')), ('/I"{0}"' -f (Join-Path $Root 'Source\VoxelForge\Private')),
('/Fe"{0}"' -f $OutputPath) ('/Fe"{0}"' -f $OutputPath)
) )
# Run up to sixteen compiler processes on the permitted full-machine runner. # Run at most eight compiler processes on the half-load runner.
foreach ($Source in $Sources) { $RspLines += ('"{0}"' -f $Source) } foreach ($Source in $Sources) { $RspLines += ('"{0}"' -f $Source) }
$RspLines += @('/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib') $RspLines += @('/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib')
[IO.File]::WriteAllLines($ResponsePath,$RspLines,[Text.UTF8Encoding]::new($false)) [IO.File]::WriteAllLines($ResponsePath,$RspLines,[Text.UTF8Encoding]::new($false))
Push-Location $BuildDir Push-Location $BuildDir
try { try {
# This PowerShell process already runs Normal with affinity 0xFFFF; # This PowerShell process already runs BelowNormal with affinity 0xFF00;
# compile directly so cmd returns cl.exe's real exit code. # compile directly so cmd returns cl.exe's real exit code.
$CommandLine = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$ResponsePath $CommandLine = 'call "{0}" >nul && cl.exe @"{1}"' -f $VcVars,$ResponsePath
& $env:ComSpec /d /s /c $CommandLine & $env:ComSpec /d /s /c $CommandLine
@@ -1,141 +1,166 @@
# Shape world speed round # Shape world speed round — final half-load verification
## Result ## Result
The one-second whole-view goal does **not** pass. On the final rebuilt source, cold process-start to all 6,498 tiles through LOD6 took 1.958 s at 8 workers and 1.629 s at 16. The measured shape build plus uncached mesh generation took 1.805 s / 1.454 s. LOD6 remains the largest single streaming stage. The full-detail 960-tile area took 0.896 s / 0.703 s for shape build plus mesh generation; process-start to completion was 1.057 s / 0.825 s. Collision cooking and GPU upload are not measured by this CPU harness. The whole-view target remains unmet. The cold, uncached, full-payload view generated all 6,498 tiles through LOD6 in 2.135 s and 2.174 s at 8 workers. The mean process-start-to-ready time is 2.155 s. Shape compilation plus full-payload meshing averaged 1.945 s, so the generation path is about 0.945 s over the one-second mark; the process-start headline is 1.155 s over. Hashing was disabled for those timing runs. Collision cooking and the plugin’s GPU upload are not included.
Implementation: [shape compiler and field](../../../Source/StandaloneShapeWorld.h), [streaming/edit benchmark](../../../Source/StreamingWorld.h), [viewer measurements](../../../Source/StandaloneViewer.cpp), and [machine runner](RunMachine.ps1). The final source and measurements are in [StandaloneShapeWorld.h](../../../Source/StandaloneShapeWorld.h), [StreamingWorld.h](../../../Source/StreamingWorld.h), [StandaloneViewer.cpp](../../../Source/StandaloneViewer.cpp), [Build.ps1](../../../Build.ps1), and this report. The six current captures are linked below.
| Cold job | Workers | Shape build | Uncached mesh | Shape + mesh | Process start → completion | Cache hits | ## Machine rule and headline timings
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
| 64 m full-detail area, 960 tiles | 8 | 0.725 s | 0.171 s | **0.896 s** | **1.057 s** | 0 |
| 64 m full-detail area, 960 tiles | 16 | 0.549 s | 0.154 s | **0.703 s** | **0.825 s** | 0 |
| Whole view, 6,498 tiles through LOD6 | 8 | 0.844 s | 0.961 s | **1.805 s** | **1.958 s** | 0 |
| Whole view, 6,498 tiles through LOD6 | 16 | 0.672 s | 0.782 s | **1.454 s** | **1.629 s** | 0 |
The first four stage columns use separate cold, no-cache processes. Process-start timing begins immediately before `Start-Process`, includes executable launch and world setup, and ends after the final tile is meshed; it still excludes interactive GPU upload and collision cooking. The worker runner is Normal priority on affinity `0xFFFF` and polls at 50 ms. Peak private memory was 0.41 / 0.47 GiB for whole-view 8 / 16-worker runs. All builds and benchmark processes in this continuation ran at BelowNormal priority with affinity 0xFF00. Builds use /MP8. The final headline uses 8 workers, full UV/color/group payload, no mesh cache, and two no-hash cold runs. No current 16-worker timing was run after the reset.
The 3×3×3 LOD0 neighborhood completed in 18 ms / 19 ms after streaming started. The whole 960-tile full-detail run took 1.057 s / 0.825 s from process start, so at 16 workers even the full-detail area was ready inside one second. The standalone harness does not cook collision, so the required collidable-ready time remains unverified. | Cold whole-view run | Process wall | Shape stage | Full clipmap stream | Shape + stream | Cache hits |
The edit probe submits a 10 m sphere crater, a rounded-box cut, and a capsule rim as one field transaction, centered 24 m from the player across the LOD0/LOD1 seam. One remesh batch completed all 14 intersecting tiles: 12 at LOD0 and 2 at LOD1. Remesh wall time was 12 ms / 13 ms and edit-request-to-ready was 15 ms / 18 ms at 8 / 16 workers, with zero cache hits. This CPU harness does not update an interactive GPU scene or cook collision.
## Profile and before/after timings
The baseline is the first cold, profiled source at the start of this speed round (`profile_whole_view_w8/w16`). “After” uses the final selected source (`restored_final_whole_w8/w16`). Timings vary by a few milliseconds between cold runs; the table uses one no-hash run at each worker count.
| Whole-view stage | Before, 8 | After, 8 | Before, 16 | After, 16 |
| --- | ---: | ---: | ---: | ---: |
| Local shapes and line bins | 57.6 ms | 54.6 ms | 57.7 ms | 54.2 ms |
| Rock intervals, packed signs, candidate axes and mask merge | 132.6 ms | 123.8 ms | 126.7 ms | 114.0 ms |
| Surface detail band | 539.0 ms | 537.3 ms | 395.1 ms | 375.2 ms |
| Far parent shapes and spatial bins | 194.7 ms | 120.4 ms | 191.6 ms | 121.1 ms |
| **Shape build** | **932.3 ms** | **843.5 ms** | **779.7 ms** | **672.1 ms** |
| **All LOD mesh generation** | **1,064 ms** | **961 ms** | **850 ms** | **782 ms** |
| **Shape build + mesh** | **1.996 s** | **1.805 s** | **1.630 s** | **1.454 s** |
At 16 workers the final interval subprofile was: local primitives and line bins 54 ms, rock interval/sign lines 20 ms, candidate-axis lines 55 ms, candidate-mask merge 25 ms (about 4 ms serial remainder), surface detail 375 ms, far-parent generation 76 ms, and far spatial-bin construction 45 ms. The 138,188,413-node field used 36.71 MiB of packed words, 55.06 MiB of candidate-axis words, a 131.79 MiB surface proxy, and 72.78 MiB of primitive storage. The far spatial index has 328,944 bins and 1,428,924 primitive references. Its compact bin lists hold up to four IDs inline in 181,237 bins; 147,707 larger bins spill to a small heap array. Whole-view processes peaked at 0.41 / 0.48 GiB private memory at 8 / 16 workers.
Per-LOD completion time is measured from stream start and is cumulative, not a sum of LOD durations:
| LOD | Tiles | Density samples | Before complete, 8 / 16 | After complete, 8 / 16 |
| ---: | ---: | ---: | ---: | ---: |
| 0 | 512 | 21.952 M | 0.107 / 0.092 s | 0.130 / 0.102 s |
| 1 | 448 | 19.208 M | 0.190 / 0.153 s | 0.210 / 0.161 s |
| 2 | 320 | 2.195 M | 0.221 / 0.178 s | 0.233 / 0.178 s |
| 3 | 208 | 1.427 M | 0.244 / 0.196 s | 0.251 / 0.194 s |
| 4 | 144 | 0.988 M | 0.255 / 0.205 s | 0.261 / 0.200 s |
| 5 | 96 | 0.658 M | 0.264 / 0.211 s | 0.268 / 0.206 s |
| 6 | 4,770 | 32.717 M | 1.061 / 0.836 s | 0.948 / 0.778 s |
LOD6 emits 1,795,216 vertices, 10,047,528 indices, and 79.42 MiB of CPU mesh data. It starts at about 0.25 / 0.21 s and finishes at 0.948 / 0.778 s at 8 / 16 workers; while active it keeps the worker pool busy. Whole-stream worker-idle estimates were 1.7% / 1.4%. It is the main mesh bottleneck.
A sequential profile of 32 nearby LOD6 tiles measured 33.1 ms total: density-grid evaluation 14.3 ms, cell classification 2.6 ms, gradients 0.3 ms, edge interpolation 0.3 ms, triangle/vertex emission 12.9 ms, and other mesher work 0.5 ms. This sample diagnoses the per-tile stages; it is not a replacement for the full parallel whole-view timing. Density evaluation and output emission remain similarly expensive.
## Changes and research decisions
- `EffectiveRockDistance` now uses one spatial-bin lookup and evaluates cave and solid-overlay distances in the same primitive pass. Lattice semantics and floating-point result ordering were kept. The two full-clipmap hashes stayed bit-identical.
- Candidate masks from the three interval axes are merged in parallel by line, using atomic OR on disjoint output words. The candidate set did not change; this reduced the interval/sign stage by about 13 ms at 8 workers and 10 ms at 16 in these runs.
- The post-interval far build no longer rebuilds axis-line indices that only the already-finished interval solver uses. Reserving the far spatial hash map from the measured primitive count reduced the original far-bin stage from about 112 ms to 97 ms. Replacing per-bin vectors with a 24-byte four-ID inline/spill list then reduced the measured 16-worker far-bin construction to 45 ms. Primitive IDs are still appended in ascending order, which preserves each bin's field-query order.
- The inline-list build had 328,944 far bins: 181,237 fit up to four IDs inline and 147,707 spill to heap storage. Across the full stream, density and CPU mesh hashes remained unchanged. The mesher now also reserves its existing per-worker vertex map from `2 × cells²`; the 32-tile LOD6 attribution sample moved from 33.513 ms to 33.145 ms. The whole-view difference is small and within run variation, so this reserve is retained as a low-cost allocation hint rather than claimed as a major speedup.
- Research: [Abseil's Swiss-table design notes](https://abseil.io/about/design/swisstables) and Meta's [F14 hash-table write-up](https://engineering.fb.com/2019/04/25/developer-tools/f14/) describe how inline storage and cache-aware lookup can reduce pointer chasing and allocation overhead. That motivated two prototypes. A flat open-addressed spatial index cut 16-worker bin construction from about 45 ms to 33 ms, but increased the measured streaming stage from about 761 ms to 804 ms; shape plus mesh was 1.471 s versus 1.440 s with the compact inline-bin hash map. I rejected the flat index because the query cost erased its build-time win. A first inline-list layout with a separate vector enlarged every map node and also streamed slower; the retained 24-byte union layout avoids that metadata growth.
- Added per-stage/per-LOD timing and worker accounting, a 27-tile neighborhood-ready metric, and a filtered one-queue edit-batch benchmark. The edit brush SDF uses explicit SSE2 square root instructions, with `/fp:precise` and `/arch:AVX2`; it does not add CRT transcendental calls or FMA contraction.
- A thread-local last-bin cache was tried and rejected: hashes matched but the LOD6 run showed no stable speed gain. A parallel far-bin builder was also rejected: hashes matched, but spatial-bin time stayed around 107 ms and whole-view time did not improve. Skipping the unused axis-bin rebuild alone saved only a few milliseconds; the capacity reservation was the useful part.
- Research: VTK's [Flying Edges documentation](https://vtk.org/doc/nightly/release/9.7/html/classvtkFlyingEdges3D.html) and the [LDAV 2015 paper](https://doi.org/10.1109/LDAV.2015.7348069) describe edge-oriented passes, count/prefix-sum allocation, preallocated output, and avoiding coincident-point merging. The current mesher still spends comparable sampled time in density-grid evaluation and vertex/triangle emission. A full multi-pass conversion is the strongest untested mesher option, but it is a larger rewrite with separate material-class, skirt, and degeneracy handling to preserve.
- Built and measured a per-worker lattice-edge vertex cache with the existing position hash retained as a fallback. The first version collapsed 2,477 vertices on the 16-worker whole view (1,792,739 vs 1,795,216) with the same triangle count, so it was rejected as a mesh change. The exact-key fallback version restored vertex counts, but totals were 1.884 / 1.484 s versus 1.886 / 1.471 s at 8 / 16 workers: no stable gain. Both prototypes were removed.
- Tested overlapping early far-tile meshing with the shape build. That path queued LOD2+ before the far-parent shapes and spatial index were built, so it could mesh against an incomplete field; its timing is invalid for a speed claim and the path was removed. Even the invalid trial was slower at 16 workers. Correct overlap needs an immutable, separately built far field before any dependent tile starts.
- The final no-hash composite whole view is 1.805 s at 8 workers and 1.454 s at 16; it still needs another 454 ms removed at 16 to meet the one-second target. The single-critical-tile split from the plugin review does not fit this LOD6 workload: there are 4,770 queued coarse tiles, and the worker pool is busy while they run. The 6-worker point, SIMD primitive batches, forward differences, Morton page order, and explicit coarse surfaces were not prototyped in this pass; explicit surfaces and a multi-pass edge mesher remain plausible larger experiments.
- Intel's [Intrinsics Guide](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html) cautions that an intrinsic may expand to a slower instruction sequence. AVX2 batching of the irregular primitive/bin lookup was not assumed to help without a result-preserving prototype.
The final inline-bin source improves the comparable shape-build-plus-mesh result by about 9.6% at 8 workers and 10.8% at 16 versus this round's first cold baseline. The earlier round-three report's 2.095 / 1.617 s figures compare with 1.805 / 1.454 s here. Process-start timing is 1.958 / 1.629 s, so the one-second whole-view goal remains unmet.
## Determinism, routes, and surface checks
Four cold no-cache hash runs on the final built source (two at 16 workers, one each at 8 and 1) matched exactly. The field hash covers sampled density floats. The CPU-geometry hash covers each streamed tile's float positions, float normals, and indices, with tile keys included in deterministic LOD/Z/Y/X order. UV and color buffers are not stored in `FStandaloneCpuTile` and are not included in this geometry hash.
| Output | Density hash, identical in all runs | CPU geometry hash, identical in all runs |
| --- | --- | --- |
| Full-detail mesh | `0xa071e48a996e2828` | `0x6f270c621d39302f` |
| Whole clipmap | `0x630c43d7c68d5cbb` | `0xe3caf5b44244a992` |
| LOD0 | `0xc31fc55744c45928` | `0x7a70def3eb3e11ad` |
| LOD1 | `0xd74d6563f970d9f9` | `0xf47463988641b397` |
| LOD2 | `0xb0e440b4c9276bd7` | `0x9ed0581189757ee8` |
| LOD3 | `0xda4d3afb3467dd64` | `0xd8101bfb3ad87925` |
| LOD4 | `0x903537240ad6ae68` | `0x64c0b5e6a2e4f8ce` |
| LOD5 | `0x7c9e900fda72f05f` | `0x58c5139c40172196` |
| LOD6 | `0xc67d1f383fbf29ca` | `0xdc35accf7789860d` |
All runs reported zero mesh-cache hits. Each shape build retained all 5,642/5,642 passage route samples as open.
The saved earlier `final_exact_hash_*` set differs in full-detail, whole-clipmap, LOD0, and LOD1 hashes; LOD2–LOD6 match. The source/output change responsible for that older-to-current hash shift is still unexplained. The current hash set is stable across 1 / 8 / 16 workers and two 16-worker runs, but I do not claim bit identity with that earlier saved executable.
This verifies repeatability on this AVX2 host across 1, 8, and 16 workers. A second hardware host was not available, so cross-machine bit identity remains unverified here.
| Check | Final result |
| --- | --- |
| Chamber grid-stair index | 0.003 (threshold 0.400) |
| Tunnel grid-stair index | 0.028 (threshold 0.400) |
| Passage grid-stair index | 0.205 (threshold 0.400) |
| Shared tunnel joins | 280; endpoint radius and slope deltas 0.000000 / 0.00000000 |
| Whole-view level-zero sheet audit | 0 sub-voxel sheets, 0 isolated small components; 4 small boundary-touching components |
| Chamber flat-patch audit | 5.0% at the 16-voxel patch rule, largest 433 samples; audit passes, though it is not a visual sign-off |
The baseline tunnel stair audit without `--round-tunnels` measured 0.434 and failed the 0.400 threshold; the final tunnel view measures 0.028. The baseline chamber (0.041) and passage (0.276) already passed that threshold, so those two are improvements rather than fail-to-pass proofs. The earlier invalid baseline command with `--round-tunnels` remains recorded in [`round2/REPORT.md`](../round2/REPORT.md).
I reran the owner-field scan on the compact-bin source with the round-two settings: 8 workers, 960 full-resolution tiles for close views, 4-sample vertex stride, and 4,770 LOD6 tiles for far. `mesh_sample_mismatch=0` in each scan. The later vertex-map reservation only changes capacity; final no-cache density and CPU geometry hashes match the pre-reservation build exactly. The older saved scalar hashes differ, so these refreshed sign and distance results replace the stale “unchanged” claim.
| View | Lattice samples | Changed signs, before → current | Mean sampled surface distance, before → current | Max, before → current | Unmatched, before → current |
| --- | ---: | ---: | ---: | ---: | ---: | | --- | ---: | ---: | ---: | ---: | ---: |
| Chamber and tunnel region | 35,103,600 | 5.978% → 5.895% | 1.0325 m → 1.0826 m | 6.4677 m → 6.4840 m | 9,393 → 8,832 | | A, 8 workers | 2.174 s | 0.828 s | 1.116 s | 1.944 s | 0 |
| Passage | 37,240,000 | 3.311% → 3.304% | 0.9814 m → 1.0292 m | 6.3422 m → 6.3750 m | 12,602 → 12,520 | | B, 8 workers | 2.135 s | 0.836 s | 1.110 s | 1.946 s | 0 |
| Far view, LOD6 | 9,470,835 | 9.041% → 9.041% | 2.1667 m → 2.1588 m | 5.3070 m → 5.3070 m | 783,976 → 783,967 | | Mean | **2.155 s** | **0.832 s** | **1.113 s** | **1.945 s** | 0 |
Closest-view sign disagreement fell slightly while mean surface distance rose; the far view changed almost not at all. These are sampled symmetric nearest-vertex distances, not continuous Hausdorff distances, and do not indicate that the generated world looks close to the owner. The process runner includes executable start and world setup. The stream stage holds the 8-worker pool at 98.9–99.4% busy; LOD6 is the long pole. The full payload allocates 235.49 MiB across the view; the standalone viewer’s geometry copy is 115.67 MiB. The 27-tile LOD0 player neighborhood is CPU-mesh-ready 23 ms after streaming starts. Collision readiness remains unmeasured.
## Captures and visual review ### Stage comparison and per-LOD profile
The final rebuilt captures are in `rebuilt_after_*.png`. Five are byte-identical to the previous exact captures; the passage differs in 4 of 1,024,000 pixels by at most 1 channel value. The current captures look unchanged at display scale. Against the owner images, the chamber still has a broad smooth ceiling and floor where the owner view has more connected forms; the tunnel has smoother, more repetitive relief; the passage retains a horizontal shelf/dark opening and lacks the owner's layered profile; the far image lacks the owner's larger connected forms. The look remains visibly different and is not a match. The first column is the earlier speed-round W8 baseline from the stale report. That run used the former full-machine policy and the older viewer payload. The current column uses the restored half-load policy and emits all mesh arrays. These values show where the work moved, but are not an apples-to-apples speedup claim.
| View | Before | Final after | Owner | | Whole-view stage | Earlier W8 baseline | Current W8 mean | Current run range |
| --- | --- | --- | --- | | --- | ---: | ---: | ---: |
| Chamber | [before](../round2/final_after_chamber.png) | [after](rebuilt_after_chamber.png) | [owner](../final/chamber_fixed_owner.png) | | Local shapes and line bins | 57.6 ms | 53.8 ms | 53.773–53.785 ms |
| Tunnel | [before](../round2/final_after_tunnel.png) | [after](rebuilt_after_tunnel.png) | [owner](../final/tunnel_fixed_owner.png) | | Rock intervals, packed signs, candidate axes and mask merge | 132.6 ms | 138.3 ms | 138.111–138.506 ms |
| Passage | [before](../round2/final_after_passage.png) | [after](rebuilt_after_passage.png) | [owner](../final/passage_fixed_owner.png) | | Surface detail band | 539.0 ms | 589.8 ms | 589.734–589.833 ms |
| Far view | [before](../round2/final_after_far.png) | [after](rebuilt_after_far.png) | [owner](../final/far_owner.png) | | Far-parent shapes and spatial bins | 194.7 ms | 41.9 ms | 38.438–45.407 ms |
| Passage-floor close-up | [before](../round2/final_after_passage_floor_closeup.png) | [after](rebuilt_after_passage_floor_closeup.png) | no owner close-up | | Shape build | 932.3 ms | 831.7 ms | 827.765–835.705 ms |
| Tunnel bend | [before](../round2/final_after_tunnel_bend.png) | [after](rebuilt_after_tunnel_bend.png) | no owner close-up | | All-LOD full-payload mesh generation | 1,064 ms | 1,113 ms | 1,110–1,116 ms |
| Shape build + mesh generation | 1.996 s | 1.945 s | 1.944–1.946 s |
## Build, process limits, and failed attempts Per-LOD completion is cumulative from stream start. Each tile count and allocation figure is from the full-payload run.
The final build is [`vertex_map_reserve_build.log`](vertex_map_reserve_build.log). It succeeded with `/MP16`; the build host ran at Normal priority and affinity `0xFFFF`. It still reports the existing `vswhere.exe` setup diagnostic and six C4838 narrowing warnings in `VoxelStrateManager.cpp`. Benchmark processes ran Normal with `0xFFFF`; final whole-view no-cache runs peaked at 0.41 / 0.48 GiB private memory at 8 / 16 workers. All test processes were launched by this task; no external viewer was closed. | LOD | Tiles | Density samples | Complete | Full payload allocation | Viewer geometry copy |
| ---: | ---: | ---: | ---: | ---: | ---: |
| 0 | 512 | 21.952 M | 0.137 s | 33.60 MiB | 16.30 MiB |
| 1 | 448 | 19.208 M | 0.214 s | 20.37 MiB | 10.07 MiB |
| 2 | 320 | 2.195 M | 0.231 s | 6.04 MiB | 2.96 MiB |
| 3 | 208 | 1.427 M | 0.244 s | 4.23 MiB | 2.09 MiB |
| 4 | 144 | 0.988 M | 0.251 s | 2.99 MiB | 1.49 MiB |
| 5 | 96 | 0.658 M | 0.258 s | 3.17 MiB | 1.57 MiB |
| 6 | 4,770 | 32.717 M | 1.109 s | 165.09 MiB | 81.18 MiB |
- One build wrapper invocation used a relative `Tee-Object` path. `Build.ps1` changed the working directory, so the log path resolved under `Standalone\Build` and the wrapper returned 1. I checked that no compiler remained active and reran with an absolute log path; the final build succeeded. This was a logging-path failure, not a compiler failure. LOD6 emits 1,834,816 vertices and 10,272,252 indices. It accounts for 32.717 million density samples and most of the stream time. The detailed timings are in [run A](final_full_payload_w8_a.stdout.log) and [run B](final_full_payload_w8_b.stdout.log); their process records are [A](final_full_payload_w8_a.runner.log) and [B](final_full_payload_w8_b.runner.log).
- One benchmark invocation referenced `Research\...\RunMachine.ps1` from the plugin root instead of `Standalone\Research\...`; PowerShell rejected the script path before an executable started. The corrected invocation completed.
- A `rg | head` inspection hit a Cygwin `CreateFileMapping` permission diagnostic; the source query was repeated with PowerShell and no build or benchmark was affected.
- The simple edge-cache prototype that produced fewer vertices and the early-far overlap prototype were rejected as described above. Their build/run logs remain in this directory; no such code remains in the final source.
- The parallel far-bin experiment built and ran successfully and kept exact hashes, but did not improve the bin stage or wall time; its code was removed.
- The historical `resume2_before_tunnel_audit.stdout.log` used `--round-tunnels` and is not a valid base-world comparison. The corrected no-`--round-tunnels` baseline is retained at 0.434 and failed the final 0.400 threshold, as described above.
- No compiler build or benchmark executable failed during this speed round. The flat-map and first larger inline-bin prototypes ran successfully but were rejected for slower total results. The corrected baseline stair audit still fails at 0.434 against the 0.400 threshold; this failure is preserved in the earlier report. No commit, push, or stash was made.
## Logs ## Required mesh payload and skip verification
Baseline profiles: `profile_whole_w8.stdout.log`, `profile_whole_w16.stdout.log`. Final no-hash whole-view stages: `measured_whole_w8/w16.stdout.log`; process walls: `measured_whole_w8/w16.runner.log`. Full detail: `measured_detail_w8/w16.stdout.log` and `measured_detail_w8/w16.runner.log`. Final determinism on the exact rebuilt source: `final_source_hash_w1.stdout.log`, `_w8.stdout.log`, `_w16_a.stdout.log`, `_w16_b.stdout.log`. Cross-LOD edit latency: `reserve_edit_w8/w16.stdout.log`. Refreshed owner scans: `final_owner_bins_chamber_w8.stdout.log`, `final_owner_bins_passage_w8.stdout.log`, `final_owner_bins_far_w8.stdout.log`. Mesher attribution: `lod6_sample32_profile.stdout.log` and `vertex_map_reserve_profile.stdout.log`. Current audits/captures: `rebuilt_audit_*.stdout.log`, `rebuilt_shard_audit_w16.stdout.log`, `rebuilt_capture_*.stdout.log`, and `rebuilt_after_*.png`. I inspected the actual plugin source read-only. [FVoxelMeshData](../../../../Source/VoxelForge/Public/VoxelTypes.h) contains one UV and one FColor per vertex, including dominant/neighbor biome palette, slope, and border blend channels. [BuildTileStreamSet](../../../../Source/VoxelForge/Private/VoxelWorld.cpp) enables TexCoords, Colors, and PolyGroups and copies those values into the game stream. It assigns group 0 to ground and group 1 to sky-cap ceiling; the ceiling uses material slot 1 and does not cast shadows. The mesher’s [UV/color emission](../../../../Source/VoxelForge/Private/VoxelMarchingCubesMesher.cpp) and cap ordering show why the viewer-only skips cannot be used for game output.
The current default is full payload. In the standalone copy, UV storage is FVector2f, matching the float UV sent to the game stream; this halves the UV staging width from FVector2D and saves about 20.6 MB over 2.58 million vertices. No stable speed gain appeared. FColor is already four-byte packed. The dense per-tile ceiling-column cache preserved hashes but did not improve stream time (1.099 s versus 1.098 s), so it was reverted. The final source retains the original per-column memo and emits ceiling/ground groups. I did not derive UVs or biome colors in a new shader: the game material path currently consumes TexCoord0 and the packed vertex-color channels, and plugin material/source changes were read-only in this task.
The three diagnostic skip variants were independently hash-checked at W8 over full detail, whole clipmap, and LOD0–6. Density hashes and a canonical oriented-triangle topology hash match full output in all nine outputs. The topology digest uses vertex position/normal identities, so it is insensitive to vertex IDs and triangle ordering.
| Diagnostic mode | Density equal | Serialized mesh hash equal | Oriented topology equal | Full payload hash equal |
| --- | ---: | ---: | ---: | ---: |
| Skip UV and colors | 9/9 | 9/9 | 9/9 | 0/9 |
| Skip material groups | 9/9 | 6/9 | 9/9 | 6/9 |
| Skip both | 9/9 | 6/9 | 9/9 | 0/9 |
The group skip reorders index output in three results; the geometric triangles and winding still match. The payload hash changes when UV/color arrays are omitted. These are diagnostic results only: dropping either data path changes the game’s stream contract. Combined skips took 1.533 s and 1.536 s process wall in no-hash runs, about 0.620 s below the full-payload mean, but this is not a valid game-generation result. Hash logs: [UV/colors](canonical_topology_skip_uv_w8.stdout.log), [groups](canonical_topology_skip_groups_w8.stdout.log), and [both](canonical_topology_skip_both_w8.stdout.log).
### Far-bin reserve A/B
The final far-bin reserve is 24 buckets per primitive; the axis-bin path retains 96. I rebuilt the same source with the old 2× far-bin estimate, then restored and rebuilt the final 24× source. All nine full-payload outputs matched exactly across density, mesh, payload, and topology hashes between the two reserve settings. The final 1-worker and 8-worker matrix also matches exactly.
Across two no-hash samples, the 2× build spent about 47.9 ms building far spatial bins; the 24× build averaged about 41.9 ms. Full stream time was effectively unchanged (1.1135 s versus 1.113 s), within run variation. I keep the capacity hint as a small build-stage improvement, not a whole-view speed claim. The old-estimate build and runs are logged in [build](reserve_ratio_old2_build.log), [hash](reserve_ratio_old2_hash_w8.stdout.log), [timing A](reserve_ratio_old2_timing_w8.stdout.log), and [timing B](reserve_ratio_old2_timing_w8_b.stdout.log).
## Final determinism matrix
The final executable was rebuilt after restoring the 24× reserve. The 1-worker and 8-worker cold hash runs had zero cache hits and all 5,642/5,642 route samples open. Each of the nine output hashes below matched exactly at both worker counts, including the full payload hash.
| Output | Density hash | Geometry mesh hash | Full payload hash | Oriented topology hash |
| --- | --- | --- | --- | --- |
| Full detail | 0xa071e48a996e2828 | 0x6f270c621d39302f | 0x33c7353b2eda16a7 | 0x1b39c8cc2ffed4b3 |
| Whole clipmap | 0x95c3f446cce0eaea | 0x5036df929534d3ef | 0xb9ed5e975705753b | 0xb0eec830b29044e1 |
| LOD0 | 0xc31fc55744c45928 | 0x7a70def3eb3e11ad | 0x5780ec294cd873c5 | 0xa3126405acc20185 |
| LOD1 | 0xd74d6563f970d9f9 | 0xf47463988641b397 | 0x7740ea61a08d30c7 | 0xb8a7cace0866efd3 |
| LOD2 | 0x44f878e3420376a2 | 0x17de625b9ceda2a9 | 0x1fe6c3de98251541 | 0x1c4ab868773fe313 |
| LOD3 | 0x80713f3f7e379bf9 | 0xec609008c8248eeb | 0x8f9c8b3d627933b4 | 0x27361f6a30c71959 |
| LOD4 | 0x726efa9eb9933226 | 0xdcb8c20437152e74 | 0xd01d47eeed04fd3a | 0x94972f103fa9bcac |
| LOD5 | 0x4345a3990bb8b68a | 0x8b7d9b5c1a0872a4 | 0x396b044b5a774097 | 0x0c5fb1df85d0d38c |
| LOD6 | 0xc78f04f08fc84094 | 0x2eab9309b9493d47 | 0xa93aedb363c18aa5 | 0x4f6059255abed405 |
These checks establish repeatability on this AVX2 host across 1 and 8 workers. Cross-machine identity was not tested.
## What helped and what was rejected
Retained work from the measured speed round:
- Parallel candidate-mask merging reduced the interval/sign stage by about 14 ms in the predecessor’s 16-worker profile while preserving hashes.
- A line-local value-noise cache reuses relief noise corners along Z; its 16-worker detail-band sample fell from about 375 ms to 298 ms with exact hashes.
- A lattice-specific mesher entry point removed repeated coordinate rounding. The predecessor’s 16-worker full-detail stream moved from about 183 ms to 129–141 ms in hash runs.
- Fusing repeated rock-distance evaluation preserved hashes and lowered one 16-worker hash-run stream sample from 698 ms to 624 ms.
- Direct far capsules replaced 181,836 distant connector primitives with 16,491 while keeping all routes open and hashes stable across worker counts. They cut far-parent construction by roughly 75 ms in the predecessor’s profile.
- The compact inline/spill bin list reduced far-bin construction while keeping primitive IDs in ascending query order. A flat open-addressed table and an earlier larger inline layout were rejected because total streaming slowed.
- The per-worker vertex-map reserve remains a small allocation hint: a 32-tile LOD6 sample moved from 33.513 ms to 33.145 ms, with no stable whole-view gain.
- The current 24× far-bin capacity hint saves about 6 ms in the measured far-bin stage against 2×, with exact hashes and no measurable full-stream change.
Rejected or not tested:
- Skipping UV/colors and skipping material groups improved the standalone diagnostic viewer only. They remain off by default because the game consumes those arrays and sections.
- The dense ceiling-column cache produced no stable stream improvement and was removed.
- A thread-local last-bin cache, parallel far-bin builder, per-tile field snapshot, and per-worker edge cache did not improve the final total. The first edge-cache version removed 2,477 vertices and was rejected as a geometry change.
- Early far-tile overlap changed LOD0/1 hashes while far parents were incomplete; the path was removed.
- The multi-pass Flying Edges-style mesher, AVX2 primitive batches, forward differences, Morton page order, Bernstein bounds, six workers, and explicit coarse far surfaces were not prototyped. The measured workload has 4,770 queued LOD6 tiles and a busy worker pool, so splitting one critical tile is not the current bottleneck.
Research retained for these choices: [Abseil Swiss tables](https://abseil.io/about/design/swisstables), [Meta F14](https://engineering.fb.com/2019/04/25/developer-tools/f14/), [VTK Flying Edges](https://vtk.org/doc/nightly/release/9.7/html/classvtkFlyingEdges3D.html), the [LDAV 2015 paper](https://doi.org/10.1109/LDAV.2015.7348069), and Intel’s [Intrinsics Guide](https://www.intel.com/content/www/us/en/docs/intrinsics-guide/index.html). In particular, the latter cautions that an intrinsic does not guarantee a faster instruction sequence.
## Routes, joins, shape checks, and edit latency
Every final shape build reports 5,642/5,642 passage centerline samples open and retains the native landing floors. The three fixed-view grid-stair checks pass the 0.400 threshold.
| Fixed view | Grid-stair metric | Flat-patch share | Largest flat patch | Result |
| --- | ---: | ---: | ---: | --- |
| Chamber | 0.003 | 5.0% | 433 samples | Pass |
| Tunnel | 0.028 | 5.0% | 433 samples | Pass |
| Passage | 0.205 | 8.0% | 520 samples | Pass |
The chamber, tunnel, and passage detail checks pass at a 4-voxel supremum with an 8-voxel band. Shared tunnel joins: 280 in chamber/tunnel and 240 in passage; endpoint radius and slope deltas are 0.000000 and 0.00000000. The room-floor, floor-slope, C1 floor-profile, and tunnel-radius audits pass. The whole-view level-zero shard audit covers 512 tiles and finds zero thin sheets, zero isolated small components, and four small boundary-touching components.
The crater benchmark is one transaction containing three brushes (10 m diameter, 24 m from the player): a sphere carve, rounded-box cut, and capsule rim. At 8 workers it remeshed all 14 affected tiles with zero cache hits: 12 LOD0 and 2 LOD1. Remesh time was 14 ms; edit-to-ready was 17 ms. Collision cooking is unavailable in the standalone harness.
Current audit logs: [chamber](final_audit_chamber_w8.stdout.log), [tunnel](final_audit_tunnel_w8.stdout.log), [passage](final_audit_passage_w8.stdout.log), [whole-view shard/join audit](final_shard_audit_w8.stdout.log), and [edit transaction](final_edit_transaction_w8.stdout.log).
## Six fixed captures and visual review
All six captures were regenerated from the final full-payload executable at 8 workers and the half-load process limit.
| View | Fixed camera in metres; yaw/pitch in degrees | Current capture |
| --- | --- | --- |
| Chamber | (-11.75, -123.25, -19.85); 0 / 0 | [chamber](final_halflimit_after_chamber.png) |
| Tunnel | (-11.75, -123.25, -19.85); 90 / 0 | [tunnel](final_halflimit_after_tunnel.png) |
| Passage | (-39.19, 6.27, -28.51); 63 / 0 | [passage](final_halflimit_after_passage.png) |
| Far view | (-11.61244, -118.2212, -19.8452); 0 / 0 | [far](final_halflimit_after_far.png) |
| Passage-floor close-up | (-39.19, 6.27, -25.51); 63 / -25 | [floor close-up](final_halflimit_after_passage_floor_closeup.png) |
| Tunnel bend | (-198.97, -99.57, -45.58); 33.59 / 23.17 | [bend](final_halflimit_after_tunnel_bend.png) |
Five captures are byte-identical to the preceding rebuilt set. The passage view differs in 662 of 1,024,000 pixels, with a maximum channel delta of 97; side-by-side inspection shows the same framing and visible geometry. Against the owner images, the visual gap remains: the chamber still has broad smooth ceiling/floor areas, the tunnel has repeated relief bands, the passage keeps a horizontal shelf and dark opening instead of the owner’s layered profile, and the far view lacks the owner’s larger connected forms. The look is not a match.
The four owner-comparison references remain [chamber](../final/chamber_fixed_owner.png), [tunnel](../final/tunnel_fixed_owner.png), [passage](../final/passage_fixed_owner.png), and [far view](../final/far_owner.png). The earlier fixed captures are in [round 2](../round2/REPORT.md).
## Build limits and failed invocations
The final build is [final_halfload_build.log](final_halfload_build.log). It succeeded with /MP8. The build and workload processes ran BelowNormal on affinity 0xFF00. The recurring French vswhere.exe setup diagnostic still appears, followed by a successful compile; six existing C4838 narrowing warnings remain in VoxelStrateManager.cpp.
Failures during this continuation, with corrected work preserved:
- The first dense ceiling-column cache build used CellsPerAxis before its declaration. That build failed; the declaration was moved before use, the retry built, and the cache was later removed after a no-gain timing result.
- One audit runner call named StandaloneViewer.exe, which does not exist. RunMachine stopped before launching a child; the corrected executable is VoxelForgeStandalone.exe.
- The first six-capture wrapper assembled --camera and --capture arguments as separate strings. Each process exited 2 on Invalid --camera value before opening a viewer. The arguments were corrected and all six final captures completed with exit code 0.
- An earlier wrapper call used a relative Tee-Object path after Build.ps1 changed directories. Its log redirection failed; the build was checked and rerun with an absolute log path.
- Older exploratory audit and optimization failures from the look round remain documented in [round 2](../round2/REPORT.md) and its retained logs.
No external viewer was closed. No commit, push, or stash was made. All source, report, and capture edits are within Standalone.
## Principal logs
- Final no-hash full-payload timings: final_full_payload_w8_a/b.stdout.log and final_full_payload_w8_a/b.runner.log.
- Final restored hash matrix: final_restored_full_hash_w1.stdout.log and final_restored_full_hash_w8.stdout.log.
- Skip checks: canonical_topology_full_hash_w1/w8.stdout.log and canonical_topology_skip_uv_w8, canonical_topology_skip_groups_w8, canonical_topology_skip_both_w8.stdout.log.
- Reserve A/B: reserve_ratio_old2_build.log and reserve_ratio_old2_*.stdout.log.
- Current audits, edit run, and captures are listed above; capture process output is in final_capture_*_w8.stdout.log.
- Machine runner policy: RunMachine.ps1. Build flags and limits: Build.ps1.
@@ -16,8 +16,8 @@ $StdoutPath = Join-Path $RoundDir ($LogStem + '.stdout.log')
$StderrPath = Join-Path $RoundDir ($LogStem + '.stderr.log') $StderrPath = Join-Path $RoundDir ($LogStem + '.stderr.log')
$HostProcess = Get-Process -Id $PID $HostProcess = Get-Process -Id $PID
$HostProcess.PriorityClass = 'Normal' $HostProcess.PriorityClass = 'BelowNormal'
$HostProcess.ProcessorAffinity = [IntPtr]0xFFFF $HostProcess.ProcessorAffinity = [IntPtr]0xFF00
$RunClock = [Diagnostics.Stopwatch]::StartNew() $RunClock = [Diagnostics.Stopwatch]::StartNew()
$Child = Start-Process -FilePath $ExecutablePath -ArgumentList $ExecutableArguments ` $Child = Start-Process -FilePath $ExecutablePath -ArgumentList $ExecutableArguments `
-WorkingDirectory $StandaloneRoot -WindowStyle Hidden -PassThru ` -WorkingDirectory $StandaloneRoot -WindowStyle Hidden -PassThru `
@@ -27,8 +27,8 @@ $PeakPrivateMemory = 0L
while (-not $Child.HasExited) { while (-not $Child.HasExited) {
$Child.Refresh() $Child.Refresh()
if ($Child.HasExited) { break } if ($Child.HasExited) { break }
$Child.PriorityClass = 'Normal' $Child.PriorityClass = 'BelowNormal'
$Child.ProcessorAffinity = [IntPtr]0xFFFF $Child.ProcessorAffinity = [IntPtr]0xFF00
$PeakWorkingSet = [Math]::Max($PeakWorkingSet,$Child.WorkingSet64) $PeakWorkingSet = [Math]::Max($PeakWorkingSet,$Child.WorkingSet64)
$PeakPrivateMemory = [Math]::Max($PeakPrivateMemory,$Child.PrivateMemorySize64) $PeakPrivateMemory = [Math]::Max($PeakPrivateMemory,$Child.PrivateMemorySize64)
Start-Sleep -Milliseconds 50 Start-Sleep -Milliseconds 50
@@ -36,5 +36,5 @@ while (-not $Child.HasExited) {
$Child.WaitForExit() $Child.WaitForExit()
$RunClock.Stop() $RunClock.Stop()
$Child.Refresh() $Child.Refresh()
Write-Output "RUN_EXIT pid=$($Child.Id) priority=Normal affinity=0xFFFF wall_s=$([math]::Round($RunClock.Elapsed.TotalSeconds,3)) peak_working_set_gb=$([math]::Round($PeakWorkingSet/1GB,2)) peak_private_gb=$([math]::Round($PeakPrivateMemory/1GB,2)) cpu_s=$([math]::Round($Child.CPU,3)) exit_code=$($Child.ExitCode)" Write-Output "RUN_EXIT pid=$($Child.Id) priority=BelowNormal affinity=0xFF00 wall_s=$([math]::Round($RunClock.Elapsed.TotalSeconds,3)) peak_working_set_gb=$([math]::Round($PeakWorkingSet/1GB,2)) peak_private_gb=$([math]::Round($PeakPrivateMemory/1GB,2)) cpu_s=$([math]::Round($Child.CPU,3)) exit_code=$($Child.ExitCode)"
exit $Child.ExitCode exit $Child.ExitCode
@@ -0,0 +1,87 @@
I’ll read the committed round-three report first, then establish a fresh cold baseline with stage, LOD, worker, and idle-time breakdowns before changing the standalone. I’ll preserve the fixed visual and determinism checks throughout.
I’m continuing from the measured round-three state. First I’ll read its report, then I’ll run fresh 8- and 16-worker cold baselines and inspect which stages dominate before selecting any optimization.
The committed report confirms the last source state, and the prior shape/quality audits are available. The cold whole-view benchmark already reports aggregate shape stages and worker-time per LOD, but not per-LOD wall intervals or neighborhood readiness; I’m recording those gaps while I take the fresh baseline.
The fresh baseline reproduces the gap: at 16 workers the compiler stage is 0.783 s and uncached meshing is 0.827 s; at 8 workers those are 1.050 s and 1.078 s. In the mesh work, LOD6 accounts for about 6.5 worker-seconds at 8 workers and 9.7 at 16, so the far shell is the main parallel workload; the sign/detail compiler’s surface band is the largest single compile stage. I’m tracing both now and adding the missing wall-time slices before changing them.
The profile shows two useful implementation facts: the sign pass already does interval solving and packed words, and tile meshing is already independent per worker with thread-local density scratch. The missing profiler slices are now instrumented, including per-LOD completion and worker busy time. I’m rebuilding this measurement-only change, then I’ll use those timings to decide whether page batching or evaluator work is the right target.
The finer profile changes the optimization choice: at 16 workers, the detail pass is 0.395 s wall but has about 6.2 worker-seconds of work, while far LOD6 costs about 10 worker-seconds. The hot field path does three spatial hash lookups per sample (cave, overlay, critical-air) against the same bin. I’m preserving a pre-change density hash now, then I’ll combine those lookups into one ordered primitive scan and verify bitwise equality.
The density hashes are identical for the full clipmap and every LOD before and after merging the field lookups. The no-hash run improved full-stream worker work from 13.27 to 11.65 s at 16 workers, with LOD6 dropping from 9.73 to 8.60 s. I’m testing a second, exact optimization: build the vertical candidate mask directly, then merge the two horizontal masks in parallel with atomic bit-ORs. This targets the 40 ms serial mask pass without changing candidate membership.
The parallel mask merge passes the exact per-LOD density hashes and cuts the interval/sign stage by roughly 14 ms on the 16-worker run. I’ve also found another cheap reuse opportunity in the far evaluator: adjacent samples revisit the same spatial bin, so I’m adding a generation-checked per-worker last-bin pointer. It changes only lookup, not bin contents or density math.
The profile still points to two expensive regions: fine detail evaluation in the shape build, and LOD6 meshing. I’m checking the experimental bin-cache edit now; its runs kept the hash but showed no reliable speedup, so I’ll remove it unless the source check says it was already reverted, then continue with neighborhood and edit measurements.
I’ve added two measurement hooks before tuning further: cold readiness for the 3×3×3 level-zero player neighborhood, and a single queued edit transaction that combines sphere, rounded-box, and capsule brushes, then remeshes only intersecting visible tiles at each LOD. The edit benchmark uses no mesh cache; the standalone still cannot cook collision, so I’ll report mesh readiness separately from actual collision readiness. I’m building this instrumentation now.
The edit transaction now completes its 27 affected LOD0 tiles in 19 ms at 8 workers and 11 ms at 16, with zero cache hits. In this view no coarser tile intersects the brush bounds; the report will list only the levels actually affected. The current build succeeded with the known `vswhere` setup message and six unrelated narrowing warnings. Next I’m measuring neighborhood readiness and the final cold 8/16 worker timings.
Cold results are now in: the full-detail region plus its 27-tile neighborhood is 0.714 s at 8 workers and 0.595 s at 16, with mesh-only neighborhood readiness at 17/19 ms. The whole view is still 1.881/1.527 s because the detailed field build and the 6,498-tile mesh run sequentially; the coarsest LOD alone consumes 0.957/0.753 s. I’m checking whether a low-risk overlap or meshing change can close that gap without changing the field.
The far-parent timing includes a redundant rebuild of the three axis-line indices after interval compilation is already finished. Those indices only serve the interval solver; far-field queries use spatial bins. I’m removing that post-solver rebuild and will reject it if exact mesh hashes or route/quality checks change. This is a measured 100+ ms target and avoids a field approximation.
The parallel far-bin assembly preserved every mesh hash, but it did not lower the 107–112 ms bin stage or whole-view time; its extra per-worker maps are not justified. I’m dropping that trial and trying only capacity reservation on the existing deterministic builder. The coarse LOD remains the dominant mesh stage, so I’ll keep the failed speed hypothesis and its measurements in the report.
The capacity reservation is a useful low-risk win: the far spatial-bin stage fell from about 112 ms to 96 ms, with 328,944 bins and 1.58 million references. The exact field and per-LOD mesh hashes stayed the same in the axis-skip trial; I’m doing the final hash sweep on this reserved-builder source now, followed by the route, stair, sheet, and fixed-view checks.
The final-source audits pass: all 5,642 route samples remain open; chamber/tunnel/passage grid-stair indices are 0.003/0.028/0.205 against 0.400; the whole-view level-zero mesh has 0 thin sheets and 0 isolated small components. Three small components touch tile boundaries, which the per-tile audit cannot classify as detached. I’m refreshing the six fixed captures now, then I’ll review them alongside the owner views.
The exact final rebuild succeeded after I replaced the edit probe’s square roots with explicit SSE instructions, keeping that added field operation deterministic under the stated constraints. Hashes still match. I’m refreshing the no-hash whole-view timings and image captures from that exact executable before I close out the report.
One refreshed passage PNG has a different file hash from its earlier capture, although it looks identical at display scale. I’m checking decoded pixels before calling it unchanged; I’ll record the exact result rather than treating a file-hash mismatch as harmless metadata.
The final exact-source runs confirm the same limiting pattern: LOD6 costs about 0.99 s at 8 workers and 0.74 s at 16; shape compilation adds another 0.88/0.74 s before it can begin. I’m looking at the mesher’s sampling and allocation path once more to see whether there is a field-preserving structural cut larger than the bin reservation. If there isn’t, the report will call out the remaining gap plainly.
The mesher already reuses per-thread density grids and avoids a shared vertex append lock, so there is no cheap mesher-side switch left to try. One larger safe-to-test option is overlapping far LOD work with fine field compilation: start only coarse tiles whose sample bounds cannot touch the unfinished fine lattice, then queue the near levels once that lattice is ready. I’m implementing this as an opt-in cold benchmark path and will keep it only if hashes, routes, and images stay exact.
I’m checking the overlap experiment’s working-tree state first. If the partial patch didn’t land cleanly, I’ll apply it in small pieces, then build and compare hashes before deciding whether it stays.
The overlap path is now wired as an opt-in benchmark: it splits the requested worker pool between far tiles and the shape compiler, then adds the remaining workers once compilation finishes. First I’m building the reordered shape stage and running the existing hash audit; the far parents are added before interval compilation now, so exact equality is the first gate.
The first exact hash run caught a real flaw in the moved stage: the far build changed the spatial bin size used by fine-detail sampling, which altered LOD0/1 meshes. I’m separating the far query index from the fine compiler’s bins now, so far tiles can start early while detail compilation keeps its original sampling index and results. I won’t keep the reorder unless the old hashes return.
I’ve removed the overlap prototype after it failed the exact output check; I’m restoring the prior build and stream path now. The useful result from the failed prototype is that far LOD2–6 hashes stayed stable while LOD0/1 changed, so that ordering isn’t safe without a deeper split of the local and far field data.
The overlap idea is shelved. I’m doing the last audit against the rebuilt source now: fresh worker-count hashes and cold 8/16 timings, then I’ll align the report to those outputs and record the rejected ordering experiment. The old saved hash set does not match the rebuilt binary, so I’m treating that mismatch as a failed comparison and checking whether the rebuilt result is internally deterministic.
The rebuilt binary is stable across 1, 8, and 16 workers, but it produces a different LOD0/1 density hash from the saved “final exact” run; LOD2–6 still match. I’m treating that as an unresolved output-baseline discrepancy, not claiming the prior hashes passed. I’m gathering the remaining cold neighborhood and edit timings against this rebuilt binary so the report separates verified results from the old record.
I found the fixed camera table in the round-two report, so I can refresh the same six views instead of relying on the older PNGs. I’m capturing chamber, tunnel, passage, far, floor close-up, and bend from the rebuilt binary now; this will show whether the LOD0/1 hash difference is visible.
The six refreshed fixed-view screenshots confirm the rebuild looks unchanged: five are byte-identical to the saved captures; the passage differs at only four pixels by one channel value. I’m rerunning the fixed-view stair/detail audits and the thin-sheet shard audit on this binary before I finalize the report.
The current rebuild’s hashes match the earlier far-first prototype; the mismatch is with the older saved hash record, not between the two stage orders. That gives me a safe test target: I’m re-enabling the prestart only behind an explicit benchmark flag, then I’ll require exact matches against the rebuilt baseline and keep it only if it reduces whole-view wall time.
The measured overlap run is not a win: at 8 workers it reaches 1.831 s versus 1.888 s sequential, but at 16 workers it regresses to 1.553 s versus 1.497 s. The split doubles contention during the detail band, stretching its wall time to 0.802 s at 16. I’m removing this path; it does not justify its complexity or meet the one-second target.
I’m removing the measured-but-unhelpful overlap prototype, then I’ll rebuild the exact clean source and refresh its hash and report evidence. The no-overlap profile still misses the whole-view target, so I’ll keep the report explicit about that gap and the unexplained difference from the older saved hashes.
The clean rebuild command hit a PowerShell logging-path error because `Build.ps1` temporarily changes directories; the compiler may still be running. I’m checking that process before retrying with an absolute log path so I don’t overlap two builds.
The overlap code is gone and the `/MP16` rebuild succeeded. I’m now verifying determinism against that rebuilt executable at 1, 8, and 16 workers, including a second 16-worker run; then I’ll update the report with the rejected overlap timings and the final build record.
The rebuilt measurements put the remaining cost in two places: at 16 workers the shape build is about 0.73 s, while LOD6 meshing is about 0.74 s; LOD6 alone accounts for most mesh time. The early-stream experiment overlapped work before the far-field shape existed, so I’m treating it as invalid for correctness and retaining only its timing as a rejected prototype. I’m checking the mesher’s tile workload and current field loops before choosing the next change.
The source already uses cell pruning and thread-local output buffers, but every emitted triangle still performs repeated position-key hash lookups to deduplicate shared grid-edge vertices. VTK’s Flying Edges design avoids that pattern with edge-indexed vertices and preallocated passes. I’m testing a smaller compatible step: cache each lattice edge’s vertex index per worker, while retaining the old position-hash fallback for degenerate crossings. It should preserve vertex order and geometry while cutting repeated map probes.
The edge cache builds successfully. It preserves the old coordinate hash as a fallback, so its correctness path only changes repeated references to the same lattice edge. I’m measuring the whole view at both worker counts first; if the output checks pass and the gain is stable, I’ll keep it, otherwise I’ll revert it.
The exact-preserving version keeps the vertex and triangle counts unchanged, but it does not give a stable whole-view win: 8 workers moved from 1.996 s combined to 1.994 s, while 16 moved from 1.471 s to 1.484 s. I’m removing it; the first version’s slightly lower vertex count was also a real geometry difference, so it is rejected. I’ll document both candidate results and leave only measured, correct code in the source.
The final rebuilt owner scans are taking longer than the earlier close-view checks: the far LOD6 scan is still processing its 4,770-tile sample set. It is the only heavy process running; the 8-worker close scans completed normally. I’m letting this single scan finish before touching the final report.
The fresh owner scans reproduce the round‑two sign percentages and sampled surface distances to the displayed precision; the saved scalar hashes differ, but the measured signs and geometry-distance metrics do not. The far scan passed with zero field-sample mismatches. I’ve restored and rebuilt the baseline mesher after rejecting the edge-cache prototype; I’m doing the final hash verification on that exact executable now.
The new process clock shows the real cold command-line wall time is higher than “shape build + stream” alone: **2.113 s at 8 workers and 1.682 s at 16** from process start through the farthest LOD. The one-second whole-view target therefore remains unmet even before GPU upload. I’m measuring the same process-start boundary for the full-detail workload too.
The improved crater benchmark now straddles the LOD0/LOD1 seam: one transaction remeshed 12 LOD0 and 2 LOD1 tiles, with zero cache hits. It took 14 ms edit-to-ready at 8 workers and 27 ms at 16. I’m rebuilding and hashing this exact final source so the report’s edit case and determinism evidence refer to the same tree.
The profile confirms why the overlap and small cache tweaks weren’t enough. At 16 workers, shape compilation is 0.727 s; the stream is 0.744 s, with LOD6 consuming 8.6 of 11.7 worker-seconds and about 79 MiB of mesh output. The sampled mesher breakdown puts density-grid evaluation and triangle/vertex emission at similar cost. I’m updating the report to distinguish composite stage time from true process-start wall time and to include the correct cross-LOD edit case.
The final status check caught only a whitespace difference in the rejected mesher experiment; I’ve restored the source exactly. I’m doing one last `/MP16` rebuild of that restored tree, then I’ll verify the report links and leave the workspace uncommitted.
I’ll pick up at the mesh-hash instrumentation, first checking the current report and source state, then I’ll finish the hash output and rebuild before running the exact-source determinism set.
The current report’s “mesh” hashes are actually density-only hashes. I’ve added a separate deterministic hash over each emitted CPU tile’s vertex positions, normals, and indices; I’m compiling that diagnostic now, then I’ll regenerate 1/8/16-worker hashes from the rebuilt executable.
The rebuilt hashes now cover both the scalar samples and actual CPU mesh arrays; all four cold runs match across 1, 8, and 16 workers with zero cache hits. The latest profile still points at two large costs: LOD6 meshing and serial far-parent construction. I’m looking for a result-preserving optimization in those measured hotspots before settling the report.
The far spatial index creates about 329,000 tiny per-bin vectors. They hold an average of roughly five primitive IDs, so most pay for a separate heap allocation even though the lists are very small. I’m testing a four-ID inline bin list that keeps the same primitive order and spills only larger bins to a vector; it avoids the extra count/fill pass and should preserve field results exactly.
The inline bins passed the first exact checks: the whole density and mesh hashes are unchanged, all 5,642 route samples remain open, and far-bin construction fell from 97 ms to 53 ms in the 16-worker profile. I’ll measure the no-hash cold path at both worker counts now; the hash diagnostic itself adds enough work that I won’t use its wall time as the speed result.
The first inline-list version cut bin-build time, but it enlarged every hash-map node and appears to have offset some of the gain during field queries. I’m keeping the four-item inline layout while packing it into the same 24-byte footprint as `std::vector`; this tests whether allocation savings survive without the larger lookup structure.
The compact four-item version compiles and preserves the measured shape checks. One 16-worker no-hash run is at 0.679 s shape compile + 0.761 s meshing, versus the prior 0.727 + 0.744 s; I’m separating run variance from lookup cost and testing whether the hash-map node layout is now the limiting factor.
I’ve replaced the far/local primitive lookup table with a flat open-addressed index while keeping each bin’s primitive IDs in insertion order. This follows the cache-locality rationale from Abseil’s Swiss-table design notes and Meta’s F14 write-up; I’m treating it strictly as a benchmarked experiment because open addressing can lose if probe chains outweigh the locality benefit.
The flat table built and preserved the route checks, but the no-hash whole stream rose to 0.804 s from about 0.744 s while the shape build fell. That makes the saved 12 ms in shape work a net loss; I’m rejecting that table and retaining the compact inline bin lists on the existing hash map, whose combined stage result was better.
The final no-hash whole-view run is 0.828 s shape + 0.986 s streaming at 8 workers, and 0.679 + 0.758 s at 16. Full detail is also freshly remeasured below one second of process wall at 16 workers (0.889 s); I’m refreshing the one-transaction cross-LOD crater timing next, then I’ll rerun the complete final determinism matrix.
The crater transaction still reaches every intersecting tile in one batch: 14 affected tiles, zero cache hits, and 20 ms / 27 ms request-to-ready at 8 / 16 workers. The CPU neighborhood meshes are ready in 18 / 19 ms after streaming starts; this standalone runner cannot measure the plugin’s collision cook. I’m now hashing density and mesh outputs on the final compact-bin build.
The final owner scans on this source reproduce the prior lattice and surface-distance results exactly: chamber 5.895% sign disagreement / 1.0826 m mean, passage 3.304% / 1.0292 m, and far 9.041% / 2.1588 m. I’m trying one small mesher allocation change next: reserve the existing per-worker vertex map from the tile’s cell count to avoid repeated growth while keeping the same key and emission order.
The vertex-map reserve is result-preserving and slightly reduced the 32-tile LOD6 sample profile: 33.513 → 33.145 ms, including stream building 13.177 → 12.904 ms. That’s a small signal, so I’m checking cold whole-view timings before deciding whether to keep it.
The vertex-map reserve was retained: its 32-tile attribution improved slightly, and the no-hash 16-worker whole view stayed at 0.756 s for streaming while the crater batch measured 15 / 18 ms end-to-end at 8 / 16 workers. I’m running the final four hash jobs now so the determinism evidence matches the exact source that will be reported.
The final hash matrix passes: density and CPU mesh hashes match across 1, 8, and 16 workers, including both 16-worker repeats; every run reports zero cache hits and all 5,642 route samples open. I’m saving runner wall-time records for the report, then I’ll update the stage table and list the experiments that helped or were rejected.
One hash scope detail surfaced before the report: the current mesh digest covers positions, normals, and indices, but omits UVs, colors, and surface-group counts. I’m extending it to cover every array emitted by `FVoxelMeshData`; then the final determinism claim will cover the complete uncached mesh payload, not only geometry.
I’ll reconcile the report with the final measured logs, check the working tree and remaining processes, then summarize the target miss and verified results. I’ll keep all edits and artifacts inside `Standalone\` and won’t commit or stash.
The final logs confirm the main cost is split between compiling the field (0.672 s) and meshing LOD6 (0.778 s at 16 workers); both pools are nearly saturated. The compact bin change improved construction, but it cannot close the remaining 0.454 s gap by itself. I’m checking the field and mesher hot paths now for a larger, hash-preserving reduction before I finalize the report.
A promising exact-output optimization surfaced in the field compiler: detail candidates are visited in ascending Z on each X/Y line, while the relief uses three interpolated value-noise octaves. Those calls currently rehash the same X/Y corners many times. I’m adding a line-local cache that reuses those hashes across Z samples; it keeps the same interpolation order and does not persist across cold runs.
The code compiled cleanly. My first hash-run launch passed the argument list as one comma-joined string, so the executable treated it as a data path and exited before benchmarking. I’ve preserved that failed run and am correcting the runner invocation before measuring.
The cached relief preserved every density and mesh hash at 16 workers and cut the measured detail-band stage from about 375 ms to 298 ms. The remaining stream path also re-detects integer lattice coordinates with three rounding operations per sample. Since the mesher already knows those coordinates are integral, I’m adding a lattice-specific entry point that uses the same packed field values directly, then I’ll verify exact hashes before keeping it.
The lattice-specific dispatch also preserves all hashes and improves the 16-worker full-detail stream from 0.183 s to 0.129–0.141 s in hash runs. I also tested a per-tile snapshot to bypass atomic shape-pointer loads; its stream timing did not beat the prior run, so I’m removing that complexity and keeping the two changes with measured gains.
I found a larger redundant cost in the analytic mesh path: every non-packed density query evaluates `EffectiveRockDistance` once to get its sign, then evaluates the same field again to get its magnitude. Coarse tiles make many such queries. I’m fusing those evaluations into one while retaining the existing sign clamp and edit-brush order; exact mesh hashes will decide whether it stays.
The fused evaluation kept the 16-worker density and mesh hashes unchanged and cut the hash-run stream stage from 0.698 s to 0.624 s. I found another avoidable cost on far tiles: they need the final rock distance and critical-air override, but the current evaluator also computes surface-family and detail-influence metadata that those points never use. I’m testing a reduced analytic path with the same cave/overlay distance order.
I’m testing a larger far-field representation change: the parent network currently approximates each long connector with many overlapping ellipsoid segments, producing roughly 181,000 primitives and 1.43 million bin references. The code already has an exact swept capsule primitive, so I’m trying one constant-radius capsule per distant connector. That should remove construction seams and bin work; I’ll keep it only if route, surface checks, and the far capture remain coherent.
The capsule trial passed routes and preserved LOD0/1 hashes. It cut far-parent construction from about 83 ms to 8 ms; bin construction stayed near 46 ms, while far meshing rose slightly because capsule distance evaluation recomputes its axis each sample. I’m caching each capsule’s immutable axis data now to reduce that per-sample cost, then I’ll judge the far geometry and compare the hashes across worker counts.
The direct far capsules cut parent generation by roughly 75 ms and reduce the far network from 181,836 primitives to 16,491. The two 16-worker hash runs and the 1- and 8-worker runs match exactly; LOD0/1 are unchanged and all 5,642 route samples stay open. I’m checking intermediate worker counts and the refreshed far/mesh audits before choosing the final report numbers.
The final surface checks pass: the grid-stair, detail-family, floor-join, tunnel-join and thin-sheet audits are all green; the full stream reports zero thin sheets and zero isolated small components in view. I’ve started a fresh 8-worker LOD6 owner scan because the distant connector shape changed; the closer views retain their prior hashes.
I’ll continue from the saved candidate: first verify the source and report state, then run the remaining read-only checks and update the report with the final measurements and any unresolved limits. I’ll keep all edits and artifacts under `Standalone\` and won’t touch commits or external viewers.
The existing report predates the latest optimizations, so its headline figures and hashes are stale. The latest profile points to two remaining costs—surface detail evaluation and the 4,770-tile far LOD stream—so I’m checking their measurements and the exact current source before deciding on one more targeted optimization.
The coarse LOD stream is the largest measured stage: LOD6 alone samples 32.7 million density points and emits 81 MiB of CPU mesh data, while the workers stay 96.6% busy. I found a safe standalone-only cost to test: the mesher builds UV and biome-color arrays that the CPU stream immediately discards. I’ll add a switch that skips only those unused arrays, leaving density, positions, normals, triangle classification, and skirts intact, then compare hashes and cold timings.
The first safe fast path produced 6498/6498 tiles in 0.384 s at 16 workers (down from 0.572 s); hashes aren’t checked yet, but vertex/index counts match. It still misses the cold process target. The CPU viewer also drops the ceiling/ground material groups, so I’m testing a second standalone-only skip: bypass that per-vertex classification and keep the same triangles in one index group. This changes index ordering, but should leave the rendered geometry unchanged; I’ll verify both images and deterministic hashes.
The material grouping skip reduced full-view meshing again: 0.315 s, with a repeat at 0.285 s; the best cold process wall is 0.958 s. The one-second result is too close to trust from a single sample. I found one more low-risk build cost: the spatial hash reserves buckets from primitive count, while measured far bins number about 23 per primitive. I’m adjusting reserve sizing to the observed bin-to-primitive ratio and will keep it only if repeat timings improve with unchanged hashes.
Takeover continuation — half-load rule restored
- Read the predecessor progress, report, local logs, and commit summary before continuing. All builds and heavy runs in this continuation used BelowNormal priority and affinity 0xFF00; the final build is /MP8. Updated the Build.ps1 comment to reflect the cap.
- Read the actual plugin mesh/material path without editing it: UV0 and vertex colors are enabled and copied; FColor carries biome/slope/border channels; polygroup 0 is ground and 1 is the sky-cap ceiling material, which does not cast shadows. Full payload remains the headline. Standalone UVs are stored as FVector2f to match stream precision; the measured staging saving had no stable time gain. The dense per-tile group cache was reverted after no stream improvement.
- Hash-checked each skip path over full detail, full clipmap, and LOD0–6. Density and canonical oriented topology match the full payload in 9/9 outputs for UV/color skip, group skip, and both. Group skip changes serialized mesh order in 3/9, as expected; full payload hashes differ when attributes are omitted. Combined no-hash skips took 1.533/1.536 s process wall, recorded only as a viewer-side number.
- Compared the far-bin 24x reserve to the old 2x estimate on otherwise-current source. W8 density, geometry, complete payload, and oriented topology hashes all match in all nine outputs. Far-bin construction averaged 47.9 ms at 2x and 41.9 ms at 24x; whole stream stayed about 1.113 s. Kept this as a small stage allocation win. Restored 24x and rebuilt final source.
- Final restored W1/W8 matrices match for all four hashes in all nine outputs; 5,642/5,642 route samples remain open and cache hits are zero. Final no-hash full-payload W8 process walls were 2.174 and 2.135 s (mean 2.155); shape stage mean 0.832 s, stream mean 1.113 s, full payload allocation 235.49 MiB. LOD6 remains the main stream cost at 1.109 s.
- Final half-load chamber/tunnel/passage audits pass (grid-stair 0.003/0.028/0.205), whole LOD0 shard audit passes with zero sheets and zero isolated components, and the 10 m three-brush crater is one transaction remeshing 12 LOD0 + 2 LOD1 tiles in 17 ms to ready.
- Regenerated six fixed captures under the cap; five are byte-identical to the preceding rebuilt captures. Passage differs in 662/1,024,000 pixels (max channel delta 97) but looks unchanged side by side; owner/world visual differences remain, documented in REPORT.md.
- Failed invocations preserved in REPORT.md: one wrong executable name stopped before starting a child, and the first capture batch split --camera/--capture values and exited 2 before opening a viewer. Corrected runs all passed. One earlier dense-cache build had CellsPerAxis out of scope; corrected build succeeded before that prototype was reverted. The final build succeeds with recurring vswhere diagnostic and six existing C4838 warnings.
- Updated REPORT.md with the full-payload stage table, per-LOD data, complete hashes, skip and reserve A/B, route/edit/audit results, six captures, and failure log. No changes outside Standalone; no commit, push, or stash.
+1 -1
View File
@@ -1856,7 +1856,7 @@ private:
ActiveSpatialBinSize=BinResolution; ActiveSpatialBinSize=BinResolution;
// Primitive bounds overlap multiple cells. Reserve from the measured bins-per-primitive // Primitive bounds overlap multiple cells. Reserve from the measured bins-per-primitive
// density for each grid instead of repeatedly rehashing from a 2x primitive guess. // density for each grid instead of repeatedly rehashing from a 2x primitive guess.
const size_t BinsPerPrimitive=bBuildAxisBins?96u:24u; const size_t BinsPerPrimitive=bBuildAxisBins?96u:24u;
SpatialBins.reserve(std::max<size_t>(Primitives.size()*BinsPerPrimitive,1u)); SpatialBins.reserve(std::max<size_t>(Primitives.size()*BinsPerPrimitive,1u));
if(!bBuildAxisBins) if(!bBuildAxisBins)
{ {
+51 -13
View File
@@ -1704,7 +1704,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
int RunStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels, int RunStreamingBenchmark(FStandaloneOwnerWorld& World,const FVector& CameraPositionVoxels,
int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels, int32 WorkerCount,int32 MaxLevel,bool bRoundTunnels,
bool bEnhancedWalls,const FStandaloneMeshCache* Cache, bool bEnhancedWalls,const FStandaloneMeshCache* Cache,
bool bHashDensity,bool bShardAudit,float CameraYaw,float CameraPitch, bool bHashDensity,bool bSkipUvAndColors,bool bSkipMaterialGroups,
bool bShardAudit,float CameraYaw,float CameraPitch,
const std::string& BakeCachePath, const std::string& BakeCachePath,
const FStandaloneMeshCacheMetadata& CacheMetadata) const FStandaloneMeshCacheMetadata& CacheMetadata)
{ {
@@ -1723,6 +1724,8 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
{return Tile.Level<World.FullResClipLevels;})); {return Tile.Level<World.FullResClipLevels;}));
std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows; std::array<std::vector<FHashedTile>,MaxLevels+1> HashRows;
std::array<std::vector<FHashedTile>,MaxLevels+1> MeshHashRows; std::array<std::vector<FHashedTile>,MaxLevels+1> MeshHashRows;
std::array<std::vector<FHashedTile>,MaxLevels+1> PayloadHashRows;
std::array<std::vector<FHashedTile>,MaxLevels+1> TopologyHashRows;
std::array<uint64,MaxLevels+1> SamplesByLevel{}; std::array<uint64,MaxLevels+1> SamplesByLevel{};
std::array<uint64,MaxLevels+1> VerticesByLevel{}; std::array<uint64,MaxLevels+1> VerticesByLevel{};
std::array<uint64,MaxLevels+1> IndicesByLevel{}; std::array<uint64,MaxLevels+1> IndicesByLevel{};
@@ -1731,6 +1734,7 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::array<double,MaxLevels+1> FirstCompletionByLevel{}; std::array<double,MaxLevels+1> FirstCompletionByLevel{};
std::array<double,MaxLevels+1> LastCompletionByLevel{}; std::array<double,MaxLevels+1> LastCompletionByLevel{};
std::array<uint64,MaxLevels+1> OutputBytesByLevel{}; std::array<uint64,MaxLevels+1> OutputBytesByLevel{};
std::array<uint64,MaxLevels+1> MeshPayloadAllocatedBytesByLevel{};
FirstCompletionByLevel.fill(std::numeric_limits<double>::infinity()); FirstCompletionByLevel.fill(std::numeric_limits<double>::infinity());
std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> PlayerNeighborhoodTiles; std::unordered_set<FVoxelTileKey,FStandaloneTileKeyHash> PlayerNeighborhoodTiles;
for(const FVoxelTileKey& Tile:Desired.Tiles) for(const FVoxelTileKey& Tile:Desired.Tiles)
@@ -1744,14 +1748,16 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
if(!BakeCachePath.empty())CacheTiles.reserve(Desired.Tiles.size()); if(!BakeCachePath.empty())CacheTiles.reserve(Desired.Tiles.size());
const auto Start=std::chrono::steady_clock::now(); const auto Start=std::chrono::steady_clock::now();
FStandaloneStreamingGenerator Streamer(World,WorkerCount, FStandaloneStreamingGenerator Streamer(World,WorkerCount,
bHashDensity||!BakeCachePath.empty(),Cache,MaxLevel); bHashDensity||!BakeCachePath.empty(),Cache,MaxLevel,
bSkipUvAndColors,bSkipMaterialGroups);
const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels); const FStandaloneStreamingUpdate Update=Streamer.Start(CameraPositionVoxels);
const double LOD0Width=static_cast<double>(2*World.ClipRadius+2) const double LOD0Width=static_cast<double>(2*World.ClipRadius+2)
*CHUNK_SIZE*VOXEL_SIZE/100.0; *CHUNK_SIZE*VOXEL_SIZE/100.0;
std::printf("[StreamBenchmarkConfig] clip_radius=%d lod0_cube=%.0fm voxel=25cm fullres_levels=%d max_level_filter=%d workers=%d full_detail_tiles=%d desired_tiles=%d variant=%s hash=%s\n", std::printf("[StreamBenchmarkConfig] clip_radius=%d lod0_cube=%.0fm voxel=25cm fullres_levels=%d max_level_filter=%d workers=%d full_detail_tiles=%d desired_tiles=%d variant=%s uv_colors=%s material_groups=%s hash=%s\n",
World.ClipRadius,LOD0Width,World.FullResClipLevels,MaxLevel<0?World.MaxClipLevel:MaxLevel, World.ClipRadius,LOD0Width,World.FullResClipLevels,MaxLevel<0?World.MaxClipLevel:MaxLevel,
Streamer.GetWorkerCount(),FullDetailExpected,Update.DesiredTiles, Streamer.GetWorkerCount(),FullDetailExpected,Update.DesiredTiles,
bRoundTunnels?(bEnhancedWalls?"round-detailed":"round-legacy"):"base", bRoundTunnels?(bEnhancedWalls?"round-detailed":"round-legacy"):"base",
bSkipUvAndColors?"skipped":"full",bSkipMaterialGroups?"skipped":"full",
(bHashDensity||!BakeCachePath.empty())?"on":"off"); (bHashDensity||!BakeCachePath.empty())?"on":"off");
std::fflush(stdout); std::fflush(stdout);
int32 Finished=0,FullDetailFinished=0; int32 Finished=0,FullDetailFinished=0;
@@ -1790,8 +1796,11 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
LastCompletionByLevel[Level]=std::max(LastCompletionByLevel[Level],CompletedSeconds); LastCompletionByLevel[Level]=std::max(LastCompletionByLevel[Level],CompletedSeconds);
OutputBytesByLevel[Level]+=static_cast<uint64>(Tile.Vertices.capacity())*sizeof(FStandaloneCpuVertex) OutputBytesByLevel[Level]+=static_cast<uint64>(Tile.Vertices.capacity())*sizeof(FStandaloneCpuVertex)
+static_cast<uint64>(Tile.Indices.capacity())*sizeof(uint32); +static_cast<uint64>(Tile.Indices.capacity())*sizeof(uint32);
MeshPayloadAllocatedBytesByLevel[Level]+=Tile.MeshPayloadAllocatedBytes;
if(Tile.bHasDensityHash)HashRows[Level].push_back({Tile.Key,Tile.DensityHash}); if(Tile.bHasDensityHash)HashRows[Level].push_back({Tile.Key,Tile.DensityHash});
if(Tile.bHasMeshHash)MeshHashRows[Level].push_back({Tile.Key,Tile.MeshHash}); if(Tile.bHasMeshHash)MeshHashRows[Level].push_back({Tile.Key,Tile.MeshHash});
if(Tile.bHasPayloadHash)PayloadHashRows[Level].push_back({Tile.Key,Tile.PayloadHash});
if(Tile.bHasTopologyHash)TopologyHashRows[Level].push_back({Tile.Key,Tile.TopologyHash});
} }
if(Level<World.FullResClipLevels)++FullDetailFinished; if(Level<World.FullResClipLevels)++FullDetailFinished;
if(bShardAudit&&Level==0) if(bShardAudit&&Level==0)
@@ -1820,16 +1829,22 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::chrono::steady_clock::now()-Start).count(); std::chrono::steady_clock::now()-Start).count();
std::vector<FHashedTile> FullRows; std::vector<FHashedTile> FullRows;
std::vector<FHashedTile> FullMeshRows; std::vector<FHashedTile> FullMeshRows;
std::vector<FHashedTile> FullPayloadRows;
std::vector<FHashedTile> FullTopologyRows;
for(int32 Level=0;Level<World.FullResClipLevels&&Level<=MaxLevels;++Level) for(int32 Level=0;Level<World.FullResClipLevels&&Level<=MaxLevels;++Level)
{ {
FullRows.insert(FullRows.end(),HashRows[Level].begin(),HashRows[Level].end()); FullRows.insert(FullRows.end(),HashRows[Level].begin(),HashRows[Level].end());
FullMeshRows.insert(FullMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end()); FullMeshRows.insert(FullMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end());
FullPayloadRows.insert(FullPayloadRows.end(),PayloadHashRows[Level].begin(),PayloadHashRows[Level].end());
FullTopologyRows.insert(FullTopologyRows.end(),TopologyHashRows[Level].begin(),TopologyHashRows[Level].end());
} }
if(bHashDensity||!BakeCachePath.empty()) if(bHashDensity||!BakeCachePath.empty())
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 density_hash=0x%016llx mesh_hash=0x%016llx\n", std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 density_hash=0x%016llx mesh_hash=0x%016llx payload_hash=0x%016llx topology_hash=0x%016llx\n",
FullDetailSeconds,FullDetailFinished,FullDetailExpected, FullDetailSeconds,FullDetailFinished,FullDetailExpected,
static_cast<unsigned long long>(HashTileRows(FullRows)), static_cast<unsigned long long>(HashTileRows(FullRows)),
static_cast<unsigned long long>(HashTileRows(FullMeshRows))); static_cast<unsigned long long>(HashTileRows(FullMeshRows)),
static_cast<unsigned long long>(HashTileRows(FullPayloadRows)),
static_cast<unsigned long long>(HashTileRows(FullTopologyRows)));
else else
std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 hash=off\n", std::printf("[StreamFullDetail] wall=%.3fs generated=%d/%d resident_tiles_cpu_only=1 hash=off\n",
FullDetailSeconds,FullDetailFinished,FullDetailExpected); FullDetailSeconds,FullDetailFinished,FullDetailExpected);
@@ -1846,26 +1861,40 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
std::printf("[PlayerNeighborhood] lod0_tiles=%zu/%zu mesh_ready=missing collision_cook=unavailable_in_standalone\n", std::printf("[PlayerNeighborhood] lod0_tiles=%zu/%zu mesh_ready=missing collision_cook=unavailable_in_standalone\n",
PlayerNeighborhoodReady.size(),PlayerNeighborhoodTiles.size()); PlayerNeighborhoodReady.size(),PlayerNeighborhoodTiles.size());
const double TotalWorkerSeconds=std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0); const double TotalWorkerSeconds=std::accumulate(WorkerSecondsByLevel.begin(),WorkerSecondsByLevel.end(),0.0);
const uint64 TotalMeshPayloadAllocatedBytes=std::accumulate(
MeshPayloadAllocatedBytesByLevel.begin(),MeshPayloadAllocatedBytesByLevel.end(),uint64{0});
const uint64 TotalViewerOutputBytes=std::accumulate(
OutputBytesByLevel.begin(),OutputBytesByLevel.end(),uint64{0});
const double WorkerBusyPercent=100.0*TotalWorkerSeconds const double WorkerBusyPercent=100.0*TotalWorkerSeconds
/(std::max(1,WorkerCount)*std::max(0.001,FullClipSeconds)); /(std::max(1,WorkerCount)*std::max(0.001,FullClipSeconds));
std::vector<FHashedTile> AllRows; std::vector<FHashedTile> AllRows;
std::vector<FHashedTile> AllMeshRows; std::vector<FHashedTile> AllMeshRows;
std::vector<FHashedTile> AllPayloadRows;
std::vector<FHashedTile> AllTopologyRows;
if(bHashDensity||!BakeCachePath.empty()) if(bHashDensity||!BakeCachePath.empty())
for(int32 Level=0;Level<=MaxLevels;++Level) for(int32 Level=0;Level<=MaxLevels;++Level)
{ {
AllRows.insert(AllRows.end(),HashRows[Level].begin(),HashRows[Level].end()); AllRows.insert(AllRows.end(),HashRows[Level].begin(),HashRows[Level].end());
AllMeshRows.insert(AllMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end()); AllMeshRows.insert(AllMeshRows.end(),MeshHashRows[Level].begin(),MeshHashRows[Level].end());
AllPayloadRows.insert(AllPayloadRows.end(),PayloadHashRows[Level].begin(),PayloadHashRows[Level].end());
AllTopologyRows.insert(AllTopologyRows.end(),TopologyHashRows[Level].begin(),TopologyHashRows[Level].end());
} }
if(bHashDensity||!BakeCachePath.empty()) if(bHashDensity||!BakeCachePath.empty())
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% tiles=%d density_hash=0x%016llx mesh_hash=0x%016llx\n", std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% payload_alloc=%.2fMiB viewer_output=%.2fMiB tiles=%d density_hash=0x%016llx mesh_hash=0x%016llx payload_hash=0x%016llx topology_hash=0x%016llx\n",
FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent, FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent,
std::max(0.0,100.0-WorkerBusyPercent), std::max(0.0,100.0-WorkerBusyPercent),
static_cast<double>(TotalMeshPayloadAllocatedBytes)/1048576.0,
static_cast<double>(TotalViewerOutputBytes)/1048576.0,
DesiredTotal,static_cast<unsigned long long>(HashTileRows(AllRows)), DesiredTotal,static_cast<unsigned long long>(HashTileRows(AllRows)),
static_cast<unsigned long long>(HashTileRows(AllMeshRows))); static_cast<unsigned long long>(HashTileRows(AllMeshRows)),
static_cast<unsigned long long>(HashTileRows(AllPayloadRows)),
static_cast<unsigned long long>(HashTileRows(AllTopologyRows)));
else else
std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% tiles=%d hash=off\n", std::printf("[StreamFullClipmap] wall=%.3fs generated=%d/%d worker_sum=%.3fs worker_busy=%.1f%% worker_idle=%.1f%% payload_alloc=%.2fMiB viewer_output=%.2fMiB tiles=%d hash=off\n",
FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent, FullClipSeconds,Finished,DesiredTotal,TotalWorkerSeconds,WorkerBusyPercent,
std::max(0.0,100.0-WorkerBusyPercent),DesiredTotal); std::max(0.0,100.0-WorkerBusyPercent),
static_cast<double>(TotalMeshPayloadAllocatedBytes)/1048576.0,
static_cast<double>(TotalViewerOutputBytes)/1048576.0,DesiredTotal);
std::printf("[StreamCacheUse] hits=%llu requested=%d\n", std::printf("[StreamCacheUse] hits=%llu requested=%d\n",
static_cast<unsigned long long>(Streamer.GetCacheHits()),DesiredTotal); static_cast<unsigned long long>(Streamer.GetCacheHits()),DesiredTotal);
if(bShardAudit) if(bShardAudit)
@@ -1882,22 +1911,26 @@ float3 Lit=Rock*(Ambient+0.88*Diffuse)+float3(0.50,0.69,0.82)*Head*0.75;
{ {
if(TilesByLevel[Level]==0)continue; if(TilesByLevel[Level]==0)continue;
if(bHashDensity||!BakeCachePath.empty()) if(bHashDensity||!BakeCachePath.empty())
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% output=%.2fMiB vertices=%llu indices=%llu density_hash=0x%016llx mesh_hash=0x%016llx\n", std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% payload_alloc=%.2fMiB viewer_output=%.2fMiB vertices=%llu indices=%llu density_hash=0x%016llx mesh_hash=0x%016llx payload_hash=0x%016llx topology_hash=0x%016llx\n",
Level,static_cast<unsigned long long>(TilesByLevel[Level]), Level,static_cast<unsigned long long>(TilesByLevel[Level]),
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level], static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
FirstCompletionByLevel[Level],LastCompletionByLevel[Level], FirstCompletionByLevel[Level],LastCompletionByLevel[Level],
100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])), 100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])),
static_cast<double>(MeshPayloadAllocatedBytesByLevel[Level])/1048576.0,
static_cast<double>(OutputBytesByLevel[Level])/1048576.0, static_cast<double>(OutputBytesByLevel[Level])/1048576.0,
static_cast<unsigned long long>(VerticesByLevel[Level]), static_cast<unsigned long long>(VerticesByLevel[Level]),
static_cast<unsigned long long>(IndicesByLevel[Level]), static_cast<unsigned long long>(IndicesByLevel[Level]),
static_cast<unsigned long long>(HashTileRows(HashRows[Level])), static_cast<unsigned long long>(HashTileRows(HashRows[Level])),
static_cast<unsigned long long>(HashTileRows(MeshHashRows[Level]))); static_cast<unsigned long long>(HashTileRows(MeshHashRows[Level])),
static_cast<unsigned long long>(HashTileRows(PayloadHashRows[Level])),
static_cast<unsigned long long>(HashTileRows(TopologyHashRows[Level])));
else else
std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% output=%.2fMiB vertices=%llu indices=%llu hash=off\n", std::printf("[StreamLevel] lod=%d tiles=%llu samples=%.3fM worker_sum=%.3fs first_ready=%.3fs complete=%.3fs worker_busy=%.1f%% payload_alloc=%.2fMiB viewer_output=%.2fMiB vertices=%llu indices=%llu hash=off\n",
Level,static_cast<unsigned long long>(TilesByLevel[Level]), Level,static_cast<unsigned long long>(TilesByLevel[Level]),
static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level], static_cast<double>(SamplesByLevel[Level])/1.0e6,WorkerSecondsByLevel[Level],
FirstCompletionByLevel[Level],LastCompletionByLevel[Level], FirstCompletionByLevel[Level],LastCompletionByLevel[Level],
100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])), 100.0*WorkerSecondsByLevel[Level]/(WorkerCount*std::max(0.001,LastCompletionByLevel[Level])),
static_cast<double>(MeshPayloadAllocatedBytesByLevel[Level])/1048576.0,
static_cast<double>(OutputBytesByLevel[Level])/1048576.0, static_cast<double>(OutputBytesByLevel[Level])/1048576.0,
static_cast<unsigned long long>(VerticesByLevel[Level]), static_cast<unsigned long long>(VerticesByLevel[Level]),
static_cast<unsigned long long>(IndicesByLevel[Level])); static_cast<unsigned long long>(IndicesByLevel[Level]));
@@ -2004,6 +2037,8 @@ int main(int Argc,char** Argv)
bool bStreamBenchmark=false; bool bStreamBenchmark=false;
bool bShardAudit=false; bool bShardAudit=false;
bool bStreamHash=false; bool bStreamHash=false;
bool bSkipUvAndColors=false;
bool bSkipMaterialGroups=false;
bool bEditBenchmark=false; bool bEditBenchmark=false;
bool bRoundWallCompare=false; bool bRoundWallCompare=false;
int32 RoundCompareTileCount=16; int32 RoundCompareTileCount=16;
@@ -2037,6 +2072,8 @@ int main(int Argc,char** Argv)
else if(Arg=="--edit-benchmark") else if(Arg=="--edit-benchmark")
{bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;} {bEditBenchmark=true;bStreamBenchmark=true;App.bShapeWorld=true;}
else if(Arg=="--stream-hash")bStreamHash=true; else if(Arg=="--stream-hash")bStreamHash=true;
else if(Arg=="--skip-uv-colors")bSkipUvAndColors=true;
else if(Arg=="--skip-material-groups")bSkipMaterialGroups=true;
else if(Arg=="--shard-audit")bShardAudit=true; else if(Arg=="--shard-audit")bShardAudit=true;
else if(Arg=="--compare-round-walls") else if(Arg=="--compare-round-walls")
{bRoundWallCompare=true;App.bRoundTunnels=true;} {bRoundWallCompare=true;App.bRoundTunnels=true;}
@@ -2125,7 +2162,8 @@ int main(int Argc,char** Argv)
FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0), FVector(App.Position.X*4.0,App.Position.Y*4.0,App.Position.Z*4.0),
App.WorkerCount,StreamBenchmarkMaxLevel,App.bRoundTunnels, App.WorkerCount,StreamBenchmarkMaxLevel,App.bRoundTunnels,
App.bRoundTunnelEnhanced,(!App.bShapeWorld&&App.MeshCache.IsLoaded())?&App.MeshCache:nullptr, App.bRoundTunnelEnhanced,(!App.bShapeWorld&&App.MeshCache.IsLoaded())?&App.MeshCache:nullptr,
bStreamHash,bShardAudit,App.Yaw,App.Pitch,BakeMeshCachePath,App.MakeMeshCacheMetadata()); bStreamHash,bSkipUvAndColors,bSkipMaterialGroups,bShardAudit,
App.Yaw,App.Pitch,BakeMeshCachePath,App.MakeMeshCacheMetadata());
} }
if(BenchmarkLevel>=0) if(BenchmarkLevel>=0)
return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount, return RunTileBenchmark(*App.World,App.Position,BenchmarkLevel,BenchmarkCount,
+104 -3
View File
@@ -42,9 +42,94 @@ struct FStandaloneCpuTile
bool bHasDensityHash=false; bool bHasDensityHash=false;
uint64 MeshHash=14695981039346656037ull; uint64 MeshHash=14695981039346656037ull;
bool bHasMeshHash=false; bool bHasMeshHash=false;
uint64 PayloadHash=14695981039346656037ull;
bool bHasPayloadHash=false;
uint64 TopologyHash=14695981039346656037ull;
bool bHasTopologyHash=false;
uint64 MeshPayloadAllocatedBytes=0;
std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{}; std::array<uint64,VoxelDensityProfile::BucketCount> ProfileCycles{};
}; };
inline uint64 HashStandaloneMeshPayload(const FVoxelMeshData& Mesh)
{
uint64 Hash=14695981039346656037ull;
const auto AddWord=[&](uint64 Word,int32 Bytes)
{
for(int32 I=0;I<Bytes;++I)
{
Hash^=static_cast<uint8>(Word>>(I*8));
Hash*=1099511628211ull;
}
};
const auto AddDouble=[&](double Value)
{ AddWord(std::bit_cast<uint64>(Value),8); };
AddWord(static_cast<uint64>(Mesh.Vertices.Num()),8);
for(const FVector& Position:Mesh.Vertices)
{ AddDouble(Position.X);AddDouble(Position.Y);AddDouble(Position.Z); }
AddWord(static_cast<uint64>(Mesh.Normals.Num()),8);
for(const FVector& Normal:Mesh.Normals)
{ AddDouble(Normal.X);AddDouble(Normal.Y);AddDouble(Normal.Z); }
AddWord(static_cast<uint64>(Mesh.UVs.Num()),8);
for(const auto& UV:Mesh.UVs)
{ AddDouble(static_cast<double>(UV.X));AddDouble(static_cast<double>(UV.Y)); }
AddWord(static_cast<uint64>(Mesh.Colors.Num()),8);
for(const FColor& Color:Mesh.Colors)
{ AddWord(Color.R,1);AddWord(Color.G,1);AddWord(Color.B,1);AddWord(Color.A,1); }
AddWord(static_cast<uint64>(Mesh.Triangles.Num()),8);
for(const int32 Index:Mesh.Triangles)AddWord(static_cast<uint32>(Index),4);
AddWord(static_cast<uint32>(Mesh.NumSurfaceVertices),4);
AddWord(static_cast<uint32>(Mesh.NumSkirtTriangles),4);
AddWord(static_cast<uint32>(Mesh.NumCeilingTriangles),4);
return Hash;
}
inline uint64 HashStandaloneMeshTopology(const FVoxelMeshData& Mesh)
{
uint64 Hash=14695981039346656037ull;
const auto AddWord=[&](uint64 Word,int32 Bytes)
{
for(int32 I=0;I<Bytes;++I)
{
Hash^=static_cast<uint8>(Word>>(I*8));
Hash*=1099511628211ull;
}
};
using FTriangleKey=std::array<uint64,3>;
const auto VertexKey=[&](int32 Index)
{
uint64 VertexHash=14695981039346656037ull;
const auto AddVertexWord=[&](uint64 Word)
{
for(int32 Byte=0;Byte<8;++Byte)
{
VertexHash^=static_cast<uint8>(Word>>(Byte*8));
VertexHash*=1099511628211ull;
}
};
if(Index<0||Index>=Mesh.Vertices.Num()||Index>=Mesh.Normals.Num())
{ AddVertexWord(UINT64_MAX);return VertexHash; }
const FVector& Position=Mesh.Vertices[Index];
const FVector& Normal=Mesh.Normals[Index];
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Position.X)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Position.Y)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Position.Z)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Normal.X)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Normal.Y)));
AddVertexWord(std::bit_cast<uint64>(static_cast<double>(Normal.Z)));
return VertexHash;
};
std::vector<FTriangleKey> Triangles;
Triangles.reserve(static_cast<size_t>(Mesh.Triangles.Num()/3));
for(int32 I=0;I+2<Mesh.Triangles.Num();I+=3)
Triangles.push_back({VertexKey(Mesh.Triangles[I]),
VertexKey(Mesh.Triangles[I+1]),VertexKey(Mesh.Triangles[I+2])});
std::sort(Triangles.begin(),Triangles.end());
AddWord(static_cast<uint64>(Triangles.size()),8);
for(const FTriangleKey& Triangle:Triangles)
for(const uint64 VertexHash:Triangle)AddWord(VertexHash,8);
return Hash;
}
inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile) inline uint64 HashStandaloneCpuTileMesh(const FStandaloneCpuTile& Tile)
{ {
uint64 Hash=14695981039346656037ull; uint64 Hash=14695981039346656037ull;
@@ -310,9 +395,12 @@ public:
explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=8, explicit FStandaloneStreamingGenerator(FStandaloneOwnerWorld& InWorld,int32 InWorkerCount=8,
bool bInHashDensity=false, bool bInHashDensity=false,
const FStandaloneMeshCache* InMeshCache=nullptr, const FStandaloneMeshCache* InMeshCache=nullptr,
int32 InMaxLevelFilter=-1) int32 InMaxLevelFilter=-1,
bool bInSkipUvAndColors=false,
bool bInSkipMaterialGroups=false)
:World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,16)),bHashDensity(bInHashDensity), :World(&InWorld),WorkerCount(FMath::Clamp(InWorkerCount,1,16)),bHashDensity(bInHashDensity),
MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter) MeshCache(InMeshCache),MaxLevelFilter(InMaxLevelFilter),
bSkipUvAndColors(bInSkipUvAndColors),bSkipMaterialGroups(bInSkipMaterialGroups)
{ {
VoxelGenLOD::InitializeConsoleSwitches(); VoxelGenLOD::InitializeConsoleSwitches();
UVoxelMarchingCubesMesher::InitializeConsoleSwitches(); UVoxelMarchingCubesMesher::InitializeConsoleSwitches();
@@ -454,7 +542,8 @@ private:
} }
UVoxelMarchingCubesMesher Mesher; UVoxelMarchingCubesMesher Mesher;
Mesher.SetGenerator(World->Generator.get()); Mesher.SetGenerator(World->Generator.get());
Mesher.bStandaloneCompactPayload=true; Mesher.bStandaloneSkipUvAndColors=bSkipUvAndColors;
Mesher.bStandaloneSkipMaterialGroups=bSkipMaterialGroups;
Mesher.bGenerateSkirts=World->bGenerateSkirts; Mesher.bGenerateSkirts=World->bGenerateSkirts;
Mesher.SkirtCells=static_cast<float>(World->SkirtCells); Mesher.SkirtCells=static_cast<float>(World->SkirtCells);
Mesher.LODOctaveDrop=World->LODOctaveDrop; Mesher.LODOctaveDrop=World->LODOctaveDrop;
@@ -490,6 +579,16 @@ private:
} }
} }
Out.DensitySamples=static_cast<uint64>(FMath::Max<int64>(0,SampleCount)); Out.DensitySamples=static_cast<uint64>(FMath::Max<int64>(0,SampleCount));
Out.MeshPayloadAllocatedBytes=static_cast<uint64>(Mesh.Vertices.GetAllocatedSize())
+static_cast<uint64>(Mesh.Normals.GetAllocatedSize())
+static_cast<uint64>(Mesh.UVs.GetAllocatedSize())
+static_cast<uint64>(Mesh.Colors.GetAllocatedSize())
+static_cast<uint64>(Mesh.Triangles.GetAllocatedSize());
if(bHashDensity)
{
Out.PayloadHash=HashStandaloneMeshPayload(Mesh);Out.bHasPayloadHash=true;
Out.TopologyHash=HashStandaloneMeshTopology(Mesh);Out.bHasTopologyHash=true;
}
Out.Vertices.reserve(static_cast<size_t>(Mesh.Vertices.Num())); Out.Vertices.reserve(static_cast<size_t>(Mesh.Vertices.Num()));
for(int32 I=0;I<Mesh.Vertices.Num();++I) for(int32 I=0;I<Mesh.Vertices.Num();++I)
{ {
@@ -534,6 +633,8 @@ private:
bool bHashDensity=false; bool bHashDensity=false;
const FStandaloneMeshCache* MeshCache=nullptr; const FStandaloneMeshCache* MeshCache=nullptr;
int32 MaxLevelFilter=-1; int32 MaxLevelFilter=-1;
bool bSkipUvAndColors=false;
bool bSkipMaterialGroups=false;
std::atomic<uint64> CacheHitCount{0}; std::atomic<uint64> CacheHitCount{0};
mutable std::mutex Mutex; mutable std::mutex Mutex;
std::condition_variable Wake; std::condition_variable Wake;
@@ -159,8 +159,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
// mémoïsé par colonne quantifiée au pas de la grille → ≤ (colonnes touchées) appels ; // mémoïsé par colonne quantifiée au pas de la grille → ≤ (colonnes touchées) appels ;
// ~zéro sur une tuile de sol pur, borné par lattice² sur une tuile de cap. // ~zéro sur une tuile de sol pur, borné par lattice² sur une tuile de cap.
static thread_local TArray<uint8> VertexClasses; // 0 = sol, 1 = sky-cap static thread_local TArray<uint8> VertexClasses; // 0 = sol, 1 = sky-cap
static thread_local TMap<FIntVector, FVector2f> SurfColMemo; // (Xq,Yq,chunkZ) → (TerrainZ, CeilSurf) ; X=FLT_MAX ⇒ pas SurfaceWorld static thread_local TMap<FIntVector, FVector2f> SurfColMemo; // (Xq,Yq,chunkZ) → (TerrainZ, CeilSurf)
if (!bStandaloneCompactPayload) if (!bStandaloneSkipMaterialGroups)
{ {
VertexClasses.Reset(); VertexClasses.Reset();
SurfColMemo.Reset(); SurfColMemo.Reset();
@@ -194,10 +194,10 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
} }
MeshData.Normals.Add(Normal); MeshData.Normals.Add(Normal);
if (!bStandaloneCompactPayload) if (!bStandaloneSkipUvAndColors)
{ {
// UVs planaires — le triplanar mapping se fait dans le matériau. // UVs planaires — le triplanar mapping se fait dans le matériau.
MeshData.UVs.Add(FVector2D(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE)); MeshData.UVs.Add(FVector2f(WorldPos.X / VOXEL_SIZE, WorldPos.Y / VOXEL_SIZE));
//----------------------------------------------------------------- //-----------------------------------------------------------------
// VERTEX COLOUR — masks for biome palette, slope and border blend. // VERTEX COLOUR — masks for biome palette, slope and border blend.
@@ -211,7 +211,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
MeshData.Colors.Add(FColor(R, G, Bc, A)); MeshData.Colors.Add(FColor(R, G, Bc, A));
} }
if (!bStandaloneCompactPayload) if (!bStandaloneSkipMaterialGroups)
{ {
// F17 — classify down-facing vertices for material-only ceiling grouping. // F17 — classify down-facing vertices for material-only ceiling grouping.
uint8 SurfClass = 0; uint8 SurfClass = 0;
@@ -1241,7 +1241,7 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]); const int32 Idx2 = GetOrCreateVertex(EdgeVertices[E2], EdgeNormals[E2]);
TArray<int32>* Dst = &GroundTris; TArray<int32>* Dst = &GroundTris;
if (!bStandaloneCompactPayload) if (!bStandaloneSkipMaterialGroups)
{ {
// F17 — majority vote (at least two ceiling vertices => ceiling triangle). // F17 — majority vote (at least two ceiling vertices => ceiling triangle).
const int32 CapVotes = (int32)VertexClasses[Idx0] const int32 CapVotes = (int32)VertexClasses[Idx0]
@@ -1314,9 +1314,9 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const int32 Idx = MeshData.Vertices.Num(); const int32 Idx = MeshData.Vertices.Num();
MeshData.Vertices.Add(Pos); MeshData.Vertices.Add(Pos);
MeshData.Normals.Add(Nrm); MeshData.Normals.Add(Nrm);
if (!bStandaloneCompactPayload) if (!bStandaloneSkipUvAndColors)
{ {
MeshData.UVs.Add(FVector2D(Pos.X / VOXEL_SIZE, Pos.Y / VOXEL_SIZE)); MeshData.UVs.Add(FVector2f(Pos.X / VOXEL_SIZE, Pos.Y / VOXEL_SIZE));
MeshData.Colors.Add(Col); // parallel with the other vertex arrays MeshData.Colors.Add(Col); // parallel with the other vertex arrays
} }
return Idx; return Idx;
@@ -1342,8 +1342,8 @@ FVoxelMeshData UVoxelMarchingCubesMesher::GenerateMesh(FIntVector OriginVoxels,
const FVector NA = MeshData.Normals[iA]; const FVector NA = MeshData.Normals[iA];
const FVector NB = MeshData.Normals[iB]; const FVector NB = MeshData.Normals[iB];
const FColor CA = bStandaloneCompactPayload ? FColor::White : MeshData.Colors[iA]; const FColor CA = bStandaloneSkipUvAndColors ? FColor::White : MeshData.Colors[iA];
const FColor CB = bStandaloneCompactPayload ? FColor::White : MeshData.Colors[iB]; const FColor CB = bStandaloneSkipUvAndColors ? FColor::White : MeshData.Colors[iB];
const int32 iA2 = AddSkirtVert(PA - NA * SkirtDepth, NA, CA); const int32 iA2 = AddSkirtVert(PA - NA * SkirtDepth, NA, CA);
const int32 iB2 = AddSkirtVert(PB - NB * SkirtDepth, NB, CB); const int32 iB2 = AddSkirtVert(PB - NB * SkirtDepth, NB, CB);
@@ -116,10 +116,10 @@ public:
// SETTINGS // SETTINGS
//========================================================================= //=========================================================================
// Standalone CPU streaming consumes positions, normals and indices only. When enabled, // Optional standalone benchmark switches. Runtime callers leave both false and retain the
// GenerateMesh omits UVs, colors and material-only ceiling grouping. Runtime callers leave // complete FVoxelMeshData contract consumed by the game materials and section setup.
// this false and keep the complete FVoxelMeshData contract. bool bStandaloneSkipUvAndColors = false;
bool bStandaloneCompactPayload = false; bool bStandaloneSkipMaterialGroups = false;
// Seuil de l'isosurface dans le champ de densité. // Seuil de l'isosurface dans le champ de densité.
// Convention MC: densité < IsoLevel = solide, >= = air. // Convention MC: densité < IsoLevel = solide, >= = air.
@@ -366,7 +366,7 @@ struct FVoxelMeshData
{ {
TArray<FVector> Vertices; // Positions monde TArray<FVector> Vertices; // Positions monde
TArray<int32> Triangles; // Indices, 3 par triangle TArray<int32> Triangles; // Indices, 3 par triangle
TArray<FVector2D> UVs; // Coords de texture (une par vertex) TArray<FVector2f> UVs; // Packed-for-upload float UVs (one per vertex)
TArray<FVector> Normals; // Normale lissée (gradient de densité) TArray<FVector> Normals; // Normale lissée (gradient de densité)
TArray<FColor> Colors; // Masques matériau F6 (R=palette biome dominant, G=pente, TArray<FColor> Colors; // Masques matériau F6 (R=palette biome dominant, G=pente,
// B=poids de fondu de bordure, A=palette biome voisin) // B=poids de fondu de bordure, A=palette biome voisin)