SchOS/tools/windows/splashshot.ps1
Nexoniarz ed77244df0 boot/uefi: the system information panel, and the offset that faked a version
ui_panel was written for this screen during the loader restyle and then
never called -- dead code sitting in boot/common/ui.c waiting for the row
that would use it. This is that row: F8 -> System Information, showing
firmware vendor and revision, UEFI revision, CPU brand, usable memory,
current mode and whether ACPI was found.

Everything is read when the screen is drawn rather than captured at
startup, because the resolution row above it can have changed the mode,
and a panel still reporting what the machine booted with is worse than no
panel at all.

Two facts are deliberately read through the same code that feeds the
kernel -- efi_type_to_e820() for what counts as usable memory, and
find_rsdp() for ACPI. A panel with its own idea of either would be a
second implementation to keep in step, and the ACPI line exists precisely
because a missing RSDP means no MADT, no IOAPIC and no SMP, which has
already been debugged once from the far end.

The screenshot caught a bug in the screen it was taken to verify: UEFI
revision read "21587.2286". EFI_TABLE_HEADER begins with a UINT64
Signature, so Revision is at offset 8 and I had read offset 4 -- the
upper half of "IBI SYST", formatted as a version number. It now reads 2.7
under OVMF, which is correct.

Verified by driving the menu over the QEMU monitor and reading the
pixels: five rows with the hint updated to 1-5, the panel centred, and
every value cross-checkable -- EDK II, firmware 1.0, UEFI 2.7, "QEMU
Virtual CPU version 2.5+" against -cpu qemu64, 1018 MB of the 1 GB given,
1280x800 matching what fbshot captures, ACPI present. The highlight sits
9 px inside both panel borders, symmetric.

F8 has a 1.5 s window, so the capture presses it repeatedly through boot
rather than guessing when OVMF finishes -- and only F8, since any other
key breaks the wait and boots.

Also: every ESP drive now says format=raw. QEMU warned about probing the
format on each of the fifteen or so places that attach it, which is new
noise on what is now the only boot path. 110 [ok], no failures, no
warnings.
2026-08-11 10:52:43 +02:00

179 lines
7.6 KiB
PowerShell

