IOS: boot original BootMii through MINI

This commit is contained in:
2026-08-25 19:46:28 +02:00
parent 08c9886169
commit 5387658e0c
6 changed files with 194 additions and 10 deletions
+18 -2
View File
@@ -3,6 +3,7 @@
#include "Core/HW/SI/SI.h"
#include <algorithm>
#include <array>
#include <cstring>
#include <memory>
@@ -151,11 +152,17 @@ void SerialInterfaceManager::RunSIBuffer(u64 user_data, s64 cycles_late)
const s32 expected_response_length = ConvertSILengthField(m_com_csr.INLNGTH);
#if defined(_DEBUG)
const std::vector<u8> request_copy(m_si_buffer.data(), m_si_buffer.data() + request_length);
const std::vector<u8> request_copy(m_si_output_buffer.data(),
m_si_output_buffer.data() + request_length);
#endif
// The communication RAM is bidirectional, but the SI transfer engine retains the bytes written
// by the CPU separately from the response it places in the readable RAM. In particular, a new
// TSTART without another RAM write retransmits the previous request. BootMii relies on this
// when it probes a GameCube controller twice during startup.
auto transfer_buffer = m_si_output_buffer;
auto* const device = m_channel[m_com_csr.CHANNEL].device.get();
const s32 actual_response_length = device->RunBuffer(m_si_buffer.data(), request_length);
const s32 actual_response_length = device->RunBuffer(transfer_buffer.data(), request_length);
DEBUG_LOG_FMT(SERIALINTERFACE,
"RunSIBuffer chan: {} request_length: {} expected_response_length: {} "
@@ -186,6 +193,11 @@ void SerialInterfaceManager::RunSIBuffer(u64 user_data, s64 cycles_late)
// 2) Investigate the timeout period for NOREP0
if (actual_response_length != 0)
{
if (actual_response_length > 0)
{
const size_t response_length = std::min<size_t>(actual_response_length, m_si_buffer.size());
std::copy_n(transfer_buffer.begin(), response_length, m_si_buffer.begin());
}
m_com_csr.TSTART = 0;
m_com_csr.COMERR = actual_response_length < 0;
if (actual_response_length < 0)
@@ -226,6 +238,7 @@ void SerialInterfaceManager::DoState(PointerWrap& p)
p.Do(m_status_reg);
p.Do(m_exi_clock_count);
p.Do(m_si_buffer);
p.Do(m_si_output_buffer);
}
void SerialInterfaceManager::RegisterEvents()
@@ -285,6 +298,7 @@ void SerialInterfaceManager::Init()
// m_exi_clock_count.LOCK = 1;
m_si_buffer = {};
m_si_output_buffer = {};
}
void SerialInterfaceManager::Shutdown()
@@ -312,6 +326,7 @@ void SerialInterfaceManager::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
auto& si = system.GetSerialInterface();
val = Common::swap32(val);
std::memcpy(&si.m_si_buffer[i], &val, sizeof(val));
std::memcpy(&si.m_si_output_buffer[i], &val, sizeof(val));
}));
}
for (size_t i = 0; i < m_si_buffer.size(); i += sizeof(u16))
@@ -328,6 +343,7 @@ void SerialInterfaceManager::RegisterMMIO(MMIO::Mapping* mmio, u32 base)
auto& si = system.GetSerialInterface();
val = Common::swap16(val);
std::memcpy(&si.m_si_buffer[i], &val, sizeof(val));
std::memcpy(&si.m_si_output_buffer[i], &val, sizeof(val));
}));
}
+1
View File
@@ -237,6 +237,7 @@ private:
USIStatusReg m_status_reg;
USIEXIClockCount m_exi_clock_count;
std::array<u8, BUFFER_SIZE> m_si_buffer{};
std::array<u8, BUFFER_SIZE> m_si_output_buffer{};
Core::System& m_system;
};
+110 -1
View File
@@ -60,6 +60,24 @@ constexpr bool IsDIAddress(u32 address)
return address >= DI_BASE && address < DI_BASE + DI_SIZE;
}
// Broadway sees the EXI register bank at 0x0d006800. Starlet reaches the same
// physical controller through Hollywood's 0x0d806800 alias. The 0x40-byte
// reset-vector aperture which follows it is separate and remains backed by
// StarletMemory for boot2 to populate.
constexpr u32 STARLET_EXI_BASE = 0x0d806800;
constexpr u32 BROADWAY_EXI_BASE = 0x0d006800;
constexpr u32 EXI_REGISTER_SIZE = 0x3c;
constexpr bool IsStarletEXIAddress(u32 address)
{
return address >= STARLET_EXI_BASE && address < STARLET_EXI_BASE + EXI_REGISTER_SIZE;
}
constexpr u32 TranslateStarletEXIAddress(u32 address)
{
return BROADWAY_EXI_BASE + address - STARLET_EXI_BASE;
}
constexpr u32 SHA_BASE = 0x0d030000;
constexpr u32 SHA_CMD = SHA_BASE + 0x00;
constexpr u32 SHA_SRC = SHA_BASE + 0x04;
@@ -316,8 +334,15 @@ constexpr u32 HW_SRNPROT = HW_BASE + 0x60;
constexpr u32 HW_AHBPROT = HW_BASE + 0x64;
constexpr u32 HW_TIMER = HW_BASE + 0x10;
constexpr u32 HW_ALARM = HW_BASE + 0x14;
constexpr u32 HW_GPIO_ENABLE = HW_BASE + 0xdc;
constexpr u32 HW_GPIO_OUT = HW_BASE + 0xe0;
constexpr u32 HW_GPIO_DIR = HW_BASE + 0xe4;
constexpr u32 HW_GPIO_IN = HW_BASE + 0xe8;
constexpr u32 HW_GPIO_INTLVL = HW_BASE + 0xec;
constexpr u32 HW_GPIO_INTFLAG = HW_BASE + 0xf0;
constexpr u32 HW_GPIO_INTMASK = HW_BASE + 0xf4;
constexpr u32 HW_GPIO_STRAPS = HW_BASE + 0xf8;
constexpr u32 HW_GPIO_OWNER = HW_BASE + 0xfc;
constexpr u32 HW_DIFLAGS = HW_BASE + 0x180;
constexpr u32 HW_SPARE0 = HW_BASE + 0x188;
constexpr u32 HW_BOOT0 = HW_BASE + 0x18c;
@@ -374,6 +399,7 @@ constexpr u32 GPIO_EEP_CS = 0x400;
constexpr u32 GPIO_EEP_CLK = 0x800;
constexpr u32 GPIO_EEP_MOSI = 0x1000;
constexpr u32 GPIO_EEP_MISO = 0x2000;
constexpr u32 GPIO_VALID_MASK = 0x00ffffff;
constexpr u32 SRNPROT_SRAM_SPLIT_MODE = 1U << 5;
constexpr u32 BOOT0_DISABLE = 1U << 12;
@@ -523,6 +549,7 @@ void StarletMemory::Reset()
m_ddr_seq_address = 0;
m_ddr_bist_address = 0;
m_gpio_out = 0;
m_gpio_interrupt_flags = 0;
m_seeprom_command = 0;
m_seeprom_output = 0;
m_seeprom_input = 0;
@@ -663,6 +690,7 @@ void StarletMemory::DoState(PointerWrap& p)
p.Do(m_ddr_seq_address);
p.Do(m_ddr_bist_address);
p.Do(m_gpio_out);
p.Do(m_gpio_interrupt_flags);
p.Do(m_seeprom_command);
p.Do(m_seeprom_output);
p.Do(m_seeprom_input);
@@ -3029,7 +3057,17 @@ u8 StarletMemory::Read8(u32 address)
}
if (word_address == HW_GPIO_IN)
{
const u32 value = m_seeprom_miso ? GPIO_EEP_MISO : 0;
const u32 value = GetGPIOInput();
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (word_address == HW_GPIO_INTFLAG)
{
const u32 value = m_gpio_interrupt_flags;
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (word_address == HW_GPIO_STRAPS)
{
constexpr u32 value = 0;
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (IsEHCIAddress(word_address))
@@ -3064,6 +3102,13 @@ u8 StarletMemory::Read8(u32 address)
const u32 value = mmio ? mmio->Read<u32>(m_system, word_address) : 0;
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (IsStarletEXIAddress(word_address))
{
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
const u32 value =
mmio ? mmio->Read<u32>(m_system, TranslateStarletEXIAddress(word_address)) : 0;
return static_cast<u8>(value >> (24 - (address & 3) * 8));
}
if (word_address >= SDHC_BASE && word_address < SDHC_BASE + SDHC_SIZE)
{
const u32 value = ReadSDHCRegister(word_address);
@@ -3136,6 +3181,11 @@ u32 StarletMemory::Read32(u32 address)
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
return mmio ? mmio->Read<u32>(m_system, address) : 0;
}
if ((address & 3) == 0 && IsStarletEXIAddress(address))
{
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
return mmio ? mmio->Read<u32>(m_system, TranslateStarletEXIAddress(address)) : 0;
}
if (IsMemoryAddress(address) && IsMemoryAddress(address + 3))
return m_system.GetMemory().Read_U32(address);
@@ -3223,6 +3273,20 @@ void StarletMemory::Write8(u32 address, u8 value)
}
return;
}
if (IsStarletEXIAddress(word_address))
{
WriteMapped8(address, value);
if ((address & 3) == 3)
{
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
if (mmio)
{
mmio->Write<u32>(m_system, TranslateStarletEXIAddress(word_address),
ReadRegister(word_address));
}
}
return;
}
if (ehci &&
(address == EHCI_BASE + EHCI_USB_STATUS || address == EHCI_BASE + EHCI_PORT_STATUS_1 ||
address == EHCI_BASE + EHCI_PORT_STATUS_2))
@@ -3307,6 +3371,28 @@ void StarletMemory::Write8(u32 address, u8 value)
m_sram_split_mode = (word & SRNPROT_SRAM_SPLIT_MODE) != 0;
else if (word_address == HW_GPIO_OUT)
HandleGPIOWrite(word);
else if (word_address == HW_GPIO_INTFLAG)
{
// Hollywood's GPIO interrupt flag is write-one-to-clear. A level-triggered
// flag cannot be cleared while its input still matches the selected active
// level. Keep that rule limited to already-latched flags; merely selecting
// an active level does not itself invent an edge or a pending interrupt.
const u32 pending_before = m_gpio_interrupt_flags;
const u32 active = ~(GetGPIOInput() ^ ReadRegister(HW_GPIO_INTLVL)) & GPIO_VALID_MASK;
m_gpio_interrupt_flags = (pending_before & ~word) | (pending_before & active);
UpdateGPIOInterrupt();
}
else if (word_address == HW_GPIO_INTMASK || word_address == HW_GPIO_OWNER)
{
UpdateGPIOInterrupt();
}
else if (word_address == HW_GPIO_ENABLE || word_address == HW_GPIO_DIR ||
word_address == HW_GPIO_INTLVL)
{
// These GPIO configuration registers are backed by m_registers. Mask off
// the unused upper byte exposed by Hollywood's 24-pin bank.
WriteMapped8(word_address, 0);
}
else if (ehci)
HandleEHCIWrite(word_address);
else if (ohci_controller)
@@ -3350,6 +3436,13 @@ void StarletMemory::Write32(u32 address, u32 value)
mmio->Write<u32>(m_system, address, value);
return;
}
if ((address & 3) == 0 && IsStarletEXIAddress(address))
{
MMIO::Mapping* const mmio = m_system.GetMemory().GetMMIOMapping();
if (mmio)
mmio->Write<u32>(m_system, TranslateStarletEXIAddress(address), value);
return;
}
if (IsMemoryAddress(address) && IsMemoryAddress(address + 3))
{
@@ -4294,4 +4387,20 @@ void StarletMemory::HandleGPIOWrite(u32 value)
}
}
}
u32 StarletMemory::GetGPIOInput() const
{
// POWER and EJECT are external active-high inputs and remain low until the
// emulator grows explicit front-panel button events. The serial EEPROM MISO
// line is driven by the emulated SEEPROM state machine.
return m_seeprom_miso ? GPIO_EEP_MISO : 0;
}
void StarletMemory::UpdateGPIOInterrupt()
{
const u32 mask = ReadRegister(HW_GPIO_INTMASK);
const u32 owner = ReadRegister(HW_GPIO_OWNER);
m_system.GetWiiIPC().SetStarletInterrupt(
INT_CAUSE_GPIO_STARLET, (m_gpio_interrupt_flags & mask & ~owner & GPIO_VALID_MASK) != 0);
}
} // namespace IOS::LLE
@@ -177,6 +177,8 @@ private:
void CompressSHA1(const u8* block);
void HandleOTPCommand(u32 command);
void HandleGPIOWrite(u32 value);
u32 GetGPIOInput() const;
void UpdateGPIOInterrupt();
u32 GetTimer() const;
Core::System& m_system;
@@ -254,6 +256,7 @@ private:
u16 m_ddr_seq_address = 0;
u16 m_ddr_bist_address = 0;
u32 m_gpio_out = 0;
u32 m_gpio_interrupt_flags = 0;
u16 m_seeprom_command = 0;
u16 m_seeprom_output = 0;
u16 m_seeprom_input = 0;
@@ -178,6 +178,41 @@ TEST(StarletTimer, ZeroDelayAlarmMatchesImmediatelyAndUsesIRQW1C)
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_TIMER, INT_CAUSE_TIMER);
}
TEST(StarletGPIO, InterruptFlagIsWriteOneToClear)
{
constexpr u32 hardware_base = 0x0d800000;
constexpr u32 gpio_interrupt_level = hardware_base + 0xec;
constexpr u32 gpio_interrupt_flag = hardware_base + 0xf0;
constexpr u32 gpio_interrupt_mask = hardware_base + 0xf4;
constexpr u32 gpio_owner = hardware_base + 0xfc;
constexpr u32 power = 1;
Core::DeclareAsCPUThread();
auto& system = Core::System::GetInstance();
system.GetWiiIPC().Reset();
StarletMemory memory(system);
memory.Reset();
const auto read_word = [&memory](u32 address) {
return static_cast<u32>(memory.Read8(address)) << 24 |
static_cast<u32>(memory.Read8(address + 1)) << 16 |
static_cast<u32>(memory.Read8(address + 2)) << 8 | memory.Read8(address + 3);
};
const auto write_word = [&memory](u32 address, u32 value) {
memory.Write8(address, static_cast<u8>(value >> 24));
memory.Write8(address + 1, static_cast<u8>(value >> 16));
memory.Write8(address + 2, static_cast<u8>(value >> 8));
memory.Write8(address + 3, static_cast<u8>(value));
};
// BootMii selects POWER as active-high, waits, then clears the pending flag.
// With the emulated front-panel button idle-low this must read back cleared.
write_word(gpio_interrupt_level, power);
write_word(gpio_interrupt_mask, power);
write_word(gpio_owner, 0);
write_word(gpio_interrupt_flag, power);
EXPECT_EQ(read_word(gpio_interrupt_flag), 0u);
EXPECT_EQ(system.GetWiiIPC().ReadStarletRegister(0x38) & INT_CAUSE_GPIO_STARLET, 0u);
}
TEST(StarletAHBPROT, OriginalIOSMaskIsPreservedForBroadway)
{
constexpr u32 ahbprot = 0x0d800064;
+27 -7
View File
@@ -7,7 +7,9 @@ stub, release of both Broadway reset lines, and execution of the IOS-loaded Powe
Dolphin's normal Broadway core. The end-to-end path has rendered the original French System Menu
health-and-safety screen through Dolphin's Direct3D 11 backend. The original IOS80 Bluetooth stack
has also accepted an emulated paired Wii Remote, completed both L2CAP HID channels, exchanged the
Menu's setup reports, and delivered sustained input reports.
Menu's setup reports, and delivered sustained input reports. Later validation launched the
Homebrew Channel through an original IOS58 reload, ran an unmodified HackMii Installer through
LetterBomb, and reached the original BootMii menu from its boot2 installation.
The implementation never writes to `nand.bin`. The whole `dumps/` directory is ignored by Git so
that boot ROMs, console keys, and NAND contents cannot accidentally be committed.
@@ -131,10 +133,19 @@ X2 and immediately submit the next request with X1 before Starlet is scheduled a
Starlet IRQ/FIQ masks.
- BootMii SEEPROM data exposed through the original 93C56-style GPIO serial protocol, including
read, write-enable/disable, word write/erase, and whole-array write/erase commands in COW memory.
- Hollywood's Starlet GPIO bank, including enable/output/direction/input, ownership, straps,
interrupt level/mask, and write-one-to-clear interrupt flags. POWER and EJECT have their idle
levels and the EEPROM MISO pin remains connected to the existing serial model.
- Immediate AHM memory-flush acknowledgement, the indirect DDR/SEQ/BIST register banks used by
boot1 training, and the hardware-controlled boot0 ROM overlay/SRAM-bank swap.
- PPC/ARM IPC mailboxes, Starlet-side access to both control registers, and Broadway
SRESET/HRESET hold/release transitions.
- The Starlet EXI window at `0x0d806800` is forwarded to Dolphin's existing EXI controller at
`0x0d006800`, while the adjacent reset-vector aperture remains backed by Starlet memory. This
exposes the original EXI device state and transfer completion semantics used by MINI.
- The Broadway Serial Interface keeps an outgoing request latch separate from the readable
response RAM. Starting another transfer without rewriting communication RAM therefore repeats
the previous command, matching the controller probing performed by BootMii's `ppcboot.elf`.
- External SD host-controller and card path at `0x0d070000`: reversed-little-endian SDHCI
capabilities/version, card-detect state, self-clearing software reset, internal-clock
stabilization, write-one-to-clear interrupt status, the IOS initialization command subset,
@@ -279,6 +290,11 @@ An isolated boot probe using the local, mutually matching dumps has executed thi
Wii Remote input, and opened the HackMii installation menu. No HackMii binary, timer, syscall, or
detection check was patched. Its preparation phase is currently much slower than on hardware
because ARM-heavy IOS transients still run through the Starlet interpreter.
20. The dump's existing BootMii-as-boot2 installation intercepted a cold boot and loaded its
original `armboot.bin`/MINI from the virtual SD card. MINI initialized the emulated GPIO and
EXI paths, published its EXI Broadway boot vector, released Broadway, and ran the original
`ppcboot.elf`. The interactive four-icon BootMii menu rendered without a host-side firmware
jump, a patched BootMii binary, or a synthesized SI reply.
The probe never prints ROM, NAND, key, or firmware instruction bytes. The committed unit suite
covers ARM-to-Thumb loads into PC, high Starlet exception vectors, privileged `LDM ... ^` user-bank
@@ -301,12 +317,13 @@ the three pre-existing bad-ECC pages in the source dump, a valid EXI reset vecto
The current end-to-end boundary is a rendered, controller-connected and post-health-screen System
Menu with its populated channel grid, followed by a successful original IOS80-to-IOS58 reload, a
rendered Homebrew Channel, and LetterBomb reaching the interactive HackMii Installer menu. The path
sustains PPC-to-original-IOS filesystem, DI, Bluetooth HID, SDIO/Wi-Fi and network-service traffic.
The stabilized Menu has been measured at 59.91 FPS and HBC at 59.94 FPS. It proves the emulated
first Wii Remote's pairing, L2CAP setup, command exchange and input-report path, but not every
extension, multiple-controller scenario, reconnection edge case, packet-level networking, resource
manager, or timing-sensitive exploit.
rendered Homebrew Channel, LetterBomb reaching the interactive HackMii Installer menu, and the
dump's BootMii-as-boot2 installation reaching its original interactive UI through MINI and
`ppcboot.elf`. The path sustains PPC-to-original-IOS filesystem, DI, Bluetooth HID, SDIO/Wi-Fi and
network-service traffic. The stabilized Menu has been measured at 59.91 FPS and HBC at 59.94 FPS.
It proves the emulated first Wii Remote's pairing, L2CAP setup, command exchange and input-report
path, but not every extension, multiple-controller scenario, reconnection edge case, packet-level
networking, resource manager, or timing-sensitive exploit.
## Remaining blockers
@@ -342,6 +359,9 @@ than a drop-in replacement for Dolphin's mature IOS HLE mode.
public Starlet/boot-chain emulator and high-vector behavior.
- [WiiBrew External Interface](https://www.wiibrew.org/wiki/Hardware/External_Interface) for the
documented EXI boot-buffer and Broadway reset-vector address mapping.
- [WiiBrew GPIOs](https://www.wiibrew.org/wiki/Hardware/Hollywood/GPIOs) and
[Serial Interface](https://www.wiibrew.org/wiki/Hardware/Serial_Interface) for the public GPIO
register and SI communication-RAM behavior.
- [WiiBrew NAND Interface](https://www.wiibrew.org/wiki/Hardware/NAND_Interface) and
[NAND layout](https://www.wiibrew.org/wiki/Hardware/NAND) for the command register, DMA buffers,
chip geometry, and supported device IDs.