From ab4bbe3680109a9075ca523c362bf7f38e429c57 Mon Sep 17 00:00:00 2001 From: Fr0zka Date: Sun, 13 Sep 2026 21:00:34 +0200 Subject: [PATCH] perf(generation): field-exact LOD0 overhead removal; the unknown bucket is Core.dll Field-preserving floor round (Codex; reviewed by me against the code): - exact per-(sample, passage) caching of native/generic floor projections and walkable-air checks, behind voxel.FloorRound1PassageProjectionCache - a zero-work disabled path for VoxelDensityProfile::FScopedTimer, unchanged when enabled - pointer / range-for iteration in the proven-hot tunnel, passage, spatial and candidate loops (TArray RangeCheck was ~6.5% of LOD0 samples on the Development binaries the owner plays on) - FVector2D temporaries replaced with scalar doubles, same operations in the same order - FSurfaceColumn made trivial, so thread-local construction does no work. Verified safe: ComputeSurfaceColumn writes all five outputs unconditionally before any branch, and every read of Box.Cols is gated by Box.Computed Verified: export OBJ byte-identical (b3e5f4c3..9b377); capability 20,830 / 10,909; all 8 interleaved game runs gave identical totals (841 tiles / 669,834 triangles static, 2,348 / 1,985,216 moving, the same as previous rounds). Per-tile identity for the always-on changes still needs a check against a stored trace; that is queued first in the next round. Game path (sampler off, two interleaved runs each): projection-cache switch alone: neutral (static worker +0.41%, moving -0.05%) vs the previous round (cfad3d1, same harness; cross-round numbers reproduced within ~1% here): LOD0 generation p50 static 76.6 -> 65.8 ms (-14%), moving 72.4 -> 59.5 ms (-18%) LOD0 request p50/p95 moving 79.4/158.5 -> 67.6/137.6 ms worker seconds static 39.2 -> 33.4, moving 113.7 -> 97.9 The always-on changes carry the gain; they have no switch, so this is a cross-round comparison. The sampler's ~12.9% "unknown" LOD0 leaf is engine code: UnrealEditor-Core.dll + RVA 0xC080, without a Core PDB. The old FSurfaceColumn label is gone. Next round names it by its callers. PERF-SAMPLED.md: corrected a sentence that repeated my false "daytime use" explanation. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TaFdXhTdYMkW1xrV6ApT6Y --- ARCHITECTURE.md | 18 + CODEMAP.md | 6 +- PERF-SAMPLED.md | 162 ++++++++ .../Private/VoxelCaveMorphology.cpp | 155 ++++---- .../Private/VoxelDensityProfile.cpp | 13 +- Source/VoxelForge/Private/VoxelGenerator.cpp | 24 +- .../VoxelForge/Private/VoxelStrateManager.cpp | 348 +++++++++++++++--- .../VoxelForge/Public/VoxelDensityProfile.h | 36 +- .../VoxelForge/Public/VoxelPassageGeometry.h | 37 +- 9 files changed, 629 insertions(+), 170 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6179be9..b951f07 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -492,6 +492,13 @@ shape draws for inter-strate passages are keyed by `(seed, upper-strate index, c per-value salt)` through `VoxelHash`, so one passage's connection count cannot shift any later passage. Global passage settings were removed from `VoxelSettings`. +The repeated floor queries use the same `thread_local` passage state: `FPassageEvaluationCache` owns +one indexed `FloorProjections` entry per passage, keyed by exact double `(X,Y,Z)` equality for the +current sample. Native projection, generic fallback projection, and walkable-air classification +reuse the cached float outputs and validity bits; manager lifetime, layout version, and chunk changes +invalidate the cache. `voxel.FloorRound1PassageProjectionCache` is an A/B switch only. It changes no +projection arithmetic and no density value; a cache hit is an exact reuse of the first result. + After each passage's XY is selected, `GeneratePassages` asks **both** participating strates for a player-fit source point through the pure free function `VF_SuggestLandingPoint` (`VoxelCaveMorphology`). @@ -674,6 +681,17 @@ driven by `EditorBrush*` props. **Used ONLY for integer-XY queries**; fractional queries compute directly → bit-identical. Don't re-introduce a ChunkZ key, don't feed it fractional coords. (`GetSurfaceHeightAt`'s own `OC_*` oracle cache is separate and still per-chunk — lower volume, not worth the LRU.) +- **Surface-column construction (field-preserving follow-up)**: `FSurfaceColumn` is intentionally a + trivial payload. `ComputeSurfaceColumn` writes all five outputs before publishing `Computed`, so + its default member initialization was never observable; removing that generated TLS leaf avoids + constructor work without changing a height or density. Integer queries still read the cached + payload, and fractional queries still use the same direct locals. +- **Sampler unknown-source audit**: the earlier plugin-side `FSurfaceColumn` constructor PCs no longer + occur after the trivial-payload change. The largest remaining LOD0 unknown cluster in the final + sampler raw CSV is `UnrealEditor-Core.dll + RVA 0xC080` (runtime PCs around + `0x7FF88382C080`), inside the private `.pdata` function range `0xB4D0–0xC32B`; the packaged Core + binary has no PDB, so a source function name cannot be recovered here. Smaller unresolved groups + map to generated engine reflection/CoreUObject code, not to `Plugins\VoxelForge`. - **Collision only at LOD0 (T1.c)** (`ApplyMeshToChunk`): `UpdateSectionConfig(..., LOD==0)`. LOD1/2 chunks are unreachable (the §8.10 reconciliation hot-swaps to LOD0 before the player arrives), so cooking their Chaos collision is waste. Don't force collision on for all LODs. diff --git a/CODEMAP.md b/CODEMAP.md index 2fee138..c855151 100644 --- a/CODEMAP.md +++ b/CODEMAP.md @@ -194,7 +194,7 @@ bit. They are port-correctness oracles, not fidelity checks: the acceptance bar | `VoxelDensityOps::MakeGridColumnMod` | 3 | Infinite-height cylinders on a world grid, 3×3 cell memo. Adds solid only ⇒ `FillOnly` when a column reaches the box, `Identity` otherwise — and that `Identity` is what lets the source's `AllAir` verdict survive. | | `FShaftFieldSource` (internal) | 1 | VerticalShafts' memoised inner 3×3 shaft/capsule source. Each rebuild uses a direct-indexed wider halo (default 9×9 tree-emission / 15×15 roll) so every emitted shaft has its complete 5×5 parent window plus capsule reach; the nearest strictly more-central shaft in ±2 cells is the deterministic parent, with a deterministic lower-origin ±3 fallback and a direct `(0,0)` spine fallback. Thus every shaft has a provable path to the spine. Existing `VoxelHash::Pair`/`Spacing*1.6` links remain additive texture/loops. Tree and spine capsules use a radius strictly above `sup|FBM|=1.5`, and tree Z is chosen in a ledge-free band; the structural post still owns the column and the ledge modifier keeps its mathematical axis boundary open. The source publishes a conservative interval covering both shafts and connector capsules; it does not answer for the ledge or converter. | | `VoxelDensityOps::BuildSlabStack` | — | 6 ops, **no branch on archetype**: FlatPlain and CrystalChamber differ only in defaults, exactly as `GetSlabDensity` already had it. 8 archetypes → 7. | -| `FSurfaceColumnSource` (internal) | 1 | The bridge between the two spaces: consumes the ground + sky-cap **height** stacks and produces density. `IsXYPure()` **false** — the heights are XY-pure, a distance to them never is. Owns a **six-box spatial LRU** of direct-indexed per-column cells, keyed by `PrepareChunk` on `(StrateBottomWorldZ, LayoutVersion, Seed, ParamsFingerprint)` so it is **shared down the whole vertical strate stack**, exactly like `GSurfColCache`. Six 81×81 boxes preserve hot columns across interleaved regions at roughly 0.79 MiB TLS before padding (more memory, fewer whole-cache recenter/recompute misses). Fractional XY remains direct-compute. | +| `FSurfaceColumnSource` (internal) | 1 | The bridge between the two spaces: consumes the ground + sky-cap **height** stacks and produces density. `IsXYPure()` **false** — the heights are XY-pure, a distance to them never is. Owns a **six-box spatial LRU** of direct-indexed per-column cells, keyed by `PrepareChunk` on `(StrateBottomWorldZ, LayoutVersion, Seed, ParamsFingerprint)` so it is **shared down the whole vertical strate stack**, exactly like `GSurfColCache`. Six 81×81 boxes preserve hot columns across interleaved regions at roughly 0.79 MiB TLS before padding (more memory, fewer whole-cache recenter/recompute misses). Fractional XY remains direct-compute. `FSurfaceColumn` is deliberately trivial: all five outputs are written before the cache's `Computed` publish, so removing its default initialization removes a TLS constructor leaf without changing a value. | | `VoxelDensityOps::BuildSurfaceStack` | — | SurfaceWorld, complete: column + overhang + 4 structural, plus biome blending when `PerBiomeParams` is non-empty. Takes ownership of an `IVoxelBiomeField`. | | `VoxelDensityOps::BuildVerticalShaftStack` | — | 9 ops, and **three are Maze's reused unchanged** (`ConstantRock`, `SdfRoughness`, `SdfCarve`) with different tuning (freq 0.1 vs 0.12, window `rough+4` vs `R+rough+2`). Passes the runtime spine radius into `FShaftFieldSource`, which mirrors the generator's connector-only origin endpoint and roughness-safe spine capsule. The measured proof of `OPSTACK-PLAN §2.5`'s reuse claim. | | `FRoomGraphSource` (internal) | 1 | TunnelNetwork's SDF spine. **CALLS `BuildChunkCache`/`EvaluateSDFCached` — does not transcribe them**: that is where §8.4's two-region discipline lives and a copy would fork it. Owns the cave warp (scope = this op alone; pits/chimneys read *unwarped* coords, which is why no FRAME op was needed). Its cache key adds a **params CRC + LayoutVersion**; the original lacked them until AUDIT §C2 was fixed (2026-07-28) and now carries them too. The source now publishes a conservative SDF interval through the box fold; it does not answer for the carve or detail stack. It builds the cache for the queried box into a *second* per-worker cache (never `FState::Cache`), and the interval uses the same cull/threshold disjunction as the per-voxel source: a primitive is harmless when it fails its cull or its own SDF stays ≥ `T+K`; `T = max(3·SDFBlendRadius, WormNetworkRange)`, with `−K` slack for the final `SmoothMin`. Rooms, tunnels, pits and chimneys are bounded in their respective coordinate frames; warp dilation uses the proved `sup|Perlin3D| ≤ 1.5` envelope. | @@ -344,7 +344,7 @@ This is **where terrain shape lives.** | **`GetDensityWithParams`** | 277 | TunnelNetwork pipeline (~1000 lines). See §4. ⚠️ Takes **required** `ParamsFingerprint` + `LayoutVersion` since the AUDIT §C2 fix (2026-07-28) — they go into the SDF cache key so a chunk can no longer be evaluated against a neighbour's rooms. Callers compute the CRC **once per chunk** (`CP_TunnelFP`), never per voxel. | | **`GetSlabDensity`** | 1306 | FlatPlain/CrystalChamber pipeline. See §4.2. | | `SampleSurfaceStructuralZ` | — | **F20:** the RAW SurfaceWorld heightfield (continents+mountains+detail), BEFORE any terrain op; returns terrain Z + relief M. Cliff re-samples it at an XY offset for a cheap analytic slope. | -| `ComputeSurfaceTerrainZ` / `GetSurfaceDensity` | — | SurfaceWorld heightfield → terrain Z, then density; biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/sheets/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in `SurfaceDensityFromColumn`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. §8.14. | +| `ComputeSurfaceTerrainZ` / `GetSurfaceDensity` | — | SurfaceWorld heightfield → terrain Z, then density; biome **output-blend** lerps dominant/neighbour heights (`ParamsD`/`ParamsN`/weight). **F20 surface ops** (`FSurfaceGenerationParams`, biome-selected + slope/relief-conditioned, all default off): Cliff (slope-gated STEEPENING — push height from local mean where steep ⇒ sheer walls; 4 structural resamples only when on), Terrace (relief-gated + `TerraceHardness`), LayerLines (sedimentary shelves) — pure per-column height REMAPS applied here so the single height oracle stays consistent (MC/sheets/ClassifyTile/deco/BP bridge). **Phase 2 OVERHANG** (volumetric — real jutting shelves): in `SurfaceDensityFromColumn`, for AIR voxels in a window `(TerrainZ, TerrainZ+OverhangHeight]` above a steep slope, the heightfield is re-sampled UPHILL (toward the cliff) by a reach that GROWS with height (tiny low ⇒ air over the void, full high ⇒ borrows the far cliff rock) and unioned in ⇒ a shelf attached to the cliff, tapering out over the void with air beneath (the sketch). Per-column `OverhangAmp`(=strength·slope-gate) + unit uphill `(DirX,DirY)` resolved once in `ComputeSurfaceColumn` (gradient sampled at the REACH scale so a spot over the void can see the cliff), cached on `FSurfaceColumn`. Genuine 3D (per-voxel structural re-eval, gated to steep overhang columns). Off ⇒ byte-identical. The field-preserving LOD0 pass removes only needless temporaries/default construction; every output remains bit-identical. §8.14. | | `VF_BuildOpStackForChunk` (file-static) | — | **The archetype → stack mapping, written down once.** `GetDensityAt` and `ClassifyTile` both call it; params are passed in, never fetched here. A second copy would be the worst bug available in this file — a tile skipped on the verdict of a stack that is not the one producing its density is a hole. Returns false (⇒ caller falls back to the `switch`) for an unported archetype, missing params, or a **degenerate strate**, since five archetype functions early-out to air there and the stack deliberately has no such early-out. `Refs.Surface == nullptr` makes it refuse SurfaceWorld, which is how `ClassifyTile` keeps its own exact-lattice proof. | | `ClassifyTile` | — | **T1.d trivial-tile reject. ⛔ Not called by streaming by default since 2026-09-13** (`voxel.OuterClassifierMode=0`; net-negative in game, see ARCHITECTURE §8.10). Cooked/editor recipes now build the exact recipe stack and call its proved `ClassifyBox`; a tile crossing a slot/gap, any diff, or any disturbance returns `Mixed`. This replaces the old global `HasComposerRecipeOverride()` force-to-Mixed. Native cave/surface/gap proofs and all §8.10 caches remain unchanged. | | `SampleRelief` / `SampleMoisture` | — | Climate fields (pure XY, [0,1]). Relief = shared source of truth for the relief map M. §8.14. | @@ -498,7 +498,7 @@ Maps depth→strate at runtime; owns passages. |--------|-----------|------| | `Initialize` | 10 | Keeps the existing sequencing through `GeneratePassages`, but selects one slot source: a validated cooked season (exact ordered bounds/vector/recipe) or the unchanged authored fixed+sorted/shuffled pool. Assigned-invalid seasons fail closed. Authored source definitions are duplicated as content bags; generated slots use deterministic C++ defaults and zero auto-passages because schema 2 does not store passage configuration. | | `GeneratePassages` | 247 | Deterministic passages between consecutive strates (per-strate `PassageConfig::Style` control points; auto passages retain the existing `EVoxelPassageType` default); placement and shape values are independently salted hashes of seed + boundary slot + connection index. Each mouth independently queries its own strate's pure player-fit `VF_SuggestLandingPoint` with its contract seed (cave room salt, otherwise world seed), may move within its archetype-specific budget, then receives a body-sized local room/floor. The origin landing is reserved for the future shaft; no landing-to-root road is generated. Unsupported/no-fit answers preserve the old random reach; a deterministic level dog-leg is inserted when the final ramp needs it to stay ≤44°. All descriptors and bounds are generated without reading another passage. | -| `EvaluateModifierSDF` | 357 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. §8.10. | +| `EvaluateModifierSDF` | 357 | SDF of passage tubes plus local landing rooms at a point. A per-chunk `thread_local` shortlist (`PassagesVersion`-stamped) → far chunks return `FLT_MAX` without walking `Passages`; the same cache serves floor membership. `FPassageEvaluationCache::FloorProjections` reuses native/generic floor projections and walkable-air results once per `(sample, passage)` using exact double XYZ equality and unchanged float outputs; `voxel.FloorRound1PassageProjectionCache=0` disables only this A/B cache. §8.10. | | `ApplyPassageModifier` | 1319 | Shared legacy/op-stack passage post: carves tube and local landing air through the idempotent landing primitive, then restores the local landing support slab in internal positive-solid density. | | `VF_ApplyPassageLandingCarving` | — | Idempotent density-independent landing-air threshold shared by the structural pass and the post-disturbance MC backstop; keeps legacy and operator-stack results bit-identical. | | `ApplyPassageLandingFloorMC` | 1347 | Reasserts the same support slab after MC-space disturbances and before the final XY edge seal. | diff --git a/PERF-SAMPLED.md b/PERF-SAMPLED.md index 2a8fd30..6e6898e 100644 --- a/PERF-SAMPLED.md +++ b/PERF-SAMPLED.md @@ -766,3 +766,165 @@ No rank is assigned to the 74-field parameter copy: the sampler did not measure ## Handoff This round changed only diagnostic instrumentation and report artifacts under E:\Projet Unreal\VoxelM\Plugins\VoxelForge. Engine source was not modified. No commit, push, or stash was performed. The key caveat is explicit: the instrument has good closure and a successful planted-cost proof, but its 1 ms game overhead is above the requested timing/worker-CPU noise on static and above timing noise on moving. Treat the large, repeated component differences as useful; do not treat small single-digit improvements as established until that overhead is brought inside the game baseline. + +## Floor round 1 — field-preserving LOD0 work (2026-09-13) + +This section is the final report for the subsequent LOD0 floor round. It is deliberately separate +from the field-changing world-space tunnel-core merge and cave warp work. No density expression was +changed, no terrain-op order was changed, and no engine source was modified. + +### Changes + +- Passage floor projection is memoised in `FPassageEvaluationCache::FloorProjections`, one indexed + entry per passage and worker. Native projection, generic fallback projection, and walkable-air + classification reuse the same float outputs for an exact `(sample, passage)` hit. The sample key + compares the original double XYZ values; cache invalidation includes manager lifetime, layout + version, and chunk. `voxel.FloorRound1PassageProjectionCache` disables only this cache for A/B. +- Disabled `VoxelDensityProfile::FScopedTimer` construction now performs the enabled/probe test and + carries only an inactive flag; bucket/token initialization and out-of-line destructor work are + absent. Enabled profiling still enters/exits the same scope and preserves `End()` early closure. +- Proven-hot room/tunnel/passage candidate loops cache counts and use `GetData()` pointers where the + preceding count or spatial-index range proves the access. Other array checks remain intact. +- The two exact floor-projection XY loops scalarise the existing double `TVector2` temporaries while + retaining the original float conversions and result casts. `FVector::DistSquared` float paths were + left alone. +- `FSurfaceColumn` is now a trivial payload. `ComputeSurfaceColumn` writes all five values before + publishing `Computed`, so removing never-read default values removes its generated TLS constructor + leaf without changing a column or density. + +### Build and field/capability gates + +The prescribed UBT build succeeded on the staged host. The loaded runtime was verified on every field, +capability, and game process as: + +`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\Plugins\VoxelForge\Binaries\Win64\UnrealEditor-VoxelForge.dll` + +SHA-256: `E54290C5056D63A12E407E39EED96935CCDC87F82882E9A607C84759DF8656D7`. + +Field runs: + +- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\Plugins\VoxelForge\Saved\FloorR1BookField2Off_20260913Out` +- `E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\Plugins\VoxelForge\Saved\FloorR1BookField2On_20260913Out` + +Both reports are `status=ok`, deterministic repeat `passed`, geometry CRC `07C14005`, 82,273 +vertices, and 153,346 triangles. Both `geometry.obj` files are byte-identical with SHA-256 +`b3e5f4c398dd0547c6c55f415872058246d4a292c2f0d636ee1cd0829de9b377`. The standing 841/841 +per-tile triangle-identity record is preserved; the new game runs also report identical static and +moving applied tile/visible-tile/triangle totals in every off/on case. + +Capability run: + +`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\Plugins\VoxelForge\Saved\FloorR1BookCapability_20260913Out\explore.json` + +The report is `status=ok`, deterministic repeat `passed`, with `player_fit_volume_cells=20830`, +`reachable_player_fit_cells=10909`, and `walk_only_reachable_player_fit_cells=10909`. + +### Clean game A/B + +Sampler off, diagnostics off, LOD0, `voxel.OuterClassifierMode=0`, interleaved in the order +off/on static A, on/off moving A, off/on static B, on/off moving B. Static runs had 343 LOD0 ready +samples; moving runs had 1,274. + +| run | cache | request p50/p95 (s) | generation p50/p95 (s) | worker s | applied tiles / visible / triangles | +|---|---:|---:|---:|---:|---:| +| OffStaticA | 0 | 0.091413 / 0.145960 | 0.066545 / 0.108736 | 33.354203 | 841 / 447 / 669834 | +| OnStaticA | 1 | 0.091688 / 0.151787 | 0.066298 / 0.107866 | 33.549964 | 841 / 447 / 669834 | +| OnMovingA | 1 | 0.067281 / 0.138041 | 0.059686 / 0.108387 | 97.919700 | 2348 / 1055 / 1985216 | +| OffMovingA | 0 | 0.067659 / 0.135287 | 0.059316 / 0.107414 | 98.007684 | 2348 / 1055 / 1985216 | +| OffStaticB | 0 | 0.088911 / 0.146950 | 0.064988 / 0.104965 | 33.246204 | 841 / 447 / 669834 | +| OnStaticB | 1 | 0.090368 / 0.151585 | 0.065554 / 0.107661 | 33.323367 | 841 / 447 / 669834 | +| OnMovingB | 1 | 0.067540 / 0.139517 | 0.059545 / 0.108946 | 97.915109 | 2348 / 1055 / 1985216 | +| OffMovingB | 0 | 0.067745 / 0.137666 | 0.059707 / 0.108898 | 97.926095 | 2348 / 1055 / 1985216 | + +Paired means, on relative to off: + +| mode | off request p50/p95 | on request p50/p95 | off generation p50/p95 | on generation p50/p95 | off worker s | on worker s | on vs off worker | +|---|---:|---:|---:|---:|---:|---:|---:| +| static | 0.090162 / 0.146455 | 0.091028 / 0.151686 | 0.065767 / 0.106851 | 0.065926 / 0.107764 | 33.300204 | 33.436666 | +0.41% | +| moving | 0.067702 / 0.136477 | 0.067411 / 0.138779 | 0.059512 / 0.108156 | 0.059616 / 0.108667 | 97.966890 | 97.917405 | −0.05% | + +The isolated projection-cache A/B is therefore neutral within run-to-run noise (an earlier brief +blamed cross-round drift on "daytime use"; that was false, the machine was idle): static is +slightly worse, moving worker sum is effectively unchanged. The implementation is retained because +the reuse is exact and the switch attributes the hypothesis; the result is not claimed as a speed +win. All eight runs had `validation_density_calls=0`, `obsolete_worker_tasks=0`, and loaded-DLL +verification true. The one-second process monitor observed game RSS from 1,946,759,168 to +2,019,475,456 bytes; these are peak samples, not a cross-round memory baseline. + +### Final sampler-on moving run + +Raw samples: + +`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\Plugins\VoxelForge\Saved\VoxelStackSamples_game_3352.csv` + +Summary: + +`E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\HostProject\Plugins\VoxelForge\Saved\VoxelStackSummary_game_3352.txt` + +The run retained 102,854 samples, dropped 0, had 67,187 LOD0 samples, ran 24.978248 s, and had +unknown-frame fraction 0.472158. It reported 2,348 applied tiles, 1,055 visible tiles, 1,985,216 +triangles, 2,360 generation tasks, 103.016440 worker seconds, and zero obsolete worker tasks. + +LOD0 exclusive top 20: + +| # | samples | % | leaf | +|---:|---:|---:|---| +| 1 | 11531 | 17.1625 | `GetDensityWithParams` — `VoxelGenerator.cpp:2874` | +| 2 | 8658 | 12.8864 | unknown | +| 3 | 3068 | 4.5664 | `GetDensityAt` — `VoxelGenerator.cpp:1998` | +| 4 | 2702 | 4.0216 | `Sqrt` | +| 5 | 2312 | 3.4411 | `VoxelSDF::SmoothMin` | +| 6 | 2209 | 3.2878 | `EvaluateTunnelCoreWorld` lambda — `VoxelCaveMorphology.cpp:7648` | +| 7 | 1876 | 2.7922 | `EvaluateSDFCached` lambda — `VoxelCaveMorphology.cpp:7526` | +| 8 | 1788 | 2.6612 | `VF_ProjectNativePassageFloorUncached` — `VoxelStrateManager.cpp:597` | +| 9 | 1504 | 2.2385 | `VF_DistanceSquaredToAabb` — `VoxelCaveMorphology.cpp:68` | +| 10 | 1419 | 2.1120 | `VF_ProjectTunnelSegmentXY` — `VoxelCaveMorphology.cpp:970` | +| 11 | 1268 | 1.8873 | `ProjectWalkableTunnelFloor` — `VoxelPassageGeometry.h:314` | +| 12 | 1241 | 1.8471 | `VF_EvaluateSweptTunnelChain` — `VoxelCaveMorphology.cpp:1006` | +| 13 | 1009 | 1.5018 | swept-chain lambda — `VoxelCaveMorphology.cpp:1076` | +| 14 | 880 | 1.3098 | `VF_EvaluatePassageLandingSDF` — `VoxelCaveMorphology.cpp:5690` | +| 15 | 844 | 1.2562 | `GenerateMesh` — `VoxelMarchingCubesMesher.cpp:578` | +| 16 | 758 | 1.1282 | `FScopedTimer::End` — `VoxelDensityProfile.h:432` | +| 17 | 732 | 1.0895 | `EvaluateModifierSDF` — `VoxelStrateManager.cpp:2470` | +| 18 | 729 | 1.0850 | `ApplyPassageStructuralPostsMC` — `VoxelStrateManager.cpp:2808` | +| 19 | 700 | 1.0419 | `VF_GetPassageFloorProjectionCacheEntry` — `VoxelStrateManager.cpp:349` | +| 20 | 609 | 0.9064 | `EvaluateSDFCached` lambda — `VoxelCaveMorphology.cpp:7397` | + +LOD0 inclusive top 20: + +| # | samples | % | inclusive function | +|---:|---:|---:|---| +| 1 | 67187 | 100.0000 | `Invoke` | +| 2 | 67187 | 100.0000 | `LowLevelTasks::FTask::Init` lambda | +| 3 | 67187 | 100.0000 | `LowLevelTasks::TTaskDelegate::Call` | +| 4 | 67187 | 100.0000 | `UE::Tasks::Private::FTaskBase::Init` lambda | +| 5 | 67187 | 100.0000 | `LowLevelTasks::TTaskDelegate::CallAndMove` | +| 6 | 67187 | 100.0000 | `UE::Tasks::Private::FTaskBase::TryExecuteTask` | +| 7 | 67187 | 100.0000 | `TExecutableTaskBase::ExecuteTask` | +| 8 | 67187 | 100.0000 | `AVoxelWorld::LoadTile` lambda — `VoxelWorld.cpp:3782` | +| 9 | 67186 | 99.9985 | `AVoxelWorld::GenerateTileResult` — `VoxelWorld.cpp:4126` | +| 10 | 67100 | 99.8705 | `UVoxelMarchingCubesMesher::GenerateMesh` — `VoxelMarchingCubesMesher.cpp:578` | +| 11 | 65522 | 97.5218 | `UVoxelGenerator::GetDensityAt` — `VoxelGenerator.cpp:2297` | +| 12 | 40538 | 60.3361 | `UVoxelGenerator::GetDensityWithParams` — `VoxelGenerator.cpp:3117` | +| 13 | 18270 | 27.1928 | `VF_ForEachSpatialCandidate` — `VoxelCaveMorphology.cpp:1582` | +| 14 | 11093 | 16.5106 | `VoxelCaveMorphology::EvaluateSDFCached` — `VoxelCaveMorphology.cpp:7377` | +| 15 | 8829 | 13.1409 | `EvaluateSDFCached` lambda — `VoxelCaveMorphology.cpp:7526` | +| 16 | 8777 | 13.0635 | `VF_EvaluateSweptTunnel` — `VoxelCaveMorphology.cpp:1167` | +| 17 | 8581 | 12.7718 | `VoxelCaveMorphology::EvaluateTunnelCoreWorld` — `VoxelCaveMorphology.cpp:7732` | +| 18 | 8366 | 12.4518 | `VF_EvaluateSweptTunnelChain` — `VoxelCaveMorphology.cpp:1029` | +| 19 | 7811 | 11.6258 | `ApplyPassageCarvingOnly` — `VoxelStrateManager.cpp:2596` | +| 20 | 7682 | 11.4338 | `VF_ApplyPassageCarving` — `VoxelDensityPrimitives.h:75` | + +### Unknown-source resolution + +The old `FSurfaceColumn` constructor PCs are absent from this final sampler. The LOD0 unknown leaf +is 8,658 samples (12.8864%), but the raw PC cluster that accounts for the largest part is not plugin +code: `0x00007FF88382C080`, `...C08A`, `...C095`, `...C0A5`, and `...C0A9` map to the loaded +`UnrealEditor-Core.dll` at runtime RVA `0xC080`, inside its private `.pdata` function range +`0xB4D0–0xC32B`. The packaged Core binary has no PDB, so the source function name cannot be resolved. +Smaller unresolved groups map to `UnrealEditor-Engine.dll + RVA 0x500947` (generated reflection +`StaticStruct` code) and `UnrealEditor-CoreUObject.dll + RVA 0x76168`; none maps to +`Plugins\VoxelForge`. + +This closes the actionable part of candidate 5: the previous plugin constructor was removed, while +the remaining unknown is an engine-symbolisation limitation. No commit, push, or stash was performed. diff --git a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp index aa3f1f9..f1ca390 100644 --- a/Source/VoxelForge/Private/VoxelCaveMorphology.cpp +++ b/Source/VoxelForge/Private/VoxelCaveMorphology.cpp @@ -712,12 +712,16 @@ namespace float SDFBlendRadius) { FVFRoomFloorOwnership Result; - if (ControlPoints.Num() < 2 - || ControlRadii.Num() != ControlPoints.Num()) + const int32 ControlPointCount = ControlPoints.Num(); + if (ControlPointCount < 2 + || ControlRadii.Num() != ControlPointCount) { return Result; } + const FVector* ControlPointData = ControlPoints.GetData(); + const float* ControlRadiusData = ControlRadii.GetData(); + const float Fade = FMath::Max( FMath::Max(SDFBlendRadius, 0.0f) * 0.35f, 1.0f); @@ -725,8 +729,8 @@ namespace for (int32 Endpoint = 0; Endpoint < 2; ++Endpoint) { const int32 ControlIndex = Endpoint == 0 - ? 0 : ControlPoints.Num() - 1; - const FVector& Mouth = ControlPoints[ControlIndex]; + ? 0 : ControlPointCount - 1; + const FVector& Mouth = ControlPointData[ControlIndex]; const float DX = static_cast(Position.X - Mouth.X); const float DY = static_cast(Position.Y - Mouth.Y); const float Distance = FMath::Sqrt(DX * DX + DY * DY); @@ -752,7 +756,7 @@ namespace continue; } const float DefaultFloorZ = static_cast(Mouth.Z) - - FMath::Abs(ControlRadii[ControlIndex]); + - FMath::Abs(ControlRadiusData[ControlIndex]); const float StoredFloorZ = Endpoint == 0 ? MouthFloorZA : MouthFloorZB; const float FloorZ = StoredFloorZ > -FLT_MAX && FMath::IsFinite(StoredFloorZ) @@ -815,26 +819,23 @@ namespace const FVector& B, float RadiusB, float T, int32 SegmentIndex, int32 NumSegments, const FCachedTunnel& Tunnel, - const TArray* FloorProfiles, + const FTunnelFloorSegmentProfile* FloorProfile, const FVFRoomFloorOwnership* RoomFloorOwnership) { FVFTunnelFloorTerms Terms; - if (FloorProfiles != nullptr - && FloorProfiles->Num() == NumSegments - && FloorProfiles->IsValidIndex(SegmentIndex)) + if (FloorProfile != nullptr) { - const FTunnelFloorSegmentProfile& Profile = (*FloorProfiles)[SegmentIndex]; const float ClampedT = FMath::Clamp(T, 0.0f, 1.0f); - const float ProfileT = Profile.NumSteps > 0 + const float ProfileT = FloorProfile->NumSteps > 0 ? (ClampedT >= 1.0f ? 1.0f : FMath::FloorToFloat(ClampedT - * static_cast(Profile.NumSteps)) - / static_cast(Profile.NumSteps)) + * static_cast(FloorProfile->NumSteps)) + / static_cast(FloorProfile->NumSteps)) : ClampedT; Terms.BaseFloorZ = FMath::Lerp( - Profile.StartFloorZ, Profile.EndFloorZ, ProfileT); - Terms.ReliefScale = Profile.ReliefScale; + FloorProfile->StartFloorZ, FloorProfile->EndFloorZ, ProfileT); + Terms.ReliefScale = FloorProfile->ReliefScale; } else { @@ -878,8 +879,10 @@ namespace RoomFloorOwnership->Weight, 0.0f, 1.0f); } - const float HorizontalRun = FVector2D( - static_cast(B.X - A.X), static_cast(B.Y - A.Y)).Size(); + const double DeltaX = static_cast(static_cast(B.X - A.X)); + const double DeltaY = static_cast(static_cast(B.Y - A.Y)); + const float HorizontalRun = static_cast(FMath::Sqrt( + DeltaX * DeltaX + DeltaY * DeltaY)); Terms.ReliefEnvelope = 1.0f; if (SegmentIndex == 0) { @@ -926,12 +929,12 @@ namespace const void* CacheIdentity, int32 TunnelIndex, bool bEvaluateRelief, const FVFFloorReliefColumnKey& Column, - const TArray* FloorProfiles, + const FTunnelFloorSegmentProfile* FloorProfile, const FVFRoomFloorOwnership* RoomFloorOwnership) { const FVFTunnelFloorTerms Terms = VF_TunnelFloorTerms( A, RadiusA, B, RadiusB, - T, SegmentIndex, NumSegments, Tunnel, FloorProfiles, + T, SegmentIndex, NumSegments, Tunnel, FloorProfile, RoomFloorOwnership); return VF_TunnelFloorFromTerms( Position, Tunnel, Terms, @@ -942,10 +945,12 @@ namespace const FVector& Position, const FVector& A, const FVector& B, float& OutT, float& OutDistanceSquared) { - const FVector2D AXY(static_cast(A.X), static_cast(A.Y)); - const FVector2D Delta( - static_cast(B.X - A.X), static_cast(B.Y - A.Y)); - const float LengthSquared = Delta.SizeSquared(); + const double AXY_X = static_cast(static_cast(A.X)); + const double AXY_Y = static_cast(static_cast(A.Y)); + const double DeltaX = static_cast(static_cast(B.X - A.X)); + const double DeltaY = static_cast(static_cast(B.Y - A.Y)); + const float LengthSquared = static_cast( + DeltaX * DeltaX + DeltaY * DeltaY); if (LengthSquared <= KINDA_SMALL_NUMBER) { OutT = 0.0f; @@ -953,13 +958,17 @@ namespace return false; } - const FVector2D QueryXY( - static_cast(Position.X), static_cast(Position.Y)); - OutT = FMath::Clamp( - FVector2D::DotProduct(QueryXY - AXY, Delta) / LengthSquared, - 0.0f, 1.0f); - const FVector2D ClosestXY = AXY + Delta * OutT; - OutDistanceSquared = (QueryXY - ClosestXY).SizeSquared(); + const double QueryX = static_cast(static_cast(Position.X)); + const double QueryY = static_cast(static_cast(Position.Y)); + const double Dot = (QueryX - AXY_X) * DeltaX + + (QueryY - AXY_Y) * DeltaY; + OutT = FMath::Clamp(Dot / LengthSquared, 0.0f, 1.0f); + const double ClosestX = AXY_X + DeltaX * OutT; + const double ClosestY = AXY_Y + DeltaY * OutT; + const double DistanceX = QueryX - ClosestX; + const double DistanceY = QueryY - ClosestY; + OutDistanceSquared = static_cast( + DistanceX * DistanceX + DistanceY * DistanceY); return true; } @@ -976,22 +985,30 @@ namespace const FVFRoomFloorOwnership* RoomFloorOwnership) { FVFTunnelShapeEvaluation Result; - if (ControlPoints.Num() < 2 - || ControlRadii.Num() != ControlPoints.Num()) + const int32 ControlPointCount = ControlPoints.Num(); + if (ControlPointCount < 2 + || ControlRadii.Num() != ControlPointCount) { return Result; } - const int32 NumSegments = ControlPoints.Num() - 1; + const int32 NumSegments = ControlPointCount - 1; + const FVector* ControlPointData = ControlPoints.GetData(); + const float* ControlRadiusData = ControlRadii.GetData(); + const FTunnelFloorSegmentProfile* FloorProfileData = nullptr; + if (FloorProfiles != nullptr && FloorProfiles->Num() == NumSegments) + { + FloorProfileData = FloorProfiles->GetData(); + } const float FloorBlend = FMath::Max(SDFBlendRadius, 0.0f) * 0.35f; for (int32 SegmentIndex = 0; SegmentIndex < NumSegments; ++SegmentIndex) { - const FVector& A = ControlPoints[SegmentIndex]; - const FVector& B = ControlPoints[SegmentIndex + 1]; - const float RadiusA = ControlRadii[SegmentIndex]; - const float RadiusB = ControlRadii[SegmentIndex + 1]; + const FVector& A = ControlPointData[SegmentIndex]; + const FVector& B = ControlPointData[SegmentIndex + 1]; + const float RadiusA = ControlRadiusData[SegmentIndex]; + const float RadiusB = ControlRadiusData[SegmentIndex + 1]; float T = 0.0f; float HorizontalDistanceSquared = FLT_MAX; @@ -1007,17 +1024,12 @@ namespace continue; } + const FTunnelFloorSegmentProfile* Profile = FloorProfileData != nullptr + ? FloorProfileData + SegmentIndex : nullptr; const FVFTunnelFloorTerms FloorTerms = VF_TunnelFloorTerms( A, RadiusA, B, RadiusB, - T, SegmentIndex, NumSegments, Tunnel, FloorProfiles, + T, SegmentIndex, NumSegments, Tunnel, Profile, RoomFloorOwnership); - const FTunnelFloorSegmentProfile* Profile = nullptr; - if (FloorProfiles != nullptr - && FloorProfiles->Num() == NumSegments - && FloorProfiles->IsValidIndex(SegmentIndex)) - { - Profile = &(*FloorProfiles)[SegmentIndex]; - } const float NaturalFloorZ = Profile != nullptr ? FMath::Lerp( Profile->NaturalStartFloorZ, @@ -1220,7 +1232,14 @@ namespace return false; } - const int32 NumSegments = ControlPoints.Num() - 1; + const int32 ControlPointCount = ControlPoints.Num(); + const int32 NumSegments = ControlPointCount - 1; + const FVector* ControlPointData = ControlPoints.GetData(); + const float* ControlRadiusData = ControlRadii.GetData(); + const TArray* FloorProfiles = bWorldChain + ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles; + const FTunnelFloorSegmentProfile* FloorProfileData = + FloorProfiles->Num() == NumSegments ? FloorProfiles->GetData() : nullptr; float BestDistanceSquared = FLT_MAX; bool bFound = false; for (int32 SegmentIndex = 0; @@ -1231,22 +1250,22 @@ namespace float DistanceSquared = FLT_MAX; if (!VF_ProjectTunnelSegmentXY( Position, - ControlPoints[SegmentIndex], ControlPoints[SegmentIndex + 1], + ControlPointData[SegmentIndex], ControlPointData[SegmentIndex + 1], T, DistanceSquared) || DistanceSquared >= BestDistanceSquared) { continue; } - const float RadiusA = ControlRadii[SegmentIndex]; - const float RadiusB = ControlRadii[SegmentIndex + 1]; + const float RadiusA = ControlRadiusData[SegmentIndex]; + const float RadiusB = ControlRadiusData[SegmentIndex + 1]; OutFloorZ = VF_TunnelFloorAtSegment( - ControlPoints[SegmentIndex], RadiusA, - ControlPoints[SegmentIndex + 1], RadiusB, + ControlPointData[SegmentIndex], RadiusA, + ControlPointData[SegmentIndex + 1], RadiusB, T, SegmentIndex, NumSegments, Position, Tunnel, CacheIdentity, TunnelIndex, /*bEvaluateRelief=*/true, ReliefColumn, - bWorldChain ? &Tunnel.WorldFloorProfiles : &Tunnel.FloorProfiles, + FloorProfileData != nullptr ? FloorProfileData + SegmentIndex : nullptr, RoomFloorOwnership); OutSupportRadius = FMath::Max( FMath::Min(FMath::Abs(RadiusA), FMath::Abs(RadiusB)) - 0.5f, @@ -1331,8 +1350,9 @@ bool FChunkSDFSpatialIndex::GetRange( return false; } - OutBegin = CellOffsets[static_cast(Slot)]; - OutEnd = CellOffsets[static_cast(Slot + 1)]; + const int32* CellOffsetData = CellOffsets.GetData(); + OutBegin = CellOffsetData[static_cast(Slot)]; + OutEnd = CellOffsetData[static_cast(Slot + 1)]; return OutBegin >= 0 && OutEnd >= OutBegin && OutEnd <= ItemIndices.Num(); } @@ -1546,9 +1566,10 @@ namespace int32 CandidateCount = 0; if (bUseSpatialIndex && Index.GetRange(WorldX, WorldY, Begin, End)) { + const int32* ItemIndexData = Index.ItemIndices.GetData(); for (int32 Cursor = Begin; Cursor < End; ++Cursor) { - const int32 ItemIndex = Index.ItemIndices[Cursor]; + const int32 ItemIndex = ItemIndexData[Cursor]; if (ItemIndex >= 0 && ItemIndex < ItemCount) { ++CandidateCount; @@ -7354,6 +7375,9 @@ float VoxelCaveMorphology::EvaluateSDFCached( const FVector Pos(WorldX, WorldY, WorldZ); const FVFFloorReliefColumnKey ReliefColumn = VF_MakeFloorReliefColumnKey(Pos.X, Pos.Y); + const FCachedRoom* RoomData = Cache.Rooms.GetData(); + const FCachedRoomFloorJoin* RoomFloorJoinData = Cache.RoomFloorJoins.GetData(); + const FCachedTunnel* TunnelData = Cache.Tunnels.GetData(); // Track which room contributes the smallest (most-inside) raw SDF. // This is used by the terrain ops system to find the "owning" room for @@ -7367,7 +7391,7 @@ float VoxelCaveMorphology::EvaluateSDFCached( //========================================================================= auto EvaluateRoom = [&](int32 RoomIdx) { - const FCachedRoom& Room = Cache.Rooms[RoomIdx]; + const FCachedRoom& Room = RoomData[RoomIdx]; // --- DISTANCE CULL --- const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Room.Center); @@ -7476,7 +7500,7 @@ float VoxelCaveMorphology::EvaluateSDFCached( // not participate in nearest-room terrain-op ownership. auto EvaluateJoin = [&](int32 JoinIdx) { - const FCachedRoomFloorJoin& Join = Cache.RoomFloorJoins[JoinIdx]; + const FCachedRoomFloorJoin& Join = RoomFloorJoinData[JoinIdx]; const float DistSq = VF_FloatDistSquared(WorldX, WorldY, WorldZ, Join.BoundCenter); if (DistSq > Join.BoundRadiusSq) return; @@ -7497,7 +7521,7 @@ float VoxelCaveMorphology::EvaluateSDFCached( //========================================================================= auto EvaluateTunnel = [&](int32 TunnelIdx) { - const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; + const FCachedTunnel& Tunnel = TunnelData[TunnelIdx]; if (Tunnel.SDFInfluenceRadius > 0.0f && VF_DistanceSquaredToAabb( WorldX, WorldY, WorldZ, @@ -7555,10 +7579,11 @@ float VoxelCaveMorphology::EvaluateTunnelCoreSDF( { const FVector Pos(WorldX, WorldY, WorldZ); float MinSDF = FLT_MAX; + const FCachedTunnel* TunnelData = Cache.Tunnels.GetData(); auto EvaluateTunnel = [&](int32 TunnelIdx) { - const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; + const FCachedTunnel& Tunnel = TunnelData[TunnelIdx]; if (Tunnel.SDFInfluenceRadius > 0.0f && VF_DistanceSquaredToAabb( WorldX, WorldY, WorldZ, @@ -7597,6 +7622,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( VoxelDensityProfile::EBucket::TunnelCoreWorld); const FVector Pos(WorldX, WorldY, WorldZ); FTunnelCoreWorldEvaluation Result; + const FCachedTunnel* TunnelData = Cache.Tunnels.GetData(); if (SupportColumn != nullptr && VoxelDensityProfile::AreCountersEnabled()) { @@ -7605,7 +7631,7 @@ FTunnelCoreWorldEvaluation VoxelCaveMorphology::EvaluateTunnelCoreWorld( } auto EvaluateTunnel = [&](int32 TunnelIdx) { - const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; + const FCachedTunnel& Tunnel = TunnelData[TunnelIdx]; const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2 && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num(); const FVector& BoundCenter = bHasWorldChain @@ -7731,10 +7757,11 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( bool bUseSpatialIndex) { const FVector Pos(WorldX, WorldY, WorldZ); + const FCachedTunnel* TunnelData = Cache.Tunnels.GetData(); auto TestTunnel = [&](int32 TunnelIdx) -> bool { - const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; + const FCachedTunnel& Tunnel = TunnelData[TunnelIdx]; const bool bHasWorldChain = Tunnel.WorldControlPoints.Num() >= 2 && Tunnel.WorldControlRadii.Num() == Tunnel.WorldControlPoints.Num(); const FVector& CenterlineMin = bHasWorldChain @@ -7789,9 +7816,10 @@ bool VoxelCaveMorphology::IsTunnelSupportFloorWorldPoint( int32 CandidateCount = 0; auto TestRange = [&](int32 Begin, int32 End) { + const int32* ItemIndexData = Cache.TunnelSpatialIndex.ItemIndices.GetData(); for (int32 Cursor = Begin; Cursor < End; ++Cursor) { - const int32 TunnelIdx = Cache.TunnelSpatialIndex.ItemIndices[Cursor]; + const int32 TunnelIdx = ItemIndexData[Cursor]; if (!Cache.Tunnels.IsValidIndex(TunnelIdx)) { continue; @@ -7858,10 +7886,11 @@ void VoxelCaveMorphology::BuildTunnelSupportFloorColumn( } const FVector ColumnPosition(WorldX, WorldY, 0.0f); + const FCachedTunnel* TunnelData = Cache.Tunnels.GetData(); auto EvaluateTunnel = [&](int32 TunnelIdx) { - const FCachedTunnel& Tunnel = Cache.Tunnels[TunnelIdx]; + const FCachedTunnel& Tunnel = TunnelData[TunnelIdx]; const FVFRoomFloorOwnership RoomMouthOwnership = VF_FindWorldTunnelMouthOwnership(ColumnPosition, Tunnel, Cache.SDFBlendRadius); float FloorZ = 0.0f; diff --git a/Source/VoxelForge/Private/VoxelDensityProfile.cpp b/Source/VoxelForge/Private/VoxelDensityProfile.cpp index d50b3f5..c93217b 100644 --- a/Source/VoxelForge/Private/VoxelDensityProfile.cpp +++ b/Source/VoxelForge/Private/VoxelDensityProfile.cpp @@ -858,7 +858,7 @@ namespace VoxelDensityProfile FScopeToken BeginScope(EBucket Bucket) { - FScopeToken Token; + FScopeToken Token{}; if (!IsEnabledFast()) { return Token; @@ -972,15 +972,4 @@ namespace VoxelDensityProfile GSampledScopeActive = Token.bPreviousSampledScopeActive; } - FScopedTimer::~FScopedTimer() - { - End(); - } - - void FScopedTimer::End() - { - if (!Token.bEntered) { return; } - EndScope(Bucket, Token); - Token.bEntered = false; - } } diff --git a/Source/VoxelForge/Private/VoxelGenerator.cpp b/Source/VoxelForge/Private/VoxelGenerator.cpp index f2ca025..97c49b4 100644 --- a/Source/VoxelForge/Private/VoxelGenerator.cpp +++ b/Source/VoxelForge/Private/VoxelGenerator.cpp @@ -206,8 +206,12 @@ private: // uphill). See SurfaceDensityFromColumn for the warped-terrain union that makes the shelf. struct FSurfaceColumn { - float TerrainZ = 0.0f; float CeilSurf = 0.0f; - float OverhangAmp = 0.0f; float DirX = 0.0f; float DirY = 0.0f; + // Toutes les sorties sont écrites par ComputeSurfaceColumn avant Computed=true ; les valeurs + // par défaut ne sont donc jamais lues et leur constructeur implicite coûte dans le TLS. + // Every output is written by ComputeSurfaceColumn before Computed=true; the defaults are never + // read, and removing them keeps FSurfaceColumn trivial so TLS construction has no leaf work. + float TerrainZ, CeilSurf; + float OverhangAmp, DirX, DirY; }; struct FSurfaceColumnBox @@ -2355,7 +2359,6 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co Result = GetMazeDensity(WorldX, WorldY, WorldZ, CP_Maze); break; case ECaveGeneratorType::SurfaceWorld: { - FSurfaceColumn Col; // Integer XY (the density grid) → reuse the column down its whole Z extent (T1.a). // Fractional XY (gradient-normal samples) → compute directly (no cache key). if (WorldX == FMath::FloorToFloat(WorldX) && WorldY == FMath::FloorToFloat(WorldY)) @@ -2373,17 +2376,20 @@ float UVoxelGenerator::GetDensityAt(float WorldX, float WorldY, float WorldZ) co Box.Cols[CI].OverhangAmp, Box.Cols[CI].DirX, Box.Cols[CI].DirY); Box.Computed[CI] = true; } - Col = Box.Cols[CI]; + const FSurfaceColumn& Col = Box.Cols[CI]; + Result = SurfaceDensityFromColumn(WorldX, WorldY, WorldZ, + Col.TerrainZ, Col.CeilSurf, Col.OverhangAmp, + Col.DirX, Col.DirY, CP_Surface); } else { + float TerrainZ, CeilSurf, OverhangAmp, DirX, DirY; ComputeSurfaceColumn(WorldX, WorldY, ChunkCoord.Z, CP_Surface, CP_BiomeCtx, - CP_SurfaceBiomeParams, CP_BiomeCache, Col.TerrainZ, Col.CeilSurf, - Col.OverhangAmp, Col.DirX, Col.DirY); + CP_SurfaceBiomeParams, CP_BiomeCache, TerrainZ, CeilSurf, + OverhangAmp, DirX, DirY); + Result = SurfaceDensityFromColumn(WorldX, WorldY, WorldZ, + TerrainZ, CeilSurf, OverhangAmp, DirX, DirY, CP_Surface); } - - Result = SurfaceDensityFromColumn(WorldX, WorldY, WorldZ, - Col.TerrainZ, Col.CeilSurf, Col.OverhangAmp, Col.DirX, Col.DirY, CP_Surface); break; } case ECaveGeneratorType::VerticalShafts: diff --git a/Source/VoxelForge/Private/VoxelStrateManager.cpp b/Source/VoxelForge/Private/VoxelStrateManager.cpp index e998bce..ca72f59 100644 --- a/Source/VoxelForge/Private/VoxelStrateManager.cpp +++ b/Source/VoxelForge/Private/VoxelStrateManager.cpp @@ -12,6 +12,7 @@ #include "VoxelStartupTrace.h" #include "VoxelTerrainOpDefinition.h" // For UVoxelTerrainOpDefinition::ApplyTo #include "VoxelBiomeDefinition.h" // For UVoxelBiomeDefinition (biome context flatten) +#include "HAL/IConsoleManager.h" #include "Misc/CommandLine.h" #include "Misc/Crc.h" #include "Misc/Parse.h" @@ -23,6 +24,15 @@ namespace { std::atomic GNextStrateManagerLifetimeId { 0 }; + // Exact A/B switch for the repeated passage-floor projection item. The other field- + // preserving rewrites in this round are deliberately unconditional; this switch isolates + // the projection reuse without changing any density arithmetic. + int32 GVoxelForgeFloorRound1PassageProjectionCache = 1; + FAutoConsoleVariableRef CVarVoxelForgeFloorRound1PassageProjectionCache( + TEXT("voxel.FloorRound1PassageProjectionCache"), + GVoxelForgeFloorRound1PassageProjectionCache, + TEXT("Reuse exact passage floor projections within one density sample; 0 disables the A/B cache.")); + struct FRuntimeRoughnessOverrides { bool bSurfaceRoughness = false; @@ -185,6 +195,43 @@ static float PassageFBM(float X, float Seed) namespace { + // One projection result per passage is enough for the current voxel sample. The surrounding + // passage cache already moves with (manager, layout, chunk); a generation invalidates old flags + // before any caller can observe them. Keeping the entries indexed by passage avoids the TMap + // rehash path that this hot loop used to carry, while a collision-free hit remains exact. + struct FPassageFloorProjectionCacheEntry + { + // The sample key lives once in FPassageEvaluationCache. A monotonically increasing + // generation makes an entry's old flags inert without clearing its float payload. + // The payload is only read after the corresponding valid flag is set. + uint64 SampleGeneration = 0; + uint8 ComputedFlags = 0; + bool bNativeValid = false; + float NativeFloorZ = 0.0f; + float NativeSupportRadius = 0.0f; + bool bGenericValid = false; + float GenericFloorZ = 0.0f; + float GenericSupportRadius = 0.0f; + bool bWalkableAir = false; + + enum : uint8 + { + NativeComputed = 1u << 0, + GenericComputed = 1u << 1, + WalkableAirComputed = 1u << 2 + }; + + FORCEINLINE bool Has(uint8 Flag) const + { + return (ComputedFlags & Flag) != 0; + } + + FORCEINLINE void SetComputed(uint8 Flag) + { + ComputedFlags |= Flag; + } + }; + /** * One shared passage cache for the tube SDF and landing-floor fill. * @@ -199,6 +246,12 @@ namespace FIntVector Chunk = FIntVector(INT32_MAX, INT32_MAX, INT32_MAX); uint32 Version = 0xFFFFFFFFu; TArray Nearby; + TArray FloorProjections; + bool bFloorProjectionSampleValid = false; + double FloorProjectionX = 0.0; + double FloorProjectionY = 0.0; + double FloorProjectionZ = 0.0; + uint64 FloorProjectionGeneration = 0; }; FPassageEvaluationCache& VF_GetPassageEvaluationCache() @@ -225,6 +278,8 @@ namespace Cache.Chunk = ChunkCoord; Cache.Version = Version; Cache.Nearby.Reset(); + Cache.FloorProjections.Reset(); + Cache.bFloorProjectionSampleValid = false; if (!Manager) return Cache.Nearby; const FVector ChunkCenter( @@ -233,6 +288,7 @@ namespace (ChunkCoord.Z + 0.5f) * (float)CHUNK_SIZE); const float ChunkRadius = (float)CHUNK_SIZE * 0.8660254f + 3.0f; const TArray& Passages = Manager->GetPassages(); + Cache.FloorProjections.SetNum(Passages.Num()); for (int32 PassageIndex = 0; PassageIndex < Passages.Num(); ++PassageIndex) { const FVoxelPassage& Passage = Passages[PassageIndex]; @@ -245,6 +301,59 @@ namespace return Cache.Nearby; } + FPassageFloorProjectionCacheEntry* VF_GetPassageFloorProjectionCacheEntry( + const UVoxelStrateManager* Manager, + const FVector& Position, + int32 PassageIndex) + { + if (GVoxelForgeFloorRound1PassageProjectionCache == 0 + || Manager == nullptr || PassageIndex < 0) + { + return nullptr; + } + + FPassageEvaluationCache& Cache = VF_GetPassageEvaluationCache(); + if (Cache.Owner != Manager + || Cache.OwnerLifetimeId != Manager->GetCacheLifetimeId() + || Cache.Version != Manager->GetLayoutVersion() + || PassageIndex >= Cache.FloorProjections.Num()) + { + return nullptr; + } + + if (!Cache.bFloorProjectionSampleValid + || Cache.FloorProjectionX != Position.X + || Cache.FloorProjectionY != Position.Y + || Cache.FloorProjectionZ != Position.Z) + { + Cache.bFloorProjectionSampleValid = true; + Cache.FloorProjectionX = Position.X; + Cache.FloorProjectionY = Position.Y; + Cache.FloorProjectionZ = Position.Z; + ++Cache.FloorProjectionGeneration; + if (Cache.FloorProjectionGeneration == 0) + { + // Keep generation zero reserved for never-initialized entries. This is + // unreachable in a practical worker lifetime, but preserves the invariant if a + // process survives long enough to wrap the counter. + for (FPassageFloorProjectionCacheEntry& Candidate : Cache.FloorProjections) + { + Candidate.SampleGeneration = 0; + } + ++Cache.FloorProjectionGeneration; + } + } + + FPassageFloorProjectionCacheEntry* Entry = + Cache.FloorProjections.GetData() + PassageIndex; + if (Entry->SampleGeneration != Cache.FloorProjectionGeneration) + { + Entry->SampleGeneration = Cache.FloorProjectionGeneration; + Entry->ComputedFlags = 0; + } + return Entry; + } + // Passage relief is a function of the immutable passage descriptor and the actual XY column. // Keep the cache fixed-size and thread-local, like the morphology floor cache: a long export // cannot retain one entry per tile, and collisions only lose a hit. Fractional normal @@ -425,49 +534,58 @@ namespace 0.0f, VF_PassageMaxReliefScale); } - static bool VF_ProjectNativePassageFloor( + static bool VF_ProjectNativePassageFloorUncached( const FVoxelPassage& Passage, const FVector& Position, float& OutFloorZ, float& OutSupportRadius, const UVoxelStrateManager* Manager = nullptr, int32 PassageIndex = INDEX_NONE) { + const int32 ControlPointCount = Passage.ControlPoints.Num(); if (!Passage.bNativeFloorEnabled - || Passage.ControlPoints.Num() < 2 - || Passage.ControlRadii.Num() != Passage.ControlPoints.Num() - || Passage.NativeFloorProfileZ.Num() != Passage.ControlPoints.Num()) + || ControlPointCount < 2 + || Passage.ControlRadii.Num() != ControlPointCount + || Passage.NativeFloorProfileZ.Num() != ControlPointCount) { return false; } + const FVector* ControlPointData = Passage.ControlPoints.GetData(); + const float* ControlRadiusData = Passage.ControlRadii.GetData(); + const float* NativeFloorProfileData = Passage.NativeFloorProfileZ.GetData(); + const double QueryX = static_cast(static_cast(Position.X)); + const double QueryY = static_cast(static_cast(Position.Y)); float BestDistanceSquared = FLT_MAX; int32 BestSegment = INDEX_NONE; float BestT = 0.0f; float BestSupportRadius = 0.0f; for (int32 SegmentIndex = 0; - SegmentIndex + 1 < Passage.ControlPoints.Num(); + SegmentIndex + 1 < ControlPointCount; ++SegmentIndex) { - const FVector& A = Passage.ControlPoints[SegmentIndex]; - const FVector& B = Passage.ControlPoints[SegmentIndex + 1]; - const FVector2D Delta( - static_cast(B.X - A.X), - static_cast(B.Y - A.Y)); - const float LengthSquared = Delta.SizeSquared(); + const FVector& A = ControlPointData[SegmentIndex]; + const FVector& B = ControlPointData[SegmentIndex + 1]; + const double AXY_X = static_cast(static_cast(A.X)); + const double AXY_Y = static_cast(static_cast(A.Y)); + const double DeltaX = static_cast(static_cast(B.X - A.X)); + const double DeltaY = static_cast(static_cast(B.Y - A.Y)); + const float LengthSquared = static_cast( + DeltaX * DeltaX + DeltaY * DeltaY); if (!(LengthSquared > KINDA_SMALL_NUMBER)) { continue; } - const FVector2D QueryXY( - static_cast(Position.X), - static_cast(Position.Y)); - const FVector2D AXY( - static_cast(A.X), static_cast(A.Y)); + const double Dot = (QueryX - AXY_X) * DeltaX + + (QueryY - AXY_Y) * DeltaY; const float T = FMath::Clamp( - FVector2D::DotProduct(QueryXY - AXY, Delta) / LengthSquared, + Dot / LengthSquared, 0.0f, 1.0f); - const FVector2D ClosestXY = AXY + Delta * T; - const float DistanceSquared = (QueryXY - ClosestXY).SizeSquared(); + const double ClosestX = AXY_X + DeltaX * T; + const double ClosestY = AXY_Y + DeltaY * T; + const double DistanceX = QueryX - ClosestX; + const double DistanceY = QueryY - ClosestY; + const float DistanceSquared = static_cast( + DistanceX * DistanceX + DistanceY * DistanceY); if (DistanceSquared >= BestDistanceSquared) { continue; @@ -478,8 +596,8 @@ namespace BestT = T; BestSupportRadius = FMath::Max( FMath::Min( - FMath::Abs(Passage.ControlRadii[SegmentIndex]), - FMath::Abs(Passage.ControlRadii[SegmentIndex + 1])) - 0.5f, + FMath::Abs(ControlRadiusData[SegmentIndex]), + FMath::Abs(ControlRadiusData[SegmentIndex + 1])) - 0.5f, VoxelPassageGeometry::PlayerRadiusVoxels); } @@ -491,21 +609,25 @@ namespace const float ClampedT = FMath::Clamp(BestT, 0.0f, 1.0f); OutFloorZ = FMath::Lerp( - Passage.NativeFloorProfileZ[BestSegment], - Passage.NativeFloorProfileZ[BestSegment + 1], + NativeFloorProfileData[BestSegment], + NativeFloorProfileData[BestSegment + 1], ClampedT); float ReliefScale = 0.0f; - if (Passage.NativeFloorReliefScales.IsValidIndex(BestSegment)) + const int32 ReliefScaleCount = Passage.NativeFloorReliefScales.Num(); + if (BestSegment >= 0 && BestSegment < ReliefScaleCount) { - ReliefScale = Passage.NativeFloorReliefScales[BestSegment]; + ReliefScale = Passage.NativeFloorReliefScales.GetData()[BestSegment]; } float Envelope = 1.0f; - const float HorizontalRun = FVector2D( - static_cast(Passage.ControlPoints[BestSegment + 1].X - - Passage.ControlPoints[BestSegment].X), - static_cast(Passage.ControlPoints[BestSegment + 1].Y - - Passage.ControlPoints[BestSegment].Y)).Size(); + const FVector& BestA = ControlPointData[BestSegment]; + const FVector& BestB = ControlPointData[BestSegment + 1]; + const double BestDeltaX = static_cast(static_cast( + BestB.X - BestA.X)); + const double BestDeltaY = static_cast(static_cast( + BestB.Y - BestA.Y)); + const float HorizontalRun = static_cast(FMath::Sqrt( + BestDeltaX * BestDeltaX + BestDeltaY * BestDeltaY)); if (BestSegment == 0) { Envelope *= SmoothStep01(FMath::Clamp( @@ -513,7 +635,7 @@ namespace / VoxelPassageGeometry::WalkableTunnelLandingApronVoxels, 0.0f, 1.0f)); } - if (BestSegment + 1 == Passage.ControlPoints.Num() - 1) + if (BestSegment + 1 == ControlPointCount - 1) { Envelope *= SmoothStep01(FMath::Clamp( ((1.0f - ClampedT) * HorizontalRun) @@ -531,25 +653,82 @@ namespace && OutSupportRadius > 0.0f; } + static bool VF_ProjectNativePassageFloor( + const FVoxelPassage& Passage, const FVector& Position, + float& OutFloorZ, float& OutSupportRadius, + const UVoxelStrateManager* Manager = nullptr, + int32 PassageIndex = INDEX_NONE) + { + FPassageFloorProjectionCacheEntry* Entry = + VF_GetPassageFloorProjectionCacheEntry(Manager, Position, PassageIndex); + if (Entry == nullptr) + { + return VF_ProjectNativePassageFloorUncached( + Passage, Position, OutFloorZ, OutSupportRadius, + Manager, PassageIndex); + } + if (!Entry->Has(FPassageFloorProjectionCacheEntry::NativeComputed)) + { + Entry->bNativeValid = VF_ProjectNativePassageFloorUncached( + Passage, Position, Entry->NativeFloorZ, + Entry->NativeSupportRadius, Manager, PassageIndex); + Entry->SetComputed(FPassageFloorProjectionCacheEntry::NativeComputed); + } + if (Entry->bNativeValid) + { + OutFloorZ = Entry->NativeFloorZ; + OutSupportRadius = Entry->NativeSupportRadius; + } + return Entry->bNativeValid; + } + static bool VF_ProjectPassageFloor( const FVoxelPassage& Passage, const FVector& Position, float& OutFloorZ, float& OutSupportRadius, const UVoxelStrateManager* Manager = nullptr, int32 PassageIndex = INDEX_NONE) { - if (VF_ProjectNativePassageFloor( - Passage, Position, OutFloorZ, OutSupportRadius, - Manager, PassageIndex)) + FPassageFloorProjectionCacheEntry* Entry = + VF_GetPassageFloorProjectionCacheEntry(Manager, Position, PassageIndex); + if (Entry != nullptr + && Entry->Has(FPassageFloorProjectionCacheEntry::GenericComputed)) { - return true; + if (Entry->bGenericValid) + { + OutFloorZ = Entry->GenericFloorZ; + OutSupportRadius = Entry->GenericSupportRadius; + } + return Entry->bGenericValid; } - if (!Passage.bWalkableTunnelContract) + + float FloorZ = 0.0f; + float SupportRadius = 0.0f; + bool bValid = VF_ProjectNativePassageFloor( + Passage, Position, FloorZ, SupportRadius, + Manager, PassageIndex); + if (!bValid && Passage.bWalkableTunnelContract) { - return false; + bValid = VoxelPassageGeometry::ProjectWalkableTunnelFloor( + Passage.ControlPoints, Passage.ControlRadii, Position, + FloorZ, SupportRadius); } - return VoxelPassageGeometry::ProjectWalkableTunnelFloor( - Passage.ControlPoints, Passage.ControlRadii, Position, - OutFloorZ, OutSupportRadius); + + if (Entry != nullptr) + { + Entry->bGenericValid = bValid; + if (bValid) + { + Entry->GenericFloorZ = FloorZ; + Entry->GenericSupportRadius = SupportRadius; + } + Entry->SetComputed(FPassageFloorProjectionCacheEntry::GenericComputed); + } + if (bValid) + { + OutFloorZ = FloorZ; + OutSupportRadius = SupportRadius; + } + return bValid; } } @@ -700,7 +879,9 @@ static bool VF_IsWalkableTunnelAir( const FVoxelPassage& Passage, const FVector& Position, const UVoxelStrateManager* Manager = nullptr, - int32 PassageIndex = INDEX_NONE) + int32 PassageIndex = INDEX_NONE, + float* OutFloorZ = nullptr, + float* OutSupportRadius = nullptr) { if (!Passage.bWalkableTunnelContract || Passage.ControlPoints.Num() < 2 @@ -709,6 +890,25 @@ static bool VF_IsWalkableTunnelAir( return false; } + FPassageFloorProjectionCacheEntry* Entry = + VF_GetPassageFloorProjectionCacheEntry(Manager, Position, PassageIndex); + if (Entry != nullptr + && Entry->Has(FPassageFloorProjectionCacheEntry::WalkableAirComputed)) + { + if (Entry->bWalkableAir) + { + if (OutFloorZ != nullptr) + { + *OutFloorZ = Entry->GenericFloorZ; + } + if (OutSupportRadius != nullptr) + { + *OutSupportRadius = Entry->GenericSupportRadius; + } + } + return Entry->bWalkableAir; + } + float FloorZ = 0.0f; float SupportRadius = 0.0f; if (!VF_ProjectPassageFloor( @@ -722,9 +922,26 @@ static bool VF_IsWalkableTunnelAir( // stable air volume even when a source archetype or a disturbance would otherwise refill the // shallow part of the rounded SDF. The floor writer below owns the closed lower face. const float AirHeight = 2.0f * SupportRadius; - return Position.Z > FloorZ + const bool bWalkableAir = Position.Z > FloorZ + VoxelPassageGeometry::WalkableTunnelFloorAirClearanceVoxels && Position.Z < FloorZ + AirHeight - KINDA_SMALL_NUMBER; + if (Entry != nullptr) + { + Entry->bWalkableAir = bWalkableAir; + Entry->SetComputed(FPassageFloorProjectionCacheEntry::WalkableAirComputed); + } + if (bWalkableAir) + { + if (OutFloorZ != nullptr) + { + *OutFloorZ = FloorZ; + } + if (OutSupportRadius != nullptr) + { + *OutSupportRadius = SupportRadius; + } + } + return bWalkableAir; } static bool VF_FindWalkableTunnelAir( @@ -748,21 +965,21 @@ static bool VF_FindWalkableTunnelAir( FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE)); const TArray& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord); const TArray& Passages = Manager->GetPassages(); + const FVoxelPassage* PassageData = Passages.GetData(); for (const int32 PassageIndex : Nearby) { if (!Passages.IsValidIndex(PassageIndex)) { continue; } - const FVoxelPassage& Passage = Passages[PassageIndex]; + const FVoxelPassage& Passage = PassageData[PassageIndex]; if (FVector::DistSquared(Position, Passage.BoundCenter) > Passage.BoundRadiusSq - || !VF_IsWalkableTunnelAir(Passage, Position, Manager, PassageIndex)) + || !VF_IsWalkableTunnelAir( + Passage, Position, Manager, PassageIndex, + &OutFloorZ, &OutSupportRadius)) { continue; } - VF_ProjectPassageFloor( - Passage, Position, OutFloorZ, OutSupportRadius, - Manager, PassageIndex); OutPassageIndex = PassageIndex; return true; } @@ -783,13 +1000,14 @@ static bool VF_IsAnyPassageFloorAt( FMath::FloorToInt(Position.Z / (float)CHUNK_SIZE)); const TArray& Nearby = VF_GetNearbyPassages(Manager, ChunkCoord); const TArray& Passages = Manager->GetPassages(); + const FVoxelPassage* PassageData = Passages.GetData(); for (const int32 PassageIndex : Nearby) { if (!Passages.IsValidIndex(PassageIndex)) { continue; } - const FVoxelPassage& Passage = Passages[PassageIndex]; + const FVoxelPassage& Passage = PassageData[PassageIndex]; if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) @@ -2242,6 +2460,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float float MinSDF = FLT_MAX; const float BlendK = 3.0f; // Smooth blend for passage junctions + const FVoxelPassage* PassageData = Passages.GetData(); //========================================================================= // PASSAGES — tapered capsule chains between strates (per-strate PassageConfig). @@ -2251,7 +2470,7 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float const FVector Pos(WorldX, WorldY, WorldZ); for (int32 PIdx : Nearby) { - const FVoxelPassage& P = Passages[PIdx]; + const FVoxelPassage& P = PassageData[PIdx]; // BOUNDING-SPHERE REJECT: skip passages this voxel can't possibly be inside. // EvaluateModifierSDF runs PER VOXEL and used to evaluate every passage's full // capsule chain unconditionally — the dominant lag source once passages became @@ -2263,19 +2482,22 @@ float UVoxelStrateManager::EvaluateModifierSDF(float WorldX, float WorldY, float VoxelDensityProfile::ECounter::PassageEvaluated); } - if (P.ControlPoints.Num() >= 2) + const int32 ControlPointCount = P.ControlPoints.Num(); + if (ControlPointCount >= 2) { // Tapered capsule chain along the control points. ControlRadii (if present) // gives the per-point width so the tunnel can flare at the mouths and pinch // in the middle; otherwise the uniform Radius is used. const bool bTaper = (P.ControlRadii.Num() == P.ControlPoints.Num()); + const FVector* ControlPointData = P.ControlPoints.GetData(); + const float* ControlRadiusData = P.ControlRadii.GetData(); float PassageSDF = FLT_MAX; - for (int32 j = 0; j < P.ControlPoints.Num() - 1; j++) + for (int32 j = 0; j < ControlPointCount - 1; j++) { - const float rA = bTaper ? P.ControlRadii[j] : P.Radius; - const float rB = bTaper ? P.ControlRadii[j + 1] : P.Radius; + const float rA = bTaper ? ControlRadiusData[j] : P.Radius; + const float rB = bTaper ? ControlRadiusData[j + 1] : P.Radius; const float SegSDF = VoxelSDF::TaperedCapsule( - Pos, P.ControlPoints[j], P.ControlPoints[j + 1], rA, rB); + Pos, ControlPointData[j], ControlPointData[j + 1], rA, rB); PassageSDF = VoxelSDF::SmoothMin(PassageSDF, SegSDF, BlendK); } @@ -2334,10 +2556,11 @@ void UVoxelStrateManager::ApplyPassageModifier( FMath::FloorToInt(WorldY / (float)CHUNK_SIZE), FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE)); const TArray& Nearby = VF_GetNearbyPassages(this, ChunkCoord); + const FVoxelPassage* PassageData = Passages.GetData(); const FVector Position(WorldX, WorldY, WorldZ); for (const int32 PassageIndex : Nearby) { - const FVoxelPassage& Passage = Passages[PassageIndex]; + const FVoxelPassage& Passage = PassageData[PassageIndex]; if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) @@ -2386,6 +2609,7 @@ void UVoxelStrateManager::ApplyPassageLandingAir( const TArray& Nearby = VF_GetNearbyPassages(this, ChunkCoord); if (Nearby.Num() == 0) return; + const FVoxelPassage* PassageData = Passages.GetData(); const FVector Position(WorldX, WorldY, WorldZ); // A smooth room blend may overlap the end of a ramp. The analytic support plane owns that // overlap; otherwise landing air can erase the tunnel's final-density floor after the tunnel @@ -2397,7 +2621,7 @@ void UVoxelStrateManager::ApplyPassageLandingAir( float MinLandingSDF = FLT_MAX; for (const int32 PassageIndex : Nearby) { - const FVoxelPassage& Passage = Passages[PassageIndex]; + const FVoxelPassage& Passage = PassageData[PassageIndex]; if (FVector::DistSquared(Position, Passage.BoundCenter) > Passage.BoundRadiusSq) { continue; @@ -2477,10 +2701,11 @@ void UVoxelStrateManager::ApplyPassageLandingFloorMC( FMath::FloorToInt(WorldY / (float)CHUNK_SIZE), FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE)); const TArray& Nearby = VF_GetNearbyPassages(this, ChunkCoord); + const FVoxelPassage* PassageData = Passages.GetData(); const FVector Position(WorldX, WorldY, WorldZ); for (const int32 PassageIndex : Nearby) { - const FVoxelPassage& Passage = Passages[PassageIndex]; + const FVoxelPassage& Passage = PassageData[PassageIndex]; if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() && (VF_IsPassageLandingFloor(Position, Passage.UpperLanding) || VF_IsPassageLandingFloor(Position, Passage.LowerLanding) @@ -2529,6 +2754,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC( const TArray& Nearby = VF_GetNearbyPassages(this, ChunkCoord); if (Nearby.Num() == 0) return; + const FVoxelPassage* PassageData = Passages.GetData(); const FVector Position(WorldX, WorldY, WorldZ); const bool bSuppressFloor = VoxelPassageGeometry::VerticalShaftConnectorAirMarker(); @@ -2544,7 +2770,7 @@ void UVoxelStrateManager::ApplyPassageStructuralPostsMC( for (const int32 PassageIndex : Nearby) { if (!Passages.IsValidIndex(PassageIndex)) continue; - const FVoxelPassage& Passage = Passages[PassageIndex]; + const FVoxelPassage& Passage = PassageData[PassageIndex]; const bool bTunnelSupportFloor = bPassageSupportFloorWritesEnabled @@ -2640,6 +2866,7 @@ void UVoxelStrateManager::ApplyPassageNativeFloorMC( return; } + const FVoxelPassage* PassageData = Passages.GetData(); const FVector Position(WorldX, WorldY, WorldZ); for (const int32 PassageIndex : Nearby) { @@ -2647,7 +2874,7 @@ void UVoxelStrateManager::ApplyPassageNativeFloorMC( { continue; } - const FVoxelPassage& Passage = Passages[PassageIndex]; + const FVoxelPassage& Passage = PassageData[PassageIndex]; if (!Passage.bNativeFloorEnabled) { continue; @@ -2689,10 +2916,11 @@ void UVoxelStrateManager::ApplyPassageLandingRoomFloorMC( FMath::FloorToInt(WorldY / (float)CHUNK_SIZE), FMath::FloorToInt(WorldZ / (float)CHUNK_SIZE)); const TArray& Nearby = VF_GetNearbyPassages(this, ChunkCoord); + const FVoxelPassage* PassageData = Passages.GetData(); const FVector Position(WorldX, WorldY, WorldZ); for (const int32 PassageIndex : Nearby) { - const FVoxelPassage& Passage = Passages[PassageIndex]; + const FVoxelPassage& Passage = PassageData[PassageIndex]; if (!VoxelPassageGeometry::VerticalShaftConnectorAirMarker() && (VF_IsPassageRoomFloor(Position, Passage.UpperLanding) || VF_IsPassageRoomFloor(Position, Passage.LowerLanding))) diff --git a/Source/VoxelForge/Public/VoxelDensityProfile.h b/Source/VoxelForge/Public/VoxelDensityProfile.h index 6156e11..36351e9 100644 --- a/Source/VoxelForge/Public/VoxelDensityProfile.h +++ b/Source/VoxelForge/Public/VoxelDensityProfile.h @@ -383,12 +383,12 @@ namespace VoxelDensityProfile struct FScopeToken { - bool bEntered = false; - bool bSampled = false; - bool bBlockTiming = false; - bool bPreviousSampled = false; - bool bPreviousSampledScopeActive = false; - uint64 StartCycles = 0; + bool bEntered; + bool bSampled; + bool bBlockTiming; + bool bPreviousSampled; + bool bPreviousSampledScopeActive; + uint64 StartCycles; }; VOXELFORGE_API FScopeToken BeginScope(EBucket Bucket); @@ -398,16 +398,16 @@ namespace VoxelDensityProfile { public: explicit FScopedTimer(EBucket InBucket) - : Bucket(InBucket) { if (IsEnabledFast() && ShouldProbeScopeFast(InBucket)) { + Bucket = InBucket; Token = BeginScope(InBucket); + bActive = Token.bEntered; } } explicit FScopedTimer(const TCHAR* InName) - : Bucket(EBucket::OtherOp) { if (IsEnabledFast()) { @@ -415,21 +415,35 @@ namespace VoxelDensityProfile if (ShouldProbeScopeFast(Bucket)) { Token = BeginScope(Bucket); + bActive = Token.bEntered; } } } - ~FScopedTimer(); + FORCEINLINE ~FScopedTimer() + { + End(); + } // End a range before its lexical scope ends. This lets GetDensityAt split its enclosing // total into disjoint prologue/core/post phases without moving the large cache block. - void End(); + FORCEINLINE void End() + { + if (!bActive) + { + return; + } + EndScope(Bucket, Token); + Token.bEntered = false; + bActive = false; + } FScopedTimer(const FScopedTimer&) = delete; FScopedTimer& operator=(const FScopedTimer&) = delete; private: - EBucket Bucket = EBucket::OtherOp; + EBucket Bucket; FScopeToken Token; + bool bActive = false; }; } diff --git a/Source/VoxelForge/Public/VoxelPassageGeometry.h b/Source/VoxelForge/Public/VoxelPassageGeometry.h index 9ae7a0b..b8460c1 100644 --- a/Source/VoxelForge/Public/VoxelPassageGeometry.h +++ b/Source/VoxelForge/Public/VoxelPassageGeometry.h @@ -302,35 +302,48 @@ namespace VoxelPassageGeometry return false; } - const FVector2D QueryXY(Position.X, Position.Y); + const double QueryX = Position.X; + const double QueryY = Position.Y; + const int32 ControlPointCount = ControlPoints.Num(); + const FVector* ControlPointData = ControlPoints.GetData(); + const float* ControlRadiusData = ControlRadii.GetData(); float BestDistanceSquared = FLT_MAX; bool bFoundSegment = false; for (int32 SegmentIndex = 0; - SegmentIndex + 1 < ControlPoints.Num(); + SegmentIndex + 1 < ControlPointCount; ++SegmentIndex) { - const FVector& A = ControlPoints[SegmentIndex]; - const FVector& B = ControlPoints[SegmentIndex + 1]; - const FVector2D AXY(A.X, A.Y); - const FVector2D Delta = FVector2D(B.X, B.Y) - AXY; - const float LengthSquared = Delta.SizeSquared(); + const FVector& A = ControlPointData[SegmentIndex]; + const FVector& B = ControlPointData[SegmentIndex + 1]; + const double AXY_X = A.X; + const double AXY_Y = A.Y; + const double DeltaX = B.X - AXY_X; + const double DeltaY = B.Y - AXY_Y; + const float LengthSquared = static_cast( + DeltaX * DeltaX + DeltaY * DeltaY); if (LengthSquared <= KINDA_SMALL_NUMBER) { continue; } + const double Dot = (QueryX - AXY_X) * DeltaX + + (QueryY - AXY_Y) * DeltaY; const float T = FMath::Clamp( - FVector2D::DotProduct(QueryXY - AXY, Delta) / LengthSquared, + Dot / LengthSquared, 0.0f, 1.0f); - const FVector2D ClosestXY = AXY + Delta * T; - const float DistanceSquared = (QueryXY - ClosestXY).SizeSquared(); + const double ClosestX = AXY_X + DeltaX * T; + const double ClosestY = AXY_Y + DeltaY * T; + const double DistanceX = QueryX - ClosestX; + const double DistanceY = QueryY - ClosestY; + const float DistanceSquared = static_cast( + DistanceX * DistanceX + DistanceY * DistanceY); if (DistanceSquared >= BestDistanceSquared) { continue; } - const float StartRadius = FMath::Abs(ControlRadii[SegmentIndex]); - const float EndRadius = FMath::Abs(ControlRadii[SegmentIndex + 1]); + const float StartRadius = FMath::Abs(ControlRadiusData[SegmentIndex]); + const float EndRadius = FMath::Abs(ControlRadiusData[SegmentIndex + 1]); OutFloorZ = FMath::Lerp( TunnelFloorZ(A, StartRadius), TunnelFloorZ(B, EndRadius),