Files
VoxelForge/Standalone/Research/WorldAssemblerM3/Round9/CaptureViewerSequence.ps1
T
Fr0zkaandClaude Opus 5.5 c0596c5f24 wip(assembler): round S5 -- streaming gates + trace; coarse-first order kept opt-in (no win)
Walkable/enclosed/refined gates defined; JSONL trace joins request, worker start,
build/apply and discards. Coarse-first + dynamic near reserve behind
voxel.StreamingCoarseFirst / voxel.StreamingDynamicNearReserve (default 0 =
S4 order): matched runs showed no walkability or full-view win and slower
LOD0/1 refinement. Instruments settled: 3.1 s standalone reproduced (16.7 s was
another generation route); black early captures were clear-only frames.
Hashes equal S4; Core-off, M2, SurfaceWorld/FloatingIslands BuildHost pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MmSoF64BbRFmk9C3p1VsBJ
2026-10-19 04:42:41 +02:00

176 lines
7.8 KiB
PowerShell

[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$Label,
[Parameter(Mandatory = $true)][string[]]$ViewerArguments,
[ValidateSet('Spawn', 'Flight')][string]$CaptureMode = 'Spawn',
[switch]$WaitForFlightDrain
)
# Capture through the viewer's D3D back buffer; no desktop or window capture is used.
# Capturer le tampon D3D du viewer; aucune capture du bureau ou de la fenetre n'est utilisee.
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Add-Type -AssemblyName System.Drawing
$PluginRoot = [IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../../../..'))
$ViewerExe = Join-Path $PluginRoot 'Standalone/bin/VoxelForgeCoreStandalone.exe'
$CaptureRoot = Join-Path $PSScriptRoot 'captures'
$LogRoot = Join-Path $PSScriptRoot 'viewer_logs'
[IO.Directory]::CreateDirectory($CaptureRoot) | Out-Null
[IO.Directory]::CreateDirectory($LogRoot) | Out-Null
if (-not ('VoxelForgeR9MemoryProbe' -as [type])) {
Add-Type -TypeDefinition @'
using System;
using System.Runtime.InteropServices;
public static class VoxelForgeR9MemoryProbe {
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct MEMORYSTATUSEX {
public uint dwLength, dwMemoryLoad;
public ulong ullTotalPhys, ullAvailPhys, ullTotalPageFile, ullAvailPageFile;
public ulong ullTotalVirtual, ullAvailVirtual, ullAvailExtendedVirtual;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GlobalMemoryStatusEx(ref MEMORYSTATUSEX status);
public static ulong AvailablePhysicalBytes() {
var status = new MEMORYSTATUSEX();
status.dwLength = (uint)Marshal.SizeOf(typeof(MEMORYSTATUSEX));
if (!GlobalMemoryStatusEx(ref status)) throw new InvalidOperationException("GlobalMemoryStatusEx failed.");
return status.ullAvailPhys;
}
}
'@
}
if ([VoxelForgeR9MemoryProbe]::AvailablePhysicalBytes() -lt 4GB) {
throw 'Viewer launch skipped: less than 4 GiB of free physical memory.'
}
if (Get-Process UnrealEditor* -ErrorAction SilentlyContinue) {
throw 'Viewer launch skipped: an UnrealEditor process is still running.'
}
if (-not (Test-Path -LiteralPath $ViewerExe -PathType Leaf)) {
throw "Standalone viewer is missing: $ViewerExe"
}
Add-Type -AssemblyName System.Drawing
$SafeLabel = $Label -replace '[^A-Za-z0-9_-]', '_'
$SequenceSuffix = if ($CaptureMode -eq 'Spawn') { 'spawn' } else { 'mid_sprint' }
$CapturePrefix = Join-Path $CaptureRoot "${SafeLabel}_${SequenceSuffix}"
$RunArguments = [Collections.Generic.List[string]]::new()
foreach ($Argument in $ViewerArguments) { $RunArguments.Add($Argument) }
if ($CaptureMode -eq 'Spawn') {
$RunArguments.Add("--capture-spawn-sequence-prefix=$CapturePrefix")
$RunArguments.Add('--quit-after-spawn-captures')
$ExpectedIndices = @(0, 1, 2)
$TimeLimitSeconds = 60
} else {
$RunArguments.Add("--capture-flight-sequence-prefix=$CapturePrefix")
if (-not $WaitForFlightDrain) { $RunArguments.Add('--flight-capture-close-index=1') }
$ExpectedIndices = @(1)
$TimeLimitSeconds = if ($WaitForFlightDrain) { 900 } else { 45 }
}
$StdoutPath = Join-Path $LogRoot "$SafeLabel`_$SequenceSuffix.stdout.log"
$StderrPath = Join-Path $LogRoot "$SafeLabel`_$SequenceSuffix.stderr.log"
$QuotedArguments = @($RunArguments | ForEach-Object { '"' + $_.Replace('"', '\"') + '"' })
$ArgumentLine = $QuotedArguments -join ' '
$Process = Start-Process -FilePath $ViewerExe -ArgumentList $ArgumentLine `
-WorkingDirectory (Split-Path -Parent $ViewerExe) -PassThru -WindowStyle Normal `
-RedirectStandardOutput $StdoutPath -RedirectStandardError $StderrPath
$Started = [Diagnostics.Stopwatch]::StartNew()
try {
$Process.PriorityClass = [Diagnostics.ProcessPriorityClass]::Normal
$Process.ProcessorAffinity = [IntPtr]0xFFF
while ($true) {
$Process.Refresh()
if ($Process.HasExited) { break }
if ($Process.PrivateMemorySize64 -gt 8GB) {
throw "Owned viewer PID $($Process.Id) exceeded the 8 GiB private-memory limit."
}
if ($Started.Elapsed.TotalSeconds -gt $TimeLimitSeconds) {
throw "Owned viewer PID $($Process.Id) exceeded the $TimeLimitSeconds-second capture limit."
}
Start-Sleep -Milliseconds 200
}
$Process.WaitForExit()
$Process.Refresh()
if ($Process.ExitCode -ne 0) {
throw "Owned viewer PID $($Process.Id) exited with code $($Process.ExitCode)."
}
} catch {
$Process.Refresh()
if (-not $Process.HasExited) {
[void]$Process.CloseMainWindow()
if (-not $Process.WaitForExit(3000)) {
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
$Process.WaitForExit()
}
}
throw
}
function Save-DownscaledJpeg([string]$Source, [string]$Destination, [long]$Quality) {
$Original = [Drawing.Image]::FromFile($Source)
try {
$Width = [Math]::Min(960, $Original.Width)
$Height = [int][Math]::Round($Original.Height * $Width / $Original.Width)
$Small = New-Object Drawing.Bitmap($Width, $Height)
$Graphics = [Drawing.Graphics]::FromImage($Small)
try {
$Graphics.InterpolationMode = [Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$Graphics.PixelOffsetMode = [Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$Graphics.DrawImage($Original, 0, 0, $Width, $Height)
$Codec = [Drawing.Imaging.ImageCodecInfo]::GetImageEncoders() |
Where-Object { $_.MimeType -eq 'image/jpeg' } | Select-Object -First 1
$Parameters = New-Object Drawing.Imaging.EncoderParameters(1)
try {
$Parameters.Param[0] = New-Object Drawing.Imaging.EncoderParameter(
[Drawing.Imaging.Encoder]::Quality, $Quality)
$Small.Save($Destination, $Codec, $Parameters)
} finally {
$Parameters.Dispose()
}
} finally {
$Graphics.Dispose()
$Small.Dispose()
}
} finally {
$Original.Dispose()
}
}
$ViewerLog = Get-Content -LiteralPath $StdoutPath -Raw
foreach ($Index in $ExpectedIndices) {
$PngPath = '{0}_{1:D2}.png' -f $CapturePrefix, $Index
if (-not (Test-Path -LiteralPath $PngPath -PathType Leaf)) {
throw "Viewer exited normally but did not write required back-buffer capture index $Index."
}
if ($CaptureMode -eq 'Spawn') {
$CaptureTime = @('0.5s', '1.0s', '2.0s')[$Index]
$JpegPath = Join-Path $CaptureRoot "${SafeLabel}_spawn_$CaptureTime.jpg"
} else {
$JpegPath = Join-Path $CaptureRoot "${SafeLabel}_mid_sprint.jpg"
}
Save-DownscaledJpeg $PngPath $JpegPath 72
if ((Get-Item -LiteralPath $JpegPath).Length -ge 300KB) {
Save-DownscaledJpeg $PngPath $JpegPath 48
}
$Jpeg = Get-Item -LiteralPath $JpegPath
if ($Jpeg.Length -ge 300KB) { throw "Downscaled capture exceeds 300 KB: $($Jpeg.Name)" }
Remove-Item -LiteralPath $PngPath -Force
$Timing = [regex]::Match($ViewerLog, "\[(Spawn|Flight)SequenceCapture\] index=$Index elapsed_s=([0-9.]+)")
$Elapsed = if ($Timing.Success) { $Timing.Groups[2].Value } else { 'missing' }
Write-Host ('capture={0} elapsed_s={1} size={2} bytes' -f $Jpeg.Name, $Elapsed, $Jpeg.Length)
}
if ($CaptureMode -eq 'Flight') {
# Keep only the reviewed mid-flight JPG; remove this run's unused raw D3D frames.
# Garder uniquement le JPG de vol examine ; supprimer les tampons D3D bruts inutilises de ce run.
foreach ($Index in 0..4) {
$PngPath = '{0}_{1:D2}.png' -f $CapturePrefix, $Index
if (Test-Path -LiteralPath $PngPath -PathType Leaf) {
Remove-Item -LiteralPath $PngPath -Force
}
}
}
Write-Host ('viewer_closed=1 pid={0} exit_code={1} elapsed_s={2:0.000} priority=Normal affinity=0xFFF' -f
$Process.Id, $Process.ExitCode, $Started.Elapsed.TotalSeconds)