Files
VoxelForge/Docs/archive/CODEX-TASK-2-PERF-REPORT.md
T
Claude 1de44af7cc chore(repo): archive historical docs under Docs/archive, drop tracked pycache
Move closed audits, task briefs, design rounds, perf reports, the OPSTACK
plan/progress logs and WORK-NEXT.md into Docs/archive/ (git mv, history kept).
Whitelist Docs/ in the strict .gitignore, ignore Tools/**/__pycache__/, and
remove the tracked .pyc. Repoint references in the kept docs and in non-test
source comments to the new paths.
2026-09-28 13:46:17 +00:00

141 lines
7.6 KiB
Markdown

# Task 2 performance report
Date: 2026-09-09
Repository: `E:\Projet Unreal\VoxelM\Plugins\VoxelForge`
Work began at `experimental` / `e63f016`.
## Measurement rule
All performance numbers below are per-case `run.elapsed_seconds` from the batch report. This is
the wall clock for a real commandlet case through the export or render result; it includes the
shared-density-grid build when enabled. `summary.mesh_seconds` and profiler buckets are included
only as diagnostics, never as the performance claim.
The export comparison is the same `128^3` `TunnelNetwork`, seed `1337`, slot `1`, operator stack,
step `1`, with three cases in one commandlet process on each side. The 17% reporting floor is
therefore met by both run count and the retained density-grid result.
## Part 1 — block early-out
I tested an `8^3` block reject behind conservative fallback logic. On this cave/stack world, every
one of the 4,096 blocks was left uncertain and went through normal sampling:
| batch | runs | end-to-end wall seconds | blocks skipped | skip rate | result |
| --- | ---: | --- | ---: | ---: | --- |
| baseline | 3 | 3.805057, 3.522129, 3.569186 (avg 3.632124) | — | — | reference |
| block reject | 3 | 6.341454, 6.562621, 7.300234 (avg 6.734770) | 0 / 4,096 | 0.00% | **reverted** |
The candidate added classification work without skipping a block (+85.42% wall clock). It also
did not produce a useful supremum for the complete cave stack, so I did not promote a guessed
`max` parameter into a topology decision. `ClassifyTile` remains untouched: its stack, disturbance,
diff-layer, and cave guards are not a safe substitute for a new block proof. A false AllSolid
verdict here would delete a 3D cave, which is not an acceptable trade.
This did not clear the floor and was reverted.
## Part 2 — density-grid reuse
The canonical mesher requests 35^3 samples per 32-cell tile. For 64 tiles that is:
- tile requests: `64 * 35^3 = 2,744,000`;
- unique points in the expanded 128-cell lattice: `131^3 = 2,248,091`;
- duplicate evaluations: `495,909` (18.07%).
The first implementation evaluated the shared lattice as Z planes. It removed all duplicates but
was slower because it destroyed the generator's spatial locality:
| candidate | runs | end-to-end wall seconds | duplicates | result |
| --- | ---: | --- | --- | --- |
| matched control | 3 | 3.668954, 3.633445, 3.655658 (avg 3.652686) | 495,909 → 495,909 | reference |
| global-plane precompute | 3 | 5.349520, 5.352927, 5.510752 (avg 5.404400) | 495,909 → 0 | **reverted** |
| owner-grid precompute | 3 | 1.950207, 1.973673, 2.161305 (avg 2.028395) | 495,909 → 0 | retained candidate |
The retained implementation partitions the shared lattice into 64 disjoint rectangular owner
regions. Each global sample is generated exactly once, then each tile copies its local halo window
from the immutable run-scoped grid. The precompute is deliberately inside the wall-clock timer;
the final three-case repeat measured:
| batch | runs | end-to-end wall seconds | average |
| --- | ---: | --- | ---: |
| final control (`density_grid_reuse=false`) | 3 | 3.659587, 3.619013, 3.582257 | 3.620286 s |
| final reuse (`density_grid_reuse=true`) | 3 | 1.908017, 1.854344, 1.884170 | 1.882177 s |
That is a **48.01% wall-clock reduction / 1.92x speedup**, clearing the 17% floor. The final
reuse runs averaged 1.474271 s for the shared-grid build, and still won end to end. The 128^3
shared lattice is about 8.6 MiB of float storage. Direct explorer runs default to reuse; a batch
case can pass `density_grid_reuse=false` for a matched control.
The run-scoped handoff is confined to the explorer's finite canonical export. The streaming game
path has no one-shot set of neighboring tiles or invalidation boundary, so it was not given a
locking plane cache that could serialize workers or outlive a generation epoch.
## Part 3 — task granularity
The final control observed 15 worker thread IDs and 64 logical tile invocations. Across its three
runs, the measured tile-job averages were:
| metric | average |
| --- | ---: |
| fastest logical tile job | 0.188817 s |
| mean logical tile job | 0.593751 s |
| slowest logical tile job | 0.955788 s |
| sum of tile-job durations | 38.000045 s |
| tile parallel wall | 3.150925 s |
| launch-fringe estimate | 14.1 us |
| workers / logical jobs | 15 / 64 |
I tested `MinBatchSize=4` with three matched cases: wall seconds `3.590884, 3.634734,
3.753986` (avg `3.659868`) versus the final control average `3.620286`. The tile parallel bucket
was slightly lower (`3.125197` s vs `3.150925` s), but end-to-end wall clock was **1.09% slower**.
The observed worker count and logical job count stayed 15 / 64, and the launch-fringe estimate
remained about 13.1 us. This is the required bucket-win/no-wall-win case: the tuning was reverted;
the production/default setting remains `MinBatchSize=1`.
`ParallelForWithExistingTaskContext` was not adopted. The measured launch fringe is negligible
relative to 0.59 s tile work, and the mesher already uses thread-local scratch storage. No claim is
made here about the separate game workload of many small streaming submissions.
## Part 4 — renderer
No renderer rewrite or RMC change was justified. The source audit and three final render cases
showed:
- the canonical mesh is built once per explorer world;
- the triangle cache/BVH is built once and reused by all eight viewpoints;
- each viewpoint owns one `FColor` output allocation;
- the per-pixel traversal uses a fixed stack array, not a heap allocation;
- camera movement does not call the mesher again.
The three 512x288 render cases had total case wall seconds `8.704730, 8.985744, 8.517606`
(avg `8.736027`), including camera-seed walking. Raster-only diagnostics averaged 1.482921 s
for eight views, 0.286297 s for the first view, 0.170946 s per additional view, and 0.083190 s
for one-time acceleration construction. This is consistent with the owner's approximately
0.15 s/view observation; meshing benefits from Part 2 automatically. There was no renderer
change, so there is no before/after performance claim to report or keep.
## Memory/index follow-up
The existing profiler research measured 431,162,116 bytes (411.2 MiB) across 15 workers in the
large run. In the representative 128^3 breakdown, spatial-index bins and candidate arrays alone
accounted for 214,345,644 bytes, inside a 360,063,124-byte cache total. The index addition did not
show an end-to-end wall-clock gain in the prior measurements.
I did not remove it silently. The explicit follow-up proposal is an index-on/index-off batch
ablation before deletion: removing it should recover roughly that allocation, at the cost of
broader room/tunnel candidate scans and potentially slower density queries. That trade is not
proven by this task's export comparison.
## Determinism, invariants, and scope
All three final control hashes and all three final reuse hashes are `ED0F1F31`; every case reported
`json_deterministic=yes`. The three render cases also repeated the same geometry hash
`847BE4B9`. Owner regions are disjoint, tile results are merged in fixed Z/Y/X order, no RNG or
runtime CPU-feature dispatch was added, and the shared grid is read-only after its deterministic
fill. The only worker writes are to distinct cells of that run-owned output lattice; generator and
world state remain read-only.
The build succeeded with UnrealBuildTool after the final source change. Changes are limited to the
VoxelForge source and this report, with generated evidence under the plugin's `Saved` directory.
RealtimeMeshComponent was not modified. No VoxelM root, sibling plugin, `Content`, or `Config`
files were written; no links or historical worktrees were created.