fix(streaming): no stale geometry over its replacement -- edit ghost 40s -> 0.17s

The owner reported a wall staying visible after he carved through it, and overlapping LODs when crossing slopes at speed. Two causes: an edit only re-meshed the level-0 tiles of the touched chunks, so coarser tiles kept pre-edit geometry until something else re-queued them; and superseded tiles stayed drawn until the budgeted teardown reached them.

Now: edits fence late pre-edit async results and re-mesh every affected loaded/pending level; a replaced tile is hidden as soon as its replacement is fully covered, while destruction and collision removal stay budgeted (the anti-spike rule that made teardown budgeted in the first place). Load-before-unload is unchanged, so no holes.

Measured: edit ghost 40.095 s (unresolved at test exit) -> 0.172 s; 6000 cm/s slope overlap 19.952 s with 153 overlapping pairs -> 0.000 s with zero events. Gate stress PASS (113.68 m, 2 holds, 0 full stops). Apply-frame cost peaked at 0.979 ms against a 2 ms budget.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
This commit is contained in:
Fr0zkaandClaude Opus 5 committed 2026-09-23 21:10:20 +02:00
1 parent f36bc1f9b2
commit 7486c299d4
3 files changed
+615 -38

No files matched your search

+68 -4
View File
@@ -1,6 +1,6 @@
[CmdletBinding()]
param(
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism')]
[ValidateSet('canonical', 'owner', 'probe', 'perf', 'visual-edit', 'visual-lod', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'parity', 'horizon', 'horizon-render', 'determinism')]
[string]$Scenario = 'canonical',
[switch]$Build,
[ValidateSet('owner', 'default')]
@@ -64,6 +64,10 @@ $SurfaceFallMetrics = [ordered]@{
status = 'not_run'
}
$StrateCrossingMetrics = [ordered]@{ requested = $Scenario -eq 'crossing'; status = 'not_run' }
$VisualStalenessMetrics = [ordered]@{
requested = $Scenario -in @('visual-edit', 'visual-lod')
status = 'not_run'
}
$CollisionGateStressMetrics = [ordered]@{
requested = $Scenario -eq 'gate-stress' -or
($Scenario -eq 'tests' -and [string]::IsNullOrWhiteSpace($TestFilter))
@@ -500,7 +504,7 @@ function Sync-OwnerAssets {
$null = Copy-Item -LiteralPath $source -Destination $target -Force -PassThru
[void]$rows.Add([ordered]@{
name = $assetName
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'horizon', 'horizon-render', 'determinism')
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'horizon', 'horizon-render', 'determinism')
source_path = $source
source_sha256 = Get-Sha256 $source
staged_path = $target
@@ -548,7 +552,7 @@ function Get-AssetEvidence {
$target = Join-Path $HostRoot "Content\VoxelForge\$assetName.uasset"
[void]$rows.Add([ordered]@{
name = $assetName
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'horizon', 'horizon-render', 'determinism')
used_by_scenario = $Scenario -in @('owner', 'probe', 'parity', 'tests', 'surface-fall', 'crossing', 'gate-stress', 'visual-edit', 'visual-lod', 'horizon', 'horizon-render', 'determinism')
source_path = $source
source_sha256 = Get-Sha256 $source
staged_path = $target
@@ -595,6 +599,7 @@ function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap, [bool]$E
[void]$args.Add('-FullStdOutLogOutput')
[void]$args.Add('-forcelogflush')
[void]$args.Add('-NoZenAutoLaunch')
[void]$args.Add('-notraceserver')
[void]$args.Add('-nocrashreports')
[void]$args.Add('-WindowsPlatformCrashContext.ForceCrashReportDialogOff=1')
[void]$args.Add("-enginesaveddirsuffix=VoxelForgeTest_$safeLabel")
@@ -613,7 +618,7 @@ function New-CommonArguments([string]$Kind, [string]$LogPath, $CvarMap, [bool]$E
$switch = [string]$key
if (-not $switch.StartsWith('-')) { $switch = "-$switch" }
$value = Get-ValueText $CvarMap[$key]
if ($value -match '\s') {
if ($value -match '\s' -or $value.Contains(',')) {
[void]$args.Add(('{0}="{1}"' -f $switch, $value))
} else {
[void]$args.Add("$switch=$value")
@@ -994,6 +999,29 @@ function Get-CollisionGateStressMetrics([string]$LogPath) {
return $metrics
}
function Get-VisualStalenessMetrics([string]$LogPath) {
$lineMatch = @(Select-String -LiteralPath $LogPath -Pattern '\[VoxelForgeVisualStaleness\] result=' -ErrorAction SilentlyContinue |
Select-Object -Last 1)
if ($lineMatch.Count -eq 0) { return [ordered]@{ status = 'missing'; log = $LogPath } }
$line = [string]$lineMatch[0].Line
$metrics = [ordered]@{ status = 'failed'; log = $LogPath; raw = $line }
$result = [regex]::Match($line, 'result=(?<v>PASS|FAIL)')
if ($result.Success -and $result.Groups['v'].Value -eq 'PASS') { $metrics.status = 'passed' }
foreach ($key in @('mode', 'edit_applied', 'edit_resolved', 'edit_stale_duration_s',
'edit_stale_tiles_max', 'lod_overlap_max_s', 'lod_overlap_events',
'lod_overlap_pairs_max')) {
$match = [regex]::Match($line, ('{0}=(?<v>[^ ]+)' -f $key))
if (-not $match.Success) { continue }
if ($key -in @('mode', 'edit_applied', 'edit_resolved', 'edit_stale_tiles_max',
'lod_overlap_events', 'lod_overlap_pairs_max')) {
$metrics[$key] = [int]$match.Groups['v'].Value
} else {
$metrics[$key] = [double]$match.Groups['v'].Value
}
}
return $metrics
}
function Get-TestMetrics($Ledger) {
if ($null -eq $Ledger) { return @() }
$logPaths = @($Ledger.log, $Ledger.stdout, $Ledger.stderr) | Where-Object {
@@ -1353,6 +1381,40 @@ try {
(Join-Path $RunRoot 'Logs\perf.log') (Join-Path $RunRoot 'Logs\perf.stdout.log') (Join-Path $RunRoot 'Logs\perf.stderr.log')
$GameMetrics = Get-StreamingMetrics (Join-Path $RunRoot 'Logs\perf.log')
}
'visual-edit' {
$baseCvars['voxel.TestVisualStaleness'] = 1
$baseCvars['voxel.TestModification'] = 1
$baseCvars['voxel.TestModificationTarget'] = 'loaded-geometry'
$baseCvars['voxel.TestExitSeconds'] = 45
$baseCvars['voxel.TestMaxClipLevel'] = 5
$baseCvars['voxel.OuterClassifierMode'] = 0
$visualLog = Join-Path $RunRoot 'Logs\visual-edit.log'
$args = New-CommonArguments 'game' $visualLog (Merge-Cvars $baseCvars)
$gameLedger = Invoke-UnrealLaunch 'visual-edit-game' $args `
$visualLog (Join-Path $RunRoot 'Logs\visual-edit.stdout.log') (Join-Path $RunRoot 'Logs\visual-edit.stderr.log')
$VisualStalenessMetrics = Get-VisualStalenessMetrics $visualLog
if ($VisualStalenessMetrics.status -ne 'passed') {
Add-Failure "visual edit repro did not pass: $($VisualStalenessMetrics.status)"
}
}
'visual-lod' {
$baseCvars['voxel.TestVisualStaleness'] = 2
$baseCvars['voxel.TestMove'] = 1
$baseCvars['voxel.TestMoveSpeedCmPerSecond'] = 6000
$baseCvars['voxel.TestMoveStartSeconds'] = 25
$baseCvars['voxel.TestMoveDirection'] = '1, 0, 0.15'
$baseCvars['voxel.TestExitSeconds'] = 45
$baseCvars['voxel.TestMaxClipLevel'] = 5
$baseCvars['voxel.OuterClassifierMode'] = 0
$visualLog = Join-Path $RunRoot 'Logs\visual-lod.log'
$args = New-CommonArguments 'game' $visualLog (Merge-Cvars $baseCvars)
$gameLedger = Invoke-UnrealLaunch 'visual-lod-game' $args `
$visualLog (Join-Path $RunRoot 'Logs\visual-lod.stdout.log') (Join-Path $RunRoot 'Logs\visual-lod.stderr.log')
$VisualStalenessMetrics = Get-VisualStalenessMetrics $visualLog
if ($VisualStalenessMetrics.status -ne 'passed') {
Add-Failure "visual LOD repro did not pass: $($VisualStalenessMetrics.status)"
}
}
'surface-fall' {
$baseCvars['voxel.TestSurfaceFall'] = 1
$baseCvars['voxel.TestSurfaceFallSpawnHeightVoxels'] = 20
@@ -1551,6 +1613,7 @@ $result = [ordered]@{
determinism = $DeterminismEvidence
surface_fall = $SurfaceFallMetrics
strate_crossing = $StrateCrossingMetrics
visual_staleness = $VisualStalenessMetrics
collision_gate_stress = $CollisionGateStressMetrics
worker_seconds = [ordered]@{
commandlet = if ($CommandletMetrics.Contains('worker_seconds')) { $CommandletMetrics.worker_seconds } else { $null }
@@ -1584,6 +1647,7 @@ try {
[void]$summaryLines.Add("obj_sha256=$($result.obj_sha256)")
[void]$summaryLines.Add("surface_fall=$(ConvertTo-CompactJson $result.surface_fall)")
[void]$summaryLines.Add("strate_crossing=$(ConvertTo-CompactJson $result.strate_crossing)")
[void]$summaryLines.Add("visual_staleness=$(ConvertTo-CompactJson $result.visual_staleness)")
[void]$summaryLines.Add("collision_gate_stress=$(ConvertTo-CompactJson $result.collision_gate_stress)")
[void]$summaryLines.Add("probe_amplitudes=$(ConvertTo-CompactJson $result.probe_amplitudes)")
[void]$summaryLines.Add("worker_seconds=$(ConvertTo-CompactJson $result.worker_seconds)")