diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/AnalyzeStep6NearAudit.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/AnalyzeStep6NearAudit.ps1 new file mode 100644 index 0000000..6d03658 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/AnalyzeStep6NearAudit.ps1 @@ -0,0 +1,32 @@ +$ErrorActionPreference='Stop' +$log=Join-Path $PSScriptRoot 'step6_whole_near_packed.stdout.log' +$tiles=[Collections.Generic.List[object]]::new() +foreach($line in [IO.File]::ReadLines($log)){ + if(-not $line.StartsWith('[GpuAgreement]')){continue} + $count=[regex]::Match($line,'vertex_comparisons_cpu_gpu=(\d+)') + $mean=[regex]::Match($line,'vertex_mean_cm=([0-9.eE+-]+)') + $max=[regex]::Match($line,'vertex_max_cm=([0-9.eE+-]+)') + $topology=[regex]::Match($line,'topology_diff_cells_hw=(\d+) topology_diff_cells_warp=(\d+)') + $repeat=[regex]::Match($line,'repeated_gpu_outputs_identical=(\d)') + if($count.Success -and $mean.Success -and $max.Success -and $topology.Success){ + $tiles.Add([pscustomobject]@{ + Count=[double]$count.Groups[1].Value + MeanCm=[double]::Parse($mean.Groups[1].Value,[Globalization.CultureInfo]::InvariantCulture) + MaxCm=[double]::Parse($max.Groups[1].Value,[Globalization.CultureInfo]::InvariantCulture) + TopologyGpu=[int64]$topology.Groups[1].Value + TopologyWarp=[int64]$topology.Groups[2].Value + Repeat=if($repeat.Success){[int]$repeat.Groups[1].Value}else{0} + }) + } +} +$compared=($tiles|Measure-Object -Property Count -Sum).Sum +$weighted=0.0 +foreach($tile in $tiles){$weighted+=$tile.Count*$tile.MeanCm} +$maxTile=$tiles|Sort-Object MaxCm -Descending|Select-Object -First 1 +$maxGpuTopology=($tiles|Measure-Object -Property TopologyGpu -Sum).Sum +$maxWarpTopology=($tiles|Measure-Object -Property TopologyWarp -Sum).Sum +$repeatFailures=($tiles|Where-Object Repeat -ne 1).Count +Write-Output ("tiles={0} vertex_pairs={1} weighted_mean_cm={2:R} weighted_mean_mm={3:R} max_cm={4:R} max_mm={5:R} max_tile={6} topology_gpu={7} topology_warp={8} repeat_failures={9}" -f ` + $tiles.Count,[int64]$compared,($weighted/[Math]::Max(1.0,$compared)), + (10.0*$weighted/[Math]::Max(1.0,$compared)),$maxTile.MaxCm,(10.0*$maxTile.MaxCm), + $maxTile.ToString(),[int64]$maxGpuTopology,[int64]$maxWarpTopology,$repeatFailures) diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldStep6NearRunner.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldStep6NearRunner.ps1 new file mode 100644 index 0000000..27399ec --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/BuildGpuFieldStep6NearRunner.ps1 @@ -0,0 +1,105 @@ +[CmdletBinding()] +param([switch]$SkipCore,[string]$OutputName='VoxelForgeGpuFieldStep6NearRunner.exe') + +$ErrorActionPreference = 'Stop' +$HostProcess = Get-Process -Id $PID +$HostProcess.PriorityClass = 'BelowNormal' +$HostProcess.ProcessorAffinity = [IntPtr]0xFF00 +$Standalone = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..')) +$BuildDir = Join-Path $Standalone 'Build\VoxelForgeGpuFieldStep6NearRunner' +$WrapperDir = Join-Path $BuildDir 'wrappers' +$BinDir = Join-Path $Standalone 'bin' +$Output = Join-Path $BinDir $OutputName +$CoreLib = Join-Path $BuildDir 'VoxelForgeGpuFieldPortCore.lib' +$VcVars = 'E:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvars64.bat' + +if (-not (Test-Path -LiteralPath $VcVars -PathType Leaf)) { throw "MSVC setup script missing: $VcVars" } +[IO.Directory]::CreateDirectory($BuildDir) | Out-Null +[IO.Directory]::CreateDirectory($WrapperDir) | Out-Null +[IO.Directory]::CreateDirectory($BinDir) | Out-Null + +$Manifest = Join-Path $Standalone 'Core\core_sources.txt' +$Sources = @(Get-Content -LiteralPath $Manifest | ForEach-Object { $_.Trim() } | + Where-Object { $_ -and -not $_.StartsWith('#') }) +$CompileSources = [Collections.Generic.List[string]]::new() +foreach ($SourceName in $Sources) { + $Source = [IO.Path]::GetFullPath((Join-Path $Standalone $SourceName)) + if (-not $Source.StartsWith($Standalone, [StringComparison]::OrdinalIgnoreCase)) { + throw "Core source escapes Standalone: $SourceName" + } + if ($SourceName.Replace('\','/') -eq 'Core/src/VoxelForgeCore.cpp') { + $CompileSources.Add($Source) + continue + } + $WrapperName = 'gpu_vfcore_' + ($SourceName.Replace('\','/') -replace '[^A-Za-z0-9]+','_') + $WrapperPath = Join-Path $WrapperDir ($WrapperName + '.cpp') + $SourceInclude = $Source.Replace('\','/') + $WrapperText = @" +#include "CoreNamespacePreamble.h" +namespace vfcore::detail +{ +#include "$SourceInclude" +} +"@ + [IO.File]::WriteAllText($WrapperPath, $WrapperText, [Text.UTF8Encoding]::new($false)) + $CompileSources.Add($WrapperPath) +} + +$IncludeArgs = @( + ('/I"{0}"' -f (Join-Path $Standalone 'Core\include')), + ('/I"{0}"' -f (Join-Path $Standalone 'Core\src')), + ('/I"{0}"' -f (Join-Path $Standalone 'Compat')), + ('/I"{0}"' -f (Join-Path $Standalone 'Source')), + ('/I"{0}"' -f (Join-Path $Standalone 'Source\VoxelForge\Public')), + ('/I"{0}"' -f (Join-Path $Standalone 'Source\VoxelForge\Private')) +) +$Common = @('/nologo','/std:c++20','/O2','/arch:AVX2','/fp:precise','/EHsc','/MP8','/D_CRT_SECURE_NO_WARNINGS') + $IncludeArgs +$CoreRsp = Join-Path $BuildDir 'field_core_compile.rsp' +[IO.File]::WriteAllLines($CoreRsp, $Common + @('/c') + @($CompileSources | ForEach-Object { '"{0}"' -f $_ }), [Text.UTF8Encoding]::new($false)) +$ObjectNames = @($CompileSources | ForEach-Object { '"{0}.obj"' -f [IO.Path]::GetFileNameWithoutExtension($_) }) +$LibRsp = Join-Path $BuildDir 'field_core_library.rsp' +[IO.File]::WriteAllLines($LibRsp, @('/nologo',('/OUT:"{0}"' -f $CoreLib)) + $ObjectNames, [Text.UTF8Encoding]::new($false)) +$RunnerRsp = Join-Path $BuildDir 'field_runner_link.rsp' +$Runner = Join-Path $PSScriptRoot 'GpuFieldPort.cpp' +$RunnerRspLines = $Common + @( + ('/Fe"{0}"' -f $Output), + ('"{0}"' -f $Runner), + ('"{0}"' -f $CoreLib), + '/link','d3d11.lib','dxgi.lib','d3dcompiler.lib','user32.lib','gdi32.lib','psapi.lib' +) +[IO.File]::WriteAllLines($RunnerRsp, $RunnerRspLines, [Text.UTF8Encoding]::new($false)) + +Push-Location $BuildDir +try { + $BuildCommands = [Collections.Generic.List[string]]::new() + if (-not $SkipCore) { + $BuildCommands.Add(('call "{0}" >nul && cl.exe @"{1}"' -f $VcVars, $CoreRsp)) + $BuildCommands.Add(('call "{0}" >nul && lib.exe @"{1}"' -f $VcVars, $LibRsp)) + } + $BuildCommands.Add(('call "{0}" >nul && cl.exe @"{1}"' -f $VcVars, $RunnerRsp)) + foreach ($BuildCommand in $BuildCommands) { + $attempt = 0 + do { + & $env:ComSpec /d /s /c $BuildCommand + $code = $LASTEXITCODE + if ($code -eq -1073741502 -and $attempt -eq 0) { + Write-Host '0xC0000142 encountered; waiting 60 seconds before the single permitted retry.' + Start-Sleep -Seconds 60 + $attempt++ + continue + } + if ($code -eq -1073741502) { throw '0xC0000142 repeated after the single retry; stopping.' } + if ($code -ne 0) { throw "GPU port build command failed with exit code $code" } + break + } while ($true) + } + if (-not $SkipCore) { + & (Join-Path $Standalone 'Core\CheckCoreSymbols.ps1') -LibraryPath $CoreLib + if ($LASTEXITCODE -ne 0) { throw "GPU port Core symbol check failed with exit code $LASTEXITCODE" } + } + if (-not (Test-Path -LiteralPath $Output -PathType Leaf)) { throw "GPU runner missing: $Output" } +} finally { + Pop-Location +} +Write-Host "Built $CoreLib" +Write-Host "Built $Output" diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep6Captures.py b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep6Captures.py new file mode 100644 index 0000000..ddb8d61 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep6Captures.py @@ -0,0 +1,64 @@ +from pathlib import Path + +import numpy as np +from PIL import Image + + +ROOT = Path(__file__).resolve().parent +VIEWS = ( + "chamber", + "tunnel", + "passage", + "far", + "passage_floor_closeup", + "tunnel_bend", + "remote_yaw0", + "remote_yaw90", + "remote_yaw180", + "remote_yaw270", +) + +rows = [] +for view in VIEWS: + cpu_path = ROOT / f"step6_cpu_{view}.png" + gpu_path = ROOT / f"step6_gpu_{view}.png" + cpu = np.asarray(Image.open(cpu_path).convert("RGB"), dtype=np.int16) + gpu = np.asarray(Image.open(gpu_path).convert("RGB"), dtype=np.int16) + if cpu.shape != gpu.shape: + raise RuntimeError(f"dimension mismatch for {view}: {cpu.shape} vs {gpu.shape}") + diff = np.abs(cpu - gpu) + pixel_max = diff.max(axis=2) + rows.append( + { + "view": view, + "size": f"{cpu.shape[1]}x{cpu.shape[0]}", + "mae_rgb": float(diff.mean()), + "rmse_rgb": float(np.sqrt(np.square(diff, dtype=np.float64).mean())), + "p95_pixel_max": float(np.percentile(pixel_max, 95)), + "p99_pixel_max": float(np.percentile(pixel_max, 99)), + "max_pixel_channel": int(pixel_max.max()), + "changed_pct": float(np.count_nonzero(pixel_max) * 100.0 / pixel_max.size), + "over_8_pct": float(np.count_nonzero(pixel_max > 8) * 100.0 / pixel_max.size), + "over_32_pct": float(np.count_nonzero(pixel_max > 32) * 100.0 / pixel_max.size), + } + ) + +lines = [ + "# Step 6 CPU/GPU capture comparison", + "", + "Each pair is a 1280x800 back-buffer capture from the fixed camera. `mae_rgb` and `rmse_rgb` are channel errors on the 0 to 255 scale; pixel percentiles use the maximum RGB channel error per pixel.", + "", + "| View | MAE RGB | RMSE RGB | p95 pixel max | p99 pixel max | Max channel | Changed pixels | >8 | >32 |", + "|---|---:|---:|---:|---:|---:|---:|---:|---:|", +] +for row in rows: + lines.append( + f"| {row['view']} | {row['mae_rgb']:.4f} | {row['rmse_rgb']:.4f} | " + f"{row['p95_pixel_max']:.0f} | {row['p99_pixel_max']:.0f} | {row['max_pixel_channel']} | " + f"{row['changed_pct']:.2f}% | {row['over_8_pct']:.2f}% | {row['over_32_pct']:.2f}% |" + ) + +report = ROOT / "step6_capture_comparison.md" +report.write_text("\n".join(lines) + "\n", encoding="utf-8") +print("\n".join(lines)) +print(f"\nWrote {report}") diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep7Captures.py b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep7Captures.py new file mode 100644 index 0000000..04ce5a7 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompareStep7Captures.py @@ -0,0 +1,64 @@ +from pathlib import Path + +import numpy as np +from PIL import Image + + +ROOT = Path(__file__).resolve().parent +VIEWS = ( + "chamber", + "tunnel", + "passage", + "far", + "passage_floor_closeup", + "tunnel_bend", + "remote_yaw0", + "remote_yaw90", + "remote_yaw180", + "remote_yaw270", +) + +rows = [] +for view in VIEWS: + cpu_path = ROOT / f"step7_cpu_{view}.png" + gpu_path = ROOT / f"step7_gpu_{view}.png" + cpu = np.asarray(Image.open(cpu_path).convert("RGB"), dtype=np.int16) + gpu = np.asarray(Image.open(gpu_path).convert("RGB"), dtype=np.int16) + if cpu.shape != gpu.shape: + raise RuntimeError(f"dimension mismatch for {view}: {cpu.shape} vs {gpu.shape}") + diff = np.abs(cpu - gpu) + pixel_max = diff.max(axis=2) + rows.append( + { + "view": view, + "size": f"{cpu.shape[1]}x{cpu.shape[0]}", + "mae_rgb": float(diff.mean()), + "rmse_rgb": float(np.sqrt(np.square(diff, dtype=np.float64).mean())), + "p95_pixel_max": float(np.percentile(pixel_max, 95)), + "p99_pixel_max": float(np.percentile(pixel_max, 99)), + "max_pixel_channel": int(pixel_max.max()), + "changed_pct": float(np.count_nonzero(pixel_max) * 100.0 / pixel_max.size), + "over_8_pct": float(np.count_nonzero(pixel_max > 8) * 100.0 / pixel_max.size), + "over_32_pct": float(np.count_nonzero(pixel_max > 32) * 100.0 / pixel_max.size), + } + ) + +lines = [ + "# Step 6 CPU/GPU capture comparison", + "", + "Each pair is a 1280x800 back-buffer capture from the fixed camera. `mae_rgb` and `rmse_rgb` are channel errors on the 0 to 255 scale; pixel percentiles use the maximum RGB channel error per pixel.", + "", + "| View | MAE RGB | RMSE RGB | p95 pixel max | p99 pixel max | Max channel | Changed pixels | >8 | >32 |", + "|---|---:|---:|---:|---:|---:|---:|---:|---:|", +] +for row in rows: + lines.append( + f"| {row['view']} | {row['mae_rgb']:.4f} | {row['rmse_rgb']:.4f} | " + f"{row['p95_pixel_max']:.0f} | {row['p99_pixel_max']:.0f} | {row['max_pixel_channel']} | " + f"{row['changed_pct']:.2f}% | {row['over_8_pct']:.2f}% | {row['over_32_pct']:.2f}% |" + ) + +report = ROOT / "step7_capture_comparison.md" +report.write_text("\n".join(lines) + "\n", encoding="utf-8") +print("\n".join(lines)) +print(f"\nWrote {report}") diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompileStep6GpuShaders.ps1 b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompileStep6GpuShaders.ps1 new file mode 100644 index 0000000..c681303 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/CompileStep6GpuShaders.ps1 @@ -0,0 +1,25 @@ +[CmdletBinding()] +param() +$ErrorActionPreference='Stop' +$hostProcess=Get-Process -Id $PID +$hostProcess.PriorityClass='BelowNormal' +$hostProcess.ProcessorAffinity=[IntPtr]0xFF00 +$standalone=[IO.Path]::GetFullPath((Join-Path $PSScriptRoot '..\..\..')) +$shader=Join-Path $standalone 'Research\OwnerShapeWorld\gpu_first_port\VoxelForgeGpuMesher.hlsl' +$output=Join-Path $PSScriptRoot 'precompiled' +$fxc=(Get-ChildItem -LiteralPath 'C:\Program Files (x86)\Windows Kits\10\bin' -Filter fxc.exe -Recurse -ErrorAction SilentlyContinue | + Where-Object {$_.FullName -match '\\x64\\fxc\.exe$'} | Sort-Object FullName -Descending | Select-Object -First 1).FullName +if(-not $fxc){throw 'fxc.exe was not found'} +[IO.Directory]::CreateDirectory($output)|Out-Null +foreach($entry in @('EvaluateFarFieldCS','ClassifyCS','ClassifyFarCS','ScanGroupsCS','EmitCS','NoiseProbeCS','BuildFarConnectorRingsCS','EmitFarConnectorPanelsCS')){ + $binary=Join-Path $output ($entry+'.cso') + for($attempt=0;$attempt -lt 2;$attempt++){ + & $fxc /nologo /T cs_5_0 /E $entry /O3 /Gis /Ges /Fo $binary $shader + $code=$LASTEXITCODE + if($code -eq -1073741502 -and $attempt -eq 0){Write-Output '0xC0000142 encountered; waiting 60 seconds before one retry.';Start-Sleep -Seconds 60;continue} + if($code -eq -1073741502){throw "0xC0000142 repeated while compiling $entry"} + if($code -ne 0){throw "fxc failed for $entry with exit code $code"} + break + } + Write-Output "COMPILED $entry -> $binary" +} diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/precompiled/ClassifyFarSkirtsCS.cso b/Standalone/Research/OwnerShapeWorld/gpu_field_port/precompiled/ClassifyFarSkirtsCS.cso new file mode 100644 index 0000000..e574a4a Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/precompiled/ClassifyFarSkirtsCS.cso differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/precompiled/EmitFarSkirtsCS.cso b/Standalone/Research/OwnerShapeWorld/gpu_field_port/precompiled/EmitFarSkirtsCS.cso new file mode 100644 index 0000000..fd3ed55 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/precompiled/EmitFarSkirtsCS.cso differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/precompiled/ScanFarSkirtsCS.cso b/Standalone/Research/OwnerShapeWorld/gpu_field_port/precompiled/ScanFarSkirtsCS.cso new file mode 100644 index 0000000..20999b4 Binary files /dev/null and b/Standalone/Research/OwnerShapeWorld/gpu_field_port/precompiled/ScanFarSkirtsCS.cso differ diff --git a/Standalone/Research/OwnerShapeWorld/gpu_field_port/step8_report.md b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step8_report.md new file mode 100644 index 0000000..11c5e89 --- /dev/null +++ b/Standalone/Research/OwnerShapeWorld/gpu_field_port/step8_report.md @@ -0,0 +1,58 @@ +# GPU port, step 8 report + +Build: `VoxelForgeGpuFieldStep8Final.exe`, from the current Standalone sources with FXC bytecode regenerated. All viewer runs used `/MP8`, BelowNormal priority, affinity `0xFF00`; the capture and interleaved-run scripts enforced the 8 GB process ceiling and closed each viewer they started. No `0xC0000142` occurred. + +## First-present stall + +The controlled no-hold runs reproduce a long first GPU-backed present on this machine. The first `Present` after one GPU tile had been submitted took 2.36–2.59 s, including with `DXGI_PRESENT_DO_NOT_WAIT`; it returned `DXGI_STATUS_OCCLUDED`. A CPU-only first present took 1.54 ms. The long call reproduced after a CPU run, after a 60-second idle, with a renamed executable, and in visible and hidden window controls. That identifies the trigger as the first GPU-backed present in the process. It does not identify the internal AMD/Windows cause; the owner's separate warm-process results remain different. + +GPU mode now presents a CPU-ready frame before submitting far work, then retains that front buffer while the GPU clipmap warms. Across three interleaved GPU launches, the first CPU present took 2.45–2.84 ms at 0.958–0.983 s; the first fully resident GPU clipmap was ready at 4.093–4.137 s. The first post-warm present took 0.200–0.337 ms. This hides the long present from the first displayed frame; it does not make the full GPU clipmap ready sooner. + +## Whole-view timing and GPU budget + +Commands in `RunStep8ColdInterleaved.ps1`: `VoxelForgeGpuFieldStep8Final.exe --clip-radius=3 --workers=8 --quit-after-clipmap --cpu-far` and the same command with `--gpu-far`, interleaved CPU/GPU runs. + +| Path | Process start to full clipmap ready | +|---|---:| +| CPU, A/B/C | 1.489 / 1.346 / 1.345 s | +| GPU hybrid, A/B/C | 4.131 / 4.137 / 4.093 s | + +The GPU hybrid median is 4.131 s versus 1.346 s for CPU, 2.785 s slower. Headless CPU clipmap generation on this final build was 0.780 s at 8 workers and 3.939 s at 1 worker. + +For the GPU runs, 1,434 tiles were submitted in 46–47 batches of up to 32. GPU dispatch timestamps totaled 703–820 ms, or 0.490–0.572 ms per tile on average; the slowest tile took 79–127 ms. Maximum batch spans were 86–134 ms. At 90 Hz, the average suggests about 19–22 tiles per 11.111 ms slice, but the current per-tile and batch tails exceed a frame budget by a wide margin and need finer slicing. + +CPU far-input preparation was 178–191 ms wall time (1.35–1.43 worker-seconds); upload/enqueue summed to 49–116 ms. The GPU queue envelope was 3.01–3.13 s while measured GPU busy time was 0.735–0.820 s; the timeline attributes 2.20–2.39 s to idle/poll time, with a single pump call taking about 1.92–2.08 s. The hybrid does not beat CPU yet. The measured floor is at least the 0.70–0.82 s of GPU dispatch plus preparation and submission; the remaining multi-second wait/pump overhead is still on the critical path. + +Peak GPU-mode private memory was 3.84–3.99 GB and sampled DXGI local memory peaked at about 3.03 GB, conservatively below 8 GB combined. + +## GPU skirts and field agreement + +The skirt field query now applies the CPU-decided critical-passage primitive rule to arbitrary clipping probes, and evaluates triangle-center probes in the CPU's centimeter-to-voxel arithmetic order. Ordinary rock/air field values remain GPU-evaluated. This reduced the remote face audit from 13 mismatching tiles to one. + +| Audit | Tiles | GPU skirt triangles | CPU/GPU skirt result | +|---|---:|---:|---| +| Owner-start camera, all exposed far faces | 1,098 | 42,828 | 0 triangle differences; mean vertex distance 0.000233 cm; max 0.11055 cm; GPU and WARP pass | +| Remote camera `(500,0,0)`, all exposed far faces | 1,138 | 43,676 | 8 triangle differences on tile `(4,3,-1,lod3)`; mean 0.000504 cm; max 17.9066 cm; GPU and WARP fail that tile | + +The remote skirt audit is therefore not a full pass. Its remaining discrepancy is too large to waive as float noise. + +The whole remote far-surface audit tested all 1,150 eligible tiles (116 near-transition keys were intentionally skipped). Hardware and WARP had zero lattice sign/topology differences; all 145,494 protected cells and 88 thin-feature cells had zero differences. Repeat GPU outputs were bit-identical. CPU/GPU surface vertex mean distance was 0.1163 cm (1.163 mm), max 64.5602 cm across 2,543,435 comparisons, with 245 tiles over the quantization bound. These largest outliers are far/off-view LOD7/8 vertices. GPU/WARP mean was 0.000453 cm and max 0.8839 cm across 12,229,092 comparisons. + +The GPU-mesh far-survival audit passed: 7,200/7,200 boundary samples, 10,976/10,976 connectors, zero boundary holes, zero missing connector samples, zero connector holes, zero unsampled connectors. + +## Captures and remaining remote gap + +`RunStep8CaptureMatrix.ps1 -ExecutableName VoxelForgeGpuFieldStep8Final.exe` captured all six fixed views and four remote yaws only after `[InitialClipmapReady]`. The pixel comparison is in [step8_capture_comparison.md](step8_capture_comparison.md). The six local views remain close. Remote MAE RGB was 4.2933 at yaw 0, 1.5692 at yaw 90, 1.6511 at yaw 180, and 1.7465 at yaw 270. The horizontal mid-band still shows a visible GPU gap against the CPU captures, including at full residency. The skirt emitter and connector isolation captures did not eliminate it, so the remote look audit fails. + +Capture files are `step8_{cpu,gpu}_{chamber,tunnel,passage,far,passage_floor_closeup,tunnel_bend,remote_yaw0,remote_yaw90,remote_yaw180,remote_yaw270}.png` in this directory. + +## CPU reference checks + +Final-build headless runs at 1 and 8 workers both passed Walkability `5642/5642`. The W1 and W8 CPU hashes match each other and the saved reference at all 44 fields: + +- Density: `0xdb2991c73215af4c` +- Mesh: `0x8236c5edd7fc8e4f` +- Payload: `0x4c48fa75a543d58e` +- Topology: `0xbea22e76b34a4dfc` + +The port is not ready to call complete: the startup present is hidden successfully, and owner-start skirts, far survival, CPU hashes, and protected/thin topology pass. The remote skirt has one large outlier, remote captures still show a gap, and the hybrid whole-view remains slower than CPU.