# Photograph the boot splash.
#
# The splash is on screen from the moment stage2 draws it until
# console_takeover() clears it at the first exec(), which is a window of a
# couple of seconds -- so a single screendump at a guessed delay is a coin
# flip. This takes a burst instead and keeps every frame, which makes the
# question "what does boot actually look like, second by second" answerable
# rather than a matter of catching it at the right instant.
#
# Same shape as tools/crashshot.ps1: monitor on TCP because QEMU on Windows has
# no unix sockets, serial to a logfile so the transcript survives.
#
# tools/splashshot.ps1 frames every 400ms for 8s
# tools/splashshot.ps1 -Every 200 -For 4
param(
[int]$Every = 400, # milliseconds between frames
[int]$For = 8, # seconds to keep capturing
[string]$OutDir = "build/shots",
[string]$Qemu
)
# QEMU, on whichever host this is.
#
# PowerShell 7 runs on Linux, macOS and the BSDs as well as Windows, so these
# scripts are usable from all of them -- but the old default was a hardcoded
# scoop path under $env:USERPROFILE, which exists on exactly one of those. PATH
# first, because a package-managed qemu is on it everywhere; the scoop location
# is only a fallback for the Windows install that does not add itself to PATH.
function Find-Qemu {
$exe = if ($IsWindows -or $env:OS -eq 'Windows_NT') { 'qemu-system-x86_64.exe' }
else { 'qemu-system-x86_64' }
$onPath = Get-Command $exe -ErrorAction SilentlyContinue
if ($onPath) { return $onPath.Source }
if ($env:USERPROFILE) {
$scoop = Join-Path (Join-Path $env:USERPROFILE 'scoop/apps/qemu/current') $exe
if (Test-Path $scoop) { return $scoop }
}
foreach ($p in @('/usr/bin', '/usr/local/bin', '/opt/homebrew/bin')) {
$c = Join-Path $p $exe
if (Test-Path $c) { return $c }
}
throw "qemu-system-x86_64 not found on PATH. Install QEMU, or pass -Qemu <path>."
}
if (-not $Qemu) { $Qemu = Find-Qemu }
$ErrorActionPreference = 'Stop'
# Repo root is two levels up: this script lives in tools/windows/.
$root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
$log = Join-Path $root 'build/splashshot.log'
$dir = Join-Path $root $OutDir
Remove-Item -Recurse -Force $dir -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force $dir | Out-Null
Remove-Item -Force $log -ErrorAction SilentlyContinue
$esp = Join-Path $root 'build/esp'
# OVMF and the staged ESP, because there is one boot path now. The old
# schos.img was a raw disk with the boot sector at LBA 0 -- a layout only the
# BIOS loader could use, and it went with it.
$code = Join-Path $root 'build/OVMF_CODE.fd'
$vars = Join-Path $root 'build/OVMF_VARS.fd'
foreach ($f in @($code, $vars)) {
if (-not (Test-Path $f)) { throw "no $f -- run 'make qemu-uefi' once to fetch OVMF" }
}
if (-not (Test-Path (Join-Path $esp 'EFI/BOOT/BOOTX64.EFI'))) {
throw "no staged ESP at $esp -- run 'make esp' first"
}
$runVars = Join-Path $root "build/splashshot-OVMF_VARS.fd"
Copy-Item -Force $vars $runVars
# The root filesystem. SchFS is the only root there is: the kernel mounts
# device 1 in forkret() and panics if it is not a SchFS volume, so this is
# not a choice and there is no -SchFS switch any more. It used to default to
# build/fs.img, which stopped being bootable the moment SchFS became root.
$hdb = Join-Path $root 'build/schfs.img'
$monPort = 55556 # not crashshot's port, so the two can run back to back
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $Qemu
$psi.Arguments = @(
# -snapshot with file.locking=off, rather than booting from copies. QEMU
# takes an exclusive lock on its drive images, so an interactive session the
# developer already has open would otherwise make this fail with "Failed to
# get write lock" -- and a capture script is exactly the thing you want to
# run without first shutting down what you were looking at. Copying was the
# first fix and it meant duplicating build/fs.img, all 153 MB of it, on
# every run. -snapshot keeps writes in a temporary overlay, so the originals
# are untouched either way.
"-drive if=pflash,format=raw,unit=0,readonly=on,file=`"$code`""
"-drive if=pflash,format=raw,unit=1,file=`"$runVars`""
"-drive file=fat:rw:`"$esp`",index=0,media=disk,format=raw"
"-drive file=`"$hdb`",format=raw,index=1,media=disk,file.locking=off"
'-snapshot'
'-m 1G -smp 1 -cpu qemu64,+rdtscp -machine pc-i440fx-9.2'
'-display none'
"-chardev stdio,id=c0,logfile=`"$log`""
'-serial chardev:c0'
"-monitor tcp:127.0.0.1:$monPort,server,nowait"
) -join ' '
$psi.RedirectStandardInput = $true
$psi.UseShellExecute = $false
Write-Host "==> booting" -ForegroundColor Cyan
$p = [System.Diagnostics.Process]::Start($psi)
$frames = @()
try {
# Connect immediately -- the interesting frames are the early ones, so any
# fixed warm-up sleep here is time spent missing them.
$client = $null
for ($i = 0; $i -lt 40 -and -not $client; $i++) {
try { $client = New-Object System.Net.Sockets.TcpClient('127.0.0.1', $monPort) }
catch { Start-Sleep -Milliseconds 100 }
}
if (-not $client) { throw "could not reach the QEMU monitor on port $monPort" }
$writer = New-Object System.IO.StreamWriter($client.GetStream())
$writer.AutoFlush = $true
$n = [int](($For * 1000) / $Every)
$sw = [System.Diagnostics.Stopwatch]::StartNew()
for ($i = 0; $i -lt $n; $i++) {
$ms = [int]$sw.ElapsedMilliseconds
$f = Join-Path $dir ("{0:d5}ms.ppm" -f $ms)
$writer.WriteLine("screendump $f")
$frames += $f
Start-Sleep -Milliseconds $Every
}
$writer.WriteLine('quit')
Start-Sleep -Milliseconds 500
$client.Close()
} finally {
if (-not $p.HasExited) { $p.Kill() }
Start-Sleep -Milliseconds 800
}
# Report each frame by its distinct-colour count. Two colours means a text
# console (or a blank screen); an anti-aliased logo pushes it into the dozens.
# The same reason the full-stack harness counted pixels rather than log lines: a capture
# that exists is not a capture that drew anything.
Write-Host "==> frames" -ForegroundColor Cyan
$kept = 0
foreach ($f in $frames) {
if (-not (Test-Path $f)) { continue }
$kept++
$b = [System.IO.File]::ReadAllBytes($f)
# PPM P6: "P6\n<w> <h>\n255\n" then raw RGB. Skip three whitespace-run
# separated header fields rather than assuming a fixed header length --
# QEMU's is compact but that is not guaranteed.
$pos = 2; $fields = 0
while ($fields -lt 3 -and $pos -lt $b.Length) {
while ($pos -lt $b.Length -and [char]$b[$pos] -match '\s') { $pos++ }
while ($pos -lt $b.Length -and [char]$b[$pos] -notmatch '\s') { $pos++ }
$fields++
}
$pos++
$seen = @{}
for ($i = $pos; $i + 2 -lt $b.Length; $i += 3 * 7) { # sample every 7th pixel
$seen[($b[$i] -shl 16) -bor ($b[$i+1] -shl 8) -bor $b[$i+2]] = 1
}
$name = Split-Path $f -Leaf
$c = $seen.Count
$tag = if ($c -ge 16) { 'graphical' } elseif ($c -le 3) { 'flat/text' } else { 'sparse' }
$col = if ($c -ge 16) { 'Green' } else { 'DarkGray' }
Write-Host (" {0,-12} {1,4} colours {2}" -f $name, $c, $tag) -ForegroundColor $col
}
if ($kept -eq 0) { Write-Host "FAIL: no frames captured" -ForegroundColor Red; exit 1 }
Write-Host "==> $kept frames in $dir" -ForegroundColor Green
# Convert to PNG if ImageMagick is around, so they can just be looked at.
if (Get-Command magick -ErrorAction SilentlyContinue) {
foreach ($f in $frames) {
if (Test-Path $f) { & magick $f ([System.IO.Path]::ChangeExtension($f, '.png')) }
}
Write-Host "==> PNGs alongside" -ForegroundColor Green
}