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:
Fr0zkaandClaude Opus 5.5 committed 2026-10-19 08:10:10 +02:00
1 parent b9c13c37c0
commit 5eb9ee76ff
7 files changed
+726 -53

No files matched your search

+60 -8
View File
@@ -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
View File
@@ -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)
+331 -29
View File
@@ -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)
{
+15 -2
View File
@@ -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.
+13 -1
View File
@@ -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'