# 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 ." } 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 \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 }