1449 lines of boot/bios/, tools/sign.pl and tools/padstage2.pl, plus the build rules, the disk image and the ISO entry that existed to carry them. Everything since roughly 2012 boots UEFI, and CSM -- the firmware-side BIOS emulation that lets a legacy loader run on such a machine at all -- has been going away since Intel dropped support for it in 2020. So this is not a path being retired early; it is one whose remaining hardware is already the exception. The cost was never the code, it was the tax on everything near it. Stage 2 had a 65-sector ceiling it overflowed silently -- 25440 bytes against 24576 -- and because the kernel is written at sector 1 + STAGE2_SECTORS, an overflowing loader was overwritten by the very kernel it was loading. The resolution list could not be shared with UEFI either: VBE mode setting is INT 10h, so matching it needed unreal mode, which is the riskiest code in the tree for a feature that already worked elsewhere. What replaces schos.img is the staged ESP, handed to QEMU as a virtual FAT volume. That is a development arrangement and not an installable disk; a real GPT image with a real FAT partition is the installer's job, and until it exists nothing here writes a disk a machine boots unaided. Secure Boot is untouched too -- the loader is unsigned. The ISO keeps one El Torito entry, the FAT image holding loader and kernel. It never booted from a USB stick via BIOS anyway: the firmware presents the stick as a hard disk while stage2main.c read the kernel back over ATAPI. The 32-bit multiboot entry in entry.S now has no caller and is left in place. Deleting kernel entry code is its own change with its own way of failing silently, and it should not ride along with this one. Ported with it: run.ps1, the six screenshot tools, and schcom_test.sh, each with its own OVMF varstore copy so no run inherits another's UEFI variables. Two things this shook out. OVMF now belongs in `all`: nothing boots without firmware, `make clean` wipes build/ including the copy fetched from the host, and a clean followed by a build left every run script throwing. And it cannot be listed as a prerequisite at the top of the file -- make expands prerequisites when it reads the line, and $(OVMF_CODE) is defined further down, so naming it there appends nothing and says nothing. Verified from a clean tree: 110 [ok] and no failures on the full suite, the ISO boots with no hard disk attached, schfs-check passes, schcom's end-to-end pixel test passes, and fbshot captures a real 1280x800 frame.
179 lines
7.6 KiB
PowerShell
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"
|
|
"-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
|
|
}
|