research(L64, R33): regression bisect -- Core +16% worker time L14->L61, not 2x; four-strate plan baseline
Per-round bisect of a176d07..3202193 (56 builds; timings inflated by concurrent UE builds) plus a clean quiet-machine endpoint re-measure: L14 12.62 vs L61 14.65 worker-s (+16%), LOD0 3.99 vs 4.99 ms/tile. The owner's Windows x2 is therefore mostly on the Unreal side or not like-for-like. Four-strate U2 plan: all strates generate and are deterministic; passage 0-1 lower mouth is out of FI's band, 0/3 interfaces pass, FI/TN/VS use legacy evaluators. R33: ranked Unreal-side suspects and Windows experiments. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016dhxM43dnWvUu2B5Ph1iG1
This commit is contained in:
1 parent
9136f5f0e7
commit
e157847509
3 files changed
+215
No files matched your search
@@ -0,0 +1,66 @@
|
||||
# R33 — Revised report
|
||||
|
||||
## Ranked suspects
|
||||
|
||||
1. **Resolved-layout composition on density samples — strongest match for per-tile cost ×2.** This path was absent at L14. L23 changed a Core sample return from boundary-band handling to full composition:
|
||||
|
||||
```diff
|
||||
- return ApplyEditBrushes(ApplyResolvedStrateBoundaryBand(NativeDensity,X,Y,Z),X,Y,Z);
|
||||
+ return ApplyEditBrushes(ApplyResolvedStrateComposition(NativeDensity,X,Y,Z),X,Y,Z);
|
||||
```
|
||||
|
||||
The new call is at [ShapeWorldInternal.h](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:3017). `ApplyResolvedStrateComposition` runs graph lookup, boundary-band composition, passage carving, optional stamps and floor caps at [line 1819](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:1819). L37 also added this per-sample passage scan:
|
||||
|
||||
```cpp
|
||||
for(const auto& passage:ResolvedLayout->passages)
|
||||
```
|
||||
|
||||
at [line 957](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:957), before the early return at [line 966](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:966).
|
||||
|
||||
The Unreal adapter emits the resolved layout, including ceiling, bedrock and planned passages, at [VoxelForgeCoreAdapter.cpp:1751](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1751), [line 1765](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1765), and [line 1785](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1785). That serialization dates to L22; it was not added by U1. The L14 adapter and Core sample path had no `resolved_layout_json`, `ResolvedLayout`, or `ApplyResolvedStrateComposition`.
|
||||
|
||||
**Call chain:** `BuildDesiredTiles` takes the Core branch at [VoxelWorld.cpp:10988](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:10988); `BuildCoreDesiredTiles` calls `World->desiredTiles()` at [line 10525](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:10525). A missing key reaches `LoadCoreTile`, which launches a worker and calls `World->generateTile()` at [line 13293](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13293). Core mesh sampling then reaches the changed density return above. This multiplies work across existing samples in eligible native-field tiles; it does not increase their sample count. The checked-in standalone manifest has no `resolved_layout_json`, so the Linux headless bisect may not exercise this Unreal-fed composition path.
|
||||
|
||||
**Windows experiment:** There is no existing toggle for the whole composition. For the cheapest broad test, temporarily omit the `Root->SetStringField(TEXT("resolved_layout_json"), …)` line at [VoxelForgeCoreAdapter.cpp:1785](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1785), rebuild Runtime, and rerun the same `-StreamingTraceOnly` command. Keep tile keys, LODs and cells per tile fixed. A large fall in per-LOD worker time supports this path; little change refutes it as the dominant cost. This is a measurement-only geometry change. If it falls, a follow-up can isolate passages by constructing the Core world from `Snapshot->withoutResolvedLayoutPassages()` at [VoxelWorld.cpp:7663](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7663). A drop there implicates passage scanning/carving specifically.
|
||||
|
||||
2. **Authored CoreGeometry — conditional for the current re-saved assets; ruled out for the U1 trace by its report.** U1 added the optional property at [VoxelStrateDefinition.h:125](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateDefinition.h:125). The adapter skips definitions with no recipe or authored arrays:
|
||||
|
||||
```cpp
|
||||
if (!Definition || !HasCoreGeometry(*Definition)) continue;
|
||||
```
|
||||
|
||||
at [VoxelForgeCoreAdapter.cpp:1037](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1037). When active, native recipes, stamps, rooms or tunnels can add per-sample field work. The U1 report says its old assets had CoreGeometry off and arrays empty ([report lines 135–140](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md:135)). U2 shows all four `DA_Strate` asset hashes changed, while `DA_Settings` did not ([report lines 9–15](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Unreal2/REPORT.md:9)); it does not audit whether CoreGeometry is active.
|
||||
|
||||
**Windows experiment:** In the Asset Editor, for each active `DA_Strate1–4`, set `CoreGeometry.GeneratorRecipe=None` and clear `Stamps`, `Rooms`, and `Tunnels`. Rerun and compare tiles in those strates. A fall confirms authored geometry was contributing; no change refutes it. If those fields are already empty, this cannot explain the result.
|
||||
|
||||
3. **L44’s expanded shape-free proof — limited to cold, pre-compile tiles near passage columns.** The native-only path was already present at L14; L44 did not disable it for Unreal. It added a conservative support-column envelope in `ResolvedPassageTouchesBox`, beginning with `if(passage.controlPoints.size()>=2)` at [VoxelForgeCore.cpp:1275](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:1275). Before shape compilation, Unreal calls `isTileShapeFree()` and caches the result at [VoxelWorld.cpp:13100](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13100). Core selects native-only generation while the shape is not ready at [VoxelForgeCore.cpp:2007](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2007), then checks the proof again inside tile generation.
|
||||
|
||||
This can move passage-column tiles from the early native-only path to the compiled-shape path. It cannot explain a doubled cost across every steady-state tile. The proof check itself is per tile, not per density sample.
|
||||
|
||||
**Windows experiment:** Add a temporary log at the successful `LoadCoreTile` admission point recording tile key and `bShapeReady`; correlate those entries with worker times and the existing `[VoxelForgeCoreRuntime] compiled=1` log at [VoxelWorld.cpp:7792](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7792). If slow jobs are mostly post-compile, this is refuted. If they cluster among pre-compile tiles near the passage envelope, temporarily revert only L44’s added column-bound block and compare those same tiles. A drop supports an eligibility-path effect; the revert is diagnostic because it weakens the shape-free correctness proof.
|
||||
|
||||
4. **A profiling or hash-audit switch left on — possible multiplier, but no evidence it was enabled.** Core density hashing is disabled by default (`bCaptureVoxelForgeCoreHashesForValidation=false` at [VoxelWorld.h:315](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelWorld.h:315)); `Options.hashDensity` receives that value at [VoxelWorld.cpp:13284](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13284). The density and tile profiling CVars also default to zero at [VoxelWorld.cpp:265](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:265) and [line 446](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:446). These switches existed at L14.
|
||||
|
||||
**Windows experiment:** Append `-voxel.ProfileTileGeneration=0 -voxel.ProfileDensity=0 -voxel.ProfileDensityFull=0 -voxel.PerfAttribution=0 -voxel.SampleStacks=0` to the Unreal launch, and confirm the actor’s `bCaptureVoxelForgeCoreHashesForValidation` is false. A large fall confirms an override was active; no change rules this out. U1’s documented trace command contains no profiling flags, but its report does not record effective INI values.
|
||||
|
||||
5. **Floating-island upper seal — low and version-gated.** L18 added the upper-seal call at [ShapeWorldInternal.h:6514](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6514); it is reached only for a floating-island recipe with `geometryVersion` 3 (the surrounding branch is `geometryVersion<4` at [line 6504](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/ShapeWorldInternal.h:6504)). The L22 generated recipe version is 4, so the normal generated path skips it. It cannot explain all tiles.
|
||||
|
||||
**Windows experiment:** First log the active floating-island recipe version. If it is 3, run once with `-voxel.TunnelAblateBoundarySeal=1`. A fall limited to Floating Islands tiles supports this operation; no change refutes it. At version 4 or later, this test is inapplicable.
|
||||
|
||||
## Unreal path versus standalone
|
||||
|
||||
Unreal serializes the resolved layout during initialization, parses it into `ResolvedWorldSnapshot`, and constructs the Core `World` at [VoxelWorld.cpp:7610](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7610) and [line 7663](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7663). The headless worker also calls `CoreWorld->desiredTiles()` and `CoreWorld->generateTile()` ([StreamingWorld.h:606](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:606), [line 903](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:903)). Its checked-in owner manifest does not include the resolved-layout field, which Core parses only when that field is present ([VoxelForgeCore.cpp:989](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:989)).
|
||||
|
||||
Unreal’s `worker_task_s` includes more than `generateTile()`: after Core returns, it copies vertices, normals, UVs, colors and indices into Unreal arrays and builds the RealtimeMesh stream set before stamping the worker end time ([VoxelWorld.cpp:13333](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13333), [line 13358](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13358)). That can make Unreal tiles costlier than standalone tiles in absolute time, especially if content emits more geometry. The copy path predates L14. Unreal tile and compile work also uses `UE::Tasks::Launch`; those tasks execute asynchronously and can overlap on workers, so reported elapsed task time can include scheduler contention ([Epic Tasks System documentation](https://dev.epicgames.com/documentation/unreal-engine/tasks-systems-in-unreal-engine)).
|
||||
|
||||
The shared-cache settings show no LOD0 discrepancy: both paths default to `useSharedColumnCache=true` and `memoizeMcGradients=true`; Core forces direct-mapped column caching for `lod<=2` in either caller at [VoxelForgeCore.cpp:2003](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Core/src/VoxelForgeCore.cpp:2003). `VoxelClipmapDesiredTiles` gained bounded wide-division helpers, but Core `BuildDesiredTiles` returns through `BuildCoreDesiredTiles` before the legacy helper path at [VoxelWorld.cpp:10988](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:10988). Those changes do not add density-sample work.
|
||||
|
||||
The other named U1 changes are outside the tile worker hot path: `VoxelSettings::PostLoad` migrates units once when loading assets at [VoxelSettings.cpp:31](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelSettings.cpp:31); `StrateManager::Initialize` resolves metre values into cached slot values at [VoxelStrateManager.cpp:1430](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelStrateManager.cpp:1430); `VoxelContentManager::Initialize` parses resolved-layout anchors at [VoxelContentManager.cpp:332](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelContentManager.cpp:332). The Core native SurfaceWorld and Floating Islands sample adapters do not call those conversions per sample. Content landmark checks can affect ticks if authored, but they are not tile-worker density work.
|
||||
|
||||
`FullResClipLevels`, `CoarseTileCells`, `CoreClipRadius`, and `CoreMaxClipLevel` still need a like-for-like value check. They alter tile-set composition and/or cells per tile, not the composition cost per existing sample. U1’s `compiled=1` log prints clip radius, max level and voxel size; compare those and per-LOD cell counts with L14 before interpreting total worker-seconds.
|
||||
|
||||
## Asset provenance and limits
|
||||
|
||||
U1 says its timing run used the “current owner plan” at 25 cm and reports 2,578 initial tiles ([Unreal1 report lines 64–78](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md:64)). Its audited BuildHost assets are the old U1 hashes, and it says no packages were saved ([lines 135–171](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Unreal1/REPORT.md:135)). U2 records new hashes for all four `DA_Strate` assets, the same `DA_Settings` hash, and a regenerated four-strata/four-passage manifest ([Unreal2 report lines 9–25](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Research/WorldAssemblerM3/Unreal2/REPORT.md:9)). The best reading is that U1’s trace used the old U1 asset set, not today’s re-saved packages. The U1 trace itself does not embed asset hashes, however, and the local L14 report is Linux headless rather than the Windows baseline. L14’s loaded Windows asset hashes and exact clip settings are unknown, so content parity is unproven.
|
||||
|
||||
U1 reports a cold worker sum of 34.777 seconds, while the request’s rounded regression figure is about 30 seconds. The report also says the comparison was not a controlled A/B. I did not inspect the owner’s raw BuildHost traces or verify effective Windows INI overrides. No scripts, builds, tests, binaries, or source edits were performed.
|
||||
@@ -0,0 +1,19 @@
|
||||
# Ranked suspects
|
||||
|
||||
1. **Resolved passage and strata geometry** — [VoxelForgeCoreAdapter.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelForgeCoreAdapter.cpp:1584), [VoxelStrateTypes.h](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelStrateTypes.h:2074). The adapter sends each strate’s passage settings into Core’s resolved layout. The default passage style changed from Straight to Worm, with one connection and 20 segments by default. L18–L37 also added shared ceiling/bedrock composition, planned passages, support geometry, transitions, and mouth fades. These can add field queries and mesh crossings where tiles overlap them. U1 confirms CoreGeometry is empty in the owner assets, but does not report the effective passage settings. **Windows check:** log each strate’s resolved style, connection count, segments, planned route count, plus tile vertices and Core time. Then run a diagnostic A/B with a duplicate settings asset set to `Connections=0`. This changes geometry, so compare only cost and sample counts.
|
||||
|
||||
2. **Unreal may mesh a wider Z band than standalone.** [VoxelWorld.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13270) sets Core mesher options but leaves band limits at their unbounded defaults. Standalone sets `bandMinZVoxel` and `bandMaxZVoxel` from the current strate when its content-cut setting is active ([StreamingWorld.h](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:823)); those bounds restrict mesher sampling. Core also applies its own band limit to eligible native SurfaceWorld tiles, so this mainly affects other or non-native-safe tiles, and only when they cross the band. **Windows check:** add a temporary `voxel.CoreStrateBand` switch to pass the standalone-equivalent bounds; compare matched tile keys, density sample counts, and Core time.
|
||||
|
||||
3. **Unreal’s post-Core mesh conversion.** [VoxelWorld.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13333) copies Core vertices and indices into Unreal arrays and builds an RMC stream set after `generateTile`. Standalone also copies the mesh, but into CPU vectors. This wrapper code was not changed in the range; larger meshes from the new geometry could still make this stage costly. **Windows check:** log separate durations for `generateTile` and conversion/`BuildTileStreamSet`, with vertex and index counts. The existing cycle boundaries bracket these stages at lines 13296 and 13358.
|
||||
|
||||
4. **Shape-free readiness can affect startup scheduling, not steady tile cost.** [VoxelWorld.cpp](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13100) caches `isTileShapeFree` results while compilation is pending and admits proven native-only tiles. Core uses the same native-only rule in standalone; L44 did not disable it for Unreal. A stricter proof could delay initial submissions or raise tick cost during compilation, but should not double worker time after the shape is ready. **Windows check:** count shape-free/dependent checks and requests while `bShapeReady=false`; record `nativeOnlyRoute` for early tiles.
|
||||
|
||||
5. **Legacy-only hot helpers and optional background work are low-probability suspects.** L60’s unsigned hash fix in [VoxelCaveMorphology.h](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelCaveMorphology.h:261) can affect legacy noise evaluation, but is not the Core tile field path. L61’s clipmap arithmetic is scheduling/bookkeeping; the bounded seal helper is for legacy density generation. Density-volume fills are separate tasks and default off ([VoxelSettings.h](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Public/VoxelSettings.h:317)). **Windows check:** confirm Core remains enabled, hash capture/profile are off, and the density-volume startup log says disabled. If investigating legacy fallback, profile its density field separately.
|
||||
|
||||
The source defaults for `FullResClipLevels` (2), `CoarseTileCells` (16), `CoreClipRadius` (3), and `CoreMaxClipLevel` (8) did not change in this range. U1 reports the owner’s clip radius/max level as 3/8. Its unit migration preserved the audited physical heights, transitions, gap, and 8 m layout width, so those migrations do not indicate a resolution increase. The shared column cache is Core-owned and enabled by default in both paths; standalone explicitly enables it. Unreal’s gradient memoization defaults on too. Core forces direct-mapped indexing for LOD 0–2 regardless of the Unreal cache CVar, so that CVar is not a useful LOD0 fix.
|
||||
|
||||
## Adapter vs standalone
|
||||
|
||||
Unreal serializes the owner assets into a resolved snapshot, parses it, creates `VoxelForgeCore::World`, then requests tiles through `World::generateTile` ([initialization](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:7591), [tile request](/home/jahni/VoxelM/Plugins/VoxelForge/Source/VoxelForge/Private/VoxelWorld.cpp:13288)). Standalone’s headless streamer also calls that Core method ([StreamingWorld.h](/home/jahni/VoxelM/Plugins/VoxelForge/Standalone/Source/StreamingWorld.h:882)). They share the Core field and mesher, but differ in task scheduling, tile post-processing, and the Z-band options noted above. CoreGeometry resolution, JSON conversion, anchor parsing, and layout-unit conversion happen at initialization; U1’s owner assets have no CoreGeometry, so those additions do not resolve stamps or graphs per tile.
|
||||
|
||||
For the Windows run, use a Task Graph Insights trace (`-trace=default,task`) to compare worker availability and task contention alongside the stage timings; Epic documents task lifecycle tracing in [Task Graph Insights](https://dev.epicgames.com/documentation/unreal-engine/task-graph-insights-in-unreal-engine-5?lang=en-US). The checked-in U1 report contains the timing and asset audit, but the raw `Saved/BuildHost` logs were not present in this checkout.
|
||||
Reference in new issue
Block a user