wip(assembler): round S7 -- workers kept busy: in-game cold full view 17.5 s -> 7.8 s
Two admission blockers found by per-frame decline codes: the near reserve held four slots idle after LOD0 was done (coarse capped at 6), and finished-but- unapplied results counted against the task cap. Admission now counts queued + running tasks; near reserve releases once the walkable ring is ready while stationary (voxel.StreamingDynamicNearReserve, default on). Walkable ring 0.65 -> 0.58 s, full LOD0 2.28 -> 1.44 s, busy workers 2.2 -> 5.6 of 10. Old behaviour behind voxel.StreamingLegacyPendingResultCap. RMC source reviewed (read-only): batching/fast-cook ranked, not yet worth it on measured apply cost. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
1 parent
b9c13c37c0
commit
5eb9ee76ff
7 files changed
+726
-53
No files matched your search
+60
-8
@@ -578,7 +578,12 @@ driven by `EditorBrush*` props.
|
||||
the result through the normal MPSC queue; the game thread reads the token again before applying.
|
||||
`FChunkResult::Epoch` still guards the generation epoch. Desired work is sorted from the true pawn
|
||||
position, with the occupied/support LOD0 tile and the next tile along the heading in an absolute
|
||||
high-priority prefix. Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.)
|
||||
high-priority prefix. Admission counts queued/running `ActiveTaskCount`; `PendingTiles` stays until
|
||||
game-thread apply for duplicate suppression and result bookkeeping, but completed MPSC results do
|
||||
not occupy a worker slot. `voxel.StreamingLegacyPendingResultCap=1` restores the old result-inclusive
|
||||
admission count. `voxel.StreamingDynamicNearReserve=1` holds the authored near reserve while moving
|
||||
or before the walkable gate, then releases it to coarse work after the stationary ring is ready.
|
||||
Stationary player ≈ free. (Old per-frame O(loaded×desired) scan = 22ms.)
|
||||
- **LOD** changes HOT-SWAP (`LoadTile` only, never unload-first) → no holes. LOD
|
||||
reconciliation lives in the PERSISTENT per-frame submit loop (same loop as new-chunk loads),
|
||||
NOT as a one-shot on the boundary-cross frame — a one-shot drops every chunk past the task
|
||||
@@ -1454,8 +1459,10 @@ opt-in because it did not beat the legacy order on the gates and the viewer coul
|
||||
presentation: set `voxel.StreamingCoarseFirst=1` to admit coarse desired levels from the maximum LOD
|
||||
down to `max(2, FullResClipLevels)`, favoring tiles ahead of travel and then nearer tiles within each
|
||||
LOD; LOD0/1 refinement follows. Default `0` preserves the S4 LOD0-to-LOD8 order.
|
||||
`voxel.StreamingDynamicNearReserve=1` opts into keeping the authored near-task reserve while the pawn
|
||||
moves or until the stationary walkable ring is ready; default `0` preserves the previous task budget.
|
||||
`voxel.StreamingDynamicNearReserve=1` keeps the authored near-task reserve while the pawn moves or
|
||||
until the stationary walkable ring is ready; this is now the default. Set it to `0` only for the
|
||||
legacy-admission negative control. After the stationary nine-tile ring is ready, coarse work can use
|
||||
the formerly reserved slots.
|
||||
The standalone queue defaults to legacy order; `--coarse-first-stream-order` opts into the S5 tiers
|
||||
and `--legacy-stream-order` explicitly selects the legacy order.
|
||||
|
||||
@@ -1498,12 +1505,10 @@ with a 30 second ceiling; at `0`, the authored budget is unchanged. On the match
|
||||
the boost changed full-set readiness from 17.412 s to 17.346 s and removed count-cap frames, but did
|
||||
not improve end-to-end residency. Keep the normal 4 / 2 ms guard for play.
|
||||
|
||||
The trace found the shared worker pool was not saturated: the 10-slot idle estimate was about 132
|
||||
The S6 trace found the shared pool was not saturated: its 10-slot idle estimate was about 132
|
||||
slot-seconds on legacy order and 73 on gated coarse-first, while game-thread tile applies totaled only
|
||||
about 0.24 seconds during cold residency. Results waited in the result queue, and some frames reached
|
||||
the four-mesh cap, but expanding the apply budget alone did not move the wall clock. The remaining
|
||||
floor is worker-side work supply/admission; this trace does not isolate why available worker slots
|
||||
stay idle. The 1.57 s target remains unmet; the headless trace is not a render-time measurement.
|
||||
about 0.24 seconds. S7's frame-level admission and cumulative worker-task accounting revisited that
|
||||
open question; see §8.28.
|
||||
|
||||
The report records per-LOD worker, queue, result-queue and apply totals, collision latency, frame
|
||||
percentiles, hashes, regressions and the command lines. The standalone 60 m/s viewer now counts
|
||||
@@ -1513,6 +1518,53 @@ W1/W8/W16 world and LOD hashes are unchanged, as are `Epoch`, cancellation, `Mod
|
||||
shutdown-drain and game-thread UObject-creation rules. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Round10/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round10/REPORT.md).
|
||||
|
||||
### 8.28 Shared admission accounting and worker supply — World Assembler S7 / M3 Round 11
|
||||
|
||||
`PendingTiles` is a game-thread result-bookkeeping set: its entry lasts from task submission until
|
||||
`ApplyTileResult`, so it can include work that has finished and is waiting in one of the MPSC result
|
||||
queues. It is not the count of active generation tasks. The S6 admission path used
|
||||
`PendingTiles.Num()` for that cap. On the S7 old-path control, after LOD0 had no missing keys, 877
|
||||
admission frames still had LOD1+ work missing and hit the four-slot near reserve; at a representative
|
||||
frame, occupancy was 6/10 while only four tasks were queued/running and two completed results were
|
||||
waiting to apply. 806 of those 877 frames had a positive completed-result estimate.
|
||||
|
||||
Admission now uses `ActiveTaskCount`, which is incremented before launching the UE task and released
|
||||
by its RAII guard when the worker lambda exits. This still counts queued and running work, but completed
|
||||
results do not consume an admission slot. `voxel.StreamingLegacyPendingResultCap=1` restores the old
|
||||
`PendingTiles` accounting; the test harness's `NearReserveMode=Legacy` also sets
|
||||
`voxel.StreamingDynamicNearReserve=0`, reproducing the S6 path. `StreamingDynamicNearReserve` now
|
||||
defaults to `1`; `StreamingCoarseFirst` remains `0`, and the apply boost remains `0` in the matched
|
||||
measurement.
|
||||
|
||||
On the 2,578-key owner SurfaceWorld trace, the old-path control reached the full set in 17.527 s,
|
||||
walkable ring in 0.650 s, and full LOD0 in 2.281 s. Dynamic admission reached them in 7.780 s,
|
||||
0.581 s, and 1.439 s. Its first-request-to-ready interval was 7.342 s with 41.364 worker-seconds,
|
||||
or 5.63 integrated busy-worker equivalents. Using the S6 10-slot idle formula on that same interval
|
||||
gives 32.06 slot-seconds, versus 133.01 on the S7 old-path control and 132.1 in S6. The ten S7
|
||||
spawn samples that ended with free admission capacity and missing keys all recorded a submission
|
||||
stop at the 10-task cap (one worker could finish before the end-of-pass sample); the frame trace
|
||||
found no near-reserve decline, all-loaded latch, walkable gate failure, or inner `LoadTile` rejection
|
||||
in those passes. Result-aware admission therefore removes the measured cold-set blocker and
|
||||
reproduces the legacy control within 0.7%, but the 7.78 s full-set result still misses the rough
|
||||
4.14 s `worker-seconds / 10 slots` estimate. Across the full 45 s trace, free-capacity frames with
|
||||
missing keys were classified by LOD as near reserve or full cap; none was unclassified.
|
||||
|
||||
The per-frame `running_workers` value is biased high when sampled only at admission/apply boundaries;
|
||||
S7 added cumulative task intervals and found 5.63 busy-worker equivalents over the first-request-to-
|
||||
ready span, with eight sustained overlaps in the reconstructed intervals; a zero-duration endpoint
|
||||
tie briefly counts nine. This locates
|
||||
the remaining gap downstream of an admission refusal, but does not distinguish UE task-pool dispatch
|
||||
from work internal to a tile. Measured worker task time rose 9.7% versus the negative control
|
||||
(16.04 ms versus 14.62 ms per initial tile); hashes match and the trace records elapsed task time,
|
||||
not CPU cycles. During the later +Y move, the loader's `bCoreRegionCompilePending` guard also
|
||||
blocked 10 legacy and 11 dynamic submission passes despite free tasks; each inner-decline record
|
||||
had that flag set. The tick trace reports aggregate `TestWorld->Tick()` only: spawn p95/max was
|
||||
2.262/5.230 ms and walk p95/max was 2.512/28.893 ms; no subsystem marker names the walk
|
||||
maximum. The nine-tile ring and full LOD0 are faster than S6. Tile output hashes did not change, so
|
||||
the recipe version remains unchanged. Null-RHI runs do not prove player-visible enclosure. See
|
||||
[`Standalone/Research/WorldAssemblerM3/Round11/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round11/REPORT.md)
|
||||
for the audit counts, RMC review, quality matrix, commands and binary hashes.
|
||||
|
||||
## 9. Multiplayer model (listen-server first, dedicated-friendly)
|
||||
|
||||
> **Status: transport is DESIGN ONLY — nothing is networked in-tree yet** (no `Replicated`/`HasAuthority`/RPCs; a
|
||||
|
||||
+19
-13
@@ -370,7 +370,7 @@ casse cette arithmetique *structurellement* — `BeginPlay` loggue une **Error**
|
||||
| `RecordTileHash` / `WriteTileHashDump` | — | Headless `-voxel.TileHashDump=<absolute path>` parity instrument. Records the exact `FVoxelMeshData` used by the streaming result, including empty/classified tiles, then writes sorted absolute tile keys, LOD step/cells, content-band limits, settings digest context, and the shared `VoxelGeometryHash` at `EndPlay`. |
|
||||
| `SyncRemeshTile` | — | **INSTANT DIG**: level-0 same-frame re-mesh on the game thread. Uses `PreEditDensityCache` when the tile is resident in the bounded local cache, replays the current DiffLayer over that exact procedural lattice, then meshes/applies inline so a carve is visible THIS frame. A cache miss uses normal generation; it captures the unedited lattice if the tile needs meshing, while uniform-tile proofs remain enabled. |
|
||||
| `PreEditDensityCache` / `VoxelForgePreEditDensityCache::BuildEditedGrid` | — | Per-world cache of the pawn's current L0 tile: exact 35³ pre-edit samples, max **171,500 bytes**. It is pruned when the pawn crosses chunk boundaries and cleared on `RegenerateAllChunks`. `BuildEditedGrid` snapshots touched diff chunks once and reproduces `GetDensityAt`'s final `pre-edit - EvaluateMods` operation before the per-call shared-grid mesher path. |
|
||||
| `UpdateChunksAroundPosition` | 8603 | Shared Core/legacy scheduler. Builds the desired set and delta-culls leavers; admits the occupied/support tile plus collision-required LOD0 tiles in the pawn's 3×3 XY ring as `CollisionCritical`. S5 coarse-first admission is opt-in (`voxel.StreamingCoarseFirst=1`): it submits remaining coarse levels from max LOD down through `max(2, FullResClipLevels)`, then LOD0/1 refinement; ties favor travel direction, then distance. Default `0` preserves S4's LOD0-to-LOD8 order. `voxel.StreamingDynamicNearReserve=1` opts into holding the authored near-worker reserve while moving or until the stationary walkable ring is ready; default `0`. The clipmap stays non-overlapping and existing load-before-unload / `TransitionHold` reconciliation remains authoritative; S5 adds no overlapping placeholder layer. Standalone defaults to legacy; use `--coarse-first-stream-order` to opt in. §8.10 + §8.26. |
|
||||
| `UpdateChunksAroundPosition` / `GetGenerationAdmissionCount` | 10990 / 10971 | Shared Core/legacy scheduler. Builds the desired set and delta-culls leavers; admits the occupied/support tile plus collision-required LOD0 tiles in the pawn's 3×3 XY ring as `CollisionCritical`. S5 coarse-first admission stays opt-in (`voxel.StreamingCoarseFirst=1`). `voxel.StreamingDynamicNearReserve=1` is now the default: it keeps the authored near reserve while moving or until the stationary walkable ring is ready, then makes those slots available to coarse work. Admission counts submitted queued/running tasks, not completed results waiting in the MPSC result queues; `voxel.StreamingLegacyPendingResultCap=1` restores the old `PendingTiles`-based cap for the negative control. `PendingTiles` still lasts through game-thread apply for duplicate suppression and result bookkeeping. The clipmap stays non-overlapping and load-before-unload / `TransitionHold` reconciliation remains authoritative. Standalone has no mirror of this actor scheduler. §8.10 + §8.26–8.28. |
|
||||
| `AdvanceHeadlessCollisionGateStressTest` / `ObserveHeadlessCollisionGateStressTest` / `MaybeFinishHeadlessCollisionGateStressTest` / `MeasureHeadlessCollisionGateStressTerrain` | — | Explicit `-voxel.TestCollisionGateStress=1` harness only. The normal spawn-height cvar offsets from the measured density surface; `voxel.TestCollisionGateStressDeepDrop=1` carves a process-local box shaft from above the top to just above the floor tile, extending along the post-landing route so the pawn does not run into the shaft sidewall, temporarily raises only the transient test DiffLayer brush-radius allowance for that fixture, removes only the local pawn's redundant streaming anchor for this isolated repro, proves every sample in two complete outside-ring level-0 tiles is air before seeding their known-empty status, prewarms the known-empty in-ring column, and asserts that the support walk reaches the in-layout floor outside the aligned level-0 render ring before the pawn drops. The log reports whether the fixed gate retains that otherwise out-of-ring target. Other streaming anchors remain active. The fixture rejects a target or floor outside the layout, a non-air empty-tile proof, or a floor inside the aligned level-0 ring. The `GetDensityAt` column at capsule feet defines the local air/solid surface; the deep-drop column crossing is cached after its first exact scan. Each frame records actor position, velocity, direct foot tile and gate support readiness, gate state, signed feet clearance, and a ≤0.5-voxel sweep between positions. PASS requires no measured clearance below −5 cm; deep-drop gate-on also requires horizontal travel during a hold. Result logs include the terminal density column, first crossing below spawn height, hold count/time, and gated horizontal progress. |
|
||||
| `BuildDesiredTiles` | 6482 | Builds `DesiredSorted`+`DesiredStamped` (player clipmap shells), then adds streaming anchors and the pending player's collision-only support column before the leaver sweep. It orders from the true local pawn position and builds `CriticalDesiredTiles` for the unresolved support target, occupied/support and heading tiles. Also rebuilds `DesiredTransitionDescendants` (ancestor → desired descendants, including face/edge-touching replacements) for the reconcile pass. |
|
||||
| `CollisionGateDesiredTiles` / `RequestCollisionGateSupportTile` | 5062 | Retains an unresolved support target and up to three level-0 tiles below it across desired-set rebuilds. The additional keys are hidden collision-only tiles, queued ahead of ordinary streaming, and pruned once resolved or outside the active column. |
|
||||
@@ -996,19 +996,25 @@ exact-density-root LOD7/8 agreement audit with its datum-offset negative control
|
||||
ARCHITECTURE §8.25 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Round8/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round8/REPORT.md).
|
||||
|
||||
### 3.14 Cold streaming apply trace and gated coarse order — World Assembler S6 / M3 Round 10
|
||||
### 3.14 Cold streaming admission and apply trace — World Assembler S7 / M3 Round 11
|
||||
|
||||
`AVoxelWorld::ProcessPendingChunks` records per-frame result draining, visible-mesh applies, the
|
||||
authored/startup budgets, per-LOD apply counts and worker occupancy through
|
||||
`VoxelStartupTrace::RecordApplyFrame`. Direct `tile` events separate request-to-worker queue,
|
||||
generation work, worker-result queue and game-thread apply time; `collision_ready` records the
|
||||
RealtimeMesh body-update latency. `AVoxelWorld::UpdateChunksAroundPosition` samples resident counts
|
||||
every 250 ms (for observation only) and admits coarse-first work only after the required nine-tile
|
||||
walkable ring is ready. The order and startup apply boost are opt-in CVars; the boost is default-off.
|
||||
`VoxelForgeExploreCommandlet.cpp` drives the staged owner-plan trace and moving actor check. The
|
||||
standalone viewer's 60 m/s crossing counter now accepts multi-chunk forward deltas. See
|
||||
ARCHITECTURE §8.27 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Round10/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round10/REPORT.md).
|
||||
`AVoxelWorld::UpdateChunksAroundPosition` writes a `scheduler_admission` record on each submission
|
||||
pass, plus `scheduler_skip` for core-readiness bypasses and all-loaded latches that still have missing
|
||||
work. The admission record includes desired/missing/pending keys by LOD, task caps, submitted attempts,
|
||||
stop level, decline reason, `bCoreRegionCompilePending`, pending-result estimate, and cumulative
|
||||
worker-task time. Rule codes are 1=full task cap, 2=near reserve, 3=all-loaded latch, 4=inner
|
||||
`LoadTile`/`LoadCoreTile` rejection, and 5=desired work still missing with apparent capacity.
|
||||
This is the trace used to separate the
|
||||
old near-reserve stop from completed-but-unapplied results counted as active. The default admission
|
||||
count uses queued/running `ActiveTaskCount`; `voxel.StreamingLegacyPendingResultCap=1` restores the
|
||||
old pending-result cap. The negative-control harness sets that together with
|
||||
`voxel.StreamingDynamicNearReserve=0`. `ProcessPendingChunks` still records result draining,
|
||||
visible-mesh applies, per-LOD counts and budget use; direct `tile` events separate worker queue,
|
||||
task time, result queue and game-thread apply. `collision_ready` records RealtimeMesh submit-to-body
|
||||
update latency. The per-frame `running_workers` sample aliases short task bursts, so use the
|
||||
cumulative task intervals for worker-seconds. `VoxelForgeExploreCommandlet.cpp` drives the staged
|
||||
owner-plan trace and moving actor check. See ARCHITECTURE §8.27–8.28 and
|
||||
[`Standalone/Research/WorldAssemblerM3/Round11/REPORT.md`](Standalone/Research/WorldAssemblerM3/Round11/REPORT.md).
|
||||
|
||||
## 4. The density pipeline (most-edited hot path)
|
||||
|
||||
|
||||
@@ -185,11 +185,17 @@ namespace
|
||||
GVoxelForgeStreamingCoarseFirst,
|
||||
TEXT("Prioritize current walkable tiles, then far coarse clipmap levels, then near refinement; 0 restores legacy LOD order."));
|
||||
|
||||
int32 GVoxelForgeStreamingDynamicNearReserve = 0;
|
||||
int32 GVoxelForgeStreamingDynamicNearReserve = 1;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingDynamicNearReserve(
|
||||
TEXT("voxel.StreamingDynamicNearReserve"),
|
||||
GVoxelForgeStreamingDynamicNearReserve,
|
||||
TEXT("Keep the authored near-worker reserve while the pawn moves or its walkable ring is incomplete; release it when stationary and ready."));
|
||||
TEXT("Keep the authored near-worker reserve while moving or before the walkable ring is ready; default 1 releases idle slots after that gate, 0 restores the legacy fixed reserve."));
|
||||
|
||||
int32 GVoxelForgeStreamingLegacyPendingResultCap = 0;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeStreamingLegacyPendingResultCap(
|
||||
TEXT("voxel.StreamingLegacyPendingResultCap"),
|
||||
GVoxelForgeStreamingLegacyPendingResultCap,
|
||||
TEXT("Compatibility control: 1 counts completed results awaiting apply against the generation task cap; default 0 caps only queued or running generation tasks."));
|
||||
|
||||
int32 GVoxelForgeProfileTileGeneration = 0;
|
||||
FAutoConsoleVariableRef CVarVoxelForgeProfileTileGeneration(
|
||||
@@ -1944,6 +1950,9 @@ void AVoxelWorld::BeginPlay()
|
||||
PeakObservedPawnSpeedCmPerSecond = 0.0f;
|
||||
TotalWorkerGenerationCycles.store(0, std::memory_order_relaxed);
|
||||
TotalWorkerGenerationTasks.store(0, std::memory_order_relaxed);
|
||||
TotalGenerationWorkerTaskCycles.store(0, std::memory_order_relaxed);
|
||||
LastStreamingSchedulerSampleSeconds = 0.0;
|
||||
LastStreamingSchedulerWorkerCycles = 0;
|
||||
TotalObsoleteWorkerCycles.store(0, std::memory_order_relaxed);
|
||||
TotalObsoleteWorkerTasks.store(0, std::memory_order_relaxed);
|
||||
TotalValidationDensityCalls.store(0, std::memory_order_relaxed);
|
||||
@@ -2060,6 +2069,8 @@ void AVoxelWorld::BeginPlay()
|
||||
GVoxelForgeStreamingCoarseFirst);
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingDynamicNearReserve="),
|
||||
GVoxelForgeStreamingDynamicNearReserve);
|
||||
FParse::Value(FCommandLine::Get(), TEXT("voxel.StreamingLegacyPendingResultCap="),
|
||||
GVoxelForgeStreamingLegacyPendingResultCap);
|
||||
if (FParse::Value(FCommandLine::Get(), TEXT("voxel.Step1PrequeueVisibleLOD0="),
|
||||
CommandLineStep1PrequeueVisibleLOD0))
|
||||
{
|
||||
@@ -10954,6 +10965,15 @@ int32 AVoxelWorld::GetMaxConcurrentTasks() const
|
||||
|
||||
int32 AVoxelWorld::GetActiveGenerationTaskCount() const
|
||||
{
|
||||
return FMath::Max(0, ActiveTaskCount.load(std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
int32 AVoxelWorld::GetGenerationAdmissionCount() const
|
||||
{
|
||||
if (GVoxelForgeStreamingLegacyPendingResultCap == 0)
|
||||
{
|
||||
return GetActiveGenerationTaskCount();
|
||||
}
|
||||
const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0
|
||||
? Step1HeldApplyTiles.Num() : 0;
|
||||
return FMath::Max(0, PendingTiles.Num() - HeldCompletedResults);
|
||||
@@ -10976,13 +10996,56 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
const bool bTraceStreamingScheduler = VoxelForgeStartupTrace::IsActive();
|
||||
const double StreamingUpdateStartSeconds = bTraceStreamingScheduler
|
||||
? FPlatformTime::Seconds() : 0.0;
|
||||
const int32 PendingTilesAtUpdateEntry = PendingTiles.Num();
|
||||
double DesiredRebuildMilliseconds = 0.0;
|
||||
const auto RecordSchedulerSkip = [&](const TCHAR* Reason)
|
||||
{
|
||||
if (!bTraceStreamingScheduler) return;
|
||||
const double Now = FPlatformTime::Seconds();
|
||||
int32 DesiredMissing = 0;
|
||||
for (const FVoxelTileKey& Tile : DesiredSorted)
|
||||
{
|
||||
if (!PendingTiles.Contains(Tile) && !LoadedTiles.Contains(Tile))
|
||||
{
|
||||
++DesiredMissing;
|
||||
}
|
||||
}
|
||||
const int32 ActiveTasks = ActiveTaskCount.load(std::memory_order_relaxed);
|
||||
const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0
|
||||
? Step1HeldApplyTiles.Num() : 0;
|
||||
const int32 PendingResultsEstimate = FMath::Max(
|
||||
0, PendingTiles.Num() - ActiveTasks - HeldCompletedResults);
|
||||
const int32 MaxTasks = GetMaxConcurrentTasks();
|
||||
const int32 AdmissionOccupancy = GetGenerationAdmissionCount();
|
||||
const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed);
|
||||
const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles
|
||||
? TotalWorkerCycles - LastStreamingSchedulerWorkerCycles : 0;
|
||||
const double IntervalStart = LastStreamingSchedulerSampleSeconds > 0.0
|
||||
? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds;
|
||||
const double IntervalSeconds = FMath::Max(0.0, Now - IntervalStart);
|
||||
const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta);
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_skip"), FString::Printf(
|
||||
TEXT("\"frame\":%llu,\"reason\":\"%s\",\"desired_count\":%d,"
|
||||
"\"desired_missing\":%d,\"pending_tiles\":%d,\"admission_occupancy\":%d,"
|
||||
"\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d,"
|
||||
"\"task_cap\":%d,\"legacy_pending_result_cap\":%d,"
|
||||
"\"worker_busy_s_interval\":%s,\"worker_busy_avg\":%s"),
|
||||
static_cast<unsigned long long>(GFrameCounter), Reason, DesiredSorted.Num(),
|
||||
DesiredMissing, PendingTiles.Num(), AdmissionOccupancy, ActiveTasks,
|
||||
ActiveGenerationWorkerCount.load(std::memory_order_relaxed),
|
||||
PendingResultsEstimate, MaxTasks, GVoxelForgeStreamingLegacyPendingResultCap,
|
||||
*FString::SanitizeFloat(BusySeconds),
|
||||
*FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0)));
|
||||
LastStreamingSchedulerSampleSeconds = Now;
|
||||
LastStreamingSchedulerWorkerCycles = TotalWorkerCycles;
|
||||
};
|
||||
if (bUseVoxelForgeCore)
|
||||
{
|
||||
const TSharedPtr<FVoxelForgeCoreRuntimeState, ESPMode::ThreadSafe> State = VoxelForgeCoreState;
|
||||
if (!State.IsValid() || State->bInitialFailed.load(std::memory_order_acquire)
|
||||
|| !State->bReady.load(std::memory_order_acquire))
|
||||
{
|
||||
RecordSchedulerSkip(TEXT("core_not_ready"));
|
||||
return; // Core mode never falls through to the legacy generator.
|
||||
}
|
||||
if (State->bRebuildDesired.exchange(false, std::memory_order_acq_rel))
|
||||
@@ -11004,6 +11067,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
else
|
||||
{
|
||||
StartPendingCoreRegionCompile();
|
||||
RecordSchedulerSkip(TEXT("core_region_compile_pending"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -11058,6 +11122,19 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
: ((bPawnMoving || !bWalkableRingReadyForReserve) ? AuthoredNearTaskReserve : 0);
|
||||
const int32 NearTaskReserve = EffectiveNearTaskReserve;
|
||||
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearTaskReserve);
|
||||
const int32 FirstCoarseLevel = FMath::Max(
|
||||
2, Settings ? Settings->FullResClipLevels : 2);
|
||||
const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0
|
||||
&& bWalkableRingReadyForReserve;
|
||||
int32 SchedulerSubmittedByLevel[9] = {};
|
||||
int32 SchedulerLoadAttemptsByLevel[9] = {};
|
||||
int32 SchedulerLevelCapReturns[9] = {};
|
||||
int32 SchedulerLevelCapBreaks[9] = {};
|
||||
int32 SchedulerCriticalFullCapBreaks = 0;
|
||||
int32 SchedulerDirtyFullCapBreaks = 0;
|
||||
int32 SchedulerBandCoarseCapBreaks = 0;
|
||||
int32 SchedulerOrderStopLevel = -1;
|
||||
int32 SchedulerOrderStopCap = 0;
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& !GVoxelForgeStreamingBudgetReported)
|
||||
{
|
||||
@@ -11070,7 +11147,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
TEXT("[VoxelForgeStreamingBudget] logical_cores=%d automatic_core_cap=%d "
|
||||
"authored_core_cap=%d asset_task_cap=%d effective_generation_cap=%d "
|
||||
"step1_generation_cap=%d step1_max_mesh_applies=%d "
|
||||
"near_task_reserve=%d dynamic_reserve=%d moving=%d walkable_ring_ready=%d "
|
||||
"near_task_reserve=%d dynamic_reserve=%d legacy_result_cap=%d "
|
||||
"moving=%d walkable_ring_ready=%d "
|
||||
"coarse_task_cap=%d coarse_first=%d "
|
||||
"max_mesh_applies=%d max_apply_ms=%.3f max_unloads=%d"),
|
||||
LogicalCores,
|
||||
@@ -11082,6 +11160,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
GVoxelForgeStep1MaxMeshApplies,
|
||||
NearTaskReserve,
|
||||
GVoxelForgeStreamingDynamicNearReserve,
|
||||
GVoxelForgeStreamingLegacyPendingResultCap,
|
||||
bPawnMoving ? 1 : 0,
|
||||
bWalkableRingReadyForReserve ? 1 : 0,
|
||||
CoarseTaskCap,
|
||||
@@ -11126,7 +11205,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
// Rebuild the desired tile set when the player crosses a level-0 tile boundary — or when a
|
||||
// streaming anchor moved / (un)registered (bAnchorsMoved / bForceDesiredRebuild).
|
||||
//=========================================================================
|
||||
if (CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild)
|
||||
const bool bDesiredRebuiltThisUpdate =
|
||||
CenterChunk != LastUpdateCenter || bAnchorsMoved || bForceDesiredRebuild;
|
||||
if (bDesiredRebuiltThisUpdate)
|
||||
{
|
||||
LastUpdateCenter = CenterChunk;
|
||||
bAllChunksLoaded = false;
|
||||
@@ -11375,10 +11456,26 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
// Coarse work is capped below the full task budget so later near requests keep admission.
|
||||
// Le travail grossier reste sous le budget total pour reserver des admissions aux tuiles proches.
|
||||
//=========================================================================
|
||||
if (!bAllChunksLoaded)
|
||||
const bool bSubmissionLoopRan = !bAllChunksLoaded;
|
||||
if (bSubmissionLoopRan)
|
||||
{
|
||||
TRACE_CPUPROFILER_EVENT_SCOPE(VoxelForge_SubmitTiles);
|
||||
int32 Submitted = 0;
|
||||
const auto SubmitTileWithTraceCount = [&](const FVoxelTileKey& Tile,
|
||||
EVoxelTileWorkPriority Priority)
|
||||
{
|
||||
if (Tile.Level >= 0 && Tile.Level < 9)
|
||||
{
|
||||
++SchedulerLoadAttemptsByLevel[Tile.Level];
|
||||
}
|
||||
const uint64 OrdinalBefore = NextStreamingRequestOrdinal;
|
||||
LoadTile(Tile, Priority);
|
||||
if (NextStreamingRequestOrdinal != OrdinalBefore
|
||||
&& Tile.Level >= 0 && Tile.Level < 9)
|
||||
{
|
||||
++SchedulerSubmittedByLevel[Tile.Level];
|
||||
}
|
||||
};
|
||||
|
||||
// FLOOR RESPONSIVENESS — the support tile and collision-required 3x3 XY ring lead ordinary work.
|
||||
// PRIORITE AU SOL — la tuile de support et l'anneau XY 3x3 requis pour la collision passent avant le reste.
|
||||
@@ -11386,9 +11483,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
// Ils passent en BackgroundHigh afin que le sol du pion obtienne les premiers workers disponibles.
|
||||
for (const FVoxelTileKey& T : CriticalDesiredTiles)
|
||||
{
|
||||
if (GetActiveGenerationTaskCount() >= MaxTasks) break;
|
||||
if (GetGenerationAdmissionCount() >= MaxTasks)
|
||||
{
|
||||
++SchedulerCriticalFullCapBreaks;
|
||||
break;
|
||||
}
|
||||
if (PendingTiles.Contains(T) || LoadedTiles.Contains(T)) continue;
|
||||
LoadTile(T, EVoxelTileWorkPriority::CollisionCritical);
|
||||
SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::CollisionCritical);
|
||||
++Submitted;
|
||||
}
|
||||
|
||||
@@ -11396,12 +11497,16 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
// still in flight is KEPT queued (retried next frame) so its stale pre-carve result is corrected.
|
||||
for (auto It = DirtyRemeshQueue.CreateIterator(); It; ++It)
|
||||
{
|
||||
if (GetActiveGenerationTaskCount() >= MaxTasks) break;
|
||||
if (GetGenerationAdmissionCount() >= MaxTasks)
|
||||
{
|
||||
++SchedulerDirtyFullCapBreaks;
|
||||
break;
|
||||
}
|
||||
const FVoxelTileKey T = *It;
|
||||
if (!IsDesired(T)) { It.RemoveCurrent(); continue; } // obsolete — drop
|
||||
if (PendingTiles.Contains(T)) { continue; } // in flight — retry after it lands
|
||||
It.RemoveCurrent();
|
||||
LoadTile(T, EVoxelTileWorkPriority::Edited);
|
||||
SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Edited);
|
||||
++Submitted;
|
||||
}
|
||||
|
||||
@@ -11416,8 +11521,9 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
auto SubmitLevel = [&](int32 Level, EVoxelTileWorkPriority Priority, int32 AdmissionCap,
|
||||
bool bPreferTravelDirection)
|
||||
{
|
||||
if (GetActiveGenerationTaskCount() >= AdmissionCap)
|
||||
if (GetGenerationAdmissionCount() >= AdmissionCap)
|
||||
{
|
||||
if (Level >= 0 && Level < 9) { ++SchedulerLevelCapReturns[Level]; }
|
||||
return;
|
||||
}
|
||||
TArray<FVoxelTileKey> LevelTiles;
|
||||
@@ -11467,10 +11573,14 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
}
|
||||
for (const FVoxelTileKey& T : LevelTiles)
|
||||
{
|
||||
if (GetActiveGenerationTaskCount() >= AdmissionCap) break;
|
||||
if (GetGenerationAdmissionCount() >= AdmissionCap)
|
||||
{
|
||||
if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; }
|
||||
break;
|
||||
}
|
||||
if (PendingTiles.Contains(T)) continue;
|
||||
if (LoadedTiles.Contains(T)) continue;
|
||||
LoadTile(T, Priority);
|
||||
SubmitTileWithTraceCount(T, Priority);
|
||||
++Submitted;
|
||||
}
|
||||
};
|
||||
@@ -11483,12 +11593,8 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
// Keep the walkable ring on the legacy near-first path; only spend its reserved task slots
|
||||
// on coarse work after all nine collision gates are ready.
|
||||
// Garder l'anneau praticable en priorite ; les taches grossieres commencent apres ses neuf collisions.
|
||||
const bool bUseCoarseFirstOrder = GVoxelForgeStreamingCoarseFirst != 0
|
||||
&& bWalkableRingReadyForReserve;
|
||||
if (bUseCoarseFirstOrder)
|
||||
{
|
||||
const int32 FirstCoarseLevel = FMath::Max(
|
||||
2, Settings ? Settings->FullResClipLevels : 2);
|
||||
for (int32 Level = MaxDesiredLevel; Level >= FirstCoarseLevel; --Level)
|
||||
{
|
||||
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
|
||||
@@ -11508,7 +11614,13 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
{
|
||||
SubmitLevel(Level, EVoxelTileWorkPriority::Horizon,
|
||||
CoarseTaskCap, false);
|
||||
if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break;
|
||||
if (GetGenerationAdmissionCount() >= CoarseTaskCap)
|
||||
{
|
||||
if (Level >= 0 && Level < 9) { ++SchedulerLevelCapBreaks[Level]; }
|
||||
SchedulerOrderStopLevel = Level;
|
||||
SchedulerOrderStopCap = CoarseTaskCap;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bMeasureReadyTransitionVisibility)
|
||||
@@ -11523,11 +11635,15 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
// mesh reste visible jusqu'au résultat — même schéma que RemeshDirtyChunks).
|
||||
for (auto It = BandRemeshQueue.CreateIterator(); It; ++It)
|
||||
{
|
||||
if (GetActiveGenerationTaskCount() >= CoarseTaskCap) break;
|
||||
if (GetGenerationAdmissionCount() >= CoarseTaskCap)
|
||||
{
|
||||
++SchedulerBandCoarseCapBreaks;
|
||||
break;
|
||||
}
|
||||
const FVoxelTileKey T = *It;
|
||||
It.RemoveCurrent();
|
||||
if (!IsDesired(T) || PendingTiles.Contains(T) || !LoadedTiles.Contains(T)) continue;
|
||||
LoadTile(T);
|
||||
SubmitTileWithTraceCount(T, EVoxelTileWorkPriority::Horizon);
|
||||
++Submitted;
|
||||
}
|
||||
|
||||
@@ -11540,7 +11656,7 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
GVoxelForgeMaxPendingTilesObserved, MaxTasks, Submitted);
|
||||
}
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& GetActiveGenerationTaskCount() >= CoarseTaskCap
|
||||
&& GetGenerationAdmissionCount() >= CoarseTaskCap
|
||||
&& !GVoxelForgeGenerationCapHitReported)
|
||||
{
|
||||
UE_LOG(LogTemp, Display,
|
||||
@@ -11604,8 +11720,10 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
TEXT("\"scope\":\"desired_residency_not_visual_enclosure\",\"center\":[%d,%d,%d],"
|
||||
"\"position_cm\":[%s,%s,%s],\"speed_cm_s\":%s,\"walkable_ready\":%d,"
|
||||
"\"desired_by_lod\":%s,\"resident_by_lod\":%s,\"pending_by_lod\":%s,"
|
||||
"\"active_generation\":%d,\"task_cap\":%d,\"near_reserve\":%d,"
|
||||
"\"coarse_first\":%d,\"dynamic_near_reserve\":%d,\"epoch\":%u,"
|
||||
"\"active_generation\":%d,\"admission_occupancy\":%d,"
|
||||
"\"task_cap\":%d,\"near_reserve\":%d,"
|
||||
"\"coarse_first\":%d,\"dynamic_near_reserve\":%d,"
|
||||
"\"legacy_pending_result_cap\":%d,\"epoch\":%u,"
|
||||
"\"update_chunks_ms\":%s,\"desired_rebuild_ms\":%s"),
|
||||
CenterChunk.X, CenterChunk.Y, CenterChunk.Z,
|
||||
*FString::SanitizeFloat(LocalPawn.X), *FString::SanitizeFloat(LocalPawn.Y),
|
||||
@@ -11613,13 +11731,177 @@ void AVoxelWorld::UpdateChunksAroundPosition(const FVector& CenterPosition, APaw
|
||||
*FString::SanitizeFloat(PlayerHeading.Size2D()),
|
||||
bWalkableRingReadyForReserve ? 1 : 0,
|
||||
*DesiredCounts, *ResidentCounts, *PendingCounts,
|
||||
GetActiveGenerationTaskCount(), MaxTasks, NearTaskReserve,
|
||||
GetActiveGenerationTaskCount(), GetGenerationAdmissionCount(), MaxTasks, NearTaskReserve,
|
||||
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
||||
GVoxelForgeStreamingLegacyPendingResultCap,
|
||||
GenerationEpoch,
|
||||
*FString::SanitizeFloat(StreamingUpdateMilliseconds),
|
||||
*FString::SanitizeFloat(DesiredRebuildMilliseconds)));
|
||||
LastStreamingTraceSampleSeconds = TraceNow;
|
||||
}
|
||||
|
||||
// Mesurer chaque decision d'admission avec l'etat du meme tick, y compris les resultats en attente.
|
||||
// Capture each admission decision from this tick, including completed results still awaiting apply.
|
||||
int32 DesiredMissingByLevel[9] = {};
|
||||
int32 LoadedByLevel[9] = {};
|
||||
int32 PendingByLevel[9] = {};
|
||||
int32 AdmissionCapByLevel[9] = {};
|
||||
int32 DeclinedRuleByLevel[9] = {};
|
||||
for (const FVoxelTileKey& Tile : DesiredSorted)
|
||||
{
|
||||
if (Tile.Level < 0 || Tile.Level > 8) continue;
|
||||
if (PendingTiles.Contains(Tile))
|
||||
{
|
||||
++PendingByLevel[Tile.Level];
|
||||
}
|
||||
else if (LoadedTiles.Contains(Tile))
|
||||
{
|
||||
++LoadedByLevel[Tile.Level];
|
||||
}
|
||||
else
|
||||
{
|
||||
++DesiredMissingByLevel[Tile.Level];
|
||||
}
|
||||
}
|
||||
|
||||
const int32 HeldCompletedResults = GVoxelForgeStep1PrequeueVisibleLOD0 > 0
|
||||
? Step1HeldApplyTiles.Num() : 0;
|
||||
const int32 AdmissionOccupancy = GetGenerationAdmissionCount();
|
||||
const int32 ActiveTaskGuard = ActiveTaskCount.load(std::memory_order_relaxed);
|
||||
const int32 PendingResultsEstimate = FMath::Max(
|
||||
0, PendingTiles.Num() - ActiveTaskGuard - HeldCompletedResults);
|
||||
int32 FullCapDeclinedTiles = 0;
|
||||
int32 ReserveDeclinedTiles = 0;
|
||||
int32 InnerLoadDeclinedTiles = 0;
|
||||
for (int32 Level = 0; Level < 9; ++Level)
|
||||
{
|
||||
AdmissionCapByLevel[Level] = bUseCoarseFirstOrder
|
||||
? (Level >= FirstCoarseLevel ? CoarseTaskCap : MaxTasks)
|
||||
: (Level == 0 ? MaxTasks : CoarseTaskCap);
|
||||
if (DesiredMissingByLevel[Level] <= 0) continue;
|
||||
|
||||
if (bAllChunksLoaded)
|
||||
{
|
||||
DeclinedRuleByLevel[Level] = 3; // Loaded latch while unadmitted desired keys remain.
|
||||
// Verrou charge alors que des cles desirees restent non admises.
|
||||
}
|
||||
else if (SchedulerLevelCapReturns[Level] > 0
|
||||
|| SchedulerLevelCapBreaks[Level] > 0
|
||||
|| (SchedulerOrderStopLevel >= 0 && Level >= SchedulerOrderStopLevel))
|
||||
{
|
||||
const int32 DeclinedAtCap = AdmissionCapByLevel[Level] > 0
|
||||
? AdmissionCapByLevel[Level] : SchedulerOrderStopCap;
|
||||
if (DeclinedAtCap < MaxTasks)
|
||||
{
|
||||
DeclinedRuleByLevel[Level] = 2; // Near reserve reached during this submit pass.
|
||||
// Reserve proche atteinte pendant cette passe d'admission.
|
||||
ReserveDeclinedTiles += DesiredMissingByLevel[Level];
|
||||
}
|
||||
else
|
||||
{
|
||||
DeclinedRuleByLevel[Level] = 1; // Full task cap reached during this submit pass.
|
||||
// Capacite totale des taches atteinte pendant cette passe.
|
||||
FullCapDeclinedTiles += DesiredMissingByLevel[Level];
|
||||
}
|
||||
}
|
||||
else if (AdmissionOccupancy >= MaxTasks)
|
||||
{
|
||||
DeclinedRuleByLevel[Level] = 1; // Full task admission cap.
|
||||
// Capacite totale d'admission des taches atteinte.
|
||||
FullCapDeclinedTiles += DesiredMissingByLevel[Level];
|
||||
}
|
||||
else if (AdmissionOccupancy >= AdmissionCapByLevel[Level]
|
||||
&& AdmissionCapByLevel[Level] < MaxTasks)
|
||||
{
|
||||
DeclinedRuleByLevel[Level] = 2; // Authored near reserve held back this LOD.
|
||||
// La reserve proche configuree retient ce niveau de detail.
|
||||
ReserveDeclinedTiles += DesiredMissingByLevel[Level];
|
||||
}
|
||||
else if (SchedulerLoadAttemptsByLevel[Level] > SchedulerSubmittedByLevel[Level])
|
||||
{
|
||||
DeclinedRuleByLevel[Level] = 4; // LoadTile/LoadCoreTile declined despite a scheduler attempt.
|
||||
// LoadTile/LoadCoreTile a refuse la tuile apres la tentative du scheduler.
|
||||
InnerLoadDeclinedTiles += DesiredMissingByLevel[Level];
|
||||
}
|
||||
else
|
||||
{
|
||||
DeclinedRuleByLevel[Level] = 5; // Candidate remains with apparent admission capacity.
|
||||
// La tuile desiree reste manquante avec une capacite d'admission apparente.
|
||||
}
|
||||
}
|
||||
|
||||
const auto IntArrayJson = [](const int32 Values[9])
|
||||
{
|
||||
FString Json = TEXT("[");
|
||||
for (int32 Index = 0; Index < 9; ++Index)
|
||||
{
|
||||
if (Index > 0) Json += TEXT(",");
|
||||
Json += FString::FromInt(Values[Index]);
|
||||
}
|
||||
Json += TEXT("]");
|
||||
return Json;
|
||||
};
|
||||
const double SchedulerTraceNow = FPlatformTime::Seconds();
|
||||
const uint64 TotalWorkerCycles = TotalGenerationWorkerTaskCycles.load(std::memory_order_relaxed);
|
||||
const uint64 WorkerCyclesDelta = TotalWorkerCycles >= LastStreamingSchedulerWorkerCycles
|
||||
? TotalWorkerCycles - LastStreamingSchedulerWorkerCycles : 0;
|
||||
const double IntervalStart = LastStreamingSchedulerSampleSeconds > 0.0
|
||||
? LastStreamingSchedulerSampleSeconds : StreamingUpdateStartSeconds;
|
||||
const double IntervalSeconds = FMath::Max(0.0, SchedulerTraceNow - IntervalStart);
|
||||
const double BusySeconds = FPlatformTime::ToSeconds64(WorkerCyclesDelta);
|
||||
VoxelForgeStartupTrace::RecordEvent(TEXT("scheduler_admission"), FString::Printf(
|
||||
TEXT("\"frame\":%llu,\"center\":[%d,%d,%d],\"desired_rebuilt\":%d,"
|
||||
"\"submission_loop_ran\":%d,\"all_chunks_loaded\":%d,"
|
||||
"\"walkable_ready\":%d,\"moving\":%d,"
|
||||
"\"core_region_compile_pending\":%d,\"core_ready\":%d,\"coarse_first\":%d,"
|
||||
"\"dynamic_near_reserve\":%d,\"legacy_pending_result_cap\":%d,"
|
||||
"\"max_tasks\":%d,\"near_reserve\":%d,"
|
||||
"\"coarse_cap\":%d,\"first_coarse_level\":%d,"
|
||||
"\"submission_stop_level\":%d,\"submission_stop_cap\":%d,"
|
||||
"\"pending_entry\":%d,\"pending_after\":%d,\"admission_occupancy\":%d,"
|
||||
"\"active_task_guard\":%d,\"running_workers\":%d,\"pending_results_estimate\":%d,"
|
||||
"\"worker_busy_s_interval\":%s,\"worker_busy_avg\":%s,"
|
||||
"\"desired_missing_by_lod\":%s,\"loaded_by_lod\":%s,\"pending_by_lod\":%s,"
|
||||
"\"submitted_by_lod\":%s,\"load_attempts_by_lod\":%s,"
|
||||
"\"admission_cap_by_lod\":%s,\"declined_rule_by_lod\":%s,"
|
||||
"\"level_cap_returns_by_lod\":%s,\"level_cap_breaks_by_lod\":%s,"
|
||||
"\"full_cap_declined_tiles\":%d,\"reserve_declined_tiles\":%d,"
|
||||
"\"inner_load_declined_tiles\":%d,\"critical_full_cap_breaks\":%d,"
|
||||
"\"dirty_full_cap_breaks\":%d,\"band_coarse_cap_breaks\":%d"),
|
||||
static_cast<unsigned long long>(GFrameCounter), CenterChunk.X, CenterChunk.Y, CenterChunk.Z,
|
||||
bDesiredRebuiltThisUpdate ? 1 : 0, bSubmissionLoopRan ? 1 : 0, bAllChunksLoaded ? 1 : 0,
|
||||
bWalkableRingReadyForReserve ? 1 : 0, bPawnMoving ? 1 : 0,
|
||||
bCoreRegionCompilePending ? 1 : 0, IsVoxelForgeCoreReady() ? 1 : 0,
|
||||
GVoxelForgeStreamingCoarseFirst, GVoxelForgeStreamingDynamicNearReserve,
|
||||
GVoxelForgeStreamingLegacyPendingResultCap, MaxTasks,
|
||||
NearTaskReserve, CoarseTaskCap, FirstCoarseLevel,
|
||||
SchedulerOrderStopLevel, SchedulerOrderStopCap,
|
||||
PendingTilesAtUpdateEntry, PendingTiles.Num(), AdmissionOccupancy, ActiveTaskGuard,
|
||||
ActiveGenerationWorkerCount.load(std::memory_order_relaxed), PendingResultsEstimate,
|
||||
*FString::SanitizeFloat(BusySeconds),
|
||||
*FString::SanitizeFloat(IntervalSeconds > 0.0 ? BusySeconds / IntervalSeconds : 0.0),
|
||||
*IntArrayJson(DesiredMissingByLevel), *IntArrayJson(LoadedByLevel),
|
||||
*IntArrayJson(PendingByLevel), *IntArrayJson(SchedulerSubmittedByLevel),
|
||||
*IntArrayJson(SchedulerLoadAttemptsByLevel), *IntArrayJson(AdmissionCapByLevel),
|
||||
*IntArrayJson(DeclinedRuleByLevel), *IntArrayJson(SchedulerLevelCapReturns),
|
||||
*IntArrayJson(SchedulerLevelCapBreaks), FullCapDeclinedTiles, ReserveDeclinedTiles,
|
||||
InnerLoadDeclinedTiles, SchedulerCriticalFullCapBreaks,
|
||||
SchedulerDirtyFullCapBreaks, SchedulerBandCoarseCapBreaks));
|
||||
LastStreamingSchedulerSampleSeconds = SchedulerTraceNow;
|
||||
LastStreamingSchedulerWorkerCycles = TotalWorkerCycles;
|
||||
}
|
||||
else if (bTraceStreamingScheduler)
|
||||
{
|
||||
// Report a loaded latch only if desired work is still unadmitted; the normal no-missing path stays quiet.
|
||||
// Signaler le verrou charge seulement s'il reste du travail desire non admis.
|
||||
for (const FVoxelTileKey& Tile : DesiredSorted)
|
||||
{
|
||||
if (!PendingTiles.Contains(Tile) && !LoadedTiles.Contains(Tile))
|
||||
{
|
||||
RecordSchedulerSkip(TEXT("all_chunks_loaded_latch"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11641,7 +11923,7 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
|
||||
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
|
||||
? CoarseTaskCap : MaxTasks;
|
||||
if (GetActiveGenerationTaskCount() >= AdmissionCap)
|
||||
if (GetGenerationAdmissionCount() >= AdmissionCap)
|
||||
{
|
||||
if (GVoxelForgeProfileTileGeneration != 0
|
||||
&& !GVoxelForgeGenerationCapHitReported)
|
||||
@@ -11812,8 +12094,18 @@ void AVoxelWorld::LoadTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority Wor
|
||||
struct FWorkerGuard
|
||||
{
|
||||
std::atomic<int32>& Counter;
|
||||
~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||
} WorkerGuard{ActiveGenerationWorkerCount};
|
||||
std::atomic<uint64>& BusyCycles;
|
||||
uint64 StartedCycles;
|
||||
~FWorkerGuard()
|
||||
{
|
||||
const uint64 EndedCycles = FPlatformTime::Cycles64();
|
||||
if (EndedCycles >= StartedCycles)
|
||||
{
|
||||
BusyCycles.fetch_add(EndedCycles - StartedCycles, std::memory_order_relaxed);
|
||||
}
|
||||
Counter.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
} WorkerGuard{ActiveGenerationWorkerCount, TotalGenerationWorkerTaskCycles, WorkerTaskStartCycles};
|
||||
|
||||
FChunkResult Result;
|
||||
Result.WorkerTaskStartCycles = WorkerTaskStartCycles;
|
||||
@@ -11942,7 +12234,7 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
const int32 CoarseTaskCap = FMath::Max(1, MaxTasks - NearReserve);
|
||||
const int32 AdmissionCap = WorkPriority == EVoxelTileWorkPriority::Horizon
|
||||
? CoarseTaskCap : MaxTasks;
|
||||
if (GetActiveGenerationTaskCount() >= AdmissionCap) return;
|
||||
if (GetGenerationAdmissionCount() >= AdmissionCap) return;
|
||||
|
||||
PendingTiles.Add(Tile);
|
||||
const TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe> Cancellation =
|
||||
@@ -11987,8 +12279,18 @@ void AVoxelWorld::LoadCoreTile(const FVoxelTileKey& Tile, EVoxelTileWorkPriority
|
||||
struct FWorkerGuard
|
||||
{
|
||||
std::atomic<int32>& Counter;
|
||||
~FWorkerGuard() { Counter.fetch_sub(1, std::memory_order_relaxed); }
|
||||
} WorkerGuard{ActiveGenerationWorkerCount};
|
||||
std::atomic<uint64>& BusyCycles;
|
||||
uint64 StartedCycles;
|
||||
~FWorkerGuard()
|
||||
{
|
||||
const uint64 EndedCycles = FPlatformTime::Cycles64();
|
||||
if (EndedCycles >= StartedCycles)
|
||||
{
|
||||
BusyCycles.fetch_add(EndedCycles - StartedCycles, std::memory_order_relaxed);
|
||||
}
|
||||
Counter.fetch_sub(1, std::memory_order_relaxed);
|
||||
}
|
||||
} WorkerGuard{ActiveGenerationWorkerCount, TotalGenerationWorkerTaskCycles, WorkerTaskStartCycles};
|
||||
|
||||
auto Enqueue = [this, WorkPriority](FChunkResult&& Result)
|
||||
{
|
||||
|
||||
@@ -443,7 +443,12 @@ public:
|
||||
* capped to (logical cores − 2) so small CPUs don't thrash on a flat 16 (background
|
||||
* priority stops frame starvation, not the context-switch overhead). */
|
||||
int32 GetMaxConcurrentTasks() const;
|
||||
/** Submitted generation tasks still queued or running; completed MPSC results do not occupy admission.
|
||||
* Taches de generation soumises, en file ou en cours ; les resultats MPSC termines ne bloquent pas l'admission. */
|
||||
int32 GetActiveGenerationTaskCount() const;
|
||||
/** Count queued/running work for admission; the legacy CVar can include finished results awaiting apply.
|
||||
* Compter le travail en file/en cours ; le CVar de compatibilite peut inclure les resultats finis en attente. */
|
||||
int32 GetGenerationAdmissionCount() const;
|
||||
/** Reserve task slots for collision-critical/edit/LOD0 work; far work uses the remainder. */
|
||||
int32 GetNearTaskReserve(int32 MaxTasks) const;
|
||||
|
||||
@@ -1093,7 +1098,9 @@ public:
|
||||
TQueue<FChunkResult, EQueueMode::Mpsc> EditedProcessQueue;
|
||||
TQueue<FChunkResult, EQueueMode::Mpsc> NearProcessQueue;
|
||||
TQueue<FChunkResult, EQueueMode::Mpsc> ProcessQueue;
|
||||
TSet<FVoxelTileKey> PendingTiles; // tiles with a gen task in flight
|
||||
// Keys stay pending from task submission until the game thread applies the result; completed results can wait here.
|
||||
// Les cles restent en attente de la soumission jusqu'a l'application sur le thread de jeu ; les resultats termines y attendent.
|
||||
TSet<FVoxelTileKey> PendingTiles;
|
||||
// Game-thread-owned map; workers retain the shared token and only read its atomic flag.
|
||||
TMap<FVoxelTileKey, TSharedPtr<FVoxelTileCancellationState, ESPMode::ThreadSafe>>
|
||||
PendingTileCancellation;
|
||||
@@ -1121,7 +1128,8 @@ public:
|
||||
// Active pendant les mutations de génération lancées par l'éditeur, sans signifier la destruction.
|
||||
std::atomic<bool> bGenerationPaused{false};
|
||||
|
||||
// Number of async tasks currently running — EndPlay waits for this to reach 0
|
||||
// Submitted async tasks (including UE-queued work) — EndPlay waits for this to reach 0.
|
||||
// Taches asynchrones soumises (y compris la file UE) — EndPlay attend que le compteur revienne a 0.
|
||||
std::atomic<int32> ActiveTaskCount{0};
|
||||
// Active workers are counted inside the launched tile task, distinct from tasks queued by UE.
|
||||
// Les travailleurs actifs sont comptes dans la tache de tuile, distincts des taches en attente UE.
|
||||
@@ -1199,6 +1207,11 @@ public:
|
||||
int32 EffectiveNearTaskReserve = -1;
|
||||
uint64 NextStreamingRequestOrdinal = 0;
|
||||
std::atomic<uint64> NextStreamingGenerationOrdinal{0};
|
||||
// Cumulative worker-task wall cycles avoid aliasing the instantaneous per-frame counter.
|
||||
// Les cycles cumules des taches evitent l'aliasing du compteur instantane echantillonne par frame.
|
||||
std::atomic<uint64> TotalGenerationWorkerTaskCycles{0};
|
||||
double LastStreamingSchedulerSampleSeconds = 0.0;
|
||||
uint64 LastStreamingSchedulerWorkerCycles = 0;
|
||||
|
||||
struct FReadyTransitionVisibilitySample
|
||||
{
|
||||
|
||||
@@ -1283,3 +1283,30 @@ S5. The new trace, regressions, run ledger, exact log-captured command lines, ha
|
||||
spots are in [WorldAssemblerM3/Round10/REPORT.md](../../WorldAssemblerM3/Round10/REPORT.md).
|
||||
|
||||
— Luna
|
||||
|
||||
|
||||
## World assembler S7 / M3 Round 11 — 2026-10-19
|
||||
|
||||
The BuildHost trace counted the old scheduler declines: after LOD0 completed, coarse work
|
||||
hit the four-slot near reserve for 877 frames; 806 also had finished results waiting to
|
||||
apply and counted against the old cap. The shared dynamic reserve and active-task count cut
|
||||
the cold 2,578-key set from 17.527 s to 7.780 s. The walkable ring is 0.581 s and full
|
||||
LOD0 is 1.439 s, both inside the requested limits. The full set still misses the roughly
|
||||
4.2 s worker-seconds/10-slots estimate, and measured worker task-seconds rose about 10%,
|
||||
so this is a throughput win with a tile-cost question left to profile. The moving trace also
|
||||
shows a separate `bCoreRegionCompilePending` gate: 10 legacy and 11 dynamic submission passes
|
||||
were rejected during region rebuilds despite free slots.
|
||||
|
||||
W1/W8/W16 determinism, Core-off, M2 fixtures, SurfaceWorld/FloatingIslands, S3 ground
|
||||
agreement and S4 cap coverage passed. Three Standalone captures are saved, but their window
|
||||
was hidden/occluded and they do not sign off player-visible enclosure. I reviewed RMC's
|
||||
update batching, async collision and render-proxy paths; no change was clearly worth the
|
||||
measured apply cost this round. Full commands, hashes, RMC source references, quality
|
||||
limits and the async audit are in [WorldAssemblerM3/Round11/REPORT.md](../../WorldAssemblerM3/Round11/REPORT.md).
|
||||
|
||||
No new source file was added. The likely compile spots are `VoxelWorld.cpp` and
|
||||
`VoxelWorld.h` in the VoxelForge runtime module; the harness controls changed in
|
||||
`Tools/TestVoxelForgeCoreWorldHost.ps1`. No owner project/editor, asset or external plugin
|
||||
file was modified. No commit, push or stash was made.
|
||||
|
||||
— Luna
|
||||
@@ -0,0 +1,261 @@
|
||||
# World assembler S7 / M3 Round 11 — scheduler admission and RMC review
|
||||
|
||||
**Result:** the cold full set fell from 17.595 s on the S7 legacy control to 7.762 s with
|
||||
the shared scheduler change. The 0.581 s walkable ring and 1.437 s full LOD0 meet the
|
||||
0.63 s and 2.25 s targets. The 7.76 s full set still misses the roughly 4.2 s
|
||||
worker-seconds / 10-slots estimate. RMC section-update batching was reviewed but not
|
||||
implemented: measured game-thread tile apply is only about 0.24 s for the whole set, and
|
||||
the null-RHI run cannot measure render-proxy cost.
|
||||
|
||||
## 1. What the trace says
|
||||
|
||||
`UpdateChunksAroundPosition` runs each world tick. For the 2,578-key spawn, the per-frame
|
||||
trace found the decline after that call, in admission accounting:
|
||||
|
||||
| Admission evidence | Count / example | Meaning |
|
||||
|---|---:|---|
|
||||
| Legacy near reserve after no LOD0 keys remained, with LOD1+ still missing | 876 frames | The four reserved near slots stayed unused while coarse keys were eligible. |
|
||||
| Those frames with a positive finished-result estimate | 806 / 877 | `PendingTiles` included completed results waiting for game-thread apply. |
|
||||
| Representative legacy frame | t=3.292474 s: active=4, completed estimate=2, cap occupancy=6/10 | The old pending-result count hit the coarse cap although only four tasks were queued/running. |
|
||||
| Dynamic admission passes before the initial full-set milestone | 409 | Ten ended with missing keys and a post-pass sample below the 10-task count. All ten recorded a submission stop at cap 10. |
|
||||
| Declines in those ten dynamic passes | 0 reserve, 0 all-loaded, 0 walkable-gate failure, 0 inner `LoadTile` rejection | A worker could finish between the full-cap stop and the end-of-pass sample, leaving occupancy at 9/10; the stop fields show that the pass had hit cap 10. |
|
||||
|
||||
The two old blockers were independent: the near reserve capped coarse admission at six,
|
||||
and completed-but-unapplied results occupied the old `PendingTiles` cap. The scheduler now
|
||||
counts `ActiveTaskCount` (queued and running tasks); `PendingTiles` remains through apply
|
||||
for duplicate suppression and result bookkeeping. The default `voxel.StreamingDynamicNearReserve=1`
|
||||
keeps the near reserve while moving or before the stationary walkable gate, then releases
|
||||
it to coarse work. `voxel.StreamingLegacyPendingResultCap=1` and the harness legacy mode
|
||||
restore both old behaviors for the negative control.
|
||||
|
||||
Across the complete 45 s trace (including movement from t=30 s), the end-of-update records
|
||||
contained both missing keys and observed capacity below 10 in 1,236 legacy and 280 dynamic
|
||||
frames. Per-LOD reason codes classify the legacy set as reserve in 1,226 frames and inner
|
||||
loader return in 10. The dynamic trace has reserve in 259, full-cap stop in 12 and inner
|
||||
loader return in 11; codes can overlap when different LODs hit different rules in one
|
||||
update. No free-capacity frame had an unclassified missing LOD. The four additional legacy
|
||||
`scheduler_skip` records with missing keys were region-compile gates, described below.
|
||||
In `declined_rule_by_lod`, codes are 1=full task cap, 2=near reserve, 3=all-loaded latch,
|
||||
4=inner `LoadTile`/`LoadCoreTile` return, and 5=work still missing with apparent capacity.
|
||||
Dynamic code-2 frames include the intentional near reserve held while moving; the trace
|
||||
keeps this distinct from a worker-pool or result-accounting stall.
|
||||
|
||||
Admission itself is no longer the measured source of idle slots on the dynamic spawn: each
|
||||
pass submitted work or recorded a stop at the 10-task cap. The ten end-of-pass samples that
|
||||
showed missing keys and occupancy 9/10 had already recorded that full-cap stop; a worker
|
||||
finished before the trace snapshot. Cumulative task intervals show
|
||||
5.63 busy-worker equivalents over first request to ready, with eight sustained overlapping
|
||||
tile tasks; one zero-duration endpoint tie counts nine. This does not identify whether the
|
||||
remaining gap comes from UE task-pool dispatch or work within a tile. Boundary samples of
|
||||
`running_workers` alias short task bursts and are not a time average.
|
||||
|
||||
The same 45 s actor run also includes a +Y move beginning at t=30 s. On that moving path,
|
||||
`LoadCoreTile`'s `bCoreRegionCompilePending` early return (`VoxelWorld.cpp:12224`) is a
|
||||
separate gate. The trace recorded four top-level region-compile skips in the legacy control
|
||||
with 116–187 desired keys missing and only 0–1 active tasks. It also recorded 10 legacy and
|
||||
11 dynamic admission passes with inner load declines; every one had
|
||||
`core_region_compile_pending=1`, zero submissions, and 108–190 legacy or 92–144 dynamic tile
|
||||
attempts.
|
||||
This explains idle slots during those region rebuilds, but it occurs after the cold initial
|
||||
set and is separate from the reserve/result-count fix. The initial 20 `core_not_ready`
|
||||
skips had zero missing desired keys; no all-loaded latch skip had missing work.
|
||||
|
||||
## 2. Matched BuildHost result
|
||||
|
||||
Fresh sequential null-RHI processes used the staged owner `SurfaceWorld` plan, seed 0,
|
||||
25 cm Core scale, 2,578 desired keys, 10 actor task slots, a manually ticked world at 16 ms,
|
||||
and a +Y 600 cm/s move beginning at 30 s. Apply boost and coarse-first were off in both S7
|
||||
runs. “Idle slot-s” is `10 × (first request to full ready) − cumulative worker task time`;
|
||||
busy equivalents use the same interval. S6's 3.27 average was sampled only on frames with
|
||||
pending results and is not directly comparable to the integrated S7 value.
|
||||
|
||||
| Run | Full set | Walkable 9/9 ring | Full LOD0 (512) | Worker task-s | Integrated busy equivalents | Idle slot-s | Tick spawn p95/max | Tick walk p95/max |
|
||||
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
||||
| S6 legacy reference | 17.412 s | 0.632 s | 2.252 s | 37.64 | 3.27 sampled on pending-result frames | 132.1 | 1.406 / 5.059 ms | 1.771 / 29.247 ms |
|
||||
| S7 old-path negative control | 17.527 s | 0.650 s | 2.281 s | 37.696 | 2.208 | 133.012 | 1.437 / 5.286 ms | 1.957 / 29.244 ms |
|
||||
| S7 dynamic reserve + active-task cap | **7.780 s** | **0.581 s** | **1.439 s** | 41.364 | **5.634** | **32.056** | 2.262 / 5.230 ms | 2.512 / 28.893 ms |
|
||||
|
||||
The negative control reproduces S6 legacy within 0.7% in full-set time and 0.2% in worker
|
||||
task-seconds. Dynamic admission cuts full-set time by 55.3%, reduces idle slots by about
|
||||
76% against S6, and meets both near-work targets. It does not meet the roughly 4.14 s
|
||||
`41.364 / 10` estimate. Measured worker task time rose about 9.7% versus the S7 negative
|
||||
control (about 16.04 ms per initial tile versus 14.62 ms). Geometry hashes did not change
|
||||
and the density/mesh recipe was untouched; the trace measures task elapsed time, not CPU
|
||||
cycles, so it cannot separate contention from other elapsed-time variation. This is a
|
||||
measured tile-cost increase to watch in a rendered/profiled run, not evidence of a content
|
||||
quality change.
|
||||
|
||||
The largest walk `UWorld::Tick` sample remains about 29 ms. The trace wraps aggregate
|
||||
`TestWorld->Tick()` at `VoxelForgeExploreCommandlet.cpp:17495–17501`; it has no subsystem
|
||||
marker at the maximum, so the owner cannot be named from this trace. Null RHI also means
|
||||
these are not rendered frame times.
|
||||
|
||||
## 3. RealtimeMeshComponent review
|
||||
|
||||
VoxelForge creates plain `FRealtimeMeshStreamSet` data in `BuildTileStreamSet`
|
||||
(`Source/VoxelForge/Private/VoxelWorld.cpp:616`, called from worker paths at 12441 and
|
||||
12876). Component mutation stays on the game thread in `ApplyMeshToTile` (13064): each tile
|
||||
removes and recreates one section group and sets its material slots (13273–13288). Ground
|
||||
and cap are material/polygroup sections in that tile group; the world uses clipmap tiles,
|
||||
not an RMC multi-LOD chain. The whole cold set's measured game-thread apply was about
|
||||
0.24 s in S6 (roughly 3% of the S7 7.76 s wall time).
|
||||
|
||||
Ranked opportunities, with this round's evidence:
|
||||
|
||||
1. **Batch a tile's RMC writes.** RMC's `Public/Data/RealtimeMeshUpdateBuilder.h:390–406`
|
||||
documents grouping mesh, LOD, section-group, section and collision updates behind one
|
||||
write lock and proxy publish; `Private/Data/RealtimeMeshUpdateBuilder.cpp:222` commits,
|
||||
with render work queued at 64–67. This could trim game-thread update overhead, but the
|
||||
measured apply ceiling is only ~0.24 s for 2,578 tiles. It is not the cold-set floor.
|
||||
2. **Measure fast collision cooking.** RMC `Public/Core/RealtimeMeshCollision.h:32–55`
|
||||
exposes async cook, fast cook, complex-as-simple, normal flipping, deformable collision
|
||||
and mesh merging. The current BuildHost trace reports async=1, complex-as-simple=1,
|
||||
fast-cook=0, merge=0; async cooking is dispatched in
|
||||
`Private/RealtimeMeshManaged.cpp:695–707`. Cook CPU is not isolated by the existing
|
||||
submit-to-body-update measurement, and changing cook mode could affect collision
|
||||
quality, so defer until collision timing and walkability are audited together.
|
||||
3. **Batch across tiles / share mesh components.** This might reduce component and proxy
|
||||
overhead, but RMC builder commits target a mesh and VoxelForge's per-tile collision,
|
||||
unload and transition ownership would change. Larger risk than the measured apply cost.
|
||||
4. **Use RMC's LOD/section-group structure more broadly.** RMC has explicit LOD and section
|
||||
group update operations (builder header above); VoxelForge currently creates one group
|
||||
per tile and changes clipmap tiles between LODs. A multi-LOD mesh would be an architectural
|
||||
change, with no null-RHI timing evidence for a payoff.
|
||||
5. **Render resource/proxy work.** `Private/RenderProxy/RealtimeMeshGPUBuffer.cpp:49–60`
|
||||
creates resources asynchronously only when the RHI supports multithreaded resources;
|
||||
otherwise it falls back to the render thread. RMC's proxy publish path is in the builder
|
||||
implementation above. Null RHI cannot measure either path, so no change was made.
|
||||
|
||||
No RMC files were modified. Its source was read only at
|
||||
`E:\Projet Unreal\VoxelM\Plugins\RealtimeMeshComponent`.
|
||||
|
||||
## 4. Quality and determinism
|
||||
|
||||
- Standalone W1/W8/W16 produce identical full and LOD0–8 hashes: density
|
||||
`c10697a8f48380c7`, mesh `430d2be2bdd8b988`, payload `b7b33e14092225c9`, topology
|
||||
`c5c46d66606a819e`. The parser's first ad-hoc comparison was corrected; a direct per-LOD
|
||||
comparison shows all nine levels equal.
|
||||
- BuildHost SurfaceWorld at 25 cm passed the full actor/collision/anchor/carve check and
|
||||
W1/W8/W16 matrix. Hashes: density `0cd2a1a4152ee4aa`, mesh `80a5b054b904b1a2`, payload
|
||||
`bf2f1b40dff38acc`, topology `56074198682d5e18`.
|
||||
- Core-off legacy at 12.5 cm passed. M2 V1 and V2 each passed five fixtures plus same-LOD
|
||||
border checks. FloatingIslands at 25 cm passed W1/W8/W16, fixed water, edits, collision,
|
||||
LOD0 and LOD1.
|
||||
- S3 ground / S4 LOD7–8 agreement passed: maximum ground delta 0.0427 cm, cross-LOD delta
|
||||
0.0 cm, and the 100 cm datum-offset negative control was detected.
|
||||
- S4 Standalone far-cap audit passed: 385/385 requested cap bands emitted; 128 agreement
|
||||
points had 0.0504 cm maximum error against 2 cm tolerance; the 100 cm bad-datum control
|
||||
was detected; 2,520/2,520 rays covered all 35 view points.
|
||||
- Three Standalone spawn captures are under `Saved/BuildHost/S7/captures/`; each is
|
||||
4,097,178 bytes. Sampled terrain surfaces show no obvious opening. The capture window
|
||||
reported hidden/occluded, so these back-buffer submissions are not an owner-visible
|
||||
enclosure sign-off. The standalone does not mirror the shared Unreal actor scheduler.
|
||||
[Frame 00](../../../../Saved/BuildHost/S7/captures/s7_spawn_view_00.png),
|
||||
[frame 01](../../../../Saved/BuildHost/S7/captures/s7_spawn_view_01.png), and
|
||||
[frame 02](../../../../Saved/BuildHost/S7/captures/s7_spawn_view_02.png).
|
||||
- Scheduler changes did not alter tile output hashes, so the recipe/version is unchanged.
|
||||
Water remains fixed for the season; player carve/fill remains unrestricted.
|
||||
|
||||
## 5. Build, commands and run record
|
||||
|
||||
BuildHost compilation passed with 12 actions and normal priority:
|
||||
|
||||
```powershell
|
||||
& '.\Tools\BuildVoxelForgeCoreHost.ps1' -MaxParallelActions 12 -TimeoutSeconds 900
|
||||
```
|
||||
|
||||
The first build attempt failed on duplicate `TraceNow` declarations (`C2374` / `C2086`); I
|
||||
removed the duplicate and rebuilt successfully. The final build's async-safety checks passed.
|
||||
The standalone full build passed:
|
||||
|
||||
```powershell
|
||||
& '.\Standalone\Build.ps1' -Target All -CompileWorkers 12 -BuildPriority Normal
|
||||
```
|
||||
|
||||
It warned that `vswhere.exe` was not recognized and reported existing C4838 conversions in
|
||||
`VoxelStrateManager.cpp:3974–3975`; it still completed successfully.
|
||||
|
||||
BuildHost quality commands (all passed):
|
||||
|
||||
```powershell
|
||||
& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -CoreOffTest -CoreVoxelSizeCm 12.5 -TimeoutSeconds 900
|
||||
& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -HybridFixturesOnly -CoreMesherRecipe WorldAssemblerHybridV1 -CoreVoxelSizeCm 25 -TimeoutSeconds 900
|
||||
& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -HybridFixturesOnly -CoreMesherRecipe WorldAssemblerHybridV2 -CoreVoxelSizeCm 25 -TimeoutSeconds 900
|
||||
& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -FloatingIslandsOnly -CoreVoxelSizeCm 25 -TimeoutSeconds 900
|
||||
& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -CoreVoxelSizeCm 25 -TimeoutSeconds 900
|
||||
```
|
||||
|
||||
The final extra trace fields only report Core readiness/region-compile state; they do not
|
||||
change tile generation. After adding them I rebuilt and reran both matched actor modes.
|
||||
|
||||
Matched BuildHost actor commands:
|
||||
|
||||
```powershell
|
||||
& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Legacy -CoreVoxelSizeCm 25 -TimeoutSeconds 900
|
||||
& '.\Tools\TestVoxelForgeCoreWorldHost.ps1' -StreamingTraceOnly -StartupApplyBoost 0 -StreamingOrder Legacy -NearReserveMode Dynamic -CoreVoxelSizeCm 25 -TimeoutSeconds 900
|
||||
```
|
||||
|
||||
Standalone W1/W8/W16 command template (substitute `1`, `8`, `16`; each was a fresh process):
|
||||
|
||||
```powershell
|
||||
Standalone\bin\VoxelForgeCoreStandalone.exe --workers=1 --world-assembler-prototype --stream-benchmark --stream-hash --force-generate --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --process-priority=Normal --process-affinity=4095 --frame-limit=60
|
||||
```
|
||||
|
||||
Standalone S4 audit command:
|
||||
|
||||
```powershell
|
||||
Standalone\bin\VoxelForgeCoreStandalone.exe --world-assembler-audit --workers=12 --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --process-priority=Normal --process-affinity=4095 --frame-limit=60
|
||||
```
|
||||
|
||||
The first capture attempt added `--stream-benchmark` to the viewer capture command. It exited
|
||||
0 but generated no PNG because the benchmark path exits before the viewer capture loop. The
|
||||
corrected command was:
|
||||
|
||||
```powershell
|
||||
Standalone\bin\VoxelForgeCoreStandalone.exe --world-assembler-prototype --force-generate --workers=12 --clip-radius=3 --max-clip-level=8 --camera=0,-35,-487.94,0,-22 --process-priority=Normal --process-affinity=4095 --frame-limit=60 --capture-spawn-sequence-prefix=E:\Projet Unreal\VoxelM\Plugins\VoxelForge\Saved\BuildHost\S7\captures\s7_spawn_view --quit-after-spawn-captures
|
||||
```
|
||||
|
||||
The three capture outputs and standalone logs are under `Saved/BuildHost/S7/`. BuildHost
|
||||
stdout and JSONL traces are under
|
||||
`Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/`; the
|
||||
expanded Unreal command lines are preserved in those stdout logs: [legacy stdout](../../../../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation-owner_surface_stream_trace_only_applyboost_0_legacy_order_nearreserve_legacy_trace.stdout.log),
|
||||
[legacy JSONL](../../../../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation-owner_surface_stream_trace_only_applyboost_0_legacy_order_nearreserve_legacy_trace.trace.jsonl),
|
||||
[dynamic stdout](../../../../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation-owner_surface_stream_trace_only_applyboost_0_legacy_order_nearreserve_dynamic_trace.stdout.log),
|
||||
and [dynamic JSONL](../../../../Saved/BuildHost/HostProject/Plugins/VoxelForge/Saved/CoreWorldValidation/Logs/CoreWorldValidation-owner_surface_stream_trace_only_applyboost_0_legacy_order_nearreserve_dynamic_trace.trace.jsonl). The legacy/dynamic pair
|
||||
used the wrapper commands above, staged `DA_Settings`, `-nullrhi -unattended -nop4
|
||||
-nosplash -NoSound -corelimit=12 -run=VoxelForgeExplore -seed=0 -archetype=TunnelNetwork
|
||||
-coreworldtest=1 -corevoxelsizecm=25 -settingsref=/Game/VoxelForge/DA_Settings.DA_Settings
|
||||
-coreworldstreamtrace=1 -voxel.TestMove=1 -voxel.TestMoveSpeedCmPerSecond=600
|
||||
-voxel.TestMoveStartSeconds=30 -voxel.TestMoveDirection=0,1,0
|
||||
-voxel.StartupTraceUntilEndPlay=1 -voxel.CollisionGate=0`, and the recorded mode CVars.
|
||||
|
||||
SHA-256 of the executables/modules used for these runs:
|
||||
|
||||
| Binary | SHA-256 |
|
||||
|---|---|
|
||||
| UE 5.7 `UnrealEditor-Cmd.exe` | `ECCE9996191F7F7E69B3D104EECDD0E678AE69176FDCCD79828C3E75A0654D6D` |
|
||||
| BuildHost `UnrealEditor-VoxelForge.dll` | `29CFC3AB0BDF0B6048029660160D38F8AC3E9FF59ED057BD06D4CA42C3FFBBD2` |
|
||||
| BuildHost `UnrealEditor-VoxelForgeCore.dll` | `54259552340FE81EA05640C622E9E5E5C0AC68C95F87DC718326D25EF133B2AC` |
|
||||
| BuildHost `UnrealEditor-VoxelForgeEditor.dll` | `5FF5FCF82EC59A61319CBF979EC9A7BA081971B4D35088082BBF7A81C4C4794D` |
|
||||
| `Standalone/bin/VoxelForgeCoreStandalone.exe` | `0F1610730D9BC038E9E60B5ECBF84BA8AD562748E2737F7C1C288F8CE0FE0CED` |
|
||||
|
||||
All runs were sequential at normal priority, with at most 12 build/Standalone workers;
|
||||
BuildHost tests used the harness's 12-core limit. Free memory was at least 17.4 GB before
|
||||
each actor launch; the largest BuildHost quality process stayed below 3.58 GB (the final A/B
|
||||
actors stayed below 3.53 GB; Standalone capture below 1.14 GB). No process exceeded 8 GB.
|
||||
The WMI memory query was denied, so preflight used `GlobalMemoryStatusEx`. No new `UECC-*`
|
||||
folder or `0xC0000142` occurred; the
|
||||
only crash directory is the pre-existing 2026-09-17 `UECC-Windows-F1A3EFD5469F5DD62E68EBBACAC84376_0000`.
|
||||
|
||||
Async diff audit: `GenerationEpoch` still rejects stale work; the result queues remain
|
||||
`EQueueMode::Mpsc`; `EndPlay` sets shutdown, waits for tasks and drains queues; workers check
|
||||
`bShuttingDown`; `ModsLock` is unchanged; UObject and RMC component mutation remains on the
|
||||
game thread. No generated header, `Binaries/`, or `Intermediate/` source was edited. No new
|
||||
source file was added. Modified compile spots are `Source/VoxelForge/Private/VoxelWorld.cpp`
|
||||
(CVar setup, admission accounting, task lifetime accounting and trace) and
|
||||
`Source/VoxelForge/Public/VoxelWorld.h` (scheduler/task bookkeeping comments and declarations);
|
||||
the expected compile target is the VoxelForge runtime module for Win64 Development in the
|
||||
BuildHost. `Tools/TestVoxelForgeCoreWorldHost.ps1` changed only the harness controls.
|
||||
|
||||
No owner project/editor or asset was opened, built or edited. RealtimeMeshComponent was
|
||||
read-only; no external source was changed. No commit, push or stash was made.
|
||||
@@ -3,6 +3,7 @@ param(
|
||||
[ValidateRange(60, 900)][int]$TimeoutSeconds = 900,
|
||||
[ValidateRange(12.5, 25.0)][double]$CoreVoxelSizeCm = 25.0,
|
||||
[ValidateSet('CoarseFirst', 'Legacy')][string]$StreamingOrder = 'Legacy',
|
||||
[ValidateSet('Auto', 'Legacy', 'Dynamic')][string]$NearReserveMode = 'Auto',
|
||||
[switch]$EnforceCollisionGate,
|
||||
[switch]$StartupTrace,
|
||||
[ValidateRange(0, 1)][int]$StartupApplyBoost = 0,
|
||||
@@ -145,6 +146,11 @@ if ($SharedFeatureAuditOnly) {
|
||||
$RunName = 'shared_feature_audit'
|
||||
}
|
||||
$RunName += if ($StreamingOrder -eq 'Legacy') { '_legacy_order' } else { '_coarse_first' }
|
||||
$RunName += switch ($NearReserveMode) {
|
||||
'Legacy' { '_nearreserve_legacy' }
|
||||
'Dynamic' { '_nearreserve_dynamic' }
|
||||
default { '_nearreserve_auto' }
|
||||
}
|
||||
if ($EnforceCollisionGate) { $RunName += '_collision_gate' }
|
||||
if ($StartupTrace -or $StreamingTraceOnly) { $RunName += '_trace' }
|
||||
|
||||
@@ -262,9 +268,15 @@ $BaseArguments = @(
|
||||
('-out="{0}"' -f $OutputDirectory)
|
||||
)
|
||||
$CoarseFirstValue = if ($StreamingOrder -eq 'CoarseFirst') { 1 } else { 0 }
|
||||
$DynamicReserveValue = if ($StreamingOrder -eq 'CoarseFirst') { 1 } else { 0 }
|
||||
$DynamicReserveValue = switch ($NearReserveMode) {
|
||||
'Legacy' { 0 }
|
||||
'Dynamic' { 1 }
|
||||
default { 1 }
|
||||
}
|
||||
$LegacyPendingResultCapValue = if ($NearReserveMode -eq 'Legacy') { 1 } else { 0 }
|
||||
$BaseArguments += "-voxel.StreamingCoarseFirst=$CoarseFirstValue"
|
||||
$BaseArguments += "-voxel.StreamingDynamicNearReserve=$DynamicReserveValue"
|
||||
$BaseArguments += "-voxel.StreamingLegacyPendingResultCap=$LegacyPendingResultCapValue"
|
||||
$BaseArguments += "-voxel.StreamingStartupApplyBoost=$StartupApplyBoost"
|
||||
if ($StreamingTraceOnly) {
|
||||
$BaseArguments += '-coreworldstreamtrace=1'
|
||||
|
||||
Reference in new issue
Block a user