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.
This commit is contained in:
1 parent
a8dbbab62d
commit
54f5a74bbf
28 files changed
+1767
-13
No files matched your search
@@ -1 +1,26 @@
|
|||||||
*.sln text eol=crlf
|
*.sln text eol=crlf
|
||||||
|
|
||||||
|
# Preserve the qualified StarletOS integration bytes across operating systems.
|
||||||
|
CMakeLists.txt -text
|
||||||
|
Run-Wii-Starlet-Bootstrap.ps1 -text
|
||||||
|
Source/Core/Core/Boot/Boot.cpp -text
|
||||||
|
Source/Core/Core/CMakeLists.txt -text
|
||||||
|
Source/Core/Core/Config/MainSettings.cpp -text
|
||||||
|
Source/Core/Core/Config/MainSettings.h -text
|
||||||
|
Source/Core/Core/ConfigManager.cpp -text
|
||||||
|
Source/Core/Core/HW/HW.cpp -text
|
||||||
|
Source/Core/Core/IOS/Starlet/ARMCore.cpp -text
|
||||||
|
Source/Core/Core/IOS/Starlet/BootstrapELF.cpp -text
|
||||||
|
Source/Core/Core/IOS/Starlet/BootstrapELF.h -text
|
||||||
|
Source/Core/Core/IOS/Starlet/Starlet.cpp -text
|
||||||
|
Source/Core/Core/IOS/Starlet/Starlet.h -text
|
||||||
|
Source/Core/Core/IOS/Starlet/StarletMemory.cpp -text
|
||||||
|
Source/Core/Core/IOS/Starlet/StarletMemory.h -text
|
||||||
|
Source/Core/DolphinNoGUI/PlatformHeadless.cpp -text
|
||||||
|
Source/UnitTests/CMakeLists.txt -text
|
||||||
|
Source/UnitTests/Core/CMakeLists.txt -text
|
||||||
|
Source/UnitTests/Core/IOS/Starlet/BootstrapELFTest.cpp -text
|
||||||
|
Source/UnitTests/Core/IOS/Starlet/DMATest.cpp -text
|
||||||
|
Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp -text
|
||||||
|
docs/Wii_Starlet_Bootstrap.md -text
|
||||||
|
docs/StarletOS_Checkpoint.json -text
|
||||||
@@ -75,3 +75,6 @@ CMakeUserPresets.json
|
|||||||
|
|
||||||
# Generated bytecode from the performance analysis helpers
|
# Generated bytecode from the performance analysis helpers
|
||||||
/Tools/PerformanceTrace/__pycache__/
|
/Tools/PerformanceTrace/__pycache__/
|
||||||
|
|
||||||
|
# Historical local script backup; active script is tracked.
|
||||||
|
/Run-Wii-Starlet-Bootstrap.ps1.before-*
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Instructions for the StarletOS Dolphin fork
|
||||||
|
|
||||||
|
Read `docs/StarletOS_Handoff.md`, `docs/StarletOS_Checkpoint.json` and
|
||||||
|
`docs/Wii_Starlet_Bootstrap.md` before working on the Starlet integration.
|
||||||
|
Also read the companion StarletOS HANDOFF and current phase SPEC/STATUS when
|
||||||
|
changing support for its tests. Current user instructions take precedence.
|
||||||
|
|
||||||
|
- One writer per checkout. Inspect Git status and the latest handoff before
|
||||||
|
editing; preserve unrelated changes and never overwrite another agent's work.
|
||||||
|
- Fetch the user's fork before work and inspect divergence. Use fast-forward
|
||||||
|
pulls only on a clean branch; never reset or force-push to hide divergence.
|
||||||
|
- The publication destination is `https://git.yaya48.xyz/Yaya48/dolphin`.
|
||||||
|
On the original checkout it is named `yaya`; on a fresh clone it is normally
|
||||||
|
`origin`. The github.com/dolphin-emu upstream is NOT the publication target.
|
||||||
|
- Preserve qualified source bytes/line endings. Do not broadly normalize the
|
||||||
|
repository. Exact integration files are protected by `.gitattributes`.
|
||||||
|
- Do not commit dumps, console keys/OTP/SEEPROM, Nintendo binaries, real device
|
||||||
|
identifiers, credentials, private emulator profiles or diagnostic captures.
|
||||||
|
Stage explicit paths and inspect staged contents AND pending commits before
|
||||||
|
publishing. Never use force-add to bypass these exclusions.
|
||||||
|
- Respect companion phase boundaries and public-source/clean-room rules. A
|
||||||
|
fake emulated device is supporting test evidence, not the physical contract.
|
||||||
|
- Update the handoff with changes, reasons, commands/results, limitations and
|
||||||
|
the next exact action at every stop. Preserve old evidence and distinguish
|
||||||
|
source inspection, build results, emulation and physical observations.
|
||||||
|
- Commit and push completed work to the user's fork, verify the remote SHA,
|
||||||
|
then update StarletOS's pinned Dolphin commit/hashes. Never leave a required
|
||||||
|
source change only in a private bundle or a copy in another repository.
|
||||||
|
- Keep generated caches and private evidence locally; no blanket cleanup of
|
||||||
|
ignored files. Do not modify a removable SD or console for a Git-only task.
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# Shared instructions
|
||||||
|
|
||||||
|
Read AGENTS.md and docs/StarletOS_Handoff.md before working in this fork.
|
||||||
@@ -23,6 +23,14 @@ set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
|
|||||||
|
|
||||||
project(dolphin-emu)
|
project(dolphin-emu)
|
||||||
|
|
||||||
|
# CMake 4.3 can decode French MSVC's /showIncludes prefix through the console code page,
|
||||||
|
# turning nonbreaking spaces into " ". Ninja then records zero header dependencies and may
|
||||||
|
# reuse objects after a class layout changes. Give Ninja the actual UTF-8 prefix.
|
||||||
|
if(MSVC AND CMAKE_GENERATOR STREQUAL "Ninja" AND CMAKE_CL_SHOWINCLUDES_PREFIX MATCHES "^Remarque")
|
||||||
|
string(ASCII 194 160 _msvc_french_nbsp)
|
||||||
|
set(CMAKE_CL_SHOWINCLUDES_PREFIX "Remarque${_msvc_french_nbsp}: inclusion du fichier${_msvc_french_nbsp}: ")
|
||||||
|
endif()
|
||||||
|
|
||||||
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/CMake)
|
list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/CMake)
|
||||||
|
|
||||||
# Support functions
|
# Support functions
|
||||||
|
|||||||
@@ -0,0 +1,322 @@
|
|||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -556,7 +556,9 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard,
|
|||||||
if (!executable.reader->IsValid())
|
if (!executable.reader->IsValid())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (system.GetStarlet())
|
const bool paired_starlet_probe = system.GetStarlet() &&
|
||||||
|
!Config::Get(Config::MAIN_WII_STARLET_BOOTSTRAP_ELF).empty();
|
||||||
|
if (system.GetStarlet() && !paired_starlet_probe)
|
||||||
{
|
{
|
||||||
PanicAlertFmtT("Direct executable boot is unavailable with Wii IOS LLE. Boot the NAND "
|
PanicAlertFmtT("Direct executable boot is unavailable with Wii IOS LLE. Boot the NAND "
|
||||||
"through Starlet, then launch the title through the original IOS.");
|
"through Starlet, then launch the title through the original IOS.");
|
||||||
@@ -580,8 +582,11 @@ bool CBoot::BootUp(Core::System& system, const Core::CPUThreadGuard& guard,
|
|||||||
|
|
||||||
// Because there is no TMD to get the requested system (IOS) version from,
|
// Because there is no TMD to get the requested system (IOS) version from,
|
||||||
// we default to IOS58, which is the version used by the Homebrew Channel.
|
// we default to IOS58, which is the version used by the Homebrew Channel.
|
||||||
SetupWiiMemory(system, IOS::HLE::IOSC::ConsoleType::Retail);
|
if (!paired_starlet_probe)
|
||||||
system.GetIOS()->BootIOS(Titles::IOS(58));
|
{
|
||||||
|
SetupWiiMemory(system, IOS::HLE::IOSC::ConsoleType::Retail);
|
||||||
|
system.GetIOS()->BootIOS(Titles::IOS(58));
|
||||||
|
}
|
||||||
|
|
||||||
// The Apploader writes an IOS-like version number into memory.
|
// The Apploader writes an IOS-like version number into memory.
|
||||||
// Older versions of OSInit read it to check IOS compatibility.
|
// Older versions of OSInit read it to check IOS compatibility.
|
||||||
|
|||||||
@@ -404,6 +404,8 @@ add_library(core
|
|||||||
IOS/MIOS.h
|
IOS/MIOS.h
|
||||||
IOS/Starlet/ARMCore.cpp
|
IOS/Starlet/ARMCore.cpp
|
||||||
IOS/Starlet/ARMCore.h
|
IOS/Starlet/ARMCore.h
|
||||||
|
IOS/Starlet/BootstrapELF.cpp
|
||||||
|
IOS/Starlet/BootstrapELF.h
|
||||||
IOS/Starlet/AX88772.cpp
|
IOS/Starlet/AX88772.cpp
|
||||||
IOS/Starlet/AX88772.h
|
IOS/Starlet/AX88772.h
|
||||||
IOS/Starlet/NANDJournal.cpp
|
IOS/Starlet/NANDJournal.cpp
|
||||||
|
|||||||
@@ -339,6 +339,8 @@ const Info<std::string> MAIN_LOAD_PATH{{System::Main, "General", "LoadPath"}, ""
|
|||||||
const Info<std::string> MAIN_RESOURCEPACK_PATH{{System::Main, "General", "ResourcePackPath"}, ""};
|
const Info<std::string> MAIN_RESOURCEPACK_PATH{{System::Main, "General", "ResourcePackPath"}, ""};
|
||||||
const Info<std::string> MAIN_FS_PATH{{System::Main, "General", "NANDRootPath"}, ""};
|
const Info<std::string> MAIN_FS_PATH{{System::Main, "General", "NANDRootPath"}, ""};
|
||||||
const Info<std::string> MAIN_WII_LLE_DUMP_PATH{{System::Main, "General", "WiiIOSLLEDumpPath"}, ""};
|
const Info<std::string> MAIN_WII_LLE_DUMP_PATH{{System::Main, "General", "WiiIOSLLEDumpPath"}, ""};
|
||||||
|
const Info<std::string> MAIN_WII_STARLET_BOOTSTRAP_ELF{
|
||||||
|
{System::Main, "General", "WiiStarletBootstrapELF"}, ""};
|
||||||
const Info<std::string> MAIN_WII_SD_CARD_IMAGE_PATH{{System::Main, "General", "WiiSDCardPath"}, ""};
|
const Info<std::string> MAIN_WII_SD_CARD_IMAGE_PATH{{System::Main, "General", "WiiSDCardPath"}, ""};
|
||||||
const Info<std::string> MAIN_WII_SD_CARD_SYNC_FOLDER_PATH{
|
const Info<std::string> MAIN_WII_SD_CARD_SYNC_FOLDER_PATH{
|
||||||
{System::Main, "General", "WiiSDCardSyncFolder"}, ""};
|
{System::Main, "General", "WiiSDCardSyncFolder"}, ""};
|
||||||
|
|||||||
@@ -212,6 +212,7 @@ extern const Info<std::string> MAIN_LOAD_PATH;
|
|||||||
extern const Info<std::string> MAIN_RESOURCEPACK_PATH;
|
extern const Info<std::string> MAIN_RESOURCEPACK_PATH;
|
||||||
extern const Info<std::string> MAIN_FS_PATH;
|
extern const Info<std::string> MAIN_FS_PATH;
|
||||||
extern const Info<std::string> MAIN_WII_LLE_DUMP_PATH;
|
extern const Info<std::string> MAIN_WII_LLE_DUMP_PATH;
|
||||||
|
extern const Info<std::string> MAIN_WII_STARLET_BOOTSTRAP_ELF;
|
||||||
extern const Info<std::string> MAIN_WII_SD_CARD_IMAGE_PATH;
|
extern const Info<std::string> MAIN_WII_SD_CARD_IMAGE_PATH;
|
||||||
extern const Info<std::string> MAIN_WII_SD_CARD_SYNC_FOLDER_PATH;
|
extern const Info<std::string> MAIN_WII_SD_CARD_SYNC_FOLDER_PATH;
|
||||||
extern const Info<std::string> MAIN_WFS_PATH;
|
extern const Info<std::string> MAIN_WFS_PATH;
|
||||||
|
|||||||
@@ -385,7 +385,11 @@ struct SetGameMetadata
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
*region = DiscIO::Region::Unknown;
|
*region = DiscIO::Region::Unknown;
|
||||||
system.SetIsWii(executable.reader->IsWii());
|
// A paired Starlet bootstrap plus a direct PPC probe is a Wii session even
|
||||||
|
// when the tiny probe has no HID4 instruction for the executable heuristic.
|
||||||
|
const bool paired_starlet_probe = Config::Get(Config::MAIN_WII_IOS_LLE) &&
|
||||||
|
!Config::Get(Config::MAIN_WII_STARLET_BOOTSTRAP_ELF).empty();
|
||||||
|
system.SetIsWii(executable.reader->IsWii() || paired_starlet_probe);
|
||||||
|
|
||||||
// Strip the .elf/.dol file extension and directories before the name
|
// Strip the .elf/.dol file extension and directories before the name
|
||||||
SplitPath(executable.path, nullptr, &config->m_debugger_game_id, nullptr);
|
SplitPath(executable.path, nullptr, &config->m_debugger_game_id, nullptr);
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ void Init(Core::System& system, const Sram* override_sram)
|
|||||||
system.GetWiiIPC().Init();
|
system.GetWiiIPC().Init();
|
||||||
if (Config::Get(Config::MAIN_WII_IOS_LLE))
|
if (Config::Get(Config::MAIN_WII_IOS_LLE))
|
||||||
{
|
{
|
||||||
auto starlet = std::make_unique<IOS::LLE::Starlet>(system);
|
auto starlet = IOS::LLE::Starlet::Create(system);
|
||||||
std::string error;
|
std::string error;
|
||||||
if (starlet->Init(Config::Get(Config::MAIN_WII_LLE_DUMP_PATH), &error))
|
if (starlet->Init(Config::Get(Config::MAIN_WII_LLE_DUMP_PATH), &error))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2142,6 +2142,41 @@ void ARMCore::ExecuteHalfwordDataTransfer(u32 instruction)
|
|||||||
const u32 indexed = add_offset ? base + offset : base - offset;
|
const u32 indexed = add_offset ? base + offset : base - offset;
|
||||||
const u32 address = preindex ? indexed : base;
|
const u32 address = preindex ? indexed : base;
|
||||||
|
|
||||||
|
// ARMv5TE encodes LDRD/STRD in the extra load/store space with L clear.
|
||||||
|
// The even register names the first word; its odd successor names the
|
||||||
|
// second. ARM926 requires an 8-byte-aligned effective address.
|
||||||
|
if (!load && (type == 2 || type == 3))
|
||||||
|
{
|
||||||
|
const bool update_base = !preindex || writeback;
|
||||||
|
if ((rd & 1) != 0 || rd >= 14 || (update_base && (rn == rd || rn == rd + 1)))
|
||||||
|
{
|
||||||
|
UndefinedInstruction(instruction);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((address & 7) != 0)
|
||||||
|
{
|
||||||
|
m_cp15.fault_status = 1; // alignment fault
|
||||||
|
m_cp15.fault_address = address;
|
||||||
|
EnterException(Mode::Abort, STARLET_EXCEPTION_VECTOR_BASE + 0x10, m_instruction_address + 8);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (type == 2) // LDRD
|
||||||
|
{
|
||||||
|
const u32 first = Read32(address);
|
||||||
|
const u32 second = Read32(address + 4);
|
||||||
|
m_registers[rd] = first;
|
||||||
|
m_registers[rd + 1] = second;
|
||||||
|
}
|
||||||
|
else // STRD
|
||||||
|
{
|
||||||
|
Write32(address, ReadRegisterForStore(rd));
|
||||||
|
Write32(address + 4, ReadRegisterForStore(rd + 1));
|
||||||
|
}
|
||||||
|
if (update_base)
|
||||||
|
m_registers[rn] = indexed;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (load)
|
if (load)
|
||||||
{
|
{
|
||||||
u32 value;
|
u32 value;
|
||||||
@@ -2169,8 +2204,6 @@ void ARMCore::ExecuteHalfwordDataTransfer(u32 instruction)
|
|||||||
{
|
{
|
||||||
if (type != 1)
|
if (type != 1)
|
||||||
{
|
{
|
||||||
// ARMv5 also uses this space for LDRD/STRD. They are deliberately
|
|
||||||
// rejected until their alignment and abort behavior is implemented.
|
|
||||||
UndefinedInstruction(instruction);
|
UndefinedInstruction(instruction);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
// Copyright 2026 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include "Core/IOS/Starlet/BootstrapELF.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <optional>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include "Core/IOS/Starlet/ARMCore.h"
|
||||||
|
|
||||||
|
namespace IOS::LLE
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr u32 TEXT_BASE = 0xffff0000;
|
||||||
|
constexpr u64 TEXT_END = 0x1'0000'0000ULL;
|
||||||
|
constexpr u32 DATA_BASE = 0xfffe0000;
|
||||||
|
constexpr u32 DATA_END = 0xfffe4000;
|
||||||
|
// Revolution IOS Phase 06H: MEM2, for the optional disposable-image text segment.
|
||||||
|
constexpr u32 MEM2_BASE = 0x10000000;
|
||||||
|
constexpr u64 MEM2_END = 0x14000000ULL;
|
||||||
|
constexpr u32 TRACE_SIZE = 1032;
|
||||||
|
constexpr u32 ELF_HEADER_SIZE = 52;
|
||||||
|
constexpr u32 PROGRAM_HEADER_SIZE = 32;
|
||||||
|
constexpr u32 SECTION_HEADER_SIZE = 40;
|
||||||
|
constexpr u32 SYMBOL_SIZE = 16;
|
||||||
|
|
||||||
|
struct Segment
|
||||||
|
{
|
||||||
|
u32 offset = 0;
|
||||||
|
u32 address = 0;
|
||||||
|
u32 file_size = 0;
|
||||||
|
u32 memory_size = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool Fits(std::span<const u8> image, u64 offset, u64 size)
|
||||||
|
{
|
||||||
|
return offset <= image.size() && size <= image.size() - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
u16 Read16(std::span<const u8> image, size_t offset)
|
||||||
|
{
|
||||||
|
return static_cast<u16>((static_cast<u16>(image[offset]) << 8) | image[offset + 1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
u32 Read32(std::span<const u8> image, size_t offset)
|
||||||
|
{
|
||||||
|
return (static_cast<u32>(image[offset]) << 24) | (static_cast<u32>(image[offset + 1]) << 16) |
|
||||||
|
(static_cast<u32>(image[offset + 2]) << 8) | image[offset + 3];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Fail(std::string* error, std::string_view reason)
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
*error = reason;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string_view> SymbolName(std::span<const u8> image, u32 strings_offset,
|
||||||
|
u32 strings_size, u32 name_offset)
|
||||||
|
{
|
||||||
|
if (name_offset >= strings_size)
|
||||||
|
return std::nullopt;
|
||||||
|
const size_t start = static_cast<size_t>(strings_offset) + name_offset;
|
||||||
|
const auto first = image.begin() + start;
|
||||||
|
const auto last = image.begin() + strings_offset + strings_size;
|
||||||
|
const auto end = std::find(first, last, 0);
|
||||||
|
if (end == last)
|
||||||
|
return std::nullopt;
|
||||||
|
return std::string_view(reinterpret_cast<const char*>(&*first), static_cast<size_t>(end - first));
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool LoadBootstrapELF(std::span<const u8> image, ARMBus& bus, BootstrapELFInfo* info,
|
||||||
|
std::string* error, bool allow_mem2_text)
|
||||||
|
{
|
||||||
|
if (!info)
|
||||||
|
return Fail(error, "missing bootstrap ELF result");
|
||||||
|
if (!Fits(image, 0, ELF_HEADER_SIZE) || image[0] != 0x7f || image[1] != 'E' || image[2] != 'L' ||
|
||||||
|
image[3] != 'F' || image[4] != 1 || image[5] != 2 || image[6] != 1 ||
|
||||||
|
Read16(image, 16) != 2 || Read16(image, 18) != 40 || Read32(image, 20) != 1 ||
|
||||||
|
Read16(image, 40) != ELF_HEADER_SIZE)
|
||||||
|
{
|
||||||
|
return Fail(error, "expected ELF32 big-endian ARM executable");
|
||||||
|
}
|
||||||
|
|
||||||
|
const u32 entry = Read32(image, 24);
|
||||||
|
const u32 program_offset = Read32(image, 28);
|
||||||
|
const u32 section_offset = Read32(image, 32);
|
||||||
|
const u16 program_size = Read16(image, 42);
|
||||||
|
const u16 program_count = Read16(image, 44);
|
||||||
|
const u16 section_size = Read16(image, 46);
|
||||||
|
const u16 section_count = Read16(image, 48);
|
||||||
|
const bool program_count_valid = program_count == 2 || (allow_mem2_text && program_count == 3);
|
||||||
|
if (entry != TEXT_BASE || program_size != PROGRAM_HEADER_SIZE || !program_count_valid ||
|
||||||
|
section_size != SECTION_HEADER_SIZE || section_count == 0 ||
|
||||||
|
!Fits(image, program_offset, static_cast<u64>(program_size) * program_count) ||
|
||||||
|
!Fits(image, section_offset, static_cast<u64>(section_size) * section_count))
|
||||||
|
{
|
||||||
|
return Fail(error, "bootstrap ELF has an unsupported entry or header table");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<Segment, 3> storage;
|
||||||
|
const std::span<Segment> segments(storage.data(), program_count);
|
||||||
|
u32 text_count = 0;
|
||||||
|
u32 data_count = 0;
|
||||||
|
u32 mem2_count = 0;
|
||||||
|
for (u32 index = 0; index < segments.size(); ++index)
|
||||||
|
{
|
||||||
|
const size_t offset = program_offset + index * PROGRAM_HEADER_SIZE;
|
||||||
|
const u32 file_offset = Read32(image, offset + 4);
|
||||||
|
const u32 address = Read32(image, offset + 8);
|
||||||
|
const u32 physical_address = Read32(image, offset + 12);
|
||||||
|
const u32 file_size = Read32(image, offset + 16);
|
||||||
|
const u32 memory_size = Read32(image, offset + 20);
|
||||||
|
const u32 flags = Read32(image, offset + 24);
|
||||||
|
const bool text =
|
||||||
|
address == TEXT_BASE && flags == 5 && static_cast<u64>(address) + memory_size <= TEXT_END;
|
||||||
|
const bool data =
|
||||||
|
address == DATA_BASE && flags == 6 && static_cast<u64>(address) + memory_size <= DATA_END;
|
||||||
|
const bool mem2 = allow_mem2_text && flags == 5 && address >= MEM2_BASE &&
|
||||||
|
static_cast<u64>(address) + memory_size <= MEM2_END;
|
||||||
|
if (Read32(image, offset) != 1 || address != physical_address || file_size == 0 ||
|
||||||
|
memory_size < file_size || !Fits(image, file_offset, file_size) ||
|
||||||
|
(!text && !data && !mem2))
|
||||||
|
{
|
||||||
|
return Fail(error, "bootstrap ELF has an invalid SRAM load segment");
|
||||||
|
}
|
||||||
|
text_count += text;
|
||||||
|
data_count += data;
|
||||||
|
mem2_count += mem2;
|
||||||
|
segments[index] = {file_offset, address, file_size, memory_size};
|
||||||
|
}
|
||||||
|
if (text_count != 1 || data_count != 1 || mem2_count != program_count - 2u)
|
||||||
|
return Fail(error, "bootstrap ELF must have one text and one data segment");
|
||||||
|
|
||||||
|
BootstrapELFInfo parsed{entry};
|
||||||
|
bool found_trace = false;
|
||||||
|
bool found_fault_code = false;
|
||||||
|
bool found_fault_lr = false;
|
||||||
|
bool found_fault_spsr = false;
|
||||||
|
for (u32 index = 0; index < section_count; ++index)
|
||||||
|
{
|
||||||
|
const size_t offset = section_offset + index * SECTION_HEADER_SIZE;
|
||||||
|
if (Read32(image, offset + 4) != 2)
|
||||||
|
continue;
|
||||||
|
const u32 symbols_offset = Read32(image, offset + 16);
|
||||||
|
const u32 symbols_size = Read32(image, offset + 20);
|
||||||
|
const u32 strings_index = Read32(image, offset + 24);
|
||||||
|
const u32 entry_size = Read32(image, offset + 36);
|
||||||
|
if (entry_size != SYMBOL_SIZE || symbols_size % SYMBOL_SIZE != 0 ||
|
||||||
|
!Fits(image, symbols_offset, symbols_size) || strings_index >= section_count)
|
||||||
|
{
|
||||||
|
return Fail(error, "bootstrap ELF has an invalid symbol table");
|
||||||
|
}
|
||||||
|
const size_t strings_header = section_offset + strings_index * SECTION_HEADER_SIZE;
|
||||||
|
const u32 strings_offset = Read32(image, strings_header + 16);
|
||||||
|
const u32 strings_size = Read32(image, strings_header + 20);
|
||||||
|
if (Read32(image, strings_header + 4) != 3 || !Fits(image, strings_offset, strings_size))
|
||||||
|
{
|
||||||
|
return Fail(error, "bootstrap ELF has an invalid symbol string table");
|
||||||
|
}
|
||||||
|
for (u32 symbol = 0; symbol < symbols_size / SYMBOL_SIZE; ++symbol)
|
||||||
|
{
|
||||||
|
const size_t symbol_offset = symbols_offset + symbol * SYMBOL_SIZE;
|
||||||
|
const auto name =
|
||||||
|
SymbolName(image, strings_offset, strings_size, Read32(image, symbol_offset));
|
||||||
|
if (!name)
|
||||||
|
return Fail(error, "bootstrap ELF has an invalid symbol name");
|
||||||
|
const u32 address = Read32(image, symbol_offset + 4);
|
||||||
|
if (*name == "REV_TRACE")
|
||||||
|
{
|
||||||
|
if (Read32(image, symbol_offset + 8) != TRACE_SIZE)
|
||||||
|
return Fail(error, "bootstrap ELF trace layout does not match the monitor");
|
||||||
|
parsed.trace = address;
|
||||||
|
found_trace = true;
|
||||||
|
}
|
||||||
|
else if (*name == "__fault_code")
|
||||||
|
{
|
||||||
|
parsed.fault_code = address;
|
||||||
|
found_fault_code = true;
|
||||||
|
}
|
||||||
|
else if (*name == "__fault_lr")
|
||||||
|
{
|
||||||
|
parsed.fault_lr = address;
|
||||||
|
found_fault_lr = true;
|
||||||
|
}
|
||||||
|
else if (*name == "__fault_spsr")
|
||||||
|
{
|
||||||
|
parsed.fault_spsr = address;
|
||||||
|
found_fault_spsr = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const auto data_segment =
|
||||||
|
std::find_if(segments.begin(), segments.end(),
|
||||||
|
[](const Segment& segment) { return segment.address == DATA_BASE; });
|
||||||
|
const auto in_data = [data_segment](u32 address, u32 size) {
|
||||||
|
return address >= data_segment->address &&
|
||||||
|
static_cast<u64>(address) + size <=
|
||||||
|
static_cast<u64>(data_segment->address) + data_segment->memory_size;
|
||||||
|
};
|
||||||
|
if (!found_trace || !found_fault_code || !found_fault_lr || !found_fault_spsr ||
|
||||||
|
!in_data(parsed.trace, TRACE_SIZE) || !in_data(parsed.fault_code, 4) ||
|
||||||
|
!in_data(parsed.fault_lr, 4) || !in_data(parsed.fault_spsr, 4))
|
||||||
|
{
|
||||||
|
return Fail(error, "bootstrap ELF is missing the trace or fault symbols");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const Segment& segment : segments)
|
||||||
|
{
|
||||||
|
for (u32 index = 0; index < segment.file_size; ++index)
|
||||||
|
bus.Write8(segment.address + index, image[segment.offset + index]);
|
||||||
|
for (u32 index = segment.file_size; index < segment.memory_size; ++index)
|
||||||
|
bus.Write8(segment.address + index, 0);
|
||||||
|
}
|
||||||
|
*info = parsed;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} // namespace IOS::LLE
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2026 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <span>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "Common/CommonTypes.h"
|
||||||
|
|
||||||
|
namespace IOS::LLE
|
||||||
|
{
|
||||||
|
class ARMBus;
|
||||||
|
|
||||||
|
struct BootstrapELFInfo
|
||||||
|
{
|
||||||
|
u32 entry = 0;
|
||||||
|
u32 trace = 0;
|
||||||
|
u32 fault_code = 0;
|
||||||
|
u32 fault_lr = 0;
|
||||||
|
u32 fault_spsr = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load the Phase 01 ELF into a bus already configured with the high SRAM mapping.
|
||||||
|
// All file and address validation completes before the first bus write.
|
||||||
|
// Revolution IOS Phase 06H: `allow_mem2_text` (set only with the emulator-only disposable
|
||||||
|
// writable NAND) additionally accepts one read/execute segment wholly inside MEM2. The SRAM
|
||||||
|
// text and data segments stay mandatory; without the flag the rules are unchanged.
|
||||||
|
bool LoadBootstrapELF(std::span<const u8> image, ARMBus& bus, BootstrapELFInfo* info,
|
||||||
|
std::string* error, bool allow_mem2_text = false);
|
||||||
|
} // namespace IOS::LLE
|
||||||
@@ -4,18 +4,24 @@
|
|||||||
#include "Core/IOS/Starlet/Starlet.h"
|
#include "Core/IOS/Starlet/Starlet.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <cstdlib>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "Common/ChunkFile.h"
|
#include "Common/ChunkFile.h"
|
||||||
#include "Common/GekkoDisassembler.h"
|
#include "Common/GekkoDisassembler.h"
|
||||||
|
#include "Common/IOFile.h"
|
||||||
#include "Common/Logging/Log.h"
|
#include "Common/Logging/Log.h"
|
||||||
#include "Common/PerformanceTrace.h"
|
#include "Common/PerformanceTrace.h"
|
||||||
#include "Common/Timer.h"
|
#include "Common/Timer.h"
|
||||||
#include "Core/Config/MainSettings.h"
|
#include "Core/Config/MainSettings.h"
|
||||||
#include "Core/CoreTiming.h"
|
#include "Core/CoreTiming.h"
|
||||||
|
#include "Core/HW/Memmap.h"
|
||||||
#include "Core/HW/ProcessorInterface.h"
|
#include "Core/HW/ProcessorInterface.h"
|
||||||
#include "Core/HW/WII_IPC.h"
|
#include "Core/HW/WII_IPC.h"
|
||||||
#include "Core/IOS/Starlet/ARMCore.h"
|
#include "Core/IOS/Starlet/ARMCore.h"
|
||||||
|
#include "Core/IOS/Starlet/BootstrapELF.h"
|
||||||
#include "Core/IOS/Starlet/StarletMemory.h"
|
#include "Core/IOS/Starlet/StarletMemory.h"
|
||||||
#include "Core/PowerPC/MMU.h"
|
#include "Core/PowerPC/MMU.h"
|
||||||
#include "Core/PowerPC/PowerPC.h"
|
#include "Core/PowerPC/PowerPC.h"
|
||||||
@@ -23,6 +29,11 @@
|
|||||||
|
|
||||||
namespace IOS::LLE
|
namespace IOS::LLE
|
||||||
{
|
{
|
||||||
|
std::unique_ptr<Starlet> Starlet::Create(Core::System& system)
|
||||||
|
{
|
||||||
|
return std::make_unique<Starlet>(system);
|
||||||
|
}
|
||||||
|
|
||||||
Starlet::Starlet(Core::System& system) : m_system(system)
|
Starlet::Starlet(Core::System& system) : m_system(system)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
@@ -37,11 +48,87 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
|
|||||||
// Allocate the concrete bus here; its device-state members are part of the
|
// Allocate the concrete bus here; its device-state members are part of the
|
||||||
// allocation size and must stay in lockstep with StarletMemory's definition.
|
// allocation size and must stay in lockstep with StarletMemory's definition.
|
||||||
m_memory = std::make_unique<StarletMemory>(m_system);
|
m_memory = std::make_unique<StarletMemory>(m_system);
|
||||||
if (!m_memory->Init(dump_directory, error))
|
const std::string bootstrap_path = Config::Get(Config::MAIN_WII_STARLET_BOOTSTRAP_ELF);
|
||||||
|
if (bootstrap_path.empty() && !m_memory->Init(dump_directory, error))
|
||||||
{
|
{
|
||||||
m_memory.reset();
|
m_memory.reset();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!bootstrap_path.empty())
|
||||||
|
{
|
||||||
|
File::IOFile file(bootstrap_path, "rb");
|
||||||
|
const u64 size = file ? file.GetSize() : 0;
|
||||||
|
if (size == 0 || size > 16 * 1024 * 1024)
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
*error = "WiiStarletBootstrapELF must name a readable ELF of at most 16 MiB";
|
||||||
|
m_memory.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::vector<u8> image(static_cast<size_t>(size));
|
||||||
|
if (!file.ReadBytes(image.data(), image.size()))
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
*error = "could not read WiiStarletBootstrapELF";
|
||||||
|
m_memory.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_memory->PrepareBootstrap();
|
||||||
|
// Revolution IOS Phase 06H, emulator only: a disposable writable NAND, and with it the
|
||||||
|
// MEM2 text segment of the disposable-image kernel. Nothing else accepts either.
|
||||||
|
const char* disposable_source = std::getenv("REV_BOOTSTRAP_NAND_DISPOSABLE");
|
||||||
|
const bool disposable = disposable_source && *disposable_source;
|
||||||
|
BootstrapELFInfo info;
|
||||||
|
if (!LoadBootstrapELF(image, *m_memory, &info, error, disposable))
|
||||||
|
{
|
||||||
|
m_memory.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_bootstrap = info;
|
||||||
|
INFO_LOG_FMT(IOS, "Starlet bootstrap ELF loaded: {} entry={:#010x} trace={:#010x}",
|
||||||
|
bootstrap_path, info.entry, info.trace);
|
||||||
|
if (disposable)
|
||||||
|
{
|
||||||
|
const char* read_only_path = std::getenv("REV_BOOTSTRAP_NAND");
|
||||||
|
const char* copy_path = std::getenv("REV_BOOTSTRAP_NAND_COPY");
|
||||||
|
if ((read_only_path && *read_only_path) || !copy_path || !*copy_path)
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
*error = "REV_BOOTSTRAP_NAND_DISPOSABLE needs REV_BOOTSTRAP_NAND_COPY and excludes "
|
||||||
|
"REV_BOOTSTRAP_NAND";
|
||||||
|
m_memory.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!m_memory->AttachDisposableNAND(disposable_source, copy_path, error))
|
||||||
|
{
|
||||||
|
m_memory.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
WARN_LOG_FMT(IOS, "Starlet bootstrap NAND DISPOSABLE: source {} (read-only), copy {}",
|
||||||
|
disposable_source, copy_path);
|
||||||
|
}
|
||||||
|
// Revolution IOS Phase 04B: optionally expose a raw NAND image, strictly read-only, so a
|
||||||
|
// bootstrap ELF can validate its NAND driver. Program and erase always fail in this mode.
|
||||||
|
if (const char* nand_path = std::getenv("REV_BOOTSTRAP_NAND"); nand_path && *nand_path)
|
||||||
|
{
|
||||||
|
if (!m_memory->AttachReadOnlyNAND(nand_path, error))
|
||||||
|
{
|
||||||
|
m_memory.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
INFO_LOG_FMT(IOS, "Starlet bootstrap NAND attached read-only: {}", nand_path);
|
||||||
|
}
|
||||||
|
// Revolution IOS Phase 04C: optional keys.bin so OTP reads match the attached dump.
|
||||||
|
if (const char* keys_path = std::getenv("REV_BOOTSTRAP_KEYS"); keys_path && *keys_path)
|
||||||
|
{
|
||||||
|
if (!m_memory->AttachKeys(keys_path, error))
|
||||||
|
{
|
||||||
|
m_memory.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
INFO_LOG_FMT(IOS, "Starlet bootstrap keys attached");
|
||||||
|
}
|
||||||
|
}
|
||||||
// ARMCore owns generation-tagged software TLB and instruction caches, so
|
// ARMCore owns generation-tagged software TLB and instruction caches, so
|
||||||
// construct it only after the complete physical bus exists.
|
// construct it only after the complete physical bus exists.
|
||||||
m_core = std::make_unique<ARMCore>(*m_memory,
|
m_core = std::make_unique<ARMCore>(*m_memory,
|
||||||
@@ -51,7 +138,9 @@ bool Starlet::Init(const std::string& dump_directory, std::string* error)
|
|||||||
#else
|
#else
|
||||||
m_core->SetJitEnabled(false);
|
m_core->SetJitEnabled(false);
|
||||||
#endif
|
#endif
|
||||||
m_core->Reset(StarletMemory::BOOT_ROM_BASE);
|
m_core->Reset(m_bootstrap ? m_bootstrap->entry : StarletMemory::BOOT_ROM_BASE);
|
||||||
|
if (m_bootstrap)
|
||||||
|
m_core->SetBigEndian(true);
|
||||||
m_next_jit_diagnostic_instruction = 25'000'000;
|
m_next_jit_diagnostic_instruction = 25'000'000;
|
||||||
m_next_realtime_diagnostic_ms = Common::Timer::NowMs() + 1000;
|
m_next_realtime_diagnostic_ms = Common::Timer::NowMs() + 1000;
|
||||||
m_realtime_diagnostic_slice_count = 0;
|
m_realtime_diagnostic_slice_count = 0;
|
||||||
@@ -101,6 +190,7 @@ void Starlet::Shutdown()
|
|||||||
m_run_event = nullptr;
|
m_run_event = nullptr;
|
||||||
m_core.reset();
|
m_core.reset();
|
||||||
m_memory.reset();
|
m_memory.reset();
|
||||||
|
m_bootstrap.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Starlet::DoState(PointerWrap& p)
|
void Starlet::DoState(PointerWrap& p)
|
||||||
@@ -110,6 +200,14 @@ void Starlet::DoState(PointerWrap& p)
|
|||||||
return;
|
return;
|
||||||
m_memory->DoState(p);
|
m_memory->DoState(p);
|
||||||
m_core->DoState(p);
|
m_core->DoState(p);
|
||||||
|
if (p.IsReadMode())
|
||||||
|
{
|
||||||
|
m_bootstrap_trace_length = 0;
|
||||||
|
m_last_probe_status = 0;
|
||||||
|
m_bootstrap_line_length = 0;
|
||||||
|
m_bootstrap_outcome_logged = false;
|
||||||
|
m_bootstrap_fault_logged = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
u32 Starlet::GetPC() const
|
u32 Starlet::GetPC() const
|
||||||
@@ -201,6 +299,41 @@ void Starlet::RunSlice(s64 cycles_late)
|
|||||||
ARM_IDLE_SLICE_CYCLES :
|
ARM_IDLE_SLICE_CYCLES :
|
||||||
(ipc_handshake_active ? ARM_IPC_SLICE_CYCLES : ARM_ACTIVE_SLICE_CYCLES);
|
(ipc_handshake_active ? ARM_IPC_SLICE_CYCLES : ARM_ACTIVE_SLICE_CYCLES);
|
||||||
m_core->RunCycles(arm_cycles);
|
m_core->RunCycles(arm_cycles);
|
||||||
|
if (m_bootstrap && !m_bootstrap_outcome_logged)
|
||||||
|
TraceBootstrap();
|
||||||
|
// Observe the PPC test program's result word. This is diagnostic only: all
|
||||||
|
// request and response traffic still passes through shared RAM and IPC MMIO.
|
||||||
|
if (m_bootstrap)
|
||||||
|
{
|
||||||
|
constexpr u32 PPC_PROBE_STATUS_ADDRESS = 0x00003c00;
|
||||||
|
const u32 status = m_system.GetMemory().Read_U32(PPC_PROBE_STATUS_ADDRESS);
|
||||||
|
if ((status & 0xffff0000) == 0x52500000 && status != m_last_probe_status)
|
||||||
|
{
|
||||||
|
m_last_probe_status = status;
|
||||||
|
INFO_LOG_FMT(IOS, "Broadway ios-probe status={:#010x}", status);
|
||||||
|
if (status == 0x52500002 || status == 0x52500003 || status == 0x52500005 ||
|
||||||
|
(status & 0xfffff000) == 0x5250e000)
|
||||||
|
{
|
||||||
|
constexpr u32 PPC_PROBE_COUNTERS_ADDRESS = 0x00003c20;
|
||||||
|
INFO_LOG_FMT(IOS,
|
||||||
|
"Broadway ios-probe counters sent={} received={} invalid={} duplicate={} "
|
||||||
|
"timeout={}",
|
||||||
|
m_system.GetMemory().Read_U32(PPC_PROBE_COUNTERS_ADDRESS),
|
||||||
|
m_system.GetMemory().Read_U32(PPC_PROBE_COUNTERS_ADDRESS + 4),
|
||||||
|
m_system.GetMemory().Read_U32(PPC_PROBE_COUNTERS_ADDRESS + 8),
|
||||||
|
m_system.GetMemory().Read_U32(PPC_PROBE_COUNTERS_ADDRESS + 12),
|
||||||
|
m_system.GetMemory().Read_U32(PPC_PROBE_COUNTERS_ADDRESS + 16));
|
||||||
|
// 0x52500005 / 0x5250e2xx: Revolution IOS Phase 04D /dev/fs cases.
|
||||||
|
if (status == 0x52500003 || status == 0x52500005 ||
|
||||||
|
(status & 0xffffff00) == 0x5250e100 || (status & 0xffffff00) == 0x5250e200)
|
||||||
|
{
|
||||||
|
INFO_LOG_FMT(IOS, "Broadway ios-probe checks={} failures={}",
|
||||||
|
m_system.GetMemory().Read_U32(PPC_PROBE_COUNTERS_ADDRESS + 20),
|
||||||
|
m_system.GetMemory().Read_U32(PPC_PROBE_COUNTERS_ADDRESS + 24));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Keep host-clock reads and diagnostic formatting off the normal scheduler path.
|
// Keep host-clock reads and diagnostic formatting off the normal scheduler path.
|
||||||
const bool check_realtime_diagnostic =
|
const bool check_realtime_diagnostic =
|
||||||
@@ -415,4 +548,57 @@ void Starlet::RunSlice(s64 cycles_late)
|
|||||||
const s64 next = std::max<s64>(1, static_cast<s64>(broadway_cycles) - cycles_late);
|
const s64 next = std::max<s64>(1, static_cast<s64>(broadway_cycles) - cycles_late);
|
||||||
m_system.GetCoreTiming().ScheduleEvent(next, m_run_event);
|
m_system.GetCoreTiming().ScheduleEvent(next, m_run_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Starlet::TraceBootstrap()
|
||||||
|
{
|
||||||
|
const BootstrapELFInfo& bootstrap = *m_bootstrap;
|
||||||
|
const u32 fault_code = m_memory->Read32(bootstrap.fault_code);
|
||||||
|
if (fault_code != 0 && !m_bootstrap_fault_logged)
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(IOS,
|
||||||
|
"Starlet bootstrap exception {}: LR={:#010x} SPSR={:#010x} PC={:#010x}",
|
||||||
|
fault_code, m_memory->Read32(bootstrap.fault_lr),
|
||||||
|
m_memory->Read32(bootstrap.fault_spsr), m_core->GetRegister(15));
|
||||||
|
m_bootstrap_fault_logged = true;
|
||||||
|
m_bootstrap_outcome_logged = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (m_memory->Read32(bootstrap.trace) != 0x52455631)
|
||||||
|
return;
|
||||||
|
const u32 length = m_memory->Read32(bootstrap.trace + 4);
|
||||||
|
if (length > 1024)
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(IOS, "Starlet bootstrap trace length is invalid: {}", length);
|
||||||
|
m_bootstrap_outcome_logged = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (length < m_bootstrap_trace_length)
|
||||||
|
{
|
||||||
|
m_bootstrap_trace_length = 0;
|
||||||
|
m_bootstrap_line_length = 0;
|
||||||
|
}
|
||||||
|
for (u32 index = m_bootstrap_trace_length; index < length; ++index)
|
||||||
|
{
|
||||||
|
const char character = static_cast<char>(m_memory->Read8(bootstrap.trace + 8 + index));
|
||||||
|
if (character == '\n')
|
||||||
|
{
|
||||||
|
const std::string_view line(m_bootstrap_line.data(), m_bootstrap_line_length);
|
||||||
|
INFO_LOG_FMT(IOS, "Starlet bootstrap: {}", line);
|
||||||
|
if (line == "REV:BOOT:IDLE" || line.starts_with("REV:PANIC") ||
|
||||||
|
line.starts_with("REV:FATAL"))
|
||||||
|
{
|
||||||
|
INFO_LOG_FMT(IOS, "Starlet bootstrap stopped at PC={:#010x} after {} instructions",
|
||||||
|
m_core->GetRegister(15), m_core->GetExecutedInstructions());
|
||||||
|
m_bootstrap_outcome_logged = true;
|
||||||
|
}
|
||||||
|
m_bootstrap_line_length = 0;
|
||||||
|
}
|
||||||
|
else if (m_bootstrap_line_length < m_bootstrap_line.size())
|
||||||
|
{
|
||||||
|
m_bootstrap_line[m_bootstrap_line_length++] = character;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
m_bootstrap_trace_length = length;
|
||||||
|
}
|
||||||
} // namespace IOS::LLE
|
} // namespace IOS::LLE
|
||||||
@@ -3,11 +3,13 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "Common/CommonTypes.h"
|
#include "Common/CommonTypes.h"
|
||||||
|
#include "Core/IOS/Starlet/BootstrapELF.h"
|
||||||
|
|
||||||
class PointerWrap;
|
class PointerWrap;
|
||||||
|
|
||||||
@@ -32,6 +34,8 @@ class StarletMemory;
|
|||||||
class Starlet final
|
class Starlet final
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
// Allocate in Starlet.cpp so callers never depend on this class's size.
|
||||||
|
static std::unique_ptr<Starlet> Create(Core::System& system);
|
||||||
explicit Starlet(Core::System& system);
|
explicit Starlet(Core::System& system);
|
||||||
~Starlet();
|
~Starlet();
|
||||||
|
|
||||||
@@ -77,10 +81,18 @@ private:
|
|||||||
|
|
||||||
static void RunCallback(Core::System& system, u64 userdata, s64 cycles_late);
|
static void RunCallback(Core::System& system, u64 userdata, s64 cycles_late);
|
||||||
void RunSlice(s64 cycles_late);
|
void RunSlice(s64 cycles_late);
|
||||||
|
void TraceBootstrap();
|
||||||
|
|
||||||
Core::System& m_system;
|
Core::System& m_system;
|
||||||
std::unique_ptr<StarletMemory> m_memory;
|
std::unique_ptr<StarletMemory> m_memory;
|
||||||
std::unique_ptr<ARMCore> m_core;
|
std::unique_ptr<ARMCore> m_core;
|
||||||
|
std::optional<BootstrapELFInfo> m_bootstrap;
|
||||||
|
std::array<char, 256> m_bootstrap_line{};
|
||||||
|
u32 m_bootstrap_line_length = 0;
|
||||||
|
u32 m_bootstrap_trace_length = 0;
|
||||||
|
u32 m_last_probe_status = 0;
|
||||||
|
bool m_bootstrap_outcome_logged = false;
|
||||||
|
bool m_bootstrap_fault_logged = false;
|
||||||
CoreTiming::EventType* m_run_event = nullptr;
|
CoreTiming::EventType* m_run_event = nullptr;
|
||||||
u64 m_next_jit_diagnostic_instruction = 25'000'000;
|
u64 m_next_jit_diagnostic_instruction = 25'000'000;
|
||||||
u64 m_next_realtime_diagnostic_ms = 0;
|
u64 m_next_realtime_diagnostic_ms = 0;
|
||||||
|
|||||||
@@ -726,6 +726,85 @@ void StarletMemory::Reset()
|
|||||||
InitializeWiimotes();
|
InitializeWiimotes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StarletMemory::PrepareBootstrap()
|
||||||
|
{
|
||||||
|
m_initialized = false;
|
||||||
|
Reset();
|
||||||
|
m_boot0_mapped = false;
|
||||||
|
m_sram_split_mode = true;
|
||||||
|
WriteRegister(HW_BOOT0, BOOT0_DISABLE);
|
||||||
|
WriteRegister(HW_SRNPROT, SRNPROT_SRAM_SPLIT_MODE);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StarletMemory::AttachReadOnlyNAND(const std::string& path, std::string* error)
|
||||||
|
{
|
||||||
|
m_nand_journal.reset();
|
||||||
|
m_nand_overlay.clear();
|
||||||
|
m_nand = File::IOFile(path, "rb", File::SharedAccess::Read);
|
||||||
|
const u64 nand_size = m_nand ? m_nand.GetSize() : 0;
|
||||||
|
if (nand_size != NAND_IMAGE_SIZE && nand_size != BOOTMII_NAND_IMAGE_SIZE)
|
||||||
|
{
|
||||||
|
m_nand.Close();
|
||||||
|
if (error)
|
||||||
|
*error = "the bootstrap NAND must be a 0x21000000-byte raw NAND image or a "
|
||||||
|
"0x21000400-byte BootMii dump";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_nand_read_only = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StarletMemory::AttachDisposableNAND(const std::string& source, const std::string& copy,
|
||||||
|
std::string* error)
|
||||||
|
{
|
||||||
|
m_nand_disposable_copy.Close();
|
||||||
|
if (copy.empty() || File::Exists(copy))
|
||||||
|
{
|
||||||
|
if (error)
|
||||||
|
*error = "the disposable NAND copy must name a file that does not exist yet";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!AttachReadOnlyNAND(source, error))
|
||||||
|
return false;
|
||||||
|
// "x": creation fails if the file appeared since the check above.
|
||||||
|
File::IOFile output(copy, "wbx");
|
||||||
|
std::array<u8, 0x10000> buffer{};
|
||||||
|
const u64 size = m_nand.GetSize();
|
||||||
|
bool copied = output.IsOpen() && m_nand.Seek(0, File::SeekOrigin::Begin);
|
||||||
|
for (u64 offset = 0; copied && offset < size; offset += buffer.size())
|
||||||
|
{
|
||||||
|
const size_t count = static_cast<size_t>(std::min<u64>(buffer.size(), size - offset));
|
||||||
|
copied = m_nand.ReadArray(buffer.data(), count) && output.WriteArray(buffer.data(), count);
|
||||||
|
}
|
||||||
|
copied = copied && output.Flush();
|
||||||
|
output.Close();
|
||||||
|
if (copied)
|
||||||
|
m_nand_disposable_copy = File::IOFile(copy, "r+b");
|
||||||
|
if (!copied || !m_nand_disposable_copy || m_nand_disposable_copy.GetSize() != size)
|
||||||
|
{
|
||||||
|
m_nand_disposable_copy.Close();
|
||||||
|
m_nand.Close();
|
||||||
|
if (error)
|
||||||
|
*error = "could not create the disposable NAND copy";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_nand_read_only = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool StarletMemory::AttachKeys(const std::string& path, std::string* error)
|
||||||
|
{
|
||||||
|
File::IOFile keys_file(path, "rb");
|
||||||
|
if (!keys_file || keys_file.GetSize() != m_keys.size() || !keys_file.ReadArray(&m_keys))
|
||||||
|
{
|
||||||
|
m_keys.fill(0);
|
||||||
|
if (error)
|
||||||
|
*error = "the bootstrap keys must be a 1024-byte BootMii keys.bin";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void StarletMemory::DoState(PointerWrap& p)
|
void StarletMemory::DoState(PointerWrap& p)
|
||||||
{
|
{
|
||||||
const std::lock_guard wifi_sdio_lock(m_wifi_sdio_register_mutex);
|
const std::lock_guard wifi_sdio_lock(m_wifi_sdio_register_mutex);
|
||||||
@@ -2020,7 +2099,11 @@ void StarletMemory::ExecuteOHCI1HCICommand(const std::vector<u8>& command)
|
|||||||
case HCI_CMD_READ_STORED_LINK_KEY:
|
case HCI_CMD_READ_STORED_LINK_KEY:
|
||||||
{
|
{
|
||||||
const bool read_all = parameter_size >= sizeof(hci_read_stored_link_key_cp) && command[9] != 0;
|
const bool read_all = parameter_size >= sizeof(hci_read_stored_link_key_cp) && command[9] != 0;
|
||||||
const u16 key_count = read_all ? static_cast<u16>(m_wiimotes.size()) : 0;
|
// Bootstrap mode resets these slots without constructing Wiimote devices.
|
||||||
|
// Only initialized devices have identities and keys to return.
|
||||||
|
const u16 key_count = read_all ? static_cast<u16>(std::ranges::count_if(
|
||||||
|
m_wiimotes, [](const auto& wiimote) { return !!wiimote; })) :
|
||||||
|
0;
|
||||||
append_le16(&reply, 255);
|
append_le16(&reply, 255);
|
||||||
append_le16(&reply, key_count);
|
append_le16(&reply, key_count);
|
||||||
if (key_count != 0)
|
if (key_count != 0)
|
||||||
@@ -2028,6 +2111,8 @@ void StarletMemory::ExecuteOHCI1HCICommand(const std::vector<u8>& command)
|
|||||||
std::vector<u8> parameters = {static_cast<u8>(key_count)};
|
std::vector<u8> parameters = {static_cast<u8>(key_count)};
|
||||||
for (const auto& wiimote : m_wiimotes)
|
for (const auto& wiimote : m_wiimotes)
|
||||||
{
|
{
|
||||||
|
if (!wiimote)
|
||||||
|
continue;
|
||||||
parameters.insert(parameters.end(), wiimote->GetBD().begin(), wiimote->GetBD().end());
|
parameters.insert(parameters.end(), wiimote->GetBD().begin(), wiimote->GetBD().end());
|
||||||
parameters.insert(parameters.end(), wiimote->GetLinkKey().begin(),
|
parameters.insert(parameters.end(), wiimote->GetLinkKey().begin(),
|
||||||
wiimote->GetLinkKey().end());
|
wiimote->GetLinkKey().end());
|
||||||
@@ -4154,6 +4239,21 @@ bool StarletMemory::CommitNANDPages(const NANDJournal::Pages& pages)
|
|||||||
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::NAND);
|
const Common::PerformanceTrace::Scope trace(Common::PerformanceTrace::Region::NAND);
|
||||||
if (m_nand_journal && !m_nand_journal->Append(pages))
|
if (m_nand_journal && !m_nand_journal->Append(pages))
|
||||||
return false;
|
return false;
|
||||||
|
// Phase 06H: mirror into the disposable copy first; a failure leaves the array unchanged.
|
||||||
|
if (m_nand_disposable_copy)
|
||||||
|
{
|
||||||
|
for (const auto& [page, data] : pages)
|
||||||
|
{
|
||||||
|
if (!m_nand_disposable_copy.Seek(static_cast<s64>(page) * NAND_RAW_PAGE_SIZE,
|
||||||
|
File::SeekOrigin::Begin) ||
|
||||||
|
!m_nand_disposable_copy.WriteArray(data))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!m_nand_disposable_copy.Flush())
|
||||||
|
return false;
|
||||||
|
}
|
||||||
for (const auto& [page, data] : pages)
|
for (const auto& [page, data] : pages)
|
||||||
m_nand_overlay.insert_or_assign(page, data);
|
m_nand_overlay.insert_or_assign(page, data);
|
||||||
return true;
|
return true;
|
||||||
@@ -4545,6 +4645,11 @@ bool StarletMemory::ReadNANDStatus(u32 command)
|
|||||||
|
|
||||||
bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input)
|
bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input)
|
||||||
{
|
{
|
||||||
|
if (m_nand_read_only)
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(IOS, "Read-only bootstrap NAND refused program command {:#010x}", command);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!(command & NAND_CTRL_WRITE))
|
if (!(command & NAND_CTRL_WRITE))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
@@ -4590,7 +4695,7 @@ bool StarletMemory::StageNANDProgram(u32 command, bool random_data_input)
|
|||||||
bool StarletMemory::CommitNANDProgram()
|
bool StarletMemory::CommitNANDProgram()
|
||||||
{
|
{
|
||||||
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NANDPrograms);
|
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NANDPrograms);
|
||||||
if (!m_nand_program_pending)
|
if (m_nand_read_only || !m_nand_program_pending)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
NANDPage raw{};
|
NANDPage raw{};
|
||||||
@@ -4628,6 +4733,11 @@ bool StarletMemory::CommitNANDProgram()
|
|||||||
|
|
||||||
bool StarletMemory::StageNANDErase()
|
bool StarletMemory::StageNANDErase()
|
||||||
{
|
{
|
||||||
|
if (m_nand_read_only)
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(IOS, "Read-only bootstrap NAND refused erase setup");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
m_nand_erase_page = ReadRegister(NAND_ADDR2) & 0xffffff;
|
m_nand_erase_page = ReadRegister(NAND_ADDR2) & 0xffffff;
|
||||||
m_nand_erase_pending = m_nand_erase_page < NAND_PAGE_COUNT;
|
m_nand_erase_pending = m_nand_erase_page < NAND_PAGE_COUNT;
|
||||||
if (m_nand_erase_pending)
|
if (m_nand_erase_pending)
|
||||||
@@ -4638,7 +4748,7 @@ bool StarletMemory::StageNANDErase()
|
|||||||
bool StarletMemory::CommitNANDErase()
|
bool StarletMemory::CommitNANDErase()
|
||||||
{
|
{
|
||||||
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NANDErases);
|
Common::PerformanceTrace::Add(Common::PerformanceTrace::Counter::NANDErases);
|
||||||
if (!m_nand_erase_pending)
|
if (m_nand_read_only || !m_nand_erase_pending)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const u32 block_start = m_nand_erase_page & ~(NAND_PAGES_PER_BLOCK - 1);
|
const u32 block_start = m_nand_erase_page & ~(NAND_PAGES_PER_BLOCK - 1);
|
||||||
|
|||||||
@@ -56,6 +56,22 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Init(const std::string& dump_directory, std::string* error);
|
bool Init(const std::string& dump_directory, std::string* error);
|
||||||
|
// Set up empty SRAM with BOOT0 hidden and the IOS-style high SRAM split for an external ELF.
|
||||||
|
void PrepareBootstrap();
|
||||||
|
// Bootstrap-ELF mode only: expose a raw NAND image to the NAND controller strictly read-only.
|
||||||
|
// No journal is opened; every program or erase command then fails with the controller error
|
||||||
|
// bit, so a Revolution IOS read-only driver can be validated without any write path.
|
||||||
|
bool AttachReadOnlyNAND(const std::string& path, std::string* error);
|
||||||
|
// Revolution IOS Phase 06H, emulator only: a disposable writable NAND. `source` is opened
|
||||||
|
// read-only exactly as above and is never written. `copy` must not exist; it is created as a
|
||||||
|
// byte-for-byte copy of the source, and every committed program or erase is mirrored into it
|
||||||
|
// before the in-memory overlay changes, so it holds the mutated image for independent
|
||||||
|
// checking. No journal is opened.
|
||||||
|
bool AttachDisposableNAND(const std::string& source, const std::string& copy,
|
||||||
|
std::string* error);
|
||||||
|
// Revolution IOS Phase 04C: bootstrap mode loads no keys, so the OTP reads zero. This
|
||||||
|
// loads a 1024-byte BootMii keys.bin so OTP reads return the console's own words.
|
||||||
|
bool AttachKeys(const std::string& path, std::string* error);
|
||||||
void Reset();
|
void Reset();
|
||||||
void DoState(PointerWrap& p);
|
void DoState(PointerWrap& p);
|
||||||
|
|
||||||
@@ -321,10 +337,13 @@ private:
|
|||||||
u64 m_arm_cycles = 0;
|
u64 m_arm_cycles = 0;
|
||||||
u32 m_timer_offset = 0;
|
u32 m_timer_offset = 0;
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
|
bool m_nand_read_only = false;
|
||||||
bool m_boot0_mapped = true;
|
bool m_boot0_mapped = true;
|
||||||
bool m_sram_split_mode = false;
|
bool m_sram_split_mode = false;
|
||||||
// Host persistence is not serialized; savestates contain the complete overlay above.
|
// Host persistence is not serialized; savestates contain the complete overlay above.
|
||||||
std::unique_ptr<NANDJournal> m_nand_journal;
|
std::unique_ptr<NANDJournal> m_nand_journal;
|
||||||
|
// Phase 06H: the disposable copy mirroring committed NAND pages (bootstrap mode only).
|
||||||
|
File::IOFile m_nand_disposable_copy;
|
||||||
std::unique_ptr<AX88772> m_ethernet;
|
std::unique_ptr<AX88772> m_ethernet;
|
||||||
std::unique_ptr<SlirpNetwork> m_ethernet_network;
|
std::unique_ptr<SlirpNetwork> m_ethernet_network;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
|
#include "Common/Logging/Log.h"
|
||||||
#include "Core/Core.h"
|
#include "Core/Core.h"
|
||||||
|
#include "Core/HW/Memmap.h"
|
||||||
#include "Core/System.h"
|
#include "Core/System.h"
|
||||||
#include "DolphinNoGUI/Platform.h"
|
#include "DolphinNoGUI/Platform.h"
|
||||||
|
|
||||||
@@ -26,10 +28,43 @@ void PlatformHeadless::SetTitle(const std::string& title)
|
|||||||
|
|
||||||
void PlatformHeadless::MainLoop()
|
void PlatformHeadless::MainLoop()
|
||||||
{
|
{
|
||||||
|
bool diff_observed = false;
|
||||||
while (m_running.IsSet())
|
while (m_running.IsSet())
|
||||||
{
|
{
|
||||||
UpdateRunningFlag();
|
UpdateRunningFlag();
|
||||||
Core::HostDispatchJobs(Core::System::GetInstance());
|
auto& system = Core::System::GetInstance();
|
||||||
|
Core::HostDispatchJobs(system);
|
||||||
|
if (!diff_observed && Core::IsRunning(system))
|
||||||
|
{
|
||||||
|
auto& memory = system.GetMemory();
|
||||||
|
const u32 status = memory.Read_U32(0x00003c00);
|
||||||
|
if (status == 0x52500004 || (status & 0xffffff00) == 0x5250e100)
|
||||||
|
{
|
||||||
|
diff_observed = true;
|
||||||
|
INFO_LOG_FMT(IOS, "IOS_PROBE_DIFF_DONE status={:#010x}", status);
|
||||||
|
constexpr u32 trace_base = 0x00003d00;
|
||||||
|
const u32 count = memory.Read_U32(trace_base);
|
||||||
|
if (count > 8)
|
||||||
|
{
|
||||||
|
ERROR_LOG_FMT(IOS, "IOS_PROBE_DIFF_BAD_COUNT count={}", count);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (u32 index = 0; index < count; ++index)
|
||||||
|
{
|
||||||
|
const u32 record = trace_base + 0x20 + index * 24;
|
||||||
|
INFO_LOG_FMT(IOS,
|
||||||
|
"IOS_PROBE_JSON {{\"schema\":1,\"index\":{},\"operation\":{},"
|
||||||
|
"\"argument\":{},\"return\":{},\"output\":{},\"ticks\":{},"
|
||||||
|
"\"status\":{}}}",
|
||||||
|
index, memory.Read_U32(record), memory.Read_U32(record + 4),
|
||||||
|
static_cast<s32>(memory.Read_U32(record + 8)),
|
||||||
|
memory.Read_U32(record + 12), memory.Read_U32(record + 16),
|
||||||
|
memory.Read_U32(record + 20));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ add_custom_command(TARGET tests POST_BUILD
|
|||||||
)
|
)
|
||||||
add_dependencies(unittests tests)
|
add_dependencies(unittests tests)
|
||||||
|
|
||||||
|
# Runs a real Starlet ELF through the fork's CPU and SRAM bus without NAND or a frontend.
|
||||||
|
add_executable(StarletBootstrapProbe EXCLUDE_FROM_ALL
|
||||||
|
${CMAKE_SOURCE_DIR}/Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp StubHost.cpp)
|
||||||
|
target_link_libraries(StarletBootstrapProbe PRIVATE fmt::fmt core uicommon)
|
||||||
|
|
||||||
macro(add_dolphin_test target)
|
macro(add_dolphin_test target)
|
||||||
add_library(${target} OBJECT ${ARGN})
|
add_library(${target} OBJECT ${ARGN})
|
||||||
target_link_libraries(${target} PUBLIC fmt::fmt gtest::gtest PRIVATE core uicommon)
|
target_link_libraries(${target} PUBLIC fmt::fmt gtest::gtest PRIVATE core uicommon)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ add_dolphin_test(DSPAssemblyTest
|
|||||||
add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
|
add_dolphin_test(ESFormatsTest IOS/ES/FormatsTest.cpp)
|
||||||
|
|
||||||
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
|
add_dolphin_test(StarletARMCoreTest IOS/Starlet/ARMCoreTest.cpp)
|
||||||
|
add_dolphin_test(StarletBootstrapELFTest IOS/Starlet/BootstrapELFTest.cpp)
|
||||||
add_dolphin_test(StarletDMATest IOS/Starlet/DMATest.cpp)
|
add_dolphin_test(StarletDMATest IOS/Starlet/DMATest.cpp)
|
||||||
add_dolphin_test(StarletSRAMReadTest IOS/Starlet/SRAMReadTest.cpp)
|
add_dolphin_test(StarletSRAMReadTest IOS/Starlet/SRAMReadTest.cpp)
|
||||||
add_dolphin_test(StarletSRAMWriteTest IOS/Starlet/SRAMWriteTest.cpp)
|
add_dolphin_test(StarletSRAMWriteTest IOS/Starlet/SRAMWriteTest.cpp)
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
// Copyright 2026 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include "Core/IOS/Starlet/ARMCore.h"
|
||||||
|
#include "Core/IOS/Starlet/BootstrapELF.h"
|
||||||
|
|
||||||
|
namespace IOS::LLE
|
||||||
|
{
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
constexpr u32 TEXT_BASE = 0xffff0000;
|
||||||
|
constexpr u32 DATA_BASE = 0xfffe0000;
|
||||||
|
|
||||||
|
class RecordingBus final : public ARMBus
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
u8 Read8(u32 address) override
|
||||||
|
{
|
||||||
|
const auto it = bytes.find(address);
|
||||||
|
return it == bytes.end() ? 0 : it->second;
|
||||||
|
}
|
||||||
|
void Write8(u32 address, u8 value) override { bytes[address] = value; }
|
||||||
|
|
||||||
|
std::unordered_map<u32, u8> bytes;
|
||||||
|
};
|
||||||
|
|
||||||
|
void Put16(std::array<u8, 0x300>& image, size_t offset, u16 value)
|
||||||
|
{
|
||||||
|
image[offset] = static_cast<u8>(value >> 8);
|
||||||
|
image[offset + 1] = static_cast<u8>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Put32(std::array<u8, 0x300>& image, size_t offset, u32 value)
|
||||||
|
{
|
||||||
|
image[offset] = static_cast<u8>(value >> 24);
|
||||||
|
image[offset + 1] = static_cast<u8>(value >> 16);
|
||||||
|
image[offset + 2] = static_cast<u8>(value >> 8);
|
||||||
|
image[offset + 3] = static_cast<u8>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::array<u8, 0x300> MakeImage()
|
||||||
|
{
|
||||||
|
std::array<u8, 0x300> image{};
|
||||||
|
image[0] = 0x7f;
|
||||||
|
image[1] = 'E';
|
||||||
|
image[2] = 'L';
|
||||||
|
image[3] = 'F';
|
||||||
|
image[4] = 1; // ELF32
|
||||||
|
image[5] = 2; // big endian
|
||||||
|
image[6] = 1;
|
||||||
|
Put16(image, 16, 2); // ET_EXEC
|
||||||
|
Put16(image, 18, 40); // ARM
|
||||||
|
Put32(image, 20, 1); // ELF version
|
||||||
|
Put32(image, 24, TEXT_BASE);
|
||||||
|
Put32(image, 28, 52); // program headers
|
||||||
|
Put32(image, 32, 0x200); // section headers
|
||||||
|
Put16(image, 40, 52);
|
||||||
|
Put16(image, 42, 32);
|
||||||
|
Put16(image, 44, 2);
|
||||||
|
Put16(image, 46, 40);
|
||||||
|
Put16(image, 48, 3);
|
||||||
|
|
||||||
|
Put32(image, 52, 1);
|
||||||
|
Put32(image, 56, 0x100);
|
||||||
|
Put32(image, 60, TEXT_BASE);
|
||||||
|
Put32(image, 64, TEXT_BASE);
|
||||||
|
Put32(image, 68, 4);
|
||||||
|
Put32(image, 72, 8);
|
||||||
|
Put32(image, 76, 5);
|
||||||
|
image[0x100] = 0xea; // ARM branch instruction
|
||||||
|
|
||||||
|
Put32(image, 84, 1);
|
||||||
|
Put32(image, 88, 0x110);
|
||||||
|
Put32(image, 92, DATA_BASE);
|
||||||
|
Put32(image, 96, DATA_BASE);
|
||||||
|
Put32(image, 100, 4);
|
||||||
|
Put32(image, 104, 0x420);
|
||||||
|
Put32(image, 108, 6);
|
||||||
|
image[0x110] = 0x12;
|
||||||
|
image[0x111] = 0x34;
|
||||||
|
image[0x112] = 0x56;
|
||||||
|
image[0x113] = 0x78;
|
||||||
|
|
||||||
|
constexpr char names[] = "\0REV_TRACE\0__fault_code\0__fault_lr\0__fault_spsr";
|
||||||
|
for (size_t index = 0; index < sizeof(names); ++index)
|
||||||
|
image[0x180 + index] = static_cast<u8>(names[index]);
|
||||||
|
Put32(image, 0x228 + 4, 3); // SHT_STRTAB
|
||||||
|
Put32(image, 0x228 + 16, 0x180);
|
||||||
|
Put32(image, 0x228 + 20, sizeof(names));
|
||||||
|
Put32(image, 0x250 + 4, 2); // SHT_SYMTAB
|
||||||
|
Put32(image, 0x250 + 16, 0x120);
|
||||||
|
Put32(image, 0x250 + 20, 5 * 16);
|
||||||
|
Put32(image, 0x250 + 24, 1);
|
||||||
|
Put32(image, 0x250 + 36, 16);
|
||||||
|
|
||||||
|
const auto symbol = [&image](size_t index, u32 name, u32 address, u32 size) {
|
||||||
|
const size_t offset = 0x120 + index * 16;
|
||||||
|
Put32(image, offset, name);
|
||||||
|
Put32(image, offset + 4, address);
|
||||||
|
Put32(image, offset + 8, size);
|
||||||
|
};
|
||||||
|
symbol(1, 1, DATA_BASE + 8, 1032);
|
||||||
|
symbol(2, 11, DATA_BASE + 0x410, 4);
|
||||||
|
symbol(3, 24, DATA_BASE + 0x414, 4);
|
||||||
|
symbol(4, 35, DATA_BASE + 0x418, 4);
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BootstrapELFTest, LoadsSegmentsAndZerosUninitializedData)
|
||||||
|
{
|
||||||
|
const auto image = MakeImage();
|
||||||
|
RecordingBus bus;
|
||||||
|
BootstrapELFInfo info;
|
||||||
|
std::string error;
|
||||||
|
ASSERT_TRUE(LoadBootstrapELF(image, bus, &info, &error)) << error;
|
||||||
|
EXPECT_EQ(info.entry, TEXT_BASE);
|
||||||
|
EXPECT_EQ(info.trace, DATA_BASE + 8);
|
||||||
|
EXPECT_EQ(info.fault_spsr, DATA_BASE + 0x418);
|
||||||
|
EXPECT_EQ(bus.Read8(TEXT_BASE), 0xea);
|
||||||
|
EXPECT_EQ(bus.Read8(TEXT_BASE + 4), 0);
|
||||||
|
EXPECT_EQ(bus.Read32(DATA_BASE), 0x12345678u);
|
||||||
|
EXPECT_EQ(bus.Read32(DATA_BASE + 0x41c), 0u);
|
||||||
|
EXPECT_EQ(bus.bytes.size(), size_t{8 + 0x420});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BootstrapELFTest, RejectsMalformedImageBeforeWritingMemory)
|
||||||
|
{
|
||||||
|
auto image = MakeImage();
|
||||||
|
RecordingBus bus;
|
||||||
|
BootstrapELFInfo info;
|
||||||
|
std::string error;
|
||||||
|
image[5] = 1;
|
||||||
|
EXPECT_FALSE(LoadBootstrapELF(image, bus, &info, &error));
|
||||||
|
EXPECT_TRUE(bus.bytes.empty());
|
||||||
|
|
||||||
|
image = MakeImage();
|
||||||
|
Put32(image, 0x120 + 2 * 16 + 4, DATA_BASE + 0x420);
|
||||||
|
EXPECT_FALSE(LoadBootstrapELF(image, bus, &info, &error));
|
||||||
|
EXPECT_TRUE(bus.bytes.empty());
|
||||||
|
|
||||||
|
image = MakeImage();
|
||||||
|
Put32(image, 88, 0x300);
|
||||||
|
EXPECT_FALSE(LoadBootstrapELF(image, bus, &info, &error));
|
||||||
|
EXPECT_TRUE(bus.bytes.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revolution IOS Phase 06H: the optional MEM2 text segment of the emulator-only
|
||||||
|
// disposable-image kernel, accepted only when the caller allows it.
|
||||||
|
constexpr u32 MEM2_TEXT = 0x12600000;
|
||||||
|
|
||||||
|
std::array<u8, 0x300> MakeMem2Image(u32 address, u32 flags)
|
||||||
|
{
|
||||||
|
auto image = MakeImage();
|
||||||
|
Put16(image, 44, 3);
|
||||||
|
Put32(image, 116, 1);
|
||||||
|
Put32(image, 120, 0x114);
|
||||||
|
Put32(image, 124, address);
|
||||||
|
Put32(image, 128, address);
|
||||||
|
Put32(image, 132, 4);
|
||||||
|
Put32(image, 136, 0x10);
|
||||||
|
Put32(image, 140, flags);
|
||||||
|
Put32(image, 0x114, 0xe12fff1e); // BX LR
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BootstrapELFTest, Mem2TextSegmentOnlyWhenAllowed)
|
||||||
|
{
|
||||||
|
RecordingBus bus;
|
||||||
|
BootstrapELFInfo info;
|
||||||
|
std::string error;
|
||||||
|
auto image = MakeMem2Image(MEM2_TEXT, 5);
|
||||||
|
EXPECT_FALSE(LoadBootstrapELF(image, bus, &info, &error));
|
||||||
|
EXPECT_TRUE(bus.bytes.empty());
|
||||||
|
|
||||||
|
ASSERT_TRUE(LoadBootstrapELF(image, bus, &info, &error, true)) << error;
|
||||||
|
EXPECT_EQ(bus.Read32(MEM2_TEXT), 0xe12fff1eu);
|
||||||
|
EXPECT_EQ(bus.Read32(MEM2_TEXT + 0xc), 0u);
|
||||||
|
EXPECT_EQ(bus.Read8(TEXT_BASE), 0xea);
|
||||||
|
EXPECT_EQ(bus.Read32(DATA_BASE), 0x12345678u);
|
||||||
|
EXPECT_EQ(bus.bytes.size(), size_t{8 + 0x420 + 0x10});
|
||||||
|
|
||||||
|
RecordingBus plain;
|
||||||
|
const auto two_segments = MakeImage();
|
||||||
|
EXPECT_TRUE(LoadBootstrapELF(two_segments, plain, &info, &error, true)) << error;
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(BootstrapELFTest, RejectsInvalidMem2TextSegmentBeforeWritingMemory)
|
||||||
|
{
|
||||||
|
RecordingBus bus;
|
||||||
|
BootstrapELFInfo info;
|
||||||
|
std::string error;
|
||||||
|
// Writable, outside MEM2, crossing its end, or a second SRAM text segment.
|
||||||
|
for (const auto& image : {MakeMem2Image(MEM2_TEXT, 6), MakeMem2Image(0x0fff0000, 5),
|
||||||
|
MakeMem2Image(0x13fffff8, 5), MakeMem2Image(TEXT_BASE, 5)})
|
||||||
|
{
|
||||||
|
EXPECT_FALSE(LoadBootstrapELF(image, bus, &info, &error, true));
|
||||||
|
EXPECT_TRUE(bus.bytes.empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ARMCoreTest, Armv5DoublewordTransfersAndAlignmentAbort)
|
||||||
|
{
|
||||||
|
RecordingBus bus;
|
||||||
|
bus.Write32(TEXT_BASE, 0xe1c020d0); // LDRD r2, r3, [r0]
|
||||||
|
bus.Write32(TEXT_BASE + 4, 0xe1c120f0); // STRD r2, r3, [r1]
|
||||||
|
bus.Write32(DATA_BASE, 0x11223344);
|
||||||
|
bus.Write32(DATA_BASE + 4, 0x55667788);
|
||||||
|
|
||||||
|
ARMCore core(bus);
|
||||||
|
core.Reset(TEXT_BASE);
|
||||||
|
core.SetBigEndian(true);
|
||||||
|
core.SetRegister(0, DATA_BASE);
|
||||||
|
core.SetRegister(1, DATA_BASE + 8);
|
||||||
|
core.Step();
|
||||||
|
EXPECT_EQ(core.GetRegister(2), 0x11223344u);
|
||||||
|
EXPECT_EQ(core.GetRegister(3), 0x55667788u);
|
||||||
|
core.Step();
|
||||||
|
EXPECT_EQ(bus.Read32(DATA_BASE + 8), 0x11223344u);
|
||||||
|
EXPECT_EQ(bus.Read32(DATA_BASE + 12), 0x55667788u);
|
||||||
|
|
||||||
|
core.Reset(TEXT_BASE);
|
||||||
|
core.SetBigEndian(true);
|
||||||
|
core.SetRegister(0, DATA_BASE + 4);
|
||||||
|
core.Step();
|
||||||
|
EXPECT_EQ(core.GetRegister(15), TEXT_BASE + 0x10);
|
||||||
|
EXPECT_EQ(core.GetRegister(14), TEXT_BASE + 8);
|
||||||
|
EXPECT_EQ(core.GetCP15State().fault_address, DATA_BASE + 4);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
} // namespace IOS::LLE
|
||||||
@@ -135,6 +135,21 @@ protected:
|
|||||||
EXPECT_EQ(m_memory->GetDMAMemoryPointer(address, 4), nullptr);
|
EXPECT_EQ(m_memory->GetDMAMemoryPointer(address, 4), nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CheckBootstrapStoredKeys()
|
||||||
|
{
|
||||||
|
m_memory->PrepareBootstrap();
|
||||||
|
for (const u8 read_all : {0, 1})
|
||||||
|
{
|
||||||
|
SCOPED_TRACE(read_all);
|
||||||
|
m_memory->m_ohci1_hci_events.clear();
|
||||||
|
m_memory->ExecuteOHCI1HCICommand({0x0d, 0x0c, 7, 0, 0, 0, 0, 0, 0, read_all});
|
||||||
|
ASSERT_EQ(m_memory->m_ohci1_hci_events.size(), 1u);
|
||||||
|
// Command Complete: capacity 255, zero stored keys, no Return Link Keys event.
|
||||||
|
EXPECT_EQ(m_memory->m_ohci1_hci_events.front(),
|
||||||
|
(std::vector<u8>{0x0e, 8, 1, 0x0d, 0x0c, 0, 0xff, 0, 0, 0}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void CheckOHCI()
|
void CheckOHCI()
|
||||||
{
|
{
|
||||||
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
|
for (const u32 base : {0x10000U, 0x10010000U, 0x0d404000U})
|
||||||
@@ -381,6 +396,11 @@ TEST_F(StarletDMATest, OHCIUsesTwoPhysicalPagesAndLittleEndianDescriptors)
|
|||||||
CheckOHCI();
|
CheckOHCI();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_F(StarletDMATest, BootstrapReadStoredKeysHasNoWiimoteDevices)
|
||||||
|
{
|
||||||
|
CheckBootstrapStoredKeys();
|
||||||
|
}
|
||||||
|
|
||||||
TEST_F(StarletDMATest, NANDSeparatesDataSpareAndECCIncludingPartialTransfers)
|
TEST_F(StarletDMATest, NANDSeparatesDataSpareAndECCIncludingPartialTransfers)
|
||||||
{
|
{
|
||||||
CheckNAND();
|
CheckNAND();
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
// Copyright 2026 Dolphin Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Core/Core.h"
|
||||||
|
#include "Core/HW/WII_IPC.h"
|
||||||
|
#include "Core/IOS/Starlet/ARMCore.h"
|
||||||
|
#include "Core/IOS/Starlet/BootstrapELF.h"
|
||||||
|
#include "Core/IOS/Starlet/StarletMemory.h"
|
||||||
|
#include "Core/System.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
enum class ExpectedOutcome
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Panic,
|
||||||
|
FatalData,
|
||||||
|
UndefinedFault,
|
||||||
|
Phase2
|
||||||
|
};
|
||||||
|
|
||||||
|
bool Run(const std::string& path, bool jit, ExpectedOutcome expected)
|
||||||
|
{
|
||||||
|
std::ifstream file(path, std::ios::binary | std::ios::ate);
|
||||||
|
if (!file || file.tellg() <= 0 || file.tellg() > 16 * 1024 * 1024)
|
||||||
|
{
|
||||||
|
std::cerr << "Expected a readable Starlet ELF of at most 16 MiB: " << path << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const size_t size = static_cast<size_t>(file.tellg());
|
||||||
|
std::vector<u8> image(size);
|
||||||
|
file.seekg(0);
|
||||||
|
if (!file.read(reinterpret_cast<char*>(image.data()), size))
|
||||||
|
{
|
||||||
|
std::cerr << "Could not read Starlet ELF: " << path << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Core::DeclareAsCPUThread();
|
||||||
|
auto& system = Core::System::GetInstance();
|
||||||
|
system.GetWiiIPC().Reset();
|
||||||
|
IOS::LLE::StarletMemory memory(system);
|
||||||
|
memory.PrepareBootstrap();
|
||||||
|
IOS::LLE::BootstrapELFInfo info;
|
||||||
|
std::string error;
|
||||||
|
if (!IOS::LLE::LoadBootstrapELF(image, memory, &info, &error))
|
||||||
|
{
|
||||||
|
std::cerr << error << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
IOS::LLE::ARMCore core(memory);
|
||||||
|
core.SetJitEnabled(jit);
|
||||||
|
core.Reset(info.entry);
|
||||||
|
core.SetBigEndian(true);
|
||||||
|
constexpr u32 TRACE_MAGIC = 0x52455631;
|
||||||
|
constexpr u32 TRACE_CAPACITY = 1024;
|
||||||
|
for (int slice = 0; slice < 512; ++slice)
|
||||||
|
{
|
||||||
|
core.SetIRQLine(system.GetWiiIPC().IsStarletIRQAsserted());
|
||||||
|
core.SetFIQLine(system.GetWiiIPC().IsStarletFIQAsserted());
|
||||||
|
core.RunCycles(4096);
|
||||||
|
const u32 fault_code = memory.Read32(info.fault_code);
|
||||||
|
if (fault_code != 0)
|
||||||
|
{
|
||||||
|
std::cerr << "Starlet exception " << fault_code << " LR=" << std::hex
|
||||||
|
<< memory.Read32(info.fault_lr) << " SPSR=" << memory.Read32(info.fault_spsr)
|
||||||
|
<< " PC=" << core.GetRegister(15) << std::dec << '\n';
|
||||||
|
return expected == ExpectedOutcome::UndefinedFault && fault_code == 1 &&
|
||||||
|
memory.Read32(info.fault_lr) != 0 && memory.Read32(info.fault_spsr) != 0;
|
||||||
|
}
|
||||||
|
if (memory.Read32(info.trace) != TRACE_MAGIC)
|
||||||
|
continue;
|
||||||
|
const u32 length = memory.Read32(info.trace + 4);
|
||||||
|
if (length > TRACE_CAPACITY)
|
||||||
|
{
|
||||||
|
std::cerr << "Invalid REV_TRACE length: " << length << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::string trace(length, '\0');
|
||||||
|
for (u32 index = 0; index < length; ++index)
|
||||||
|
trace[index] = static_cast<char>(memory.Read8(info.trace + 8 + index));
|
||||||
|
if (trace.find("REV:BOOT:IDLE\n") == std::string::npos &&
|
||||||
|
trace.find("REV:PANIC") == std::string::npos &&
|
||||||
|
trace.find("REV:FATAL") == std::string::npos)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
std::cout << trace;
|
||||||
|
std::cout << "PC=0x" << std::hex << core.GetRegister(15) << std::dec
|
||||||
|
<< " instructions=" << core.GetExecutedInstructions() << '\n';
|
||||||
|
const bool early_startup_ok = trace.find("REV:BOOT:ENTRY\n") != std::string::npos &&
|
||||||
|
trace.find("REV:BOOT:BSS_OK\n") != std::string::npos;
|
||||||
|
if (expected == ExpectedOutcome::FatalData)
|
||||||
|
{
|
||||||
|
return early_startup_ok && trace.find("REV:FATAL:DATA\n") != std::string::npos &&
|
||||||
|
trace.find("REV:BOOT:DATA_OK\n") == std::string::npos;
|
||||||
|
}
|
||||||
|
const bool startup_ok = early_startup_ok &&
|
||||||
|
trace.find("REV:BOOT:DATA_OK\n") != std::string::npos &&
|
||||||
|
trace.find("REV:BOOT:STACK_OK\n") != std::string::npos &&
|
||||||
|
trace.find("REV:BOOT:RUST\n") != std::string::npos;
|
||||||
|
if (expected == ExpectedOutcome::Phase2)
|
||||||
|
{
|
||||||
|
const std::string_view required[] = {
|
||||||
|
"REV:P2:START\n", "ARCH PASS\n", "CRITICAL PASS\n",
|
||||||
|
"TIMER PASS\n", "MONOTONIC PASS\n", "MEM PASS\n",
|
||||||
|
"IRQ PASS\n", "BARRIER PASS\n", "LOGGING PASS\n",
|
||||||
|
"REV:P2:PASS\n", "REV:P2:KERNEL_READY\n", "REV:BOOT:IDLE\n"};
|
||||||
|
bool phase2_ok = true;
|
||||||
|
for (const std::string_view marker : required)
|
||||||
|
phase2_ok &= trace.find(marker) != std::string::npos;
|
||||||
|
return startup_ok && phase2_ok && trace.find(" FAIL\n") == std::string::npos;
|
||||||
|
}
|
||||||
|
const bool outcome_ok =
|
||||||
|
expected == ExpectedOutcome::Panic ?
|
||||||
|
trace.find("REV:PANIC") != std::string::npos :
|
||||||
|
expected == ExpectedOutcome::Idle && trace.find("REV:BOOT:IDLE\n") != std::string::npos;
|
||||||
|
return startup_ok && outcome_ok;
|
||||||
|
}
|
||||||
|
std::cerr << "No terminal bootstrap marker after 2097152 ARM cycles, PC=0x" << std::hex
|
||||||
|
<< core.GetRegister(15) << std::dec << '\n';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main(int argc, char** argv)
|
||||||
|
{
|
||||||
|
if (argc < 2 || argc > 4)
|
||||||
|
{
|
||||||
|
std::cerr << "Usage: StarletBootstrapProbe <ELF> [--jit] "
|
||||||
|
"[--expect-panic|--expect-fatal-data|--expect-undefined-fault|--expect-phase2]\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
bool jit = false;
|
||||||
|
ExpectedOutcome expected = ExpectedOutcome::Idle;
|
||||||
|
bool outcome_selected = false;
|
||||||
|
for (int index = 2; index < argc; ++index)
|
||||||
|
{
|
||||||
|
const std::string_view option = argv[index];
|
||||||
|
if (option == "--jit")
|
||||||
|
jit = true;
|
||||||
|
else if (option == "--expect-panic" || option == "--expect-fatal-data" ||
|
||||||
|
option == "--expect-undefined-fault" || option == "--expect-phase2")
|
||||||
|
{
|
||||||
|
if (outcome_selected)
|
||||||
|
{
|
||||||
|
std::cerr << "Select only one expected outcome\n";
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
outcome_selected = true;
|
||||||
|
expected = option == "--expect-panic" ? ExpectedOutcome::Panic :
|
||||||
|
option == "--expect-fatal-data" ? ExpectedOutcome::FatalData :
|
||||||
|
option == "--expect-undefined-fault" ? ExpectedOutcome::UndefinedFault :
|
||||||
|
ExpectedOutcome::Phase2;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
std::cerr << "Unknown option: " << option << '\n';
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Run(argv[1], jit, expected) ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
{
|
||||||
|
"recordedAt": "2026-10-03",
|
||||||
|
"purpose": "Publish the exact existing StarletOS integration, without emulator behavior changes",
|
||||||
|
"sourceBaseCommit": "a8dbbab62de0ec429d9d887c39b9669c75f51c21",
|
||||||
|
"previousRemoteMaster": "b931671fde1e0461cbca37eeb8348a25d44ab76e",
|
||||||
|
"companionRepository": "https://git.yaya48.xyz/Yaya48/StarletOS",
|
||||||
|
"companionCheckpoint": "efa8b310da2c3e3bd36d762080d42d410a5413c9",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "CMakeLists.txt",
|
||||||
|
"sha256": "1ed308f4ccc9d6a4405d9ec232a4ac64fb88aa9c69a4773ee93fe7c62b357d4d",
|
||||||
|
"bytes": 34657
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Run-Wii-Starlet-Bootstrap.ps1",
|
||||||
|
"sha256": "486f99f899fde18371b7bfc2b67fcf12a64ceb748285cc00ae4a71716349d07a",
|
||||||
|
"bytes": 18583
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/Boot/Boot.cpp",
|
||||||
|
"sha256": "43a0b1c2e60c9a362fe3637ba0aa7b23e8b75ce5e5517cd3515b50ec596c43a5",
|
||||||
|
"bytes": 27009
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/CMakeLists.txt",
|
||||||
|
"sha256": "32dc85b995c9beb3739cc1ea554c308042e16b6c270071550728d18c85ada79e",
|
||||||
|
"bytes": 24388
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/Config/MainSettings.cpp",
|
||||||
|
"sha256": "e331e8f7145ebfce5cd3d2be1b3dabe1dce597e8040132402f04a0152526ce56",
|
||||||
|
"bytes": 47583
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/Config/MainSettings.h",
|
||||||
|
"sha256": "9052fbc4a1da18ea3e8a11a93d62ef06423da74671b1f9b1091bb62f1fcff62b",
|
||||||
|
"bytes": 18437
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/ConfigManager.cpp",
|
||||||
|
"sha256": "7d369d0264f5a1ad0df5ac44e9b6ea0f514a196ec7f80952218f9439ab17ea10",
|
||||||
|
"bytes": 19807
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/HW/HW.cpp",
|
||||||
|
"sha256": "7e9bcfff87f526510aad149e781ae57140e32f7b32beafec00271fbcdb152848",
|
||||||
|
"bytes": 4526
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/IOS/Starlet/ARMCore.cpp",
|
||||||
|
"sha256": "40d45aed7c5edb4540105dcdee4dd8dcde88c21ba7f3279eff0a9564f2d02a29",
|
||||||
|
"bytes": 75597
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/IOS/Starlet/BootstrapELF.cpp",
|
||||||
|
"sha256": "344e6e38922b64fa58d9755c2df5fd752b00cf3dc73114809f6e9cada724c42a",
|
||||||
|
"bytes": 8554
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/IOS/Starlet/BootstrapELF.h",
|
||||||
|
"sha256": "b25184cb4d3934e02220417b8e4b8005fc57010f4d3ab7af697c98f11afb305f",
|
||||||
|
"bytes": 943
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/IOS/Starlet/Starlet.cpp",
|
||||||
|
"sha256": "78e3b006501af06fbef220d95213690f8a31ed5b3c36699c6e07ba58be3ba852",
|
||||||
|
"bytes": 26039
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/IOS/Starlet/Starlet.h",
|
||||||
|
"sha256": "5dbdd1a854fd17377a339886949d80ebb1969d6453a4bdfd5a6f9a503bbb4f2a",
|
||||||
|
"bytes": 3553
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/IOS/Starlet/StarletMemory.cpp",
|
||||||
|
"sha256": "34900b6c340e412f99ddb3ce6ba17bdb4d85ba7b27b2adc522315b158b675fca",
|
||||||
|
"bytes": 189757
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/Core/IOS/Starlet/StarletMemory.h",
|
||||||
|
"sha256": "cfa3aee13f260590ae9f7cdd48f8f08d1c67e4e6d647ef3eca6e01d472dbdd29",
|
||||||
|
"bytes": 14873
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/Core/DolphinNoGUI/PlatformHeadless.cpp",
|
||||||
|
"sha256": "1390cff0a139f0b0a90559e0fda855446d279190827a6c5079718421924f4041",
|
||||||
|
"bytes": 2533
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/UnitTests/CMakeLists.txt",
|
||||||
|
"sha256": "bb0aeb694c2f328b891e5e961fe0af16c5b30ff1a5f02d4a8d84bdfa5930b581",
|
||||||
|
"bytes": 1659
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/UnitTests/Core/CMakeLists.txt",
|
||||||
|
"sha256": "563899ae142ba4d0e146bb125bd35c8387a77d25db5ac48cf8ba48a32cb30314",
|
||||||
|
"bytes": 2043
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/UnitTests/Core/IOS/Starlet/BootstrapELFTest.cpp",
|
||||||
|
"sha256": "62d483a737b68cca00e51c4a93511b4863022ed2f4b90e721ff36325630cfc3a",
|
||||||
|
"bytes": 7057
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Source/UnitTests/Core/IOS/Starlet/DMATest.cpp",
|
||||||
|
"sha256": "15b0816292fff6dcf48d485466f7bf495f2128ba625f3e7c1f225614be631e84",
|
||||||
|
"bytes": 16450
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "Tools/StarletBootstrapProbe/StarletBootstrapProbe.cpp",
|
||||||
|
"sha256": "83873691164d9b92e3be62f699146feaf8ab713bcb45d25821975e7454f56ede",
|
||||||
|
"bytes": 6179
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "docs/Wii_Starlet_Bootstrap.md",
|
||||||
|
"sha256": "bd7ec585472dc44815da8fcb2be6d726b91f04dd0964d82097842fb4ba764021",
|
||||||
|
"bytes": 4937
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"checks": {
|
||||||
|
"sourceSnapshotMatchesPriorStarletOSArchive": true,
|
||||||
|
"unpublishedHistoricalFileVersionsScanned": 67,
|
||||||
|
"privateFindings": 0,
|
||||||
|
"newFullBuildOrHardwareTest": "NOT RUN: publication of existing qualified bytes only; see companion phase reports"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
# StarletOS integration handoff — 2026-10-03
|
||||||
|
|
||||||
|
This checkpoint publishes the existing emulator changes used by the companion
|
||||||
|
[StarletOS repository](https://git.yaya48.xyz/Yaya48/StarletOS). It is a recovered
|
||||||
|
source checkpoint, not a new claim that stock IOS or physical hardware is fully
|
||||||
|
supported. The Rust project's current phase is 10C, stock NCD probe H02B awaiting
|
||||||
|
SD staging and its physical result; consult its HANDOFF and phase reports.
|
||||||
|
|
||||||
|
## Published work
|
||||||
|
|
||||||
|
- The existing local commits c230971f07 (NAND conformance/diagnostic tools) and
|
||||||
|
a8dbbab62d (SD CMD6/conformance report) precede this integration checkpoint.
|
||||||
|
- 16 modified files and 6 previously untracked files are now tracked here:
|
||||||
|
ARM bootstrap ELF loader and tests, direct PPC/Starlet launch, read-only NAND
|
||||||
|
and explicit disposable-copy support, diagnostics and regression runner,
|
||||||
|
ARMv5TE LDRD/STRD handling, and the existing MSVC dependency-detection fix.
|
||||||
|
- `docs/StarletOS_Checkpoint.json` records SHA-256s of the 22 exact source files.
|
||||||
|
The companion repository also pins the published Dolphin commit.
|
||||||
|
- Every runtime byte matches the source snapshot captured before publication.
|
||||||
|
Only Git attributes, ignore rules and handoff metadata were added this turn.
|
||||||
|
- Inspection of 67 changed file versions in the two pending commits found no
|
||||||
|
available private NAND key/content markers, device identifiers or credentials.
|
||||||
|
The staged integration files were inspected separately before publication.
|
||||||
|
- No full rebuild, new emulator test or Wii test is claimed by this Git import.
|
||||||
|
Historical qualification remains in the companion project's phase reports.
|
||||||
|
|
||||||
|
## Other computers
|
||||||
|
|
||||||
|
Clone this fork beside `starlet-ios-rust`, checkout the commit pinned in
|
||||||
|
`StarletOS/support/dolphin/manifest.json`, then follow the companion
|
||||||
|
`docs/DEVELOPMENT.md`. The previous private base bundle and source overlay are
|
||||||
|
no longer needed to retrieve these sources: their commits are on this server.
|
||||||
|
Use `docs/Wii_Starlet_Bootstrap.md` for the existing build and test runner.
|
||||||
|
|
||||||
|
NAND, keys, user profiles, traces and generated builds remain local/ignored.
|
||||||
|
The `.starlet_user3` profile is needed only for the full Menu scenarios; the
|
||||||
|
standalone Starlet bootstrap probe does not need a NAND or keys.
|
||||||
|
The old `.before-p10a-h07b` script is retained locally as an ignored backup.
|
||||||
|
|
||||||
|
## Future sessions
|
||||||
|
|
||||||
|
Read AGENTS.md. Record the starting commit, changed behavior, actual tests and
|
||||||
|
remaining work. Update this handoff and any changed file hashes, commit to this
|
||||||
|
fork, then update the pinned dependency in StarletOS. Never label a build, a
|
||||||
|
source comparison or a fake emulator device as a physical Wii observation.
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
# Starlet Rust bootstrap in this Dolphin fork
|
||||||
|
|
||||||
|
This fork can load the Phase 01 Revolution IOS ELF directly into Starlet SRAM. The loader uses
|
||||||
|
the ELF's two `PT_LOAD` segments, zeroes their uninitialized tails, starts the ARM core at
|
||||||
|
`0xffff0000` in big-endian supervisor mode, and publishes `REV_TRACE` lines in the IOS log.
|
||||||
|
No boot0, keys, or NAND dump is needed for the standalone probe.
|
||||||
|
|
||||||
|
## Fast development loop
|
||||||
|
|
||||||
|
Build the Rust ELF from `starlet-ios-rust`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cargo build --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure this Dolphin fork with `ENABLE_TESTS=ON`, then build `StarletBootstrapProbe`. Run
|
||||||
|
the build commands from an x64 Native Tools Command Prompt (or after `vcvars64.bat`). For the
|
||||||
|
local MSVC configuration:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake --build .starlet_msvc2 --target StarletBootstrapProbe
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\Run-Wii-Starlet-Bootstrap.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The script defaults to the sibling Rust repository's release ELF. It prints every trace line,
|
||||||
|
the stopping PC, and the ARM instruction count; it exits nonzero for a missing marker, fault,
|
||||||
|
or invalid ELF. `-JIT` exercises the fork's ARM JIT.
|
||||||
|
|
||||||
|
To check the panic path without replacing the ordinary release artifact:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd ..\starlet-ios-rust
|
||||||
|
cargo build --release --features panic-smoke --target-dir target/panic-smoke
|
||||||
|
cd ..\dolphin
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\Run-Wii-Starlet-Bootstrap.ps1 `
|
||||||
|
-ELF '../starlet-ios-rust/target/panic-smoke/armebv5te-none-eabi/release/revolution-ios-bootstrap' `
|
||||||
|
-ExpectedOutcome Panic
|
||||||
|
```
|
||||||
|
|
||||||
|
To exercise a Rust fatal check and a synchronous ARM exception, derive two diagnostic
|
||||||
|
ELF copies from the release artifact:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd ..\starlet-ios-rust
|
||||||
|
py -3 tools/make_smoke_elf.py target/armebv5te-none-eabi/release/revolution-ios-bootstrap target/phase01-smoke
|
||||||
|
cd ..\dolphin
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\Run-Wii-Starlet-Bootstrap.ps1 `
|
||||||
|
-ELF '../starlet-ios-rust/target/phase01-smoke/fatal-data.elf' -ExpectedOutcome Fatal
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\Run-Wii-Starlet-Bootstrap.ps1 `
|
||||||
|
-ELF '../starlet-ios-rust/target/phase01-smoke/undefined-entry.elf' -ExpectedOutcome Fault
|
||||||
|
```
|
||||||
|
|
||||||
|
Each command also accepts `-JIT` and `-FullDolphin`. The fatal image changes only the loaded
|
||||||
|
`.data` probe, producing `REV:FATAL:DATA`. The exception image replaces the reset branch with
|
||||||
|
an undefined instruction; the fault record reports code 1 and raw LR/SPSR. Both passed in the
|
||||||
|
standalone probe and full Dolphin, with interpreter and JIT, on 2026-09-23. The source release
|
||||||
|
ELF remains unchanged.
|
||||||
|
|
||||||
|
The probe uses the actual `ARMCore` and `StarletMemory` implementations. It initializes the
|
||||||
|
high SRAM mapping, runs 4096-cycle slices, checks `__fault_code`, and decodes `REV_TRACE`. It
|
||||||
|
does not start Broadway, graphics, USB, or a Wii title. The two ELF parser unit tests run with:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cmake --build .starlet_msvc2 --target tests
|
||||||
|
.starlet_msvc2\Binaries\Tests\tests.exe --gtest_filter=BootstrapELFTest.*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Full Dolphin run
|
||||||
|
|
||||||
|
Set `Dolphin.General.WiiStarletBootstrapELF` to the absolute ELF path and
|
||||||
|
`Dolphin.Core.WiiIOSLLE=True`. This bypasses the boot0/keys/NAND dump initialization in
|
||||||
|
`StarletMemory` and starts the loaded program at its ELF entry. To run through `DolphinNoGUI`
|
||||||
|
using the existing `.starlet_user3` Wii profile:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File .\Run-Wii-Starlet-Bootstrap.ps1 -FullDolphin
|
||||||
|
```
|
||||||
|
|
||||||
|
This mode launches the Wii System Menu title only to initialize the full emulator; the Starlet
|
||||||
|
runtime is the supplied Rust ELF. It uses the Null video backend and headless platform, reads
|
||||||
|
new IOS log lines, and stops its own process after the expected marker. A populated Wii user
|
||||||
|
profile is required for this full frontend path. The standalone probe is the reproducible path
|
||||||
|
for the Phase 01 CPU and SRAM contract.
|
||||||
|
|
||||||
|
The accepted ELF is ELF32, big-endian ARM `ET_EXEC`, entry `0xffff0000`, with exactly one
|
||||||
|
executable segment at `0xffff0000` (up to 64 KiB) and one writable segment at `0xfffe0000`
|
||||||
|
(up to 16 KiB). It must expose `REV_TRACE`, `__fault_code`, `__fault_lr`, and
|
||||||
|
`__fault_spsr` in the data segment's loaded memory span. Rejected images never write to SRAM.
|
||||||
|
|
||||||
|
The Rust code reaches `REV:BOOT:IDLE` in both interpreter and JIT; the panic-smoke image
|
||||||
|
reaches `REV:PANIC`, and the diagnostic images reach fatal and fault states. All four outcomes
|
||||||
|
pass through the full `DolphinNoGUI` path. An initial
|
||||||
|
incremental MSVC build crashed before the first slice because `HW.cpp` still allocated the old
|
||||||
|
`Starlet` class size; AddressSanitizer identified the overflow. A forced rebuild fixed it,
|
||||||
|
and `Starlet::Create` now keeps allocation in the same translation unit as the class definition.
|
||||||
|
The CMake configuration also corrects a French MSVC `/showIncludes` prefix that made Ninja
|
||||||
|
record zero header dependencies. After rebuilding `HW.cpp`, Ninja records `Starlet.h` as a
|
||||||
|
dependency.
|
||||||
Reference in new issue
Block a user