Files
dolphin/Run-Wii-Starlet-Bootstrap.ps1
Yaya48 54f5a74bbf Track exact StarletOS bootstrap integration and checkpoint provenance
Recover 16 modified and 6 previously untracked integration files. All 22 source SHA-256s match the pre-publication StarletOS snapshot. Add handoff instructions, byte-preserving attributes and private backup exclusion. This commit publishes existing behavior; it does not claim a new full build or hardware qualification. The two preceding local commits and staged files were screened before publication.
2026-10-03 16:27:07 +02:00

323 lines
18 KiB
PowerShell

param(
[string]$ELF = '../starlet-ios-rust/target/armebv5te-none-eabi/release/revolution-ios-bootstrap',
[string]$BuildDirectory = '.starlet_msvc2',
[string]$UserDirectory = '.starlet_user3',
[ValidateSet('Idle', 'Panic', 'Fatal', 'Fault', 'Phase2', 'Phase3Raw', 'Phase3Ios', 'Phase4Fs', 'Phase5Dir', 'Phase5Es', 'Phase5Client', 'Phase6Write', 'Phase6Verify', 'Phase8Client', 'Phase9Resolve', 'Phase9Launch', 'Phase9Menu', 'Phase9Rescue', 'Phase10AOh1')][string]$ExpectedOutcome = 'Idle',
[string]$PpcDol = '',
[int]$TimeoutSeconds = 30,
# Phase9Launch: the launched program's minimum check count. 5 for the
# P09-H02 payload and a requested P09-H02M launch; 3 for a P09-H02M launch
# made by the kernel's watchdog (no launcher mailbox to check).
[ValidateRange(1, 5)][int]$Phase9MinimumChecks = 5,
# Phase10AOh1: the program's minimum check count. 40 for the P10A-H07
# reduced smoke; 16 for the P10A-H07B launched payload.
[ValidateRange(1, 1000)][int]$Phase10AMinimumChecks = 40,
[switch]$JIT,
[switch]$FullDolphin
)
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent $MyInvocation.MyCommand.Path
$elfCandidate = if ([System.IO.Path]::IsPathRooted($ELF)) { $ELF } else { Join-Path $repoRoot $ELF }
$elfPath = (Resolve-Path -LiteralPath $elfCandidate).Path
$dolPath = if ($PpcDol) { (Resolve-Path -LiteralPath $PpcDol).Path } else { '' }
$probePath = Join-Path $repoRoot "$BuildDirectory/Binaries/Tests/StarletBootstrapProbe.exe"
if (-not $FullDolphin) {
if ($PpcDol) { throw 'PpcDol requires FullDolphin' }
if (-not (Test-Path -LiteralPath $probePath -PathType Leaf)) {
throw "Build StarletBootstrapProbe first: $probePath"
}
$probeArguments = @($elfPath)
if ($JIT) { $probeArguments += '--jit' }
if ($ExpectedOutcome -eq 'Panic') { $probeArguments += '--expect-panic' }
if ($ExpectedOutcome -eq 'Fatal') { $probeArguments += '--expect-fatal-data' }
if ($ExpectedOutcome -eq 'Fault') { $probeArguments += '--expect-undefined-fault' }
if ($ExpectedOutcome -eq 'Phase2') { $probeArguments += '--expect-phase2' }
& $probePath @probeArguments
exit $LASTEXITCODE
}
$dolphinPath = Join-Path $repoRoot "$BuildDirectory/Binaries/DolphinNoGUI.exe"
$userPath = (Resolve-Path -LiteralPath (Join-Path $repoRoot $UserDirectory)).Path
$logPath = Join-Path $userPath 'Logs/dolphin.log'
if (-not (Test-Path -LiteralPath $dolphinPath -PathType Leaf)) {
throw "Build DolphinNoGUI first: $dolphinPath"
}
if ($TimeoutSeconds -lt 1) { throw 'TimeoutSeconds must be positive' }
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $logPath) | Out-Null
$bootArguments = if ($dolPath) { @('-e', "`"$dolPath`"") } else { @('-n', '0000000100000002') }
$arguments = @('-u', "`"$userPath`"") + $bootArguments + @(
'-C', 'Dolphin.Core.WiiIOSLLE=True',
'-C', 'Dolphin.Core.WiiSDCard=False',
'-C', "Dolphin.Core.WiiStarletJIT=$($JIT.IsPresent)",
'-C', "`"Dolphin.General.WiiStarletBootstrapELF=$elfPath`"",
'-C', 'Logger.Options.WriteToFile=True',
'-C', 'Logger.Options.Verbosity=4',
'-C', 'Logger.Logs.IOS=True',
'-v', 'Null', '-p', 'headless'
)
$position = if (Test-Path -LiteralPath $logPath) { (Get-Item -LiteralPath $logPath).Length } else { 0 }
$pending = ''
$trace = [System.Collections.Generic.List[string]]::new()
$rescueStarted = $false
$outcome = $null
$faultLine = $null
$probeCounters = $null
$probeChecks = $null
$dolphin = Start-Process -FilePath $dolphinPath -ArgumentList $arguments `
-WorkingDirectory $repoRoot -PassThru -WindowStyle Hidden
try {
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
while ((Get-Date) -lt $deadline) {
if (Test-Path -LiteralPath $logPath) {
$stream = [System.IO.File]::Open($logPath, [System.IO.FileMode]::Open,
[System.IO.FileAccess]::Read, [System.IO.FileShare]::ReadWrite)
try {
if ($position -gt $stream.Length) { $position = 0; $pending = '' }
[void]$stream.Seek($position, [System.IO.SeekOrigin]::Begin)
$reader = [System.IO.StreamReader]::new($stream, [System.Text.Encoding]::UTF8,
$false, 4096, $true)
try { $chunk = $reader.ReadToEnd() } finally { $reader.Dispose() }
$position = $stream.Position
} finally {
$stream.Dispose()
}
if ($chunk.Length -gt 0) {
$lines = ($pending + $chunk) -split "`n"
$pending = $lines[-1]
for ($index = 0; $index -lt $lines.Length - 1; ++$index) {
$line = $lines[$index]
if ($line -match 'Starlet bootstrap: (.+)$') {
$message = $Matches[1].TrimEnd("`r")
$trace.Add($message)
Write-Host $message
if ($message -eq 'REV:BOOT:IDLE' -and $ExpectedOutcome -notin @('Phase3Raw', 'Phase3Ios', 'Phase4Fs', 'Phase5Dir', 'Phase5Es', 'Phase5Client', 'Phase6Write', 'Phase6Verify', 'Phase8Client', 'Phase9Launch', 'Phase9Menu', 'Phase9Rescue', 'Phase10AOh1')) {
$outcome = if ($ExpectedOutcome -eq 'Phase2') { 'Phase2' } else { 'Idle' }
}
elseif ($message -like 'REV:PANIC*') { $outcome = 'Panic' }
# Revolution IOS Phase 09 P09-H01: the read-only System Menu resolution ends.
elseif ($ExpectedOutcome -eq 'Phase9Resolve' -and $message -like 'REV:P9:H01 DONE*') { $outcome = 'Phase9Resolve' }
elseif ($ExpectedOutcome -eq 'Phase9Resolve' -and $message -like 'REV:P9:H01 *FAIL*') { $outcome = 'Phase9ResolveFailed' }
# Revolution IOS Phase 09 P09-H04: the System Menu's observation ends with the kernel's
# summary (no request for a while, or none at all); a load failure ends it too.
elseif ($ExpectedOutcome -eq 'Phase9Menu' -and ($message -like 'REV:P9:H04 QUIET *' -or $message -like 'REV:P9:H04 NOIPC *')) { $outcome = 'Phase9Menu' }
elseif ($ExpectedOutcome -eq 'Phase9Menu' -and $message -like 'REV:P9:H0[34] FAIL*') { $outcome = 'Phase9MenuFailed' }
# P09-H04 rescue: the kernel stopped the Menu and launches the rescue screen, whose
# status (0x52500005 once drawn) ends the run below.
elseif ($ExpectedOutcome -eq 'Phase9Rescue' -and $message -like 'REV:P9:H04 RESCUE GO *') { $rescueStarted = $true }
elseif ($ExpectedOutcome -eq 'Phase9Rescue' -and ($message -like 'REV:P9:H0[34] FAIL*' -or $message -like 'REV:P9:H04 RESCUE PLAN FAIL*')) { $outcome = 'Phase9RescueFailed' }
elseif ($message -like 'REV:FATAL*') { $outcome = 'Fatal' }
} elseif ($line -match 'Starlet bootstrap exception|Starlet bootstrap trace length is invalid') {
$outcome = 'Fault'
$faultLine = $line.TrimEnd("`r")
Write-Host $faultLine
} elseif ($line -match 'Broadway ios-probe status=(0x[0-9a-f]+)') {
$probeStatus = $Matches[1]
Write-Host "PPC: $probeStatus"
if ($probeStatus -eq '0x52500002') { $outcome = 'Phase3Raw' }
elseif ($probeStatus -eq '0x52500005' -and $ExpectedOutcome -eq 'Phase10AOh1') { $outcome = 'Phase10AOh1' }
elseif ($probeStatus -eq '0x52500003') { $outcome = 'Phase3Ios' }
# Phase 09 P09-H02: the payload's success is final only with the kernel's
# release and first-request lines (checked after each chunk below).
elseif ($probeStatus -eq '0x52500005' -and $ExpectedOutcome -eq 'Phase9Launch') { $payloadPassed = $true }
elseif ($ExpectedOutcome -eq 'Phase9Rescue') { if ($probeStatus -eq '0x52500005' -and $rescueStarted) { $outcome = 'Phase9Rescue' } }
elseif ($probeStatus -eq '0x52500005') { $outcome = $(if ($ExpectedOutcome -in @('Phase5Dir', 'Phase5Es', 'Phase5Client', 'Phase6Write', 'Phase6Verify', 'Phase8Client')) { $ExpectedOutcome } else { 'Phase4Fs' }) }
# Phase 09 P09-H02M: a later program's status (the launched monitor
# starting) voids an earlier program's pass and its checks.
elseif ($ExpectedOutcome -eq 'Phase9Launch' -and $probeStatus -notmatch '^0x5250e') { $payloadPassed = $false; $probeChecks = $null }
elseif ($probeStatus -match '^0x5250e') { $outcome = 'ProbeFailed' }
} elseif ($line -match 'Broadway ios-probe counters sent=(\d+) received=(\d+) invalid=(\d+) duplicate=(\d+) timeout=(\d+)') {
$probeCounters = @([int]$Matches[1], [int]$Matches[2], [int]$Matches[3],
[int]$Matches[4], [int]$Matches[5])
Write-Host "PPC counters: $($probeCounters -join '/')"
} elseif ($line -match 'Broadway ios-probe checks=(\d+) failures=(\d+)') {
$probeChecks = @([int]$Matches[1], [int]$Matches[2])
Write-Host "PPC checks: $($probeChecks -join '/')"
}
}
}
}
if (-not $outcome -and $ExpectedOutcome -eq 'Phase9Launch' -and $payloadPassed -and $probeChecks -and
@($trace | Where-Object { $_ -like 'REV:P9:H02 GO *' }).Count -eq 1 -and
@($trace | Where-Object { $_ -like 'REV:P9:H02 FIRST CMD *' }).Count -eq 1) {
$outcome = 'Phase9Launch'
}
if ($outcome) { break }
if ($dolphin.HasExited) { break }
Start-Sleep -Milliseconds 200
}
if ($outcome -ne $ExpectedOutcome) {
$exitDetail = if ($dolphin.HasExited) { "Dolphin exited with code $($dolphin.ExitCode). " } else { '' }
throw "$exitDetail Bootstrap outcome '$outcome' (expected '$ExpectedOutcome'); see $logPath"
}
if ($ExpectedOutcome -eq 'Fault') {
if ($faultLine -notmatch 'Starlet bootstrap exception 1: LR=0x[0-9a-f]+ SPSR=0x[0-9a-f]+ PC=0x[0-9a-f]+') {
throw "Expected an undefined-instruction exception with registers; see $logPath"
}
}
if ($ExpectedOutcome -eq 'Fatal') {
if (-not $trace.Contains('REV:BOOT:ENTRY') -or
-not $trace.Contains('REV:BOOT:BSS_OK') -or
-not $trace.Contains('REV:FATAL:DATA') -or
$trace.Contains('REV:BOOT:DATA_OK')) {
throw "Fatal data check did not produce the expected markers; see $logPath"
}
}
if ($ExpectedOutcome -eq 'Phase2') {
$requiredPhase2 = @('REV:P2:START', 'ARCH PASS', 'CRITICAL PASS', 'TIMER PASS',
'MONOTONIC PASS', 'MEM PASS', 'IRQ PASS', 'BARRIER PASS', 'LOGGING PASS',
'REV:P2:PASS', 'REV:P2:KERNEL_READY', 'REV:BOOT:IDLE')
$missingPhase2 = @($requiredPhase2 | Where-Object { -not $trace.Contains($_) })
if ($missingPhase2.Count -gt 0 -or @($trace | Where-Object { $_ -like '* FAIL' }).Count -gt 0) {
throw "Phase 02 self-test markers are incomplete; see $logPath"
}
}
if ($ExpectedOutcome -eq 'Phase3Raw' -and
(-not $trace.Contains('REV:P3A:START') -or
-not $trace.Contains('REV:P3A:ARM_128') -or
-not $trace.Contains('REV:P3A:ARM_PASS') -or
-not $probeCounters -or
$probeCounters[0] -ne 128 -or $probeCounters[1] -ne 128 -or
$probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or
$probeCounters[4] -ne 0)) {
throw "Phase 03A Starlet markers are incomplete; see $logPath"
}
if ($ExpectedOutcome -eq 'Phase3Ios' -and
(-not $trace.Contains('REV:P3B:START') -or
-not $trace.Contains('REV:P3B:OPEN_OK') -or
-not $trace.Contains('REV:P3B:PING_OK') -or
-not $trace.Contains('REV:P3B:CLOSE_OK') -or
-not $probeCounters -or $probeCounters[0] -lt 200 -or
$probeCounters[0] -ne $probeCounters[1] -or
$probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or
$probeCounters[4] -ne 0 -or
-not $probeChecks -or $probeChecks[0] -lt 260 -or
$probeChecks[1] -ne 0)) {
throw "Phase 03B IOS probe markers or counters are incomplete; see $logPath"
}
# Revolution IOS Phase 04D: the shared /dev/fs cases (ppc/common/fs_cases.h).
if ($ExpectedOutcome -eq 'Phase4Fs' -and
(-not $trace.Contains('REV:P3B:START') -or
@($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or
-not $probeCounters -or $probeCounters[0] -lt 100 -or
$probeCounters[0] -ne $probeCounters[1] -or
$probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or
$probeCounters[4] -ne 0 -or
-not $probeChecks -or $probeChecks[0] -lt 30 -or
$probeChecks[1] -ne 0)) {
throw "Phase 04D /dev/fs markers or counters are incomplete; see $logPath"
}
# Revolution IOS Phase 05B: the READDIR/GETUSAGE cases (ppc/common/fs_dir_cases.h).
if ($ExpectedOutcome -eq 'Phase5Dir' -and
(-not $trace.Contains('REV:P3B:START') -or
@($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or
-not $probeCounters -or $probeCounters[0] -lt 600 -or
$probeCounters[0] -ne $probeCounters[1] -or
$probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or
$probeCounters[4] -ne 0 -or
-not $probeChecks -or $probeChecks[0] -lt 13 -or
$probeChecks[1] -ne 0)) {
throw "Phase 05B READDIR/GETUSAGE markers or counters are incomplete; see $logPath"
}
# Revolution IOS Phase 05C/05D: the /dev/es cases (ppc/common/es_cases.h).
if ($ExpectedOutcome -eq 'Phase5Es' -and
(-not $trace.Contains('REV:P3B:START') -or
@($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or
-not $probeCounters -or $probeCounters[0] -lt 600 -or
$probeCounters[0] -ne $probeCounters[1] -or
$probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or
$probeCounters[4] -ne 0 -or
-not $probeChecks -or $probeChecks[0] -lt 18 -or
$probeChecks[1] -ne 0)) {
throw "Phase 05C /dev/es markers or counters are incomplete; see $logPath"
}
# P10A-H07 reduced smoke: real service on the emulator USB double; no
# local loopback and no physical acceptance. No synthetic IPC counters.
if ($ExpectedOutcome -eq 'Phase10AOh1' -and
(-not $trace.Contains('REV:P10A:OH1 READY REAL USB') -or
@($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or
-not $probeChecks -or $probeChecks[0] -lt $Phase10AMinimumChecks -or $probeChecks[1] -ne 0)) {
throw 'Phase 10A OH1 reduced smoke markers or checks are incomplete'
}
# Revolution IOS Phase 05F: the unchanged libogc stock client (ppc/stock-client).
if ($ExpectedOutcome -eq 'Phase5Client' -and
(-not $trace.Contains('REV:P3B:START') -or
@($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or
-not $probeCounters -or $probeCounters[0] -lt 16 -or
$probeCounters[0] -ne $probeCounters[1] -or
-not $probeChecks -or $probeChecks[0] -lt 16 -or
$probeChecks[1] -ne 0)) {
throw "Phase 05F stock client markers or counters are incomplete; see $logPath"
}
# Revolution IOS Phase 06H: the writable scenario (ppc/fs-write-probe, mode Write) on the
# emulator-only writable kernel over the disposable NAND (REV_BOOTSTRAP_NAND_DISPOSABLE).
if ($ExpectedOutcome -eq 'Phase6Write' -and
(-not $trace.Contains('REV:P3B:START') -or
@($trace | Where-Object { $_ -like 'REV:P6:FS MOUNT GEN * WRITABLE' }).Count -ne 1 -or
@($trace | Where-Object { $_ -like 'REV:P6:FS GEN * REMOUNT *' }).Count -ne 0 -or
-not $probeCounters -or $probeCounters[0] -lt 100 -or
$probeCounters[0] -ne $probeCounters[1] -or
$probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or
$probeCounters[4] -ne 0 -or
-not $probeChecks -or $probeChecks[0] -lt 60 -or
$probeChecks[1] -ne 0)) {
throw "Phase 06H writable scenario markers or counters are incomplete; see $logPath"
}
# Revolution IOS Phase 06H: the resulting image after a fresh boot of the read-only kernel
# (ppc/fs-write-probe, mode Verify).
if ($ExpectedOutcome -eq 'Phase6Verify' -and
(-not $trace.Contains('REV:P3B:START') -or
@($trace | Where-Object { $_ -like 'REV:P4:FS MOUNT GEN * AUTH YES *' }).Count -ne 1 -or
-not $probeCounters -or $probeCounters[0] -lt 30 -or
$probeCounters[0] -ne $probeCounters[1] -or
$probeCounters[2] -ne 0 -or $probeCounters[3] -ne 0 -or
$probeCounters[4] -ne 0 -or
-not $probeChecks -or $probeChecks[0] -lt 25 -or
$probeChecks[1] -ne 0)) {
throw "Phase 06H verification markers or counters are incomplete; see $logPath"
}
# Revolution IOS Phase 08 H07R (section 69): the libogc read-after-reboot client
# (ppc/p8-client/reboot.c), the same boot.dol as on the console, on the emulator-only
# writable kernel over the disposable NAND. It writes only its status word (0x52500005 when
# every check of its part passed); the part is proven by the NAND copy, outside this script.
if ($ExpectedOutcome -eq 'Phase8Client' -and
(-not $trace.Contains('REV:P3B:START') -or
@($trace | Where-Object { $_ -like 'REV:P6:FS MOUNT GEN * WRITABLE' }).Count -ne 1)) {
throw "Phase 08 client markers are incomplete; see $logPath"
}
# Revolution IOS Phase 09 P09-H02: Broadway stopped and restarted into the payload while the
# kernel stays resident; the payload's own checks passed.
if ($ExpectedOutcome -eq 'Phase9Launch' -and
(@($trace | Where-Object { $_ -like 'REV:P9:H02 GO *' }).Count -ne 1 -or
@($trace | Where-Object { $_ -like 'REV:P9:H02 FIRST CMD *' }).Count -ne 1 -or
-not $probeChecks -or $probeChecks[0] -lt $Phase9MinimumChecks -or $probeChecks[1] -ne 0)) {
throw "Phase 09 launch markers or checks are incomplete; see $logPath"
}
# Revolution IOS Phase 09 P09-H04: the Menu was resolved, profiled, loaded and released
# (M0-M4) before the observation ended.
if ($ExpectedOutcome -in @('Phase9Menu', 'Phase9Rescue')) {
$missingMenu = @('REV:P9:H04 M0 ', 'REV:P9:H04 M1 ', 'REV:P9:H04 M2 STOPPED LOWMEM VERIFY OK', 'REV:P9:H04 M3 ', 'REV:P9:H04 M4 ') |
Where-Object { $prefix = $_; @($trace | Where-Object { $_.StartsWith($prefix) }).Count -ne 1 }
if (@($missingMenu).Count -gt 0) {
throw "Phase 09 System Menu milestones are incomplete ($($missingMenu -join ', ')); see $logPath"
}
}
$required = @('REV:BOOT:ENTRY', 'REV:BOOT:BSS_OK', 'REV:BOOT:DATA_OK',
'REV:BOOT:STACK_OK', 'REV:BOOT:RUST', 'Revolution IOS',
'Starlet Rust bootstrap reached')
$missing = @($required | Where-Object { -not $trace.Contains($_) })
if ($ExpectedOutcome -in @('Idle', 'Phase2') -and $missing.Count -gt 0) {
throw "Bootstrap reached idle without all expected markers; see $logPath"
}
Write-Host "Starlet bootstrap $outcome passed ($($trace.Count) trace lines)."
} finally {
if (-not $dolphin.HasExited) {
Stop-Process -Id $dolphin.Id
$dolphin.WaitForExit()
}
}