Files
EVEngine/scripts/capture_example.ps1
西风逍遥游 e3ad78d30c docs: add example render gallery with real engine captures
Render 14 examples with the prebuilt eve host and capture the actual
swapchain frame via gfx.saveFramePng (capture_root.nut wrapper), then
publish the PNGs under docs/images/examples with a gallery README and a
link from the user guide.

Adds scripts/capture_example.ps1 + scripts/capture_root.nut for
regenerating the captures on any example.
2026-08-21 22:48:40 +08:00

249 lines
10 KiB
PowerShell

#!/usr/bin/env pwsh
<#
.SYNOPSIS
Launch an EVEngine example and capture a real rendered frame to PNG.
.DESCRIPTION
Runs a prebuilt `eve.exe` (see -Eve) as `eve run -r scripts/capture_root.nut
<examples/<Example>>`. The wrapper root script mirrors the engine's standard
load.nut, waits N frames for the scene to settle, then calls
`gfx.saveFramePng()` so the engine itself writes the last presented frame
from the swapchain readback. If the engine-side capture does not produce a
PNG, the script falls back to capturing the game window's client area via
Win32 (PrintWindow / CopyFromScreen).
.EXAMPLE
./scripts/capture_example.ps1 -Example weather -Out docs/images/weather.png -Frames 240
.PARAMETER Example
Name of an example directory under examples/ (must contain main.nut).
.PARAMETER Out
Absolute or repo-relative output PNG path. Default: docs/images/<Example>.png
.PARAMETER Frames
Number of frames to run before capturing (default 180, ~3s at 60fps).
.PARAMETER WaitSec
Extra seconds to wait for the process to finish after capture (default 15).
.PARAMETER KeepLogs
Keep the eve stdout/stderr logs in $env:TEMP for debugging (default off).
.PARAMETER Eve
Path to eve.exe. Defaults to $env:EVE, the parent EVEngine workspace build,
or a local build/win32-debug binary.
#>
param(
[Parameter(Mandatory = $true)][string]$Example,
[string]$Out,
[int]$Frames = 180,
[int]$WaitSec = 15,
[switch]$KeepLogs,
[string]$Eve
)
$ErrorActionPreference = "Stop"
$RepoRoot = Split-Path -Parent $PSScriptRoot
if (-not $Eve) {
if ($env:EVE) {
$Eve = $env:EVE
} elseif (Test-Path "C:\Users\xiaofans\Workspace\Agents\EVEngine\build\win32-debug\src\engine\eve.exe") {
$Eve = "C:\Users\xiaofans\Workspace\Agents\EVEngine\build\win32-debug\src\engine\eve.exe"
} else {
$Eve = Join-Path $RepoRoot "build\win32-debug\src\engine\eve.exe"
}
}
if (-not (Test-Path $Eve)) { throw "eve.exe not found at $Eve (set -Eve)" }
$GameDir = Join-Path $RepoRoot "examples\$Example"
if (-not (Test-Path (Join-Path $GameDir "main.nut"))) {
throw "examples\$Example has no main.nut"
}
if (-not $Out) { $Out = Join-Path $RepoRoot "docs\images\$Example.png" }
$Out = [IO.Path]::GetFullPath($Out)
$OutFwd = $Out.Replace('\', '/')
$RootScript = Join-Path $PSScriptRoot "capture_root.nut"
if (-not (Test-Path $RootScript)) { throw "capture_root.nut not found next to this script" }
function Show-CaptureStats([string]$Path) {
Add-Type -AssemblyName System.Drawing
$img = [System.Drawing.Bitmap]::FromFile($Path)
try {
$colors = @{}
$r = 0; $g = 0; $b = 0; $n = 0
$stepX = [Math]::Max(1, [int]($img.Width / 40))
$stepY = [Math]::Max(1, [int]($img.Height / 40))
for ($x = 0; $x -lt $img.Width; $x += $stepX) {
for ($y = 0; $y -lt $img.Height; $y += $stepY) {
$p = $img.GetPixel($x, $y)
$r += $p.R; $g += $p.G; $b += $p.B; $n++
$colors["$($p.R),$($p.G),$($p.B)"] = 1
}
}
Write-Host "[stats] $($img.Width)x$($img.Height) sampled=$n unique=$($colors.Count) avgRGB=$([int]($r/$n)),$([int]($g/$n)),$([int]($b/$n))"
} finally {
$img.Dispose()
}
}
$stamp = Get-Date -Format "HHmmssfff"
$stdoutLog = Join-Path $env:TEMP "eve_${Example}_${stamp}.out.log"
$stderrLog = Join-Path $env:TEMP "eve_${Example}_${stamp}.err.log"
$settingsFile = Join-Path $GameDir "capture_settings.nut"
$proc = $null
try {
# Per-run settings consumed by capture_root.nut (removed afterwards).
$settings = @"
capture_path <- "$OutFwd";
capture_frame <- $Frames;
capture_tries <- 30;
"@
Set-Content -Path $settingsFile -Value $settings -Encoding ASCII
$proc = Start-Process -FilePath $Eve `
-ArgumentList @("run", "-r", $RootScript, $GameDir) `
-WorkingDirectory $RepoRoot `
-RedirectStandardOutput $stdoutLog `
-RedirectStandardError $stderrLog `
-PassThru
Write-Host "[capture] running '$Example' ($Frames frames) ..."
$deadline = (Get-Date).AddSeconds([Math]::Max(30, 20 + $Frames / 30 + $WaitSec))
$engineCapture = $false
while ((Get-Date) -lt $deadline) {
if ($proc.HasExited) {
# stdout is pipe-buffered: re-read once more after exit to catch the
# final flush (CAPTURED may land in the file only now).
$tail = ""
foreach ($log in @($stdoutLog, $stderrLog)) {
if (Test-Path $log) {
$tail += Get-Content $log -Raw -ErrorAction SilentlyContinue
}
}
if ($tail -match "CAPTURED ") { $engineCapture = $true }
break
}
$tail = ""
foreach ($log in @($stdoutLog, $stderrLog)) {
if (Test-Path $log) {
$tail += Get-Content $log -Raw -ErrorAction SilentlyContinue
}
}
if ($tail -match "CAPTURED ") { $engineCapture = $true; break }
if ($tail -match "CAPTURE_FAILED") { break }
Start-Sleep -Milliseconds 500
}
if ($engineCapture) {
# Give the PNG writer a moment to flush the file.
Start-Sleep -Seconds 1
}
# The engine writes the PNG itself; if it exists and is non-empty, treat the
# capture as successful even when the CAPTURED marker was not observed.
if ((Test-Path $Out) -and (Get-Item $Out).Length -gt 0) {
$fi = Get-Item $Out
if ($fi.Length -eq 0) { throw "screenshot is empty: $Out" }
Write-Host "[capture] OK (engine frame): $Out ($($fi.Length) bytes)"
Show-CaptureStats $Out
return
}
# ---- Fallback: capture the game window client area (desktop) ----
Write-Host "[capture] engine capture unavailable; falling back to window capture ..."
Add-Type @"
using System;
using System.Runtime.InteropServices;
public static class EveCapture {
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
[DllImport("user32.dll")] public static extern bool GetClientRect(IntPtr hWnd, out RECT rect);
[DllImport("user32.dll")] public static extern bool ClientToScreen(IntPtr hWnd, ref POINT pt);
[DllImport("user32.dll")] public static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, uint nFlags);
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc cb, IntPtr lParam);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint pid);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder sb, int max);
public delegate bool EnumProc(IntPtr hWnd, IntPtr lParam);
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
[StructLayout(LayoutKind.Sequential)] public struct POINT { public int X, Y; }
}
"@
Add-Type -AssemblyName System.Drawing
[EveCapture]::SetProcessDPIAware() | Out-Null
$script:targetPid = $proc.Id
$hwnd = [IntPtr]::Zero
$deadline2 = (Get-Date).AddSeconds(20)
while ((Get-Date) -lt $deadline2 -and -not $proc.HasExited) {
$script:windows = [System.Collections.Generic.List[IntPtr]]::new()
$cb = {
param($h, $l)
$pid2 = 0
[EveCapture]::GetWindowThreadProcessId($h, [ref]$pid2) | Out-Null
if ($pid2 -eq $script:targetPid) { $script:windows.Add($h) }
return $true
}
[EveCapture]::EnumWindows($cb, [IntPtr]::Zero) | Out-Null
foreach ($cand in $script:windows) {
$r = New-Object EveCapture+RECT
if (-not [EveCapture]::GetClientRect($cand, [ref]$r)) { continue }
if (($r.Right - $r.Left) -le 0 -or ($r.Bottom - $r.Top) -le 0) { continue }
if (-not [EveCapture]::IsWindowVisible($cand)) { continue }
$cls = New-Object System.Text.StringBuilder 128
[EveCapture]::GetClassName($cand, $cls, $cls.Capacity) | Out-Null
if ($cls.ToString() -eq "SDL_app") { $hwnd = $cand; break }
}
if ($hwnd -ne [IntPtr]::Zero) { break }
Start-Sleep -Milliseconds 300
}
if ($hwnd -eq [IntPtr]::Zero) {
$log = (Get-Content $stderrLog -Raw -ErrorAction SilentlyContinue) + "`n" + (Get-Content $stdoutLog -Raw -ErrorAction SilentlyContinue)
throw "no SDL window found. log: $log"
}
$client = New-Object EveCapture+RECT
[EveCapture]::GetClientRect($hwnd, [ref]$client) | Out-Null
$w = $client.Right - $client.Left
$h = $client.Bottom - $client.Top
if ($w -le 0 -or $h -le 0) { throw "invalid client rect ${w}x${h}" }
$dir = Split-Path -Parent $Out
if ($dir) { New-Item -ItemType Directory -Force -Path $dir | Out-Null }
$bmp = New-Object System.Drawing.Bitmap($w, $h)
try {
$gfx = [System.Drawing.Graphics]::FromImage($bmp)
try {
$hdc = $gfx.GetHdc()
$pw = [EveCapture]::PrintWindow($hwnd, $hdc, 3)
$gfx.ReleaseHdc($hdc)
if (-not $pw) {
$origin = New-Object EveCapture+POINT
[EveCapture]::ClientToScreen($hwnd, [ref]$origin) | Out-Null
$gfx2 = [System.Drawing.Graphics]::FromImage($bmp)
try {
$gfx2.CopyFromScreen($origin.X, $origin.Y, 0, 0, $bmp.Size,
[System.Drawing.CopyPixelOperation]::SourceCopy)
} finally {
$gfx2.Dispose()
}
}
} finally {
$gfx.Dispose()
}
$bmp.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png)
} finally {
$bmp.Dispose()
}
$fi = Get-Item $Out
if ($fi.Length -eq 0) { throw "screenshot is empty: $Out" }
Write-Host "[capture] OK (window capture): $Out (${w}x${h}, $($fi.Length) bytes)"
Show-CaptureStats $Out
} finally {
if ($proc -and -not $proc.HasExited) {
Stop-Process -Id $proc.Id -Force
Wait-Process -Id $proc.Id -ErrorAction SilentlyContinue
}
Remove-Item $settingsFile -ErrorAction SilentlyContinue
if (-not $KeepLogs) {
Remove-Item $stdoutLog, $stderrLog -ErrorAction SilentlyContinue
} else {
Write-Host "[debug] logs kept: $stdoutLog / $stderrLog"
}
}