wip(standalone): add missing GPU skirt shader binaries and step 6-8 scripts/report

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-07 06:14:04 +02:00
1 parent b6ac950073
commit 7f64e2e0aa
9 files changed
+348

No files matched your search

@@ -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)
@@ -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"
@@ -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}")
@@ -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}")
@@ -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"
}
@@ -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.